dsh-code-index 0.6.1 → 0.7.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
@@ -1533,8 +1533,8 @@ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile
1533
1533
  if (!qualified) {
1534
1534
  const locals = named.filter((s) => s.file === file.path && lexicallyVisible(s, call));
1535
1535
  if (locals.length > 0) {
1536
- const unique = locals.length === 1;
1537
- const owned = unique && isStructuralOwner(call, locals[0]);
1536
+ const unique2 = locals.length === 1;
1537
+ const owned = unique2 && isStructuralOwner(call, locals[0]);
1538
1538
  const provenance = owned && call.fromId !== null ? "structural-owner" : "local-unique";
1539
1539
  return toEdges(locals, call, file.path, provenance, owned ? "exact" : "name-only", maxCandidates);
1540
1540
  }
@@ -1618,10 +1618,10 @@ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile
1618
1618
  return [];
1619
1619
  }
1620
1620
  function toEdges(candidates, call, filePath, provenance, resolution, maxCandidates) {
1621
- const unique = dedupeSymbols(candidates);
1622
- const count = unique.length;
1621
+ const unique2 = dedupeSymbols(candidates);
1622
+ const count = unique2.length;
1623
1623
  const cap = maxCandidates > 0 ? Math.floor(maxCandidates) : 0;
1624
- return unique.slice(0, cap).map((target) => ({
1624
+ return unique2.slice(0, cap).map((target) => ({
1625
1625
  sourceId: call.fromId,
1626
1626
  targetId: target.id,
1627
1627
  callSite: { file: filePath, line: call.line },
@@ -1701,7 +1701,7 @@ function isSubsequence(query, name2) {
1701
1701
  }
1702
1702
  return false;
1703
1703
  }
1704
- function searchSymbols(index, filter, limit = 50) {
1704
+ function searchSymbols(index, filter, limit = 50, refs = callerCounts(index)) {
1705
1705
  const q = (filter.query ?? "").trim();
1706
1706
  const filePat = filter.file?.trim().toLowerCase();
1707
1707
  const hits = [];
@@ -1715,7 +1715,6 @@ function searchSymbols(index, filter, limit = 50) {
1715
1715
  hits.push({ ...sym, score });
1716
1716
  }
1717
1717
  }
1718
- const refs = callerCounts(index);
1719
1718
  hits.sort((a, b) => {
1720
1719
  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);
1721
1720
  return ab;
@@ -2715,6 +2714,409 @@ function renderChangeContext(result, maxChars = 12e3) {
2715
2714
  return out;
2716
2715
  }
2717
2716
 
2717
+ // src/context.ts
2718
+ var TEST_PATH_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/i;
2719
+ var FILE_TOKEN_RE = /[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+/g;
2720
+ var CODE_TOKEN_RE = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?/g;
2721
+ var STOP_WORDS = /* @__PURE__ */ new Set(
2722
+ [
2723
+ "a",
2724
+ "an",
2725
+ "and",
2726
+ "are",
2727
+ "be",
2728
+ "by",
2729
+ "does",
2730
+ "for",
2731
+ "from",
2732
+ "how",
2733
+ "in",
2734
+ "is",
2735
+ "it",
2736
+ "my",
2737
+ "of",
2738
+ "on",
2739
+ "or",
2740
+ "the",
2741
+ "this",
2742
+ "to",
2743
+ "what",
2744
+ "where",
2745
+ "why",
2746
+ "with"
2747
+ ]
2748
+ );
2749
+ var CHANGE_RE = /\b(fix|bug|broken|breaks?|change|changed|current|working\s+tree|regression|impact|modified|duplicate|fails?|failure)\b/i;
2750
+ var TEST_RE3 = /\b(test|tests|spec|specs|failing|assert|coverage)\b/i;
2751
+ var ARCH_RE = /\b(architecture|architectural|flow|loading|authentication|authorization|plugin|startup|overview|pipeline|lifecycle|design|works?)\b/i;
2752
+ var EXPLORE_RE = /\b(where|implement|add|feature|introduce|entry\s+point|similar|explore|understand|locate)\b/i;
2753
+ function unique(values, key) {
2754
+ const seen = /* @__PURE__ */ new Set();
2755
+ const out = [];
2756
+ for (const value of values) {
2757
+ const id = key(value);
2758
+ if (seen.has(id)) continue;
2759
+ seen.add(id);
2760
+ out.push(value);
2761
+ }
2762
+ return out;
2763
+ }
2764
+ function clamp2(value, min, max, fallback) {
2765
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
2766
+ return Math.min(max, Math.max(min, Math.floor(value)));
2767
+ }
2768
+ function wordBoundary(name2) {
2769
+ return new RegExp(`(^|[^A-Za-z0-9_$])${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=$|[^A-Za-z0-9_$])`);
2770
+ }
2771
+ function symbolLabel2(symbol) {
2772
+ return `${symbol.kind} ${symbol.signature || symbol.name} \u2014 ${symbol.file}:${symbol.line}`;
2773
+ }
2774
+ function isTestFile(file) {
2775
+ return TEST_PATH_RE2.test(file);
2776
+ }
2777
+ function fileMentionMatches(file, mentions) {
2778
+ const lower = file.toLowerCase();
2779
+ return mentions.some((mention) => {
2780
+ const normalized = mention.replace(/\\/g, "/").toLowerCase();
2781
+ return lower === normalized || lower.endsWith(`/${normalized}`) || lower.includes(normalized);
2782
+ });
2783
+ }
2784
+ function routeTask(task, index) {
2785
+ const text = task.trim();
2786
+ const mentionedFiles = unique(text.match(FILE_TOKEN_RE) ?? [], (value) => value.toLowerCase()).map((value) => value.replace(/\\/g, "/")).filter((value) => !index || index.files.some((file) => fileMentionMatches(file.path, [value])));
2787
+ const exactSymbolNames = index ? unique(
2788
+ index.files.flatMap((file) => file.symbols.map((symbol) => symbol.name)),
2789
+ (name2) => name2
2790
+ ).filter((name2) => wordBoundary(name2).test(text)).sort((a, b) => b.length - a.length || a.localeCompare(b)) : [];
2791
+ const codeTokens = unique(
2792
+ (text.match(CODE_TOKEN_RE) ?? []).map((token) => token.replace(/^[^A-Za-z_$]+|[^A-Za-z0-9_$]+$/g, "")).filter((token) => token.length >= 3 && !STOP_WORDS.has(token.toLowerCase())),
2793
+ (token) => token.toLowerCase()
2794
+ );
2795
+ const queries = unique(
2796
+ [...exactSymbolNames, ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
2797
+ (value) => value.toLowerCase()
2798
+ ).slice(0, 12);
2799
+ const kind = TEST_RE3.test(text) || mentionedFiles.some(isTestFile) ? "test" : CHANGE_RE.test(text) ? "change" : exactSymbolNames.length > 0 ? "symbol" : EXPLORE_RE.test(text) ? "exploration" : ARCH_RE.test(text) ? "architecture" : "ambiguous";
2800
+ const includeChangeContext = kind === "change" || kind === "test";
2801
+ const includeRelationships = kind === "change" || kind === "test" || kind === "symbol";
2802
+ const sources = /* @__PURE__ */ new Set();
2803
+ if (kind === "change" || kind === "test") sources.add("change context");
2804
+ if (kind === "architecture" || kind === "exploration" || kind === "ambiguous") {
2805
+ sources.add("repo map");
2806
+ }
2807
+ if (queries.length > 0) sources.add("ranked search");
2808
+ if (includeRelationships) sources.add("call graph");
2809
+ if (mentionedFiles.length > 0) sources.add("file relevance");
2810
+ if (sources.size === 0) sources.add("repo map");
2811
+ return {
2812
+ kind,
2813
+ queries,
2814
+ mentionedFiles,
2815
+ exactSymbolNames,
2816
+ sources: [...sources],
2817
+ includeChangeContext,
2818
+ includeRelationships
2819
+ };
2820
+ }
2821
+ function routeScore(route, hit) {
2822
+ let score = hit.score * 30;
2823
+ if (route.exactSymbolNames.includes(hit.name)) score += 100;
2824
+ if (fileMentionMatches(hit.file, route.mentionedFiles)) score += 30;
2825
+ if (isTestFile(hit.file) && route.kind !== "test") score -= 10;
2826
+ return score;
2827
+ }
2828
+ function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles, refs) {
2829
+ const hits = [];
2830
+ for (const query of route.queries) {
2831
+ hits.push(...searchSymbols(index, { query }, Math.max(maxSymbols * 3, 20), refs));
2832
+ }
2833
+ const byId = /* @__PURE__ */ new Map();
2834
+ for (const hit of hits) {
2835
+ const exact = route.exactSymbolNames.includes(hit.name);
2836
+ let score = routeScore(route, hit);
2837
+ if (changedIds.has(hit.id)) score += 80;
2838
+ else if (changedFiles.has(hit.file)) score += 55;
2839
+ if (route.kind === "test" && isTestFile(hit.file)) score += 35;
2840
+ const candidate = { symbol: hit, score, provenance: exact ? "exact" : "lexical" };
2841
+ const previous = byId.get(hit.id);
2842
+ if (!previous || candidate.score > previous.score) byId.set(hit.id, candidate);
2843
+ }
2844
+ if (route.includeChangeContext) {
2845
+ for (const file of index.files) {
2846
+ if (!changedFiles.has(file.path)) continue;
2847
+ for (const symbol of file.symbols) {
2848
+ if (byId.has(symbol.id)) continue;
2849
+ byId.set(symbol.id, {
2850
+ symbol,
2851
+ score: 70,
2852
+ provenance: "lexical"
2853
+ });
2854
+ }
2855
+ }
2856
+ }
2857
+ return [...byId.values()].sort(
2858
+ (a, b) => b.score - a.score || Number(b.provenance === "exact") - Number(a.provenance === "exact") || a.symbol.file.localeCompare(b.symbol.file) || a.symbol.line - b.symbol.line || a.symbol.id.localeCompare(b.symbol.id)
2859
+ ).slice(0, maxSymbols);
2860
+ }
2861
+ function edgeText(graph, edge, direction) {
2862
+ const source = edge.sourceId ? graph.symbolsById.get(edge.sourceId) : void 0;
2863
+ const target = graph.symbolsById.get(edge.targetId);
2864
+ const sourceText = source ? `${source.name} (${source.file}:${source.line})` : "module";
2865
+ const targetText = target ? `${target.name} (${target.file}:${target.line})` : edge.name;
2866
+ const arrow = direction === "caller" ? `${sourceText} \u2192 ${targetText}` : `${sourceText} \u2192 ${targetText}`;
2867
+ return `${arrow} at ${edge.callSite.file}:${edge.callSite.line} [${edge.resolution}; ${edge.provenance}]`;
2868
+ }
2869
+ function collectRelationships(graph, primary) {
2870
+ const rows = [];
2871
+ const seen = /* @__PURE__ */ new Set();
2872
+ const add = (edge, direction, priority) => {
2873
+ const key = `${edge.sourceId ?? "module"}:${edge.targetId}:${edge.callSite.file}:${edge.callSite.line}`;
2874
+ if (seen.has(key)) return;
2875
+ seen.add(key);
2876
+ rows.push({
2877
+ key,
2878
+ text: edgeText(graph, edge, direction),
2879
+ resolution: edge.resolution,
2880
+ priority
2881
+ });
2882
+ };
2883
+ for (const candidate of primary) {
2884
+ for (const edge of graph.incoming.get(candidate.symbol.id) ?? []) add(edge, "caller", 100);
2885
+ for (const edge of graph.outgoing.get(candidate.symbol.id) ?? []) add(edge, "callee", 90);
2886
+ }
2887
+ return rows.sort(
2888
+ (a, b) => b.priority - a.priority || a.resolution.localeCompare(b.resolution) || a.text.localeCompare(b.text)
2889
+ );
2890
+ }
2891
+ function addFile(files, path9, score, reason) {
2892
+ const previous = files.get(path9);
2893
+ if (!previous || score > previous.score) files.set(path9, { path: path9, score, reason });
2894
+ }
2895
+ function gatherFiles(index, route, primary, change, maxFiles) {
2896
+ const files = /* @__PURE__ */ new Map();
2897
+ for (const file of index.files) {
2898
+ const base = Math.min(scoreFile(file), 8);
2899
+ if (route.kind === "architecture" || route.kind === "exploration" || route.kind === "ambiguous") {
2900
+ addFile(files, file.path, base, "structural importance");
2901
+ }
2902
+ if (fileMentionMatches(file.path, route.mentionedFiles)) addFile(files, file.path, 120, "task-mentioned file");
2903
+ }
2904
+ for (const candidate of primary) {
2905
+ addFile(files, candidate.symbol.file, candidate.score + 45, "primary symbol");
2906
+ }
2907
+ if (change) {
2908
+ for (const changed of change.changed) addFile(files, changed.symbol.file, 115, "current change");
2909
+ for (const dependent of change.importDependents) addFile(files, dependent.file, 65, "import dependent");
2910
+ for (const test of change.tests) addFile(files, test.file, 75, "likely affected test");
2911
+ for (const pathRow of change.paths) {
2912
+ addFile(files, pathRow.entry.file, 60, "entry path");
2913
+ addFile(files, pathRow.changed.file, 90, "changed path");
2914
+ }
2915
+ }
2916
+ for (const entry of rankRepoMap(index, { topFiles: maxFiles * 2, symbolsPerFile: 8 })) {
2917
+ addFile(files, entry.path, Math.min(35, entry.score + 10), "repo map");
2918
+ }
2919
+ return [...files.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, maxFiles);
2920
+ }
2921
+ function confidenceOf(primary, relationships, change) {
2922
+ const out = { exact: 0, "import-scoped": 0, "name-only": 0 };
2923
+ for (const symbol of primary) if (symbol.provenance === "exact") out.exact++;
2924
+ for (const relation of relationships) out[relation.resolution]++;
2925
+ if (change) {
2926
+ for (const row of [...change.directCallers, ...change.impact]) out[row.resolution]++;
2927
+ for (const dependent of change.importDependents) out[dependent.resolution]++;
2928
+ for (const test of change.tests) out[test.resolution]++;
2929
+ }
2930
+ return out;
2931
+ }
2932
+ function renderRows(result, budgetChars) {
2933
+ const lines = [
2934
+ `Task context \u2014 ${result.route.kind}`,
2935
+ `Task: ${result.task}`,
2936
+ `Sources: ${result.route.sources.join(", ")}`,
2937
+ "",
2938
+ "Primary symbols:"
2939
+ ];
2940
+ if (result.primarySymbols.length === 0) lines.push("- (none found)");
2941
+ const optional = [];
2942
+ for (const candidate2 of result.primarySymbols) {
2943
+ optional.push({
2944
+ priority: 1e3 + candidate2.score,
2945
+ key: `symbol:${candidate2.symbol.id}`,
2946
+ section: "primary",
2947
+ line: `- [${candidate2.provenance}] ${symbolLabel2(candidate2.symbol)}`
2948
+ });
2949
+ }
2950
+ optional.push({ priority: 700, key: "files-header", section: "files", line: "\nRelevant files:" });
2951
+ for (const file of result.relevantFiles) {
2952
+ optional.push({ priority: 500 + file.score, key: `file:${file.path}`, section: "files", line: `- ${file.path} (${file.reason})` });
2953
+ }
2954
+ optional.push({ priority: 400, key: "relationships-header", section: "relationships", line: "\nRelationships:" });
2955
+ for (const relation of result.relationships) {
2956
+ optional.push({ priority: relation.priority, key: `relation:${relation.key}`, section: "relationships", line: `- ${relation.text}` });
2957
+ }
2958
+ if (result.changeContext) {
2959
+ optional.push({ priority: 850, key: "changes-header", section: "changes", line: "\nCurrent changes:" });
2960
+ for (const changed of result.changeContext.changed) {
2961
+ optional.push({
2962
+ priority: 840,
2963
+ key: `changed:${changed.side}:${changed.symbol.id}`,
2964
+ section: "changes",
2965
+ line: `- ${changed.change} ${symbolLabel2(changed.symbol)} [${changed.resolution}]`
2966
+ });
2967
+ }
2968
+ if (result.changeContext.unmapped.length > 0) {
2969
+ optional.push({
2970
+ priority: 830,
2971
+ key: "changes-unmapped",
2972
+ section: "changes",
2973
+ line: `- unmapped: ${result.changeContext.unmapped.map((entry) => entry.file).join(", ")}`
2974
+ });
2975
+ }
2976
+ }
2977
+ if (result.tests.length > 0) {
2978
+ optional.push({ priority: 350, key: "tests-header", section: "tests", line: "\nLikely affected tests:" });
2979
+ for (const test of result.tests) optional.push({ priority: 340, key: `test:${test}`, section: "tests", line: `- ${test}` });
2980
+ }
2981
+ if (result.warnings.length > 0) {
2982
+ optional.push({ priority: 200, key: "warnings-header", section: "warnings", line: "\nWarnings:" });
2983
+ for (const warning of result.warnings) optional.push({ priority: 190, key: `warning:${warning}`, section: "warnings", line: `- ${warning}` });
2984
+ }
2985
+ const ordered = optional.map((item, index) => ({ ...item, index })).sort((a, b) => b.priority - a.priority || a.index - b.index || a.key.localeCompare(b.key));
2986
+ const footerReserve = 100;
2987
+ let truncated = false;
2988
+ const seen = /* @__PURE__ */ new Set();
2989
+ for (const item of ordered) {
2990
+ if (seen.has(item.key)) continue;
2991
+ const next = lines.join("\n") + `
2992
+ ${item.line}`;
2993
+ if (next.length + footerReserve > budgetChars) {
2994
+ truncated = true;
2995
+ continue;
2996
+ }
2997
+ seen.add(item.key);
2998
+ lines.push(item.line);
2999
+ }
3000
+ lines.push(
3001
+ "",
3002
+ `Confidence: exact ${result.confidence.exact}; import-scoped ${result.confidence["import-scoped"]}; name-only ${result.confidence["name-only"]}`
3003
+ );
3004
+ const body = lines.join("\n");
3005
+ const withFooter = (bodyText, wasTruncated) => {
3006
+ let usedChars = bodyText.length + 1;
3007
+ for (let iteration = 0; iteration < 10; iteration++) {
3008
+ const footer2 = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
3009
+ const text2 = `${bodyText}
3010
+ ${footer2}`;
3011
+ const nextUsedChars = text2.length;
3012
+ if (nextUsedChars === usedChars) return { text: text2, usedChars: nextUsedChars };
3013
+ usedChars = nextUsedChars;
3014
+ }
3015
+ const footer = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
3016
+ const text = `${bodyText}
3017
+ ${footer}`;
3018
+ return { text, usedChars: text.length };
3019
+ };
3020
+ const complete = withFooter(body, truncated);
3021
+ if (complete.usedChars <= budgetChars) {
3022
+ return { text: complete.text, usedChars: complete.usedChars, truncated };
3023
+ }
3024
+ truncated = true;
3025
+ const candidate = (prefixLength) => {
3026
+ const prefix = `${body.slice(0, prefixLength).trimEnd()}\u2026`;
3027
+ return withFooter(prefix, true);
3028
+ };
3029
+ let low = 0;
3030
+ let high = body.length;
3031
+ let best = candidate(0);
3032
+ while (low <= high) {
3033
+ const middle = Math.floor((low + high) / 2);
3034
+ const current = candidate(middle);
3035
+ if (current.usedChars <= budgetChars) {
3036
+ best = current;
3037
+ low = middle + 1;
3038
+ } else {
3039
+ high = middle - 1;
3040
+ }
3041
+ }
3042
+ return { text: best.text, usedChars: best.usedChars, truncated };
3043
+ }
3044
+ async function buildTaskContext(index, task, options = {}) {
3045
+ const budgetChars = clamp2(options.budgetChars, 300, 2e4, 5e3);
3046
+ const maxFiles = clamp2(options.maxFiles, 1, 50, 12);
3047
+ const maxSymbols = clamp2(options.maxSymbols, 1, 50, 10);
3048
+ const route = routeTask(task, index);
3049
+ let changeContext = null;
3050
+ const warnings = [];
3051
+ if (route.includeChangeContext) {
3052
+ try {
3053
+ changeContext = await buildChangeContext(index, {
3054
+ kind: "git",
3055
+ root: index.root,
3056
+ baseRef: "HEAD"
3057
+ });
3058
+ warnings.push(...changeContext.warnings);
3059
+ } catch (error) {
3060
+ warnings.push(`change context unavailable: ${error.message ?? String(error)}`);
3061
+ }
3062
+ }
3063
+ const changedIds = new Set((changeContext?.changed ?? []).map((entry) => entry.symbol.id));
3064
+ const changedFiles = new Set((changeContext?.changed ?? []).map((entry) => entry.symbol.file));
3065
+ const primarySymbols = gatherPrimarySymbols(
3066
+ index,
3067
+ route,
3068
+ maxSymbols,
3069
+ changedIds,
3070
+ changedFiles,
3071
+ callerCounts(index)
3072
+ );
3073
+ const graph = route.includeRelationships ? buildReferenceGraph(index) : null;
3074
+ const relationships = graph ? collectRelationships(graph, primarySymbols) : [];
3075
+ if (changeContext) {
3076
+ const existing = new Set(relationships.map((row) => row.key));
3077
+ for (const row of changeContext.directCallers) {
3078
+ const key = `change:${row.symbol.id}:${row.via.callSite.file}:${row.via.callSite.line}`;
3079
+ if (existing.has(key)) continue;
3080
+ existing.add(key);
3081
+ relationships.push({
3082
+ key,
3083
+ text: `${row.symbol.name} (${row.symbol.file}:${row.symbol.line}) \u2192 changed symbol at ${row.via.callSite.file}:${row.via.callSite.line} [${row.resolution}; ${row.via.reason}]`,
3084
+ resolution: row.resolution,
3085
+ priority: 110
3086
+ });
3087
+ }
3088
+ relationships.sort((a, b) => b.priority - a.priority || a.text.localeCompare(b.text));
3089
+ }
3090
+ const relevantFiles = gatherFiles(index, route, primarySymbols, changeContext, maxFiles);
3091
+ const tests = unique(changeContext?.tests.map((test) => test.file) ?? [], (file) => file);
3092
+ const confidence = confidenceOf(primarySymbols, relationships, changeContext);
3093
+ const partial = {
3094
+ root: index.root,
3095
+ task,
3096
+ route,
3097
+ primarySymbols,
3098
+ relevantFiles,
3099
+ relationships,
3100
+ changeContext,
3101
+ tests,
3102
+ confidence,
3103
+ warnings
3104
+ };
3105
+ const rendered = renderRows(partial, budgetChars);
3106
+ return {
3107
+ ...partial,
3108
+ budget: {
3109
+ usedChars: rendered.usedChars,
3110
+ budgetChars,
3111
+ truncated: rendered.truncated
3112
+ }
3113
+ };
3114
+ }
3115
+ function renderTaskContext(result) {
3116
+ const rendered = renderRows(result, result.budget.budgetChars);
3117
+ return rendered.text;
3118
+ }
3119
+
2718
3120
  // src/tools.ts
2719
3121
  import { defineTool } from "@deepseek-ai/dsh-tools";
2720
3122
  import path8 from "path";
@@ -2726,7 +3128,8 @@ var DEFAULTS = {
2726
3128
  mapMaxChars: 3200,
2727
3129
  mapTtlMs: 6e4,
2728
3130
  autoInject: true,
2729
- codeHealth: false
3131
+ codeHealth: false,
3132
+ toolSurface: "full"
2730
3133
  };
2731
3134
  var state = { current: { ...DEFAULTS } };
2732
3135
  function applyConfig(partial) {
@@ -2744,6 +3147,9 @@ function applyConfig(partial) {
2744
3147
  if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
2745
3148
  state.current.mapTtlMs = DEFAULTS.mapTtlMs;
2746
3149
  }
3150
+ if (state.current.toolSurface !== "full" && state.current.toolSurface !== "compact") {
3151
+ state.current.toolSurface = DEFAULTS.toolSurface;
3152
+ }
2747
3153
  }
2748
3154
  function getConfig() {
2749
3155
  return state.current;
@@ -3155,6 +3561,60 @@ var tools = [
3155
3561
  }
3156
3562
  }
3157
3563
  }),
3564
+ defineTool({
3565
+ name: "code_context",
3566
+ description: "Return the smallest useful structural context for a code task. Pass the task in plain language; the deterministic router combines ranked symbols, relevant files, call/import relationships, repository structure, current Git changes, affected tests, provenance, and a hard character budget. Existing specialized tools remain available for focused follow-up queries.",
3567
+ parameters: {
3568
+ task: {
3569
+ type: "string",
3570
+ required: true,
3571
+ description: "The code task or question the agent is currently trying to complete."
3572
+ },
3573
+ budgetChars: {
3574
+ type: "number",
3575
+ description: "Hard output budget in characters (default 5000)."
3576
+ },
3577
+ maxFiles: {
3578
+ type: "number",
3579
+ description: "Maximum relevant files to include (default 12)."
3580
+ },
3581
+ maxSymbols: {
3582
+ type: "number",
3583
+ description: "Maximum primary symbols to include (default 10)."
3584
+ },
3585
+ repoRoot: {
3586
+ type: "string",
3587
+ description: "Optional absolute repo path; defaults to the session workspace root."
3588
+ }
3589
+ },
3590
+ output: {
3591
+ schema: { type: "string" },
3592
+ render: (_args, value) => [{ type: "text", text: value }]
3593
+ },
3594
+ presentCall: (args) => ({
3595
+ card: "generic",
3596
+ title: "Task context",
3597
+ kind: "search",
3598
+ rawInput: { task: args.task, budgetChars: args.budgetChars }
3599
+ }),
3600
+ async execute(args, exec) {
3601
+ try {
3602
+ if (typeof args.task !== "string" || args.task.trim().length === 0) {
3603
+ throw new Error("task must be a non-empty string");
3604
+ }
3605
+ const root = await resolveRoot(args.repoRoot, exec);
3606
+ const index = await getIndex(root);
3607
+ const result = await buildTaskContext(index, args.task, {
3608
+ budgetChars: args.budgetChars,
3609
+ maxFiles: args.maxFiles,
3610
+ maxSymbols: args.maxSymbols
3611
+ });
3612
+ return renderTaskContext(result);
3613
+ } catch (error) {
3614
+ return `code_context: ${error.message ?? String(error)}`;
3615
+ }
3616
+ }
3617
+ }),
3158
3618
  defineTool({
3159
3619
  name: "code_health",
3160
3620
  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).",
@@ -3204,7 +3664,8 @@ var settingsSchema = Schema.object({
3204
3664
  mapMaxChars: Schema.number().default(3200),
3205
3665
  mapTtlMs: Schema.number().default(6e4),
3206
3666
  autoInject: Schema.boolean().default(true),
3207
- codeHealth: Schema.boolean().default(false)
3667
+ codeHealth: Schema.boolean().default(false),
3668
+ toolSurface: Schema.string().default("full")
3208
3669
  });
3209
3670
  function registerSettings(getService, pluginConfig, onResolved) {
3210
3671
  const service = getService("settings");
@@ -3236,7 +3697,8 @@ function apply(ctx, pluginConfig) {
3236
3697
  );
3237
3698
  });
3238
3699
  }
3239
- for (const tool of tools) {
3700
+ const visibleTools = getConfig().toolSurface === "compact" ? tools.filter((tool) => ["code_index", "code_context", "code_health"].includes(tool.name)) : tools;
3701
+ for (const tool of visibleTools) {
3240
3702
  if (tool.name === "code_health" && !getConfig().codeHealth) continue;
3241
3703
  disposers.push(ctx.tools.register(tool));
3242
3704
  console.log(`[dsh-code-index] registered tool: ${tool.name}`);
@@ -3306,6 +3768,7 @@ export {
3306
3768
  buildIndexWithCache,
3307
3769
  buildModuleGraph,
3308
3770
  buildSymbolTable,
3771
+ buildTaskContext,
3309
3772
  callerCounts,
3310
3773
  changeContextInputFromArgs,
3311
3774
  defaultCachePath,
@@ -3326,6 +3789,8 @@ export {
3326
3789
  renderChangeContext,
3327
3790
  renderHit,
3328
3791
  renderRepoMap,
3792
+ renderTaskContext,
3793
+ routeTask,
3329
3794
  saveIndex,
3330
3795
  scanRepo,
3331
3796
  scoreFile,