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/README.md
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Give your agents an inbox.</strong><br>
|
|
9
|
-
Run dozens of
|
|
9
|
+
Run dozens of Claude Code and Codex agents in parallel, each in its own git worktree,<br>
|
|
10
|
+
and only hear from the ones that need you.
|
|
10
11
|
</p>
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
13
|
-
<a href="#
|
|
14
|
+
<a href="#install">Get it</a> ·
|
|
14
15
|
<a href="#how-it-works">How it works</a> ·
|
|
15
16
|
<a href="#what-matters-most-goes-first">Priorities</a> ·
|
|
16
17
|
<a href="#built-for-the-keyboard">Keys</a> ·
|
|
@@ -23,8 +24,8 @@
|
|
|
23
24
|
</p>
|
|
24
25
|
|
|
25
26
|
<p align="center">
|
|
26
|
-
Free and open source.
|
|
27
|
-
No server, no account, no extra subscription.
|
|
27
|
+
Free and open source. A Mac app, or in your browser on any platform with <code>npx agentbox-app</code>,<br>
|
|
28
|
+
on the Claude Code or Codex plan you already have. No server, no account, no extra subscription.
|
|
28
29
|
</p>
|
|
29
30
|
|
|
30
31
|
---
|
|
@@ -41,7 +42,7 @@ npx agentbox-app
|
|
|
41
42
|
|
|
42
43
|
It starts on your machine and opens a tab. On Linux that open goes through `xdg-open`. Nothing is hosted: it listens only on `127.0.0.1`, and the address carries a key that is new every run. Add `--help` to see the options. `--no-open` prints the address and leaves the browser to you.
|
|
43
44
|
|
|
44
|
-
From a checkout, build the screen once, then start it the same way:
|
|
45
|
+
From a checkout, build the screen once, then start it the same way. The install links the checkout's own command, so `npx` runs this copy rather than looking for `agentbox` on your PATH:
|
|
45
46
|
|
|
46
47
|
```
|
|
47
48
|
npm install
|
|
@@ -58,7 +59,9 @@ npm install
|
|
|
58
59
|
npm start
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
On Linux, `npm install` puts `agentbox` on `~/.local/bin` and adds the app to the launcher. The entry is `$XDG_DATA_HOME/applications/agentbox.desktop` (`~/.local/share/applications` when that variable is unset), and it runs `agentbox`. That command starts this checkout. Open the app from the launcher.
|
|
62
|
+
On Linux, `npm install` puts `agentbox` on `~/.local/bin` and adds the app to the launcher. The entry is `$XDG_DATA_HOME/applications/agentbox.desktop` (`~/.local/share/applications` when that variable is unset), and it runs `agentbox`. That command starts this checkout, and it is written before Electron has downloaded its binary, so the first launch off the launcher does that download. Open the app from the launcher.
|
|
63
|
+
|
|
64
|
+
Two of this repo's dependencies need an install script to work: `node-pty`, which is the terminal, and `esbuild`. `package.json` names them in `allowScripts`, by name rather than pinned to a version, so a dependency bump does not need re-approving. `electron-winstaller` is denied there: it is only for packing a Windows installer, which this repo does not do. npm 11 and newer print anything in neither list on every install, and `npm approve-scripts --allow-scripts-pending` lists them.
|
|
62
65
|
|
|
63
66
|
`npm start` builds the app and opens it. On first run it walks you through connecting Claude Code or Codex and choosing a folder to work in.
|
|
64
67
|
|
|
@@ -84,7 +87,8 @@ than you could ever watch and only deal with one when it actually needs you.
|
|
|
84
87
|
### 1. File the work
|
|
85
88
|
|
|
86
89
|
Paste a list, send over a board of issues, or type one task. Each item becomes
|
|
87
|
-
its own row, and each row is its own agent, already working
|
|
90
|
+
its own row, and each row is its own agent, already working in its own git
|
|
91
|
+
worktree and on its own branch, so no two agents touch the same files.
|
|
88
92
|
|
|
89
93
|
<img src="docs/readme/file.webp" alt="Twelve issues from a project board go over in one message and appear as twelve agents working" width="100%">
|
|
90
94
|
|
|
@@ -231,7 +235,7 @@ Nothing, unless you are running a build that was signed and released with a
|
|
|
231
235
|
PostHog key baked into it. A build made from this repo has no key, so a clone
|
|
232
236
|
and a fork are silent, and with no key the whole path is off at every send.
|
|
233
237
|
|
|
234
|
-
A released build counts
|
|
238
|
+
A released build counts eighteen things, and they are counts:
|
|
235
239
|
|
|
236
240
|
- the app was opened
|
|
237
241
|
- the app was used today, once a day
|
|
@@ -245,6 +249,12 @@ A released build counts twelve things, and they are counts:
|
|
|
245
249
|
- a task finished
|
|
246
250
|
- an agent started work
|
|
247
251
|
- an agent stopped work
|
|
252
|
+
- the app was quit
|
|
253
|
+
- the corner tag was hidden
|
|
254
|
+
- the corner tag was turned off
|
|
255
|
+
- setup offered agents to import
|
|
256
|
+
- setup imported agents
|
|
257
|
+
- counts were turned off
|
|
248
258
|
|
|
249
259
|
Each carries a random install id, the app and platform version, and at most
|
|
250
260
|
seven other things: how long something took, how many of a thing there were,
|
|
@@ -262,6 +272,11 @@ accident. Crash reports travel the same way and are scrubbed first
|
|
|
262
272
|
- In the app: Settings, then the "Counts and crash reports" switch.
|
|
263
273
|
- In `zero.config.json`: `"diagnostics": false`.
|
|
264
274
|
|
|
275
|
+
Either way nothing is sent from that moment, with one exception: turning the
|
|
276
|
+
switch off in the app sends one last count saying counts were turned off, so
|
|
277
|
+
you are not read as someone who stopped using Agentbox. Editing the file sends
|
|
278
|
+
nothing at all.
|
|
279
|
+
|
|
265
280
|
The update check is separate and is not routed through any of this. It is a
|
|
266
281
|
request to github.com for the latest release, it carries no identity and no
|
|
267
282
|
key, and it only happens in a packaged build.
|
|
@@ -320,7 +335,7 @@ or `?engines=codex` give two.
|
|
|
320
335
|
|
|
321
336
|
To open the app as somebody who has never seen it, with its own throwaway home
|
|
322
337
|
and nothing of yours inside it, press `⌘K` or `Ctrl+K` and choose "Open Agentbox as a new
|
|
323
|
-
user". `npm run fresh` does the same from a terminal. `ZERO_NO_SUPERVISOR=1` applies to `npm start` and to `npx agentbox-app`.
|
|
338
|
+
user". `npm run fresh` does the same from a terminal: a packaged app when there is one, this checkout through Electron when there is not, which is the way in on Linux. `ZERO_NO_SUPERVISOR=1` applies to `npm start` and to `npx agentbox-app`.
|
|
324
339
|
|
|
325
340
|
The app is named in exactly one place, `shared/product-name.mjs`. Change it
|
|
326
341
|
there and run `node scripts/product-name.mjs --write`, and everything else
|
package/bin/agentbox.mjs
CHANGED
|
@@ -15,8 +15,11 @@ import fs from 'node:fs';
|
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
|
|
18
|
-
import { bootHeadless, createServer, newToken } from '../main/serve.mjs';
|
|
18
|
+
import { bootHeadless, createServer, newToken, picturesTheAppOwns } from '../main/serve.mjs';
|
|
19
19
|
import { appHome } from '../main/store/home.mjs';
|
|
20
|
+
import { learnShellDataDirs } from '../main/shell-path.mjs';
|
|
21
|
+
import { startFreezeRecorder } from '../main/freeze-recorder.mjs';
|
|
22
|
+
import { watchNewChatsFor } from '../main/new-chats-watch.mjs';
|
|
20
23
|
|
|
21
24
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
25
|
const appDir = path.dirname(here);
|
|
@@ -59,10 +62,22 @@ if (!fs.existsSync(path.join(dist, 'index.html'))) {
|
|
|
59
62
|
process.exit(1);
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
// The login folders the shell profile names, read before the first worker can
|
|
66
|
+
// start, as main/main.mjs does. The worker scrub keeps CLAUDE_CONFIG_DIR and
|
|
67
|
+
// CODEX_HOME from reaching a worker by inheritance, so without this a copy's
|
|
68
|
+
// workers signed in to ~/.claude and ~/.codex (w-6d8c543a43).
|
|
69
|
+
await learnShellDataDirs();
|
|
62
70
|
const booted = await bootHeadless({ dataDir, appDir, userDir: dataDir });
|
|
71
|
+
// What held the thread each time it froze, as the desktop records it.
|
|
72
|
+
startFreezeRecorder({
|
|
73
|
+
dir: path.join(dataDir, 'memory-log'),
|
|
74
|
+
appDir,
|
|
75
|
+
onFreeze: (f) => console.warn(`zero: the app froze for ${f.ms} ms in ${f.where}`),
|
|
76
|
+
});
|
|
63
77
|
// ZERO_NO_SUPERVISOR: the desktop already honors this (main/main.mjs). This
|
|
64
78
|
// entry is the same inbox, so a launch that asks for no agents must not start
|
|
65
79
|
// them here either.
|
|
80
|
+
let newChatsWatch = null;
|
|
66
81
|
if (!process.env.ZERO_NO_SUPERVISOR) {
|
|
67
82
|
// Whoever was signed in to the team last time is signed in again.
|
|
68
83
|
booted.team?.start().then(() => booted.supervisor.wake?.()).catch((err) => console.warn(`team: ${err.message}`));
|
|
@@ -70,6 +85,8 @@ if (!process.env.ZERO_NO_SUPERVISOR) {
|
|
|
70
85
|
// tab only moved work when something woke the supervisor: a queued thread sat
|
|
71
86
|
// queued and a scheduled one never fired (found 2026-10-01 on a test copy).
|
|
72
87
|
booted.supervisor.start();
|
|
88
|
+
// The chats setup's "Keep bringing in new chats" tick asks for.
|
|
89
|
+
newChatsWatch = watchNewChatsFor({ store: booted.store, config: booted.config, supervisor: booted.supervisor });
|
|
73
90
|
}
|
|
74
91
|
const token = newToken();
|
|
75
92
|
const server = createServer({
|
|
@@ -77,6 +94,10 @@ const server = createServer({
|
|
|
77
94
|
token,
|
|
78
95
|
listeners: booted.window.listeners,
|
|
79
96
|
dist,
|
|
97
|
+
// The pictures in a thread: a tab cannot open `agentbox-img://`, which only
|
|
98
|
+
// the desktop window answers, so without this door every local picture is
|
|
99
|
+
// drawn as missing.
|
|
100
|
+
files: picturesTheAppOwns(booted),
|
|
80
101
|
});
|
|
81
102
|
|
|
82
103
|
const wanted = Number(flag('port', 0)) || 0;
|
|
@@ -99,6 +120,7 @@ server.on('error', (err) => {
|
|
|
99
120
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
100
121
|
process.on(signal, () => {
|
|
101
122
|
console.log('\n Stopping.');
|
|
123
|
+
newChatsWatch?.stop();
|
|
102
124
|
booted.host.app.quit();
|
|
103
125
|
server.close(() => process.exit(0));
|
|
104
126
|
setTimeout(() => process.exit(0), 3000).unref();
|
package/briefs/message-rules.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
<!-- autonomy-rules:start -->
|
|
2
|
+
These rules format a final answer; they do not tell you when to stop working.
|
|
3
|
+
Explicit autonomy applies to the agreed objective, not to unrelated work.
|
|
4
|
+
When the person delegates a plan and asks you to keep going, carry out its
|
|
5
|
+
authorized steps without asking for another go-ahead. A completed step is a checkpoint, not a reason to end the run.
|
|
6
|
+
Give progress updates while continuing. Earlier autonomy remains in force until they change it;
|
|
7
|
+
a status question during that work gets a brief update, then work continues.
|
|
8
|
+
Do not ask them to approve a step they already authorized.
|
|
9
|
+
|
|
10
|
+
If one step is blocked, continue other independent authorized work first.
|
|
11
|
+
Stop when the objective is complete, they ask you to stop,
|
|
12
|
+
you need a decision or resource only they can supply, or repeated attempts produce no progress.
|
|
13
|
+
Follow narrower instructions immediately.
|
|
14
|
+
Autonomy does not bypass permission checks or specific approval requirements.
|
|
15
|
+
An approval covers only the thing it approved.
|
|
16
|
+
Without explicit delegation, finish the requested task and return; a question
|
|
17
|
+
does not authorize a project, and "continue" alone does not authorize unrelated work.
|
|
18
|
+
Authorized work continues until a real stopping point, even if older defaults ask for proposals or options.
|
|
19
|
+
<!-- autonomy-rules:end -->
|
|
20
|
+
|
|
1
21
|
This is about the LAST message you send, the one that ends your run. Whoever
|
|
2
22
|
asked for it reads it in a list beside twenty others and usually decides what to
|
|
3
23
|
do next without opening anything. Ten seconds is the real budget.
|
|
@@ -73,7 +93,10 @@ WHAT THE APP CAN SHOW, so you can lay out what you made the way it reads best:
|
|
|
73
93
|
- A sound or a film plays where it is named; a localhost address opens beside
|
|
74
94
|
the message. Headings, lists, tables and bold all draw.
|
|
75
95
|
The arrangement is yours. The 200 words are for your report, not for the words
|
|
76
|
-
that help them read what you show.
|
|
96
|
+
that help them read what you show. A suggestion when they choose between
|
|
97
|
+
designs: show each one stacked one under the next, in the order of your options,
|
|
98
|
+
with its option's number and a line over or under it saying what differs, so
|
|
99
|
+
each is big enough to judge without opening it.
|
|
77
100
|
|
|
78
101
|
Steps they must take are a numbered list, five at most, the first one doable now.
|
|
79
102
|
|
|
@@ -91,19 +114,24 @@ ended up with, and read them back together: if someone said option one
|
|
|
91
114
|
out loud, it has to answer line one. When it does not, they have two questions
|
|
92
115
|
stacked on each other and must open the message to see which is which.
|
|
93
116
|
|
|
117
|
+
OFFER CHOICES WHENEVER THEIR NEXT REPLY IS CLEAR. A number pressed is quicker
|
|
118
|
+
than a reply typed, and options are drawn as numbered choices. So when you end
|
|
119
|
+
with steps for them to take, offer the reply they would send once the steps are
|
|
120
|
+
done, such as "Done, check it again", and when a decision has two or three
|
|
121
|
+
sensible answers, offer them. Leave options out only when there is truly
|
|
122
|
+
nothing to pick.
|
|
123
|
+
|
|
94
124
|
Offer one to four options; two or three is usual, and one is fine. Mark exactly
|
|
95
125
|
one recommended. Every option has to be something you would start on without asking
|
|
96
126
|
another question first. Name each choice in your prose too, because the list is
|
|
97
127
|
often drawn elsewhere on the screen and a sentence pointing at "option two"
|
|
98
128
|
then points at nothing.
|
|
99
129
|
|
|
100
|
-
IF YOUR RUN
|
|
101
|
-
branch, a draft, a page you built:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
because you did not offer it. The only run that ends with no options
|
|
106
|
-
is one that changed nothing.
|
|
130
|
+
IF YOUR RUN LEAVES AN UNAPPROVED CHANGE, THERE IS SOMETHING TO DECIDE. Edited
|
|
131
|
+
files, a branch, a draft, a page you built: offer what still needs their decision,
|
|
132
|
+
in their words: ship it, change this part first, throw it away. Already authorized actions do not create another decision.
|
|
133
|
+
Do not manufacture a pick to continue an agreed plan. When an approval is
|
|
134
|
+
still required, offer it explicitly so they can choose it.
|
|
107
135
|
|
|
108
136
|
NEVER OFFER WHAT YOUR RUN ALREADY DID. Shipped, merged or sent: line one says
|
|
109
137
|
so, and no option offers it again. When only closing is left, offer the one
|
package/briefs/worker.md
CHANGED
|
@@ -46,7 +46,9 @@ sentences, 45 words at most, that bring the thread back to someone who has
|
|
|
46
46
|
forgotten it, what was asked, what was found and the facts needed to decide.
|
|
47
47
|
`progress` is shown as DONE: the steps taken so far, one per line, oldest first
|
|
48
48
|
and newest last, six lines at most of 15 words each; send the whole list each
|
|
49
|
-
time. A longer one is refused.
|
|
49
|
+
time. A longer one is refused. EVERY LINE IS A STEP THAT FINISHED: one you are
|
|
50
|
+
about to take goes on after it happens, never before, because a DONE list
|
|
51
|
+
claiming a commit that never ran is how the work gets thrown away. Leave `solution` out; it is no longer shown. If
|
|
50
52
|
it waits on another thread, pass that thread's id in `blockedBy`. The person may have edited the summary; read it
|
|
51
53
|
before you rewrite it, and keep what they said unless it is no longer true.
|
|
52
54
|
<!-- store-tools:end -->
|
|
@@ -91,6 +93,8 @@ are a good general assistant first and this brief is scaffolding around that.
|
|
|
91
93
|
If following a rule here would make your answer worse than a plain Claude Code
|
|
92
94
|
session's, you are misapplying it. Answer well, then apply what actually fits.
|
|
93
95
|
|
|
96
|
+
Finish the authorized objective, not merely the current step.
|
|
97
|
+
|
|
94
98
|
## Doing the work
|
|
95
99
|
|
|
96
100
|
<!-- store-tools:start -->
|
|
@@ -101,14 +105,13 @@ session's, you are misapplying it. Answer well, then apply what actually fits.
|
|
|
101
105
|
the only one that changes it.
|
|
102
106
|
- Actions outside your standing grants do not silently fail any more: the
|
|
103
107
|
permission system asks the founder LIVE (an approval card in their inbox app)
|
|
104
|
-
and blocks you until they answer,
|
|
105
|
-
interruptions, because they are: exhaust what your
|
|
106
|
-
batch the gated part into as few commands as possible,
|
|
107
|
-
|
|
108
|
+
and blocks you until they answer, with no deadline on it: you may wait hours.
|
|
109
|
+
Spend these asks like interruptions, because they are: exhaust what your
|
|
110
|
+
grants already allow, batch the gated part into as few commands as possible,
|
|
111
|
+
and if they deny, do not retry it; finish what you can and file the
|
|
108
112
|
remainder as a review with the exact commands.
|
|
109
|
-
- EVERYTHING YOU FILE IS A PROPOSAL: nothing you
|
|
110
|
-
|
|
111
|
-
note to another agent.
|
|
113
|
+
- EVERYTHING YOU FILE IS A PROPOSAL: nothing you file runs until they approve
|
|
114
|
+
it from the inbox; do already authorized work in this run instead of filing it.
|
|
112
115
|
- Code changes happen in the product's repo (your cwd), on a branch, committed
|
|
113
116
|
as you go. Name the branch the way this repo already names branches; read a
|
|
114
117
|
few with `git branch` and follow them. Never commit to main; the founder
|
|
@@ -157,14 +160,14 @@ session's, you are misapplying it. Answer well, then apply what actually fits.
|
|
|
157
160
|
you alone before it started you, and it takes it back when you exit.
|
|
158
161
|
- Actions outside your standing grants do not silently fail any more: the
|
|
159
162
|
permission system asks the founder LIVE (an approval card in their inbox app)
|
|
160
|
-
and blocks you until they answer,
|
|
161
|
-
interruptions, because they are: exhaust what your
|
|
162
|
-
batch the gated part into as few commands as possible,
|
|
163
|
-
|
|
163
|
+
and blocks you until they answer, with no deadline on it: you may wait hours.
|
|
164
|
+
Spend these asks like interruptions, because they are: exhaust what your
|
|
165
|
+
grants already allow, batch the gated part into as few commands as possible,
|
|
166
|
+
and if they deny, do not retry it; finish what you can and say what is
|
|
164
167
|
left in your last message, with the exact commands.
|
|
165
|
-
- You cannot file work, so nothing you find becomes a task by itself.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
- You cannot file work, so nothing you find becomes a task by itself. Work
|
|
169
|
+
outside their authorization goes in your last message as something they can
|
|
170
|
+
say yes to. Continue work they already authorized.
|
|
168
171
|
- Code changes happen in the product's repo (your cwd), on a branch, committed
|
|
169
172
|
as you go. Name the branch the way this repo already names branches; read a
|
|
170
173
|
few with `git branch` and follow them. Never commit to main; the founder
|
|
@@ -191,15 +194,15 @@ session's, you are misapplying it. Answer well, then apply what actually fits.
|
|
|
191
194
|
<!-- no-store-tools:end -->
|
|
192
195
|
|
|
193
196
|
<!-- subagent-rule:start -->
|
|
194
|
-
## One
|
|
195
|
-
|
|
196
|
-
Split work into separate work items when each piece needs its own answer from
|
|
197
|
-
the founder or its own session. Keep helpers inside one item when their work
|
|
198
|
-
combines into a single result.
|
|
197
|
+
## One job is one thread
|
|
199
198
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
DEFAULT TO ONE THREAD; one PR is one thread. A second costs them one more thing
|
|
200
|
+
to read, rank and start or close. Ask how many ANSWERS you need, not how many
|
|
201
|
+
THINGS you found: twenty issues off one call are ONE thread when the answer is
|
|
202
|
+
"yes, fix them". Never hand work to another thread that you could do here, and
|
|
203
|
+
run parallel work as subagents in this one: subagents are not threads. File a
|
|
204
|
+
second only for a separate job, UNDER this one (`parent`), which keeps it out of
|
|
205
|
+
their inbox.
|
|
203
206
|
<!-- subagent-rule:end -->
|
|
204
207
|
|
|
205
208
|
## Honesty
|
|
@@ -242,3 +245,23 @@ whatever it says. On a run with nothing in it, say that in one line and stop.
|
|
|
242
245
|
One line costs them a second to read and tells them the job is still alive.
|
|
243
246
|
<!-- no-store-tools:end -->
|
|
244
247
|
<!-- repeat-rules:end -->
|
|
248
|
+
<!-- teammate-rules:start -->
|
|
249
|
+
|
|
250
|
+
## If this item is one run of a teammate
|
|
251
|
+
|
|
252
|
+
Its labels carry `teammate:<id>`, and the system prompt tells you which
|
|
253
|
+
teammate you are. This run is your own work, not something the person asked
|
|
254
|
+
for, so the rules above about leaving their rows open do not apply to it. The
|
|
255
|
+
team chat is where you talk; their inbox is only for what needs them.
|
|
256
|
+
|
|
257
|
+
Finish one of two ways.
|
|
258
|
+
|
|
259
|
+
Nothing needs them: post a one-line summary with team_post, then update this
|
|
260
|
+
item with `status: "done"` and its labels plus `"clean"`, all in one call
|
|
261
|
+
(labels replace the array, so send every one). This is the ordinary finish. A
|
|
262
|
+
report they did not ask a question about belongs in the chat, not their inbox.
|
|
263
|
+
|
|
264
|
+
They need to decide, approve or sign in: end with that question as your last
|
|
265
|
+
message, short: a bold first line, two or three sentences, numbered options
|
|
266
|
+
with your recommendation first. Leave the item open. It reaches their inbox.
|
|
267
|
+
<!-- teammate-rules:end -->
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
// THE ACCOUNTS AGENTBOX
|
|
2
|
-
// remembered to type into a config file.
|
|
1
|
+
// THE ACCOUNTS AGENTBOX KNOWS ABOUT ARE THE ONES ON THE DISK, not the ones
|
|
2
|
+
// somebody remembered to type into a config file.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// writes and nothing in the app checks against reality, so an account set up
|
|
10
|
-
// the ordinary way (a new CLAUDE_CONFIG_DIR, a /login) was invisible BY
|
|
11
|
-
// CONSTRUCTION. There was no symptom: the page drew two healthy rows and every
|
|
12
|
-
// word on them was true.
|
|
4
|
+
// The register of accounts was the hand-written `authProfiles` key in
|
|
5
|
+
// zero.config.json, which nothing in the app writes and nothing in the app
|
|
6
|
+
// checks against reality, so an account set up the ordinary way (a new
|
|
7
|
+
// CLAUDE_CONFIG_DIR, a /login) was invisible BY CONSTRUCTION: it could not be
|
|
8
|
+
// seen on the Accounts page and could not be picked.
|
|
13
9
|
// tests/an-account-she-signed-into-is-not-invisible.test.mjs has the folders,
|
|
14
10
|
// the ids and the CLI's own sentence.
|
|
15
11
|
//
|
|
12
|
+
// KNOWING ABOUT AN ACCOUNT IS NOT SENDING WORK TO IT (w-dac993a554). This list
|
|
13
|
+
// is what the Accounts page draws and what somebody picks from; the work goes
|
|
14
|
+
// to one account, `Supervisor#_theOneAccount`. A second subscription found here
|
|
15
|
+
// sits idle until it is picked or a project is tied to it.
|
|
16
|
+
//
|
|
16
17
|
// THE RULE, and it is four lines because each one was a way to get this wrong:
|
|
17
18
|
//
|
|
18
19
|
// - A folder under the home directory named `.claude` or `.claude-<anything>`
|
|
19
20
|
// that holds a login is an account.
|
|
20
21
|
// - IT ONLY COUNTS IF ITS SUBSCRIPTION IS NEW. Two folders holding one
|
|
21
22
|
// account is a real setup (~/.claude and ~/.claude-second both signed in
|
|
22
|
-
// as the same person), and counting it twice
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// - NOTHING THE USER CONFIGURED IS
|
|
27
|
-
//
|
|
28
|
-
//
|
|
23
|
+
// as the same person), and counting it twice draws one subscription as two
|
|
24
|
+
// rows to choose between. The test is on `accountUuid`, the id Claude Code
|
|
25
|
+
// stores for the subscription itself, never on the email, because one
|
|
26
|
+
// email can hold more than one subscription.
|
|
27
|
+
// - NOTHING THE USER CONFIGURED IS REORDERED, and nothing is dropped except
|
|
28
|
+
// a home-directory folder that fails the two rules above and below: one
|
|
29
|
+
// with no login, or a second one holding a subscription already listed.
|
|
30
|
+
// Discovered accounts go after the configured ones.
|
|
29
31
|
// - A folder with no login is not an account. Folders such as
|
|
30
32
|
// ~/.claude-swap-backup or ~/.claude-worktrees are exactly that, and
|
|
31
33
|
// neither is a place to send work.
|
|
@@ -47,6 +49,7 @@ import fs from 'node:fs';
|
|
|
47
49
|
import os from 'node:os';
|
|
48
50
|
import path from 'node:path';
|
|
49
51
|
import { accountIdentity, defaultClaudeHome } from './account-tooling.mjs';
|
|
52
|
+
import { shellDataDirsNow } from './shell-path.mjs';
|
|
50
53
|
|
|
51
54
|
// The listing, held against the home folder's mtime. Creating or deleting a
|
|
52
55
|
// folder there moves it; signing IN does not, which is why only the listing is
|
|
@@ -98,35 +101,87 @@ export function discoverProfiles({ home = os.homedir() } = {}) {
|
|
|
98
101
|
out.push({ dir, email: id.email, accountUuid: id.accountUuid });
|
|
99
102
|
}
|
|
100
103
|
// The default login leads, because it is the one every other account is a
|
|
101
|
-
// second of
|
|
102
|
-
//
|
|
104
|
+
// second of and the one work runs on until somebody picks another
|
|
105
|
+
// (`_theOneAccount` takes the first of this list). The rest keep the disk's
|
|
106
|
+
// alphabetical order, so the order does not change between ticks.
|
|
103
107
|
return out.sort((a, b) => (a.dir === dflt ? -1 : b.dir === dflt ? 1 : 0));
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/** A `.claude-<anything>` folder directly in the home directory: the shape
|
|
111
|
+
* discovery looks for and Add account makes. */
|
|
112
|
+
function isHomeFolder(dir, home) {
|
|
113
|
+
const full = path.resolve(dir);
|
|
114
|
+
return path.dirname(full) === path.resolve(home) && path.basename(full).startsWith('.claude-');
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
/**
|
|
107
|
-
* THE PROFILES THE FLEET ACTUALLY RUNS ON: the configured ones first,
|
|
108
|
-
*
|
|
109
|
-
*
|
|
118
|
+
* THE PROFILES THE FLEET ACTUALLY RUNS ON: the configured ones first, in her
|
|
119
|
+
* order, then any subscription found on the disk that none of the configured
|
|
120
|
+
* ones is already running on.
|
|
110
121
|
*
|
|
111
122
|
* Profiles are the strings the rest of the app has always used: the literal
|
|
112
123
|
* 'default' for ~/.claude (which is what spawning with no CLAUDE_CONFIG_DIR
|
|
113
124
|
* means), and an absolute folder path for anything else. It always answers
|
|
114
125
|
* with at least ['default'], because somewhere has to be spawnable. */
|
|
115
|
-
export function effectiveProfiles(configured, { home = os.homedir() } = {}) {
|
|
126
|
+
export function effectiveProfiles(configured, { home = os.homedir(), shellDir = shellDataDirsNow().CLAUDE_CONFIG_DIR } = {}) {
|
|
116
127
|
const mine = (Array.isArray(configured) ? configured : []).map((p) => p || 'default');
|
|
117
|
-
const list = mine.length ? [...mine] : ['default'];
|
|
118
128
|
const dflt = defaultClaudeHome(home);
|
|
119
129
|
const dirOf = (p) => (p === 'default' ? dflt : p);
|
|
130
|
+
const isDefault = (p) => p === 'default' || dirOf(p) === dflt;
|
|
131
|
+
const idOf = (p) => accountIdentity(isDefault(p) ? null : dirOf(p), { home });
|
|
120
132
|
|
|
121
|
-
// Which subscriptions her own list already covers
|
|
122
|
-
//
|
|
123
|
-
// so it cannot be the reason a working account is left out.
|
|
133
|
+
// Which subscriptions her own list already covers, the default login's first
|
|
134
|
+
// so that it is the one kept when a second folder holds the same account.
|
|
124
135
|
const have = new Set();
|
|
125
|
-
for (const p of
|
|
126
|
-
const id =
|
|
136
|
+
for (const p of mine) {
|
|
137
|
+
const id = isDefault(p) ? idOf(p) : null;
|
|
127
138
|
if (id?.accountUuid) have.add(id.accountUuid);
|
|
128
139
|
}
|
|
140
|
+
|
|
141
|
+
// A `.claude-<n>` FOLDER IN THE HOME DIRECTORY IS AN ACCOUNT ONLY WHEN IT IS
|
|
142
|
+
// SIGNED IN, AND ONLY ONCE PER SUBSCRIPTION, whether discovery found it or
|
|
143
|
+
// the config names it. Add account used to write its folder into the config
|
|
144
|
+
// before the sign-in, so an abandoned one sat in Settings as "Not signed in"
|
|
145
|
+
// and was handed every other task; and one account signed into two folders
|
|
146
|
+
// was listed twice (tests/an-account-shows-once-and-only-once-it-is-signed-in).
|
|
147
|
+
// The default login is never judged, and a folder anywhere else is a
|
|
148
|
+
// hand-written setting and stays exactly as written.
|
|
149
|
+
const list = [];
|
|
150
|
+
for (const p of mine) {
|
|
151
|
+
if (isDefault(p)) { list.push(p); continue; }
|
|
152
|
+
const id = idOf(p);
|
|
153
|
+
if (isHomeFolder(dirOf(p), home)) {
|
|
154
|
+
if (!id?.accountUuid || have.has(id.accountUuid)) continue;
|
|
155
|
+
}
|
|
156
|
+
if (id?.accountUuid) have.add(id.accountUuid);
|
|
157
|
+
list.push(p);
|
|
158
|
+
}
|
|
159
|
+
if (!list.length) list.push('default');
|
|
160
|
+
|
|
161
|
+
// THE LOGIN THE PERSON'S SHELL PROFILE POINTS AT (w-6d8c543a43). Their
|
|
162
|
+
// terminal's `claude` reads the folder `CLAUDE_CONFIG_DIR` names in .zshrc,
|
|
163
|
+
// which an app opened from the Dock never sees. When the default holds no
|
|
164
|
+
// login, or the same account, the shell's folder takes its place; a
|
|
165
|
+
// different account at ~/.claude keeps running beside it. It is always a
|
|
166
|
+
// folder, never 'default', even when it IS ~/.claude: Claude Code names its
|
|
167
|
+
// keychain entry after the variable, so naming the folder out loud is a
|
|
168
|
+
// different sign-in from naming nothing.
|
|
169
|
+
const shell = typeof shellDir === 'string' && path.isAbsolute(shellDir) ? shellDir : null;
|
|
170
|
+
const shellId = shell ? accountIdentity(shell, { home }) : null;
|
|
171
|
+
if (shellId?.accountUuid && !list.includes(shell)) {
|
|
172
|
+
const dfltId = idOf('default');
|
|
173
|
+
const takesDefault = !dfltId?.accountUuid || dfltId.accountUuid === shellId.accountUuid;
|
|
174
|
+
if (takesDefault && list.some(isDefault)) {
|
|
175
|
+
list.splice(list.findIndex(isDefault), 1, shell);
|
|
176
|
+
for (let i = list.length - 1; i >= 0; i--) if (list[i] !== shell && isDefault(list[i])) list.splice(i, 1);
|
|
177
|
+
have.add(shellId.accountUuid);
|
|
178
|
+
} else if (!have.has(shellId.accountUuid)) {
|
|
179
|
+
have.add(shellId.accountUuid);
|
|
180
|
+
list.push(shell);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
129
183
|
const listed = new Set(list.map((p) => dirOf(p)));
|
|
184
|
+
if (shell && list.includes(shell)) listed.add(dflt);
|
|
130
185
|
|
|
131
186
|
for (const found of discoverProfiles({ home })) {
|
|
132
187
|
if (listed.has(found.dir)) continue;
|
|
@@ -138,6 +193,43 @@ export function effectiveProfiles(configured, { home = os.homedir() } = {}) {
|
|
|
138
193
|
return list;
|
|
139
194
|
}
|
|
140
195
|
|
|
196
|
+
/**
|
|
197
|
+
* WHAT IS WRONG WITH THE SUBSCRIPTION THIS ROW STANDS FOR, not merely with the
|
|
198
|
+
* folder it is named after.
|
|
199
|
+
*
|
|
200
|
+
* Trouble is filed per FOLDER, because a folder is what a worker is handed.
|
|
201
|
+
* `effectiveProfiles` above lists one row per SUBSCRIPTION, dropping a second
|
|
202
|
+
* folder holding an account it already has. So a limit hit in the dropped
|
|
203
|
+
* folder is filed under a key no row carries and is drawn nowhere, while the
|
|
204
|
+
* row for that same subscription goes on showing whatever was last filed on
|
|
205
|
+
* the listed folder, however old.
|
|
206
|
+
*
|
|
207
|
+
* Measured 2026-10-16: ~/.claude-3 hit a session limit at 3:15pm and the row
|
|
208
|
+
* for that subscription showed an "organization has turned Claude Code off"
|
|
209
|
+
* filed on ~/.claude five days earlier. Both folders hold accountUuid
|
|
210
|
+
* 8fe4a357; the newest fact about the account was the invisible one.
|
|
211
|
+
*
|
|
212
|
+
* THE NEWEST READING WINS, because that is the only honest answer when two
|
|
213
|
+
* folders of one subscription disagree: each entry is what was true when a
|
|
214
|
+
* run last died there, and nothing clears a folder nothing spawns on.
|
|
215
|
+
* Codex keys are never considered — an account's key carries its engine. */
|
|
216
|
+
export function troubleForProfile(troubles, profile, { home = os.homedir() } = {}) {
|
|
217
|
+
const dflt = defaultClaudeHome(home);
|
|
218
|
+
const dirOf = (p) => (p === 'default' ? dflt : p);
|
|
219
|
+
const idOf = (p) => accountIdentity(dirOf(p) === dflt ? null : dirOf(p), { home });
|
|
220
|
+
const own = troubles?.[profile] ?? null;
|
|
221
|
+
const mine = idOf(profile)?.accountUuid;
|
|
222
|
+
if (!mine) return own;
|
|
223
|
+
const when = (t) => t?.at ?? t?.since ?? 0;
|
|
224
|
+
let best = own;
|
|
225
|
+
for (const [key, t] of Object.entries(troubles ?? {})) {
|
|
226
|
+
if (!t || key === profile || key.startsWith('codex:')) continue;
|
|
227
|
+
if (idOf(key)?.accountUuid !== mine) continue;
|
|
228
|
+
if (when(t) > when(best)) best = t;
|
|
229
|
+
}
|
|
230
|
+
return best;
|
|
231
|
+
}
|
|
232
|
+
|
|
141
233
|
/**
|
|
142
234
|
* Test seam only: forget the cached directory listing. Nothing in the app
|
|
143
235
|
* calls this — the mtime check is what keeps it honest at runtime. */
|
package/main/account-tooling.mjs
CHANGED
|
@@ -213,7 +213,7 @@ export function accountIdentity(dir, { home = os.homedir() } = {}) {
|
|
|
213
213
|
try {
|
|
214
214
|
const acct = JSON.parse(fs.readFileSync(file, 'utf8'))?.oauthAccount;
|
|
215
215
|
if (acct?.emailAddress) {
|
|
216
|
-
value = { email: acct.emailAddress, accountUuid: acct.accountUuid ?? null };
|
|
216
|
+
value = { email: acct.emailAddress, accountUuid: acct.accountUuid ?? null, ...(acct.organizationUuid ? { organizationUuid: acct.organizationUuid } : {}) };
|
|
217
217
|
}
|
|
218
218
|
} catch {
|
|
219
219
|
// A half-written file, or one Claude Code has changed the shape of. The
|
|
@@ -224,6 +224,34 @@ export function accountIdentity(dir, { home = os.homedir() } = {}) {
|
|
|
224
224
|
return value;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* A CLOUD LOGIN'S NAME, from the one login here in the same organization. A
|
|
229
|
+
* cloud token can say its organization but not its email. Null when no login
|
|
230
|
+
* here is in it, or when two different emails are, so a team organization is
|
|
231
|
+
* never pinned on whichever member happens to be signed in here.
|
|
232
|
+
*/
|
|
233
|
+
export function nameForOrganization(org, identities) {
|
|
234
|
+
if (!org) return null;
|
|
235
|
+
const emails = new Set(identities.filter((id) => id?.organizationUuid === org && id.email).map((id) => id.email));
|
|
236
|
+
return emails.size === 1 ? [...emails][0] : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The folder here holding a cloud login, so the two share one reading. A login
|
|
241
|
+
* that says its email is that person in its organization; one that says only
|
|
242
|
+
* its organization is the one email here in it, and none when two people here
|
|
243
|
+
* share it.
|
|
244
|
+
*/
|
|
245
|
+
export function profileForCloudLogin(login, identities) {
|
|
246
|
+
return identities.find((id) => isCloudLoginOf(login, id, identities))?.profile ?? null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Whether a cloud login is the same subscription as one login here, by `profileForCloudLogin`'s rule. */
|
|
250
|
+
export function isCloudLoginOf({ org, email }, identity, identities) {
|
|
251
|
+
if (!org || !identity?.email || identity.organizationUuid !== org) return false;
|
|
252
|
+
return (email || nameForOrganization(org, identities)) === identity.email;
|
|
253
|
+
}
|
|
254
|
+
|
|
227
255
|
/**
|
|
228
256
|
* THE SENTENCE FOR TWO ROWS THAT TURNED OUT TO BE ONE ACCOUNT. It only ever
|
|
229
257
|
* speaks on a machine that already has two profile folders, so it describes a
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// EVERY CLAUDE ACCOUNT'S OWN USAGE, for the Settings page.
|
|
2
|
+
//
|
|
3
|
+
// main/claude-usage.mjs reads the account in use, for the corner on every
|
|
4
|
+
// screen. This reads all of them, only when Settings asks, so the page can say
|
|
5
|
+
// which login has room, and so a reading can take back a mark the account no
|
|
6
|
+
// longer deserves (`Supervisor#noteUsageReading`).
|
|
7
|
+
import { ClaudeUsage, STALE_MS } from './claude-usage.mjs';
|
|
8
|
+
|
|
9
|
+
export class AccountUsage {
|
|
10
|
+
constructor({ bin, env = () => process.env, onReading = () => {}, run, now = () => Date.now(), where } = {}) {
|
|
11
|
+
this._bin = bin;
|
|
12
|
+
this._env = env;
|
|
13
|
+
this._onReading = onReading;
|
|
14
|
+
this._run = run;
|
|
15
|
+
this._now = now;
|
|
16
|
+
this._where = where;
|
|
17
|
+
this._readers = new Map();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_reader(profile) {
|
|
21
|
+
let reader = this._readers.get(profile);
|
|
22
|
+
if (!reader) {
|
|
23
|
+
reader = new ClaudeUsage({
|
|
24
|
+
bin: this._bin, run: this._run, now: this._now, where: this._where,
|
|
25
|
+
account: () => ({ profile, env: this._env() }),
|
|
26
|
+
});
|
|
27
|
+
this._readers.set(profile, reader);
|
|
28
|
+
}
|
|
29
|
+
return reader;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One answer per account, in the order given: its limits, or why there are none. */
|
|
33
|
+
async readAll(profiles, { refresh = false } = {}) {
|
|
34
|
+
return Promise.all(profiles.map(async (profile) => {
|
|
35
|
+
const reader = this._reader(profile);
|
|
36
|
+
const last = reader.peek();
|
|
37
|
+
const fresh = !refresh && last && this._now() - last.at < STALE_MS;
|
|
38
|
+
const result = fresh ? { ok: true, reading: last } : await reader.refreshNow();
|
|
39
|
+
if (result.ok && result.reading) {
|
|
40
|
+
if (!fresh) this._onReading(profile, result.reading);
|
|
41
|
+
return { ...result.reading, profile };
|
|
42
|
+
}
|
|
43
|
+
return { ...(reader.peek() ?? {}), profile, error: result.error ?? null };
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}
|