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/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-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,6 +17,17 @@ 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
|
|
|
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
|
+
|
|
19
31
|
// THE PROMPT LEAVES THE LINE WHEN REPLIES ARRIVE ON STDIN. Both the print flag
|
|
20
32
|
// and the input-format flags are read off the engine's `run` definition, so an
|
|
21
33
|
// engine of this family that spells either of them differently is a definition
|
|
@@ -45,13 +57,35 @@ export function attachClaudeInput(child) {
|
|
|
45
57
|
// end of the run, and closing there killed the suite it was waiting on
|
|
46
58
|
// (tests/an-agent-waiting-on-its-own-tests-does-not-come-back-to-you).
|
|
47
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();
|
|
48
65
|
// Input only ever closes at a result with nothing out, so a closed input is
|
|
49
66
|
// the one proof the run reached its end. A session that exits with input
|
|
50
67
|
// still open was stopped part-way, whatever its last turn said
|
|
51
68
|
// (tests/a-worker-whose-helpers-are-still-working-does-not-come-back-to-you).
|
|
52
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;
|
|
53
76
|
const closeInput = () => { closed = true; ended = true; clearTimeout(wakeTimer); child.stdin.end(); };
|
|
54
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
|
+
};
|
|
55
89
|
const fail = reason => {
|
|
56
90
|
closed = true;
|
|
57
91
|
for (const request of pending.values()) {
|
|
@@ -81,12 +115,20 @@ export function attachClaudeInput(child) {
|
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
117
|
if (event.type === 'system' && event.subtype === 'bridge_state') child.emit('bridge-state', event);
|
|
84
|
-
|
|
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; }
|
|
85
127
|
if (event.type === 'user' && !event.parent_tool_use_id && typeof event.uuid === 'string' && !seenUsers.has(event.uuid)) {
|
|
86
128
|
seenUsers.add(event.uuid);
|
|
87
129
|
const content = event.message?.content;
|
|
88
130
|
const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
|
|
89
|
-
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);
|
|
90
132
|
if (text) { atResult = false; child.emit('input-turn', event); }
|
|
91
133
|
}
|
|
92
134
|
if (event.type === 'user' && pending.has(event.uuid)) {
|
|
@@ -109,7 +151,9 @@ export function attachClaudeInput(child) {
|
|
|
109
151
|
}
|
|
110
152
|
if (event.type === 'result') {
|
|
111
153
|
atResult = true;
|
|
154
|
+
inTurn = false;
|
|
112
155
|
if (mayClose()) closeInput();
|
|
156
|
+
else if (waiting.length) nameWhatIsStillOut();
|
|
113
157
|
}
|
|
114
158
|
}
|
|
115
159
|
});
|
|
@@ -136,7 +180,7 @@ export function attachClaudeInput(child) {
|
|
|
136
180
|
// ONLY WHILE A MESSAGE OF HERS IS WAITING. The cut turn ends in a result,
|
|
137
181
|
// and with nothing pending that result closes the input and ends the run.
|
|
138
182
|
child.interrupt = () => {
|
|
139
|
-
if (closed || !pending.
|
|
183
|
+
if (closed || ![...pending.values()].some(request => !request.taken)) return Promise.resolve({ interrupted: false });
|
|
140
184
|
child.lastInterruptAt = Date.now();
|
|
141
185
|
return child.control({ subtype: 'interrupt' }).then(() => ({ interrupted: true }));
|
|
142
186
|
};
|
|
@@ -153,7 +197,7 @@ export function attachClaudeInput(child) {
|
|
|
153
197
|
child.steer = text => new Promise((resolve, reject) => {
|
|
154
198
|
if (closed || !child.stdin.writable) return reject(turnClosed('The turn has ended. Send the message again to continue it.'));
|
|
155
199
|
const uuid = randomUUID();
|
|
156
|
-
pending.set(uuid, { resolve, reject });
|
|
200
|
+
pending.set(uuid, { resolve, reject, taken: !inTurn });
|
|
157
201
|
try {
|
|
158
202
|
child.stdin.write(JSON.stringify({ type: 'user', uuid, message: { role: 'user', content: text }, parent_tool_use_id: null, session_id: '' }) + '\n');
|
|
159
203
|
} catch (error) {
|
package/main/claude-plan.mjs
CHANGED
|
@@ -96,8 +96,28 @@ export function isMaxTier(raw) {
|
|
|
96
96
|
* Claude Code has not fetched a profile for yet: all of them come back
|
|
97
97
|
* `known: false`, which every caller here treats as "leave everything exactly
|
|
98
98
|
* as it was". */
|
|
99
|
+
const planCache = new Map();
|
|
100
|
+
|
|
99
101
|
export function readPlan(profile, { home = os.homedir(), file = null } = {}) {
|
|
100
102
|
const at = file ?? planFile(profile, home);
|
|
103
|
+
// Keyed on the file's mtime AND its size, as accountIdentity is keyed on the
|
|
104
|
+
// mtime alone: the cloud tiles ask on every snapshot.
|
|
105
|
+
//
|
|
106
|
+
// THE SIZE IS THERE BECAUSE AN MTIME CAN STAND STILL (w-dac993a554). Two
|
|
107
|
+
// writes inside one tick of the filesystem's clock carry the same mtime, and
|
|
108
|
+
// the cache then served the first one's plan for ever. A subscription
|
|
109
|
+
// renewed in that window read as ended until the file was written again, so
|
|
110
|
+
// `Supervisor#_forgetRenewedSubscriptions` never let the account back in.
|
|
111
|
+
let stat = null;
|
|
112
|
+
try { stat = fs.statSync(at); } catch { /* read below says none */ }
|
|
113
|
+
const hit = planCache.get(at);
|
|
114
|
+
if (stat && hit?.mtime === stat.mtimeMs && hit?.size === stat.size) return hit.plan;
|
|
115
|
+
const plan = readPlanNow(at);
|
|
116
|
+
if (stat) planCache.set(at, { mtime: stat.mtimeMs, size: stat.size, plan });
|
|
117
|
+
return plan;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readPlanNow(at) {
|
|
101
121
|
const none = { known: false, max: false, label: null, tier: null, from: null, file: at };
|
|
102
122
|
let account = null;
|
|
103
123
|
try {
|
|
@@ -128,6 +148,34 @@ export function readPlan(profile, { home = os.homedir(), file = null } = {}) {
|
|
|
128
148
|
return { known: true, ended: false, max: isMaxTier(tier), label: planLabel(tier), tier, from, file: at };
|
|
129
149
|
}
|
|
130
150
|
|
|
151
|
+
/**
|
|
152
|
+
* WHICH ACCOUNT AND PLAN ONE LOGIN IS ON, as a string that changes only when
|
|
153
|
+
* one of them does: an upgrade, extra usage switched on, a renewal, or a
|
|
154
|
+
* /login as somebody else. Claude Code rewrites this file all day for other
|
|
155
|
+
* reasons, so only these fields count (named as Claude Code 2.1.305 writes
|
|
156
|
+
* them). Null when the file names no account or cannot be read.
|
|
157
|
+
*/
|
|
158
|
+
const PLAN_FIELDS = ['accountUuid', 'organizationUuid', 'organizationType', 'organizationRateLimitTier',
|
|
159
|
+
'userRateLimitTier', 'seatTier', 'billingType', 'hasExtraUsageEnabled', 'planDisplayName', 'subscriptionCreatedAt'];
|
|
160
|
+
const stampCache = new Map();
|
|
161
|
+
|
|
162
|
+
export function planStamp(profile, { home = os.homedir(), file = null } = {}) {
|
|
163
|
+
const at = file ?? planFile(profile, home);
|
|
164
|
+
let mtime;
|
|
165
|
+
try { mtime = fs.statSync(at).mtimeMs; } catch { return null; }
|
|
166
|
+
const hit = stampCache.get(at);
|
|
167
|
+
if (hit?.mtime === mtime) return hit.stamp;
|
|
168
|
+
let stamp = null;
|
|
169
|
+
try {
|
|
170
|
+
const account = JSON.parse(fs.readFileSync(at, 'utf8'))?.oauthAccount;
|
|
171
|
+
if (account?.accountUuid) {
|
|
172
|
+
stamp = JSON.stringify(PLAN_FIELDS.map((k) => (k === 'hasExtraUsageEnabled' ? account[k] === true : account[k] ?? null)));
|
|
173
|
+
}
|
|
174
|
+
} catch { /* half written: the next write has a new mtime and is read again */ }
|
|
175
|
+
stampCache.set(at, { mtime, stamp });
|
|
176
|
+
return stamp;
|
|
177
|
+
}
|
|
178
|
+
|
|
131
179
|
/**
|
|
132
180
|
* THE PLAN ON EVERY ACCOUNT AGENTBOX IS CONFIGURED TO RUN ON, in the same order
|
|
133
181
|
* the supervisor round-robins them. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Whether Claude Code still has a session open somewhere, read off the state
|
|
2
|
+
// file it keeps for each open session (`<home>/sessions/<pid>.json`, with the
|
|
3
|
+
// session id and the pid holding it). A state file whose process has gone is a
|
|
4
|
+
// session somebody closed.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
function alive(pid) {
|
|
9
|
+
try { process.kill(pid, 0); return true; } catch (err) { return err?.code === 'EPERM'; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function claudeSessionOpen(sessionId, homes = []) {
|
|
13
|
+
const id = String(sessionId ?? '').trim();
|
|
14
|
+
if (!id) return false;
|
|
15
|
+
for (const home of new Set(homes.filter(Boolean))) {
|
|
16
|
+
const dir = path.join(home, 'sessions');
|
|
17
|
+
let names = [];
|
|
18
|
+
try { names = fs.readdirSync(dir); } catch { continue; }
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
if (!name.endsWith('.json')) continue;
|
|
21
|
+
let rec = null;
|
|
22
|
+
try { rec = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { continue; }
|
|
23
|
+
const pid = Number(rec?.pid);
|
|
24
|
+
if (rec?.sessionId === id && pid > 0 && alive(pid)) return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { attachClaudeInput } from './claude-input.mjs';
|
|
3
|
+
|
|
4
|
+
// No user message is submitted. The CLI owns authentication and the provider request.
|
|
5
|
+
export function runClaudeUsage(bin, args, opts, done) {
|
|
6
|
+
let child, timer, killTimer, finished = false, bytes = 0;
|
|
7
|
+
const finish = (error, report = '') => {
|
|
8
|
+
if (finished) return;
|
|
9
|
+
finished = true;
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
if (child?.exitCode == null && child?.pid) {
|
|
12
|
+
child.kill('SIGTERM');
|
|
13
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
14
|
+
killTimer.unref();
|
|
15
|
+
}
|
|
16
|
+
done(error, report);
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
child = attachClaudeInput(spawn(bin, args, { cwd: opts.cwd, env: opts.env, stdio: ['pipe', 'pipe', 'ignore'] }));
|
|
20
|
+
child.holdInput(true);
|
|
21
|
+
child.once('error', error => finish(error));
|
|
22
|
+
child.once('exit', () => {
|
|
23
|
+
clearTimeout(killTimer);
|
|
24
|
+
finish(Error('Claude Code closed before reporting usage.'));
|
|
25
|
+
});
|
|
26
|
+
child.stdout.on('data', data => {
|
|
27
|
+
bytes += data.length;
|
|
28
|
+
if (bytes > opts.maxBuffer) finish(Error('Claude Code sent too much usage output.'));
|
|
29
|
+
});
|
|
30
|
+
timer = setTimeout(() => finish(Error('Claude Code did not report usage in time.')), opts.timeout);
|
|
31
|
+
void (async () => {
|
|
32
|
+
try {
|
|
33
|
+
await child.control({ subtype: 'initialize' }, opts.timeout);
|
|
34
|
+
const report = await child.control({ subtype: 'get_usage', skip_behaviors: true }, opts.timeout);
|
|
35
|
+
finish(null, JSON.stringify(report));
|
|
36
|
+
} catch (error) { finish(error); }
|
|
37
|
+
})();
|
|
38
|
+
} catch (error) { finish(error); }
|
|
39
|
+
return child;
|
|
40
|
+
}
|
package/main/claude-usage.mjs
CHANGED
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// The pure half is shared/claude-usage.mjs: the text in, the limits out, the
|
|
4
|
-
// words the pill prints. This is the half that shells out, and everything about
|
|
5
|
-
// its shape comes from one measurement.
|
|
6
|
-
//
|
|
7
|
-
// TIMED TWICE ON HER MAC, 2026-08-31: 23.2s and 28.0s for `claude -p '/usage'`.
|
|
8
|
-
// The percentage did not move between the two, which is the other half of the
|
|
9
|
-
// answer: this is not a number that changes second to second, so nothing is lost
|
|
10
|
-
// by reading it every few minutes and everything is lost by reading it on a
|
|
11
|
-
// timer the window waits for.
|
|
12
|
-
//
|
|
13
|
-
// SO IT IS A CACHE WITH A BACKGROUND REFRESH. Only the explicit Settings
|
|
14
|
-
// refresh waits for a result; snapshots always return immediately:
|
|
15
|
-
//
|
|
16
|
-
// - `read` answers instantly, always, from the last measurement or with null.
|
|
17
|
-
// It never blocks and never throws.
|
|
18
|
-
// - A refresh is kicked off when the cached figure is older than STALE_MS,
|
|
19
|
-
// and only ever one at a time.
|
|
20
|
-
// - The window is told when a new one lands, the same way everything else in
|
|
21
|
-
// this app tells it: a push.
|
|
22
|
-
//
|
|
23
|
-
// WHY NOT A SESSION. Agentbox spawns real workers through the supervisor and could
|
|
24
|
-
// have asked one of them. It must not: a worker is a unit of her work, it lands
|
|
25
|
-
// in her inbox and burns a slot in the fleet, and this is a status reading. It
|
|
26
|
-
// gets its own short-lived child process with no store, no brief and no row.
|
|
27
|
-
//
|
|
28
|
-
// WHAT IS NOT SENT ANYWHERE. The output carries her usage figures and nothing
|
|
29
|
-
// else leaves this machine because of it; `main/analytics.mjs` is the only file
|
|
30
|
-
// that sends, and none of its eight counts is fed from here.
|
|
1
|
+
// Cached provider usage, read through Claude Code without starting a model turn.
|
|
2
|
+
// Snapshots never wait; only an explicit refresh waits for the CLI's answer.
|
|
31
3
|
|
|
32
|
-
import { execFile } from 'node:child_process';
|
|
33
4
|
import fs from 'node:fs';
|
|
34
5
|
import os from 'node:os';
|
|
35
6
|
import path from 'node:path';
|
|
36
|
-
import {
|
|
7
|
+
import { readUsageReport } from '../shared/claude-usage.mjs';
|
|
37
8
|
import { localZone, USAGE_UNREADABLE } from '../shared/usage.mjs';
|
|
9
|
+
import { runClaudeUsage } from './claude-usage-process.mjs';
|
|
10
|
+
import { NO_METERED_KEY } from './harnesses/claude.mjs';
|
|
38
11
|
|
|
39
12
|
// WHERE THE READING RUNS, AND WHY IT IS NOT "WHEREVER THE APP IS". An app
|
|
40
13
|
// opened from Finder has `/` as its working directory, and this used to spawn
|
|
@@ -62,9 +35,7 @@ export function usageCwd() {
|
|
|
62
35
|
// than the number itself is reported to.
|
|
63
36
|
export const STALE_MS = 5 * 60_000;
|
|
64
37
|
|
|
65
|
-
|
|
66
|
-
// this the reading is not late, it is not coming.
|
|
67
|
-
const TIMEOUT_MS = 90_000;
|
|
38
|
+
const TIMEOUT_MS = 30_000;
|
|
68
39
|
|
|
69
40
|
// After a failure, how long before it is worth trying again. A machine that is
|
|
70
41
|
// signed out, rate limited, or has no Claude Code at all would otherwise be
|
|
@@ -87,7 +58,7 @@ export class ClaudeUsage {
|
|
|
87
58
|
* the account's name ('default' or its CLAUDE_CONFIG_DIR) and `env` is the
|
|
88
59
|
* environment to run it in. Supervisor.usageAccount is the real one.
|
|
89
60
|
*/
|
|
90
|
-
constructor({ bin, onChange = () => {}, run =
|
|
61
|
+
constructor({ bin, onChange = () => {}, run = runClaudeUsage, now = () => Date.now(), where = usageCwd, account = () => ({ profile: 'default' }) } = {}) {
|
|
91
62
|
this._bin = bin;
|
|
92
63
|
this._selectedProfile = null;
|
|
93
64
|
this._generation = 0;
|
|
@@ -164,7 +135,7 @@ export class ClaudeUsage {
|
|
|
164
135
|
const profile = this._syncProfile();
|
|
165
136
|
const generation = this._generation;
|
|
166
137
|
const bin = (() => { try { return this._bin(); } catch { return null; } })();
|
|
167
|
-
const failure = () => ({ ok: false, error
|
|
138
|
+
const failure = (error = USAGE_UNREADABLE) => ({ ok: false, error });
|
|
168
139
|
if (!bin) { this._missed(); return Promise.resolve(failure()); }
|
|
169
140
|
this._busy = true;
|
|
170
141
|
this._pendingGeneration = generation;
|
|
@@ -178,8 +149,13 @@ export class ClaudeUsage {
|
|
|
178
149
|
this._syncProfile();
|
|
179
150
|
if (generation !== this._generation) { resolve(failure()); this._onChange(); return; }
|
|
180
151
|
// Failed or unreadable output cannot acquire a fresh checked time.
|
|
181
|
-
const limits = err ? [] :
|
|
182
|
-
if (!limits.length) {
|
|
152
|
+
const limits = err ? [] : readUsageReport(stdout, this._now());
|
|
153
|
+
if (!limits.length) {
|
|
154
|
+
this._missed();
|
|
155
|
+
const unsupported = /get_usage.*(?:not supported|unknown)|unknown.*get_usage/i.test(err?.message ?? '');
|
|
156
|
+
resolve(failure(unsupported ? 'Update Claude Code to read usage for this account.' : USAGE_UNREADABLE));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
183
159
|
this._last = { limits, at: this._now(), zone: localZone(), profile };
|
|
184
160
|
resolve({ ok: true, reading: this.peek() });
|
|
185
161
|
// Even unchanged percentages now have a newly checked time to draw.
|
|
@@ -192,7 +168,9 @@ export class ClaudeUsage {
|
|
|
192
168
|
if (profile !== 'default') env.CLAUDE_CONFIG_DIR = profile;
|
|
193
169
|
try { this._run(
|
|
194
170
|
bin,
|
|
195
|
-
['-p', '
|
|
171
|
+
['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
|
|
172
|
+
'--no-session-persistence', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--tools', '',
|
|
173
|
+
'--settings', JSON.stringify({ ...NO_METERED_KEY, disableAllHooks: true })],
|
|
196
174
|
// `cwd` is the whole fix for the permission panels; see usageCwd above.
|
|
197
175
|
{ cwd: this._where(), env, timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' },
|
|
198
176
|
finish,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SIGNING IN TO AGENTBOX CLOUD FROM THIS MAC.
|
|
2
|
+
//
|
|
3
|
+
// A Google sign-in, through the database's own page, is traded once with the
|
|
4
|
+
// cloud server (cloud/gateway/) for a cloud key, kept beside cloud.json; the
|
|
5
|
+
// Mac's cloud.json then names the server, so its runs go there
|
|
6
|
+
// (main/cloud/clients.mjs). The database session itself is not kept. A Mac
|
|
7
|
+
// set up on its owner's own AWS account is never moved onto the server.
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { cloudConfigPath, readCloudConfig, readCloudKey, writeCloudKey, cloudKeyPath, cloudAccountConfig, forgetCloudKey } from './config.mjs';
|
|
12
|
+
import { withDeadline, plainAuthError } from '../team/session.mjs';
|
|
13
|
+
import { signInWithGoogle, cancelGoogleSignIn } from '../team/sign-in.mjs';
|
|
14
|
+
import { Name } from '../../shared/product-name.mjs';
|
|
15
|
+
|
|
16
|
+
const CALL_MS = 30_000;
|
|
17
|
+
const RETURN_URL = 'http://127.0.0.1:54783/auth/callback';
|
|
18
|
+
|
|
19
|
+
export { cloudAccountConfig };
|
|
20
|
+
|
|
21
|
+
async function ask(gateway, route, { method = 'GET', key, body } = {}) {
|
|
22
|
+
const init = { method, headers: { ...(key ? { authorization: `Bearer ${key}` } : {}), ...(body ? { 'content-type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined };
|
|
23
|
+
const res = await fetch(`${gateway.replace(/\/+$/, '')}${route}`, { ...init, signal: AbortSignal.timeout(CALL_MS) });
|
|
24
|
+
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The version of the terms a person accepts now. */
|
|
28
|
+
export async function currentCloudTerms(config) {
|
|
29
|
+
return (await ask(config.gateway, '/v1/terms')).body.terms ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The ways in that the app's sign-in page offers (team/SignInPage.tsx):
|
|
34
|
+
* Google, an email and a password, or a new account. Each gives the access
|
|
35
|
+
* token of a fresh sign-in, held in memory only: the cloud key is what this
|
|
36
|
+
* Mac keeps, so nothing asks the keychain.
|
|
37
|
+
*/
|
|
38
|
+
export function cloudAccountSession({ config, openExternal, log = () => {}, createClient = null }) {
|
|
39
|
+
let lastUrl = null;
|
|
40
|
+
const client = async () => {
|
|
41
|
+
const make = createClient || (await import('@supabase/supabase-js')).createClient;
|
|
42
|
+
const mem = new Map();
|
|
43
|
+
return make(config.supabaseUrl, config.anonKey, {
|
|
44
|
+
auth: { flowType: 'pkce', persistSession: true, autoRefreshToken: false, storage: { getItem: (k) => mem.get(k) ?? null, setItem: (k, v) => mem.set(k, v), removeItem: (k) => mem.delete(k) } },
|
|
45
|
+
global: { fetch: withDeadline() },
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const tokenOf = (data) => {
|
|
49
|
+
if (!data?.session?.access_token) throw new Error('Signing in came back without a session. Try again.');
|
|
50
|
+
return data.session.access_token;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
async google() {
|
|
54
|
+
const c = await client();
|
|
55
|
+
await signInWithGoogle({ client: c, openExternal, returnUrl: config.signInReturn || RETURN_URL, log, onUrl: (u) => { lastUrl = u; } });
|
|
56
|
+
return tokenOf((await c.auth.getSession()).data);
|
|
57
|
+
},
|
|
58
|
+
async email(email, password) {
|
|
59
|
+
const { data, error } = await (await client()).auth.signInWithPassword({ email: String(email ?? '').trim(), password: String(password ?? '') });
|
|
60
|
+
if (error) throw new Error(plainAuthError(error.message));
|
|
61
|
+
return tokenOf(data);
|
|
62
|
+
},
|
|
63
|
+
// With confirmation on, a new account has no session until its email's
|
|
64
|
+
// link is followed: null, and the page says to check the inbox.
|
|
65
|
+
async signUp({ name, email, password }) {
|
|
66
|
+
const { data, error } = await (await client()).auth.signUp({
|
|
67
|
+
email: String(email ?? '').trim(), password: String(password ?? ''),
|
|
68
|
+
options: { data: { full_name: String(name ?? '').trim() || undefined } },
|
|
69
|
+
});
|
|
70
|
+
if (error) throw new Error(plainAuthError(error.message));
|
|
71
|
+
return data?.session?.access_token ?? null;
|
|
72
|
+
},
|
|
73
|
+
cancel: () => cancelGoogleSignIn(),
|
|
74
|
+
reopen: () => (lastUrl ? openExternal(lastUrl) : null),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One press on the sign-in page, to a cloud key. It agrees to the current
|
|
80
|
+
* terms only when the page showed them (`acceptTerms`, and every new account);
|
|
81
|
+
* otherwise a person who never agreed gets `needsTerms`, and `agree` finishes
|
|
82
|
+
* that same sign-in once they do.
|
|
83
|
+
*/
|
|
84
|
+
export async function cloudSignIn({ config, session, method, email, password, name, acceptTerms = false, env = process.env, home = os.homedir() }) {
|
|
85
|
+
let token;
|
|
86
|
+
let agreeing = acceptTerms;
|
|
87
|
+
try {
|
|
88
|
+
if (method === 'google') token = await session.google();
|
|
89
|
+
else if (method === 'email') token = await session.email(email, password);
|
|
90
|
+
else if (method === 'sign-up') {
|
|
91
|
+
agreeing = true;
|
|
92
|
+
token = await session.signUp({ name, email, password });
|
|
93
|
+
if (!token) return { ok: true, confirm: true };
|
|
94
|
+
} else if (method === 'agree') {
|
|
95
|
+
token = session.held;
|
|
96
|
+
agreeing = true;
|
|
97
|
+
if (!token) return { ok: false, error: 'Sign in again to finish.' };
|
|
98
|
+
} else return { ok: false, error: 'No such way to sign in.' };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { ok: false, error: err?.message || String(err), ...(err?.cancelled ? { cancelled: true } : {}) };
|
|
101
|
+
}
|
|
102
|
+
session.held = token;
|
|
103
|
+
const acceptedTerms = agreeing ? await currentCloudTerms(config) : undefined;
|
|
104
|
+
const out = await signInToCloud({ config, acceptedTerms, googleSignIn: async () => token, env, home });
|
|
105
|
+
if (out.ok) { session.held = null; return out; }
|
|
106
|
+
if (out.error === 'terms' && !agreeing) return { ok: false, needsTerms: true, terms: out.terms ?? null };
|
|
107
|
+
return { ok: false, error: out.message || out.error };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `googleSignIn` gives the access token of whichever sign-in the page made.
|
|
111
|
+
export async function signInToCloud({ config, acceptedTerms, name = os.hostname(), env = process.env, home = os.homedir(), googleSignIn }) {
|
|
112
|
+
const now = readCloudConfig(env, home);
|
|
113
|
+
if (now?.bucket) return { ok: false, error: 'own-aws', message: 'This Mac runs the cloud in its own AWS account.' };
|
|
114
|
+
let token;
|
|
115
|
+
try { token = await googleSignIn(); } catch (err) { return { ok: false, error: 'google', message: err?.message || String(err) }; }
|
|
116
|
+
const made = await ask(config.gateway, '/v1/devices', { method: 'POST', key: token, body: { acceptedTerms, name: String(name).slice(0, 80) } });
|
|
117
|
+
if (made.status !== 200 || !made.body.key) return { ok: false, error: made.body.error || 'refused', message: made.body.message || `the cloud said ${made.status}`, ...(made.body.terms ? { terms: made.body.terms } : {}) };
|
|
118
|
+
writeCloudKey(made.body.key, env, home);
|
|
119
|
+
const file = cloudConfigPath(env, home);
|
|
120
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
fs.writeFileSync(file, JSON.stringify({ gateway: config.gateway }, null, 2) + '\n', { mode: 0o600 });
|
|
122
|
+
const me = await ask(config.gateway, '/v1/me', { key: made.body.key });
|
|
123
|
+
return { ok: true, ...(me.status === 200 ? me.body : {}) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The server's cloud and this Mac's key for it, or null when signed out.
|
|
127
|
+
function signedIn(env, home) {
|
|
128
|
+
const gateway = readCloudConfig(env, home)?.gateway;
|
|
129
|
+
const key = gateway ? readCloudKey(env, home) : null;
|
|
130
|
+
return key ? { gateway, key } : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The plan, its agents and hours, and what is used, as the server counts them. */
|
|
134
|
+
export async function cloudPlan({ env = process.env, home = os.homedir() } = {}) {
|
|
135
|
+
const at = signedIn(env, home);
|
|
136
|
+
if (!at) return { signedIn: false };
|
|
137
|
+
const me = await ask(at.gateway, '/v1/me', { key: at.key });
|
|
138
|
+
if (me.status === 401) forgetCloudKey(at.key, env, home);
|
|
139
|
+
return me.status === 200 ? { signedIn: true, ...me.body } : { signedIn: me.status !== 401, error: me.body.error || 'refused', message: me.body.message };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** End every other Mac's cloud key, for a Mac that was lost; this one stays signed in. */
|
|
143
|
+
export async function signOutOtherMacs({ env = process.env, home = os.homedir() } = {}) {
|
|
144
|
+
const at = signedIn(env, home);
|
|
145
|
+
if (!at) return { ok: false, error: 'signed-out', message: `Sign in to ${Name} Cloud first.` };
|
|
146
|
+
const out = await ask(at.gateway, '/v1/devices', { method: 'DELETE', key: at.key, body: { others: true } });
|
|
147
|
+
if (out.status === 401) forgetCloudKey(at.key, env, home);
|
|
148
|
+
return out.status === 200 ? { ok: true } : { ok: false, error: out.body.error || 'refused', message: out.body.message || `the cloud said ${out.status}` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only Stripe's own two pages are opened: whatever else a server answers stays shut.
|
|
152
|
+
const STRIPE_PAGE = /^https:\/\/(checkout|billing)\.stripe\.com\//;
|
|
153
|
+
|
|
154
|
+
async function openStripe(route, body, openExternal, env, home) {
|
|
155
|
+
const at = signedIn(env, home);
|
|
156
|
+
if (!at) return { ok: false, error: 'signed-out', message: `Sign in to ${Name} Cloud first.` };
|
|
157
|
+
const out = await ask(at.gateway, route, { method: 'POST', key: at.key, body });
|
|
158
|
+
if (out.status !== 200) return { ok: false, error: out.body.error || 'refused', message: out.body.message || `the cloud said ${out.status}` };
|
|
159
|
+
if (typeof out.body.url !== 'string' || !STRIPE_PAGE.test(out.body.url)) return { ok: false, error: 'not-stripe', message: 'The cloud answered with a page that is not Stripe\'s.' };
|
|
160
|
+
await openExternal(out.body.url);
|
|
161
|
+
return { ok: true, ...(out.body.where ? { where: out.body.where } : {}) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Stripe's checkout for a plan, in the browser; the billing page for someone already paying. */
|
|
165
|
+
export const payForCloud = ({ plan, openExternal, env = process.env, home = os.homedir() }) => openStripe('/v1/checkout', { plan }, openExternal, env, home);
|
|
166
|
+
|
|
167
|
+
/** Stripe's billing page: change plan, cancel, the card, invoices. */
|
|
168
|
+
export const openCloudBilling = ({ openExternal, env = process.env, home = os.homedir() }) => openStripe('/v1/billing', {}, openExternal, env, home);
|
|
169
|
+
|
|
170
|
+
/** Let this Mac's cloud key go, there and here. An own-AWS setup is left alone. */
|
|
171
|
+
export async function signOutOfCloud({ env = process.env, home = os.homedir() } = {}) {
|
|
172
|
+
const now = readCloudConfig(env, home);
|
|
173
|
+
if (!now?.gateway) return { ok: true };
|
|
174
|
+
const key = readCloudKey(env, home);
|
|
175
|
+
if (key) await ask(now.gateway, '/v1/devices', { method: 'DELETE', key }).catch(() => {});
|
|
176
|
+
fs.rmSync(cloudKeyPath(env, home), { force: true });
|
|
177
|
+
fs.rmSync(cloudConfigPath(env, home), { force: true });
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|