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.
@@ -0,0 +1,250 @@
1
+ import path from "node:path";
2
+
3
+ export const OLD_START = "<!-- CODEX-CONTEXT-INIT:START -->";
4
+ export const OLD_END = "<!-- CODEX-CONTEXT-INIT:END -->";
5
+ export const PROJECT_START = "<!-- CODEX-CONTEXT-INIT:PROJECT:START -->";
6
+ export const PROJECT_END = "<!-- CODEX-CONTEXT-INIT:PROJECT:END -->";
7
+ export const GLOBAL_START = "<!-- CODEX-CONTEXT-INIT:GLOBAL:START -->";
8
+ export const GLOBAL_END = "<!-- CODEX-CONTEXT-INIT:GLOBAL:END -->";
9
+
10
+ export const DEFAULT_MAX_FILE_SIZE_KB = 300;
11
+ export const SCHEMA_VERSION = 2;
12
+ export const GENERATOR_VERSION = "0.1.0";
13
+ export const HEAVY_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", "coverage", ".next", ".nuxt", "target", "vendor", ".venv", "__pycache__"]);
14
+ export const CONTEXT_FILES = ["index.json", "summary.md", "symbols.md", "files.md", "routes.md", "dependencies.md", "recent_changes.md"];
15
+ export const RELEVANT_CONTEXT_FILE = "relevant.md";
16
+ export const SECRET_FILE_NAMES = new Set([".env", "id_rsa", "id_ed25519"]);
17
+ export const SECRET_PREFIXES = [".env.", "secrets.", "credentials."];
18
+ export const SECRET_SUFFIXES = [".pem", ".key"];
19
+ export const DEPENDENCY_FILES = ["package.json", "requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
20
+ export const RELEVANT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".rs", ".go", ".java", ".cs", ".json", ".md", ".yml", ".yaml", ".toml", ".gradle", ".xml"]);
21
+ export const RELEVANT_FILE_NAMES = new Set([...DEPENDENCY_FILES.map((file) => file.toLowerCase()), "dockerfile"]);
22
+
23
+ export const requiredFiles = [
24
+ path.join(".codex", "AGENTS.md"),
25
+ path.join(".codex", "templates", "project_context.template.md"),
26
+ path.join(".codex", "templates", "architecture.template.md"),
27
+ path.join(".codex", "templates", "task.template.md"),
28
+ path.join(".codex", "templates", "decision_log.template.md"),
29
+ "project_context.md",
30
+ "architecture.md",
31
+ "task.md",
32
+ "decision_log.md"
33
+ ];
34
+
35
+ export const contextFiles = CONTEXT_FILES.map((file) => path.join(".codex", "context", file));
36
+
37
+ export const globalManagedBlock = `${GLOBAL_START}
38
+ # Global Codex Token Optimization Rules
39
+
40
+ You are operating in AGGRESSIVE TOKEN OPTIMIZATION MODE.
41
+
42
+ ## Global Behavior
43
+
44
+ - Complete coding tasks with the fewest tokens, fewest file reads, smallest diff, and shortest useful response.
45
+ - Do not explain unless explicitly asked.
46
+ - Do not teach.
47
+ - Do not summarize the repository.
48
+ - Do not restate the user request.
49
+ - Do not provide alternatives unless blocked.
50
+ - Do not create long plans.
51
+ - Ask clarifying questions only when the task is impossible or unsafe without one.
52
+
53
+ ## Context Usage
54
+
55
+ - Read the minimum files required.
56
+ - Prefer targeted search over broad exploration.
57
+ - Never scan the whole repository unless explicitly requested.
58
+ - Stop reading once enough context is found.
59
+ - Use open files, visible errors, and user-provided context first.
60
+
61
+ ## Editing
62
+
63
+ - Make the smallest correct change.
64
+ - Prefer local fixes over refactors.
65
+ - Reuse existing patterns.
66
+ - Do not rename, reformat, or reorganize unrelated code.
67
+ - Avoid dependency changes unless essential.
68
+
69
+ ## Validation
70
+
71
+ - Run only the narrowest relevant check.
72
+ - Prefer targeted tests over full test suites.
73
+ - If validation is skipped, say why in one short sentence.
74
+
75
+ ## Response Format
76
+
77
+ Return only:
78
+
79
+ CHANGED
80
+ - path/to/file
81
+
82
+ VALIDATION
83
+ - command or "not run"
84
+
85
+ DONE
86
+ ${GLOBAL_END}`;
87
+
88
+ export const projectManagedBlock = `${PROJECT_START}
89
+ # Project Codex Context Rules
90
+
91
+ ## Precomputed Context Engine
92
+
93
+ Before broad repository search, read these generated context files if present:
94
+
95
+ 1. .codex/context/relevant.md
96
+ 2. .codex/context/summary.md
97
+ 3. .codex/context/dependencies.md
98
+ 4. .codex/context/files.md
99
+ 5. .codex/context/symbols.md
100
+ 6. .codex/context/routes.md
101
+ 7. .codex/context/recent_changes.md
102
+ 8. .codex/context/index.json
103
+
104
+ If \`.codex/context/relevant.md\` exists, treat it as the task-specific context shortlist generated from the user's latest query.
105
+
106
+ Use these as pre-indexed repository context.
107
+
108
+ Rules:
109
+ - Prefer these files before scanning directories.
110
+ - Use importance scores to identify likely relevant files.
111
+ - Use them to identify the smallest relevant file set.
112
+ - Do not treat them as always complete.
113
+ - If generated context conflicts with source code, source code wins.
114
+ - After meaningful code changes, update the context index by running:
115
+ \`codex-context-init index\`
116
+
117
+ ## Context Source Priority
118
+
119
+ Then read these project-maintained files when present:
120
+
121
+ 1. task.md
122
+ 2. architecture.md
123
+ 3. decision_log.md
124
+ 4. project_context.md
125
+
126
+ Do not scan the repository until these context files have been checked.
127
+
128
+ ## Project Documentation Rules
129
+
130
+ - Update task.md after meaningful progress.
131
+ - Update decision_log.md only when a technical decision is made.
132
+ - Update architecture.md only when structure, dependencies, boundaries, or data flow change.
133
+ - Update project_context.md only when product goals, constraints, or scope change.
134
+ - Do not generate extra documentation unless requested.
135
+
136
+ ## Project Search Rules
137
+
138
+ - Prefer context files before broad repository search.
139
+ - Search only files directly related to the current task.
140
+ - Stop searching once sufficient context is found.
141
+ ${PROJECT_END}`;
142
+
143
+ export const templates = {
144
+ "project_context.md": `# Project Context
145
+
146
+ ## Product / Project Name
147
+
148
+ TODO
149
+
150
+ ## Goal
151
+
152
+ TODO
153
+
154
+ ## Users
155
+
156
+ TODO
157
+
158
+ ## Core Features
159
+
160
+ TODO
161
+
162
+ ## Non-Goals
163
+
164
+ TODO
165
+
166
+ ## Constraints
167
+
168
+ - Prefer small, maintainable changes.
169
+ - Prefer existing patterns.
170
+ - Avoid unnecessary dependencies.
171
+ - Optimize Codex token usage.
172
+
173
+ ## Current Scope
174
+
175
+ TODO
176
+ `,
177
+ "architecture.md": `# Architecture
178
+
179
+ ## Overview
180
+
181
+ TODO
182
+
183
+ ## Tech Stack
184
+
185
+ TODO
186
+
187
+ ## Main Components
188
+
189
+ TODO
190
+
191
+ ## Data Flow
192
+
193
+ TODO
194
+
195
+ ## Important Directories
196
+
197
+ TODO
198
+
199
+ ## Integration Points
200
+
201
+ TODO
202
+
203
+ ## Constraints
204
+
205
+ - Keep architecture simple.
206
+ - Avoid premature abstractions.
207
+ - Prefer modular boundaries.
208
+ `,
209
+ "task.md": `# Task
210
+
211
+ ## Current Task
212
+
213
+ TODO
214
+
215
+ ## Status
216
+
217
+ Not started.
218
+
219
+ ## Relevant Files
220
+
221
+ TODO
222
+
223
+ ## Acceptance Criteria
224
+
225
+ TODO
226
+
227
+ ## Notes for Codex
228
+
229
+ - Read this file first.
230
+ - Only inspect files directly related to the current task.
231
+ - Keep changes minimal.
232
+ `,
233
+ "decision_log.md": `# Decision Log
234
+
235
+ Record only meaningful technical decisions.
236
+
237
+ ## Format
238
+
239
+ ### YYYY-MM-DD - Decision Title
240
+
241
+ Decision:
242
+ TODO
243
+
244
+ Reason:
245
+ TODO
246
+
247
+ Impact:
248
+ TODO
249
+ `
250
+ };
@@ -0,0 +1 @@
1
+ export { runContextDoctor, runDebug, runDoctor, runProjectDoctor } from "../../core.js";
@@ -0,0 +1 @@
1
+ export { getGlobalAgentsPath, runGlobalDoctor, runGlobalSetup } from "../../core.js";
@@ -0,0 +1,19 @@
1
+ export {
2
+ collectFiles,
3
+ countEligibleFiles,
4
+ detectDependencies,
5
+ extractExports,
6
+ extractFileMetadata,
7
+ extractImports,
8
+ extractRouteHints,
9
+ extractSymbols,
10
+ generateSummary,
11
+ getWatchDirs,
12
+ isIgnoredPath,
13
+ isIgnoredWorkspacePath,
14
+ isLikelyBinary,
15
+ runContextClean,
16
+ runContextIndex,
17
+ runIndex,
18
+ writeContextArtifacts
19
+ } from "../../core.js";
@@ -0,0 +1 @@
1
+ export { runQuery } from "../../core.js";
@@ -0,0 +1 @@
1
+ export { runNew, runSync } from "../../core.js";
@@ -0,0 +1 @@
1
+ export { runProjectUpgrade, runUpgrade, upsertManagedBlock } from "../../core.js";
@@ -0,0 +1 @@
1
+ export * from "./utils/logger.js";
@@ -0,0 +1,16 @@
1
+ export function emptyParseResult() {
2
+ return { imports: [], exports: [], symbols: [], routes: [], headings: [], dependencies: [] };
3
+ }
4
+
5
+ export function safeParse(parse, content, context = {}) {
6
+ try {
7
+ return parse(content, context);
8
+ } catch (error) {
9
+ context.logger?.warn?.(`Parser failed for ${context.relativePath || "unknown file"}: ${error.message}`);
10
+ return emptyParseResult();
11
+ }
12
+ }
13
+
14
+ export function parseGeneric() {
15
+ return emptyParseResult();
16
+ }
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ import { parseGeneric } from "./genericParser.js";
3
+ import { parseJavaScript } from "./javascriptParser.js";
4
+ import { parseJson } from "./jsonParser.js";
5
+ import { parseMarkdown } from "./markdownParser.js";
6
+ import { parsePython } from "./pythonParser.js";
7
+ import { parseTypeScript } from "./typescriptParser.js";
8
+
9
+ export function parseFile(content, context = {}) {
10
+ const ext = context.ext || path.extname(context.relativePath || "").toLowerCase();
11
+ const fileName = context.fileName || path.basename(context.relativePath || "");
12
+ const parserContext = { ...context, ext, fileName };
13
+ if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) return parseJavaScript(content, parserContext);
14
+ if ([".ts", ".tsx"].includes(ext)) return parseTypeScript(content, parserContext);
15
+ if (ext === ".py") return parsePython(content, parserContext);
16
+ if ([".md", ".mdx"].includes(ext)) return parseMarkdown(content, parserContext);
17
+ if (ext === ".json") return parseJson(content, parserContext);
18
+ return parseGeneric(content, parserContext);
19
+ }
@@ -0,0 +1,41 @@
1
+ import { emptyParseResult, safeParse } from "./genericParser.js";
2
+
3
+ const ROUTE_RE = /\b(app|router)\.(get|post|put|patch|delete|use)\s*\(\s*["'`]([^"'`]+)["'`]/i;
4
+ const REACT_ROUTE_RE = /<(?:Route|Link|NavLink)\b[^>]*(?:path|to)=["'`]([^"'`]+)["'`]/i;
5
+
6
+ export function parseJavaScript(content, context = {}) {
7
+ return safeParse((text) => {
8
+ const result = emptyParseResult();
9
+ const lines = text.split(/\r?\n/);
10
+ for (let i = 0; i < lines.length; i += 1) {
11
+ const line = lines[i];
12
+ const importMatch = line.match(/^\s*import\s+.*?\s+from\s+["'`]([^"'`]+)|^\s*import\s+["'`]([^"'`]+)|require\(\s*["'`]([^"'`]+)["'`]\s*\)/);
13
+ const importName = importMatch?.[1] || importMatch?.[2] || importMatch?.[3];
14
+ if (importName) result.imports.push(importName);
15
+
16
+ const exportMatch = line.match(/^\s*export\s+(?:default\s+)?(?:(?:async\s+)?(?:function|class|const|let|var)\s+)?([A-Za-z_$][\w$]*)?/);
17
+ if (exportMatch) result.exports.push(exportMatch[1] || "default");
18
+
19
+ const fn = line.match(/^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/);
20
+ if (fn) result.symbols.push({ type: /^[A-Z]/.test(fn[1]) ? "component" : "function", name: fn[1], line: i + 1 });
21
+
22
+ const arrow = line.match(/^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/);
23
+ if (arrow) result.symbols.push({ type: /^[A-Z]/.test(arrow[1]) ? "component" : "function", name: arrow[1], line: i + 1 });
24
+
25
+ const cls = line.match(/^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/);
26
+ if (cls) result.symbols.push({ type: /^[A-Z].*(Component|Page|Form|View)$/.test(cls[1]) ? "component" : "class", name: cls[1], line: i + 1 });
27
+
28
+ const exportedConst = line.match(/^\s*export\s+const\s+([A-Za-z_$][\w$]*)/);
29
+ if (exportedConst && !result.symbols.some((item) => item.name === exportedConst[1])) {
30
+ result.symbols.push({ type: "exported constant", name: exportedConst[1], line: i + 1 });
31
+ }
32
+
33
+ const route = line.match(ROUTE_RE);
34
+ if (route) result.routes.push({ method: route[2].toUpperCase(), path: route[3], source: context.relativePath, line: i + 1, kind: "api" });
35
+
36
+ const uiRoute = line.match(REACT_ROUTE_RE);
37
+ if (uiRoute) result.routes.push({ method: "", path: uiRoute[1], source: context.relativePath, line: i + 1, kind: "ui" });
38
+ }
39
+ return result;
40
+ }, content, context);
41
+ }
@@ -0,0 +1,17 @@
1
+ import { emptyParseResult, safeParse } from "./genericParser.js";
2
+
3
+ export function parseJson(content, context = {}) {
4
+ return safeParse((text) => {
5
+ const result = emptyParseResult();
6
+ if (context.fileName !== "package.json") return result;
7
+ const pkg = JSON.parse(text);
8
+ result.dependencies.push({
9
+ file: context.relativePath,
10
+ packageName: pkg.name || "",
11
+ scripts: Object.keys(pkg.scripts || {}),
12
+ dependencies: Object.keys(pkg.dependencies || {}),
13
+ devDependencies: Object.keys(pkg.devDependencies || {})
14
+ });
15
+ return result;
16
+ }, content, context);
17
+ }
@@ -0,0 +1,13 @@
1
+ import { emptyParseResult, safeParse } from "./genericParser.js";
2
+
3
+ export function parseMarkdown(content, context = {}) {
4
+ return safeParse((text) => {
5
+ const result = emptyParseResult();
6
+ const lines = text.split(/\r?\n/);
7
+ for (let i = 0; i < lines.length; i += 1) {
8
+ const heading = lines[i].match(/^(#{1,6})\s+(.+)/);
9
+ if (heading) result.headings.push({ level: heading[1].length, text: heading[2].trim(), line: i + 1 });
10
+ }
11
+ return result;
12
+ }, content, context);
13
+ }
@@ -0,0 +1,24 @@
1
+ import { emptyParseResult, safeParse } from "./genericParser.js";
2
+
3
+ export function parsePython(content, context = {}) {
4
+ return safeParse((text) => {
5
+ const result = emptyParseResult();
6
+ const lines = text.split(/\r?\n/);
7
+ for (let i = 0; i < lines.length; i += 1) {
8
+ const line = lines[i];
9
+ const imported = line.match(/^\s*import\s+([A-Za-z0-9_.,\s]+)|^\s*from\s+([A-Za-z0-9_.]+)\s+import/);
10
+ const importName = imported?.[1] || imported?.[2];
11
+ if (importName) result.imports.push(importName.trim());
12
+
13
+ const fn = line.match(/^\s*def\s+([A-Za-z_]\w*)/);
14
+ if (fn) result.symbols.push({ type: "function", name: fn[1], line: i + 1 });
15
+
16
+ const cls = line.match(/^\s*class\s+([A-Za-z_]\w*)/);
17
+ if (cls) result.symbols.push({ type: "class", name: cls[1], line: i + 1 });
18
+
19
+ const route = line.match(/^\s*@(app|router)\.(get|post|put|patch|delete|route)\s*\(\s*["']([^"']+)["']/i);
20
+ if (route) result.routes.push({ method: route[2].toUpperCase() === "ROUTE" ? "" : route[2].toUpperCase(), path: route[3], source: context.relativePath, line: i + 1, kind: "api" });
21
+ }
22
+ return result;
23
+ }, content, context);
24
+ }
@@ -0,0 +1,5 @@
1
+ import { parseJavaScript } from "./javascriptParser.js";
2
+
3
+ export function parseTypeScript(content, context = {}) {
4
+ return parseJavaScript(content, context);
5
+ }
@@ -0,0 +1,104 @@
1
+ import path from "node:path";
2
+
3
+ const STOP_WORDS = new Set(["what", "which", "where", "how", "do", "does", "the", "a", "an", "to", "for", "of", "in", "on", "with", "and", "or", "is", "are"]);
4
+
5
+ const SYNONYMS = {
6
+ auth: ["auth", "authentication", "authorize", "authorization", "login", "logout", "session", "token", "jwt", "user"],
7
+ database: ["db", "database", "model", "schema", "migration", "repository", "query"],
8
+ api: ["api", "route", "controller", "endpoint", "handler", "request", "response"],
9
+ ui: ["ui", "component", "page", "view", "screen", "form"],
10
+ config: ["config", "settings", "env", "environment"],
11
+ test: ["test", "spec", "mock", "fixture"]
12
+ };
13
+
14
+ function normalizeTerm(term) {
15
+ const cleaned = term.toLowerCase().replace(/[^a-z0-9_-]/g, "");
16
+ if (cleaned.length > 3 && cleaned.endsWith("s")) return cleaned.slice(0, -1);
17
+ return cleaned;
18
+ }
19
+
20
+ export function queryTerms(question) {
21
+ const base = question.split(/\s+/).map(normalizeTerm).filter((term) => term && !STOP_WORDS.has(term));
22
+ const expanded = new Set(base);
23
+ for (const term of base) {
24
+ for (const values of Object.values(SYNONYMS)) {
25
+ if (values.includes(term)) values.forEach((value) => expanded.add(value));
26
+ }
27
+ }
28
+ return [...expanded];
29
+ }
30
+
31
+ function includesTerm(value, term) {
32
+ return String(value || "").toLowerCase().includes(term);
33
+ }
34
+
35
+ function add(reasons, reason) {
36
+ if (!reasons.includes(reason)) reasons.push(reason);
37
+ }
38
+
39
+ export function scoreFileForQuery(file, terms, recentPaths = new Set()) {
40
+ let score = 0;
41
+ const reasons = [];
42
+ const filePath = file.path || "";
43
+ const filename = path.basename(filePath);
44
+ const ext = file.ext || path.extname(filePath);
45
+ const generated = /(^|\/)(dist|build|out|coverage|generated)(\/|$)|(\.min\.)/i.test(filePath);
46
+
47
+ for (const term of terms) {
48
+ if (includesTerm(filePath, term)) {
49
+ score += 40;
50
+ add(reasons, `path matched ${term}`);
51
+ }
52
+ if (includesTerm(filename, term)) {
53
+ score += 30;
54
+ add(reasons, `filename matched ${term}`);
55
+ }
56
+ for (const symbol of file.symbols || []) {
57
+ if (includesTerm(symbol.name, term)) {
58
+ score += 25;
59
+ add(reasons, `symbol matched ${symbol.name}`);
60
+ }
61
+ }
62
+ for (const route of file.routes || file.routeHints || []) {
63
+ const routePath = route.path || route.route || "";
64
+ if (includesTerm(routePath, term)) {
65
+ score += 25;
66
+ add(reasons, `route matched ${routePath}`);
67
+ }
68
+ }
69
+ for (const value of [...(file.imports || []), ...(file.exports || [])]) {
70
+ if (includesTerm(value, term)) {
71
+ score += 20;
72
+ add(reasons, `import/export matched ${value}`);
73
+ }
74
+ }
75
+ for (const heading of file.headings || []) {
76
+ if (includesTerm(heading.text, term)) {
77
+ score += 15;
78
+ add(reasons, `heading matched ${heading.text}`);
79
+ }
80
+ }
81
+ }
82
+
83
+ if (terms.some((term) => includesTerm(ext, term))) {
84
+ score += 10;
85
+ add(reasons, `extension matched ${ext}`);
86
+ }
87
+ if (recentPaths.has(filePath)) {
88
+ score += 10;
89
+ add(reasons, "recently changed");
90
+ }
91
+ if (Number.isFinite(file.importanceScore)) {
92
+ const bonus = Math.max(0, Math.min(20, Math.round(file.importanceScore / 5)));
93
+ if (bonus) {
94
+ score += bonus;
95
+ add(reasons, `importance score bonus ${bonus}`);
96
+ }
97
+ }
98
+ if (generated) {
99
+ score -= 20;
100
+ add(reasons, "generated/build-like file penalty");
101
+ }
102
+
103
+ return { path: filePath, score, reasons };
104
+ }
@@ -0,0 +1,16 @@
1
+ import { runGlobalDoctor, runGlobalSetup } from "../engine/global.js";
2
+ import { runProjectUpgrade } from "../engine/upgrade.js";
3
+
4
+ export class AgentService {
5
+ setupGlobal() {
6
+ return runGlobalSetup();
7
+ }
8
+
9
+ doctorGlobal() {
10
+ return runGlobalDoctor();
11
+ }
12
+
13
+ upgradeProject(root) {
14
+ return runProjectUpgrade(root);
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import { runContextClean, runContextDoctor, runContextIndex } from "../engine/index.js";
2
+
3
+ export class ContextService {
4
+ index(root, options) {
5
+ return runContextIndex(root, options);
6
+ }
7
+
8
+ doctor(root) {
9
+ return runContextDoctor(root);
10
+ }
11
+
12
+ clean(root) {
13
+ return runContextClean(root);
14
+ }
15
+ }
@@ -0,0 +1,19 @@
1
+ import { collectFiles, getWatchDirs, isIgnoredPath, isIgnoredWorkspacePath } from "../engine/index.js";
2
+
3
+ export class RepositoryService {
4
+ collectFiles(root, options) {
5
+ return collectFiles(root, options);
6
+ }
7
+
8
+ getWatchDirs(root) {
9
+ return getWatchDirs(root);
10
+ }
11
+
12
+ isIgnoredPath(relativePath) {
13
+ return isIgnoredPath(relativePath);
14
+ }
15
+
16
+ isIgnoredWorkspacePath(root, file) {
17
+ return isIgnoredWorkspacePath(root, file);
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export * from "../config.js";
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function ensureDir(dir) {
5
+ fs.mkdirSync(dir, { recursive: true });
6
+ }
7
+
8
+ export function fileExists(file) {
9
+ return fs.existsSync(file);
10
+ }
11
+
12
+ export function writeFileAtomic(file, content) {
13
+ ensureDir(path.dirname(file));
14
+ const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
15
+ fs.writeFileSync(temp, content, "utf8");
16
+ fs.renameSync(temp, file);
17
+ }
18
+
19
+ export function writeFileForce(file, content) {
20
+ ensureDir(path.dirname(file));
21
+ fs.writeFileSync(file, content, "utf8");
22
+ }
23
+
24
+ export function writeFileIfMissing(file, content) {
25
+ if (fileExists(file)) return false;
26
+ writeFileForce(file, content);
27
+ return true;
28
+ }
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureDir } from "./fsSafe.js";
4
+
5
+ function format(level, message) {
6
+ return `${new Date().toISOString()} [${level}] ${message}`;
7
+ }
8
+
9
+ export function createLogger(options = {}) {
10
+ const verbose = Boolean(options.verbose);
11
+ const sink = options.sink ?? console;
12
+ const root = options.root ?? process.cwd();
13
+ const logFile = options.logFile ?? path.join(root, ".codex", "logs", "latest.log");
14
+
15
+ function write(level, message) {
16
+ const line = format(level, message);
17
+ try {
18
+ ensureDir(path.dirname(logFile));
19
+ fs.appendFileSync(logFile, `${line}\n`, "utf8");
20
+ } catch {
21
+ // Logging must never break the command being run.
22
+ }
23
+ return line;
24
+ }
25
+
26
+ return {
27
+ verbose,
28
+ logFile,
29
+ info(message) {
30
+ sink.log?.(message);
31
+ write("INFO", message);
32
+ },
33
+ warn(message) {
34
+ if (sink.warn) sink.warn(message);
35
+ else sink.log?.(message);
36
+ write("WARN", message);
37
+ },
38
+ error(error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ const output = verbose && error instanceof Error ? error.stack : message;
41
+ sink.error?.(output);
42
+ write("ERROR", message);
43
+ },
44
+ debug(message) {
45
+ if (verbose) sink.log?.(message);
46
+ write("DEBUG", message);
47
+ }
48
+ };
49
+ }