agentbox-app 0.1.12 → 0.1.13

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 (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,271 @@
1
+ // OPENCODE'S OWN TRANSPORT, AND WHY IT IS THE SERVER AND NOT THE CLI.
2
+ //
3
+ // Measured against a real OpenCode 1.18.35 on 2026-10-07. `opencode run` is
4
+ // the obvious road -- it is non-interactive, `--format json` prints one JSON
5
+ // object per line, and that is nearly the shape main/harnesses/claude-stream
6
+ // already reads. It is also the wrong road, and the measurement says so in one
7
+ // line. Run with `permission: { bash: "ask" }` in config, the CLI printed:
8
+ //
9
+ // ! permission requested: bash (echo probe-run-mode); auto-rejecting
10
+ //
11
+ // and handed the model "The user rejected permission to use this specific tool
12
+ // call." With no permission config at all it ran the command with no card.
13
+ // `--auto` approves everything not explicitly denied. So the CLI offers
14
+ // approve-everything or refuse-everything, and an Agentbox card is neither.
15
+ //
16
+ // The HTTP server does carry a real approval. `opencode serve` raises
17
+ // `permission.asked` on its `/event` stream with the command in
18
+ // `metadata.command`, and `POST /session/:id/permissions/:id` with
19
+ // `{ response: "once" }` releases it while `{ response: "reject" }` refuses it.
20
+ // Both measured end to end against the real binary, both answers, 200 each
21
+ // time, the tool running in the first case and coming back
22
+ // "The user rejected permission to use this specific tool call." in the second.
23
+ // That is the same card contract main/codex-approvals.mjs already fills, which
24
+ // is why this file exists and a second stream parser does not.
25
+ //
26
+ // AND IT IS WHY WE DO NOT PASS `--auto` ANYWHERE. A grep for it is a test
27
+ // (tests/opencode-asks-before-it-runs-a-command), because the flag would make
28
+ // every run quietly approve its own file writes and shell commands.
29
+ import { EventEmitter } from 'node:events';
30
+ import { Name } from '../shared/product-name.mjs';
31
+
32
+ /** How long a single HTTP call to the local server may take. */
33
+ const CALL_MS = 60_000;
34
+
35
+ /**
36
+ * A client for one `opencode serve`.
37
+ *
38
+ * `password` is the server's own basic-auth secret, which Agentbox generates
39
+ * per server and which OpenCode reads from `OPENCODE_SERVER_PASSWORD`. It is
40
+ * NOT a provider credential: OpenCode loads those itself, from its own
41
+ * `~/.local/share/opencode/auth.json`. Agentbox never has one to pass, which
42
+ * is the rule in CLAUDE.md and the reason this harness can exist at all.
43
+ */
44
+ export function openCodeClient({ baseUrl, password = null, username = 'opencode', fetchImpl = globalThis.fetch } = {}) {
45
+ if (!baseUrl) throw Error('openCodeClient needs the address of a running OpenCode server');
46
+ const root = String(baseUrl).replace(/\/+$/, '');
47
+ const authorization = password ? `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` : null;
48
+
49
+ const request = async (path, { method = 'GET', body, signal, stream = false } = {}) => {
50
+ const response = await fetchImpl(root + path, {
51
+ method,
52
+ headers: {
53
+ ...(authorization ? { authorization } : {}),
54
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
55
+ ...(stream ? { accept: 'text/event-stream' } : {}),
56
+ },
57
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
58
+ signal: signal ?? AbortSignal.timeout(CALL_MS),
59
+ });
60
+ if (!response.ok) throw Error(`OpenCode answered ${response.status} for ${method} ${path}`);
61
+ if (stream) return response;
62
+ const text = await response.text();
63
+ if (!text) return null;
64
+ try { return JSON.parse(text); } catch { return text; }
65
+ };
66
+
67
+ return {
68
+ url: root,
69
+ request,
70
+ createSession: (body = {}) => request('/session', { method: 'POST', body }),
71
+ prompt: (sessionId, body) => request(`/session/${encodeURIComponent(sessionId)}/prompt_async`, { method: 'POST', body }),
72
+ abort: sessionId => request(`/session/${encodeURIComponent(sessionId)}/abort`, { method: 'POST', body: {} }),
73
+ // `{ response }` and not `{ action }`: the field name is off the real
74
+ // OpenAPI document the running server serves at /doc.
75
+ replyPermission: (sessionId, permissionId, response) =>
76
+ request(`/session/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(permissionId)}`, { method: 'POST', body: { response } }),
77
+ summarize: (sessionId, model) =>
78
+ request(`/session/${encodeURIComponent(sessionId)}/summarize`, { method: 'POST', body: splitModel(model) }),
79
+ runCommand: (sessionId, body) => request(`/session/${encodeURIComponent(sessionId)}/command`, { method: 'POST', body }),
80
+ providers: () => request('/config/providers'),
81
+ commands: () => request('/command'),
82
+ config: () => request('/config'),
83
+ health: () => request('/global/health'),
84
+ events: ({ signal } = {}) => eventStream(request, signal),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * `provider/model` as the two fields OpenCode's API wants.
90
+ *
91
+ * Its model ids are provider-qualified and the slash is part of the id people
92
+ * see (`opencode/big-pickle`, `anthropic/claude-sonnet-4-5`), but every server
93
+ * endpoint takes `providerID` and `modelID` apart. A model with no slash is not
94
+ * guessed at: it is left for the server's own default to answer, because
95
+ * inventing a provider here is how a run dies on a model nobody chose.
96
+ */
97
+ export function splitModel(model) {
98
+ const text = typeof model === 'string' ? model.trim() : '';
99
+ const at = text.indexOf('/');
100
+ if (at <= 0 || at === text.length - 1) return {};
101
+ return { providerID: text.slice(0, at), modelID: text.slice(at + 1) };
102
+ }
103
+
104
+ /** The `/event` stream, as an async iterable of `{ type, properties }`. */
105
+ function eventStream(request, outerSignal) {
106
+ const controller = new AbortController();
107
+ outerSignal?.addEventListener('abort', () => controller.abort(), { once: true });
108
+ const opened = request('/event', { stream: true, signal: controller.signal });
109
+
110
+ return {
111
+ close: () => controller.abort(),
112
+ async *[Symbol.asyncIterator]() {
113
+ const response = await opened;
114
+ const reader = response.body.getReader();
115
+ const decoder = new TextDecoder();
116
+ let buffer = '';
117
+ try {
118
+ while (true) {
119
+ const { value, done } = await reader.read();
120
+ if (done) return;
121
+ buffer += decoder.decode(value, { stream: true });
122
+ let index;
123
+ while ((index = buffer.indexOf('\n')) >= 0) {
124
+ const line = buffer.slice(0, index).trim();
125
+ buffer = buffer.slice(index + 1);
126
+ if (!line.startsWith('data:')) continue;
127
+ const payload = line.slice(5).trim();
128
+ if (!payload) continue;
129
+ try { yield JSON.parse(payload); } catch { /* a half-written frame is not news */ }
130
+ }
131
+ }
132
+ } finally {
133
+ try { await reader.cancel(); } catch { /* already gone */ }
134
+ }
135
+ },
136
+ };
137
+ }
138
+
139
+ /** The permission events, old and new, that mean "a card is wanted". */
140
+ const ASKED = new Set(['permission.asked', 'permission.v2.asked']);
141
+ /** What the reply endpoint calls yes and no. */
142
+ const ALLOW = 'once';
143
+ const DENY = 'reject';
144
+
145
+ /**
146
+ * ONE OPENCODE RUN, SHAPED LIKE A CHILD PROCESS.
147
+ *
148
+ * The supervisor's spawn path wants something with `stderr`, `on('event')` and
149
+ * one `exit`, because that is what a Claude Code child and a Codex worker both
150
+ * look like from there (main/supervisor.mjs, `_spawnWorker`). This is the third
151
+ * one, and it owns nothing the other two own: its events are OpenCode's own
152
+ * names, passed through untranslated, exactly as the Codex adapter passes its
153
+ * notifications through rather than pretending to be Claude's JSON lines.
154
+ *
155
+ * DENY IS THE ANSWER NOBODY HAS TO CHOOSE. A card that is never answered, a
156
+ * handler that throws, a spool that has gone away, a worker killed mid-question
157
+ * -- every one of those refuses the tool call. The opposite default would mean
158
+ * a crashed inbox silently approves shell commands, which is the one failure
159
+ * here that cannot be taken back.
160
+ */
161
+ export function createOpenCodeWorker({
162
+ client,
163
+ plan = {},
164
+ cwd = null,
165
+ resumeSessionId = null,
166
+ onApproval = null,
167
+ title = null,
168
+ } = {}) {
169
+ if (!client) throw Error('createOpenCodeWorker needs a client for a running OpenCode server');
170
+
171
+ const worker = new EventEmitter();
172
+ worker.stderr = new EventEmitter();
173
+ worker.stdout = new EventEmitter();
174
+ worker.sessionId = resumeSessionId ?? null;
175
+
176
+ const say = text => {
177
+ const line = String(text ?? '').trim();
178
+ if (line) worker.stderr.emit('data', line);
179
+ };
180
+
181
+ let ended = false;
182
+ let stream = null;
183
+ const answered = new Set();
184
+
185
+ const stop = async () => {
186
+ if (ended) return;
187
+ ended = true;
188
+ try { stream?.close(); } catch { /* already closed */ }
189
+ if (worker.sessionId) { try { await client.abort(worker.sessionId); } catch { /* the run is over either way */ } }
190
+ };
191
+
192
+ /**
193
+ * One card, and the reply that releases or refuses the tool.
194
+ *
195
+ * The id is remembered BEFORE the card is raised, so a request the stream
196
+ * repeats -- which it does, `permission.asked` can arrive again for a request
197
+ * still open -- costs one card and not two.
198
+ */
199
+ const decide = async (type, properties) => {
200
+ const id = properties?.id;
201
+ const session = properties?.sessionID;
202
+ if (!id || session !== worker.sessionId || answered.has(id)) return;
203
+ answered.add(id);
204
+
205
+ let allowed = false;
206
+ try {
207
+ allowed = onApproval ? await onApproval(type, properties) === true : false;
208
+ } catch (error) {
209
+ say(`${Name} could not ask about ${String(properties?.metadata?.command ?? 'a tool call')}: ${error?.message ?? error}`);
210
+ allowed = false;
211
+ }
212
+ if (ended) allowed = false;
213
+
214
+ try {
215
+ await client.replyPermission(session, id, allowed ? ALLOW : DENY);
216
+ } catch (error) {
217
+ say(`OpenCode did not take the answer to that question: ${error?.message ?? error}`);
218
+ }
219
+ };
220
+
221
+ const run = async () => {
222
+ try {
223
+ if (!worker.sessionId) {
224
+ const session = await client.createSession({ ...(title ? { title } : {}), ...(cwd ? { directory: cwd } : {}) });
225
+ worker.sessionId = session?.id ?? null;
226
+ if (!worker.sessionId) throw Error('OpenCode did not name the session it opened');
227
+ }
228
+
229
+ // The stream is opened BEFORE the prompt. Opening it after is a race the
230
+ // fast answers win: `permission.asked` for the first tool call can beat
231
+ // our own subscription, and nothing would ever answer that card.
232
+ stream = client.events();
233
+ const reading = (async () => {
234
+ for await (const event of stream) {
235
+ if (ended) return;
236
+ const type = String(event?.type ?? '');
237
+ const properties = event?.properties ?? {};
238
+ worker.emit('event', type, properties);
239
+ if (ASKED.has(type)) { await decide(type, properties); continue; }
240
+ if (properties?.sessionID && properties.sessionID !== worker.sessionId) continue;
241
+ if (type === 'session.error') {
242
+ worker.error = properties?.error ?? 'OpenCode reported an error';
243
+ return;
244
+ }
245
+ if (type === 'session.idle') return;
246
+ }
247
+ })();
248
+
249
+ await client.prompt(worker.sessionId, {
250
+ ...splitModel(plan.model),
251
+ ...(plan.agent ? { agent: plan.agent } : {}),
252
+ parts: [{ type: 'text', text: String(plan.prompt ?? '') }],
253
+ });
254
+
255
+ await reading;
256
+ } catch (error) {
257
+ worker.error = error?.message ?? String(error);
258
+ say(worker.error);
259
+ } finally {
260
+ const already = ended;
261
+ ended = true;
262
+ try { stream?.close(); } catch { /* already closed */ }
263
+ if (!already && worker.sessionId) { try { await client.abort(worker.sessionId); } catch { /* nothing left to stop */ } }
264
+ worker.emit('exit', worker.error ? 1 : 0, null);
265
+ }
266
+ };
267
+
268
+ worker.kill = () => { void stop(); };
269
+ worker.done = run();
270
+ return worker;
271
+ }
@@ -0,0 +1,287 @@
1
+ // READING AN OPENCODE RUN, IN OPENCODE'S OWN WORDS.
2
+ //
3
+ // The same five readers Claude Code and Codex each provide -- capture, live
4
+ // text, summary, trace, activity -- over OpenCode's `/event` stream. Nothing
5
+ // here translates an OpenCode event into a pretend Claude JSON line or a
6
+ // pretend Codex notification, for the reason docs/harnesses.md gives: the
7
+ // vocabularies are not the same shape and a fake one loses what the real one
8
+ // carries. OpenCode's `edit` approval, for instance, arrives with a whole
9
+ // unified diff in it, which neither of the other two does.
10
+ //
11
+ // EVERY SHAPE BELOW WAS READ OFF A RUNNING OPENCODE 1.18.35 ON 2026-10-07,
12
+ // not off the docs, which do not enumerate the event payloads at all. The
13
+ // ones that matter:
14
+ //
15
+ // session.updated properties.info.{cost,tokens,title,model,directory}
16
+ // message.part.updated properties.part.type: text | reasoning | tool |
17
+ // step-start | step-finish
18
+ // message.part.delta properties.{partID,field,delta} ("field":"text")
19
+ // permission.asked properties.{id,sessionID,permission,patterns,
20
+ // metadata:{command} | metadata:{filepath,diff}}
21
+ // session.idle properties.sessionID
22
+ // session.error properties.{sessionID,error}
23
+ //
24
+ // A tool part carries `state.status` of pending | running | completed | error,
25
+ // with `state.input`, `state.output` and `state.error` beside it.
26
+
27
+ /** How much streamed prose is kept before it is dropped, as the others do. */
28
+ const SAYING_CAP = 20_000;
29
+
30
+ /** Tool names worth naming on the row, in the words a person would use. */
31
+ const TOOL_WORDS = {
32
+ bash: 'Bash', edit: 'Edit', write: 'Write', read: 'Read', glob: 'Glob',
33
+ grep: 'Grep', list: 'List', webfetch: 'Fetch', websearch: 'Search',
34
+ task: 'Task', todowrite: 'Todo', patch: 'Edit', skill: 'Skill',
35
+ };
36
+
37
+ const toolWord = name => {
38
+ const id = String(name ?? '').trim();
39
+ if (!id) return null;
40
+ return TOOL_WORDS[id.toLowerCase()] ?? id;
41
+ };
42
+
43
+ const partOf = properties => (properties && typeof properties === 'object' ? properties.part ?? null : null);
44
+
45
+ /**
46
+ * WHAT THE RUN WAS, kept on the session as it goes.
47
+ *
48
+ * `sessionId` is the one OpenCode resumes by, and it is on nearly every event
49
+ * as `properties.sessionID`. It is taken from the first event that carries one
50
+ * rather than from the session-creation response, so a resumed run and a fresh
51
+ * one record it the same way.
52
+ *
53
+ * The result is the LAST assistant text of the turn. OpenCode has no single
54
+ * "here is the answer" event the way Codex's `turn/completed` does: prose
55
+ * arrives as `text` parts that are updated in place, and the final one is the
56
+ * answer. So each completed text part overwrites the last, and whatever is
57
+ * standing when the session goes idle is the result.
58
+ */
59
+ export function captureOpenCodeEvent(session, type, properties) {
60
+ try {
61
+ if (!session || typeof type !== 'string') return;
62
+ const p = properties ?? {};
63
+ if (!session.sessionId && typeof p.sessionID === 'string' && p.sessionID) session.sessionId = p.sessionID;
64
+
65
+ if (type === 'session.updated') {
66
+ const info = p.info ?? {};
67
+ if (Number.isFinite(info.cost)) session.opencodeCost = info.cost;
68
+ if (info.tokens && typeof info.tokens === 'object') session.opencodeTokens = info.tokens;
69
+ return;
70
+ }
71
+
72
+ if (type === 'session.error') {
73
+ session.result = openCodeErrorText(p.error) ?? 'OpenCode stopped with an error it did not name.';
74
+ session.resultIsError = true;
75
+ return;
76
+ }
77
+
78
+ if (type === 'message.part.updated') {
79
+ const part = partOf(p);
80
+ if (!part) return;
81
+ // A WRITE THIS RUN MADE, so the card can say what changed. The `edit` and
82
+ // `write` tools report the file they touched in `state.input.filePath`,
83
+ // which is the only place a path appears for a change OpenCode has
84
+ // already applied.
85
+ if (part.type === 'tool' && part.state?.status === 'completed') {
86
+ const path = part.state?.input?.filePath ?? part.state?.input?.path;
87
+ if (typeof path === 'string' && path && (part.tool === 'edit' || part.tool === 'write' || part.tool === 'patch')) {
88
+ session.opencodeChange ??= [];
89
+ if (!session.opencodeChange.includes(path)) session.opencodeChange.push(path);
90
+ }
91
+ }
92
+ if (part.type === 'step-finish') {
93
+ if (Number.isFinite(part.cost)) session.opencodeStepCost = part.cost;
94
+ if (part.tokens && typeof part.tokens === 'object') session.opencodeTokens = part.tokens;
95
+ }
96
+ // Only the assistant's prose is an answer. `reasoning` is the model
97
+ // thinking out loud and is never the result, which is why it is matched
98
+ // by name here rather than falling through to the text branch.
99
+ if (part.type === 'text') {
100
+ const text = String(part.text ?? '').trim();
101
+ if (text) { session.result = text; session.resultIsError = false; }
102
+ }
103
+ return;
104
+ }
105
+ } catch {
106
+ // A reader that throws kills a run that was going fine. Same rule as the
107
+ // other two harnesses: read what parses and ignore what does not.
108
+ }
109
+ }
110
+
111
+ /** The error on `session.error`, which is an object and not a string. */
112
+ export function openCodeErrorText(error) {
113
+ if (!error) return null;
114
+ if (typeof error === 'string') return error.trim() || null;
115
+ const name = typeof error.name === 'string' ? error.name : '';
116
+ const message = typeof error.data?.message === 'string' ? error.data.message
117
+ : typeof error.message === 'string' ? error.message : '';
118
+ const joined = [name, message].filter(Boolean).join(': ');
119
+ return joined || null;
120
+ }
121
+
122
+ /**
123
+ * THE PROSE AS IT IS TYPED, for the line on the row that moves.
124
+ *
125
+ * Deltas arrive as `message.part.delta` with a `field` saying which field of
126
+ * which part is growing. Only `text` is shown: `reasoning` deltas are the
127
+ * model's private thinking, and streaming those onto her row would put words
128
+ * on screen that are not what the run decided.
129
+ */
130
+ export function openCodeStreamingText(session, type, properties) {
131
+ try {
132
+ if (!session) return false;
133
+ const p = properties ?? {};
134
+
135
+ if (type === 'message.part.updated') {
136
+ const part = partOf(p);
137
+ // A tool starting means the talking stopped. What is already typed stays
138
+ // until the trace carries the same words, exactly as Codex's reader does.
139
+ if (part?.type === 'text' && !String(part.text ?? '')) { session.saying = ''; session.sayingAt = Date.now(); return true; }
140
+ return false;
141
+ }
142
+
143
+ if (type !== 'message.part.delta') return false;
144
+ if (p.field !== 'text') return false;
145
+ const piece = String(p.delta ?? '');
146
+ if (!piece) return false;
147
+ // A delta for a reasoning part has the same `field: "text"`, so the part it
148
+ // belongs to is what tells them apart. Without a remembered prose part id
149
+ // nothing is streamed, which is the quiet answer rather than the wrong one.
150
+ if (session.opencodeProsePart && p.partID && p.partID !== session.opencodeProsePart) return false;
151
+ if (!session.sayingAt) session.sayingAt = Date.now();
152
+ const next = (session.saying ?? '') + piece;
153
+ session.saying = next.length > SAYING_CAP ? '' : next;
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /** Which part the prose is going into, so reasoning deltas can be told apart. */
161
+ export function rememberOpenCodeProsePart(session, type, properties) {
162
+ if (!session || type !== 'message.part.updated') return;
163
+ const part = partOf(properties);
164
+ if (part?.type === 'text' && typeof part.id === 'string') session.opencodeProsePart = part.id;
165
+ if (part?.type === 'step-start') session.opencodeProsePart = null;
166
+ }
167
+
168
+ /** One short line for the row: what it just did. */
169
+ export function summarizeOpenCodeEvent(type, properties) {
170
+ try {
171
+ const p = properties ?? {};
172
+ if (type !== 'message.part.updated') return null;
173
+ const part = partOf(p);
174
+ if (!part) return null;
175
+ if (part.type === 'text') {
176
+ const text = String(part.text ?? '').trim();
177
+ return text ? text.slice(0, 300) : null;
178
+ }
179
+ if (part.type === 'tool' && part.state?.status === 'completed') {
180
+ const word = toolWord(part.tool);
181
+ return word ? `tool: ${word}` : null;
182
+ }
183
+ return null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /** One line for the run trace, the record she can open afterwards. */
190
+ export function traceOpenCodeEvent(type, properties) {
191
+ try {
192
+ const p = properties ?? {};
193
+ const at = new Date().toISOString().slice(11, 19);
194
+
195
+ if (type === 'session.error') {
196
+ const text = openCodeErrorText(p.error);
197
+ return text ? `${at} [error] ${text.slice(0, 300)}` : null;
198
+ }
199
+
200
+ if (type !== 'message.part.updated') return null;
201
+ const part = partOf(p);
202
+ if (!part) return null;
203
+
204
+ if (part.type === 'text') {
205
+ const text = String(part.text ?? '').trim();
206
+ return text ? `${at} ${text}` : null;
207
+ }
208
+
209
+ if (part.type === 'tool' && (part.state?.status === 'completed' || part.state?.status === 'error')) {
210
+ const word = toolWord(part.tool);
211
+ if (!word) return null;
212
+ const input = part.state?.input ?? {};
213
+ const detail = String(input.command ?? input.filePath ?? input.path ?? input.pattern ?? '').trim();
214
+ const failed = part.state?.status === 'error' ? ' (refused)' : '';
215
+ return `${at} [${word}]${failed}${detail ? ` ${detail.slice(0, 200)}` : ''}`;
216
+ }
217
+
218
+ return null;
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+
224
+ /** What it is doing right now, for the row's activity line. */
225
+ export function openCodeActivity(type, properties) {
226
+ try {
227
+ if (type !== 'message.part.updated') return null;
228
+ const part = partOf(properties);
229
+ if (part?.type !== 'tool') return null;
230
+ if (part.state?.status !== 'pending' && part.state?.status !== 'running') return null;
231
+ const word = toolWord(part.tool);
232
+ return word ? { tool: word } : null;
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * THE APPROVAL, IN THE WORDS A CARD NEEDS.
240
+ *
241
+ * `bash` asks carry `metadata.command`. `edit` asks carry `metadata.filepath`
242
+ * AND `metadata.diff`, a whole unified diff of the change it wants to make --
243
+ * measured, not assumed. That is strictly more than a Codex patch approval
244
+ * arrives with, and it means an OpenCode card can show the change itself
245
+ * rather than a filename.
246
+ */
247
+ export function openCodeApprovalCard(properties) {
248
+ const p = properties ?? {};
249
+ const kind = typeof p.permission === 'string' ? p.permission : '';
250
+ const metadata = p.metadata ?? {};
251
+ const command = typeof metadata.command === 'string' ? metadata.command.trim() : '';
252
+ const filepath = typeof metadata.filepath === 'string' ? metadata.filepath.trim() : '';
253
+ const diff = typeof metadata.diff === 'string' ? metadata.diff : '';
254
+ const patterns = Array.isArray(p.patterns) ? p.patterns.filter(x => typeof x === 'string') : [];
255
+
256
+ if (command) return { kind: kind || 'bash', title: 'Run a command', detail: command, command, patterns };
257
+ if (filepath) return { kind: kind || 'edit', title: 'Change a file', detail: filepath, filepath, diff, patterns };
258
+ return { kind: kind || 'tool', title: 'Use a tool', detail: patterns.join(', ') || kind || 'a tool', patterns };
259
+ }
260
+
261
+ /**
262
+ * THE PERMISSION POLICY A WORKER RUNS UNDER, as OpenCode's own config.
263
+ *
264
+ * This is the whole reason Agentbox can draw a card for OpenCode at all.
265
+ * Measured 2026-10-07: with no permission config the server runs shell
266
+ * commands and writes files with no card; with this config it raises
267
+ * `permission.asked` and waits. The server reports back what it loaded at
268
+ * `GET /config`, which is how the adapter can tell the policy really took.
269
+ *
270
+ * `read`, `glob`, `grep` and `list` are deliberately left alone: a card for
271
+ * every file read is the 2026-08-05 card flood again, and reading inside the
272
+ * task's own folder is what the folder is for.
273
+ */
274
+ export function openCodePermissionConfig() {
275
+ return {
276
+ $schema: 'https://opencode.ai/config.json',
277
+ permission: {
278
+ bash: 'ask',
279
+ edit: 'ask',
280
+ webfetch: 'ask',
281
+ websearch: 'ask',
282
+ external_directory: 'ask',
283
+ task: 'allow',
284
+ todowrite: 'allow',
285
+ },
286
+ };
287
+ }