ReviewOS

also looking at this

stacks/uplink

docs: the documentation the README points at

#63
Merged glennmichael123 wants to merge docs/the-missing-docs into main
9 files +509 -10
README.mdmodified+10-6
Changes to README.md
@@ -145,7 +145,7 @@ Pushing to `main` deploys https://uplink.stacksjs.com once CI passes (`.github/w
145145| `./buddy uplink:ask "<prompt>"` | Run one prompt exactly as a text would, without Messages |
146146| `./buddy uplink:release` | Build, sign and notarize Uplink.app and publish it to GitHub Releases |
147147| `./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 |
149149
150150Configuration lives in `.env`; see the `UPLINK_*` block in `.env.example`, which covers both
151151engines (`UPLINK_ENGINE`, `UPLINK_CLAUDE_BIN`, `UPLINK_CODEX_BIN` and the rest). The service picks
@@ -159,11 +159,15 @@ up edits by itself.
159159 `codex-engine.ts`, `engines.ts`), replies (`sender.ts`, `format.ts`), the launchd service and app
160160 bundle (`service.ts`, `launcher.ts`) and setup checks (`doctor.ts`).
161161- `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.
166170
167171```bash
168./pantry/.bin/bun test tests/unit/uplink
172./buddy test
169173```
config/docs.tsmodified+27-4
Changes to config/docs.ts
@@ -4,9 +4,11 @@ import { env } from '@stacksjs/env'
44/**
55 * **Documentation Configuration**
66 *
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.
1012 */
1113
1214// APP_URL is often a bare host, which is not a URL on its own.
@@ -18,7 +20,12 @@ const config: BunPressOptions = {
1820 docsDir: './docs',
1921 outDir: './dist/docs',
2022
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 ],
2229
2330 markdown: {
2431 title: `${env.APP_NAME || 'Uplink'} Documentation`,
@@ -38,6 +45,22 @@ const config: BunPressOptions = {
3845 text: 'Getting Started',
3946 items: [
4047 { 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' },
4164 ],
4265 },
4366 ],
docs/configuration.mdadded+92-0
Changes to docs/configuration.md
@@ -0,0 +1,92 @@
1# Configuration
2
3There 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
9Both end up in the same shape. `config/uplink.ts` holds the defaults, and every key can be
10overridden 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
23A `settings.json` this build cannot read falls back per field rather than refusing to start, so a
24hand edit with a typo cannot stop the app.
25
26## Every variable
27
28These 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
37Anyone 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
51Codex 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
54service 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
66The prefix matters for more than looks: it is how Uplink tells its own replies apart from new
67commands 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
79text 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
91Two 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`.
docs/index.mdadded+55-0
Changes to docs/index.md
@@ -0,0 +1,55 @@
1# Uplink
2
3Text your Mac from anywhere, even over satellite with no data, and an agent answers.
4
5Newer iPhones can send iMessages over satellite when there is no cell or Wi-Fi signal, but nothing
6that needs the internet works. Uplink turns those texts into a way to reach a coding agent: it
7watches Messages on your Mac for texts from you, runs each one through Claude Code or Codex, and
8texts 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
23what 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
26its Full Disk Access grant is separate from the app's. Also in [Install](./install.md).
27
28Only run one of them. They share a launchd label, and the app stops the source service when it
29starts.
30
31## Texting it
32
33Send yourself a message. Anything that is not one of the words below becomes a task for the agent,
34running 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
46Only 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
docs/install.mdadded+81-0
Changes to docs/install.md
@@ -0,0 +1,81 @@
1# Install
2
3Two 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).
9It is signed with a Developer ID and notarized by Apple, so it opens like any other app.
10
11To check what you downloaded against the digest on the release page:
12
13```bash
14shasum -a 256 ~/Downloads/Uplink.dmg
15```
16
17Drag it to Applications and open it. The menubar walks you through three things:
18
191. **Full Disk Access**, which it opens System Settings for. See [Permissions](./permissions.md).
202. **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.
223. **Permission to control Messages**, which macOS asks for the first time it tries to reply.
23
24Then text yourself `ping`. The answer starts with a satellite.
25
26Uplink 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
28page has an **Activate Uplink** button that opens the app with the key filled in, and a licensed
29Mac keeps answering for two weeks without reaching the licence server, so it works off-grid.
30
31## From source
32
33The 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
37git clone https://github.com/stacksjs/uplink && cd uplink
38```
39
40```bash
41bun install && ./buddy uplink:install
42```
43
44That creates `.env` and the database if they are missing, builds
45`storage/uplink/Uplink.app` and starts it at login.
46
47Then grant **Full Disk Access to Uplink (source)** in System Settings, and sign in to the agent you
48want:
49
50```bash
51claude setup-token
52```
53
54```bash
55./buddy env:set CLAUDE_CODE_OAUTH_TOKEN "$(pbpaste | tr -d '[:space:]')"
56```
57
58A long-lived token, because a background service cannot answer a browser prompt. For Codex instead:
59
60```bash
61codex login # or: codex login --device-auth, over SSH
62```
63
64```bash
65./buddy env:set UPLINK_ENGINE codex
66```
67
68Check it, then text yourself `ping`:
69
70```bash
71./buddy uplink:doctor
72```
73
74## Texting yourself over satellite
75
76Test this once before relying on it. A text to your own number can be delivered to your other
77devices without going through the satellite link at all, so confirm the Mac receives a self-text
78sent while in satellite mode.
79
80If it does not, use a second Apple ID: sign Messages on the Mac into it, and set the allow list to
81your own phone number.
docs/permissions.mdadded+43-0
Changes to docs/permissions.md
@@ -0,0 +1,43 @@
1# Permissions
2
3Uplink asks macOS for two things. Both are granted to a specific app rather than to Uplink in
4general, which is the part that surprises people.
5
6## Full Disk Access, to read Messages
7
8Messages keeps your texts in a SQLite database at `~/Library/Messages/chat.db`, which macOS
9protects. 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
19Uplink only ever reads that database. It never writes to it.
20
21## Control of Messages, to reply
22
23Replies are sent through AppleScript, so macOS asks once whether Uplink may control Messages. This
24is the Automation permission, and it is asked the first time a reply is actually sent rather than
25at setup, which is why a first install can look finished and then fail on the first text.
26
27Uplink now asks for it during setup for that reason. If you denied it, it is in System Settings
28under Privacy & Security, Automation, and Uplink needs the Messages row ticked.
29
30## What happens if one is missing
31
32Neither 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
43No Accessibility, no Screen Recording, no Location, no contacts. The two above are the whole list.
docs/security.mdadded+66-0
Changes to docs/security.md
@@ -0,0 +1,66 @@
1# Security
2
3Uplink lets a text message run a coding agent on your Mac with no one there to approve anything.
4That 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
8A 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,
10so there are no prompts.
11
12Inside the directory your text names, that agent can read files, write files and run commands, with
13your 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
16unlocked, whoever holds it can text your Mac. Treat the allow list the way you would treat SSH keys.
17
18You 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
23The 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
36Three things, and nothing else.
37
38**The prompt and whatever the agent reads go to the agent's vendor**, Anthropic or OpenAI, under
39your own account and their privacy terms. That is what running Claude Code or Codex means, and it
40is 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
45licence key and nothing else. A licensed Mac keeps working for two weeks without reaching it, so an
46off-grid Mac is not cut off. Buying a licence involves Stripe, which holds your email and payment
47details; Uplink stores the key, your email and Stripe's customer and subscription ids.
48
49Nothing else is sent anywhere. Runs, prompts, replies and their costs are recorded in a SQLite file
50on the Mac and go nowhere.
51
52## What is stored, and where
53
54Two secrets are in the Keychain, not in a file: your Claude Code token and your licence key, both
55under 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
57get swept into backups and dotfile repositories.
58
59Everything 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
65404 wherever the site is deployed, and the deploy checks that on every release, because a 200 there
66would be a privacy failure rather than a bug.
docs/troubleshooting.mdadded+71-0
Changes to docs/troubleshooting.md
@@ -0,0 +1,71 @@
1# Troubleshooting
2
3## It is not answering
4
5Work down this list. The first two cover almost everything.
6
7### 1. Read the log
8
9```bash
10tail -n 100 ~/Library/Logs/Uplink.log
11```
12
13This is the whole support story for the downloaded app: there is no crash reporting and no
14diagnostics bundle. It records every poll failure, every refused reply and every crash, with a
15stack. 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
19The popover lists what setup still needs, and each line says what is wrong rather than only that
20something 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
29From a clone, `./buddy uplink:doctor` checks the same things and says how to fix each one. That
30command 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
43Look for a run that is still going. `status` texts back what it is working on. `stop` cancels it
44and anything queued behind it.
45
46One thread runs one task at a time, so a long task blocks the ones behind it. That is deliberate:
47two runs resuming one agent session would each write their own history.
48
49## A text got "that arrived while this Mac was asleep"
50
51It did, and it was not run. The cutoff exists so a Mac waking up does not replay a day of
52instructions 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
56A text to your own number can be delivered to your other devices without going through the
57satellite link at all. Test it once in satellite mode before relying on it.
58
59If it does not arrive, sign Messages on the Mac into a second Apple ID and set the allow list to
60your own phone number instead.
61
62## It says it cannot resume the conversation after switching agents
63
64That is expected. A session id belongs to the agent that made it, and the other one cannot resume
65it, so texting `claude` or `codex` starts a fresh conversation. The working directory stays.
66
67## Nothing here helped
68
69Open 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
71share; the log records what you texted.
docs/uninstall.mdadded+64-0
Changes to docs/uninstall.md
@@ -0,0 +1,64 @@
1# Uninstall
2
3One sequence per way of installing. Dragging the app to the Trash is not enough on its own: it
4leaves a LaunchAgent pointing at a binary that is no longer there, which launchd keeps trying to
5start.
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
14launchctl bootout gui/$(id -u)/com.stacksjs.uplink 2>/dev/null
15rm -f ~/Library/LaunchAgents/com.stacksjs.uplink.plist
16```
17
18**3. Delete the app.**
19
20```bash
21rm -rf /Applications/Uplink.app
22```
23
24**4. Delete its data.** Conversations, runs, settings and the cached licence answer.
25
26```bash
27rm -rf ~/Library/Application\ Support/Uplink
28```
29
30**5. Delete the logs.**
31
32```bash
33rm -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
39security delete-generic-password -s com.stacksjs.uplink -a claude-oauth-token
40security 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
44Access, and from Automation.
45
46## From source
47
48```bash
49./buddy uplink:uninstall
50```
51
52That stops the service and removes it from login. Then delete the clone, and follow steps 4 to 7
53above, with one difference: the from-source build's bundle id is `com.stacksjs.uplink.source`, so
54that 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
57clone rather than leaving it around.
58
59## Your licence
60
61Uninstalling does not cancel a subscription. Manage it from **Manage** in the menubar before you
62remove the app, or from the Stripe customer portal link in the email you got when you bought it.
63
64A lifetime licence has nothing to cancel, and the key keeps working if you install Uplink again.