agentbox-app 0.1.7 → 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.
- package/README.md +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +9 -10
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +39 -18
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +255 -27
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
package/main/claude-bin.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
49
|
-
// wins untouched and is never searched around, because that
|
|
50
|
-
//
|
|
51
|
-
// a setting
|
|
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
|
package/main/claude-input.mjs
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 (
|
|
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.
|
package/main/claude-plan.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
16
|
-
//
|
|
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
|
|
21
|
-
// ~/.claude-second/.claude.json the same two
|
|
22
|
-
// profileFetchedAt
|
|
20
|
+
// profileFetchedAt (within the day)
|
|
21
|
+
// ~/.claude-second/.claude.json the same two fields
|
|
22
|
+
// profileFetchedAt (within the day)
|
|
23
23
|
//
|
|
24
|
-
//
|
|
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
|
|
34
|
-
//
|
|
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
|
|
40
|
-
//
|
|
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.
|
package/main/code-change.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
44
|
+
// work they have to account for.
|
|
45
45
|
const SCRATCH = /^\/(private\/)?(tmp|var\/folders)\//;
|
|
46
46
|
|
|
47
47
|
/**
|
package/main/codex-account.mjs
CHANGED
|
@@ -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:
|
|
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. */
|
package/main/codex-bin.mjs
CHANGED
|
@@ -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
|
+
}
|
package/main/codex-models.mjs
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
|
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(
|
|
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;
|