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