agentbox-app 0.1.6
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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- 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/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.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-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- 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/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +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/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Where Codex actually is, and whether we are entitled to say it is not here.
|
|
2
|
+
//
|
|
3
|
+
// The sibling of main/claude-bin.mjs, and it keeps that file's rule rather than
|
|
4
|
+
// re-arguing it.
|
|
5
|
+
//
|
|
6
|
+
// So `found` decides which binary would be spawned and `certain` decides
|
|
7
|
+
// whether a screen may SAY there is nothing here, and they are never the same
|
|
8
|
+
// value.
|
|
9
|
+
//
|
|
10
|
+
// ONE DIFFERENCE FROM CLAUDE CODE, AND IT CHANGES THE SCREEN. Claude Code is
|
|
11
|
+
// required: the app will not open an inbox without it. Codex is optional, and
|
|
12
|
+
// an absence is the ordinary case rather than a fault. So nothing here ever
|
|
13
|
+
// produces a warning. All a missing Codex does is leave the second engine out
|
|
14
|
+
// of the map that shared/engines.mjs reads, which is silence rather than a
|
|
15
|
+
// sentence about software she never asked for.
|
|
16
|
+
//
|
|
17
|
+
// NOTHING HERE SPAWNS ANYTHING, AND main/config.mjs IS WHAT CALLS IT.
|
|
18
|
+
//
|
|
19
|
+
// It was inert when it was written -- "no config key, no snapshot field, no
|
|
20
|
+
// Settings row" -- and that stopped being true the moment the supervisor
|
|
21
|
+
// started spawning `config.codexBin`. Until this was wired in, that path
|
|
22
|
+
// arrived on `config` through the unknown-key spread in main/config.mjs and
|
|
23
|
+
// went to `spawn` with no finder and no existence check behind it, which is the
|
|
24
|
+
// one path in the second engine that becomes a process and was the only one
|
|
25
|
+
// with no guard on it. `resolveCodexBin` is now what answers "is there a Codex
|
|
26
|
+
// on this Mac", and its answer is the fact `engineFor` refuses on.
|
|
27
|
+
//
|
|
28
|
+
// There is still no Settings row, which is a real gap and not a claim of one:
|
|
29
|
+
// she can set `codexBin` in zero.config.json and nothing on screen says whether
|
|
30
|
+
// it was found.
|
|
31
|
+
//
|
|
32
|
+
// ------------------------------------------------------------------------
|
|
33
|
+
// MEASURED ON THIS MAC 2026-09-04, BECAUSE THE 2026-08-25 ORIGINAL WAS WRITTEN
|
|
34
|
+
// AGAINST codex-cli 0.144.0 AND SIX OF ITS ASSUMPTIONS HAVE MOVED SINCE.
|
|
35
|
+
//
|
|
36
|
+
// Installed here: codex-cli 0.148.0 at /opt/homebrew/bin/codex. That is a
|
|
37
|
+
// Homebrew CASK, not a formula as the original said, and the bin entry is a
|
|
38
|
+
// SYMLINK into /opt/homebrew/Caskroom/codex/0.148.0/bin/codex. `brew info
|
|
39
|
+
// --cask codex` the same minute reports 0.148.0 installed against 0.152.1
|
|
40
|
+
// available, so the moment mid-upgrade where that link points at nothing is a
|
|
41
|
+
// live state on this machine and not a hypothetical. It is the same shape the
|
|
42
|
+
// dangling-symlink rule was written for in the sibling file.
|
|
43
|
+
//
|
|
44
|
+
// THE INSTALL METHOD THE DOCS NOW LEAD WITH DID NOT EXIST IN THE ORIGINAL.
|
|
45
|
+
// `https://chatgpt.com/codex/install.sh`, read 2026-09-04:
|
|
46
|
+
//
|
|
47
|
+
// BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}"
|
|
48
|
+
// CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
|
|
49
|
+
// STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone"
|
|
50
|
+
//
|
|
51
|
+
// It links ~/.local/bin/codex at a versioned file under that root and swaps the
|
|
52
|
+
// file on every update. Three things follow, and all three are changes from the
|
|
53
|
+
// original: that path goes FIRST (the tool's own answer settles a machine set
|
|
54
|
+
// up the documented way in one stat call, exactly as ~/.local/bin/claude does
|
|
55
|
+
// next door), the standalone store's own bin is worth listing for the case
|
|
56
|
+
// where CODEX_INSTALL_DIR put the link somewhere no list covers, and CODEX_HOME
|
|
57
|
+
// can move every trace below out from under us.
|
|
58
|
+
//
|
|
59
|
+
// AND THE STORE KEEPS THE BINARY IN TWO PLACES, WHICH THE FIRST VERSION OF THIS
|
|
60
|
+
// FILE MISSED. Same script, read the same day:
|
|
61
|
+
//
|
|
62
|
+
// line 951: ln -sf "bin/codex" "$stage_release/codex"
|
|
63
|
+
// line 1021: version_from_binary "$release_dir/bin/codex" \
|
|
64
|
+
// || version_from_binary "$release_dir/codex"
|
|
65
|
+
//
|
|
66
|
+
// Every staged release carries the binary at `<release>/bin/codex` AND at
|
|
67
|
+
// `<release>/codex`, and the installer accepts either when it goes looking for
|
|
68
|
+
// a version. `current` is a symlink at the live release, so BOTH
|
|
69
|
+
// `current/bin/codex` and `current/codex` resolve on a machine set up the
|
|
70
|
+
// documented way. Each is one stat call, so listing both costs nothing, and
|
|
71
|
+
// listing only one is how the app tells somebody their Codex is missing while
|
|
72
|
+
// they are looking at it.
|
|
73
|
+
//
|
|
74
|
+
// WHAT ~/.codex REALLY HOLDS NOW: 38 entries. auth.json, config.toml, sessions/
|
|
75
|
+
// and history.jsonl are all still there, so the original's evidence list is not
|
|
76
|
+
// wrong. It is INCOMPLETE, and it is aging in a specific way. The directory
|
|
77
|
+
// also holds state_5.sqlite, thread_history_1.sqlite, logs_2.sqlite,
|
|
78
|
+
// memories_1.sqlite, queue_1.sqlite, goals_1.sqlite, models_cache.json,
|
|
79
|
+
// installation_id and shell_snapshots/, and the split matters: sessions/ holds
|
|
80
|
+
// 1691 rollout files with the newest written 2026-09-03, while history.jsonl
|
|
81
|
+
// has not been touched since 2026-06-03. The live record moved into sqlite
|
|
82
|
+
// files whose names carry a schema number that increments, which is a name no
|
|
83
|
+
// list can hold. That is why the directory itself stays on the list, last and
|
|
84
|
+
// broad, and why nothing below tries to enumerate what is inside it.
|
|
85
|
+
//
|
|
86
|
+
// AND THE LINK MOVED. The original's install URL, developers.openai.com/codex/
|
|
87
|
+
// cli, answers 308 to learn.chatgpt.com/docs/codex/cli.
|
|
88
|
+
//
|
|
89
|
+
// The npm package is unchanged and still real (@openai/codex, 0.153.0 on
|
|
90
|
+
// 2026-09-04), so the package-manager paths and the per-Node directories below
|
|
91
|
+
// are carried over exactly as they were.
|
|
92
|
+
|
|
93
|
+
import fs from 'node:fs';
|
|
94
|
+
import os from 'node:os';
|
|
95
|
+
import path from 'node:path';
|
|
96
|
+
import { askShell } from './claude-bin.mjs';
|
|
97
|
+
|
|
98
|
+
// In order, and the first that exists wins. The shell installer's own path is
|
|
99
|
+
// first for the same reason its sibling's is: it is the answer the tool itself
|
|
100
|
+
// produces. Everything after it is a package manager's default bin directory,
|
|
101
|
+
// because Codex ships as a Homebrew cask and as an npm package rather than with
|
|
102
|
+
// an installer that owns a folder.
|
|
103
|
+
export function candidatePaths(home = os.homedir()) {
|
|
104
|
+
return [
|
|
105
|
+
path.join(home, '.local/bin/codex'), // the shell installer's link
|
|
106
|
+
path.join(home, '.codex/packages/standalone/current/bin/codex'),
|
|
107
|
+
path.join(home, '.codex/packages/standalone/current/codex'),
|
|
108
|
+
'/opt/homebrew/bin/codex', // the Homebrew cask, Apple Silicon
|
|
109
|
+
'/usr/local/bin/codex', // Homebrew on Intel, and npm -g
|
|
110
|
+
path.join(home, '.bun/bin/codex'),
|
|
111
|
+
path.join(home, '.volta/bin/codex'),
|
|
112
|
+
path.join(home, '.asdf/shims/codex'),
|
|
113
|
+
path.join(home, '.npm-global/bin/codex'), // the two npm prefixes people
|
|
114
|
+
path.join(home, '.npm-packages/bin/codex'), // actually write in a guide
|
|
115
|
+
path.join(home, '.yarn/bin/codex'),
|
|
116
|
+
path.join(home, 'Library/pnpm/codex'), // pnpm's own bin on macOS
|
|
117
|
+
path.join(home, '.deno/bin/codex'),
|
|
118
|
+
path.join(home, 'bin/codex'), // a hand-rolled ~/bin on PATH
|
|
119
|
+
'/opt/local/bin/codex', // MacPorts
|
|
120
|
+
'/usr/bin/codex',
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// One bin directory per installed Node, under a name no list can hold. A global
|
|
125
|
+
// npm install under nvm, fnm, n or asdf lands there and nowhere else, and a
|
|
126
|
+
// double-clicked Electron app inherits launchd's PATH, which has never heard of
|
|
127
|
+
// any of them.
|
|
128
|
+
export function versionedPaths(home = os.homedir(), readdir = defaultReaddir) {
|
|
129
|
+
const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
|
|
130
|
+
return [
|
|
131
|
+
...under('.nvm/versions/node', 'bin/codex'),
|
|
132
|
+
...under('.local/share/fnm/node-versions', 'installation/bin/codex'),
|
|
133
|
+
...under('Library/Application Support/fnm/node-versions', 'installation/bin/codex'),
|
|
134
|
+
...under('n/versions/node', 'bin/codex'),
|
|
135
|
+
...under('.asdf/installs/nodejs', 'bin/codex'),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function defaultReaddir(dir) {
|
|
140
|
+
try { return fs.readdirSync(dir); } catch { return []; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// WHAT CODEX LEAVES BEHIND. None of these is a binary and none is ever spawned.
|
|
144
|
+
// They exist only to stop the app claiming an absence it has not established.
|
|
145
|
+
//
|
|
146
|
+
// Both homes are looked in when CODEX_HOME is set, and that is on purpose: a
|
|
147
|
+
// CODEX_HOME inherited from somewhere else is not proof the ordinary directory
|
|
148
|
+
// is gone, and the whole job of this list is to make an absence hard to claim
|
|
149
|
+
// rather than easy.
|
|
150
|
+
export function evidencePaths(home = os.homedir(), env = process.env) {
|
|
151
|
+
const homes = [env?.CODEX_HOME, path.join(home, '.codex')].filter(Boolean);
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const dir of new Set(homes)) {
|
|
154
|
+
out.push(
|
|
155
|
+
path.join(dir, 'auth.json'), // it has been logged in
|
|
156
|
+
path.join(dir, 'config.toml'),
|
|
157
|
+
path.join(dir, 'sessions'), // one folder tree of past runs
|
|
158
|
+
path.join(dir, 'history.jsonl'),
|
|
159
|
+
path.join(dir, 'packages/standalone/releases'), // the installer's version store
|
|
160
|
+
dir, // and the directory itself, last
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A path that is a symlink to nothing. `existsSync` follows the link and says
|
|
167
|
+
// false, which is indistinguishable from nothing being there at all, and the
|
|
168
|
+
// two mean opposite things. Both of Codex's normal installs are a symlink into
|
|
169
|
+
// a versioned directory that gets swapped on every update, so this is the
|
|
170
|
+
// ordinary shape of a Codex that is being upgraded rather than an odd one.
|
|
171
|
+
function defaultDangles(p) {
|
|
172
|
+
try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function installEvidence({
|
|
176
|
+
home = os.homedir(), env = process.env, exists = fs.existsSync, dangles = defaultDangles,
|
|
177
|
+
} = {}) {
|
|
178
|
+
for (const p of evidencePaths(home, env)) if (exists(p)) return p;
|
|
179
|
+
for (const p of candidatePaths(home)) if (dangles(p)) return p;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const INSTALL_URL = 'https://learn.chatgpt.com/docs/codex/cli';
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The whole search, a pure function of its inputs so a test can put Codex
|
|
187
|
+
* anywhere without touching the machine it runs on.
|
|
188
|
+
*
|
|
189
|
+
* Returns the same shape its sibling does: { path, found, certain, from,
|
|
190
|
+
* searched, evidence }, so a caller can hold the two side by side without
|
|
191
|
+
* learning two vocabularies.
|
|
192
|
+
*/
|
|
193
|
+
export function findCodexBin({
|
|
194
|
+
configured = null,
|
|
195
|
+
home = os.homedir(),
|
|
196
|
+
env = process.env,
|
|
197
|
+
exists = fs.existsSync,
|
|
198
|
+
shellLookup = () => askShell({ bin: 'codex' }),
|
|
199
|
+
readdir = defaultReaddir,
|
|
200
|
+
dangles = defaultDangles,
|
|
201
|
+
} = {}) {
|
|
202
|
+
if (configured) {
|
|
203
|
+
return {
|
|
204
|
+
path: configured, found: exists(configured), certain: true, from: 'settings',
|
|
205
|
+
searched: [], evidence: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
|
|
210
|
+
for (const candidate of searched) {
|
|
211
|
+
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fromShell = shellSaid(shellLookup());
|
|
215
|
+
if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
|
|
216
|
+
|
|
217
|
+
// NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
|
|
218
|
+
// shell we could reach missed, so the last question is whether this Mac shows
|
|
219
|
+
// any sign of Codex anyway.
|
|
220
|
+
const evidence = installEvidence({ home, env, exists, dangles });
|
|
221
|
+
return {
|
|
222
|
+
path: null,
|
|
223
|
+
found: false,
|
|
224
|
+
certain: fromShell.answered && !evidence,
|
|
225
|
+
from: null,
|
|
226
|
+
searched,
|
|
227
|
+
evidence,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// askShell used to answer with a path or null. Anything handing back the old
|
|
232
|
+
// shape is read as an answer, because that is what it always meant.
|
|
233
|
+
function shellSaid(result) {
|
|
234
|
+
if (typeof result === 'string') return { path: result, answered: true };
|
|
235
|
+
if (!result || typeof result !== 'object') return { path: null, answered: true };
|
|
236
|
+
return { path: result.path ?? null, answered: result.answered !== false };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ------------------------------- the cache -------------------------------- */
|
|
240
|
+
// Only the SHELL step is remembered, for the same thirty seconds and the same
|
|
241
|
+
// reason: the path checks are stat calls, and caching those is how a settings
|
|
242
|
+
// screen ends up still saying "not found" after somebody has installed it.
|
|
243
|
+
|
|
244
|
+
const SHELL_TTL = 30_000;
|
|
245
|
+
let shellAnswer = null;
|
|
246
|
+
let shellAskedAt = 0;
|
|
247
|
+
|
|
248
|
+
export function resolveCodexBin(configured = null, { now = Date.now(), shellLookup = () => askShell({ bin: 'codex' }), ...where } = {}) {
|
|
249
|
+
const remembered = () => {
|
|
250
|
+
if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
|
|
251
|
+
shellAnswer = shellLookup();
|
|
252
|
+
shellAskedAt = now;
|
|
253
|
+
return shellAnswer;
|
|
254
|
+
};
|
|
255
|
+
return findCodexBin({ ...where, configured, shellLookup: remembered });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function forgetCodexBin() {
|
|
259
|
+
shellAnswer = null;
|
|
260
|
+
shellAskedAt = 0;
|
|
261
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// One operation on an existing thread. No prompt, worker queue or automatic retry.
|
|
2
|
+
import { resumeThreadParams } from './codex-session.mjs';
|
|
3
|
+
|
|
4
|
+
export async function compactCodexThread({ server, threadId, threadParams = {}, onState = () => {}, timeoutMs = 120000 }) {
|
|
5
|
+
if (!threadId) return 'missing';
|
|
6
|
+
let watched = false, sent = false, turnId = null, timer, finish;
|
|
7
|
+
const completion = new Promise(resolve => { finish = resolve; });
|
|
8
|
+
const handlers = {
|
|
9
|
+
onNotification(method, params) {
|
|
10
|
+
if (!sent || params?.threadId !== threadId) return;
|
|
11
|
+
const id = params?.turn?.id ?? params?.turnId;
|
|
12
|
+
if (method === 'turn/started' && !turnId && typeof id === 'string') turnId = id;
|
|
13
|
+
if (method === 'turn/completed' && turnId && id === turnId) {
|
|
14
|
+
finish(params.turn?.status === 'completed' ? 'done' : 'failed');
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
onClosed() { finish('failed'); },
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
const read = await server.request('thread/read', { threadId, includeTurns: true });
|
|
21
|
+
if (read?.thread?.status?.type === 'active' || read?.thread?.turns?.some(turn => turn.status === 'inProgress')) return 'busy';
|
|
22
|
+
// Resuming a dormant thread with an active goal can start autonomous work.
|
|
23
|
+
// Compaction must not wake a goal outside the tracked worker lifecycle.
|
|
24
|
+
try {
|
|
25
|
+
const { goal } = await server.request('thread/goal/get', { threadId });
|
|
26
|
+
if (goal?.status === 'active') return 'busy';
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code !== -32601 && !/method not found|unknown (method|variant)/i.test(error?.message ?? '')) throw error;
|
|
29
|
+
}
|
|
30
|
+
watched = true;
|
|
31
|
+
await server.resumeThread(resumeThreadParams(threadId, threadParams), handlers);
|
|
32
|
+
onState('running');
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
if (turnId) Promise.resolve(server.interruptTurn(threadId, turnId)).catch(() => {});
|
|
35
|
+
finish('failed');
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
sent = true;
|
|
38
|
+
// An empty response acknowledges the request; only turn/completed settles it.
|
|
39
|
+
await server.request('thread/compact/start', { threadId });
|
|
40
|
+
const result = await completion;
|
|
41
|
+
onState(result);
|
|
42
|
+
return result;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return error?.code === -32601 || /method not found|unknown (method|variant)/i.test(error?.message ?? '') ? 'unavailable' : 'failed';
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
if (watched) server.unwatch(threadId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// WHICH MODELS CODEX CAN BE ASKED FOR, READ OFF THIS MAC RATHER THAN LISTED.
|
|
2
|
+
//
|
|
3
|
+
// She is right and the card was wrong. `renderer/src/models.ts` holds four
|
|
4
|
+
// names -- Opus, Sonnet, Haiku, Fable -- and drew them whatever the engine was,
|
|
5
|
+
// so a task set to run on Codex read "On Opus. With Codex.", which is two
|
|
6
|
+
// sentences that cannot both be true.
|
|
7
|
+
//
|
|
8
|
+
// AND THE LIST IS NOT OURS TO KEEP. Claude Code's four are safe to generate
|
|
9
|
+
// once into a committed file because `--model` there takes an ALIAS (`opus` is
|
|
10
|
+
// whatever Opus is on the day the task runs, resolved by the CLI at spawn), so
|
|
11
|
+
// `shared/claude-models.generated.mjs` cannot silently point at an old model.
|
|
12
|
+
// Codex has no aliases: `-m` and `thread/start`'s `model` want the exact slug
|
|
13
|
+
// of one specific model, and OpenAI ships new ones on their own schedule. A
|
|
14
|
+
// hardcoded Codex list is a list that is wrong the week after it is written,
|
|
15
|
+
// and a generated one is a list that is wrong the week after the build.
|
|
16
|
+
//
|
|
17
|
+
// SO IT IS READ AT THE MOMENT IT IS ASKED FOR, AND THE CLI HAS ALREADY FETCHED
|
|
18
|
+
// IT. codex-cli keeps `<home>/models_cache.json`, refreshed against the server
|
|
19
|
+
// with an etag, holding every model the logged-in account may use. Measured on
|
|
20
|
+
// this Mac 2026-09-04 against codex-cli 0.148.0: 198,977 bytes, 8 models,
|
|
21
|
+
// `fetched_at` 2026-09-04T10:21:25.048219Z, `client_version` "0.148.0". Six
|
|
22
|
+
// carry `visibility: "list"` and two carry `"hide"`. Reading that file IS the
|
|
23
|
+
// fetch: one readFileSync of a file the CLI maintains, no network, and it
|
|
24
|
+
// cannot disagree with what Codex will actually accept.
|
|
25
|
+
//
|
|
26
|
+
// NOTHING HERE SPAWNS ANYTHING. `codex` has no subcommand that prints its
|
|
27
|
+
// models (checked against 0.148.0), and even if it did, a picker must never be
|
|
28
|
+
// the reason a login shell is spawned while she is typing.
|
|
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.
|
|
34
|
+
//
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// TWO QUESTIONS, NOT ONE, AND THE 2026-08-26 ORIGINAL ONLY ASKED THE FIRST.
|
|
37
|
+
//
|
|
38
|
+
// WHAT MAY SHE BE OFFERED -> `codexModels`, the six with `visibility: "list"`.
|
|
39
|
+
// `gpt-reserve` and `codex-auto-review` are Codex's own internal models and
|
|
40
|
+
// a person who chose one would get a session that behaves nothing like a
|
|
41
|
+
// coding agent.
|
|
42
|
+
//
|
|
43
|
+
// WHAT WOULD CODEX ACCEPT -> `codexKnownSlugs`, all eight. This one is new,
|
|
44
|
+
// and it exists because main/supervisor.mjs now REFUSES to start a run
|
|
45
|
+
// whose model this Mac's Codex does not know. Answering that question with
|
|
46
|
+
// the offer list would refuse `gpt-reserve` -- a slug Codex takes -- and a
|
|
47
|
+
// false refusal is a row that cannot run for a reason that is not true.
|
|
48
|
+
//
|
|
49
|
+
// `priority` is ascending and is the order the CLI's own picker uses, so the
|
|
50
|
+
// newest frontier model is first and the small old ones are last. Sorted by it
|
|
51
|
+
// rather than by name, so the list never reorders itself when a model ships.
|
|
52
|
+
//
|
|
53
|
+
// REASONING LEVELS ARE PER MODEL AND THEY REALLY DIFFER. Measured in the same
|
|
54
|
+
// file the same day: `gpt-5.6-sol` supports low/medium/high/xhigh/max/ultra and
|
|
55
|
+
// `gpt-5.5` stops at xhigh; the defaults differ too (`low` against `medium`).
|
|
56
|
+
// One shared list beside a picker would offer her a level half these models
|
|
57
|
+
// cannot run.-- but they are carried here so the slice that does draw them does
|
|
58
|
+
// not have to come back and re-read a 199KB file.
|
|
59
|
+
|
|
60
|
+
import fs from 'node:fs';
|
|
61
|
+
import os from 'node:os';
|
|
62
|
+
import path from 'node:path';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* WHICH CODEX HOME. Her own setting first, then the environment, then the
|
|
66
|
+
* default -- the same precedence the install script itself uses, read
|
|
67
|
+
* 2026-09-04: `CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"`. An empty
|
|
68
|
+
* `CODEX_HOME` falls through exactly as `${VAR:-default}` does, so an
|
|
69
|
+
* exported-but-blank one cannot turn this into a read at the root of the disk.
|
|
70
|
+
*
|
|
71
|
+
* main/codex-bin.mjs looks under BOTH `CODEX_HOME` and `~/.codex` for signs of
|
|
72
|
+
* an install, and that is not this rule rather than a contradiction of it: its
|
|
73
|
+
* job is to make an ABSENCE hard to claim, so two homes is the right answer
|
|
74
|
+
* there. This one has to name the single home a specific Codex reads, because
|
|
75
|
+
* offering models out of the wrong one would offer models a different login
|
|
76
|
+
* cannot run.
|
|
77
|
+
*/
|
|
78
|
+
export function codexHome({ configured = null, env = process.env, home = os.homedir() } = {}) {
|
|
79
|
+
return configured || env?.CODEX_HOME || path.join(home, '.codex');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The models this Mac's Codex may be OFFERED, in the order it ranks them.
|
|
84
|
+
*
|
|
85
|
+
* `{ id, label, description, defaultLevel, levels }`, where `levels` is the
|
|
86
|
+
* model's own `[{ id, description }]`. Every word in it is Codex's; nothing is
|
|
87
|
+
* typed here, for the reason `shared/claude-models.generated.mjs` gives about
|
|
88
|
+
* its own labels -- a name typed into a file of ours looks authoritative while
|
|
89
|
+
* being a guess.
|
|
90
|
+
*
|
|
91
|
+
* An unreadable, absent or half-written cache answers `[]` rather than a guess.
|
|
92
|
+
* `[]` is not "there are no models"; it is "this Mac cannot say", and the
|
|
93
|
+
* supervisor's refusal is built to need a list before it refuses anything.
|
|
94
|
+
*/
|
|
95
|
+
export function codexModels({ home = codexHome(), read = readJson } = {}) {
|
|
96
|
+
return modelRows(read(path.join(home, 'models_cache.json')))
|
|
97
|
+
.filter((m) => m.visibility === 'list')
|
|
98
|
+
.sort((a, b) => priorityOf(a) - priorityOf(b))
|
|
99
|
+
.map((m) => ({
|
|
100
|
+
id: String(m.slug),
|
|
101
|
+
label: String(m.display_name || m.slug),
|
|
102
|
+
description: typeof m.description === 'string' ? m.description : '',
|
|
103
|
+
defaultLevel: typeof m.default_reasoning_level === 'string' ? m.default_reasoning_level : null,
|
|
104
|
+
levels: (Array.isArray(m.supported_reasoning_levels) ? m.supported_reasoning_levels : [])
|
|
105
|
+
.filter((l) => l && typeof l.effort === 'string' && l.effort)
|
|
106
|
+
.map((l) => ({ id: l.effort, description: typeof l.description === 'string' ? l.description : '' })),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Every slug in the cache, hidden ones included: what Codex would accept if it
|
|
112
|
+
* were handed the word, which is a different question from what she may pick.
|
|
113
|
+
*/
|
|
114
|
+
export function codexKnownSlugs({ home = codexHome(), read = readJson } = {}) {
|
|
115
|
+
return new Set(modelRows(read(path.join(home, 'models_cache.json'))).map((m) => String(m.slug)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The reasoning levels ONE model advertises, hidden models included, as the
|
|
120
|
+
* bare words `turn/start`'s `effort` takes.
|
|
121
|
+
*
|
|
122
|
+
* Null when this Mac cannot say -- no readable cache, or a slug it does not
|
|
123
|
+
* list -- which the caller has to tell apart from a model that lists none:
|
|
124
|
+
* the first is "carry the word and let Codex answer", the second is a level
|
|
125
|
+
* nothing can run. Same two questions as `codexKnownSlugs` against
|
|
126
|
+
* `codexModels`, asked about the other field.
|
|
127
|
+
*/
|
|
128
|
+
export function codexModelLevels(slug, { home = codexHome(), read = readJson } = {}) {
|
|
129
|
+
const cache = read(path.join(home, 'models_cache.json'));
|
|
130
|
+
if (!cache) return null;
|
|
131
|
+
const row = modelRows(cache).find((m) => m.slug === slug);
|
|
132
|
+
if (!row) return null;
|
|
133
|
+
return (Array.isArray(row.supported_reasoning_levels) ? row.supported_reasoning_levels : [])
|
|
134
|
+
.filter((l) => l && typeof l.effort === 'string' && l.effort)
|
|
135
|
+
.map((l) => l.effort);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* WHAT CODEX RUNS ON WHEN NOBODY HAS CHOSEN, WHICH IS HER OWN SETTING.
|
|
140
|
+
*
|
|
141
|
+
* Read out of the bare `model = "..."` line at the top of the home's
|
|
142
|
+
* config.toml, because that is the model her `codex` in a terminal already
|
|
143
|
+
* uses, and a fleet quietly running a different one would be a fleet she cannot
|
|
144
|
+
* reason about. Hers on 2026-09-04 is `gpt-5.6-sol`.
|
|
145
|
+
*
|
|
146
|
+
* A DELIBERATELY SMALL PARSER, and the smallness is the safety. The only thing
|
|
147
|
+
* it is ever allowed to take out of that file is one top-level string, so it
|
|
148
|
+
* scans line by line and STOPS AT THE FIRST TABLE HEADER. That is not a
|
|
149
|
+
* shortcut, it is TOML: once a `[table]` opens, every key below it belongs to
|
|
150
|
+
* that table, so a bare top-level key can only appear above the first one. Her
|
|
151
|
+
* real config is 234 lines with 71 `[projects."..."]` tables in it, and a regex
|
|
152
|
+
* over the whole file reports whichever project it hit first as her default.
|
|
153
|
+
*
|
|
154
|
+
* `model_reasoning_effort` sits on the line directly below `model` in that same
|
|
155
|
+
* file and shares its first eight characters, which is why the key is anchored
|
|
156
|
+
* on both sides rather than merely at the start.
|
|
157
|
+
*
|
|
158
|
+
* NULL IS A REAL ANSWER: pass no model at all and let Codex decide, which is
|
|
159
|
+
* what happens today. The 2026-08-26 version fell back to the top-priority
|
|
160
|
+
* model in the list instead, and that is our sort order's first row rather than
|
|
161
|
+
* Codex's default. The two are equal on this Mac by luck, and a screen printing
|
|
162
|
+
* one as the other would be wrong with nothing here able to notice.
|
|
163
|
+
*/
|
|
164
|
+
export function codexDefaultModel({ home = codexHome(), read = readText } = {}) {
|
|
165
|
+
const text = read(path.join(home, 'config.toml'));
|
|
166
|
+
if (!text) return null;
|
|
167
|
+
for (const raw of String(text).split('\n')) {
|
|
168
|
+
const line = raw.trim();
|
|
169
|
+
if (line.startsWith('[')) return null; // a table opens; the top level is over
|
|
170
|
+
const hit = /^model\s*=\s*["']([^"']+)["']/.exec(line);
|
|
171
|
+
if (hit) return hit[1];
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The `models` array, or nothing, however badly the file is malformed. */
|
|
177
|
+
function modelRows(cache) {
|
|
178
|
+
const rows = Array.isArray(cache?.models) ? cache.models : [];
|
|
179
|
+
return rows.filter((m) => m && typeof m === 'object' && typeof m.slug === 'string' && m.slug);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A model with no priority sorts last rather than first. A new field arriving
|
|
183
|
+
// with a null in it must not silently jump to the top of her picker.
|
|
184
|
+
function priorityOf(m) {
|
|
185
|
+
return Number.isFinite(m.priority) ? m.priority : Number.MAX_SAFE_INTEGER;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readJson(file) {
|
|
189
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readText(file) {
|
|
193
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
194
|
+
}
|