agentbox-app 0.1.12 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +311 -260
- package/main/agent-updates.mjs +9 -22
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +42 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +57 -7
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +64 -16
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +94 -59
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-bin.mjs +51 -249
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +246 -506
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +112 -11
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +83 -5
- package/main/codex.mjs +95 -363
- package/main/config.mjs +37 -5
- package/main/corner-tag.mjs +810 -0
- package/main/crash-report.mjs +26 -3
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +545 -0
- package/main/engine-setup.mjs +25 -24
- package/main/feedback.mjs +14 -31
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +105 -0
- package/main/harnesses/codex.mjs +72 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +96 -0
- package/main/ipc.mjs +520 -135
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/local-app-frames.mjs +82 -0
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +248 -37
- package/main/memory-gate-history.mjs +85 -20
- package/main/memory-gate-server.mjs +129 -11
- package/main/memory-gate.mjs +62 -8
- package/main/memory-log.mjs +124 -0
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +64 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +571 -0
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +314 -0
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +18 -47
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/runs-on.mjs +32 -35
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +155 -9
- package/main/settings.mjs +364 -50
- package/main/shell-path.mjs +234 -0
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +134 -20
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +45 -11
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +227 -53
- package/main/supervisor.mjs +3865 -699
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +897 -105
- package/main/task-terminals.mjs +147 -41
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/updater.mjs +71 -9
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +365 -0
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +30 -5
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
- package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
- package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +62 -34
- package/shared/agents.d.mts +1 -0
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +45 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +36 -0
- package/shared/corner-tag.mjs +396 -0
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +76 -300
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +111 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +31 -0
- package/shared/notify-rules.mjs +55 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +4 -1
- package/shared/rank.mjs +82 -19
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -13
- package/shared/runs-on.mjs +31 -52
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +24 -10
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +484 -30
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +12 -0
- package/shared/usage.mjs +56 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +150 -29
- package/shared/work-lines.mjs +93 -245
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -48,8 +48,8 @@ export function wantsPriority(item, product, me) {
|
|
|
48
48
|
// AND NEVER OVER THE LEVEL THE SENDER PICKED (w-7ba439c883). A message now
|
|
49
49
|
// crosses carrying how urgent the person who wrote it thought it was, and a
|
|
50
50
|
// guess from here would overwrite it: the sender's line is composeItem's, so
|
|
51
|
-
// it claims no authority the fold would defend, and a
|
|
52
|
-
//
|
|
51
|
+
// it claims no authority the fold would defend, and a guess from here is not
|
|
52
|
+
// what the sender said.
|
|
53
53
|
// A row nobody chose a level for is composeItem's 0, so a number above it is
|
|
54
54
|
// exactly "somebody picked one", and `by` says it was not this Mac.
|
|
55
55
|
if (set?.by && set.by !== me && Number.isFinite(item.priority) && item.priority > 0) return false;
|
package/main/mic.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Getting the microphone with one press, for a voice note.
|
|
2
|
+
//
|
|
3
|
+
// macOS asks on behalf of the app that opened Agentbox (Terminal, when it is
|
|
4
|
+
// run from a checkout), and once that app has been refused it never shows its
|
|
5
|
+
// Allow box again: asking just answers no. So the press clears that one
|
|
6
|
+
// refusal, for that one app and the microphone only, and asks again, which
|
|
7
|
+
// brings the Allow box back. Only ever on the press, never on its own.
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
export const SETTINGS_URL = 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone';
|
|
11
|
+
|
|
12
|
+
const NAMES = {
|
|
13
|
+
'com.apple.Terminal': 'Terminal',
|
|
14
|
+
'com.googlecode.iterm2': 'iTerm',
|
|
15
|
+
'com.microsoft.VSCode': 'VS Code',
|
|
16
|
+
'dev.warp.Warp-Stable': 'Warp',
|
|
17
|
+
'com.mitchellh.ghostty': 'Ghostty',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The app macOS asks about, from the bundle id macOS gave this process. */
|
|
21
|
+
export function micOwner(env = process.env) {
|
|
22
|
+
const id = env.__CFBundleIdentifier;
|
|
23
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/.test(id)) return { id: null, name: null };
|
|
24
|
+
return { id, name: NAMES[id] ?? id.split('.').at(-1) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 'granted', or 'settings' once the Settings page has been opened instead. */
|
|
28
|
+
export async function getMic({ systemPreferences, openExternal, owner = micOwner(), run = execFile }) {
|
|
29
|
+
const status = systemPreferences.getMediaAccessStatus('microphone');
|
|
30
|
+
if (status === 'granted') return 'granted';
|
|
31
|
+
if (status === 'denied' && owner.id) {
|
|
32
|
+
await new Promise((resolve) => run('tccutil', ['reset', 'Microphone', owner.id], { timeout: 10_000 }, () => resolve()));
|
|
33
|
+
}
|
|
34
|
+
const granted = await systemPreferences.askForMediaAccess('microphone').catch(() => false);
|
|
35
|
+
if (granted) return 'granted';
|
|
36
|
+
openExternal(SETTINGS_URL);
|
|
37
|
+
return 'settings';
|
|
38
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// NEW CHATS COME IN ON THEIR OWN, when somebody ticked "Keep bringing in new
|
|
2
|
+
// chats from Claude Code and Codex" on setup's last screen or turned it on in
|
|
3
|
+
// Settings (`importNew`, from the moment `importNewSince`).
|
|
4
|
+
//
|
|
5
|
+
// A chat comes in when it was started after that moment, in a folder one of
|
|
6
|
+
// the projects points at, and has gone quiet. Claude Code may still have it
|
|
7
|
+
// open: people leave it open for hours, and a reply here to an open chat
|
|
8
|
+
// carries on a copy (Supervisor#importedThreadFor). It is filed the way setup
|
|
9
|
+
// files one: waiting on you to Needs you, finished to Done. Nothing asks about
|
|
10
|
+
// a chat first; asking rows are what the old Codex watch was removed for.
|
|
11
|
+
import { productsForThread } from '../shared/codex-import.mjs';
|
|
12
|
+
import { isShared } from '../shared/team-rules.mjs';
|
|
13
|
+
import { appSessionFolders } from './agent-homes.mjs';
|
|
14
|
+
import { outsideAgentsMode } from './settings.mjs';
|
|
15
|
+
import { claudeSessionOpen } from './claude-session-open.mjs';
|
|
16
|
+
import { readCodexThreadsOffThread } from './codex-threads.mjs';
|
|
17
|
+
import { readRecentThreads } from './importable-threads.mjs';
|
|
18
|
+
|
|
19
|
+
export const NEW_CHATS_POLL_MS = 60_000;
|
|
20
|
+
/** How long a chat has to have been still before it comes in. */
|
|
21
|
+
export const QUIET_MS = 10 * 60_000;
|
|
22
|
+
const PASS_DEADLINE_MS = 90_000;
|
|
23
|
+
|
|
24
|
+
function idsWithRows(items) {
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
for (const item of items ?? []) {
|
|
27
|
+
for (const label of item?.labels ?? []) {
|
|
28
|
+
if (typeof label !== 'string') continue;
|
|
29
|
+
if (label.startsWith('thread:')) ids.add(label.slice(7));
|
|
30
|
+
else if (label.startsWith('codex:')) ids.add(label.slice(6));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ids;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Which of `threads` come in now, grouped by the project each lands in. */
|
|
37
|
+
export function newChatsToBring(threads, { products, items, since, now, isOpen = () => false, openShownLive = false }) {
|
|
38
|
+
const had = idsWithRows(items);
|
|
39
|
+
const bySlug = new Map();
|
|
40
|
+
for (const t of threads ?? []) {
|
|
41
|
+
const id = String(t?.id ?? '').trim();
|
|
42
|
+
if (!id || had.has(id)) continue;
|
|
43
|
+
if (!(Number(t.startedAt) > since)) continue;
|
|
44
|
+
if (now - Number(t.when ?? 0) < QUIET_MS) continue;
|
|
45
|
+
if (t.source === 'codex' ? t.live : openShownLive && isOpen(t)) continue;
|
|
46
|
+
const slug = productsForThread(t, products)[0]?.slug;
|
|
47
|
+
if (!slug) continue;
|
|
48
|
+
if (!bySlug.has(slug)) bySlug.set(slug, []);
|
|
49
|
+
bySlug.get(slug).push(t);
|
|
50
|
+
had.add(id);
|
|
51
|
+
}
|
|
52
|
+
return bySlug;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function scanNewChats({ store, config, readThreads, isOpen, now = Date.now() }) {
|
|
56
|
+
if (config?.importNew !== true) return { brought: 0 };
|
|
57
|
+
const since = Number(config.importNewSince) || now;
|
|
58
|
+
// Only as far back as the switch, and never further than the import card reads.
|
|
59
|
+
const days = Math.min(10, Math.max(1, Math.ceil((now - since) / 86400e3) + 1));
|
|
60
|
+
// Never a project shared with a team: teammates would read chats nobody chose to share.
|
|
61
|
+
const products = store.listProducts().filter((p) => !isShared(p));
|
|
62
|
+
const folders = products.map((p) => p.repoPath).filter(Boolean);
|
|
63
|
+
const threads = await readThreads({ days, since, folders, now });
|
|
64
|
+
// An open session already has its own row when "Show while they run" is on.
|
|
65
|
+
const openShownLive = outsideAgentsMode(config) !== 'off';
|
|
66
|
+
const picks = newChatsToBring(threads, { products, items: store.listItems(), since, now, isOpen, openShownLive });
|
|
67
|
+
let brought = 0;
|
|
68
|
+
for (const [slug, list] of picks) {
|
|
69
|
+
brought += store.importThreadRows(slug, list, { now, land: 'sorted' }).added;
|
|
70
|
+
brought += store.importCodexThreads(slug, list, { now, land: 'sorted' }).added;
|
|
71
|
+
}
|
|
72
|
+
return { brought };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function startNewChatsWatch({
|
|
76
|
+
store, config, readThreads, isOpen,
|
|
77
|
+
everyMs = NEW_CHATS_POLL_MS, firstDelayMs = 30_000, deadlineMs = PASS_DEADLINE_MS, log = console.warn,
|
|
78
|
+
}) {
|
|
79
|
+
// One pass at a time, and a pass that never answers is given up on so the
|
|
80
|
+
// next one still runs.
|
|
81
|
+
let running = null;
|
|
82
|
+
const run = () => {
|
|
83
|
+
if (running) return running;
|
|
84
|
+
let timer = null;
|
|
85
|
+
const deadline = new Promise((_, reject) => {
|
|
86
|
+
timer = setTimeout(() => reject(new Error(`no answer in ${Math.round(deadlineMs / 1000)}s`)), deadlineMs);
|
|
87
|
+
});
|
|
88
|
+
running = Promise.race([scanNewChats({ store, config, readThreads, isOpen }), deadline])
|
|
89
|
+
.catch((e) => log('new chats watch:', e?.message ?? e))
|
|
90
|
+
.finally(() => { clearTimeout(timer); running = null; });
|
|
91
|
+
return running;
|
|
92
|
+
};
|
|
93
|
+
const first = setTimeout(run, firstDelayMs);
|
|
94
|
+
const timer = setInterval(run, everyMs);
|
|
95
|
+
return {
|
|
96
|
+
scanNow: run,
|
|
97
|
+
stop: () => { clearTimeout(first); clearInterval(timer); },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The watch as the app runs it, on the desktop (main/main.mjs) and in the browser copy (bin/agentbox.mjs). */
|
|
102
|
+
export function watchNewChatsFor({ store, config, supervisor, ...timing }) {
|
|
103
|
+
return startNewChatsWatch({
|
|
104
|
+
store,
|
|
105
|
+
config,
|
|
106
|
+
readThreads: (opts) => readRecentThreads({ ...opts, config, supervisor, readCodex: readCodexThreadsOffThread }),
|
|
107
|
+
isOpen: (t) => claudeSessionOpen(t.id, appSessionFolders({ config, supervisor }).dirs),
|
|
108
|
+
...timing,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A throwaway copy's window says so, and stands where you can see both.
|
|
2
|
+
//
|
|
3
|
+
// "Open Agentbox as a new user" (main/fresh-user.mjs) launches the app again
|
|
4
|
+
// inside a throwaway home, beside the one it was opened from. The offset is a
|
|
5
|
+
// window's shoulder rather than a tile: the point is to see there are two, with
|
|
6
|
+
// the real one still readable behind. A copy that cannot fit the offset is left
|
|
7
|
+
// centred rather than pushed half off the screen, which a 1440-wide laptop is.
|
|
8
|
+
|
|
9
|
+
/** How far down and across a second copy stands. */
|
|
10
|
+
export const OFFSET = 48;
|
|
11
|
+
|
|
12
|
+
/** The window options a copy adds, given whether it is a throwaway one. */
|
|
13
|
+
export function newUserWindow({ fresh, name, screen, width = 1440, height = 900 } = {}) {
|
|
14
|
+
if (!fresh) return { title: name };
|
|
15
|
+
const title = `${name} (new user)`;
|
|
16
|
+
if (!screen) return { title };
|
|
17
|
+
const centred = {
|
|
18
|
+
x: Math.round(screen.x + (screen.width - width) / 2),
|
|
19
|
+
y: Math.round(screen.y + (screen.height - height) / 2),
|
|
20
|
+
};
|
|
21
|
+
const x = centred.x + OFFSET;
|
|
22
|
+
const y = centred.y + OFFSET;
|
|
23
|
+
if (x + width > screen.x + screen.width || y + height > screen.y + screen.height) return { title };
|
|
24
|
+
return { title, x, y };
|
|
25
|
+
}
|
package/main/notify.mjs
CHANGED
|
@@ -13,19 +13,54 @@
|
|
|
13
13
|
// AND ONCE, NOT TWENTY TIMES. One at a time is not once. `spoke` below is the
|
|
14
14
|
// fix, and shared/notify-rules.mjs carries the rule.
|
|
15
15
|
|
|
16
|
+
import path from 'node:path';
|
|
16
17
|
import { atTheApp, banner, shouldSpeak } from '../shared/notify-rules.mjs';
|
|
17
18
|
import { NAME } from '../shared/product-name.mjs';
|
|
18
19
|
|
|
20
|
+
// THE MARK THE BANNER WEARS, and why it is a png and not the .icns every other
|
|
21
|
+
// part of this repo uses.
|
|
22
|
+
//
|
|
23
|
+
// macOS draws the app's OWN bundle icon down the left of a banner, and no app
|
|
24
|
+
// can override that. From a source checkout the bundle being asked is the
|
|
25
|
+
// Electron.app in node_modules, which scripts/brand-electron.mjs renames and
|
|
26
|
+
// re-icons, and macOS still drew a generic grey square: "it shows a plain
|
|
27
|
+
// placeholder", looking at a real one, 2026-10-07.
|
|
28
|
+
//
|
|
29
|
+
// An image handed to the notification is the half we DO control; macOS attaches
|
|
30
|
+
// it to the banner. Measured the same day in the Electron the app runs from:
|
|
31
|
+
// nativeImage.createFromPath on build/icon.icns comes back empty, 0x0, so the
|
|
32
|
+
// icns is not a candidate however convenient it would be. The png beside it,
|
|
33
|
+
// drawn from the same art, reads 256x256.
|
|
34
|
+
//
|
|
35
|
+
// AN EMPTY IMAGE IS NOT A MARK. createFromPath answers a missing or unreadable
|
|
36
|
+
// file with an empty image rather than an error, so "it returned something" is
|
|
37
|
+
// the wrong question, and handing that something to macOS is how this goes
|
|
38
|
+
// quietly back to a grey square. Returns null when there is nothing to show,
|
|
39
|
+
// and the banner then looks exactly as it did before any of this.
|
|
40
|
+
export const MARK_FILE = path.join('build', 'notification-icon.png');
|
|
41
|
+
|
|
42
|
+
export function markFor({ root, createFromPath }) {
|
|
43
|
+
try {
|
|
44
|
+
const img = createFromPath(path.join(root, MARK_FILE));
|
|
45
|
+
return img && !img.isEmpty() ? img : null;
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
19
49
|
// A BURST IS ONE PIECE OF NEWS. Four workers finishing inside the same
|
|
20
50
|
// supervisor tick is the ordinary case, and it arrives as four pushes over a
|
|
21
51
|
// second or two. Waiting a beat before speaking turns that into one banner
|
|
22
52
|
// with one sound instead of four rewrites and four chimes.
|
|
23
53
|
const COALESCE_MS = 1500;
|
|
24
54
|
|
|
25
|
-
|
|
55
|
+
// `onChange` hears that what it has told you about changed: the tray counts
|
|
56
|
+
// all of it (main/corner-tag.mjs), so a banner never speaks for a thread the
|
|
57
|
+
// tray leaves out.
|
|
58
|
+
export function createNotifier({ window, Notification, powerMonitor, icon = null, coalesceMs = COALESCE_MS, onChange = () => {} }) {
|
|
26
59
|
// Everything she has not seen since she last had the window, keyed by id so
|
|
27
60
|
// a repeated push is idempotent. Insertion order is arrival order.
|
|
28
61
|
const unseen = new Map();
|
|
62
|
+
const toldAt = new Map();
|
|
63
|
+
const changed = () => { try { onChange(); } catch { /* the tray catches up on its next look */ } };
|
|
29
64
|
let live = null; // the one notification currently on screen, if any
|
|
30
65
|
let timer = null;
|
|
31
66
|
// HOW MANY TIMES SHE HAS BEEN INTERRUPTED SINCE SHE LAST HAD THE WINDOW.
|
|
@@ -53,11 +88,14 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
|
|
|
53
88
|
// dropped rather than banked: showing them later, when she steps away, would
|
|
54
89
|
// be telling her about work she already watched arrive.
|
|
55
90
|
const seen = () => {
|
|
91
|
+
const had = unseen.size > 0;
|
|
56
92
|
unseen.clear();
|
|
93
|
+
toldAt.clear();
|
|
57
94
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
58
95
|
spoke = false;
|
|
59
96
|
spokeAsk = false;
|
|
60
97
|
takeDown();
|
|
98
|
+
if (had) changed();
|
|
61
99
|
};
|
|
62
100
|
|
|
63
101
|
const speak = () => {
|
|
@@ -81,6 +119,9 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
|
|
|
81
119
|
title: say.title,
|
|
82
120
|
body: say.body,
|
|
83
121
|
silent: false,
|
|
122
|
+
// Omitted rather than passed empty when there is nothing to show, so a
|
|
123
|
+
// banner with no mark is byte for byte the banner that shipped before.
|
|
124
|
+
...(icon ? { icon } : {}),
|
|
84
125
|
});
|
|
85
126
|
} catch { return; }
|
|
86
127
|
// A banner she can act on. Clicking it brings Agentbox forward and opens the
|
|
@@ -123,33 +164,51 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
|
|
|
123
164
|
// Asked before banking anything, so that a burst arriving while she is
|
|
124
165
|
// at the app is dropped rather than saved up for when she leaves.
|
|
125
166
|
if (atTheApp({ focused: focused(), idleMs: idleMs() })) return seen();
|
|
167
|
+
let banked = false;
|
|
126
168
|
for (const a of arrivals) {
|
|
127
169
|
if (!a || !a.id || unseen.has(a.id)) continue;
|
|
128
170
|
unseen.set(a.id, a);
|
|
171
|
+
toldAt.set(a.id, Date.now());
|
|
172
|
+
banked = true;
|
|
129
173
|
}
|
|
174
|
+
if (banked) changed();
|
|
130
175
|
if (!unseen.size || timer) return;
|
|
131
176
|
timer = setTimeout(speak, coalesceMs);
|
|
132
177
|
},
|
|
133
178
|
// The window came forward. Whatever was on screen is now behind her.
|
|
134
179
|
seen,
|
|
180
|
+
// What it has told you about, or is about to, since you were last at the
|
|
181
|
+
// app, and when each arrived.
|
|
182
|
+
told: () => [...toldAt].map(([id, at]) => ({ id, at })),
|
|
135
183
|
// Tests, and nothing else.
|
|
136
184
|
_pending: () => [...unseen.values()],
|
|
137
185
|
_spoken: () => ({ spoke, spokeAsk }),
|
|
138
186
|
};
|
|
139
187
|
}
|
|
140
188
|
|
|
141
|
-
|
|
189
|
+
// `onArrivals` hears every arrival too, with the inbox they arrived in, for
|
|
190
|
+
// the phone's alerts (main/phone-alerts.mjs), which make their own call.
|
|
191
|
+
const needsYouOf = (payload) => (Array.isArray(payload.needsYou) ? payload.needsYou : []);
|
|
192
|
+
export function installNotifier({ app, window, Notification, powerMonitor, ipcMain, nativeImage, onArrivals = () => {}, onChange = () => {} }) {
|
|
142
193
|
if (Notification.isSupported && !Notification.isSupported()) {
|
|
143
|
-
|
|
194
|
+
ipcMain.handle('zero:notify', (_e, payload = {}) => { try { onArrivals(Array.isArray(payload.arrivals) ? payload.arrivals : [], needsYouOf(payload)); } catch {} });
|
|
195
|
+
return { add() {}, seen() {}, told: () => [] };
|
|
144
196
|
}
|
|
145
|
-
|
|
197
|
+
// app.getAppPath() is the repo from source and the asar inside the bundle
|
|
198
|
+
// when it is packaged, and the mark sits at the same place in both.
|
|
199
|
+
const icon = nativeImage
|
|
200
|
+
? markFor({ root: app.getAppPath(), createFromPath: (p) => nativeImage.createFromPath(p) })
|
|
201
|
+
: null;
|
|
202
|
+
const notifier = createNotifier({ window, Notification, powerMonitor, icon, onChange });
|
|
146
203
|
window.on('focus', () => notifier.seen());
|
|
147
204
|
// Unlocking with Agentbox already the front app never fires 'focus' (it never
|
|
148
205
|
// lost it), so without this the banner that fired while the screen was
|
|
149
206
|
// locked would sit in Notification Center after she is back at the row.
|
|
150
207
|
try { powerMonitor.on('unlock-screen', () => { if (window.isFocused()) notifier.seen(); }); } catch {}
|
|
151
208
|
ipcMain.handle('zero:notify', (_e, payload = {}) => {
|
|
152
|
-
|
|
209
|
+
const arrivals = Array.isArray(payload.arrivals) ? payload.arrivals : [];
|
|
210
|
+
try { onArrivals(arrivals, needsYouOf(payload)); } catch {}
|
|
211
|
+
notifier.add(arrivals);
|
|
153
212
|
});
|
|
154
213
|
return notifier;
|
|
155
214
|
}
|
|
@@ -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
|
+
}
|