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.
- package/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
- package/codemini-web/dist/index.html +23 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +289 -289
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5171 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- 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
|
-
|
|
67
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
for (let index =
|
|
75
|
-
const ch =
|
|
76
|
-
|
|
77
|
-
if (ch === '
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
273
|
+
return null;
|
|
93
274
|
}
|
|
94
275
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
154
|
-
|
|
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
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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 =
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
await
|
|
403
|
-
await writeJson(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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:
|
|
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} ::
|
|
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
|
+
}
|