@oar-sh/oar 0.9.7 → 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 (64) hide show
  1. package/CHANGELOG.md +344 -0
  2. package/README.md +89 -40
  3. package/bin/oar.js +1023 -135
  4. package/package.json +4 -5
  5. package/server/README.md +20 -1
  6. package/server/claude-worker/claude-sdk-adapter.mjs +0 -11
  7. package/server/claude-worker/claude-session-process.mjs +196 -28
  8. package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
  9. package/server/db-schema.mjs +8 -0
  10. package/server/migrations/0008-message-pins.mjs +85 -0
  11. package/server/public/app/activity-replay-state.mjs +68 -5
  12. package/server/public/app/api-client.js +36 -2
  13. package/server/public/app/attachments-view.js +508 -125
  14. package/server/public/app/back-guard.mjs +58 -0
  15. package/server/public/app/bootstrap.js +170 -59
  16. package/server/public/app/context-usage-view.mjs +127 -8
  17. package/server/public/app/conversation-view.js +172 -32
  18. package/server/public/app/external-link-policy.mjs +4 -1
  19. package/server/public/app/file-version.mjs +58 -0
  20. package/server/public/app/journal-view.js +16 -2
  21. package/server/public/app/pinned-messages-view.mjs +253 -0
  22. package/server/public/app/pwa-install.js +6 -1
  23. package/server/public/app/relay-dot-state.mjs +8 -1
  24. package/server/public/app/router.js +8 -2
  25. package/server/public/app/scrollbar-hover.mjs +78 -0
  26. package/server/public/app/settings-modal.js +4 -1
  27. package/server/public/app/socket-handlers.js +8 -1
  28. package/server/public/app/store.js +32 -0
  29. package/server/public/app/transcript-separators.mjs +48 -11
  30. package/server/public/app/viewer-chat-gallery.mjs +59 -0
  31. package/server/public/app/viewer-controls-state.mjs +59 -0
  32. package/server/public/app/viewer-gallery.mjs +46 -0
  33. package/server/public/app/viewer-gestures.mjs +105 -0
  34. package/server/public/app/viewer-history.mjs +43 -0
  35. package/server/public/app/viewer-tap.mjs +112 -0
  36. package/server/public/app-icon-192.png +0 -0
  37. package/server/public/app-icon-512.png +0 -0
  38. package/server/public/app-icon-light-192.png +0 -0
  39. package/server/public/app-icon-light-512.png +0 -0
  40. package/server/public/app-icon-light-maskable-512.png +0 -0
  41. package/server/public/app-icon-light.svg +4 -0
  42. package/server/public/app-icon-maskable-512.png +0 -0
  43. package/server/public/app-icon.svg +3 -3
  44. package/server/public/favicon.ico +0 -0
  45. package/server/public/index.html +487 -67
  46. package/server/public/manifest.webmanifest +9 -3
  47. package/server/public/sw.js +6 -1
  48. package/server/repositories/message-pin-statements.mjs +39 -0
  49. package/server/repositories/message-repository.mjs +5 -0
  50. package/server/routes/messages-routes.mjs +36 -62
  51. package/server/routes/sessions-routes.mjs +107 -26
  52. package/server/server-runtime.mjs +39 -8
  53. package/server/services/claude-context-usage.mjs +51 -3
  54. package/server/services/cloudflared-tunnel-service.mjs +139 -41
  55. package/server/services/file-serving.mjs +155 -0
  56. package/server/services/message-pin-list.mjs +52 -0
  57. package/server/services/oar-cli-helpers.mjs +205 -4
  58. package/server/services/oar-cli-menu.mjs +240 -0
  59. package/server/services/oar-cli-wsl.mjs +129 -0
  60. package/server/services/pwa-manifest-theme.mjs +30 -0
  61. package/server/services/safe-served-content.mjs +61 -11
  62. package/server/services/session-history-refresh-service.mjs +24 -0
  63. package/server/services/session-worker-registry-service.mjs +18 -1
  64. package/server/services/update-install-service.mjs +9 -3
package/README.md CHANGED
@@ -1,3 +1,10 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="server/public/app-icon.svg">
4
+ <img src="server/public/app-icon-light.svg" alt="The OAR logo: an otter standing upright" width="120" height="120">
5
+ </picture>
6
+ </p>
7
+
1
8
  # OAR — Open Agent Relay
2
9
 
3
10
  Drive the coding agents on your machine from any browser (phone, tablet, or a second computer) through a self-hosted web relay.
@@ -29,23 +36,23 @@ OAR is under active development, so expect occasional rough edges and some provi
29
36
  <table>
30
37
  <tr>
31
38
  <td width="42%" rowspan="3" valign="top">
32
- <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.3"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.3" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></a>
39
+ <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.8"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.8" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></a>
33
40
  <br><sub>On a phone: the same conversations, installable as an app.</sub>
34
41
  </td>
35
42
  <td width="58%" valign="top">
36
- <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.3"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.3" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></a>
43
+ <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.8"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.8" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></a>
37
44
  <br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
38
45
  </td>
39
46
  </tr>
40
47
  <tr>
41
48
  <td valign="top">
42
- <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.3"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.3" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></a>
49
+ <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.8"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.8" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></a>
43
50
  <br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
44
51
  </td>
45
52
  </tr>
46
53
  <tr>
47
54
  <td valign="top">
48
- <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.3"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.3" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></a>
55
+ <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.8"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.8" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></a>
49
56
  <br><sub>Read the reasoning, not just the answer.</sub>
50
57
  </td>
51
58
  </tr>
@@ -61,14 +68,15 @@ You need Node.js plus whatever the runtimes you actually use need. Nothing else
61
68
 
62
69
  | Requirement | Needed for | Notes |
63
70
  | ----------- | ---------- | ----- |
64
- | Node.js 22.13 or newer | always | Both installers check this. Running the development test suite needs Node 24 (see [DEVELOPING.md](DEVELOPING.md#node-version)) |
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)) |
65
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 |
66
- | 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` |
67
74
  | OpenAI API key | OpenAI and OpenAI Image chats | Entered in **⚙️ Settings**, stored in the relay database |
68
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 |
69
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)) |
70
77
  | Cursor API key | Cursor chats | Entered in **⚙️ Settings**, stored in the relay database |
71
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)) |
72
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 |
73
81
 
74
82
  ### macOS and Linux
@@ -77,12 +85,27 @@ You need Node.js plus whatever the runtimes you actually use need. Nothing else
77
85
  curl -fsSL oar.sh/install | sh
78
86
  ```
79
87
 
80
- The script checks for Node.js 22.13 or newer, runs `npm install -g @oar-sh/oar`, and then hands off to `oar setup` (with `--defaults` when there is no terminal to ask questions in). It refuses to run as root, because OAR installs per user. `OAR_VERSION=x.y.z` pins a version, `OAR_CHANNEL=beta` follows the beta channel, and `OAR_DRY_RUN=1` only prints what it would do:
88
+ The script looks at what the machine has, prints what it is about to do, and asks once. After a yes it installs OAR, writes the config with a fresh auth token, starts the relay, and prints the relay URL with a QR code. Nothing needs root (it refuses to run as root, because OAR installs per user), and everything lands in your home directory:
89
+
90
+ | The machine has | What the installer does |
91
+ | --------------- | ----------------------- |
92
+ | Node.js 22.13 or newer whose global npm folder you can write (nvm, fnm, Homebrew, a user prefix) | `npm install -g @oar-sh/oar`, as you would by hand |
93
+ | Node.js 22.13 or newer whose global npm folder needs root (a distribution or NodeSource package) | Installs the package into `~/.oar/npm` with your Node.js and puts an `oar` command into `~/.local/bin`. Your npm setup is not changed |
94
+ | No Node.js, or an older one | Downloads the current Node.js 24 from nodejs.org into `~/.oar/runtime/node` (checksum verified, about 30 MB), installs the package into `~/.oar/npm` with it, and puts an `oar` command into `~/.local/bin`. Only OAR uses that Node.js; one of your own stays first on `PATH` |
95
+
96
+ When it puts the command into `~/.local/bin` and that folder is not on your `PATH`, it adds one line to your shell's startup file (`~/.bashrc`, `~/.zshrc`, or `~/.profile`) and says so.
97
+
98
+ The relay is started as a systemd user service where a systemd user session exists, with lingering enabled, so it is back after a reboot and keeps running while you are logged out. Elsewhere (macOS, containers, WSL without systemd) it is started in the background; after a reboot, `oar start` starts it again.
99
+
100
+ `--yes` skips the question, for scripts and CI; without a terminal to ask on, the installer stops and says so. `OAR_VERSION=x.y.z` pins a version, `OAR_CHANNEL=beta` follows the beta channel, `OAR_PORT=n` sets the relay port, `OAR_NODE=private` uses OAR's own Node.js even when yours would do, and `OAR_DRY_RUN=1` only prints the plan:
81
101
 
82
102
  ```bash
83
- curl -fsSL oar.sh/install | OAR_VERSION=0.9.6 sh
103
+ curl -fsSL oar.sh/install | sh -s -- --yes
104
+ curl -fsSL oar.sh/install | OAR_VERSION=0.9.9 OAR_PORT=3400 sh
84
105
  ```
85
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).
108
+
86
109
  Read it before you run it: [oar.sh/install](https://oar.sh/install).
87
110
 
88
111
  ### Windows
@@ -91,15 +114,17 @@ Read it before you run it: [oar.sh/install](https://oar.sh/install).
91
114
  irm oar.sh/install.ps1 | iex
92
115
  ```
93
116
 
94
- The PowerShell installer (PowerShell 5 or newer) does the same: 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.
95
118
 
96
119
  ### npm
97
120
 
98
121
  ```bash
99
- npm install -g @oar-sh/oar
122
+ npm install -g --allow-scripts=better-sqlite3,koffi @oar-sh/oar
100
123
  oar setup
101
124
  ```
102
125
 
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.
127
+
103
128
  ### From a git checkout
104
129
 
105
130
  ```bash
@@ -126,13 +151,13 @@ Development workflows, tests, and relay internals are in [DEVELOPING.md](DEVELOP
126
151
 
127
152
  ### Start the relay
128
153
 
129
- `oar setup` creates the config and prints the relay URL with a QR code, but it does not start the relay. 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.
130
155
 
131
- - **Linux (global install):** 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.
132
- - **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.
133
- - **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.
134
159
 
135
- To run only the relay from a global install, without a Copilot session, start the server the way the systemd unit does:
160
+ To run the relay of a global install in the foreground, start the server the way the systemd unit does:
136
161
 
137
162
  ```bash
138
163
  COPILOT_WEB_RELAY_CONFIG="$HOME/.oar/config.json" \
@@ -143,25 +168,28 @@ node "$(npm root -g)/@oar-sh/oar/server/server.js"
143
168
 
144
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.
145
170
 
146
- 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.
147
172
 
148
173
  ### The `oar` command
149
174
 
150
175
  | Command | What it does |
151
176
  | ------- | ------------ |
152
- | `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 |
153
- | `oar -- <args>` | The same, passing `<args>` through to the Copilot CLI (`gh copilot -- <args>`) |
154
- | `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`) |
155
- | `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 |
156
- | `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 |
157
- | `oar setup [--defaults]` | 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 |
158
- | `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 |
159
- | `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`, 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 |
160
- | `oar --version`, `oar --help` | Prints the version, or the usage |
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 |
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 |
188
+ | `oar --version`, `oar help` | Prints the version, or the usage. An unknown command or option prints the usage and exits with code 2 |
161
189
 
162
190
  ### Where OAR keeps its files
163
191
 
164
- A global install keeps its state in `~/.oar` (`%APPDATA%\oar` on Windows; `OAR_STATE_ROOT` moves it): `config.json`, `data/` (the database and uploads), and `logs/` (`server.log` when `oar` started the relay, and a `worker-<session>.log` per Node session worker). `npm install -g` updates never touch it. A git checkout uses `server/config.json`, `server/data/`, `server/uploads/`, and `server/logs/` instead.
192
+ A global install keeps its state in `~/.oar` (`%APPDATA%\oar` on Windows; `OAR_STATE_ROOT` moves it): `config.json`, `data/` (the database and uploads), and `logs/` (`server.log` when `oar` started the relay, and a `worker-<session>.log` per Node session worker). `npm install -g` updates never touch it. The macOS and Linux installer may add two folders there that are not state: `npm/` (the package, when npm's own global folder was not writable) and `runtime/node/` (OAR's own Node.js, when the machine had none new enough). A git checkout uses `server/config.json`, `server/data/`, `server/uploads/`, and `server/logs/` instead.
165
193
 
166
194
  `COPILOT_WEB_RELAY_CONFIG`, `COPILOT_WEB_RELAY_DATA_DIR`, and `COPILOT_WEB_RELAY_LOG_DIR` override the config file, the data directory, and the log directory. `oar doctor` prints the paths in use.
167
195
 
@@ -440,12 +468,13 @@ Use **🤗 Select Models** to choose which variants show up in the composer, the
440
468
  - Question cards for clarification: one-click choices, multi-select checkmarks, free text, and multi-field structured forms validated against their JSON schema
441
469
  - Mathematical and scientific notation rendering for TeX/LaTeX equations and chemical formulas, written as `$…$`, `\(…\)`, `$$…$$` or `\[…\]`. A single `$` opens a formula only when a non-space character follows it directly and the next `$` has a non-space character directly before it and no digit directly after it (a digit is allowed after something that looks like TeX, as in `$x^2$3`), so prices such as `$5 and $9` stay text
442
470
  - **Context usage** modal with a per-category token breakdown of the model's context window, plus a per-conversation **auto-compact window** slider for Claude sessions
443
- - **Transcript breaks**: day separators, a marker where a Claude session auto-compacted its context, and matching dots beside the scrollbar
471
+ - **Transcript breaks**: day separators, a marker where a Claude session auto-compacted its context (shown while the compaction runs), and matching dots beside the scrollbar
444
472
  - **Plan usage** modal with subscription credits, rate-limit windows, and reset countdowns for Copilot, Claude, Claude Cloud, Cursor, and Grok
445
473
  - **Image conversations** (OpenAI Image): generate images in chat and iterate on a generated image with **Edit this image**
446
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
447
475
  - **Screenshot annotations**: mark up an uploaded screenshot with highlighter strokes before or after sending; the original upload is never modified
448
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
449
478
  - Conversation history in local SQLite, including the Copilot sessions already stored on the host
450
479
  - `/compact` continues in a fresh conversation seeded with a summary; `/preview` publishes a local dev server or folder on a public preview host
451
480
  - Workspace and drive browser with file previews, **Git changes** with a diff viewer, and `@file:` / `@folder:` reference tokens
@@ -525,13 +554,17 @@ The composer draft, attachments included, is saved per conversation on the relay
525
554
  - **Grok** — the live weekly subscription quota, read with the host's Grok CLI login, plus per-turn tokens and estimated cost from the agent's prompt result. An optional monthly USD allowance in Settings adds an estimated remaining meter; the card is hidden when Grok is disabled. Billing: [console.x.ai](https://console.x.ai).
526
555
  - Per-reply usage lines are recorded only for Copilot turns — OpenAI, Claude, Claude Cloud, Cursor, and Grok turns do not consume Copilot premium requests, and no usage line is attached to them.
527
556
  - Use the **🧠** context button for a per-category breakdown of the conversation's context window (Claude sessions also show the auto-compact window, the thinking controls and the folder's commit attribution there): a usage bar, a token/percentage table, and free space. Claude sessions report exact SDK categories; Copilot sessions show the coarser system/tools + messages + buffer split, labelled as a lower-bound estimate when the runtime no longer emits full buckets. A Claude Cloud session reports how much of its window is used after each turn, without categories.
528
- - Claude conversations additionally get an **auto-compact window** slider in that modal. Claude Code compacts a session once it approaches a model-tuned window (around 967k tokens on a 1M-context model), which is why long conversations rarely compact at all; setting a smaller window makes it happen sooner and keeps turns cheaper. *Auto* hands the choice back to the CLI. The smallest window is 100k, because the CLI silently ignores anything below that and falls back to its own default. The line beneath the slider reports the window actually in force and where it came from — your setting, the model default, or the `CLAUDE_CODE_AUTO_COMPACT_WINDOW` environment override — and fills in once the conversation's first turn completes. The change reaches a running session on its next message.
529
- - The transcript marks day boundaries, and marks the point where a Claude session compacted its context with the tokens before and after. Both appear as dots beside the scrollbar for the messages currently loaded.
557
+ - Claude conversations additionally get an **auto-compact window** slider in that modal. Claude Code compacts a session once it approaches a model-tuned window (around 967k tokens on a 1M-context model), which is why long conversations rarely compact at all; setting a smaller window makes it happen sooner and keeps turns cheaper. *Auto* hands the choice back to the CLI. The smallest window is 100k, because the CLI silently ignores anything below that and falls back to its own default. The line beneath the slider reports the window actually in force and where it came from — your setting, the model default, or the `CLAUDE_CODE_AUTO_COMPACT_WINDOW` environment override — and fills in once the conversation's first turn completes. The slider is saved when the modal closes. Claude Code reads the window only when a session starts, so a running session restarts once, before the next message, to pick up a new one; while a turn or background work is running the change waits, and the note under the slider says for what. A window below what the conversation already uses compacts it on the next message, which the slider warns about. The modal's headline shows the tokens in use, the compaction window and, when known, the model's own limit.
558
+ - The transcript marks day boundaries, and marks the point where a Claude session compacted its context with the tokens before and after. While a compaction runs, the line reads *Compacting context…* and the turn's bubble says so; it turns into the final line when the compaction ends. Both kinds appear as dots beside the scrollbar for the messages currently loaded.
530
559
 
531
560
  ### Sharing
532
561
 
533
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.
534
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
+
535
568
  ### Notifications and the app
536
569
 
537
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.
@@ -614,7 +647,7 @@ The config file is `server/config.json` in a git checkout and `~/.oar/config.jso
614
647
  | `cloudflaredTunnel.mode` | `disabled` | Cloudflare tunnel mode (`disabled` or `managed`; `enabled: true` is a legacy alias for `managed`) |
615
648
  | `cloudflaredTunnel.required` | `false` | Pause dequeue while the managed Cloudflare tunnel is disconnected |
616
649
  | `cloudflaredTunnel.token` | — | The tunnel token from your own Cloudflare Zero Trust tunnel |
617
- | `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` |
618
651
  | `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
619
652
  | `previews.enabled` | `false` | Publish local dev servers on a separate listener (see [docs/preview-servers.md](docs/preview-servers.md)) |
620
653
  | `previews.port` | `port + 1` | Loopback port for the preview listener; `0` picks an ephemeral port |
@@ -700,11 +733,18 @@ An alternative to the SSH tunnel that needs no VPS and no inbound port: the rela
700
733
  supervises Cloudflare's `cloudflared` binary, and Cloudflare carries your hostname down
701
734
  to `127.0.0.1:3333`.
702
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
+
703
744
  1. In your own Cloudflare Zero Trust account, create a tunnel and route a public hostname on
704
745
  your zone to `http://localhost:3333` (or your `port`). That routing lives in Cloudflare, not
705
746
  on this machine.
706
- 2. Put the tunnel's token into the config and restart the relay. (`oar setup` can switch the
707
- 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`).
708
748
 
709
749
  ```json
710
750
  "cloudflaredTunnel": {
@@ -720,17 +760,21 @@ Environment overrides: `COPILOT_CLOUDFLARED_MODE`, `COPILOT_CLOUDFLARED_TOKEN`,
720
760
  `COPILOT_CLOUDFLARED_BINARY`.
721
761
 
722
762
  `localhostOnly` stays `true`: `cloudflared` connects outbound and nothing binds publicly.
723
- The binary resolves from `cloudflaredTunnel.binary`, then the optional `cloudflared` npm
724
- package, then `PATH`; a managed config with no resolvable binary is reported as a config
725
- 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,
726
770
  and repeated fast exits (deleted tunnel or bad token) are reported as `auth-or-config`
727
771
  instead of hammering Cloudflare.
728
772
 
729
773
  The relay status dot turns **amber** while the Cloudflare tunnel is connected, so it is
730
774
  obvious at a glance that the relay is reachable from the internet rather than only from
731
775
  this machine. It stays green when no tunnel is configured and grey when the relay itself
732
- is unreachable; a managed tunnel that has dropped keeps the dot green — the relay still
733
- 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.
734
778
 
735
779
  ### Session-worker path guard
736
780
 
@@ -865,11 +909,12 @@ under **General** (60 minutes by default), which does not know about the wait.
865
909
 
866
910
  ## Security notes
867
911
 
868
- - 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.
869
- - 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.
870
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.
871
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.
872
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.
873
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.
874
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.
875
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.
@@ -880,7 +925,8 @@ under **General** (60 minutes by default), which does not know about the wait.
880
925
  | Symptom | What to check |
881
926
  | ---------------------------------- | -------------------------------------------------------------------------------- |
882
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 |
883
- | `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>` |
884
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)) |
885
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 |
886
932
  | Wrong or old model list | **🤗 Select Models → Refresh** reruns discovery; `/api/model-variants` shows the Copilot catalog's `source` and `refreshedAt` |
@@ -898,6 +944,9 @@ under **General** (60 minutes by default), which does not know about the wait.
898
944
  | A conversation will not delete | It is still working: a running turn, one waiting on a question card or approval, or live background tasks. Stop it, then delete |
899
945
  | A conversation seems wedged | **☠️ Kill session** in the `⋯` menu stops its worker; retry the turn or send a new message |
900
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 |
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 |
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 |
901
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 |
902
951
  | No usage line under a reply | Expected for OpenAI, Claude, Claude Cloud, Cursor, and Grok turns; only Copilot turns record plan usage |
903
952