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/LICENSE +21 -0
- package/README.md +332 -0
- package/bin/codex-context-init.js +160 -0
- package/package.json +104 -0
- package/src/core/config.js +250 -0
- package/src/core/engine/doctor.js +1 -0
- package/src/core/engine/global.js +1 -0
- package/src/core/engine/index.js +19 -0
- package/src/core/engine/query.js +1 -0
- package/src/core/engine/sync.js +1 -0
- package/src/core/engine/upgrade.js +1 -0
- package/src/core/logger.js +1 -0
- package/src/core/parsers/genericParser.js +16 -0
- package/src/core/parsers/index.js +19 -0
- package/src/core/parsers/javascriptParser.js +41 -0
- package/src/core/parsers/jsonParser.js +17 -0
- package/src/core/parsers/markdownParser.js +13 -0
- package/src/core/parsers/pythonParser.js +24 -0
- package/src/core/parsers/typescriptParser.js +5 -0
- package/src/core/scoring/queryScorer.js +104 -0
- package/src/core/services/AgentService.js +16 -0
- package/src/core/services/ContextService.js +15 -0
- package/src/core/services/RepositoryService.js +19 -0
- package/src/core/utils/config.js +1 -0
- package/src/core/utils/fsSafe.js +28 -0
- package/src/core/utils/logger.js +49 -0
- package/src/core.js +881 -0
- package/src/extension/extension.cjs +163 -0
- package/src/extension.js +141 -0
|
@@ -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,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
|
+
}
|