dsh-code-index 0.1.0 → 0.2.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
@@ -9,6 +9,14 @@ import path from "path";
9
9
  import Parser from "web-tree-sitter";
10
10
  var require2 = createRequire(import.meta.url);
11
11
  var WASM_DIR = path.dirname(require2.resolve("tree-sitter-wasms/out/tree-sitter-typescript.wasm"));
12
+ var GRAMMAR_NAMES = {
13
+ typescript: "tree-sitter-typescript",
14
+ javascript: "tree-sitter-javascript",
15
+ python: "tree-sitter-python",
16
+ go: "tree-sitter-go",
17
+ rust: "tree-sitter-rust",
18
+ java: "tree-sitter-java"
19
+ };
12
20
  var EXT_TO_LANG = {
13
21
  ".ts": "typescript",
14
22
  ".tsx": "typescript",
@@ -17,7 +25,12 @@ var EXT_TO_LANG = {
17
25
  ".js": "javascript",
18
26
  ".jsx": "javascript",
19
27
  ".mjs": "javascript",
20
- ".cjs": "javascript"
28
+ ".cjs": "javascript",
29
+ ".py": "python",
30
+ ".pyi": "python",
31
+ ".go": "go",
32
+ ".rs": "rust",
33
+ ".java": "java"
21
34
  };
22
35
  function languageForFile(filePath) {
23
36
  const ext = path.extname(filePath).toLowerCase();
@@ -43,6 +56,29 @@ var QUERIES = {
43
56
  (method_definition) @method
44
57
  (class_declaration) @class
45
58
  (variable_declarator) @variable
59
+ `,
60
+ python: `
61
+ (function_definition) @function
62
+ (class_definition) @class
63
+ `,
64
+ go: `
65
+ (function_declaration) @function
66
+ (method_declaration) @method
67
+ (type_spec) @type
68
+ `,
69
+ rust: `
70
+ (function_item) @function
71
+ (function_signature_item) @method
72
+ (struct_item) @class
73
+ (enum_item) @enum
74
+ (trait_item) @interface
75
+ `,
76
+ java: `
77
+ (class_declaration) @class
78
+ (interface_declaration) @interface
79
+ (enum_declaration) @enum
80
+ (method_declaration) @method
81
+ (constructor_declaration) @method
46
82
  `
47
83
  };
48
84
  var CAPTURE_KINDS = {
@@ -55,6 +91,29 @@ var CAPTURE_KINDS = {
55
91
  variable: { kind: "variable" },
56
92
  field: { kind: "field" }
57
93
  };
94
+ var IMPORT_QUERIES = {
95
+ typescript: `
96
+ (import_statement) @import
97
+ (export_statement) @reexport
98
+ `,
99
+ javascript: `
100
+ (import_statement) @import
101
+ (export_statement) @reexport
102
+ `,
103
+ python: `
104
+ (import_from_statement) @import
105
+ (import_statement) @import
106
+ `,
107
+ go: `
108
+ (import_spec) @import
109
+ `,
110
+ rust: `
111
+ (use_declaration) @use
112
+ `,
113
+ java: `
114
+ (import_declaration) @import
115
+ `
116
+ };
58
117
  var parserPromise = null;
59
118
  async function getParser() {
60
119
  if (!parserPromise) {
@@ -71,82 +130,184 @@ var languageCache = /* @__PURE__ */ new Map();
71
130
  function getLanguage(id) {
72
131
  let entry = languageCache.get(id);
73
132
  if (!entry) {
74
- entry = getParser().then(async (parser) => {
75
- const grammarName = id === "typescript" ? "tree-sitter-typescript" : "tree-sitter-javascript";
76
- const grammarPath = path.join(WASM_DIR, `${grammarName}.wasm`);
133
+ entry = getParser().then(async () => {
134
+ const grammarPath = path.join(WASM_DIR, `${GRAMMAR_NAMES[id]}.wasm`);
77
135
  const bytes = await readFile(grammarPath);
78
136
  const lang = await Parser.Language.load(bytes);
79
137
  const q = lang.query(QUERIES[id]);
80
138
  q.delete();
139
+ const iq = lang.query(IMPORT_QUERIES[id]);
140
+ iq.delete();
81
141
  return lang;
82
142
  });
83
143
  languageCache.set(id, entry);
84
144
  }
85
145
  return entry;
86
146
  }
87
- async function extractSymbols(code, id) {
147
+ async function extractAll(code, id) {
88
148
  const lang = await getLanguage(id);
89
149
  const parser = await getParser();
90
150
  parser.setLanguage(lang);
91
151
  const tree = parser.parse(code);
92
152
  try {
93
- const query = lang.query(QUERIES[id]);
94
- const rows = [];
153
+ const symbols = [];
154
+ const symbolQuery = lang.query(QUERIES[id]);
95
155
  try {
96
- const captures = query.captures(tree.rootNode);
156
+ const captures = symbolQuery.captures(tree.rootNode);
97
157
  for (const cap of captures) {
98
158
  const def = CAPTURE_KINDS[cap.name];
99
159
  if (!def) continue;
100
160
  const node = cap.node;
161
+ if (def.kind === "variable" && !isModuleLevelVariable(node)) continue;
101
162
  const name2 = nameOf(node);
102
163
  if (!name2) continue;
103
- rows.push({
164
+ symbols.push({
104
165
  name: name2,
105
- kind: def.kind,
166
+ kind: kindFor(id, node, def.kind),
106
167
  file: "",
107
168
  // set by the caller (extractor is file-agnostic)
108
169
  line: node.startPosition.row + 1,
109
170
  endLine: node.endPosition.row + 1,
110
- exported: isExported(node),
171
+ exported: isExported(id, node),
111
172
  signature: signatureFor(node)
112
173
  });
113
174
  }
114
175
  } finally {
115
- query.delete();
176
+ symbolQuery.delete();
116
177
  }
117
- rows.sort((a, b) => a.line - b.line);
118
- return rows;
178
+ const imports = [];
179
+ const importQuery = lang.query(IMPORT_QUERIES[id]);
180
+ try {
181
+ for (const cap of importQuery.captures(tree.rootNode)) {
182
+ const spec = specifierOf(id, cap.node);
183
+ if (spec) imports.push(spec);
184
+ }
185
+ } finally {
186
+ importQuery.delete();
187
+ }
188
+ symbols.sort((a, b) => a.line - b.line);
189
+ return { symbols, imports };
119
190
  } finally {
120
191
  tree.delete();
121
192
  }
122
193
  }
194
+ async function extractSymbols(code, id) {
195
+ return (await extractAll(code, id)).symbols;
196
+ }
123
197
  function nameOf(node) {
124
198
  const field = node.childForFieldName?.("name");
125
199
  if (field) return field.text.trim();
126
200
  return "";
127
201
  }
202
+ function specifierOf(id, node) {
203
+ switch (node.type) {
204
+ case "import_statement":
205
+ if (id === "python") return dottedToPath(node.childForFieldName("name")?.text);
206
+ return stripQuotes(node.childForFieldName("source")?.text);
207
+ case "export_statement":
208
+ return stripQuotes(node.childForFieldName("source")?.text);
209
+ case "import_from_statement": {
210
+ const raw = node.childForFieldName("module_name")?.text;
211
+ if (raw == null) return null;
212
+ return raw.startsWith(".") ? pythonRelative(raw) : dottedToPath(raw);
213
+ }
214
+ case "import_spec":
215
+ return stripQuotes(node.childForFieldName("path")?.text);
216
+ case "use_declaration":
217
+ return rustUsePath(node.childForFieldName("argument")?.text);
218
+ case "import_declaration":
219
+ return dottedToPath(node.namedChildren[0]?.text);
220
+ default:
221
+ return null;
222
+ }
223
+ }
224
+ function rustUsePath(text) {
225
+ if (!text) return null;
226
+ const base = text.split("{")[0].trim();
227
+ if (!base) return null;
228
+ const parts = base.split("::").filter(Boolean);
229
+ if (parts[0] === "crate") parts.shift();
230
+ return parts.map((p) => p === "super" ? ".." : p === "self" ? "." : p).join("/");
231
+ }
232
+ function stripQuotes(text) {
233
+ if (text == null || text.length < 2) return null;
234
+ const first = text[0];
235
+ const last = text[text.length - 1];
236
+ if (first === "'" && last === "'" || first === '"' && last === '"') {
237
+ return text.slice(1, -1);
238
+ }
239
+ return null;
240
+ }
241
+ function dottedToPath(text) {
242
+ if (!text) return null;
243
+ return text.trim().replace(/\./g, "/");
244
+ }
245
+ function pythonRelative(raw) {
246
+ const dots = raw.match(/^\.+/)?.[0].length ?? 0;
247
+ const rest = raw.slice(dots).replace(/\./g, "/");
248
+ const prefix = dots === 1 ? "./" : "../".repeat(dots - 1);
249
+ return prefix + rest;
250
+ }
128
251
  function signatureFor(node) {
129
252
  const name2 = nameOf(node);
130
- const params = node.namedChildren.find(
131
- (c) => c.type === "formal_parameters" || c.type === "method_parameters"
253
+ const params = node.childForFieldName?.("parameters") ?? node.namedChildren.find(
254
+ (c) => c.type === "formal_parameters" || c.type === "method_parameters" || c.type === "parameters" || c.type === "parameter_list"
132
255
  );
133
256
  if (params) {
134
- return `${name2}${params.text}`;
257
+ return `${name2}${collapseSpace(params.text)}`;
135
258
  }
136
259
  const first = node.namedChildren[0];
137
- return first ? first.text.trim() : name2;
260
+ return first ? collapseSpace(first.text) : name2;
138
261
  }
139
- function isExported(node) {
140
- let parent = node.parent;
141
- let depth = 0;
142
- while (parent && depth < 3) {
262
+ function collapseSpace(text) {
263
+ return text.replace(/\s+/g, " ").replace(/\( /g, "(").replace(/ \)/g, ")").replace(/ ,/g, ",").replace(/,\)/g, ")").trim();
264
+ }
265
+ function isModuleLevelVariable(node) {
266
+ const declaration = node.parent;
267
+ const container = declaration?.parent;
268
+ return container?.type === "program" || container?.type === "export_statement";
269
+ }
270
+ function isExported(id, node) {
271
+ if (id === "python") {
272
+ return node.parent?.type === "module";
273
+ }
274
+ if (id === "go") {
275
+ const name2 = nameOf(node);
276
+ return !!name2 && /^[A-Z]/.test(name2);
277
+ }
278
+ if (id === "rust") {
279
+ return node.namedChildren.some(
280
+ (c) => c.type === "visibility_modifier" && c.text === "pub"
281
+ );
282
+ }
283
+ if (id === "java") {
284
+ if (node.parent?.type === "interface_body") return true;
285
+ return node.namedChildren.some((c) => c.type === "modifiers" && /\bpublic\b/.test(c.text));
286
+ }
287
+ for (let parent = node.parent; parent; parent = parent.parent) {
143
288
  if (parent.type === "export_statement") return true;
144
- if (parent.type === "statement_block") return false;
145
- parent = parent.parent;
146
- depth++;
289
+ if (parent.type === "statement_block" || parent.type === "class_body") return false;
290
+ if (parent.type === "program") return false;
147
291
  }
148
292
  return false;
149
293
  }
294
+ function kindFor(id, node, kind) {
295
+ if (id === "python" && kind === "function") {
296
+ const inClassBody = node.parent?.type === "block" && node.parent?.parent?.type === "class_definition";
297
+ if (inClassBody) return "method";
298
+ }
299
+ if (id === "go" && node.type === "type_spec") {
300
+ const type = node.childForFieldName("type")?.type;
301
+ if (type === "struct_type") return "class";
302
+ if (type === "interface_type") return "interface";
303
+ return "type";
304
+ }
305
+ if (id === "rust" && node.type === "function_item") {
306
+ const inImpl = node.parent?.type === "declaration_list" && node.parent?.parent?.type === "impl_item";
307
+ if (inImpl) return "method";
308
+ }
309
+ return kind;
310
+ }
150
311
  async function parseFileToSymbols(filePath, repoRoot, code) {
151
312
  const lang = languageForFile(filePath);
152
313
  if (!lang) return [];
@@ -183,7 +344,12 @@ var SUPPORTED_EXTS = /* @__PURE__ */ new Set([
183
344
  ".js",
184
345
  ".jsx",
185
346
  ".mjs",
186
- ".cjs"
347
+ ".cjs",
348
+ ".py",
349
+ ".pyi",
350
+ ".go",
351
+ ".rs",
352
+ ".java"
187
353
  ]);
188
354
  async function scanRepo(root, options = {}) {
189
355
  const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
@@ -240,11 +406,19 @@ async function loadIndex(cachePath) {
240
406
  const raw = await readFile2(cachePath, "utf8");
241
407
  const parsed = JSON.parse(raw);
242
408
  if (!parsed || typeof parsed.root !== "string" || !Array.isArray(parsed.files)) return null;
243
- return parsed;
409
+ return healSymbolFiles(parsed);
244
410
  } catch {
245
411
  return null;
246
412
  }
247
413
  }
414
+ function healSymbolFiles(index) {
415
+ for (const file of index.files) {
416
+ for (const symbol of file.symbols) {
417
+ if (!symbol.file) symbol.file = file.path;
418
+ }
419
+ }
420
+ return index;
421
+ }
248
422
  async function saveIndex(cachePath, index) {
249
423
  await mkdir(path3.dirname(cachePath), { recursive: true });
250
424
  const temporaryPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
@@ -283,12 +457,13 @@ async function buildIndex(root, options = {}, previous = null) {
283
457
  } catch {
284
458
  return null;
285
459
  }
286
- const symbols = await extractSymbols(code, lang);
460
+ const { symbols, imports } = await extractAll(code, lang);
287
461
  return {
288
462
  path: f.rel,
289
463
  lang,
290
464
  mtimeMs: f.mtimeMs,
291
- symbols: symbols.map((s) => ({ ...s, file: f.rel }))
465
+ symbols: symbols.map((s) => ({ ...s, file: f.rel })),
466
+ imports
292
467
  };
293
468
  })
294
469
  );
@@ -367,8 +542,17 @@ function matchScore(name2, query) {
367
542
  if (n === q) return 1;
368
543
  if (n.startsWith(q)) return 0.8;
369
544
  if (n.includes(q)) return 0.5;
545
+ if (q.length >= 3 && isSubsequence(q, n)) return 0.3;
370
546
  return 0;
371
547
  }
548
+ function isSubsequence(query, name2) {
549
+ let i = 0;
550
+ for (const ch of name2) {
551
+ if (ch === query[i]) i++;
552
+ if (i === query.length) return true;
553
+ }
554
+ return false;
555
+ }
372
556
  function searchSymbols(index, filter, limit = 50) {
373
557
  const q = (filter.query ?? "").trim();
374
558
  const filePat = filter.file?.trim().toLowerCase();
@@ -397,6 +581,7 @@ function renderHit(hit) {
397
581
  }
398
582
 
399
583
  // src/repomap.ts
584
+ import path5 from "path";
400
585
  var KIND_WEIGHT = {
401
586
  class: 1,
402
587
  interface: 1,
@@ -409,18 +594,25 @@ var KIND_WEIGHT = {
409
594
  import: 0.05,
410
595
  module: 0.3
411
596
  };
597
+ var TEST_PATH_RE = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/;
412
598
  function scoreFile(file) {
413
599
  let score = 0;
414
600
  for (const sym of file.symbols) {
415
601
  score += KIND_WEIGHT[sym.kind] ?? 0.3;
416
602
  if (sym.exported) score += 0.3;
417
603
  }
418
- return score / (1 + file.symbols.length * 0.04);
604
+ score /= 1 + file.symbols.length * 0.04;
605
+ if (TEST_PATH_RE.test(file.path)) score *= 0.2;
606
+ return score;
419
607
  }
420
608
  function rankRepoMap(index, options = {}) {
421
609
  const topFiles = options.topFiles ?? 24;
422
610
  const perFile = options.symbolsPerFile ?? 18;
423
- const ranked = index.files.filter((f) => f.symbols.length > 0).map((f) => ({ file: f, score: scoreFile(f) })).sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path)).slice(0, topFiles);
611
+ const refs = countReferences(index.files);
612
+ const ranked = index.files.filter((f) => f.symbols.length > 0).map((f) => ({
613
+ file: f,
614
+ score: scoreFile(f) + REF_WEIGHT * (refs.get(f.path) ?? 0)
615
+ })).sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path)).slice(0, topFiles);
424
616
  return ranked.map(({ file, score }) => ({
425
617
  path: file.path,
426
618
  score,
@@ -432,6 +624,45 @@ function rankRepoMap(index, options = {}) {
432
624
  }))
433
625
  }));
434
626
  }
627
+ var REF_WEIGHT = 0.5;
628
+ function countReferences(files) {
629
+ const fileSet = new Set(files.map((f) => f.path));
630
+ const counts = /* @__PURE__ */ new Map();
631
+ for (const file of files) {
632
+ const targets = /* @__PURE__ */ new Set();
633
+ for (const spec of file.imports ?? []) {
634
+ const target = resolveImport(spec, file.path, fileSet);
635
+ if (target && target !== file.path) targets.add(target);
636
+ }
637
+ for (const target of targets) {
638
+ counts.set(target, (counts.get(target) ?? 0) + 1);
639
+ }
640
+ }
641
+ return counts;
642
+ }
643
+ function resolveImport(spec, fromPath, fileSet) {
644
+ if (!spec) return null;
645
+ if (spec.startsWith("./") || spec.startsWith("../")) {
646
+ const base = path5.posix.normalize(path5.posix.join(path5.posix.dirname(fromPath), spec));
647
+ return candidateFor(base, fileSet);
648
+ }
649
+ const segments = spec.split("/").filter(Boolean);
650
+ for (let skip = 0; skip < segments.length; skip++) {
651
+ const hit = candidateFor(segments.slice(skip).join("/"), fileSet);
652
+ if (hit) return hit;
653
+ }
654
+ return null;
655
+ }
656
+ function candidateFor(base, fileSet) {
657
+ for (const ext of SUPPORTED_EXTS) {
658
+ if (fileSet.has(`${base}${ext}`)) return `${base}${ext}`;
659
+ }
660
+ for (const ext of SUPPORTED_EXTS) {
661
+ if (fileSet.has(`${base}/index${ext}`)) return `${base}/index${ext}`;
662
+ if (fileSet.has(`${base}/__init__${ext}`)) return `${base}/__init__${ext}`;
663
+ }
664
+ return null;
665
+ }
435
666
  function renderRepoMap(entries, options = {}) {
436
667
  const maxChars = options.maxChars ?? 3200;
437
668
  const lines = ["# repo map"];
@@ -454,13 +685,14 @@ function renderRepoMap(entries, options = {}) {
454
685
 
455
686
  // src/tools.ts
456
687
  import { defineTool } from "@deepseek-ai/dsh-tools";
457
- import path5 from "path";
688
+ import path6 from "path";
458
689
 
459
690
  // src/config.ts
460
691
  var DEFAULTS = {
461
692
  excludeDirs: [],
462
693
  mapTopFiles: 24,
463
694
  mapMaxChars: 3200,
695
+ mapTtlMs: 6e4,
464
696
  autoInject: true
465
697
  };
466
698
  var state = { current: { ...DEFAULTS } };
@@ -476,6 +708,9 @@ function applyConfig(partial) {
476
708
  if (!Number.isFinite(state.current.mapMaxChars) || state.current.mapMaxChars < 200) {
477
709
  state.current.mapMaxChars = DEFAULTS.mapMaxChars;
478
710
  }
711
+ if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
712
+ state.current.mapTtlMs = DEFAULTS.mapTtlMs;
713
+ }
479
714
  }
480
715
  function getConfig() {
481
716
  return state.current;
@@ -507,7 +742,7 @@ function createIndexCache(load, ttlMs = 6e4, now = Date.now) {
507
742
  }
508
743
  return {
509
744
  get(root, force = false) {
510
- const resolvedRoot = path5.resolve(root);
745
+ const resolvedRoot = path6.resolve(root);
511
746
  const key = cacheKeyForRoot(resolvedRoot);
512
747
  const running = inFlight.get(key);
513
748
  if (running) {
@@ -544,7 +779,7 @@ async function resolveRoot(arg, exec) {
544
779
  const cwd = exec.agent?.session?.header?.cwd;
545
780
  const base = arg ?? cwd ?? process.cwd();
546
781
  const root = await findRepoRoot(base);
547
- if (!root) throw new Error(`no git repository found from ${path5.resolve(base)}`);
782
+ if (!root) throw new Error(`no git repository found from ${path6.resolve(base)}`);
548
783
  return root;
549
784
  }
550
785
  var tools = [
@@ -698,7 +933,6 @@ var tools = [
698
933
  // src/index.ts
699
934
  var name = "dsh-code-index";
700
935
  var inject = ["tools", "systemPrompt"];
701
- var MAP_TTL_MS = 6e4;
702
936
  function apply(ctx, pluginConfig) {
703
937
  applyConfig(pluginConfig);
704
938
  invalidateIndexCache();
@@ -744,7 +978,7 @@ function apply(ctx, pluginConfig) {
744
978
  // before tool guidance (100–199), after persona (0)
745
979
  text: () => {
746
980
  const now = Date.now();
747
- if (cached && now - cached.at < MAP_TTL_MS) return cached.text;
981
+ if (cached && now - cached.at < getConfig().mapTtlMs) return cached.text;
748
982
  void warmMap();
749
983
  return cached?.text ?? "";
750
984
  }
@@ -772,6 +1006,7 @@ export {
772
1006
  buildIndex,
773
1007
  buildIndexWithCache,
774
1008
  defaultCachePath,
1009
+ extractAll,
775
1010
  extractSymbols,
776
1011
  findRepoRoot,
777
1012
  inject,