dsh-codebase-chat 0.21.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/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(index, filePath, embed);
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(index, searchQuery, embed);
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(index, query, embed);
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 sep3, posix as posixPath } from "path";
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(sep3).join("/");
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
- const report = await analyzeProject(project);
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(`