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/README.md +33 -5
- package/dist/cli.js +409 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -2
- package/dist/index.js +390 -75
- package/dist/index.js.map +1 -1
- package/lib/index.js +62 -7
- package/package.json +13 -10
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/context.ts
|
|
2
|
-
import { join as
|
|
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
|
|
32
|
-
import { dirname, join as
|
|
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 ||
|
|
387
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
913
|
+
const projectName = absProject.split(sep2).pop() ?? "project";
|
|
617
914
|
progress?.(`Indexing ${projectName}...`);
|
|
618
|
-
const
|
|
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 =
|
|
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 =
|
|
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(
|
|
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,
|
|
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
|
|
990
|
-
import { readFile as
|
|
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
|
-
|
|
1107
|
-
|
|
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
|
|
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
|
|
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
|