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/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
5
|
|
|
6
6
|
// src/context.ts
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join5 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/tokenizer.ts
|
|
10
10
|
import { encode, decode } from "gpt-tokenizer";
|
|
@@ -33,14 +33,281 @@ function truncateToTokens(text, maxTokens) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/indexer.ts
|
|
36
|
-
import { mkdir, readFile as
|
|
37
|
-
import { dirname, join as
|
|
36
|
+
import { mkdir, readFile as readFile3, stat as stat2, writeFile } from "fs/promises";
|
|
37
|
+
import { dirname as dirname2, join as join4, relative as relative2, sep as sep2 } from "path";
|
|
38
38
|
import { existsSync } from "fs";
|
|
39
39
|
|
|
40
40
|
// src/extractor.ts
|
|
41
41
|
import { parse } from "@babel/parser";
|
|
42
42
|
import traverse from "@babel/traverse";
|
|
43
43
|
import * as t from "@babel/types";
|
|
44
|
+
|
|
45
|
+
// src/merge-gaps.ts
|
|
46
|
+
function mergeGaps(content, relPath, namedChunks, visitedRanges) {
|
|
47
|
+
const lines = content.split("\n");
|
|
48
|
+
if (lines.length === 0) return namedChunks;
|
|
49
|
+
const covered = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const [start2, end] of visitedRanges) {
|
|
51
|
+
for (let i = start2; i <= end; i++) covered.add(i);
|
|
52
|
+
}
|
|
53
|
+
const all = [];
|
|
54
|
+
let start = 1;
|
|
55
|
+
for (let i = 1; i <= lines.length; i++) {
|
|
56
|
+
if (!covered.has(i)) continue;
|
|
57
|
+
if (i > start) {
|
|
58
|
+
const gapText = lines.slice(start - 1, i - 1).join("\n");
|
|
59
|
+
if (gapText.trim()) all.push({ relPath, startLine: start, endLine: i - 1, content: gapText, tokens: countTokens(gapText), kind: "file" });
|
|
60
|
+
}
|
|
61
|
+
start = i + 1;
|
|
62
|
+
}
|
|
63
|
+
if (start <= lines.length) {
|
|
64
|
+
const gapText = lines.slice(start - 1).join("\n");
|
|
65
|
+
if (gapText.trim()) all.push({ relPath, startLine: start, endLine: lines.length, content: gapText, tokens: countTokens(gapText), kind: "file" });
|
|
66
|
+
}
|
|
67
|
+
return [...all, ...namedChunks].sort((a, b) => a.startLine - b.startLine);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/treesitter.ts
|
|
71
|
+
import { createRequire } from "module";
|
|
72
|
+
import { dirname, join } from "path";
|
|
73
|
+
var require2 = createRequire(import.meta.url);
|
|
74
|
+
var GRAMMAR_BY_EXT = {
|
|
75
|
+
".py": "python",
|
|
76
|
+
".go": "go",
|
|
77
|
+
".rs": "rust",
|
|
78
|
+
".java": "java",
|
|
79
|
+
".cs": "c_sharp",
|
|
80
|
+
".php": "php"
|
|
81
|
+
};
|
|
82
|
+
var DECL_TYPES = {
|
|
83
|
+
python: {
|
|
84
|
+
function_definition: "function",
|
|
85
|
+
class_definition: "class"
|
|
86
|
+
},
|
|
87
|
+
go: {
|
|
88
|
+
function_declaration: "function",
|
|
89
|
+
method_declaration: "method",
|
|
90
|
+
type_declaration: "type",
|
|
91
|
+
import_declaration: "import",
|
|
92
|
+
const_declaration: "unknown",
|
|
93
|
+
var_declaration: "unknown"
|
|
94
|
+
},
|
|
95
|
+
rust: {
|
|
96
|
+
function_item: "function",
|
|
97
|
+
struct_item: "type",
|
|
98
|
+
enum_item: "type",
|
|
99
|
+
union_item: "type",
|
|
100
|
+
trait_item: "type",
|
|
101
|
+
type_item: "type",
|
|
102
|
+
impl_item: "type",
|
|
103
|
+
mod_item: "unknown",
|
|
104
|
+
use_declaration: "import",
|
|
105
|
+
macro_definition: "unknown"
|
|
106
|
+
},
|
|
107
|
+
java: {
|
|
108
|
+
class_declaration: "class",
|
|
109
|
+
interface_declaration: "type",
|
|
110
|
+
enum_declaration: "type",
|
|
111
|
+
record_declaration: "type",
|
|
112
|
+
annotation_type_declaration: "type",
|
|
113
|
+
method_declaration: "method",
|
|
114
|
+
constructor_declaration: "method",
|
|
115
|
+
field_declaration: "unknown",
|
|
116
|
+
import_declaration: "import",
|
|
117
|
+
package_declaration: "import"
|
|
118
|
+
},
|
|
119
|
+
c_sharp: {
|
|
120
|
+
class_declaration: "class",
|
|
121
|
+
interface_declaration: "type",
|
|
122
|
+
struct_declaration: "type",
|
|
123
|
+
enum_declaration: "type",
|
|
124
|
+
record_declaration: "type",
|
|
125
|
+
delegate_declaration: "type",
|
|
126
|
+
method_declaration: "method",
|
|
127
|
+
constructor_declaration: "method",
|
|
128
|
+
property_declaration: "method",
|
|
129
|
+
field_declaration: "unknown",
|
|
130
|
+
using_directive: "import"
|
|
131
|
+
},
|
|
132
|
+
php: {
|
|
133
|
+
function_definition: "function",
|
|
134
|
+
class_declaration: "class",
|
|
135
|
+
interface_declaration: "type",
|
|
136
|
+
trait_declaration: "type",
|
|
137
|
+
enum_declaration: "type",
|
|
138
|
+
method_declaration: "method",
|
|
139
|
+
namespace_use_declaration: "import",
|
|
140
|
+
namespace_definition: "unknown"
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var RECURSE_INTO = /* @__PURE__ */ new Set([
|
|
144
|
+
"program",
|
|
145
|
+
"module",
|
|
146
|
+
"translation_unit",
|
|
147
|
+
"compilation_unit",
|
|
148
|
+
"source_file",
|
|
149
|
+
"namespace_declaration",
|
|
150
|
+
"file_scoped_namespace_declaration",
|
|
151
|
+
"namespace_definition",
|
|
152
|
+
"declaration_list",
|
|
153
|
+
"decorated_definition",
|
|
154
|
+
"export_statement"
|
|
155
|
+
]);
|
|
156
|
+
var CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
157
|
+
"class_definition",
|
|
158
|
+
"class_declaration",
|
|
159
|
+
"interface_declaration",
|
|
160
|
+
"enum_declaration",
|
|
161
|
+
"record_declaration",
|
|
162
|
+
"struct_declaration",
|
|
163
|
+
"impl_item",
|
|
164
|
+
"trait_item",
|
|
165
|
+
"trait_declaration"
|
|
166
|
+
]);
|
|
167
|
+
var MEMBER_TYPES = /* @__PURE__ */ new Set([
|
|
168
|
+
"function_definition",
|
|
169
|
+
"method_declaration",
|
|
170
|
+
"method_definition",
|
|
171
|
+
"function_item",
|
|
172
|
+
"constructor_declaration",
|
|
173
|
+
"property_declaration"
|
|
174
|
+
]);
|
|
175
|
+
var NAME_TYPES = /* @__PURE__ */ new Set([
|
|
176
|
+
"identifier",
|
|
177
|
+
"type_identifier",
|
|
178
|
+
"field_identifier",
|
|
179
|
+
"simple_type",
|
|
180
|
+
"name"
|
|
181
|
+
]);
|
|
182
|
+
var parser = null;
|
|
183
|
+
var initPromise = null;
|
|
184
|
+
var languages = /* @__PURE__ */ new Map();
|
|
185
|
+
var langPromises = /* @__PURE__ */ new Map();
|
|
186
|
+
async function initTreeSitter() {
|
|
187
|
+
if (parser) return true;
|
|
188
|
+
initPromise ??= (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const mod = await import("web-tree-sitter");
|
|
191
|
+
const ParserClass = mod.default ?? mod;
|
|
192
|
+
const wasm = require2.resolve("web-tree-sitter/tree-sitter.wasm");
|
|
193
|
+
await ParserClass.init({ locateFile: () => wasm });
|
|
194
|
+
parser = new ParserClass();
|
|
195
|
+
return true;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
return initPromise;
|
|
201
|
+
}
|
|
202
|
+
function grammarPath(name) {
|
|
203
|
+
const pkg = require2.resolve("tree-sitter-wasms/package.json");
|
|
204
|
+
return join(dirname(pkg), "out", `tree-sitter-${name}.wasm`);
|
|
205
|
+
}
|
|
206
|
+
async function ensureLanguage(name) {
|
|
207
|
+
if (languages.has(name)) return languages.get(name);
|
|
208
|
+
let p = langPromises.get(name);
|
|
209
|
+
if (!p) {
|
|
210
|
+
p = (async () => {
|
|
211
|
+
try {
|
|
212
|
+
const mod = await import("web-tree-sitter");
|
|
213
|
+
const ParserClass = mod.default ?? mod;
|
|
214
|
+
const lang = await ParserClass.Language.load(grammarPath(name));
|
|
215
|
+
languages.set(name, lang);
|
|
216
|
+
return lang;
|
|
217
|
+
} catch {
|
|
218
|
+
languages.set(name, null);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
langPromises.set(name, p);
|
|
223
|
+
}
|
|
224
|
+
return p;
|
|
225
|
+
}
|
|
226
|
+
async function ensureTreeSitterForExt(ext) {
|
|
227
|
+
const name = GRAMMAR_BY_EXT[ext.toLowerCase()];
|
|
228
|
+
if (!name) return false;
|
|
229
|
+
if (!await initTreeSitter()) return false;
|
|
230
|
+
return await ensureLanguage(name) != null;
|
|
231
|
+
}
|
|
232
|
+
function disposeTreeSitter() {
|
|
233
|
+
try {
|
|
234
|
+
parser?.delete();
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
parser = null;
|
|
238
|
+
initPromise = null;
|
|
239
|
+
languages.clear();
|
|
240
|
+
langPromises.clear();
|
|
241
|
+
}
|
|
242
|
+
function treeSitterReady(ext) {
|
|
243
|
+
const name = GRAMMAR_BY_EXT[ext.toLowerCase()];
|
|
244
|
+
return !!name && !!parser && languages.get(name) != null;
|
|
245
|
+
}
|
|
246
|
+
function nodeName(node) {
|
|
247
|
+
const named = node.childForFieldName("name");
|
|
248
|
+
if (named) return named.text;
|
|
249
|
+
for (const child of node.namedChildren) {
|
|
250
|
+
if (NAME_TYPES.has(child.type)) return child.text;
|
|
251
|
+
const inner = child.childForFieldName("name");
|
|
252
|
+
if (inner) return inner.text;
|
|
253
|
+
}
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
function toChunk(node, kind, relPath, content, name) {
|
|
257
|
+
const startLine = node.startPosition.row + 1;
|
|
258
|
+
const endLine = node.endPosition.row + 1;
|
|
259
|
+
const text = content.split("\n").slice(startLine - 1, endLine).join("\n");
|
|
260
|
+
return { relPath, startLine, endLine, content: text, tokens: countTokens(text), kind, name };
|
|
261
|
+
}
|
|
262
|
+
function extractWithTreeSitter(ext, relPath, content) {
|
|
263
|
+
if (!parser || !treeSitterReady(ext)) return void 0;
|
|
264
|
+
const name = GRAMMAR_BY_EXT[ext.toLowerCase()];
|
|
265
|
+
const lang = languages.get(name);
|
|
266
|
+
const declTypes = DECL_TYPES[name] ?? {};
|
|
267
|
+
let tree;
|
|
268
|
+
try {
|
|
269
|
+
parser.setLanguage(lang);
|
|
270
|
+
tree = parser.parse(content);
|
|
271
|
+
} catch {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
if (!tree) return void 0;
|
|
275
|
+
try {
|
|
276
|
+
const chunks = [];
|
|
277
|
+
const visitedRanges = [];
|
|
278
|
+
const handle = (child) => {
|
|
279
|
+
const kind = declTypes[child.type];
|
|
280
|
+
if (kind) {
|
|
281
|
+
const target = child.parent?.type === "decorated_definition" ? child.parent : child;
|
|
282
|
+
chunks.push(toChunk(target, kind, relPath, content, nodeName(child)));
|
|
283
|
+
visitedRanges.push([target.startPosition.row + 1, target.endPosition.row + 1]);
|
|
284
|
+
if (CONTAINER_TYPES.has(child.type)) {
|
|
285
|
+
const body = child.childForFieldName("body") ?? child;
|
|
286
|
+
for (const member of body.namedChildren) {
|
|
287
|
+
if (MEMBER_TYPES.has(member.type)) {
|
|
288
|
+
chunks.push(toChunk(member, "method", relPath, content, nodeName(member)));
|
|
289
|
+
} else if (declTypes[member.type]) {
|
|
290
|
+
handle(member);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (RECURSE_INTO.has(child.type) || child.namedChildren.some((c) => declTypes[c.type] || RECURSE_INTO.has(c.type))) {
|
|
297
|
+
visit(child);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const visit = (node) => {
|
|
301
|
+
for (const child of node.namedChildren) handle(child);
|
|
302
|
+
};
|
|
303
|
+
visit(tree.rootNode);
|
|
304
|
+
return mergeGaps(content, relPath, chunks, visitedRanges);
|
|
305
|
+
} finally {
|
|
306
|
+
tree.delete();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/extractor.ts
|
|
44
311
|
var JS_LIKE = /* @__PURE__ */ new Set([
|
|
45
312
|
".js",
|
|
46
313
|
".jsx",
|
|
@@ -162,51 +429,6 @@ function extractTopLevelWithBabel(relPath, content) {
|
|
|
162
429
|
});
|
|
163
430
|
return mergeGaps(content, relPath, chunks, visitedRanges);
|
|
164
431
|
}
|
|
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
432
|
function extractWithRegex(relPath, content) {
|
|
211
433
|
const lines = content.split("\n");
|
|
212
434
|
const chunks = [];
|
|
@@ -255,6 +477,13 @@ function extractChunks(relPath, content) {
|
|
|
255
477
|
return extractWithRegex(relPath, content);
|
|
256
478
|
}
|
|
257
479
|
}
|
|
480
|
+
if (treeSitterReady(ext)) {
|
|
481
|
+
try {
|
|
482
|
+
const chunks = extractWithTreeSitter(ext, relPath, content);
|
|
483
|
+
if (chunks && chunks.length) return chunks;
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
}
|
|
258
487
|
return extractWithRegex(relPath, content);
|
|
259
488
|
}
|
|
260
489
|
|
|
@@ -309,10 +538,64 @@ function cosineSimilarity(a, b) {
|
|
|
309
538
|
}
|
|
310
539
|
|
|
311
540
|
// src/project.ts
|
|
312
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
313
|
-
import { extname, join, resolve, isAbsolute } from "path";
|
|
541
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
542
|
+
import { extname, join as join3, relative, resolve, sep, isAbsolute } from "path";
|
|
314
543
|
import { createHash } from "crypto";
|
|
315
544
|
import { homedir } from "os";
|
|
545
|
+
|
|
546
|
+
// src/config.ts
|
|
547
|
+
import { readFile } from "fs/promises";
|
|
548
|
+
import { join as join2 } from "path";
|
|
549
|
+
var CONFIG_FILE = ".codebase-chat.json";
|
|
550
|
+
var configCache = /* @__PURE__ */ new Map();
|
|
551
|
+
function strArray(v) {
|
|
552
|
+
if (!Array.isArray(v)) return void 0;
|
|
553
|
+
const out = v.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
554
|
+
return out.length ? out.map((s) => s.trim()) : void 0;
|
|
555
|
+
}
|
|
556
|
+
function sanitize(raw) {
|
|
557
|
+
if (raw == null || typeof raw !== "object") return {};
|
|
558
|
+
const o = raw;
|
|
559
|
+
const cfg = {};
|
|
560
|
+
if (o.lang === "fr" || o.lang === "en") cfg.lang = o.lang;
|
|
561
|
+
if (typeof o.maxTokens === "number" && Number.isFinite(o.maxTokens) && o.maxTokens > 0) {
|
|
562
|
+
cfg.maxTokens = Math.floor(o.maxTokens);
|
|
563
|
+
}
|
|
564
|
+
const ignoreDirs = strArray(o.ignoreDirs);
|
|
565
|
+
if (ignoreDirs) cfg.ignoreDirs = ignoreDirs;
|
|
566
|
+
const ignoreFiles = strArray(o.ignoreFiles);
|
|
567
|
+
if (ignoreFiles) cfg.ignoreFiles = ignoreFiles;
|
|
568
|
+
const ignoreGlobs = strArray(o.ignoreGlobs);
|
|
569
|
+
if (ignoreGlobs) cfg.ignoreGlobs = ignoreGlobs;
|
|
570
|
+
const protectedPaths = strArray(o.protectedPaths);
|
|
571
|
+
if (protectedPaths) cfg.protectedPaths = protectedPaths;
|
|
572
|
+
return cfg;
|
|
573
|
+
}
|
|
574
|
+
async function loadProjectConfig(absProject) {
|
|
575
|
+
const cached = configCache.get(absProject);
|
|
576
|
+
if (cached) return cached;
|
|
577
|
+
let cfg = {};
|
|
578
|
+
try {
|
|
579
|
+
cfg = sanitize(JSON.parse(await readFile(join2(absProject, CONFIG_FILE), "utf8")));
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
configCache.set(absProject, cfg);
|
|
583
|
+
return cfg;
|
|
584
|
+
}
|
|
585
|
+
function escapeSegment(s) {
|
|
586
|
+
return s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
587
|
+
}
|
|
588
|
+
function globToRegExp(glob) {
|
|
589
|
+
const src = glob.replace(/\\/g, "/").split("**").map(escapeSegment).join(".*");
|
|
590
|
+
return new RegExp(`^${src}$`);
|
|
591
|
+
}
|
|
592
|
+
function matchesAnyGlob(relPath, globs) {
|
|
593
|
+
if (!globs || globs.length === 0) return false;
|
|
594
|
+
const rel = relPath.replace(/\\/g, "/");
|
|
595
|
+
return globs.some((g) => globToRegExp(g).test(rel));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/project.ts
|
|
316
599
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
317
600
|
".ts",
|
|
318
601
|
".tsx",
|
|
@@ -368,6 +651,14 @@ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
368
651
|
".dsh-vision-router"
|
|
369
652
|
]);
|
|
370
653
|
var DEFAULT_SKIP_FILES = /* @__PURE__ */ new Set([]);
|
|
654
|
+
async function getWalkOptions(absProject) {
|
|
655
|
+
const cfg = await loadProjectConfig(absProject);
|
|
656
|
+
return {
|
|
657
|
+
skipDirs: /* @__PURE__ */ new Set([...DEFAULT_SKIP_DIRS, ...cfg.ignoreDirs ?? []]),
|
|
658
|
+
skipFiles: /* @__PURE__ */ new Set([...DEFAULT_SKIP_FILES, ...cfg.ignoreFiles ?? []]),
|
|
659
|
+
ignoreGlobs: cfg.ignoreGlobs ?? []
|
|
660
|
+
};
|
|
661
|
+
}
|
|
371
662
|
function projectHash(absProject) {
|
|
372
663
|
return createHash("sha256").update(absProject.toLowerCase()).digest("hex").slice(0, 16);
|
|
373
664
|
}
|
|
@@ -388,16 +679,16 @@ async function findProjectRoot(absProject) {
|
|
|
388
679
|
}
|
|
389
680
|
}
|
|
390
681
|
function getCacheDir() {
|
|
391
|
-
const base = process.env.CODEBASE_CACHE_DIR || process.env.LOCALAPPDATA || process.env.APPDATA ||
|
|
392
|
-
return
|
|
682
|
+
const base = process.env.CODEBASE_CACHE_DIR || process.env.LOCALAPPDATA || process.env.APPDATA || join3(homedir(), ".cache");
|
|
683
|
+
return join3(base, "dsh-codebase-chat-cache");
|
|
393
684
|
}
|
|
394
685
|
function cacheFilePath(absProject) {
|
|
395
|
-
return
|
|
686
|
+
return join3(getCacheDir(), `${projectHash(absProject)}.json`);
|
|
396
687
|
}
|
|
397
688
|
function fileHash(stats, firstBytes = "") {
|
|
398
689
|
return createHash("sha256").update(`${stats.mtimeMs}:${stats.size}:${firstBytes.slice(0, 512)}`).digest("hex").slice(0, 24);
|
|
399
690
|
}
|
|
400
|
-
async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DEFAULT_SKIP_FILES) {
|
|
691
|
+
async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DEFAULT_SKIP_FILES, ignoreGlobs = []) {
|
|
401
692
|
const queue = [startDir];
|
|
402
693
|
while (queue.length) {
|
|
403
694
|
const dir = queue.shift();
|
|
@@ -408,13 +699,15 @@ async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DE
|
|
|
408
699
|
continue;
|
|
409
700
|
}
|
|
410
701
|
for (const entry of entries) {
|
|
411
|
-
const fullPath =
|
|
702
|
+
const fullPath = join3(dir, entry.name);
|
|
703
|
+
const rel = relative(startDir, fullPath).split(sep).join("/");
|
|
412
704
|
if (entry.isDirectory()) {
|
|
413
|
-
if (!skipDirs.has(entry.name)) queue.push(fullPath);
|
|
705
|
+
if (!skipDirs.has(entry.name) && !matchesAnyGlob(rel, ignoreGlobs)) queue.push(fullPath);
|
|
414
706
|
continue;
|
|
415
707
|
}
|
|
416
708
|
if (!entry.isFile()) continue;
|
|
417
709
|
if (skipFiles.has(entry.name)) continue;
|
|
710
|
+
if (matchesAnyGlob(rel, ignoreGlobs)) continue;
|
|
418
711
|
const ext = extname(entry.name).toLowerCase();
|
|
419
712
|
if (!SOURCE_EXTS.has(ext)) continue;
|
|
420
713
|
yield fullPath;
|
|
@@ -423,7 +716,7 @@ async function* walkFiles(startDir, skipDirs = DEFAULT_SKIP_DIRS, skipFiles = DE
|
|
|
423
716
|
}
|
|
424
717
|
async function safeReadText(filePath) {
|
|
425
718
|
try {
|
|
426
|
-
const text = await
|
|
719
|
+
const text = await readFile2(filePath, "utf8");
|
|
427
720
|
return text;
|
|
428
721
|
} catch {
|
|
429
722
|
return void 0;
|
|
@@ -443,7 +736,7 @@ async function buildTree(startDir, maxLines = 500, skipDirs = DEFAULT_SKIP_DIRS)
|
|
|
443
736
|
for (const entry of entries) {
|
|
444
737
|
if (lines.length >= maxLines) return;
|
|
445
738
|
if (skipDirs.has(entry.name)) continue;
|
|
446
|
-
const fullPath =
|
|
739
|
+
const fullPath = join3(dir, entry.name);
|
|
447
740
|
if (entry.isDirectory()) {
|
|
448
741
|
lines.push(`${prefix}${entry.name}/`);
|
|
449
742
|
await walk(fullPath, `${prefix} `);
|
|
@@ -582,7 +875,7 @@ async function loadIndex(projectPath) {
|
|
|
582
875
|
const p = cacheFilePath(absProject);
|
|
583
876
|
if (!existsSync(p)) return null;
|
|
584
877
|
try {
|
|
585
|
-
const raw = await
|
|
878
|
+
const raw = await readFile3(p, "utf8");
|
|
586
879
|
const data = JSON.parse(raw);
|
|
587
880
|
if (data.version !== INDEX_VERSION) return null;
|
|
588
881
|
return data;
|
|
@@ -592,7 +885,7 @@ async function loadIndex(projectPath) {
|
|
|
592
885
|
}
|
|
593
886
|
async function saveIndex(index) {
|
|
594
887
|
const p = cacheFilePath(index.projectPath);
|
|
595
|
-
await mkdir(
|
|
888
|
+
await mkdir(dirname2(p), { recursive: true });
|
|
596
889
|
await writeFile(p, JSON.stringify(index), "utf8");
|
|
597
890
|
}
|
|
598
891
|
async function embedIndex(index, progress) {
|
|
@@ -618,17 +911,18 @@ async function embedIndex(index, progress) {
|
|
|
618
911
|
}
|
|
619
912
|
async function buildIndex(projectPath, progress) {
|
|
620
913
|
const absProject = await findProjectRoot(resolveProjectPath(projectPath));
|
|
621
|
-
const projectName = absProject.split(
|
|
914
|
+
const projectName = absProject.split(sep2).pop() ?? "project";
|
|
622
915
|
progress?.(`Indexing ${projectName}...`);
|
|
623
|
-
const
|
|
916
|
+
const walk = await getWalkOptions(absProject);
|
|
917
|
+
const tree = await buildTree(absProject, void 0, walk.skipDirs);
|
|
624
918
|
const startDir = absProject;
|
|
625
919
|
const previous = await loadIndex(absProject);
|
|
626
920
|
const previousFiles = previous?.projectPath === absProject ? previous.files : {};
|
|
627
921
|
const files = {};
|
|
628
922
|
let totalTokens = 0;
|
|
629
923
|
let reused = 0;
|
|
630
|
-
for await (const fullPath of walkFiles(startDir)) {
|
|
631
|
-
const relPath =
|
|
924
|
+
for await (const fullPath of walkFiles(startDir, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
|
|
925
|
+
const relPath = relative2(startDir, fullPath).split(sep2).join("/");
|
|
632
926
|
progress?.(`Reading ${relPath}`);
|
|
633
927
|
const fstats = await stat2(fullPath);
|
|
634
928
|
const cached = previousFiles[relPath];
|
|
@@ -641,6 +935,8 @@ async function buildIndex(projectPath, progress) {
|
|
|
641
935
|
const text = await safeReadText(fullPath);
|
|
642
936
|
if (!text) continue;
|
|
643
937
|
const hash = fileHash(fstats, text);
|
|
938
|
+
const ext = relPath.slice(relPath.lastIndexOf(".")).toLowerCase();
|
|
939
|
+
await ensureTreeSitterForExt(ext);
|
|
644
940
|
const chunks = extractChunks(relPath, text).map((chunk) => ({
|
|
645
941
|
...chunk,
|
|
646
942
|
// recompute tokens to be safe
|
|
@@ -678,7 +974,7 @@ async function getIndex(projectPath, progress, force = false) {
|
|
|
678
974
|
if (existing && existing.projectPath === absProject) {
|
|
679
975
|
let stale = false;
|
|
680
976
|
for (const file of Object.values(existing.files)) {
|
|
681
|
-
const fullPath =
|
|
977
|
+
const fullPath = join4(absProject, file.relPath);
|
|
682
978
|
try {
|
|
683
979
|
const fstats = await stat2(fullPath);
|
|
684
980
|
if (fstats.mtimeMs !== file.mtimeMs || fstats.size !== file.size) {
|
|
@@ -897,7 +1193,7 @@ async function extractProductConstraints(absProject) {
|
|
|
897
1193
|
const candidates = ["README.md", "README.MD", "readme.md", "MEMORY.md", "CONTRIBUTING.md"];
|
|
898
1194
|
const constraints = [];
|
|
899
1195
|
for (const name of candidates) {
|
|
900
|
-
const text = await safeReadText(
|
|
1196
|
+
const text = await safeReadText(join5(absProject, name));
|
|
901
1197
|
if (!text) continue;
|
|
902
1198
|
const regex = /(?:constraint|contrainte|must|doit|interdit|forbidden|rule|règle|limitation)[\s\S]{0,200}/gi;
|
|
903
1199
|
let m;
|
|
@@ -917,9 +1213,12 @@ function formatChunk(chunk) {
|
|
|
917
1213
|
${chunk.content}`;
|
|
918
1214
|
}
|
|
919
1215
|
async function buildContext(options) {
|
|
920
|
-
const { project, query, filePath, searchQuery,
|
|
921
|
-
const labels = getLabels(lang);
|
|
1216
|
+
const { project, query, filePath, searchQuery, instruction, embed = false } = options;
|
|
922
1217
|
const absProject = await findProjectRoot(resolveProjectPath(project));
|
|
1218
|
+
const cfg = await loadProjectConfig(absProject);
|
|
1219
|
+
const maxTokens = options.maxTokens ?? cfg.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
1220
|
+
const lang = options.lang ?? cfg.lang ?? "fr";
|
|
1221
|
+
const labels = getLabels(lang);
|
|
923
1222
|
const index = await getIndex(absProject);
|
|
924
1223
|
if (embed) {
|
|
925
1224
|
try {
|
|
@@ -991,8 +1290,8 @@ ${finalInstruction}`;
|
|
|
991
1290
|
}
|
|
992
1291
|
|
|
993
1292
|
// src/analysis.ts
|
|
994
|
-
import { basename, extname as extname2, join as
|
|
995
|
-
import { readFile as
|
|
1293
|
+
import { basename, extname as extname2, join as join6, relative as relative3, sep as sep3, posix as posixPath } from "path";
|
|
1294
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
996
1295
|
var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
997
1296
|
var ENTRY_BASENAMES = /* @__PURE__ */ new Set(["index", "main", "app", "cli", "server", "bin", "mod"]);
|
|
998
1297
|
var SKIP_EXTS = /* @__PURE__ */ new Set([".d.ts", ".test.ts", ".test.js", ".spec.ts", ".spec.js", ".config.js", ".config.ts", ".config.mjs"]);
|
|
@@ -1068,6 +1367,8 @@ function findCycles(edges) {
|
|
|
1068
1367
|
function looksLikeEntry(rel, pkg) {
|
|
1069
1368
|
const base = basename(rel).toLowerCase().replace(extname2(rel), "");
|
|
1070
1369
|
if (ENTRY_BASENAMES.has(base)) return true;
|
|
1370
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(rel) || rel.includes("__tests__/") || /^e2e[.-]/.test(basename(rel))) return true;
|
|
1371
|
+
if (/\.(config|rc)\.[cm]?[jt]s$/.test(rel)) return true;
|
|
1071
1372
|
if (/^(pages|app|routes|api|bin|scripts)\//.test(rel) || rel.includes("/pages/") || rel.includes("/routes/")) return true;
|
|
1072
1373
|
const fields = [pkg?.main, pkg?.module, pkg?.bin, pkg?.exports?.["."]];
|
|
1073
1374
|
for (const f of fields.flatMap((v) => typeof v === "string" ? [v] : v ? Object.values(v) : [])) {
|
|
@@ -1108,8 +1409,9 @@ async function analyzeProject(projectPath) {
|
|
|
1108
1409
|
const abs = await findProjectRoot(resolveProjectPath(projectPath));
|
|
1109
1410
|
const fileTexts = /* @__PURE__ */ new Map();
|
|
1110
1411
|
const codeFiles = [];
|
|
1111
|
-
|
|
1112
|
-
|
|
1412
|
+
const walk = await getWalkOptions(abs);
|
|
1413
|
+
for await (const full of walkFiles(abs, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
|
|
1414
|
+
const rel = relative3(abs, full).split(sep3).join("/");
|
|
1113
1415
|
const ext = extname2(rel).toLowerCase();
|
|
1114
1416
|
if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
|
|
1115
1417
|
const text = await safeReadText(full);
|
|
@@ -1119,7 +1421,7 @@ async function analyzeProject(projectPath) {
|
|
|
1119
1421
|
}
|
|
1120
1422
|
let pkg = {};
|
|
1121
1423
|
try {
|
|
1122
|
-
pkg = JSON.parse(await
|
|
1424
|
+
pkg = JSON.parse(await readFile4(join6(abs, "package.json"), "utf8"));
|
|
1123
1425
|
} catch {
|
|
1124
1426
|
}
|
|
1125
1427
|
const known = new Set(codeFiles);
|
|
@@ -1159,7 +1461,7 @@ async function analyzeProject(projectPath) {
|
|
|
1159
1461
|
hotspots.sort((a, b) => b.score - a.score);
|
|
1160
1462
|
const codeLines = [...fileTexts.values()].reduce((s, t2) => s + t2.split("\n").length, 0);
|
|
1161
1463
|
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;
|
|
1464
|
+
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;
|
|
1163
1465
|
const score = Math.max(0, Math.min(100, 100 - penalties));
|
|
1164
1466
|
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 65 ? "C" : score >= 50 ? "D" : "E";
|
|
1165
1467
|
return {
|
|
@@ -1170,7 +1472,7 @@ async function analyzeProject(projectPath) {
|
|
|
1170
1472
|
unusedFiles,
|
|
1171
1473
|
unusedExports,
|
|
1172
1474
|
duplicates,
|
|
1173
|
-
hotspots
|
|
1475
|
+
hotspots,
|
|
1174
1476
|
score,
|
|
1175
1477
|
grade
|
|
1176
1478
|
};
|
|
@@ -1224,6 +1526,17 @@ function formatHealthReport(r, lang = "fr") {
|
|
|
1224
1526
|
}
|
|
1225
1527
|
|
|
1226
1528
|
// src/cli.ts
|
|
1529
|
+
var ExitSignal = class {
|
|
1530
|
+
constructor(code) {
|
|
1531
|
+
this.code = code;
|
|
1532
|
+
}
|
|
1533
|
+
code;
|
|
1534
|
+
};
|
|
1535
|
+
function exit(code) {
|
|
1536
|
+
disposeTreeSitter();
|
|
1537
|
+
setTimeout(() => process.exit(code), 2e3).unref();
|
|
1538
|
+
throw new ExitSignal(code);
|
|
1539
|
+
}
|
|
1227
1540
|
function printHelp() {
|
|
1228
1541
|
console.log(`
|
|
1229
1542
|
dsh-codebase-chat CLI
|
|
@@ -1245,9 +1558,13 @@ Options:
|
|
|
1245
1558
|
-t, --stats Print indexing stats
|
|
1246
1559
|
-H, --health Deterministic static analysis (cycles, dead code, dupes, complexity)
|
|
1247
1560
|
-e, --embed Enable local semantic embeddings (slower, more relevant)
|
|
1248
|
-
--lang <en|fr> Language for headings (default: fr)
|
|
1561
|
+
--lang <en|fr> Language for headings (default: .codebase-chat.json lang, else fr)
|
|
1249
1562
|
-h, --help Show this help
|
|
1250
1563
|
|
|
1564
|
+
Config:
|
|
1565
|
+
.codebase-chat.json Per-project settings: lang, maxTokens, ignoreDirs,
|
|
1566
|
+
ignoreFiles, ignoreGlobs, protectedPaths
|
|
1567
|
+
|
|
1251
1568
|
Environment:
|
|
1252
1569
|
CODEBASE_CACHE_DIR Directory for the index cache
|
|
1253
1570
|
`);
|
|
@@ -1263,20 +1580,21 @@ async function main() {
|
|
|
1263
1580
|
stats: { type: "boolean", short: "t", default: false },
|
|
1264
1581
|
health: { type: "boolean", short: "H", default: false },
|
|
1265
1582
|
embed: { type: "boolean", short: "e", default: false },
|
|
1266
|
-
lang: { type: "string"
|
|
1583
|
+
lang: { type: "string" },
|
|
1267
1584
|
help: { type: "boolean", short: "h", default: false }
|
|
1268
1585
|
},
|
|
1269
1586
|
allowPositionals: false
|
|
1270
1587
|
});
|
|
1271
1588
|
if (values.help) {
|
|
1272
1589
|
printHelp();
|
|
1273
|
-
|
|
1590
|
+
exit(0);
|
|
1274
1591
|
}
|
|
1275
|
-
const lang = values.lang === "en" ? "en" : "fr";
|
|
1276
1592
|
const project = resolveProjectPath(values.project);
|
|
1593
|
+
const cfg = await loadProjectConfig(project);
|
|
1594
|
+
const lang = values.lang === "en" || values.lang === "fr" ? values.lang : cfg.lang ?? "fr";
|
|
1277
1595
|
if (values.index) {
|
|
1278
1596
|
await getIndex(project, (m) => console.log(m), true);
|
|
1279
|
-
|
|
1597
|
+
exit(0);
|
|
1280
1598
|
}
|
|
1281
1599
|
if (values.stats) {
|
|
1282
1600
|
const index = await getIndex(project, (m) => console.log(m));
|
|
@@ -1288,12 +1606,12 @@ async function main() {
|
|
|
1288
1606
|
console.log(`Tokens: ${totalTokens}`);
|
|
1289
1607
|
console.log(`Terms: ${termCount}`);
|
|
1290
1608
|
console.log(`Cache: ${index.projectHash}`);
|
|
1291
|
-
|
|
1609
|
+
exit(0);
|
|
1292
1610
|
}
|
|
1293
1611
|
if (values.health) {
|
|
1294
1612
|
const report = await analyzeProject(project);
|
|
1295
1613
|
console.log(formatHealthReport(report, lang));
|
|
1296
|
-
|
|
1614
|
+
exit(0);
|
|
1297
1615
|
}
|
|
1298
1616
|
if (values.ask || values.search || values.file) {
|
|
1299
1617
|
const result = await buildContext({
|
|
@@ -1310,13 +1628,19 @@ async function main() {
|
|
|
1310
1628
|
console.log(`Project: ${result.absProject}`);
|
|
1311
1629
|
console.log(`Chunks: ${result.chunks.length}`);
|
|
1312
1630
|
console.log(`Tokens: ${result.tokenCount}`);
|
|
1313
|
-
|
|
1631
|
+
exit(0);
|
|
1314
1632
|
}
|
|
1315
1633
|
printHelp();
|
|
1316
|
-
|
|
1634
|
+
exit(1);
|
|
1317
1635
|
}
|
|
1318
1636
|
main().catch((err) => {
|
|
1637
|
+
disposeTreeSitter();
|
|
1638
|
+
if (err instanceof ExitSignal) {
|
|
1639
|
+
process.exitCode = err.code;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1319
1642
|
console.error(err?.message ?? err);
|
|
1320
|
-
process.
|
|
1643
|
+
process.exitCode = 1;
|
|
1644
|
+
setTimeout(() => process.exit(1), 2e3).unref();
|
|
1321
1645
|
});
|
|
1322
1646
|
//# sourceMappingURL=cli.js.map
|