dsh-code-index 0.4.0 → 0.5.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 +6 -1
- package/README.zh.md +6 -1
- package/dist/client.js +279 -0
- package/dist/client.js.map +1 -0
- package/dist/index.js +428 -7
- package/dist/index.js.map +1 -1
- package/package.json +21 -4
package/dist/index.js
CHANGED
|
@@ -162,6 +162,66 @@ var IMPORT_QUERIES = {
|
|
|
162
162
|
(preproc_include) @import
|
|
163
163
|
`
|
|
164
164
|
};
|
|
165
|
+
var CALL_QUERIES = {
|
|
166
|
+
typescript: `
|
|
167
|
+
(call_expression function: (identifier) @call)
|
|
168
|
+
(call_expression function: (member_expression property: (property_identifier) @call))
|
|
169
|
+
(new_expression constructor: (identifier) @call)
|
|
170
|
+
`,
|
|
171
|
+
javascript: `
|
|
172
|
+
(call_expression function: (identifier) @call)
|
|
173
|
+
(call_expression function: (member_expression property: (property_identifier) @call))
|
|
174
|
+
(new_expression constructor: (identifier) @call)
|
|
175
|
+
`,
|
|
176
|
+
python: `
|
|
177
|
+
(call function: (identifier) @call)
|
|
178
|
+
(call function: (attribute attribute: (identifier) @call))
|
|
179
|
+
`,
|
|
180
|
+
go: `
|
|
181
|
+
(call_expression function: (identifier) @call)
|
|
182
|
+
(call_expression function: (selector_expression field: (field_identifier) @call))
|
|
183
|
+
`,
|
|
184
|
+
rust: `
|
|
185
|
+
(call_expression function: (identifier) @call)
|
|
186
|
+
(call_expression function: (field_expression field: (field_identifier) @call))
|
|
187
|
+
(call_expression function: (scoped_identifier name: (identifier) @call))
|
|
188
|
+
`,
|
|
189
|
+
java: `
|
|
190
|
+
(method_invocation name: (identifier) @call)
|
|
191
|
+
(object_creation_expression type: (type_identifier) @call)
|
|
192
|
+
`,
|
|
193
|
+
cpp: `
|
|
194
|
+
(call_expression function: (identifier) @call)
|
|
195
|
+
(call_expression function: (field_expression field: (field_identifier) @call))
|
|
196
|
+
(call_expression function: (qualified_identifier name: (identifier) @call))
|
|
197
|
+
`,
|
|
198
|
+
c: `
|
|
199
|
+
(call_expression function: (identifier) @call)
|
|
200
|
+
(call_expression function: (field_expression field: (field_identifier) @call))
|
|
201
|
+
`
|
|
202
|
+
};
|
|
203
|
+
var CALLABLE_NODES = {
|
|
204
|
+
typescript: /* @__PURE__ */ new Set([
|
|
205
|
+
"function_declaration",
|
|
206
|
+
"generator_function_declaration",
|
|
207
|
+
"method_definition",
|
|
208
|
+
"function_expression",
|
|
209
|
+
"arrow_function"
|
|
210
|
+
]),
|
|
211
|
+
javascript: /* @__PURE__ */ new Set([
|
|
212
|
+
"function_declaration",
|
|
213
|
+
"generator_function_declaration",
|
|
214
|
+
"method_definition",
|
|
215
|
+
"function_expression",
|
|
216
|
+
"arrow_function"
|
|
217
|
+
]),
|
|
218
|
+
python: /* @__PURE__ */ new Set(["function_definition"]),
|
|
219
|
+
go: /* @__PURE__ */ new Set(["function_declaration", "method_declaration", "func_literal"]),
|
|
220
|
+
rust: /* @__PURE__ */ new Set(["function_item", "function_signature_item"]),
|
|
221
|
+
java: /* @__PURE__ */ new Set(["method_declaration", "constructor_declaration"]),
|
|
222
|
+
cpp: /* @__PURE__ */ new Set(["function_definition", "lambda_expression"]),
|
|
223
|
+
c: /* @__PURE__ */ new Set(["function_definition"])
|
|
224
|
+
};
|
|
165
225
|
var parserPromise = null;
|
|
166
226
|
async function getParser() {
|
|
167
227
|
if (!parserPromise) {
|
|
@@ -184,6 +244,7 @@ function getLanguage(id) {
|
|
|
184
244
|
const lang = await Language.load(bytes);
|
|
185
245
|
new Query(lang, QUERIES[id]).delete();
|
|
186
246
|
new Query(lang, IMPORT_QUERIES[id]).delete();
|
|
247
|
+
new Query(lang, CALL_QUERIES[id]).delete();
|
|
187
248
|
return lang;
|
|
188
249
|
});
|
|
189
250
|
languageCache.set(id, entry);
|
|
@@ -232,6 +293,24 @@ async function extractAll(code, id) {
|
|
|
232
293
|
} finally {
|
|
233
294
|
importQuery.delete();
|
|
234
295
|
}
|
|
296
|
+
const calls = [];
|
|
297
|
+
const callQuery = new Query(lang, CALL_QUERIES[id]);
|
|
298
|
+
try {
|
|
299
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
300
|
+
for (const cap of callQuery.captures(tree.rootNode)) {
|
|
301
|
+
const name2 = cap.node.text.trim();
|
|
302
|
+
if (!name2) continue;
|
|
303
|
+
const line = cap.node.startPosition.row + 1;
|
|
304
|
+
const from = enclosingCallable(id, cap.node);
|
|
305
|
+
const key = `${from}|${name2}|${line}`;
|
|
306
|
+
if (seen2.has(key)) continue;
|
|
307
|
+
seen2.add(key);
|
|
308
|
+
calls.push({ name: name2, line, from });
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
callQuery.delete();
|
|
312
|
+
}
|
|
313
|
+
calls.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
235
314
|
symbols.sort((a, b) => a.line - b.line);
|
|
236
315
|
const seen = /* @__PURE__ */ new Set();
|
|
237
316
|
const deduped = symbols.filter((s) => {
|
|
@@ -240,7 +319,7 @@ async function extractAll(code, id) {
|
|
|
240
319
|
seen.add(key);
|
|
241
320
|
return true;
|
|
242
321
|
});
|
|
243
|
-
return { symbols: deduped, imports };
|
|
322
|
+
return { symbols: deduped, imports, calls };
|
|
244
323
|
} finally {
|
|
245
324
|
tree.delete();
|
|
246
325
|
}
|
|
@@ -282,6 +361,19 @@ function cppNameOf(node) {
|
|
|
282
361
|
}
|
|
283
362
|
return "";
|
|
284
363
|
}
|
|
364
|
+
function enclosingCallable(id, node) {
|
|
365
|
+
const callable = CALLABLE_NODES[id];
|
|
366
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
367
|
+
if (!callable.has(parent.type)) continue;
|
|
368
|
+
const named = nameOf(id, parent);
|
|
369
|
+
if (named) return named;
|
|
370
|
+
const declaration = parent.parent;
|
|
371
|
+
const field = declaration?.type === "variable_declarator" ? "name" : null;
|
|
372
|
+
if (field) return declaration?.childForFieldName?.(field)?.text.trim() ?? "";
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
285
377
|
function specifierOf(id, node) {
|
|
286
378
|
switch (node.type) {
|
|
287
379
|
case "import_statement":
|
|
@@ -617,13 +709,14 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
617
709
|
} catch {
|
|
618
710
|
return null;
|
|
619
711
|
}
|
|
620
|
-
const { symbols, imports } = await extractAll(code, lang);
|
|
712
|
+
const { symbols, imports, calls } = await extractAll(code, lang);
|
|
621
713
|
return {
|
|
622
714
|
path: f.rel,
|
|
623
715
|
lang,
|
|
624
716
|
mtimeMs: f.mtimeMs,
|
|
625
717
|
symbols: symbols.map((s) => ({ ...s, file: f.rel })),
|
|
626
|
-
imports
|
|
718
|
+
imports,
|
|
719
|
+
calls
|
|
627
720
|
};
|
|
628
721
|
})
|
|
629
722
|
);
|
|
@@ -669,12 +762,22 @@ function indexesEqual(left, right) {
|
|
|
669
762
|
return false;
|
|
670
763
|
}
|
|
671
764
|
if (file.symbols.length !== other.symbols.length) return false;
|
|
765
|
+
if (!callsEqual(file.calls, other.calls)) return false;
|
|
672
766
|
return file.symbols.every((symbol, symbolIndex) => {
|
|
673
767
|
const candidate = other.symbols[symbolIndex];
|
|
674
768
|
return candidate !== void 0 && symbol.name === candidate.name && symbol.kind === candidate.kind && symbol.file === candidate.file && symbol.line === candidate.line && symbol.endLine === candidate.endLine && symbol.exported === candidate.exported && symbol.signature === candidate.signature;
|
|
675
769
|
});
|
|
676
770
|
});
|
|
677
771
|
}
|
|
772
|
+
function callsEqual(left, right) {
|
|
773
|
+
const a = left ?? [];
|
|
774
|
+
const b = right ?? [];
|
|
775
|
+
if (a.length !== b.length) return false;
|
|
776
|
+
return a.every((call, index) => {
|
|
777
|
+
const other = b[index];
|
|
778
|
+
return other !== void 0 && call.name === other.name && call.line === other.line && call.from === other.from;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
678
781
|
async function findRepoRoot(startDir, maxLevels = 12) {
|
|
679
782
|
let dir = path4.resolve(startDir);
|
|
680
783
|
for (let level = 0; level < maxLevels; level++) {
|
|
@@ -695,6 +798,56 @@ function symbolCount(index) {
|
|
|
695
798
|
return index.files.reduce((n, f) => n + f.symbols.length, 0);
|
|
696
799
|
}
|
|
697
800
|
|
|
801
|
+
// src/refgraph.ts
|
|
802
|
+
function buildSymbolTable(index) {
|
|
803
|
+
const table = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const file of index.files) {
|
|
805
|
+
for (const symbol of file.symbols) {
|
|
806
|
+
const list = table.get(symbol.name);
|
|
807
|
+
if (list) list.push(symbol);
|
|
808
|
+
else table.set(symbol.name, [symbol]);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
for (const list of table.values()) {
|
|
812
|
+
list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
813
|
+
}
|
|
814
|
+
return table;
|
|
815
|
+
}
|
|
816
|
+
function callerCounts(index) {
|
|
817
|
+
const counts = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const file of index.files) {
|
|
819
|
+
for (const call of file.calls ?? []) {
|
|
820
|
+
counts.set(call.name, (counts.get(call.name) ?? 0) + 1);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return counts;
|
|
824
|
+
}
|
|
825
|
+
function symbolRefs(index, symbol, limit = 200) {
|
|
826
|
+
const table = buildSymbolTable(index);
|
|
827
|
+
const defs = table.get(symbol) ?? [];
|
|
828
|
+
const callers = [];
|
|
829
|
+
const calleeFirstLine = /* @__PURE__ */ new Map();
|
|
830
|
+
for (const file of index.files) {
|
|
831
|
+
for (const call of file.calls ?? []) {
|
|
832
|
+
if (call.name === symbol) {
|
|
833
|
+
callers.push({ file: file.path, line: call.line, from: call.from });
|
|
834
|
+
}
|
|
835
|
+
if (call.from === symbol && !calleeFirstLine.has(call.name)) {
|
|
836
|
+
calleeFirstLine.set(call.name, call.line);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
callers.sort(
|
|
841
|
+
(a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.from.localeCompare(b.from)
|
|
842
|
+
);
|
|
843
|
+
const callees = [...calleeFirstLine.entries()].map(([name2, line]) => ({ name: name2, line, targets: table.get(name2) ?? [] })).sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
844
|
+
return {
|
|
845
|
+
defs,
|
|
846
|
+
callers: callers.slice(0, limit),
|
|
847
|
+
callees: callees.slice(0, limit)
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
698
851
|
// src/search.ts
|
|
699
852
|
function matchScore(name2, query) {
|
|
700
853
|
const q = query.toLowerCase();
|
|
@@ -727,8 +880,9 @@ function searchSymbols(index, filter, limit = 50) {
|
|
|
727
880
|
hits.push({ ...sym, score });
|
|
728
881
|
}
|
|
729
882
|
}
|
|
883
|
+
const refs = callerCounts(index);
|
|
730
884
|
hits.sort((a, b) => {
|
|
731
|
-
const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
|
|
885
|
+
const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || (refs.get(b.name) ?? 0) - (refs.get(a.name) ?? 0) || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
|
|
732
886
|
return ab;
|
|
733
887
|
});
|
|
734
888
|
return hits.slice(0, limit);
|
|
@@ -888,6 +1042,76 @@ function renderRepoMap(entries, options = {}) {
|
|
|
888
1042
|
return totalSymbols > 0 ? text : "";
|
|
889
1043
|
}
|
|
890
1044
|
|
|
1045
|
+
// src/health.ts
|
|
1046
|
+
function buildModuleGraph(index) {
|
|
1047
|
+
const nodes = index.files.map((file) => file.path).sort((a, b) => a.localeCompare(b));
|
|
1048
|
+
const fileSet = new Set(nodes);
|
|
1049
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1050
|
+
for (const file of index.files) {
|
|
1051
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1052
|
+
for (const spec of file.imports ?? []) {
|
|
1053
|
+
const target = resolveImport(spec, file.path, fileSet);
|
|
1054
|
+
if (target && target !== file.path) targets.add(target);
|
|
1055
|
+
}
|
|
1056
|
+
edges.set(file.path, targets);
|
|
1057
|
+
}
|
|
1058
|
+
return { nodes, edges };
|
|
1059
|
+
}
|
|
1060
|
+
function findCycles(index) {
|
|
1061
|
+
const { nodes, edges } = buildModuleGraph(index);
|
|
1062
|
+
const discovery = /* @__PURE__ */ new Map();
|
|
1063
|
+
const low = /* @__PURE__ */ new Map();
|
|
1064
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1065
|
+
const stack = [];
|
|
1066
|
+
const components = [];
|
|
1067
|
+
let counter = 0;
|
|
1068
|
+
const visit = (node) => {
|
|
1069
|
+
discovery.set(node, counter);
|
|
1070
|
+
low.set(node, counter);
|
|
1071
|
+
counter++;
|
|
1072
|
+
stack.push(node);
|
|
1073
|
+
onStack.add(node);
|
|
1074
|
+
for (const next of edges.get(node) ?? []) {
|
|
1075
|
+
if (!discovery.has(next)) {
|
|
1076
|
+
visit(next);
|
|
1077
|
+
low.set(node, Math.min(low.get(node), low.get(next)));
|
|
1078
|
+
} else if (onStack.has(next)) {
|
|
1079
|
+
low.set(node, Math.min(low.get(node), discovery.get(next)));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (low.get(node) === discovery.get(node)) {
|
|
1083
|
+
const component = [];
|
|
1084
|
+
for (; ; ) {
|
|
1085
|
+
const member = stack.pop();
|
|
1086
|
+
onStack.delete(member);
|
|
1087
|
+
component.push(member);
|
|
1088
|
+
if (member === node) break;
|
|
1089
|
+
}
|
|
1090
|
+
if (component.length > 1) components.push(component.sort());
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
for (const node of nodes) {
|
|
1094
|
+
if (!discovery.has(node)) visit(node);
|
|
1095
|
+
}
|
|
1096
|
+
components.sort((a, b) => a[0].localeCompare(b[0]));
|
|
1097
|
+
return components;
|
|
1098
|
+
}
|
|
1099
|
+
var ENTRY_RE = /(^|\/)(index|main|cli|app|server|mod|lib|__init__)\.[A-Za-z0-9]+$|(^|\/)(bin|cmd|scripts)\/|\.config\.[A-Za-z0-9]+$/;
|
|
1100
|
+
var TEST_RE = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/;
|
|
1101
|
+
function findOrphanModules(index) {
|
|
1102
|
+
const { nodes, edges } = buildModuleGraph(index);
|
|
1103
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
1104
|
+
for (const targets of edges.values()) {
|
|
1105
|
+
for (const target of targets) incoming.set(target, (incoming.get(target) ?? 0) + 1);
|
|
1106
|
+
}
|
|
1107
|
+
const withSymbols = new Set(
|
|
1108
|
+
index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
|
|
1109
|
+
);
|
|
1110
|
+
return nodes.filter(
|
|
1111
|
+
(path7) => withSymbols.has(path7) && (incoming.get(path7) ?? 0) === 0 && (edges.get(path7)?.size ?? 0) === 0 && !ENTRY_RE.test(path7) && !TEST_RE.test(path7)
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
891
1115
|
// src/tools.ts
|
|
892
1116
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
893
1117
|
import path6 from "path";
|
|
@@ -898,7 +1122,8 @@ var DEFAULTS = {
|
|
|
898
1122
|
mapTopFiles: 24,
|
|
899
1123
|
mapMaxChars: 3200,
|
|
900
1124
|
mapTtlMs: 6e4,
|
|
901
|
-
autoInject: true
|
|
1125
|
+
autoInject: true,
|
|
1126
|
+
codeHealth: false
|
|
902
1127
|
};
|
|
903
1128
|
var state = { current: { ...DEFAULTS } };
|
|
904
1129
|
function applyConfig(partial) {
|
|
@@ -987,6 +1212,38 @@ async function resolveRoot(arg, exec) {
|
|
|
987
1212
|
if (!root) throw new Error(`no git repository found from ${path6.resolve(base)}`);
|
|
988
1213
|
return root;
|
|
989
1214
|
}
|
|
1215
|
+
function hitsToSearchMeta(hits) {
|
|
1216
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1217
|
+
for (const raw of hits) {
|
|
1218
|
+
const hit = raw;
|
|
1219
|
+
if (!hit.file || typeof hit.line !== "number") continue;
|
|
1220
|
+
const label = `${hit.kind ?? ""} ${hit.signature || hit.name || ""}`.trim();
|
|
1221
|
+
const list = byFile.get(hit.file) ?? [];
|
|
1222
|
+
list.push({ lineNumber: hit.line, line: label });
|
|
1223
|
+
byFile.set(hit.file, list);
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
shape: "matches",
|
|
1227
|
+
files: [...byFile.entries()].map(([path7, matches]) => ({ path: path7, matches })),
|
|
1228
|
+
truncated: false,
|
|
1229
|
+
total: hits.length
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
function searchCardFromMeta(meta) {
|
|
1233
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
|
|
1234
|
+
const record = meta;
|
|
1235
|
+
if (record.shape !== "matches" || !Array.isArray(record.files)) return void 0;
|
|
1236
|
+
const files = record.files.filter(
|
|
1237
|
+
(entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
|
|
1238
|
+
);
|
|
1239
|
+
return {
|
|
1240
|
+
card: "search",
|
|
1241
|
+
shape: "matches",
|
|
1242
|
+
files,
|
|
1243
|
+
truncated: record.truncated === true,
|
|
1244
|
+
total: typeof record.total === "number" ? record.total : 0
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
990
1247
|
var tools = [
|
|
991
1248
|
defineTool({
|
|
992
1249
|
name: "code_index",
|
|
@@ -1059,8 +1316,16 @@ var tools = [
|
|
|
1059
1316
|
render: (_args, value) => value.map((v) => ({
|
|
1060
1317
|
type: "text",
|
|
1061
1318
|
text: renderHit(v)
|
|
1062
|
-
}))
|
|
1319
|
+
})),
|
|
1320
|
+
presentationMeta: (_args, value) => hitsToSearchMeta(value)
|
|
1063
1321
|
},
|
|
1322
|
+
presentCall: (args) => ({
|
|
1323
|
+
card: "generic",
|
|
1324
|
+
title: args.kind ? `List ${args.kind} symbols` : "List symbols",
|
|
1325
|
+
kind: "search",
|
|
1326
|
+
rawInput: { query: args.query, file: args.file, kind: args.kind }
|
|
1327
|
+
}),
|
|
1328
|
+
presentResult: (_args, result) => searchCardFromMeta(result.meta),
|
|
1064
1329
|
async execute(args, exec) {
|
|
1065
1330
|
const root = await resolveRoot(args.repoRoot, exec);
|
|
1066
1331
|
const index = await getIndex(root);
|
|
@@ -1095,8 +1360,16 @@ var tools = [
|
|
|
1095
1360
|
render: (_args, value) => value.map((v) => ({
|
|
1096
1361
|
type: "text",
|
|
1097
1362
|
text: renderHit(v)
|
|
1098
|
-
}))
|
|
1363
|
+
})),
|
|
1364
|
+
presentationMeta: (_args, value) => hitsToSearchMeta(value)
|
|
1099
1365
|
},
|
|
1366
|
+
presentCall: (args) => ({
|
|
1367
|
+
card: "generic",
|
|
1368
|
+
title: `Search "${args.query}"`,
|
|
1369
|
+
kind: "search",
|
|
1370
|
+
rawInput: { query: args.query }
|
|
1371
|
+
}),
|
|
1372
|
+
presentResult: (_args, result) => searchCardFromMeta(result.meta),
|
|
1100
1373
|
async execute(args, exec) {
|
|
1101
1374
|
const root = await resolveRoot(args.repoRoot, exec);
|
|
1102
1375
|
const index = await getIndex(root);
|
|
@@ -1132,9 +1405,141 @@ var tools = [
|
|
|
1132
1405
|
return `code_index: ${error.message ?? String(error)}`;
|
|
1133
1406
|
}
|
|
1134
1407
|
}
|
|
1408
|
+
}),
|
|
1409
|
+
defineTool({
|
|
1410
|
+
name: "code_refs",
|
|
1411
|
+
description: "Trace a symbol through the call graph: who calls it (callers) and what it calls (callees), resolved to in-repo definitions with file:line. Complements code_search \u2014 search finds a definition, code_refs shows how it is used. Resolution is name-based, so like-named symbols are reported as candidate definitions.",
|
|
1412
|
+
parameters: {
|
|
1413
|
+
symbol: {
|
|
1414
|
+
type: "string",
|
|
1415
|
+
required: true,
|
|
1416
|
+
description: "Symbol name to trace (case-sensitive)."
|
|
1417
|
+
},
|
|
1418
|
+
direction: {
|
|
1419
|
+
type: "string",
|
|
1420
|
+
enum: ["callers", "callees", "both"],
|
|
1421
|
+
description: 'Which side of the call graph to return (default "both").'
|
|
1422
|
+
},
|
|
1423
|
+
limit: {
|
|
1424
|
+
type: "number",
|
|
1425
|
+
description: "Max rows per side (default 50)."
|
|
1426
|
+
},
|
|
1427
|
+
repoRoot: {
|
|
1428
|
+
type: "string",
|
|
1429
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
output: {
|
|
1433
|
+
schema: { type: "string" },
|
|
1434
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
1435
|
+
},
|
|
1436
|
+
presentCall: (args) => ({
|
|
1437
|
+
card: "generic",
|
|
1438
|
+
title: `Refs ${args.symbol}`,
|
|
1439
|
+
kind: "search",
|
|
1440
|
+
rawInput: { symbol: args.symbol, direction: args.direction }
|
|
1441
|
+
}),
|
|
1442
|
+
async execute(args, exec) {
|
|
1443
|
+
try {
|
|
1444
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
1445
|
+
const index = await getIndex(root);
|
|
1446
|
+
const refs = symbolRefs(index, args.symbol, args.limit ?? 50);
|
|
1447
|
+
const direction = args.direction ?? "both";
|
|
1448
|
+
const lines = [];
|
|
1449
|
+
if (refs.defs.length > 0) {
|
|
1450
|
+
lines.push("definitions:");
|
|
1451
|
+
for (const def of refs.defs) {
|
|
1452
|
+
lines.push(` ${def.kind} ${def.signature || def.name} \u2014 ${def.file}:${def.line}`);
|
|
1453
|
+
}
|
|
1454
|
+
} else {
|
|
1455
|
+
lines.push(`no definition of "${args.symbol}" in this repo`);
|
|
1456
|
+
}
|
|
1457
|
+
if (direction === "callers" || direction === "both") {
|
|
1458
|
+
lines.push("", `callers (${refs.callers.length}):`);
|
|
1459
|
+
if (refs.callers.length === 0) lines.push(" (none)");
|
|
1460
|
+
for (const caller of refs.callers) {
|
|
1461
|
+
lines.push(` ${caller.file}:${caller.line}${caller.from ? ` in ${caller.from}` : ""}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (direction === "callees" || direction === "both") {
|
|
1465
|
+
lines.push("", `callees (${refs.callees.length}):`);
|
|
1466
|
+
if (refs.callees.length === 0) lines.push(" (none)");
|
|
1467
|
+
for (const callee of refs.callees) {
|
|
1468
|
+
const target = callee.targets[0];
|
|
1469
|
+
lines.push(
|
|
1470
|
+
` ${callee.name} :${callee.line}${target ? ` \u2192 ${target.file}:${target.line}` : " (external)"}`
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return lines.join("\n");
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
return `code_refs: ${error.message ?? String(error)}`;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}),
|
|
1480
|
+
defineTool({
|
|
1481
|
+
name: "code_health",
|
|
1482
|
+
description: "Report structural health from the import graph: circular dependencies (import cycles) and orphan modules (symbol-bearing files that nothing imports and that import nothing, excluding entry points and tests).",
|
|
1483
|
+
parameters: {
|
|
1484
|
+
limit: {
|
|
1485
|
+
type: "number",
|
|
1486
|
+
description: "Max cycles and orphan rows to list (default 50)."
|
|
1487
|
+
},
|
|
1488
|
+
repoRoot: {
|
|
1489
|
+
type: "string",
|
|
1490
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
1491
|
+
}
|
|
1492
|
+
},
|
|
1493
|
+
output: {
|
|
1494
|
+
schema: { type: "string" },
|
|
1495
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
1496
|
+
},
|
|
1497
|
+
presentCall: () => ({ card: "generic", title: "Code health", kind: "read" }),
|
|
1498
|
+
async execute(args, exec) {
|
|
1499
|
+
try {
|
|
1500
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
1501
|
+
const index = await getIndex(root);
|
|
1502
|
+
const limit = args.limit ?? 50;
|
|
1503
|
+
const cycles = findCycles(index);
|
|
1504
|
+
const orphans = findOrphanModules(index);
|
|
1505
|
+
const lines = [`repo health \u2014 ${index.files.length} files`];
|
|
1506
|
+
lines.push("", `circular dependencies (${cycles.length}):`);
|
|
1507
|
+
if (cycles.length === 0) lines.push(" (none)");
|
|
1508
|
+
for (const cycle of cycles.slice(0, limit)) lines.push(` ${cycle.join(" \u2192 ")}`);
|
|
1509
|
+
lines.push("", `orphan modules (${orphans.length}):`);
|
|
1510
|
+
if (orphans.length === 0) lines.push(" (none)");
|
|
1511
|
+
for (const orphan of orphans.slice(0, limit)) lines.push(` ${orphan}`);
|
|
1512
|
+
return lines.join("\n");
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
return `code_health: ${error.message ?? String(error)}`;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1135
1517
|
})
|
|
1136
1518
|
];
|
|
1137
1519
|
|
|
1520
|
+
// src/settings.ts
|
|
1521
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
1522
|
+
var SETTINGS_NAMESPACE = "code-index";
|
|
1523
|
+
var settingsSchema = Schema.object({
|
|
1524
|
+
excludeDirs: Schema.array(Schema.string()).default([]),
|
|
1525
|
+
mapTopFiles: Schema.number().default(24),
|
|
1526
|
+
mapMaxChars: Schema.number().default(3200),
|
|
1527
|
+
mapTtlMs: Schema.number().default(6e4),
|
|
1528
|
+
autoInject: Schema.boolean().default(true),
|
|
1529
|
+
codeHealth: Schema.boolean().default(false)
|
|
1530
|
+
});
|
|
1531
|
+
function registerSettings(getService, pluginConfig, onResolved) {
|
|
1532
|
+
const service = getService("settings");
|
|
1533
|
+
if (!service) return;
|
|
1534
|
+
const scope = service.register(SETTINGS_NAMESPACE, settingsSchema, {
|
|
1535
|
+
base: pluginConfig ?? {}
|
|
1536
|
+
});
|
|
1537
|
+
onResolved(scope.get());
|
|
1538
|
+
scope.watch((next) => {
|
|
1539
|
+
onResolved(next);
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1138
1543
|
// src/index.ts
|
|
1139
1544
|
var name = "dsh-code-index";
|
|
1140
1545
|
var inject = ["tools", "systemPrompt"];
|
|
@@ -1144,7 +1549,17 @@ function apply(ctx, pluginConfig) {
|
|
|
1144
1549
|
ctx.effect(() => {
|
|
1145
1550
|
const disposers = [];
|
|
1146
1551
|
console.log("[dsh-code-index] plugin loaded");
|
|
1552
|
+
if (ctx.inject) {
|
|
1553
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
1554
|
+
registerSettings(
|
|
1555
|
+
(name2) => settingsCtx.get(name2),
|
|
1556
|
+
pluginConfig,
|
|
1557
|
+
(resolved) => applyConfig(resolved)
|
|
1558
|
+
);
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1147
1561
|
for (const tool of tools) {
|
|
1562
|
+
if (tool.name === "code_health" && !getConfig().codeHealth) continue;
|
|
1148
1563
|
disposers.push(ctx.tools.register(tool));
|
|
1149
1564
|
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
1150
1565
|
}
|
|
@@ -1210,9 +1625,14 @@ export {
|
|
|
1210
1625
|
apply,
|
|
1211
1626
|
buildIndex,
|
|
1212
1627
|
buildIndexWithCache,
|
|
1628
|
+
buildModuleGraph,
|
|
1629
|
+
buildSymbolTable,
|
|
1630
|
+
callerCounts,
|
|
1213
1631
|
defaultCachePath,
|
|
1214
1632
|
extractAll,
|
|
1215
1633
|
extractSymbols,
|
|
1634
|
+
findCycles,
|
|
1635
|
+
findOrphanModules,
|
|
1216
1636
|
findRepoRoot,
|
|
1217
1637
|
inject,
|
|
1218
1638
|
languageForFile,
|
|
@@ -1227,6 +1647,7 @@ export {
|
|
|
1227
1647
|
scoreFile,
|
|
1228
1648
|
searchSymbols,
|
|
1229
1649
|
symbolCount,
|
|
1650
|
+
symbolRefs,
|
|
1230
1651
|
tools
|
|
1231
1652
|
};
|
|
1232
1653
|
//# sourceMappingURL=index.js.map
|