dsh-codebase-chat 0.22.0 → 0.23.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 +185 -200
- package/dist/cli.js +114 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -2
- package/dist/index.js +62 -11
- package/dist/index.js.map +1 -1
- package/lib/index.js +41 -13
- package/package.json +9 -10
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
+
import { watch } from "fs";
|
|
6
|
+
import { sep as sep5 } from "path";
|
|
5
7
|
|
|
6
8
|
// src/context.ts
|
|
7
9
|
import { join as join5 } from "path";
|
|
@@ -1169,6 +1171,33 @@ function selectChunks(scored, maxTokens, maxChunkTokens = Infinity) {
|
|
|
1169
1171
|
return { chunks: result, tokens: used };
|
|
1170
1172
|
}
|
|
1171
1173
|
|
|
1174
|
+
// src/diff.ts
|
|
1175
|
+
import { execFile } from "child_process";
|
|
1176
|
+
import { sep as sep3 } from "path";
|
|
1177
|
+
import { promisify } from "util";
|
|
1178
|
+
var run = promisify(execFile);
|
|
1179
|
+
async function git(absProject, args) {
|
|
1180
|
+
const { stdout } = await run("git", ["-C", absProject, ...args], { maxBuffer: 16 * 1024 * 1024 });
|
|
1181
|
+
return stdout;
|
|
1182
|
+
}
|
|
1183
|
+
function toRelList(output) {
|
|
1184
|
+
return output.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => l.split(sep3).join("/"));
|
|
1185
|
+
}
|
|
1186
|
+
async function getChangedFiles(absProject, base) {
|
|
1187
|
+
try {
|
|
1188
|
+
const [tracked, untracked] = await Promise.all([
|
|
1189
|
+
// Working tree vs base — covers committed, staged and unstaged edits.
|
|
1190
|
+
git(absProject, ["diff", "--name-only", base, "--"]),
|
|
1191
|
+
git(absProject, ["ls-files", "--others", "--exclude-standard"])
|
|
1192
|
+
]);
|
|
1193
|
+
const files = /* @__PURE__ */ new Set([...toRelList(tracked), ...toRelList(untracked)]);
|
|
1194
|
+
return { files, ok: true };
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1197
|
+
return { files: /* @__PURE__ */ new Set(), ok: false, error: msg.split("\n")[0] };
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1172
1201
|
// src/context.ts
|
|
1173
1202
|
var DEFAULT_MAX_TOKENS = 6e4;
|
|
1174
1203
|
var HEAD_BUDGET_TOKENS = 800;
|
|
@@ -1179,14 +1208,18 @@ function getLabels(lang) {
|
|
|
1179
1208
|
tree: "File tree",
|
|
1180
1209
|
noConstraints: "No explicit constraints documented.",
|
|
1181
1210
|
constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
|
|
1182
|
-
answerIn: "Answer in English."
|
|
1211
|
+
answerIn: "Answer in English.",
|
|
1212
|
+
diffScope: (base, n) => `Scope: ${n} file(s) changed vs ${base}`,
|
|
1213
|
+
diffUnavailable: (base) => `Scope: diff vs ${base} unavailable (not a git repo?) \u2014 full project`
|
|
1183
1214
|
} : {
|
|
1184
1215
|
project: "Projet",
|
|
1185
1216
|
focus: "Focus",
|
|
1186
1217
|
tree: "Arborescence",
|
|
1187
1218
|
noConstraints: "Aucune contrainte explicite document\xE9e.",
|
|
1188
1219
|
constraints: "CONTRAINTES PRODUIT IDENTIFI\xC9ES",
|
|
1189
|
-
answerIn: "R\xE9ponds obligatoirement en fran\xE7ais."
|
|
1220
|
+
answerIn: "R\xE9ponds obligatoirement en fran\xE7ais.",
|
|
1221
|
+
diffScope: (base, n) => `P\xE9rim\xE8tre : ${n} fichier(s) modifi\xE9(s) vs ${base}`,
|
|
1222
|
+
diffUnavailable: (base) => `P\xE9rim\xE8tre : diff vs ${base} indisponible (pas un repo git ?) \u2014 projet complet`
|
|
1190
1223
|
};
|
|
1191
1224
|
}
|
|
1192
1225
|
async function extractProductConstraints(absProject) {
|
|
@@ -1213,13 +1246,30 @@ function formatChunk(chunk) {
|
|
|
1213
1246
|
${chunk.content}`;
|
|
1214
1247
|
}
|
|
1215
1248
|
async function buildContext(options) {
|
|
1216
|
-
const { project, query, filePath, searchQuery, instruction, embed = false } = options;
|
|
1249
|
+
const { project, query, filePath, searchQuery, instruction, embed = false, diff } = options;
|
|
1217
1250
|
const absProject = await findProjectRoot(resolveProjectPath(project));
|
|
1218
1251
|
const cfg = await loadProjectConfig(absProject);
|
|
1219
1252
|
const maxTokens = options.maxTokens ?? cfg.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
1220
1253
|
const lang = options.lang ?? cfg.lang ?? "fr";
|
|
1221
1254
|
const labels = getLabels(lang);
|
|
1222
1255
|
const index = await getIndex(absProject);
|
|
1256
|
+
let scopedIndex = index;
|
|
1257
|
+
let diffFiles;
|
|
1258
|
+
let scopeLine = "";
|
|
1259
|
+
if (diff && !filePath) {
|
|
1260
|
+
const scope = await getChangedFiles(absProject, diff);
|
|
1261
|
+
if (scope.ok) {
|
|
1262
|
+
diffFiles = [...scope.files].filter((f) => index.files[f]);
|
|
1263
|
+
const files = {};
|
|
1264
|
+
for (const f of diffFiles) files[f] = index.files[f];
|
|
1265
|
+
scopedIndex = { ...index, files };
|
|
1266
|
+
scopeLine = `${labels.diffScope(diff, diffFiles.length)}
|
|
1267
|
+
`;
|
|
1268
|
+
} else {
|
|
1269
|
+
scopeLine = `${labels.diffUnavailable(diff)}
|
|
1270
|
+
`;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1223
1273
|
if (embed) {
|
|
1224
1274
|
try {
|
|
1225
1275
|
await embedIndex(index);
|
|
@@ -1241,17 +1291,17 @@ async function buildContext(options) {
|
|
|
1241
1291
|
);
|
|
1242
1292
|
selectedChunks = chunks;
|
|
1243
1293
|
} else {
|
|
1244
|
-
const scored = await scoreChunks(
|
|
1294
|
+
const scored = await scoreChunks(scopedIndex, filePath, embed);
|
|
1245
1295
|
const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
|
|
1246
1296
|
if (chunks.length === 0) throw new Error(`File not found: ${filePath}`);
|
|
1247
1297
|
selectedChunks = chunks;
|
|
1248
1298
|
}
|
|
1249
1299
|
} else if (searchQuery) {
|
|
1250
|
-
const scored = await scoreChunks(
|
|
1300
|
+
const scored = await scoreChunks(scopedIndex, searchQuery, embed);
|
|
1251
1301
|
const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
|
|
1252
1302
|
selectedChunks = chunks;
|
|
1253
1303
|
} else {
|
|
1254
|
-
const scored = await scoreChunks(
|
|
1304
|
+
const scored = await scoreChunks(scopedIndex, query, embed);
|
|
1255
1305
|
const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
|
|
1256
1306
|
selectedChunks = chunks;
|
|
1257
1307
|
}
|
|
@@ -1261,7 +1311,7 @@ ${constraints.map((c) => `- ${c}`).join("\n")}` : `== ${labels.constraints} ==
|
|
|
1261
1311
|
${labels.noConstraints}`;
|
|
1262
1312
|
const head = `${labels.project} : ${absProject}
|
|
1263
1313
|
${labels.focus} : ${focus}
|
|
1264
|
-
${constraintsText}
|
|
1314
|
+
${scopeLine}${constraintsText}
|
|
1265
1315
|
|
|
1266
1316
|
== ${labels.tree} ==
|
|
1267
1317
|
${index.tree}
|
|
@@ -1285,12 +1335,13 @@ ${finalInstruction}`;
|
|
|
1285
1335
|
absProject,
|
|
1286
1336
|
context: prompt,
|
|
1287
1337
|
chunks: selectedChunks,
|
|
1288
|
-
tokenCount
|
|
1338
|
+
tokenCount,
|
|
1339
|
+
diffFiles
|
|
1289
1340
|
};
|
|
1290
1341
|
}
|
|
1291
1342
|
|
|
1292
1343
|
// src/analysis.ts
|
|
1293
|
-
import { basename, extname as extname2, join as join6, relative as relative3, sep as
|
|
1344
|
+
import { basename, extname as extname2, join as join6, relative as relative3, sep as sep4, posix as posixPath } from "path";
|
|
1294
1345
|
import { readFile as readFile4 } from "fs/promises";
|
|
1295
1346
|
var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
1296
1347
|
var ENTRY_BASENAMES = /* @__PURE__ */ new Set(["index", "main", "app", "cli", "server", "bin", "mod"]);
|
|
@@ -1405,15 +1456,16 @@ function findDuplicates(fileTexts) {
|
|
|
1405
1456
|
}
|
|
1406
1457
|
return [...groups.values()].sort((a, b) => b.lines - a.lines).slice(0, 15);
|
|
1407
1458
|
}
|
|
1408
|
-
async function analyzeProject(projectPath) {
|
|
1459
|
+
async function analyzeProject(projectPath, opts = {}) {
|
|
1409
1460
|
const abs = await findProjectRoot(resolveProjectPath(projectPath));
|
|
1410
1461
|
const fileTexts = /* @__PURE__ */ new Map();
|
|
1411
1462
|
const codeFiles = [];
|
|
1412
1463
|
const walk = await getWalkOptions(abs);
|
|
1413
1464
|
for await (const full of walkFiles(abs, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
|
|
1414
|
-
const rel = relative3(abs, full).split(
|
|
1465
|
+
const rel = relative3(abs, full).split(sep4).join("/");
|
|
1415
1466
|
const ext = extname2(rel).toLowerCase();
|
|
1416
1467
|
if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
|
|
1468
|
+
if (opts.files && !opts.files.has(rel)) continue;
|
|
1417
1469
|
const text = await safeReadText(full);
|
|
1418
1470
|
if (!text) continue;
|
|
1419
1471
|
codeFiles.push(rel);
|
|
@@ -1548,6 +1600,8 @@ Usage:
|
|
|
1548
1600
|
npx dsh-codebase-chat --project <path> --index
|
|
1549
1601
|
npx dsh-codebase-chat --project <path> --stats
|
|
1550
1602
|
npx dsh-codebase-chat --project <path> --health
|
|
1603
|
+
npx dsh-codebase-chat --project <path> --health --diff main
|
|
1604
|
+
npx dsh-codebase-chat --project <path> --watch
|
|
1551
1605
|
|
|
1552
1606
|
Options:
|
|
1553
1607
|
-p, --project <path> Project directory (default: current directory)
|
|
@@ -1557,6 +1611,8 @@ Options:
|
|
|
1557
1611
|
-i, --index Force re-index the project
|
|
1558
1612
|
-t, --stats Print indexing stats
|
|
1559
1613
|
-H, --health Deterministic static analysis (cycles, dead code, dupes, complexity)
|
|
1614
|
+
-d, --diff <ref> Scope --ask/--search/--health to files changed vs a git ref
|
|
1615
|
+
-w, --watch Keep the index hot \u2014 rebuild incrementally on file changes
|
|
1560
1616
|
-e, --embed Enable local semantic embeddings (slower, more relevant)
|
|
1561
1617
|
--lang <en|fr> Language for headings (default: .codebase-chat.json lang, else fr)
|
|
1562
1618
|
-h, --help Show this help
|
|
@@ -1579,6 +1635,8 @@ async function main() {
|
|
|
1579
1635
|
index: { type: "boolean", short: "i", default: false },
|
|
1580
1636
|
stats: { type: "boolean", short: "t", default: false },
|
|
1581
1637
|
health: { type: "boolean", short: "H", default: false },
|
|
1638
|
+
diff: { type: "string", short: "d" },
|
|
1639
|
+
watch: { type: "boolean", short: "w", default: false },
|
|
1582
1640
|
embed: { type: "boolean", short: "e", default: false },
|
|
1583
1641
|
lang: { type: "string" },
|
|
1584
1642
|
help: { type: "boolean", short: "h", default: false }
|
|
@@ -1608,8 +1666,50 @@ async function main() {
|
|
|
1608
1666
|
console.log(`Cache: ${index.projectHash}`);
|
|
1609
1667
|
exit(0);
|
|
1610
1668
|
}
|
|
1669
|
+
if (values.watch) {
|
|
1670
|
+
const abs = await findProjectRoot(project);
|
|
1671
|
+
const walk = await getWalkOptions(abs);
|
|
1672
|
+
await getIndex(abs, (m) => console.log(m), true);
|
|
1673
|
+
console.log(lang === "en" ? "Watch mode \u2014 the index stays hot while you code. (Ctrl+C to quit)" : "Mode watch \u2014 l\u2019index reste \xE0 jour pendant que tu codes. (Ctrl+C pour quitter)");
|
|
1674
|
+
let timer;
|
|
1675
|
+
let pending = /* @__PURE__ */ new Set();
|
|
1676
|
+
const watcher = watch(abs, { recursive: true }, (_event, filename) => {
|
|
1677
|
+
if (!filename) return;
|
|
1678
|
+
const rel = filename.split(sep5).join("/");
|
|
1679
|
+
if (rel.split("/").some((p) => walk.skipDirs.has(p))) return;
|
|
1680
|
+
pending.add(rel);
|
|
1681
|
+
if (timer) clearTimeout(timer);
|
|
1682
|
+
timer = setTimeout(() => {
|
|
1683
|
+
const changed = [...pending];
|
|
1684
|
+
pending = /* @__PURE__ */ new Set();
|
|
1685
|
+
const t0 = Date.now();
|
|
1686
|
+
void getIndex(abs, () => {
|
|
1687
|
+
}).then(() => {
|
|
1688
|
+
const dt = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
1689
|
+
const list = changed.slice(0, 4).join(", ") + (changed.length > 4 ? ` +${changed.length - 4}` : "");
|
|
1690
|
+
console.log(lang === "en" ? `Changed: ${list} \u2192 index refreshed (${dt}s)` : `Modifi\xE9 : ${list} \u2192 index \xE0 jour (${dt}s)`);
|
|
1691
|
+
});
|
|
1692
|
+
}, 500);
|
|
1693
|
+
});
|
|
1694
|
+
process.on("SIGINT", () => {
|
|
1695
|
+
watcher.close();
|
|
1696
|
+
exit(0);
|
|
1697
|
+
});
|
|
1698
|
+
await new Promise(() => {
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1611
1701
|
if (values.health) {
|
|
1612
|
-
|
|
1702
|
+
let scope;
|
|
1703
|
+
if (values.diff) {
|
|
1704
|
+
const s = await getChangedFiles(await findProjectRoot(project), values.diff);
|
|
1705
|
+
if (s.ok) {
|
|
1706
|
+
scope = { files: s.files };
|
|
1707
|
+
console.log(lang === "en" ? `Diff scope: ${s.files.size} file(s) changed vs ${values.diff}` : `P\xE9rim\xE8tre diff : ${s.files.size} fichier(s) modifi\xE9(s) vs ${values.diff}`);
|
|
1708
|
+
} else {
|
|
1709
|
+
console.error(lang === "en" ? `warning: diff vs "${values.diff}" unavailable (${s.error}) \u2014 full project` : `attention : diff vs "${values.diff}" indisponible (${s.error}) \u2014 projet complet`);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
const report = await analyzeProject(project, scope);
|
|
1613
1713
|
console.log(formatHealthReport(report, lang));
|
|
1614
1714
|
exit(0);
|
|
1615
1715
|
}
|
|
@@ -1620,7 +1720,8 @@ async function main() {
|
|
|
1620
1720
|
searchQuery: values.search,
|
|
1621
1721
|
filePath: values.file,
|
|
1622
1722
|
lang,
|
|
1623
|
-
embed: values.embed
|
|
1723
|
+
embed: values.embed,
|
|
1724
|
+
diff: values.diff
|
|
1624
1725
|
});
|
|
1625
1726
|
console.log(result.context);
|
|
1626
1727
|
console.log(`
|