agentbox-app 0.1.11 → 0.1.13
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 -16
- package/bin/agentbox.mjs +18 -9
- package/briefs/message-rules.md +15 -4
- package/main/account-tooling.mjs +2 -1
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -20
- package/main/analytics.mjs +30 -0
- package/main/approvals.mjs +14 -5
- package/main/claude-bin.mjs +45 -310
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +14 -2
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -248
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +40 -13
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/demo.mjs +17 -14
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +27 -27
- package/main/feedback.mjs +11 -6
- package/main/fresh-user.mjs +1 -1
- package/main/git-change.mjs +8 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/instruction-settings.mjs +47 -11
- package/main/ipc.mjs +175 -22
- package/main/local-app-frames.mjs +82 -0
- package/main/machine.mjs +1 -1
- package/main/main.mjs +120 -5
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +48 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/refresh-agent-models.mjs +2 -1
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +8 -7
- package/main/runs-on.mjs +90 -0
- package/main/serve.mjs +124 -10
- package/main/settings.mjs +171 -11
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +25 -7
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +37 -5
- package/main/store/project.mjs +15 -19
- package/main/store.mjs +10 -4
- package/main/supervisor.mjs +1030 -447
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +491 -82
- package/main/task-terminals.mjs +49 -8
- package/main/team/index.mjs +2 -2
- package/main/team/projects.mjs +1 -1
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/mcp/core/page.mjs +1 -1
- package/mcp/core/work.mjs +34 -3
- package/mcp/tools.mjs +2 -1
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +31 -7
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +3 -1
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +19 -0
- package/shared/claude-commands.generated.mjs +8 -6
- package/shared/claude-models.generated.mjs +10 -8
- package/shared/codex-modes.mjs +1 -1
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/demo-world.mjs +1 -1
- package/shared/engines.mjs +20 -5
- package/shared/fresh-user-home.mjs +12 -1
- package/shared/harness-definitions.mjs +103 -0
- package/shared/instruction-defaults.json +1 -1
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/project-folder-check.mjs +3 -3
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +10 -0
- package/shared/runs-on.mjs +55 -0
- package/shared/shortcuts.mjs +11 -5
- package/shared/spawn-trouble.mjs +58 -7
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +28 -2
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-B-2Az952.css +0 -32
- package/renderer/dist/assets/index-D6qncgai.js +0 -1168
package/README.md
CHANGED
|
@@ -39,11 +39,17 @@ There are two ways to run it: in the browser, or as a Mac app. Both need Node 22
|
|
|
39
39
|
npx agentbox-app
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
It starts on your machine and opens a tab. Nothing is hosted: it listens only
|
|
43
|
-
on your own computer, and the address carries a key that is new every run. Add
|
|
44
|
-
`--help` to see the options.
|
|
42
|
+
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.
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
From a checkout, build the screen once, then start it the same way:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
npm install
|
|
48
|
+
npm run build
|
|
49
|
+
npx agentbox-app
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**As a desktop app.** Build it from this repo. `npm start` is the desktop command on Omarchy and on a Mac:
|
|
47
53
|
|
|
48
54
|
```
|
|
49
55
|
git clone https://github.com/savannahfeder/agentbox.git
|
|
@@ -52,11 +58,11 @@ npm install
|
|
|
52
58
|
npm start
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
`npm
|
|
56
|
-
|
|
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
|
+
|
|
63
|
+
`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.
|
|
57
64
|
|
|
58
|
-
macOS may ask to let Terminal find devices on your local network. Agentbox
|
|
59
|
-
does not need that, so Don't Allow is fine.
|
|
65
|
+
On macOS, the system may ask to let Terminal find devices on your local network. Agentbox does not need that, so Don't Allow is fine.
|
|
60
66
|
|
|
61
67
|
## The chat window is the bottleneck
|
|
62
68
|
|
|
@@ -130,7 +136,7 @@ asked. Code changes open right beside the row.
|
|
|
130
136
|
Twenty agents means twenty things competing for your attention and for a limited
|
|
131
137
|
number of running sessions. Agentbox keeps one order and uses it for both.
|
|
132
138
|
|
|
133
|
-
- **Tag a task** Urgent, High, Medium or Low, when you file it or later from `⌘K`.
|
|
139
|
+
- **Tag a task** Urgent, High, Medium or Low, when you file it or later from `⌘K` or `Ctrl+K`.
|
|
134
140
|
- **Rank your projects** once by dragging them into order. A project's place
|
|
135
141
|
counts for more than any single task's tag, so your most important project
|
|
136
142
|
never waits behind a side one.
|
|
@@ -146,7 +152,7 @@ number of running sessions. Agentbox keeps one order and uses it for both.
|
|
|
146
152
|
|
|
147
153
|
<p align="center">
|
|
148
154
|
<img src="docs/readme/priority-palette.png" alt="The command palette showing Priority Urgent, High, Medium and Low for the selected row" width="60%"><br>
|
|
149
|
-
<sub>Any row's priority is a <code>⌘K</code> away.</sub>
|
|
155
|
+
<sub>Any row's priority is a <code>⌘K</code> or <code>Ctrl+K</code> away.</sub>
|
|
150
156
|
</p>
|
|
151
157
|
|
|
152
158
|
## Built for the keyboard
|
|
@@ -164,10 +170,10 @@ number of running sessions. Agentbox keeps one order and uses it for both.
|
|
|
164
170
|
| `L` | Later: put a task off until a time you pick |
|
|
165
171
|
| `S` | Show or hide the summary, in an open task |
|
|
166
172
|
| `Z` | Undo |
|
|
167
|
-
| `⌘K` | Everything else |
|
|
173
|
+
| `⌘K` or `Ctrl+K` | Everything else |
|
|
168
174
|
|
|
169
175
|
Fifteen skins, each in light and dark, from plain slate to woodblock and riso
|
|
170
|
-
prints. Press `⌘K` and type "theme".
|
|
176
|
+
prints. Press `⌘K` or `Ctrl+K` and type "theme". `Ctrl+K` is the same palette on Linux, where there is no Command key.
|
|
171
177
|
|
|
172
178
|
## Runs on the plan you already have
|
|
173
179
|
|
|
@@ -191,8 +197,10 @@ nothing new to pay for.
|
|
|
191
197
|
default works unset and they all live in `main/config.mjs`. The ones that
|
|
192
198
|
matter:
|
|
193
199
|
|
|
194
|
-
- `storeRoot`: where the store lives.
|
|
195
|
-
in your home directory.
|
|
200
|
+
- `storeRoot`: where the store lives. On macOS this defaults to a folder
|
|
201
|
+
named after the app in your home directory. On Linux it defaults to
|
|
202
|
+
`$XDG_DATA_HOME/agentbox`, which is `~/.local/share/agentbox` when that
|
|
203
|
+
variable is unset.
|
|
196
204
|
- `claudeBin`: the binary to run. The app finds `claude` itself if you leave
|
|
197
205
|
this alone.
|
|
198
206
|
- `maxConcurrentSessions`: how many agents run at once. The queue absorbs the
|
|
@@ -311,8 +319,8 @@ picks which coding agents the fixture Mac has, where nothing is one agent,
|
|
|
311
319
|
or `?engines=codex` give two.
|
|
312
320
|
|
|
313
321
|
To open the app as somebody who has never seen it, with its own throwaway home
|
|
314
|
-
and nothing of yours inside it, press `⌘K` and choose "Open Agentbox as a new
|
|
315
|
-
user". `npm run fresh` does the same from a terminal.
|
|
322
|
+
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`.
|
|
316
324
|
|
|
317
325
|
The app is named in exactly one place, `shared/product-name.mjs`. Change it
|
|
318
326
|
there and run `node scripts/product-name.mjs --write`, and everything else
|
package/bin/agentbox.mjs
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import fs from 'node:fs';
|
|
15
|
-
import os from 'node:os';
|
|
16
15
|
import { spawn } from 'node:child_process';
|
|
17
16
|
import { fileURLToPath } from 'node:url';
|
|
18
17
|
|
|
19
18
|
import { bootHeadless, createServer, newToken } from '../main/serve.mjs';
|
|
19
|
+
import { appHome } from '../main/store/home.mjs';
|
|
20
20
|
|
|
21
21
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
const appDir = path.dirname(here);
|
|
@@ -43,9 +43,13 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
43
43
|
|
|
44
44
|
// WHERE THE STORE LIVES. A copy run out of npm has no folder of its own to
|
|
45
45
|
// write into, and writing inside node_modules would be thrown away by the next
|
|
46
|
-
// install.
|
|
46
|
+
// install. The default is appHome(): a dot-folder already on disk, else the
|
|
47
|
+
// XDG data directory on Linux and `~/.agentbox` on macOS, where this command
|
|
48
|
+
// has always kept it. Not the desktop config's default, which on macOS is
|
|
49
|
+
// `~/Agentbox` and would open every existing Mac user on an empty inbox.
|
|
50
|
+
// `--dir` and AGENTBOX_HOME are the escape hatches.
|
|
47
51
|
const dataDir = path.resolve(
|
|
48
|
-
flag('dir', process.env.AGENTBOX_HOME ??
|
|
52
|
+
flag('dir', process.env.AGENTBOX_HOME ?? appHome()),
|
|
49
53
|
);
|
|
50
54
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
51
55
|
|
|
@@ -56,12 +60,17 @@ if (!fs.existsSync(path.join(dist, 'index.html'))) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
const booted = await bootHeadless({ dataDir, appDir, userDir: dataDir });
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
booted.supervisor.
|
|
63
|
+
// ZERO_NO_SUPERVISOR: the desktop already honors this (main/main.mjs). This
|
|
64
|
+
// entry is the same inbox, so a launch that asks for no agents must not start
|
|
65
|
+
// them here either.
|
|
66
|
+
if (!process.env.ZERO_NO_SUPERVISOR) {
|
|
67
|
+
// Whoever was signed in to the team last time is signed in again.
|
|
68
|
+
booted.team?.start().then(() => booted.supervisor.wake?.()).catch((err) => console.warn(`team: ${err.message}`));
|
|
69
|
+
// AND THE AGENTS' CLOCK, as the desktop starts it (main/main.mjs). Without it a
|
|
70
|
+
// tab only moved work when something woke the supervisor: a queued thread sat
|
|
71
|
+
// queued and a scheduled one never fired (found 2026-10-01 on a test copy).
|
|
72
|
+
booted.supervisor.start();
|
|
73
|
+
}
|
|
65
74
|
const token = newToken();
|
|
66
75
|
const server = createServer({
|
|
67
76
|
channels: booted.channels,
|
package/briefs/message-rules.md
CHANGED
|
@@ -59,10 +59,21 @@ opens beside the message by itself. Not the source files you edited, which are
|
|
|
59
59
|
no use to them.
|
|
60
60
|
|
|
61
61
|
A PICTURE YOU SHOW IS SAVED IN THE PRODUCT'S DOCS FOLDER, the one your brief
|
|
62
|
-
names, in designs/ under this task's id, and named
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
names, in designs/ under this task's id, and named by its full path. Not in
|
|
63
|
+
your code checkout, which is deleted once the work ships, and never in /tmp,
|
|
64
|
+
which the app will not draw at all.
|
|
65
|
+
|
|
66
|
+
WHAT THE APP CAN SHOW, so you can lay out what you made the way it reads best:
|
|
67
|
+
- A picture's path, anywhere in the message, is drawn right there at full
|
|
68
|
+
width, and a click opens it. It is drawn as a block, so inside a sentence or
|
|
69
|
+
a bullet it splits the line.
|
|
70
|
+
- A folder's path on a line of its own draws its newest six pictures as a grid.
|
|
71
|
+
- An html page in designs/ opens beside the message by itself, so one page can
|
|
72
|
+
set several options side by side with notes on each.
|
|
73
|
+
- A sound or a film plays where it is named; a localhost address opens beside
|
|
74
|
+
the message. Headings, lists, tables and bold all draw.
|
|
75
|
+
The arrangement is yours. The 200 words are for your report, not for the words
|
|
76
|
+
that help them read what you show.
|
|
66
77
|
|
|
67
78
|
Steps they must take are a numbered list, five at most, the first one doable now.
|
|
68
79
|
|
package/main/account-tooling.mjs
CHANGED
|
@@ -198,7 +198,8 @@ function list(names) {
|
|
|
198
198
|
const identityCache = new Map();
|
|
199
199
|
|
|
200
200
|
export function accountIdentity(dir, { home = os.homedir() } = {}) {
|
|
201
|
-
|
|
201
|
+
let file = path.join(dir || defaultClaudeHome(home), '.claude.json');
|
|
202
|
+
if (!dir && !fs.existsSync(file)) file = path.join(home, '.claude.json');
|
|
202
203
|
// Cached on the file's own mtime rather than on a timer. That file is ~120 KB
|
|
203
204
|
// on this machine and the settings page re-reads on every poll, so parsing it
|
|
204
205
|
// each time would be real work for an answer that changes about twice a
|
package/main/acp.mjs
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE TRANSPORT FOR EVERY AGENT THAT SPEAKS THE AGENT CLIENT PROTOCOL.
|
|
3
|
+
*
|
|
4
|
+
* Grok (`grok agent stdio`), pi, Gemini CLI and Hermes (`hermes acp`) all speak
|
|
5
|
+
* it, so this file is what a user of ours asked for when they wrote
|
|
6
|
+
* github.com/savannahfeder/agentbox/issues/21: one engine instead of one
|
|
7
|
+
* adapter per tool. ACP is JSON-RPC 2.0 over stdio, one object per line, and
|
|
8
|
+
* the reason it is worth a file of its own is that permission requests are IN
|
|
9
|
+
* the protocol — an approval card can carry a real approval rather than one
|
|
10
|
+
* inferred from the bottom of a terminal.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THIS FILE IS AND IS NOT. It is the wire: framing, negotiation, the
|
|
13
|
+
* session calls, and the two directions of request. It holds no opinion about
|
|
14
|
+
* which tool is on the other end, no card drawing, and no admission policy. A
|
|
15
|
+
* tool is not offered to anyone because this file works; see docs/harnesses.md.
|
|
16
|
+
*
|
|
17
|
+
* THE RULES IT EXISTS TO KEEP, every one of them a lesson
|
|
18
|
+
* main/codex-app-server.mjs paid for first:
|
|
19
|
+
*
|
|
20
|
+
* - A PERMISSION REQUEST IS ALWAYS ANSWERED. The agent is blocked on us while
|
|
21
|
+
* it waits, so a dropped answer is not a missing card, it is a task parked
|
|
22
|
+
* forever with nothing on the row. If the asker throws, or never comes back
|
|
23
|
+
* inside the deadline, the answer is a refusal rather than silence.
|
|
24
|
+
* - A REFUSAL IS SPOKEN IN THE AGENT'S OWN WORDS. Option ids belong to the
|
|
25
|
+
* tool, so "deny" means finding the option whose KIND rejects. If it offered
|
|
26
|
+
* none, the answer is `cancelled`: we never invent an id, and we never
|
|
27
|
+
* silently pick `allow_always` for someone who pressed Allow once.
|
|
28
|
+
* - A CANCELLED TURN ANSWERS `cancelled`, which the protocol requires, so a
|
|
29
|
+
* card nobody pressed cannot land as an approval after the fact. And a
|
|
30
|
+
* request is answered ONCE.
|
|
31
|
+
* - A DOOR WE NEVER OPENED IS REFUSED OUT LOUD. We advertise no filesystem and
|
|
32
|
+
* no terminal, so `fs/*` and `terminal/*` get a method-not-found error.
|
|
33
|
+
* Going quiet on a JSON-RPC request is the same hang in the other direction.
|
|
34
|
+
* - READING NEVER STOPS WHILE A QUESTION WAITS, because the agent may still be
|
|
35
|
+
* streaming, and because its other sessions must not be held up by this one.
|
|
36
|
+
*/
|
|
37
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
38
|
+
|
|
39
|
+
// The version this client negotiates. ACP numbers its protocol, and an agent
|
|
40
|
+
// that answers with another one has told us something we must not paper over.
|
|
41
|
+
export const ACP_PROTOCOL_VERSION = 1;
|
|
42
|
+
|
|
43
|
+
// What we lend the agent. Nothing: it has its own filesystem and its own
|
|
44
|
+
// terminal, and anything advertised here is a door we then have to police.
|
|
45
|
+
export const CLIENT_CAPABILITIES = Object.freeze({
|
|
46
|
+
fs: Object.freeze({ readTextFile: false, writeTextFile: false }),
|
|
47
|
+
terminal: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The four kinds the protocol defines for a permission option. The ids beside
|
|
51
|
+
// them are the tool's own; only the kind is portable.
|
|
52
|
+
export const PERMISSION_KINDS = Object.freeze(['allow_once', 'allow_always', 'reject_once', 'reject_always']);
|
|
53
|
+
const REJECTIONS = Object.freeze(['reject_once', 'reject_always']);
|
|
54
|
+
|
|
55
|
+
// How long a question may stand before it is refused on the person's behalf.
|
|
56
|
+
// Matches the Codex spool, because it is the same person and the same patience.
|
|
57
|
+
export const PERMISSION_TIMEOUT_MS = 15 * 60_000;
|
|
58
|
+
export const REQUEST_TIMEOUT_MS = 2 * 60_000;
|
|
59
|
+
// A line that never ends is not a message, it is a leak.
|
|
60
|
+
export const MAX_FRAME_LENGTH = 16 * 1024 * 1024;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* THE OPTION THAT MEANS NO, out of the ones this agent actually offered.
|
|
64
|
+
*
|
|
65
|
+
* `reject_once` before `reject_always`, because refusing one operation is the
|
|
66
|
+
* smaller claim and the only one a single card can honestly make: remembering
|
|
67
|
+
* a refusal forever is a different decision that nobody pressed. Null when the
|
|
68
|
+
* agent offered no refusal at all, which is answered by cancelling the turn.
|
|
69
|
+
*/
|
|
70
|
+
export function rejectionOption(options = []) {
|
|
71
|
+
for (const kind of REJECTIONS) {
|
|
72
|
+
const found = options.find(option => option?.kind === kind);
|
|
73
|
+
if (found?.optionId) return found.optionId;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether an id the asker chose is one the agent really put on the table. */
|
|
79
|
+
export function offeredOption(options = [], optionId) {
|
|
80
|
+
return options.some(option => option?.optionId === optionId) ? optionId : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createAcpClient({
|
|
84
|
+
transport = null,
|
|
85
|
+
spawn = null,
|
|
86
|
+
clientInfo = { name: 'agentbox', version: '1.0.0' },
|
|
87
|
+
onNotification = null,
|
|
88
|
+
onPermission = null,
|
|
89
|
+
permissionTimeoutMs = PERMISSION_TIMEOUT_MS,
|
|
90
|
+
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
91
|
+
maxFrameLength = MAX_FRAME_LENGTH,
|
|
92
|
+
stderrLimit = 64 * 1024,
|
|
93
|
+
} = {}) {
|
|
94
|
+
const child = transport ?? spawn?.();
|
|
95
|
+
if (!child) throw Error('createAcpClient needs a transport or a spawn that makes one');
|
|
96
|
+
|
|
97
|
+
const pending = new Map(); // our request id -> { resolve, reject, timer, method }
|
|
98
|
+
const permissions = new Map(); // their request id -> { sessionId, options, answer }
|
|
99
|
+
const decoder = new StringDecoder('utf8');
|
|
100
|
+
let nextId = 1;
|
|
101
|
+
let buffer = '';
|
|
102
|
+
let stderrText = '';
|
|
103
|
+
let gone = null; // null while live; the reason once not
|
|
104
|
+
let negotiated = null; // what initialize came back with
|
|
105
|
+
|
|
106
|
+
/* ------------------------------- writing -------------------------------- */
|
|
107
|
+
|
|
108
|
+
function write(message) {
|
|
109
|
+
if (gone) return false;
|
|
110
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
111
|
+
try {
|
|
112
|
+
// A write that is accepted and then fails reports it in the callback,
|
|
113
|
+
// and that is the one that matters: a refusal that was "written" and
|
|
114
|
+
// never arrived cannot be told apart from an approval.
|
|
115
|
+
child.stdin.write(line, error => { if (error) close(`Write failed: ${error.message}`); });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
close(`Write failed: ${error.message}`);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function request(method, params) {
|
|
124
|
+
if (gone) return Promise.reject(Error(gone));
|
|
125
|
+
const id = nextId++;
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
pending.delete(id);
|
|
129
|
+
reject(Error(`${method} did not answer within ${requestTimeoutMs}ms`));
|
|
130
|
+
}, requestTimeoutMs);
|
|
131
|
+
timer.unref?.();
|
|
132
|
+
pending.set(id, { resolve, reject, timer, method });
|
|
133
|
+
if (!write({ jsonrpc: '2.0', id, method, params })) {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
pending.delete(id);
|
|
136
|
+
reject(Error(gone ?? 'The agent connection is gone'));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const notify = (method, params) => write({ jsonrpc: '2.0', method, params });
|
|
142
|
+
|
|
143
|
+
/* ------------------------------ permissions ------------------------------ */
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Answer one permission request, once.
|
|
147
|
+
*
|
|
148
|
+
* `optionId` when the person chose one of the offered options; null for
|
|
149
|
+
* `cancelled`, which is what a cancelled turn and an agent that offered no
|
|
150
|
+
* refusal both come to. The entry is dropped before the write, so a late
|
|
151
|
+
* answer from a card, a deadline and a cancellation racing each other still
|
|
152
|
+
* only ever put one response on the wire.
|
|
153
|
+
*/
|
|
154
|
+
function settlePermission(id, optionId) {
|
|
155
|
+
const entry = permissions.get(id);
|
|
156
|
+
if (!entry) return false;
|
|
157
|
+
permissions.delete(id);
|
|
158
|
+
clearTimeout(entry.timer);
|
|
159
|
+
const outcome = optionId ? { outcome: 'selected', optionId } : { outcome: 'cancelled' };
|
|
160
|
+
write({ jsonrpc: '2.0', id, result: { outcome } });
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function askPermission(id, params) {
|
|
165
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
166
|
+
const deny = () => settlePermission(id, rejectionOption(options));
|
|
167
|
+
const timer = setTimeout(deny, permissionTimeoutMs);
|
|
168
|
+
timer.unref?.();
|
|
169
|
+
permissions.set(id, { sessionId: params?.sessionId ?? null, options, timer });
|
|
170
|
+
if (!onPermission) return deny();
|
|
171
|
+
// The asker may answer whenever: the reading loop is not waiting on it.
|
|
172
|
+
(async () => onPermission({ requestId: id, ...params }))()
|
|
173
|
+
.then(chosen => settlePermission(id, offeredOption(options, chosen)))
|
|
174
|
+
.catch(() => deny());
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ------------------------------- reading -------------------------------- */
|
|
179
|
+
|
|
180
|
+
function handle(message) {
|
|
181
|
+
if (message.id != null && (message.result !== undefined || message.error !== undefined)) {
|
|
182
|
+
const waiting = pending.get(message.id);
|
|
183
|
+
if (!waiting) return; // a response to something we gave up on
|
|
184
|
+
pending.delete(message.id);
|
|
185
|
+
clearTimeout(waiting.timer);
|
|
186
|
+
if (message.error) waiting.reject(Error(`${waiting.method}: ${message.error.message ?? 'failed'}`));
|
|
187
|
+
else waiting.resolve(message.result);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (message.id != null && message.method) {
|
|
191
|
+
if (message.method === 'session/request_permission') return void askPermission(message.id, message.params);
|
|
192
|
+
// Everything else is a capability we did not advertise. Say so.
|
|
193
|
+
write({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `${message.method} is not supported by this client` } });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (message.method) onNotification?.(message.method, message.params ?? {});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function absorb(chunk) {
|
|
200
|
+
if (gone) return;
|
|
201
|
+
buffer += decoder.write(chunk);
|
|
202
|
+
let index;
|
|
203
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
204
|
+
const line = buffer.slice(0, index).trim();
|
|
205
|
+
buffer = buffer.slice(index + 1);
|
|
206
|
+
if (!line) continue;
|
|
207
|
+
let message = null;
|
|
208
|
+
try { message = JSON.parse(line); } catch { continue; } // a line we cannot read is not a reason to die
|
|
209
|
+
if (message && typeof message === 'object') handle(message);
|
|
210
|
+
}
|
|
211
|
+
if (buffer.length > maxFrameLength) close(`A frame longer than ${maxFrameLength} bytes was too long to be a message`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function close(reason) {
|
|
215
|
+
if (gone) return gone;
|
|
216
|
+
gone = reason;
|
|
217
|
+
// Every question still standing goes with the connection: an answer whose
|
|
218
|
+
// asker has quit is worth nothing, and the agent is already gone.
|
|
219
|
+
for (const id of [...permissions.keys()]) {
|
|
220
|
+
const entry = permissions.get(id);
|
|
221
|
+
permissions.delete(id);
|
|
222
|
+
clearTimeout(entry.timer);
|
|
223
|
+
}
|
|
224
|
+
for (const [id, waiting] of [...pending]) {
|
|
225
|
+
pending.delete(id);
|
|
226
|
+
clearTimeout(waiting.timer);
|
|
227
|
+
waiting.reject(Error(reason));
|
|
228
|
+
}
|
|
229
|
+
try { child.stdin.end(); } catch {}
|
|
230
|
+
try { child.kill?.(); } catch {}
|
|
231
|
+
return gone;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
child.stdout?.on('data', absorb);
|
|
235
|
+
child.stdout?.on('end', () => close(gone ?? 'The agent closed its output'));
|
|
236
|
+
child.stderr?.on('data', chunk => {
|
|
237
|
+
if (stderrText.length < stderrLimit) stderrText += chunk.toString().slice(0, stderrLimit - stderrText.length);
|
|
238
|
+
});
|
|
239
|
+
child.on?.('error', error => close(`The agent could not be run: ${error.message}`));
|
|
240
|
+
child.on?.('exit', code => close(gone ?? `The agent exited with code ${code}`));
|
|
241
|
+
|
|
242
|
+
/* -------------------------------- the API -------------------------------- */
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
async initialize() {
|
|
246
|
+
negotiated = await request('initialize', {
|
|
247
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
248
|
+
clientCapabilities: { fs: { ...CLIENT_CAPABILITIES.fs }, terminal: CLIENT_CAPABILITIES.terminal },
|
|
249
|
+
clientInfo,
|
|
250
|
+
});
|
|
251
|
+
return negotiated;
|
|
252
|
+
},
|
|
253
|
+
/**
|
|
254
|
+
* WHAT THE AGENT SAID IT CAN DO, and nothing we assumed. Speaking ACP does
|
|
255
|
+
* not make a tool resumable: `loadSession` is optional in the protocol, so
|
|
256
|
+
* a tool that cannot do it must be read as cannot rather than as untested.
|
|
257
|
+
*/
|
|
258
|
+
can(capability) {
|
|
259
|
+
const agent = negotiated?.agentCapabilities ?? {};
|
|
260
|
+
if (capability === 'image' || capability === 'audio' || capability === 'embeddedContext') {
|
|
261
|
+
return agent.promptCapabilities?.[capability] === true;
|
|
262
|
+
}
|
|
263
|
+
return agent[capability] === true;
|
|
264
|
+
},
|
|
265
|
+
authMethods: () => negotiated?.authMethods ?? [],
|
|
266
|
+
authenticate: methodId => request('authenticate', { methodId }),
|
|
267
|
+
newSession: ({ cwd, mcpServers = [], additionalDirectories }) =>
|
|
268
|
+
request('session/new', { cwd, mcpServers, ...(additionalDirectories ? { additionalDirectories } : {}) }),
|
|
269
|
+
loadSession({ sessionId, cwd, mcpServers = [] }) {
|
|
270
|
+
// Refused before anything reaches the wire, so a thread cannot be
|
|
271
|
+
// silently restarted as an empty conversation on a tool that cannot
|
|
272
|
+
// resume. The caller's job is then to say so on the row.
|
|
273
|
+
if (!this.can('loadSession')) return Promise.reject(Error('This agent cannot resume a session, so there is no thread to load'));
|
|
274
|
+
return request('session/load', { sessionId, cwd, mcpServers });
|
|
275
|
+
},
|
|
276
|
+
prompt: ({ sessionId, text, blocks = null }) =>
|
|
277
|
+
request('session/prompt', { sessionId, prompt: blocks ?? [{ type: 'text', text }] }),
|
|
278
|
+
/**
|
|
279
|
+
* Stop the turn, and answer what it was parked on. The protocol says a
|
|
280
|
+
* cancelled turn MUST be answered `cancelled`, and this is the only place
|
|
281
|
+
* that is true of, so it happens here rather than at a call site.
|
|
282
|
+
*/
|
|
283
|
+
cancel(sessionId) {
|
|
284
|
+
for (const [id, entry] of [...permissions]) {
|
|
285
|
+
if (!sessionId || entry.sessionId === sessionId) settlePermission(id, null);
|
|
286
|
+
}
|
|
287
|
+
return notify('session/cancel', { sessionId });
|
|
288
|
+
},
|
|
289
|
+
/** An answer from a card. False when the question is no longer standing. */
|
|
290
|
+
answerPermission: (requestId, optionId) => settlePermission(requestId, optionId),
|
|
291
|
+
openPermissions: () => permissions.size,
|
|
292
|
+
closed: () => gone,
|
|
293
|
+
close,
|
|
294
|
+
diagnostics: () => stderrText,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// EVERY FOLDER CLAUDE CODE AND CODEX KEEP SESSIONS IN, so the import reads all
|
|
2
|
+
// of them rather than the one each tool uses by default (w-e46f2e9cea).
|
|
3
|
+
//
|
|
4
|
+
// Claude Code writes a session to `$CLAUDE_CONFIG_DIR/projects`, and to
|
|
5
|
+
// `~/.claude/projects` only when that variable is unset (read out of the 2.1.293
|
|
6
|
+
// binary, 2026-10-11). Codex does the same with `CODEX_HOME` and `~/.codex`.
|
|
7
|
+
// The import read only the two defaults, and on the Mac this was measured on
|
|
8
|
+
// every Claude Code session its owner had run lived in `~/.claude-2`. The walk's
|
|
9
|
+
// last card is drawn only when the finder hands something back, so a finder
|
|
10
|
+
// looking in the wrong folder does not draw an empty card. It draws no card,
|
|
11
|
+
// and the person is never told importing exists.
|
|
12
|
+
//
|
|
13
|
+
// READING THE VARIABLE IS NOT ENOUGH. An app opened from the Dock never sees
|
|
14
|
+
// what a shell profile exports, so somebody with `export CLAUDE_CONFIG_DIR=...`
|
|
15
|
+
// in their .zshrc gets an app whose environment has no such variable. The
|
|
16
|
+
// folders are looked for instead, by the same rule main/account-discovery.mjs
|
|
17
|
+
// uses to find a second login: `.claude` or `.claude-<anything>` directly under
|
|
18
|
+
// home. That module asks whether a folder holds a LOGIN; this one asks whether
|
|
19
|
+
// it holds SESSIONS, which is a different question with a different answer. A
|
|
20
|
+
// backup folder holds no login and may hold the only copy of a conversation,
|
|
21
|
+
// and two folders signed in as one account are one account but two sets of
|
|
22
|
+
// sessions.
|
|
23
|
+
//
|
|
24
|
+
// A FOLDER COUNTS ONLY IF IT HAS SESSIONS IN IT: `projects/` for Claude Code,
|
|
25
|
+
// `sessions/` for Codex. `~/.claude-worktrees` is named like a data folder and
|
|
26
|
+
// is not one. A folder named `claude-src`, with no dot, is somebody's checkout
|
|
27
|
+
// and is never looked at.
|
|
28
|
+
//
|
|
29
|
+
// Duplicates are the reader's to settle, by session id: a backup folder with a
|
|
30
|
+
// copy of a conversation is ordinary, and the newest copy is the one to offer.
|
|
31
|
+
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import { shellDataDirsNow } from './shell-path.mjs';
|
|
36
|
+
|
|
37
|
+
const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
38
|
+
|
|
39
|
+
/** `.claude`, `.claude-work`, `.codex-2`: every directory under home named `.<tool>` or `.<tool>-<anything>`. */
|
|
40
|
+
function shapedUnderHome(home, tool) {
|
|
41
|
+
let names = [];
|
|
42
|
+
try { names = fs.readdirSync(home); } catch { return []; }
|
|
43
|
+
return names
|
|
44
|
+
.filter((n) => n === `.${tool}` || n.startsWith(`.${tool}-`))
|
|
45
|
+
.sort()
|
|
46
|
+
.map((n) => path.join(home, n));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The candidates, in the order given, kept when they hold `inner/` and named
|
|
51
|
+
* once each. Two spellings of one folder (a trailing slash, a symlink) are
|
|
52
|
+
* one folder. */
|
|
53
|
+
function keepWithSessions(candidates, inner) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const c of candidates) {
|
|
57
|
+
if (typeof c !== 'string' || !c.trim()) continue;
|
|
58
|
+
const dir = path.resolve(c.trim());
|
|
59
|
+
let real = dir;
|
|
60
|
+
try { real = fs.realpathSync(dir); } catch { continue; }
|
|
61
|
+
if (seen.has(real)) continue;
|
|
62
|
+
if (!isDir(path.join(dir, inner))) continue;
|
|
63
|
+
seen.add(real);
|
|
64
|
+
out.push(dir);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A configured profile is 'default' (the tool's own default folder) or a folder. */
|
|
70
|
+
const profileDirs = (configured, dflt) => (Array.isArray(configured) ? configured : [configured])
|
|
71
|
+
.filter((p) => typeof p === 'string' && p.trim())
|
|
72
|
+
.map((p) => (p === 'default' ? dflt : p));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* EVERY CLAUDE CODE DATA FOLDER ON THIS MACHINE THAT HOLDS SESSIONS.
|
|
76
|
+
*
|
|
77
|
+
* `env` is the app's own environment and is passed in rather than read here,
|
|
78
|
+
* so a test with a made-up home is not handed the real folder the test runner
|
|
79
|
+
* happens to have in CLAUDE_CONFIG_DIR. `configured` is `authProfiles`. The
|
|
80
|
+
* last candidate is `~/.config/claude`, where some Claude Code versions kept
|
|
81
|
+
* their data; 2.1.293 no longer does, but a machine that still has sessions
|
|
82
|
+
* there gets them read. */
|
|
83
|
+
export function claudeDataDirs({ home = os.homedir(), env = {}, configured = [] } = {}) {
|
|
84
|
+
const dflt = path.join(home, '.claude');
|
|
85
|
+
const xdg = env?.XDG_CONFIG_HOME ? path.join(env.XDG_CONFIG_HOME, 'claude') : path.join(home, '.config', 'claude');
|
|
86
|
+
return keepWithSessions([
|
|
87
|
+
env?.CLAUDE_CONFIG_DIR,
|
|
88
|
+
...profileDirs(configured, dflt),
|
|
89
|
+
dflt,
|
|
90
|
+
...shapedUnderHome(home, 'claude'),
|
|
91
|
+
xdg,
|
|
92
|
+
], 'projects');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* BOTH LISTS, AS THE RUNNING APP SEES THEM: its own environment, the Claude
|
|
97
|
+
* profiles it was configured with, and the Codex homes its supervisor runs on.
|
|
98
|
+
* The one call the import card, the Codex watch and the import answer share,
|
|
99
|
+
* so the three cannot disagree about where somebody's sessions are. */
|
|
100
|
+
export function sessionFolders({ claudeProfiles = [], codexHomes = [], env = process.env, home = os.homedir() } = {}) {
|
|
101
|
+
return {
|
|
102
|
+
dirs: claudeDataDirs({ home, env, configured: claudeProfiles }),
|
|
103
|
+
codexDirs: codexDataDirs({ home, env, configured: codexHomes }),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* THE CODEX HOMES THE SUPERVISOR RUNS ON, or as many of them as can be named.
|
|
109
|
+
* Each profile is resolved on its own, so one that throws costs that one home
|
|
110
|
+
* and not the list (found by Codex's review of this change, 2026-10-11). */
|
|
111
|
+
export function codexHomesOf(supervisor) {
|
|
112
|
+
let profiles = [];
|
|
113
|
+
try { profiles = supervisor?._codexProfiles?.() ?? []; } catch { profiles = []; }
|
|
114
|
+
if (!Array.isArray(profiles)) return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const p of profiles) {
|
|
117
|
+
try {
|
|
118
|
+
const h = supervisor._codexProfileHome(p);
|
|
119
|
+
if (typeof h === 'string' && h.trim()) out.push(h);
|
|
120
|
+
} catch { /* that one home is unknown; the rest still count */ }
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* WHAT THE RUNNING APP HANDS EVERY READER: its own config and supervisor, made
|
|
127
|
+
* safe. A missing config, a supervisor without the Codex methods, or a home
|
|
128
|
+
* that will not resolve each lose only their own part. The first draft built
|
|
129
|
+
* both lists inside one try, so any one of those left the import reading
|
|
130
|
+
* nothing beyond the defaults, and without the app's own CLAUDE_CONFIG_DIR or
|
|
131
|
+
* CODEX_HOME either. */
|
|
132
|
+
export function appSessionFolders({ config = null, supervisor = null, env = process.env, shellEnv = shellDataDirsNow(), home = os.homedir() } = {}) {
|
|
133
|
+
let claudeProfiles = [];
|
|
134
|
+
try { claudeProfiles = config?.authProfiles ?? []; } catch { claudeProfiles = []; }
|
|
135
|
+
if (!Array.isArray(claudeProfiles)) claudeProfiles = [claudeProfiles];
|
|
136
|
+
// AND WHAT THE LOGIN SHELL EXPORTS (main/shell-path.mjs), which an app opened
|
|
137
|
+
// from the Dock never sees in its own environment.
|
|
138
|
+
return sessionFolders({
|
|
139
|
+
claudeProfiles: [...claudeProfiles, shellEnv?.CLAUDE_CONFIG_DIR],
|
|
140
|
+
codexHomes: [...codexHomesOf(supervisor), shellEnv?.CODEX_HOME],
|
|
141
|
+
env,
|
|
142
|
+
home,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* EVERY CODEX DATA FOLDER ON THIS MACHINE THAT HOLDS SESSIONS. Same rule:
|
|
148
|
+
* `CODEX_HOME`, whatever the app was configured with, `~/.codex` and every
|
|
149
|
+
* `~/.codex-<name>`. */
|
|
150
|
+
export function codexDataDirs({ home = os.homedir(), env = {}, configured = [] } = {}) {
|
|
151
|
+
const dflt = path.join(home, '.codex');
|
|
152
|
+
return keepWithSessions([
|
|
153
|
+
env?.CODEX_HOME,
|
|
154
|
+
...profileDirs(configured, dflt),
|
|
155
|
+
dflt,
|
|
156
|
+
...shapedUnderHome(home, 'codex'),
|
|
157
|
+
], 'sessions');
|
|
158
|
+
}
|