@oar-sh/oar 0.9.0

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 (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "OAR",
3
+ "short_name": "OAR",
4
+ "description": "OAR — Open Agent Relay: drive your local coding agents from any browser.",
5
+ "id": "./__copilot_remote_pwa__",
6
+ "start_url": "./",
7
+ "scope": "./",
8
+ "display_override": ["standalone"],
9
+ "display": "standalone",
10
+ "background_color": "#161b22",
11
+ "theme_color": "#161b22",
12
+ "icons": [
13
+ {
14
+ "src": "app-icon.svg?v=25",
15
+ "sizes": "any",
16
+ "type": "image/svg+xml",
17
+ "purpose": "any"
18
+ },
19
+ {
20
+ "src": "app-icon-192.png",
21
+ "sizes": "192x192",
22
+ "type": "image/png",
23
+ "purpose": "any"
24
+ },
25
+ {
26
+ "src": "app-icon-512.png",
27
+ "sizes": "512x512",
28
+ "type": "image/png",
29
+ "purpose": "any"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,307 @@
1
+ const SW_URL = new URL(self.location.href);
2
+ const SW_VERSION = String(SW_URL.searchParams.get('v') || '').trim() || '0';
3
+ const CACHE_NAME = `copilot-remote-shell-v${SW_VERSION}`;
4
+ const REGISTRATION_SCOPE_PATH = (() => {
5
+ const path = new URL(self.registration.scope).pathname || '/';
6
+ return path.endsWith('/') ? path : `${path}/`;
7
+ })();
8
+ const STATIC_ASSETS = [
9
+ './',
10
+ 'index.html',
11
+ 'manifest.webmanifest',
12
+ 'app-icon-192.png',
13
+ 'app-icon-512.png',
14
+ 'favicon.ico',
15
+ 'app-icon.svg',
16
+ ];
17
+
18
+ function sameOrigin(url) {
19
+ return url.origin === self.location.origin;
20
+ }
21
+
22
+ function isApiRequest(url) {
23
+ return /\/api(?:\/|$)/.test(url.pathname) || /\/socket\.io(?:\/|$)/.test(url.pathname);
24
+ }
25
+
26
+ function isPwaMetadataRequest(url) {
27
+ if (!url.pathname.startsWith(REGISTRATION_SCOPE_PATH)) return false;
28
+ const relativePath = url.pathname.slice(REGISTRATION_SCOPE_PATH.length);
29
+ return /^(?:manifest\.webmanifest|app-icon(?:-\d+)?\.png|app-icon\.svg|favicon\.ico)$/.test(relativePath);
30
+ }
31
+
32
+ function isApplicationModuleRequest(url) {
33
+ if (!url.pathname.startsWith(`${REGISTRATION_SCOPE_PATH}app/`)) return false;
34
+ return /\.(?:m?js)$/i.test(url.pathname);
35
+ }
36
+
37
+ async function cacheShell() {
38
+ const cache = await caches.open(CACHE_NAME);
39
+ const assets = STATIC_ASSETS.map((asset) => new URL(asset, self.registration.scope).href);
40
+ await cache.addAll(assets);
41
+ }
42
+
43
+ async function networkFirst(request, cacheKey) {
44
+ const cache = await caches.open(CACHE_NAME);
45
+ try {
46
+ const response = await fetch(request);
47
+ if (response && response.ok) {
48
+ cache.put(cacheKey, response.clone());
49
+ }
50
+ return response;
51
+ } catch {
52
+ const cached = await cache.match(cacheKey);
53
+ if (cached) return cached;
54
+ throw new Error('Offline');
55
+ }
56
+ }
57
+
58
+ async function cacheFirst(request, cacheKey) {
59
+ const cache = await caches.open(CACHE_NAME);
60
+ const cached = await cache.match(cacheKey);
61
+ if (cached) return cached;
62
+ const response = await fetch(request);
63
+ if (response && response.ok) {
64
+ cache.put(cacheKey, response.clone());
65
+ }
66
+ return response;
67
+ }
68
+
69
+ self.addEventListener('install', (event) => {
70
+ event.waitUntil((async () => {
71
+ await cacheShell();
72
+ self.skipWaiting();
73
+ })());
74
+ });
75
+
76
+ self.addEventListener('activate', (event) => {
77
+ event.waitUntil((async () => {
78
+ const keys = await caches.keys();
79
+ await Promise.all(keys.map((key) => (key === CACHE_NAME ? Promise.resolve() : caches.delete(key))));
80
+ await self.clients.claim();
81
+ })());
82
+ });
83
+
84
+ self.addEventListener('fetch', (event) => {
85
+ const { request } = event;
86
+ if (request.method !== 'GET') return;
87
+
88
+ const url = new URL(request.url);
89
+ if (!sameOrigin(url) || isApiRequest(url)) return;
90
+
91
+ if (request.mode === 'navigate') {
92
+ event.respondWith(networkFirst(request, new URL('./', self.registration.scope).href));
93
+ return;
94
+ }
95
+
96
+ if (isPwaMetadataRequest(url)) {
97
+ event.respondWith(networkFirst(request, request.url));
98
+ return;
99
+ }
100
+
101
+ if (isApplicationModuleRequest(url)) {
102
+ event.respondWith(networkFirst(request, request.url));
103
+ return;
104
+ }
105
+
106
+ event.respondWith(cacheFirst(request, request.url));
107
+ });
108
+
109
+ // ─── Web Push ────────────────────────────────────────────────────────────────
110
+
111
+ function scopeUrl(relative) {
112
+ return new URL(relative, self.registration.scope).href;
113
+ }
114
+
115
+ async function handlePushEvent(event) {
116
+ let payload = null;
117
+ try {
118
+ payload = event.data ? event.data.json() : null;
119
+ } catch {
120
+ payload = null;
121
+ }
122
+ const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
123
+ const choices = Array.isArray(data.choices) ? data.choices : [];
124
+ const maxActions = Number(Notification.maxActions || 0);
125
+ const actions = choices.slice(0, maxActions).map((choice, index) => ({
126
+ action: `choice-${index}`,
127
+ title: String(choice).slice(0, 40),
128
+ }));
129
+ await self.registration.showNotification(payload?.title || 'OAR', {
130
+ body: payload?.body || '',
131
+ tag: payload?.tag || undefined,
132
+ // Carried so a delivery delayed by doze is not misread as current.
133
+ timestamp: Number(payload?.timestamp) || Date.now(),
134
+ data,
135
+ icon: scopeUrl('app-icon-192.png'),
136
+ badge: scopeUrl('app-icon-192.png'),
137
+ actions,
138
+ });
139
+ }
140
+
141
+ self.addEventListener('push', (event) => {
142
+ event.waitUntil(handlePushEvent(event));
143
+ });
144
+
145
+ async function answerQuestionFromNotification(questionId, answer) {
146
+ // Same-origin POST with credentials so the HttpOnly auth cookie applies.
147
+ const response = await fetch(scopeUrl(`api/relay-question/${encodeURIComponent(questionId)}/answer`), {
148
+ method: 'POST',
149
+ credentials: 'include',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({ answer }),
152
+ });
153
+ return response.ok;
154
+ }
155
+
156
+ async function focusOrOpenConversation(conversationId) {
157
+ const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
158
+ const scopePath = new URL(self.registration.scope).pathname;
159
+ const appClient = clients.find((client) => new URL(client.url).pathname.startsWith(scopePath));
160
+ if (appClient) {
161
+ try {
162
+ await appClient.focus();
163
+ } catch {}
164
+ if (conversationId) {
165
+ appClient.postMessage({ type: 'copilot-open-conversation', conversationId });
166
+ }
167
+ return;
168
+ }
169
+ const target = conversationId
170
+ ? scopeUrl(`?push_conv=${encodeURIComponent(conversationId)}`)
171
+ : scopeUrl('./');
172
+ await self.clients.openWindow(target);
173
+ }
174
+
175
+ async function handleNotificationClick(event) {
176
+ const data = event.notification?.data || {};
177
+ const action = String(event.action || '');
178
+ const choiceMatch = action.match(/^choice-(\d+)$/);
179
+ if (choiceMatch && data.questionId) {
180
+ const choice = (Array.isArray(data.choices) ? data.choices : [])[Number(choiceMatch[1])];
181
+ if (choice) {
182
+ event.notification.close();
183
+ const ok = await answerQuestionFromNotification(data.questionId, String(choice));
184
+ // If the answer did not land (expired, already answered, offline), fall
185
+ // through to opening the conversation so the user can see why.
186
+ if (ok) return;
187
+ }
188
+ }
189
+ event.notification.close();
190
+ await focusOrOpenConversation(String(data.conversationId || ''));
191
+ }
192
+
193
+ self.addEventListener('notificationclick', (event) => {
194
+ event.waitUntil(handleNotificationClick(event));
195
+ });
196
+
197
+ // ─── Background Sync outbox ──────────────────────────────────────────────────
198
+ // Mirror of the IndexedDB layout in app/sync-outbox.mjs (this worker is a
199
+ // classic script and cannot import it). The page enqueues failed sends,
200
+ // ask_user answers, and pagehide draft flushes; this handler replays them in
201
+ // order when connectivity returns.
202
+
203
+ const SYNC_DB_NAME = 'copilot-remote-sync';
204
+ const SYNC_DB_VERSION = 1;
205
+ const SYNC_STORE_NAME = 'outbox';
206
+ const SYNC_TAG = 'copilot-outbox';
207
+ const SYNC_MAX_REPLAY_ATTEMPTS = 10;
208
+
209
+ function openSyncDb() {
210
+ return new Promise((resolve, reject) => {
211
+ const request = indexedDB.open(SYNC_DB_NAME, SYNC_DB_VERSION);
212
+ request.onupgradeneeded = () => {
213
+ const db = request.result;
214
+ if (!db.objectStoreNames.contains(SYNC_STORE_NAME)) {
215
+ db.createObjectStore(SYNC_STORE_NAME, { autoIncrement: true });
216
+ }
217
+ };
218
+ request.onsuccess = () => resolve(request.result);
219
+ request.onerror = () => reject(request.error || new Error('IndexedDB unavailable'));
220
+ });
221
+ }
222
+
223
+ function syncRequestAsPromise(request) {
224
+ return new Promise((resolve, reject) => {
225
+ request.onsuccess = () => resolve(request.result);
226
+ request.onerror = () => reject(request.error || new Error('IndexedDB request failed'));
227
+ });
228
+ }
229
+
230
+ async function readSyncEntries(db) {
231
+ const store = db.transaction(SYNC_STORE_NAME, 'readonly').objectStore(SYNC_STORE_NAME);
232
+ const [keys, values] = await Promise.all([
233
+ syncRequestAsPromise(store.getAllKeys()),
234
+ syncRequestAsPromise(store.getAll()),
235
+ ]);
236
+ return keys.map((key, index) => ({ key, value: values[index] }));
237
+ }
238
+
239
+ function deleteSyncEntry(db, key) {
240
+ const tx = db.transaction(SYNC_STORE_NAME, 'readwrite');
241
+ tx.objectStore(SYNC_STORE_NAME).delete(key);
242
+ return new Promise((resolve) => {
243
+ tx.oncomplete = resolve;
244
+ tx.onerror = resolve;
245
+ tx.onabort = resolve;
246
+ });
247
+ }
248
+
249
+ function updateSyncEntry(db, key, value) {
250
+ const tx = db.transaction(SYNC_STORE_NAME, 'readwrite');
251
+ tx.objectStore(SYNC_STORE_NAME).put(value, key);
252
+ return new Promise((resolve) => {
253
+ tx.oncomplete = resolve;
254
+ tx.onerror = resolve;
255
+ tx.onabort = resolve;
256
+ });
257
+ }
258
+
259
+ // A 4xx (other than 408/429) is a server decision about this exact request —
260
+ // including the 409 DUPLICATE_MESSAGE_ID a replayed send gets when the first
261
+ // ambiguous attempt actually landed — so the entry is finished either way.
262
+ function isFinalSyncResponse(response) {
263
+ if (!response) return false;
264
+ if (response.ok) return true;
265
+ return response.status >= 400 && response.status < 500
266
+ && response.status !== 408 && response.status !== 429;
267
+ }
268
+
269
+ async function replaySyncOutbox() {
270
+ const db = await openSyncDb();
271
+ try {
272
+ const entries = await readSyncEntries(db);
273
+ for (const { key, value } of entries) {
274
+ let response = null;
275
+ try {
276
+ response = await fetch(scopeUrl(String(value.path || '').replace(/^\//, '')), {
277
+ method: value.method || 'POST',
278
+ credentials: 'include',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: value.body,
281
+ });
282
+ } catch {
283
+ response = null;
284
+ }
285
+ if (isFinalSyncResponse(response)) {
286
+ await deleteSyncEntry(db, key);
287
+ continue;
288
+ }
289
+ const attempts = Number(value.attempts || 0) + 1;
290
+ if (attempts >= SYNC_MAX_REPLAY_ATTEMPTS) {
291
+ await deleteSyncEntry(db, key);
292
+ continue;
293
+ }
294
+ await updateSyncEntry(db, key, { ...value, attempts });
295
+ // Rejecting keeps ordering intact and makes the browser retry the sync
296
+ // with its own backoff.
297
+ throw new Error('outbox replay incomplete');
298
+ }
299
+ } finally {
300
+ try { db.close(); } catch {}
301
+ }
302
+ }
303
+
304
+ self.addEventListener('sync', (event) => {
305
+ if (event.tag !== SYNC_TAG) return;
306
+ event.waitUntil(replaySyncOutbox());
307
+ });
@@ -0,0 +1,4 @@
1
+ 'use strict';
2
+
3
+ // Intentional relay restart requested via authenticated localhost API.
4
+ export const RELAY_RESTART_EXIT_CODE = 75;
@@ -0,0 +1,170 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { RELAY_RESTART_EXIT_CODE } from './relay-exit-codes.mjs';
4
+
5
+ // Role selection travels on argv, never in the environment. The runtime's env is
6
+ // inherited by tmux worker sessions and by the Copilot CLI it launches, and the
7
+ // CLI extension spawns the next server from that same env — so an env-based role
8
+ // flag silently decided the role of unrelated future servers.
9
+ export const RELAY_RUNTIME_FLAG = '--relay-runtime';
10
+ export const RELAY_SUPERVISED_FLAG = '--supervised';
11
+
12
+ function hasFlag(argv, flag) {
13
+ return Array.isArray(argv) && argv.includes(flag);
14
+ }
15
+
16
+ /** True for the child this supervisor spawned: run the runtime in-process. */
17
+ export function isRelayRuntimeInvocation(argv = process.argv.slice(2)) {
18
+ return hasFlag(argv, RELAY_RUNTIME_FLAG);
19
+ }
20
+
21
+ /** True when an outer supervisor owns restarts and wants the runtime here. */
22
+ export function isExternallySupervised(argv = process.argv.slice(2)) {
23
+ return hasFlag(argv, RELAY_SUPERVISED_FLAG);
24
+ }
25
+
26
+ export function spawnRelayRuntime({
27
+ env = process.env,
28
+ cwd = process.cwd(),
29
+ scriptPath,
30
+ args = [],
31
+ execArgv = process.execArgv,
32
+ spawnImpl = spawn,
33
+ stdio = ['ignore', 'pipe', 'pipe'],
34
+ detached = false,
35
+ windowsHide = true,
36
+ logger = console,
37
+ } = {}) {
38
+ const child = spawnImpl(process.execPath, [...execArgv, scriptPath, RELAY_RUNTIME_FLAG, ...args], {
39
+ cwd,
40
+ env,
41
+ detached,
42
+ stdio,
43
+ windowsHide,
44
+ });
45
+ logger?.log?.(`[relay] launched runtime pid=${child.pid || 'none'} script=${path.basename(String(scriptPath || ''))}`);
46
+ return child;
47
+ }
48
+
49
+ function stopChildProcess(child, { signal = 'SIGTERM', killAfterMs = 1200 } = {}) {
50
+ if (!child || child.exitCode !== null) return;
51
+ try {
52
+ child.kill(signal);
53
+ } catch {
54
+ return;
55
+ }
56
+ setTimeout(() => {
57
+ if (!child || child.exitCode !== null) return;
58
+ try { child.kill('SIGKILL'); } catch {}
59
+ }, killAfterMs);
60
+ }
61
+
62
+ function waitForChildExit(child) {
63
+ return new Promise((resolve) => {
64
+ let settled = false;
65
+ const finish = (result) => {
66
+ if (settled) return;
67
+ settled = true;
68
+ resolve(result);
69
+ };
70
+ child.once?.('error', (error) => finish({ code: null, signal: null, error }));
71
+ child.once?.('exit', (code, signal) => finish({ code, signal, error: null }));
72
+ });
73
+ }
74
+
75
+ export async function runDirectRelaySupervisor({
76
+ scriptPath,
77
+ args = [],
78
+ cwd = process.cwd(),
79
+ env = process.env,
80
+ execArgv = process.execArgv,
81
+ spawnImpl = spawn,
82
+ stdio = ['ignore', 'pipe', 'pipe'],
83
+ delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
84
+ exitImpl = (code) => process.exit(code),
85
+ logger = console,
86
+ installSignalHandlers = true,
87
+ restartDelayMs = 500,
88
+ maxCrashRestarts = 3,
89
+ } = {}) {
90
+ let runtimeProc = null;
91
+ let shuttingDown = false;
92
+ let shutdownExitCode = 0;
93
+
94
+ const requestShutdown = (signal = 'SIGTERM', exitCode = 0) => {
95
+ if (shuttingDown) return;
96
+ shuttingDown = true;
97
+ shutdownExitCode = exitCode;
98
+ stopChildProcess(runtimeProc, { signal });
99
+ };
100
+
101
+ const handleSigInt = () => requestShutdown('SIGINT', 0);
102
+ const handleSigTerm = () => requestShutdown('SIGTERM', 0);
103
+
104
+ if (installSignalHandlers) {
105
+ process.on('SIGINT', handleSigInt);
106
+ process.on('SIGTERM', handleSigTerm);
107
+ }
108
+
109
+ try {
110
+ let crashCount = 0;
111
+ while (true) {
112
+ runtimeProc = spawnRelayRuntime({
113
+ env,
114
+ cwd,
115
+ scriptPath,
116
+ args,
117
+ execArgv,
118
+ spawnImpl,
119
+ stdio,
120
+ detached: false,
121
+ logger,
122
+ });
123
+ if (runtimeProc.stdout?.on) {
124
+ runtimeProc.stdout.on('data', (chunk) => process.stdout.write(chunk));
125
+ }
126
+ if (runtimeProc.stderr?.on) {
127
+ runtimeProc.stderr.on('data', (chunk) => process.stderr.write(chunk));
128
+ }
129
+
130
+ const result = await waitForChildExit(runtimeProc);
131
+ runtimeProc = null;
132
+
133
+ if (shuttingDown) {
134
+ return await exitImpl(shutdownExitCode);
135
+ }
136
+
137
+ if (result.error) {
138
+ logger?.error?.(`[relay] runtime launch failed: ${result.error?.message || result.error}`);
139
+ return await exitImpl(1);
140
+ }
141
+
142
+ const exitCode = Number.isInteger(Number(result.code)) ? Number(result.code) : null;
143
+ if (exitCode === RELAY_RESTART_EXIT_CODE) {
144
+ logger?.log?.(`[relay] runtime requested restart; relaunching ${path.basename(String(scriptPath || ''))}...`);
145
+ crashCount = 0;
146
+ await delay(restartDelayMs);
147
+ continue;
148
+ }
149
+
150
+ if (exitCode === 0) {
151
+ return await exitImpl(0);
152
+ }
153
+
154
+ const attempt = crashCount + 1;
155
+ logger?.error?.(`[relay] runtime crashed with exit code ${exitCode ?? 'null'} (crash ${attempt}/${maxCrashRestarts}).`);
156
+ if (attempt >= maxCrashRestarts) {
157
+ logger?.error?.('[relay] crash limit reached; supervisor stopping.');
158
+ return await exitImpl(1);
159
+ }
160
+ crashCount = attempt;
161
+ await delay(restartDelayMs);
162
+ }
163
+
164
+ } finally {
165
+ if (installSignalHandlers) {
166
+ process.off('SIGINT', handleSigInt);
167
+ process.off('SIGTERM', handleSigTerm);
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,27 @@
1
+ # Relay Tool Guidance
2
+
3
+ For any user-facing question or clarification, use the ask_user tool so the web relay can render question cards and buttons. Never ask questions in plain assistant text.
4
+
5
+ When using ask_user, ALWAYS include a `choices` array with 2-6 answer options so the web relay can render clickable buttons. Example:
6
+ ```json
7
+ {
8
+ "question": "<QUIZ_QUESTION_TEXT>",
9
+ "choices": ["<CHOICE_A>", "<CHOICE_B>", "<CHOICE_C>", "<CHOICE_D>"]
10
+ }
11
+ ```
12
+ At runtime, inject the question and choices from a random item in your quiz pool.
13
+ Only omit choices when the question genuinely requires freeform text input (e.g., "What is your name?").
14
+
15
+ In autopilot, still call ask_user when user input is truly blocking, because the relay bridge can surface the question even when the direct SDK question hook is bypassed.
16
+
17
+ For relay restarts in extension-managed mode, require explicit user permission first, then use the authenticated localhost API `POST /api/relay/shutdown`. Do not restart by killing processes or using respawn scripts.
18
+
19
+ Note: shutdown is queued and only completes when the current turn finishes, so it is pointless to wait for it to interrupt an active turn.
20
+
21
+ Use `restart: true` in the request body when the user wants a real relay restart rather than a plain shutdown. Example request body: `{ "reason": "manual-restart", "requestedBy": "localhost-api", "restart": true }`.
22
+
23
+ ## Preview servers
24
+
25
+ Publish a local web server or a static directory on a public preview URL so the user can open it from any device, or list/close existing previews. Use this whenever the user wants to see, try, or share something with a web UI — never ask them to open localhost or forward a port. For a dev server: call this FIRST with {action:"create", port} to get the basePath, then start the server configured to serve under that basePath (Vite --base, Next basePath, Express mount prefix); the proxy forwards X-Forwarded-Prefix but does not rewrite bodies, so root-absolute asset paths will not work. For plain files or a build output: pass {action:"create", dir} instead and the relay serves the directory itself — no dev server needed. Always tell the user the returned URL and that the link is public: anyone who has it can reach the app without logging in. Previews never expire on their own; close them with {action:"close"} when the user is done.
26
+
27
+ There is no `preview` tool on this provider — use the authenticated localhost API (same auth as `POST /api/relay/shutdown`): `POST /api/previews` with `{ "conversationId": "<conv>", "port": 5173, "label": "web app" }` (or `"dir": "./dist"` instead of the port) publishes, `GET /api/previews` lists, `DELETE /api/previews/:token` closes the link without touching the dev server behind it. A 503 with `details` means the preview lane is disabled or misconfigured — surface those details, they are the operator's fix. See `docs/preview-servers.md`.