litcodex-ai 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/bin/litcodex.js +12 -0
  4. package/dist/cli.d.ts +23 -0
  5. package/dist/cli.js +183 -0
  6. package/dist/config-migration/backup.d.ts +2 -0
  7. package/dist/config-migration/backup.js +42 -0
  8. package/dist/config-migration/catalog.d.ts +22 -0
  9. package/dist/config-migration/catalog.js +99 -0
  10. package/dist/config-migration/cli.d.ts +14 -0
  11. package/dist/config-migration/cli.js +85 -0
  12. package/dist/config-migration/config-paths.d.ts +4 -0
  13. package/dist/config-migration/config-paths.js +64 -0
  14. package/dist/config-migration/errors.d.ts +11 -0
  15. package/dist/config-migration/errors.js +28 -0
  16. package/dist/config-migration/index.d.ts +44 -0
  17. package/dist/config-migration/index.js +210 -0
  18. package/dist/config-migration/multi-agent-v2-guard.d.ts +2 -0
  19. package/dist/config-migration/multi-agent-v2-guard.js +106 -0
  20. package/dist/config-migration/root-settings.d.ts +6 -0
  21. package/dist/config-migration/root-settings.js +104 -0
  22. package/dist/config-migration/state.d.ts +16 -0
  23. package/dist/config-migration/state.js +40 -0
  24. package/dist/config-migration/toml-shape.d.ts +8 -0
  25. package/dist/config-migration/toml-shape.js +107 -0
  26. package/dist/install/codex.d.ts +34 -0
  27. package/dist/install/codex.js +94 -0
  28. package/dist/install/doctor.d.ts +12 -0
  29. package/dist/install/doctor.js +83 -0
  30. package/dist/install/errors.d.ts +19 -0
  31. package/dist/install/errors.js +43 -0
  32. package/dist/install/execute.d.ts +39 -0
  33. package/dist/install/execute.js +193 -0
  34. package/dist/install/index.d.ts +19 -0
  35. package/dist/install/index.js +193 -0
  36. package/dist/install/marketplace.d.ts +5 -0
  37. package/dist/install/marketplace.js +10 -0
  38. package/dist/install/plan.d.ts +3 -0
  39. package/dist/install/plan.js +54 -0
  40. package/dist/install/render-plan.d.ts +3 -0
  41. package/dist/install/render-plan.js +10 -0
  42. package/dist/install/types.d.ts +45 -0
  43. package/dist/install/types.js +5 -0
  44. package/model-catalog.json +31 -0
  45. package/node_modules/@litcodex/lit-loop/CHANGELOG.md +19 -0
  46. package/node_modules/@litcodex/lit-loop/LICENSE +21 -0
  47. package/node_modules/@litcodex/lit-loop/NOTICE +8 -0
  48. package/node_modules/@litcodex/lit-loop/README.md +37 -0
  49. package/node_modules/@litcodex/lit-loop/agents/litcodex-explorer.toml +75 -0
  50. package/node_modules/@litcodex/lit-loop/agents/litcodex-librarian.toml +98 -0
  51. package/node_modules/@litcodex/lit-loop/agents/litcodex-litwork-reviewer.toml +21 -0
  52. package/node_modules/@litcodex/lit-loop/agents/litcodex-metis.toml +64 -0
  53. package/node_modules/@litcodex/lit-loop/agents/litcodex-momus.toml +68 -0
  54. package/node_modules/@litcodex/lit-loop/agents/litcodex-plan.toml +163 -0
  55. package/node_modules/@litcodex/lit-loop/directive.md +85 -0
  56. package/node_modules/@litcodex/lit-loop/directives/lit-plan.md +286 -0
  57. package/node_modules/@litcodex/lit-loop/directives/litgoal.md +103 -0
  58. package/node_modules/@litcodex/lit-loop/directives/litwork.md +363 -0
  59. package/node_modules/@litcodex/lit-loop/dist/_scaffold.d.ts +1 -0
  60. package/node_modules/@litcodex/lit-loop/dist/_scaffold.js +3 -0
  61. package/node_modules/@litcodex/lit-loop/dist/cli.d.ts +6 -0
  62. package/node_modules/@litcodex/lit-loop/dist/cli.js +44 -0
  63. package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.d.ts +18 -0
  64. package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.js +94 -0
  65. package/node_modules/@litcodex/lit-loop/dist/codex-hook.d.ts +38 -0
  66. package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +126 -0
  67. package/node_modules/@litcodex/lit-loop/dist/directive.d.ts +35 -0
  68. package/node_modules/@litcodex/lit-loop/dist/directive.js +80 -0
  69. package/node_modules/@litcodex/lit-loop/dist/goal-status.d.ts +12 -0
  70. package/node_modules/@litcodex/lit-loop/dist/goal-status.js +25 -0
  71. package/node_modules/@litcodex/lit-loop/dist/guards.d.ts +73 -0
  72. package/node_modules/@litcodex/lit-loop/dist/guards.js +215 -0
  73. package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +17 -0
  74. package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +94 -0
  75. package/node_modules/@litcodex/lit-loop/dist/loop-cli.d.ts +19 -0
  76. package/node_modules/@litcodex/lit-loop/dist/loop-cli.js +106 -0
  77. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-render.d.ts +7 -0
  78. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-render.js +39 -0
  79. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-types.d.ts +52 -0
  80. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-types.js +7 -0
  81. package/node_modules/@litcodex/lit-loop/dist/loop-doctor.d.ts +21 -0
  82. package/node_modules/@litcodex/lit-loop/dist/loop-doctor.js +283 -0
  83. package/node_modules/@litcodex/lit-loop/dist/loop-errors.d.ts +15 -0
  84. package/node_modules/@litcodex/lit-loop/dist/loop-errors.js +43 -0
  85. package/node_modules/@litcodex/lit-loop/dist/loop-handlers.d.ts +18 -0
  86. package/node_modules/@litcodex/lit-loop/dist/loop-handlers.js +311 -0
  87. package/node_modules/@litcodex/lit-loop/dist/loop-model.d.ts +51 -0
  88. package/node_modules/@litcodex/lit-loop/dist/loop-model.js +165 -0
  89. package/node_modules/@litcodex/lit-loop/dist/loop-stdout.d.ts +6 -0
  90. package/node_modules/@litcodex/lit-loop/dist/loop-stdout.js +11 -0
  91. package/node_modules/@litcodex/lit-loop/dist/loop-types.d.ts +26 -0
  92. package/node_modules/@litcodex/lit-loop/dist/loop-types.js +8 -0
  93. package/node_modules/@litcodex/lit-loop/dist/markers.d.ts +9 -0
  94. package/node_modules/@litcodex/lit-loop/dist/markers.js +14 -0
  95. package/node_modules/@litcodex/lit-loop/dist/modes.d.ts +15 -0
  96. package/node_modules/@litcodex/lit-loop/dist/modes.js +56 -0
  97. package/node_modules/@litcodex/lit-loop/dist/state-paths.d.ts +41 -0
  98. package/node_modules/@litcodex/lit-loop/dist/state-paths.js +111 -0
  99. package/node_modules/@litcodex/lit-loop/dist/state-store.d.ts +39 -0
  100. package/node_modules/@litcodex/lit-loop/dist/state-store.js +419 -0
  101. package/node_modules/@litcodex/lit-loop/dist/state-types.d.ts +90 -0
  102. package/node_modules/@litcodex/lit-loop/dist/state-types.js +61 -0
  103. package/node_modules/@litcodex/lit-loop/dist/trigger.d.ts +54 -0
  104. package/node_modules/@litcodex/lit-loop/dist/trigger.js +75 -0
  105. package/node_modules/@litcodex/lit-loop/package.json +27 -0
  106. package/package.json +30 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LitCodex Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/wjgoarxiv/litcodex/master/cover.png" alt="LitCodex — loop-native agent harness for Codex" width="100%" />
3
+ </div>
4
+
5
+ <h1 align="center">litcodex-ai</h1>
6
+
7
+ <p align="center">
8
+ <em>The <strong>LitCodex</strong> installer + CLI for <a href="https://github.com/openai/codex">Codex</a> — type <code>lit</code> and Codex loops until the goal is verified.</em>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <img src="https://img.shields.io/npm/v/litcodex-ai?color=ff6a00&label=litcodex-ai" alt="npm version" />
13
+ <img src="https://img.shields.io/badge/platform-Codex%20CLI-ff6a00" alt="Codex CLI" />
14
+ <img src="https://img.shields.io/badge/node-%E2%89%A520-3c873a" alt="node >= 20" />
15
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license" />
16
+ </p>
17
+
18
+ ---
19
+
20
+ > [!NOTE]
21
+ > `litcodex` is a **self-contained** CLI: it registers the `litcodex` Codex plugin and the bare-`lit`
22
+ > `UserPromptSubmit` hook with no `npx` forwarding to any other package. One install, no accounts.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ npm install -g litcodex-ai
28
+ litcodex install
29
+ ```
30
+
31
+ `litcodex install` registers the marketplace + plugin and installs the hook into Codex, preserving
32
+ your existing `~/.codex/config.toml` — it backs up before any change and never overwrites unrelated
33
+ keys. Preview the plan first with `litcodex --dry-run install`.
34
+
35
+ ## Commands
36
+
37
+ | Command | Purpose |
38
+ | --- | --- |
39
+ | `litcodex install [--dry-run]` | Register the LitCodex Codex plugin + hook (dry-run prints the plan, mutates nothing) |
40
+ | `litcodex doctor` | Diagnose the installation |
41
+ | `litcodex uninstall` | Remove the LitCodex registration |
42
+ | `litcodex config migrate` | Apply the managed Codex config keys (backup-before-write) |
43
+ | `litcodex loop <create\|status\|run\|checkpoint\|record-evidence\|doctor>` | Drive the lit-loop runtime |
44
+ | `litcodex hook user-prompt-submit` | The Codex hook entrypoint (invoked by the host) |
45
+
46
+ ## How it works
47
+
48
+ Once installed, typing a bare **`lit`** in Codex activates **lit-loop** — an evidence-bound,
49
+ autonomous work loop that decomposes the task, records real evidence per step, and keeps going until
50
+ every success criterion passes. Durable loop state lives under `.litcodex/lit-loop/` in your project
51
+ (`brief.md`, `goals.json`, `ledger.jsonl`, `evidence/`).
52
+
53
+ LitCodex also ships a sibling family of triggers — `litwork`, `lit-plan`, `litgoal` — plus a 20-skill
54
+ library and hook components, all registered as a single Codex plugin.
55
+
56
+ > [!TIP]
57
+ > See the [repository README](https://github.com/wjgoarxiv/litcodex#readme) for the full quickstart,
58
+ > the mode family, the loop model, and troubleshooting.
59
+
60
+ ## License
61
+
62
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ // Authored ESM entry for the `litcodex` bin (A3 D1: self-contained CLI).
3
+ //
4
+ // This file owns the shebang and process.exit. It imports the compiled,
5
+ // self-contained dispatcher from ../dist/cli.js and runs it. It performs NO
6
+ // forwarding and spawns NO child process — all routing lives in the dispatcher.
7
+
8
+ import { runCli } from "../dist/cli.js";
9
+
10
+ const exitCode = await runCli(process.argv.slice(2));
11
+
12
+ process.exit(exitCode);
package/dist/cli.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** Error code emitted when a subcommand is not in the routing table. */
2
+ export declare const UNKNOWN_COMMAND_CODE: "LITCODEX_INSTALL_UNKNOWN_COMMAND";
3
+ /** Router usage code (EX_USAGE) for an unknown `config` sub-subcommand. */
4
+ export declare const CONFIG_USAGE_EXIT_CODE: 64;
5
+ /** Outcome of dispatching an argv vector. Pure: no process side effects. */
6
+ export interface DispatchResult {
7
+ readonly stdout: string;
8
+ readonly stderr: string;
9
+ readonly exitCode: number;
10
+ }
11
+ /**
12
+ * Pure dispatcher. Parses argv (already sliced past `node bin`), strips the position-independent
13
+ * `--dry-run` flag, answers `--help`/`--version` locally, and routes the first non-flag token.
14
+ * Unknown token → exit 1. No process spawns. No forwarding.
15
+ */
16
+ export declare function dispatch(argv: readonly string[]): DispatchResult;
17
+ /**
18
+ * Async process entry used by `bin/litcodex.js`. It owns the side-effecting routes: install/doctor/
19
+ * uninstall (install/* modules, which are the ONLY code that ever spawns `codex`), `config migrate`
20
+ * (M13, in-process), and the bundled `@litcodex/lit-loop` runtime routes `loop`/`hook`. Returns the
21
+ * integer exit code; the bin owns `process.exit`. No forwarding, no exec-wrapper, no harness.
22
+ */
23
+ export declare function runCli(argv: readonly string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,183 @@
1
+ // Self-contained LitCodex CLI dispatcher (A3 D1).
2
+ //
3
+ // This compiles to dist/cli.js and is imported by bin/litcodex.js. It owns the routing table for
4
+ // every litcodex subcommand and is fully self-contained: it NEVER spawns a child process itself and
5
+ // NEVER forwards to a separate package. The ONLY child process the installer ever spawns is `codex`,
6
+ // and that spawn lives behind the install/* modules (dist/install/*.js), never in this file —
7
+ // keeping dist/cli.js free of any spawn/forwarder token. Unknown subcommands exit 1 with a
8
+ // plain-text usage notice (LITCODEX_INSTALL_UNKNOWN_COMMAND).
9
+ //
10
+ // Two surfaces:
11
+ // - `dispatch` — the PURE synchronous router (help/version/unknown + a pure dry-run install plan).
12
+ // No I/O, no spawn; used by the routing unit tests.
13
+ // - `runCli` — the async process entry the bin awaits. It owns the real, side-effecting routes:
14
+ // install/doctor/uninstall (install/* modules), `config migrate` (M13, in-process), and the
15
+ // bundled lit-loop runtime routes `loop <sub>` and `hook user-prompt-submit`.
16
+ import { createRequire } from "node:module";
17
+ import { runConfigMigrateCli } from "./config-migration/cli.js";
18
+ import { resolveCodexHome } from "./install/codex.js";
19
+ import { runDoctorCli, runInstallCli, runUninstallCli } from "./install/index.js";
20
+ import { LITCODEX_REPO_URL } from "./install/marketplace.js";
21
+ import { buildInstallPlan } from "./install/plan.js";
22
+ import { renderInstallPlan } from "./install/render-plan.js";
23
+ /** Error code emitted when a subcommand is not in the routing table. */
24
+ export const UNKNOWN_COMMAND_CODE = "LITCODEX_INSTALL_UNKNOWN_COMMAND";
25
+ /** Router usage code (EX_USAGE) for an unknown `config` sub-subcommand. */
26
+ export const CONFIG_USAGE_EXIT_CODE = 64;
27
+ const manifest = createRequire(import.meta.url)("../package.json");
28
+ const ok = (stdout) => ({ stdout, stderr: "", exitCode: 0 });
29
+ /**
30
+ * Pure dry-run install plan renderer for the synchronous router. Resolves typed options from the
31
+ * process env WITHOUT any spawn or fs write, then renders the ordered LitCodex plan text.
32
+ */
33
+ function renderInstallDryRun(rest) {
34
+ const plan = buildInstallPlan({
35
+ dryRun: true,
36
+ noTui: rest.includes("--no-tui"),
37
+ autonomous: rest.includes("--codex-autonomous"),
38
+ force: rest.includes("--force"),
39
+ json: rest.includes("--json"),
40
+ codexHome: resolveCodexHome(process.env),
41
+ repoUrl: LITCODEX_REPO_URL,
42
+ repoRoot: process.cwd(),
43
+ });
44
+ return `${renderInstallPlan(plan)}\n`;
45
+ }
46
+ /**
47
+ * Pure synchronous router. For `install --dry-run` it renders the real plan (pure). For the other
48
+ * side-effecting routes it returns a non-error placeholder (the real work runs in `runCli`), so the
49
+ * routing-level contract (known route ≠ exit 1, no forwarder token) is unit-testable without I/O.
50
+ */
51
+ const ROUTES = {
52
+ install: (rest, dryRun) => (dryRun ? ok(renderInstallDryRun(rest)) : ok("litcodex install\n")),
53
+ doctor: () => ok("litcodex doctor\n"),
54
+ uninstall: () => ok("litcodex uninstall\n"),
55
+ config: configRouteHandler,
56
+ loop: () => ok("litcodex loop\n"),
57
+ hook: () => ok("litcodex hook\n"),
58
+ };
59
+ /**
60
+ * Synchronous arm of the `config` route. The real `config migrate` work is async and is run by
61
+ * `runCli` BEFORE `dispatch`; this sync handler only handles routing-level outcomes: an unknown
62
+ * sub-subcommand → usage exit 64; a recognized `config migrate` → exit 0 placeholder.
63
+ */
64
+ function configRouteHandler(rest) {
65
+ if (rest[0] === "migrate") {
66
+ return ok("");
67
+ }
68
+ const sub = rest[0] ?? "<none>";
69
+ return {
70
+ stdout: "",
71
+ stderr: `litcodex config: unknown subcommand "${sub}". Did you mean \`config migrate\`?\n`,
72
+ exitCode: CONFIG_USAGE_EXIT_CODE,
73
+ };
74
+ }
75
+ const KNOWN_SUBCOMMANDS = Object.keys(ROUTES);
76
+ function renderHelp() {
77
+ return [
78
+ "litcodex — self-contained LitCodex CLI for Codex",
79
+ "",
80
+ "Usage:",
81
+ " litcodex install [--dry-run] [options] Install LitCodex into Codex",
82
+ " litcodex doctor Diagnose the LitCodex install",
83
+ " litcodex uninstall Remove LitCodex from Codex",
84
+ " litcodex config migrate Migrate Codex config for LitCodex",
85
+ " litcodex loop <sub> Run a Lit-Loop subcommand",
86
+ " litcodex hook user-prompt-submit Run the UserPromptSubmit hook",
87
+ " litcodex --dry-run <command> Resolve a command without applying it",
88
+ " litcodex --help Show this help",
89
+ " litcodex --version Show the litcodex-ai version",
90
+ "",
91
+ "Examples:",
92
+ " litcodex install",
93
+ " litcodex doctor",
94
+ " litcodex --dry-run install --no-tui",
95
+ "",
96
+ ].join("\n");
97
+ }
98
+ function renderUnknown(subcommand) {
99
+ const usage = [
100
+ `${UNKNOWN_COMMAND_CODE}: unknown subcommand "${subcommand}".`,
101
+ `Known subcommands: ${KNOWN_SUBCOMMANDS.join(", ")}.`,
102
+ "Run `litcodex --help` for usage.",
103
+ "",
104
+ ].join("\n");
105
+ return { stdout: "", stderr: usage, exitCode: 1 };
106
+ }
107
+ /**
108
+ * Pure dispatcher. Parses argv (already sliced past `node bin`), strips the position-independent
109
+ * `--dry-run` flag, answers `--help`/`--version` locally, and routes the first non-flag token.
110
+ * Unknown token → exit 1. No process spawns. No forwarding.
111
+ */
112
+ export function dispatch(argv) {
113
+ const dryRun = argv.includes("--dry-run");
114
+ const rest = argv.filter((token) => token !== "--dry-run");
115
+ if (rest.includes("--help") || rest.includes("-h")) {
116
+ return ok(renderHelp());
117
+ }
118
+ if (rest.includes("--version") || rest.includes("-v")) {
119
+ return ok(`${manifest.version}\n`);
120
+ }
121
+ const subcommand = rest[0];
122
+ if (subcommand === undefined) {
123
+ return ok(renderHelp());
124
+ }
125
+ const handler = ROUTES[subcommand];
126
+ if (handler === undefined) {
127
+ return renderUnknown(subcommand);
128
+ }
129
+ return handler(rest.slice(1), dryRun);
130
+ }
131
+ /**
132
+ * Async process entry used by `bin/litcodex.js`. It owns the side-effecting routes: install/doctor/
133
+ * uninstall (install/* modules, which are the ONLY code that ever spawns `codex`), `config migrate`
134
+ * (M13, in-process), and the bundled `@litcodex/lit-loop` runtime routes `loop`/`hook`. Returns the
135
+ * integer exit code; the bin owns `process.exit`. No forwarding, no exec-wrapper, no harness.
136
+ */
137
+ export async function runCli(argv) {
138
+ const dryRun = argv.includes("--dry-run");
139
+ const rest = argv.filter((token) => token !== "--dry-run");
140
+ const isHelp = rest.includes("--help") || rest.includes("-h");
141
+ const isVersion = rest.includes("--version") || rest.includes("-v");
142
+ if (!isHelp && !isVersion) {
143
+ const head = rest[0];
144
+ if (head === "config" && rest[1] === "migrate") {
145
+ const passthrough = dryRun ? [...rest.slice(2), "--dry-run"] : rest.slice(2);
146
+ return runConfigMigrateCli(passthrough);
147
+ }
148
+ if (head === "install") {
149
+ // Preserve position-independent --dry-run: pass the args after `install`, re-injecting
150
+ // --dry-run (which `rest` already stripped) so the flag survives regardless of position.
151
+ const installArgs = dryRun ? [...rest.slice(1), "--dry-run"] : rest.slice(1);
152
+ return runInstallCli(installArgs);
153
+ }
154
+ if (head === "doctor") {
155
+ return runDoctorCli(rest.slice(1));
156
+ }
157
+ if (head === "uninstall") {
158
+ return runUninstallCli(rest.slice(1));
159
+ }
160
+ if (head === "loop") {
161
+ const { loopCommand } = await import("@litcodex/lit-loop/dist/loop-cli.js");
162
+ return loopCommand(rest.slice(1), { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin });
163
+ }
164
+ if (head === "hook") {
165
+ if (rest[1] !== "user-prompt-submit") {
166
+ return dispatchExit(renderUnknown(rest[1] ?? "hook"));
167
+ }
168
+ const { runUserPromptSubmitHookCli } = await import("@litcodex/lit-loop/dist/hook-cli.js");
169
+ return runUserPromptSubmitHookCli(process.stdin, process.stdout, process.stderr);
170
+ }
171
+ }
172
+ return dispatchExit(dispatch(argv));
173
+ }
174
+ /** Write a pure dispatch result to the process streams and return its exit code. */
175
+ function dispatchExit(result) {
176
+ if (result.stdout) {
177
+ process.stdout.write(result.stdout);
178
+ }
179
+ if (result.stderr) {
180
+ process.stderr.write(result.stderr);
181
+ }
182
+ return result.exitCode;
183
+ }
@@ -0,0 +1,2 @@
1
+ /** Back up an existing config before mutation. Returns the backup path or null. */
2
+ export declare function backupConfigFile(configPath: string): Promise<string | null>;
@@ -0,0 +1,42 @@
1
+ // M13 — pre-write backup (S13 backup.ts).
2
+ //
3
+ // Copies an EXISTING config to <path>.litcodex-bak.<UTC-compact>.<pid> BEFORE
4
+ // any mutation. Returns the backup path, or null when the source does not exist
5
+ // (nothing to back up). Throws CodexConfigMigrationError(BACKUP_FAILED) on copy
6
+ // failure. The backup name components are all controlled/sanitized — never
7
+ // derived from file content (no backup-path injection).
8
+ import { copyFile } from "node:fs/promises";
9
+ import { CodexConfigMigrationError } from "./errors.js";
10
+ /** Back up an existing config before mutation. Returns the backup path or null. */
11
+ export async function backupConfigFile(configPath) {
12
+ const backupPath = `${configPath}.litcodex-bak.${compactTimestamp()}.${process.pid}`;
13
+ try {
14
+ await copyFile(configPath, backupPath);
15
+ return backupPath;
16
+ }
17
+ catch (error) {
18
+ if (isErrnoCode(error, "ENOENT")) {
19
+ return null;
20
+ }
21
+ throw new CodexConfigMigrationError("BACKUP_FAILED", "Could not create a pre-write backup of the Codex config.", configPath, {
22
+ errno: errnoOf(error),
23
+ });
24
+ }
25
+ }
26
+ /** UTC ISO compact: YYYYMMDDTHHMMSSZ (sortable, no path-illegal chars). */
27
+ function compactTimestamp() {
28
+ return new Date()
29
+ .toISOString()
30
+ .replace(/\.\d+Z$/, "Z")
31
+ .replace(/[:-]/g, "");
32
+ }
33
+ function isErrnoCode(error, code) {
34
+ return error instanceof Error && "code" in error && error.code === code;
35
+ }
36
+ function errnoOf(error) {
37
+ if (error instanceof Error && "code" in error) {
38
+ const code = error.code;
39
+ return typeof code === "string" ? code : null;
40
+ }
41
+ return null;
42
+ }
@@ -0,0 +1,22 @@
1
+ export interface ReasoningProfile {
2
+ model: string;
3
+ model_context_window: number;
4
+ model_reasoning_effort: string;
5
+ plan_mode_reasoning_effort: string;
6
+ }
7
+ /** A partial set of root keys used to detect a stale/managed config. */
8
+ export type ProfileMatch = Readonly<Record<string, string | number>>;
9
+ export interface ManagedProfile {
10
+ version: string;
11
+ match: ProfileMatch;
12
+ }
13
+ export interface ModelCatalog {
14
+ version: string;
15
+ current: ReasoningProfile;
16
+ roles: Readonly<Record<string, Partial<ReasoningProfile>>>;
17
+ managedProfiles: readonly ManagedProfile[];
18
+ }
19
+ /** Used whenever the on-disk catalog is missing, malformed, or schema-invalid. */
20
+ export declare const FALLBACK_CATALOG: ModelCatalog;
21
+ /** Read the catalog. Never throws — returns FALLBACK_CATALOG on any failure. */
22
+ export declare function readModelCatalog(env?: NodeJS.ProcessEnv): Promise<ModelCatalog>;
@@ -0,0 +1,99 @@
1
+ // M13 — model catalog reader (S13 catalog.ts).
2
+ //
3
+ // Loads the reasoning catalog. NEVER throws: on a missing file, malformed JSON,
4
+ // or a schema failure it returns FALLBACK_CATALOG. The catalog ships as the
5
+ // authored data file `model-catalog.json` at the package root, resolved at
6
+ // runtime by this `import ... with { type: "json" }` (NOT baked into dist/ by
7
+ // tsc). The package `files` array MUST therefore include `model-catalog.json`
8
+ // (A3 G7 option (a): ["bin","dist","model-catalog.json","README.md","LICENSE"])
9
+ // or a stripped tarball install crashes here at module load. An explicit
10
+ // LITCODEX_MODEL_CATALOG_PATH env var overrides the bundled copy for testing.
11
+ //
12
+ // VERIFY-LIVE (A3 Part C #5): the `current` model/window/effort values
13
+ // (gpt-5.5 / 400000 / high / xhigh) and the legacy `managedProfiles` are
14
+ // UNVERIFIED against the live Codex model catalog — confirm at runtime, do not
15
+ // block. They are config data, not legacy brand tokens.
16
+ import { readFile } from "node:fs/promises";
17
+ import bundledCatalog from "../../model-catalog.json" with { type: "json" };
18
+ /** Used whenever the on-disk catalog is missing, malformed, or schema-invalid. */
19
+ export const FALLBACK_CATALOG = parseCatalog(bundledCatalog) ?? {
20
+ version: "fallback.gpt-5.5-400k-high",
21
+ current: {
22
+ model: "gpt-5.5",
23
+ model_context_window: 400_000,
24
+ model_reasoning_effort: "high",
25
+ plan_mode_reasoning_effort: "xhigh",
26
+ },
27
+ roles: {
28
+ default: {
29
+ model: "gpt-5.5",
30
+ model_context_window: 400_000,
31
+ model_reasoning_effort: "high",
32
+ plan_mode_reasoning_effort: "xhigh",
33
+ },
34
+ },
35
+ managedProfiles: [{ version: "legacy.gpt-5.5-272k", match: { model: "gpt-5.5", model_context_window: 272_000 } }],
36
+ };
37
+ /** Read the catalog. Never throws — returns FALLBACK_CATALOG on any failure. */
38
+ export async function readModelCatalog(env = process.env) {
39
+ const override = env["LITCODEX_MODEL_CATALOG_PATH"]?.trim();
40
+ if (override === undefined || override === "") {
41
+ return FALLBACK_CATALOG;
42
+ }
43
+ try {
44
+ const parsed = parseCatalog(JSON.parse(await readFile(override, "utf8")));
45
+ return parsed ?? FALLBACK_CATALOG;
46
+ }
47
+ catch (error) {
48
+ if (error instanceof Error) {
49
+ return FALLBACK_CATALOG;
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+ function parseCatalog(value) {
55
+ if (!isRecord(value)) {
56
+ return null;
57
+ }
58
+ if (typeof value["version"] !== "string") {
59
+ return null;
60
+ }
61
+ const current = value["current"];
62
+ if (!isReasoningProfile(current)) {
63
+ return null;
64
+ }
65
+ const rawProfiles = value["managedProfiles"];
66
+ if (!Array.isArray(rawProfiles)) {
67
+ return null;
68
+ }
69
+ const managedProfiles = [];
70
+ for (const profile of rawProfiles) {
71
+ if (!isRecord(profile) || typeof profile["version"] !== "string" || !isMatchRecord(profile["match"])) {
72
+ return null;
73
+ }
74
+ managedProfiles.push({ version: profile["version"], match: profile["match"] });
75
+ }
76
+ const roles = isRecord(value["roles"]) ? value["roles"] : {};
77
+ return { version: value["version"], current, roles, managedProfiles };
78
+ }
79
+ function isReasoningProfile(value) {
80
+ return (isRecord(value) &&
81
+ typeof value["model"] === "string" &&
82
+ typeof value["model_context_window"] === "number" &&
83
+ typeof value["model_reasoning_effort"] === "string" &&
84
+ typeof value["plan_mode_reasoning_effort"] === "string");
85
+ }
86
+ function isMatchRecord(value) {
87
+ if (!isRecord(value)) {
88
+ return false;
89
+ }
90
+ for (const entry of Object.values(value)) {
91
+ if (typeof entry !== "string" && typeof entry !== "number") {
92
+ return false;
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+ function isRecord(value) {
98
+ return typeof value === "object" && value !== null && !Array.isArray(value);
99
+ }
@@ -0,0 +1,14 @@
1
+ export interface ConfigMigrateCliArgs {
2
+ sessionStart: boolean;
3
+ json: boolean;
4
+ cwd: string | null;
5
+ dryRun: boolean;
6
+ }
7
+ /** Parse the sliced `config migrate` argv (everything after `config migrate`). */
8
+ export declare function parseConfigMigrateArgs(argv: readonly string[]): ConfigMigrateCliArgs;
9
+ /**
10
+ * Run the config-migrate route in-process. Resolves args, calls
11
+ * migrateCodexConfig, renders, returns an exit code. Returns (does not throw)
12
+ * for mapped errors. session-start mode swallows everything and returns 0.
13
+ */
14
+ export declare function runConfigMigrateCli(argv: readonly string[], out?: (text: string) => void, err?: (text: string) => void): Promise<number>;
@@ -0,0 +1,85 @@
1
+ // M13 — `config migrate` CLI glue (S13 cli.ts + addendum Gap A).
2
+ //
3
+ // Pure-of-process-spawn CLI entry. Parses the already-sliced `config migrate`
4
+ // args, runs migrateCodexConfig IN-PROCESS, renders stdout/stderr, returns an
5
+ // exit code per the parent Exit-code table. NEVER spawns a child process and
6
+ // NEVER forwards. session-start mode always returns 0.
7
+ import { CodexConfigMigrationError, exitCodeForMigrationError } from "./errors.js";
8
+ import { migrateCodexConfig } from "./index.js";
9
+ /** Parse the sliced `config migrate` argv (everything after `config migrate`). */
10
+ export function parseConfigMigrateArgs(argv) {
11
+ let sessionStart = false;
12
+ let json = false;
13
+ let dryRun = false;
14
+ let cwd = null;
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const arg = argv[i];
17
+ if (arg === "--session-start") {
18
+ sessionStart = true;
19
+ }
20
+ else if (arg === "--json") {
21
+ json = true;
22
+ }
23
+ else if (arg === "--dry-run") {
24
+ dryRun = true;
25
+ }
26
+ else if (arg === "--cwd") {
27
+ const next = argv[i + 1];
28
+ if (next !== undefined) {
29
+ cwd = next;
30
+ i += 1;
31
+ }
32
+ }
33
+ else if (arg?.startsWith("--cwd=")) {
34
+ cwd = arg.slice("--cwd=".length);
35
+ }
36
+ }
37
+ return { sessionStart, json, cwd, dryRun };
38
+ }
39
+ /**
40
+ * Run the config-migrate route in-process. Resolves args, calls
41
+ * migrateCodexConfig, renders, returns an exit code. Returns (does not throw)
42
+ * for mapped errors. session-start mode swallows everything and returns 0.
43
+ */
44
+ export async function runConfigMigrateCli(argv, out = (text) => process.stdout.write(text), err = (text) => process.stderr.write(text)) {
45
+ const args = parseConfigMigrateArgs(argv);
46
+ const mode = args.sessionStart ? "session-start" : "install";
47
+ const cwd = args.cwd ?? process.cwd();
48
+ let result;
49
+ try {
50
+ result = await migrateCodexConfig({ mode, cwd, dryRun: args.dryRun });
51
+ }
52
+ catch (error) {
53
+ if (args.sessionStart) {
54
+ return 0; // fire-and-forget: never block a turn.
55
+ }
56
+ if (error instanceof CodexConfigMigrationError) {
57
+ err(`${JSON.stringify({ ok: false, code: error.code, configPath: error.configPath, message: error.message, details: error.details })}\n`);
58
+ return exitCodeForMigrationError(error.code);
59
+ }
60
+ throw error;
61
+ }
62
+ if (args.json) {
63
+ out(`${JSON.stringify(result)}\n`);
64
+ }
65
+ else {
66
+ out(renderText(result));
67
+ }
68
+ return 0;
69
+ }
70
+ function renderText(result) {
71
+ const userModified = result.skipped.filter((s) => s.reason === "user-modified");
72
+ const lines = [
73
+ `litcodex config: ${result.changed.length} file(s) updated, ${result.backups.length} backup(s) created, ${userModified.length} skipped (user-modified)`,
74
+ ];
75
+ for (const path of result.changed) {
76
+ lines.push(`updated: ${path}`);
77
+ }
78
+ for (const path of result.backups) {
79
+ lines.push(`backup: ${path}`);
80
+ }
81
+ for (const skip of userModified) {
82
+ lines.push(`skipped: ${skip.path} (user-modified)`);
83
+ }
84
+ return `${lines.join("\n")}\n`;
85
+ }
@@ -0,0 +1,4 @@
1
+ export declare function configPaths(args: {
2
+ env: NodeJS.ProcessEnv;
3
+ cwd: string;
4
+ }): Promise<string[]>;
@@ -0,0 +1,64 @@
1
+ // M13 — config-path discovery (S13 config-paths.ts).
2
+ //
3
+ // Discovers the global $CODEX_HOME/config.toml plus every project-local
4
+ // .codex/config.toml from `cwd` up to homedir(). Symlinked .codex dirs/files
5
+ // are SKIPPED (lstat, not stat) so a symlinked .codex can never redirect a
6
+ // write to an attacker-chosen target. CODEX_HOME is kept verbatim (host runtime
7
+ // var, not a LitCodex-owned name). Returns deduped absolute paths, global first.
8
+ import { lstat } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join, resolve } from "node:path";
11
+ export async function configPaths(args) {
12
+ const { env, cwd } = args;
13
+ const codexHomeRaw = env["CODEX_HOME"]?.trim();
14
+ const codexHome = resolve(codexHomeRaw !== undefined && codexHomeRaw !== "" ? codexHomeRaw : join(homedir(), ".codex"));
15
+ const paths = new Set([join(codexHome, "config.toml")]);
16
+ for (const projectConfig of projectConfigPaths({ cwd, stopAt: homedir() })) {
17
+ if (!(await isRegularFile(projectConfig))) {
18
+ continue;
19
+ }
20
+ if (!(await isRegularDirectory(dirname(projectConfig)))) {
21
+ continue;
22
+ }
23
+ paths.add(projectConfig);
24
+ }
25
+ return [...paths];
26
+ }
27
+ function projectConfigPaths(args) {
28
+ const paths = [];
29
+ let current = resolve(args.cwd);
30
+ const stop = resolve(args.stopAt);
31
+ while (true) {
32
+ paths.push(join(current, ".codex", "config.toml"));
33
+ if (current === stop || current === dirname(current)) {
34
+ break;
35
+ }
36
+ current = dirname(current);
37
+ }
38
+ return paths;
39
+ }
40
+ async function isRegularFile(path) {
41
+ try {
42
+ return (await lstat(path)).isFile();
43
+ }
44
+ catch (error) {
45
+ if (isErrnoCode(error, "ENOENT")) {
46
+ return false;
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+ async function isRegularDirectory(path) {
52
+ try {
53
+ return (await lstat(path)).isDirectory();
54
+ }
55
+ catch (error) {
56
+ if (isErrnoCode(error, "ENOENT")) {
57
+ return false;
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+ function isErrnoCode(error, code) {
63
+ return error instanceof Error && "code" in error && error.code === code;
64
+ }
@@ -0,0 +1,11 @@
1
+ /** Closed set of failure classes the migration can raise. */
2
+ export type CodexConfigMigrationCode = "CONFIG_MALFORMED" | "CONFIG_UNWRITABLE" | "BACKUP_FAILED" | "STATE_UNWRITABLE";
3
+ /** Machine-readable migration error. `code` drives the install-mode exit code. */
4
+ export declare class CodexConfigMigrationError extends Error {
5
+ readonly code: CodexConfigMigrationCode;
6
+ readonly configPath: string | null;
7
+ readonly details: Readonly<Record<string, unknown>>;
8
+ constructor(code: CodexConfigMigrationCode, message: string, configPath: string | null, details?: Record<string, unknown>);
9
+ }
10
+ /** Install-mode exit code for a migration error (parent Exit-code table). */
11
+ export declare function exitCodeForMigrationError(code: CodexConfigMigrationCode): number;