codex-token-saver 1.0.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.
package/src/core.js ADDED
@@ -0,0 +1,881 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { execFileSync } from "node:child_process";
6
+ import { ensureDir, fileExists, writeFileAtomic, writeFileForce, writeFileIfMissing } from "./core/utils/fsSafe.js";
7
+ import { createLogger } from "./core/utils/logger.js";
8
+ import {
9
+ DEFAULT_MAX_FILE_SIZE_KB,
10
+ DEPENDENCY_FILES,
11
+ GENERATOR_VERSION,
12
+ GLOBAL_END,
13
+ GLOBAL_START,
14
+ HEAVY_DIRS,
15
+ OLD_END,
16
+ OLD_START,
17
+ PROJECT_END,
18
+ PROJECT_START,
19
+ RELEVANT_EXTENSIONS,
20
+ RELEVANT_FILE_NAMES,
21
+ RELEVANT_CONTEXT_FILE,
22
+ SCHEMA_VERSION,
23
+ SECRET_FILE_NAMES,
24
+ SECRET_PREFIXES,
25
+ SECRET_SUFFIXES,
26
+ contextFiles,
27
+ globalManagedBlock,
28
+ projectManagedBlock,
29
+ requiredFiles,
30
+ templates
31
+ } from "./core/utils/config.js";
32
+ import { parseFile } from "./core/parsers/index.js";
33
+ import { queryTerms, scoreFileForQuery } from "./core/scoring/queryScorer.js";
34
+
35
+ export {
36
+ GLOBAL_END,
37
+ GLOBAL_START,
38
+ OLD_END,
39
+ OLD_START,
40
+ PROJECT_END,
41
+ PROJECT_START,
42
+ contextFiles,
43
+ globalManagedBlock,
44
+ projectManagedBlock,
45
+ requiredFiles
46
+ } from "./core/utils/config.js";
47
+ export { createLogger, ensureDir, fileExists, writeFileAtomic, writeFileForce, writeFileIfMissing };
48
+
49
+ export function getGlobalAgentsPath() {
50
+ return path.join(os.homedir(), ".codex", "AGENTS.md");
51
+ }
52
+
53
+ function newlineOf(content) {
54
+ return content.includes("\r\n") ? "\r\n" : "\n";
55
+ }
56
+
57
+ function finish(content, newline) {
58
+ return content.replace(/[\r\n]*$/, "") + newline;
59
+ }
60
+
61
+ export function upsertManagedBlock(existingContent, startMarker, endMarker, newBlock) {
62
+ const hasStart = existingContent.includes(startMarker);
63
+ const hasEnd = existingContent.includes(endMarker);
64
+ const newline = newlineOf(existingContent || newBlock);
65
+ const block = newBlock.replace(/\n/g, newline);
66
+
67
+ if (hasStart !== hasEnd) return { ok: false, error: "Managed block has only one marker." };
68
+
69
+ if (hasStart) {
70
+ const start = existingContent.indexOf(startMarker);
71
+ const end = existingContent.indexOf(endMarker);
72
+ if (end < start) return { ok: false, error: "Managed block markers are out of order." };
73
+ const next = existingContent.slice(0, start) + block + existingContent.slice(end + endMarker.length);
74
+ return { ok: true, content: finish(next, newline), action: "updated" };
75
+ }
76
+
77
+ const base = existingContent.trimEnd();
78
+ const next = base ? `${base}${newline}${newline}${block}` : block;
79
+ return { ok: true, content: finish(next, newline), action: existingContent ? "appended" : "created" };
80
+ }
81
+
82
+ function migrateOldProjectMarkers(content) {
83
+ return content.replace(OLD_START, PROJECT_START).replace(OLD_END, PROJECT_END);
84
+ }
85
+
86
+ function filesFor(root) {
87
+ return {
88
+ [path.join(root, ".codex", "AGENTS.md")]: projectManagedBlock,
89
+ [path.join(root, ".codex", "templates", "project_context.template.md")]: templates["project_context.md"],
90
+ [path.join(root, ".codex", "templates", "architecture.template.md")]: templates["architecture.md"],
91
+ [path.join(root, ".codex", "templates", "task.template.md")]: templates["task.md"],
92
+ [path.join(root, ".codex", "templates", "decision_log.template.md")]: templates["decision_log.md"],
93
+ [path.join(root, "project_context.md")]: templates["project_context.md"],
94
+ [path.join(root, "architecture.md")]: templates["architecture.md"],
95
+ [path.join(root, "task.md")]: templates["task.md"],
96
+ [path.join(root, "decision_log.md")]: templates["decision_log.md"]
97
+ };
98
+ }
99
+
100
+ export function runNew(projectName, options = {}) {
101
+ const started = Date.now();
102
+ if (!projectName) throw new Error("Project name is required");
103
+ const root = path.resolve(projectName);
104
+ ensureDir(root);
105
+ let created = 0;
106
+ let updated = 0;
107
+ for (const [file, content] of Object.entries(filesFor(root))) {
108
+ if (options.force) {
109
+ writeFileForce(file, content);
110
+ updated += 1;
111
+ } else if (writeFileIfMissing(file, content)) {
112
+ created += 1;
113
+ }
114
+ }
115
+ return { ok: true, action: "new", root, created, updated, durationMs: Date.now() - started, warnings: [] };
116
+ }
117
+
118
+ export function runSync(root = process.cwd()) {
119
+ const started = Date.now();
120
+ let created = 0;
121
+ for (const [file, content] of Object.entries(filesFor(root))) {
122
+ if (writeFileIfMissing(file, content)) created += 1;
123
+ }
124
+ return { ok: true, action: "sync", root, created, durationMs: Date.now() - started, warnings: [] };
125
+ }
126
+
127
+ export function runProjectDoctor(root = process.cwd()) {
128
+ const results = requiredFiles.map((file) => {
129
+ const found = fileExists(path.join(root, file));
130
+ return { file, found, line: `${found ? "OK" : "MISSING"} ${file} ${found ? "found" : "missing"}` };
131
+ });
132
+ return { ok: results.every((result) => result.found), results };
133
+ }
134
+
135
+ export function runDoctor(root = process.cwd()) {
136
+ return runProjectDoctor(root);
137
+ }
138
+
139
+ export function runProjectUpgrade(root = process.cwd()) {
140
+ const started = Date.now();
141
+ const file = path.join(root, ".codex", "AGENTS.md");
142
+ const existing = fileExists(file) ? migrateOldProjectMarkers(fs.readFileSync(file, "utf8")) : "";
143
+ const result = upsertManagedBlock(existing, PROJECT_START, PROJECT_END, projectManagedBlock);
144
+ if (!result.ok) throw new Error(result.error);
145
+ writeFileForce(file, result.content);
146
+ return { ok: true, command: "project-upgrade", action: result.action, file, durationMs: Date.now() - started, warnings: [] };
147
+ }
148
+
149
+ export function runUpgrade(root = process.cwd()) {
150
+ return runProjectUpgrade(root);
151
+ }
152
+
153
+ export function runGlobalSetup() {
154
+ const started = Date.now();
155
+ const file = getGlobalAgentsPath();
156
+ const existing = fileExists(file) ? fs.readFileSync(file, "utf8") : "";
157
+ const result = upsertManagedBlock(existing, GLOBAL_START, GLOBAL_END, globalManagedBlock);
158
+ if (!result.ok) throw new Error(result.error);
159
+ writeFileForce(file, result.content);
160
+ return { ok: true, command: "global", action: result.action, file, durationMs: Date.now() - started, warnings: [] };
161
+ }
162
+
163
+ export function runGlobalDoctor() {
164
+ const file = getGlobalAgentsPath();
165
+ const exists = fileExists(file);
166
+ const content = exists ? fs.readFileSync(file, "utf8") : "";
167
+ const hasBlock = content.includes(GLOBAL_START) && content.includes(GLOBAL_END);
168
+ const results = [
169
+ { found: exists, line: `${exists ? "OK" : "MISSING"} ~/.codex/AGENTS.md ${exists ? "found" : "missing"}` },
170
+ { found: hasBlock, line: `${hasBlock ? "OK" : "MISSING"} global managed block ${hasBlock ? "found" : "missing"}` }
171
+ ];
172
+ return { ok: results.every((result) => result.found), results };
173
+ }
174
+
175
+ function isSecretFile(name) {
176
+ return SECRET_FILE_NAMES.has(name)
177
+ || SECRET_PREFIXES.some((prefix) => name.startsWith(prefix))
178
+ || SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix));
179
+ }
180
+
181
+ function toDisplayPath(file) {
182
+ return file.split(path.sep).join("/");
183
+ }
184
+
185
+ function topLevelOf(file) {
186
+ const [first] = file.split(path.sep);
187
+ return first || ".";
188
+ }
189
+
190
+ function languageFor(file) {
191
+ const ext = path.extname(file).toLowerCase();
192
+ const name = path.basename(file).toLowerCase();
193
+ if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) return "JavaScript";
194
+ if ([".ts", ".tsx"].includes(ext)) return "TypeScript";
195
+ if (ext === ".py") return "Python";
196
+ if (ext === ".rs") return "Rust";
197
+ if (ext === ".go") return "Go";
198
+ if (ext === ".java") return "Java";
199
+ if (ext === ".cs") return "C#";
200
+ if ([".cpp", ".cc", ".cxx", ".c", ".h", ".hpp"].includes(ext)) return "C/C++";
201
+ if ([".json", ".jsonc"].includes(ext)) return "JSON";
202
+ if ([".md", ".mdx"].includes(ext)) return "Markdown";
203
+ if ([".yml", ".yaml"].includes(ext)) return "YAML";
204
+ if ([".html", ".css", ".scss", ".sass"].includes(ext)) return ext.slice(1).toUpperCase();
205
+ if (name === "dockerfile") return "Docker";
206
+ return ext ? ext.slice(1) : "Other";
207
+ }
208
+
209
+ function isRelevantFile(file) {
210
+ const ext = path.extname(file).toLowerCase();
211
+ const name = path.basename(file).toLowerCase();
212
+ return RELEVANT_EXTENSIONS.has(ext) || RELEVANT_FILE_NAMES.has(name);
213
+ }
214
+
215
+ export function isIgnoredPath(relativePath) {
216
+ const parts = relativePath.split(/[\\/]+/);
217
+ return parts.some((part) => HEAVY_DIRS.has(part) || isSecretFile(part)) || isIgnoredContextPath(parts);
218
+ }
219
+
220
+ function isIgnoredContextPath(relativeParts) {
221
+ return relativeParts.length >= 2 && relativeParts[0] === ".codex" && relativeParts[1] === "context";
222
+ }
223
+
224
+ export function isLikelyBinary(buffer) {
225
+ if (buffer.length === 0) return false;
226
+ let control = 0;
227
+ for (let i = 0; i < buffer.length; i += 1) {
228
+ const value = buffer[i];
229
+ if (value === 0) return true;
230
+ if (value < 7 || (value > 14 && value < 32)) control += 1;
231
+ }
232
+ return control / buffer.length > 0.2;
233
+ }
234
+
235
+ function isBinaryFile(file) {
236
+ const buffer = Buffer.alloc(8000);
237
+ const fd = fs.openSync(file, "r");
238
+ try {
239
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
240
+ return isLikelyBinary(buffer.subarray(0, bytes));
241
+ } finally {
242
+ fs.closeSync(fd);
243
+ }
244
+ }
245
+
246
+ function walk(root, dir, options, state, files) {
247
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
248
+ if (options.limit && files.length >= options.limit) return;
249
+ const full = path.join(dir, entry.name);
250
+ const relativeParts = path.relative(root, full).split(path.sep);
251
+ if (entry.isDirectory()) {
252
+ if (HEAVY_DIRS.has(entry.name) || isIgnoredContextPath(relativeParts)) {
253
+ state.ignored += 1;
254
+ continue;
255
+ }
256
+ walk(root, full, options, state, files);
257
+ continue;
258
+ }
259
+ if (!entry.isFile()) continue;
260
+ if (isSecretFile(entry.name)) {
261
+ state.ignored += 1;
262
+ continue;
263
+ }
264
+ const stat = fs.statSync(full);
265
+ if (stat.size > options.maxFileSizeBytes) {
266
+ state.skippedLarge += 1;
267
+ continue;
268
+ }
269
+ if (isBinaryFile(full)) {
270
+ state.ignored += 1;
271
+ continue;
272
+ }
273
+ files.push({ full, relative: path.relative(root, full), size: stat.size, mtimeMs: stat.mtimeMs, ext: path.extname(entry.name).toLowerCase() || "(none)" });
274
+ }
275
+ }
276
+
277
+ export function getWatchDirs(root = process.cwd()) {
278
+ const dirs = [];
279
+ function visit(dir) {
280
+ dirs.push(dir);
281
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
282
+ if (!entry.isDirectory()) continue;
283
+ const full = path.join(dir, entry.name);
284
+ const relativeParts = path.relative(root, full).split(path.sep);
285
+ if (HEAVY_DIRS.has(entry.name) || isIgnoredContextPath(relativeParts)) continue;
286
+ visit(full);
287
+ }
288
+ }
289
+ visit(root);
290
+ return dirs;
291
+ }
292
+
293
+ export function isIgnoredWorkspacePath(root, file) {
294
+ const parts = path.relative(root, file).split(path.sep);
295
+ return parts.some((part) => HEAVY_DIRS.has(part) || isSecretFile(part)) || isIgnoredContextPath(parts);
296
+ }
297
+
298
+ function scanProject(root, options = {}) {
299
+ const state = { ignored: 0, skippedLarge: 0 };
300
+ const files = [];
301
+ const maxFileSizeKb = options.maxFileSizeKb ?? DEFAULT_MAX_FILE_SIZE_KB;
302
+ walk(root, root, { maxFileSizeBytes: maxFileSizeKb * 1024, limit: options.limit }, state, files);
303
+ return { root, files, ignored: state.ignored, skippedLarge: state.skippedLarge, maxFileSizeKb };
304
+ }
305
+
306
+ export function collectFiles(root = process.cwd(), options = {}) {
307
+ return scanProject(root, options).files;
308
+ }
309
+
310
+ export function countEligibleFiles(root = process.cwd(), options = {}) {
311
+ return scanProject(root, { ...options, limit: options.limit ?? 1000 }).files.length;
312
+ }
313
+
314
+ function readText(file) {
315
+ return fs.readFileSync(file, "utf8");
316
+ }
317
+
318
+ function hashText(content) {
319
+ return crypto.createHash("sha256").update(content).digest("hex");
320
+ }
321
+
322
+ function writeFileIfChanged(file, content) {
323
+ if (fileExists(file) && hashText(fs.readFileSync(file)) === hashText(content)) return false;
324
+ writeFileAtomic(file, content);
325
+ return true;
326
+ }
327
+
328
+ function topLanguages(files) {
329
+ const counts = new Map();
330
+ for (const file of files) counts.set(file.language, (counts.get(file.language) || 0) + 1);
331
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]);
332
+ }
333
+
334
+ export function extractImports(content, ext) {
335
+ const imports = [];
336
+ const pattern = /^\s*import\s+.*?\s+from\s+["'`]([^"'`]+)|^\s*import\s+["'`]([^"'`]+)|require\(\s*["'`]([^"'`]+)["'`]\s*\)|^\s*from\s+([A-Za-z0-9_.$-]+)\s+import|^\s*import\s+([A-Za-z0-9_.$-]+)/;
337
+ if (![".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".go", ".java"].includes(ext)) return imports;
338
+ for (const line of content.split(/\r?\n/)) {
339
+ const match = line.match(pattern);
340
+ const name = match?.[1] || match?.[2] || match?.[3] || match?.[4] || match?.[5];
341
+ if (name) imports.push(name);
342
+ }
343
+ return [...new Set(imports)].slice(0, 50);
344
+ }
345
+
346
+ export function extractExports(content, ext) {
347
+ const exports = [];
348
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
349
+ for (const line of content.split(/\r?\n/)) {
350
+ const match = line.match(/^\s*export\s+(?:default\s+)?(?:(?:async\s+)?(?:function|class|const|let|var|type|interface)\s+)?([A-Za-z_$][\w$]*)?/);
351
+ if (match) exports.push(match[1] || "default");
352
+ }
353
+ }
354
+ return [...new Set(exports)].slice(0, 50);
355
+ }
356
+
357
+ export function extractSymbols(content, ext) {
358
+ const symbols = [];
359
+ const lines = content.split(/\r?\n/);
360
+ for (let i = 0; i < lines.length; i += 1) {
361
+ const line = lines[i];
362
+ let match;
363
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
364
+ match = line.match(/^\s*(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s+([A-Z][A-Za-z0-9_$]*|[A-Za-z_$][\w$]*)|^\s*(?:export\s+)?const\s+([A-Z][A-Za-z0-9_$]*)\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/);
365
+ } else if (ext === ".py") {
366
+ match = line.match(/^\s*(?:def|class)\s+([A-Za-z_]\w*)/);
367
+ } else if (ext === ".go") {
368
+ match = line.match(/^\s*(?:func|type)\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)|^\s*type\s+([A-Za-z_]\w*)\s+struct/);
369
+ } else if (ext === ".java") {
370
+ match = line.match(/^\s*(?:public\s+)?(?:class|interface)\s+([A-Za-z_]\w*)|^\s*public\s+(?:static\s+)?[\w<>\[\]]+\s+([A-Za-z_]\w*)\s*\(/);
371
+ } else if ([".md", ".mdx"].includes(ext)) {
372
+ match = line.match(/^(#{1,6})\s+(.+)/);
373
+ if (match) symbols.push({ name: match[2].trim(), line: i + 1 });
374
+ continue;
375
+ }
376
+ const name = match?.[1] || match?.[2];
377
+ if (name) symbols.push({ name, line: i + 1 });
378
+ }
379
+ return symbols.slice(0, 80);
380
+ }
381
+
382
+ export function extractRouteHints(content, ext, relativePath = "") {
383
+ const routeHints = [];
384
+ const lines = content.split(/\r?\n/);
385
+ const routePattern = /<(?:Route|Link|NavLink)\b[^>]*(?:path|to)=["'`]([^"'`]+)|\bpath\s*:\s*["'`]([^"'`]+)|(?:app|router)\.(?:get|post|put|patch|delete|use)\s*\(\s*["'`]([^"'`]+)|@(?:app|router)\.(?:get|post|put|patch|delete|route)\s*\(\s*["'`]([^"'`]+)|@app\.route\s*\(\s*["'`]([^"'`]+)/;
386
+ for (let i = 0; i < lines.length; i += 1) {
387
+ const match = lines[i].match(routePattern);
388
+ const route = match?.[1] || match?.[2] || match?.[3] || match?.[4] || match?.[5];
389
+ if (route) routeHints.push({ route, line: i + 1 });
390
+ }
391
+ const parts = relativePath.split(path.sep);
392
+ if ((parts[0] === "pages" || parts[0] === "app" || parts.includes("pages") || parts.includes("app")) && [".js", ".jsx", ".ts", ".tsx", ".mdx"].includes(ext)) {
393
+ routeHints.push({ route: nextRouteFromPath(relativePath), line: 1 });
394
+ }
395
+ return routeHints.slice(0, 50);
396
+ }
397
+
398
+ export function extractFileMetadata(filePath, content) {
399
+ const ext = path.extname(filePath).toLowerCase();
400
+ const parsed = parseFile(content, { relativePath: filePath, ext, fileName: path.basename(filePath) });
401
+ const testHints = [];
402
+ const lines = content.split(/\r?\n/);
403
+ for (let i = 0; i < lines.length; i += 1) {
404
+ if (/\b(describe|it|test)\s*\(|^\s*def\s+test_/.test(lines[i])) testHints.push({ line: i + 1 });
405
+ }
406
+ return {
407
+ imports: parsed.imports.length ? parsed.imports : extractImports(content, ext),
408
+ exports: parsed.exports.length ? parsed.exports : extractExports(content, ext),
409
+ symbols: parsed.symbols.length ? parsed.symbols : extractSymbols(content, ext),
410
+ headings: parsed.headings,
411
+ dependencies: parsed.dependencies,
412
+ routes: parsed.routes,
413
+ routeHints: parsed.routes.length ? parsed.routes : extractRouteHints(content, ext, filePath),
414
+ testHints: testHints.slice(0, 50)
415
+ };
416
+ }
417
+
418
+ function nextRouteFromPath(file) {
419
+ const parts = file.split(path.sep);
420
+ const start = parts.indexOf("pages") >= 0 ? parts.indexOf("pages") : parts.indexOf("app");
421
+ if (start < 0) return "";
422
+ const routeParts = parts.slice(start + 1);
423
+ const last = routeParts.pop() || "";
424
+ const cleanLast = last.replace(/\.[^.]+$/, "");
425
+ if (!["page", "route", "index"].includes(cleanLast)) routeParts.push(cleanLast);
426
+ const route = routeParts.filter((part) => !part.startsWith("_") && !part.startsWith("(")).map((part) => part.replace(/\[(.+?)\]/g, ":$1")).join("/");
427
+ return `/${route}`;
428
+ }
429
+
430
+ function enrichFiles(files) {
431
+ return files.map((file) => {
432
+ const content = readText(file.full);
433
+ return { ...file, hash: hashText(content), language: languageFor(file.relative), ...extractFileMetadata(file.relative, content) };
434
+ });
435
+ }
436
+
437
+ function recentChangedPaths(root) {
438
+ try {
439
+ const output = execFileSync("git", ["status", "--short"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
440
+ if (!output) return new Set();
441
+ return new Set(output.split(/\r?\n/).map((line) => line.slice(3).trim().replace(/^"|"$/g, "")).filter(Boolean));
442
+ } catch {
443
+ return new Set();
444
+ }
445
+ }
446
+
447
+ function scoreFile(file, recentPaths) {
448
+ let score = 0;
449
+ const reasons = [];
450
+ const display = toDisplayPath(file.relative);
451
+ const base = path.basename(file.relative).toLowerCase();
452
+ const add = (points, reason) => {
453
+ score += points;
454
+ reasons.push(reason);
455
+ };
456
+ if (/^(index|main|app|server)\.[^.]+$/i.test(base)) add(30, "entrypoint file");
457
+ if (/^(package|tsconfig)\.json$/i.test(base) || /^(vite|next)\.config\./i.test(base)) add(25, "package/config file");
458
+ if (/routes?|router|app\/api|(^|\/)pages(\/|$)/i.test(display)) add(20, "route file");
459
+ if (file.exports.length) add(15, "has exports");
460
+ if (file.symbols.length || file.headings.length) add(15, "has symbols");
461
+ if (file.imports.length) add(10, "has imports");
462
+ if (/(^|\/)(__tests__|tests?|spec)(\/|$)|(\.test|\.spec)\.[^.]+$/i.test(display)) add(10, "test file");
463
+ if (recentPaths.has(display) || recentPaths.has(file.relative)) add(10, "recently changed");
464
+ if (/(^|\/)(dist|build|out|coverage|generated)(\/|$)|(\.min\.)/i.test(display)) add(-20, "generated/build-like file");
465
+ return { importanceScore: score, importanceReasons: reasons };
466
+ }
467
+
468
+ function groupedFilesMd(files) {
469
+ const groups = new Map();
470
+ for (const file of files.filter((item) => isRelevantFile(item.relative)).sort((a, b) => b.importanceScore - a.importanceScore || a.relative.localeCompare(b.relative))) {
471
+ const top = topLevelOf(file.relative);
472
+ if (!groups.has(top)) groups.set(top, []);
473
+ groups.get(top).push(file);
474
+ }
475
+ const rows = ["# Files", ""];
476
+ for (const [top, group] of [...groups.entries()].sort()) {
477
+ rows.push(`## ${toDisplayPath(top)}`, "");
478
+ for (const file of group.slice(0, 40)) rows.push(`- ${toDisplayPath(file.relative)} (${file.language}, score ${file.importanceScore})`);
479
+ rows.push("");
480
+ }
481
+ return rows.join("\n").trimEnd() + "\n";
482
+ }
483
+
484
+ function symbolsMd(files) {
485
+ const rows = ["# Symbols", ""];
486
+ for (const file of files.filter((item) => item.symbols.length || item.exports.length || item.headings.length).slice(0, 250)) {
487
+ rows.push(`## ${toDisplayPath(file.relative)}`);
488
+ for (const item of file.symbols.slice(0, 30)) rows.push(`- ${item.type || "symbol"} ${item.name}`);
489
+ for (const item of file.headings.slice(0, 20)) rows.push(`- heading ${item.text}`);
490
+ if (file.exports.length) rows.push(`- Exports: ${file.exports.join(", ")}`);
491
+ rows.push("");
492
+ }
493
+ if (rows.length === 2) rows.push("None found.");
494
+ return rows.join("\n").trimEnd() + "\n";
495
+ }
496
+
497
+ function routeHintsMd(files) {
498
+ const api = [];
499
+ const ui = [];
500
+ for (const file of files) {
501
+ for (const hint of file.routeHints) {
502
+ const routePath = hint.path || hint.route;
503
+ const row = `- ${hint.method ? `${hint.method} ` : ""}${routePath} - ${toDisplayPath(file.relative)}`;
504
+ if (hint.kind === "ui" || !hint.method) ui.push(row);
505
+ else api.push(row);
506
+ }
507
+ }
508
+ return `# Routes\n\n## API Routes\n\n${api.length ? api.join("\n") : "None found."}\n\n## UI Routes\n\n${ui.length ? ui.join("\n") : "None found."}\n`;
509
+ }
510
+
511
+ function collectDependencies(root, files) {
512
+ const rows = ["# Dependencies", ""];
513
+ const names = new Set(files.map((file) => path.basename(file.relative).toLowerCase()));
514
+ const parsedPackage = files.flatMap((file) => file.dependencies || []).find((item) => path.basename(item.file || "") === "package.json");
515
+ const packageFile = path.join(root, "package.json");
516
+ if (parsedPackage) {
517
+ rows.push("## package.json", "");
518
+ if (parsedPackage.packageName) rows.push(`- name: ${parsedPackage.packageName}`);
519
+ if (parsedPackage.scripts.length) rows.push(`- scripts: ${parsedPackage.scripts.join(", ")}`);
520
+ if (parsedPackage.dependencies.length) rows.push(`- dependencies: ${parsedPackage.dependencies.join(", ")}`);
521
+ if (parsedPackage.devDependencies.length) rows.push(`- devDependencies: ${parsedPackage.devDependencies.join(", ")}`);
522
+ rows.push("");
523
+ } else if (names.has("package.json") && fileExists(packageFile)) {
524
+ try {
525
+ const pkg = JSON.parse(readText(packageFile));
526
+ const scripts = Object.keys(pkg.scripts || {});
527
+ const dependencies = Object.keys(pkg.dependencies || {});
528
+ const devDependencies = Object.keys(pkg.devDependencies || {});
529
+ rows.push("## package.json", "");
530
+ if (scripts.length) rows.push(`- scripts: ${scripts.join(", ")}`);
531
+ if (dependencies.length) rows.push(`- dependencies: ${dependencies.join(", ")}`);
532
+ if (devDependencies.length) rows.push(`- devDependencies: ${devDependencies.join(", ")}`);
533
+ rows.push("");
534
+ } catch {
535
+ rows.push("## package.json", "", "- Could not parse package.json", "");
536
+ }
537
+ }
538
+ for (const depFile of DEPENDENCY_FILES.filter((file) => file !== "package.json")) {
539
+ if (names.has(depFile.toLowerCase())) rows.push(`- ${depFile}`);
540
+ }
541
+ if (rows.length === 2) rows.push("None found.");
542
+ return rows.join("\n").trimEnd() + "\n";
543
+ }
544
+
545
+ export function detectDependencies(root) {
546
+ const found = [];
547
+ for (const name of DEPENDENCY_FILES) {
548
+ const file = path.join(root, name);
549
+ if (!fileExists(file)) continue;
550
+ if (name === "package.json") {
551
+ try {
552
+ const pkg = JSON.parse(readText(file));
553
+ found.push({
554
+ file: name,
555
+ scripts: Object.keys(pkg.scripts || {}),
556
+ dependencies: Object.keys(pkg.dependencies || {}),
557
+ devDependencies: Object.keys(pkg.devDependencies || {})
558
+ });
559
+ } catch {
560
+ found.push({ file: name, error: "Could not parse package.json" });
561
+ }
562
+ } else {
563
+ found.push({ file: name });
564
+ }
565
+ }
566
+ return found;
567
+ }
568
+
569
+ function entrypointCandidates(files) {
570
+ const names = ["src/index.ts", "src/index.js", "src/main.ts", "src/main.js", "src/App.tsx", "src/App.jsx", "app/page.tsx", "pages/index.tsx", "index.js", "server.js", "main.py", "app.py"];
571
+ const lookup = new Set(files.map((file) => toDisplayPath(file.relative)));
572
+ return names.filter((name) => lookup.has(name));
573
+ }
574
+
575
+ function testCandidates(files) {
576
+ return files.filter((file) => /(^|[\\/.])(__tests__|tests?|spec)[\\/.]|(\.test|\.spec)\.[^.]+$/.test(toDisplayPath(file.relative))).map((file) => toDisplayPath(file.relative)).slice(0, 30);
577
+ }
578
+
579
+ function likelyProjectType(files) {
580
+ const names = new Set(files.map((file) => toDisplayPath(file.relative)));
581
+ if (names.has("next.config.js") || names.has("next.config.mjs") || [...names].some((name) => name.startsWith("app/") || name.startsWith("pages/"))) return "Next.js or React web app";
582
+ if (names.has("package.json")) return "Node.js project";
583
+ if (names.has("pyproject.toml") || names.has("requirements.txt")) return "Python project";
584
+ if (names.has("Cargo.toml")) return "Rust project";
585
+ if (names.has("go.mod")) return "Go project";
586
+ return "Unknown";
587
+ }
588
+
589
+ function importantDirs(files) {
590
+ const counts = new Map();
591
+ for (const file of files) {
592
+ const top = topLevelOf(file.relative);
593
+ counts.set(top, (counts.get(top) || 0) + 1);
594
+ }
595
+ return [...counts.entries()].filter(([dir]) => dir !== ".").sort((a, b) => b[1] - a[1]).slice(0, 12).map(([dir, count]) => `${toDisplayPath(dir)} (${count})`);
596
+ }
597
+
598
+ function buildRunScripts(root) {
599
+ const packageFile = path.join(root, "package.json");
600
+ if (fileExists(packageFile)) {
601
+ try {
602
+ const pkg = JSON.parse(readText(packageFile));
603
+ return Object.keys(pkg.scripts || {}).filter((name) => /^(dev|start|build|test|lint|check)$/.test(name)).map((name) => `npm run ${name}`);
604
+ } catch {
605
+ return [];
606
+ }
607
+ }
608
+ return [];
609
+ }
610
+
611
+ function mdList(title, rows, empty = "None found.") {
612
+ return `# ${title}\n\n${rows.length ? rows.join("\n") : empty}\n`;
613
+ }
614
+
615
+ function gitStatus(root) {
616
+ try {
617
+ const output = execFileSync("git", ["status", "--short"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
618
+ return output || "No changes.";
619
+ } catch {
620
+ return "Git status unavailable.";
621
+ }
622
+ }
623
+
624
+ function contextDirFor(root) {
625
+ return path.join(root, ".codex", "context");
626
+ }
627
+
628
+ function indexPathFor(root) {
629
+ return path.join(contextDirFor(root), "index.json");
630
+ }
631
+
632
+ function relevantPathFor(root) {
633
+ return path.join(contextDirFor(root), RELEVANT_CONTEXT_FILE);
634
+ }
635
+
636
+ function readIndex(root) {
637
+ const file = indexPathFor(root);
638
+ if (!fileExists(file)) throw new Error("Context index not found. Run `codex-context-init index` first.");
639
+ try {
640
+ return JSON.parse(readText(file));
641
+ } catch {
642
+ throw new Error("Context index not found. Run `codex-context-init index` first.");
643
+ }
644
+ }
645
+
646
+ export function generateSummary(index) {
647
+ const files = index._files || [];
648
+ const languageRows = Object.entries(index.languageCounts || {}).slice(0, 8).map(([language, count]) => `- ${language}: ${count}`);
649
+ const entries = entrypointCandidates(files);
650
+ const tests = testCandidates(files);
651
+ const scripts = buildRunScripts(index._root);
652
+ const dirs = importantDirs(files);
653
+ const topImportant = [...files].sort((a, b) => b.importanceScore - a.importanceScore || a.relative.localeCompare(b.relative)).slice(0, 10).map((file) => `- ${toDisplayPath(file.relative)} (${file.importanceScore})`);
654
+ return `# Context Summary\n\n- Root: ${index.root}\n- Likely project type: ${likelyProjectType(files)}\n- Files indexed: ${index.fileCount}\n- Route count: ${index.routeCount}\n- Symbol count: ${index.symbolCount}\n- Dependency file count: ${index.dependencyFileCount}\n- Recent changed files count: ${index.recentChangesCount}\n\n## Primary Languages\n\n${languageRows.length ? languageRows.join("\n") : "None found."}\n\n## Important Directories\n\n${dirs.length ? dirs.map((dir) => `- ${dir}`).join("\n") : "None found."}\n\n## Top Important Files\n\n${topImportant.length ? topImportant.join("\n") : "None found."}\n\n## Entrypoint Candidates\n\n${entries.length ? entries.map((entry) => `- ${entry}`).join("\n") : "None found."}\n\n## Test Candidates\n\n${tests.length ? tests.map((test) => `- ${test}`).join("\n") : "None found."}\n\n## Build / Run Script Candidates\n\n${scripts.length ? scripts.map((script) => `- ${script}`).join("\n") : "None found."}\n`;
655
+ }
656
+
657
+ export function writeContextArtifacts(root, index) {
658
+ const contextDir = path.join(root, ".codex", "context");
659
+ const files = index._files || [];
660
+ const artifacts = {
661
+ "index.json": JSON.stringify({
662
+ schemaVersion: index.schemaVersion,
663
+ generatedAt: index.generatedAt,
664
+ generatorVersion: index.generatorVersion,
665
+ root: index.root,
666
+ fileCount: index.fileCount,
667
+ languageCounts: index.languageCounts,
668
+ symbolCount: index.symbolCount,
669
+ routeCount: index.routeCount,
670
+ dependencyFileCount: index.dependencyFileCount,
671
+ recentChangesCount: index.recentChangesCount,
672
+ maxFileSizeKb: index.maxFileSizeKb,
673
+ counts: index.counts,
674
+ files: index.files
675
+ }, null, 2),
676
+ "summary.md": generateSummary(index),
677
+ "symbols.md": symbolsMd(files),
678
+ "files.md": groupedFilesMd(files),
679
+ "routes.md": routeHintsMd(files),
680
+ "dependencies.md": collectDependencies(root, files),
681
+ "recent_changes.md": `# Recent Changes\n\n${gitStatus(root)}\n`
682
+ };
683
+ let written = 0;
684
+ for (const [file, content] of Object.entries(artifacts)) {
685
+ if (writeFileIfChanged(path.join(contextDir, file), content)) written += 1;
686
+ }
687
+ return { artifacts: Object.keys(artifacts).length, written };
688
+ }
689
+
690
+ export function runIndex(root = process.cwd(), options = {}) {
691
+ const started = Date.now();
692
+ const scan = scanProject(root, options);
693
+ const recentPaths = recentChangedPaths(root);
694
+ const files = enrichFiles(scan.files.sort((a, b) => a.relative.localeCompare(b.relative))).map((file) => ({ ...file, ...scoreFile(file, recentPaths) }));
695
+ const languageCounts = Object.fromEntries(topLanguages(files));
696
+ const symbolCount = files.reduce((sum, file) => sum + file.symbols.length + file.headings.length, 0);
697
+ const routeCount = files.reduce((sum, file) => sum + file.routeHints.length, 0);
698
+ const dependencyFileCount = detectDependencies(root).length;
699
+ const index = {
700
+ schemaVersion: SCHEMA_VERSION,
701
+ generatedAt: new Date().toISOString(),
702
+ generatorVersion: GENERATOR_VERSION,
703
+ root: path.basename(root),
704
+ fileCount: files.length,
705
+ languageCounts,
706
+ symbolCount,
707
+ routeCount,
708
+ dependencyFileCount,
709
+ recentChangesCount: recentPaths.size,
710
+ maxFileSizeKb: scan.maxFileSizeKb,
711
+ counts: { files: files.length, ignored: scan.ignored, skippedLarge: scan.skippedLarge },
712
+ files: files.map((file) => ({
713
+ path: toDisplayPath(file.relative),
714
+ ext: file.ext,
715
+ size: file.size,
716
+ hash: file.hash,
717
+ imports: file.imports,
718
+ exports: file.exports,
719
+ symbols: file.symbols,
720
+ routes: file.routeHints,
721
+ routeHints: file.routeHints,
722
+ headings: file.headings,
723
+ testHints: file.testHints,
724
+ importanceScore: file.importanceScore,
725
+ importanceReasons: file.importanceReasons
726
+ })),
727
+ _files: files,
728
+ _root: root
729
+ };
730
+ const artifactResult = writeContextArtifacts(root, index);
731
+ const upgrade = runProjectUpgrade(root);
732
+ return {
733
+ ok: true,
734
+ action: "index",
735
+ root,
736
+ filesIndexed: files.length,
737
+ skippedFiles: scan.skippedLarge + scan.ignored,
738
+ ignored: scan.ignored,
739
+ skippedLarge: scan.skippedLarge,
740
+ artifacts: artifactResult.artifacts,
741
+ written: artifactResult.written,
742
+ durationMs: Date.now() - started,
743
+ warnings: [],
744
+ upgrade
745
+ };
746
+ }
747
+
748
+ export function runContextIndex(root = process.cwd(), options = {}) {
749
+ return runIndex(root, options);
750
+ }
751
+
752
+ export function runContextDoctor(root = process.cwd()) {
753
+ const artifactResults = contextFiles.map((file) => {
754
+ const found = fileExists(path.join(root, file));
755
+ return { file, found, line: `${found ? "OK" : "MISSING"} ${file} ${found ? "found" : "missing"}` };
756
+ });
757
+ const indexPath = path.join(root, ".codex", "context", "index.json");
758
+ let indexValid = false;
759
+ let schemaVersion = "unavailable";
760
+ let generatedAt = "unavailable";
761
+ let fileCount = "unavailable";
762
+ let noSecretsIndexed = false;
763
+ if (fileExists(indexPath)) {
764
+ try {
765
+ const index = JSON.parse(readText(indexPath));
766
+ indexValid = Boolean(index && Array.isArray(index.files));
767
+ schemaVersion = index.schemaVersion ? String(index.schemaVersion) : "unavailable";
768
+ const parsedDate = Date.parse(index.generatedAt);
769
+ generatedAt = Number.isNaN(parsedDate) ? "invalid generatedAt" : index.generatedAt;
770
+ const matchesLength = Number.isInteger(index.fileCount) && index.fileCount === index.files.length;
771
+ fileCount = matchesLength ? String(index.fileCount) : "invalid file count";
772
+ noSecretsIndexed = index.files.every((file) => file.path && !isIgnoredPath(file.path));
773
+ } catch {
774
+ generatedAt = "invalid index.json";
775
+ fileCount = "invalid index.json";
776
+ }
777
+ }
778
+ const agentsPath = path.join(root, ".codex", "AGENTS.md");
779
+ const agentsReferencesContext = fileExists(agentsPath) && readText(agentsPath).includes("Precomputed Context Engine");
780
+ const relevantPath = relevantPathFor(root);
781
+ const relevantReadable = !fileExists(relevantPath) || fs.statSync(relevantPath).isFile();
782
+ const results = [
783
+ ...artifactResults,
784
+ { file: "index.json", found: indexValid, line: `${indexValid ? "OK" : "MISSING"} index.json valid` },
785
+ { file: "schemaVersion", found: schemaVersion !== "unavailable", line: `${schemaVersion !== "unavailable" ? "OK" : "MISSING"} schemaVersion ${schemaVersion}` },
786
+ { file: "fileCount", found: /^\d+$/.test(fileCount), line: `${/^\d+$/.test(fileCount) ? "OK" : "MISSING"} file count ${fileCount}` },
787
+ { file: "generatedAt", found: !generatedAt.startsWith("invalid") && generatedAt !== "unavailable", line: `${!generatedAt.startsWith("invalid") && generatedAt !== "unavailable" ? "OK" : "MISSING"} generatedAt ${generatedAt}` },
788
+ { file: "secrets", found: noSecretsIndexed, line: `${noSecretsIndexed ? "OK" : "MISSING"} no secret files indexed` },
789
+ {
790
+ file: path.join(".codex", "AGENTS.md"),
791
+ found: agentsReferencesContext,
792
+ line: `${agentsReferencesContext ? "OK" : "MISSING"} .codex/AGENTS.md references context engine`
793
+ },
794
+ {
795
+ file: path.join(".codex", "context", RELEVANT_CONTEXT_FILE),
796
+ found: relevantReadable,
797
+ line: `${fileExists(relevantPath) ? "OK" : "INFO"} .codex/context/relevant.md ${fileExists(relevantPath) ? "readable" : "missing optional"}`
798
+ }
799
+ ];
800
+ return { ok: results.every((result) => result.found), results };
801
+ }
802
+
803
+ export function runContextClean(root = process.cwd()) {
804
+ const started = Date.now();
805
+ const dir = path.join(root, ".codex", "context");
806
+ if (!fileExists(dir)) return { ok: true, action: "context-clean", removed: false, dir, durationMs: Date.now() - started, warnings: [] };
807
+ fs.rmSync(dir, { recursive: true, force: true });
808
+ return { ok: true, action: "context-clean", removed: true, dir, durationMs: Date.now() - started, warnings: [] };
809
+ }
810
+
811
+ function detectedContext(file) {
812
+ const rows = [];
813
+ if (file.exports?.length) rows.push(`- exports: ${file.exports.slice(0, 8).join(", ")}`);
814
+ if (file.symbols?.length) rows.push(`- symbols: ${file.symbols.slice(0, 8).map((item) => item.name).join(", ")}`);
815
+ if (file.routes?.length) rows.push(`- routes: ${file.routes.slice(0, 8).map((item) => `${item.method ? `${item.method} ` : ""}${item.path || item.route}`).join(", ")}`);
816
+ if (file.headings?.length) rows.push(`- headings: ${file.headings.slice(0, 8).map((item) => item.text).join(", ")}`);
817
+ return rows.length ? rows.join("\n") : "- none";
818
+ }
819
+
820
+ function relevantMarkdown(question, matches, byPath) {
821
+ const lines = ["# Relevant Context", "", `Query: ${question}`, "", `Generated: ${new Date().toISOString()}`, "", "## Top Matches", ""];
822
+ matches.forEach((match, index) => {
823
+ const file = byPath.get(match.path) || {};
824
+ lines.push(`### ${index + 1}. ${match.path}`, `Score: ${match.score}`, "", "Reasons:");
825
+ for (const reason of match.reasons.slice(0, 8)) lines.push(`- ${reason}`);
826
+ lines.push("", "Detected context:", detectedContext(file), "");
827
+ });
828
+ lines.push("## Suggested Codex Usage", "", "Before editing, inspect only the top relevant files first.", "If these files are insufficient, then perform a targeted search.", "", "## Notes", "", "Generated by codex-context-init query.", "Source code remains the source of truth.", "");
829
+ return lines.join("\n");
830
+ }
831
+
832
+ export function runQuery(root = process.cwd(), question, options = {}) {
833
+ const started = Date.now();
834
+ if (!question || !question.trim()) throw new Error("Query question is required.");
835
+ const topCount = Number.parseInt(options.top ?? 10, 10) || 10;
836
+ const index = readIndex(root);
837
+ const terms = queryTerms(question);
838
+ const recentPaths = new Set((index.files || [])
839
+ .filter((file) => (file.importanceReasons || []).includes("recently changed"))
840
+ .map((file) => file.path));
841
+ const byPath = new Map((index.files || []).map((file) => [file.path, file]));
842
+ const matches = (index.files || [])
843
+ .map((file) => scoreFileForQuery(file, terms, recentPaths))
844
+ .filter((match) => match.score > 0)
845
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
846
+ .slice(0, topCount);
847
+ const relevantPath = relevantPathFor(root);
848
+ writeFileAtomic(relevantPath, relevantMarkdown(question, matches, byPath));
849
+ return {
850
+ ok: true,
851
+ action: "query",
852
+ question,
853
+ topCount,
854
+ relevantPath: path.join(".codex", "context", RELEVANT_CONTEXT_FILE),
855
+ matches,
856
+ durationMs: Date.now() - started,
857
+ warnings: []
858
+ };
859
+ }
860
+
861
+ export function runDebug(root = process.cwd()) {
862
+ const globalAgentsPath = getGlobalAgentsPath();
863
+ const projectAgentsPath = path.join(root, ".codex", "AGENTS.md");
864
+ const contextDoctor = runContextDoctor(root);
865
+ const projectDoctor = runProjectDoctor(root);
866
+ const relevantPath = relevantPathFor(root);
867
+ const relevantExists = fileExists(relevantPath);
868
+ const results = [
869
+ { line: `OS ${os.platform()} ${os.release()}`, found: true },
870
+ { line: `Node ${process.version}`, found: true },
871
+ { line: "CLI version 0.1.0", found: true },
872
+ { line: `${fileExists(globalAgentsPath) ? "OK" : "MISSING"} global AGENTS ${globalAgentsPath}`, found: fileExists(globalAgentsPath) },
873
+ { line: `${fileExists(projectAgentsPath) ? "OK" : "MISSING"} project AGENTS ${projectAgentsPath}`, found: fileExists(projectAgentsPath) },
874
+ { line: `${contextDoctor.ok ? "OK" : "MISSING"} context status`, found: contextDoctor.ok },
875
+ { line: `${relevantExists ? "OK" : "MISSING"} relevant.md ${relevantExists ? "present" : "missing"}`, found: true },
876
+ { line: `relevant.md modified ${relevantExists ? fs.statSync(relevantPath).mtime.toISOString() : "n/a"}`, found: true },
877
+ { line: `Log location ${path.join(root, ".codex", "logs", "latest.log")}`, found: true }
878
+ ];
879
+ const ok = results.every((result) => result.found) && projectDoctor.ok;
880
+ return { ok, action: "debug", root, results, durationMs: 0, warnings: [] };
881
+ }