codemini-cli 0.5.9 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/OPERATIONS.md +242 -242
  2. package/README.md +588 -489
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-HgeDi9HJ.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-C4tKT3v4.js → index-B71xykPM.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
  7. package/codemini-web/dist/index.html +23 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +1 -1
  13. package/skills/brainstorm/SKILL.md +77 -72
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -292
  24. package/src/core/chat-runtime.js +6261 -6240
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +289 -287
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -529
  39. package/src/core/project-instructions.js +98 -0
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -317
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -42
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5171 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. package/codemini-web/dist/assets/mermaid-GHXKKRXX-CDgkkDBg.js +0 -1
@@ -1,595 +1,803 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
4
- import { INDEX_SKIP_DIRS as SKIP_DIRS, SOURCE_EXTENSIONS, EXTENSION_LANGUAGE_MAP } from './constants.js';
5
- import { sha256 } from './crypto-utils.js';
6
- import { BoundedCache } from './bounded-cache.js';
7
- import { trimInline, normalizeRelativePath, escapeRegex } from './string-utils.js';
8
-
9
- const PROJECT_MARKER_FILES = new Set([
10
- 'package.json',
11
- 'tsconfig.json',
12
- 'pyproject.toml',
13
- 'requirements.txt',
14
- 'go.mod',
15
- 'Cargo.toml',
16
- 'composer.json',
17
- 'Gemfile',
18
- 'pom.xml',
19
- 'build.gradle',
20
- 'build.gradle.kts',
21
- 'Makefile',
22
- '.gitignore'
23
- ]);
24
-
25
- const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
26
-
27
- const initCache = new BoundedCache({ maxSize: 32, ttlMs: 10 * 60 * 1000 });
28
- const PROJECT_CONTEXT_MAX_FILES = 6;
29
-
30
- function clipList(values, max = 32) {
31
- return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].slice(0, max);
32
- }
33
-
34
- function rel(cwd, filePath) {
35
- return normalizeRelativePath(path.relative(cwd, filePath));
36
- }
37
-
38
- async function safeStat(filePath) {
39
- try {
40
- return await fs.stat(filePath);
41
- } catch {
42
- return null;
43
- }
44
- }
45
-
46
- async function safeReadJson(filePath, fallback) {
47
- try {
48
- return JSON.parse(await fs.readFile(filePath, 'utf8'));
49
- } catch {
50
- return fallback;
51
- }
52
- }
53
-
54
- function tokenizeQuery(text) {
55
- return [...new Set(String(text || '').toLowerCase().match(/[a-z0-9_./-]+/g) || [])].filter(Boolean);
56
- }
57
-
58
- function trimMultiline(value, max = 1800) {
59
- const text = String(value || '').trim();
60
- if (!text) return '';
61
- if (text.length <= max) return text;
62
- return `${text.slice(0, max - 3).trimEnd()}...`;
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
4
+ import { INDEX_SKIP_DIRS as SKIP_DIRS, SOURCE_EXTENSIONS, EXTENSION_LANGUAGE_MAP } from './constants.js';
5
+ import { sha256 } from './crypto-utils.js';
6
+ import { BoundedCache } from './bounded-cache.js';
7
+ import { trimInline, normalizeRelativePath, escapeRegex } from './string-utils.js';
8
+
9
+ const PROJECT_MARKER_FILES = new Set([
10
+ 'package.json',
11
+ 'tsconfig.json',
12
+ 'pyproject.toml',
13
+ 'requirements.txt',
14
+ 'go.mod',
15
+ 'Cargo.toml',
16
+ 'composer.json',
17
+ 'Gemfile',
18
+ 'pom.xml',
19
+ 'build.gradle',
20
+ 'build.gradle.kts',
21
+ 'Makefile',
22
+ '.gitignore'
23
+ ]);
24
+
25
+ const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
26
+
27
+ const initCache = new BoundedCache({ maxSize: 32, ttlMs: 10 * 60 * 1000 });
28
+ const ignoreRulesCache = new BoundedCache({ maxSize: 128, ttlMs: 60 * 1000 });
29
+ const PROJECT_CONTEXT_MAX_FILES = 6;
30
+
31
+ function clipList(values, max = 32) {
32
+ return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].slice(0, max);
33
+ }
34
+
35
+ function rel(cwd, filePath) {
36
+ return normalizeRelativePath(path.relative(cwd, filePath));
37
+ }
38
+
39
+ async function safeStat(filePath) {
40
+ try {
41
+ return await fs.stat(filePath);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ async function safeReadJson(filePath, fallback) {
48
+ try {
49
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ }
54
+
55
+ function tokenizeQuery(text) {
56
+ return [...new Set(String(text || '').toLowerCase().match(/[a-z0-9_./-]+/g) || [])].filter(Boolean);
57
+ }
58
+
59
+ function trimMultiline(value, max = 1800) {
60
+ const text = String(value || '').trim();
61
+ if (!text) return '';
62
+ if (text.length <= max) return text;
63
+ return `${text.slice(0, max - 3).trimEnd()}...`;
64
+ }
65
+
66
+ async function writeJson(filePath, value) {
67
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
68
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
69
+ }
70
+
71
+ function gitignorePatternToRegex(pattern) {
72
+ const normalized = normalizeRelativePath(pattern);
73
+ let regexBody = '';
74
+ for (let index = 0; index < normalized.length; index += 1) {
75
+ const ch = normalized[index];
76
+ const next = normalized[index + 1];
77
+ if (ch === '*') {
78
+ if (next === '*') {
79
+ regexBody += '.*';
80
+ index += 1;
81
+ } else {
82
+ regexBody += '[^/]*';
83
+ }
84
+ continue;
85
+ }
86
+ if (ch === '?') {
87
+ regexBody += '[^/]';
88
+ continue;
89
+ }
90
+ regexBody += escapeRegex(ch);
91
+ }
92
+ return new RegExp(`^${regexBody}$`);
93
+ }
94
+
95
+ async function readIgnoreFileRules(cwd, fileName) {
96
+ const filePath = path.join(cwd, fileName);
97
+ const stat = await safeStat(filePath);
98
+ const cacheKey = `${filePath}:${Number(stat?.mtimeMs || 0)}:${Number(stat?.size || 0)}`;
99
+ if (ignoreRulesCache.has(cacheKey)) return ignoreRulesCache.get(cacheKey);
100
+
101
+ for (const key of ignoreRulesCache.keys()) {
102
+ if (String(key).startsWith(`${filePath}:`) && key !== cacheKey) {
103
+ ignoreRulesCache.delete(key);
104
+ }
105
+ }
106
+
107
+ try {
108
+ if (!stat?.isFile()) {
109
+ ignoreRulesCache.set(cacheKey, []);
110
+ return [];
111
+ }
112
+ const raw = await fs.readFile(filePath, 'utf8');
113
+ const rules = raw
114
+ .split(/\r?\n/)
115
+ .map((line) => line.trim())
116
+ .filter((line) => line && !line.startsWith('#'))
117
+ .map((line) => {
118
+ const negated = line.startsWith('!');
119
+ const source = negated ? line.slice(1) : line;
120
+ const dirOnly = source.endsWith('/');
121
+ const anchored = source.startsWith('/');
122
+ const normalized = normalizeRelativePath(dirOnly ? source.slice(0, -1) : source);
123
+ return {
124
+ negated,
125
+ dirOnly,
126
+ anchored,
127
+ normalized,
128
+ hasSlash: normalized.includes('/'),
129
+ regex: gitignorePatternToRegex(normalized)
130
+ };
131
+ })
132
+ .filter((rule) => rule.normalized);
133
+ ignoreRulesCache.set(cacheKey, rules);
134
+ return rules;
135
+ } catch {
136
+ ignoreRulesCache.set(cacheKey, []);
137
+ return [];
138
+ }
139
+ }
140
+
141
+ async function readProjectIgnoreRules(cwd) {
142
+ const [gitignoreRules, llmignoreRules] = await Promise.all([
143
+ readIgnoreFileRules(cwd, '.gitignore'),
144
+ readIgnoreFileRules(cwd, '.llmignore')
145
+ ]);
146
+ return {
147
+ gitignoreRules,
148
+ llmignoreRules,
149
+ combinedRules: [...gitignoreRules, ...llmignoreRules]
150
+ };
151
+ }
152
+
153
+ function matchesGitignoreRule(rule, relativePath, isDirectory) {
154
+ if (!rule || !relativePath) return false;
155
+ if (rule.dirOnly && !isDirectory) return false;
156
+ const normalizedPath = normalizeRelativePath(relativePath);
157
+ if (!normalizedPath) return false;
158
+ if (rule.anchored || rule.hasSlash) {
159
+ return rule.regex.test(normalizedPath);
160
+ }
161
+ return normalizedPath.split('/').some((segment) => rule.regex.test(segment));
162
+ }
163
+
164
+ function shouldIgnorePath(relativePath, isDirectory, gitignoreRules = []) {
165
+ const normalizedPath = normalizeRelativePath(relativePath);
166
+ if (!normalizedPath) return false;
167
+ const segments = normalizedPath.split('/').filter(Boolean);
168
+ if (segments.some((segment) => SKIP_DIRS.has(segment))) return true;
169
+ if (segments.some((segment) => /^venv[-_]/i.test(segment) || /\.egg-info$/i.test(segment))) return true;
170
+ let ignored = false;
171
+ for (const rule of gitignoreRules) {
172
+ if (!matchesGitignoreRule(rule, normalizedPath, isDirectory)) continue;
173
+ ignored = !rule.negated;
174
+ }
175
+ return ignored;
176
+ }
177
+
178
+ async function detectWorkspaceKind(cwd) {
179
+ const gitDir = await safeStat(path.join(cwd, '.git'));
180
+ if (gitDir?.isDirectory()) return 'project';
181
+ for (const marker of PROJECT_MARKER_FILES) {
182
+ const stat = await safeStat(path.join(cwd, marker));
183
+ if (stat?.isFile()) return 'project';
184
+ }
185
+ return 'directory';
186
+ }
187
+
188
+ async function findNearestProjectRoot(startDir, workspaceRoot) {
189
+ let current = path.resolve(startDir);
190
+ const root = path.resolve(workspaceRoot);
191
+ while (current.startsWith(root)) {
192
+ if ((await detectWorkspaceKind(current)) === 'project') return current;
193
+ if (current === root) break;
194
+ const parent = path.dirname(current);
195
+ if (parent === current) break;
196
+ current = parent;
197
+ }
198
+ return null;
199
+ }
200
+
201
+ async function findProjectRootFromFile(workspaceRoot, relativePath = '') {
202
+ const absolutePath = path.resolve(workspaceRoot, String(relativePath || '.'));
203
+ const stat = await safeStat(absolutePath);
204
+ const probeStart = stat?.isDirectory() ? absolutePath : path.dirname(absolutePath);
205
+ return findNearestProjectRoot(probeStart, workspaceRoot);
206
+ }
207
+
208
+ async function findNearestIndexedProjectRoot(startDir, workspaceRoot) {
209
+ let current = path.resolve(startDir);
210
+ const root = path.resolve(workspaceRoot);
211
+ while (current.startsWith(root)) {
212
+ const projectMapStat = await safeStat(getProjectMapPath(current));
213
+ const fileIndexStat = await safeStat(getFileIndexPath(current));
214
+ if (projectMapStat?.isFile() && fileIndexStat?.isFile()) return current;
215
+ if (current === root) break;
216
+ const parent = path.dirname(current);
217
+ if (parent === current) break;
218
+ current = parent;
219
+ }
220
+ return null;
221
+ }
222
+
223
+ async function walkFiles(cwd, start = cwd, out = [], ignoreRules = []) {
224
+ const entries = await fs.readdir(start, { withFileTypes: true });
225
+ for (const entry of entries) {
226
+ const absolutePath = path.join(start, entry.name);
227
+ const relativePath = rel(cwd, absolutePath);
228
+ if (entry.isDirectory()) {
229
+ if (shouldIgnorePath(relativePath, true, ignoreRules)) continue;
230
+ await walkFiles(cwd, absolutePath, out, ignoreRules);
231
+ continue;
232
+ }
233
+ if (shouldIgnorePath(relativePath, false, ignoreRules)) continue;
234
+ out.push(absolutePath);
235
+ }
236
+ return out;
237
+ }
238
+
239
+ function categorizeDirectory(relativeDir) {
240
+ const text = String(relativeDir || '').toLowerCase();
241
+ if (!text || text === '.') return 'root';
242
+ if (/(^|\/)(src|app|apps)\b/.test(text)) return 'source';
243
+ if (/(^|\/)(test|tests|__tests__|spec)\b/.test(text)) return 'test';
244
+ if (/(^|\/)(scripts|bin)\b/.test(text)) return 'script';
245
+ if (/(^|\/)(config|configs)\b/.test(text)) return 'config';
246
+ return 'other';
247
+ }
248
+
249
+ function extractMatches(regex, text, group = 1) {
250
+ const out = [];
251
+ for (const match of String(text || '').matchAll(regex)) {
252
+ const value = String(match[group] || '').trim();
253
+ if (value) out.push(value);
254
+ }
255
+ return out;
63
256
  }
64
257
 
65
- async function writeJson(filePath, value) {
66
- await fs.mkdir(path.dirname(filePath), { recursive: true });
67
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
258
+ function lineNumberForIndex(content, index) {
259
+ return String(content || '').slice(0, Math.max(0, index)).split(/\r?\n/).length;
68
260
  }
69
261
 
70
- function gitignorePatternToRegex(pattern) {
71
- const normalized = normalizeRelativePath(pattern);
72
- let regexBody = '';
73
- for (let index = 0; index < normalized.length; index += 1) {
74
- const ch = normalized[index];
75
- const next = normalized[index + 1];
76
- if (ch === '*') {
77
- if (next === '*') {
78
- regexBody += '.*';
79
- index += 1;
80
- } else {
81
- regexBody += '[^/]*';
82
- }
83
- continue;
84
- }
85
- if (ch === '?') {
86
- regexBody += '[^/]';
87
- continue;
262
+ function findBraceRange(content, openBraceIndex) {
263
+ if (openBraceIndex < 0) return null;
264
+ let depth = 0;
265
+ for (let index = openBraceIndex; index < content.length; index += 1) {
266
+ const ch = content[index];
267
+ if (ch === '{') depth += 1;
268
+ if (ch === '}') {
269
+ depth -= 1;
270
+ if (depth === 0) return { start: openBraceIndex, end: index + 1 };
88
271
  }
89
- regexBody += escapeRegex(ch);
90
272
  }
91
- return new RegExp(`^${regexBody}$`);
273
+ return null;
92
274
  }
93
275
 
94
- async function readIgnoreFileRules(cwd, fileName) {
95
- try {
96
- const raw = await fs.readFile(path.join(cwd, fileName), 'utf8');
97
- return raw
98
- .split(/\r?\n/)
99
- .map((line) => line.trim())
100
- .filter((line) => line && !line.startsWith('#'))
101
- .map((line) => {
102
- const negated = line.startsWith('!');
103
- const source = negated ? line.slice(1) : line;
104
- const dirOnly = source.endsWith('/');
105
- const anchored = source.startsWith('/');
106
- const normalized = normalizeRelativePath(dirOnly ? source.slice(0, -1) : source);
107
- return {
108
- negated,
109
- dirOnly,
110
- anchored,
111
- normalized,
112
- hasSlash: normalized.includes('/'),
113
- regex: gitignorePatternToRegex(normalized)
114
- };
115
- })
116
- .filter((rule) => rule.normalized);
117
- } catch {
118
- return [];
119
- }
276
+ function inferSymbolType(kind) {
277
+ if (kind === 'class') return 'class';
278
+ if (kind === 'method') return 'method';
279
+ if (kind === 'const') return 'function';
280
+ return 'function';
120
281
  }
121
282
 
122
- async function readProjectIgnoreRules(cwd) {
123
- const [gitignoreRules, llmignoreRules] = await Promise.all([
124
- readIgnoreFileRules(cwd, '.gitignore'),
125
- readIgnoreFileRules(cwd, '.llmignore')
126
- ]);
127
- return {
128
- gitignoreRules,
129
- llmignoreRules,
130
- combinedRules: [...gitignoreRules, ...llmignoreRules]
131
- };
283
+ function extractCallNames(content) {
284
+ return clipList(
285
+ extractMatches(/\b([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?)\s*\(/g, content)
286
+ .filter((name) => !['if', 'for', 'while', 'switch', 'return', 'function', 'class', 'catch'].includes(String(name).split('.')[0])),
287
+ 64
288
+ );
132
289
  }
133
290
 
134
- function matchesGitignoreRule(rule, relativePath, isDirectory) {
135
- if (!rule || !relativePath) return false;
136
- if (rule.dirOnly && !isDirectory) return false;
137
- const normalizedPath = normalizeRelativePath(relativePath);
138
- if (!normalizedPath) return false;
139
- if (rule.anchored || rule.hasSlash) {
140
- return rule.regex.test(normalizedPath);
141
- }
142
- return normalizedPath.split('/').some((segment) => rule.regex.test(segment));
291
+ function extractSemanticWrites(calls) {
292
+ return clipList((calls || []).filter((name) => /\.(insert|update|upsert|delete|save|write|create)$/i.test(String(name)) || /^(insert|update|upsert|delete|save|write|create)$/i.test(String(name))), 16);
143
293
  }
144
294
 
145
- function shouldIgnorePath(relativePath, isDirectory, gitignoreRules = []) {
146
- const normalizedPath = normalizeRelativePath(relativePath);
147
- if (!normalizedPath) return false;
148
- const topName = normalizedPath.split('/')[0];
149
- if (topName && SKIP_DIRS.has(topName)) return true;
150
- let ignored = false;
151
- for (const rule of gitignoreRules) {
152
- if (!matchesGitignoreRule(rule, normalizedPath, isDirectory)) continue;
153
- ignored = !rule.negated;
154
- }
155
- return ignored;
295
+ function extractSemanticEmits(calls, content) {
296
+ const eventNames = extractMatches(/\b(?:emit|publish|dispatch)\s*\(\s*['"`]([^'"`]+)['"`]/g, content);
297
+ return clipList([
298
+ ...eventNames,
299
+ ...(calls || []).filter((name) => /\.(emit|publish|dispatch)$/i.test(String(name)) || /^(emit|publish|dispatch)$/i.test(String(name)))
300
+ ], 16);
156
301
  }
157
302
 
158
- async function detectWorkspaceKind(cwd) {
159
- const gitDir = await safeStat(path.join(cwd, '.git'));
160
- if (gitDir?.isDirectory()) return 'project';
161
- for (const marker of PROJECT_MARKER_FILES) {
162
- const stat = await safeStat(path.join(cwd, marker));
163
- if (stat?.isFile()) return 'project';
164
- }
165
- return 'directory';
166
- }
303
+ function extractSymbolDefinitions(relativePath, content, imports = []) {
304
+ const definitions = [];
305
+ const patterns = [
306
+ { kind: 'class', regex: /\b(?:export\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)[^{]*\{/g },
307
+ { kind: 'function', regex: /\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g },
308
+ { kind: 'const', regex: /\b(?:export\s+)?const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g },
309
+ { kind: 'python', regex: /^\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*\)\s*:/gm },
310
+ { kind: 'go', regex: /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*\)\s*\{/gm }
311
+ ];
167
312
 
168
- async function findNearestProjectRoot(startDir, workspaceRoot) {
169
- let current = path.resolve(startDir);
170
- const root = path.resolve(workspaceRoot);
171
- while (current.startsWith(root)) {
172
- if ((await detectWorkspaceKind(current)) === 'project') return current;
173
- if (current === root) break;
174
- const parent = path.dirname(current);
175
- if (parent === current) break;
176
- current = parent;
313
+ for (const { kind, regex } of patterns) {
314
+ for (const match of String(content || '').matchAll(regex)) {
315
+ const name = String(match[1] || '').trim();
316
+ if (!name) continue;
317
+ const start = match.index || 0;
318
+ const openBrace = content.indexOf('{', start);
319
+ const braceRange = openBrace >= 0 ? findBraceRange(content, openBrace) : null;
320
+ const end = braceRange?.end || content.indexOf('\n', start + String(match[0] || '').length);
321
+ const safeEnd = end > start ? end : start + String(match[0] || '').length;
322
+ const body = content.slice(start, safeEnd);
323
+ const startLine = lineNumberForIndex(content, start);
324
+ const endLine = lineNumberForIndex(content, safeEnd);
325
+ const signature = trimInline(String(match[0] || '').replace(/\s*\{\s*$/, '').replace(/\s*:\s*$/, ''), 220);
326
+ const calls = extractCallNames(body);
327
+ definitions.push({
328
+ symbol_id: `${relativePath}#${name}`,
329
+ name,
330
+ type: inferSymbolType(kind),
331
+ file: relativePath,
332
+ range: { start_line: startLine, end_line: endLine },
333
+ signature,
334
+ calls,
335
+ called_by: [],
336
+ imports: clipList(imports, 12),
337
+ writes: extractSemanticWrites(calls),
338
+ emits: extractSemanticEmits(calls, body),
339
+ used_by: []
340
+ });
341
+
342
+ if (kind === 'class' && braceRange) {
343
+ const classBodyStart = openBrace + 1;
344
+ const classBody = content.slice(classBodyStart, braceRange.end - 1);
345
+ for (const methodMatch of classBody.matchAll(/^\s*(?:async\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/gm)) {
346
+ const methodName = String(methodMatch[1] || '').trim();
347
+ if (!methodName || ['if', 'for', 'while', 'switch', 'catch'].includes(methodName)) continue;
348
+ const leadingWhitespace = String(methodMatch[0] || '').search(/\S/);
349
+ const methodStart = classBodyStart + (methodMatch.index || 0) + Math.max(0, leadingWhitespace);
350
+ const methodOpenBrace = content.indexOf('{', methodStart);
351
+ const methodBraceRange = findBraceRange(content, methodOpenBrace);
352
+ const methodEnd = methodBraceRange?.end || methodStart + String(methodMatch[0] || '').length;
353
+ const methodBody = content.slice(methodStart, methodEnd);
354
+ const methodCalls = extractCallNames(methodBody);
355
+ definitions.push({
356
+ symbol_id: `${relativePath}#${name}.${methodName}`,
357
+ name: `${name}.${methodName}`,
358
+ type: 'method',
359
+ file: relativePath,
360
+ range: {
361
+ start_line: lineNumberForIndex(content, methodStart),
362
+ end_line: lineNumberForIndex(content, methodEnd)
363
+ },
364
+ signature: trimInline(`${name}.${String(methodMatch[0] || '').replace(/\s*\{\s*$/, '')}`, 220),
365
+ calls: methodCalls,
366
+ called_by: [],
367
+ imports: clipList(imports, 12),
368
+ writes: extractSemanticWrites(methodCalls),
369
+ emits: extractSemanticEmits(methodCalls, methodBody),
370
+ used_by: []
371
+ });
372
+ }
373
+ }
374
+ }
177
375
  }
178
- return null;
179
- }
180
376
 
181
- async function findProjectRootFromFile(workspaceRoot, relativePath = '') {
182
- const absolutePath = path.resolve(workspaceRoot, String(relativePath || '.'));
183
- const stat = await safeStat(absolutePath);
184
- const probeStart = stat?.isDirectory() ? absolutePath : path.dirname(absolutePath);
185
- return findNearestProjectRoot(probeStart, workspaceRoot);
377
+ return definitions
378
+ .sort((left, right) => left.range.start_line - right.range.start_line || left.name.localeCompare(right.name))
379
+ .slice(0, 200);
186
380
  }
187
381
 
188
- async function findNearestIndexedProjectRoot(startDir, workspaceRoot) {
189
- let current = path.resolve(startDir);
190
- const root = path.resolve(workspaceRoot);
191
- while (current.startsWith(root)) {
192
- const projectMapStat = await safeStat(getProjectMapPath(current));
193
- const fileIndexStat = await safeStat(getFileIndexPath(current));
194
- if (projectMapStat?.isFile() && fileIndexStat?.isFile()) return current;
195
- if (current === root) break;
196
- const parent = path.dirname(current);
197
- if (parent === current) break;
198
- current = parent;
382
+ function enrichSymbolGraph(files) {
383
+ const nextFiles = (Array.isArray(files) ? files : []).map((entry) => ({
384
+ ...entry,
385
+ symbols: Array.isArray(entry.symbols) ? entry.symbols.map((symbol) => ({ ...symbol, called_by: [], used_by: [] })) : []
386
+ }));
387
+ const symbols = nextFiles.flatMap((entry) => entry.symbols || []);
388
+ const byName = new Map();
389
+ for (const symbol of symbols) {
390
+ const shortName = String(symbol.name || '').split('.').pop();
391
+ if (!shortName) continue;
392
+ if (!byName.has(shortName)) byName.set(shortName, []);
393
+ byName.get(shortName).push(symbol);
199
394
  }
200
- return null;
201
- }
202
395
 
203
- async function walkFiles(cwd, start = cwd, out = [], ignoreRules = []) {
204
- const entries = await fs.readdir(start, { withFileTypes: true });
205
- for (const entry of entries) {
206
- const absolutePath = path.join(start, entry.name);
207
- const relativePath = rel(cwd, absolutePath);
208
- if (entry.isDirectory()) {
209
- if (shouldIgnorePath(relativePath, true, ignoreRules)) continue;
210
- await walkFiles(cwd, absolutePath, out, ignoreRules);
211
- continue;
396
+ for (const source of symbols) {
397
+ for (const rawCall of source.calls || []) {
398
+ const callName = String(rawCall || '').split('.').pop();
399
+ const targets = byName.get(callName) || [];
400
+ for (const target of targets) {
401
+ if (!target?.symbol_id || target.symbol_id === source.symbol_id) continue;
402
+ target.called_by = clipList([...(target.called_by || []), source.symbol_id], 32);
403
+ }
212
404
  }
213
- if (shouldIgnorePath(relativePath, false, ignoreRules)) continue;
214
- out.push(absolutePath);
215
405
  }
216
- return out;
217
- }
218
-
219
- function categorizeDirectory(relativeDir) {
220
- const text = String(relativeDir || '').toLowerCase();
221
- if (!text || text === '.') return 'root';
222
- if (/(^|\/)(src|app|apps)\b/.test(text)) return 'source';
223
- if (/(^|\/)(test|tests|__tests__|spec)\b/.test(text)) return 'test';
224
- if (/(^|\/)(scripts|bin)\b/.test(text)) return 'script';
225
- if (/(^|\/)(config|configs)\b/.test(text)) return 'config';
226
- return 'other';
227
- }
228
406
 
229
- function extractMatches(regex, text, group = 1) {
230
- const out = [];
231
- for (const match of String(text || '').matchAll(regex)) {
232
- const value = String(match[group] || '').trim();
233
- if (value) out.push(value);
234
- }
235
- return out;
407
+ return nextFiles;
236
408
  }
237
409
 
238
410
  function buildFileEntry(relativePath, content, stat) {
239
411
  const ext = path.extname(relativePath).toLowerCase();
240
412
  const imports = clipList([
241
- ...extractMatches(/import\s+(?:[^'"]*from\s+)?['"]([^'"]+)['"]/g, content),
242
- ...extractMatches(/require\(\s*['"]([^'"]+)['"]\s*\)/g, content),
243
- ...extractMatches(/\buse\s+([A-Za-z0-9_:\\]+)/g, ext === '.rs' ? content : '')
244
- ]);
245
- const exports = clipList([
246
- ...extractMatches(/export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/g, content),
247
- ...extractMatches(/export\s+class\s+([A-Za-z0-9_$]+)/g, content),
248
- ...extractMatches(/export\s+const\s+([A-Za-z0-9_$]+)/g, content),
249
- ...extractMatches(/module\.exports\s*=\s*([A-Za-z0-9_$]+)/g, content),
250
- ...extractMatches(/exports\.([A-Za-z0-9_$]+)/g, content)
251
- ]);
252
- const functions = clipList([
253
- ...extractMatches(/\bfunction\s+([A-Za-z0-9_$]+)/g, content),
254
- ...extractMatches(/\bdef\s+([A-Za-z0-9_]+)/g, content),
255
- ...extractMatches(/\bfunc\s+([A-Za-z0-9_]+)/g, content),
256
- ...extractMatches(/\bfn\s+([A-Za-z0-9_]+)/g, content),
257
- ...extractMatches(/^\s*(?:public|private|protected|internal)?\s*(?:static\s+)?[A-Za-z0-9_<>,[\]?]+\s+([A-Za-z0-9_]+)\s*\(/gm, content),
258
- ...extractMatches(/^\s*function\s+([A-Za-z0-9_]+)/gm, content),
259
- ...extractMatches(/^\s*def\s+([A-Za-z0-9_]+)/gm, content)
260
- ]);
261
- const classes = clipList([
262
- ...extractMatches(/\bclass\s+([A-Za-z0-9_$]+)/g, content)
263
- ]);
264
- const calls = clipList([
265
- ...extractMatches(/\b([A-Za-z0-9_$]+)\s*\(/g, content).filter((name) => !['if', 'for', 'while', 'switch', 'return', 'function', 'class', 'catch'].includes(name))
266
- ], 64);
413
+ ...extractMatches(/import\s+(?:[^'"]*from\s+)?['"]([^'"]+)['"]/g, content),
414
+ ...extractMatches(/require\(\s*['"]([^'"]+)['"]\s*\)/g, content),
415
+ ...extractMatches(/\buse\s+([A-Za-z0-9_:\\]+)/g, ext === '.rs' ? content : '')
416
+ ]);
417
+ const exports = clipList([
418
+ ...extractMatches(/export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/g, content),
419
+ ...extractMatches(/export\s+class\s+([A-Za-z0-9_$]+)/g, content),
420
+ ...extractMatches(/export\s+const\s+([A-Za-z0-9_$]+)/g, content),
421
+ ...extractMatches(/module\.exports\s*=\s*([A-Za-z0-9_$]+)/g, content),
422
+ ...extractMatches(/exports\.([A-Za-z0-9_$]+)/g, content)
423
+ ]);
424
+ const functions = clipList([
425
+ ...extractMatches(/\bfunction\s+([A-Za-z0-9_$]+)/g, content),
426
+ ...extractMatches(/\bdef\s+([A-Za-z0-9_]+)/g, content),
427
+ ...extractMatches(/\bfunc\s+([A-Za-z0-9_]+)/g, content),
428
+ ...extractMatches(/\bfn\s+([A-Za-z0-9_]+)/g, content),
429
+ ...extractMatches(/^\s*(?:public|private|protected|internal)?\s*(?:static\s+)?[A-Za-z0-9_<>,[\]?]+\s+([A-Za-z0-9_]+)\s*\(/gm, content),
430
+ ...extractMatches(/^\s*function\s+([A-Za-z0-9_]+)/gm, content),
431
+ ...extractMatches(/^\s*def\s+([A-Za-z0-9_]+)/gm, content)
432
+ ]);
433
+ const classes = clipList([
434
+ ...extractMatches(/\bclass\s+([A-Za-z0-9_$]+)/g, content)
435
+ ]);
436
+ const calls = extractCallNames(content);
437
+ const symbols = extractSymbolDefinitions(relativePath, content, imports);
267
438
 
268
439
  return {
269
- file: relativePath,
270
- language: LANGUAGE_BY_EXT[ext] || 'text',
271
- hash: sha256(content),
272
- size: Number(stat?.size || content.length || 0),
273
- mtimeMs: Number(stat?.mtimeMs || 0),
274
- imports,
275
- exports,
440
+ file: relativePath,
441
+ language: LANGUAGE_BY_EXT[ext] || 'text',
442
+ hash: sha256(content),
443
+ size: Number(stat?.size || content.length || 0),
444
+ mtimeMs: Number(stat?.mtimeMs || 0),
445
+ imports,
446
+ exports,
276
447
  functions,
277
448
  classes,
278
- calls
449
+ calls,
450
+ symbols
279
451
  };
280
452
  }
281
-
282
- async function scanProject(cwd) {
283
- const workspaceKind = await detectWorkspaceKind(cwd);
284
- if (workspaceKind !== 'project') {
285
- return {
286
- workspaceKind,
287
- projectMap: null,
288
- fileIndex: null,
289
- ignoreRules: []
290
- };
291
- }
292
-
293
- const { gitignoreRules, llmignoreRules, combinedRules } = await readProjectIgnoreRules(cwd);
294
- const allFiles = await walkFiles(cwd, cwd, [], combinedRules);
295
- const relativeFiles = allFiles.map((filePath) => rel(cwd, filePath));
296
- const sourceFiles = allFiles.filter((filePath) => SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase()));
297
-
298
- const packageJson = await safeReadJson(path.join(cwd, 'package.json'), null);
299
- const tsconfigExists = Boolean(await safeStat(path.join(cwd, 'tsconfig.json')));
300
- const sourceRoots = clipList(relativeFiles.filter((value) => /^(src|app|apps)\b/.test(value)).map((value) => value.split('/')[0]), 12);
301
- const testRoots = clipList(relativeFiles.filter((value) => /^(tests|test|__tests__)\b/.test(value)).map((value) => value.split('/')[0]), 12);
302
- const entryCandidates = clipList(
303
- relativeFiles.filter((value) => /(^|\/)(main|index|server|app)\.(js|jsx|mjs|cjs|ts|tsx|py|go|rs|java|cs|php|rb)$/.test(value)),
304
- 16
305
- );
306
- const languages = clipList(sourceFiles.map((filePath) => LANGUAGE_BY_EXT[path.extname(filePath).toLowerCase()] || '').filter(Boolean), 16);
307
- const importantFiles = clipList(
308
- relativeFiles.filter((value) => ['package.json', 'tsconfig.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'composer.json', 'Gemfile'].includes(value)),
309
- 16
310
- );
311
- const packageManagers = clipList([
312
- packageJson ? 'npm' : '',
313
- relativeFiles.includes('bun.lockb') ? 'bun' : '',
314
- relativeFiles.includes('pnpm-lock.yaml') ? 'pnpm' : '',
315
- relativeFiles.includes('yarn.lock') ? 'yarn' : ''
316
- ].filter(Boolean));
317
- const frameworkHints = clipList([
318
- packageJson?.dependencies?.react || packageJson?.devDependencies?.react ? 'react' : '',
319
- packageJson?.dependencies?.express ? 'express' : '',
320
- packageJson?.dependencies?.vue ? 'vue' : '',
321
- packageJson?.dependencies?.next ? 'next' : '',
322
- tsconfigExists ? 'typescript' : ''
323
- ].filter(Boolean));
324
-
325
- const directories = {};
326
- for (const value of relativeFiles) {
327
- const dir = path.posix.dirname(value);
328
- if (!dir || dir === '.') continue;
329
- if (!(dir in directories)) directories[dir] = categorizeDirectory(dir);
330
- }
331
-
332
- const files = [];
453
+
454
+ async function scanProject(cwd) {
455
+ const workspaceKind = await detectWorkspaceKind(cwd);
456
+ if (workspaceKind !== 'project') {
457
+ return {
458
+ workspaceKind,
459
+ projectMap: null,
460
+ fileIndex: null,
461
+ ignoreRules: []
462
+ };
463
+ }
464
+
465
+ const { gitignoreRules, llmignoreRules, combinedRules } = await readProjectIgnoreRules(cwd);
466
+ const allFiles = await walkFiles(cwd, cwd, [], combinedRules);
467
+ const relativeFiles = allFiles.map((filePath) => rel(cwd, filePath));
468
+ const sourceFiles = allFiles.filter((filePath) => SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase()));
469
+
470
+ const packageJson = await safeReadJson(path.join(cwd, 'package.json'), null);
471
+ const tsconfigExists = Boolean(await safeStat(path.join(cwd, 'tsconfig.json')));
472
+ const sourceRoots = clipList(relativeFiles.filter((value) => /^(src|app|apps)\b/.test(value)).map((value) => value.split('/')[0]), 12);
473
+ const testRoots = clipList(relativeFiles.filter((value) => /^(tests|test|__tests__)\b/.test(value)).map((value) => value.split('/')[0]), 12);
474
+ const entryCandidates = clipList(
475
+ relativeFiles.filter((value) => /(^|\/)(main|index|server|app)\.(js|jsx|mjs|cjs|ts|tsx|py|go|rs|java|cs|php|rb)$/.test(value)),
476
+ 16
477
+ );
478
+ const languages = clipList(sourceFiles.map((filePath) => LANGUAGE_BY_EXT[path.extname(filePath).toLowerCase()] || '').filter(Boolean), 16);
479
+ const importantFiles = clipList(
480
+ relativeFiles.filter((value) => ['package.json', 'tsconfig.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'composer.json', 'Gemfile'].includes(value)),
481
+ 16
482
+ );
483
+ const packageManagers = clipList([
484
+ packageJson ? 'npm' : '',
485
+ relativeFiles.includes('bun.lockb') ? 'bun' : '',
486
+ relativeFiles.includes('pnpm-lock.yaml') ? 'pnpm' : '',
487
+ relativeFiles.includes('yarn.lock') ? 'yarn' : ''
488
+ ].filter(Boolean));
489
+ const frameworkHints = clipList([
490
+ packageJson?.dependencies?.react || packageJson?.devDependencies?.react ? 'react' : '',
491
+ packageJson?.dependencies?.express ? 'express' : '',
492
+ packageJson?.dependencies?.vue ? 'vue' : '',
493
+ packageJson?.dependencies?.next ? 'next' : '',
494
+ tsconfigExists ? 'typescript' : ''
495
+ ].filter(Boolean));
496
+
497
+ const directories = {};
498
+ for (const value of relativeFiles) {
499
+ const dir = path.posix.dirname(value);
500
+ if (!dir || dir === '.') continue;
501
+ if (!(dir in directories)) directories[dir] = categorizeDirectory(dir);
502
+ }
503
+
504
+ let files = [];
333
505
  for (const filePath of sourceFiles) {
334
506
  const content = await fs.readFile(filePath, 'utf8');
335
507
  const stat = await fs.stat(filePath);
336
508
  files.push(buildFileEntry(rel(cwd, filePath), content, stat));
337
509
  }
338
-
339
- return {
340
- workspaceKind,
341
- projectMap: {
342
- projectRoot: cwd,
343
- workspaceKind,
344
- languages,
345
- packageManagers,
346
- importantFiles,
347
- sourceRoots,
348
- testRoots,
349
- entryCandidates,
350
- frameworkHints,
351
- directories,
352
- gitignoreEnabled: gitignoreRules.length > 0,
353
- llmignoreEnabled: llmignoreRules.length > 0,
354
- updatedAt: new Date().toISOString()
355
- },
356
- fileIndex: {
357
- updatedAt: new Date().toISOString(),
358
- files
359
- },
360
- ignoreRules: combinedRules
361
- };
362
- }
363
-
364
- export async function initializeProjectIndex(cwd = process.cwd()) {
365
- const targetRoot = (await findNearestProjectRoot(cwd, cwd)) || path.resolve(cwd);
366
- const cacheKey = targetRoot;
367
- if (initCache.has(cacheKey)) return initCache.get(cacheKey);
368
- const promise = (async () => {
369
- const workspaceDir = getProjectWorkspaceDir(cwd);
370
- await fs.mkdir(workspaceDir, { recursive: true });
371
- const { workspaceKind, projectMap, fileIndex } = await scanProject(targetRoot);
372
- if (workspaceKind !== 'project' || !projectMap || !fileIndex) {
373
- return {
374
- workspaceKind,
375
- projectRoot: null,
376
- projectMap: null,
377
- fileIndex: null,
378
- summary: '',
379
- skipped: true
380
- };
381
- }
382
- await fs.mkdir(getProjectIndexDir(targetRoot), { recursive: true });
383
- await writeJson(getProjectMapPath(targetRoot), projectMap);
384
- await writeJson(getFileIndexPath(targetRoot), fileIndex);
385
- return {
386
- workspaceKind,
387
- projectRoot: targetRoot,
388
- projectMap,
389
- fileIndex,
390
- summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
391
- };
392
- })();
393
- initCache.set(cacheKey, promise);
394
- try {
395
- return await promise;
396
- } catch (error) {
397
- initCache.delete(cacheKey);
398
- throw error;
399
- }
400
- }
401
-
402
- export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '') {
403
- if (!relativePath) return null;
404
- const workspaceDir = getProjectWorkspaceDir(cwd);
405
- await fs.mkdir(workspaceDir, { recursive: true });
406
- const projectRoot = await findProjectRootFromFile(cwd, relativePath);
407
- if (!projectRoot) return null;
408
- const fileIndexPath = getFileIndexPath(projectRoot);
409
- const { combinedRules } = await readProjectIgnoreRules(projectRoot);
410
- const absolutePath = path.join(cwd, relativePath);
411
- const stat = await safeStat(absolutePath);
412
- let action = 'updated';
413
- const projectRelativePath = path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
414
- const current = await safeReadJson(fileIndexPath, { updatedAt: '', files: [] });
415
- const files = Array.isArray(current.files) ? [...current.files] : [];
416
- const index = files.findIndex((entry) => entry.file === projectRelativePath);
417
-
418
- if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()), combinedRules)) {
419
- if (index >= 0) files.splice(index, 1);
420
- action = 'removed';
421
- } else if (!stat || !stat.isFile()) {
422
- if (index >= 0) files.splice(index, 1);
423
- action = 'removed';
424
- } else {
425
- const ext = path.extname(relativePath).toLowerCase();
426
- if (!SOURCE_EXTENSIONS.has(ext)) {
427
- if (index >= 0) files.splice(index, 1);
428
- action = 'removed';
429
- } else {
430
- const content = await fs.readFile(absolutePath, 'utf8');
431
- const nextEntry = buildFileEntry(projectRelativePath, content, stat);
432
- if (index >= 0) {
433
- files[index] = nextEntry;
434
- } else {
435
- files.push(nextEntry);
436
- action = 'added';
437
- }
438
- }
439
- }
440
-
510
+ files = enrichSymbolGraph(files);
511
+
512
+ return {
513
+ workspaceKind,
514
+ projectMap: {
515
+ projectRoot: cwd,
516
+ workspaceKind,
517
+ languages,
518
+ packageManagers,
519
+ importantFiles,
520
+ sourceRoots,
521
+ testRoots,
522
+ entryCandidates,
523
+ frameworkHints,
524
+ directories,
525
+ gitignoreEnabled: gitignoreRules.length > 0,
526
+ llmignoreEnabled: llmignoreRules.length > 0,
527
+ updatedAt: new Date().toISOString()
528
+ },
529
+ fileIndex: {
530
+ updatedAt: new Date().toISOString(),
531
+ files
532
+ },
533
+ ignoreRules: combinedRules
534
+ };
535
+ }
536
+
537
+ export async function initializeProjectIndex(cwd = process.cwd()) {
538
+ const targetRoot = (await findNearestProjectRoot(cwd, cwd)) || path.resolve(cwd);
539
+ const cacheKey = targetRoot;
540
+ if (initCache.has(cacheKey)) return initCache.get(cacheKey);
541
+ const promise = (async () => {
542
+ const workspaceDir = getProjectWorkspaceDir(cwd);
543
+ await fs.mkdir(workspaceDir, { recursive: true });
544
+ const { workspaceKind, projectMap, fileIndex } = await scanProject(targetRoot);
545
+ if (workspaceKind !== 'project' || !projectMap || !fileIndex) {
546
+ return {
547
+ workspaceKind,
548
+ projectRoot: null,
549
+ projectMap: null,
550
+ fileIndex: null,
551
+ summary: '',
552
+ skipped: true
553
+ };
554
+ }
555
+ await fs.mkdir(getProjectIndexDir(targetRoot), { recursive: true });
556
+ await writeJson(getProjectMapPath(targetRoot), projectMap);
557
+ await writeJson(getFileIndexPath(targetRoot), fileIndex);
558
+ return {
559
+ workspaceKind,
560
+ projectRoot: targetRoot,
561
+ projectMap,
562
+ fileIndex,
563
+ summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
564
+ };
565
+ })();
566
+ initCache.set(cacheKey, promise);
567
+ try {
568
+ return await promise;
569
+ } catch (error) {
570
+ initCache.delete(cacheKey);
571
+ throw error;
572
+ }
573
+ }
574
+
575
+ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '') {
576
+ if (!relativePath) return null;
577
+ const workspaceDir = getProjectWorkspaceDir(cwd);
578
+ await fs.mkdir(workspaceDir, { recursive: true });
579
+ const projectRoot = await findProjectRootFromFile(cwd, relativePath);
580
+ if (!projectRoot) return null;
581
+ const fileIndexPath = getFileIndexPath(projectRoot);
582
+ const { combinedRules } = await readProjectIgnoreRules(projectRoot);
583
+ const absolutePath = path.join(cwd, relativePath);
584
+ const stat = await safeStat(absolutePath);
585
+ let action = 'updated';
586
+ const projectRelativePath = path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
587
+ const current = await safeReadJson(fileIndexPath, { updatedAt: '', files: [] });
588
+ const files = Array.isArray(current.files) ? [...current.files] : [];
589
+ const index = files.findIndex((entry) => entry.file === projectRelativePath);
590
+
591
+ if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()), combinedRules)) {
592
+ if (index >= 0) files.splice(index, 1);
593
+ action = 'removed';
594
+ } else if (!stat || !stat.isFile()) {
595
+ if (index >= 0) files.splice(index, 1);
596
+ action = 'removed';
597
+ } else {
598
+ const ext = path.extname(relativePath).toLowerCase();
599
+ if (!SOURCE_EXTENSIONS.has(ext)) {
600
+ if (index >= 0) files.splice(index, 1);
601
+ action = 'removed';
602
+ } else {
603
+ const content = await fs.readFile(absolutePath, 'utf8');
604
+ const nextEntry = buildFileEntry(projectRelativePath, content, stat);
605
+ if (index >= 0) {
606
+ files[index] = nextEntry;
607
+ } else {
608
+ files.push(nextEntry);
609
+ action = 'added';
610
+ }
611
+ }
612
+ }
613
+
614
+ const enrichedFiles = enrichSymbolGraph(files);
441
615
  await writeJson(fileIndexPath, {
442
616
  updatedAt: new Date().toISOString(),
443
- files: files.sort((left, right) => left.file.localeCompare(right.file))
617
+ files: enrichedFiles.sort((left, right) => left.file.localeCompare(right.file))
444
618
  });
445
-
446
- return {
447
- path: projectRelativePath,
448
- projectRoot,
449
- action,
450
- summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
451
- };
452
- }
453
-
454
- export async function buildProjectContextSnippet(cwd = process.cwd(), userText = '') {
455
- const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
456
- if (!indexedRoot) return '';
457
-
458
- const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
459
- const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
460
- if (!projectMap || !Array.isArray(fileIndex?.files)) return '';
461
-
462
- const lines = [
463
- 'Project Context:',
464
- `- project_root: ${indexedRoot}`,
465
- `- languages: ${(projectMap.languages || []).slice(0, 6).join(', ') || 'unknown'}`,
466
- `- source_roots: ${(projectMap.sourceRoots || []).slice(0, 6).join(', ') || 'none'}`,
467
- `- test_roots: ${(projectMap.testRoots || []).slice(0, 6).join(', ') || 'none'}`,
468
- `- entry_candidates: ${(projectMap.entryCandidates || []).slice(0, 6).join(', ') || 'none'}`,
469
- `- framework_hints: ${(projectMap.frameworkHints || []).slice(0, 6).join(', ') || 'none'}`
470
- ];
471
-
472
- const tokens = tokenizeQuery(userText);
473
- const scored = [];
474
- for (const entry of fileIndex.files) {
475
- let score = 0;
476
- const fileText = String(entry.file || '').toLowerCase();
477
- for (const token of tokens) {
478
- if (fileText.includes(token)) score += 5;
479
- if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) score += 4;
480
- if ((entry.functions || []).some((value) => String(value).toLowerCase() === token)) score += 4;
481
- if ((entry.classes || []).some((value) => String(value).toLowerCase() === token)) score += 4;
482
- if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) score += 1;
483
- }
484
- if (score > 0) scored.push({ entry, score });
485
- }
486
- scored.sort((left, right) => right.score - left.score || String(left.entry.file).localeCompare(String(right.entry.file)));
487
- const selected = scored.slice(0, PROJECT_CONTEXT_MAX_FILES).map((item) => item.entry);
488
- if (selected.length > 0) {
489
- lines.push('- relevant_files:');
619
+
620
+ return {
621
+ path: projectRelativePath,
622
+ projectRoot,
623
+ action,
624
+ summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
625
+ };
626
+ }
627
+
628
+ export async function buildProjectContextSnippet(cwd = process.cwd(), userText = '') {
629
+ const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
630
+ if (!indexedRoot) return '';
631
+
632
+ const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
633
+ const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
634
+ if (!projectMap || !Array.isArray(fileIndex?.files)) return '';
635
+
636
+ const lines = [
637
+ 'Project Context:',
638
+ `- project_root: ${indexedRoot}`,
639
+ `- languages: ${(projectMap.languages || []).slice(0, 6).join(', ') || 'unknown'}`,
640
+ `- source_roots: ${(projectMap.sourceRoots || []).slice(0, 6).join(', ') || 'none'}`,
641
+ `- test_roots: ${(projectMap.testRoots || []).slice(0, 6).join(', ') || 'none'}`,
642
+ `- entry_candidates: ${(projectMap.entryCandidates || []).slice(0, 6).join(', ') || 'none'}`,
643
+ `- framework_hints: ${(projectMap.frameworkHints || []).slice(0, 6).join(', ') || 'none'}`
644
+ ];
645
+
646
+ const tokens = tokenizeQuery(userText);
647
+ const scored = [];
648
+ for (const entry of fileIndex.files) {
649
+ let score = 0;
650
+ const fileText = String(entry.file || '').toLowerCase();
651
+ for (const token of tokens) {
652
+ if (fileText.includes(token)) score += 5;
653
+ if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) score += 4;
654
+ if ((entry.functions || []).some((value) => String(value).toLowerCase() === token)) score += 4;
655
+ if ((entry.classes || []).some((value) => String(value).toLowerCase() === token)) score += 4;
656
+ if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) score += 1;
657
+ }
658
+ if (score > 0) scored.push({ entry, score });
659
+ }
660
+ scored.sort((left, right) => right.score - left.score || String(left.entry.file).localeCompare(String(right.entry.file)));
661
+ const selected = scored.slice(0, PROJECT_CONTEXT_MAX_FILES).map((item) => item.entry);
662
+ if (selected.length > 0) {
663
+ lines.push('- relevant_files:');
490
664
  for (const entry of selected) {
665
+ const symbolText = (entry.symbols || [])
666
+ .slice(0, 4)
667
+ .map((symbol) => `${symbol.name}@${symbol.range?.start_line || '?'}`)
668
+ .join(', ');
491
669
  lines.push(
492
- ` - ${entry.file} :: exports=[${(entry.exports || []).slice(0, 4).join(', ')}] functions=[${(entry.functions || []).slice(0, 4).join(', ')}] classes=[${(entry.classes || []).slice(0, 4).join(', ')}]`
670
+ ` - ${entry.file} :: symbols=[${symbolText}] exports=[${(entry.exports || []).slice(0, 4).join(', ')}] classes=[${(entry.classes || []).slice(0, 4).join(', ')}]`
493
671
  );
494
672
  }
495
673
  }
496
-
497
- const snippet = trimMultiline(lines.join('\n'));
498
- return snippet;
499
- }
500
-
501
- export async function queryProjectIndex(cwd = process.cwd(), args = {}) {
502
- const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
503
- if (!indexedRoot) {
504
- return {
505
- query: String(args?.query || '').trim(),
506
- project_root: '',
507
- project_map: null,
508
- matches: []
509
- };
510
- }
511
-
512
- const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
513
- const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
514
- const query = String(args?.query || '').trim();
515
- const pathPrefix = normalizeRelativePath(args?.path || args?.path_prefix || '');
516
- const languageFilter = String(args?.language || '').trim().toLowerCase();
517
- const maxResults = Math.max(1, Math.min(20, Number(args?.max_results || 8) || 8));
518
- const files = Array.isArray(fileIndex?.files) ? fileIndex.files : [];
519
- const tokens = tokenizeQuery(query);
520
-
674
+
675
+ const snippet = trimMultiline(lines.join('\n'));
676
+ return snippet;
677
+ }
678
+
679
+ export async function queryProjectIndex(cwd = process.cwd(), args = {}) {
680
+ const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
681
+ if (!indexedRoot) {
682
+ return {
683
+ query: String(args?.query || '').trim(),
684
+ project_root: '',
685
+ project_map: null,
686
+ matches: []
687
+ };
688
+ }
689
+
690
+ const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
691
+ const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
692
+ const query = String(args?.query || '').trim();
693
+ const pathPrefix = normalizeRelativePath(args?.path || args?.path_prefix || '');
694
+ const languageFilter = String(args?.language || '').trim().toLowerCase();
695
+ const maxResults = Math.max(1, Math.min(20, Number(args?.max_results || 8) || 8));
696
+ const files = Array.isArray(fileIndex?.files) ? fileIndex.files : [];
697
+ const tokens = tokenizeQuery(query);
698
+
521
699
  const matches = [];
522
700
  for (const entry of files) {
523
- const relativePath = String(entry?.file || '');
524
- if (!relativePath) continue;
525
- if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
526
- if (languageFilter && String(entry?.language || '').toLowerCase() !== languageFilter) continue;
527
-
528
- let score = 0;
529
- const reasons = [];
530
- const fileText = relativePath.toLowerCase();
701
+ const relativePath = String(entry?.file || '');
702
+ if (!relativePath) continue;
703
+ if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
704
+ if (languageFilter && String(entry?.language || '').toLowerCase() !== languageFilter) continue;
705
+
706
+ let score = 0;
707
+ const reasons = [];
708
+ const fileText = relativePath.toLowerCase();
709
+ const symbolMatches = [];
531
710
  for (const token of tokens) {
532
711
  if (!token) continue;
533
712
  if (fileText.includes(token)) {
534
713
  score += 5;
535
- reasons.push(`path:${token}`);
536
- }
537
- if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) {
538
- score += 4;
539
- reasons.push(`export:${token}`);
540
- }
541
- if ((entry.functions || []).some((value) => String(value).toLowerCase().includes(token))) {
542
- score += 4;
543
- reasons.push(`function:${token}`);
544
- }
545
- if ((entry.classes || []).some((value) => String(value).toLowerCase().includes(token))) {
546
- score += 4;
547
- reasons.push(`class:${token}`);
548
- }
714
+ reasons.push(`path:${token}`);
715
+ }
716
+ if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) {
717
+ score += 4;
718
+ reasons.push(`export:${token}`);
719
+ }
720
+ if ((entry.functions || []).some((value) => String(value).toLowerCase().includes(token))) {
721
+ score += 4;
722
+ reasons.push(`function:${token}`);
723
+ }
724
+ if ((entry.classes || []).some((value) => String(value).toLowerCase().includes(token))) {
725
+ score += 4;
726
+ reasons.push(`class:${token}`);
727
+ }
549
728
  if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) {
550
729
  score += 2;
551
730
  reasons.push(`import:${token}`);
552
731
  }
732
+ for (const symbol of entry.symbols || []) {
733
+ const nameText = String(symbol.name || '').toLowerCase();
734
+ const idText = String(symbol.symbol_id || '').toLowerCase();
735
+ if (nameText.includes(token) || idText.includes(token)) {
736
+ score += 6;
737
+ reasons.push(`symbol:${token}`);
738
+ symbolMatches.push(symbol);
739
+ } else if ((symbol.calls || []).some((value) => String(value).toLowerCase().includes(token))) {
740
+ score += 3;
741
+ reasons.push(`calls:${token}`);
742
+ symbolMatches.push(symbol);
743
+ } else if ((symbol.called_by || []).some((value) => String(value).toLowerCase().includes(token))) {
744
+ score += 3;
745
+ reasons.push(`called_by:${token}`);
746
+ symbolMatches.push(symbol);
747
+ }
748
+ }
553
749
  }
554
-
555
- if (!query) {
556
- if ((projectMap?.entryCandidates || []).includes(relativePath)) score += 3;
557
- if ((projectMap?.importantFiles || []).includes(relativePath)) score += 2;
558
- if (String(relativePath).startsWith('src/')) score += 1;
559
- }
560
-
561
- if (score <= 0 && query) continue;
562
- matches.push({
563
- file: relativePath,
564
- language: entry.language || 'text',
565
- score,
566
- reasons: clipList(reasons, 8),
750
+
751
+ if (!query) {
752
+ if ((projectMap?.entryCandidates || []).includes(relativePath)) score += 3;
753
+ if ((projectMap?.importantFiles || []).includes(relativePath)) score += 2;
754
+ if (String(relativePath).startsWith('src/')) score += 1;
755
+ }
756
+
757
+ if (score <= 0 && query) continue;
758
+ matches.push({
759
+ file: relativePath,
760
+ language: entry.language || 'text',
761
+ score,
762
+ reasons: clipList(reasons, 8),
567
763
  exports: clipList(entry.exports || [], 6),
568
764
  functions: clipList(entry.functions || [], 6),
569
765
  classes: clipList(entry.classes || [], 6),
570
- imports: clipList(entry.imports || [], 6)
766
+ imports: clipList(entry.imports || [], 6),
767
+ symbols: clipList((symbolMatches.length > 0 ? symbolMatches : entry.symbols || []).map((symbol) => ({
768
+ symbol_id: symbol.symbol_id,
769
+ name: symbol.name,
770
+ type: symbol.type,
771
+ range: symbol.range,
772
+ signature: symbol.signature,
773
+ calls: clipList(symbol.calls || [], 8),
774
+ called_by: clipList(symbol.called_by || [], 8),
775
+ imports: clipList(symbol.imports || [], 6),
776
+ writes: clipList(symbol.writes || [], 6),
777
+ emits: clipList(symbol.emits || [], 6)
778
+ })), 6)
571
779
  });
572
780
  }
573
-
574
- matches.sort((left, right) => right.score - left.score || String(left.file).localeCompare(String(right.file)));
575
-
576
- return {
577
- query,
578
- project_root: indexedRoot,
579
- project_map: projectMap
580
- ? {
581
- workspace_kind: projectMap.workspaceKind || 'project',
582
- languages: clipList(projectMap.languages || [], 8),
583
- package_managers: clipList(projectMap.packageManagers || [], 8),
584
- important_files: clipList(projectMap.importantFiles || [], 8),
585
- source_roots: clipList(projectMap.sourceRoots || [], 8),
586
- test_roots: clipList(projectMap.testRoots || [], 8),
587
- entry_candidates: clipList(projectMap.entryCandidates || [], 8),
588
- framework_hints: clipList(projectMap.frameworkHints || [], 8),
589
- gitignore_enabled: Boolean(projectMap.gitignoreEnabled),
590
- llmignore_enabled: Boolean(projectMap.llmignoreEnabled)
591
- }
592
- : null,
593
- matches: matches.slice(0, maxResults)
594
- };
595
- }
781
+
782
+ matches.sort((left, right) => right.score - left.score || String(left.file).localeCompare(String(right.file)));
783
+
784
+ return {
785
+ query,
786
+ project_root: indexedRoot,
787
+ project_map: projectMap
788
+ ? {
789
+ workspace_kind: projectMap.workspaceKind || 'project',
790
+ languages: clipList(projectMap.languages || [], 8),
791
+ package_managers: clipList(projectMap.packageManagers || [], 8),
792
+ important_files: clipList(projectMap.importantFiles || [], 8),
793
+ source_roots: clipList(projectMap.sourceRoots || [], 8),
794
+ test_roots: clipList(projectMap.testRoots || [], 8),
795
+ entry_candidates: clipList(projectMap.entryCandidates || [], 8),
796
+ framework_hints: clipList(projectMap.frameworkHints || [], 8),
797
+ gitignore_enabled: Boolean(projectMap.gitignoreEnabled),
798
+ llmignore_enabled: Boolean(projectMap.llmignoreEnabled)
799
+ }
800
+ : null,
801
+ matches: matches.slice(0, maxResults)
802
+ };
803
+ }