dsh-codebase-chat 0.16.4 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -147
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1646 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +1557 -0
- package/dist/index.js.map +1 -0
- package/lib/client.js +2 -2
- package/lib/index.js +2992 -2857
- package/package.json +95 -70
package/dist/index.js
ADDED
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
// src/context.ts
|
|
2
|
+
import { join as join5 } from "path";
|
|
3
|
+
|
|
4
|
+
// src/tokenizer.ts
|
|
5
|
+
import { encode, decode } from "gpt-tokenizer";
|
|
6
|
+
function countTokens(text) {
|
|
7
|
+
return encode(text).length;
|
|
8
|
+
}
|
|
9
|
+
function chunkByTokens(text, maxTokens, overlapTokens = 0) {
|
|
10
|
+
if (maxTokens <= 0) throw new RangeError("maxTokens must be positive");
|
|
11
|
+
const tokens = encode(text);
|
|
12
|
+
const chunks = [];
|
|
13
|
+
const step = maxTokens - overlapTokens;
|
|
14
|
+
for (let i = 0; i < tokens.length; i += step) {
|
|
15
|
+
const end = Math.min(i + maxTokens, tokens.length);
|
|
16
|
+
const slice = tokens.slice(i, end);
|
|
17
|
+
chunks.push(decode(slice));
|
|
18
|
+
if (end === tokens.length) break;
|
|
19
|
+
}
|
|
20
|
+
return chunks;
|
|
21
|
+
}
|
|
22
|
+
function truncateToTokens(text, maxTokens) {
|
|
23
|
+
const tokens = encode(text);
|
|
24
|
+
if (tokens.length <= maxTokens) return text;
|
|
25
|
+
const keep = Math.max(0, maxTokens - 5);
|
|
26
|
+
const truncated = tokens.slice(0, keep);
|
|
27
|
+
return `${decode(truncated)} [...]`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/indexer.ts
|
|
31
|
+
import { mkdir, readFile as readFile3, stat as stat2, writeFile } from "fs/promises";
|
|
32
|
+
import { dirname as dirname2, join as join4, relative as relative2, sep as sep2 } from "path";
|
|
33
|
+
import { existsSync } from "fs";
|
|
34
|
+
|
|
35
|
+
// src/extractor.ts
|
|
36
|
+
import { parse } from "@babel/parser";
|
|
37
|
+
import traverse from "@babel/traverse";
|
|
38
|
+
import * as t from "@babel/types";
|
|
39
|
+
|
|
40
|
+
// src/merge-gaps.ts
|
|
41
|
+
function mergeGaps(content, relPath, namedChunks, visitedRanges) {
|
|
42
|
+
const lines = content.split("\n");
|
|
43
|
+
if (lines.length === 0) return namedChunks;
|
|
44
|
+
const covered = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const [start2, end] of visitedRanges) {
|
|
46
|
+
for (let i = start2; i <= end; i++) covered.add(i);
|
|
47
|
+
}
|
|
48
|
+
const all = [];
|
|
49
|
+
let start = 1;
|
|
50
|
+
for (let i = 1; i <= lines.length; i++) {
|
|
51
|
+
if (!covered.has(i)) continue;
|
|
52
|
+
if (i > start) {
|
|
53
|
+
const gapText = lines.slice(start - 1, i - 1).join("\n");
|
|
54
|
+
if (gapText.trim()) all.push({ relPath, startLine: start, endLine: i - 1, content: gapText, tokens: countTokens(gapText), kind: "file" });
|
|
55
|
+
}
|
|
56
|
+
start = i + 1;
|
|
57
|
+
}
|
|
58
|
+
if (start <= lines.length) {
|
|
59
|
+
const gapText = lines.slice(start - 1).join("\n");
|
|
60
|
+
if (gapText.trim()) all.push({ relPath, startLine: start, endLine: lines.length, content: gapText, tokens: countTokens(gapText), kind: "file" });
|
|
61
|
+
}
|
|
62
|
+
return [...all, ...namedChunks].sort((a, b) => a.startLine - b.startLine);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/treesitter.ts
|
|
66
|
+
import { createRequire } from "module";
|
|
67
|
+
import { dirname, join } from "path";
|
|
68
|
+
var require2 = createRequire(import.meta.url);
|
|
69
|
+
var GRAMMAR_BY_EXT = {
|
|
70
|
+
".py": "python",
|
|
71
|
+
".go": "go",
|
|
72
|
+
".rs": "rust",
|
|
73
|
+
".java": "java",
|
|
74
|
+
".cs": "c_sharp",
|
|
75
|
+
".php": "php"
|
|
76
|
+
};
|
|
77
|
+
var DECL_TYPES = {
|
|
78
|
+
python: {
|
|
79
|
+
function_definition: "function",
|
|
80
|
+
class_definition: "class"
|
|
81
|
+
},
|
|
82
|
+
go: {
|
|
83
|
+
function_declaration: "function",
|
|
84
|
+
method_declaration: "method",
|
|
85
|
+
type_declaration: "type",
|
|
86
|
+
import_declaration: "import",
|
|
87
|
+
const_declaration: "unknown",
|
|
88
|
+
var_declaration: "unknown"
|
|
89
|
+
},
|
|
90
|
+
rust: {
|
|
91
|
+
function_item: "function",
|
|
92
|
+
struct_item: "type",
|
|
93
|
+
enum_item: "type",
|
|
94
|
+
union_item: "type",
|
|
95
|
+
trait_item: "type",
|
|
96
|
+
type_item: "type",
|
|
97
|
+
impl_item: "type",
|
|
98
|
+
mod_item: "unknown",
|
|
99
|
+
use_declaration: "import",
|
|
100
|
+
macro_definition: "unknown"
|
|
101
|
+
},
|
|
102
|
+
java: {
|
|
103
|
+
class_declaration: "class",
|
|
104
|
+
interface_declaration: "type",
|
|
105
|
+
enum_declaration: "type",
|
|
106
|
+
record_declaration: "type",
|
|
107
|
+
annotation_type_declaration: "type",
|
|
108
|
+
method_declaration: "method",
|
|
109
|
+
constructor_declaration: "method",
|
|
110
|
+
field_declaration: "unknown",
|
|
111
|
+
import_declaration: "import",
|
|
112
|
+
package_declaration: "import"
|
|
113
|
+
},
|
|
114
|
+
c_sharp: {
|
|
115
|
+
class_declaration: "class",
|
|
116
|
+
interface_declaration: "type",
|
|
117
|
+
struct_declaration: "type",
|
|
118
|
+
enum_declaration: "type",
|
|
119
|
+
record_declaration: "type",
|
|
120
|
+
delegate_declaration: "type",
|
|
121
|
+
method_declaration: "method",
|
|
122
|
+
constructor_declaration: "method",
|
|
123
|
+
property_declaration: "method",
|
|
124
|
+
field_declaration: "unknown",
|
|
125
|
+
using_directive: "import"
|
|
126
|
+
},
|
|
127
|
+
php: {
|
|
128
|
+
function_definition: "function",
|
|
129
|
+
class_declaration: "class",
|
|
130
|
+
interface_declaration: "type",
|
|
131
|
+
trait_declaration: "type",
|
|
132
|
+
enum_declaration: "type",
|
|
133
|
+
method_declaration: "method",
|
|
134
|
+
namespace_use_declaration: "import",
|
|
135
|
+
namespace_definition: "unknown"
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var RECURSE_INTO = /* @__PURE__ */ new Set([
|
|
139
|
+
"program",
|
|
140
|
+
"module",
|
|
141
|
+
"translation_unit",
|
|
142
|
+
"compilation_unit",
|
|
143
|
+
"source_file",
|
|
144
|
+
"namespace_declaration",
|
|
145
|
+
"file_scoped_namespace_declaration",
|
|
146
|
+
"namespace_definition",
|
|
147
|
+
"declaration_list",
|
|
148
|
+
"decorated_definition",
|
|
149
|
+
"export_statement"
|
|
150
|
+
]);
|
|
151
|
+
var CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
152
|
+
"class_definition",
|
|
153
|
+
"class_declaration",
|
|
154
|
+
"interface_declaration",
|
|
155
|
+
"enum_declaration",
|
|
156
|
+
"record_declaration",
|
|
157
|
+
"struct_declaration",
|
|
158
|
+
"impl_item",
|
|
159
|
+
"trait_item",
|
|
160
|
+
"trait_declaration"
|
|
161
|
+
]);
|
|
162
|
+
var MEMBER_TYPES = /* @__PURE__ */ new Set([
|
|
163
|
+
"function_definition",
|
|
164
|
+
"method_declaration",
|
|
165
|
+
"method_definition",
|
|
166
|
+
"function_item",
|
|
167
|
+
"constructor_declaration",
|
|
168
|
+
"property_declaration"
|
|
169
|
+
]);
|
|
170
|
+
var NAME_TYPES = /* @__PURE__ */ new Set([
|
|
171
|
+
"identifier",
|
|
172
|
+
"type_identifier",
|
|
173
|
+
"field_identifier",
|
|
174
|
+
"simple_type",
|
|
175
|
+
"name"
|
|
176
|
+
]);
|
|
177
|
+
var parser = null;
|
|
178
|
+
var initPromise = null;
|
|
179
|
+
var languages = /* @__PURE__ */ new Map();
|
|
180
|
+
var langPromises = /* @__PURE__ */ new Map();
|
|
181
|
+
async function initTreeSitter() {
|
|
182
|
+
if (parser) return true;
|
|
183
|
+
initPromise ??= (async () => {
|
|
184
|
+
try {
|
|
185
|
+
const mod = await import("web-tree-sitter");
|
|
186
|
+
const ParserClass = mod.default ?? mod;
|
|
187
|
+
const wasm = require2.resolve("web-tree-sitter/tree-sitter.wasm");
|
|
188
|
+
await ParserClass.init({ locateFile: () => wasm });
|
|
189
|
+
parser = new ParserClass();
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
return initPromise;
|
|
196
|
+
}
|
|
197
|
+
function grammarPath(name) {
|
|
198
|
+
const pkg = require2.resolve("tree-sitter-wasms/package.json");
|
|
199
|
+
return join(dirname(pkg), "out", `tree-sitter-${name}.wasm`);
|
|
200
|
+
}
|
|
201
|
+
async function ensureLanguage(name) {
|
|
202
|
+
if (languages.has(name)) return languages.get(name);
|
|
203
|
+
let p = langPromises.get(name);
|
|
204
|
+
if (!p) {
|
|
205
|
+
p = (async () => {
|
|
206
|
+
try {
|
|
207
|
+
const mod = await import("web-tree-sitter");
|
|
208
|
+
const ParserClass = mod.default ?? mod;
|
|
209
|
+
const lang = await ParserClass.Language.load(grammarPath(name));
|
|
210
|
+
languages.set(name, lang);
|
|
211
|
+
return lang;
|
|
212
|
+
} catch {
|
|
213
|
+
languages.set(name, null);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
langPromises.set(name, p);
|
|
218
|
+
}
|
|
219
|
+
return p;
|
|
220
|
+
}
|
|
221
|
+
async function ensureTreeSitterForExt(ext) {
|
|
222
|
+
const name = GRAMMAR_BY_EXT[ext.toLowerCase()];
|
|
223
|
+
if (!name) return false;
|
|
224
|
+
if (!await initTreeSitter()) return false;
|
|
225
|
+
return await ensureLanguage(name) != null;
|
|
226
|
+
}
|
|
227
|
+
function disposeTreeSitter() {
|
|
228
|
+
try {
|
|
229
|
+
parser?.delete();
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
parser = null;
|
|
233
|
+
initPromise = null;
|
|
234
|
+
languages.clear();
|
|
235
|
+
langPromises.clear();
|
|
236
|
+
}
|
|
237
|
+
function treeSitterReady(ext) {
|
|
238
|
+
const name = GRAMMAR_BY_EXT[ext.toLowerCase()];
|
|
239
|
+
return !!name && !!parser && languages.get(name) != null;
|
|
240
|
+
}
|
|
241
|
+
function nodeName(node) {
|
|
242
|
+
const named = node.childForFieldName("name");
|
|
243
|
+
if (named) return named.text;
|
|
244
|
+
for (const child of node.namedChildren) {
|
|
245
|
+
if (NAME_TYPES.has(child.type)) return child.text;
|
|
246
|
+
const inner = child.childForFieldName("name");
|
|
247
|
+
if (inner) return inner.text;
|
|
248
|
+
}
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
function toChunk(node, kind, relPath, content, name) {
|
|
252
|
+
const startLine = node.startPosition.row + 1;
|
|
253
|
+
const endLine = node.endPosition.row + 1;
|
|
254
|
+
const text = content.split("\n").slice(startLine - 1, endLine).join("\n");
|
|
255
|
+
return { relPath, startLine, endLine, content: text, tokens: countTokens(text), kind, name };
|
|
256
|
+
}
|
|
257
|
+
function extractWithTreeSitter(ext, relPath, content) {
|
|
258
|
+
if (!parser || !treeSitterReady(ext)) return void 0;
|
|
259
|
+
const name = GRAMMAR_BY_EXT[ext.toLowerCase()];
|
|
260
|
+
const lang = languages.get(name);
|
|
261
|
+
const declTypes = DECL_TYPES[name] ?? {};
|
|
262
|
+
let tree;
|
|
263
|
+
try {
|
|
264
|
+
parser.setLanguage(lang);
|
|
265
|
+
tree = parser.parse(content);
|
|
266
|
+
} catch {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
if (!tree) return void 0;
|
|
270
|
+
try {
|
|
271
|
+
const chunks = [];
|
|
272
|
+
const visitedRanges = [];
|
|
273
|
+
const handle = (child) => {
|
|
274
|
+
const kind = declTypes[child.type];
|
|
275
|
+
if (kind) {
|
|
276
|
+
const target = child.parent?.type === "decorated_definition" ? child.parent : child;
|
|
277
|
+
chunks.push(toChunk(target, kind, relPath, content, nodeName(child)));
|
|
278
|
+
visitedRanges.push([target.startPosition.row + 1, target.endPosition.row + 1]);
|
|
279
|
+
if (CONTAINER_TYPES.has(child.type)) {
|
|
280
|
+
const body = child.childForFieldName("body") ?? child;
|
|
281
|
+
for (const member of body.namedChildren) {
|
|
282
|
+
if (MEMBER_TYPES.has(member.type)) {
|
|
283
|
+
chunks.push(toChunk(member, "method", relPath, content, nodeName(member)));
|
|
284
|
+
} else if (declTypes[member.type]) {
|
|
285
|
+
handle(member);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (RECURSE_INTO.has(child.type) || child.namedChildren.some((c) => declTypes[c.type] || RECURSE_INTO.has(c.type))) {
|
|
292
|
+
visit(child);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const visit = (node) => {
|
|
296
|
+
for (const child of node.namedChildren) handle(child);
|
|
297
|
+
};
|
|
298
|
+
visit(tree.rootNode);
|
|
299
|
+
return mergeGaps(content, relPath, chunks, visitedRanges);
|
|
300
|
+
} finally {
|
|
301
|
+
tree.delete();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/extractor.ts
|
|
306
|
+
var JS_LIKE = /* @__PURE__ */ new Set([
|
|
307
|
+
".js",
|
|
308
|
+
".jsx",
|
|
309
|
+
".mjs",
|
|
310
|
+
".cjs",
|
|
311
|
+
".ts",
|
|
312
|
+
".tsx"
|
|
313
|
+
]);
|
|
314
|
+
function getName(node) {
|
|
315
|
+
if (t.isFunctionDeclaration(node) || t.isClassDeclaration(node) || t.isTSInterfaceDeclaration(node) || t.isTSTypeAliasDeclaration(node)) {
|
|
316
|
+
return node.id?.name;
|
|
317
|
+
}
|
|
318
|
+
if (t.isVariableDeclaration(node)) {
|
|
319
|
+
const first = node.declarations[0];
|
|
320
|
+
if (first && t.isIdentifier(first.id)) return first.id.name;
|
|
321
|
+
}
|
|
322
|
+
if (t.isObjectMethod(node) || t.isClassMethod(node) || t.isClassPrivateMethod(node)) {
|
|
323
|
+
const key = node.key;
|
|
324
|
+
if (t.isIdentifier(node.key) || t.isStringLiteral(node.key) || t.isNumericLiteral(node.key)) {
|
|
325
|
+
return String(key.name ?? key.value ?? "anonymous");
|
|
326
|
+
}
|
|
327
|
+
if (t.isPrivateName(node.key)) {
|
|
328
|
+
return node.key.id?.name;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (t.isExportNamedDeclaration(node) && node.declaration) {
|
|
332
|
+
return getName(node.declaration);
|
|
333
|
+
}
|
|
334
|
+
if (t.isExportDefaultDeclaration(node) && node.declaration) {
|
|
335
|
+
if (t.isFunctionDeclaration(node.declaration) || t.isClassDeclaration(node.declaration)) {
|
|
336
|
+
return getName(node.declaration) ?? "default";
|
|
337
|
+
}
|
|
338
|
+
if (t.isVariableDeclaration(node.declaration)) {
|
|
339
|
+
return getName(node.declaration) ?? "default";
|
|
340
|
+
}
|
|
341
|
+
return "default";
|
|
342
|
+
}
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
function getKind(node) {
|
|
346
|
+
if (t.isClassMethod(node) || t.isClassPrivateMethod(node) || t.isObjectMethod(node)) return "method";
|
|
347
|
+
if (t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) return "function";
|
|
348
|
+
if (t.isClassDeclaration(node) || t.isClassExpression(node)) return "class";
|
|
349
|
+
if (t.isTSInterfaceDeclaration(node) || t.isTSTypeAliasDeclaration(node)) return "type";
|
|
350
|
+
if (t.isImportDeclaration(node) || t.isExportAllDeclaration(node) || t.isExportNamespaceSpecifier(node)) return "import";
|
|
351
|
+
if (t.isVariableDeclaration(node)) return "unknown";
|
|
352
|
+
if (t.isExportNamedDeclaration(node) && node.declaration) return getKind(node.declaration);
|
|
353
|
+
if (t.isExportDefaultDeclaration(node) && node.declaration) return getKind(node.declaration);
|
|
354
|
+
if (t.isExportNamedDeclaration(node) || t.isExportDefaultDeclaration(node)) return "import";
|
|
355
|
+
return "unknown";
|
|
356
|
+
}
|
|
357
|
+
function sliceLines(content, start, end) {
|
|
358
|
+
const lines = content.split("\n");
|
|
359
|
+
return lines.slice(start - 1, end).join("\n");
|
|
360
|
+
}
|
|
361
|
+
function extractTopLevelWithBabel(relPath, content) {
|
|
362
|
+
const ast = parse(content, {
|
|
363
|
+
sourceType: "module",
|
|
364
|
+
allowImportExportEverywhere: true,
|
|
365
|
+
allowReturnOutsideFunction: true,
|
|
366
|
+
plugins: [
|
|
367
|
+
"typescript",
|
|
368
|
+
"jsx",
|
|
369
|
+
"decorators-legacy",
|
|
370
|
+
"classProperties",
|
|
371
|
+
"asyncGenerators",
|
|
372
|
+
"bigInt",
|
|
373
|
+
"dynamicImport",
|
|
374
|
+
"exportDefaultFrom",
|
|
375
|
+
"nullishCoalescingOperator",
|
|
376
|
+
"numericSeparator",
|
|
377
|
+
"objectRestSpread",
|
|
378
|
+
"optionalCatchBinding",
|
|
379
|
+
"optionalChaining",
|
|
380
|
+
"topLevelAwait"
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
const chunks = [];
|
|
384
|
+
const visitedRanges = [];
|
|
385
|
+
traverse(ast, {
|
|
386
|
+
enter(path) {
|
|
387
|
+
const node = path.node;
|
|
388
|
+
if (t.isFunctionDeclaration(node) || t.isClassDeclaration(node) || t.isTSInterfaceDeclaration(node) || t.isTSTypeAliasDeclaration(node) || t.isVariableDeclaration(node) || t.isImportDeclaration(node) || t.isExportNamedDeclaration(node) || t.isExportDefaultDeclaration(node) || t.isExportAllDeclaration(node)) {
|
|
389
|
+
const loc = node.loc;
|
|
390
|
+
if (!loc) return;
|
|
391
|
+
if (path.parentPath && !t.isProgram(path.parentPath.node)) return;
|
|
392
|
+
if (t.isClassDeclaration(node) && node.body?.body) {
|
|
393
|
+
for (const member of node.body.body) {
|
|
394
|
+
if (!member.loc) continue;
|
|
395
|
+
if (t.isClassMethod(member) || t.isClassPrivateMethod(member) || t.isClassProperty(member)) {
|
|
396
|
+
const methodChunk = {
|
|
397
|
+
relPath,
|
|
398
|
+
startLine: member.loc.start.line,
|
|
399
|
+
endLine: member.loc.end.line,
|
|
400
|
+
content: sliceLines(content, member.loc.start.line, member.loc.end.line),
|
|
401
|
+
tokens: 0,
|
|
402
|
+
kind: getKind(member),
|
|
403
|
+
name: getName(member)
|
|
404
|
+
};
|
|
405
|
+
methodChunk.tokens = countTokens(methodChunk.content);
|
|
406
|
+
chunks.push(methodChunk);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const chunk = {
|
|
411
|
+
relPath,
|
|
412
|
+
startLine: loc.start.line,
|
|
413
|
+
endLine: loc.end.line,
|
|
414
|
+
content: sliceLines(content, loc.start.line, loc.end.line),
|
|
415
|
+
tokens: 0,
|
|
416
|
+
kind: getKind(node),
|
|
417
|
+
name: getName(node)
|
|
418
|
+
};
|
|
419
|
+
chunk.tokens = countTokens(chunk.content);
|
|
420
|
+
chunks.push(chunk);
|
|
421
|
+
visitedRanges.push([loc.start.line, loc.end.line]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
return mergeGaps(content, relPath, chunks, visitedRanges);
|
|
426
|
+
}
|
|
427
|
+
function extractWithRegex(relPath, content) {
|
|
428
|
+
const lines = content.split("\n");
|
|
429
|
+
const chunks = [];
|
|
430
|
+
let currentStart = 1;
|
|
431
|
+
let currentLines = [];
|
|
432
|
+
const re = /^(export\s+)?(?:async\s+)?(?:function\s+\w+|class\s+\w+|const\s+\w+|let\s+\w+|var\s+\w+|interface\s+\w+|type\s+\w+|def\s+\w+|struct\s+\w+|fn\s+\w+)/;
|
|
433
|
+
for (let i = 0; i < lines.length; i++) {
|
|
434
|
+
const line = lines[i];
|
|
435
|
+
if (line.match(re) && currentLines.length > 0) {
|
|
436
|
+
const chunk = {
|
|
437
|
+
relPath,
|
|
438
|
+
startLine: currentStart,
|
|
439
|
+
endLine: i,
|
|
440
|
+
content: currentLines.join("\n"),
|
|
441
|
+
tokens: 0,
|
|
442
|
+
kind: "unknown"
|
|
443
|
+
};
|
|
444
|
+
chunk.tokens = countTokens(chunk.content);
|
|
445
|
+
chunks.push(chunk);
|
|
446
|
+
currentStart = i + 1;
|
|
447
|
+
currentLines = [line];
|
|
448
|
+
} else {
|
|
449
|
+
currentLines.push(line);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (currentLines.length) {
|
|
453
|
+
const chunk = {
|
|
454
|
+
relPath,
|
|
455
|
+
startLine: currentStart,
|
|
456
|
+
endLine: lines.length,
|
|
457
|
+
content: currentLines.join("\n"),
|
|
458
|
+
tokens: 0,
|
|
459
|
+
kind: "unknown"
|
|
460
|
+
};
|
|
461
|
+
chunk.tokens = countTokens(chunk.content);
|
|
462
|
+
chunks.push(chunk);
|
|
463
|
+
}
|
|
464
|
+
return chunks.filter((c) => c.content.trim());
|
|
465
|
+
}
|
|
466
|
+
function extractChunks(relPath, content) {
|
|
467
|
+
const ext = relPath.slice(relPath.lastIndexOf(".")).toLowerCase();
|
|
468
|
+
if (JS_LIKE.has(ext)) {
|
|
469
|
+
try {
|
|
470
|
+
return extractTopLevelWithBabel(relPath, content);
|
|
471
|
+
} catch {
|
|
472
|
+
return extractWithRegex(relPath, content);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (treeSitterReady(ext)) {
|
|
476
|
+
try {
|
|
477
|
+
const chunks = extractWithTreeSitter(ext, relPath, content);
|
|
478
|
+
if (chunks && chunks.length) return chunks;
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return extractWithRegex(relPath, content);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/embeddings.ts
|
|
486
|
+
import { pipeline } from "@xenova/transformers";
|
|
487
|
+
var DEFAULT_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
488
|
+
var extractor = null;
|
|
489
|
+
var activeModel = "";
|
|
490
|
+
async function getExtractor(model = DEFAULT_MODEL) {
|
|
491
|
+
if (extractor && activeModel === model) return extractor;
|
|
492
|
+
activeModel = model;
|
|
493
|
+
extractor = await pipeline("feature-extraction", model, {
|
|
494
|
+
quantized: true
|
|
495
|
+
});
|
|
496
|
+
return extractor;
|
|
497
|
+
}
|
|
498
|
+
function prepareText(text, maxTokens = 256) {
|
|
499
|
+
if (countTokens(text) <= maxTokens) return text;
|
|
500
|
+
return chunkByTokens(text, maxTokens)[0] ?? text.slice(0, 1024);
|
|
501
|
+
}
|
|
502
|
+
function tensorToVectors(tensor) {
|
|
503
|
+
const [count, dim] = tensor.dims;
|
|
504
|
+
const vectors = [];
|
|
505
|
+
for (let i = 0; i < count; i++) {
|
|
506
|
+
vectors.push(Array.from(tensor.data.subarray(i * dim, (i + 1) * dim)));
|
|
507
|
+
}
|
|
508
|
+
return vectors;
|
|
509
|
+
}
|
|
510
|
+
async function getEmbedding(text, model = DEFAULT_MODEL) {
|
|
511
|
+
const pipe = await getExtractor(model);
|
|
512
|
+
const out = await pipe(prepareText(text), {
|
|
513
|
+
pooling: "mean",
|
|
514
|
+
normalize: true
|
|
515
|
+
});
|
|
516
|
+
return Array.from(out.data);
|
|
517
|
+
}
|
|
518
|
+
async function getEmbeddings(texts, model = DEFAULT_MODEL) {
|
|
519
|
+
const pipe = await getExtractor(model);
|
|
520
|
+
const inputs = texts.map((t2) => prepareText(t2));
|
|
521
|
+
const out = await pipe(inputs, {
|
|
522
|
+
pooling: "mean",
|
|
523
|
+
normalize: true
|
|
524
|
+
});
|
|
525
|
+
return tensorToVectors(out);
|
|
526
|
+
}
|
|
527
|
+
function cosineSimilarity(a, b) {
|
|
528
|
+
let dot = 0;
|
|
529
|
+
for (let i = 0; i < a.length; i++) {
|
|
530
|
+
dot += a[i] * b[i];
|
|
531
|
+
}
|
|
532
|
+
return dot;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/project.ts
|
|
536
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
537
|
+
import { extname, join as join3, relative, resolve, sep, isAbsolute } from "path";
|
|
538
|
+
import { createHash } from "crypto";
|
|
539
|
+
import { homedir } from "os";
|
|
540
|
+
|
|
541
|
+
// src/config.ts
|
|
542
|
+
import { readFile } from "fs/promises";
|
|
543
|
+
import { join as join2 } from "path";
|
|
544
|
+
var CONFIG_FILE = ".codebase-chat.json";
|
|
545
|
+
var configCache = /* @__PURE__ */ new Map();
|
|
546
|
+
function strArray(v) {
|
|
547
|
+
if (!Array.isArray(v)) return void 0;
|
|
548
|
+
const out = v.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
549
|
+
return out.length ? out.map((s) => s.trim()) : void 0;
|
|
550
|
+
}
|
|
551
|
+
function sanitize(raw) {
|
|
552
|
+
if (raw == null || typeof raw !== "object") return {};
|
|
553
|
+
const o = raw;
|
|
554
|
+
const cfg = {};
|
|
555
|
+
if (o.lang === "fr" || o.lang === "en") cfg.lang = o.lang;
|
|
556
|
+
if (typeof o.maxTokens === "number" && Number.isFinite(o.maxTokens) && o.maxTokens > 0) {
|
|
557
|
+
cfg.maxTokens = Math.floor(o.maxTokens);
|
|
558
|
+
}
|
|
559
|
+
const ignoreDirs = strArray(o.ignoreDirs);
|
|
560
|
+
if (ignoreDirs) cfg.ignoreDirs = ignoreDirs;
|
|
561
|
+
const ignoreFiles = strArray(o.ignoreFiles);
|
|
562
|
+
if (ignoreFiles) cfg.ignoreFiles = ignoreFiles;
|
|
563
|
+
const ignoreGlobs = strArray(o.ignoreGlobs);
|
|
564
|
+
if (ignoreGlobs) cfg.ignoreGlobs = ignoreGlobs;
|
|
565
|
+
const protectedPaths = strArray(o.protectedPaths);
|
|
566
|
+
if (protectedPaths) cfg.protectedPaths = protectedPaths;
|
|
567
|
+
return cfg;
|
|
568
|
+
}
|
|
569
|
+
async function loadProjectConfig(absProject) {
|
|
570
|
+
const cached = configCache.get(absProject);
|
|
571
|
+
if (cached) return cached;
|
|
572
|
+
let cfg = {};
|
|
573
|
+
try {
|
|
574
|
+
cfg = sanitize(JSON.parse(await readFile(join2(absProject, CONFIG_FILE), "utf8")));
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
configCache.set(absProject, cfg);
|
|
578
|
+
return cfg;
|
|
579
|
+
}
|
|
580
|
+
function clearConfigCache(absProject) {
|
|
581
|
+
if (absProject == null) configCache.clear();
|
|
582
|
+
else configCache.delete(absProject);
|
|
583
|
+
}
|
|
584
|
+
function escapeSegment(s) {
|
|
585
|
+
return s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
586
|
+
}
|
|
587
|
+
function globToRegExp(glob) {
|
|
588
|
+
const src = glob.replace(/\\/g, "/").split("**").map(escapeSegment).join(".*");
|
|
589
|
+
return new RegExp(`^${src}$`);
|
|
590
|
+
}
|
|
591
|
+
function matchesAnyGlob(relPath, globs) {
|
|
592
|
+
if (!globs || globs.length === 0) return false;
|
|
593
|
+
const rel = relPath.replace(/\\/g, "/");
|
|
594
|
+
return globs.some((g) => globToRegExp(g).test(rel));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/project.ts
|
|
598
|
+
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
599
|
+
".ts",
|
|
600
|
+
".tsx",
|
|
601
|
+
".js",
|
|
602
|
+
".jsx",
|
|
603
|
+
".mjs",
|
|
604
|
+
".cjs",
|
|
605
|
+
".vue",
|
|
606
|
+
".svelte",
|
|
607
|
+
".py",
|
|
608
|
+
".rs",
|
|
609
|
+
".go",
|
|
610
|
+
".java",
|
|
611
|
+
".kt",
|
|
612
|
+
".swift",
|
|
613
|
+
".cs",
|
|
614
|
+
".cpp",
|
|
615
|
+
".c",
|
|
616
|
+
".h",
|
|
617
|
+
".hpp",
|
|
618
|
+
".css",
|
|
619
|
+
".scss",
|
|
620
|
+
".less",
|
|
621
|
+
".html",
|
|
622
|
+
".json",
|
|
623
|
+
".yaml",
|
|
624
|
+
".yml",
|
|
625
|
+
".md"
|
|
626
|
+
]);
|
|
627
|
+
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
628
|
+
"node_modules",
|
|
629
|
+
".git",
|
|
630
|
+
"dist",
|
|
631
|
+
"build",
|
|
632
|
+
"out",
|
|
633
|
+
".output",
|
|
634
|
+
"coverage",
|
|
635
|
+
"tmp",
|
|
636
|
+
"temp",
|
|
637
|
+
".cache",
|
|
638
|
+
".turbo",
|
|
639
|
+
".next",
|
|
640
|
+
"android",
|
|
641
|
+
"ios",
|
|
642
|
+
"e2e-shots",
|
|
643
|
+
"playstore_screenshots",
|
|
644
|
+
".cursor",
|
|
645
|
+
".idea",
|
|
646
|
+
".memsearch",
|
|
647
|
+
".vscode",
|
|
648
|
+
"__pycache__",
|
|
649
|
+
".dsh-tmp",
|
|
650
|
+
".dsh-vision-router"
|
|
651
|
+
]);
|
|
652
|
+
var DEFAULT_SKIP_FILES = /* @__PURE__ */ new Set([]);
|
|
653
|
+
async function getWalkOptions(absProject) {
|
|
654
|
+
const cfg = await loadProjectConfig(absProject);
|
|
655
|
+
return {
|
|
656
|
+
skipDirs: /* @__PURE__ */ new Set([...DEFAULT_SKIP_DIRS, ...cfg.ignoreDirs ?? []]),
|
|
657
|
+
skipFiles: /* @__PURE__ */ new Set([...DEFAULT_SKIP_FILES, ...cfg.ignoreFiles ?? []]),
|
|
658
|
+
ignoreGlobs: cfg.ignoreGlobs ?? []
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function projectHash(absProject) {
|
|
662
|
+
return createHash("sha256").update(absProject.toLowerCase()).digest("hex").slice(0, 16);
|
|
663
|
+
}
|
|
664
|
+
function resolveProjectPath(projectPath) {
|
|
665
|
+
const raw = (projectPath ?? "").trim().toLowerCase().replace(/['"]/g, "");
|
|
666
|
+
if (raw === "dako") return "D:\\Nouveau dossier";
|
|
667
|
+
if (!projectPath) return process.cwd();
|
|
668
|
+
if (isAbsolute(projectPath)) return resolve(projectPath);
|
|
669
|
+
return resolve(process.cwd(), projectPath);
|
|
670
|
+
}
|
|
671
|
+
async function findProjectRoot(absProject) {
|
|
672
|
+
try {
|
|
673
|
+
const s = await stat(absProject);
|
|
674
|
+
if (s.isDirectory()) return absProject;
|
|
675
|
+
return resolve(absProject, "..");
|
|
676
|
+
} catch {
|
|
677
|
+
throw new Error(`Project path not found: ${absProject}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function getCacheDir() {
|
|
681
|
+
const base = process.env.CODEBASE_CACHE_DIR || process.env.LOCALAPPDATA || process.env.APPDATA || join3(homedir(), ".cache");
|
|
682
|
+
return join3(base, "dsh-codebase-chat-cache");
|
|
683
|
+
}
|
|
684
|
+
function cacheFilePath(absProject) {
|
|
685
|
+
return join3(getCacheDir(), `${projectHash(absProject)}.json`);
|
|
686
|
+
}
|
|
687
|
+
function fileHash(stats, firstBytes = "") {
|
|
688
|
+
return createHash("sha256").update(`${stats.mtimeMs}:${stats.size}:${firstBytes.slice(0, 512)}`).digest("hex").slice(0, 24);
|
|
689
|
+
}
|
|
690
|
+
async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DEFAULT_SKIP_FILES, ignoreGlobs = []) {
|
|
691
|
+
const queue = [startDir];
|
|
692
|
+
while (queue.length) {
|
|
693
|
+
const dir = queue.shift();
|
|
694
|
+
let entries;
|
|
695
|
+
try {
|
|
696
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
697
|
+
} catch {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
const fullPath = join3(dir, entry.name);
|
|
702
|
+
const rel = relative(startDir, fullPath).split(sep).join("/");
|
|
703
|
+
if (entry.isDirectory()) {
|
|
704
|
+
if (!skipDirs.has(entry.name) && !matchesAnyGlob(rel, ignoreGlobs)) queue.push(fullPath);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (!entry.isFile()) continue;
|
|
708
|
+
if (skipFiles.has(entry.name)) continue;
|
|
709
|
+
if (matchesAnyGlob(rel, ignoreGlobs)) continue;
|
|
710
|
+
const ext = extname(entry.name).toLowerCase();
|
|
711
|
+
if (!SOURCE_EXTS.has(ext)) continue;
|
|
712
|
+
yield fullPath;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
async function safeReadText(filePath) {
|
|
717
|
+
try {
|
|
718
|
+
const text = await readFile2(filePath, "utf8");
|
|
719
|
+
return text;
|
|
720
|
+
} catch {
|
|
721
|
+
return void 0;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function buildTree(startDir, maxLines = 500, skipDirs = DEFAULT_SKIP_DIRS) {
|
|
725
|
+
const lines = [];
|
|
726
|
+
async function walk(dir, prefix = "") {
|
|
727
|
+
if (lines.length >= maxLines) return;
|
|
728
|
+
let entries;
|
|
729
|
+
try {
|
|
730
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
731
|
+
} catch {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
entries.sort((a, b) => a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1);
|
|
735
|
+
for (const entry of entries) {
|
|
736
|
+
if (lines.length >= maxLines) return;
|
|
737
|
+
if (skipDirs.has(entry.name)) continue;
|
|
738
|
+
const fullPath = join3(dir, entry.name);
|
|
739
|
+
if (entry.isDirectory()) {
|
|
740
|
+
lines.push(`${prefix}${entry.name}/`);
|
|
741
|
+
await walk(fullPath, `${prefix} `);
|
|
742
|
+
} else {
|
|
743
|
+
lines.push(`${prefix}${entry.name}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
await walk(startDir);
|
|
748
|
+
return lines.join("\n");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/indexer.ts
|
|
752
|
+
var INDEX_VERSION = 5;
|
|
753
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
754
|
+
"the",
|
|
755
|
+
"is",
|
|
756
|
+
"are",
|
|
757
|
+
"was",
|
|
758
|
+
"were",
|
|
759
|
+
"be",
|
|
760
|
+
"been",
|
|
761
|
+
"being",
|
|
762
|
+
"have",
|
|
763
|
+
"has",
|
|
764
|
+
"had",
|
|
765
|
+
"do",
|
|
766
|
+
"does",
|
|
767
|
+
"did",
|
|
768
|
+
"will",
|
|
769
|
+
"would",
|
|
770
|
+
"could",
|
|
771
|
+
"should",
|
|
772
|
+
"may",
|
|
773
|
+
"might",
|
|
774
|
+
"must",
|
|
775
|
+
"shall",
|
|
776
|
+
"can",
|
|
777
|
+
"need",
|
|
778
|
+
"dare",
|
|
779
|
+
"ought",
|
|
780
|
+
"used",
|
|
781
|
+
"to",
|
|
782
|
+
"of",
|
|
783
|
+
"in",
|
|
784
|
+
"for",
|
|
785
|
+
"on",
|
|
786
|
+
"with",
|
|
787
|
+
"at",
|
|
788
|
+
"by",
|
|
789
|
+
"from",
|
|
790
|
+
"as",
|
|
791
|
+
"and",
|
|
792
|
+
"or",
|
|
793
|
+
"but",
|
|
794
|
+
"so",
|
|
795
|
+
"yet",
|
|
796
|
+
"a",
|
|
797
|
+
"an",
|
|
798
|
+
"this",
|
|
799
|
+
"that",
|
|
800
|
+
"these",
|
|
801
|
+
"those",
|
|
802
|
+
"it",
|
|
803
|
+
"its",
|
|
804
|
+
"he",
|
|
805
|
+
"she",
|
|
806
|
+
"they",
|
|
807
|
+
"them",
|
|
808
|
+
"their",
|
|
809
|
+
"we",
|
|
810
|
+
"us",
|
|
811
|
+
"our",
|
|
812
|
+
"you",
|
|
813
|
+
"your",
|
|
814
|
+
"i",
|
|
815
|
+
"me",
|
|
816
|
+
"my",
|
|
817
|
+
"le",
|
|
818
|
+
"la",
|
|
819
|
+
"les",
|
|
820
|
+
"un",
|
|
821
|
+
"une",
|
|
822
|
+
"des",
|
|
823
|
+
"du",
|
|
824
|
+
"de",
|
|
825
|
+
"et",
|
|
826
|
+
"ou",
|
|
827
|
+
"que",
|
|
828
|
+
"qui",
|
|
829
|
+
"quoi",
|
|
830
|
+
"dont",
|
|
831
|
+
"ce",
|
|
832
|
+
"cet",
|
|
833
|
+
"cette",
|
|
834
|
+
"ces",
|
|
835
|
+
"est",
|
|
836
|
+
"sont",
|
|
837
|
+
"etait",
|
|
838
|
+
"etaient",
|
|
839
|
+
"avoir",
|
|
840
|
+
"etre",
|
|
841
|
+
"faire",
|
|
842
|
+
"dans",
|
|
843
|
+
"pour",
|
|
844
|
+
"sur",
|
|
845
|
+
"avec",
|
|
846
|
+
"par",
|
|
847
|
+
"a",
|
|
848
|
+
"au",
|
|
849
|
+
"aux"
|
|
850
|
+
]);
|
|
851
|
+
function tokenizeTerms(text) {
|
|
852
|
+
return text.replace(/[^a-zA-Z0-9\u00C0-\u017F]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/\s+/).filter(Boolean).filter((t2) => t2.length > 1 && !STOP_WORDS.has(t2));
|
|
853
|
+
}
|
|
854
|
+
function buildInvertedIndex(files) {
|
|
855
|
+
const index = {};
|
|
856
|
+
for (const file of Object.values(files)) {
|
|
857
|
+
for (const chunk of file.chunks) {
|
|
858
|
+
const terms = /* @__PURE__ */ new Set([
|
|
859
|
+
...tokenizeTerms(chunk.content),
|
|
860
|
+
...tokenizeTerms(chunk.relPath),
|
|
861
|
+
...tokenizeTerms(chunk.name ?? "")
|
|
862
|
+
]);
|
|
863
|
+
for (const term of terms) {
|
|
864
|
+
if (!index[term]) index[term] = {};
|
|
865
|
+
if (!index[term][file.relPath]) index[term][file.relPath] = 0;
|
|
866
|
+
index[term][file.relPath] += 1;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return index;
|
|
871
|
+
}
|
|
872
|
+
async function loadIndex(projectPath) {
|
|
873
|
+
const absProject = await findProjectRoot(resolveProjectPath(projectPath));
|
|
874
|
+
const p = cacheFilePath(absProject);
|
|
875
|
+
if (!existsSync(p)) return null;
|
|
876
|
+
try {
|
|
877
|
+
const raw = await readFile3(p, "utf8");
|
|
878
|
+
const data = JSON.parse(raw);
|
|
879
|
+
if (data.version !== INDEX_VERSION) return null;
|
|
880
|
+
return data;
|
|
881
|
+
} catch {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function saveIndex(index) {
|
|
886
|
+
const p = cacheFilePath(index.projectPath);
|
|
887
|
+
await mkdir(dirname2(p), { recursive: true });
|
|
888
|
+
await writeFile(p, JSON.stringify(index), "utf8");
|
|
889
|
+
}
|
|
890
|
+
async function embedIndex(index, progress) {
|
|
891
|
+
const allChunks = Object.values(index.files).flatMap((f) => f.chunks);
|
|
892
|
+
const missing = allChunks.filter((c) => !c.embedding || c.embedding.length === 0);
|
|
893
|
+
if (missing.length === 0) return;
|
|
894
|
+
const batchSize = 32;
|
|
895
|
+
for (let i = 0; i < missing.length; i += batchSize) {
|
|
896
|
+
const batch = missing.slice(i, i + batchSize);
|
|
897
|
+
progress?.(`Embedding ${i + batch.length}/${missing.length} chunks...`);
|
|
898
|
+
try {
|
|
899
|
+
const vectors = await getEmbeddings(batch.map((c) => c.content));
|
|
900
|
+
for (let j = 0; j < batch.length; j++) {
|
|
901
|
+
batch[j].embedding = vectors[j];
|
|
902
|
+
}
|
|
903
|
+
} catch (err) {
|
|
904
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
905
|
+
progress?.(`Embedding failed: ${msg}`);
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
await saveIndex(index);
|
|
910
|
+
}
|
|
911
|
+
async function buildIndex(projectPath, progress) {
|
|
912
|
+
const absProject = await findProjectRoot(resolveProjectPath(projectPath));
|
|
913
|
+
const projectName = absProject.split(sep2).pop() ?? "project";
|
|
914
|
+
progress?.(`Indexing ${projectName}...`);
|
|
915
|
+
const walk = await getWalkOptions(absProject);
|
|
916
|
+
const tree = await buildTree(absProject, void 0, walk.skipDirs);
|
|
917
|
+
const startDir = absProject;
|
|
918
|
+
const previous = await loadIndex(absProject);
|
|
919
|
+
const previousFiles = previous?.projectPath === absProject ? previous.files : {};
|
|
920
|
+
const files = {};
|
|
921
|
+
let totalTokens = 0;
|
|
922
|
+
let reused = 0;
|
|
923
|
+
for await (const fullPath of walkFiles(startDir, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
|
|
924
|
+
const relPath = relative2(startDir, fullPath).split(sep2).join("/");
|
|
925
|
+
progress?.(`Reading ${relPath}`);
|
|
926
|
+
const fstats = await stat2(fullPath);
|
|
927
|
+
const cached = previousFiles[relPath];
|
|
928
|
+
if (cached && cached.mtimeMs === fstats.mtimeMs && cached.size === fstats.size) {
|
|
929
|
+
files[relPath] = cached;
|
|
930
|
+
reused++;
|
|
931
|
+
totalTokens += cached.chunks.reduce((sum, c) => sum + c.tokens, 0);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const text = await safeReadText(fullPath);
|
|
935
|
+
if (!text) continue;
|
|
936
|
+
const hash = fileHash(fstats, text);
|
|
937
|
+
const ext = relPath.slice(relPath.lastIndexOf(".")).toLowerCase();
|
|
938
|
+
await ensureTreeSitterForExt(ext);
|
|
939
|
+
const chunks = extractChunks(relPath, text).map((chunk) => ({
|
|
940
|
+
...chunk,
|
|
941
|
+
// recompute tokens to be safe
|
|
942
|
+
tokens: countTokens(chunk.content)
|
|
943
|
+
}));
|
|
944
|
+
totalTokens += chunks.reduce((sum, c) => sum + c.tokens, 0);
|
|
945
|
+
files[relPath] = {
|
|
946
|
+
relPath,
|
|
947
|
+
size: fstats.size,
|
|
948
|
+
mtimeMs: fstats.mtimeMs,
|
|
949
|
+
hash,
|
|
950
|
+
chunks
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const now = Date.now();
|
|
954
|
+
const index = {
|
|
955
|
+
projectPath: absProject,
|
|
956
|
+
projectHash: projectHash(absProject),
|
|
957
|
+
version: INDEX_VERSION,
|
|
958
|
+
createdAt: previous?.createdAt ?? now,
|
|
959
|
+
updatedAt: now,
|
|
960
|
+
tree,
|
|
961
|
+
constraints: [],
|
|
962
|
+
files,
|
|
963
|
+
terms: buildInvertedIndex(files)
|
|
964
|
+
};
|
|
965
|
+
await saveIndex(index);
|
|
966
|
+
progress?.(`Indexed ${Object.keys(files).length} files, ~${totalTokens} tokens${reused ? ` (${reused} unchanged reused)` : ""}`);
|
|
967
|
+
return index;
|
|
968
|
+
}
|
|
969
|
+
async function getIndex(projectPath, progress, force = false) {
|
|
970
|
+
const resolved = resolveProjectPath(projectPath);
|
|
971
|
+
const absProject = await findProjectRoot(resolved);
|
|
972
|
+
const existing = force ? null : await loadIndex(resolved);
|
|
973
|
+
if (existing && existing.projectPath === absProject) {
|
|
974
|
+
let stale = false;
|
|
975
|
+
for (const file of Object.values(existing.files)) {
|
|
976
|
+
const fullPath = join4(absProject, file.relPath);
|
|
977
|
+
try {
|
|
978
|
+
const fstats = await stat2(fullPath);
|
|
979
|
+
if (fstats.mtimeMs !== file.mtimeMs || fstats.size !== file.size) {
|
|
980
|
+
stale = true;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
stale = true;
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (!stale) {
|
|
989
|
+
progress?.("Loaded index from cache");
|
|
990
|
+
return existing;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return buildIndex(projectPath, progress);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/retriever.ts
|
|
997
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
998
|
+
"the",
|
|
999
|
+
"is",
|
|
1000
|
+
"are",
|
|
1001
|
+
"was",
|
|
1002
|
+
"were",
|
|
1003
|
+
"be",
|
|
1004
|
+
"been",
|
|
1005
|
+
"being",
|
|
1006
|
+
"have",
|
|
1007
|
+
"has",
|
|
1008
|
+
"had",
|
|
1009
|
+
"do",
|
|
1010
|
+
"does",
|
|
1011
|
+
"did",
|
|
1012
|
+
"will",
|
|
1013
|
+
"would",
|
|
1014
|
+
"could",
|
|
1015
|
+
"should",
|
|
1016
|
+
"may",
|
|
1017
|
+
"might",
|
|
1018
|
+
"must",
|
|
1019
|
+
"shall",
|
|
1020
|
+
"can",
|
|
1021
|
+
"need",
|
|
1022
|
+
"dare",
|
|
1023
|
+
"ought",
|
|
1024
|
+
"used",
|
|
1025
|
+
"to",
|
|
1026
|
+
"of",
|
|
1027
|
+
"in",
|
|
1028
|
+
"for",
|
|
1029
|
+
"on",
|
|
1030
|
+
"with",
|
|
1031
|
+
"at",
|
|
1032
|
+
"by",
|
|
1033
|
+
"from",
|
|
1034
|
+
"as",
|
|
1035
|
+
"and",
|
|
1036
|
+
"or",
|
|
1037
|
+
"but",
|
|
1038
|
+
"so",
|
|
1039
|
+
"yet",
|
|
1040
|
+
"a",
|
|
1041
|
+
"an",
|
|
1042
|
+
"this",
|
|
1043
|
+
"that",
|
|
1044
|
+
"these",
|
|
1045
|
+
"those",
|
|
1046
|
+
"it",
|
|
1047
|
+
"its",
|
|
1048
|
+
"he",
|
|
1049
|
+
"she",
|
|
1050
|
+
"they",
|
|
1051
|
+
"them",
|
|
1052
|
+
"their",
|
|
1053
|
+
"we",
|
|
1054
|
+
"us",
|
|
1055
|
+
"our",
|
|
1056
|
+
"you",
|
|
1057
|
+
"your",
|
|
1058
|
+
"i",
|
|
1059
|
+
"me",
|
|
1060
|
+
"my",
|
|
1061
|
+
"le",
|
|
1062
|
+
"la",
|
|
1063
|
+
"les",
|
|
1064
|
+
"un",
|
|
1065
|
+
"une",
|
|
1066
|
+
"des",
|
|
1067
|
+
"du",
|
|
1068
|
+
"de",
|
|
1069
|
+
"et",
|
|
1070
|
+
"ou",
|
|
1071
|
+
"que",
|
|
1072
|
+
"qui",
|
|
1073
|
+
"quoi",
|
|
1074
|
+
"dont",
|
|
1075
|
+
"ce",
|
|
1076
|
+
"cet",
|
|
1077
|
+
"cette",
|
|
1078
|
+
"ces",
|
|
1079
|
+
"est",
|
|
1080
|
+
"sont",
|
|
1081
|
+
"etait",
|
|
1082
|
+
"etaient",
|
|
1083
|
+
"avoir",
|
|
1084
|
+
"etre",
|
|
1085
|
+
"faire",
|
|
1086
|
+
"dans",
|
|
1087
|
+
"pour",
|
|
1088
|
+
"sur",
|
|
1089
|
+
"avec",
|
|
1090
|
+
"par",
|
|
1091
|
+
"a",
|
|
1092
|
+
"au",
|
|
1093
|
+
"aux"
|
|
1094
|
+
]);
|
|
1095
|
+
function tokenizeQuery(text) {
|
|
1096
|
+
return text.replace(/[^a-zA-Z0-9\u00C0-\u017F]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/\s+/).filter(Boolean).filter((t2) => t2.length > 2 && !STOP_WORDS2.has(t2));
|
|
1097
|
+
}
|
|
1098
|
+
function getChunkKey(chunk) {
|
|
1099
|
+
return `${chunk.relPath}:${chunk.startLine}:${chunk.endLine}`;
|
|
1100
|
+
}
|
|
1101
|
+
function lexicalScore(index, query) {
|
|
1102
|
+
const terms = tokenizeQuery(query);
|
|
1103
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1104
|
+
if (terms.length === 0) return scores;
|
|
1105
|
+
for (const term of terms) {
|
|
1106
|
+
const posting = index.terms[term];
|
|
1107
|
+
if (!posting) continue;
|
|
1108
|
+
for (const [relPath, count] of Object.entries(posting)) {
|
|
1109
|
+
const file = index.files[relPath];
|
|
1110
|
+
if (!file) continue;
|
|
1111
|
+
for (const chunk of file.chunks) {
|
|
1112
|
+
const key = getChunkKey(chunk);
|
|
1113
|
+
const contentHit = chunk.content.toLowerCase().includes(term);
|
|
1114
|
+
const nameHit = chunk.name ? tokenizeQuery(chunk.name).includes(term) : false;
|
|
1115
|
+
if (!contentHit && !nameHit) continue;
|
|
1116
|
+
const bonus = (nameHit ? 4 : 0) + (chunk.kind === "function" || chunk.kind === "method" ? 1 : 0) + (contentHit ? 1 : 0);
|
|
1117
|
+
const prev = scores.get(key) ?? 0;
|
|
1118
|
+
scores.set(key, prev + count + bonus);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return scores;
|
|
1123
|
+
}
|
|
1124
|
+
async function scoreChunks(index, query, embed = false) {
|
|
1125
|
+
const lexScores = lexicalScore(index, query);
|
|
1126
|
+
const allChunks = [];
|
|
1127
|
+
for (const file of Object.values(index.files)) {
|
|
1128
|
+
allChunks.push(...file.chunks);
|
|
1129
|
+
}
|
|
1130
|
+
let queryEmbedding = null;
|
|
1131
|
+
const hasEmbeddings = embed && allChunks.some((c) => c.embedding && c.embedding.length > 0);
|
|
1132
|
+
if (hasEmbeddings) {
|
|
1133
|
+
try {
|
|
1134
|
+
queryEmbedding = await getEmbedding(query);
|
|
1135
|
+
} catch {
|
|
1136
|
+
queryEmbedding = null;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const scored = [];
|
|
1140
|
+
for (const chunk of allChunks) {
|
|
1141
|
+
const key = getChunkKey(chunk);
|
|
1142
|
+
let score = lexScores.get(key) ?? 0;
|
|
1143
|
+
if (queryEmbedding && chunk.embedding && chunk.embedding.length === queryEmbedding.length) {
|
|
1144
|
+
const sim = cosineSimilarity(queryEmbedding, chunk.embedding);
|
|
1145
|
+
score += sim * 50;
|
|
1146
|
+
} else if (score === 0 && !hasEmbeddings) {
|
|
1147
|
+
score = 0.1;
|
|
1148
|
+
}
|
|
1149
|
+
scored.push({ ...chunk, score });
|
|
1150
|
+
}
|
|
1151
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
1152
|
+
}
|
|
1153
|
+
function selectChunks(scored, maxTokens, maxChunkTokens = Infinity) {
|
|
1154
|
+
const result = [];
|
|
1155
|
+
const covered = /* @__PURE__ */ new Map();
|
|
1156
|
+
let used = 0;
|
|
1157
|
+
for (const chunk of scored) {
|
|
1158
|
+
if (chunk.tokens > maxChunkTokens) continue;
|
|
1159
|
+
if (used + chunk.tokens > maxTokens) continue;
|
|
1160
|
+
const ranges = covered.get(chunk.relPath) ?? [];
|
|
1161
|
+
const duplicated = ranges.some(([s, e]) => chunk.startLine >= s && chunk.endLine <= e);
|
|
1162
|
+
if (duplicated) continue;
|
|
1163
|
+
ranges.push([chunk.startLine, chunk.endLine]);
|
|
1164
|
+
covered.set(chunk.relPath, ranges);
|
|
1165
|
+
result.push(chunk);
|
|
1166
|
+
used += chunk.tokens;
|
|
1167
|
+
}
|
|
1168
|
+
return { chunks: result, tokens: used };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/context.ts
|
|
1172
|
+
var DEFAULT_MAX_TOKENS = 6e4;
|
|
1173
|
+
var HEAD_BUDGET_TOKENS = 800;
|
|
1174
|
+
function getLabels(lang) {
|
|
1175
|
+
return lang === "en" ? {
|
|
1176
|
+
project: "Project",
|
|
1177
|
+
focus: "Focus",
|
|
1178
|
+
tree: "File tree",
|
|
1179
|
+
noConstraints: "No explicit constraints documented.",
|
|
1180
|
+
constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
|
|
1181
|
+
answerIn: "Answer in English."
|
|
1182
|
+
} : {
|
|
1183
|
+
project: "Projet",
|
|
1184
|
+
focus: "Focus",
|
|
1185
|
+
tree: "Arborescence",
|
|
1186
|
+
noConstraints: "Aucune contrainte explicite document\xE9e.",
|
|
1187
|
+
constraints: "CONTRAINTES PRODUIT IDENTIFI\xC9ES",
|
|
1188
|
+
answerIn: "R\xE9ponds obligatoirement en fran\xE7ais."
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
async function extractProductConstraints(absProject) {
|
|
1192
|
+
const candidates = ["README.md", "README.MD", "readme.md", "MEMORY.md", "CONTRIBUTING.md"];
|
|
1193
|
+
const constraints = [];
|
|
1194
|
+
for (const name of candidates) {
|
|
1195
|
+
const text = await safeReadText(join5(absProject, name));
|
|
1196
|
+
if (!text) continue;
|
|
1197
|
+
const regex = /(?:constraint|contrainte|must|doit|interdit|forbidden|rule|règle|limitation)[\s\S]{0,200}/gi;
|
|
1198
|
+
let m;
|
|
1199
|
+
while ((m = regex.exec(text)) !== null) {
|
|
1200
|
+
const line = m[0].replace(/\s+/g, " ").trim();
|
|
1201
|
+
if (line.length > 20) constraints.push(`[${name}] ${line}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return constraints.slice(0, 12);
|
|
1205
|
+
}
|
|
1206
|
+
function formatChunk(chunk) {
|
|
1207
|
+
const kind = chunk.kind.toUpperCase();
|
|
1208
|
+
const source = `[source: ${chunk.relPath}:${chunk.startLine}-${chunk.endLine}]`;
|
|
1209
|
+
const kindLabel = chunk.kind === "unknown" ? "" : ` (${kind})`;
|
|
1210
|
+
const header = `--- ${chunk.relPath}${chunk.name ? ` :: ${chunk.name}` : ""}${kindLabel} ${source} ---`;
|
|
1211
|
+
return `${header}
|
|
1212
|
+
${chunk.content}`;
|
|
1213
|
+
}
|
|
1214
|
+
async function buildContext(options) {
|
|
1215
|
+
const { project, query, filePath, searchQuery, instruction, embed = false } = options;
|
|
1216
|
+
const absProject = await findProjectRoot(resolveProjectPath(project));
|
|
1217
|
+
const cfg = await loadProjectConfig(absProject);
|
|
1218
|
+
const maxTokens = options.maxTokens ?? cfg.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
1219
|
+
const lang = options.lang ?? cfg.lang ?? "fr";
|
|
1220
|
+
const labels = getLabels(lang);
|
|
1221
|
+
const index = await getIndex(absProject);
|
|
1222
|
+
if (embed) {
|
|
1223
|
+
try {
|
|
1224
|
+
await embedIndex(index);
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const focus = filePath ?? searchQuery ?? query;
|
|
1229
|
+
let selectedChunks = [];
|
|
1230
|
+
const bodyLimit = maxTokens - HEAD_BUDGET_TOKENS;
|
|
1231
|
+
const maxChunkTokens = Math.floor(bodyLimit / 2);
|
|
1232
|
+
if (filePath) {
|
|
1233
|
+
const all = Object.values(index.files);
|
|
1234
|
+
const target = all.find((f) => f.relPath === filePath) ?? all.filter((f) => f.relPath.endsWith(`/${filePath}`) || f.relPath.endsWith(filePath)).sort((a, b) => a.relPath.length - b.relPath.length)[0];
|
|
1235
|
+
if (target) {
|
|
1236
|
+
const { chunks } = selectChunks(
|
|
1237
|
+
target.chunks.map((c) => ({ ...c, score: 0 })),
|
|
1238
|
+
bodyLimit,
|
|
1239
|
+
Infinity
|
|
1240
|
+
);
|
|
1241
|
+
selectedChunks = chunks;
|
|
1242
|
+
} else {
|
|
1243
|
+
const scored = await scoreChunks(index, filePath, embed);
|
|
1244
|
+
const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
|
|
1245
|
+
if (chunks.length === 0) throw new Error(`File not found: ${filePath}`);
|
|
1246
|
+
selectedChunks = chunks;
|
|
1247
|
+
}
|
|
1248
|
+
} else if (searchQuery) {
|
|
1249
|
+
const scored = await scoreChunks(index, searchQuery, embed);
|
|
1250
|
+
const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
|
|
1251
|
+
selectedChunks = chunks;
|
|
1252
|
+
} else {
|
|
1253
|
+
const scored = await scoreChunks(index, query, embed);
|
|
1254
|
+
const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
|
|
1255
|
+
selectedChunks = chunks;
|
|
1256
|
+
}
|
|
1257
|
+
const constraints = await extractProductConstraints(absProject);
|
|
1258
|
+
const constraintsText = constraints.length ? `== ${labels.constraints} ==
|
|
1259
|
+
${constraints.map((c) => `- ${c}`).join("\n")}` : `== ${labels.constraints} ==
|
|
1260
|
+
${labels.noConstraints}`;
|
|
1261
|
+
const head = `${labels.project} : ${absProject}
|
|
1262
|
+
${labels.focus} : ${focus}
|
|
1263
|
+
${constraintsText}
|
|
1264
|
+
|
|
1265
|
+
== ${labels.tree} ==
|
|
1266
|
+
${index.tree}
|
|
1267
|
+
`;
|
|
1268
|
+
const headTokens = countTokens(head);
|
|
1269
|
+
const body = selectedChunks.map((c) => formatChunk(c)).join("\n\n");
|
|
1270
|
+
const bodyBudget = Math.max(0, maxTokens - headTokens - 100);
|
|
1271
|
+
const truncatedBody = truncateToTokens(body, bodyBudget);
|
|
1272
|
+
const baseInstruction = `${labels.answerIn}
|
|
1273
|
+
Answer the question or perform the requested task using the code context above. Cite every technical claim with [source: relative/path:line]. Provide confidence and severity where relevant.`;
|
|
1274
|
+
const finalInstruction = instruction ? `${instruction}
|
|
1275
|
+
|
|
1276
|
+
${baseInstruction}` : baseInstruction;
|
|
1277
|
+
const prompt = `${head}
|
|
1278
|
+
|
|
1279
|
+
${truncatedBody}
|
|
1280
|
+
|
|
1281
|
+
${finalInstruction}`;
|
|
1282
|
+
const tokenCount = countTokens(prompt);
|
|
1283
|
+
return {
|
|
1284
|
+
absProject,
|
|
1285
|
+
context: prompt,
|
|
1286
|
+
chunks: selectedChunks,
|
|
1287
|
+
tokenCount
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/analysis.ts
|
|
1292
|
+
import { basename, extname as extname2, join as join6, relative as relative3, sep as sep3, posix as posixPath } from "path";
|
|
1293
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1294
|
+
var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
1295
|
+
var ENTRY_BASENAMES = /* @__PURE__ */ new Set(["index", "main", "app", "cli", "server", "bin", "mod"]);
|
|
1296
|
+
var SKIP_EXTS = /* @__PURE__ */ new Set([".d.ts", ".test.ts", ".test.js", ".spec.ts", ".spec.js", ".config.js", ".config.ts", ".config.mjs"]);
|
|
1297
|
+
var IMPORT_RE = /(?:import|export)\s+(?:[\w*{}\s,]+\s+from\s+)?['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)|import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1298
|
+
var EXPORT_RE = /export\s+(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)|export\s*\{\s*([^}]+)\}|export\s+default\b/g;
|
|
1299
|
+
function parseImports(text, relPath, known) {
|
|
1300
|
+
const out = [];
|
|
1301
|
+
let m;
|
|
1302
|
+
IMPORT_RE.lastIndex = 0;
|
|
1303
|
+
while ((m = IMPORT_RE.exec(text)) !== null) {
|
|
1304
|
+
const spec = m[1] ?? m[2] ?? m[3];
|
|
1305
|
+
if (!spec || !spec.startsWith(".")) continue;
|
|
1306
|
+
const base = posixPath.normalize(posixPath.join(posixPath.dirname(relPath), spec));
|
|
1307
|
+
const noExt = base.replace(/\.(js|jsx|mjs|cjs|ts|tsx)$/, "");
|
|
1308
|
+
for (const cand of [base, `${noExt}.ts`, `${noExt}.tsx`, `${noExt}.js`, `${noExt}.jsx`, `${noExt}.mjs`, `${base}/index.ts`, `${base}/index.js`]) {
|
|
1309
|
+
if (known.has(cand)) {
|
|
1310
|
+
out.push(cand);
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return out;
|
|
1316
|
+
}
|
|
1317
|
+
function parseExports(text) {
|
|
1318
|
+
const out = [];
|
|
1319
|
+
let m;
|
|
1320
|
+
EXPORT_RE.lastIndex = 0;
|
|
1321
|
+
while ((m = EXPORT_RE.exec(text)) !== null) {
|
|
1322
|
+
const line = text.slice(0, m.index).split("\n").length;
|
|
1323
|
+
if (m[1]) out.push({ name: m[1], line });
|
|
1324
|
+
else if (m[2]) {
|
|
1325
|
+
for (const part of m[2].split(",")) {
|
|
1326
|
+
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
1327
|
+
if (name) out.push({ name, line });
|
|
1328
|
+
}
|
|
1329
|
+
} else out.push({ name: "default", line });
|
|
1330
|
+
}
|
|
1331
|
+
return out;
|
|
1332
|
+
}
|
|
1333
|
+
function findCycles(edges) {
|
|
1334
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1335
|
+
for (const e of edges) {
|
|
1336
|
+
if (!adj.has(e.from)) adj.set(e.from, []);
|
|
1337
|
+
adj.get(e.from).push(e.to);
|
|
1338
|
+
}
|
|
1339
|
+
const cycles = [];
|
|
1340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1341
|
+
const stack = [];
|
|
1342
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1343
|
+
function dfs(node) {
|
|
1344
|
+
stack.push(node);
|
|
1345
|
+
onStack.add(node);
|
|
1346
|
+
for (const next of adj.get(node) ?? []) {
|
|
1347
|
+
if (onStack.has(next)) {
|
|
1348
|
+
const cycle = stack.slice(stack.indexOf(next)).concat(next);
|
|
1349
|
+
const body = cycle.slice(0, -1);
|
|
1350
|
+
const minIdx = body.indexOf(body.reduce((a, b) => a < b ? a : b));
|
|
1351
|
+
const key = body.slice(minIdx).concat(body.slice(0, minIdx)).join(">");
|
|
1352
|
+
if (!seen.has(key)) {
|
|
1353
|
+
seen.add(key);
|
|
1354
|
+
cycles.push({ path: cycle });
|
|
1355
|
+
}
|
|
1356
|
+
} else if (!stack.includes(next)) {
|
|
1357
|
+
dfs(next);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
stack.pop();
|
|
1361
|
+
onStack.delete(node);
|
|
1362
|
+
}
|
|
1363
|
+
for (const n of adj.keys()) dfs(n);
|
|
1364
|
+
return cycles;
|
|
1365
|
+
}
|
|
1366
|
+
function looksLikeEntry(rel, pkg) {
|
|
1367
|
+
const base = basename(rel).toLowerCase().replace(extname2(rel), "");
|
|
1368
|
+
if (ENTRY_BASENAMES.has(base)) return true;
|
|
1369
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(rel) || rel.includes("__tests__/") || /^e2e[.-]/.test(basename(rel))) return true;
|
|
1370
|
+
if (/\.(config|rc)\.[cm]?[jt]s$/.test(rel)) return true;
|
|
1371
|
+
if (/^(pages|app|routes|api|bin|scripts)\//.test(rel) || rel.includes("/pages/") || rel.includes("/routes/")) return true;
|
|
1372
|
+
const fields = [pkg?.main, pkg?.module, pkg?.bin, pkg?.exports?.["."]];
|
|
1373
|
+
for (const f of fields.flatMap((v) => typeof v === "string" ? [v] : v ? Object.values(v) : [])) {
|
|
1374
|
+
if (typeof f === "string" && rel.endsWith(f.replace(/^\.\//, ""))) return true;
|
|
1375
|
+
}
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
function complexityOf(text) {
|
|
1379
|
+
const matches = text.match(/\b(if|else if|for|while|case|catch|&&|\|\||\?)\b|\?\./g);
|
|
1380
|
+
return 1 + (matches ? matches.length : 0);
|
|
1381
|
+
}
|
|
1382
|
+
var WINDOW = 6;
|
|
1383
|
+
function findDuplicates(fileTexts) {
|
|
1384
|
+
const windows = /* @__PURE__ */ new Map();
|
|
1385
|
+
for (const [file, text] of fileTexts) {
|
|
1386
|
+
const lines = text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && l !== "{" && l !== "}");
|
|
1387
|
+
for (let i = 0; i + WINDOW <= lines.length; i++) {
|
|
1388
|
+
const key = lines.slice(i, i + WINDOW).join("\n");
|
|
1389
|
+
if (key.length < 60) continue;
|
|
1390
|
+
if (!windows.has(key)) windows.set(key, []);
|
|
1391
|
+
const arr = windows.get(key);
|
|
1392
|
+
if (!arr.some((w) => w.file === file)) arr.push({ file, line: i + 1 });
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1396
|
+
for (const [key, hits] of windows) {
|
|
1397
|
+
if (hits.length < 2) continue;
|
|
1398
|
+
const files = [...new Set(hits.map((h) => h.file))].sort();
|
|
1399
|
+
const gk = files.join("|");
|
|
1400
|
+
const preview = key.split("\n")[0].slice(0, 80);
|
|
1401
|
+
const g = groups.get(gk);
|
|
1402
|
+
if (g) g.lines += WINDOW;
|
|
1403
|
+
else groups.set(gk, { files, lines: WINDOW, preview });
|
|
1404
|
+
}
|
|
1405
|
+
return [...groups.values()].sort((a, b) => b.lines - a.lines).slice(0, 15);
|
|
1406
|
+
}
|
|
1407
|
+
async function analyzeProject(projectPath) {
|
|
1408
|
+
const abs = await findProjectRoot(resolveProjectPath(projectPath));
|
|
1409
|
+
const fileTexts = /* @__PURE__ */ new Map();
|
|
1410
|
+
const codeFiles = [];
|
|
1411
|
+
const walk = await getWalkOptions(abs);
|
|
1412
|
+
for await (const full of walkFiles(abs, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
|
|
1413
|
+
const rel = relative3(abs, full).split(sep3).join("/");
|
|
1414
|
+
const ext = extname2(rel).toLowerCase();
|
|
1415
|
+
if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
|
|
1416
|
+
const text = await safeReadText(full);
|
|
1417
|
+
if (!text) continue;
|
|
1418
|
+
codeFiles.push(rel);
|
|
1419
|
+
fileTexts.set(rel, text);
|
|
1420
|
+
}
|
|
1421
|
+
let pkg = {};
|
|
1422
|
+
try {
|
|
1423
|
+
pkg = JSON.parse(await readFile4(join6(abs, "package.json"), "utf8"));
|
|
1424
|
+
} catch {
|
|
1425
|
+
}
|
|
1426
|
+
const known = new Set(codeFiles);
|
|
1427
|
+
const edges = [];
|
|
1428
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1429
|
+
for (const rel of codeFiles) {
|
|
1430
|
+
for (const to of parseImports(fileTexts.get(rel), rel, known)) {
|
|
1431
|
+
edges.push({ from: rel, to });
|
|
1432
|
+
inDegree.set(to, (inDegree.get(to) ?? 0) + 1);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const cycles = findCycles(edges);
|
|
1436
|
+
const unusedFiles = codeFiles.filter((rel) => !inDegree.has(rel) && !looksLikeEntry(rel, pkg)).sort();
|
|
1437
|
+
const otherText = /* @__PURE__ */ new Map();
|
|
1438
|
+
for (const [file, text] of fileTexts) otherText.set(file, text);
|
|
1439
|
+
const unusedExports = [];
|
|
1440
|
+
for (const rel of codeFiles) {
|
|
1441
|
+
for (const exp of parseExports(fileTexts.get(rel))) {
|
|
1442
|
+
if (exp.name === "default") continue;
|
|
1443
|
+
let used = false;
|
|
1444
|
+
for (const [otherFile, text] of fileTexts) {
|
|
1445
|
+
if (otherFile === rel) continue;
|
|
1446
|
+
if (new RegExp(`\\b${exp.name.replace(/[$_]/g, "\\$&")}\\b`).test(text)) {
|
|
1447
|
+
used = true;
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (!used) unusedExports.push({ file: rel, name: exp.name, line: exp.line });
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
const duplicates = findDuplicates(fileTexts);
|
|
1455
|
+
const hotspots = [];
|
|
1456
|
+
for (const [rel, text] of fileTexts) {
|
|
1457
|
+
const score2 = complexityOf(text);
|
|
1458
|
+
if (score2 >= 12) hotspots.push({ file: rel, startLine: 1, score: score2 });
|
|
1459
|
+
}
|
|
1460
|
+
hotspots.sort((a, b) => b.score - a.score);
|
|
1461
|
+
const codeLines = [...fileTexts.values()].reduce((s, t2) => s + t2.split("\n").length, 0);
|
|
1462
|
+
const dupLines = duplicates.reduce((s, g) => s + g.lines, 0);
|
|
1463
|
+
const penalties = cycles.length * 6 + unusedFiles.length * 2 + Math.min(unusedExports.length, 20) * 1 + Math.round(dupLines / Math.max(codeLines, 1) * 100) + Math.min(hotspots.length, 15) * 2;
|
|
1464
|
+
const score = Math.max(0, Math.min(100, 100 - penalties));
|
|
1465
|
+
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 65 ? "C" : score >= 50 ? "D" : "E";
|
|
1466
|
+
return {
|
|
1467
|
+
projectPath: abs,
|
|
1468
|
+
analyzedFiles: codeFiles.length,
|
|
1469
|
+
importEdges: edges.length,
|
|
1470
|
+
cycles,
|
|
1471
|
+
unusedFiles,
|
|
1472
|
+
unusedExports,
|
|
1473
|
+
duplicates,
|
|
1474
|
+
hotspots,
|
|
1475
|
+
score,
|
|
1476
|
+
grade
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function formatHealthReport(r, lang = "fr") {
|
|
1480
|
+
const t2 = lang === "en" ? {
|
|
1481
|
+
title: "STATIC ANALYSIS",
|
|
1482
|
+
files: "files analyzed",
|
|
1483
|
+
edges: "local imports",
|
|
1484
|
+
cycles: "Circular dependencies",
|
|
1485
|
+
none: "none",
|
|
1486
|
+
unusedFiles: "Unused files (candidates)",
|
|
1487
|
+
unusedExports: "Unused exports (candidates)",
|
|
1488
|
+
dupes: "Duplicate code blocks",
|
|
1489
|
+
hotspots: "Complexity hotspots",
|
|
1490
|
+
score: "Health score",
|
|
1491
|
+
noteUnused: "candidates \u2014 entry points and framework conventions excluded"
|
|
1492
|
+
} : {
|
|
1493
|
+
title: "ANALYSE STATIQUE",
|
|
1494
|
+
files: "fichiers analys\xE9s",
|
|
1495
|
+
edges: "imports locaux",
|
|
1496
|
+
cycles: "D\xE9pendances circulaires",
|
|
1497
|
+
none: "aucune",
|
|
1498
|
+
unusedFiles: "Fichiers inutilis\xE9s (candidats)",
|
|
1499
|
+
unusedExports: "Exports inutilis\xE9s (candidats)",
|
|
1500
|
+
dupes: "Blocs de code dupliqu\xE9s",
|
|
1501
|
+
hotspots: "Hotspots de complexit\xE9",
|
|
1502
|
+
score: "Score de sant\xE9",
|
|
1503
|
+
noteUnused: "candidats \u2014 points d\u2019entr\xE9e et conventions exclus"
|
|
1504
|
+
};
|
|
1505
|
+
const out = [];
|
|
1506
|
+
out.push(`== ${t2.title} \u2014 ${basename(r.projectPath)} ==`);
|
|
1507
|
+
out.push(`${t2.score}: ${r.score}/100 (${r.grade}) \xB7 ${r.analyzedFiles} ${t2.files} \xB7 ${r.importEdges} ${t2.edges}`);
|
|
1508
|
+
out.push("");
|
|
1509
|
+
out.push(`\u25CF ${t2.cycles} (${r.cycles.length})`);
|
|
1510
|
+
for (const c of r.cycles.slice(0, 10)) out.push(` ${c.path.join(" \u2192 ")}`);
|
|
1511
|
+
if (r.cycles.length === 0) out.push(` ${t2.none}`);
|
|
1512
|
+
out.push("");
|
|
1513
|
+
out.push(`\u25CF ${t2.unusedFiles} (${r.unusedFiles.length}) \u2014 ${t2.noteUnused}`);
|
|
1514
|
+
for (const f of r.unusedFiles.slice(0, 15)) out.push(` ${f}`);
|
|
1515
|
+
out.push("");
|
|
1516
|
+
out.push(`\u25CF ${t2.unusedExports} (${r.unusedExports.length})`);
|
|
1517
|
+
for (const e of r.unusedExports.slice(0, 15)) out.push(` ${e.file}:${e.line} \u2014 ${e.name}`);
|
|
1518
|
+
out.push("");
|
|
1519
|
+
out.push(`\u25CF ${t2.dupes} (${r.duplicates.length})`);
|
|
1520
|
+
for (const d of r.duplicates.slice(0, 8)) out.push(` ${d.lines} lines \xD7 ${d.files.length} files \u2014 ${d.files.join(", ")}`);
|
|
1521
|
+
out.push("");
|
|
1522
|
+
out.push(`\u25CF ${t2.hotspots} (${r.hotspots.length})`);
|
|
1523
|
+
for (const h of r.hotspots.slice(0, 10)) out.push(` ${h.file} \u2014 score ${h.score}`);
|
|
1524
|
+
return out.join("\n");
|
|
1525
|
+
}
|
|
1526
|
+
export {
|
|
1527
|
+
CONFIG_FILE,
|
|
1528
|
+
analyzeProject,
|
|
1529
|
+
buildContext,
|
|
1530
|
+
buildIndex,
|
|
1531
|
+
chunkByTokens,
|
|
1532
|
+
clearConfigCache,
|
|
1533
|
+
cosineSimilarity,
|
|
1534
|
+
countTokens,
|
|
1535
|
+
disposeTreeSitter,
|
|
1536
|
+
embedIndex,
|
|
1537
|
+
ensureTreeSitterForExt,
|
|
1538
|
+
extractChunks,
|
|
1539
|
+
findProjectRoot,
|
|
1540
|
+
formatHealthReport,
|
|
1541
|
+
getEmbedding,
|
|
1542
|
+
getEmbeddings,
|
|
1543
|
+
getExtractor,
|
|
1544
|
+
getIndex,
|
|
1545
|
+
globToRegExp,
|
|
1546
|
+
initTreeSitter,
|
|
1547
|
+
loadIndex,
|
|
1548
|
+
loadProjectConfig,
|
|
1549
|
+
matchesAnyGlob,
|
|
1550
|
+
resolveProjectPath,
|
|
1551
|
+
saveIndex,
|
|
1552
|
+
scoreChunks,
|
|
1553
|
+
selectChunks,
|
|
1554
|
+
treeSitterReady,
|
|
1555
|
+
truncateToTokens
|
|
1556
|
+
};
|
|
1557
|
+
//# sourceMappingURL=index.js.map
|