agentbox-app 0.1.13 → 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/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -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-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- 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-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- 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.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- 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 +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- 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-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -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 +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- 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/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- 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 +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- 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-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- 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 +60 -34
- 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 +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- 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 +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- 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 +8 -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 +6 -0
- package/shared/notify-rules.mjs +36 -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/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- 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 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- 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/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
package/main/engine-setup.mjs
CHANGED
|
@@ -66,10 +66,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
66
66
|
/**
|
|
67
67
|
* One setup per tool, held for the life of the app.
|
|
68
68
|
*
|
|
69
|
-
* `find(engine)` is the finder, looking again from scratch
|
|
70
|
-
*
|
|
71
|
-
* the app runs on what was just installed). `spawn` and
|
|
72
|
-
* the tests; nothing in the app passes them.
|
|
69
|
+
* `find(engine)` is the finder, looking again from scratch, and may answer
|
|
70
|
+
* with a promise (the caller passes `lookAgainFor`, which also moves the
|
|
71
|
+
* config so the rest of the app runs on what was just installed). `spawn` and
|
|
72
|
+
* `wait` are seams for the tests; nothing in the app passes them.
|
|
73
73
|
*/
|
|
74
74
|
export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
|
|
75
75
|
const runEnv = terminalEnv(env);
|
|
@@ -109,7 +109,11 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
109
109
|
|
|
110
110
|
async function go(j, run) {
|
|
111
111
|
const live = () => j.run === run;
|
|
112
|
-
let hit = find(j.engine);
|
|
112
|
+
let hit = await find(j.engine);
|
|
113
|
+
if (!live()) return;
|
|
114
|
+
// The card's first row says Found or Install off this, because it ticks
|
|
115
|
+
// that row either way.
|
|
116
|
+
j.already = !!hit?.found;
|
|
113
117
|
if (!hit?.found) {
|
|
114
118
|
j.phase = 'installing';
|
|
115
119
|
const { file, args } = installCommand(j.engine);
|
|
@@ -119,7 +123,7 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
119
123
|
j.running = null;
|
|
120
124
|
if (!live()) return;
|
|
121
125
|
if (code !== 0) return fail(j, `The installer stopped: ${lastLine(j.log) || `exit ${code}`}`);
|
|
122
|
-
hit = find(j.engine);
|
|
126
|
+
hit = await find(j.engine);
|
|
123
127
|
if (!hit?.found) return fail(j, 'The installer finished, but the app still cannot find it.');
|
|
124
128
|
}
|
|
125
129
|
j.bin = hit.path;
|
|
@@ -140,7 +144,15 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
140
144
|
ended = false;
|
|
141
145
|
watchLogin(login);
|
|
142
146
|
}
|
|
143
|
-
|
|
147
|
+
// ASK ONCE MORE BEFORE CALLING IT CLOSED. A sign-in that finishes while
|
|
148
|
+
// the check above is running exits before that check answers, and the
|
|
149
|
+
// check, asked a moment too early, says no. Without this, somebody who
|
|
150
|
+
// had just approved it was told it was closed.
|
|
151
|
+
if (ended) {
|
|
152
|
+
if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
|
|
153
|
+
if (!live()) return;
|
|
154
|
+
return fail(j, `The sign-in was closed before it finished.${lastLine(j.log) ? ` ${lastLine(j.log)}` : ''}`);
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
}
|
|
146
158
|
|
|
@@ -172,14 +184,14 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
172
184
|
},
|
|
173
185
|
status(engine) {
|
|
174
186
|
const j = job(engine);
|
|
175
|
-
return { engine, phase: j.phase, error: j.error, log: j.log };
|
|
187
|
+
return { engine, phase: j.phase, error: j.error, log: j.log, bin: j.bin ?? null, already: !!j.already };
|
|
176
188
|
},
|
|
177
189
|
/**
|
|
178
190
|
* WHETHER THIS TOOL CAN RUN AN AGENT RIGHT NOW: found, and signed in by its
|
|
179
191
|
* own account. The walk asks before it asks anything, so a Mac that is
|
|
180
192
|
* already set up never sees the plan question. */
|
|
181
193
|
async readiness(engine) {
|
|
182
|
-
const hit = find(engine);
|
|
194
|
+
const hit = await find(engine);
|
|
183
195
|
if (!hit?.found) return { engine, found: false, signedIn: false };
|
|
184
196
|
try { return { engine, found: true, signedIn: await signedIn({ engine, log: '' }, hit.path) }; }
|
|
185
197
|
catch (error) { return { engine, found: true, signedIn: false, error: error.message }; }
|
package/main/feedback.mjs
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// It is not behind the diagnostics switch. That switch is about what Agentbox
|
|
17
17
|
// sends on its own; this is something a person typed and pressed Send on.
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import { readEnv } from '../shared/product-name.mjs';
|
|
20
|
+
|
|
21
|
+
export { sendFeedback } from '../shared/feedback.mjs';
|
|
21
22
|
|
|
22
23
|
export const FEEDBACK_ADDRESS = 'https://lywvthwkhkfzhqivvond.supabase.co/functions/v1/feedback';
|
|
23
24
|
|
|
@@ -35,26 +36,3 @@ const clean = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
|
35
36
|
export function feedbackUrl(config, baked = BAKED, env = process.env) {
|
|
36
37
|
return clean(config?.feedbackUrl) ?? clean(readEnv('FEEDBACK_URL', env)) ?? clean(baked);
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
// Answers { ok: true } or { ok: false, error } with the error as a sentence.
|
|
40
|
-
// It never throws, so the card always has something to say.
|
|
41
|
-
export async function sendFeedback({ url, payload, fetchImpl = globalThis.fetch, timeoutMs = 60_000 }) {
|
|
42
|
-
if (!url) return { ok: false, error: `Feedback is not set up in this copy of ${Name}.` };
|
|
43
|
-
// Checked again here, off the bytes that would actually travel, since the
|
|
44
|
-
// renderer is not the last word on what leaves this machine.
|
|
45
|
-
const sized = (payload?.files ?? []).map((f) => ({ size: Math.floor((String(f?.data ?? '').length * 3) / 4) }));
|
|
46
|
-
const check = checkFeedback({ text: payload?.text, files: sized });
|
|
47
|
-
if (!check.ok) return { ok: false, error: check.reason };
|
|
48
|
-
try {
|
|
49
|
-
const res = await fetchImpl(url, {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
headers: { 'content-type': 'application/json' },
|
|
52
|
-
body: JSON.stringify(payload),
|
|
53
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
54
|
-
});
|
|
55
|
-
if (res?.ok) return { ok: true };
|
|
56
|
-
return { ok: false, error: 'It did not send. Try again in a moment.' };
|
|
57
|
-
} catch {
|
|
58
|
-
return { ok: false, error: 'It did not send. Check your connection and try again.' };
|
|
59
|
-
}
|
|
60
|
-
}
|
package/main/folder-pictures.mjs
CHANGED
|
@@ -17,10 +17,10 @@ import { servable } from './img-scheme.mjs';
|
|
|
17
17
|
|
|
18
18
|
const PICTURE = /\.(png|jpe?g|gif|webp|avif)$/i;
|
|
19
19
|
|
|
20
|
-
export function folderPictures({ src, product, products = [], accountRoot, storeRoot, limit = 60, fs = fsDefault }) {
|
|
20
|
+
export async function folderPictures({ src, product, products = [], accountRoot, storeRoot, limit = 60, fs = fsDefault }) {
|
|
21
21
|
if (typeof src !== 'string' || !src.trim()) return { ok: false, error: 'No folder was named.' };
|
|
22
22
|
const named = src.replace(/\/+$/, '');
|
|
23
|
-
const found = resolveArtifact({ src: named, product, products, accountRoot, storeRoot, fs });
|
|
23
|
+
const found = await resolveArtifact({ src: named, product, products, accountRoot, storeRoot, fs });
|
|
24
24
|
if (!found.ok) return { ok: false, error: found.error };
|
|
25
25
|
const own = products.find((p) => p.slug === product) ?? null;
|
|
26
26
|
const roots = [accountRoot, own?.dir, own?.repoPath].filter(Boolean);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// THE OTHER END OF main/freeze-recorder.mjs: the watcher, on its own thread.
|
|
2
|
+
//
|
|
3
|
+
// The profile is rotated every `rotateMs` so it never grows, and at the end of
|
|
4
|
+
// each freeze so the freeze is in the one it hands back. The one before is
|
|
5
|
+
// kept too, because a freeze can start before a rotation and end after it.
|
|
6
|
+
import { Session } from 'node:inspector';
|
|
7
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
8
|
+
import { freezeFrom, writeFreeze } from './freeze-recorder.mjs';
|
|
9
|
+
|
|
10
|
+
const { beat, dir, appDir, thresholdMs, rotateMs, intervalUs } = workerData;
|
|
11
|
+
const stamp = new BigInt64Array(beat);
|
|
12
|
+
const nowUs = () => Number(process.hrtime.bigint() / 1000n);
|
|
13
|
+
// The main thread asleep rather than stuck (App Nap timers, say) has an idle
|
|
14
|
+
// profile. Below this share of busy samples it is not a freeze.
|
|
15
|
+
const BUSY_ENOUGH = 0.5;
|
|
16
|
+
|
|
17
|
+
const session = new Session();
|
|
18
|
+
const post = (method, params = {}) => new Promise((resolve, reject) => {
|
|
19
|
+
session.post(method, params, (err, out) => (err ? reject(err) : resolve(out)));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let running = null; // { anchorUs } of the profile being taken
|
|
23
|
+
let previous = null; // { profile, offsetUs } of the last one stopped
|
|
24
|
+
let frozeAt = null; // the last stamp before the freeze in progress, in µs
|
|
25
|
+
const seen = []; // freezes ended and not yet cut out of a profile
|
|
26
|
+
let busy = false;
|
|
27
|
+
|
|
28
|
+
async function start() {
|
|
29
|
+
const before = nowUs();
|
|
30
|
+
await post('Profiler.start');
|
|
31
|
+
running = { anchorUs: (before + nowUs()) / 2 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function rotate() {
|
|
35
|
+
const { profile } = await post('Profiler.stop');
|
|
36
|
+
const stopped = { profile, offsetUs: profile.startTime - running.anchorUs };
|
|
37
|
+
await start();
|
|
38
|
+
const profiles = [previous, stopped].filter(Boolean);
|
|
39
|
+
previous = stopped;
|
|
40
|
+
for (const f of seen.splice(0)) {
|
|
41
|
+
const read = freezeFrom(profiles, { fromUs: f.fromUs, toUs: f.toUs, appDir });
|
|
42
|
+
if (read.busy < BUSY_ENOUGH) continue;
|
|
43
|
+
const line = { at: new Date(Date.now() - (nowUs() - f.fromUs) / 1000).toISOString(), ms: Math.round((f.toUs - f.fromUs) / 1000), ...read };
|
|
44
|
+
writeFreeze(dir, line);
|
|
45
|
+
parentPort.postMessage({ freeze: { at: line.at, ms: line.ms, where: line.where } });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A freeze that ends while a rotation is under way goes round again.
|
|
50
|
+
async function turn() {
|
|
51
|
+
if (busy || !running) return;
|
|
52
|
+
busy = true;
|
|
53
|
+
try {
|
|
54
|
+
do { await rotate(); } while (seen.length);
|
|
55
|
+
} catch (err) { quit(err); } finally { busy = false; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function watch() {
|
|
59
|
+
const last = Number(Atomics.load(stamp, 0) / 1000n);
|
|
60
|
+
if (nowUs() - last > thresholdMs * 1000) { frozeAt ??= last; return; }
|
|
61
|
+
if (frozeAt === null) return;
|
|
62
|
+
seen.push({ fromUs: frozeAt, toUs: last });
|
|
63
|
+
frozeAt = null;
|
|
64
|
+
void turn();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let watching = null;
|
|
68
|
+
let rotating = null;
|
|
69
|
+
function quit(err) {
|
|
70
|
+
clearInterval(watching);
|
|
71
|
+
clearInterval(rotating);
|
|
72
|
+
try { session.disconnect(); } catch { /* already gone */ }
|
|
73
|
+
parentPort.postMessage({ gaveUp: String(err?.message ?? err) });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The timers go first: an inspector reply does not keep this thread alive.
|
|
77
|
+
watching = setInterval(watch, 50);
|
|
78
|
+
rotating = setInterval(() => { if (frozeAt === null) void turn(); }, rotateMs);
|
|
79
|
+
try {
|
|
80
|
+
session.connectToMainThread();
|
|
81
|
+
await post('Profiler.enable');
|
|
82
|
+
await post('Profiler.setSamplingInterval', { interval: intervalUs });
|
|
83
|
+
await start();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
quit(err);
|
|
86
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// WHAT THE MAIN THREAD WAS DOING WHEN THE APP FROZE.
|
|
2
|
+
//
|
|
3
|
+
// The spinning wheel is this thread not answering, and it comes when nobody is
|
|
4
|
+
// profiling. So a thread of its own watches all the time: the main thread
|
|
5
|
+
// stamps a clock every 100 ms, the
|
|
6
|
+
// watcher keeps a low-rate CPU profile of it running through the inspector,
|
|
7
|
+
// and when the stamps stop for longer than `thresholdMs` it cuts that stretch
|
|
8
|
+
// out of the profile and writes one line saying which of our functions held
|
|
9
|
+
// it, to `freeze-<day>.jsonl` beside the memory log, kept a week.
|
|
10
|
+
//
|
|
11
|
+
// It stays on this computer. Nothing here is sent anywhere.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { Worker } from 'node:worker_threads';
|
|
16
|
+
|
|
17
|
+
export const THRESHOLD_MS = 500;
|
|
18
|
+
export const KEEP_DAYS = 7;
|
|
19
|
+
const BEAT_MS = 100;
|
|
20
|
+
const FILE = /^freeze-(\d{4}-\d{2}-\d{2})\.jsonl$/;
|
|
21
|
+
const IDLE = new Set(['(idle)', '(program)']);
|
|
22
|
+
const GC = '(garbage collector)';
|
|
23
|
+
const FRAMES_KEPT = 12;
|
|
24
|
+
|
|
25
|
+
export const dayOf = (ms) => {
|
|
26
|
+
const d = new Date(ms);
|
|
27
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function fileOf(url, appDirs) {
|
|
31
|
+
if (!url) return '';
|
|
32
|
+
let file = url;
|
|
33
|
+
if (url.startsWith('file://')) { try { file = fileURLToPath(url); } catch { /* keep the url */ } }
|
|
34
|
+
const appDir = appDirs.find((d) => file.startsWith(d + path.sep));
|
|
35
|
+
if (appDir) return file.slice(appDir.length + 1);
|
|
36
|
+
if (file.startsWith('node:') || !path.isAbsolute(file)) return file;
|
|
37
|
+
// Never a home path in the log: a file outside the app is named by its last parts.
|
|
38
|
+
return `…/${file.split(path.sep).slice(-2).join('/')}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The profile names a file by its real path, which on macOS is /private/var for /var.
|
|
42
|
+
const realPath = (dir) => { try { return dir ? fs.realpathSync(dir) : ''; } catch { return ''; } };
|
|
43
|
+
|
|
44
|
+
const ours = (frame) => /\.(m?js|cjs|ts)$/.test(frame.file) && !/^(node:|…\/)/.test(frame.file) && !frame.file.includes('node_modules');
|
|
45
|
+
const named = (f) => (f.file ? `${f.fn} ${f.file}:${f.line}` : f.fn);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What held the thread between `fromUs` and `toUs`, read off one or more CPU
|
|
49
|
+
* profiles. Times are on the watcher's clock; `offsetUs` is each profile's
|
|
50
|
+
* clock minus that one. `appDir` is one path or several. Pure.
|
|
51
|
+
*/
|
|
52
|
+
export function freezeFrom(profiles, { fromUs, toUs, appDir = '' }) {
|
|
53
|
+
const appDirs = [appDir].flat().filter(Boolean);
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
let total = 0;
|
|
56
|
+
let idle = 0;
|
|
57
|
+
let gc = 0;
|
|
58
|
+
for (const { profile, offsetUs = 0 } of profiles) {
|
|
59
|
+
if (!profile?.nodes?.length || !profile.samples?.length) continue;
|
|
60
|
+
const byId = new Map(profile.nodes.map((n) => [n.id, n]));
|
|
61
|
+
const parent = new Map();
|
|
62
|
+
for (const n of profile.nodes) for (const c of n.children ?? []) parent.set(c, n.id);
|
|
63
|
+
const stacks = new Map();
|
|
64
|
+
const stackOf = (id) => {
|
|
65
|
+
if (stacks.has(id)) return stacks.get(id);
|
|
66
|
+
const frames = [];
|
|
67
|
+
for (let at = id; at != null; at = parent.get(at)) {
|
|
68
|
+
const cf = byId.get(at)?.callFrame;
|
|
69
|
+
if (!cf || cf.functionName === '(root)') continue;
|
|
70
|
+
frames.push({ fn: cf.functionName || '(anonymous)', file: fileOf(cf.url, appDirs), line: cf.url ? cf.lineNumber + 1 : 0 });
|
|
71
|
+
}
|
|
72
|
+
const leaf = frames[0]?.fn ?? '(program)';
|
|
73
|
+
const kind = frames.length <= 1 && IDLE.has(leaf) ? 'idle' : leaf === GC ? 'gc' : 'busy';
|
|
74
|
+
const first = frames.findIndex(ours);
|
|
75
|
+
const mine = first < 0 ? [] : frames.slice(first);
|
|
76
|
+
const s = { kind, frames, where: mine[0] ? named(mine[0]) : leaf, key: `${leaf}|${mine.slice(0, 10).map(named).join('<')}` };
|
|
77
|
+
stacks.set(id, s);
|
|
78
|
+
return s;
|
|
79
|
+
};
|
|
80
|
+
let t = profile.startTime - offsetUs;
|
|
81
|
+
for (let i = 0; i < profile.samples.length; i += 1) {
|
|
82
|
+
t += profile.timeDeltas?.[i] ?? 0;
|
|
83
|
+
if (t < fromUs || t > toUs) continue;
|
|
84
|
+
total += 1;
|
|
85
|
+
const s = stackOf(profile.samples[i]);
|
|
86
|
+
if (s.kind === 'idle') { idle += 1; continue; }
|
|
87
|
+
if (s.kind === 'gc') gc += 1;
|
|
88
|
+
const g = groups.get(s.key) ?? { samples: 0, s };
|
|
89
|
+
g.samples += 1;
|
|
90
|
+
groups.set(s.key, g);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const busy = total - idle;
|
|
94
|
+
const top = [...groups.values()].sort((a, b) => b.samples - a.samples).slice(0, 3);
|
|
95
|
+
return {
|
|
96
|
+
samples: total,
|
|
97
|
+
busy: total ? Math.round((busy / total) * 100) / 100 : 0,
|
|
98
|
+
gc: busy ? Math.round((gc / busy) * 100) / 100 : 0,
|
|
99
|
+
where: top[0]?.s.where ?? null,
|
|
100
|
+
stacks: top.map((g) => ({ share: Math.round((g.samples / busy) * 100) / 100, frames: g.s.frames.slice(0, FRAMES_KEPT).map(named) })),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One freeze's line, appended to today's file; older files than a week go. */
|
|
105
|
+
export function writeFreeze(dir, line, { keepDays = KEEP_DAYS } = {}) {
|
|
106
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
107
|
+
const at = Date.parse(line.at) || Date.now();
|
|
108
|
+
fs.appendFileSync(path.join(dir, `freeze-${dayOf(at)}.jsonl`), `${JSON.stringify(line)}\n`);
|
|
109
|
+
const oldest = dayOf(new Date(at).setDate(new Date(at).getDate() - (keepDays - 1)));
|
|
110
|
+
for (const f of fs.readdirSync(dir)) {
|
|
111
|
+
const m = f.match(FILE);
|
|
112
|
+
if (m && m[1] < oldest) fs.rmSync(path.join(dir, f), { force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Starts the watcher for this thread. `onFreeze` hears each line as it is
|
|
118
|
+
* written. Never throws: a recorder that breaks the app is worse than a gap.
|
|
119
|
+
*/
|
|
120
|
+
export function startFreezeRecorder({ dir, appDir = '', thresholdMs = THRESHOLD_MS, rotateMs = 60_000, intervalUs = 10_000, onFreeze = () => {} } = {}) {
|
|
121
|
+
const beat = new SharedArrayBuffer(8);
|
|
122
|
+
const stamp = new BigInt64Array(beat);
|
|
123
|
+
const tick = () => Atomics.store(stamp, 0, process.hrtime.bigint());
|
|
124
|
+
tick();
|
|
125
|
+
const timer = setInterval(tick, BEAT_MS);
|
|
126
|
+
timer.unref?.();
|
|
127
|
+
let worker = null;
|
|
128
|
+
try {
|
|
129
|
+
worker = new Worker(new URL('./freeze-recorder-thread.mjs', import.meta.url), {
|
|
130
|
+
workerData: { beat, dir, appDir: [appDir, realPath(appDir)], thresholdMs, rotateMs, intervalUs },
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
clearInterval(timer);
|
|
134
|
+
console.warn(`zero: freezes will not be recorded: ${err.message}`);
|
|
135
|
+
return { stop() {} };
|
|
136
|
+
}
|
|
137
|
+
worker.unref();
|
|
138
|
+
worker.on('message', (m) => {
|
|
139
|
+
if (m?.freeze) { try { onFreeze(m.freeze); } catch { /* the line is already on disk */ } }
|
|
140
|
+
if (m?.gaveUp) console.warn(`zero: freezes will not be recorded: ${m.gaveUp}`);
|
|
141
|
+
});
|
|
142
|
+
worker.on('error', (err) => { clearInterval(timer); console.warn(`zero: the freeze recorder stopped: ${err.message}`); });
|
|
143
|
+
return {
|
|
144
|
+
stop() { clearInterval(timer); worker?.terminate().catch(() => {}); },
|
|
145
|
+
};
|
|
146
|
+
}
|
package/main/fresh-user.mjs
CHANGED
|
@@ -44,6 +44,22 @@ export function relaunchCommand({ execPath = process.execPath, argv = process.ar
|
|
|
44
44
|
return { bin: execPath, args: appPath ? [appPath] : [] };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* THE NOTES ARE THE HONEST PART. Each is something a fresh copy can be missing
|
|
49
|
+
* without looking broken, and a copy that quietly cannot run an agent is one
|
|
50
|
+
* that lies, so they are said rather than assumed.
|
|
51
|
+
*
|
|
52
|
+
* Pure, and reads nothing, because what it says about a missing Claude Code
|
|
53
|
+
* otherwise depends on whether the computer running the test has one.
|
|
54
|
+
*/
|
|
55
|
+
export function freshUserNotes({ claudeBin, keychain, agents } = {}, { withTools = true, withAgents = true } = {}) {
|
|
56
|
+
const notes = [];
|
|
57
|
+
if (withTools && !claudeBin) notes.push('That copy has no Claude Code of its own, so its setup offers to set one up or to use ChatGPT.');
|
|
58
|
+
if (!keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
|
|
59
|
+
if (withAgents && !agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
|
|
60
|
+
return notes;
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
/**
|
|
48
64
|
* Open one. Returns what happened, in the words the app will show her: this
|
|
49
65
|
* is reported to the window rather than thrown, because a row in ⌘K that
|
|
@@ -90,15 +106,10 @@ export function openFreshUser({
|
|
|
90
106
|
return { ok: false, error: `Could not start a second ${NAME}: ${err?.message ?? err}`, home: prepared.home };
|
|
91
107
|
}
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (withTools && !prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
|
|
98
|
-
if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
|
|
99
|
-
if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
|
|
100
|
-
|
|
101
|
-
return { ok: true, home: prepared.home, agents: prepared.agents, notes, pid: child?.pid ?? null };
|
|
109
|
+
return {
|
|
110
|
+
ok: true, home: prepared.home, agents: prepared.agents, pid: child?.pid ?? null,
|
|
111
|
+
notes: freshUserNotes(prepared, { withTools, withAgents }),
|
|
112
|
+
};
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
/**
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// runs whole inside a worker thread, and if the thread cannot start the job
|
|
10
10
|
// runs here, the old way.
|
|
11
11
|
import { Worker } from 'node:worker_threads';
|
|
12
|
-
import * as
|
|
12
|
+
import * as git from './git-change.mjs';
|
|
13
|
+
import { saveRunChange } from './run-change.mjs';
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const change = { ...git, saveRunChange };
|
|
16
|
+
const JOBS = new Set(['snapshotRepo', 'saveRunChange']);
|
|
15
17
|
|
|
16
18
|
let worker = null;
|
|
17
19
|
let broken = false;
|
|
@@ -55,6 +57,11 @@ function thread() {
|
|
|
55
57
|
return worker;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Settles once no job is waiting on the thread. */
|
|
61
|
+
export async function __gitJobsIdle() {
|
|
62
|
+
while (waiting.size) await new Promise((resolve) => { setTimeout(resolve, 10); });
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
/** Run one of git-change.mjs's exports without blocking this thread. */
|
|
59
66
|
export function gitJob(name, ...args) {
|
|
60
67
|
if (!JOBS.has(name)) return Promise.reject(new Error(`not a git job: ${name}`));
|
|
@@ -65,6 +72,13 @@ export function gitJob(name, ...args) {
|
|
|
65
72
|
const id = ++seq;
|
|
66
73
|
waiting.set(id, job);
|
|
67
74
|
w.ref();
|
|
68
|
-
|
|
75
|
+
try {
|
|
76
|
+
w.postMessage({ id, name, args });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Arguments a thread cannot be handed (a function, say) run here.
|
|
79
|
+
waiting.delete(id);
|
|
80
|
+
if (!waiting.size) w.unref();
|
|
81
|
+
runHere(job);
|
|
82
|
+
}
|
|
69
83
|
});
|
|
70
84
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// THE OTHER END OF main/git-change-offthread.mjs. One job at a time, in order.
|
|
2
2
|
import { parentPort } from 'node:worker_threads';
|
|
3
|
-
import * as
|
|
3
|
+
import * as git from './git-change.mjs';
|
|
4
|
+
import { saveRunChange } from './run-change.mjs';
|
|
5
|
+
|
|
6
|
+
const jobs = { ...git, saveRunChange };
|
|
4
7
|
|
|
5
8
|
parentPort.on('message', ({ id, name, args }) => {
|
|
6
9
|
try {
|
|
7
|
-
parentPort.postMessage({ id, ok: true, value:
|
|
10
|
+
parentPort.postMessage({ id, ok: true, value: jobs[name](...args) });
|
|
8
11
|
} catch (error) {
|
|
9
12
|
parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
|
|
10
13
|
}
|
|
@@ -10,11 +10,20 @@ import { claudeModelRows } from '../claude-models.mjs';
|
|
|
10
10
|
import { linkAccountTooling } from '../account-tooling.mjs';
|
|
11
11
|
import { streamInputArgs, attachClaudeInput } from '../claude-input.mjs';
|
|
12
12
|
import { claudeLaunchEnv } from '../claude-launch-env.mjs';
|
|
13
|
+
import { cloudClaudeMode, cloudPermissionArgs } from '../cloud-permission.mjs';
|
|
13
14
|
import { claudeActivity } from '../agent-activity.mjs';
|
|
14
15
|
import { captureStream, streamingText, summarizeStreamLine, traceStreamLine } from './claude-stream.mjs';
|
|
15
16
|
import { definitionMethods, commonUpdatePlan } from './common.mjs';
|
|
17
|
+
import { findTranscript } from '../transcript-search.mjs';
|
|
16
18
|
const definition = harnessDefinition('claude');
|
|
17
19
|
|
|
20
|
+
// THE SETTINGS LAYER THAT TAKES A METERED KEY BACK OFF A RUN. Claude Code reads
|
|
21
|
+
// `env.ANTHROPIC_API_KEY` and `apiKeyHelper` out of a person's own and a
|
|
22
|
+
// project's settings files, past any scrub of the environment, and `--settings`
|
|
23
|
+
// outranks both (measured on 2.1.301, w-6d8c543a43). Every Claude Code run the
|
|
24
|
+
// app starts carries it: workers in their settings file, small calls inline.
|
|
25
|
+
export const NO_METERED_KEY = Object.freeze({ env: Object.freeze({ ANTHROPIC_API_KEY: '' }), apiKeyHelper: '' });
|
|
26
|
+
|
|
18
27
|
export const claudeHarness = {
|
|
19
28
|
...definitionMethods(definition),
|
|
20
29
|
discover: resolveClaudeBin,
|
|
@@ -33,7 +42,27 @@ export const claudeHarness = {
|
|
|
33
42
|
return [path.join(home, '.claude.json'), ...definition.credentialFiles.map(file => path.join(dir, file))];
|
|
34
43
|
},
|
|
35
44
|
tooling: profile => linkAccountTooling(profile),
|
|
36
|
-
|
|
45
|
+
// A cloud run's command line and environment. A kit for the phone is made
|
|
46
|
+
// from the same two (Supervisor#_writeKit), so the cloud's turn is this one.
|
|
47
|
+
cloudLaunch(sup, plan, { item, profile, env }) {
|
|
48
|
+
return {
|
|
49
|
+
args: streamInputArgs(definition.run, cloudPermissionArgs(plan.args, cloudClaudeMode(sup.config.cloudPermission, plan.answerMode))),
|
|
50
|
+
env: { ...env, STORE_ACCOUNT_ID: sup.config.accountId, ...storeRootEnv(sup.config.storeRoot),
|
|
51
|
+
CLAUDE_CONFIG_DIR: sup._claudeConfigDir(profile), ZERO_PRODUCT: item.product, ZERO_ITEM: item.id, ...sup._cloudDocsEnv(item) },
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
spawn(sup, plan, { cwd, item, profile, env, cloud = false }) {
|
|
55
|
+
// A CLOUD RUN: THE SAME ARGUMENTS, A DIFFERENT PROGRAM (docs/cloud/PLAN.md).
|
|
56
|
+
// main/cloud/run-cli.mjs runs Claude Code in the sandbox with exactly these
|
|
57
|
+
// arguments and is, to everything above, the worker: its stdout is the
|
|
58
|
+
// agent's stream, its stdin reaches the agent, its exit comes after the
|
|
59
|
+
// agent's work is back in `cwd`. The account folder is always named,
|
|
60
|
+
// because the run unpacks the transcript and tooling into exactly that
|
|
61
|
+
// path. No memory-gate variables: that gate is about this Mac's memory.
|
|
62
|
+
if (cloud) {
|
|
63
|
+
const launch = claudeHarness.cloudLaunch(sup, plan, { item, profile, env });
|
|
64
|
+
return sup._spawnCloudRelay(launch.args, { cwd, env: launch.env });
|
|
65
|
+
}
|
|
37
66
|
// The binary is the one this definition names, not a field spelled out
|
|
38
67
|
// here: that is the last place an engine of this family needed code.
|
|
39
68
|
return spawn(sup.config[definition.binKey], streamInputArgs(definition.run, plan.args), {
|
|
@@ -60,20 +89,12 @@ export const claudeHarness = {
|
|
|
60
89
|
transcript(sup, rec) {
|
|
61
90
|
if (!rec?.sessionId) return null;
|
|
62
91
|
const home = rec.profile && rec.profile !== 'default' ? rec.profile : path.join(os.homedir(), '.claude');
|
|
63
|
-
|
|
64
|
-
const slug = String(rec.cwd ?? '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
65
|
-
const direct = path.join(projects, slug, `${rec.sessionId}.jsonl`);
|
|
66
|
-
if (fs.existsSync(direct)) return direct;
|
|
67
|
-
try { for (const dir of fs.readdirSync(projects)) {
|
|
68
|
-
const file = path.join(projects, dir, `${rec.sessionId}.jsonl`);
|
|
69
|
-
if (fs.existsSync(file)) return file;
|
|
70
|
-
} } catch {}
|
|
71
|
-
return null;
|
|
92
|
+
return findTranscript(path.join(home, 'projects'), rec.sessionId, rec.cwd, { look: rec.cloud ? 'never' : 'now' });
|
|
72
93
|
},
|
|
73
94
|
change: () => null,
|
|
74
95
|
release: () => {},
|
|
75
96
|
memoryGate: (sup, item) => sup.memoryGateEnv(item),
|
|
76
|
-
smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model],
|
|
97
|
+
smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model, '--settings', JSON.stringify(NO_METERED_KEY)],
|
|
77
98
|
updateSettings: ({ home }) => {
|
|
78
99
|
try { return JSON.parse(fs.readFileSync(path.join(process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json'), 'utf8')); }
|
|
79
100
|
catch { return {}; }
|
package/main/harnesses/codex.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { harnessDefinition } from '../../shared/harness-definitions.mjs';
|
|
|
3
3
|
import { resolveCodexBin } from '../codex-bin.mjs';
|
|
4
4
|
import { codexLaunchEnv } from '../codex-launch-env.mjs';
|
|
5
5
|
import { codexModels, codexDefaultModel, codexModelLevels } from '../codex-models.mjs';
|
|
6
|
-
import { codexTranscriptFile, workerThreadParams } from '../codex-session.mjs';
|
|
6
|
+
import { codexTranscriptFile, workerThreadParams, refusedCodexWorker } from '../codex-session.mjs';
|
|
7
|
+
import { codexKeyRefusal } from '../codex-account.mjs';
|
|
7
8
|
import { compactCodexThread } from '../codex-compaction.mjs';
|
|
8
9
|
import { captureCodexEvent, codexStreamingText, summarizeCodexEvent, traceCodexEvent, rememberCodexChange, changeFromCodexTurn } from '../codex.mjs';
|
|
9
10
|
import { codexActivity } from '../agent-activity.mjs';
|
|
@@ -29,7 +30,13 @@ export const codexHarness = {
|
|
|
29
30
|
// rather than inheriting the refusal meant for an engine that has nowhere to
|
|
30
31
|
// put them (main/harnesses/instructions-file.mjs).
|
|
31
32
|
instructions: () => ({ by: 'thread' }),
|
|
32
|
-
|
|
33
|
+
// AN API-KEY LOGIN IS REFUSED HERE, before an app-server is asked anything:
|
|
34
|
+
// Codex reads the key out of auth.json itself, past the worker scrub, and
|
|
35
|
+
// bills every call to it (main/codex-account.mjs `codexKeyRefusal`).
|
|
36
|
+
spawn: (sup, plan, context) => {
|
|
37
|
+
const refusal = codexKeyRefusal(sup._codexProfileHome(context?.profile ?? 'default'));
|
|
38
|
+
return refusal ? refusedCodexWorker(refusal) : sup._spawnCodexWorker(plan, context);
|
|
39
|
+
},
|
|
33
40
|
readers: () => ({ capture: captureCodexEvent, stream: codexStreamingText, summarize: summarizeCodexEvent, trace: traceCodexEvent, activity: codexActivity }),
|
|
34
41
|
subscribe(child, session, absorb) {
|
|
35
42
|
child.on('event', (method, params) => { rememberCodexChange(session, method, params); absorb(method, params); });
|
|
@@ -17,22 +17,80 @@
|
|
|
17
17
|
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import { appSessionFolders } from './agent-homes.mjs';
|
|
20
|
-
import { RECENT_DAYS, readSessionThreads } from './agent-sessions.mjs';
|
|
20
|
+
import { RECENT_DAYS, readProjectChats, readSessionThreads } from './agent-sessions.mjs';
|
|
21
21
|
import { readCodexThreads } from './codex-threads.mjs';
|
|
22
22
|
|
|
23
23
|
export const OLDER_DAYS = 90;
|
|
24
24
|
export const OLDER_MOST = 20;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// `readCodex` is the app's own reader off the main process (codex-threads.mjs
|
|
27
|
+
// readCodexThreadsOffThread): a large Codex folder takes seconds to read.
|
|
28
|
+
export async function readImportableThreads({ config = null, supervisor = null, env = process.env, shellEnv, home = os.homedir(), now = Date.now(), readCodex = readCodexThreads } = {}) {
|
|
27
29
|
const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home, ...(shellEnv ? { shellEnv } : {}) });
|
|
28
|
-
const read = (days, max) => {
|
|
30
|
+
const read = async (days, max) => {
|
|
29
31
|
let claude = [];
|
|
30
32
|
let codex = [];
|
|
31
33
|
try { ({ threads: claude } = readSessionThreads({ home, dirs, now, days, ...(max ? { max } : {}) })); } catch { claude = []; }
|
|
32
|
-
try { ({ threads: codex } =
|
|
34
|
+
try { ({ threads: codex } = await readCodex({ home, codexDirs, now, days })); } catch { codex = []; }
|
|
33
35
|
return [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
|
|
34
36
|
};
|
|
35
|
-
const recent = read(RECENT_DAYS);
|
|
37
|
+
const recent = await read(RECENT_DAYS);
|
|
36
38
|
if (recent.length) return recent;
|
|
37
|
-
return read(OLDER_DAYS, OLDER_MOST).slice(0, OLDER_MOST);
|
|
39
|
+
return (await read(OLDER_DAYS, OLDER_MOST)).slice(0, OLDER_MOST);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the watch that brings new chats in reads (main/new-chats-watch.mjs):
|
|
43
|
+
* Claude Code chats in the projects' own folders since `since`, and the last
|
|
44
|
+
* `days` of Codex. */
|
|
45
|
+
export async function readRecentThreads({
|
|
46
|
+
config = null, supervisor = null, env = process.env, home = os.homedir(), now = Date.now(),
|
|
47
|
+
days = 1, since = 0, folders = [], readCodex = readCodexThreads,
|
|
48
|
+
} = {}) {
|
|
49
|
+
const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home });
|
|
50
|
+
let claude = [];
|
|
51
|
+
let codex = [];
|
|
52
|
+
try { claude = readProjectChats({ home, dirs, folders, since }); } catch { claude = []; }
|
|
53
|
+
try { ({ threads: codex } = await readCodex({ home, codexDirs, now, days })); } catch { codex = []; }
|
|
54
|
+
return [...claude, ...codex];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ------------------------ AND THE ONE THE SEARCH USES ---------------------- */
|
|
58
|
+
// THE CARD'S LIST IS TEN DAYS. THE NAME FIELD IS NOT (w-6418318cd5, from
|
|
59
|
+
// GitHub #31). A chat you had in July could not be brought in at all, so a
|
|
60
|
+
// field that only searched the same ten days would be decoration.
|
|
61
|
+
//
|
|
62
|
+
// IT IS A SECOND READ, NOT A WIDER FIRST ONE. The ten-day list is what the
|
|
63
|
+
// card draws the moment it opens and it has to be quick; this one is taken
|
|
64
|
+
// once, lazily, the first time somebody types, and the window keeps it (see
|
|
65
|
+
// `zero:search-agent-threads` in main/ipc.mjs) so every keystroke after that
|
|
66
|
+
// is a filter rather than a disk walk.
|
|
67
|
+
//
|
|
68
|
+
// AND IT CANNOT PROMISE EVERYTHING. A chat's name lives inside its transcript,
|
|
69
|
+
// and the Mac this was written on holds 29,703 of them. So it goes newest
|
|
70
|
+
// first until the cap or the budget stops it and reports `reached`, the moment
|
|
71
|
+
// it gave up at, which the card prints. Eight seconds rather than three: this
|
|
72
|
+
// is the read somebody is waiting on, watching a spinner, having already typed
|
|
73
|
+
// the word they want.
|
|
74
|
+
|
|
75
|
+
export const SEARCH_DAYS = 365;
|
|
76
|
+
const SEARCH_MOST = 6000;
|
|
77
|
+
const SEARCH_BUDGET_MS = 8000;
|
|
78
|
+
|
|
79
|
+
export async function readSearchableThreads({
|
|
80
|
+
config = null, supervisor = null, env = process.env, shellEnv, home = os.homedir(), now = Date.now(),
|
|
81
|
+
days = SEARCH_DAYS, max = SEARCH_MOST, budgetMs = SEARCH_BUDGET_MS, readCodex = readCodexThreads,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home, ...(shellEnv ? { shellEnv } : {}) });
|
|
84
|
+
let claude = [];
|
|
85
|
+
let codex = [];
|
|
86
|
+
let reached = 0;
|
|
87
|
+
try {
|
|
88
|
+
({ threads: claude, reached } = readSessionThreads({ home, dirs, now, days, max, budgetMs, ends: false }));
|
|
89
|
+
} catch { claude = []; }
|
|
90
|
+
// Codex keeps its names in an index of its own (main/codex-threads.mjs), so
|
|
91
|
+
// its side costs no transcript reads and never gives up early. Only the
|
|
92
|
+
// Claude side can run out of budget, which is why `reached` comes off it.
|
|
93
|
+
try { ({ threads: codex } = await readCodex({ home, codexDirs, now, days })); } catch { codex = []; }
|
|
94
|
+
const threads = [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
|
|
95
|
+
return { threads, reached };
|
|
38
96
|
}
|