@oar-sh/oar 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +53 -32
  3. package/bin/oar.js +814 -228
  4. package/package.json +4 -5
  5. package/server/README.md +20 -1
  6. package/server/db-schema.mjs +8 -0
  7. package/server/migrations/0008-message-pins.mjs +85 -0
  8. package/server/public/app/api-client.js +30 -0
  9. package/server/public/app/attachments-view.js +508 -125
  10. package/server/public/app/back-guard.mjs +58 -0
  11. package/server/public/app/bootstrap.js +46 -2
  12. package/server/public/app/conversation-view.js +110 -25
  13. package/server/public/app/external-link-policy.mjs +4 -1
  14. package/server/public/app/file-version.mjs +58 -0
  15. package/server/public/app/journal-view.js +16 -2
  16. package/server/public/app/pinned-messages-view.mjs +253 -0
  17. package/server/public/app/pwa-install.js +1 -1
  18. package/server/public/app/relay-dot-state.mjs +8 -1
  19. package/server/public/app/router.js +8 -2
  20. package/server/public/app/socket-handlers.js +6 -0
  21. package/server/public/app/store.js +6 -0
  22. package/server/public/app/viewer-chat-gallery.mjs +59 -0
  23. package/server/public/app/viewer-controls-state.mjs +59 -0
  24. package/server/public/app/viewer-gallery.mjs +46 -0
  25. package/server/public/app/viewer-gestures.mjs +105 -0
  26. package/server/public/app/viewer-history.mjs +43 -0
  27. package/server/public/app/viewer-tap.mjs +112 -0
  28. package/server/public/index.html +304 -54
  29. package/server/repositories/message-pin-statements.mjs +39 -0
  30. package/server/repositories/message-repository.mjs +5 -0
  31. package/server/routes/messages-routes.mjs +33 -61
  32. package/server/routes/sessions-routes.mjs +92 -24
  33. package/server/server-runtime.mjs +29 -0
  34. package/server/services/cloudflared-tunnel-service.mjs +139 -41
  35. package/server/services/file-serving.mjs +155 -0
  36. package/server/services/message-pin-list.mjs +52 -0
  37. package/server/services/oar-cli-helpers.mjs +139 -4
  38. package/server/services/oar-cli-menu.mjs +240 -0
  39. package/server/services/oar-cli-wsl.mjs +129 -0
  40. package/server/services/safe-served-content.mjs +61 -11
  41. package/server/services/session-history-refresh-service.mjs +24 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,237 @@ All notable changes to OAR are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.9.9] — 2026-10-07
9
+
10
+ Highlights: `oar` opens a menu and gains stop, restart, status, url and
11
+ service commands, and setup asks for access and port; files open in a
12
+ fullscreen viewer with galleries to swipe through, and a PDF opens in the
13
+ browser; messages can be pinned; a downloaded file is the file as it is now,
14
+ and no cache may keep an API answer; the built-in Cloudflare tunnel uses the
15
+ `cloudflared` you installed instead of a bundled copy; the three provider
16
+ SDKs are current.
17
+
18
+ ### Upgrade notes
19
+
20
+ - **The built-in Cloudflare tunnel needs `cloudflared` installed on the relay
21
+ host.** An install that ran the tunnel (`cloudflaredTunnel` in the config)
22
+ on the copy that came with OAR loses the tunnel after this update until
23
+ `cloudflared` is installed: `winget install --id Cloudflare.cloudflared`
24
+ on Windows, `brew install cloudflared` on macOS, Cloudflare's package
25
+ repository on Linux (<https://pkg.cloudflare.com/>). The relay itself
26
+ starts and works locally in the meantime. Installs with
27
+ `cloudflaredTunnel.binary` set, with `cloudflared` on the `PATH`, or
28
+ with a tunnel that runs outside OAR are not affected.
29
+
30
+ ### Added
31
+
32
+ - **The file viewer fills the screen.** A file opened from the file browser
33
+ or from a chat takes the whole screen: pictures, video and audio on black,
34
+ text in a column that reads well on a wide screen. The title, the toolbar
35
+ and the meta line float over the content; a tap on the content hides them
36
+ and brings them back, and while a video or an audio file plays they fade
37
+ by themselves after three seconds, as players do. Audio files play in the
38
+ viewer. A PDF, which the viewer cannot show, is a card with **Open in new
39
+ tab** (the browser's own viewer) next to **Download**.
40
+ - **Swipe through a folder, or through a chat's media.** A file opened from
41
+ the browser's folder pane carries its neighbours along: a sideways swipe
42
+ shows the next or the previous file, as do the arrow keys and the two
43
+ buttons below with the position between them. A picture or an attachment
44
+ tapped in a chat opens with every image, video and audio file of the
45
+ conversation to swipe through, oldest to newest. Neighbours are fetched
46
+ ahead, so a swipe lands on the file and not on "Fetching…".
47
+ - **The back gesture closes the viewer, and a swipe down dismisses it.** On
48
+ a phone the system back gesture closed the app; it now closes the viewer
49
+ and nothing else, and the address never changes. A swipe down on a picture,
50
+ a player or a card lets the content follow the finger and then go.
51
+ - **The back button never closes the installed app.** In the app installed
52
+ from the browser (Android, iOS, desktop), pressing back with nothing left
53
+ to go back to used to close it. From the first touch on, the app keeps
54
+ one entry beneath its own and steps back onto it: back closes the file
55
+ viewer and nothing more, and a back with nothing left to close shows
56
+ *Press back again to close the app* — a second press without a touch in
57
+ between closes it, as phone apps do. (Before any touch the browser lets
58
+ back leave on purpose; a page cannot change that.) In a browser tab the
59
+ back button is the browser's as before.
60
+ - **`oar` opens a menu.** In a terminal, `oar` shows whether the relay runs,
61
+ its version, URL, port, access and service, and offers Start, Stop, Restart,
62
+ Show URL and QR code, Settings, Install or remove the service, Update,
63
+ Doctor and Copilot session (arrow keys or digits, Enter, `q`). Without a
64
+ terminal it prints the status and the usage.
65
+ - **`oar stop`, `oar restart`, `oar status`, `oar url`, `oar service`.** Stop
66
+ and restart wait for running turns (the systemd service is stopped and
67
+ restarted by systemd). `oar status` exits 0 when the relay runs and 3 when
68
+ it does not. `oar url` prints the relay URL again. `oar service install`,
69
+ `remove` and `status` manage the start at login: the systemd user service
70
+ on Linux, the sign-in autostart on Windows; on macOS, and on a Linux
71
+ without a systemd user session, the command says so and names `oar start`.
72
+ - **`oar setup` asks for access and port, with options for each.** In a
73
+ terminal, setup asks for a new token, the access (this machine only, or
74
+ LAN) and the port, each with its current value; Enter keeps it. `--port`,
75
+ `--lan`, `--local` and `--new-token` answer without a question, and
76
+ `--defaults` keeps everything of an existing config.
77
+ - **WSL: a port has to be free on Windows too.** In WSL's default networking
78
+ a port can be free in the distro while a Windows program listens on it, and
79
+ a Windows browser's `localhost` then reaches that program. A new config now
80
+ takes the first port that is free on both sides, setup refuses a port
81
+ Windows holds, and `oar start`, `oar status`, `oar doctor` and `oar setup`
82
+ warn when Windows holds the relay's port. Where Windows cannot be asked,
83
+ nothing changes.
84
+ - **Pin messages in a conversation.** Every message has a **Pin** button
85
+ beside **Hide**. A conversation with pins shows a **📍** button with their
86
+ count in its header; it opens the list of pinned messages, each with a short
87
+ preview. A row scrolls to its message, also one far up in the history, and
88
+ **🗑** unpins it. Pins are stored on the relay, appear on every open device
89
+ at once, and are not part of the shared view. At most 100 per conversation.
90
+
91
+ ### Changed
92
+
93
+ - **The Copilot terminal session is `oar copilot`.** `oar` alone no longer
94
+ starts the relay and `gh copilot`. `oar copilot [-- <args>]` does; the old
95
+ forms with an option and no command (`oar -- <args>`,
96
+ `oar --install-extension`, `oar --no-install-extension`,
97
+ `oar --migrate-from <dir>`) still run it.
98
+ - **Setup no longer asks about the tunnel or the service.** The service is
99
+ `oar service install`; the tunnel is set in the config
100
+ ([Remote access](README.md#remote-access)). `oar setup --start` still
101
+ installs the service where a systemd user session exists.
102
+ - **A QR code only for an address a phone can reach.** With LAN access the
103
+ URL and the QR code carry the LAN address. With access for this machine
104
+ only, setup prints the localhost URL and one line on how to get phone
105
+ access, and no QR code. In WSL a LAN address is internal to WSL: setup says
106
+ so and names the two ways out, a tunnel or WSL's mirrored networking.
107
+ - The npm package guard allows 6.5 MiB unpacked (was 6 MiB); the command line
108
+ grew by the menu and the new commands, and the app by the pinned messages
109
+ and the file viewer.
110
+ - **A tunnel without `cloudflared` says so.** When the built-in Cloudflare
111
+ tunnel is switched on and no `cloudflared` is found, the relay starts as
112
+ usual, the tunnel stays off, and the status dot's tooltip, `oar doctor` and
113
+ the server log name the install command for the system. The relay looks
114
+ again every minute and starts the tunnel once `cloudflared` is there. With
115
+ `cloudflaredTunnel.required` the queue stays paused until the tunnel is
116
+ connected, as before.
117
+ - **`--allow-scripts` lists two packages.** A manual install on npm 12 is now
118
+ `npm install -g --allow-scripts=better-sqlite3,koffi @oar-sh/oar`;
119
+ `oar update` and the **Update** button pass the same list.
120
+ - `@github/copilot-sdk` 1.0.17 (bundled Copilot CLI 1.0.93): typed structured
121
+ outputs for `sendAndWait`, structured JSON-RPC error data, an experimental
122
+ `setTools()` to replace a session's client tools mid-session, sub-agent
123
+ start/stop hooks, and the new session events `tool.shell_output` and
124
+ `human_response.recorded`. The extension entry renames `factories` to
125
+ `workflows`; OAR passes neither. The package now pins its own dependencies
126
+ exactly, so it carries its own `zod` 4.3.6.
127
+ - `@anthropic-ai/claude-agent-sdk` 0.3.292 (bundled Claude Code 2.1.292).
128
+ Of the nine releases since 0.3.283 these touch the relay's worker: a
129
+ cut-short or interrupted streaming reply now always ends with its
130
+ `message_stop` (0.3.287, 0.3.290), so the turn-liveness watchdog sees the
131
+ end of the model's output; a follow-up turn woken by a finished background
132
+ agent no longer fails its hooks, `canUseTool` and SDK MCP calls with
133
+ "Stream closed" (0.3.284); `commands_changed` no longer arrives before
134
+ `init` (0.3.287); a user message replayed under the same `uuid` while the
135
+ first copy still waits is no longer replayed twice (0.3.290); subagent
136
+ `assistant`/`user` messages carry `agent_id`, task events carry
137
+ `parent_task_id` and `run_id`, and `background_tasks_changed` for a
138
+ finishing task now arrives after its `task_updated` and `task_notification`
139
+ (0.3.292). An omitted `permissionMode` is now left to Claude Code (0.3.286);
140
+ the worker always passes one.
141
+ - `@cursor/sdk` 1.0.36 (additive API changes only): an opt-in
142
+ `subagentInherit` for `Task` child sessions, `sessionId` in the custom-tool
143
+ context, and the SDK's *default* agent-store path now derives from SHA-256
144
+ with a one-time rename of the old MD5-named store. OAR passes its own
145
+ `stateRoot`, so its stores under `data/cursor-agents` keep their layout.
146
+ The package dropped its `@connectrpc/connect-node` dependency.
147
+
148
+ ### Removed
149
+
150
+ - **The bundled `cloudflared`.** OAR no longer installs the `cloudflared`
151
+ npm package, which downloaded the binary during the install. The tunnel
152
+ runs the `cloudflared` named in `cloudflaredTunnel.binary` (or
153
+ `COPILOT_CLOUDFLARED_BINARY`), or else the one on the `PATH`.
154
+
155
+ ### Fixed
156
+
157
+ - **A jump into the history stays where it landed.** After opening a search
158
+ result in an older part of a conversation, the page put the end of the
159
+ conversation back as soon as it refreshed itself: within a second while a
160
+ turn was running, and whenever a turn ended or the connection came back.
161
+ The window you jumped to now stays until you scroll down to the newest
162
+ message. The same holds for a jump to a pinned message.
163
+ - **On a phone, the Pin button of a message can be tapped.** A tap on a bubble
164
+ reveals its Pin and Hide buttons, but the reveal hung on the hover state the
165
+ tap leaves behind, which the browser drops at the next scroll or layout
166
+ change (seen on Linux), and the share-visibility line then took the tap
167
+ instead of the button. The tap now holds the reveal until a tap lands
168
+ outside the bubble.
169
+ - **A PDF opens in the browser's viewer again.** Served files carry a
170
+ `Content-Security-Policy` with `sandbox` so a worker-written file can never
171
+ run as script on the relay's origin; browsers refuse to show a sandboxed
172
+ PDF in their built-in viewer, so "open in a new tab" turned into a
173
+ download. A PDF alone is now served without the `sandbox` directive (PDF
174
+ viewers run no document script) and may be framed by the relay's own
175
+ origin; HTML, SVG and every other type keep the full lock-down.
176
+ - **Audio files were an opaque download.** No audio type was known, so an
177
+ `.mp3` or `.wav` went out as `application/octet-stream` with a download
178
+ disposition. Audio types are served as such, previewed as audio, and
179
+ `.bmp` and `.avif` are served as images.
180
+ - **An SVG can be previewed.** It was served as plain text so that a
181
+ worker-written SVG could never run as a page on the relay's origin, and
182
+ an `<img>` refuses plain text: the viewer and the file browser's
183
+ thumbnails showed a broken picture. An SVG now keeps its type; the
184
+ `sandbox` in every served file's `Content-Security-Policy` still denies
185
+ it script and an origin if it is opened as a page.
186
+ - **Uploaded video can seek.** A video attachment in a chat was streamed
187
+ without range support, so the player could not jump; it now goes through
188
+ the same range-aware file serving as a workspace file, and so does a
189
+ shared conversation's attachment. Their long-lived caching stays.
190
+ - **A file with a Japanese, Chinese or emoji name can be served again.** Its
191
+ name went into `Content-Disposition` as is, which the HTTP layer refuses
192
+ for characters outside Latin-1, so such a workspace file, upload or shared
193
+ attachment failed with a server error and left a file handle open each
194
+ time. The header now carries an ASCII fallback and the full name in the
195
+ `filename*` form, so the file opens and is saved under its own name.
196
+ - **A changed file is downloaded as it is now.** The **Download** button of
197
+ the file viewer handed back an older copy of a file that had been written
198
+ again under the same name. Three things were wrong:
199
+ - A click on Download did not download: since 0.9.0 every link click was
200
+ turned into "open in a new tab", so the link's own file name was dropped
201
+ and a phone showed or saved the file under its plain name, next to (or
202
+ instead of) the copy it already had. Download links are left to the
203
+ browser again.
204
+ - A file fetched within two seconds of a change arrived cut to its
205
+ previous length, because the announced length came from an older read
206
+ than the bytes. Length, validators and bytes now come from one open file.
207
+ - The address and the saved name never changed. The link now carries the
208
+ file's version (`v=…`), and the file is saved with the time it was last
209
+ changed, `report.pdf` as `report (1005-0719).pdf`.
210
+ - **No cache between the relay and you may keep an answer.** Every API
211
+ answer, errors included, now says `Cache-Control: no-store` unless the route
212
+ sets its own value, and every answer carries `CDN-Cache-Control: no-store`
213
+ and `Cloudflare-CDN-Cache-Control: no-store`, which bind a CDN in front of
214
+ a tunnel whatever cache rule its zone has. Served files also carry an
215
+ `ETag` and `Last-Modified`.
216
+ - **A file preview could stop the relay.** The preview and file routes used a
217
+ cache they were never given, so dropping a stale entry threw; in the
218
+ preview route that happened outside any handler and ended the process.
219
+ - **A changed token, port or access reaches the running relay.** `oar setup`
220
+ wrote the new value while the relay kept the old one, so the relay refused
221
+ the token in its own config. Setup now restarts a running relay after such
222
+ a change (in a terminal it asks first), with the old token on the old port,
223
+ and waits until the relay answers with the new settings. If you decline, or
224
+ the relay is still busy with a turn, `oar status`, `oar stop` and
225
+ `oar restart` still find it.
226
+ - **The command may follow its options, and an unknown one starts nothing.**
227
+ `oar --port 3339 setup` and `oar --setup` used to start the relay and
228
+ `gh copilot` instead of running setup. The command is now found wherever it
229
+ stands, `--setup`, `--start`, `--stop` and `--status` name it too, and an
230
+ unknown command or option prints the usage and exits with code 2.
231
+ - **`oar copilot` checks the relay on its port, and says when `gh` is
232
+ missing.** A lock file alone no longer counts as a running relay: the relay
233
+ has to answer on the port. Without the GitHub CLI the command says so and
234
+ starts nothing, instead of *spawn gh ENOENT*.
235
+ - **Ctrl+C in a prompt** ends the command quietly with exit code 130 instead
236
+ of *Unhandled error*.
237
+
238
+
8
239
  ## [0.9.8] — 2026-10-04
9
240
 
10
241
  Highlights: OAR has a logo, an otter, with icons that follow Day and Night
package/README.md CHANGED
@@ -70,12 +70,13 @@ You need Node.js plus whatever the runtimes you actually use need. Nothing else
70
70
  | ----------- | ---------- | ----- |
71
71
  | Node.js 22.13 or newer | always | The macOS and Linux installer fetches its own copy when yours is missing or too old; the Windows installer only checks. Running the development test suite needs Node 24 (see [DEVELOPING.md](DEVELOPING.md#node-version)) |
72
72
  | GitHub Copilot CLI (`copilot`), signed in | Copilot and OpenAI (BYOK) chats | Install it with `npm install -g @github/copilot`, then run `copilot` once on the relay host and sign in. The relay looks for the CLI's runtime when it starts, so restart the relay after installing or upgrading the CLI. Needs a GitHub Copilot plan that includes Copilot CLI |
73
- | GitHub CLI (`gh`), signed in | the `oar` launcher command; Extension-engine sessions on Windows; the Copilot card in **Check Usage** | `gh copilot` is built into current GitHub CLI releases, so there is no extension to install. The usage card also accepts a token in `GH_TOKEN` or `GITHUB_TOKEN` |
73
+ | GitHub CLI (`gh`), signed in | `oar copilot`; Extension-engine sessions on Windows; the Copilot card in **Check Usage** | `gh copilot` is built into current GitHub CLI releases, so there is no extension to install. The usage card also accepts a token in `GH_TOKEN` or `GITHUB_TOKEN` |
74
74
  | OpenAI API key | OpenAI and OpenAI Image chats | Entered in **⚙️ Settings**, stored in the relay database |
75
75
  | A Claude login on the relay host | Claude chats | Log in from **⚙️ Settings → Providers → Claude → Relogin** (the panel can install the Claude Code CLI first), or run `claude` once on the host. The relay stores no Claude key |
76
76
  | The same Claude login, from a claude.ai account with GitHub connected | Claude Cloud chats | The Claude GitHub app must be allowed on each repository a cloud chat works on (see [Claude Cloud](#claude-cloud)) |
77
77
  | Cursor API key | Cursor chats | Entered in **⚙️ Settings**, stored in the relay database |
78
78
  | Grok CLI, signed in | Grok chats | Install it and sign in from **⚙️ Settings → Providers → Grok**, or set `XAI_API_KEY` in the relay's environment |
79
+ | `cloudflared` (optional) | the built-in Cloudflare tunnel | Not included; install it yourself (see [Cloudflare Tunnel](#optional-remote-internet-access-cloudflare-tunnel)) |
79
80
  | tmux (optional) | Linux and macOS | Session workers run in detached tmux sessions you can watch from the browser (**🖥️ Inspect tmux console**). Without tmux they run as plain background processes |
80
81
 
81
82
  ### macOS and Linux
@@ -100,7 +101,7 @@ The relay is started as a systemd user service where a systemd user session exis
100
101
 
101
102
  ```bash
102
103
  curl -fsSL oar.sh/install | sh -s -- --yes
103
- curl -fsSL oar.sh/install | OAR_VERSION=0.9.8 OAR_PORT=3400 sh
104
+ curl -fsSL oar.sh/install | OAR_VERSION=0.9.9 OAR_PORT=3400 sh
104
105
  ```
105
106
 
106
107
  The installer needs `curl` or `wget`, and `tar` when it fetches Node.js. It has no Node.js build for musl systems (Alpine) or for CPUs other than x64 and arm64; there, install Node.js yourself first. Releases before 0.9.8 are installed the same way, but their `oar setup` cannot start the relay: see [Start the relay](#start-the-relay).
@@ -113,16 +114,16 @@ Read it before you run it: [oar.sh/install](https://oar.sh/install).
113
114
  irm oar.sh/install.ps1 | iex
114
115
  ```
115
116
 
116
- The PowerShell installer (PowerShell 5 or newer) expects Node.js 22.13 or newer to be installed: it checks Node.js, runs `npm install -g @oar-sh/oar`, then `oar setup`. It reads the same `OAR_VERSION`, `OAR_CHANNEL`, and `OAR_DRY_RUN` variables, for example `$env:OAR_VERSION = '0.9.6'` before the command.
117
+ The PowerShell installer (PowerShell 5 or newer) expects Node.js 22.13 or newer to be installed: it checks Node.js, runs `npm install -g @oar-sh/oar`, then `oar setup`. It reads the same `OAR_VERSION`, `OAR_CHANNEL`, and `OAR_DRY_RUN` variables, for example `$env:OAR_VERSION = '0.9.9'` before the command.
117
118
 
118
119
  ### npm
119
120
 
120
121
  ```bash
121
- npm install -g --allow-scripts=better-sqlite3,cloudflared,koffi @oar-sh/oar
122
+ npm install -g --allow-scripts=better-sqlite3,koffi @oar-sh/oar
122
123
  oar setup
123
124
  ```
124
125
 
125
- npm 12 runs no install script it was not told to allow, and three of OAR's dependencies fetch their native binary in one; without `--allow-scripts` the install succeeds there and the relay then fails to start. npm 10 and 11 accept the flag and need none. The installers and `oar update` pass it themselves.
126
+ npm 12 runs no install script it was not told to allow, and two of OAR's dependencies fetch their native binary in one; without `--allow-scripts` the install succeeds there and the relay then fails to start. npm 10 and 11 accept the flag and need none. The installers and `oar update` pass it themselves.
126
127
 
127
128
  ### From a git checkout
128
129
 
@@ -150,12 +151,11 @@ Development workflows, tests, and relay internals are in [DEVELOPING.md](DEVELOP
150
151
 
151
152
  ### Start the relay
152
153
 
153
- The macOS and Linux installer starts the relay itself. After a manual install, `oar setup` creates the config and prints the relay URL with a QR code, but it does not start the relay unless you pass `--start`. Pick one:
154
+ The macOS and Linux installer starts the relay itself. After a manual install, `oar setup` creates the config and prints the relay URL, but it does not start the relay unless you pass `--start`. `oar` alone, in a terminal, opens a menu with everything below.
154
155
 
155
- - **Any global install on macOS or Linux:** `oar start` starts the relay in the background (through the systemd user service when one is installed) and prints the relay URL. `oar setup --defaults --start` is what the installer runs: it also installs and enables the systemd user service where a systemd user session exists, and enables lingering.
156
- - **Linux (global install), by hand:** accept the systemd user service that `oar setup` offers, then run `systemctl --user enable --now oar`. The relay then starts when you log in; run `loginctl enable-linger "$USER"` once if it should keep running while you are logged out. The unit carries the `PATH` of the shell that ran `oar setup`, plus `~/.local/bin`, so the relay's sessions find the tools you have; rerun `oar setup` after your `PATH` changed.
157
- - **Windows:** start the relay once with `oar`, then choose **⚙️ Settings → General → Autostart (Windows)**: *At sign-in* opens a visible terminal after you log on, and *At system startup* runs it headless before anyone signs in, after one admin confirmation on the PC itself.
158
- - **Any platform:** `oar` starts the relay in the background if it is not already running, then opens the Copilot CLI (`gh copilot`) in the same shell. A relay that `oar` started stops again when that Copilot session ends, and `oar` needs the GitHub CLI.
156
+ - **Any global install:** `oar start` starts the relay in the background (through the systemd user service when one is installed) and prints the relay URL. `oar stop` stops it, `oar restart` restarts it, `oar status` says whether it runs; stop and restart wait for running turns.
157
+ - **At every login:** `oar service install`. On Linux that writes and enables a systemd user service and enables lingering, so the relay is back after a reboot and keeps running while you are logged out; the unit carries the `PATH` of the shell that ran the command, plus `~/.local/bin`, so the relay's sessions find the tools you have (run it again after your `PATH` changed). On Windows it switches on the same autostart as **⚙️ Settings → General → Autostart (Windows)** → *At sign-in*; *At system startup*, which runs the relay headless before anyone signs in, is set in the web UI because it needs one admin confirmation. `oar service remove` takes it out again. Not available on macOS yet, nor on a Linux without a systemd user session (containers, WSL without systemd): use `oar start` there.
158
+ - **With a Copilot terminal session:** `oar copilot` starts the relay in the background if it is not already running, then opens the Copilot CLI (`gh copilot`) in the same shell. A relay that `oar copilot` started stops again when that session ends. It needs the GitHub CLI.
159
159
 
160
160
  To run the relay of a global install in the foreground, start the server the way the systemd unit does:
161
161
 
@@ -168,22 +168,24 @@ node "$(npm root -g)/@oar-sh/oar/server/server.js"
168
168
 
169
169
  On Windows, point the same three variables at `%APPDATA%\oar\config.json`, `%APPDATA%\oar\data`, and `%APPDATA%\oar\logs`. The directory you start it from becomes the default working directory for new sessions.
170
170
 
171
- Then open the URL that `oar setup` printed, or scan its QR code. With `localhostOnly` on (the default) that is `http://localhost:3333/` on the relay host; reach it from other devices through a [tunnel](#remote-access), or set `localhostOnly` to `false` in the config file and restart the relay for LAN access. The URL carries your token once (`?token=…`); after that, the relay keeps the browser signed in with an HttpOnly cookie.
171
+ Then open the URL that `oar setup` printed (`oar url` prints it again). With access set to this machine only (the default, `localhostOnly` in the config) that is `http://localhost:3333/` on the relay host; reach it from other devices through a [tunnel](#remote-access), or switch on LAN access with `oar setup --lan`, which also prints a QR code for your phone. The URL carries your token once (`?token=…`); after that, the relay keeps the browser signed in with an HttpOnly cookie.
172
172
 
173
173
  ### The `oar` command
174
174
 
175
175
  | Command | What it does |
176
176
  | ------- | ------------ |
177
- | `oar` | Starts the relay in the background unless one is already running, then runs `gh copilot` in the same shell. The relay's output goes to its log directory, not your terminal |
178
- | `oar -- <args>` | The same, passing `<args>` through to the Copilot CLI (`gh copilot -- <args>`) |
179
- | `oar --port <port>` | Finds or starts the relay on `<port>` for this run, overriding the config's `port` without changing the file; a config this command creates saves it. Without it, `oar` uses the config's `port` (default `3333`) |
180
- | `oar --migrate-from <dir>` | Global installs: copies the relay state of a pre-rename git checkout at `<dir>` into the OAR state root, once. The source is never moved or changed, and a relay still running from it blocks the copy. `oar setup` accepts the same option |
181
- | `oar --install-extension` | Writes or refreshes the user-global Copilot CLI extension wrapper in `~/.copilot/extensions/web-relay/`, then exits. Plain `oar` does this on every run; `--no-install-extension` skips it |
182
- | `oar setup [--defaults] [--start] [--port <port>]` | Creates or updates the config: generates an auth token (with a config already present it offers a new one, which signs every device out), asks whether a new config should allow LAN access and whether to enable the managed Cloudflare tunnel, offers the systemd user service on Linux global installs, then prints the relay URL and a QR code. An existing config keeps its `localhostOnly` value. `--defaults` accepts every default without asking. `--port` sets the relay port; without it a new config takes the first free port from 3333 on, and an existing config keeps its port. `--start` (global installs) also gets the relay running without a question: with a systemd user session it writes and enables the service and enables lingering, elsewhere it starts the relay in the background; a relay that is already running is left alone |
183
- | `oar start` | Global installs: starts the relay in the background without a Copilot session, through the systemd user service when one is installed, and prints the relay URL. A relay that is already running is left alone |
184
- | `oar doctor` | Prints the version, Node.js, install mode, state root, config path, port, whether an auth token is set, the tunnel mode, the database path and size, and which provider CLIs (`gh`, `claude`, `grok`) answer. It changes nothing |
177
+ | `oar` | In a terminal: a menu with the relay's status (running or stopped, version, URL, port, access, service) and an entry for each command below; arrow keys or digits, Enter, `q`. Without a terminal: the status and the usage |
178
+ | `oar start` | Starts the relay in the background, through the systemd user service when one is installed, and prints the relay URL. A relay that is already running is left alone. Global installs only; a git checkout uses `npm start` |
179
+ | `oar stop` | Stops the relay: the systemd service through `systemctl --user stop oar`, any other relay through its own API, once no turn is running |
180
+ | `oar restart` | Restarts a running relay once no turn is running (the service through systemd, at once), or starts a stopped one |
181
+ | `oar status` | Says whether the relay runs, on which port and with which access, and whether the service is installed. Exit code 0 when it runs, 3 when it does not |
182
+ | `oar url` | Prints the relay URL. With LAN access it also prints a QR code; with access for this machine only it says how to get phone access instead. When the running relay reports a tunnel address, that address and its QR code follow |
183
+ | `oar setup` | Creates or updates the config. In a terminal it asks, showing the current value (Enter keeps it): a new auth token (which signs every device out), access (this machine only, or LAN), and the port. Options answer without a question: `--port <port>`, `--lan` or `--local`, `--new-token`; `--defaults` keeps everything of an existing config and gives a new one access for this machine only and the first free port from 3333 on. A port must be free, unless the install's own running relay holds it. When the token, the port or the access changed while the relay runs, setup restarts it (in a terminal it asks first) and waits until it answers with the new settings. `--start` (global installs, used by the installer) also installs the service where a systemd user session exists and starts the relay. `--migrate-from <dir>` copies the relay state of a pre-rename git checkout at `<dir>` into the OAR state root, once; the source is never changed, and a relay still running from it blocks the copy |
184
+ | `oar service install` \| `remove` \| `status` | Starts the relay at every login: a systemd user service on Linux, the sign-in autostart on Windows. See [Start the relay](#start-the-relay) |
185
+ | `oar copilot [-- <args>]` | Starts the relay in the background unless one already answers on its port, then runs `gh copilot` in the same shell, passing `<args>` through (`gh copilot -- <args>`). `--port <port>` finds or starts the relay on that port for this run without changing the config; a config this command creates saves it. `--install-extension` writes or refreshes the user-global Copilot CLI extension wrapper in `~/.copilot/extensions/web-relay/`, then exits; every run does that unless `--no-install-extension` is given. The same options without the word `copilot` (`oar -- <args>`, `oar --install-extension`) still run this command |
186
+ | `oar doctor` | Prints the version, Node.js, install mode, state root, config path, port, whether an auth token is set, the tunnel mode and whether the `cloudflared` it needs is installed, the database path and size, and which provider CLIs (`gh`, `claude`, `grok`) answer. It changes nothing |
185
187
  | `oar update [--beta] [--to X.Y.Z]` | Global installs: looks up the newest release of your channel in `https://oar.sh/latest.json` (`--beta` for the beta channel), installs it with `npm install -g` (the npm of the Node.js that runs `oar`, into the folder the package is installed in), and asks a running relay to restart once no turn is running. `--to X.Y.Z` installs that version straight from npm, which works even when `OAR_NO_UPDATE_CHECK=1` blocks the lookup |
186
- | `oar --version`, `oar --help` | Prints the version, or the usage |
188
+ | `oar --version`, `oar help` | Prints the version, or the usage. An unknown command or option prints the usage and exits with code 2 |
187
189
 
188
190
  ### Where OAR keeps its files
189
191
 
@@ -472,6 +474,7 @@ Use **🤗 Select Models** to choose which variants show up in the composer, the
472
474
  - Agents can embed **images, video, and audio** in a reply by their absolute path, on every runtime; clicking an embedded image opens the file viewer with zoom, download, and copy
473
475
  - **Screenshot annotations**: mark up an uploaded screenshot with highlighter strokes before or after sending; the original upload is never modified
474
476
  - **Share** a conversation by read-only link, and hide individual messages from the shared view
477
+ - **Pin** messages in a conversation and get back to them from a list that scrolls to each one
475
478
  - Conversation history in local SQLite, including the Copilot sessions already stored on the host
476
479
  - `/compact` continues in a fresh conversation seeded with a summary; `/preview` publishes a local dev server or folder on a public preview host
477
480
  - Workspace and drive browser with file previews, **Git changes** with a diff viewer, and `@file:` / `@folder:` reference tokens
@@ -558,6 +561,10 @@ The composer draft, attachments included, is saved per conversation on the relay
558
561
 
559
562
  Use **➡️ Share conversation** in the `⋯` menu to publish a read-only link. Each message has a **Hide** button that keeps it out of the shared view without deleting it; hidden messages stay fully visible to you, marked *Hidden from shared viewers*, with an **Unhide** button. Anyone with the link can read the shared view, without signing in.
560
563
 
564
+ ### Pinned messages
565
+
566
+ Each message has a **Pin** button next to **Hide**; a pinned message is marked *📍 Pinned* and offers **Unpin**. Once a conversation has pins, a **📍** button with their count appears in its header. It opens the list of pinned messages in conversation order, each with a short preview. A row closes the list and scrolls to its message, also when that message is far up in the history; **🗑** in a row unpins it. Pins are kept on the relay and show on every device at once, up to 100 per conversation. They are your own bookmarks: the shared view does not show them, and a conversation continued with `/compact` starts without them.
567
+
561
568
  ### Notifications and the app
562
569
 
563
570
  - **⚙️ Settings → Notifications** turns on push notifications per device: for questions from the agent, completed or failed turns, plan boards, and the CLI going offline. They need a secure context (HTTPS, or `localhost`), are sent only while no device has the app in the foreground, and show generic text unless you opt in to message previews on that device.
@@ -640,7 +647,7 @@ The config file is `server/config.json` in a git checkout and `~/.oar/config.jso
640
647
  | `cloudflaredTunnel.mode` | `disabled` | Cloudflare tunnel mode (`disabled` or `managed`; `enabled: true` is a legacy alias for `managed`) |
641
648
  | `cloudflaredTunnel.required` | `false` | Pause dequeue while the managed Cloudflare tunnel is disconnected |
642
649
  | `cloudflaredTunnel.token` | — | The tunnel token from your own Cloudflare Zero Trust tunnel |
643
- | `cloudflaredTunnel.binary` | *(auto)* | `cloudflared` path; defaults to the npm package, then `PATH` |
650
+ | `cloudflaredTunnel.binary` | `cloudflared` | Path or command name of the `cloudflared` you installed; defaults to `cloudflared` on `PATH` |
644
651
  | `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
645
652
  | `previews.enabled` | `false` | Publish local dev servers on a separate listener (see [docs/preview-servers.md](docs/preview-servers.md)) |
646
653
  | `previews.port` | `port + 1` | Loopback port for the preview listener; `0` picks an ephemeral port |
@@ -726,11 +733,18 @@ An alternative to the SSH tunnel that needs no VPS and no inbound port: the rela
726
733
  supervises Cloudflare's `cloudflared` binary, and Cloudflare carries your hostname down
727
734
  to `127.0.0.1:3333`.
728
735
 
736
+ OAR does not include `cloudflared`; install it on the relay host first:
737
+
738
+ | System | Install |
739
+ | ------ | ------- |
740
+ | Windows | `winget install --id Cloudflare.cloudflared` |
741
+ | macOS | `brew install cloudflared` |
742
+ | Linux | Cloudflare's package repository (Debian, Ubuntu, RHEL): [pkg.cloudflare.com](https://pkg.cloudflare.com/) |
743
+
729
744
  1. In your own Cloudflare Zero Trust account, create a tunnel and route a public hostname on
730
745
  your zone to `http://localhost:3333` (or your `port`). That routing lives in Cloudflare, not
731
746
  on this machine.
732
- 2. Put the tunnel's token into the config and restart the relay. (`oar setup` can switch the
733
- tunnel on for you; it still needs the token.)
747
+ 2. Put the tunnel's token into the config and restart the relay (`oar restart`).
734
748
 
735
749
  ```json
736
750
  "cloudflaredTunnel": {
@@ -746,17 +760,21 @@ Environment overrides: `COPILOT_CLOUDFLARED_MODE`, `COPILOT_CLOUDFLARED_TOKEN`,
746
760
  `COPILOT_CLOUDFLARED_BINARY`.
747
761
 
748
762
  `localhostOnly` stays `true`: `cloudflared` connects outbound and nothing binds publicly.
749
- The binary resolves from `cloudflaredTunnel.binary`, then the optional `cloudflared` npm
750
- package, then `PATH`; a managed config with no resolvable binary is reported as a config
751
- error instead of crashing. Connection drops reconnect with jittered exponential backoff,
763
+ The relay runs the `cloudflared` named in `cloudflaredTunnel.binary`, or else the one on
764
+ its `PATH`. When there is none, the relay starts as usual and the tunnel stays off: the
765
+ status dot's tooltip, `oar doctor` and the server log say that `cloudflared` is not
766
+ installed and how to install it. The relay looks again every minute and starts the tunnel
767
+ once `cloudflared` is there; restart the relay if it was installed into a folder that is
768
+ not on the relay's `PATH`. With `required: true` the queue stays paused for as long as
769
+ the tunnel is not connected, a missing `cloudflared` included. Connection drops reconnect with jittered exponential backoff,
752
770
  and repeated fast exits (deleted tunnel or bad token) are reported as `auth-or-config`
753
771
  instead of hammering Cloudflare.
754
772
 
755
773
  The relay status dot turns **amber** while the Cloudflare tunnel is connected, so it is
756
774
  obvious at a glance that the relay is reachable from the internet rather than only from
757
775
  this machine. It stays green when no tunnel is configured and grey when the relay itself
758
- is unreachable; a managed tunnel that has dropped keeps the dot green — the relay still
759
- answers locally — and reports the drop in the tooltip.
776
+ is unreachable; a managed tunnel that has dropped, or could not start, keeps the dot green — the relay
777
+ still answers locally — and gives the reason in the tooltip.
760
778
 
761
779
  ### Session-worker path guard
762
780
 
@@ -891,11 +909,12 @@ under **General** (60 minutes by default), which does not know about the wait.
891
909
 
892
910
  ## Security notes
893
911
 
894
- - The auth token guards the API and the Socket.IO channel. A browser signs in once, with the token or with a URL carrying `?token=` (such as the one `oar setup` prints and puts in its QR code), and then holds an HttpOnly cookie for 30 days, marked `Secure` when the relay is reached over HTTPS. Treat the token, and that URL, like a password.
895
- - Keep the config file private (the relay keeps it owner-only) and rotate `authToken` if it is exposed: `oar setup` offers a new token, which signs every device out.
912
+ - The auth token guards the API and the Socket.IO channel. A browser signs in once, with the token or with a URL carrying `?token=` (such as the one `oar setup` and `oar url` print), and then holds an HttpOnly cookie for 30 days, marked `Secure` when the relay is reached over HTTPS. Treat the token, and that URL, like a password.
913
+ - Keep the config file private (the relay keeps it owner-only) and rotate `authToken` if it is exposed: `oar setup --new-token` writes a new one and restarts the relay, which signs every device out.
896
914
  - `localhostOnly` (default `true`) keeps the relay on loopback. Beyond your LAN, use HTTPS through one of the [tunnels](#remote-access) rather than an open port.
897
915
  - Agents act with the permissions of the user the relay runs as. OAR adds reach, not a sandbox: whoever holds the token can have them run commands on the host. `workspaceRootAllowList` limits the directories conversations may start in.
898
916
  - Shared conversation links and preview links are public by design: anyone with the URL can read the shared transcript or reach the previewed app, without signing in.
917
+ - Files the relay serves (workspace and drive files, attachments) are never run as pages on its origin: HTML and XML go out as plain text, every file carries `nosniff` and a `Content-Security-Policy` with `sandbox`. An SVG keeps its type so it can be previewed, and the sandbox denies it script and an origin should it be opened as a page. A PDF is the one exception, served without `sandbox` (browsers refuse to show a sandboxed PDF in their built-in viewer, and PDF viewers run no document script) and framable only by the relay's own origin.
899
918
  - A [paired relay](#remote-relays) holds a token that opens the other relay completely. The per-relay "Agents may" limit and the "Accept prompts from other relays' agents" switch are honoured by OAR's own traffic, but anyone holding the token can still use the full API: the token is the real boundary. A token entered for a paired relay is stored like the API keys below and never sent to browsers or agents.
900
919
  - API keys you enter (OpenAI, Cursor) are stored in the relay database on the host and never sent to browsers. Claude and Grok credentials stay with their CLIs on the host.
901
920
  - Claude Cloud, when you switch it on, reads the Claude CLI's login and uses it against Anthropic API endpoints that are not a documented public API. The token is only read and held in memory, and goes to `api.anthropic.com` only (see [Claude Cloud](#claude-cloud)). A cloud agent works in Anthropic's sandbox, not on your host.
@@ -906,7 +925,8 @@ under **General** (60 minutes by default), which does not know about the wait.
906
925
  | Symptom | What to check |
907
926
  | ---------------------------------- | -------------------------------------------------------------------------------- |
908
927
  | Banner says *CLI is offline* | No session worker has reported in during the last 10 seconds, which can be normal on an idle relay. Send a message: the relay starts that conversation's worker, and the banner clears once it connects. If it stays, check `/api/status` and the worker's `worker-<session>.log` in the log directory |
909
- | `oar` exits with *Copilot CLI process error: spawn gh ENOENT* | `oar` runs `gh copilot` after starting the relay, and stops the relay again when that fails. Install the GitHub CLI, or [run only the relay](#start-the-relay) |
928
+ | `oar copilot` says the GitHub CLI is missing | It runs `gh copilot` beside the relay. Install the GitHub CLI, or [run only the relay](#start-the-relay) |
929
+ | WSL: the browser on Windows shows another relay, or refuses the token | In WSL's default networking a port can be free in the distro while a Windows program listens on it; `localhost:<port>` in a Windows browser then reaches that program. `oar setup` picks a port that is free on both sides, and `oar status`, `oar start` and `oar doctor` warn when Windows holds the relay's port: choose another with `oar setup --port <port>` |
910
930
  | Settings refuses the **SDK** engine | Install or upgrade the Copilot CLI, run `copilot` once, and restart the relay (see [engine choice](#github-copilot--engine-choice)) |
911
931
  | Messages stuck pending | Only one relay may run per data directory (a second one exits on the singleton lock), and only one process may own port `3333`. `oar doctor` shows the config and database in use |
912
932
  | Wrong or old model list | **🤗 Select Models → Refresh** reruns discovery; `/api/model-variants` shows the Copilot catalog's `source` and `refreshedAt` |
@@ -925,7 +945,8 @@ under **General** (60 minutes by default), which does not know about the wait.
925
945
  | A conversation seems wedged | **☠️ Kill session** in the `⋯` menu stops its worker; retry the turn or send a new message |
926
946
  | **🌄 Restart web relay** fails with *localhost-only* | The restart endpoint accepts loopback connections only: use it on the relay host or through a tunnel, not over a direct LAN connection |
927
947
  | `npm install -g @oar-sh/oar` fails with `EACCES` | npm's global folder needs root (a distribution or NodeSource Node.js). Do not use `sudo`; run the installer (`curl -fsSL oar.sh/install \| sh`), which installs into `~/.oar/npm` instead |
928
- | The relay does not start after an install with npm 12 (a `better-sqlite3` error in `server-err.log`) | npm 12 skipped the install scripts of OAR's native dependencies. Reinstall with `npm install -g --allow-scripts=better-sqlite3,cloudflared,koffi @oar-sh/oar` |
948
+ | The relay does not start after an install with npm 12 (a `better-sqlite3` error in `server-err.log`) | npm 12 skipped the install scripts of OAR's native dependencies. Reinstall with `npm install -g --allow-scripts=better-sqlite3,koffi @oar-sh/oar` |
949
+ | The Cloudflare tunnel stays off and the status dot's tooltip says `cloudflared is not installed` | OAR runs the `cloudflared` on the relay host and ships none. Install it (see [Cloudflare Tunnel](#optional-remote-internet-access-cloudflare-tunnel)); the tunnel starts within a minute |
929
950
  | `npm install -g @oar-sh/oar` fails with node-gyp or prebuild errors | `better-sqlite3` has no prebuilt binary for your platform; install a C/C++ build toolchain and Python, then retry |
930
951
  | No usage line under a reply | Expected for OpenAI, Claude, Claude Cloud, Cursor, and Grok turns; only Copilot turns record plan usage |
931
952