also looking at this
docs: the documentation the README points at
#63
9 files
+509
-10
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -145,7 +145,7 @@ Pushing to `main` deploys https://uplink.stacksjs.com once CI passes (`.github/w | ||
| 145 | 145 | | `./buddy uplink:ask "<prompt>"` | Run one prompt exactly as a text would, without Messages | |
| 146 | 146 | | `./buddy uplink:release` | Build, sign and notarize Uplink.app and publish it to GitHub Releases | |
| 147 | 147 | | `./buddy uplink:watch` | The watcher itself, in the foreground (needs Full Disk Access for your terminal) | |
| 148 | | `./buddy dev` | The dashboard: setup status, what it is doing, recent texts | | |
| 148 | | `./buddy dev` | Serves the dashboard at https://uplink.localhost/dashboard and the docs at /docs. Both are local only: the dashboard answers 404 in production | | |
| 149 | 149 | |
| 150 | 150 | Configuration lives in `.env`; see the `UPLINK_*` block in `.env.example`, which covers both |
| 151 | 151 | engines (`UPLINK_ENGINE`, `UPLINK_CLAUDE_BIN`, `UPLINK_CODEX_BIN` and the rest). The service picks |
| @@ -159,11 +159,15 @@ up edits by itself. | ||
| 159 | 159 | `codex-engine.ts`, `engines.ts`), replies (`sender.ts`, `format.ts`), the launchd service and app |
| 160 | 160 | bundle (`service.ts`, `launcher.ts`) and setup checks (`doctor.ts`). |
| 161 | 161 | - `app/Commands/Uplink.ts`: the `buddy uplink:*` commands. |
| 162 | - `app/Models/`: `Conversation` (a thread, its agent session and which agent) and `Run` (one text, | |
| 163 | one run). | |
| 164 | - `resources/views/index.stx`: the dashboard. | |
| 165 | - `tests/unit/uplink/`: against a real chat.db-shaped SQLite file and stub CLIs. | |
| 162 | - `app/Models/`: `Conversation` (a thread, its agent session and which agent), `Run` (one text, one | |
| 163 | run) and `License` (a key, its plan and its Stripe ids). | |
| 164 | - `app/Billing/`: Stripe Checkout, licences and the plans they come from. | |
| 165 | - `resources/views/index.stx`: the public page at https://uplink.stacksjs.com. | |
| 166 | - `resources/views/dashboard.stx`: the dashboard, local only. It calls `notFound()`, so it answers | |
| 167 | 404 wherever the site is deployed. | |
| 168 | - `docs/`: the documentation, served at /docs by `buddy dev`. | |
| 169 | - `tests/unit/`: against a real chat.db-shaped SQLite file and stub CLIs. | |
| 166 | 170 | |
| 167 | 171 | ```bash |
| 168 | ./pantry/.bin/bun test tests/unit/uplink | |
| 172 | ./buddy test | |
| 169 | 173 | ``` |
| @@ -4,9 +4,11 @@ import { env } from '@stacksjs/env' | ||
| 4 | 4 | /** |
| 5 | 5 | * **Documentation Configuration** |
| 6 | 6 | * |
| 7 | * Your app's documentation site, built with BunPress from `docs/`. There is no | |
| 8 | * `docs/` until you create one: add `docs/index.md` and `buddy dev` serves it | |
| 9 | * at /docs, and `buddy deploy` builds it. | |
| 7 | * Your app's documentation site, built with BunPress from `docs/`, served at | |
| 8 | * /docs by `buddy dev`. | |
| 9 | * | |
| 10 | * Every page below has to exist in `docs/`, because nothing checks that a nav | |
| 11 | * or sidebar link resolves. | |
| 10 | 12 | */ |
| 11 | 13 | |
| 12 | 14 | // APP_URL is often a bare host, which is not a URL on its own. |
| @@ -18,7 +20,12 @@ const config: BunPressOptions = { | ||
| 18 | 20 | docsDir: './docs', |
| 19 | 21 | outDir: './dist/docs', |
| 20 | 22 | |
| 21 | nav: [], | |
| 23 | nav: [ | |
| 24 | { text: 'Install', link: '/install' }, | |
| 25 | { text: 'Configuration', link: '/configuration' }, | |
| 26 | { text: 'Security', link: '/security' }, | |
| 27 | { text: 'GitHub', link: 'https://github.com/stacksjs/uplink' }, | |
| 28 | ], | |
| 22 | 29 | |
| 23 | 30 | markdown: { |
| 24 | 31 | title: `${env.APP_NAME || 'Uplink'} Documentation`, |
| @@ -38,6 +45,22 @@ const config: BunPressOptions = { | ||
| 38 | 45 | text: 'Getting Started', |
| 39 | 46 | items: [ |
| 40 | 47 | { text: 'Introduction', link: '/' }, |
| 48 | { text: 'Install', link: '/install' }, | |
| 49 | { text: 'Permissions', link: '/permissions' }, | |
| 50 | ], | |
| 51 | }, | |
| 52 | { | |
| 53 | text: 'Running it', | |
| 54 | items: [ | |
| 55 | { text: 'Configuration', link: '/configuration' }, | |
| 56 | { text: 'Security', link: '/security' }, | |
| 57 | ], | |
| 58 | }, | |
| 59 | { | |
| 60 | text: 'When it goes wrong', | |
| 61 | items: [ | |
| 62 | { text: 'Troubleshooting', link: '/troubleshooting' }, | |
| 63 | { text: 'Uninstall', link: '/uninstall' }, | |
| 41 | 64 | ], |
| 42 | 65 | }, |
| 43 | 66 | ], |
| @@ -0,0 +1,92 @@ | ||
| 1 | # Configuration | |
| 2 | ||
| 3 | There are two ways to configure Uplink, and which one applies depends on how you installed it. | |
| 4 | ||
| 5 | - **The app** reads `~/Library/Application Support/Uplink/settings.json`, which the menubar writes. | |
| 6 | You do not normally edit it by hand. | |
| 7 | - **From source** reads `.env`. The service picks up edits by itself, once no run is in progress. | |
| 8 | ||
| 9 | Both end up in the same shape. `config/uplink.ts` holds the defaults, and every key can be | |
| 10 | overridden by its `UPLINK_*` variable. | |
| 11 | ||
| 12 | ## What the menubar sets | |
| 13 | ||
| 14 | | Setting | What it is | | |
| 15 | | --- | --- | | |
| 16 | | Who can text it | The handles allowed to command Uplink. Empty means this Mac's own Messages handles, the "text yourself" setup | | |
| 17 | | Agent | Claude Code or Codex, for the whole installation. A thread can override it by texting `claude` or `codex` | | |
| 18 | | Model | The model alias for runs. Empty means the CLI's default | | |
| 19 | | Where runs start | The directory a run uses when a text names no path | | |
| 20 | | Open at login | Whether the LaunchAgent starts it | | |
| 21 | | Paused | Read nothing, answer nothing, until resumed | | |
| 22 | ||
| 23 | A `settings.json` this build cannot read falls back per field rather than refusing to start, so a | |
| 24 | hand edit with a typo cannot stop the app. | |
| 25 | ||
| 26 | ## Every variable | |
| 27 | ||
| 28 | These are the `.env` names. The app builds the same environment from its settings. | |
| 29 | ||
| 30 | ### Who and where | |
| 31 | ||
| 32 | | Variable | Default | What it does | | |
| 33 | | --- | --- | --- | | |
| 34 | | `UPLINK_ALLOWED` | empty | Comma separated handles allowed to command it. Empty means your own | | |
| 35 | | `UPLINK_WORKDIR` | `~` | Where a run starts when a text names no path | | |
| 36 | ||
| 37 | Anyone on that list can make an agent act on your Mac. List only yourself. | |
| 38 | ||
| 39 | ### Which agent | |
| 40 | ||
| 41 | | Variable | Default | What it does | | |
| 42 | | --- | --- | --- | | |
| 43 | | `UPLINK_ENGINE` | `claude` | `claude` or `codex` | | |
| 44 | | `CLAUDE_CODE_OAUTH_TOKEN` | none | From `claude setup-token`. Encrypt it with `./buddy env:set` | | |
| 45 | | `UPLINK_CLAUDE_MODEL` | empty | Model alias. `UPLINK_MODEL` is the older name and still works | | |
| 46 | | `UPLINK_CLAUDE_BIN` | found | Where the `claude` binary is, if it is not on PATH | | |
| 47 | | `UPLINK_CODEX_BIN` | found | Where `codex` is. Uplink also looks inside ChatGPT.app, which bundles one | | |
| 48 | | `UPLINK_CODEX_MODEL` | empty | Model the agent should use | | |
| 49 | | `UPLINK_CODEX_PERMISSION` | `bypass` | How much Codex may do without asking. Also `danger-full-access`, `workspace-write`, `read-only` | | |
| 50 | ||
| 51 | Codex keeps its own credentials under `CODEX_HOME`, so there is no token to put in `.env`. | |
| 52 | ||
| 53 | "Found" means Uplink looks where each installer puts the binary, because launchd hands a background | |
| 54 | service a bare PATH and `claude` from your shell is not necessarily findable. | |
| 55 | ||
| 56 | ### Replies | |
| 57 | ||
| 58 | | Variable | Default | What it does | | |
| 59 | | --- | --- | --- | | |
| 60 | | `UPLINK_MAX_CHARS` | `1200` | Characters per text. Longer replies are split | | |
| 61 | | `UPLINK_MAX_PARTS` | `3` | Parts sent at once. The rest waits for `more` | | |
| 62 | | `UPLINK_REPLY_PREFIX` | `🛰 ` | Every reply starts with this | | |
| 63 | | `UPLINK_ACK_AFTER_MS` | `20000` | "Working on it" once a run passes this. 0 disables | | |
| 64 | | `UPLINK_PROGRESS_EVERY_MS` | `600000` | A progress line this often. 0 disables | | |
| 65 | ||
| 66 | The prefix matters for more than looks: it is how Uplink tells its own replies apart from new | |
| 67 | commands in a thread with yourself. | |
| 68 | ||
| 69 | ### Timing | |
| 70 | ||
| 71 | | Variable | Default | What it does | | |
| 72 | | --- | --- | --- | | |
| 73 | | `UPLINK_POLL_MS` | `2000` | How often to look for new texts | | |
| 74 | | `UPLINK_TIMEOUT_MS` | `5400000` | How long one run may take before it is stopped | | |
| 75 | | `UPLINK_CATCH_UP_MS` | `1800000` | On start, skip commands older than this | | |
| 76 | | `UPLINK_SESSION_IDLE_MS` | `21600000` | A thread idle longer than this starts a fresh agent session | | |
| 77 | ||
| 78 | `UPLINK_CATCH_UP_MS` is why a Mac waking after a night asleep does not replay the whole night. A | |
| 79 | text older than it is not run, and you get one reply saying so rather than silence. | |
| 80 | ||
| 81 | ## Where things live | |
| 82 | ||
| 83 | | Path | What it is | | |
| 84 | | --- | --- | | |
| 85 | | `~/Library/Application Support/Uplink/settings.json` | The app's settings | | |
| 86 | | `~/Library/Application Support/Uplink/uplink.sqlite` | Conversations and runs | | |
| 87 | | `~/Library/Application Support/Uplink/license.json` | The last answer the licence server gave | | |
| 88 | | `~/Library/Logs/Uplink.log` | The log, rotated at 5 MB to `Uplink.log.1` | | |
| 89 | | `~/Library/LaunchAgents/com.stacksjs.uplink.plist` | What starts it at login | | |
| 90 | ||
| 91 | Two things are in the Keychain rather than in a file, both under the service | |
| 92 | `com.stacksjs.uplink`: the account `claude-oauth-token` and the account `license-key`. | |
| @@ -0,0 +1,55 @@ | ||
| 1 | # Uplink | |
| 2 | ||
| 3 | Text your Mac from anywhere, even over satellite with no data, and an agent answers. | |
| 4 | ||
| 5 | Newer iPhones can send iMessages over satellite when there is no cell or Wi-Fi signal, but nothing | |
| 6 | that needs the internet works. Uplink turns those texts into a way to reach a coding agent: it | |
| 7 | watches Messages on your Mac for texts from you, runs each one through Claude Code or Codex, and | |
| 8 | texts the answer back. | |
| 9 | ||
| 10 | ## What you need | |
| 11 | ||
| 12 | - **An Apple Silicon Mac.** The app is arm64 only, so an Intel Mac cannot run it. | |
| 13 | - **macOS 13 or later.** | |
| 14 | - **Claude Code on a Claude plan, or Codex on a ChatGPT plan.** No API key either way. | |
| 15 | - **Full Disk Access for Uplink**, so it can read Messages. See [Permissions](./permissions.md). | |
| 16 | - **Permission to control Messages**, so it can reply. Asked once. | |
| 17 | - **A Mac that stays awake.** A laptop with its lid closed sleeps unless it is on power with a | |
| 18 | display attached. | |
| 19 | ||
| 20 | ## Two ways to run it | |
| 21 | ||
| 22 | **The app.** Download it, drag it to Applications, and the menubar walks you through setup. This is | |
| 23 | what most people want. See [Install](./install.md). | |
| 24 | ||
| 25 | **From source.** A launchd service run out of a clone of the repository, with its own bundle id so | |
| 26 | its Full Disk Access grant is separate from the app's. Also in [Install](./install.md). | |
| 27 | ||
| 28 | Only run one of them. They share a launchd label, and the app stops the source service when it | |
| 29 | starts. | |
| 30 | ||
| 31 | ## Texting it | |
| 32 | ||
| 33 | Send yourself a message. Anything that is not one of the words below becomes a task for the agent, | |
| 34 | running in the directory your text names, or your home directory if it names none. | |
| 35 | ||
| 36 | | Word | What it does | | |
| 37 | | --- | --- | | |
| 38 | | `help` | The list, texted back | | |
| 39 | | `status` | What it is working on, and which agent | | |
| 40 | | `stop` | Cancel the current task and anything queued behind it | | |
| 41 | | `new` | Start a fresh conversation | | |
| 42 | | `more` | The rest of a long reply | | |
| 43 | | `ping` | Check it is alive | | |
| 44 | | `claude` / `codex` | Switch this thread to that agent | | |
| 45 | ||
| 46 | Only a whole message counts, so "stop the dev server in ~/Code/api" is still a task. | |
| 47 | ||
| 48 | ## Where to go next | |
| 49 | ||
| 50 | - [Install](./install.md) | |
| 51 | - [Permissions](./permissions.md): what macOS asks for and why | |
| 52 | - [Configuration](./configuration.md): every setting, both ways of setting them | |
| 53 | - [Security](./security.md): what a text can do, and what leaves your Mac | |
| 54 | - [Troubleshooting](./troubleshooting.md): it is not answering | |
| 55 | - [Uninstall](./uninstall.md): removing it completely | |
| @@ -0,0 +1,81 @@ | ||
| 1 | # Install | |
| 2 | ||
| 3 | Two ways, and you should run only one of them. They share the launchd label | |
| 4 | `com.stacksjs.uplink`, and the downloaded app stops the source service when it starts. | |
| 5 | ||
| 6 | ## The app | |
| 7 | ||
| 8 | [Download Uplink for Mac](https://github.com/stacksjs/uplink/releases/latest/download/Uplink.dmg). | |
| 9 | It is signed with a Developer ID and notarized by Apple, so it opens like any other app. | |
| 10 | ||
| 11 | To check what you downloaded against the digest on the release page: | |
| 12 | ||
| 13 | ```bash | |
| 14 | shasum -a 256 ~/Downloads/Uplink.dmg | |
| 15 | ``` | |
| 16 | ||
| 17 | Drag it to Applications and open it. The menubar walks you through three things: | |
| 18 | ||
| 19 | 1. **Full Disk Access**, which it opens System Settings for. See [Permissions](./permissions.md). | |
| 20 | 2. **Signing in to the agent you chose.** Claude Code opens your browser and keeps the token in | |
| 21 | your Keychain; Codex signs in once with `codex login` and keeps its own credentials. | |
| 22 | 3. **Permission to control Messages**, which macOS asks for the first time it tries to reply. | |
| 23 | ||
| 24 | Then text yourself `ping`. The answer starts with a satellite. | |
| 25 | ||
| 26 | Uplink answers texts once it is activated. A licence is | |
| 27 | [$1.99 a month, $19.99 a year, or $29.99 once](https://uplink.stacksjs.com/pricing). The thank-you | |
| 28 | page has an **Activate Uplink** button that opens the app with the key filled in, and a licensed | |
| 29 | Mac keeps answering for two weeks without reaching the licence server, so it works off-grid. | |
| 30 | ||
| 31 | ## From source | |
| 32 | ||
| 33 | The same app, run from a clone as a launchd service. Its bundle id is | |
| 34 | `com.stacksjs.uplink.source`, so its Full Disk Access grant is separate from the downloaded app's. | |
| 35 | ||
| 36 | ```bash | |
| 37 | git clone https://github.com/stacksjs/uplink && cd uplink | |
| 38 | ``` | |
| 39 | ||
| 40 | ```bash | |
| 41 | bun install && ./buddy uplink:install | |
| 42 | ``` | |
| 43 | ||
| 44 | That creates `.env` and the database if they are missing, builds | |
| 45 | `storage/uplink/Uplink.app` and starts it at login. | |
| 46 | ||
| 47 | Then grant **Full Disk Access to Uplink (source)** in System Settings, and sign in to the agent you | |
| 48 | want: | |
| 49 | ||
| 50 | ```bash | |
| 51 | claude setup-token | |
| 52 | ``` | |
| 53 | ||
| 54 | ```bash | |
| 55 | ./buddy env:set CLAUDE_CODE_OAUTH_TOKEN "$(pbpaste | tr -d '[:space:]')" | |
| 56 | ``` | |
| 57 | ||
| 58 | A long-lived token, because a background service cannot answer a browser prompt. For Codex instead: | |
| 59 | ||
| 60 | ```bash | |
| 61 | codex login # or: codex login --device-auth, over SSH | |
| 62 | ``` | |
| 63 | ||
| 64 | ```bash | |
| 65 | ./buddy env:set UPLINK_ENGINE codex | |
| 66 | ``` | |
| 67 | ||
| 68 | Check it, then text yourself `ping`: | |
| 69 | ||
| 70 | ```bash | |
| 71 | ./buddy uplink:doctor | |
| 72 | ``` | |
| 73 | ||
| 74 | ## Texting yourself over satellite | |
| 75 | ||
| 76 | Test this once before relying on it. A text to your own number can be delivered to your other | |
| 77 | devices without going through the satellite link at all, so confirm the Mac receives a self-text | |
| 78 | sent while in satellite mode. | |
| 79 | ||
| 80 | If it does not, use a second Apple ID: sign Messages on the Mac into it, and set the allow list to | |
| 81 | your own phone number. | |
| @@ -0,0 +1,43 @@ | ||
| 1 | # Permissions | |
| 2 | ||
| 3 | Uplink asks macOS for two things. Both are granted to a specific app rather than to Uplink in | |
| 4 | general, which is the part that surprises people. | |
| 5 | ||
| 6 | ## Full Disk Access, to read Messages | |
| 7 | ||
| 8 | Messages keeps your texts in a SQLite database at `~/Library/Messages/chat.db`, which macOS | |
| 9 | protects. Reading it needs Full Disk Access. | |
| 10 | ||
| 11 | **The grant is keyed to a bundle id and the exact binary inside it.** That has two consequences: | |
| 12 | ||
| 13 | - The downloaded app (`com.stacksjs.uplink`) and the from-source build | |
| 14 | (`com.stacksjs.uplink.source`) are different subjects. Granting one does not grant the other, | |
| 15 | which is deliberate: they are separate installs. | |
| 16 | - Rebuilding the from-source bundle replaces the binary, which voids the grant. `uplink:install | |
| 17 | --rebuild` will tell you to grant it again. | |
| 18 | ||
| 19 | Uplink only ever reads that database. It never writes to it. | |
| 20 | ||
| 21 | ## Control of Messages, to reply | |
| 22 | ||
| 23 | Replies are sent through AppleScript, so macOS asks once whether Uplink may control Messages. This | |
| 24 | is the Automation permission, and it is asked the first time a reply is actually sent rather than | |
| 25 | at setup, which is why a first install can look finished and then fail on the first text. | |
| 26 | ||
| 27 | Uplink now asks for it during setup for that reason. If you denied it, it is in System Settings | |
| 28 | under Privacy & Security, Automation, and Uplink needs the Messages row ticked. | |
| 29 | ||
| 30 | ## What happens if one is missing | |
| 31 | ||
| 32 | Neither failure is silent any more. | |
| 33 | ||
| 34 | - Without Full Disk Access, the menubar says it is waiting for it and the service keeps running | |
| 35 | rather than restart-looping. | |
| 36 | - Without Automation, a run completes and the reply cannot be delivered, so the run is recorded as | |
| 37 | failed and the menubar shows why. It is not recorded as done. | |
| 38 | ||
| 39 | `./buddy uplink:doctor` checks both from a clone. The menubar's setup list checks both in the app. | |
| 40 | ||
| 41 | ## What Uplink does not ask for | |
| 42 | ||
| 43 | No Accessibility, no Screen Recording, no Location, no contacts. The two above are the whole list. | |
| @@ -0,0 +1,66 @@ | ||
| 1 | # Security | |
| 2 | ||
| 3 | Uplink lets a text message run a coding agent on your Mac with no one there to approve anything. | |
| 4 | That is the product, and it is worth understanding before you point it at a machine that matters. | |
| 5 | ||
| 6 | ## What a text can do | |
| 7 | ||
| 8 | A text that passes the filter becomes an agent run with approvals turned off: | |
| 9 | `bypassPermissions` for Claude Code, `bypass` for Codex. Nobody is at the Mac to answer a prompt, | |
| 10 | so there are no prompts. | |
| 11 | ||
| 12 | Inside the directory your text names, that agent can read files, write files and run commands, with | |
| 13 | your permissions. It is the same authority you have at a terminal on that Mac. | |
| 14 | ||
| 15 | **So an iPhone with access to the allowed thread is an unlocked door.** If your phone is stolen and | |
| 16 | unlocked, whoever holds it can text your Mac. Treat the allow list the way you would treat SSH keys. | |
| 17 | ||
| 18 | You can narrow this. `UPLINK_CODEX_PERMISSION` takes `read-only` and `workspace-write` as well as | |
| 19 | `bypass`, at the cost of tasks that need to write failing instead of asking. | |
| 20 | ||
| 21 | ## What cannot reach it | |
| 22 | ||
| 23 | The filter refuses by default rather than allowing by default. A message has to pass all of these: | |
| 24 | ||
| 25 | - **iMessage only.** SMS is rejected outright, because a forwarded SMS sender id can be spoofed | |
| 26 | through bulk-SMS gateways, and the other side of that door is an agent with approvals disabled. | |
| 27 | - **Direct chats only.** A group chat never reaches it, whoever is in it. | |
| 28 | - **An allowed handle.** Empty means this Mac's own Messages handles, so the default install | |
| 29 | answers only you. | |
| 30 | - **Not a tapback, not a system item, not Uplink's own reply.** | |
| 31 | - **Not stale.** A text older than `UPLINK_CATCH_UP_MS` is not run, so a Mac waking up cannot | |
| 32 | replay a day of instructions. | |
| 33 | ||
| 34 | ## What leaves your Mac | |
| 35 | ||
| 36 | Three things, and nothing else. | |
| 37 | ||
| 38 | **The prompt and whatever the agent reads go to the agent's vendor**, Anthropic or OpenAI, under | |
| 39 | your own account and their privacy terms. That is what running Claude Code or Codex means, and it | |
| 40 | is the same as running them yourself at a terminal. | |
| 41 | ||
| 42 | **Your texts go through Apple**, as any iMessage does. | |
| 43 | ||
| 44 | **A licence check goes to `uplink.stacksjs.com`**, at launch and every six hours. It sends the | |
| 45 | licence key and nothing else. A licensed Mac keeps working for two weeks without reaching it, so an | |
| 46 | off-grid Mac is not cut off. Buying a licence involves Stripe, which holds your email and payment | |
| 47 | details; Uplink stores the key, your email and Stripe's customer and subscription ids. | |
| 48 | ||
| 49 | Nothing else is sent anywhere. Runs, prompts, replies and their costs are recorded in a SQLite file | |
| 50 | on the Mac and go nowhere. | |
| 51 | ||
| 52 | ## What is stored, and where | |
| 53 | ||
| 54 | Two secrets are in the Keychain, not in a file: your Claude Code token and your licence key, both | |
| 55 | under the service `com.stacksjs.uplink`. They are there rather than in | |
| 56 | `~/Library/Application Support` because anything running as you can read a file there, and files | |
| 57 | get swept into backups and dotfile repositories. | |
| 58 | ||
| 59 | Everything else is under `~/Library/Application Support/Uplink`. See | |
| 60 | [Configuration](./configuration.md#where-things-live). | |
| 61 | ||
| 62 | ## The dashboard is local | |
| 63 | ||
| 64 | `resources/views/dashboard.stx` lists who may text the Mac and every prompt they sent. It answers | |
| 65 | 404 wherever the site is deployed, and the deploy checks that on every release, because a 200 there | |
| 66 | would be a privacy failure rather than a bug. | |
| @@ -0,0 +1,71 @@ | ||
| 1 | # Troubleshooting | |
| 2 | ||
| 3 | ## It is not answering | |
| 4 | ||
| 5 | Work down this list. The first two cover almost everything. | |
| 6 | ||
| 7 | ### 1. Read the log | |
| 8 | ||
| 9 | ```bash | |
| 10 | tail -n 100 ~/Library/Logs/Uplink.log | |
| 11 | ``` | |
| 12 | ||
| 13 | This is the whole support story for the downloaded app: there is no crash reporting and no | |
| 14 | diagnostics bundle. It records every poll failure, every refused reply and every crash, with a | |
| 15 | stack. It rotates at 5 MB, so check `~/Library/Logs/Uplink.log.1` too if the tail looks truncated. | |
| 16 | ||
| 17 | ### 2. Check the menubar | |
| 18 | ||
| 19 | The popover lists what setup still needs, and each line says what is wrong rather than only that | |
| 20 | something is. The common ones: | |
| 21 | ||
| 22 | - **Read Messages** is waiting for Full Disk Access. See [Permissions](./permissions.md). | |
| 23 | - **Sign in** means the agent CLI is installed but not signed in. | |
| 24 | - **Install** means the CLI is not there at all, and the line gives the command. | |
| 25 | - **Send replies** means macOS has not been asked, or was refused, for permission to control | |
| 26 | Messages. This one fails after a run completes, so a task can look done and the phone hear | |
| 27 | nothing. | |
| 28 | ||
| 29 | From a clone, `./buddy uplink:doctor` checks the same things and says how to fix each one. That | |
| 30 | command needs the repository, so it is not available if you downloaded the app. | |
| 31 | ||
| 32 | ### 3. Check the obvious ones | |
| 33 | ||
| 34 | - **Is it paused?** The menubar has a pause switch. | |
| 35 | - **Is the licence current?** An expired one stops it answering. The menubar says so. | |
| 36 | - **Is the Mac awake?** A laptop with its lid closed sleeps unless it is on power with a display | |
| 37 | attached. Texts that arrive while it sleeps are not run, and you get one reply saying so. | |
| 38 | - **Are you texting from an allowed handle?** By default that is this Mac's own Messages handles. | |
| 39 | - **Are you texting over iMessage?** SMS is refused by design. A green bubble never reaches it. | |
| 40 | ||
| 41 | ## It answered once and then stopped | |
| 42 | ||
| 43 | Look for a run that is still going. `status` texts back what it is working on. `stop` cancels it | |
| 44 | and anything queued behind it. | |
| 45 | ||
| 46 | One thread runs one task at a time, so a long task blocks the ones behind it. That is deliberate: | |
| 47 | two runs resuming one agent session would each write their own history. | |
| 48 | ||
| 49 | ## A text got "that arrived while this Mac was asleep" | |
| 50 | ||
| 51 | It did, and it was not run. The cutoff exists so a Mac waking up does not replay a day of | |
| 52 | instructions that could do real work nobody wants now. Send it again to run it now. | |
| 53 | ||
| 54 | ## Texting myself does not reach the Mac | |
| 55 | ||
| 56 | A text to your own number can be delivered to your other devices without going through the | |
| 57 | satellite link at all. Test it once in satellite mode before relying on it. | |
| 58 | ||
| 59 | If it does not arrive, sign Messages on the Mac into a second Apple ID and set the allow list to | |
| 60 | your own phone number instead. | |
| 61 | ||
| 62 | ## It says it cannot resume the conversation after switching agents | |
| 63 | ||
| 64 | That is expected. A session id belongs to the agent that made it, and the other one cannot resume | |
| 65 | it, so texting `claude` or `codex` starts a fresh conversation. The working directory stays. | |
| 66 | ||
| 67 | ## Nothing here helped | |
| 68 | ||
| 69 | Open an issue with the last hundred lines of the log: | |
| 70 | <https://github.com/stacksjs/uplink/issues>. Take out anything in a prompt you would rather not | |
| 71 | share; the log records what you texted. | |
| @@ -0,0 +1,64 @@ | ||
| 1 | # Uninstall | |
| 2 | ||
| 3 | One sequence per way of installing. Dragging the app to the Trash is not enough on its own: it | |
| 4 | leaves a LaunchAgent pointing at a binary that is no longer there, which launchd keeps trying to | |
| 5 | start. | |
| 6 | ||
| 7 | ## The app | |
| 8 | ||
| 9 | **1. Quit it from the menubar.** That stops the watcher and releases its lock cleanly. | |
| 10 | ||
| 11 | **2. Remove the login item.** | |
| 12 | ||
| 13 | ```bash | |
| 14 | launchctl bootout gui/$(id -u)/com.stacksjs.uplink 2>/dev/null | |
| 15 | rm -f ~/Library/LaunchAgents/com.stacksjs.uplink.plist | |
| 16 | ``` | |
| 17 | ||
| 18 | **3. Delete the app.** | |
| 19 | ||
| 20 | ```bash | |
| 21 | rm -rf /Applications/Uplink.app | |
| 22 | ``` | |
| 23 | ||
| 24 | **4. Delete its data.** Conversations, runs, settings and the cached licence answer. | |
| 25 | ||
| 26 | ```bash | |
| 27 | rm -rf ~/Library/Application\ Support/Uplink | |
| 28 | ``` | |
| 29 | ||
| 30 | **5. Delete the logs.** | |
| 31 | ||
| 32 | ```bash | |
| 33 | rm -f ~/Library/Logs/Uplink.log ~/Library/Logs/Uplink.log.1 | |
| 34 | ``` | |
| 35 | ||
| 36 | **6. Remove both Keychain items.** There are two, and only removing the first is the easy mistake. | |
| 37 | ||
| 38 | ```bash | |
| 39 | security delete-generic-password -s com.stacksjs.uplink -a claude-oauth-token | |
| 40 | security delete-generic-password -s com.stacksjs.uplink -a license-key | |
| 41 | ``` | |
| 42 | ||
| 43 | **7. Revoke the permissions**, in System Settings, Privacy & Security: remove Uplink from Full Disk | |
| 44 | Access, and from Automation. | |
| 45 | ||
| 46 | ## From source | |
| 47 | ||
| 48 | ```bash | |
| 49 | ./buddy uplink:uninstall | |
| 50 | ``` | |
| 51 | ||
| 52 | That stops the service and removes it from login. Then delete the clone, and follow steps 4 to 7 | |
| 53 | above, with one difference: the from-source build's bundle id is `com.stacksjs.uplink.source`, so | |
| 54 | that is the name to look for in System Settings. | |
| 55 | ||
| 56 | `.env` holds your Claude Code token if you set one there rather than in the Keychain, so delete the | |
| 57 | clone rather than leaving it around. | |
| 58 | ||
| 59 | ## Your licence | |
| 60 | ||
| 61 | Uninstalling does not cancel a subscription. Manage it from **Manage** in the menubar before you | |
| 62 | remove the app, or from the Stripe customer portal link in the email you got when you bought it. | |
| 63 | ||
| 64 | A lifetime licence has nothing to cancel, and the key keeps working if you install Uplink again. | |