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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// WHAT A RUN CHANGED: the checkout read back against its photograph, then the
|
|
2
|
+
// change file written. Git commands and a whole transcript, so it runs on the
|
|
3
|
+
// git thread (main/git-change-offthread.mjs), not on the app's main thread.
|
|
4
|
+
import { changeFromRepo } from './git-change.mjs';
|
|
5
|
+
import { writeChangeForRun } from './code-change.mjs';
|
|
6
|
+
|
|
7
|
+
export function saveRunChange({ repoBefore = null, ...run }) {
|
|
8
|
+
let repos = [];
|
|
9
|
+
try {
|
|
10
|
+
if (repoBefore) repos = [changeFromRepo(repoBefore)];
|
|
11
|
+
} catch (e) { console.warn('zero: could not read the repository back:', e.message); }
|
|
12
|
+
return writeChangeForRun({ ...run, repos });
|
|
13
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// THE RUN LEDGER: one line per finished run, for agent hours (w-3d0fa23ffb).
|
|
2
|
+
//
|
|
3
|
+
// `<appHome>/runs.jsonl`, written when a run ends (supervisor `_recordRun`).
|
|
4
|
+
// Cloud runs keep their own ledger (main/cloud/usage.mjs) and are read from
|
|
5
|
+
// there. Old runs are read once from their trace logs (`backfillRuns`). Every
|
|
6
|
+
// line has a key, and every reader dedupes on it, so a run written twice is
|
|
7
|
+
// counted once.
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { appHome, machineryDir } from './store/home.mjs';
|
|
11
|
+
import { dedupeRuns, runFromTrace } from '../shared/agent-hours.mjs';
|
|
12
|
+
|
|
13
|
+
const BACKFILL_DAYS = 400;
|
|
14
|
+
const TAIL_BYTES = 400;
|
|
15
|
+
// A log with no exit line that was written to this recently is a run still
|
|
16
|
+
// going; it writes its own line when it ends.
|
|
17
|
+
const STILL_GOING_MS = 30 * 60_000;
|
|
18
|
+
|
|
19
|
+
export function runLedgerPath(home = appHome()) {
|
|
20
|
+
return path.join(home, 'runs.jsonl');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function appendRun(run, { file = runLedgerPath() } = {}) {
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
+
fs.appendFileSync(file, JSON.stringify(run) + '\n');
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
// A run that cannot be written never fails the run.
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readRuns({ file = runLedgerPath() } = {}) {
|
|
35
|
+
let text;
|
|
36
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const line of text.split('\n')) {
|
|
39
|
+
if (!line.trim()) continue;
|
|
40
|
+
try { out.push(JSON.parse(line)); } catch { /* one broken line loses only itself */ }
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function runsFromCloudUsage(lines) {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const l of lines ?? []) {
|
|
48
|
+
const at = Date.parse(l?.at);
|
|
49
|
+
const seconds = Number(l?.wallSeconds);
|
|
50
|
+
if (!Number.isFinite(at) || !(seconds > 0)) continue;
|
|
51
|
+
out.push({
|
|
52
|
+
key: `cloud:${l.runId ?? at}`,
|
|
53
|
+
at,
|
|
54
|
+
start: at - seconds * 1000,
|
|
55
|
+
item: l.item ?? null,
|
|
56
|
+
product: null,
|
|
57
|
+
engine: l.engine ?? null,
|
|
58
|
+
seconds,
|
|
59
|
+
failed: Number.isInteger(l.exitCode) ? l.exitCode !== 0 : null,
|
|
60
|
+
where: 'cloud',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function allRuns({ file = runLedgerPath(), cloudLines = [] } = {}) {
|
|
67
|
+
return dedupeRuns([...readRuns({ file }), ...runsFromCloudUsage(cloudLines)]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tailOf(file) {
|
|
71
|
+
let fd;
|
|
72
|
+
try {
|
|
73
|
+
fd = fs.openSync(file, 'r');
|
|
74
|
+
const size = fs.fstatSync(fd).size;
|
|
75
|
+
const len = Math.min(size, TAIL_BYTES);
|
|
76
|
+
const buf = Buffer.alloc(len);
|
|
77
|
+
fs.readSync(fd, buf, 0, len, size - len);
|
|
78
|
+
return buf.toString('utf8');
|
|
79
|
+
} catch {
|
|
80
|
+
return '';
|
|
81
|
+
} finally {
|
|
82
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch {}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Reads every run log of this store's projects into the ledger, once per run.
|
|
87
|
+
// Resolves to how many runs it added. It hands the event loop back every
|
|
88
|
+
// YIELD_EVERY logs: on the founder's Mac it reads 25,780 logs in 3.4 s, which
|
|
89
|
+
// in one piece would freeze the window.
|
|
90
|
+
const YIELD_EVERY = 200;
|
|
91
|
+
const breathe = () => new Promise((r) => setImmediate(r));
|
|
92
|
+
|
|
93
|
+
export async function backfillRuns({ home = appHome(), products = [], file = runLedgerPath(home), now = Date.now() } = {}) {
|
|
94
|
+
const have = new Set(readRuns({ file }).map((r) => r?.key));
|
|
95
|
+
const since = now - BACKFILL_DAYS * 86_400_000;
|
|
96
|
+
let added = 0;
|
|
97
|
+
let read = 0;
|
|
98
|
+
let pending = [];
|
|
99
|
+
const flush = () => {
|
|
100
|
+
if (!pending.length) return;
|
|
101
|
+
try {
|
|
102
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
103
|
+
fs.appendFileSync(file, pending.map((r) => JSON.stringify(r)).join('\n') + '\n');
|
|
104
|
+
added += pending.length;
|
|
105
|
+
} catch { /* the next backfill tries again */ }
|
|
106
|
+
pending = [];
|
|
107
|
+
};
|
|
108
|
+
for (const p of products) {
|
|
109
|
+
if (!p?.dir) continue;
|
|
110
|
+
const sessions = path.join(machineryDir(p.dir, home), 'sessions');
|
|
111
|
+
let items;
|
|
112
|
+
try { items = fs.readdirSync(sessions, { withFileTypes: true }); } catch { continue; }
|
|
113
|
+
for (const it of items) {
|
|
114
|
+
if (!it.isDirectory()) continue;
|
|
115
|
+
let logs;
|
|
116
|
+
try { logs = fs.readdirSync(path.join(sessions, it.name)); } catch { continue; }
|
|
117
|
+
for (const name of logs) {
|
|
118
|
+
const start = Number(/^(\d{12,14})\.log$/.exec(name)?.[1]);
|
|
119
|
+
if (!Number.isFinite(start) || start < since || start > now) continue;
|
|
120
|
+
if (++read % YIELD_EVERY === 0) { flush(); await breathe(); }
|
|
121
|
+
const full = path.join(sessions, it.name, name);
|
|
122
|
+
let mtimeMs;
|
|
123
|
+
try { mtimeMs = fs.statSync(full).mtimeMs; } catch { continue; }
|
|
124
|
+
const tail = tailOf(full);
|
|
125
|
+
if (!/# exited \(/.test(tail) && now - mtimeMs < STILL_GOING_MS) continue;
|
|
126
|
+
const run = runFromTrace({ name, text: tail, mtimeMs, item: it.name, product: p.slug ?? null });
|
|
127
|
+
if (!run || have.has(run.key)) continue;
|
|
128
|
+
pending.push({ ...run, backfilled: true });
|
|
129
|
+
have.add(run.key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
flush();
|
|
134
|
+
return added;
|
|
135
|
+
}
|
package/main/runs-on.mjs
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
//
|
|
1
|
+
// WHETHER THIS MAC IS ALREADY SIGNED INTO A CODING AGENT, AND WHICH ONE.
|
|
2
2
|
//
|
|
3
|
-
// w-e217e577e5, 2026-10-07
|
|
4
|
-
//
|
|
3
|
+
// w-e217e577e5, 2026-10-07. A new user ran an agent and, in her words, was
|
|
4
|
+
// "like, 'Oh my God, where are the tokens coming from? I didn't even connect my
|
|
5
|
+
// account.'" The app finds whichever coding agent is already signed in on the
|
|
6
|
+
// Mac and spends that subscription without ever saying so.
|
|
5
7
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// to
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// WHICH ENGINE, AND NOTHING ELSE. This read the plan tier too for a few hours,
|
|
9
|
+
// so a row in the sidebar's foot could say "Claude · Max 20x"; that row came
|
|
10
|
+
// out the same day and the tier came out with it ("very critically, I want to
|
|
11
|
+
// get rid of that Claude Max 20x plan"). The question the walk answers is "did
|
|
12
|
+
// it connect to something of mine", and the engine is the whole of that answer.
|
|
13
|
+
// What somebody is paying for is on each agent's own page in Settings, which is
|
|
14
|
+
// where you go when you actually want it (main/settings.mjs reads
|
|
15
|
+
// main/claude-plan.mjs and main/codex-account.mjs for exactly that).
|
|
13
16
|
//
|
|
14
|
-
// NULL IS AN ORDINARY ANSWER AND IT MEANS
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// is read by somebody deciding whose money this is spending.
|
|
17
|
+
// NULL IS AN ORDINARY ANSWER AND IT MEANS SAY NOTHING. A Mac mid-setup, a config
|
|
18
|
+
// file being rewritten under us, a login that is not there: all null, and the
|
|
19
|
+
// walk then says nothing at all rather than guessing. That Mac is also the one
|
|
20
|
+
// that gets the plan question instead, which says all of this out loud already.
|
|
19
21
|
//
|
|
20
22
|
// IT IS CACHED ON THE LOGIN FILES' OWN TIMES, NOT ON A CLOCK. The snapshot is
|
|
21
23
|
// built every ten seconds and `~/.claude.json` is 148 KB on the machine this was
|
|
@@ -30,11 +32,11 @@
|
|
|
30
32
|
import fs from 'node:fs';
|
|
31
33
|
import os from 'node:os';
|
|
32
34
|
import path from 'node:path';
|
|
33
|
-
import { planFile
|
|
35
|
+
import { planFile } from './claude-plan.mjs';
|
|
34
36
|
import { codexAccount, codexAuthFile } from './codex-account.mjs';
|
|
35
37
|
import { signInFiles, signInStamp } from './sign-in-files.mjs';
|
|
36
38
|
|
|
37
|
-
/** keyed by engine and folder, valued by the stamp it was read at. */
|
|
39
|
+
/** keyed by engine, home and folder, valued by the stamp it was read at. */
|
|
38
40
|
const remembered = new Map();
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -43,12 +45,11 @@ const remembered = new Map();
|
|
|
43
45
|
* @param {object} facts
|
|
44
46
|
* @param {string} facts.engine Which coding agent the workspace runs on, which
|
|
45
47
|
* the supervisor has already decided (`Supervisor#engineFacts`). Never worked
|
|
46
|
-
* out here: the capability gate lives there, and a
|
|
48
|
+
* out here: the capability gate lives there, and a line that guessed could
|
|
47
49
|
* name the subscription the fleet is not spending.
|
|
48
50
|
* @param {string} [facts.claudeProfile] The Claude login, `default` or a folder.
|
|
49
51
|
* @param {string} [facts.codexHome] The CODEX_HOME this workspace runs on.
|
|
50
|
-
* @returns {{ engine: string
|
|
51
|
-
* tool itself reports, never a label of ours.
|
|
52
|
+
* @returns {{ engine: string }|null}
|
|
52
53
|
*/
|
|
53
54
|
export function runsOn({ engine = null, home = os.homedir(), claudeProfile = 'default', codexHome = null } = {}) {
|
|
54
55
|
if (engine !== 'claude' && engine !== 'codex') return null;
|
|
@@ -63,28 +64,24 @@ export function runsOn({ engine = null, home = os.homedir(), claudeProfile = 'de
|
|
|
63
64
|
const stamp = signInStamp(signInFiles({ engine, folder, home }));
|
|
64
65
|
const seen = remembered.get(key);
|
|
65
66
|
if (seen && seen.stamp === stamp) return seen.value;
|
|
66
|
-
const value = stamp
|
|
67
|
+
const value = stamp && signedIn(engine, { home, claudeProfile, folder }) ? { engine } : null;
|
|
67
68
|
remembered.set(key, { stamp, value });
|
|
68
69
|
return value;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// different screens: the first draws nothing, the second draws the name.
|
|
79
|
-
let account = null;
|
|
72
|
+
/** Whether there is a login in there at all. Never throws, never guesses. */
|
|
73
|
+
function signedIn(engine, { home, claudeProfile, folder }) {
|
|
74
|
+
if (engine === 'codex') return !!codexAccount(null, { file: codexAuthFile(folder) });
|
|
75
|
+
// `oauthAccount` is the object Claude Code writes when somebody signs in, and
|
|
76
|
+
// its absence is the honest "nobody is". A file with no account in it is an
|
|
77
|
+
// ordinary thing: `~/.claude.json` also holds project history, so it exists on
|
|
78
|
+
// plenty of Macs nobody has logged in on.
|
|
80
79
|
try {
|
|
81
|
-
account = JSON.parse(fs.readFileSync(planFile(claudeProfile, home), 'utf8'))?.oauthAccount
|
|
80
|
+
const account = JSON.parse(fs.readFileSync(planFile(claudeProfile, home), 'utf8'))?.oauthAccount;
|
|
81
|
+
return !!account && typeof account === 'object';
|
|
82
82
|
} catch {
|
|
83
|
-
return
|
|
83
|
+
return false;
|
|
84
84
|
}
|
|
85
|
-
if (!account || typeof account !== 'object') return null;
|
|
86
|
-
const plan = planOfAccount(account);
|
|
87
|
-
return { engine: 'claude', plan: plan.known ? plan.label : null };
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
/** Forgets every cached reading. For tests, and for a sign-out that lands. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SAVING THE FILES A MESSAGE CARRIES, into a project's attachments folder.
|
|
2
|
+
//
|
|
3
|
+
// One copy, used by the reply box (main/ipc.mjs zero:save-attachments) and by
|
|
4
|
+
// a message to a person (main/team/index.mjs), whose conversation may not
|
|
5
|
+
// exist until the message makes it, so it cannot save them beforehand.
|
|
6
|
+
// Relative paths keep them readable by workers through the MCP and renderable
|
|
7
|
+
// in the message view, and they are the names the team sync sends on
|
|
8
|
+
// (main/team/sync.mjs).
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `files` are `{ name, dataBase64 }` or `{ name, srcPath }`. Answers what was
|
|
14
|
+
* saved: `{ rel, name, image }`, in order, skipping one with neither.
|
|
15
|
+
*/
|
|
16
|
+
export function saveAttachments(dir, files) {
|
|
17
|
+
const outDir = path.join(dir, 'attachments');
|
|
18
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
19
|
+
const saved = [];
|
|
20
|
+
for (const file of files ?? []) {
|
|
21
|
+
const safe = String(file?.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
|
|
22
|
+
// A RANDOM SUFFIX, THE SAME ONE THE STAGING PATH ALREADY USES. The name
|
|
23
|
+
// was the millisecond plus the filename, so two files saved in the same
|
|
24
|
+
// millisecond with the same name overwrote each other and the message
|
|
25
|
+
// ended up with two links to one picture. Screenshots are often sent in
|
|
26
|
+
// pairs, which is exactly the shape that collides. It has never been observed
|
|
27
|
+
// happening; it is one line to make impossible.
|
|
28
|
+
const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
|
|
29
|
+
const abs = path.join(dir, rel);
|
|
30
|
+
if (file?.dataBase64) fs.writeFileSync(abs, Buffer.from(file.dataBase64, 'base64'));
|
|
31
|
+
else if (file?.srcPath) fs.copyFileSync(file.srcPath, abs);
|
|
32
|
+
else continue;
|
|
33
|
+
saved.push({ rel, name: safe, image: /\.(png|jpe?g|gif|webp)$/i.test(safe) });
|
|
34
|
+
}
|
|
35
|
+
return saved;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The links a message carries for what was saved, one per line. */
|
|
39
|
+
export function attachmentMarkdown(saved) {
|
|
40
|
+
return saved.map((s) => (s.image ? `` : `[${s.name}](${s.rel})`)).join('\n');
|
|
41
|
+
}
|
package/main/serve.mjs
CHANGED
|
@@ -29,6 +29,9 @@ import crypto from 'node:crypto';
|
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
|
|
31
31
|
import { spawn } from 'node:child_process';
|
|
32
|
+
import { Readable } from 'node:stream';
|
|
33
|
+
import { mediaResponse, mediaType, servable } from './img-scheme.mjs';
|
|
34
|
+
import { readPictureShape } from './picture-shape.mjs';
|
|
32
35
|
import { loadConfig } from './config.mjs';
|
|
33
36
|
import { Store } from './store.mjs';
|
|
34
37
|
import { Supervisor } from './supervisor.mjs';
|
|
@@ -38,7 +41,9 @@ import { reportFromRenderer } from './crash-report.mjs';
|
|
|
38
41
|
import * as workItemsDisk from './store/work-items.mjs';
|
|
39
42
|
import { createTeamService, teamStateFile } from './team/index.mjs';
|
|
40
43
|
import { loadCloudConfig, supabaseSession, headlessSessionFile } from './team/session.mjs';
|
|
44
|
+
import { roomsSetup } from './team/room-setup.mjs';
|
|
41
45
|
import { appHome, carryMisplacedMachinery, storeRootEnv } from './store/home.mjs';
|
|
46
|
+
import { keepAnswerModesIn } from './answer-modes.mjs';
|
|
42
47
|
|
|
43
48
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
44
49
|
const repoRoot = path.dirname(here);
|
|
@@ -136,11 +141,16 @@ function readVersion() {
|
|
|
136
141
|
* same scrubbed path as the desktop's, which writes nothing until crash
|
|
137
142
|
* reports are set up for this process.
|
|
138
143
|
*/
|
|
139
|
-
function answerWhatOnlyTheWindowAnswered(ipcMain) {
|
|
144
|
+
export function answerWhatOnlyTheWindowAnswered(ipcMain) {
|
|
140
145
|
ipcMain.handle('zero:badge', () => null);
|
|
146
|
+
ipcMain.handle('zero:corner-tag', () => null);
|
|
141
147
|
ipcMain.handle('zero:notify', () => null);
|
|
142
148
|
ipcMain.handle('zero:boot-info', () => ({ reloaded: false, builtAt: null, recovered: null }));
|
|
149
|
+
// A room invite link only ever opens the desktop app.
|
|
150
|
+
ipcMain.handle('zero:take-join', () => null);
|
|
143
151
|
ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); return null; });
|
|
152
|
+
// The phone door belongs to the desktop app (main/phone-link.mjs); a tab is told there is none.
|
|
153
|
+
ipcMain.handle('zero:phone-link', () => null);
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
/**
|
|
@@ -170,6 +180,7 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
|
|
|
170
180
|
const inheritedHome = appHome();
|
|
171
181
|
Object.assign(process.env, storeRootEnv(config.storeRoot));
|
|
172
182
|
carryMisplacedMachinery(inheritedHome, config.storeRoot);
|
|
183
|
+
keepAnswerModesIn({ userDir, storeRoot: config.storeRoot });
|
|
173
184
|
const store = await new Store(config).init();
|
|
174
185
|
const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
|
|
175
186
|
const window = broadcastingWindow();
|
|
@@ -191,7 +202,15 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
|
|
|
191
202
|
accountRoot: config.accountRoot,
|
|
192
203
|
stateFile: teamStateFile(config.storeRoot),
|
|
193
204
|
onChange: () => ipc?.push?.(),
|
|
205
|
+
rooms: roomsSetup({ config, store, supervisor, userDir }),
|
|
194
206
|
}) : null;
|
|
207
|
+
// AND IT STOPS ITS OWN WORKERS ON THE WAY OUT, as main/main.mjs does for the
|
|
208
|
+
// desktop. Control-C reaches the whole process group, so without this each
|
|
209
|
+
// worker died of the signal itself and was written down as a run that failed
|
|
210
|
+
// on its own: a Claude task stopped that way read "it will keep failing the
|
|
211
|
+
// same way until this is fixed" and rested 15 minutes after the copy came
|
|
212
|
+
// back (tests/control-c-on-the-browser-copy-does-not-fail-its-agents).
|
|
213
|
+
host.app.on('before-quit', () => supervisor.killAll());
|
|
195
214
|
ipc = registerIpc({ store, supervisor, config, window, host, team });
|
|
196
215
|
answerWhatOnlyTheWindowAnswered(host.ipcMain);
|
|
197
216
|
return { config, store, supervisor, window, host, ipc, team, channels: host.ipcMain.handlers };
|
|
@@ -200,18 +219,98 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
|
|
|
200
219
|
/** A token in the url, because the port alone is not a door key. */
|
|
201
220
|
export const newToken = () => crypto.randomBytes(24).toString('base64url');
|
|
202
221
|
|
|
222
|
+
/**
|
|
223
|
+
* THE PICTURES A TAB MAY DRAW: the desktop window's own rule (`servable`,
|
|
224
|
+
* main/img-scheme.mjs) over its own roots (`imageRoots`, main/main.mjs), so a
|
|
225
|
+
* door opened for a screenshot can never hand out `zero.config.json`.
|
|
226
|
+
*
|
|
227
|
+
* Pictures only. A whole document is served under a grant the pane opens one
|
|
228
|
+
* at a time (main/doc-scheme.mjs) and this copy opens none.
|
|
229
|
+
*/
|
|
230
|
+
export function picturesTheAppOwns({ config, store }) {
|
|
231
|
+
return {
|
|
232
|
+
allows: (kind, file) => kind === 'img' && servable(file, [
|
|
233
|
+
config?.accountRoot,
|
|
234
|
+
...(store?.listProducts?.() ?? []).map((p) => p.repoPath),
|
|
235
|
+
].filter(Boolean)),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* THE KEY THAT ONLY READS FILES. A design page runs its own scripts and can
|
|
241
|
+
* read its own address, so the address a phone loads it from must not carry
|
|
242
|
+
* the phone's key, which starts agents and approves their requests. This is
|
|
243
|
+
* made from that key, changes when it is reset, and opens nothing but /f/.
|
|
244
|
+
*/
|
|
245
|
+
export const fileKey = (token) => crypto.createHmac('sha256', String(token ?? '')).update('agentbox-files').digest('base64url').slice(0, 32);
|
|
246
|
+
|
|
203
247
|
const MIME = {
|
|
204
248
|
'.html': 'text/html; charset=utf-8',
|
|
249
|
+
'.htm': 'text/html; charset=utf-8',
|
|
205
250
|
'.js': 'text/javascript; charset=utf-8',
|
|
251
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
206
252
|
'.css': 'text/css; charset=utf-8',
|
|
207
253
|
'.json': 'application/json; charset=utf-8',
|
|
208
254
|
'.svg': 'image/svg+xml',
|
|
209
255
|
'.png': 'image/png',
|
|
256
|
+
'.jpg': 'image/jpeg',
|
|
257
|
+
'.jpeg': 'image/jpeg',
|
|
258
|
+
'.gif': 'image/gif',
|
|
210
259
|
'.webp': 'image/webp',
|
|
260
|
+
'.avif': 'image/avif',
|
|
261
|
+
'.bmp': 'image/bmp',
|
|
262
|
+
'.ico': 'image/x-icon',
|
|
263
|
+
'.pdf': 'application/pdf',
|
|
264
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
265
|
+
'.md': 'text/plain; charset=utf-8',
|
|
211
266
|
'.woff2': 'font/woff2',
|
|
267
|
+
'.woff': 'font/woff',
|
|
268
|
+
'.ttf': 'font/ttf',
|
|
212
269
|
'.map': 'application/json; charset=utf-8',
|
|
213
270
|
};
|
|
214
271
|
|
|
272
|
+
/**
|
|
273
|
+
* One file off this computer, for the phone: /f/<file key>/<doc|img>/<path>.
|
|
274
|
+
* `files.allows(kind, file)` is the same rule the desktop's two schemes use
|
|
275
|
+
* (main/main.mjs passes it in), checked after the path is made whole, so a
|
|
276
|
+
* `..` cannot climb out of what it allows. Never from memory, for the reason
|
|
277
|
+
* the desktop's schemes give: a picture an agent redraws keeps its name.
|
|
278
|
+
*/
|
|
279
|
+
async function answerFile(req, res, url, { token, files }) {
|
|
280
|
+
const [, , key, kind, ...rest] = url.pathname.split('/');
|
|
281
|
+
if (kind !== 'doc' && kind !== 'img' && kind !== 'shape') { res.writeHead(404).end('No such kind of file.'); return; }
|
|
282
|
+
if (!sameKey(key, fileKey(typeof token === 'function' ? token() : token))) { res.writeHead(403).end('Wrong key.'); return; }
|
|
283
|
+
let file;
|
|
284
|
+
try { file = path.normalize('/' + rest.map(decodeURIComponent).join('/')); } catch { res.writeHead(400).end('Bad path.'); return; }
|
|
285
|
+
// A shape is asked of a picture, under the picture's own rule.
|
|
286
|
+
if (!path.isAbsolute(file) || !files.allows(kind === 'shape' ? 'img' : kind, file)) { res.writeHead(403).end('Not available.'); return; }
|
|
287
|
+
let stat;
|
|
288
|
+
try { stat = await fs.promises.stat(file); } catch { res.writeHead(404).end('Not there.'); return; }
|
|
289
|
+
if (!stat.isFile()) { res.writeHead(404).end('Not there.'); return; }
|
|
290
|
+
// ITS WIDTH AND HEIGHT, so a picture the phone has never seen holds its
|
|
291
|
+
// place before its last byte comes (main/picture-shape.mjs).
|
|
292
|
+
if (kind === 'shape') {
|
|
293
|
+
const shape = await readPictureShape(file).catch(() => null);
|
|
294
|
+
if (!shape) { res.writeHead(404).end('Not a picture this can measure.'); return; }
|
|
295
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
296
|
+
res.end(JSON.stringify(shape));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const media = mediaType(file);
|
|
300
|
+
if (media) {
|
|
301
|
+
const out = await mediaResponse(file, req.headers.range);
|
|
302
|
+
res.writeHead(out.status, { ...Object.fromEntries(out.headers), 'cache-control': 'no-store' });
|
|
303
|
+
if (out.body) Readable.fromWeb(out.body).pipe(res); else res.end();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
res.writeHead(200, {
|
|
307
|
+
'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
|
|
308
|
+
'content-length': String(stat.size),
|
|
309
|
+
'cache-control': 'no-store',
|
|
310
|
+
});
|
|
311
|
+
fs.createReadStream(file).pipe(res);
|
|
312
|
+
}
|
|
313
|
+
|
|
215
314
|
/**
|
|
216
315
|
* Serve one built renderer and one channel endpoint.
|
|
217
316
|
*
|
|
@@ -222,10 +321,27 @@ const MIME = {
|
|
|
222
321
|
* `listeners` is the Set off `broadcastingWindow`, which is where the pushes
|
|
223
322
|
* going the other way are waiting for somewhere to go.
|
|
224
323
|
*/
|
|
225
|
-
export function createServer(
|
|
226
|
-
return http.createServer(
|
|
324
|
+
export function createServer(options) {
|
|
325
|
+
return http.createServer(createHandler(options));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The same door as a bare request handler, so main/phone-link.mjs can put its
|
|
330
|
+
* own server in front of it. `token` may be a function, read on every request,
|
|
331
|
+
* so a key that is reset stops working at once.
|
|
332
|
+
*/
|
|
333
|
+
export function createHandler({ channels, token, listeners = new Set(), dist = path.join(repoRoot, 'renderer', 'dist'), files = null }) {
|
|
334
|
+
return async (req, res) => {
|
|
227
335
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
228
336
|
|
|
337
|
+
// A file for the phone, behind the file key rather than the phone's own.
|
|
338
|
+
// Only a server that was handed `files` has any to give.
|
|
339
|
+
if (url.pathname.startsWith('/f/')) {
|
|
340
|
+
if (!files) { res.writeHead(404).end('No files here.'); return; }
|
|
341
|
+
await answerFile(req, res, url, { token, files });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
229
345
|
// The token rides in a header on api calls and in the query on the event
|
|
230
346
|
// stream, because a browser cannot put a header on either a navigation or
|
|
231
347
|
// an EventSource. It guards the two doors that reach the store. The built
|
|
@@ -234,14 +350,29 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
|
|
|
234
350
|
// NOR DOES THE PAGE ITSELF ASK: the tab takes the token out of its address
|
|
235
351
|
// bar on load, so a reload asks for `/` bare, and refusing that drew
|
|
236
352
|
// "Wrong or missing token" on every reload (tests/a-browser-tab-survives-a-reload).
|
|
237
|
-
|
|
353
|
+
//
|
|
354
|
+
// AND THE HOST HEADER IS NOT CHECKED, asked and answered 2026-10-24.
|
|
355
|
+
// Refusing a Host that is not localhost is the usual guard against a page
|
|
356
|
+
// whose own name resolves to this machine, and it cannot go here:
|
|
357
|
+
// main/phone-link.mjs serves this same handler to a phone through a relay
|
|
358
|
+
// or over Tailscale, where the Host is neither. The token is the key,
|
|
359
|
+
// whatever name the request arrives under
|
|
360
|
+
// (tests/the-doing-half-runs-with-no-electron).
|
|
361
|
+
const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/') || url.pathname === '/file-key';
|
|
238
362
|
const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
|
|
239
|
-
if (guarded && given
|
|
363
|
+
if (guarded && !sameKey(given, typeof token === 'function' ? token() : token)) {
|
|
240
364
|
res.writeHead(403, { 'content-type': 'text/plain' });
|
|
241
365
|
res.end('Wrong or missing token. Use the url the terminal printed.');
|
|
242
366
|
return;
|
|
243
367
|
}
|
|
244
368
|
|
|
369
|
+
// The phone asks for its file key once, with its own key.
|
|
370
|
+
if (url.pathname === '/file-key') {
|
|
371
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
372
|
+
res.end(JSON.stringify({ key: fileKey(typeof token === 'function' ? token() : token) }));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
245
376
|
// The phone line left open. Seven channels talk the other way, main to
|
|
246
377
|
// screen, and every one of them is one-way, so this is an event stream
|
|
247
378
|
// rather than a websocket: the same job with no dependency to add.
|
|
@@ -316,7 +447,19 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
|
|
|
316
447
|
res.writeHead(200, { 'content-type': MIME[path.extname(file)] ?? 'application/octet-stream' });
|
|
317
448
|
res.end(buf);
|
|
318
449
|
});
|
|
319
|
-
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The key a request carries against the real one, in the same time whatever
|
|
455
|
+
* it gets right, so nobody can learn the key a character at a time by timing
|
|
456
|
+
* the refusals. Both sides are hashed first because a length that differs is
|
|
457
|
+
* itself a thing `timingSafeEqual` would refuse at once.
|
|
458
|
+
*/
|
|
459
|
+
export function sameKey(given, key) {
|
|
460
|
+
if (typeof given !== 'string' || typeof key !== 'string' || !given || !key) return false;
|
|
461
|
+
const digest = (s) => crypto.createHash('sha256').update(s).digest();
|
|
462
|
+
return crypto.timingSafeEqual(digest(given), digest(key));
|
|
320
463
|
}
|
|
321
464
|
|
|
322
465
|
function readBody(req) {
|
|
@@ -324,9 +467,12 @@ function readBody(req) {
|
|
|
324
467
|
let body = '';
|
|
325
468
|
req.on('data', (c) => {
|
|
326
469
|
body += c;
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
470
|
+
// This is here so a stuck client cannot eat the process. A PHONE'S
|
|
471
|
+
// UPLOADS DO COME THROUGH HERE, though: it has no file paths, so a
|
|
472
|
+
// picture is posted whole, as base64, to zero:stage-attachment. 8 MB of
|
|
473
|
+
// that was a 6 MB photo; an iPhone's is often more (2026-10-12). The
|
|
474
|
+
// relay passes up to 32 MB (relay/worker.mjs).
|
|
475
|
+
if (body.length > 32 * 1024 * 1024) reject(new Error('Body too large'));
|
|
330
476
|
});
|
|
331
477
|
req.on('end', () => resolve(body));
|
|
332
478
|
req.on('error', reject);
|