@oar-sh/oar 0.9.12 → 0.9.14

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 (115) hide show
  1. package/.github/extensions/web-relay/extension.mjs +9 -0
  2. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +15 -1
  3. package/.github/extensions/web-relay/skills/question-bridge.mjs +4 -1
  4. package/CHANGELOG.md +508 -0
  5. package/README.md +12 -12
  6. package/bin/oar.js +155 -94
  7. package/package.json +5 -5
  8. package/server/README.md +479 -157
  9. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
  10. package/server/claude-worker/claude-session-process.mjs +92 -15
  11. package/server/claude-worker/claude-turn-publisher.mjs +20 -1
  12. package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
  13. package/server/claude-worker/task-output-discovery.mjs +28 -2
  14. package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
  15. package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
  16. package/server/copilot-worker/copilot-prompt-context.mjs +3 -1
  17. package/server/copilot-worker/copilot-question-bridge.mjs +11 -6
  18. package/server/copilot-worker/copilot-sdk-adapter.mjs +11 -0
  19. package/server/copilot-worker/copilot-sdk-session-process.mjs +24 -1
  20. package/server/cursor-worker/cursor-ask-user-tool.mjs +1 -1
  21. package/server/cursor-worker/cursor-sdk-adapter.mjs +22 -7
  22. package/server/cursor-worker/cursor-session-worker.mjs +9 -2
  23. package/server/cursor-worker/cursor-turn-runner.mjs +18 -3
  24. package/server/db-schema.mjs +10 -0
  25. package/server/grok-worker/acp-host-services.mjs +6 -3
  26. package/server/grok-worker/grok-background-tasks.mjs +7 -0
  27. package/server/grok-worker/grok-sdk-adapter.mjs +14 -0
  28. package/server/grok-worker/grok-turn-runner.mjs +13 -3
  29. package/server/migrations/0018-background-task-rows.mjs +101 -0
  30. package/server/public/app/action-confirmations.js +12 -15
  31. package/server/public/app/api-client.js +60 -4
  32. package/server/public/app/ask-user-view.js +29 -4
  33. package/server/public/app/background-tasks-view.mjs +56 -7
  34. package/server/public/app/bootstrap.js +7 -0
  35. package/server/public/app/conversation-preferences.mjs +10 -4
  36. package/server/public/app/conversation-provider-indicator.mjs +18 -8
  37. package/server/public/app/conversation-view.js +33 -9
  38. package/server/public/app/host-suspend-ui.js +45 -44
  39. package/server/public/app/journal-view.js +9 -3
  40. package/server/public/app/push-settings.js +1 -0
  41. package/server/public/app/question-answer-rule.mjs +15 -0
  42. package/server/public/app/question-multi-select.mjs +9 -2
  43. package/server/public/app/relay-event-view.mjs +5 -1
  44. package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
  45. package/server/public/app/relay-restart-note-view.mjs +36 -0
  46. package/server/public/app/relay-restart-ui.js +568 -0
  47. package/server/public/app/settings-modal.js +52 -0
  48. package/server/public/app/socket-handlers.js +1 -1
  49. package/server/public/app/task-console-render.mjs +171 -7
  50. package/server/public/app/task-console.mjs +111 -11
  51. package/server/public/app/task-output-text.mjs +15 -6
  52. package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
  53. package/server/public/app/transcript-jump-bottom.mjs +20 -0
  54. package/server/public/index.html +76 -5
  55. package/server/relay-exit-codes.mjs +6 -0
  56. package/server/relay-self-restart.mjs +6 -1
  57. package/server/relay-tools.md +17 -9
  58. package/server/repositories/message-repository.mjs +32 -28
  59. package/server/repositories/remote-relay-repository.mjs +4 -2
  60. package/server/repositories/subagent-timeline-statements.mjs +2 -0
  61. package/server/routes/ask-user-routes.mjs +41 -7
  62. package/server/routes/messages-routes.mjs +220 -37
  63. package/server/routes/preview-routes.mjs +73 -7
  64. package/server/routes/relay-board-routes.mjs +19 -2
  65. package/server/routes/relay-restart-routes.mjs +73 -0
  66. package/server/routes/sessions-routes.mjs +79 -5
  67. package/server/server-runtime.mjs +335 -218
  68. package/server/services/about-service.mjs +13 -12
  69. package/server/services/background-task-persistence.mjs +133 -0
  70. package/server/services/background-task-store.mjs +240 -13
  71. package/server/services/conversation-model-fallback.mjs +57 -0
  72. package/server/services/cursor-session-root-service.mjs +70 -6
  73. package/server/services/data-dir-resolution.mjs +208 -19
  74. package/server/services/earlier-database-service.mjs +631 -0
  75. package/server/services/host-suspend-command.mjs +2 -1
  76. package/server/services/oar-cli-helpers.mjs +6 -1
  77. package/server/services/oar-state-layout.mjs +107 -0
  78. package/server/services/oar-state-migration-service.mjs +387 -96
  79. package/server/services/package-data-rescue-preinstall.mjs +22 -0
  80. package/server/services/package-data-rescue.mjs +137 -0
  81. package/server/services/preview-proxy-server.mjs +14 -1
  82. package/server/services/preview-registry-service.mjs +3 -2
  83. package/server/services/process-list-service.mjs +2 -1
  84. package/server/services/provider-model-selection.mjs +195 -0
  85. package/server/services/push-dispatch-service.mjs +42 -8
  86. package/server/services/relay-event-service.mjs +210 -14
  87. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  88. package/server/services/relay-restart-picture.mjs +209 -0
  89. package/server/services/relay-restart-queue.mjs +387 -0
  90. package/server/services/relay-shutdown-gate.mjs +4 -1
  91. package/server/services/relay-singleton-guard.mjs +300 -36
  92. package/server/services/remote-relay-caller-context.mjs +7 -2
  93. package/server/services/remote-relay-dispatcher.mjs +217 -28
  94. package/server/services/restart-resume-service.mjs +303 -0
  95. package/server/services/session-activity-tracker.mjs +52 -15
  96. package/server/services/session-worker-launch-service.mjs +31 -1
  97. package/server/services/session-worker-process-service.mjs +6 -4
  98. package/server/services/stale-turn-recovery.mjs +99 -0
  99. package/server/services/subagent-timeline-service.mjs +4 -1
  100. package/server/services/task-output-service.mjs +217 -18
  101. package/server/services/windows-autostart-service.mjs +329 -31
  102. package/server/services/windows-boot-autostart-service.mjs +188 -60
  103. package/shared/preview-instructions.mjs +17 -12
  104. package/shared/preview-tool-core.mjs +32 -14
  105. package/shared/question-answer-rule.mjs +63 -0
  106. package/shared/relay-events.mjs +106 -3
  107. package/shared/remote-relay-contract.mjs +32 -15
  108. package/shared/subagent-timeline.mjs +4 -0
  109. package/shared/task-output-text.mjs +196 -83
  110. package/shared/windows-system-tools.mjs +25 -0
  111. package/shared/worker-runtime/config-loader.mjs +54 -8
  112. package/shared/worker-runtime/prompt-context.mjs +3 -1
  113. package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
  114. package/shared/worker-runtime/working-directory.mjs +65 -0
  115. package/server/uploads/image_input.md +0 -540
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.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.12" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
48
+ <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.14" 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.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.12" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
52
+ <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.14" 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.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.12" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
58
+ <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.14" 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.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.12" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
64
+ <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.14" 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.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.12" 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>
70
+ <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.14" 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>
@@ -86,7 +86,7 @@ flowchart LR
86
86
 
87
87
  You need Node.js 22.13 or newer (the macOS and Linux installer brings its own when yours is missing or older) and an account with the agent you want to use; see [Prerequisites](docs/install.md#prerequisites). There are also installs [with npm](docs/install.md#npm) and [from a git checkout](docs/install.md#from-a-git-checkout).
88
88
 
89
- `oar` alone opens a menu. `oar start`, `stop`, `restart` and `status` control the relay, `oar service install` starts it at every login, `oar doctor` shows what it uses, and `oar update` installs a new release (as does **⚙️ Settings → General → Check for updates**). All the details are in [Installing and running OAR](docs/install.md): [the `oar` command](docs/install.md#the-oar-command), [where OAR keeps its files](docs/install.md#where-oar-keeps-its-files), and [updating](docs/install.md#updating).
89
+ `oar` alone opens a menu. `oar start`, `stop`, `restart` and `status` control the relay, `oar service install` starts it at boot or sign-in (a systemd user service on Linux, the sign-in autostart on Windows), `oar doctor` shows what it uses, and `oar update` installs a new release (as does **⚙️ Settings → General → Check for updates**). All the details are in [Installing and running OAR](docs/install.md): [the `oar` command](docs/install.md#the-oar-command), [where OAR keeps its files](docs/install.md#where-oar-keeps-its-files), and [updating](docs/install.md#updating).
90
90
 
91
91
  <a id="providers"></a>
92
92
  <a id="claude-cloud"></a>
@@ -116,11 +116,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
116
116
  <table>
117
117
  <tr>
118
118
  <td width="62%" valign="top">
119
- <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.12" 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>
119
+ <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.14" 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>
120
120
  <br><sub>Send a message while the agent works, and it steers the running turn.</sub>
121
121
  </td>
122
122
  <td width="38%" valign="top">
123
- <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.12" 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>
123
+ <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.14" 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>
124
124
  <br><sub>What each plan has left, in one place.</sub>
125
125
  </td>
126
126
  </tr>
@@ -129,7 +129,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
129
129
  - **Watch a turn live**: the reply, the agent's thoughts and every tool call appear as they happen, and each sub-agent gets a bubble of its own. **Stop** is always one tap away ([While a turn runs](docs/web-ui.md#while-a-turn-runs)).
130
130
  - **Steer or queue**: a message sent during a turn goes into it (Claude, Copilot) or waits behind it ([While a turn runs](docs/web-ui.md#while-a-turn-runs)).
131
131
  - **Answer questions**: the agent asks on a card with one-click choices, checkmarks or free text, and waits for you ([Question cards](docs/web-ui.md#question-cards)).
132
- - **Background work**: a task panel lists the commands and sub-agents that outlive a reply, each with its own **Stop** and its full record; a command's console shows its output live, says when it has gone quiet and counts a test run's passes and failures ([Background tasks](docs/web-ui.md#background-tasks)).
132
+ - **Background work**: a task panel lists the commands and sub-agents that outlive a reply, each with its own **Stop** and its full record; a command's console shows its output live, says when it has gone quiet and counts a test run's passes and failures; the panel and its consoles come back after a relay restart, and a queued restart shows what it waits for and asks after a maximum wait ([Background tasks](docs/web-ui.md#background-tasks), [After a relay restart](docs/web-ui.md#after-a-relay-restart)).
133
133
  - **Choose how the agent works**: `ask`, `plan`, `agent` or `autopilot`, plus model and reasoning effort, per message ([Relay modes](docs/web-ui.md#relay-modes)).
134
134
  - **Files and git**: browse the workspace, preview files, review git changes in a diff viewer, and publish a dev server as a preview link ([Files, git, and previews](docs/web-ui.md#files-git-and-previews), [preview servers](docs/preview-servers.md)).
135
135
  - **Usage and context**: plan limits and credits of every provider, and what fills the model's context window ([Usage and context](docs/web-ui.md#usage-and-context)).
@@ -137,7 +137,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
137
137
  - **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)).
138
138
  - **On your phone**: install OAR as an app and get push notifications ([Notifications and the app](docs/web-ui.md#notifications-and-the-app)).
139
139
 
140
- 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.12](CHANGELOG.md#0912--2026-10-18) as the latest.
140
+ 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.14](CHANGELOG.md#0914--2026-10-21) as the latest.
141
141
 
142
142
  ## Remote access
143
143
 
@@ -171,7 +171,7 @@ The relay keeps both tunnels running and reconnects them by itself. Beyond your
171
171
 
172
172
  - **Paired relays**: connect the OAR on your workstation with the OAR on a server. An agent on one can then read, prompt and start sessions on the other, once you mention that relay with `@`.
173
173
  - **Agent sessions**: an agent hands parts of its task to new sessions on its own relay, waits for them and reads their results. It is off until you switch it on, the first session asks for your approval, and at most 4 run at once.
174
- - **Watching sessions**: an agent subscribes to other sessions and is woken when a question waits, an error happens, or a reply arrives, instead of polling.
174
+ - **Watching sessions**: an agent subscribes to other sessions and is woken when a question waits, an error happens, or a reply arrives, instead of polling; if it asks, also when the relay runs again after a restart.
175
175
 
176
176
  ```mermaid
177
177
  flowchart LR
@@ -188,7 +188,7 @@ flowchart LR
188
188
 
189
189
  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.
190
190
 
191
- <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.12" 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>
191
+ <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.14"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.14"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.14" 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>
192
192
 
193
193
  How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
194
194
 
package/bin/oar.js CHANGED
@@ -11,8 +11,17 @@ import net from 'net';
11
11
 
12
12
  import {
13
13
  migrateStateToOarRoot,
14
- resolveOarRoot,
15
14
  } from '../server/services/oar-state-migration-service.mjs';
15
+ import { resolveDataDir, resolveUploadDir } from '../server/services/data-dir-resolution.mjs';
16
+ import { TAKEOVER_PROGRESS_FILE } from '../server/services/earlier-database-service.mjs';
17
+ import { defaultIsProcessAlive, lockHolderMayRun, processIdentity } from '../server/services/relay-singleton-guard.mjs';
18
+ import {
19
+ getLauncherLogDir,
20
+ getLegacyManagedConfigDir,
21
+ isGitCheckout,
22
+ resolveRelayStateDirs,
23
+ resolveStateLayout,
24
+ } from '../server/services/oar-state-layout.mjs';
16
25
  import {
17
26
  buildDefaultConfig, buildServicePath, buildSystemdUnit, describeRelayAddress, DOCTOR_PROBES, generateAuthToken,
18
27
  NPM_ALLOW_SCRIPTS_ARG, parseCliArgs, primaryLanAddress, renderDoctorReport, resolveNpmInvocation, usageText,
@@ -56,20 +65,42 @@ function readRelayLock(lockPath) {
56
65
  pid: Number.parseInt(String(parsed?.pid ?? ''), 10),
57
66
  startedAt: typeof parsed?.startedAt === 'string' ? parsed.startedAt : null,
58
67
  token: typeof parsed?.token === 'string' ? parsed.token : '',
68
+ format: Number(parsed?.format) || 0,
59
69
  };
60
70
  } catch {
61
71
  return null;
62
72
  }
63
73
  }
64
74
 
75
+ // What a relay that holds its lock and does not answer yet is bringing over
76
+ // (earlier-database-service), or null: the progress file names the lock's
77
+ // pid, and that pid is the lock's holder still (lockHolderMayRun: a pid
78
+ // reused after a crash is not).
79
+ function readTakeover(lockPath, isProcessAliveImpl = isProcessAlive) {
80
+ const progress = readJsonFile(path.join(path.dirname(lockPath), TAKEOVER_PROGRESS_FILE));
81
+ const lock = readJsonFile(lockPath);
82
+ const pid = Number.parseInt(String(progress?.pid ?? ''), 10);
83
+ return Number.isInteger(pid) && Number(lock?.pid) === pid && lockHolderMayRun({ lock, isProcessAlive: isProcessAliveImpl }) ? progress : null;
84
+ }
85
+
86
+ // The lock a relay that did not come up found held, and whether its holder
87
+ // is known to be a relay (its recorded identity matches): only when it is
88
+ // not may the user delete the lock. Null when no live process holds it.
89
+ function heldLock(lockPath, isProcessAliveImpl = isProcessAlive) {
90
+ const lock = readJsonFile(lockPath);
91
+ const pid = Number.parseInt(String(lock?.pid ?? ''), 10);
92
+ if (!Number.isInteger(pid) || !lockHolderMayRun({ lock, isProcessAlive: isProcessAliveImpl })) return null;
93
+ return { pid, path: lockPath, startedAt: lock.startedAt || 'unknown', known: !!lock.holder && !!processIdentity(pid) };
94
+ }
95
+
96
+ function takeoverLine(takeover, logDir) {
97
+ const what = takeover ? ` (${takeover.from}, ${Math.max(1, Math.round(Number(takeover.bytes || 0) / 1048576))} MB)` : '';
98
+ return `[oar] The relay${takeover ? ` (process ${takeover.pid})` : ''} is bringing over the database it ran on until now${what} and is still running; it answers once that is done. Its log: ${path.join(String(logDir || ''), 'server.log')}`;
99
+ }
100
+
101
+ // As the server tells it: a process this user may not signal (EPERM) lives.
65
102
  function isProcessAlive(pid) {
66
- if (!Number.isInteger(pid) || pid <= 0) return false;
67
- try {
68
- process.kill(pid, 0);
69
- return true;
70
- } catch {
71
- return false;
72
- }
103
+ return defaultIsProcessAlive(pid);
73
104
  }
74
105
 
75
106
  function readJsonFile(filePath) {
@@ -91,45 +122,66 @@ function openAppendFileDescriptor(filePath) {
91
122
  return fs.openSync(filePath, 'a');
92
123
  }
93
124
 
94
- // Pre-OAR managed config location — kept only as a migration source and as the
95
- // config home for git checkouts, where nothing may change underfoot.
96
- function getLegacyManagedConfigDir(env = process.env) {
97
- if (process.platform === 'win32') {
98
- return path.join(
99
- env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
100
- 'copilot-remote',
101
- );
102
- }
103
- return path.join(
104
- env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
105
- 'copilot-remote',
106
- );
107
- }
108
-
109
- function isGitCheckout(packageRoot) {
110
- try { return fs.existsSync(path.join(packageRoot, '.git')); } catch { return false; }
111
- }
112
-
113
125
  /**
114
- * Where launcher-managed state lives. A git checkout keeps every pre-OAR
115
- * default (repo-local server/data, legacy managed config) so development and
116
- * the two existing machines change nothing. A global install gets the OAR
117
- * state root: config, data, logs all under ~/.oar (APPDATA\oar on Windows),
118
- * which survives `npm i -g` updates.
126
+ * Brings pre-OAR state into a global install's state root. Without
127
+ * --migrate-from it runs quietly, once; with it, it runs unless that source
128
+ * was migrated already, never overwrites, and says what it did or why it did
129
+ * nothing. Database and uploads go where the relay `oar` starts will read
130
+ * them, resolved as the relay resolves them. Returns null to go on, or the
131
+ * result to stop with. A refusal stops only `oar setup` (`strict`): `oar
132
+ * copilot` warns and starts the session on the state root as it is.
119
133
  */
120
- function resolveStateLayout({ packageRoot, env = process.env } = {}) {
121
- if (isGitCheckout(packageRoot)) {
122
- return { checkout: true, root: null, configDir: getLegacyManagedConfigDir(env), dataDir: null };
134
+ async function migrateIntoStateRoot({
135
+ layout, env, packageRoot, migrateFrom = '', logger, isProcessAliveImpl = isProcessAlive, strict = true,
136
+ }) {
137
+ const serverDir = path.join(packageRoot, 'server');
138
+ const relayEnv = { COPILOT_WEB_RELAY_DATA_DIR: resolveRelayStateDirs({ env, layout }).dataDir };
139
+ const dataDir = resolveDataDir({ env: relayEnv, serverDir });
140
+ const source = migrateFrom ? path.resolve(migrateFrom) : '';
141
+ let migration;
142
+ try {
143
+ migration = await migrateStateToOarRoot({
144
+ targetRoot: layout.root,
145
+ targetDataDir: dataDir,
146
+ targetUploadDir: resolveUploadDir({ dataDir, env: relayEnv, serverDir }),
147
+ repoServerDir: source ? path.join(source, 'server') : null,
148
+ managedConfigDir: getLegacyManagedConfigDir(env),
149
+ isProcessAliveImpl,
150
+ });
151
+ } catch (error) {
152
+ logger.error?.(`[oar] State migration failed, nothing was changed at the source: ${error?.message || error}`);
153
+ return { code: 1, reason: 'migration-failed', error };
154
+ }
155
+ const refused = (message, reason) => {
156
+ logger.error?.(`[oar] ${message}`);
157
+ if (strict) return { code: 1, reason };
158
+ logger.error?.('[oar] Nothing was migrated; starting on the state root as it is.');
159
+ return null;
160
+ };
161
+ switch (migration.status) {
162
+ case 'blocked-live-relay': return refused(migration.error, 'migration-blocked');
163
+ case 'refused-relay-running': return refused(migration.error, 'migration-relay-running');
164
+ case 'refused-existing-database': return refused(migration.error, 'migration-refused');
165
+ case 'nothing-to-migrate':
166
+ return source ? refused(`Nothing to migrate from ${source}: ${migration.reason}.`, 'migration-nothing') : null;
167
+ case 'already-migrated':
168
+ if (source) logger.log?.(`[oar] ${layout.root} is already migrated from ${source}; nothing to do.`);
169
+ return null;
170
+ case 'migrated':
171
+ if (migration.movedAsideTo) {
172
+ logger.log?.(`[oar] The state root's database had no conversations yet (a fresh one); moved it aside to ${migration.movedAsideTo}.`);
173
+ }
174
+ logger.log?.(`[oar] Migrated ${source || 'earlier relay state'} into ${layout.root} (${migration.copied} items, sources untouched; receipt in backups/MIGRATION.md).`);
175
+ for (const kept of migration.kept || []) {
176
+ logger.log?.(`[oar] Kept the existing ${kept}; the one from the source was not copied.`);
177
+ }
178
+ for (const link of migration.skipped || []) {
179
+ logger.log?.(`[oar] Skipped the link ${link}: what it points to was not copied.`);
180
+ }
181
+ return null;
182
+ default:
183
+ return null;
123
184
  }
124
- const root = resolveOarRoot(env);
125
- return { checkout: false, root, configDir: root, dataDir: path.join(root, 'data') };
126
- }
127
-
128
- function getLauncherLogDir(env = process.env, layout = null) {
129
- const envLogDir = String(env.COPILOT_WEB_RELAY_LOG_DIR || '').trim();
130
- if (envLogDir) return envLogDir;
131
- if (layout && !layout.checkout) return path.join(layout.root, 'logs');
132
- return path.join(getLegacyManagedConfigDir(env), 'logs');
133
185
  }
134
186
 
135
187
  function getCopilotHomeDir(env = process.env) {
@@ -278,6 +330,7 @@ export async function launchRelay({
278
330
  fetchImpl = globalThis.fetch,
279
331
  isProcessAliveImpl = isProcessAlive,
280
332
  lockGraceMs = 5000,
333
+ readyWaitMs = 20_000,
281
334
  env = process.env,
282
335
  logger = console,
283
336
  } = {}) {
@@ -328,22 +381,11 @@ export async function launchRelay({
328
381
 
329
382
  if (!layout.checkout) {
330
383
  // Global install: state lives in the OAR root. Migrate pre-OAR state in
331
- // exactly once (marker-guarded); a live source relay blocks the migration.
332
- try {
333
- const migration = await migrateStateToOarRoot({
334
- targetRoot: layout.root,
335
- repoServerDir: migrateFromRoot ? path.join(path.resolve(migrateFromRoot), 'server') : null,
336
- managedConfigDir: getLegacyManagedConfigDir(env),
337
- logger,
338
- });
339
- if (migration.status === 'blocked-live-relay') {
340
- logger.error?.(`[oar] ${migration.error}`);
341
- return { code: 1, reason: 'migration-blocked' };
342
- }
343
- } catch (error) {
344
- logger.error?.(`[oar] State migration failed, nothing was changed at the source: ${error?.message || error}`);
345
- return { code: 1, reason: 'migration-failed', error };
346
- }
384
+ // (once, or as --migrate-from asks); a live source relay blocks it.
385
+ const stop = await migrateIntoStateRoot({
386
+ layout, env, packageRoot, migrateFrom: migrateFromRoot, logger, isProcessAliveImpl, strict: false,
387
+ });
388
+ if (stop) return stop;
347
389
  }
348
390
 
349
391
  const lockPath = layout.checkout
@@ -372,14 +414,21 @@ export async function launchRelay({
372
414
  ? await probeRelayPort(probe)
373
415
  : await probeRelayBehindLock({ ...probe, lockPath, port, isProcessAliveImpl, lockGraceMs, logger });
374
416
  if (answer === 'taken') {
375
- logger.error?.(`[oar] Port ${port} is in use by another program (or by a relay with another token).`);
417
+ logger.error?.(`[oar] Port ${port} is in use by another program, or by a relay with another token.`);
376
418
  logger.error?.('[oar] Stop that program, or pick another port: oar copilot --port <port>');
377
419
  return { code: 1, reason: 'port-taken' };
378
420
  }
421
+ if (answer === 'bringing-over') {
422
+ logger.log(takeoverLine(readTakeover(lockPath, isProcessAliveImpl), getLauncherLogDir(env, layout)));
423
+ return { code: 1, reason: 'bringing-over' };
424
+ }
379
425
  if (runtimeConfig.managed || !fs.existsSync(runtimeConfig.configPath)) {
380
426
  writeJsonFile(runtimeConfig.configPath, runtimeConfig.config);
381
427
  }
382
428
 
429
+ // Checkouts keep the repo-local server/data default; global installs point
430
+ // the server at the OAR root so updates cannot wipe the database.
431
+ const stateDirs = resolveRelayStateDirs({ env, layout });
383
432
  const baseEnv = {
384
433
  ...env,
385
434
  COPILOT_WORKSPACE_ROOT: cwd,
@@ -387,11 +436,7 @@ export async function launchRelay({
387
436
  COPILOT_WEB_RELAY_SERVER_DIR: serverDir,
388
437
  COPILOT_WEB_RELAY_CONFIG: runtimeConfig.configPath,
389
438
  COPILOT_WEB_RELAY_LOG_DIR: getLauncherLogDir(env, layout),
390
- // Checkouts keep the repo-local server/data default; global installs point
391
- // the server at the OAR root so updates cannot wipe the database.
392
- ...(layout.checkout ? {} : {
393
- COPILOT_WEB_RELAY_DATA_DIR: String(env.COPILOT_WEB_RELAY_DATA_DIR || '').trim() || layout.dataDir,
394
- }),
439
+ ...(stateDirs.dataDir ? { COPILOT_WEB_RELAY_DATA_DIR: stateDirs.dataDir } : {}),
395
440
  GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS: String(env.GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS || 'true'),
396
441
  };
397
442
 
@@ -429,7 +474,7 @@ export async function launchRelay({
429
474
  return { code: 1, reason: 'spawn-invalid-child' };
430
475
  }
431
476
 
432
- const readyDeadlineMs = 20_000;
477
+ const readyDeadlineMs = readyWaitMs;
433
478
  const ready = await new Promise((resolve) => {
434
479
  const deadline = Date.now() + readyDeadlineMs;
435
480
  const probe = async () => {
@@ -457,6 +502,14 @@ export async function launchRelay({
457
502
  probe();
458
503
  });
459
504
 
505
+ const takeover = ready ? null : readTakeover(lockPath, isProcessAliveImpl);
506
+ if (takeover) {
507
+ // Left running: a kill would cut the copy short for nothing.
508
+ logger.log(takeoverLine(takeover, logDir));
509
+ try { fs.closeSync(serverOutFd); } catch {}
510
+ try { fs.closeSync(serverErrFd); } catch {}
511
+ return { code: 1, reason: 'bringing-over' };
512
+ }
460
513
  if (!ready) {
461
514
  logger.error?.(`[oar] Relay did not become ready at ${statusUrl}`);
462
515
  try {
@@ -667,7 +720,11 @@ async function probeRelayBehindLock({
667
720
  await new Promise((resolve) => setTimeout(resolve, 300));
668
721
  answer = await probeRelayPort({ statusUrl, token, fetchImpl });
669
722
  }
670
- if (answer === 'none' && lockedByLiveProcess()) {
723
+ if (answer === 'none' && readTakeover(lockPath, isProcessAliveImpl)) return 'bringing-over';
724
+ // A lock this version wrote is left to the server, which tells a reused
725
+ // process id itself (lockHolderMayRun): removed here, a second relay
726
+ // started beside one still bringing over its database.
727
+ if (answer === 'none' && lockedByLiveProcess() && !readRelayLock(lockPath)?.format) {
671
728
  // Nothing listens: the lock is left over from a relay that did not
672
729
  // stop cleanly. The server refuses to start beside a lock whose process
673
730
  // id is alive, so it goes first.
@@ -697,6 +754,7 @@ export async function ensureRelayRunning({
697
754
  fetchImpl = globalThis.fetch,
698
755
  isProcessAliveImpl = isProcessAlive,
699
756
  lockGraceMs = 5000,
757
+ readyWaitMs = 20_000,
700
758
  homeDir = os.homedir(),
701
759
  platform = process.platform,
702
760
  logger = console,
@@ -710,8 +768,9 @@ export async function ensureRelayRunning({
710
768
  statusUrl, token, lockPath, port, fetchImpl, isProcessAliveImpl, lockGraceMs, logger,
711
769
  });
712
770
  if (answer === 'oar') return { ok: true, how: 'already', logDir };
771
+ if (answer === 'bringing-over') return { ok: false, how: 'bringing-over', logDir, takeover: readTakeover(lockPath, isProcessAliveImpl) };
713
772
  if (answer === 'taken') {
714
- logger.error?.(`[oar] Port ${port} is in use by another program (or by a relay with another token).`);
773
+ logger.error?.(`[oar] Port ${port} is in use by another program, or by a relay with another token.`);
715
774
  logger.error?.('[oar] Stop that program, or pick another port: oar setup --port <port>');
716
775
  return { ok: false, how: 'port-taken', logDir };
717
776
  }
@@ -739,7 +798,7 @@ export async function ensureRelayRunning({
739
798
  COPILOT_WEB_RELAY_SERVER_DIR: serverDir,
740
799
  COPILOT_WEB_RELAY_CONFIG: configPath,
741
800
  COPILOT_WEB_RELAY_LOG_DIR: logDir,
742
- COPILOT_WEB_RELAY_DATA_DIR: String(env.COPILOT_WEB_RELAY_DATA_DIR || '').trim() || layout.dataDir,
801
+ COPILOT_WEB_RELAY_DATA_DIR: resolveRelayStateDirs({ env, layout }).dataDir,
743
802
  },
744
803
  detached: true,
745
804
  stdio: ['ignore', outFd, errFd],
@@ -753,8 +812,10 @@ export async function ensureRelayRunning({
753
812
  try { fs.closeSync(outFd); } catch {}
754
813
  try { fs.closeSync(errFd); } catch {}
755
814
  }
756
- const ok = await waitForRelay({ statusUrl, token, fetchImpl, stillStarting: () => child.exitCode === null });
757
- return { ok, how: 'detached', logDir };
815
+ const ok = await waitForRelay({ statusUrl, token, fetchImpl, deadlineMs: readyWaitMs, stillStarting: () => child.exitCode === null });
816
+ const takeover = ok ? null : readTakeover(lockPath, isProcessAliveImpl);
817
+ if (takeover) return { ok: false, how: 'bringing-over', logDir, takeover };
818
+ return { ok, how: 'detached', logDir, ...(ok ? {} : { lock: heldLock(lockPath, isProcessAliveImpl) }) };
758
819
  }
759
820
 
760
821
  /**
@@ -916,7 +977,7 @@ async function restartRelay(ctx, paths, { old, viaService }) {
916
977
  logger.error('[oar] The relay came back on its earlier port and token: it was started with fixed ones (oar copilot). End that session, then run: oar start');
917
978
  return { ok: false, reason: 'fixed-settings' };
918
979
  }
919
- logger.error(`[oar] The relay did not come back — see ${path.join(paths.logDir, 'server-err.log')}`);
980
+ logger.error(`[oar] The relay did not come back — see ${viaService ? 'journalctl --user -u oar' : path.join(paths.logDir, 'server-err.log')}`);
920
981
  return { ok: false, reason: 'server-not-ready' };
921
982
  }
922
983
 
@@ -987,6 +1048,7 @@ function startRelay(ctx, paths) {
987
1048
  fetchImpl: ctx.fetchImpl,
988
1049
  isProcessAliveImpl: ctx.isProcessAliveImpl,
989
1050
  lockGraceMs: ctx.lockGraceMs,
1051
+ readyWaitMs: ctx.readyWaitMs,
990
1052
  homeDir: ctx.homeDir,
991
1053
  platform: ctx.platform,
992
1054
  logger: ctx.logger,
@@ -1028,12 +1090,18 @@ function warnWindowsPort(ctx, port, windowsHolder = createWindowsPortCheck(ctx))
1028
1090
  function reportRelayStart(result, logger) {
1029
1091
  // A port held by something else was already explained.
1030
1092
  if (result.how === 'port-taken') return;
1093
+ if (result.how === 'bringing-over') {
1094
+ logger.log(takeoverLine(result.takeover, result.logDir));
1095
+ return;
1096
+ }
1031
1097
  if (!result.ok) {
1032
- logger.error?.(`[oar] The relay did not come up — see ${path.join(result.logDir, 'server-err.log')}`);
1098
+ logger.error?.(`[oar] The relay did not come up — see ${result.how === 'service' ? 'journalctl --user -u oar' : path.join(result.logDir, 'server-err.log')}`);
1099
+ if (result.lock?.known) logger.error?.(`[oar] The lock ${result.lock.path} is held by relay process ${result.lock.pid} (started ${result.lock.startedAt}): oar stop, or end process ${result.lock.pid}.`);
1100
+ else if (result.lock) logger.error?.(`[oar] The lock ${result.lock.path} names process ${result.lock.pid}, which cannot be told to be a relay: if no relay is running, end process ${result.lock.pid} or delete the lock.`);
1033
1101
  return;
1034
1102
  }
1035
1103
  if (result.how === 'already') {
1036
- logger.log('[oar] The relay is already running. To load a new version, restart it: oar restart');
1104
+ logger.log('[oar] The relay is already running. It keeps the version and settings it started with until you restart it: oar restart');
1037
1105
  } else if (result.how === 'service') {
1038
1106
  logger.log('[oar] The relay is running as a systemd user service (systemctl --user status oar).');
1039
1107
  } else {
@@ -1048,7 +1116,7 @@ function requireConfig(ctx, paths) {
1048
1116
  }
1049
1117
 
1050
1118
  function reportPortTaken(ctx, paths, port) {
1051
- ctx.logger.error(`[oar] Port ${port} is held by another program, or by a relay with another token.`);
1119
+ ctx.logger.error(`[oar] Port ${port} is in use by another program, or by a relay with another token.`);
1052
1120
  // A relay that was started before its token was changed in the config
1053
1121
  // cannot be asked to restart: only its lock tells that it is there.
1054
1122
  const pid = readRelayLock(paths.lockPath)?.pid;
@@ -1077,6 +1145,7 @@ export async function runStart(options = {}) {
1077
1145
  reportRelayStart(result, logger);
1078
1146
  if (result.ok) await showAddress(ctx, paths);
1079
1147
  warnWindowsPort(ctx, endpointOf(paths.config).port);
1148
+ if (result.how === 'bringing-over') return { code: 0, reason: 'bringing-over' };
1080
1149
  return { code: result.ok ? 0 : 1, reason: result.ok ? 'started' : 'server-not-ready' };
1081
1150
  }
1082
1151
 
@@ -1092,7 +1161,7 @@ export async function runStop(options = {}) {
1092
1161
  logger.error('[oar] systemctl --user stop oar failed — see: systemctl --user status oar');
1093
1162
  return { code: 1, reason: 'service' };
1094
1163
  }
1095
- logger.log('[oar] The relay service is stopped. It starts again at the next login; to take it out: oar service remove');
1164
+ logger.log('[oar] The relay service is stopped. It starts again at the next boot (without lingering: at the next login); to take it out: oar service remove');
1096
1165
  return { code: 0, reason: 'stopped' };
1097
1166
  }
1098
1167
  const relay = await findRelay(ctx, paths);
@@ -1172,7 +1241,7 @@ export async function runStatus(options = {}) {
1172
1241
  const access = status.access === 'lan' ? 'LAN access' : 'this machine only';
1173
1242
  if (status.relay === 'running') logger.log(`[oar] The relay is running on port ${status.port} (${access}): ${status.url}`);
1174
1243
  else if (status.relay === 'stopped') logger.log(`[oar] The relay is stopped (port ${status.port}, ${access}). Start it with: oar start`);
1175
- else logger.log(`[oar] Port ${status.port} is held by another program, or by a relay with another token.`);
1244
+ else logger.log(`[oar] Port ${status.port} is in use by another program, or by a relay with another token.`);
1176
1245
  if (status.service !== 'unavailable') {
1177
1246
  logger.log(`[oar] Service: ${{ active: 'installed, running', installed: 'installed', none: 'not installed' }[status.service]}`);
1178
1247
  }
@@ -1205,7 +1274,7 @@ async function runWindowsService(ctx, paths, action) {
1205
1274
  return { code: 0, reason: 'service-status' };
1206
1275
  }
1207
1276
  if (boot) {
1208
- logger.error('[oar] The relay starts at boot through a scheduled task. Change that in the web UI: Settings → Autostart.');
1277
+ logger.error('[oar] The relay starts at boot through a scheduled task. Change that in the web UI: Settings → General → Autostart (Windows).');
1209
1278
  return { code: 1, reason: 'boot-task' };
1210
1279
  }
1211
1280
  if (action === 'install' && requireConfig(ctx, paths)) return { code: 1, reason: 'no-config' };
@@ -1261,7 +1330,7 @@ export async function runService(options = {}) {
1261
1330
  // A relay started by hand holds the port and the lock: it makes way first.
1262
1331
  if ((await runStop({ ...options, argv: [] })).reason !== 'stopped') {
1263
1332
  runQuiet('systemctl', ['--user', 'enable', 'oar'], ctx);
1264
- logger.log('[oar] The service is installed and takes over at the next login, or once the relay has stopped: oar start');
1333
+ logger.log('[oar] The service is installed and takes over at the next boot (without lingering: at the next login), or once the relay has stopped: oar start');
1265
1334
  return { code: 0, reason: 'service-install' };
1266
1335
  }
1267
1336
  }
@@ -1310,21 +1379,13 @@ export async function runSetup(options = {}) {
1310
1379
  let paths = installPaths(ctx);
1311
1380
  const { layout } = paths;
1312
1381
  if (!layout.checkout) {
1313
- const migrateFromRoot = String(optionValue(argv, '--migrate-from') || '').trim();
1314
- const migration = await migrateStateToOarRoot({
1315
- targetRoot: layout.root,
1316
- repoServerDir: migrateFromRoot ? path.join(path.resolve(migrateFromRoot), 'server') : null,
1317
- managedConfigDir: getLegacyManagedConfigDir(env),
1318
- logger,
1382
+ const migrateFrom = String(optionValue(argv, '--migrate-from') || '').trim();
1383
+ const stop = await migrateIntoStateRoot({
1384
+ layout, env, packageRoot: ctx.packageRoot, migrateFrom, logger, isProcessAliveImpl: ctx.isProcessAliveImpl,
1319
1385
  });
1320
- if (migration.status === 'blocked-live-relay') {
1321
- logger.error?.(`[oar] ${migration.error}`);
1322
- return { code: 1, reason: 'migration-blocked' };
1323
- }
1324
- if (migration.status === 'migrated') {
1325
- logger.log(`[oar] Migrated existing relay state into ${layout.root} (sources untouched).`);
1326
- paths = installPaths(ctx);
1327
- }
1386
+ if (stop) return stop;
1387
+ // A migration may have brought a config over.
1388
+ paths = installPaths(ctx);
1328
1389
  }
1329
1390
 
1330
1391
  const existing = paths.config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oar-sh/oar",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "type": "module",
5
5
  "description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
6
6
  "license": "MIT",
@@ -27,7 +27,6 @@
27
27
  "!server/config.json",
28
28
  "!server/uploads/",
29
29
  "server/uploads/README.md",
30
- "server/uploads/image_input.md",
31
30
  "shared/",
32
31
  "!shared/**/*.test.mjs",
33
32
  "!shared/**/*-test-harness.mjs",
@@ -39,6 +38,7 @@
39
38
  "LICENSE"
40
39
  ],
41
40
  "scripts": {
41
+ "preinstall": "node server/services/package-data-rescue-preinstall.mjs",
42
42
  "start": "node server/server.js",
43
43
  "copilot:relay": "gh copilot -- --allow-all",
44
44
  "test": "node --import ./scripts/node-test-keep-alive.mjs --test \"server/**/*.test.mjs\" \"shared/**/*.test.mjs\" \".github/extensions/web-relay/**/*.test.mjs\" \"scripts/**/*.test.mjs\"",
@@ -50,9 +50,9 @@
50
50
  "test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
51
51
  },
52
52
  "dependencies": {
53
- "@anthropic-ai/claude-agent-sdk": "0.3.292",
54
- "@cursor/sdk": "1.0.36",
55
- "@github/copilot-sdk": "1.0.17",
53
+ "@anthropic-ai/claude-agent-sdk": "0.3.304",
54
+ "@cursor/sdk": "1.0.38",
55
+ "@github/copilot-sdk": "1.0.23",
56
56
  "better-sqlite3": "12.11.1",
57
57
  "express": "4.22.3",
58
58
  "qrcode-terminal": "0.12.0",