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.
Files changed (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. 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
- **As a Mac app.** Build it from this repo:
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 start` builds the app and opens it. On first run it walks you through
56
- connecting Claude Code or Codex and choosing a folder to work in.
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. Defaults to a folder named after the app
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. `~/.agentbox` is the plain answer and `--dir` is the escape hatch.
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 ?? path.join(os.homedir(), '.agentbox')),
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
- // Whoever was signed in to the team last time is signed in again.
60
- booted.team?.start().then(() => booted.supervisor.wake?.()).catch((err) => console.warn(`team: ${err.message}`));
61
- // AND THE AGENTS' CLOCK, as the desktop starts it (main/main.mjs). Without it a
62
- // tab only moved work when something woke the supervisor: a queued thread sat
63
- // queued and a scheduled one never fired (found 2026-10-01 on a test copy).
64
- booted.supervisor.start();
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,
@@ -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 in the message by its full
63
- path, one per line. The app draws it where you name it. Not in your code
64
- checkout, which is deleted once the work ships, and never in /tmp, which the
65
- app will not draw at all.
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
 
@@ -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
- const file = path.join(dir || defaultClaudeHome(home), '.claude.json');
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
+ }