dsh-codebase-chat 0.19.0 → 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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/context.ts
2
- import { join as join3 } from "path";
2
+ import { join as join5 } from "path";
3
3
 
4
4
  // src/tokenizer.ts
5
5
  import { encode, decode } from "gpt-tokenizer";
@@ -28,14 +28,281 @@ function truncateToTokens(text, maxTokens) {
28
28
  }
29
29
 
30
30
  // src/indexer.ts
31
- import { mkdir, readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
32
- import { dirname, join as join2, relative, sep } from "path";
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
33
  import { existsSync } from "fs";
34
34
 
35
35
  // src/extractor.ts
36
36
  import { parse } from "@babel/parser";
37
37
  import traverse from "@babel/traverse";
38
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
39
306
  var JS_LIKE = /* @__PURE__ */ new Set([
40
307
  ".js",
41
308
  ".jsx",
@@ -157,51 +424,6 @@ function extractTopLevelWithBabel(relPath, content) {
157
424
  });
158
425
  return mergeGaps(content, relPath, chunks, visitedRanges);
159
426
  }
160
- function mergeGaps(content, relPath, namedChunks, visitedRanges) {
161
- const lines = content.split("\n");
162
- if (lines.length === 0) return namedChunks;
163
- const covered = /* @__PURE__ */ new Set();
164
- for (const [start2, end] of visitedRanges) {
165
- for (let i = start2; i <= end; i++) covered.add(i);
166
- }
167
- const all = [];
168
- let start = 1;
169
- for (let i = 1; i <= lines.length; i++) {
170
- if (!covered.has(i)) {
171
- if (i === start) {
172
- start++;
173
- continue;
174
- }
175
- continue;
176
- }
177
- if (i > start) {
178
- const gap = {
179
- relPath,
180
- startLine: start,
181
- endLine: i - 1,
182
- content: lines.slice(start - 1, i - 1).join("\n"),
183
- tokens: 0,
184
- kind: "file"
185
- };
186
- gap.tokens = countTokens(gap.content);
187
- if (gap.content.trim()) all.push(gap);
188
- }
189
- start = i + 1;
190
- }
191
- if (start <= lines.length) {
192
- const gap = {
193
- relPath,
194
- startLine: start,
195
- endLine: lines.length,
196
- content: lines.slice(start - 1).join("\n"),
197
- tokens: 0,
198
- kind: "file"
199
- };
200
- gap.tokens = countTokens(gap.content);
201
- if (gap.content.trim()) all.push(gap);
202
- }
203
- return [...all, ...namedChunks].sort((a, b) => a.startLine - b.startLine);
204
- }
205
427
  function extractWithRegex(relPath, content) {
206
428
  const lines = content.split("\n");
207
429
  const chunks = [];
@@ -250,6 +472,13 @@ function extractChunks(relPath, content) {
250
472
  return extractWithRegex(relPath, content);
251
473
  }
252
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
+ }
253
482
  return extractWithRegex(relPath, content);
254
483
  }
255
484
 
@@ -304,10 +533,68 @@ function cosineSimilarity(a, b) {
304
533
  }
305
534
 
306
535
  // src/project.ts
307
- import { readdir, readFile, stat } from "fs/promises";
308
- import { extname, join, resolve, isAbsolute } from "path";
536
+ import { readdir, readFile as readFile2, stat } from "fs/promises";
537
+ import { extname, join as join3, relative, resolve, sep, isAbsolute } from "path";
309
538
  import { createHash } from "crypto";
310
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
311
598
  var SOURCE_EXTS = /* @__PURE__ */ new Set([
312
599
  ".ts",
313
600
  ".tsx",
@@ -363,6 +650,14 @@ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
363
650
  ".dsh-vision-router"
364
651
  ]);
365
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
+ }
366
661
  function projectHash(absProject) {
367
662
  return createHash("sha256").update(absProject.toLowerCase()).digest("hex").slice(0, 16);
368
663
  }
@@ -383,16 +678,16 @@ async function findProjectRoot(absProject) {
383
678
  }
384
679
  }
385
680
  function getCacheDir() {
386
- const base = process.env.CODEBASE_CACHE_DIR || process.env.LOCALAPPDATA || process.env.APPDATA || join(homedir(), ".cache");
387
- return join(base, "dsh-codebase-chat-cache");
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");
388
683
  }
389
684
  function cacheFilePath(absProject) {
390
- return join(getCacheDir(), `${projectHash(absProject)}.json`);
685
+ return join3(getCacheDir(), `${projectHash(absProject)}.json`);
391
686
  }
392
687
  function fileHash(stats, firstBytes = "") {
393
688
  return createHash("sha256").update(`${stats.mtimeMs}:${stats.size}:${firstBytes.slice(0, 512)}`).digest("hex").slice(0, 24);
394
689
  }
395
- async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DEFAULT_SKIP_FILES) {
690
+ async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DEFAULT_SKIP_FILES, ignoreGlobs = []) {
396
691
  const queue = [startDir];
397
692
  while (queue.length) {
398
693
  const dir = queue.shift();
@@ -403,13 +698,15 @@ async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DE
403
698
  continue;
404
699
  }
405
700
  for (const entry of entries) {
406
- const fullPath = join(dir, entry.name);
701
+ const fullPath = join3(dir, entry.name);
702
+ const rel = relative(startDir, fullPath).split(sep).join("/");
407
703
  if (entry.isDirectory()) {
408
- if (!skipDirs.has(entry.name)) queue.push(fullPath);
704
+ if (!skipDirs.has(entry.name) && !matchesAnyGlob(rel, ignoreGlobs)) queue.push(fullPath);
409
705
  continue;
410
706
  }
411
707
  if (!entry.isFile()) continue;
412
708
  if (skipFiles.has(entry.name)) continue;
709
+ if (matchesAnyGlob(rel, ignoreGlobs)) continue;
413
710
  const ext = extname(entry.name).toLowerCase();
414
711
  if (!SOURCE_EXTS.has(ext)) continue;
415
712
  yield fullPath;
@@ -418,7 +715,7 @@ async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DE
418
715
  }
419
716
  async function safeReadText(filePath) {
420
717
  try {
421
- const text = await readFile(filePath, "utf8");
718
+ const text = await readFile2(filePath, "utf8");
422
719
  return text;
423
720
  } catch {
424
721
  return void 0;
@@ -438,7 +735,7 @@ async function buildTree(startDir, maxLines = 500, skipDirs = DEFAULT_SKIP_DIRS)
438
735
  for (const entry of entries) {
439
736
  if (lines.length >= maxLines) return;
440
737
  if (skipDirs.has(entry.name)) continue;
441
- const fullPath = join(dir, entry.name);
738
+ const fullPath = join3(dir, entry.name);
442
739
  if (entry.isDirectory()) {
443
740
  lines.push(`${prefix}${entry.name}/`);
444
741
  await walk(fullPath, `${prefix} `);
@@ -577,7 +874,7 @@ async function loadIndex(projectPath) {
577
874
  const p = cacheFilePath(absProject);
578
875
  if (!existsSync(p)) return null;
579
876
  try {
580
- const raw = await readFile2(p, "utf8");
877
+ const raw = await readFile3(p, "utf8");
581
878
  const data = JSON.parse(raw);
582
879
  if (data.version !== INDEX_VERSION) return null;
583
880
  return data;
@@ -587,7 +884,7 @@ async function loadIndex(projectPath) {
587
884
  }
588
885
  async function saveIndex(index) {
589
886
  const p = cacheFilePath(index.projectPath);
590
- await mkdir(dirname(p), { recursive: true });
887
+ await mkdir(dirname2(p), { recursive: true });
591
888
  await writeFile(p, JSON.stringify(index), "utf8");
592
889
  }
593
890
  async function embedIndex(index, progress) {
@@ -613,17 +910,18 @@ async function embedIndex(index, progress) {
613
910
  }
614
911
  async function buildIndex(projectPath, progress) {
615
912
  const absProject = await findProjectRoot(resolveProjectPath(projectPath));
616
- const projectName = absProject.split(sep).pop() ?? "project";
913
+ const projectName = absProject.split(sep2).pop() ?? "project";
617
914
  progress?.(`Indexing ${projectName}...`);
618
- const tree = await buildTree(absProject);
915
+ const walk = await getWalkOptions(absProject);
916
+ const tree = await buildTree(absProject, void 0, walk.skipDirs);
619
917
  const startDir = absProject;
620
918
  const previous = await loadIndex(absProject);
621
919
  const previousFiles = previous?.projectPath === absProject ? previous.files : {};
622
920
  const files = {};
623
921
  let totalTokens = 0;
624
922
  let reused = 0;
625
- for await (const fullPath of walkFiles(startDir)) {
626
- const relPath = relative(startDir, fullPath).split(sep).join("/");
923
+ for await (const fullPath of walkFiles(startDir, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
924
+ const relPath = relative2(startDir, fullPath).split(sep2).join("/");
627
925
  progress?.(`Reading ${relPath}`);
628
926
  const fstats = await stat2(fullPath);
629
927
  const cached = previousFiles[relPath];
@@ -636,6 +934,8 @@ async function buildIndex(projectPath, progress) {
636
934
  const text = await safeReadText(fullPath);
637
935
  if (!text) continue;
638
936
  const hash = fileHash(fstats, text);
937
+ const ext = relPath.slice(relPath.lastIndexOf(".")).toLowerCase();
938
+ await ensureTreeSitterForExt(ext);
639
939
  const chunks = extractChunks(relPath, text).map((chunk) => ({
640
940
  ...chunk,
641
941
  // recompute tokens to be safe
@@ -673,7 +973,7 @@ async function getIndex(projectPath, progress, force = false) {
673
973
  if (existing && existing.projectPath === absProject) {
674
974
  let stale = false;
675
975
  for (const file of Object.values(existing.files)) {
676
- const fullPath = join2(absProject, file.relPath);
976
+ const fullPath = join4(absProject, file.relPath);
677
977
  try {
678
978
  const fstats = await stat2(fullPath);
679
979
  if (fstats.mtimeMs !== file.mtimeMs || fstats.size !== file.size) {
@@ -892,7 +1192,7 @@ async function extractProductConstraints(absProject) {
892
1192
  const candidates = ["README.md", "README.MD", "readme.md", "MEMORY.md", "CONTRIBUTING.md"];
893
1193
  const constraints = [];
894
1194
  for (const name of candidates) {
895
- const text = await safeReadText(join3(absProject, name));
1195
+ const text = await safeReadText(join5(absProject, name));
896
1196
  if (!text) continue;
897
1197
  const regex = /(?:constraint|contrainte|must|doit|interdit|forbidden|rule|règle|limitation)[\s\S]{0,200}/gi;
898
1198
  let m;
@@ -912,9 +1212,12 @@ function formatChunk(chunk) {
912
1212
  ${chunk.content}`;
913
1213
  }
914
1214
  async function buildContext(options) {
915
- const { project, query, filePath, searchQuery, maxTokens = DEFAULT_MAX_TOKENS, lang = "fr", instruction, embed = false } = options;
916
- const labels = getLabels(lang);
1215
+ const { project, query, filePath, searchQuery, instruction, embed = false } = options;
917
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);
918
1221
  const index = await getIndex(absProject);
919
1222
  if (embed) {
920
1223
  try {
@@ -986,8 +1289,8 @@ ${finalInstruction}`;
986
1289
  }
987
1290
 
988
1291
  // src/analysis.ts
989
- import { basename, extname as extname2, join as join4, relative as relative2, sep as sep2, posix as posixPath } from "path";
990
- import { readFile as readFile3 } from "fs/promises";
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";
991
1294
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
992
1295
  var ENTRY_BASENAMES = /* @__PURE__ */ new Set(["index", "main", "app", "cli", "server", "bin", "mod"]);
993
1296
  var SKIP_EXTS = /* @__PURE__ */ new Set([".d.ts", ".test.ts", ".test.js", ".spec.ts", ".spec.js", ".config.js", ".config.ts", ".config.mjs"]);
@@ -1063,6 +1366,8 @@ function findCycles(edges) {
1063
1366
  function looksLikeEntry(rel, pkg) {
1064
1367
  const base = basename(rel).toLowerCase().replace(extname2(rel), "");
1065
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;
1066
1371
  if (/^(pages|app|routes|api|bin|scripts)\//.test(rel) || rel.includes("/pages/") || rel.includes("/routes/")) return true;
1067
1372
  const fields = [pkg?.main, pkg?.module, pkg?.bin, pkg?.exports?.["."]];
1068
1373
  for (const f of fields.flatMap((v) => typeof v === "string" ? [v] : v ? Object.values(v) : [])) {
@@ -1103,8 +1408,9 @@ async function analyzeProject(projectPath) {
1103
1408
  const abs = await findProjectRoot(resolveProjectPath(projectPath));
1104
1409
  const fileTexts = /* @__PURE__ */ new Map();
1105
1410
  const codeFiles = [];
1106
- for await (const full of walkFiles(abs)) {
1107
- const rel = relative2(abs, full).split(sep2).join("/");
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("/");
1108
1414
  const ext = extname2(rel).toLowerCase();
1109
1415
  if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
1110
1416
  const text = await safeReadText(full);
@@ -1114,7 +1420,7 @@ async function analyzeProject(projectPath) {
1114
1420
  }
1115
1421
  let pkg = {};
1116
1422
  try {
1117
- pkg = JSON.parse(await readFile3(join4(abs, "package.json"), "utf8"));
1423
+ pkg = JSON.parse(await readFile4(join6(abs, "package.json"), "utf8"));
1118
1424
  } catch {
1119
1425
  }
1120
1426
  const known = new Set(codeFiles);
@@ -1154,7 +1460,7 @@ async function analyzeProject(projectPath) {
1154
1460
  hotspots.sort((a, b) => b.score - a.score);
1155
1461
  const codeLines = [...fileTexts.values()].reduce((s, t2) => s + t2.split("\n").length, 0);
1156
1462
  const dupLines = duplicates.reduce((s, g) => s + g.lines, 0);
1157
- 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;
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;
1158
1464
  const score = Math.max(0, Math.min(100, 100 - penalties));
1159
1465
  const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 65 ? "C" : score >= 50 ? "D" : "E";
1160
1466
  return {
@@ -1165,7 +1471,7 @@ async function analyzeProject(projectPath) {
1165
1471
  unusedFiles,
1166
1472
  unusedExports,
1167
1473
  duplicates,
1168
- hotspots: hotspots.slice(0, 15),
1474
+ hotspots,
1169
1475
  score,
1170
1476
  grade
1171
1477
  };
@@ -1218,13 +1524,17 @@ function formatHealthReport(r, lang = "fr") {
1218
1524
  return out.join("\n");
1219
1525
  }
1220
1526
  export {
1527
+ CONFIG_FILE,
1221
1528
  analyzeProject,
1222
1529
  buildContext,
1223
1530
  buildIndex,
1224
1531
  chunkByTokens,
1532
+ clearConfigCache,
1225
1533
  cosineSimilarity,
1226
1534
  countTokens,
1535
+ disposeTreeSitter,
1227
1536
  embedIndex,
1537
+ ensureTreeSitterForExt,
1228
1538
  extractChunks,
1229
1539
  findProjectRoot,
1230
1540
  formatHealthReport,
@@ -1232,11 +1542,16 @@ export {
1232
1542
  getEmbeddings,
1233
1543
  getExtractor,
1234
1544
  getIndex,
1545
+ globToRegExp,
1546
+ initTreeSitter,
1235
1547
  loadIndex,
1548
+ loadProjectConfig,
1549
+ matchesAnyGlob,
1236
1550
  resolveProjectPath,
1237
1551
  saveIndex,
1238
1552
  scoreChunks,
1239
1553
  selectChunks,
1554
+ treeSitterReady,
1240
1555
  truncateToTokens
1241
1556
  };
1242
1557
  //# sourceMappingURL=index.js.map