codemini-cli 0.5.10 → 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 -588
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-BK75hMb2.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 -77
  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 -307
  24. package/src/core/chat-runtime.js +6261 -6253
  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 -289
  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 -548
  39. package/src/core/project-instructions.js +98 -98
  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 -323
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -52
  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-Dg9qh8mg.js +0 -1
@@ -1,614 +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 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()}...`;
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;
64
256
  }
65
257
 
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');
258
+ function lineNumberForIndex(content, index) {
259
+ return String(content || '').slice(0, Math.max(0, index)).split(/\r?\n/).length;
69
260
  }
70
261
 
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;
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 };
85
271
  }
86
- if (ch === '?') {
87
- regexBody += '[^/]';
88
- continue;
89
- }
90
- regexBody += escapeRegex(ch);
91
272
  }
92
- return new RegExp(`^${regexBody}$`);
273
+ return null;
93
274
  }
94
275
 
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
- }
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';
139
281
  }
140
282
 
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
- };
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
+ );
151
289
  }
152
290
 
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));
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);
162
293
  }
163
294
 
164
- function shouldIgnorePath(relativePath, isDirectory, gitignoreRules = []) {
165
- const normalizedPath = normalizeRelativePath(relativePath);
166
- if (!normalizedPath) return false;
167
- const topName = normalizedPath.split('/')[0];
168
- if (topName && SKIP_DIRS.has(topName)) return true;
169
- let ignored = false;
170
- for (const rule of gitignoreRules) {
171
- if (!matchesGitignoreRule(rule, normalizedPath, isDirectory)) continue;
172
- ignored = !rule.negated;
173
- }
174
- 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);
175
301
  }
176
302
 
177
- async function detectWorkspaceKind(cwd) {
178
- const gitDir = await safeStat(path.join(cwd, '.git'));
179
- if (gitDir?.isDirectory()) return 'project';
180
- for (const marker of PROJECT_MARKER_FILES) {
181
- const stat = await safeStat(path.join(cwd, marker));
182
- if (stat?.isFile()) return 'project';
183
- }
184
- return 'directory';
185
- }
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
+ ];
186
312
 
187
- async function findNearestProjectRoot(startDir, workspaceRoot) {
188
- let current = path.resolve(startDir);
189
- const root = path.resolve(workspaceRoot);
190
- while (current.startsWith(root)) {
191
- if ((await detectWorkspaceKind(current)) === 'project') return current;
192
- if (current === root) break;
193
- const parent = path.dirname(current);
194
- if (parent === current) break;
195
- 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
+ }
196
375
  }
197
- return null;
198
- }
199
376
 
200
- async function findProjectRootFromFile(workspaceRoot, relativePath = '') {
201
- const absolutePath = path.resolve(workspaceRoot, String(relativePath || '.'));
202
- const stat = await safeStat(absolutePath);
203
- const probeStart = stat?.isDirectory() ? absolutePath : path.dirname(absolutePath);
204
- 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);
205
380
  }
206
381
 
207
- async function findNearestIndexedProjectRoot(startDir, workspaceRoot) {
208
- let current = path.resolve(startDir);
209
- const root = path.resolve(workspaceRoot);
210
- while (current.startsWith(root)) {
211
- const projectMapStat = await safeStat(getProjectMapPath(current));
212
- const fileIndexStat = await safeStat(getFileIndexPath(current));
213
- if (projectMapStat?.isFile() && fileIndexStat?.isFile()) return current;
214
- if (current === root) break;
215
- const parent = path.dirname(current);
216
- if (parent === current) break;
217
- 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);
218
394
  }
219
- return null;
220
- }
221
395
 
222
- async function walkFiles(cwd, start = cwd, out = [], ignoreRules = []) {
223
- const entries = await fs.readdir(start, { withFileTypes: true });
224
- for (const entry of entries) {
225
- const absolutePath = path.join(start, entry.name);
226
- const relativePath = rel(cwd, absolutePath);
227
- if (entry.isDirectory()) {
228
- if (shouldIgnorePath(relativePath, true, ignoreRules)) continue;
229
- await walkFiles(cwd, absolutePath, out, ignoreRules);
230
- 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
+ }
231
404
  }
232
- if (shouldIgnorePath(relativePath, false, ignoreRules)) continue;
233
- out.push(absolutePath);
234
405
  }
235
- return out;
236
- }
237
406
 
238
- function categorizeDirectory(relativeDir) {
239
- const text = String(relativeDir || '').toLowerCase();
240
- if (!text || text === '.') return 'root';
241
- if (/(^|\/)(src|app|apps)\b/.test(text)) return 'source';
242
- if (/(^|\/)(test|tests|__tests__|spec)\b/.test(text)) return 'test';
243
- if (/(^|\/)(scripts|bin)\b/.test(text)) return 'script';
244
- if (/(^|\/)(config|configs)\b/.test(text)) return 'config';
245
- return 'other';
246
- }
247
-
248
- function extractMatches(regex, text, group = 1) {
249
- const out = [];
250
- for (const match of String(text || '').matchAll(regex)) {
251
- const value = String(match[group] || '').trim();
252
- if (value) out.push(value);
253
- }
254
- return out;
407
+ return nextFiles;
255
408
  }
256
409
 
257
410
  function buildFileEntry(relativePath, content, stat) {
258
411
  const ext = path.extname(relativePath).toLowerCase();
259
412
  const imports = clipList([
260
- ...extractMatches(/import\s+(?:[^'"]*from\s+)?['"]([^'"]+)['"]/g, content),
261
- ...extractMatches(/require\(\s*['"]([^'"]+)['"]\s*\)/g, content),
262
- ...extractMatches(/\buse\s+([A-Za-z0-9_:\\]+)/g, ext === '.rs' ? content : '')
263
- ]);
264
- const exports = clipList([
265
- ...extractMatches(/export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/g, content),
266
- ...extractMatches(/export\s+class\s+([A-Za-z0-9_$]+)/g, content),
267
- ...extractMatches(/export\s+const\s+([A-Za-z0-9_$]+)/g, content),
268
- ...extractMatches(/module\.exports\s*=\s*([A-Za-z0-9_$]+)/g, content),
269
- ...extractMatches(/exports\.([A-Za-z0-9_$]+)/g, content)
270
- ]);
271
- const functions = clipList([
272
- ...extractMatches(/\bfunction\s+([A-Za-z0-9_$]+)/g, content),
273
- ...extractMatches(/\bdef\s+([A-Za-z0-9_]+)/g, content),
274
- ...extractMatches(/\bfunc\s+([A-Za-z0-9_]+)/g, content),
275
- ...extractMatches(/\bfn\s+([A-Za-z0-9_]+)/g, content),
276
- ...extractMatches(/^\s*(?:public|private|protected|internal)?\s*(?:static\s+)?[A-Za-z0-9_<>,[\]?]+\s+([A-Za-z0-9_]+)\s*\(/gm, content),
277
- ...extractMatches(/^\s*function\s+([A-Za-z0-9_]+)/gm, content),
278
- ...extractMatches(/^\s*def\s+([A-Za-z0-9_]+)/gm, content)
279
- ]);
280
- const classes = clipList([
281
- ...extractMatches(/\bclass\s+([A-Za-z0-9_$]+)/g, content)
282
- ]);
283
- const calls = clipList([
284
- ...extractMatches(/\b([A-Za-z0-9_$]+)\s*\(/g, content).filter((name) => !['if', 'for', 'while', 'switch', 'return', 'function', 'class', 'catch'].includes(name))
285
- ], 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);
286
438
 
287
439
  return {
288
- file: relativePath,
289
- language: LANGUAGE_BY_EXT[ext] || 'text',
290
- hash: sha256(content),
291
- size: Number(stat?.size || content.length || 0),
292
- mtimeMs: Number(stat?.mtimeMs || 0),
293
- imports,
294
- 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,
295
447
  functions,
296
448
  classes,
297
- calls
449
+ calls,
450
+ symbols
298
451
  };
299
452
  }
300
-
301
- async function scanProject(cwd) {
302
- const workspaceKind = await detectWorkspaceKind(cwd);
303
- if (workspaceKind !== 'project') {
304
- return {
305
- workspaceKind,
306
- projectMap: null,
307
- fileIndex: null,
308
- ignoreRules: []
309
- };
310
- }
311
-
312
- const { gitignoreRules, llmignoreRules, combinedRules } = await readProjectIgnoreRules(cwd);
313
- const allFiles = await walkFiles(cwd, cwd, [], combinedRules);
314
- const relativeFiles = allFiles.map((filePath) => rel(cwd, filePath));
315
- const sourceFiles = allFiles.filter((filePath) => SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase()));
316
-
317
- const packageJson = await safeReadJson(path.join(cwd, 'package.json'), null);
318
- const tsconfigExists = Boolean(await safeStat(path.join(cwd, 'tsconfig.json')));
319
- const sourceRoots = clipList(relativeFiles.filter((value) => /^(src|app|apps)\b/.test(value)).map((value) => value.split('/')[0]), 12);
320
- const testRoots = clipList(relativeFiles.filter((value) => /^(tests|test|__tests__)\b/.test(value)).map((value) => value.split('/')[0]), 12);
321
- const entryCandidates = clipList(
322
- relativeFiles.filter((value) => /(^|\/)(main|index|server|app)\.(js|jsx|mjs|cjs|ts|tsx|py|go|rs|java|cs|php|rb)$/.test(value)),
323
- 16
324
- );
325
- const languages = clipList(sourceFiles.map((filePath) => LANGUAGE_BY_EXT[path.extname(filePath).toLowerCase()] || '').filter(Boolean), 16);
326
- const importantFiles = clipList(
327
- relativeFiles.filter((value) => ['package.json', 'tsconfig.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'composer.json', 'Gemfile'].includes(value)),
328
- 16
329
- );
330
- const packageManagers = clipList([
331
- packageJson ? 'npm' : '',
332
- relativeFiles.includes('bun.lockb') ? 'bun' : '',
333
- relativeFiles.includes('pnpm-lock.yaml') ? 'pnpm' : '',
334
- relativeFiles.includes('yarn.lock') ? 'yarn' : ''
335
- ].filter(Boolean));
336
- const frameworkHints = clipList([
337
- packageJson?.dependencies?.react || packageJson?.devDependencies?.react ? 'react' : '',
338
- packageJson?.dependencies?.express ? 'express' : '',
339
- packageJson?.dependencies?.vue ? 'vue' : '',
340
- packageJson?.dependencies?.next ? 'next' : '',
341
- tsconfigExists ? 'typescript' : ''
342
- ].filter(Boolean));
343
-
344
- const directories = {};
345
- for (const value of relativeFiles) {
346
- const dir = path.posix.dirname(value);
347
- if (!dir || dir === '.') continue;
348
- if (!(dir in directories)) directories[dir] = categorizeDirectory(dir);
349
- }
350
-
351
- 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 = [];
352
505
  for (const filePath of sourceFiles) {
353
506
  const content = await fs.readFile(filePath, 'utf8');
354
507
  const stat = await fs.stat(filePath);
355
508
  files.push(buildFileEntry(rel(cwd, filePath), content, stat));
356
509
  }
357
-
358
- return {
359
- workspaceKind,
360
- projectMap: {
361
- projectRoot: cwd,
362
- workspaceKind,
363
- languages,
364
- packageManagers,
365
- importantFiles,
366
- sourceRoots,
367
- testRoots,
368
- entryCandidates,
369
- frameworkHints,
370
- directories,
371
- gitignoreEnabled: gitignoreRules.length > 0,
372
- llmignoreEnabled: llmignoreRules.length > 0,
373
- updatedAt: new Date().toISOString()
374
- },
375
- fileIndex: {
376
- updatedAt: new Date().toISOString(),
377
- files
378
- },
379
- ignoreRules: combinedRules
380
- };
381
- }
382
-
383
- export async function initializeProjectIndex(cwd = process.cwd()) {
384
- const targetRoot = (await findNearestProjectRoot(cwd, cwd)) || path.resolve(cwd);
385
- const cacheKey = targetRoot;
386
- if (initCache.has(cacheKey)) return initCache.get(cacheKey);
387
- const promise = (async () => {
388
- const workspaceDir = getProjectWorkspaceDir(cwd);
389
- await fs.mkdir(workspaceDir, { recursive: true });
390
- const { workspaceKind, projectMap, fileIndex } = await scanProject(targetRoot);
391
- if (workspaceKind !== 'project' || !projectMap || !fileIndex) {
392
- return {
393
- workspaceKind,
394
- projectRoot: null,
395
- projectMap: null,
396
- fileIndex: null,
397
- summary: '',
398
- skipped: true
399
- };
400
- }
401
- await fs.mkdir(getProjectIndexDir(targetRoot), { recursive: true });
402
- await writeJson(getProjectMapPath(targetRoot), projectMap);
403
- await writeJson(getFileIndexPath(targetRoot), fileIndex);
404
- return {
405
- workspaceKind,
406
- projectRoot: targetRoot,
407
- projectMap,
408
- fileIndex,
409
- summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
410
- };
411
- })();
412
- initCache.set(cacheKey, promise);
413
- try {
414
- return await promise;
415
- } catch (error) {
416
- initCache.delete(cacheKey);
417
- throw error;
418
- }
419
- }
420
-
421
- export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '') {
422
- if (!relativePath) return null;
423
- const workspaceDir = getProjectWorkspaceDir(cwd);
424
- await fs.mkdir(workspaceDir, { recursive: true });
425
- const projectRoot = await findProjectRootFromFile(cwd, relativePath);
426
- if (!projectRoot) return null;
427
- const fileIndexPath = getFileIndexPath(projectRoot);
428
- const { combinedRules } = await readProjectIgnoreRules(projectRoot);
429
- const absolutePath = path.join(cwd, relativePath);
430
- const stat = await safeStat(absolutePath);
431
- let action = 'updated';
432
- const projectRelativePath = path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
433
- const current = await safeReadJson(fileIndexPath, { updatedAt: '', files: [] });
434
- const files = Array.isArray(current.files) ? [...current.files] : [];
435
- const index = files.findIndex((entry) => entry.file === projectRelativePath);
436
-
437
- if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()), combinedRules)) {
438
- if (index >= 0) files.splice(index, 1);
439
- action = 'removed';
440
- } else if (!stat || !stat.isFile()) {
441
- if (index >= 0) files.splice(index, 1);
442
- action = 'removed';
443
- } else {
444
- const ext = path.extname(relativePath).toLowerCase();
445
- if (!SOURCE_EXTENSIONS.has(ext)) {
446
- if (index >= 0) files.splice(index, 1);
447
- action = 'removed';
448
- } else {
449
- const content = await fs.readFile(absolutePath, 'utf8');
450
- const nextEntry = buildFileEntry(projectRelativePath, content, stat);
451
- if (index >= 0) {
452
- files[index] = nextEntry;
453
- } else {
454
- files.push(nextEntry);
455
- action = 'added';
456
- }
457
- }
458
- }
459
-
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);
460
615
  await writeJson(fileIndexPath, {
461
616
  updatedAt: new Date().toISOString(),
462
- files: files.sort((left, right) => left.file.localeCompare(right.file))
617
+ files: enrichedFiles.sort((left, right) => left.file.localeCompare(right.file))
463
618
  });
464
-
465
- return {
466
- path: projectRelativePath,
467
- projectRoot,
468
- action,
469
- summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
470
- };
471
- }
472
-
473
- export async function buildProjectContextSnippet(cwd = process.cwd(), userText = '') {
474
- const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
475
- if (!indexedRoot) return '';
476
-
477
- const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
478
- const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
479
- if (!projectMap || !Array.isArray(fileIndex?.files)) return '';
480
-
481
- const lines = [
482
- 'Project Context:',
483
- `- project_root: ${indexedRoot}`,
484
- `- languages: ${(projectMap.languages || []).slice(0, 6).join(', ') || 'unknown'}`,
485
- `- source_roots: ${(projectMap.sourceRoots || []).slice(0, 6).join(', ') || 'none'}`,
486
- `- test_roots: ${(projectMap.testRoots || []).slice(0, 6).join(', ') || 'none'}`,
487
- `- entry_candidates: ${(projectMap.entryCandidates || []).slice(0, 6).join(', ') || 'none'}`,
488
- `- framework_hints: ${(projectMap.frameworkHints || []).slice(0, 6).join(', ') || 'none'}`
489
- ];
490
-
491
- const tokens = tokenizeQuery(userText);
492
- const scored = [];
493
- for (const entry of fileIndex.files) {
494
- let score = 0;
495
- const fileText = String(entry.file || '').toLowerCase();
496
- for (const token of tokens) {
497
- if (fileText.includes(token)) score += 5;
498
- if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) score += 4;
499
- if ((entry.functions || []).some((value) => String(value).toLowerCase() === token)) score += 4;
500
- if ((entry.classes || []).some((value) => String(value).toLowerCase() === token)) score += 4;
501
- if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) score += 1;
502
- }
503
- if (score > 0) scored.push({ entry, score });
504
- }
505
- scored.sort((left, right) => right.score - left.score || String(left.entry.file).localeCompare(String(right.entry.file)));
506
- const selected = scored.slice(0, PROJECT_CONTEXT_MAX_FILES).map((item) => item.entry);
507
- if (selected.length > 0) {
508
- 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:');
509
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(', ');
510
669
  lines.push(
511
- ` - ${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(', ')}]`
512
671
  );
513
672
  }
514
673
  }
515
-
516
- const snippet = trimMultiline(lines.join('\n'));
517
- return snippet;
518
- }
519
-
520
- export async function queryProjectIndex(cwd = process.cwd(), args = {}) {
521
- const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
522
- if (!indexedRoot) {
523
- return {
524
- query: String(args?.query || '').trim(),
525
- project_root: '',
526
- project_map: null,
527
- matches: []
528
- };
529
- }
530
-
531
- const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
532
- const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
533
- const query = String(args?.query || '').trim();
534
- const pathPrefix = normalizeRelativePath(args?.path || args?.path_prefix || '');
535
- const languageFilter = String(args?.language || '').trim().toLowerCase();
536
- const maxResults = Math.max(1, Math.min(20, Number(args?.max_results || 8) || 8));
537
- const files = Array.isArray(fileIndex?.files) ? fileIndex.files : [];
538
- const tokens = tokenizeQuery(query);
539
-
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
+
540
699
  const matches = [];
541
700
  for (const entry of files) {
542
- const relativePath = String(entry?.file || '');
543
- if (!relativePath) continue;
544
- if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
545
- if (languageFilter && String(entry?.language || '').toLowerCase() !== languageFilter) continue;
546
-
547
- let score = 0;
548
- const reasons = [];
549
- 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 = [];
550
710
  for (const token of tokens) {
551
711
  if (!token) continue;
552
712
  if (fileText.includes(token)) {
553
713
  score += 5;
554
- reasons.push(`path:${token}`);
555
- }
556
- if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) {
557
- score += 4;
558
- reasons.push(`export:${token}`);
559
- }
560
- if ((entry.functions || []).some((value) => String(value).toLowerCase().includes(token))) {
561
- score += 4;
562
- reasons.push(`function:${token}`);
563
- }
564
- if ((entry.classes || []).some((value) => String(value).toLowerCase().includes(token))) {
565
- score += 4;
566
- reasons.push(`class:${token}`);
567
- }
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
+ }
568
728
  if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) {
569
729
  score += 2;
570
730
  reasons.push(`import:${token}`);
571
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
+ }
572
749
  }
573
-
574
- if (!query) {
575
- if ((projectMap?.entryCandidates || []).includes(relativePath)) score += 3;
576
- if ((projectMap?.importantFiles || []).includes(relativePath)) score += 2;
577
- if (String(relativePath).startsWith('src/')) score += 1;
578
- }
579
-
580
- if (score <= 0 && query) continue;
581
- matches.push({
582
- file: relativePath,
583
- language: entry.language || 'text',
584
- score,
585
- 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),
586
763
  exports: clipList(entry.exports || [], 6),
587
764
  functions: clipList(entry.functions || [], 6),
588
765
  classes: clipList(entry.classes || [], 6),
589
- 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)
590
779
  });
591
780
  }
592
-
593
- matches.sort((left, right) => right.score - left.score || String(left.file).localeCompare(String(right.file)));
594
-
595
- return {
596
- query,
597
- project_root: indexedRoot,
598
- project_map: projectMap
599
- ? {
600
- workspace_kind: projectMap.workspaceKind || 'project',
601
- languages: clipList(projectMap.languages || [], 8),
602
- package_managers: clipList(projectMap.packageManagers || [], 8),
603
- important_files: clipList(projectMap.importantFiles || [], 8),
604
- source_roots: clipList(projectMap.sourceRoots || [], 8),
605
- test_roots: clipList(projectMap.testRoots || [], 8),
606
- entry_candidates: clipList(projectMap.entryCandidates || [], 8),
607
- framework_hints: clipList(projectMap.frameworkHints || [], 8),
608
- gitignore_enabled: Boolean(projectMap.gitignoreEnabled),
609
- llmignore_enabled: Boolean(projectMap.llmignoreEnabled)
610
- }
611
- : null,
612
- matches: matches.slice(0, maxResults)
613
- };
614
- }
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
+ }