@iamdevlinph/codex-kit 1.0.17 → 1.0.18
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/assets/TEMPLATE_AGENTS.md +4 -0
- package/bin/codex-kit.js +863 -28
- package/bin/routing-hook.js +32 -3
- package/package.json +1 -1
|
@@ -123,6 +123,10 @@ conditional procedures into validated project skills.
|
|
|
123
123
|
|
|
124
124
|
- Follow the repository's organization and naming. Prefer focused files and
|
|
125
125
|
split mixed responsibilities when readability improves.
|
|
126
|
+
- Keep route and page files focused on page-level composition, data loading, and
|
|
127
|
+
orchestration. Extract substantial self-contained UI sections and complex
|
|
128
|
+
page-specific logic into colocated feature components or modules. Keep small
|
|
129
|
+
one-use markup inline; do not create components solely to reduce line count.
|
|
126
130
|
- Use intent-revealing domain names. A reader should understand what a variable
|
|
127
131
|
contains or what a helper guarantees at the call site without opening its
|
|
128
132
|
implementation. Avoid vague transformation names such as `normalized`,
|
package/bin/codex-kit.js
CHANGED
|
@@ -1,25 +1,601 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
|
|
3
|
+
// src/codex-kit.ts
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
import { resolve as resolve3 } from "node:path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/global/commands.ts
|
|
9
|
+
import {
|
|
10
|
+
copyFileSync as copyFileSync3,
|
|
11
|
+
existsSync as existsSync5,
|
|
12
|
+
mkdirSync as mkdirSync2,
|
|
13
|
+
readdirSync,
|
|
14
|
+
rmSync as rmSync4,
|
|
15
|
+
statSync
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join as join5 } from "node:path";
|
|
18
|
+
|
|
19
|
+
// src/files.ts
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import {
|
|
22
|
+
copyFileSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
renameSync,
|
|
27
|
+
writeFileSync
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { dirname } from "node:path";
|
|
30
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
31
|
+
var sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
32
|
+
var read = (file) => readFileSync(file);
|
|
33
|
+
var readText = (file) => readFileSync(file, "utf8");
|
|
34
|
+
function backup(file) {
|
|
35
|
+
if (!existsSync(file)) return null;
|
|
36
|
+
const stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:TZ.]/g, "");
|
|
37
|
+
let destination = `${file}.codex-kit.bak-${stamp()}`;
|
|
38
|
+
let suffix = 1;
|
|
39
|
+
while (existsSync(destination))
|
|
40
|
+
destination = `${file}.codex-kit.bak-${stamp()}-${suffix++}`;
|
|
41
|
+
copyFileSync(file, destination);
|
|
42
|
+
console.log(`backup: ${destination}`);
|
|
43
|
+
return destination;
|
|
44
|
+
}
|
|
45
|
+
function write(file, data) {
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
47
|
+
const temporary = `${file}.codex-kit.tmp-${process.pid}`;
|
|
48
|
+
writeFileSync(temporary, data);
|
|
49
|
+
renameSync(temporary, file);
|
|
50
|
+
}
|
|
51
|
+
function readJsonObject(file) {
|
|
52
|
+
if (!existsSync(file)) return {};
|
|
53
|
+
try {
|
|
54
|
+
const value = JSON.parse(readText(file));
|
|
55
|
+
if (isRecord(value)) return value;
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
throw new Error(
|
|
59
|
+
`${file} must contain a JSON object; fix or move it before installing.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/package.ts
|
|
64
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
65
|
+
import { dirname as dirname2, join, resolve } from "node:path";
|
|
66
|
+
import { fileURLToPath } from "node:url";
|
|
67
|
+
var ROOT = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
|
|
68
|
+
var ASSETS = join(ROOT, "assets");
|
|
69
|
+
var AGENTS_DIR = join(ASSETS, "agents");
|
|
70
|
+
var SKILLS_DIR = join(ASSETS, "skills");
|
|
71
|
+
var RECONCILE_SKILL = "codex-kit-reconcile-agents";
|
|
72
|
+
var RECONCILE_SKILL_FILE = join(
|
|
73
|
+
SKILLS_DIR,
|
|
74
|
+
RECONCILE_SKILL,
|
|
75
|
+
"SKILL.md"
|
|
76
|
+
);
|
|
77
|
+
var RECONCILE_SKILL_METADATA_FILE = join(
|
|
78
|
+
SKILLS_DIR,
|
|
79
|
+
RECONCILE_SKILL,
|
|
80
|
+
"agents",
|
|
81
|
+
"openai.yaml"
|
|
82
|
+
);
|
|
83
|
+
var ROUTING_FILE = join(ASSETS, "SUBAGENT_ROUTING.md");
|
|
84
|
+
var ROUTING_HOOK_FILE = join(ROOT, "bin", "routing-hook.js");
|
|
85
|
+
var TEMPLATE_FILE = join(ASSETS, "TEMPLATE_AGENTS.md");
|
|
86
|
+
var PACKAGE = JSON.parse(
|
|
87
|
+
readFileSync2(join(ROOT, "package.json"), "utf8")
|
|
88
|
+
);
|
|
89
|
+
var REGISTRY = PACKAGE.publishConfig?.registry ?? "https://registry.npmjs.org";
|
|
90
|
+
|
|
91
|
+
// src/global/config.ts
|
|
92
|
+
import { existsSync as existsSync3, rmSync as rmSync2 } from "node:fs";
|
|
93
|
+
import { join as join3 } from "node:path";
|
|
94
|
+
|
|
95
|
+
// src/global/state.ts
|
|
96
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync2, rmSync } from "node:fs";
|
|
97
|
+
import { join as join2 } from "node:path";
|
|
98
|
+
var STATE_FILE = ".codex-kit-state.json";
|
|
99
|
+
function loadState(home) {
|
|
100
|
+
const file = join2(home, STATE_FILE);
|
|
101
|
+
if (!existsSync2(file)) return { version: PACKAGE.version, files: {} };
|
|
102
|
+
try {
|
|
103
|
+
const state = JSON.parse(readText(file));
|
|
104
|
+
return isRecord(state) && isRecord(state.files) ? state : { version: PACKAGE.version, files: {} };
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${file} is not valid JSON; move it aside before reinstalling.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function saveState(home, state) {
|
|
112
|
+
write(join2(home, STATE_FILE), `${JSON.stringify(state, null, 2)}
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
function installFile(source, target, key, prior, force) {
|
|
116
|
+
const sourceData = read(source);
|
|
117
|
+
const sourceHash = sha256(sourceData);
|
|
118
|
+
const previous = prior.files[key];
|
|
119
|
+
if (!existsSync2(target)) {
|
|
120
|
+
write(target, sourceData);
|
|
121
|
+
console.log(`installed: ${target}`);
|
|
122
|
+
return { target, hash: sourceHash, ownership: "created", backup: null };
|
|
123
|
+
}
|
|
124
|
+
const targetHash = sha256(read(target));
|
|
125
|
+
if (targetHash === sourceHash) {
|
|
126
|
+
console.log(`unchanged: ${target}`);
|
|
127
|
+
return previous ?? {
|
|
128
|
+
target,
|
|
129
|
+
hash: sourceHash,
|
|
130
|
+
ownership: "preexisting",
|
|
131
|
+
backup: null
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const safelyOwned = previous && previous.target === target && previous.ownership !== "preexisting" && previous.hash === targetHash;
|
|
135
|
+
if (!safelyOwned && !force) {
|
|
136
|
+
console.warn(
|
|
137
|
+
`preserved modified or pre-existing file: ${target} (use --force to replace)`
|
|
138
|
+
);
|
|
139
|
+
return previous ?? null;
|
|
140
|
+
}
|
|
141
|
+
const newBackup = backup(target);
|
|
142
|
+
write(target, sourceData);
|
|
143
|
+
console.log(`updated: ${target}`);
|
|
144
|
+
return {
|
|
145
|
+
target,
|
|
146
|
+
hash: sourceHash,
|
|
147
|
+
ownership: safelyOwned ? previous.ownership : "replaced",
|
|
148
|
+
backup: safelyOwned ? previous.backup : newBackup
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function restoreFile(record) {
|
|
152
|
+
const { target } = record;
|
|
153
|
+
if (!existsSync2(target) || sha256(read(target)) !== record.hash) {
|
|
154
|
+
console.warn(`preserved modified or missing file: ${target}`);
|
|
155
|
+
} else if (record.ownership === "created") {
|
|
156
|
+
rmSync(target);
|
|
157
|
+
console.log(`removed: ${target}`);
|
|
158
|
+
} else if (record.ownership === "replaced" && record.backup && existsSync2(record.backup)) {
|
|
159
|
+
copyFileSync2(record.backup, target);
|
|
160
|
+
console.log(`restored: ${target}`);
|
|
161
|
+
} else console.log(`preserved pre-existing file: ${target}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/global/config.ts
|
|
165
|
+
function topLevelConfigEntries(contents) {
|
|
166
|
+
const entries = /* @__PURE__ */ new Map();
|
|
167
|
+
let inTable = false;
|
|
168
|
+
for (const line of contents.split("\n")) {
|
|
169
|
+
if (/^\s*\[/.test(line)) {
|
|
170
|
+
inTable = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (inTable) continue;
|
|
174
|
+
const match = /^(\s*)(model|model_reasoning_effort|plan_mode_reasoning_effort)\s*=\s*(.*?)\s*$/.exec(
|
|
175
|
+
line
|
|
176
|
+
);
|
|
177
|
+
const key = match?.[2];
|
|
178
|
+
const value = match?.[3];
|
|
179
|
+
if (key && value !== void 0 && !entries.has(key))
|
|
180
|
+
entries.set(key, { value, line });
|
|
181
|
+
}
|
|
182
|
+
return entries;
|
|
183
|
+
}
|
|
184
|
+
var tomlString = (value) => JSON.stringify(value);
|
|
185
|
+
function setTopLevelConfig(contents, desired) {
|
|
186
|
+
const lines = contents.split("\n");
|
|
187
|
+
const seen = /* @__PURE__ */ new Set();
|
|
188
|
+
let firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
189
|
+
if (firstTable < 0) firstTable = lines.length;
|
|
190
|
+
for (let index = 0; index < firstTable; index++) {
|
|
191
|
+
const match = /^(\s*)(model|model_reasoning_effort|plan_mode_reasoning_effort)\s*=\s*(.*?)\s*$/.exec(
|
|
192
|
+
lines[index] ?? ""
|
|
193
|
+
);
|
|
194
|
+
const key = match?.[2];
|
|
195
|
+
if (!match || !key || seen.has(key)) continue;
|
|
196
|
+
lines[index] = `${match[1]}${key} = ${tomlString(desired[key])}`;
|
|
197
|
+
seen.add(key);
|
|
198
|
+
}
|
|
199
|
+
const missing = Object.keys(desired).filter((key) => !seen.has(key)).map((key) => `${key} = ${tomlString(desired[key])}`);
|
|
200
|
+
if (missing.length) lines.splice(0, 0, ...missing, "");
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
function restoreTopLevelConfig(contents, config) {
|
|
204
|
+
const lines = contents.split("\n");
|
|
205
|
+
let firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
206
|
+
if (firstTable < 0) firstTable = lines.length;
|
|
207
|
+
const restored = /* @__PURE__ */ new Set();
|
|
208
|
+
for (let index = 0; index < firstTable; index++) {
|
|
209
|
+
const match = /^(\s*)(model|model_reasoning_effort|plan_mode_reasoning_effort)\s*=\s*(.*?)\s*$/.exec(
|
|
210
|
+
lines[index] ?? ""
|
|
211
|
+
);
|
|
212
|
+
const key = match?.[2];
|
|
213
|
+
if (!match || !key || restored.has(key) || match[3] !== tomlString(config.desired[key]))
|
|
214
|
+
continue;
|
|
215
|
+
const prior = config.previous[key];
|
|
216
|
+
if (prior?.present) lines[index] = `${match[1]}${key} = ${prior.value}`;
|
|
217
|
+
else {
|
|
218
|
+
lines.splice(index, 1);
|
|
219
|
+
index--;
|
|
220
|
+
firstTable--;
|
|
221
|
+
}
|
|
222
|
+
restored.add(key);
|
|
223
|
+
}
|
|
224
|
+
if (Object.values(config.previous).some((entry) => entry && !entry.present) && lines[0] === "")
|
|
225
|
+
lines.shift();
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
function configureGlobal(options) {
|
|
229
|
+
const home = options.codexHome;
|
|
230
|
+
const configFile = join3(home, "config.toml");
|
|
231
|
+
const desired = {
|
|
232
|
+
model: options.orchestrator,
|
|
233
|
+
model_reasoning_effort: options.reasoningEffort,
|
|
234
|
+
plan_mode_reasoning_effort: options.planReasoningEffort
|
|
235
|
+
};
|
|
236
|
+
const state = loadState(home);
|
|
237
|
+
const original = existsSync3(configFile) ? readText(configFile) : "";
|
|
238
|
+
const current = topLevelConfigEntries(original);
|
|
239
|
+
if (state.config?.target === configFile) {
|
|
240
|
+
const changed = Object.entries(state.config.desired).some(([key, value]) => current.get(key)?.value !== tomlString(value));
|
|
241
|
+
if (changed && !options.force) {
|
|
242
|
+
console.warn(
|
|
243
|
+
`preserved modified config: ${configFile} (use --force to replace)`
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const updated = setTopLevelConfig(original, desired);
|
|
249
|
+
if (updated !== original) {
|
|
250
|
+
backup(configFile);
|
|
251
|
+
write(configFile, updated);
|
|
252
|
+
console.log(`configured orchestrator: ${configFile}`);
|
|
253
|
+
} else console.log(`unchanged: ${configFile}`);
|
|
254
|
+
const previous = state.config?.previous ?? {};
|
|
255
|
+
for (const key of Object.keys(desired)) {
|
|
256
|
+
if (key in previous) continue;
|
|
257
|
+
const entry = current.get(key);
|
|
258
|
+
previous[key] = entry ? { present: true, value: entry.value } : { present: false };
|
|
259
|
+
}
|
|
260
|
+
state.version = PACKAGE.version;
|
|
261
|
+
state.config = { target: configFile, desired, previous };
|
|
262
|
+
saveState(home, state);
|
|
263
|
+
console.log(`Orchestrator: ${desired.model}`);
|
|
264
|
+
console.log(`Reasoning effort: ${desired.model_reasoning_effort}`);
|
|
265
|
+
console.log(
|
|
266
|
+
`Plan mode reasoning effort: ${desired.plan_mode_reasoning_effort}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
function restoreConfig(config) {
|
|
270
|
+
if (!existsSync3(config.target)) {
|
|
271
|
+
console.warn(`preserved missing config: ${config.target}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const original = readText(config.target);
|
|
275
|
+
const current = topLevelConfigEntries(original);
|
|
276
|
+
const changed = Object.entries(config.desired).some(([key, value]) => current.get(key)?.value !== tomlString(value));
|
|
277
|
+
if (changed) {
|
|
278
|
+
console.warn(`preserved modified config: ${config.target}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const restored = restoreTopLevelConfig(original, config);
|
|
282
|
+
if (restored === original) return;
|
|
283
|
+
backup(config.target);
|
|
284
|
+
if (restored.trim()) write(config.target, restored);
|
|
285
|
+
else rmSync2(config.target);
|
|
286
|
+
console.log(`restored config: ${config.target}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/global/hooks.ts
|
|
290
|
+
import { existsSync as existsSync4, rmSync as rmSync3 } from "node:fs";
|
|
291
|
+
import { join as join4 } from "node:path";
|
|
292
|
+
var shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
293
|
+
var hookCommands = (file) => ({
|
|
294
|
+
command: `/usr/bin/env node ${shellQuote(file)}`,
|
|
295
|
+
commandWindows: `node ${JSON.stringify(file)}`
|
|
296
|
+
});
|
|
297
|
+
function removeHookHandlers(root, state) {
|
|
298
|
+
const hooks = root.hooks;
|
|
299
|
+
if (!isRecord(hooks)) return;
|
|
300
|
+
for (const [event, groupsValue] of Object.entries(hooks)) {
|
|
301
|
+
if (!Array.isArray(groupsValue)) continue;
|
|
302
|
+
const groups = groupsValue.flatMap((groupValue) => {
|
|
303
|
+
if (!isRecord(groupValue) || !Array.isArray(groupValue.hooks))
|
|
304
|
+
return [groupValue];
|
|
305
|
+
const handlers = groupValue.hooks.filter(
|
|
306
|
+
(handler) => !isRecord(handler) || handler.command !== state.command && handler.commandWindows !== state.commandWindows
|
|
307
|
+
);
|
|
308
|
+
return handlers.length ? [{ ...groupValue, hooks: handlers }] : [];
|
|
309
|
+
});
|
|
310
|
+
if (groups.length) hooks[event] = groups;
|
|
311
|
+
else delete hooks[event];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function installRoutingHooks(home, previous) {
|
|
315
|
+
const target = join4(home, "hooks.json");
|
|
316
|
+
const commands = hookCommands(join4(home, "codex-kit", "routing-hook.js"));
|
|
317
|
+
const created = previous?.created ?? !existsSync4(target);
|
|
318
|
+
const root = readJsonObject(target);
|
|
319
|
+
if (previous) removeHookHandlers(root, previous);
|
|
320
|
+
const hooks = isRecord(root.hooks) ? root.hooks : {};
|
|
321
|
+
root.hooks = hooks;
|
|
322
|
+
const handler = { type: "command", ...commands, timeout: 5 };
|
|
323
|
+
hooks.UserPromptSubmit = [
|
|
324
|
+
...Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : [],
|
|
325
|
+
{ hooks: [{ ...handler, statusMessage: "Loading subagent routing" }] }
|
|
326
|
+
];
|
|
327
|
+
hooks.SubagentStart = [
|
|
328
|
+
...Array.isArray(hooks.SubagentStart) ? hooks.SubagentStart : [],
|
|
329
|
+
{ hooks: [{ ...handler, statusMessage: "Briefing delegated worker" }] }
|
|
330
|
+
];
|
|
331
|
+
const updated = `${JSON.stringify(root, null, 2)}
|
|
332
|
+
`;
|
|
333
|
+
const original = existsSync4(target) ? readText(target) : "";
|
|
334
|
+
if (updated !== original) {
|
|
335
|
+
backup(target);
|
|
336
|
+
write(target, updated);
|
|
337
|
+
console.log(`updated: ${target}`);
|
|
338
|
+
}
|
|
339
|
+
return { target, ...commands, created };
|
|
340
|
+
}
|
|
341
|
+
function uninstallRoutingHooks(state) {
|
|
342
|
+
if (!existsSync4(state.target)) {
|
|
343
|
+
console.warn(`preserved missing hooks file: ${state.target}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const root = readJsonObject(state.target);
|
|
347
|
+
removeHookHandlers(root, state);
|
|
348
|
+
if (isRecord(root.hooks) && !Object.keys(root.hooks).length)
|
|
349
|
+
delete root.hooks;
|
|
350
|
+
backup(state.target);
|
|
351
|
+
if (state.created && !Object.keys(root).length) rmSync3(state.target);
|
|
352
|
+
else write(state.target, `${JSON.stringify(root, null, 2)}
|
|
353
|
+
`);
|
|
354
|
+
console.log(`removed managed routing hooks from: ${state.target}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/global/commands.ts
|
|
358
|
+
var GLOBAL_BEGIN = "<!-- BEGIN codex-kit:subagent-routing -->";
|
|
359
|
+
var GLOBAL_END = "<!-- END codex-kit:subagent-routing -->";
|
|
360
|
+
var managedBlock = (content, begin, end) => `${begin}
|
|
361
|
+
${content.trimEnd()}
|
|
362
|
+
${end}`;
|
|
363
|
+
function replaceOrAppendBlock(original, content, begin, end) {
|
|
364
|
+
const start = original.indexOf(begin);
|
|
365
|
+
const finish = original.indexOf(end);
|
|
366
|
+
if (start >= 0 !== finish >= 0 || start >= 0 && finish < start)
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Malformed managed block: expected both ${begin} and ${end}.`
|
|
369
|
+
);
|
|
370
|
+
const block = managedBlock(content, begin, end);
|
|
371
|
+
return start >= 0 ? `${original.slice(0, start)}${block}${original.slice(finish + end.length)}` : `${original.trimEnd()}${original.trim() ? "\n\n" : ""}${block}
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
function removeBlock(original, begin, end) {
|
|
375
|
+
const start = original.indexOf(begin);
|
|
376
|
+
const finish = original.indexOf(end);
|
|
377
|
+
if (start < 0 && finish < 0) return original;
|
|
378
|
+
if (start < 0 || finish < start)
|
|
379
|
+
throw new Error("Malformed managed block in AGENTS.md.");
|
|
380
|
+
const before = original.slice(0, start).trimEnd();
|
|
381
|
+
const after = original.slice(finish + end.length).trimStart();
|
|
382
|
+
return `${before}${before && after ? "\n\n" : ""}${after}${before || after ? "\n" : ""}`;
|
|
383
|
+
}
|
|
384
|
+
function installGlobal(options) {
|
|
385
|
+
const home = options.codexHome;
|
|
386
|
+
mkdirSync2(home, { recursive: true });
|
|
387
|
+
const prior = loadState(home);
|
|
388
|
+
const next = {
|
|
389
|
+
version: PACKAGE.version,
|
|
390
|
+
files: {},
|
|
391
|
+
globalAgents: null,
|
|
392
|
+
hooks: null,
|
|
393
|
+
config: prior.config ?? null
|
|
394
|
+
};
|
|
395
|
+
for (const name of readdirSync(AGENTS_DIR).filter((name2) => name2.endsWith(".toml")).sort()) {
|
|
396
|
+
const key = `agents/${name}`;
|
|
397
|
+
const record = installFile(
|
|
398
|
+
join5(AGENTS_DIR, name),
|
|
399
|
+
join5(home, "agents", name),
|
|
400
|
+
key,
|
|
401
|
+
prior,
|
|
402
|
+
options.force
|
|
403
|
+
);
|
|
404
|
+
if (record) next.files[key] = record;
|
|
405
|
+
}
|
|
406
|
+
const sources = [
|
|
407
|
+
[ROUTING_FILE, join5(home, "SUBAGENT_ROUTING.md"), "routing"],
|
|
408
|
+
[
|
|
409
|
+
RECONCILE_SKILL_FILE,
|
|
410
|
+
join5(home, "skills", RECONCILE_SKILL, "SKILL.md"),
|
|
411
|
+
`skills/${RECONCILE_SKILL}/SKILL.md`
|
|
412
|
+
],
|
|
413
|
+
[
|
|
414
|
+
RECONCILE_SKILL_METADATA_FILE,
|
|
415
|
+
join5(home, "skills", RECONCILE_SKILL, "agents", "openai.yaml"),
|
|
416
|
+
`skills/${RECONCILE_SKILL}/agents/openai.yaml`
|
|
417
|
+
],
|
|
418
|
+
[
|
|
419
|
+
ROUTING_HOOK_FILE,
|
|
420
|
+
join5(home, "codex-kit", "routing-hook.js"),
|
|
421
|
+
"routing-hook"
|
|
422
|
+
]
|
|
423
|
+
];
|
|
424
|
+
for (const [source, target, key] of sources) {
|
|
425
|
+
const record = installFile(source, target, key, prior, options.force);
|
|
426
|
+
if (record) next.files[key] = record;
|
|
427
|
+
}
|
|
428
|
+
for (const [key, record] of Object.entries(prior.files)) {
|
|
429
|
+
if (key in next.files) continue;
|
|
430
|
+
if (!existsSync5(record.target) || sha256(read(record.target)) !== record.hash) {
|
|
431
|
+
console.warn(
|
|
432
|
+
`preserved stale modified or missing file: ${record.target}`
|
|
433
|
+
);
|
|
434
|
+
next.files[key] = record;
|
|
435
|
+
} else if (record.ownership === "created") {
|
|
436
|
+
rmSync4(record.target);
|
|
437
|
+
console.log(`removed stale: ${record.target}`);
|
|
438
|
+
} else if (record.ownership === "replaced" && record.backup && existsSync5(record.backup)) {
|
|
439
|
+
copyFileSync3(record.backup, record.target);
|
|
440
|
+
console.log(`restored stale: ${record.target}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const globalAgents = join5(home, "AGENTS.md");
|
|
444
|
+
const original = existsSync5(globalAgents) ? readText(globalAgents) : "";
|
|
445
|
+
const updated = replaceOrAppendBlock(
|
|
446
|
+
original,
|
|
447
|
+
readText(ROUTING_FILE),
|
|
448
|
+
GLOBAL_BEGIN,
|
|
449
|
+
GLOBAL_END
|
|
450
|
+
);
|
|
451
|
+
if (updated !== original) {
|
|
452
|
+
backup(globalAgents);
|
|
453
|
+
write(globalAgents, updated);
|
|
454
|
+
console.log(`updated: ${globalAgents}`);
|
|
455
|
+
}
|
|
456
|
+
next.globalAgents = { target: globalAgents };
|
|
457
|
+
next.hooks = installRoutingHooks(home, prior.hooks);
|
|
458
|
+
saveState(home, next);
|
|
459
|
+
const commitPusher = join5(home, "agents", "commit-pusher.toml");
|
|
460
|
+
if (existsSync5(commitPusher))
|
|
461
|
+
console.warn(
|
|
462
|
+
`warning: existing unmanaged commit-pusher remains at ${commitPusher}`
|
|
463
|
+
);
|
|
464
|
+
console.log(`Codex kit ${PACKAGE.version} installed under ${home}`);
|
|
465
|
+
}
|
|
466
|
+
function listGlobal(options) {
|
|
467
|
+
const home = options.codexHome;
|
|
468
|
+
const configFile = join5(home, "config.toml");
|
|
469
|
+
const globalAgents = join5(home, "AGENTS.md");
|
|
470
|
+
const state = loadState(home);
|
|
471
|
+
const config = existsSync5(configFile) ? topLevelConfigEntries(readText(configFile)) : /* @__PURE__ */ new Map();
|
|
472
|
+
const value = (key) => {
|
|
473
|
+
const raw = config.get(key)?.value;
|
|
474
|
+
if (!raw) return "not set";
|
|
475
|
+
try {
|
|
476
|
+
return String(JSON.parse(raw));
|
|
477
|
+
} catch {
|
|
478
|
+
return raw;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
console.log(`Codex home: ${home}`);
|
|
482
|
+
console.log(
|
|
483
|
+
`Config: ${configFile}${existsSync5(configFile) ? "" : " (missing)"}`
|
|
484
|
+
);
|
|
485
|
+
console.log(`Orchestrator: ${value("model")}`);
|
|
486
|
+
console.log(`Reasoning effort: ${value("model_reasoning_effort")}`);
|
|
487
|
+
console.log(
|
|
488
|
+
`Plan mode reasoning effort: ${value("plan_mode_reasoning_effort")}`
|
|
489
|
+
);
|
|
490
|
+
console.log(
|
|
491
|
+
`Kit state: ${existsSync5(join5(home, STATE_FILE)) ? state.version : "not installed"}`
|
|
492
|
+
);
|
|
493
|
+
console.log(
|
|
494
|
+
`Global routing: ${existsSync5(globalAgents) && readText(globalAgents).includes(GLOBAL_BEGIN) ? "installed" : "not installed"}`
|
|
495
|
+
);
|
|
496
|
+
console.log(
|
|
497
|
+
`Routing file: ${existsSync5(join5(home, "SUBAGENT_ROUTING.md")) ? join5(home, "SUBAGENT_ROUTING.md") : "missing"}`
|
|
498
|
+
);
|
|
499
|
+
const routingHook = state.hooks;
|
|
500
|
+
console.log(
|
|
501
|
+
`Routing hook: ${routingHook && existsSync5(routingHook.target) && readText(routingHook.target).includes(routingHook.command) ? "installed" : "not installed"}`
|
|
502
|
+
);
|
|
503
|
+
const skillTargets = [
|
|
504
|
+
[
|
|
505
|
+
join5(home, "skills", RECONCILE_SKILL, "SKILL.md"),
|
|
506
|
+
state.files[`skills/${RECONCILE_SKILL}/SKILL.md`]
|
|
507
|
+
],
|
|
508
|
+
[
|
|
509
|
+
join5(home, "skills", RECONCILE_SKILL, "agents", "openai.yaml"),
|
|
510
|
+
state.files[`skills/${RECONCILE_SKILL}/agents/openai.yaml`]
|
|
511
|
+
]
|
|
512
|
+
];
|
|
513
|
+
const skillStatus = skillTargets.every(
|
|
514
|
+
([target, record]) => existsSync5(target) && record && sha256(read(target)) === record.hash
|
|
515
|
+
) ? "installed" : skillTargets.some(([target]) => existsSync5(target)) ? "modified or incomplete" : "missing";
|
|
516
|
+
console.log(`Reconciliation skill: ${skillStatus}`);
|
|
517
|
+
console.log("Custom agents:");
|
|
518
|
+
const agentsDir = join5(home, "agents");
|
|
519
|
+
const agents = existsSync5(agentsDir) ? readdirSync(agentsDir).filter((name) => name.endsWith(".toml")).sort() : [];
|
|
520
|
+
if (!agents.length) {
|
|
521
|
+
console.log(" (none)");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const managed = new Set(
|
|
525
|
+
Object.values(state.files).map((record) => record.target)
|
|
526
|
+
);
|
|
527
|
+
for (const filename of agents) {
|
|
528
|
+
const file = join5(agentsDir, filename);
|
|
529
|
+
const contents = readText(file);
|
|
530
|
+
const field = (key) => new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m").exec(contents)?.[1] ?? "not set";
|
|
531
|
+
console.log(
|
|
532
|
+
` ${field("name")} \u2014 ${field("model")}, ${field("model_reasoning_effort")} (${managed.has(file) ? "managed" : "unmanaged"})`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function uninstallGlobal(options) {
|
|
537
|
+
const home = options.codexHome;
|
|
538
|
+
const statePath = join5(home, STATE_FILE);
|
|
539
|
+
if (!existsSync5(statePath)) {
|
|
540
|
+
console.log(`No installer state at ${statePath}; nothing removed.`);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const state = loadState(home);
|
|
544
|
+
for (const record of Object.values(state.files)) restoreFile(record);
|
|
545
|
+
const globalAgents = state.globalAgents?.target ?? join5(home, "AGENTS.md");
|
|
546
|
+
if (existsSync5(globalAgents)) {
|
|
547
|
+
const original = readText(globalAgents);
|
|
548
|
+
const updated = removeBlock(original, GLOBAL_BEGIN, GLOBAL_END);
|
|
549
|
+
if (updated !== original) {
|
|
550
|
+
backup(globalAgents);
|
|
551
|
+
if (updated) write(globalAgents, updated);
|
|
552
|
+
else rmSync4(globalAgents);
|
|
553
|
+
console.log(`removed managed routing from: ${globalAgents}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (state.config) restoreConfig(state.config);
|
|
557
|
+
if (state.hooks) uninstallRoutingHooks(state.hooks);
|
|
558
|
+
const skillDir = join5(home, "skills", RECONCILE_SKILL);
|
|
559
|
+
const metadataDir = join5(skillDir, "agents");
|
|
560
|
+
if (existsSync5(metadataDir) && statSync(metadataDir).isDirectory() && !readdirSync(metadataDir).length)
|
|
561
|
+
rmSync4(metadataDir, { recursive: true });
|
|
562
|
+
if (existsSync5(skillDir) && statSync(skillDir).isDirectory() && !readdirSync(skillDir).length)
|
|
563
|
+
rmSync4(skillDir, { recursive: true });
|
|
564
|
+
const allowancesDir = join5(home, "codex-kit", "allowances");
|
|
565
|
+
if (existsSync5(allowancesDir))
|
|
566
|
+
rmSync4(allowancesDir, { recursive: true, force: true });
|
|
567
|
+
const kitDir = join5(home, "codex-kit");
|
|
568
|
+
if (existsSync5(kitDir) && !readdirSync(kitDir).length)
|
|
569
|
+
rmSync4(kitDir, { recursive: true });
|
|
570
|
+
rmSync4(statePath);
|
|
571
|
+
console.log(`Codex kit uninstalled from ${home}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/project/commands.ts
|
|
575
|
+
import { existsSync as existsSync6, statSync as statSync2 } from "node:fs";
|
|
576
|
+
import { join as join6 } from "node:path";
|
|
577
|
+
var STATE_FILE2 = ".codex-kit-state.json";
|
|
578
|
+
var PROJECT_BEGIN = "<!-- BEGIN codex-kit:shared-template -->";
|
|
579
|
+
var PROJECT_END = "<!-- END codex-kit:shared-template -->";
|
|
580
|
+
var PROJECT_SCAFFOLD = "# Project-Specific Instructions\n\n<!-- Add repository-specific commands, architecture, and exceptions here. -->\n";
|
|
581
|
+
function loadProjectState(cwd) {
|
|
582
|
+
const file = join6(cwd, STATE_FILE2);
|
|
583
|
+
if (!existsSync6(file)) return { version: 1, template: {} };
|
|
584
|
+
try {
|
|
585
|
+
const state = JSON.parse(readText(file));
|
|
586
|
+
return isRecord(state) && isRecord(state.template) ? state : { version: 1, template: {} };
|
|
587
|
+
} catch {
|
|
588
|
+
throw new Error(`${file} is not valid JSON; move it aside before syncing.`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
var saveProjectState = (cwd, state) => write(join6(cwd, STATE_FILE2), `${JSON.stringify(state, null, 2)}
|
|
592
|
+
`);
|
|
593
|
+
var requireDirectory = (cwd) => {
|
|
594
|
+
if (!existsSync6(cwd) || !statSync2(cwd).isDirectory())
|
|
595
|
+
throw new Error(`Not a directory: ${cwd}`);
|
|
596
|
+
};
|
|
597
|
+
function initializationPrompt() {
|
|
598
|
+
return `Project guidance needs initialization. Copy everything between the markers into Codex.
|
|
23
599
|
|
|
24
600
|
===== BEGIN CODEX INITIALIZATION PROMPT =====
|
|
25
601
|
Explore this repository before changing code. First determine whether it has a
|
|
@@ -33,14 +609,17 @@ If evidence is sufficient, identify the stack, package manager, scripts,
|
|
|
33
609
|
structure, established patterns, testing tools, and generated files. Add concise
|
|
34
610
|
project-specific guidance to AGENTS.md based only on repository evidence,
|
|
35
611
|
including exact verification commands. Then use the global
|
|
36
|
-
$${
|
|
612
|
+
$${RECONCILE_SKILL} skill to merge applicable reusable guidance from
|
|
37
613
|
TEMPLATE_AGENTS.md while preserving AGENTS.md organization and local rules.
|
|
38
614
|
Validate the final instruction changes, mark the template applied only after
|
|
39
615
|
validation succeeds, and confirm codex-kit project status is up to date.
|
|
40
|
-
===== END CODEX INITIALIZATION PROMPT
|
|
616
|
+
===== END CODEX INITIALIZATION PROMPT =====`;
|
|
617
|
+
}
|
|
618
|
+
function reconciliationPrompt() {
|
|
619
|
+
return `Template reference updated. Copy everything between the markers into Codex.
|
|
41
620
|
|
|
42
621
|
===== BEGIN CODEX RECONCILIATION PROMPT =====
|
|
43
|
-
Use the global $${
|
|
622
|
+
Use the global $${RECONCILE_SKILL} skill to reconcile the existing AGENTS.md
|
|
44
623
|
with the refreshed TEMPLATE_AGENTS.md.
|
|
45
624
|
|
|
46
625
|
Inspect TEMPLATE_AGENTS.md, AGENTS.md, .codex-kit-state.json, existing
|
|
@@ -53,9 +632,224 @@ template or introduce managed markers.
|
|
|
53
632
|
Validate the final instruction changes. Mark applied only after reconciliation
|
|
54
633
|
and validation succeed, confirm codex-kit project status is up to date, then
|
|
55
634
|
report any template-worthy generalized promotion.
|
|
56
|
-
===== END CODEX RECONCILIATION PROMPT
|
|
57
|
-
|
|
58
|
-
|
|
635
|
+
===== END CODEX RECONCILIATION PROMPT =====`;
|
|
636
|
+
}
|
|
637
|
+
function syncProject(options) {
|
|
638
|
+
const { cwd } = options;
|
|
639
|
+
requireDirectory(cwd);
|
|
640
|
+
const agentsFile = join6(cwd, "AGENTS.md");
|
|
641
|
+
const stagedTemplate = join6(cwd, "TEMPLATE_AGENTS.md");
|
|
642
|
+
const desired = Buffer.from(readText(TEMPLATE_FILE));
|
|
643
|
+
const sourceHash = sha256(desired);
|
|
644
|
+
const state = loadProjectState(cwd);
|
|
645
|
+
if (existsSync6(stagedTemplate)) {
|
|
646
|
+
const currentHash = sha256(read(stagedTemplate));
|
|
647
|
+
const locallyModified = currentHash !== sourceHash && (!state.template.availableHash || currentHash !== state.template.availableHash);
|
|
648
|
+
if (locallyModified && !options.force) {
|
|
649
|
+
console.warn(
|
|
650
|
+
`preserved locally modified template: ${stagedTemplate} (use --force to replace)`
|
|
651
|
+
);
|
|
652
|
+
console.log(
|
|
653
|
+
`The installed kit has template ${PACKAGE.version}; review the local change before syncing.`
|
|
654
|
+
);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (currentHash === sourceHash) console.log(`unchanged: ${stagedTemplate}`);
|
|
658
|
+
else {
|
|
659
|
+
backup(stagedTemplate);
|
|
660
|
+
write(stagedTemplate, desired);
|
|
661
|
+
console.log(`refreshed template reference: ${stagedTemplate}`);
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
write(stagedTemplate, desired);
|
|
665
|
+
console.log(`created template reference: ${stagedTemplate}`);
|
|
666
|
+
}
|
|
667
|
+
state.version = 1;
|
|
668
|
+
state.template = {
|
|
669
|
+
...state.template,
|
|
670
|
+
availableHash: sourceHash,
|
|
671
|
+
availableVersion: PACKAGE.version
|
|
672
|
+
};
|
|
673
|
+
saveProjectState(cwd, state);
|
|
674
|
+
const createdAgents = !existsSync6(agentsFile);
|
|
675
|
+
if (createdAgents) {
|
|
676
|
+
write(agentsFile, PROJECT_SCAFFOLD);
|
|
677
|
+
console.log(`created project instructions file: ${agentsFile}`);
|
|
678
|
+
} else {
|
|
679
|
+
const agents = readText(agentsFile);
|
|
680
|
+
if (agents.includes(PROJECT_BEGIN) || agents.includes(PROJECT_END)) {
|
|
681
|
+
console.warn(`preserved legacy managed template in: ${agentsFile}`);
|
|
682
|
+
console.warn(
|
|
683
|
+
"Ask Codex to migrate it to semantic template reconciliation before applying updates."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const needsInitialization = readText(agentsFile).trim() === PROJECT_SCAFFOLD.trim();
|
|
688
|
+
console.log(
|
|
689
|
+
needsInitialization ? initializationPrompt() : reconciliationPrompt()
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
function projectStatus(options) {
|
|
693
|
+
const { cwd } = options;
|
|
694
|
+
requireDirectory(cwd);
|
|
695
|
+
const stagedTemplate = join6(cwd, "TEMPLATE_AGENTS.md");
|
|
696
|
+
const state = loadProjectState(cwd);
|
|
697
|
+
const availableHash = state.template.availableHash ?? null;
|
|
698
|
+
const localHash = existsSync6(stagedTemplate) ? sha256(read(stagedTemplate)) : null;
|
|
699
|
+
console.log(`Project: ${cwd}`);
|
|
700
|
+
if (!localHash) {
|
|
701
|
+
console.log("Status: not initialized (run codex-kit project sync)");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (!existsSync6(join6(cwd, "AGENTS.md"))) {
|
|
705
|
+
console.log("Status: AGENTS.md missing (reconcile the template first)");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
console.log(
|
|
709
|
+
`Available: ${state.template.availableVersion ?? "unknown"} (${availableHash ?? "untracked"})`
|
|
710
|
+
);
|
|
711
|
+
console.log(`Applied: ${state.template.appliedHash ?? "never"}`);
|
|
712
|
+
if (sha256(read(TEMPLATE_FILE)) !== availableHash) {
|
|
713
|
+
console.log("Status: kit template update available; run project sync");
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (localHash !== availableHash) {
|
|
717
|
+
console.log("Status: local template changed; review it before syncing");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (state.template.appliedHash !== localHash) {
|
|
721
|
+
console.log("Status: reconciliation required");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
console.log("Status: up to date");
|
|
725
|
+
}
|
|
726
|
+
function markApplied(options) {
|
|
727
|
+
const { cwd } = options;
|
|
728
|
+
requireDirectory(cwd);
|
|
729
|
+
const stagedTemplate = join6(cwd, "TEMPLATE_AGENTS.md");
|
|
730
|
+
const agentsFile = join6(cwd, "AGENTS.md");
|
|
731
|
+
if (!existsSync6(stagedTemplate))
|
|
732
|
+
throw new Error(`Missing ${stagedTemplate}; run project sync first.`);
|
|
733
|
+
if (!existsSync6(agentsFile))
|
|
734
|
+
throw new Error(
|
|
735
|
+
`Missing ${agentsFile}; reconcile the template into AGENTS.md first.`
|
|
736
|
+
);
|
|
737
|
+
const state = loadProjectState(cwd);
|
|
738
|
+
state.version = 1;
|
|
739
|
+
state.template = {
|
|
740
|
+
...state.template,
|
|
741
|
+
appliedHash: sha256(read(stagedTemplate)),
|
|
742
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
743
|
+
};
|
|
744
|
+
saveProjectState(cwd, state);
|
|
745
|
+
console.log(`recorded template reconciliation: ${stagedTemplate}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/version.ts
|
|
749
|
+
import { spawnSync } from "node:child_process";
|
|
750
|
+
function compareVersions(left, right) {
|
|
751
|
+
const parse2 = (value) => {
|
|
752
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value);
|
|
753
|
+
if (!match) throw new Error(`Invalid package version: ${value}`);
|
|
754
|
+
return {
|
|
755
|
+
numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
756
|
+
prerelease: match[4] ?? null
|
|
757
|
+
};
|
|
758
|
+
};
|
|
759
|
+
const a = parse2(left);
|
|
760
|
+
const b = parse2(right);
|
|
761
|
+
for (const [leftNumber, rightNumber] of [
|
|
762
|
+
[a.numbers[0], b.numbers[0]],
|
|
763
|
+
[a.numbers[1], b.numbers[1]],
|
|
764
|
+
[a.numbers[2], b.numbers[2]]
|
|
765
|
+
])
|
|
766
|
+
if (leftNumber !== rightNumber) return Math.sign(leftNumber - rightNumber);
|
|
767
|
+
if (a.prerelease === b.prerelease) return 0;
|
|
768
|
+
if (!a.prerelease) return 1;
|
|
769
|
+
if (!b.prerelease) return -1;
|
|
770
|
+
return Math.sign(
|
|
771
|
+
a.prerelease.localeCompare(b.prerelease, "en", { numeric: true })
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
function checkVersion() {
|
|
775
|
+
let latest = process.env.CODEX_KIT_LATEST_VERSION;
|
|
776
|
+
if (!latest) {
|
|
777
|
+
const result = spawnSync(
|
|
778
|
+
process.platform === "win32" ? "pnpm.cmd" : "pnpm",
|
|
779
|
+
["view", PACKAGE.name, "version", "--json", `--registry=${REGISTRY}`],
|
|
780
|
+
{ encoding: "utf8", timeout: 15e3 }
|
|
781
|
+
);
|
|
782
|
+
if (result.error)
|
|
783
|
+
throw new Error(`Unable to run pnpm: ${result.error.message}`);
|
|
784
|
+
if (result.status !== 0)
|
|
785
|
+
throw new Error(
|
|
786
|
+
`Unable to check ${REGISTRY}: ${result.stderr.trim() || "pnpm view failed"}`
|
|
787
|
+
);
|
|
788
|
+
try {
|
|
789
|
+
const value = JSON.parse(result.stdout);
|
|
790
|
+
latest = Array.isArray(value) && typeof value.at(-1) === "string" ? value.at(-1) : typeof value === "string" ? value : void 0;
|
|
791
|
+
} catch {
|
|
792
|
+
latest = result.stdout.trim();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (!latest) throw new Error("Registry returned no package version.");
|
|
796
|
+
console.log(`Installed: ${PACKAGE.version}`);
|
|
797
|
+
console.log(`Latest: ${latest}`);
|
|
798
|
+
const comparison = compareVersions(PACKAGE.version, latest);
|
|
799
|
+
if (comparison === 0) {
|
|
800
|
+
console.log("codex-kit is up to date.");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (comparison > 0) {
|
|
804
|
+
console.log("This local build is newer than the published package.");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
console.log(
|
|
808
|
+
`Update available. Run:
|
|
809
|
+
pnpm add --global ${PACKAGE.name}@latest
|
|
810
|
+
codex-kit global install`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/cli/options.ts
|
|
815
|
+
import { homedir } from "node:os";
|
|
816
|
+
import { join as join7, resolve as resolve2 } from "node:path";
|
|
817
|
+
function parse(argv) {
|
|
818
|
+
const options = {
|
|
819
|
+
cwd: process.cwd(),
|
|
820
|
+
codexHome: resolve2(process.env.CODEX_HOME || join7(homedir(), ".codex")),
|
|
821
|
+
orchestrator: "gpt-5.6-sol",
|
|
822
|
+
reasoningEffort: "medium",
|
|
823
|
+
planReasoningEffort: "high",
|
|
824
|
+
force: false,
|
|
825
|
+
positionals: []
|
|
826
|
+
};
|
|
827
|
+
for (let index = 0; index < argv.length; index++) {
|
|
828
|
+
const arg = argv[index];
|
|
829
|
+
if (arg === void 0) continue;
|
|
830
|
+
if (arg === "--force") options.force = true;
|
|
831
|
+
else if (arg === "--cwd" || arg === "--codex-home") {
|
|
832
|
+
const value = argv[++index];
|
|
833
|
+
if (!value) throw new Error(`${arg} requires a path.`);
|
|
834
|
+
if (arg === "--cwd") options.cwd = resolve2(value);
|
|
835
|
+
else options.codexHome = resolve2(value);
|
|
836
|
+
} else if (arg === "--orchestrator" || arg === "--model") {
|
|
837
|
+
const value = argv[++index];
|
|
838
|
+
if (!value) throw new Error(`${arg} requires a model.`);
|
|
839
|
+
options.orchestrator = value;
|
|
840
|
+
} else if (arg === "--reasoning-effort" || arg === "--plan-reasoning-effort") {
|
|
841
|
+
const value = argv[++index];
|
|
842
|
+
if (!value) throw new Error(`${arg} requires a value.`);
|
|
843
|
+
if (arg === "--reasoning-effort") options.reasoningEffort = value;
|
|
844
|
+
else options.planReasoningEffort = value;
|
|
845
|
+
} else options.positionals.push(arg);
|
|
846
|
+
}
|
|
847
|
+
return options;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/cli/main.ts
|
|
851
|
+
function help() {
|
|
852
|
+
console.log(`codex-kit ${PACKAGE.version}
|
|
59
853
|
|
|
60
854
|
Usage:
|
|
61
855
|
codex-kit <command> [options]
|
|
@@ -100,4 +894,45 @@ Examples:
|
|
|
100
894
|
codex-kit global install --force
|
|
101
895
|
codex-kit global configure --reasoning-effort medium --plan-reasoning-effort high
|
|
102
896
|
codex-kit project sync --cwd /path/to/project --force
|
|
103
|
-
codex-kit project status --cwd /path/to/project`)
|
|
897
|
+
codex-kit project status --cwd /path/to/project`);
|
|
898
|
+
}
|
|
899
|
+
function main(argv = process.argv.slice(2)) {
|
|
900
|
+
const options = parse(argv);
|
|
901
|
+
if (options.positionals.includes("--version") || options.positionals.includes("-v")) {
|
|
902
|
+
console.log(PACKAGE.version);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (!options.positionals.length || options.positionals.includes("--help") || options.positionals.includes("-h")) {
|
|
906
|
+
help();
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const [scope, action] = options.positionals;
|
|
910
|
+
if (scope === "global" && action === "install") installGlobal(options);
|
|
911
|
+
else if (scope === "global" && action === "configure")
|
|
912
|
+
configureGlobal(options);
|
|
913
|
+
else if (scope === "global" && action === "list") listGlobal(options);
|
|
914
|
+
else if (scope === "global" && action === "uninstall")
|
|
915
|
+
uninstallGlobal(options);
|
|
916
|
+
else if (scope === "project" && (action === "init" || action === "sync"))
|
|
917
|
+
syncProject(options);
|
|
918
|
+
else if (scope === "project" && action === "status") projectStatus(options);
|
|
919
|
+
else if (scope === "project" && action === "mark-applied")
|
|
920
|
+
markApplied(options);
|
|
921
|
+
else if (scope === "version" && action === "check") checkVersion();
|
|
922
|
+
else throw new Error(`Unknown command: ${options.positionals.join(" ")}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/codex-kit.ts
|
|
926
|
+
if (process.argv[1] && realpathSync(resolve3(process.argv[1])) === realpathSync(fileURLToPath2(import.meta.url))) {
|
|
927
|
+
try {
|
|
928
|
+
main();
|
|
929
|
+
} catch (error) {
|
|
930
|
+
console.error(
|
|
931
|
+
`error: ${error instanceof Error ? error.message : String(error)}`
|
|
932
|
+
);
|
|
933
|
+
process.exitCode = 1;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
export {
|
|
937
|
+
main
|
|
938
|
+
};
|
package/bin/routing-hook.js
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as r,readFileSync as n}from"node:fs";import{dirname as s,join as a,resolve as u}from"node:path";import{fileURLToPath as d}from"node:url";var c=u(s(d(import.meta.url)),".."),o=a(c,"SUBAGENT_ROUTING.md"),e=JSON.parse(n(0,"utf8"));function g(t,i){return JSON.stringify({hookSpecificOutput:{hookEventName:t,additionalContext:i}})}if(e.hook_event_name==="UserPromptSubmit"&&r(o)){let t=n(o,"utf8").trim();process.stdout.write(`Codex-kit routing policy for this turn:
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
// src/routing-hook.ts
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
var home = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
var routingFile = join(home, "SUBAGENT_ROUTING.md");
|
|
9
|
+
var input = JSON.parse(readFileSync(0, "utf8"));
|
|
10
|
+
function hookContext(event, context) {
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
hookSpecificOutput: {
|
|
13
|
+
hookEventName: event,
|
|
14
|
+
additionalContext: context
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (input.hook_event_name === "UserPromptSubmit" && existsSync(routingFile)) {
|
|
19
|
+
const routing = readFileSync(routingFile, "utf8").trim();
|
|
20
|
+
process.stdout.write(
|
|
21
|
+
`Codex-kit routing policy for this turn:
|
|
5
22
|
|
|
6
|
-
|
|
23
|
+
${routing}
|
|
24
|
+
|
|
25
|
+
Classify the task using this policy before acting. When it requires delegation, spawn the exact named role before doing that role's work. Agent definitions, not this policy, determine each role's model and reasoning effort.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (input.hook_event_name === "SubagentStart") {
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
hookContext(
|
|
31
|
+
"SubagentStart",
|
|
32
|
+
`You are the delegated ${input.agent_type ?? "worker"}. Follow the assigned scope, perform the role's work directly without further delegation, validate it, and return concise evidence.`
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|