agentbox-app 0.1.6 → 0.1.11

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 (244) hide show
  1. package/README.md +209 -55
  2. package/bin/agentbox.mjs +6 -0
  3. package/briefs/message-rules.md +13 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +9 -10
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +114 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/artifact-path.mjs +11 -0
  16. package/main/claude-bin.mjs +31 -5
  17. package/main/claude-input.mjs +45 -3
  18. package/main/claude-plan.mjs +11 -11
  19. package/main/code-change.mjs +3 -3
  20. package/main/codex-account.mjs +1 -1
  21. package/main/codex-bin.mjs +17 -0
  22. package/main/codex-memory-gate.mjs +260 -0
  23. package/main/codex-models.mjs +12 -7
  24. package/main/codex-threads.mjs +46 -23
  25. package/main/codex-usage-file.mjs +7 -7
  26. package/main/codex-usage.mjs +4 -4
  27. package/main/codex-watch.mjs +12 -12
  28. package/main/config.mjs +68 -8
  29. package/main/dev-window.mjs +9 -9
  30. package/main/doc-scheme.mjs +2 -0
  31. package/main/engine-setup.mjs +193 -0
  32. package/main/feedback.mjs +55 -0
  33. package/main/folder-pictures.mjs +48 -0
  34. package/main/frame-scrollbars.mjs +90 -0
  35. package/main/fresh-user.mjs +6 -3
  36. package/main/git-change-offthread.mjs +70 -0
  37. package/main/git-change-thread.mjs +11 -0
  38. package/main/git-change.mjs +1 -1
  39. package/main/img-scheme.mjs +70 -1
  40. package/main/instruction-settings.mjs +92 -54
  41. package/main/ipc.mjs +316 -42
  42. package/main/leftovers.mjs +518 -0
  43. package/main/live-replies.mjs +43 -5
  44. package/main/machine.mjs +47 -0
  45. package/main/main.mjs +119 -15
  46. package/main/memory-gate-history.mjs +254 -0
  47. package/main/memory-gate-server.mjs +461 -0
  48. package/main/memory-gate.mjs +206 -0
  49. package/main/message-priority.mjs +87 -0
  50. package/main/rail-note.mjs +2 -2
  51. package/main/refresh-agent-models.mjs +20 -0
  52. package/main/repeats.mjs +4 -4
  53. package/main/row-label.mjs +114 -51
  54. package/main/serve.mjs +67 -5
  55. package/main/settings.mjs +223 -26
  56. package/main/ship-queue.mjs +279 -0
  57. package/main/sign-in-files.mjs +39 -0
  58. package/main/source-updater.mjs +298 -0
  59. package/main/store/home.mjs +47 -2
  60. package/main/store/work-items.mjs +150 -6
  61. package/main/store.mjs +235 -9
  62. package/main/supervisor.mjs +1444 -406
  63. package/main/task-commands.mjs +8 -10
  64. package/main/task-folders.mjs +74 -13
  65. package/main/task-remote-control.mjs +3 -3
  66. package/main/task-terminals.mjs +2 -1
  67. package/main/team/index.mjs +440 -0
  68. package/main/team/memory-cloud.mjs +247 -0
  69. package/main/team/projects.mjs +162 -0
  70. package/main/team/session.mjs +225 -0
  71. package/main/team/sign-in.mjs +106 -0
  72. package/main/team/supabase-backend.mjs +231 -0
  73. package/main/team/sync.mjs +163 -0
  74. package/main/updater.mjs +15 -5
  75. package/main/write-before-reload.mjs +50 -0
  76. package/mcp/core/page.mjs +9 -11
  77. package/mcp/core/work.mjs +91 -25
  78. package/mcp/tools.mjs +12 -6
  79. package/package.json +21 -9
  80. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  81. package/renderer/dist/assets/index-B-2Az952.css +32 -0
  82. package/renderer/dist/assets/index-D6qncgai.js +1168 -0
  83. package/renderer/dist/index.html +2 -2
  84. package/scripts/memory-gate-hook.sh +59 -0
  85. package/shared/agent-import.mjs +38 -15
  86. package/shared/agents.d.mts +26 -8
  87. package/shared/agents.mjs +100 -108
  88. package/shared/analytics-events.mjs +19 -2
  89. package/shared/answers.mjs +12 -0
  90. package/shared/bridge-map.d.mts +12 -0
  91. package/shared/bridge-map.mjs +37 -1
  92. package/shared/claude-commands.generated.mjs +2 -2
  93. package/shared/claude-models.generated.mjs +4 -4
  94. package/shared/claude-usage.mjs +6 -6
  95. package/shared/codex-import.mjs +7 -7
  96. package/shared/codex-modes.d.mts +50 -0
  97. package/shared/codex-usage.mjs +7 -8
  98. package/shared/demo-world.mjs +5 -5
  99. package/shared/engines.mjs +27 -10
  100. package/shared/feedback.d.mts +14 -0
  101. package/shared/feedback.mjs +44 -0
  102. package/shared/first-run-examples.mjs +4 -4
  103. package/shared/first-run-practice.d.mts +16 -0
  104. package/shared/first-run-practice.mjs +176 -70
  105. package/shared/fresh-user-home.mjs +22 -3
  106. package/shared/instruction-defaults.json +2 -2
  107. package/shared/product-name.mjs +4 -2
  108. package/shared/project-folder-check.mjs +6 -0
  109. package/shared/rank.d.mts +1 -0
  110. package/shared/rank.mjs +22 -0
  111. package/shared/referenced-files.mjs +1 -1
  112. package/shared/schemes.d.mts +11 -0
  113. package/shared/schemes.mjs +1 -1
  114. package/shared/shortcuts.d.mts +41 -0
  115. package/shared/shortcuts.mjs +236 -0
  116. package/shared/spawn-trouble.d.mts +2 -0
  117. package/shared/spawn-trouble.mjs +39 -18
  118. package/shared/team-rules.d.mts +17 -0
  119. package/shared/team-rules.mjs +230 -0
  120. package/shared/team-status.d.mts +11 -0
  121. package/shared/team-status.mjs +90 -0
  122. package/shared/terminal-links.d.mts +2 -0
  123. package/shared/terminal-links.mjs +55 -0
  124. package/shared/terminal-state.d.mts +1 -0
  125. package/shared/terminal-state.mjs +16 -0
  126. package/shared/thread-cards.d.mts +19 -0
  127. package/shared/thread-cards.mjs +254 -0
  128. package/shared/usage.d.mts +1 -1
  129. package/shared/usage.mjs +6 -7
  130. package/shared/work-items.mjs +255 -27
  131. package/shared/work-lines.mjs +393 -16
  132. package/worker-permissions.json +1 -1
  133. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  134. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  135. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  136. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  137. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  138. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  139. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  140. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  141. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  142. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  143. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  144. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  145. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  146. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  147. package/renderer/dist/assets/index-D9hCAVPl.js +0 -684
  148. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  149. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  150. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  151. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  152. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  153. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  154. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  155. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  156. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  157. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  158. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  159. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  160. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  161. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  162. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  163. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  164. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  165. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  166. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  167. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  168. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  169. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  170. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  171. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  172. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  173. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  174. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  175. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  176. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  177. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  178. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  179. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  180. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  181. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  182. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  183. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  184. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  185. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  186. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  187. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  188. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  189. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  190. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  191. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  192. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  193. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  194. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  195. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  196. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  197. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  198. package/renderer/dist/emulator/data/compression/README.md +0 -11
  199. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  200. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  201. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  202. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  203. package/renderer/dist/emulator/data/cores/README.md +0 -22
  204. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  205. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  206. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  207. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  208. package/renderer/dist/emulator/data/cores/package.json +0 -65
  209. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  210. package/renderer/dist/emulator/data/emulator.css +0 -1614
  211. package/renderer/dist/emulator/data/loader.js +0 -170
  212. package/renderer/dist/emulator/data/localization/README.md +0 -63
  213. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  214. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  215. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  216. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  217. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  218. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  219. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  220. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  221. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  222. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  223. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  224. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  225. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  226. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  227. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  228. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  229. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  230. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  231. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  232. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  233. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  234. package/renderer/dist/emulator/data/src/compression.js +0 -142
  235. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  236. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  237. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  238. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  239. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  240. package/renderer/dist/emulator/data/src/storage.js +0 -125
  241. package/renderer/dist/emulator/data/version.json +0 -5
  242. package/renderer/dist/emulator/host.js +0 -299
  243. package/renderer/dist/emulator/index.html +0 -28
  244. package/renderer/dist/emulator/provenance.json +0 -6
@@ -6,7 +6,8 @@
6
6
  // the app found nothing, said "missing" on a grey dot in Settings, and gave no
7
7
  // sentence and no next step. It read as broken rather than as unfinished.
8
8
  //
9
- // HER RULE, 2026-08-23, and it is the one this file exists to keep:
9
+ // THE RULE this file exists to keep: never tell someone Claude Code is
10
+ // missing unless we are certain it is.
10
11
  //
11
12
  // So there are two different questions here and they must never be answered by
12
13
  // the same value. `found` is whether a runnable file is really there, and it
@@ -45,10 +46,10 @@
45
46
  // the same way, and it is a real state: the installer points ~/.local/bin/claude
46
47
  // at a versioned file and swaps that file on every update.
47
48
  //
48
- // Nothing here overrides her file. An explicit `claudeBin` in zero.config.json
49
- // wins untouched and is never searched around, because that file's first line
50
- // is "all paths explicit, no discovery magic" and a search that second-guesses
51
- // a setting she wrote is exactly that.
49
+ // Nothing here overrides the config file. An explicit `claudeBin` in
50
+ // zero.config.json wins untouched and is never searched around, because that
51
+ // file promises explicit paths with no discovery, and a search that
52
+ // second-guesses a written setting would break that promise.
52
53
 
53
54
  import fs from 'node:fs';
54
55
  import os from 'node:os';
@@ -96,6 +97,27 @@ function defaultReaddir(dir) {
96
97
  try { return fs.readdirSync(dir); } catch { return []; }
97
98
  }
98
99
 
100
+ // THE COPY THE CLAUDE APP KEEPS FOR ITSELF, one folder per version, newest
101
+ // first. Somebody who has only ever used Claude Code inside the Claude app has
102
+ // this and nothing else (w-9f6975906c, measured 2026-10-05: 2.1.209 there, and
103
+ // it shared the CLI's sign-in). It is searched LAST, after the shell, because it
104
+ // updates when the app does and a real install is fresher.
105
+ export function appCopyPaths(home = os.homedir(), readdir = defaultReaddir) {
106
+ const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
107
+ return readdir(dir)
108
+ .filter((v) => /^\d+(\.\d+)*$/.test(v))
109
+ .sort(newestFirst)
110
+ .map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
111
+ }
112
+
113
+ function newestFirst(a, b) {
114
+ const x = a.split('.').map(Number), y = b.split('.').map(Number);
115
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
116
+ if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
117
+ }
118
+ return 0;
119
+ }
120
+
99
121
  // WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
100
122
  // Mac. None of them is a binary and none of them is ever spawned; they exist
101
123
  // only to stop the app announcing an absence it cannot support.
@@ -263,6 +285,10 @@ export function findClaudeBin({
263
285
  const fromShell = shellSaid(shellLookup());
264
286
  if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
265
287
 
288
+ for (const candidate of appCopyPaths(home, readdir)) {
289
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
290
+ }
291
+
266
292
  // NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
267
293
  // shell we could reach missed, so the last question is whether this Mac shows
268
294
  // any sign of Claude Code anyway. If it does, we looked in the wrong places
@@ -12,6 +12,10 @@ function turnClosed(message) {
12
12
  return error;
13
13
  }
14
14
 
15
+ // How long a finished background job may go without the agent waking for it
16
+ // before the session is closed anyway. Measured wakes arrive in about a second.
17
+ const WAKE_GRACE_MS = 2 * 60 * 1000;
18
+
15
19
  export function claudeStreamArgs(args) {
16
20
  const copy = [...args];
17
21
  const index = copy.indexOf('-p');
@@ -27,7 +31,21 @@ export function attachClaudeInput(child) {
27
31
  const controls = new Map();
28
32
  const seenUsers = new Set();
29
33
  const pending = new Map();
30
- const closeInput = () => { closed = true; child.stdin.end(); };
34
+ // WHAT THE AGENT STARTED AND HAS NOT HEARD BACK FROM: a background command,
35
+ // a command Claude Code moved to the background past its time limit, a
36
+ // background helper. Claude Code lists them in `background_tasks_changed`
37
+ // and wakes the agent itself when one ends, but only while stdin is open. A
38
+ // result with any of these out is "I'll report once it finishes", not the
39
+ // end of the run, and closing there killed the suite it was waiting on
40
+ // (tests/an-agent-waiting-on-its-own-tests-does-not-come-back-to-you).
41
+ let waiting = [], wakeTimer = null;
42
+ // Input only ever closes at a result with nothing out, so a closed input is
43
+ // the one proof the run reached its end. A session that exits with input
44
+ // still open was stopped part-way, whatever its last turn said
45
+ // (tests/a-worker-whose-helpers-are-still-working-does-not-come-back-to-you).
46
+ let ended = false;
47
+ const closeInput = () => { closed = true; ended = true; clearTimeout(wakeTimer); child.stdin.end(); };
48
+ const mayClose = () => !held && atResult && !pending.size && !waiting.length;
31
49
  const fail = reason => {
32
50
  closed = true;
33
51
  for (const request of pending.values()) {
@@ -72,16 +90,29 @@ export function attachClaudeInput(child) {
72
90
  atResult = false;
73
91
  request.resolve({ accepted: true });
74
92
  }
93
+ if (event.type === 'system' && event.subtype === 'background_tasks_changed' && Array.isArray(event.tasks)) {
94
+ waiting = event.tasks;
95
+ // The last one ended between turns. Claude Code wakes the agent for
96
+ // it, and that turn's result closes as usual. The timer is only for a
97
+ // wake that never comes, so a session cannot sit open forever.
98
+ clearTimeout(wakeTimer);
99
+ if (!waiting.length && atResult && !closed) {
100
+ wakeTimer = setTimeout(() => { if (mayClose() && !closed) closeInput(); }, WAKE_GRACE_MS);
101
+ wakeTimer.unref?.();
102
+ }
103
+ }
75
104
  if (event.type === 'result') {
76
105
  atResult = true;
77
- if (!held && !pending.size) closeInput();
106
+ if (mayClose()) closeInput();
78
107
  }
79
108
  }
80
109
  });
110
+ child.waitingOn = () => [...waiting];
111
+ child.ranToTheEnd = () => ended;
81
112
  child.holdInput = value => {
82
113
  if (closed && value) throw Error('This session has ended. Open remote control again to resume it.');
83
114
  held = !!value;
84
- if (!held && atResult && !pending.size) closeInput();
115
+ if (mayClose()) closeInput();
85
116
  };
86
117
  child.control = (request, timeoutMs = 30000) => new Promise((resolve, reject) => {
87
118
  if (closed || !child.stdin.writable) return reject(Error('The agent connection is closed.'));
@@ -92,6 +123,17 @@ export function attachClaudeInput(child) {
92
123
  try { child.stdin.write(JSON.stringify({ type:'control_request', request_id, request }) + '\n'); }
93
124
  catch(error) { clearTimeout(timer); controls.delete(request_id); reject(error); }
94
125
  });
126
+ // SEND NOW: cut the step the agent is in so her waiting message is answered
127
+ // as a turn of its own, instead of after a command that may run for minutes
128
+ // (tests/send-now-cuts-the-current-step-so-the-agent-answers). Measured on a
129
+ // 45-second command: 43 s of waiting without it, 1.4 s to the answer with it.
130
+ // ONLY WHILE A MESSAGE OF HERS IS WAITING. The cut turn ends in a result,
131
+ // and with nothing pending that result closes the input and ends the run.
132
+ child.interrupt = () => {
133
+ if (closed || !pending.size) return Promise.resolve({ interrupted: false });
134
+ child.lastInterruptAt = Date.now();
135
+ return child.control({ subtype: 'interrupt' }).then(() => ({ interrupted: true }));
136
+ };
95
137
  // NO CLOCK ON THE ACKNOWLEDGEMENT. Claude replays a queued message at its next
96
138
  // break, and a break can be minutes away: a worker inside one long command
97
139
  // (a render poll, `sleep 12` fifty times) holds it until the command returns.
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // THE PROBLEM. Agentbox runs several Claude Code sessions at once and the number
6
6
  // is a flat default (`maxConcurrentSessions: 3`, main/config.mjs) that was
7
- // chosen on a Max subscription and has never been anything else. Nothing
7
+ // chosen with the biggest plan in mind and has never been anything else. Nothing
8
8
  // anywhere in the app mentions a plan: not the walk, not the Accounts page, not
9
9
  // a row that dies. So a person on a smaller plan gets three sessions started on
10
10
  // their behalf, all three burn the same allowance, and what they see is agents
@@ -12,16 +12,16 @@
12
12
  // founder was asking out loud while she watched a tester use it.
13
13
  //
14
14
  // THE ANSWER IS ALREADY ON THE DISK. Claude Code writes its own account profile
15
- // into its config file and refreshes it as it runs. MEASURED on her Mac
16
- // 2026-08-29 20:5x, both of her accounts:
15
+ // into its config file and refreshes it as it runs. On a machine with two
16
+ // accounts signed in, for example:
17
17
  //
18
18
  // ~/.claude.json oauthAccount.organizationType claude_max
19
19
  // oauthAccount.organizationRateLimitTier default_claude_max_20x
20
- // profileFetchedAt 2026-08-29 05:00Z
21
- // ~/.claude-second/.claude.json the same two values
22
- // profileFetchedAt 2026-08-29 20:47Z
20
+ // profileFetchedAt (within the day)
21
+ // ~/.claude-second/.claude.json the same two fields
22
+ // profileFetchedAt (within the day)
23
23
  //
24
- // Both fetched within the day, so the field is live rather than a one-time
24
+ // Each is refetched within the day, so the field is live rather than a one-time
25
25
  // stamp from a login months ago, and it is the honest source: it is Claude
26
26
  // Code's own answer about its own subscription, not a guess of ours.
27
27
  //
@@ -30,14 +30,14 @@
30
30
  // runs with nothing set, so its file is `~/.claude.json`; a second account runs
31
31
  // with CLAUDE_CONFIG_DIR pointing at a folder, so its file is inside that
32
32
  // folder (main/supervisor.mjs sets exactly that per spawn). Both are verified
33
- // above. Note that `~/.claude/.claude.json` also exists on her Mac and is NINE
34
- // DAYS STALE — it is what a run with CLAUDE_CONFIG_DIR=~/.claude left behind.
33
+ // above. Note that a `~/.claude/.claude.json` can also exist and be days
34
+ // STALE — it is what a run with CLAUDE_CONFIG_DIR=~/.claude left behind.
35
35
  // It is not read for the default account, and that is deliberate: the rule here
36
36
  // is Claude Code's own rule, not a search for the likeliest looking file.
37
37
  //
38
38
  // WHAT THIS DELIBERATELY DOES NOT DO. It does not guess the strings for plans
39
- // nobody here has seen. Only one plan has ever been measured on this machine,
40
- // and inventing `default_claude_pro` in a table would be a number nobody
39
+ // nobody here has seen in a real file, and inventing `default_claude_pro` in
40
+ // a table would be a number nobody
41
41
  // measured. So `max` is decided by looking for the word max, the label is
42
42
  // derived from the raw string mechanically, and anything unreadable comes back
43
43
  // `known: false` and changes nothing anywhere.
@@ -14,7 +14,7 @@
14
14
  // IT IS NOT ENOUGH ON ITS OWN, AND THAT WAS MEASURED, NOT ARGUED. An agent
15
15
  // that rewrites a file with sed or a heredoc puts nothing in its transcript,
16
16
  // and on her Mac 2026-08-24 that was 101 of the 105 runs that changed a file
17
- // over two days. Every one of those left David an empty card. So the disk is
17
+ // over two days. Every one of those left a tester an empty card. So the disk is
18
18
  // read too, by main/git-change.mjs, and `mergeChange` below puts the two
19
19
  // together: git's lines, the conversation's words, git winning where they
20
20
  // disagree.
@@ -39,9 +39,9 @@ const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
39
39
  // AND SCRATCH, WHICH IS NOT THE PRODUCT. A run writes throwaway scripts to
40
40
  // /tmp constantly and one of them landed in the tree as a folder called `tmp`
41
41
  // on the first real change we read (18 files, and one of them was
42
- // /tmp/shelf-shots.mjs). Dropped rather than shown: what David is being asked
42
+ // /tmp/shelf-shots.mjs). Dropped rather than shown: what a tester is being asked
43
43
  // to review is the product, and a file nobody will ever open again reads as
44
- // work he has to account for.
44
+ // work they have to account for.
45
45
  const SCRATCH = /^\/(private\/)?(tmp|var\/folders)\//;
46
46
 
47
47
  /**
@@ -35,7 +35,7 @@ const AUTH_CLAIM = 'https://api.openai.com/auth';
35
35
 
36
36
  /**
37
37
  * The plan ids whose proper names are known. Everything else is printed exactly
38
- * as Codex spells it, and that is deliberate: her own token reads "prolite",
38
+ * as Codex spells it, and that is deliberate: a real token can read "prolite",
39
39
  * which title-cases into "Prolite", a product name nobody at OpenAI has ever
40
40
  * used. Inventing a label is how a screen tells a confident lie about what
41
41
  * somebody is paying for, so an id we do not know is shown as the id. */
@@ -140,6 +140,19 @@ function defaultReaddir(dir) {
140
140
  try { return fs.readdirSync(dir); } catch { return []; }
141
141
  }
142
142
 
143
+ // THE COPY THE CHATGPT APP CARRIES INSIDE ITSELF. Somebody who has only ever
144
+ // used Codex in the ChatGPT app has this and nothing else (w-9f6975906c,
145
+ // measured 2026-10-05: codex-cli 0.158.0-alpha.2, sharing the CLI's sign-in).
146
+ // Searched LAST, after the shell, because it moves with the app's releases.
147
+ //
148
+ // AND THE ONE THE CODEX DESKTOP APP CARRIES (w-d5d632e503). Its reported place,
149
+ // not measured on a Mac here: none of the Macs this was written on had the app.
150
+ // A path that is not there costs one missed `exists`.
151
+ export function appCopyPaths(home = os.homedir()) {
152
+ const inside = ['ChatGPT.app/Contents/Resources/codex-cli/bin/codex', 'Codex.app/Contents/Resources/codex'];
153
+ return inside.flatMap((p) => [path.join('/Applications', p), path.join(home, 'Applications', p)]);
154
+ }
155
+
143
156
  // WHAT CODEX LEAVES BEHIND. None of these is a binary and none is ever spawned.
144
157
  // They exist only to stop the app claiming an absence it has not established.
145
158
  //
@@ -214,6 +227,10 @@ export function findCodexBin({
214
227
  const fromShell = shellSaid(shellLookup());
215
228
  if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
216
229
 
230
+ for (const candidate of appCopyPaths(home)) {
231
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
232
+ }
233
+
217
234
  // NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
218
235
  // shell we could reach missed, so the last question is whether this Mac shows
219
236
  // any sign of Codex anyway.
@@ -0,0 +1,260 @@
1
+ // HOW A CODEX WORKER'S COMMANDS COME TO ASK ABOUT MEMORY — w-e5225b62ba.
2
+ //
3
+ // w-3958c3753d built the whole of the decision (main/memory-gate.mjs), the
4
+ // coordinator (main/memory-gate-server.mjs) and the hook a worker runs
5
+ // (scripts/memory-gate-hook.sh) for Claude Code, and said plainly that Codex
6
+ // workers were not covered. This file is the Codex half, and it is small on
7
+ // purpose: everything measured below says Codex 0.160 presents a PreToolUse
8
+ // hook the way Claude Code does, so the script, the deny and the coordinator
9
+ // are carried over rather than rewritten.
10
+ //
11
+ // MEASURED ON THIS MAC, codex-cli 0.160.0, a real `codex app-server` and real
12
+ // turns, 2026-10-04. Read none of this as documentation; it was all run.
13
+ //
14
+ // THE PAYLOAD IS CLAUDE CODE'S, FIELD FOR FIELD:
15
+ // { session_id, turn_id, transcript_path, cwd, hook_event_name: "PreToolUse",
16
+ // model, permission_mode, tool_name: "Bash",
17
+ // tool_input: { command }, tool_use_id }
18
+ // and the deny it reads is the same `hookSpecificOutput.permissionDecision`
19
+ // object `denyOutput` already prints. A deny blocked the command and our
20
+ // sentence reached the model verbatim. A hook that slept eight seconds made
21
+ // the command wait and then run.
22
+ //
23
+ // THE MATCHER IS "Bash". A group with matcher "shell" listed as trusted and
24
+ // enabled and never fired once; with no matcher it fired and said its
25
+ // tool_name was "Bash". That one word was the difference between this working
26
+ // and silently doing nothing.
27
+ //
28
+ // THE FILE IS `hooks.json` IN THE CODEX HOME, `{ description?, hooks: {...} }`,
29
+ // whose event keys are PascalCase (`PreToolUse`) even though Codex's own
30
+ // errors name them in snake_case. An unknown event key is dropped in silence,
31
+ // which is why the key is pinned by a test rather than trusted to a comment.
32
+ //
33
+ // A HOOK ONLY RUNS ONCE TRUSTED, AND TRUST IS PER HOOK. `hooks/list` reports
34
+ // every hook with a `key`, a `currentHash` and a `trustStatus` of managed /
35
+ // untrusted / trusted / modified. Writing
36
+ // `hooks.state."<key>".trusted_hash = <currentHash>` through the app-server's
37
+ // own `config/batchWrite` turned OURS trusted and left a second hook in the
38
+ // same file untrusted, with an unrelated `[projects."..."]` table in
39
+ // config.toml untouched. That is the whole of "do not switch off hook trust
40
+ // for anyone's other hooks": we never touch the requirement, we satisfy it for
41
+ // one key. Editing our own command afterwards turned ours "modified" rather
42
+ // than leaving it trusted, so a hook somebody swapped under us does not
43
+ // inherit what we wrote.
44
+ //
45
+ // WHY THE ITEM IS NOT IN THE ENVIRONMENT, which is the one real difference from
46
+ // the Claude side. A Claude worker is a process per work item, so `memoryGateEnv`
47
+ // puts the item on it. ONE app-server serves every Codex thread of a login, so
48
+ // there is one environment for all of them and an item id on it would label
49
+ // every thread as whichever row started first. The thread names itself instead:
50
+ // `session_id` in the payload is the app-server's own `thread.id`, which
51
+ // main/codex.mjs already keeps as `session.sessionId`. The lookup lives in
52
+ // main/memory-gate-server.mjs (`ownerOf`).
53
+
54
+ import fsNode from 'node:fs';
55
+ import path from 'node:path';
56
+
57
+ /** What Codex calls the tool that runs a shell command. Measured, not guessed. */
58
+ export const CODEX_MATCHER = 'Bash';
59
+
60
+ /** The file in a Codex home that holds its hooks. */
61
+ export const HOOKS_FILE = 'hooks.json';
62
+
63
+ /** The two events Codex has that we want. There is no PostToolUseFailure. */
64
+ export const CODEX_EVENTS = ['PreToolUse', 'PostToolUse'];
65
+
66
+ /** How we know a hook in somebody's file is one of ours. */
67
+ const OUR_SCRIPT = 'memory-gate-hook.sh';
68
+
69
+ /**
70
+ * Whether a hook's command is one we wrote. It has to be the script ITSELF and
71
+ * not a file whose name merely starts the same way, or turning the switch off
72
+ * would delete `memory-gate-hook.sh.backup` out of somebody's config.
73
+ */
74
+ export function hookIsOurs(command) {
75
+ if (typeof command !== 'string') return false;
76
+ return new RegExp(`${OUR_SCRIPT}'?\\s+(pre|post)\\s*$`).test(command.trim());
77
+ }
78
+
79
+ /** A path inside a single-quoted shell word, app bundles and apostrophes included. */
80
+ function quoted(scriptPath) {
81
+ return `'${String(scriptPath).replace(/'/g, `'\\''`)}'`;
82
+ }
83
+
84
+ /**
85
+ * Our matcher groups, one per event.
86
+ *
87
+ * The pre hook's limit sits above the longest wait the gate will ever impose,
88
+ * for the reason the Claude side gives: a hook that times out lets its command
89
+ * run, so the app's own refusal has to arrive first. Codex counts it in seconds.
90
+ */
91
+ export function ourHookGroups(scriptPath, { maxWaitSec = 20 * 60 } = {}) {
92
+ const group = (mode, timeout) => ([{
93
+ matcher: CODEX_MATCHER,
94
+ hooks: [{ type: 'command', command: `${quoted(scriptPath)} ${mode}`, timeout }],
95
+ }]);
96
+ return {
97
+ PreToolUse: group('pre', maxWaitSec + 600),
98
+ PostToolUse: group('post', 10),
99
+ };
100
+ }
101
+
102
+ /** A hooks.json object, whatever shape the file on disk turned out to be. */
103
+ function asFile(file) {
104
+ const base = file && typeof file === 'object' && !Array.isArray(file) ? { ...file } : {};
105
+ base.hooks = base.hooks && typeof base.hooks === 'object' && !Array.isArray(base.hooks) ? { ...base.hooks } : {};
106
+ return base;
107
+ }
108
+
109
+ /** Every group in `list` that is not one of ours, in the order it was written. */
110
+ function withoutOurGroups(list) {
111
+ if (!Array.isArray(list)) return [];
112
+ return list.filter((g) => !(g?.hooks ?? []).some?.((h) => hookIsOurs(h?.command)));
113
+ }
114
+
115
+ /**
116
+ * The same hooks.json with our two hooks in it and everything else where it was.
117
+ *
118
+ * Ours go LAST so a hook somebody already had keeps running first, and any older
119
+ * entry of ours is replaced rather than kept beside the new one: the app moves
120
+ * between versions and a stale absolute path is a worker asking a script that is
121
+ * not there. Running this twice gives the same file, so turning the switch on
122
+ * again adds nothing.
123
+ */
124
+ export function withOurHooks(file, scriptPath, options) {
125
+ const out = asFile(file);
126
+ const ours = ourHookGroups(scriptPath, options);
127
+ for (const event of CODEX_EVENTS) {
128
+ out.hooks[event] = [...withoutOurGroups(out.hooks[event]), ...ours[event]];
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /**
134
+ * The same hooks.json with ours taken out, for when the switch goes off. An
135
+ * event left with nothing in it loses its key entirely, so a file that was only
136
+ * ever ours reads as a file with no hooks rather than as empty lists.
137
+ */
138
+ export function withoutOurHooks(file) {
139
+ const out = asFile(file);
140
+ for (const event of Object.keys(out.hooks)) {
141
+ const kept = withoutOurGroups(out.hooks[event]);
142
+ if (kept.length) out.hooks[event] = kept;
143
+ else delete out.hooks[event];
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /** A TOML key as `config/batchWrite` wants it: one quoted segment, escaped. */
149
+ function tomlKey(key) {
150
+ return JSON.stringify(String(key));
151
+ }
152
+
153
+ /**
154
+ * The `config/batchWrite` edits that trust our hooks and nothing else.
155
+ *
156
+ * Takes either a flat list of hook entries or a whole `hooks/list` answer
157
+ * (one record per cwd), because the same hook is reported once per cwd and must
158
+ * be asked for once. A hook already trusted needs nothing; a `managed` one is an
159
+ * admin's decision and is left alone; a `modified` one is ours whose script
160
+ * changed with the app, and re-trusting it is the whole reason this runs at every
161
+ * start rather than once at install.
162
+ */
163
+ export function ourTrustEdits(answer) {
164
+ const entries = [];
165
+ for (const row of Array.isArray(answer) ? answer : []) {
166
+ if (Array.isArray(row?.hooks)) entries.push(...row.hooks);
167
+ else if (row?.key) entries.push(row);
168
+ }
169
+ const edits = new Map();
170
+ for (const h of entries) {
171
+ if (!h?.key || !h?.currentHash) continue;
172
+ if (!hookIsOurs(h.command)) continue;
173
+ if (h.trustStatus === 'trusted' || h.trustStatus === 'managed') continue;
174
+ edits.set(h.key, { keyPath: `hooks.state.${tomlKey(h.key)}.trusted_hash`, mergeStrategy: 'upsert', value: h.currentHash });
175
+ }
176
+ return [...edits.values()];
177
+ }
178
+
179
+ /**
180
+ * PUT OUR HOOK IN A CODEX HOME, OR TAKE IT OUT, AND TRUST ONLY OUR OWN.
181
+ *
182
+ * Runs at every app start while the switch is on, not once at install, because
183
+ * three things change underneath it: the app's path (a new version moves the
184
+ * script, and Codex then calls our hook "modified" and stops running it), the
185
+ * user's own hooks.json (they may add or remove their own entries at any time),
186
+ * and the home itself (a second Codex login is a second home).
187
+ *
188
+ * NOTHING HERE WRITES config.toml. The trust goes through `config/batchWrite`,
189
+ * which is the call the Codex TUI itself uses, so Codex edits its own file and
190
+ * every other setting and every other hook's trust survives untouched. Measured
191
+ * on this Mac: an unrelated `[projects."..."]` table was still there afterwards,
192
+ * and a second hook in the same hooks.json was still `untrusted`.
193
+ *
194
+ * It is all best-effort. A Codex home we cannot write, an app-server that will
195
+ * not answer, a refused config write: each one means Codex workers run ungated,
196
+ * which is exactly where they were before this existed. It must never be a
197
+ * reason the app fails to start.
198
+ *
199
+ * @param {object} how
200
+ * @param {string} how.home The CODEX_HOME to write in.
201
+ * @param {string} how.scriptPath scripts/memory-gate-hook.sh, unpacked.
202
+ * @param {boolean} how.on Whether the memory check is on.
203
+ * @param {(method: string, params: object) => Promise<any>} [how.request] The
204
+ * app-server call. Omitted means "write the file, do not try to trust it".
205
+ * @param {string[]} [how.cwds] Folders to ask `hooks/list` about. A hook in the
206
+ * home is listed for any cwd, so one is enough; the app's own folder is used.
207
+ * @param {object} [how.fs] Injected for tests.
208
+ * @param {(line: string) => void} [how.log]
209
+ * @returns {Promise<{wrote: boolean, trusted: number, error: string|null}>}
210
+ */
211
+ export async function syncCodexMemoryGate({ home, scriptPath, on, request = null, cwds = [], fs = fsNode, log = () => {} }) {
212
+ const out = { wrote: false, trusted: 0, error: null };
213
+ if (!home || (on && !scriptPath)) { out.error = 'no codex home'; return out; }
214
+ const file = path.join(home, HOOKS_FILE);
215
+
216
+ // THEIR FILE IS READ BEFORE IT IS WRITTEN, AND A FILE WE CANNOT UNDERSTAND IS
217
+ // LEFT ALONE. A hooks.json that exists and is not JSON is somebody's own,
218
+ // broken or newer than us; replacing it to install ours would throw their work
219
+ // away. Codex workers then run ungated, which is where they already were.
220
+ let raw = null;
221
+ try { raw = fs.readFileSync(file, 'utf8'); } catch { raw = null; }
222
+ let before = null;
223
+ if (raw !== null && raw.trim()) {
224
+ try { before = JSON.parse(raw); } catch {
225
+ out.error = `${file} is not readable JSON, so it was left alone`;
226
+ log(`memory gate: ${out.error}`);
227
+ return out;
228
+ }
229
+ }
230
+ // Off, and we were never in it: nothing to do, and no file to create.
231
+ if (!on && before === null) return out;
232
+
233
+ const wanted = `${JSON.stringify(on ? withOurHooks(before, scriptPath) : withoutOurHooks(before), null, 2)}\n`;
234
+ if (raw !== wanted) {
235
+ try {
236
+ fs.mkdirSync(home, { recursive: true });
237
+ const tmp = `${file}.${process.pid}.tmp`;
238
+ fs.writeFileSync(tmp, wanted);
239
+ fs.renameSync(tmp, file);
240
+ out.wrote = true;
241
+ } catch (err) {
242
+ out.error = `could not write ${file}: ${err.message}`;
243
+ log(`memory gate: ${out.error}`);
244
+ return out;
245
+ }
246
+ }
247
+ if (!on || typeof request !== 'function') return out;
248
+
249
+ // Trust ours. `hooks/list` is what reports the key and the hash Codex computed,
250
+ // and neither can be worked out from here: the hash is not of the command text.
251
+ try {
252
+ const listed = await request('hooks/list', { cwds });
253
+ const edits = ourTrustEdits(listed?.data ?? listed);
254
+ if (!edits.length) return out;
255
+ await request('config/batchWrite', { edits, reloadUserConfig: true });
256
+ out.trusted = edits.length;
257
+ log(`memory gate: trusted ${edits.length} Codex hook${edits.length === 1 ? '' : 's'} in ${home}`);
258
+ } catch (err) { out.error = `could not trust the Codex hook: ${err.message}`; log(`memory gate: ${out.error}`); }
259
+ return out;
260
+ }
@@ -27,10 +27,9 @@
27
27
  // models (checked against 0.148.0), and even if it did, a picker must never be
28
28
  // the reason a login shell is spawned while she is typing.
29
29
  //
30
- // AND NOTHING HERE IS CACHED IN MEMORY, for the reason main/codex-bin.mjs
31
- // gives about its own stat calls: caching a cheap read is how a screen ends up
32
- // still saying the old answer after the thing changed underneath it. The CLI
33
- // rewrites this file whenever its etag moves, and a spawn is not a hot path.
30
+ // A successful model/list refresh now keeps an account-scoped live catalog in
31
+ // memory. Startup and hourly refreshes replace it; before the first success,
32
+ // the CLI-maintained disk cache remains the fallback.
34
33
  //
35
34
  // ---------------------------------------------------------------------------
36
35
  // TWO QUESTIONS, NOT ONE, AND THE 2026-08-26 ORIGINAL ONLY ASKED THE FIRST.
@@ -60,6 +59,12 @@
60
59
  import fs from 'node:fs';
61
60
  import os from 'node:os';
62
61
  import path from 'node:path';
62
+ const liveCatalogs=new Map();
63
+ export function rememberCodexModels(home,models){
64
+ const rows=models.filter(m=>typeof m.model==='string'&&m.model).map((m,i)=>({slug:m.model,display_name:m.displayName,visibility:m.hidden?'hide':'list',priority:i,default_reasoning_level:m.defaultReasoningEffort,supported_reasoning_levels:(m.supportedReasoningEfforts||[]).map(l=>({effort:l.reasoningEffort,description:l.description}))}));
65
+ if(rows.length)liveCatalogs.set(home,rows);
66
+ }
67
+ const catalogRows=(home,read)=>liveCatalogs.get(home)??modelRows(read(path.join(home,'models_cache.json')));
63
68
 
64
69
  /**
65
70
  * WHICH CODEX HOME. Her own setting first, then the environment, then the
@@ -93,7 +98,7 @@ export function codexHome({ configured = null, env = process.env, home = os.home
93
98
  * supervisor's refusal is built to need a list before it refuses anything.
94
99
  */
95
100
  export function codexModels({ home = codexHome(), read = readJson } = {}) {
96
- return modelRows(read(path.join(home, 'models_cache.json')))
101
+ return catalogRows(home,read)
97
102
  .filter((m) => m.visibility === 'list')
98
103
  .sort((a, b) => priorityOf(a) - priorityOf(b))
99
104
  .map((m) => ({
@@ -112,7 +117,7 @@ export function codexModels({ home = codexHome(), read = readJson } = {}) {
112
117
  * were handed the word, which is a different question from what she may pick.
113
118
  */
114
119
  export function codexKnownSlugs({ home = codexHome(), read = readJson } = {}) {
115
- return new Set(modelRows(read(path.join(home, 'models_cache.json'))).map((m) => String(m.slug)));
120
+ return new Set(catalogRows(home,read).map((m) => String(m.slug)));
116
121
  }
117
122
 
118
123
  /**
@@ -126,7 +131,7 @@ export function codexKnownSlugs({ home = codexHome(), read = readJson } = {}) {
126
131
  * `codexModels`, asked about the other field.
127
132
  */
128
133
  export function codexModelLevels(slug, { home = codexHome(), read = readJson } = {}) {
129
- const cache = read(path.join(home, 'models_cache.json'));
134
+ const cache = liveCatalogs.has(home)?{models:liveCatalogs.get(home)}:read(path.join(home, 'models_cache.json'));
130
135
  if (!cache) return null;
131
136
  const row = modelRows(cache).find((m) => m.slug === slug);
132
137
  if (!row) return null;