ReviewOS

stacks/uplink

public
Clone

Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.

Text your Mac, even over satellite, and Claude Code answers.

main
· 5 branches · 68 commits

Mirrored from stacksjs/uplink · synced 11 minutes ago

README.md

Uplink

Text your Mac from anywhere, even over satellite with no data, and an agent answers.

Newer iPhones can send iMessages over satellite when there is no cell or Wi-Fi signal, but nothing that needs the internet works. Uplink turns those texts into a way to reach a coding agent: it watches Messages on this Mac for texts from you, runs each one through Claude Code or Codex (on the plan you already have, no API key), and texts the answer back.

Claude Code is the default. UPLINK_ENGINE=codex changes it for the whole installation, the menubar has a picker, and texting codex or claude switches one thread.

  • "Whats the NFL score rn?" gets a web search and a two-line answer.
  • "please improve ~/Code/stacks with feature xyz" runs a full agent session inside ~/Code/stacks, with that repo's agent instructions, hooks and settings, and texts you a summary.
  • Follow-ups continue the same conversation until you text new.

How it works

iPhone --(iMessage, maybe via satellite)--> Messages on this Mac --> ~/Library/Messages/chat.db
                                                                        |
                                                  Uplink.app polls it every 2s (Full Disk Access)
                                                                        |
                                    claude -p ...  or  codex exec resume ...   (in the named repo)
                                    whichever this thread is set to
                                                                        |
iPhone <------------------------- Messages (AppleScript) <------- the reply, as plain text
  • Who can command it: only direct (never group) chats with an allowed handle. By default that is this Mac's own Messages handles, so you text yourself. UPLINK_ALLOWED sets it explicitly. A message is honored only if it passes app/Uplink/filter.ts; everything else is ignored.
  • One task at a time per thread. A second task queues. status, stop, new, more, ping, help, claude and codex are answered immediately and never queue.
  • Built for a thin link: plain text, short answers, long replies split into up to 3 texts with the rest behind more, a "Working on it" after 20s and a progress line every 10 minutes.
  • Replies start with 🛰, so they stand apart in a thread with yourself, and so Uplink never mistakes its own reply for a new command.
  • Runs are recorded in the runs table (cost in integer cents, at API rates; on Max nothing is billed per run) and shown on the dashboard.

Install

Download Uplink for Mac (macOS 13 or later, signed and notarized). Drag it to Applications and open it: the menubar walks you through Full Disk Access, signing in to the agent you picked, and who may text it. Then text yourself ping.

To check what you downloaded, compare it against the digest on the release page, which is also in resources/data/release.json:

shasum -a 256 ~/Downloads/Uplink.dmg

You need one of the two CLIs and a plan for it:

  • Claude Code, the default. The menubar takes a long-lived token from claude setup-token and keeps it in your Keychain.
  • Codex. Sign in once on the Mac with codex login, or codex login --device-auth over SSH. Codex keeps its own credentials, so Uplink stores nothing and needs no API key either.

Uplink answers texts once it is activated: $1.99 a month, $19.99 a year, or $29.99 once, and a code gets six months of Monthly free. The thank-you page has an Activate Uplink button that opens the app with the key filled in. Subscriptions are managed in Stripe's customer portal, from Manage in the menubar. A licensed Mac keeps answering for two weeks without reaching the license server, so it works off-grid.

Licensing, for maintainers

  • Plans, the six-months-free coupon (SIXMONTHS) and the customer portal are declared in config/saas.ts from app/Billing/plans.ts; ./buddy stripe:setup makes Stripe match.
  • /checkout/<plan> starts Stripe Checkout (app/Billing/checkout.ts); a code is honoured on Monthly only. /thanks issues the license (app/Billing/licenses.ts, the License model).
  • The app calls POST /api/license/check and POST /api/billing/portal (routes/api.ts), served by the api site in config/cloud.ts.

Build from source

The same app, run from this repository as a launchd service (bundle id com.stacksjs.uplink.source, so its Full Disk Access grant is separate from the downloaded app's):

  1. Get it and install the background service. uplink:install creates .env and the database if they are missing, builds storage/uplink/Uplink.app and starts it at login:

    git clone https://github.com/stacksjs/uplink && cd uplink
    bun install && ./buddy uplink:install
  2. Grant Full Disk Access to Uplink (source) in System Settings > Privacy & Security.

  3. Sign in to the agent you want. For Claude Code, a long-lived token, because a background service cannot answer a browser prompt:

    claude setup-token
    ./buddy env:set CLAUDE_CODE_OAUTH_TOKEN "$(pbpaste | tr -d '[:space:]')"

    For Codex, sign in once and it keeps its own credentials:

    codex login   # or: codex login --device-auth, over SSH
    ./buddy env:set UPLINK_ENGINE codex
  4. Check it, then text yourself ping:

    ./buddy uplink:doctor

Only run one of the two: they share a launchd label, and the downloaded app stops the source service when it starts.

Texting yourself over satellite

Test this once before relying on it. A text to your own number can be delivered to your other devices without going through the satellite link at all, so confirm the Mac receives a self-text sent while in satellite mode. If it does not, use a second Apple ID: sign Messages on this Mac into it, and set UPLINK_ALLOWED to your own phone number.

Pushing to main deploys https://uplink.stacksjs.com once CI passes (.github/workflows/deploy.yml).

Commands

CommandWhat it does
./buddy uplink:install [--rebuild]Build Uplink.app and start it at login. --rebuild recompiles it, which voids its Full Disk Access grant
./buddy uplink:doctorCheck every requirement and say how to fix what is missing
./buddy uplink:restartRestart now, e.g. after granting a permission. Editing .env restarts it on its own once no run is in progress
./buddy uplink:uninstallStop it and remove it from login
./buddy uplink:ask "<prompt>"Run one prompt exactly as a text would, without Messages
./buddy uplink:releaseBuild, sign and notarize Uplink.app and publish it to GitHub Releases
./buddy uplink:watchThe watcher itself, in the foreground (needs Full Disk Access for your terminal)
./buddy devServes the dashboard at https://uplink.localhost/dashboard and the docs at /docs. Both are local only: the dashboard answers 404 in production

Configuration lives in .env; see the UPLINK_* block in .env.example, which covers both engines (UPLINK_ENGINE, UPLINK_CLAUDE_BIN, UPLINK_CODEX_BIN and the rest). The service picks up edits by itself.

Layout

  • app/Uplink/: the daemon (uplink.ts), the chat.db reader (messages-db.ts, typedstream.ts), the command filter (filter.ts), the words it answers itself (commands.ts, including claude and codex), the two engines and the choice between them (engine.ts, codex-engine.ts, engines.ts), replies (sender.ts, format.ts), the launchd service and app bundle (service.ts, launcher.ts) and setup checks (doctor.ts).
  • app/Commands/Uplink.ts: the buddy uplink:* commands.
  • app/Models/: Conversation (a thread, its agent session and which agent), Run (one text, one run) and License (a key, its plan and its Stripe ids).
  • app/Billing/: Stripe Checkout, licences and the plans they come from.
  • resources/views/index.stx: the public page at https://uplink.stacksjs.com.
  • resources/views/dashboard.stx: the dashboard, local only. It calls notFound(), so it answers 404 wherever the site is deployed.
  • docs/: the documentation, served at /docs by buddy dev.
  • tests/unit/: against a real chat.db-shaped SQLite file and stub CLIs.
./buddy test