@oar-sh/oar 0.9.23 → 0.9.24
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.
- package/CHANGELOG.md +165 -0
- package/README.md +9 -9
- package/bin/oar.js +54 -4
- package/package.json +9 -7
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +91 -8
- package/server/public/app/bootstrap.js +204 -69
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/settings-modal.js +167 -0
- package/server/public/app/socket-handlers.js +21 -0
- package/server/public/index.html +95 -2
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/messages-routes.mjs +91 -17
- package/server/routes/sessions-routes.mjs +235 -109
- package/server/server-runtime.mjs +395 -81
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +17 -1
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +4 -2
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/oar-cli-helpers.mjs +72 -9
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/update-install-service.mjs +2 -2
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/worker-runtime/direct-openai-image.mjs +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,171 @@
|
|
|
3
3
|
All notable changes to OAR are documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
|
|
5
5
|
|
|
6
|
+
## [0.9.24] — 2026-11-08
|
|
7
|
+
|
|
8
|
+
Highlights: a much smaller install. The Claude and Cursor agent SDKs leave
|
|
9
|
+
the package, and the relay downloads the one a provider needs from npm when
|
|
10
|
+
it is set up or first used: the package takes about 50 MB on Linux x64
|
|
11
|
+
instead of about 650 MB, plus the SDKs of the providers you use. Neither the
|
|
12
|
+
start nor a first chat waits for the download; the chat shows "Installing
|
|
13
|
+
the … SDK" and runs when it ends. Copilot needs nothing downloaded. The
|
|
14
|
+
relay also checks each provider for new models on its own, every 24 hours
|
|
15
|
+
by default, announces new ones with a toast and a "New" tag, and lets you
|
|
16
|
+
choose per provider whether new models start on; OpenAI models can now be
|
|
17
|
+
picked as well. gpt-image replies above about 3 MB are no longer lost.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Check for new models.** The relay now asks each switched-on provider
|
|
22
|
+
for its model list on its own: a minute after it starts for the
|
|
23
|
+
providers that are due, then every 24 hours. Claude and Grok are asked
|
|
24
|
+
only when their CLI is installed or they are signed in. Settings →
|
|
25
|
+
General has a new Models section with the switch, how often (every 6 h,
|
|
26
|
+
12 h, 24 h, 3 days or 7 days) and one "Check <provider>" box per
|
|
27
|
+
provider, each with its last check or "Not set up". A provider whose
|
|
28
|
+
list was refreshed some other way (Refresh in Select Models, a settings
|
|
29
|
+
save, a sign-in, and for Copilot the look it takes when the relay
|
|
30
|
+
starts) waits a full interval again, and a failed check is tried again
|
|
31
|
+
after an hour at the earliest.
|
|
32
|
+
- **New models are announced.** A refresh that finds models it has not
|
|
33
|
+
seen before shows a toast, such as "2 new Cursor models (on)", whose Show
|
|
34
|
+
opens Select Models on that provider. The chat menu's Select Models has a
|
|
35
|
+
dot, and each new model a "New" tag, until Select Models is opened.
|
|
36
|
+
- **Agent SDK rows in Settings.** The Claude, Claude API, DeepSeek and
|
|
37
|
+
Cursor tabs show their SDK's state, the pinned and the installed version,
|
|
38
|
+
the install log and the next automatic retry, with Install, Cancel, Retry
|
|
39
|
+
and Reinstall. On a Windows relay, Settings → General → This relay has a
|
|
40
|
+
"Windows helper" row for koffi, with Retry.
|
|
41
|
+
- **`oar sdk`, and the agent SDKs in `oar doctor`.** `oar doctor` shows the
|
|
42
|
+
SDK folder and, per SDK, the pinned version, the installed one and its
|
|
43
|
+
state; `oar sdk` prints the same lines. `oar sdk install
|
|
44
|
+
claude|cursor|koffi [--reinstall]` installs one without the relay, for a
|
|
45
|
+
headless host whose web UI is the problem.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **The agent SDKs are downloaded on demand.** The Claude Agent SDK (Claude,
|
|
50
|
+
Claude API and DeepSeek chats) and the Cursor SDK are no longer in the
|
|
51
|
+
package, which is smaller by roughly their size: about 50 MB installed
|
|
52
|
+
on Linux x64 instead of about 650 MB. The relay downloads the one a
|
|
53
|
+
provider needs from npm, at the version this release pins: when you
|
|
54
|
+
set the provider up (a Claude login, a provider switched on, a key saved),
|
|
55
|
+
at the provider's first chat, and at start for the providers in use. The
|
|
56
|
+
start never waits for it, and neither does a first chat: it opens at
|
|
57
|
+
once, shows "Installing the … SDK" and delivers the message when the
|
|
58
|
+
install ends; a failed install shows its cause in the chat and keeps the
|
|
59
|
+
message queued until a Retry in Settings, or the automatic retry,
|
|
60
|
+
installs the SDK. The SDKs live in `sdks/` in the state root
|
|
61
|
+
(`OAR_SDK_DIR` moves it; on Windows it must be a local folder of at most
|
|
62
|
+
120 characters): about 360–370 MB for Claude on Windows and Linux x64
|
|
63
|
+
and 345–350 MB on macOS arm64, most of it the Claude Code binary, and
|
|
64
|
+
40–50 MB for Cursor. The pinned version and the one before are kept, and
|
|
65
|
+
any folder a running session uses. An install's progress (npm's output,
|
|
66
|
+
the bytes it downloads) is checked every 15 seconds: 5 minutes without
|
|
67
|
+
any counts as a network failure. An install takes at most 2 hours,
|
|
68
|
+
enough for the Claude SDK on links down to about 0.4 Mbit/s. A download
|
|
69
|
+
that fails for want of a connection, or stalls like that, is tried again
|
|
70
|
+
after 1, 5 and 15 minutes. OAR needs the npm registry once per SDK
|
|
71
|
+
version and does not work offline; the host's npm config (registry,
|
|
72
|
+
proxy, cafile, cache, offline settings) applies. Where a vendor has no
|
|
73
|
+
build, the row says "Not available on this platform": the Cursor SDK on
|
|
74
|
+
Windows arm64, both SDKs on Android under Termux (run OAR in a proot
|
|
75
|
+
Debian there), 32-bit ARM and FreeBSD. See docs/install.md, Agent SDKs.
|
|
76
|
+
- **How an SDK is installed.** `npm ci` from a lockfile that ships with OAR,
|
|
77
|
+
at exact versions, into a folder of its own that counts as installed only
|
|
78
|
+
once its marker, which lists the files and their sizes, checks out.
|
|
79
|
+
Installs run with scripts disabled, koffi's included. A git checkout runs
|
|
80
|
+
the same agent SDK versions as devDependencies, as long as they are
|
|
81
|
+
installed there (not after `npm install --omit=dev`, for example);
|
|
82
|
+
otherwise, or with `OAR_AGENT_SDK_SOURCE=store`, it downloads them like
|
|
83
|
+
an install. Below the pinned packages the versions can differ: the
|
|
84
|
+
Claude SDK's own lockfile has newer releases of eight of them than the
|
|
85
|
+
package's lockfile (@babel/runtime 7.29.13, encodeurl 2.0.1,
|
|
86
|
+
eventsource-parser 3.1.1, express-rate-limit 8.7.3, jose 6.2.15,
|
|
87
|
+
on-finished 2.4.2, range-parser 1.3.0, standardwebhooks 1.1.1).
|
|
88
|
+
- **The Copilot SDK leaves the package.** No server code used
|
|
89
|
+
`@github/copilot-sdk`: Copilot workers run on the SDK that comes with the
|
|
90
|
+
Copilot CLI, and the CLI resolves the extension's
|
|
91
|
+
`@github/copilot-sdk/extension` import itself. Copilot needs nothing
|
|
92
|
+
downloaded.
|
|
93
|
+
- **A plain `npm install -g @oar-sh/oar`.** koffi went with the Copilot
|
|
94
|
+
SDK, so no shipped dependency has an install script any more: npm 12
|
|
95
|
+
needs no `--allow-scripts`, and the **Update** button and `oar update`
|
|
96
|
+
pass none. On Windows the relay downloads koffi itself (about 3 MB) for
|
|
97
|
+
its lock check, with scripts disabled like the SDKs: its binary comes
|
|
98
|
+
prebuilt in `@koromix/koffi-win32-<arch>`.
|
|
99
|
+
- **The Windows lock check without koffi.** Until koffi is there, a relay
|
|
100
|
+
lock whose owner's start time is unknown counts as stale when it was
|
|
101
|
+
written before the last boot, or when its process id now runs a program
|
|
102
|
+
other than Node. A lock left by a crash or a reboot, whose process id
|
|
103
|
+
Windows has handed on, no longer keeps the relay from starting, which
|
|
104
|
+
matters most for a relay started at boot after an update reboot. The
|
|
105
|
+
take-over of an earlier database uses the same rule. The start after
|
|
106
|
+
koffi arrived has the full check again.
|
|
107
|
+
- `engine.io` 6.6.11 (was 6.6.9) under socket.io, from a lockfile refresh:
|
|
108
|
+
a new version of a package already in the tree.
|
|
109
|
+
- In the Claude SDK's install: `@modelcontextprotocol/sdk` 1.32.2 (was
|
|
110
|
+
1.30.0), pinned there as the SDK's peer, with `hono` 4.13.17 (was
|
|
111
|
+
4.13.9), `@hono/node-server` 2.1.6 (was 2.1.0), `fast-uri` 3.1.9 (was
|
|
112
|
+
3.1.8) and `ip-address` 10.7.6 (was 10.4.0). All are new versions of
|
|
113
|
+
packages that were already in the tree; no package was added.
|
|
114
|
+
- **Whether a new model starts on is yours to choose.** Every Select
|
|
115
|
+
Models tab, Copilot and OpenAI included, starts with "Turn new models on
|
|
116
|
+
automatically", on by default. Models that are off today stay off; only
|
|
117
|
+
models found from now on follow the switch. A new effort level of a
|
|
118
|
+
Copilot model follows the model's other levels.
|
|
119
|
+
- **A model you turn off stays off.** Each provider now remembers every
|
|
120
|
+
model it has offered and whether it is on. Unticking everything leaves
|
|
121
|
+
the default model on instead of turning every model back on; a model that
|
|
122
|
+
leaves the list and comes back keeps its state; removing a Cursor,
|
|
123
|
+
DeepSeek, Claude API or OpenAI key and adding one again finds the models
|
|
124
|
+
as they were. When every model you turned on has left the list, the
|
|
125
|
+
default model stands in and the provider's tab says so. The pickers show
|
|
126
|
+
the same models after the upgrade as before.
|
|
127
|
+
- **OpenAI models can be picked.** The Select Models OpenAI tab lists the
|
|
128
|
+
key's models with checkboxes, like the other providers; after the
|
|
129
|
+
upgrade all of them stay on.
|
|
130
|
+
|
|
131
|
+
### Fixed
|
|
132
|
+
|
|
133
|
+
- **gpt-image replies above about 3 MB were lost, and the turn was billed up
|
|
134
|
+
to five times.** The relay checked the base64 of a generated image with a
|
|
135
|
+
regular expression over the whole string. V8 stops optimising regexps once
|
|
136
|
+
a process has generated more than 1 MB of regexp code and holds more than
|
|
137
|
+
16 MB of executable memory, which a relay reaches after some uptime; the
|
|
138
|
+
unoptimised check then overflows V8's regexp stack at 4,194,304 characters
|
|
139
|
+
with "Maximum call stack size exceeded", the reply was answered as a
|
|
140
|
+
provider outage, and the queue retried the paid generation four more times
|
|
141
|
+
before "Relay timeout after 5 attempts". The base64 and data-URL checks on
|
|
142
|
+
the relay's image paths (OpenAI replies, generated images, edit
|
|
143
|
+
attachments) are character loops now; a reply the relay cannot process
|
|
144
|
+
after the provider answered 2xx fails the message at once, with the reason
|
|
145
|
+
and a note that the generation was billed, instead of being requeued, and
|
|
146
|
+
the error is logged with its stack.
|
|
147
|
+
|
|
148
|
+
### Upgrade notes
|
|
149
|
+
|
|
150
|
+
- **The first Claude or Cursor chat after the update may wait.** The new
|
|
151
|
+
relay downloads the agent SDKs of the providers in use at its first start,
|
|
152
|
+
in the background, once: about 360–370 MB for Claude (345–350 MB on
|
|
153
|
+
macOS arm64) and 40–50 MB for Cursor. A chat that comes first shows
|
|
154
|
+
"Installing the … SDK" and runs once the download is done. A host that
|
|
155
|
+
cannot reach the npm registry, or a mirror set in its npm config, cannot
|
|
156
|
+
run Claude, Claude API, DeepSeek or Cursor chats on this version.
|
|
157
|
+
- **Sessions still on 0.9.23's worker wait one worker start.** A session
|
|
158
|
+
whose worker still runs the old code is given a new worker before its
|
|
159
|
+
next message, so the first message after the update waits for that start
|
|
160
|
+
instead of failing.
|
|
161
|
+
- **No migration.** As before, the relay copies its database to the
|
|
162
|
+
backups folder before the first start of a new version.
|
|
163
|
+
- **Going back to 0.9.23** needs no database step. Install it with
|
|
164
|
+
`npm install -g --allow-scripts=koffi @oar-sh/oar@0.9.23` (in a git
|
|
165
|
+
checkout: `git checkout v0.9.23`, then `npm ci`). 0.9.23 does not use
|
|
166
|
+
`sdks/` in the state root; delete it once no 0.9.24 relay runs. Model
|
|
167
|
+
choices made on 0.9.24 do not go back with it: for Claude, Claude API,
|
|
168
|
+
Cursor, Grok and DeepSeek, 0.9.23 shows the models that were on at the
|
|
169
|
+
update, and OpenAI offers every model again; Copilot keeps its choices.
|
|
170
|
+
|
|
6
171
|
## [0.9.23] — 2026-11-07
|
|
7
172
|
|
|
8
173
|
Highlights: a dependency round. The Copilot, Claude and Cursor SDKs, zod,
|
package/README.md
CHANGED
|
@@ -45,29 +45,29 @@ flowchart LR
|
|
|
45
45
|
<table>
|
|
46
46
|
<tr>
|
|
47
47
|
<td width="42%" rowspan="4" valign="top">
|
|
48
|
-
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.
|
|
48
|
+
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.24" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
|
|
49
49
|
<br><sub>On a phone: the same conversations, installable as an app.</sub>
|
|
50
50
|
</td>
|
|
51
51
|
<td width="58%" valign="top">
|
|
52
|
-
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.
|
|
52
|
+
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.24" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
|
|
53
53
|
<br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
|
|
54
54
|
</td>
|
|
55
55
|
</tr>
|
|
56
56
|
<tr>
|
|
57
57
|
<td valign="top">
|
|
58
|
-
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.
|
|
58
|
+
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.24" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
|
|
59
59
|
<br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
|
|
60
60
|
</td>
|
|
61
61
|
</tr>
|
|
62
62
|
<tr>
|
|
63
63
|
<td valign="top">
|
|
64
|
-
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.
|
|
64
|
+
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.24" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
|
|
65
65
|
<br><sub>Read the reasoning, not just the answer.</sub>
|
|
66
66
|
</td>
|
|
67
67
|
</tr>
|
|
68
68
|
<tr>
|
|
69
69
|
<td valign="top">
|
|
70
|
-
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.
|
|
70
|
+
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.24" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
|
|
71
71
|
<br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
|
|
72
72
|
</td>
|
|
73
73
|
</tr>
|
|
@@ -118,11 +118,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
118
118
|
<table>
|
|
119
119
|
<tr>
|
|
120
120
|
<td width="62%" valign="top">
|
|
121
|
-
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.
|
|
121
|
+
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.24" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
|
|
122
122
|
<br><sub>Send a message while the agent works, and it steers the running turn.</sub>
|
|
123
123
|
</td>
|
|
124
124
|
<td width="38%" valign="top">
|
|
125
|
-
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.
|
|
125
|
+
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.24" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
|
|
126
126
|
<br><sub>What each plan has left, in one place.</sub>
|
|
127
127
|
</td>
|
|
128
128
|
</tr>
|
|
@@ -139,7 +139,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
139
139
|
- **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
|
|
140
140
|
- **On your phone**: install OAR as an app and get push notifications; back walks through the conversations you opened, a pinch zooms the whole app, and held sideways it keeps a phone's layout ([Notifications and the app](docs/web-ui.md#notifications-and-the-app), [back and forward](docs/web-ui.md#back-and-forward), [zoom](docs/web-ui.md#zoom), [a phone held sideways](docs/web-ui.md#a-phone-held-sideways)).
|
|
141
141
|
|
|
142
|
-
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.
|
|
142
|
+
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.24](CHANGELOG.md#0924--2026-11-08) as the latest.
|
|
143
143
|
|
|
144
144
|
## Remote access
|
|
145
145
|
|
|
@@ -190,7 +190,7 @@ flowchart LR
|
|
|
190
190
|
|
|
191
191
|
You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
|
|
192
192
|
|
|
193
|
-
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.
|
|
193
|
+
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.24"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.24"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.24" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
|
|
194
194
|
|
|
195
195
|
How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
|
|
196
196
|
|
package/bin/oar.js
CHANGED
|
@@ -18,6 +18,7 @@ import { NATIVE_BUILD_COMMAND, buildBetterSqlite3, needsSourceBuild } from '../s
|
|
|
18
18
|
import { DB_FILE_NAME, DB_FILE_NAMES, LEGACY_DB_FILE_NAME, resolveDatabaseFile } from '../server/services/database-file.mjs';
|
|
19
19
|
import { BACKUP_PROGRESS_FILE, BACKUP_WAIT_FILE, backupDatabase, configWarnings, describeBackups, sanitizeReason } from '../server/services/db-backup-service.mjs';
|
|
20
20
|
import { defaultIsProcessAlive, lockHolderMayRun, processIdentity } from '../server/services/relay-singleton-guard.mjs';
|
|
21
|
+
import { createAgentSdkStore } from '../server/services/agent-sdk-store.mjs';
|
|
21
22
|
import {
|
|
22
23
|
getLauncherLogDir,
|
|
23
24
|
getLegacyManagedConfigDir,
|
|
@@ -26,8 +27,9 @@ import {
|
|
|
26
27
|
resolveStateLayout,
|
|
27
28
|
} from '../server/services/oar-state-layout.mjs';
|
|
28
29
|
import {
|
|
29
|
-
buildDefaultConfig, buildServicePath, buildSystemdUnit, classifySystemdUnit, describeRelayAddress, DOCTOR_PROBES,
|
|
30
|
-
|
|
30
|
+
buildDefaultConfig, buildServicePath, buildSystemdUnit, classifySystemdUnit, describeAgentSdks, describeRelayAddress, DOCTOR_PROBES,
|
|
31
|
+
generateAuthToken, listSystemdDropIns, parseCliArgs, parseSystemdVersion, primaryLanAddress, renderAgentSdkLines,
|
|
32
|
+
renderDoctorReport,
|
|
31
33
|
resolveNpmInvocation, systemdUnitMarkerRoot, systemdUnitRunsCheckout, usageText,
|
|
32
34
|
} from '../server/services/oar-cli-helpers.mjs';
|
|
33
35
|
import { describeConfiguredTunnel, describeTunnelState } from '../server/services/cloudflared-tunnel-service.mjs';
|
|
@@ -2256,7 +2258,6 @@ export async function runDoctor(options = {}) {
|
|
|
2256
2258
|
return { id: probe.id, ok: false };
|
|
2257
2259
|
}
|
|
2258
2260
|
});
|
|
2259
|
-
probes.push({ id: 'cursor', ok: true, version: 'bundled (@cursor/sdk)' });
|
|
2260
2261
|
|
|
2261
2262
|
const port = endpointOf(paths.config).port;
|
|
2262
2263
|
const holder = paths.config ? createWindowsPortCheck(ctx)(port) : null;
|
|
@@ -2277,10 +2278,58 @@ export async function runDoctor(options = {}) {
|
|
|
2277
2278
|
warnings: holder ? [windowsPortWarning(port, holder)] : [],
|
|
2278
2279
|
// The relay resolves a relative cloudflaredTunnel.binary against server/.
|
|
2279
2280
|
tunnel: describeConfiguredTunnel(paths.config?.cloudflaredTunnel, { env: ctx.env, configBaseDir: path.join(ctx.packageRoot, 'server') }),
|
|
2281
|
+
agentSdks: describeAgentSdks({ env: ctx.env, platform: ctx.platform }),
|
|
2280
2282
|
}));
|
|
2281
2283
|
return { code: 0, reason: 'doctor' };
|
|
2282
2284
|
}
|
|
2283
2285
|
|
|
2286
|
+
/**
|
|
2287
|
+
* `oar sdk [status]`: the agent SDKs as `oar doctor` shows them.
|
|
2288
|
+
* `oar sdk install <claude|cursor|koffi> [--reinstall]`: installs one through
|
|
2289
|
+
* the relay's store, without the relay, for a headless host whose web UI is
|
|
2290
|
+
* the problem; npm's output streams to the terminal. Exit 1 when it did not
|
|
2291
|
+
* end ready.
|
|
2292
|
+
*/
|
|
2293
|
+
export async function runSdk(options = {}) {
|
|
2294
|
+
const ctx = cliContext(options);
|
|
2295
|
+
const argv = ctx.argv || [];
|
|
2296
|
+
if (argv[0] !== 'install') {
|
|
2297
|
+
ctx.logger.log(renderAgentSdkLines(describeAgentSdks({ env: ctx.env, platform: ctx.platform })).join('\n'));
|
|
2298
|
+
return { code: 0, reason: 'sdk-status' };
|
|
2299
|
+
}
|
|
2300
|
+
const id = argv[1];
|
|
2301
|
+
if (!id) {
|
|
2302
|
+
ctx.logger.error('[oar] oar sdk install needs the SDK: claude, cursor or koffi.');
|
|
2303
|
+
return { code: 2, reason: 'usage' };
|
|
2304
|
+
}
|
|
2305
|
+
const store = (ctx.createAgentSdkStore || createAgentSdkStore)({ env: ctx.env, platform: ctx.platform, logger: { log: () => {} } });
|
|
2306
|
+
let shown = '';
|
|
2307
|
+
const unsubscribe = store.subscribe((snapshot) => {
|
|
2308
|
+
const text = snapshot.sdks[id]?.log || '';
|
|
2309
|
+
// The log is the retained tail: print what is new since the last look.
|
|
2310
|
+
const fresh = text.startsWith(shown) ? text.slice(shown.length) : text;
|
|
2311
|
+
if (fresh) ctx.stdout.write(fresh);
|
|
2312
|
+
shown = text;
|
|
2313
|
+
});
|
|
2314
|
+
try {
|
|
2315
|
+
const started = store.install(id, { force: argv.includes('--reinstall'), reason: 'oar sdk install' });
|
|
2316
|
+
if (!started.ok) {
|
|
2317
|
+
ctx.logger.error(`[oar] ${started.error}`);
|
|
2318
|
+
return { code: 1, reason: 'sdk-install-failed' };
|
|
2319
|
+
}
|
|
2320
|
+
const status = started.ready ? started.status : await store.ensure(id, { reason: 'oar sdk install', retryFailed: true });
|
|
2321
|
+
if (status?.state !== 'ready') {
|
|
2322
|
+
ctx.logger.error(`[oar] ${status?.label || id}: ${status?.error || 'not installed'}`);
|
|
2323
|
+
return { code: 1, reason: 'sdk-install-failed' };
|
|
2324
|
+
}
|
|
2325
|
+
ctx.logger.log(`[oar] ${status.label} ${status.installed} is ready in ${status.dir}.`);
|
|
2326
|
+
return { code: 0, reason: 'sdk-installed' };
|
|
2327
|
+
} finally {
|
|
2328
|
+
unsubscribe();
|
|
2329
|
+
store.dispose();
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2284
2333
|
/**
|
|
2285
2334
|
* `oar backup [--reason <text>]`: a copy of the relay's database, online
|
|
2286
2335
|
* (safe while the relay runs), into the backup folder with the start's
|
|
@@ -2392,7 +2441,7 @@ async function runUpdate({ argv = [], env = process.env, logger = console, fetch
|
|
|
2392
2441
|
logger.log(`[oar] Updating @oar-sh/oar ${runningVersion} -> ${target} ...`);
|
|
2393
2442
|
const npm = resolveNpmInvocation({ packageRoot, env });
|
|
2394
2443
|
const exitCode = await new Promise((resolve) => {
|
|
2395
|
-
const child = spawnImpl(npm.command, ['install', '-g', ...npm.prefixArgs,
|
|
2444
|
+
const child = spawnImpl(npm.command, ['install', '-g', ...npm.prefixArgs, `@oar-sh/oar@${target}`], {
|
|
2396
2445
|
stdio: 'inherit',
|
|
2397
2446
|
env: npm.env,
|
|
2398
2447
|
...(process.platform === 'win32' ? { shell: true, windowsHide: true } : {}),
|
|
@@ -2523,6 +2572,7 @@ const COMMANDS = {
|
|
|
2523
2572
|
backup: runBackup,
|
|
2524
2573
|
update: runUpdate,
|
|
2525
2574
|
'build-native': runBuildNative,
|
|
2575
|
+
sdk: runSdk,
|
|
2526
2576
|
help: async ({ logger = console } = {}) => {
|
|
2527
2577
|
logger.log(usageText());
|
|
2528
2578
|
return { code: 0, reason: 'help' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oar-sh/oar",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,20 +58,22 @@
|
|
|
58
58
|
"test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
|
62
|
-
"@cursor/sdk": "1.3.1",
|
|
63
|
-
"@github/copilot-sdk": "1.0.29",
|
|
64
61
|
"better-sqlite3": "13.0.3",
|
|
65
62
|
"express": "5.3.0",
|
|
66
63
|
"qrcode-terminal": "0.12.0",
|
|
67
64
|
"socket.io": "4.8.4",
|
|
68
65
|
"web-push": "3.6.7",
|
|
69
|
-
"ws": "8.22.1"
|
|
70
|
-
"zod": "4.6.7"
|
|
66
|
+
"ws": "8.22.1"
|
|
71
67
|
},
|
|
72
68
|
"devDependencies": {
|
|
69
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
|
70
|
+
"@anthropic-ai/sdk": "0.116.0",
|
|
71
|
+
"@cursor/sdk": "1.3.1",
|
|
72
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
73
73
|
"@playwright/test": "^1.60.0",
|
|
74
|
-
"jsdom": "^30.0.1"
|
|
74
|
+
"jsdom": "^30.0.1",
|
|
75
|
+
"koffi": "3.3.4",
|
|
76
|
+
"zod": "4.6.7"
|
|
75
77
|
},
|
|
76
78
|
"overrides": {
|
|
77
79
|
"undici": "6.28.0"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
agentSdkFromCheckout,
|
|
6
|
+
getAgentSdkDescriptor,
|
|
7
|
+
isValidAgentSdkInstall,
|
|
8
|
+
writeAgentSdkLease,
|
|
9
|
+
} from './services/agent-sdk-paths.mjs';
|
|
10
|
+
import { currentBootId, processIdentity } from './services/relay-singleton-guard.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Loads an agent SDK the relay installs on demand (agent-sdk-store.mjs),
|
|
14
|
+
* wherever this process has it:
|
|
15
|
+
*
|
|
16
|
+
* 1. OAR_AGENT_SDK_DIR_<SDK>: the folder the relay launched this worker
|
|
17
|
+
* with, kept for the worker's whole life;
|
|
18
|
+
* 2. in the relay itself, the store's ready folder (setAgentSdkStore);
|
|
19
|
+
* 3. in a git checkout that has it installed, the package's devDependency.
|
|
20
|
+
*
|
|
21
|
+
* Otherwise AgentSdkMissingError. A folder is imported through its entry
|
|
22
|
+
* file by path: the bare imports in it resolve from the folder's own
|
|
23
|
+
* node_modules, so there is no NODE_PATH and no exports map to read.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const AGENT_SDK_MISSING_CODE = 'AGENT_SDK_MISSING';
|
|
27
|
+
|
|
28
|
+
export class AgentSdkMissingError extends Error {
|
|
29
|
+
constructor(sdk, detail = '') {
|
|
30
|
+
const label = getAgentSdkDescriptor(sdk)?.label || sdk;
|
|
31
|
+
super(`${label} is not installed${detail ? ` (${detail})` : ''}.`);
|
|
32
|
+
this.name = 'AgentSdkMissingError';
|
|
33
|
+
this.code = AGENT_SDK_MISSING_CODE;
|
|
34
|
+
this.sdk = sdk;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function agentSdkDirEnvName(sdk) {
|
|
39
|
+
return `OAR_AGENT_SDK_DIR_${String(sdk || '').toUpperCase()}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let relayStore = null;
|
|
43
|
+
const cache = new Map();
|
|
44
|
+
const leased = new Set();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The relay registers its store at start. From then on the environment's
|
|
48
|
+
* folder is not looked at in this process: a relay started from inside a
|
|
49
|
+
* worker's session would otherwise take that worker's SDK.
|
|
50
|
+
*/
|
|
51
|
+
export function setAgentSdkStore(store) {
|
|
52
|
+
relayStore = store || null;
|
|
53
|
+
cache.clear();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// What a checkout's devDependency gives, in the shape of the entry files.
|
|
57
|
+
const CHECKOUT_IMPORTS = {
|
|
58
|
+
claude: {
|
|
59
|
+
main: async () => ({ ...(await import('@anthropic-ai/claude-agent-sdk')), z: await import('zod') }),
|
|
60
|
+
},
|
|
61
|
+
cursor: {
|
|
62
|
+
main: () => import('@cursor/sdk'),
|
|
63
|
+
sqlite: () => import('@cursor/sdk/sqlite'),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function importFromDir(descriptor, dir, entry, { lease, env }) {
|
|
68
|
+
if (!isValidAgentSdkInstall(dir, { id: descriptor.id })) {
|
|
69
|
+
throw new AgentSdkMissingError(descriptor.id, `${dir} is not a finished install`);
|
|
70
|
+
}
|
|
71
|
+
if (lease && !leased.has(dir)) {
|
|
72
|
+
leased.add(dir);
|
|
73
|
+
writeAgentSdkLease(dir, {
|
|
74
|
+
holder: (() => { try { return processIdentity(process.pid); } catch { return null; } })(),
|
|
75
|
+
bootId: currentBootId(),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return import(pathToFileURL(path.join(dir, descriptor.entries[entry])).href);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Where loadAgentSdk would take the SDK from: { source, dir }, or null. */
|
|
82
|
+
export function resolveAgentSdkSource(sdk, { env = process.env, store = relayStore, fromCheckout } = {}) {
|
|
83
|
+
const descriptor = getAgentSdkDescriptor(sdk);
|
|
84
|
+
if (!descriptor) return null;
|
|
85
|
+
if (!store) {
|
|
86
|
+
const dir = String(env?.[agentSdkDirEnvName(descriptor.id)] || '').trim();
|
|
87
|
+
if (dir) return { source: 'env', dir: path.resolve(dir) };
|
|
88
|
+
} else {
|
|
89
|
+
const dir = store.readyDir(descriptor.id);
|
|
90
|
+
if (dir) return { source: 'store', dir };
|
|
91
|
+
}
|
|
92
|
+
if (fromCheckout ?? agentSdkFromCheckout(descriptor.id, { env })) return { source: 'checkout', dir: null };
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The SDK's module namespace (`entry`: 'main', or 'sqlite' for Cursor's
|
|
98
|
+
* local store). Claude's carries `z`, the zod its tools are written with.
|
|
99
|
+
*/
|
|
100
|
+
export async function loadAgentSdk(sdk, { entry = 'main', env = process.env, store = relayStore, fromCheckout } = {}) {
|
|
101
|
+
const descriptor = getAgentSdkDescriptor(sdk);
|
|
102
|
+
if (!descriptor || !descriptor.entries[entry]) throw new Error(`Unknown agent SDK entry: ${sdk}/${entry}`);
|
|
103
|
+
const found = resolveAgentSdkSource(descriptor.id, { env, store, fromCheckout });
|
|
104
|
+
if (!found) throw new AgentSdkMissingError(descriptor.id);
|
|
105
|
+
const key = `${descriptor.id}:${entry}:${found.source}:${found.dir || ''}`;
|
|
106
|
+
if (!cache.has(key)) {
|
|
107
|
+
const loading = found.source === 'checkout'
|
|
108
|
+
? CHECKOUT_IMPORTS[descriptor.id][entry]()
|
|
109
|
+
// The relay's own import (model discovery) holds the folder for the
|
|
110
|
+
// relay's life, a worker's for its own: both lease it.
|
|
111
|
+
: importFromDir(descriptor, found.dir, entry, { lease: true, env });
|
|
112
|
+
cache.set(key, loading);
|
|
113
|
+
loading.catch(() => { if (cache.get(key) === loading) cache.delete(key); });
|
|
114
|
+
}
|
|
115
|
+
return cache.get(key);
|
|
116
|
+
}
|