@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,311 @@
1
+ /**
2
+ * Map ACP session/update payloads onto the six relay channels used by
3
+ * Claude/Cursor workers: init, stream, thought, activity, subagent, result.
4
+ */
5
+ import { capThought } from '../../shared/thought-cap.mjs';
6
+ import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
7
+
8
+ const MAX_TOOL_DETAIL_LENGTH = 140;
9
+ const SUBAGENT_TOOL_NAMES = new Set(['task', 'agent', 'subagent']);
10
+
11
+ function truncate(text, maxLength = MAX_TOOL_DETAIL_LENGTH) {
12
+ const value = String(text || '').trim();
13
+ if (value.length <= maxLength) return value;
14
+ return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
15
+ }
16
+
17
+ export function extractTextContent(content) {
18
+ if (content == null) return '';
19
+ if (typeof content === 'string') return content;
20
+ if (typeof content === 'object') {
21
+ if (typeof content.text === 'string') return content.text;
22
+ if (content.content && typeof content.content === 'object' && typeof content.content.text === 'string') {
23
+ return content.content.text;
24
+ }
25
+ }
26
+ return '';
27
+ }
28
+
29
+ // Re-exported (rather than defined here) so existing importers of this
30
+ // normalizer keep working; the rule itself lives in shared/.
31
+ export { shouldEmitStreamUpdate };
32
+
33
+ export function summarizeToolInput(input) {
34
+ if (!input || typeof input !== 'object') {
35
+ if (typeof input === 'string') return truncate(input);
36
+ return '';
37
+ }
38
+ const candidates = [
39
+ input.command,
40
+ input.file_path,
41
+ input.path,
42
+ input.pattern,
43
+ input.description,
44
+ input.url,
45
+ input.query,
46
+ input.prompt,
47
+ input.title,
48
+ ];
49
+ for (const candidate of candidates) {
50
+ const value = String(candidate ?? '').trim();
51
+ if (value) return value;
52
+ }
53
+ try {
54
+ const serialized = JSON.stringify(input);
55
+ return serialized === '{}' ? '' : serialized;
56
+ } catch {
57
+ return '';
58
+ }
59
+ }
60
+
61
+ export function formatToolActivityText(toolName, input, title = '') {
62
+ const name = String(toolName || title || 'tool').trim() || 'tool';
63
+ const summary = summarizeToolInput(input) || String(title || '').trim();
64
+ return truncate(summary ? `Tool (${name}): ${summary}` : `Tool (${name})`);
65
+ }
66
+
67
+ export function isSubagentToolName(name) {
68
+ return SUBAGENT_TOOL_NAMES.has(String(name || '').trim().toLowerCase());
69
+ }
70
+
71
+ /**
72
+ * One instance per turn. `normalizeAcpUpdate(update)` returns zero or more
73
+ * `{ channel, payload }` actions.
74
+ */
75
+ export function createSdkMessageNormalizer() {
76
+ let streamText = '';
77
+ let lastEmittedStreamText = '';
78
+ let openThought = null; // { reasoningId, text }
79
+ let stepIndex = 0;
80
+ let thoughtIndex = 0;
81
+ let lastUsage = null;
82
+ const knownSubagentRuns = new Map();
83
+ const seenToolFrames = new Set();
84
+
85
+ function allocateThoughtId() {
86
+ const id = `grok-thought-main-${stepIndex}-${thoughtIndex}`;
87
+ thoughtIndex += 1;
88
+ return id;
89
+ }
90
+
91
+ function closeOpenThought(actions) {
92
+ if (!openThought) return;
93
+ if (openThought.text.trim()) {
94
+ actions.push({
95
+ channel: 'thought',
96
+ payload: {
97
+ reasoningId: openThought.reasoningId,
98
+ text: openThought.text,
99
+ done: true,
100
+ subagentRunId: null,
101
+ },
102
+ });
103
+ }
104
+ openThought = null;
105
+ }
106
+
107
+ function normalizeAcpUpdate(update) {
108
+ const actions = [];
109
+ if (!update || typeof update !== 'object') return actions;
110
+
111
+ const kind = String(update.sessionUpdate || update.type || '').trim();
112
+
113
+ if (kind === 'agent_message_chunk' || kind === 'agent_message') {
114
+ const chunk = extractTextContent(update.content);
115
+ if (!chunk) return actions;
116
+ streamText += chunk;
117
+ if (shouldEmitStreamUpdate(streamText, lastEmittedStreamText)) {
118
+ lastEmittedStreamText = streamText;
119
+ actions.push({
120
+ channel: 'stream',
121
+ payload: {
122
+ text: streamText,
123
+ done: false,
124
+ subagentRunId: null,
125
+ },
126
+ });
127
+ }
128
+ return actions;
129
+ }
130
+
131
+ if (kind === 'agent_thought_chunk' || kind === 'agent_thought') {
132
+ const chunk = extractTextContent(update.content);
133
+ if (!chunk) return actions;
134
+ if (!openThought) {
135
+ openThought = { reasoningId: allocateThoughtId(), text: '' };
136
+ }
137
+ openThought.text = capThought(openThought.text + chunk);
138
+ actions.push({
139
+ channel: 'thought',
140
+ payload: {
141
+ reasoningId: openThought.reasoningId,
142
+ text: openThought.text,
143
+ done: false,
144
+ subagentRunId: null,
145
+ },
146
+ });
147
+ return actions;
148
+ }
149
+
150
+ if (kind === 'tool_call' || kind === 'tool_call_update') {
151
+ const toolCallId = String(update.toolCallId || update.tool_call_id || update.id || '').trim();
152
+ const title = String(update.title || '').trim();
153
+ // ACP's `kind` on a tool_call is the tool CATEGORY (read/edit/execute/
154
+ // think/…), not the tool's name — using it as the name produced labels
155
+ // like "Tool (execute)" and made subagent detection unreachable (a
156
+ // category is never 'task'/'agent'). Name fields win, then the title,
157
+ // with the category as the last-resort label.
158
+ const toolCategory = String(update.kind || '').trim();
159
+ const explicitToolName = String(update.name || update.toolName || '').trim();
160
+ const toolName = explicitToolName || title || toolCategory || 'tool';
161
+ const status = String(update.status || (kind === 'tool_call' ? 'pending' : '')).trim().toLowerCase();
162
+ const frameKey = `${toolCallId}\u0000${status || kind}`;
163
+ if (toolCallId && seenToolFrames.has(frameKey)) return actions;
164
+ if (toolCallId) seenToolFrames.add(frameKey);
165
+
166
+ const input = update.rawInput || update.input || update.arguments || null;
167
+ const activityText = formatToolActivityText(toolName, input, title);
168
+ if (activityText) {
169
+ actions.push({
170
+ channel: 'activity',
171
+ payload: {
172
+ text: activityText,
173
+ subagentRunId: null,
174
+ },
175
+ });
176
+ }
177
+
178
+ // Subagent lifecycle keys on the explicit tool name when present, and
179
+ // otherwise on a task/agent-shaped title — the category can never match.
180
+ const subagentLike = isSubagentToolName(explicitToolName)
181
+ || (!explicitToolName && /^(task|agent|subagent)\b/i.test(title));
182
+ if (subagentLike && toolCallId) {
183
+ const displayName = title || toolName;
184
+ if (status === 'pending' || status === 'in_progress' || status === 'running' || kind === 'tool_call') {
185
+ if (!knownSubagentRuns.has(toolCallId)) {
186
+ knownSubagentRuns.set(toolCallId, displayName);
187
+ actions.push({
188
+ channel: 'subagent',
189
+ payload: {
190
+ subagentRunId: toolCallId,
191
+ displayName,
192
+ status: 'running',
193
+ parentSubagentId: null,
194
+ },
195
+ });
196
+ }
197
+ }
198
+ if (status === 'completed' || status === 'failed' || status === 'error' || status === 'cancelled') {
199
+ if (knownSubagentRuns.has(toolCallId) || status) {
200
+ actions.push({
201
+ channel: 'subagent',
202
+ payload: {
203
+ subagentRunId: toolCallId,
204
+ displayName: knownSubagentRuns.get(toolCallId) || displayName,
205
+ status: status === 'completed' ? 'completed' : 'failed',
206
+ parentSubagentId: null,
207
+ },
208
+ });
209
+ knownSubagentRuns.delete(toolCallId);
210
+ }
211
+ }
212
+ }
213
+ return actions;
214
+ }
215
+
216
+ if (kind === 'usage_update' || kind === 'usage') {
217
+ lastUsage = {
218
+ used: update.used,
219
+ size: update.size,
220
+ cost: update.cost,
221
+ ...(typeof update === 'object' ? update : {}),
222
+ };
223
+ return actions;
224
+ }
225
+
226
+ if (kind === 'plan') {
227
+ const entries = Array.isArray(update.entries) ? update.entries : [];
228
+ const lines = entries
229
+ .map((entry) => String(entry?.content || entry?.text || entry?.title || '').trim())
230
+ .filter(Boolean);
231
+ if (lines.length) {
232
+ actions.push({
233
+ channel: 'activity',
234
+ payload: {
235
+ text: truncate(`Plan: ${lines.join(' · ')}`, 200),
236
+ subagentRunId: null,
237
+ },
238
+ });
239
+ }
240
+ return actions;
241
+ }
242
+
243
+ // Ignore user_message_chunk and unknown updates.
244
+ return actions;
245
+ }
246
+
247
+ function finalizeResult({
248
+ stopReason = '',
249
+ text = '',
250
+ isError = false,
251
+ errorMessage = '',
252
+ model = '',
253
+ usage = null,
254
+ } = {}) {
255
+ const actions = [];
256
+ closeOpenThought(actions);
257
+ const finalText = String(text || streamText || '').trim();
258
+ if (finalText && finalText !== lastEmittedStreamText) {
259
+ actions.push({
260
+ channel: 'stream',
261
+ payload: {
262
+ text: finalText,
263
+ done: true,
264
+ subagentRunId: null,
265
+ },
266
+ });
267
+ lastEmittedStreamText = finalText;
268
+ streamText = finalText;
269
+ } else if (streamText) {
270
+ actions.push({
271
+ channel: 'stream',
272
+ payload: {
273
+ text: streamText,
274
+ done: true,
275
+ subagentRunId: null,
276
+ },
277
+ });
278
+ }
279
+ // Prefer the prompt-result usage (authoritative per-turn totals). Fall back
280
+ // to any usage_update stream payload the normalizer already observed.
281
+ const resolvedUsage = usage && typeof usage === 'object' ? usage : lastUsage;
282
+ actions.push({
283
+ channel: 'result',
284
+ payload: {
285
+ text: finalText,
286
+ isError: isError === true,
287
+ errorMessage: String(errorMessage || '').trim(),
288
+ stopReason: String(stopReason || '').trim(),
289
+ usage: resolvedUsage,
290
+ model: String(model || '').trim(),
291
+ },
292
+ });
293
+ return actions;
294
+ }
295
+
296
+ function finalStreamText() {
297
+ return streamText;
298
+ }
299
+
300
+ return {
301
+ normalizeAcpUpdate,
302
+ finalizeResult,
303
+ finalStreamText,
304
+ get model() {
305
+ return '';
306
+ },
307
+ get lastUsage() {
308
+ return lastUsage;
309
+ },
310
+ };
311
+ }
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: add structured answer support to relay_questions.
5
+ *
6
+ * Adds two nullable TEXT columns used for multi-field `ask_user` / elicitation
7
+ * forms while preserving the existing single-string `answer` column for
8
+ * backward compatibility:
9
+ * - structured_answer: JSON object of the user's per-field answers
10
+ * - request_schema: JSON of the elicitation requestedSchema (for validation)
11
+ *
12
+ * Safe to run repeatedly (idempotent) and against a live database — ADD COLUMN
13
+ * only takes a brief write lock and new columns default to NULL.
14
+ *
15
+ * Usage: node server/migrations/0001-add-structured-answer.mjs [path/to/copilot.db]
16
+ */
17
+
18
+ import { fileURLToPath } from 'url';
19
+ import path from 'path';
20
+ import process from 'process';
21
+ import Database from 'better-sqlite3';
22
+
23
+ const NEW_COLUMNS = [
24
+ { name: 'structured_answer', ddl: 'ALTER TABLE relay_questions ADD COLUMN structured_answer TEXT' },
25
+ { name: 'request_schema', ddl: 'ALTER TABLE relay_questions ADD COLUMN request_schema TEXT' },
26
+ ];
27
+
28
+ export function migrate(dbPath) {
29
+ const db = new Database(dbPath);
30
+ try {
31
+ const tableExists = db
32
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'relay_questions'`)
33
+ .get();
34
+ if (!tableExists) {
35
+ return { applied: [], skipped: NEW_COLUMNS.map((c) => c.name), reason: 'relay_questions table not found' };
36
+ }
37
+ const existing = new Set(
38
+ db.prepare(`PRAGMA table_info(relay_questions)`).all().map((c) => String(c.name)),
39
+ );
40
+ const applied = [];
41
+ const skipped = [];
42
+ const tx = db.transaction(() => {
43
+ for (const column of NEW_COLUMNS) {
44
+ if (existing.has(column.name)) {
45
+ skipped.push(column.name);
46
+ continue;
47
+ }
48
+ db.exec(column.ddl);
49
+ applied.push(column.name);
50
+ }
51
+ });
52
+ tx();
53
+ return { applied, skipped };
54
+ } finally {
55
+ db.close();
56
+ }
57
+ }
58
+
59
+ function defaultDbPath() {
60
+ const here = path.dirname(fileURLToPath(import.meta.url));
61
+ return path.resolve(here, '..', 'data', 'copilot.db');
62
+ }
63
+
64
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
65
+ if (isMain) {
66
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
67
+ try {
68
+ const result = migrate(dbPath);
69
+ const applied = result.applied.length ? result.applied.join(', ') : '(none)';
70
+ const skipped = result.skipped.length ? result.skipped.join(', ') : '(none)';
71
+ console.log(`Migration 0001 on ${dbPath}`);
72
+ console.log(` added: ${applied}`);
73
+ console.log(` existed: ${skipped}`);
74
+ if (result.reason) console.log(` note: ${result.reason}`);
75
+ process.exit(0);
76
+ } catch (err) {
77
+ console.error(`Migration 0001 failed: ${err?.message || err}`);
78
+ process.exit(1);
79
+ }
80
+ }
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: add a case-normalized dedupe key to recent_workspace_roots.
5
+ *
6
+ * The table's primary key used to be the raw `path`, which is case-sensitive.
7
+ * On Windows that means "C:\Git\Repo" and "c:\git\repo" are two rows for one
8
+ * directory: they burn through the 12-row cap and the UI then silently hides
9
+ * the duplicates (the client dedupes case-insensitively). This adds
10
+ *
11
+ * path_key TEXT PRIMARY KEY -- normalizeWorkspaceRootKey(path)
12
+ * path TEXT NOT NULL -- most recently seen casing, for display
13
+ *
14
+ * and collapses existing rows, keeping the newest last_seen_at for each
15
+ * directory. The grouping is done in JS, not SQL, because SQLite's LOWER() and
16
+ * COLLATE NOCASE are ASCII-only and would also fold on POSIX, where paths
17
+ * really are case-sensitive.
18
+ *
19
+ * Idempotent: re-running once path_key exists is a no-op.
20
+ *
21
+ * Usage: node server/migrations/0002-recent-workspace-roots-path-key.mjs [path/to/copilot.db]
22
+ */
23
+
24
+ import { fileURLToPath } from 'url';
25
+ import path from 'path';
26
+ import process from 'process';
27
+ import Database from 'better-sqlite3';
28
+
29
+ import { normalizeWorkspaceRootKey } from '../services/workspace-root-path-policy.mjs';
30
+
31
+ /**
32
+ * Rebuild against an already-open database handle.
33
+ * server-runtime.mjs calls this at startup so an existing database is upgraded
34
+ * before any prepared statement references path_key; the CLI entry point below
35
+ * wraps it for offline runs. Both paths therefore execute the same code.
36
+ */
37
+ export function rebuildRecentWorkspaceRootsTable(db, { platform = process.platform } = {}) {
38
+ const tableExists = db
39
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'recent_workspace_roots'`)
40
+ .get();
41
+ if (!tableExists) {
42
+ return { applied: false, reason: 'recent_workspace_roots table not found', rowsBefore: 0, rowsAfter: 0 };
43
+ }
44
+ const columns = new Set(
45
+ db.prepare(`PRAGMA table_info(recent_workspace_roots)`).all().map((column) => String(column.name)),
46
+ );
47
+ if (columns.has('path_key')) {
48
+ return { applied: false, reason: 'path_key already present', rowsBefore: 0, rowsAfter: 0 };
49
+ }
50
+
51
+ const rows = db.prepare(`SELECT path, last_seen_at FROM recent_workspace_roots`).all();
52
+ const byKey = new Map();
53
+ for (const row of rows) {
54
+ const rawPath = String(row?.path || '').trim();
55
+ if (!rawPath) continue;
56
+ const key = normalizeWorkspaceRootKey(rawPath, platform);
57
+ if (!key) continue;
58
+ const lastSeenAt = String(row?.last_seen_at || '');
59
+ const existing = byKey.get(key);
60
+ // Newest last_seen_at wins, and with it the casing shown in the picker.
61
+ if (!existing || lastSeenAt > existing.lastSeenAt) {
62
+ byKey.set(key, { key, path: rawPath, lastSeenAt });
63
+ }
64
+ }
65
+
66
+ db.transaction(() => {
67
+ db.exec(`
68
+ CREATE TABLE recent_workspace_roots__new (
69
+ path_key TEXT PRIMARY KEY,
70
+ path TEXT NOT NULL,
71
+ last_seen_at TEXT NOT NULL
72
+ );
73
+ `);
74
+ const insert = db.prepare(`
75
+ INSERT INTO recent_workspace_roots__new (path_key, path, last_seen_at)
76
+ VALUES (?, ?, ?)
77
+ `);
78
+ for (const entry of byKey.values()) insert.run(entry.key, entry.path, entry.lastSeenAt);
79
+ db.exec(`DROP INDEX IF EXISTS idx_recent_workspace_roots_last_seen`);
80
+ db.exec(`DROP TABLE recent_workspace_roots`);
81
+ db.exec(`ALTER TABLE recent_workspace_roots__new RENAME TO recent_workspace_roots`);
82
+ db.exec(`
83
+ CREATE INDEX IF NOT EXISTS idx_recent_workspace_roots_last_seen
84
+ ON recent_workspace_roots(last_seen_at DESC)
85
+ `);
86
+ })();
87
+
88
+ return { applied: true, rowsBefore: rows.length, rowsAfter: byKey.size };
89
+ }
90
+
91
+ export function migrate(dbPath, options = {}) {
92
+ const db = new Database(dbPath);
93
+ try {
94
+ return rebuildRecentWorkspaceRootsTable(db, options);
95
+ } finally {
96
+ db.close();
97
+ }
98
+ }
99
+
100
+ function defaultDbPath() {
101
+ const here = path.dirname(fileURLToPath(import.meta.url));
102
+ return path.resolve(here, '..', 'data', 'copilot.db');
103
+ }
104
+
105
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
106
+ if (isMain) {
107
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
108
+ try {
109
+ const result = migrate(dbPath);
110
+ console.log(`Migration 0002 on ${dbPath}`);
111
+ if (result.applied) {
112
+ console.log(` rebuilt: ${result.rowsBefore} row(s) -> ${result.rowsAfter} distinct directory/ies`);
113
+ } else {
114
+ console.log(` skipped: ${result.reason}`);
115
+ }
116
+ process.exit(0);
117
+ } catch (err) {
118
+ console.error(`Migration 0002 failed: ${err?.message || err}`);
119
+ process.exit(1);
120
+ }
121
+ }
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: add the push_subscriptions table for Web Push notifications.
5
+ *
6
+ * One row per subscribed browser installation. `endpoint` is unique because a
7
+ * re-subscribe from the same browser must update in place, and `device_id` is
8
+ * the durable per-device identity (localStorage `copilot_device_id`) that a
9
+ * device keeps across subscription churn. `preferences_json` carries the whole
10
+ * per-device notification preference set so it travels with the subscription.
11
+ *
12
+ * Idempotent: re-running once the table exists is a no-op. The main schema
13
+ * block in server-runtime.mjs also has a CREATE TABLE IF NOT EXISTS for fresh
14
+ * databases; this module exists so existing databases upgrade at startup and
15
+ * so the migration can be run offline against a db file.
16
+ *
17
+ * Usage: node server/migrations/0003-push-subscriptions.mjs [path/to/copilot.db]
18
+ */
19
+
20
+ import { fileURLToPath } from 'url';
21
+ import path from 'path';
22
+ import process from 'process';
23
+ import Database from 'better-sqlite3';
24
+
25
+ export const PUSH_SUBSCRIPTIONS_SCHEMA = `
26
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
27
+ id TEXT PRIMARY KEY,
28
+ device_id TEXT NOT NULL,
29
+ device_label TEXT,
30
+ endpoint TEXT NOT NULL UNIQUE,
31
+ keys_json TEXT NOT NULL,
32
+ preferences_json TEXT NOT NULL,
33
+ user_agent TEXT,
34
+ created_at TEXT NOT NULL,
35
+ updated_at TEXT NOT NULL,
36
+ last_success_at TEXT,
37
+ last_error TEXT,
38
+ failure_count INTEGER NOT NULL DEFAULT 0
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_push_subscriptions_device
42
+ ON push_subscriptions(device_id);
43
+ `;
44
+
45
+ /**
46
+ * Ensure the table exists on an already-open database handle.
47
+ * server-runtime.mjs calls this at startup; the CLI entry point below wraps it
48
+ * for offline runs. Both paths execute the same code.
49
+ */
50
+ export function ensurePushSubscriptionsTable(db) {
51
+ const tableExists = db
52
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'push_subscriptions'`)
53
+ .get();
54
+ if (tableExists) {
55
+ return { applied: false, reason: 'push_subscriptions already present' };
56
+ }
57
+ db.exec(PUSH_SUBSCRIPTIONS_SCHEMA);
58
+ return { applied: true };
59
+ }
60
+
61
+ export function migrate(dbPath) {
62
+ const db = new Database(dbPath);
63
+ try {
64
+ return ensurePushSubscriptionsTable(db);
65
+ } finally {
66
+ db.close();
67
+ }
68
+ }
69
+
70
+ function defaultDbPath() {
71
+ const here = path.dirname(fileURLToPath(import.meta.url));
72
+ return path.resolve(here, '..', 'data', 'copilot.db');
73
+ }
74
+
75
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
76
+ if (isMain) {
77
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
78
+ try {
79
+ const result = migrate(dbPath);
80
+ console.log(`Migration 0003 on ${dbPath}`);
81
+ console.log(result.applied ? ' created push_subscriptions' : ` skipped: ${result.reason}`);
82
+ process.exit(0);
83
+ } catch (err) {
84
+ console.error(`Migration 0003 failed: ${err?.message || err}`);
85
+ process.exit(1);
86
+ }
87
+ }