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
package/main/artifact-path.mjs
CHANGED
|
@@ -66,28 +66,55 @@ export function artifactRoots(prod) {
|
|
|
66
66
|
// now resolves by exact path or not at all.
|
|
67
67
|
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'sessions', 'runs', 'pending-writes', 'checkpoints']);
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
// How long one walk of a tree answers for it. A thread asks for every name it
|
|
70
|
+
// mentions the moment it opens, so the walk has to outlive that burst; a
|
|
71
|
+
// picture that lands later is found by the next look, which comes after this.
|
|
72
|
+
export const HUNT_FRESH_MS = 5000;
|
|
73
|
+
const walks = new Map();
|
|
74
|
+
|
|
75
|
+
// Every file name in one tree, read without holding the main process.
|
|
76
|
+
async function walkNames(tree, fs) {
|
|
77
|
+
const names = new Map();
|
|
78
|
+
const walk = async (dir, depth) => {
|
|
76
79
|
if (depth > 6) return;
|
|
77
80
|
let entries;
|
|
78
|
-
try { entries = fs.
|
|
81
|
+
try { entries = await fs.promises.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
82
|
+
const below = [];
|
|
79
83
|
for (const e of entries) {
|
|
80
84
|
if (e.name.startsWith('.')) continue;
|
|
81
85
|
const full = path.join(dir, e.name);
|
|
82
|
-
if (e.isDirectory()) { if (!SKIP.has(e.name)) walk(full, depth + 1); }
|
|
83
|
-
else if (e.name
|
|
84
|
-
|
|
85
|
-
try { m = fs.statSync(full).mtimeMs; } catch {}
|
|
86
|
-
if (!best || m > best.m) best = { full, m };
|
|
87
|
-
}
|
|
86
|
+
if (e.isDirectory()) { if (!SKIP.has(e.name)) below.push(walk(full, depth + 1)); }
|
|
87
|
+
else if (names.has(e.name)) names.get(e.name).push(full);
|
|
88
|
+
else names.set(e.name, [full]);
|
|
88
89
|
}
|
|
90
|
+
await Promise.all(below);
|
|
89
91
|
};
|
|
90
|
-
|
|
92
|
+
await walk(tree, 0);
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function namesIn(tree, fs, now) {
|
|
97
|
+
const kept = walks.get(tree);
|
|
98
|
+
if (kept && kept.fs === fs && now - kept.at < HUNT_FRESH_MS) return kept.names;
|
|
99
|
+
const names = walkNames(tree, fs);
|
|
100
|
+
walks.set(tree, { at: now, fs, names });
|
|
101
|
+
if (walks.size > 32) walks.delete(walks.keys().next().value);
|
|
102
|
+
return names;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Hunt the file down by NAME inside one product's trees, newest match wins.
|
|
106
|
+
// Workers write links relative to whatever directory they were standing in, so
|
|
107
|
+
// a fixed root list will always miss some of them, and every miss cost her a
|
|
108
|
+
// 10-minute detour (2026-08-05). Bounded: six levels, own product only.
|
|
109
|
+
async function huntByName(base, trees, fs, now) {
|
|
110
|
+
let best = null;
|
|
111
|
+
for (const tree of trees) {
|
|
112
|
+
for (const full of (await namesIn(tree, fs, now)).get(base) ?? []) {
|
|
113
|
+
let m;
|
|
114
|
+
try { m = (await fs.promises.stat(full)).mtimeMs; } catch { continue; }
|
|
115
|
+
if (!best || m > best.m) best = { full, m };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
91
118
|
return best?.full ?? null;
|
|
92
119
|
}
|
|
93
120
|
|
|
@@ -104,7 +131,7 @@ export function notFoundMessage(raw, own, product) {
|
|
|
104
131
|
//
|
|
105
132
|
// `products` is the whole list only so the card's own product can be found in
|
|
106
133
|
// it. Nothing in here reads another entry.
|
|
107
|
-
export function resolveArtifact({ src, product, products = [], accountRoot, storeRoot, fs = fsDefault }) {
|
|
134
|
+
export async function resolveArtifact({ src, product, products = [], accountRoot, storeRoot, fs = fsDefault, now = Date.now() }) {
|
|
108
135
|
if (typeof src !== 'string' || !src) return { ok: false, error: 'No file was named.' };
|
|
109
136
|
const raw = decodeURIComponent(src.replace(/^file:\/\//, '')).replace(/^\.\//, '');
|
|
110
137
|
const own = products.find((x) => x.slug === product) ?? null;
|
|
@@ -144,10 +171,12 @@ export function resolveArtifact({ src, product, products = [], accountRoot, stor
|
|
|
144
171
|
|
|
145
172
|
// The hunt takes the two TREES, not the root list: designs/ and attachments/
|
|
146
173
|
// sit inside the docs dir already, and walking them twice finds nothing new.
|
|
174
|
+
// A run's change record is never hunted for: `runs` is skipped, so the walk
|
|
175
|
+
// could not find it, and the open thread asks for it on every refresh.
|
|
147
176
|
const base = path.basename(raw);
|
|
148
177
|
const trees = own ? [own.dir, own.repoPath].filter(Boolean) : [];
|
|
149
|
-
if (base && trees.length) {
|
|
150
|
-
const found = huntByName(base, trees, fs);
|
|
178
|
+
if (base && trees.length && raw.split('/')[0] !== 'runs') {
|
|
179
|
+
const found = await huntByName(base, trees, fs, now);
|
|
151
180
|
if (found) return { ok: true, path: found };
|
|
152
181
|
}
|
|
153
182
|
|
package/main/claude-bin.mjs
CHANGED
|
@@ -1,338 +1,72 @@
|
|
|
1
|
-
// Where Claude Code actually is, on a machine that is not
|
|
1
|
+
// Where Claude Code actually is, on a machine that is not ours.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// THIS FILE IS NOW THE NAMES AND NOTHING ELSE. The search, the registry entry
|
|
4
|
+
// that declares Claude Code, the measured notes about where it installs and the
|
|
5
|
+
// `found` / `certain` rule all live in main/engine-bin.mjs, which does the same
|
|
6
|
+
// job for every coding agent the app can run. Until 2026-10-08 this was 338
|
|
7
|
+
// lines and main/codex-bin.mjs was 279, and they were two copies of one idea:
|
|
8
|
+
// adding a harness meant a third file and three hundred lines of test.
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// decides which path gets spawned. `certain` is whether we are entitled to SAY
|
|
15
|
-
// it is missing, and everything below exists to make `certain` hard to earn.
|
|
16
|
-
//
|
|
17
|
-
// Four steps before an absence is ever claimed, cheapest first.
|
|
18
|
-
//
|
|
19
|
-
// KNOWN PATHS. A filesystem check is instant, spawns nothing, and is the right
|
|
20
|
-
// answer for most machines. The list is every normal home for this binary: the
|
|
21
|
-
// installer's own directory, Claude Code's local install (what
|
|
22
|
-
// `claude migrate-installer` leaves behind), Homebrew on Apple Silicon and on
|
|
23
|
-
// Intel, MacPorts, and the default bin of every package manager that is not npm.
|
|
24
|
-
//
|
|
25
|
-
// VERSION MANAGER DIRECTORIES. nvm, fnm, n and asdf each keep one bin directory
|
|
26
|
-
// PER INSTALLED NODE, under a name nobody can guess, so they are read rather
|
|
27
|
-
// than listed. A global npm install under any of them lands there and nowhere
|
|
28
|
-
// else, and this is the case the shell step used to be the only cover for.
|
|
29
|
-
//
|
|
30
|
-
// THE SHELL. The catch-all for installs no list can enumerate: volta, bun, a
|
|
31
|
-
// custom npm prefix. It must be a LOGIN, INTERACTIVE shell, because those
|
|
32
|
-
// managers put themselves on PATH from `.zprofile` and `.zshrc` and nowhere
|
|
33
|
-
// else. And it is needed at all because a double-clicked Electron app inherits
|
|
34
|
-
// launchd's PATH, which is /usr/bin:/bin:/usr/sbin:/sbin and has never heard of
|
|
35
|
-
// any of them. That is the whole reason an npm install is obvious in a terminal
|
|
36
|
-
// and invisible to the app. It is asked more than once now, because ONE probe
|
|
37
|
-
// that fails to start is not a fact about anybody's Mac.
|
|
10
|
+
// WHAT IS KEPT HERE is the vocabulary the rest of the app already imports --
|
|
11
|
+
// main/config.mjs, main/settings.mjs, scripts/read-claude-commands.mjs,
|
|
12
|
+
// scripts/read-claude-models.mjs -- plus `askShell` and `shellProbes`, which
|
|
13
|
+
// several tests reach for through this file because Claude Code is the binary the
|
|
14
|
+
// app will not open an inbox without.
|
|
38
15
|
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// this Mac, whatever our search of PATH did or did not manage, and the honest
|
|
44
|
-
// thing on the screen is silence rather than a sentence telling somebody to
|
|
45
|
-
// install software they are already running. A dangling `claude` symlink counts
|
|
46
|
-
// the same way, and it is a real state: the installer points ~/.local/bin/claude
|
|
47
|
-
// at a versioned file and swaps that file on every update.
|
|
16
|
+
// THE RULE THIS FILE ARGUED FOR AND engine-bin.mjs KEEPS: never tell somebody
|
|
17
|
+
// Claude Code is missing unless we are certain it is. `found` is whether a
|
|
18
|
+
// runnable file is really there, and it decides which path gets spawned.
|
|
19
|
+
// `certain` is whether we are entitled to SAY it is missing.
|
|
48
20
|
//
|
|
49
21
|
// Nothing here overrides the config file. An explicit `claudeBin` in
|
|
50
|
-
// zero.config.json wins untouched and is never searched around
|
|
51
|
-
// file promises explicit paths with no discovery, and a search that
|
|
52
|
-
// second-guesses a written setting would break that promise.
|
|
22
|
+
// zero.config.json wins untouched and is never searched around.
|
|
53
23
|
|
|
54
|
-
import fs from 'node:fs';
|
|
55
24
|
import os from 'node:os';
|
|
56
|
-
import
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// a machine that has always worked keeps resolving exactly as it did.
|
|
61
|
-
export function candidatePaths(home = os.homedir()) {
|
|
62
|
-
return [
|
|
63
|
-
path.join(home, '.local/bin/claude'), // the installer
|
|
64
|
-
path.join(home, '.claude/local/claude'), // claude migrate-installer
|
|
65
|
-
'/opt/homebrew/bin/claude', // Homebrew, Apple Silicon
|
|
66
|
-
'/usr/local/bin/claude', // Homebrew on Intel, and npm -g
|
|
67
|
-
path.join(home, '.bun/bin/claude'), // bun
|
|
68
|
-
path.join(home, '.volta/bin/claude'), // volta
|
|
69
|
-
path.join(home, '.asdf/shims/claude'), // asdf, through its shims
|
|
70
|
-
path.join(home, '.npm-global/bin/claude'), // the two npm prefixes people
|
|
71
|
-
path.join(home, '.npm-packages/bin/claude'), // actually write in a guide
|
|
72
|
-
path.join(home, '.yarn/bin/claude'), // yarn global
|
|
73
|
-
path.join(home, 'Library/pnpm/claude'), // pnpm's own bin on macOS
|
|
74
|
-
path.join(home, '.local/share/pnpm/claude'), // pnpm's own bin on Linux
|
|
75
|
-
path.join(home, '.deno/bin/claude'), // deno
|
|
76
|
-
path.join(home, 'bin/claude'), // a hand-rolled ~/bin on PATH
|
|
77
|
-
'/opt/local/bin/claude', // MacPorts
|
|
78
|
-
'/usr/bin/claude', // packaged by something system-wide
|
|
79
|
-
];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// One bin directory per installed Node, under a name no list can hold. Read
|
|
83
|
-
// rather than guessed, and a directory that is not there just contributes
|
|
84
|
-
// nothing. This is where a global npm install under nvm, fnm, n or asdf ends
|
|
85
|
-
// up, and until now only the shell could reach it.
|
|
86
|
-
export function versionedPaths(home = os.homedir(), readdir = defaultReaddir) {
|
|
87
|
-
const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
|
|
88
|
-
return [
|
|
89
|
-
...under('.nvm/versions/node', 'bin/claude'),
|
|
90
|
-
...under('.local/share/fnm/node-versions', 'installation/bin/claude'),
|
|
91
|
-
...under('Library/Application Support/fnm/node-versions', 'installation/bin/claude'),
|
|
92
|
-
...under('n/versions/node', 'bin/claude'),
|
|
93
|
-
...under('.asdf/installs/nodejs', 'bin/claude'),
|
|
94
|
-
];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function defaultReaddir(dir) {
|
|
98
|
-
try { return fs.readdirSync(dir); } catch { return []; }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// THE COPY THE CLAUDE APP KEEPS FOR ITSELF, one folder per version, newest
|
|
102
|
-
// first. Somebody who has only ever used Claude Code inside the Claude app has
|
|
103
|
-
// this and nothing else (w-9f6975906c, measured 2026-10-05: 2.1.209 there, and
|
|
104
|
-
// it shared the CLI's sign-in). It is searched LAST, after the shell, because it
|
|
105
|
-
// updates when the app does and a real install is fresher.
|
|
106
|
-
export function appCopyPaths(home = os.homedir(), readdir = defaultReaddir) {
|
|
107
|
-
const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
|
|
108
|
-
return readdir(dir)
|
|
109
|
-
.filter((v) => /^\d+(\.\d+)*$/.test(v))
|
|
110
|
-
.sort(newestFirst)
|
|
111
|
-
.map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function newestFirst(a, b) {
|
|
115
|
-
const x = a.split('.').map(Number), y = b.split('.').map(Number);
|
|
116
|
-
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
117
|
-
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
|
|
118
|
-
}
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
|
|
123
|
-
// Mac. None of them is a binary and none of them is ever spawned; they exist
|
|
124
|
-
// only to stop the app announcing an absence it cannot support.
|
|
125
|
-
export function evidencePaths(home = os.homedir()) {
|
|
126
|
-
return [
|
|
127
|
-
path.join(home, '.claude.json'), // its own config file
|
|
128
|
-
path.join(home, '.claude/settings.json'),
|
|
129
|
-
path.join(home, '.claude/projects'), // one folder per project it has opened
|
|
130
|
-
path.join(home, '.claude/statsig'),
|
|
131
|
-
path.join(home, '.claude/.credentials.json'), // it has been logged in
|
|
132
|
-
path.join(home, '.claude/history.jsonl'),
|
|
133
|
-
path.join(home, '.local/share/claude'), // the installer's version store
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// A path that is a symlink to nothing. `existsSync` follows the link and says
|
|
138
|
-
// false, which is indistinguishable from nothing being there at all, and the
|
|
139
|
-
// two mean opposite things: an update that has swapped the versioned file out
|
|
140
|
-
// from under ~/.local/bin/claude leaves exactly this for a moment.
|
|
141
|
-
function defaultDangles(p) {
|
|
142
|
-
try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function installEvidence({ home = os.homedir(), exists = fs.existsSync, dangles = defaultDangles } = {}) {
|
|
146
|
-
for (const p of evidencePaths(home)) if (exists(p)) return p;
|
|
147
|
-
for (const p of candidatePaths(home)) if (dangles(p)) return p;
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
25
|
+
import {
|
|
26
|
+
ENGINES, findEngine, resolveEngine, forgetEngine, installEvidence as evidenceFor,
|
|
27
|
+
versionedBinPaths, askShell, shellProbes,
|
|
28
|
+
} from './engine-bin.mjs';
|
|
150
29
|
|
|
151
|
-
export
|
|
30
|
+
export { askShell, shellProbes };
|
|
152
31
|
|
|
153
|
-
|
|
154
|
-
// because it is the one that has her PATH in it; the other two because a
|
|
155
|
-
// $SHELL that will not start is a fact about $SHELL and not about Claude Code.
|
|
156
|
-
export function shellProbes(env = process.env) {
|
|
157
|
-
const preferred = env.SHELL || '/bin/zsh';
|
|
158
|
-
const out = [[preferred, '-lic', 4000], [preferred, '-lc', 2500]];
|
|
159
|
-
for (const other of ['/bin/zsh', '/bin/bash']) {
|
|
160
|
-
if (other !== preferred) out.push([other, '-lic', 2500]);
|
|
161
|
-
}
|
|
162
|
-
return out;
|
|
163
|
-
}
|
|
32
|
+
const CLAUDE = ENGINES.claude;
|
|
164
33
|
|
|
165
|
-
|
|
166
|
-
// Anything that is not an absolute path to a file that exists is treated as no
|
|
167
|
-
// path: `command -v` also prints shell functions and aliases, and a function
|
|
168
|
-
// name handed to spawn is a crash with a worse message than "not found".
|
|
169
|
-
//
|
|
170
|
-
// `answered` is the strict half. It is true only when every probe that ran came
|
|
171
|
-
// back with a real exit status, and at least one did. One shell that timed out
|
|
172
|
-
// alongside one that said no is not a machine we know anything about.
|
|
173
|
-
//
|
|
174
|
-
// `bin` is a parameter rather than the word `claude` baked into the command
|
|
175
|
-
// because Codex is a second engine (her ask, 2026-08-25) and it is found in the
|
|
176
|
-
// same way: same shells, same "two different silences" rule, same refusal to
|
|
177
|
-
// read a shell function name as a path. It is DEFAULTED, so every existing
|
|
178
|
-
// caller and every existing test asks about Claude Code as it always did, and
|
|
179
|
-
// that matters more here than it would anywhere else: this is the binary the
|
|
180
|
-
// app will not open an inbox without.
|
|
181
|
-
/**
|
|
182
|
-
* WHAT MAY BE HANDED TO A LOGIN SHELL, AND WHY THIS IS CHECKED AT ALL.
|
|
183
|
-
*
|
|
184
|
-
* The probe below interpolates `bin` into a string a LOGIN, INTERACTIVE shell
|
|
185
|
-
* executes, with her profile loaded, as her. Both callers today pass a literal
|
|
186
|
-
* ('claude' here, 'codex' in main/codex-bin.mjs), so nothing is exploitable
|
|
187
|
-
* now. But the entire direction of the second-engine work is a binary somebody
|
|
188
|
-
* can configure, and a `codexBin` read out of zero.config.json reaching this
|
|
189
|
-
* argument is one plausible slice away; at that point `command -v x; curl | sh`
|
|
190
|
-
* is a string in a file that runs itself.
|
|
191
|
-
*
|
|
192
|
-
* This function looks up program NAMES, so a name is all it takes. An allowlist
|
|
193
|
-
* rather than quoting, because quoting is a thing a later edit can undo without
|
|
194
|
-
* noticing and a shape check is not.
|
|
195
|
-
*/
|
|
196
|
-
const PROGRAM_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
34
|
+
export const INSTALL_URL = CLAUDE.installUrl;
|
|
197
35
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
let said = false;
|
|
203
|
-
let inconclusive = false;
|
|
204
|
-
for (const [shell, flags, timeout] of shellProbes(env)) {
|
|
205
|
-
let out = '';
|
|
206
|
-
try {
|
|
207
|
-
out = run(shell, [flags, `command -v ${bin}`], timeout);
|
|
208
|
-
} catch (e) {
|
|
209
|
-
// TWO DIFFERENT SILENCES, AND ONLY ONE OF THEM IS AN ANSWER.A shell that
|
|
210
|
-
// ran and exited non-zero has told us claude is not on PATH, and that is
|
|
211
|
-
// an answer. A shell that could not be started, or that ran past the
|
|
212
|
-
// timeout, has told us nothing at all, and reporting nothing as "not
|
|
213
|
-
// installed" is what put a line about missing software on the screen of
|
|
214
|
-
// people who have it.
|
|
215
|
-
if (exitedSayingNo(e)) said = true; else inconclusive = true;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const line = String(out).split('\n').map((s) => s.trim()).filter(Boolean).pop();
|
|
219
|
-
if (line && line.startsWith('/') && exists(line)) return { path: line, answered: true };
|
|
220
|
-
said = true;
|
|
221
|
-
}
|
|
222
|
-
return { path: null, answered: said && !inconclusive };
|
|
36
|
+
/** Every normal home for this binary, in order; the first that exists wins. */
|
|
37
|
+
export function candidatePaths(home = os.homedir()) {
|
|
38
|
+
return CLAUDE.candidates(home, () => []);
|
|
223
39
|
}
|
|
224
40
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// of them is a fact about Claude Code.
|
|
229
|
-
function exitedSayingNo(e) {
|
|
230
|
-
if (!e || typeof e !== 'object') return false;
|
|
231
|
-
if (e.signal || e.code === 'ETIMEDOUT' || e.code === 'ENOENT') return false;
|
|
232
|
-
return typeof e.status === 'number';
|
|
41
|
+
/** One bin directory per installed Node, read rather than guessed. */
|
|
42
|
+
export function versionedPaths(home = os.homedir(), readdir = undefined) {
|
|
43
|
+
return versionedBinPaths('claude', home, readdir);
|
|
233
44
|
}
|
|
234
45
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
function shellSaid(result) {
|
|
239
|
-
if (typeof result === 'string') return { path: result, answered: true };
|
|
240
|
-
if (!result || typeof result !== 'object') return { path: null, answered: true };
|
|
241
|
-
return { path: result.path ?? null, answered: result.answered !== false };
|
|
46
|
+
/** The copy the Claude desktop app keeps for itself, newest version first. */
|
|
47
|
+
export function appCopyPaths(home = os.homedir(), readdir = undefined) {
|
|
48
|
+
return CLAUDE.appCopies(home, readdir);
|
|
242
49
|
}
|
|
243
50
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// stdin ignored on purpose: an interactive shell handed a live terminal can
|
|
248
|
-
// sit waiting for input, and this runs on the boot path.
|
|
249
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
250
|
-
timeout,
|
|
251
|
-
});
|
|
51
|
+
/** What Claude Code leaves behind. None of it is ever spawned. */
|
|
52
|
+
export function evidencePaths(home = os.homedir()) {
|
|
53
|
+
return CLAUDE.evidence(home, {});
|
|
252
54
|
}
|
|
253
55
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
// Returns { path, found, certain, from, searched, evidence }. `found` is the
|
|
258
|
-
// only claim about the world: it means a file is really there. `certain` says
|
|
259
|
-
// whether we are allowed to tell somebody it is NOT there, so a screen can stay
|
|
260
|
-
// quiet rather than announce an absence it has not established. They come apart
|
|
261
|
-
// in exactly one case that is not a failure, and it is the case worth keeping
|
|
262
|
-
// honest: she has set `claudeBin` to a path that does not exist, so the path
|
|
263
|
-
// stays HERS (anything that spawns it should fail naming the path she chose,
|
|
264
|
-
// not one we picked) while the screen says it was not found and offers the
|
|
265
|
-
// install link, the same as any other machine.
|
|
266
|
-
export function findClaudeBin({
|
|
267
|
-
configured = null,
|
|
268
|
-
home = os.homedir(),
|
|
269
|
-
exists = fs.existsSync,
|
|
270
|
-
shellLookup = askShell,
|
|
271
|
-
readdir = defaultReaddir,
|
|
272
|
-
dangles = defaultDangles,
|
|
273
|
-
} = {}) {
|
|
274
|
-
if (configured) {
|
|
275
|
-
return {
|
|
276
|
-
path: configured, found: exists(configured), certain: true, from: 'settings',
|
|
277
|
-
searched: [], evidence: null,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
|
|
282
|
-
for (const candidate of searched) {
|
|
283
|
-
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const fromShell = shellSaid(shellLookup());
|
|
287
|
-
if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
|
|
288
|
-
|
|
289
|
-
for (const candidate of appCopyPaths(home, readdir)) {
|
|
290
|
-
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
|
|
294
|
-
// shell we could reach missed, so the last question is whether this Mac shows
|
|
295
|
-
// any sign of Claude Code anyway. If it does, we looked in the wrong places
|
|
296
|
-
// rather than at an empty machine, and nothing may be said out loud.
|
|
297
|
-
const evidence = installEvidence({ home, exists, dangles });
|
|
298
|
-
return {
|
|
299
|
-
path: null,
|
|
300
|
-
found: false,
|
|
301
|
-
certain: fromShell.answered && !evidence,
|
|
302
|
-
from: null,
|
|
303
|
-
searched,
|
|
304
|
-
evidence,
|
|
305
|
-
};
|
|
56
|
+
export function installEvidence({ home = os.homedir(), exists, dangles } = {}) {
|
|
57
|
+
return evidenceFor('claude', { home, env: {}, exists, dangles });
|
|
306
58
|
}
|
|
307
59
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// up telling someone to install Claude Code and then still saying "not found"
|
|
312
|
-
// after they have. The sentence on that screen makes a promise and a cached
|
|
313
|
-
// answer breaks it.
|
|
314
|
-
//
|
|
315
|
-
// The shell is the one part worth holding, because it is up to three
|
|
316
|
-
// subprocesses and a screen she toggles a switch on reads settings again on
|
|
317
|
-
// every write. Thirty seconds is long enough that a burst of writes pays for it
|
|
318
|
-
// once, and short enough that it is never the reason someone is still stuck.
|
|
319
|
-
|
|
320
|
-
const SHELL_TTL = 30_000;
|
|
321
|
-
let shellAnswer = null;
|
|
322
|
-
let shellAskedAt = 0;
|
|
323
|
-
|
|
324
|
-
function rememberedShellLookup(now, lookup) {
|
|
325
|
-
if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
|
|
326
|
-
shellAnswer = lookup();
|
|
327
|
-
shellAskedAt = now;
|
|
328
|
-
return shellAnswer;
|
|
60
|
+
/** The whole search. See main/engine-bin.mjs for what each answer means. */
|
|
61
|
+
export function findClaudeBin(where = {}) {
|
|
62
|
+
return findEngine('claude', where);
|
|
329
63
|
}
|
|
330
64
|
|
|
331
|
-
|
|
332
|
-
|
|
65
|
+
/** The same search with the shell step held for thirty seconds. */
|
|
66
|
+
export function resolveClaudeBin(configured = null, where = {}) {
|
|
67
|
+
return resolveEngine('claude', configured, where);
|
|
333
68
|
}
|
|
334
69
|
|
|
335
70
|
export function forgetClaudeBin() {
|
|
336
|
-
|
|
337
|
-
shellAskedAt = 0;
|
|
71
|
+
forgetEngine('claude');
|
|
338
72
|
}
|
package/main/claude-input.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { Name } from '../shared/product-name.mjs';
|
|
2
3
|
|
|
3
4
|
// THE SESSION STOPPED LISTENING BEFORE IT TOOK THIS MESSAGE. Not a refusal:
|
|
4
5
|
// the turn closed, or the process went, in the seconds between her pressing
|
|
@@ -16,12 +17,29 @@ function turnClosed(message) {
|
|
|
16
17
|
// before the session is closed anyway. Measured wakes arrive in about a second.
|
|
17
18
|
const WAKE_GRACE_MS = 2 * 60 * 1000;
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// Said to the agent, never shown to the person: the trace keeps no user turns.
|
|
21
|
+
// Paths are cut to their last part: a cloud run's relay uploads any file its
|
|
22
|
+
// input names outside the task folder (main/cloud/run.mjs `filesNamedIn`).
|
|
23
|
+
export function stillRunningNote(tasks) {
|
|
24
|
+
const plain = (text) => String(text).replace(/(^|[\s"'`(=:])\/(?:[^\s"'`<>()[\]{},;\\]*\/)+/g, '$1');
|
|
25
|
+
const names = tasks.map((t) => `"${plain(t.description || t.task_id).slice(0, 120)}"`).join(', ');
|
|
26
|
+
return `This is ${Name}, not the person. Your turn ended with ${tasks.length === 1 ? 'this' : 'these'} still running in the background: ${names}. `
|
|
27
|
+
+ 'Your run stays open, and your answer is not delivered, until nothing you started is still running. '
|
|
28
|
+
+ 'Stop any you no longer need. If you are waiting on one, say what for in one line and end your turn.';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// THE PROMPT LEAVES THE LINE WHEN REPLIES ARRIVE ON STDIN. Both the print flag
|
|
32
|
+
// and the input-format flags are read off the engine's `run` definition, so an
|
|
33
|
+
// engine of this family that spells either of them differently is a definition
|
|
34
|
+
// and not a change here. An engine with no `input` cannot take corrections on
|
|
35
|
+
// stdin, and gets its line back as it was.
|
|
36
|
+
export function streamInputArgs(run, args) {
|
|
37
|
+
if (!run?.input) return [...args];
|
|
20
38
|
const copy = [...args];
|
|
21
|
-
const index = copy.indexOf(
|
|
39
|
+
const index = copy.indexOf(run.print);
|
|
22
40
|
if (index < 0) throw Error('Missing print prompt');
|
|
23
41
|
copy.splice(index + 1, 1);
|
|
24
|
-
return [...copy,
|
|
42
|
+
return [...copy, ...run.input];
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
// Replay-user-messages acknowledges the exact UUID when Claude consumes it.
|
|
@@ -39,13 +57,35 @@ export function attachClaudeInput(child) {
|
|
|
39
57
|
// end of the run, and closing there killed the suite it was waiting on
|
|
40
58
|
// (tests/an-agent-waiting-on-its-own-tests-does-not-come-back-to-you).
|
|
41
59
|
let waiting = [], wakeTimer = null;
|
|
60
|
+
// Jobs the agent has been told are still out, and the messages that told it.
|
|
61
|
+
// A forgotten job holds a finished answer until Claude Code's own limit
|
|
62
|
+
// stops it, thirty minutes and more
|
|
63
|
+
// (tests/a-command-an-agent-left-running-does-not-hold-its-answer-back).
|
|
64
|
+
const named = new Set(), ours = new Set();
|
|
42
65
|
// Input only ever closes at a result with nothing out, so a closed input is
|
|
43
66
|
// the one proof the run reached its end. A session that exits with input
|
|
44
67
|
// still open was stopped part-way, whatever its last turn said
|
|
45
68
|
// (tests/a-worker-whose-helpers-are-still-working-does-not-come-back-to-you).
|
|
46
69
|
let ended = false;
|
|
70
|
+
// A TURN IS RUNNING between its `system/init` and its result. A message
|
|
71
|
+
// written while none is starts the next one, and a turn that starts while
|
|
72
|
+
// one waits takes it with it, so either way it is being answered, though
|
|
73
|
+
// Claude replays it only when that turn first speaks. Send now must not cut
|
|
74
|
+
// that turn: it was the answer (w-0202576e73).
|
|
75
|
+
let inTurn = false;
|
|
47
76
|
const closeInput = () => { closed = true; ended = true; clearTimeout(wakeTimer); child.stdin.end(); };
|
|
48
77
|
const mayClose = () => !held && atResult && !pending.size && !waiting.length;
|
|
78
|
+
// Not into a session she is driving, and not when her own message is about
|
|
79
|
+
// to start the next turn. Kept out of `pending`, so it never holds input open.
|
|
80
|
+
const nameWhatIsStillOut = () => {
|
|
81
|
+
if (closed || held || pending.size || !waiting.some((t) => t?.task_id && !named.has(t.task_id))) return;
|
|
82
|
+
for (const t of waiting) if (t?.task_id) named.add(t.task_id);
|
|
83
|
+
const uuid = randomUUID();
|
|
84
|
+
ours.add(uuid);
|
|
85
|
+
try {
|
|
86
|
+
child.stdin.write(JSON.stringify({ type: 'user', uuid, message: { role: 'user', content: stillRunningNote(waiting) }, parent_tool_use_id: null, session_id: '' }) + '\n');
|
|
87
|
+
} catch { /* the input is gone; `fail` hears it */ }
|
|
88
|
+
};
|
|
49
89
|
const fail = reason => {
|
|
50
90
|
closed = true;
|
|
51
91
|
for (const request of pending.values()) {
|
|
@@ -75,12 +115,20 @@ export function attachClaudeInput(child) {
|
|
|
75
115
|
}
|
|
76
116
|
}
|
|
77
117
|
if (event.type === 'system' && event.subtype === 'bridge_state') child.emit('bridge-state', event);
|
|
78
|
-
|
|
118
|
+
// A cloud run whose input the sandbox closed at the end of its turn
|
|
119
|
+
// while this Mac slept (main/cloud/run.mjs): the end of the run, as if
|
|
120
|
+
// closed here. A reply still waiting is kept for the next run.
|
|
121
|
+
if (event.type === 'system' && event.subtype === 'agentbox_input_closed_in_cloud' && !closed) closeInput();
|
|
122
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
123
|
+
inTurn = true;
|
|
124
|
+
for (const request of pending.values()) request.taken = true;
|
|
125
|
+
}
|
|
126
|
+
if (event.type === 'assistant') { atResult = false; inTurn = true; }
|
|
79
127
|
if (event.type === 'user' && !event.parent_tool_use_id && typeof event.uuid === 'string' && !seenUsers.has(event.uuid)) {
|
|
80
128
|
seenUsers.add(event.uuid);
|
|
81
129
|
const content = event.message?.content;
|
|
82
130
|
const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
|
|
83
|
-
if (held && !pending.has(event.uuid) && text && !event.isMeta) child.emit('remote-user', event);
|
|
131
|
+
if (held && !pending.has(event.uuid) && !ours.has(event.uuid) && text && !event.isMeta) child.emit('remote-user', event);
|
|
84
132
|
if (text) { atResult = false; child.emit('input-turn', event); }
|
|
85
133
|
}
|
|
86
134
|
if (event.type === 'user' && pending.has(event.uuid)) {
|
|
@@ -103,7 +151,9 @@ export function attachClaudeInput(child) {
|
|
|
103
151
|
}
|
|
104
152
|
if (event.type === 'result') {
|
|
105
153
|
atResult = true;
|
|
154
|
+
inTurn = false;
|
|
106
155
|
if (mayClose()) closeInput();
|
|
156
|
+
else if (waiting.length) nameWhatIsStillOut();
|
|
107
157
|
}
|
|
108
158
|
}
|
|
109
159
|
});
|
|
@@ -130,7 +180,7 @@ export function attachClaudeInput(child) {
|
|
|
130
180
|
// ONLY WHILE A MESSAGE OF HERS IS WAITING. The cut turn ends in a result,
|
|
131
181
|
// and with nothing pending that result closes the input and ends the run.
|
|
132
182
|
child.interrupt = () => {
|
|
133
|
-
if (closed || !pending.
|
|
183
|
+
if (closed || ![...pending.values()].some(request => !request.taken)) return Promise.resolve({ interrupted: false });
|
|
134
184
|
child.lastInterruptAt = Date.now();
|
|
135
185
|
return child.control({ subtype: 'interrupt' }).then(() => ({ interrupted: true }));
|
|
136
186
|
};
|
|
@@ -147,7 +197,7 @@ export function attachClaudeInput(child) {
|
|
|
147
197
|
child.steer = text => new Promise((resolve, reject) => {
|
|
148
198
|
if (closed || !child.stdin.writable) return reject(turnClosed('The turn has ended. Send the message again to continue it.'));
|
|
149
199
|
const uuid = randomUUID();
|
|
150
|
-
pending.set(uuid, { resolve, reject });
|
|
200
|
+
pending.set(uuid, { resolve, reject, taken: !inTurn });
|
|
151
201
|
try {
|
|
152
202
|
child.stdin.write(JSON.stringify({ type: 'user', uuid, message: { role: 'user', content: text }, parent_tool_use_id: null, session_id: '' }) + '\n');
|
|
153
203
|
} catch (error) {
|