dsh-code-index 0.6.1 → 0.8.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/CHANGELOG.md +32 -0
- package/README.md +55 -6
- package/README.zh.md +35 -6
- package/dist/client.js +76 -20
- package/dist/client.js.map +1 -1
- package/dist/index.js +802 -107
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/scripts/release-smoke.mjs +67 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/buildIndex.ts
|
|
2
|
-
import { readFile as
|
|
2
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
3
3
|
import path4 from "path";
|
|
4
4
|
|
|
5
5
|
// src/extract.ts
|
|
@@ -800,9 +800,9 @@ function rustBindings(node) {
|
|
|
800
800
|
return name2 ? [{ imported: name2, local: name2, kind: "named" }] : [];
|
|
801
801
|
}
|
|
802
802
|
case "use_as_clause": {
|
|
803
|
-
const
|
|
803
|
+
const path10 = node.childForFieldName("path");
|
|
804
804
|
const alias = node.childForFieldName("alias")?.text.trim();
|
|
805
|
-
const imported =
|
|
805
|
+
const imported = path10 ? rustLeafName(path10) : null;
|
|
806
806
|
return imported && alias ? [{ imported, local: alias, kind: "named" }] : [];
|
|
807
807
|
}
|
|
808
808
|
case "scoped_use_list": {
|
|
@@ -823,8 +823,8 @@ function rustLeafName(node) {
|
|
|
823
823
|
return node.childForFieldName("name")?.text.trim() ?? null;
|
|
824
824
|
}
|
|
825
825
|
function javaImport(node, line) {
|
|
826
|
-
const
|
|
827
|
-
const specifier = dottedToPath(
|
|
826
|
+
const path10 = node.namedChildren.find((child) => child?.type === "scoped_identifier");
|
|
827
|
+
const specifier = dottedToPath(path10?.text);
|
|
828
828
|
if (!specifier) return [];
|
|
829
829
|
const wildcard = node.namedChildren.some((child) => child?.type === "asterisk");
|
|
830
830
|
if (wildcard) {
|
|
@@ -832,7 +832,7 @@ function javaImport(node, line) {
|
|
|
832
832
|
{ specifier, line, kind: "import", bindings: [{ imported: "*", local: "*", kind: "glob" }] }
|
|
833
833
|
];
|
|
834
834
|
}
|
|
835
|
-
const imported =
|
|
835
|
+
const imported = path10?.childForFieldName("name")?.text.trim() ?? lastSlashSegment(specifier);
|
|
836
836
|
const isStatic = /^import\s+static\b/.test(node.text);
|
|
837
837
|
return [
|
|
838
838
|
{
|
|
@@ -1009,8 +1009,10 @@ async function parseFileToSymbols(filePath, repoRoot, code) {
|
|
|
1009
1009
|
}
|
|
1010
1010
|
|
|
1011
1011
|
// src/scan.ts
|
|
1012
|
-
import { readdir, stat } from "fs/promises";
|
|
1012
|
+
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
1013
|
+
import { readFileSync, statSync } from "fs";
|
|
1013
1014
|
import path2 from "path";
|
|
1015
|
+
import ignore from "ignore";
|
|
1014
1016
|
var DEFAULT_EXCLUDED_DIRS = [
|
|
1015
1017
|
"node_modules",
|
|
1016
1018
|
".git",
|
|
@@ -1054,12 +1056,51 @@ var SUPPORTED_EXTS = /* @__PURE__ */ new Set([
|
|
|
1054
1056
|
".inl",
|
|
1055
1057
|
".c"
|
|
1056
1058
|
]);
|
|
1059
|
+
function isGitIgnoredPath(root, relativePath, cache = /* @__PURE__ */ new Map()) {
|
|
1060
|
+
const rel = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1061
|
+
if (!rel) return false;
|
|
1062
|
+
const parentParts = rel.split("/").slice(0, -1);
|
|
1063
|
+
for (let depth = 0; depth <= parentParts.length; depth += 1) {
|
|
1064
|
+
const base = parentParts.slice(0, depth).join("/");
|
|
1065
|
+
const ignoreFile = path2.join(root, base, ".gitignore");
|
|
1066
|
+
let fileStat;
|
|
1067
|
+
try {
|
|
1068
|
+
fileStat = statSync(ignoreFile);
|
|
1069
|
+
} catch {
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const previous = cache.get(ignoreFile);
|
|
1073
|
+
let matcher = previous?.matcher;
|
|
1074
|
+
if (!previous || previous.mtimeMs !== fileStat.mtimeMs || previous.size !== fileStat.size) {
|
|
1075
|
+
try {
|
|
1076
|
+
matcher = ignore().add(readFileSync(ignoreFile, "utf8"));
|
|
1077
|
+
cache.set(ignoreFile, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, matcher });
|
|
1078
|
+
} catch {
|
|
1079
|
+
cache.delete(ignoreFile);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const scoped = base ? path2.posix.relative(base, rel) : rel;
|
|
1084
|
+
if (matcher?.ignores(scoped)) return true;
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1057
1088
|
async function scanRepo(root, options = {}) {
|
|
1058
1089
|
const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
|
|
1059
1090
|
const results = [];
|
|
1060
|
-
const queue = [[root, ""]];
|
|
1091
|
+
const queue = [[root, "", []]];
|
|
1061
1092
|
while (queue.length) {
|
|
1062
|
-
const [absDir, relDir] = queue.pop();
|
|
1093
|
+
const [absDir, relDir, inherited] = queue.pop();
|
|
1094
|
+
let layers = inherited;
|
|
1095
|
+
try {
|
|
1096
|
+
const rules = await readFile2(path2.join(absDir, ".gitignore"), "utf8");
|
|
1097
|
+
layers = [...inherited, { base: relDir, matcher: ignore().add(rules) }];
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
const ignoredByGit = (relativePath) => layers.some(({ base, matcher }) => {
|
|
1101
|
+
const scoped = base ? path2.posix.relative(base, relativePath) : relativePath;
|
|
1102
|
+
return scoped !== ".." && !scoped.startsWith("../") && matcher.ignores(scoped);
|
|
1103
|
+
});
|
|
1063
1104
|
let entries;
|
|
1064
1105
|
try {
|
|
1065
1106
|
entries = await readdir(absDir, { withFileTypes: true });
|
|
@@ -1071,8 +1112,8 @@ async function scanRepo(root, options = {}) {
|
|
|
1071
1112
|
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1072
1113
|
if (entry.isDirectory()) {
|
|
1073
1114
|
if (excluded.has(entry.name)) continue;
|
|
1074
|
-
queue.push([abs, rel]);
|
|
1075
|
-
} else if (entry.isFile() && SUPPORTED_EXTS.has(path2.extname(entry.name).toLowerCase())) {
|
|
1115
|
+
if (!ignoredByGit(rel)) queue.push([abs, rel, layers]);
|
|
1116
|
+
} else if (entry.isFile() && !ignoredByGit(rel) && SUPPORTED_EXTS.has(path2.extname(entry.name).toLowerCase())) {
|
|
1076
1117
|
try {
|
|
1077
1118
|
const st = await stat(abs);
|
|
1078
1119
|
results.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
@@ -1086,7 +1127,7 @@ async function scanRepo(root, options = {}) {
|
|
|
1086
1127
|
}
|
|
1087
1128
|
|
|
1088
1129
|
// src/store.ts
|
|
1089
|
-
import { mkdir, readFile as
|
|
1130
|
+
import { mkdir, readFile as readFile3, rename, rm, writeFile } from "fs/promises";
|
|
1090
1131
|
import path3 from "path";
|
|
1091
1132
|
import { createHash, randomUUID } from "crypto";
|
|
1092
1133
|
|
|
@@ -1114,7 +1155,7 @@ function legacyCachePath(root, cacheDir) {
|
|
|
1114
1155
|
}
|
|
1115
1156
|
async function loadIndex(cachePath) {
|
|
1116
1157
|
try {
|
|
1117
|
-
const raw = await
|
|
1158
|
+
const raw = await readFile3(cachePath, "utf8");
|
|
1118
1159
|
const parsed = JSON.parse(raw);
|
|
1119
1160
|
if (!parsed || typeof parsed.root !== "string" || !Array.isArray(parsed.files)) return null;
|
|
1120
1161
|
if (parsed.schemaVersion !== REPO_INDEX_SCHEMA_VERSION) return null;
|
|
@@ -1143,7 +1184,7 @@ async function saveIndex(cachePath, index) {
|
|
|
1143
1184
|
}
|
|
1144
1185
|
|
|
1145
1186
|
// src/buildIndex.ts
|
|
1146
|
-
async function buildIndex(root, options = {}, previous = null) {
|
|
1187
|
+
async function buildIndex(root, options = {}, previous = null, forcePaths = /* @__PURE__ */ new Set()) {
|
|
1147
1188
|
const scanned = await scanRepo(root, options);
|
|
1148
1189
|
const prevByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
|
|
1149
1190
|
const prevMtime = new Map((previous?.files ?? []).map((f) => [f.path, f.mtimeMs]));
|
|
@@ -1155,7 +1196,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
1155
1196
|
batch.map(async (f) => {
|
|
1156
1197
|
const lang = languageForFile(f.abs);
|
|
1157
1198
|
if (!lang) return null;
|
|
1158
|
-
if (prevMtime.get(f.rel) === f.mtimeMs) {
|
|
1199
|
+
if (prevMtime.get(f.rel) === f.mtimeMs && !forcePaths.has(f.rel) && !forcePaths.has("*")) {
|
|
1159
1200
|
const cached = prevByPath.get(f.rel);
|
|
1160
1201
|
return {
|
|
1161
1202
|
...cached,
|
|
@@ -1165,7 +1206,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
1165
1206
|
}
|
|
1166
1207
|
let code;
|
|
1167
1208
|
try {
|
|
1168
|
-
code = await
|
|
1209
|
+
code = await readFile4(f.abs, "utf8");
|
|
1169
1210
|
} catch {
|
|
1170
1211
|
return null;
|
|
1171
1212
|
}
|
|
@@ -1193,7 +1234,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
1193
1234
|
excludedDirs: [...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]
|
|
1194
1235
|
};
|
|
1195
1236
|
}
|
|
1196
|
-
async function buildIndexWithCache(root, options = {}, cacheDir) {
|
|
1237
|
+
async function buildIndexWithCache(root, options = {}, cacheDir, forcePaths = /* @__PURE__ */ new Set()) {
|
|
1197
1238
|
const cachePath = defaultCachePath(root, cacheDir);
|
|
1198
1239
|
let prev = await loadIndex(cachePath);
|
|
1199
1240
|
let loadedLegacy = false;
|
|
@@ -1205,7 +1246,7 @@ async function buildIndexWithCache(root, options = {}, cacheDir) {
|
|
|
1205
1246
|
}
|
|
1206
1247
|
}
|
|
1207
1248
|
const reusable = prev && cacheKeyForRoot(prev.root) === cacheKeyForRoot(root) ? prev : null;
|
|
1208
|
-
const fresh = await buildIndex(root, options, reusable);
|
|
1249
|
+
const fresh = await buildIndex(root, options, reusable, forcePaths);
|
|
1209
1250
|
if (prev && indexesEqual(prev, fresh)) {
|
|
1210
1251
|
if (loadedLegacy) await saveIndex(cachePath, fresh);
|
|
1211
1252
|
return loadedLegacy ? fresh : prev;
|
|
@@ -1533,8 +1574,8 @@ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile
|
|
|
1533
1574
|
if (!qualified) {
|
|
1534
1575
|
const locals = named.filter((s) => s.file === file.path && lexicallyVisible(s, call));
|
|
1535
1576
|
if (locals.length > 0) {
|
|
1536
|
-
const
|
|
1537
|
-
const owned =
|
|
1577
|
+
const unique2 = locals.length === 1;
|
|
1578
|
+
const owned = unique2 && isStructuralOwner(call, locals[0]);
|
|
1538
1579
|
const provenance = owned && call.fromId !== null ? "structural-owner" : "local-unique";
|
|
1539
1580
|
return toEdges(locals, call, file.path, provenance, owned ? "exact" : "name-only", maxCandidates);
|
|
1540
1581
|
}
|
|
@@ -1618,10 +1659,10 @@ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile
|
|
|
1618
1659
|
return [];
|
|
1619
1660
|
}
|
|
1620
1661
|
function toEdges(candidates, call, filePath, provenance, resolution, maxCandidates) {
|
|
1621
|
-
const
|
|
1622
|
-
const count =
|
|
1662
|
+
const unique2 = dedupeSymbols(candidates);
|
|
1663
|
+
const count = unique2.length;
|
|
1623
1664
|
const cap = maxCandidates > 0 ? Math.floor(maxCandidates) : 0;
|
|
1624
|
-
return
|
|
1665
|
+
return unique2.slice(0, cap).map((target) => ({
|
|
1625
1666
|
sourceId: call.fromId,
|
|
1626
1667
|
targetId: target.id,
|
|
1627
1668
|
callSite: { file: filePath, line: call.line },
|
|
@@ -1701,7 +1742,7 @@ function isSubsequence(query, name2) {
|
|
|
1701
1742
|
}
|
|
1702
1743
|
return false;
|
|
1703
1744
|
}
|
|
1704
|
-
function searchSymbols(index, filter, limit = 50) {
|
|
1745
|
+
function searchSymbols(index, filter, limit = 50, refs = callerCounts(index)) {
|
|
1705
1746
|
const q = (filter.query ?? "").trim();
|
|
1706
1747
|
const filePat = filter.file?.trim().toLowerCase();
|
|
1707
1748
|
const hits = [];
|
|
@@ -1715,7 +1756,6 @@ function searchSymbols(index, filter, limit = 50) {
|
|
|
1715
1756
|
hits.push({ ...sym, score });
|
|
1716
1757
|
}
|
|
1717
1758
|
}
|
|
1718
|
-
const refs = callerCounts(index);
|
|
1719
1759
|
hits.sort((a, b) => {
|
|
1720
1760
|
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
1761
|
return ab;
|
|
@@ -1795,13 +1835,13 @@ function findOrphanModules(index) {
|
|
|
1795
1835
|
index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
|
|
1796
1836
|
);
|
|
1797
1837
|
return nodes.filter(
|
|
1798
|
-
(
|
|
1838
|
+
(path10) => withSymbols.has(path10) && (incoming.get(path10) ?? 0) === 0 && (edges.get(path10)?.size ?? 0) === 0 && !ENTRY_RE.test(path10) && !TEST_RE.test(path10)
|
|
1799
1839
|
);
|
|
1800
1840
|
}
|
|
1801
1841
|
|
|
1802
1842
|
// src/git-diff.ts
|
|
1803
1843
|
import { execFile } from "child_process";
|
|
1804
|
-
import { readFile as
|
|
1844
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1805
1845
|
import path6 from "path";
|
|
1806
1846
|
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
1807
1847
|
var HEADER_PATH_PREFIX_RE = /^[ab]\//;
|
|
@@ -2014,7 +2054,7 @@ async function readWorkingTreeDiff(root, baseRef) {
|
|
|
2014
2054
|
const normalized = rawPath.replace(/\\/g, "/");
|
|
2015
2055
|
if (normalized === ".dsh-code-index" || normalized.startsWith(".dsh-code-index/")) continue;
|
|
2016
2056
|
try {
|
|
2017
|
-
const content = await
|
|
2057
|
+
const content = await readFile5(path6.resolve(root, normalized), "utf8");
|
|
2018
2058
|
const lines = content.length === 0 ? 0 : content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
|
|
2019
2059
|
untrackedFiles.push({ path: normalized, content, lines });
|
|
2020
2060
|
} catch {
|
|
@@ -2715,25 +2755,444 @@ function renderChangeContext(result, maxChars = 12e3) {
|
|
|
2715
2755
|
return out;
|
|
2716
2756
|
}
|
|
2717
2757
|
|
|
2758
|
+
// src/context.ts
|
|
2759
|
+
var TEST_PATH_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/i;
|
|
2760
|
+
var FILE_TOKEN_RE = /[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+/g;
|
|
2761
|
+
var CODE_TOKEN_RE = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?/g;
|
|
2762
|
+
var STOP_WORDS = /* @__PURE__ */ new Set(
|
|
2763
|
+
[
|
|
2764
|
+
"a",
|
|
2765
|
+
"an",
|
|
2766
|
+
"and",
|
|
2767
|
+
"are",
|
|
2768
|
+
"be",
|
|
2769
|
+
"by",
|
|
2770
|
+
"does",
|
|
2771
|
+
"for",
|
|
2772
|
+
"from",
|
|
2773
|
+
"how",
|
|
2774
|
+
"in",
|
|
2775
|
+
"is",
|
|
2776
|
+
"it",
|
|
2777
|
+
"my",
|
|
2778
|
+
"of",
|
|
2779
|
+
"on",
|
|
2780
|
+
"or",
|
|
2781
|
+
"the",
|
|
2782
|
+
"this",
|
|
2783
|
+
"to",
|
|
2784
|
+
"what",
|
|
2785
|
+
"where",
|
|
2786
|
+
"why",
|
|
2787
|
+
"with"
|
|
2788
|
+
]
|
|
2789
|
+
);
|
|
2790
|
+
var CHANGE_RE = /\b(fix|bug|broken|breaks?|change|changed|current|working\s+tree|regression|impact|modified|duplicate|fails?|failure)\b/i;
|
|
2791
|
+
var TEST_RE3 = /\b(test|tests|spec|specs|failing|assert|coverage)\b/i;
|
|
2792
|
+
var ARCH_RE = /\b(architecture|architectural|flow|loading|authentication|authorization|plugin|startup|overview|pipeline|lifecycle|design|works?)\b/i;
|
|
2793
|
+
var EXPLORE_RE = /\b(where|implement|add|feature|introduce|entry\s+point|similar|explore|understand|locate)\b/i;
|
|
2794
|
+
function unique(values, key) {
|
|
2795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2796
|
+
const out = [];
|
|
2797
|
+
for (const value of values) {
|
|
2798
|
+
const id = key(value);
|
|
2799
|
+
if (seen.has(id)) continue;
|
|
2800
|
+
seen.add(id);
|
|
2801
|
+
out.push(value);
|
|
2802
|
+
}
|
|
2803
|
+
return out;
|
|
2804
|
+
}
|
|
2805
|
+
function clamp2(value, min, max, fallback) {
|
|
2806
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
2807
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
2808
|
+
}
|
|
2809
|
+
function wordBoundary(name2) {
|
|
2810
|
+
return new RegExp(`(^|[^A-Za-z0-9_$])${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=$|[^A-Za-z0-9_$])`);
|
|
2811
|
+
}
|
|
2812
|
+
function symbolLabel2(symbol) {
|
|
2813
|
+
return `${symbol.kind} ${symbol.signature || symbol.name} \u2014 ${symbol.file}:${symbol.line}`;
|
|
2814
|
+
}
|
|
2815
|
+
function isTestFile(file) {
|
|
2816
|
+
return TEST_PATH_RE2.test(file);
|
|
2817
|
+
}
|
|
2818
|
+
function fileMentionMatches(file, mentions) {
|
|
2819
|
+
const lower = file.toLowerCase();
|
|
2820
|
+
return mentions.some((mention) => {
|
|
2821
|
+
const normalized = mention.replace(/\\/g, "/").toLowerCase();
|
|
2822
|
+
return lower === normalized || lower.endsWith(`/${normalized}`) || lower.includes(normalized);
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
function routeTask(task, index) {
|
|
2826
|
+
const text = task.trim();
|
|
2827
|
+
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])));
|
|
2828
|
+
const exactSymbolNames = index ? unique(
|
|
2829
|
+
index.files.flatMap((file) => file.symbols.map((symbol) => symbol.name)),
|
|
2830
|
+
(name2) => name2
|
|
2831
|
+
).filter((name2) => wordBoundary(name2).test(text)).sort((a, b) => b.length - a.length || a.localeCompare(b)) : [];
|
|
2832
|
+
const codeTokens = unique(
|
|
2833
|
+
(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())),
|
|
2834
|
+
(token) => token.toLowerCase()
|
|
2835
|
+
);
|
|
2836
|
+
const queries = unique(
|
|
2837
|
+
[...exactSymbolNames, ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
|
|
2838
|
+
(value) => value.toLowerCase()
|
|
2839
|
+
).slice(0, 12);
|
|
2840
|
+
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";
|
|
2841
|
+
const includeChangeContext = kind === "change" || kind === "test";
|
|
2842
|
+
const includeRelationships = kind === "change" || kind === "test" || kind === "symbol";
|
|
2843
|
+
const sources = /* @__PURE__ */ new Set();
|
|
2844
|
+
if (kind === "change" || kind === "test") sources.add("change context");
|
|
2845
|
+
if (kind === "architecture" || kind === "exploration" || kind === "ambiguous") {
|
|
2846
|
+
sources.add("repo map");
|
|
2847
|
+
}
|
|
2848
|
+
if (queries.length > 0) sources.add("ranked search");
|
|
2849
|
+
if (includeRelationships) sources.add("call graph");
|
|
2850
|
+
if (mentionedFiles.length > 0) sources.add("file relevance");
|
|
2851
|
+
if (sources.size === 0) sources.add("repo map");
|
|
2852
|
+
return {
|
|
2853
|
+
kind,
|
|
2854
|
+
queries,
|
|
2855
|
+
mentionedFiles,
|
|
2856
|
+
exactSymbolNames,
|
|
2857
|
+
sources: [...sources],
|
|
2858
|
+
includeChangeContext,
|
|
2859
|
+
includeRelationships
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
function routeScore(route, hit) {
|
|
2863
|
+
let score = hit.score * 30;
|
|
2864
|
+
if (route.exactSymbolNames.includes(hit.name)) score += 100;
|
|
2865
|
+
if (fileMentionMatches(hit.file, route.mentionedFiles)) score += 30;
|
|
2866
|
+
if (isTestFile(hit.file) && route.kind !== "test") score -= 10;
|
|
2867
|
+
return score;
|
|
2868
|
+
}
|
|
2869
|
+
function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles, refs) {
|
|
2870
|
+
const hits = [];
|
|
2871
|
+
for (const query of route.queries) {
|
|
2872
|
+
hits.push(...searchSymbols(index, { query }, Math.max(maxSymbols * 3, 20), refs));
|
|
2873
|
+
}
|
|
2874
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2875
|
+
for (const hit of hits) {
|
|
2876
|
+
const exact = route.exactSymbolNames.includes(hit.name);
|
|
2877
|
+
let score = routeScore(route, hit);
|
|
2878
|
+
if (changedIds.has(hit.id)) score += 80;
|
|
2879
|
+
else if (changedFiles.has(hit.file)) score += 55;
|
|
2880
|
+
if (route.kind === "test" && isTestFile(hit.file)) score += 35;
|
|
2881
|
+
const candidate = { symbol: hit, score, provenance: exact ? "exact" : "lexical" };
|
|
2882
|
+
const previous = byId.get(hit.id);
|
|
2883
|
+
if (!previous || candidate.score > previous.score) byId.set(hit.id, candidate);
|
|
2884
|
+
}
|
|
2885
|
+
if (route.includeChangeContext) {
|
|
2886
|
+
for (const file of index.files) {
|
|
2887
|
+
if (!changedFiles.has(file.path)) continue;
|
|
2888
|
+
for (const symbol of file.symbols) {
|
|
2889
|
+
if (byId.has(symbol.id)) continue;
|
|
2890
|
+
byId.set(symbol.id, {
|
|
2891
|
+
symbol,
|
|
2892
|
+
score: 70,
|
|
2893
|
+
provenance: "lexical"
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return [...byId.values()].sort(
|
|
2899
|
+
(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)
|
|
2900
|
+
).slice(0, maxSymbols);
|
|
2901
|
+
}
|
|
2902
|
+
function edgeText(graph, edge, direction) {
|
|
2903
|
+
const source = edge.sourceId ? graph.symbolsById.get(edge.sourceId) : void 0;
|
|
2904
|
+
const target = graph.symbolsById.get(edge.targetId);
|
|
2905
|
+
const sourceText = source ? `${source.name} (${source.file}:${source.line})` : "module";
|
|
2906
|
+
const targetText = target ? `${target.name} (${target.file}:${target.line})` : edge.name;
|
|
2907
|
+
const arrow = direction === "caller" ? `${sourceText} \u2192 ${targetText}` : `${sourceText} \u2192 ${targetText}`;
|
|
2908
|
+
return `${arrow} at ${edge.callSite.file}:${edge.callSite.line} [${edge.resolution}; ${edge.provenance}]`;
|
|
2909
|
+
}
|
|
2910
|
+
function collectRelationships(graph, primary) {
|
|
2911
|
+
const rows = [];
|
|
2912
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2913
|
+
const add = (edge, direction, priority) => {
|
|
2914
|
+
const key = `${edge.sourceId ?? "module"}:${edge.targetId}:${edge.callSite.file}:${edge.callSite.line}`;
|
|
2915
|
+
if (seen.has(key)) return;
|
|
2916
|
+
seen.add(key);
|
|
2917
|
+
rows.push({
|
|
2918
|
+
key,
|
|
2919
|
+
text: edgeText(graph, edge, direction),
|
|
2920
|
+
resolution: edge.resolution,
|
|
2921
|
+
priority
|
|
2922
|
+
});
|
|
2923
|
+
};
|
|
2924
|
+
for (const candidate of primary) {
|
|
2925
|
+
for (const edge of graph.incoming.get(candidate.symbol.id) ?? []) add(edge, "caller", 100);
|
|
2926
|
+
for (const edge of graph.outgoing.get(candidate.symbol.id) ?? []) add(edge, "callee", 90);
|
|
2927
|
+
}
|
|
2928
|
+
return rows.sort(
|
|
2929
|
+
(a, b) => b.priority - a.priority || a.resolution.localeCompare(b.resolution) || a.text.localeCompare(b.text)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
function addFile(files, path10, score, reason) {
|
|
2933
|
+
const previous = files.get(path10);
|
|
2934
|
+
if (!previous || score > previous.score) files.set(path10, { path: path10, score, reason });
|
|
2935
|
+
}
|
|
2936
|
+
function gatherFiles(index, route, primary, change, maxFiles) {
|
|
2937
|
+
const files = /* @__PURE__ */ new Map();
|
|
2938
|
+
for (const file of index.files) {
|
|
2939
|
+
const base = Math.min(scoreFile(file), 8);
|
|
2940
|
+
if (route.kind === "architecture" || route.kind === "exploration" || route.kind === "ambiguous") {
|
|
2941
|
+
addFile(files, file.path, base, "structural importance");
|
|
2942
|
+
}
|
|
2943
|
+
if (fileMentionMatches(file.path, route.mentionedFiles)) addFile(files, file.path, 120, "task-mentioned file");
|
|
2944
|
+
}
|
|
2945
|
+
for (const candidate of primary) {
|
|
2946
|
+
addFile(files, candidate.symbol.file, candidate.score + 45, "primary symbol");
|
|
2947
|
+
}
|
|
2948
|
+
if (change) {
|
|
2949
|
+
for (const changed of change.changed) addFile(files, changed.symbol.file, 115, "current change");
|
|
2950
|
+
for (const dependent of change.importDependents) addFile(files, dependent.file, 65, "import dependent");
|
|
2951
|
+
for (const test of change.tests) addFile(files, test.file, 75, "likely affected test");
|
|
2952
|
+
for (const pathRow of change.paths) {
|
|
2953
|
+
addFile(files, pathRow.entry.file, 60, "entry path");
|
|
2954
|
+
addFile(files, pathRow.changed.file, 90, "changed path");
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
for (const entry of rankRepoMap(index, { topFiles: maxFiles * 2, symbolsPerFile: 8 })) {
|
|
2958
|
+
addFile(files, entry.path, Math.min(35, entry.score + 10), "repo map");
|
|
2959
|
+
}
|
|
2960
|
+
return [...files.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, maxFiles);
|
|
2961
|
+
}
|
|
2962
|
+
function confidenceOf(primary, relationships, change) {
|
|
2963
|
+
const out = { exact: 0, "import-scoped": 0, "name-only": 0 };
|
|
2964
|
+
for (const symbol of primary) if (symbol.provenance === "exact") out.exact++;
|
|
2965
|
+
for (const relation of relationships) out[relation.resolution]++;
|
|
2966
|
+
if (change) {
|
|
2967
|
+
for (const row of [...change.directCallers, ...change.impact]) out[row.resolution]++;
|
|
2968
|
+
for (const dependent of change.importDependents) out[dependent.resolution]++;
|
|
2969
|
+
for (const test of change.tests) out[test.resolution]++;
|
|
2970
|
+
}
|
|
2971
|
+
return out;
|
|
2972
|
+
}
|
|
2973
|
+
function renderRows(result, budgetChars) {
|
|
2974
|
+
const lines = [
|
|
2975
|
+
`Task context \u2014 ${result.route.kind}`,
|
|
2976
|
+
`Task: ${result.task}`,
|
|
2977
|
+
`Sources: ${result.route.sources.join(", ")}`,
|
|
2978
|
+
"",
|
|
2979
|
+
"Primary symbols:"
|
|
2980
|
+
];
|
|
2981
|
+
if (result.primarySymbols.length === 0) lines.push("- (none found)");
|
|
2982
|
+
const optional = [];
|
|
2983
|
+
for (const candidate2 of result.primarySymbols) {
|
|
2984
|
+
optional.push({
|
|
2985
|
+
priority: 1e3 + candidate2.score,
|
|
2986
|
+
key: `symbol:${candidate2.symbol.id}`,
|
|
2987
|
+
section: "primary",
|
|
2988
|
+
line: `- [${candidate2.provenance}] ${symbolLabel2(candidate2.symbol)}`
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
optional.push({ priority: 700, key: "files-header", section: "files", line: "\nRelevant files:" });
|
|
2992
|
+
for (const file of result.relevantFiles) {
|
|
2993
|
+
optional.push({ priority: 500 + file.score, key: `file:${file.path}`, section: "files", line: `- ${file.path} (${file.reason})` });
|
|
2994
|
+
}
|
|
2995
|
+
optional.push({ priority: 400, key: "relationships-header", section: "relationships", line: "\nRelationships:" });
|
|
2996
|
+
for (const relation of result.relationships) {
|
|
2997
|
+
optional.push({ priority: relation.priority, key: `relation:${relation.key}`, section: "relationships", line: `- ${relation.text}` });
|
|
2998
|
+
}
|
|
2999
|
+
if (result.changeContext) {
|
|
3000
|
+
optional.push({ priority: 850, key: "changes-header", section: "changes", line: "\nCurrent changes:" });
|
|
3001
|
+
for (const changed of result.changeContext.changed) {
|
|
3002
|
+
optional.push({
|
|
3003
|
+
priority: 840,
|
|
3004
|
+
key: `changed:${changed.side}:${changed.symbol.id}`,
|
|
3005
|
+
section: "changes",
|
|
3006
|
+
line: `- ${changed.change} ${symbolLabel2(changed.symbol)} [${changed.resolution}]`
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
if (result.changeContext.unmapped.length > 0) {
|
|
3010
|
+
optional.push({
|
|
3011
|
+
priority: 830,
|
|
3012
|
+
key: "changes-unmapped",
|
|
3013
|
+
section: "changes",
|
|
3014
|
+
line: `- unmapped: ${result.changeContext.unmapped.map((entry) => entry.file).join(", ")}`
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (result.tests.length > 0) {
|
|
3019
|
+
optional.push({ priority: 350, key: "tests-header", section: "tests", line: "\nLikely affected tests:" });
|
|
3020
|
+
for (const test of result.tests) optional.push({ priority: 340, key: `test:${test}`, section: "tests", line: `- ${test}` });
|
|
3021
|
+
}
|
|
3022
|
+
if (result.warnings.length > 0) {
|
|
3023
|
+
optional.push({ priority: 200, key: "warnings-header", section: "warnings", line: "\nWarnings:" });
|
|
3024
|
+
for (const warning of result.warnings) optional.push({ priority: 190, key: `warning:${warning}`, section: "warnings", line: `- ${warning}` });
|
|
3025
|
+
}
|
|
3026
|
+
const ordered = optional.map((item, index) => ({ ...item, index })).sort((a, b) => b.priority - a.priority || a.index - b.index || a.key.localeCompare(b.key));
|
|
3027
|
+
const footerReserve = 100;
|
|
3028
|
+
let truncated = false;
|
|
3029
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3030
|
+
for (const item of ordered) {
|
|
3031
|
+
if (seen.has(item.key)) continue;
|
|
3032
|
+
const next = lines.join("\n") + `
|
|
3033
|
+
${item.line}`;
|
|
3034
|
+
if (next.length + footerReserve > budgetChars) {
|
|
3035
|
+
truncated = true;
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
seen.add(item.key);
|
|
3039
|
+
lines.push(item.line);
|
|
3040
|
+
}
|
|
3041
|
+
lines.push(
|
|
3042
|
+
"",
|
|
3043
|
+
`Confidence: exact ${result.confidence.exact}; import-scoped ${result.confidence["import-scoped"]}; name-only ${result.confidence["name-only"]}`
|
|
3044
|
+
);
|
|
3045
|
+
const body = lines.join("\n");
|
|
3046
|
+
const withFooter = (bodyText, wasTruncated) => {
|
|
3047
|
+
let usedChars = bodyText.length + 1;
|
|
3048
|
+
for (let iteration = 0; iteration < 10; iteration++) {
|
|
3049
|
+
const footer2 = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
|
|
3050
|
+
const text2 = `${bodyText}
|
|
3051
|
+
${footer2}`;
|
|
3052
|
+
const nextUsedChars = text2.length;
|
|
3053
|
+
if (nextUsedChars === usedChars) return { text: text2, usedChars: nextUsedChars };
|
|
3054
|
+
usedChars = nextUsedChars;
|
|
3055
|
+
}
|
|
3056
|
+
const footer = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
|
|
3057
|
+
const text = `${bodyText}
|
|
3058
|
+
${footer}`;
|
|
3059
|
+
return { text, usedChars: text.length };
|
|
3060
|
+
};
|
|
3061
|
+
const complete = withFooter(body, truncated);
|
|
3062
|
+
if (complete.usedChars <= budgetChars) {
|
|
3063
|
+
return { text: complete.text, usedChars: complete.usedChars, truncated };
|
|
3064
|
+
}
|
|
3065
|
+
truncated = true;
|
|
3066
|
+
const candidate = (prefixLength) => {
|
|
3067
|
+
const prefix = `${body.slice(0, prefixLength).trimEnd()}\u2026`;
|
|
3068
|
+
return withFooter(prefix, true);
|
|
3069
|
+
};
|
|
3070
|
+
let low = 0;
|
|
3071
|
+
let high = body.length;
|
|
3072
|
+
let best = candidate(0);
|
|
3073
|
+
while (low <= high) {
|
|
3074
|
+
const middle = Math.floor((low + high) / 2);
|
|
3075
|
+
const current = candidate(middle);
|
|
3076
|
+
if (current.usedChars <= budgetChars) {
|
|
3077
|
+
best = current;
|
|
3078
|
+
low = middle + 1;
|
|
3079
|
+
} else {
|
|
3080
|
+
high = middle - 1;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return { text: best.text, usedChars: best.usedChars, truncated };
|
|
3084
|
+
}
|
|
3085
|
+
async function buildTaskContext(index, task, options = {}) {
|
|
3086
|
+
const budgetChars = clamp2(options.budgetChars, 300, 2e4, 5e3);
|
|
3087
|
+
const maxFiles = clamp2(options.maxFiles, 1, 50, 12);
|
|
3088
|
+
const maxSymbols = clamp2(options.maxSymbols, 1, 50, 10);
|
|
3089
|
+
const route = routeTask(task, index);
|
|
3090
|
+
let changeContext = null;
|
|
3091
|
+
const warnings = [];
|
|
3092
|
+
if (route.includeChangeContext) {
|
|
3093
|
+
try {
|
|
3094
|
+
changeContext = await buildChangeContext(index, {
|
|
3095
|
+
kind: "git",
|
|
3096
|
+
root: index.root,
|
|
3097
|
+
baseRef: "HEAD"
|
|
3098
|
+
});
|
|
3099
|
+
warnings.push(...changeContext.warnings);
|
|
3100
|
+
} catch (error) {
|
|
3101
|
+
warnings.push(`change context unavailable: ${error.message ?? String(error)}`);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
const changedIds = new Set((changeContext?.changed ?? []).map((entry) => entry.symbol.id));
|
|
3105
|
+
const changedFiles = new Set((changeContext?.changed ?? []).map((entry) => entry.symbol.file));
|
|
3106
|
+
const primarySymbols = gatherPrimarySymbols(
|
|
3107
|
+
index,
|
|
3108
|
+
route,
|
|
3109
|
+
maxSymbols,
|
|
3110
|
+
changedIds,
|
|
3111
|
+
changedFiles,
|
|
3112
|
+
callerCounts(index)
|
|
3113
|
+
);
|
|
3114
|
+
const graph = route.includeRelationships ? buildReferenceGraph(index) : null;
|
|
3115
|
+
const relationships = graph ? collectRelationships(graph, primarySymbols) : [];
|
|
3116
|
+
if (changeContext) {
|
|
3117
|
+
const existing = new Set(relationships.map((row) => row.key));
|
|
3118
|
+
for (const row of changeContext.directCallers) {
|
|
3119
|
+
const key = `change:${row.symbol.id}:${row.via.callSite.file}:${row.via.callSite.line}`;
|
|
3120
|
+
if (existing.has(key)) continue;
|
|
3121
|
+
existing.add(key);
|
|
3122
|
+
relationships.push({
|
|
3123
|
+
key,
|
|
3124
|
+
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}]`,
|
|
3125
|
+
resolution: row.resolution,
|
|
3126
|
+
priority: 110
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
relationships.sort((a, b) => b.priority - a.priority || a.text.localeCompare(b.text));
|
|
3130
|
+
}
|
|
3131
|
+
const relevantFiles = gatherFiles(index, route, primarySymbols, changeContext, maxFiles);
|
|
3132
|
+
const tests = unique(changeContext?.tests.map((test) => test.file) ?? [], (file) => file);
|
|
3133
|
+
const confidence = confidenceOf(primarySymbols, relationships, changeContext);
|
|
3134
|
+
const partial = {
|
|
3135
|
+
root: index.root,
|
|
3136
|
+
task,
|
|
3137
|
+
route,
|
|
3138
|
+
primarySymbols,
|
|
3139
|
+
relevantFiles,
|
|
3140
|
+
relationships,
|
|
3141
|
+
changeContext,
|
|
3142
|
+
tests,
|
|
3143
|
+
confidence,
|
|
3144
|
+
warnings
|
|
3145
|
+
};
|
|
3146
|
+
const rendered = renderRows(partial, budgetChars);
|
|
3147
|
+
return {
|
|
3148
|
+
...partial,
|
|
3149
|
+
budget: {
|
|
3150
|
+
usedChars: rendered.usedChars,
|
|
3151
|
+
budgetChars,
|
|
3152
|
+
truncated: rendered.truncated
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
function renderTaskContext(result) {
|
|
3157
|
+
const rendered = renderRows(result, result.budget.budgetChars);
|
|
3158
|
+
return rendered.text;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
2718
3161
|
// src/tools.ts
|
|
2719
3162
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2720
|
-
import
|
|
3163
|
+
import path9 from "path";
|
|
2721
3164
|
|
|
2722
3165
|
// src/config.ts
|
|
3166
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
3167
|
+
var Config = Schema.object({
|
|
3168
|
+
excludeDirs: Schema.array(Schema.string()).default([]).volatile(),
|
|
3169
|
+
mapTopFiles: Schema.number().default(24).volatile(),
|
|
3170
|
+
mapMaxChars: Schema.number().default(3200).volatile(),
|
|
3171
|
+
mapTtlMs: Schema.number().default(6e4).volatile(),
|
|
3172
|
+
autoInject: Schema.boolean().default(true).volatile(),
|
|
3173
|
+
codeHealth: Schema.boolean().default(false).volatile(),
|
|
3174
|
+
toolSurface: Schema.string().default("full").volatile(),
|
|
3175
|
+
externalWatch: Schema.boolean().default(true).volatile(),
|
|
3176
|
+
watchDebounceMs: Schema.number().default(120).volatile()
|
|
3177
|
+
});
|
|
2723
3178
|
var DEFAULTS = {
|
|
2724
3179
|
excludeDirs: [],
|
|
2725
3180
|
mapTopFiles: 24,
|
|
2726
3181
|
mapMaxChars: 3200,
|
|
2727
3182
|
mapTtlMs: 6e4,
|
|
2728
3183
|
autoInject: true,
|
|
2729
|
-
codeHealth: false
|
|
3184
|
+
codeHealth: false,
|
|
3185
|
+
toolSurface: "full",
|
|
3186
|
+
externalWatch: true,
|
|
3187
|
+
watchDebounceMs: 120
|
|
2730
3188
|
};
|
|
2731
3189
|
var state = { current: { ...DEFAULTS } };
|
|
2732
3190
|
function applyConfig(partial) {
|
|
3191
|
+
const resolved = partial ? Object.fromEntries(Object.entries(partial).map(([key, value]) => [key, unwrapVolatile(value)])) : void 0;
|
|
2733
3192
|
state.current = {
|
|
2734
3193
|
...DEFAULTS,
|
|
2735
|
-
...
|
|
2736
|
-
excludeDirs: [...DEFAULTS.excludeDirs, ...
|
|
3194
|
+
...resolved ?? {},
|
|
3195
|
+
excludeDirs: [...DEFAULTS.excludeDirs, ...resolved?.excludeDirs ?? []]
|
|
2737
3196
|
};
|
|
2738
3197
|
if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {
|
|
2739
3198
|
state.current.mapTopFiles = DEFAULTS.mapTopFiles;
|
|
@@ -2744,6 +3203,19 @@ function applyConfig(partial) {
|
|
|
2744
3203
|
if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
|
|
2745
3204
|
state.current.mapTtlMs = DEFAULTS.mapTtlMs;
|
|
2746
3205
|
}
|
|
3206
|
+
if (state.current.toolSurface !== "full" && state.current.toolSurface !== "compact") {
|
|
3207
|
+
state.current.toolSurface = DEFAULTS.toolSurface;
|
|
3208
|
+
}
|
|
3209
|
+
if (typeof state.current.externalWatch !== "boolean") state.current.externalWatch = DEFAULTS.externalWatch;
|
|
3210
|
+
if (!Number.isFinite(state.current.watchDebounceMs) || state.current.watchDebounceMs < 20) {
|
|
3211
|
+
state.current.watchDebounceMs = DEFAULTS.watchDebounceMs;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
function unwrapVolatile(value) {
|
|
3215
|
+
if (typeof value !== "object" || value === null || !("get" in value) || typeof value.get !== "function") {
|
|
3216
|
+
return value;
|
|
3217
|
+
}
|
|
3218
|
+
return value.get();
|
|
2747
3219
|
}
|
|
2748
3220
|
function getConfig() {
|
|
2749
3221
|
return state.current;
|
|
@@ -2752,67 +3224,192 @@ function indexOptions() {
|
|
|
2752
3224
|
return { excludeDirs: state.current.excludeDirs };
|
|
2753
3225
|
}
|
|
2754
3226
|
|
|
2755
|
-
// src/
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
3227
|
+
// src/repo-context.ts
|
|
3228
|
+
import { realpath } from "fs/promises";
|
|
3229
|
+
import path8 from "path";
|
|
3230
|
+
import chokidar from "chokidar";
|
|
3231
|
+
function ignoredRelativePath(rel, options) {
|
|
3232
|
+
const components = rel.split(/[\\/]+/).filter(Boolean);
|
|
3233
|
+
const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
|
|
3234
|
+
return components.some((component) => excluded.has(component));
|
|
3235
|
+
}
|
|
3236
|
+
async function canonicalRepoRoot(root) {
|
|
3237
|
+
const resolved = await realpath(path8.resolve(root));
|
|
3238
|
+
return path8.normalize(resolved);
|
|
3239
|
+
}
|
|
3240
|
+
var RepoContextManager = class {
|
|
3241
|
+
contexts = /* @__PURE__ */ new Map();
|
|
3242
|
+
options;
|
|
3243
|
+
disposed = false;
|
|
3244
|
+
constructor(options) {
|
|
3245
|
+
this.options = {
|
|
3246
|
+
indexOptions: options.indexOptions,
|
|
3247
|
+
watch: options.watch ?? true,
|
|
3248
|
+
debounceMs: Math.max(20, options.debounceMs ?? 120),
|
|
3249
|
+
maxContexts: Math.max(1, options.maxContexts ?? 4),
|
|
3250
|
+
onRefresh: options.onRefresh
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
async get(root, force = false) {
|
|
3254
|
+
if (this.disposed) throw new Error("repo context manager is disposed");
|
|
3255
|
+
const canonical = await canonicalRepoRoot(root);
|
|
3256
|
+
const key = cacheKeyForRoot(canonical);
|
|
3257
|
+
let context = this.contexts.get(key);
|
|
3258
|
+
if (!context) {
|
|
3259
|
+
context = {
|
|
3260
|
+
root: canonical,
|
|
3261
|
+
index: null,
|
|
3262
|
+
lastRefreshAt: 0,
|
|
3263
|
+
dirtyFiles: /* @__PURE__ */ new Set(),
|
|
3264
|
+
ignoreMatcherCache: /* @__PURE__ */ new Map(),
|
|
3265
|
+
watcher: null,
|
|
3266
|
+
debounceTimer: null,
|
|
3267
|
+
refresh: null,
|
|
3268
|
+
disposed: false,
|
|
3269
|
+
generation: 0,
|
|
3270
|
+
lastUsedAt: Date.now()
|
|
3271
|
+
};
|
|
3272
|
+
this.contexts.set(key, context);
|
|
3273
|
+
this.attachWatcher(context);
|
|
3274
|
+
this.evictInactive();
|
|
3275
|
+
} else {
|
|
3276
|
+
this.contexts.delete(key);
|
|
3277
|
+
this.contexts.set(key, context);
|
|
3278
|
+
context.lastUsedAt = Date.now();
|
|
3279
|
+
}
|
|
3280
|
+
let index = await this.refresh(context, force);
|
|
3281
|
+
while (!context.disposed && context.dirtyFiles.size > 0) {
|
|
3282
|
+
if (context.refresh) await context.refresh;
|
|
3283
|
+
else index = await this.refresh(context);
|
|
3284
|
+
}
|
|
3285
|
+
return index;
|
|
3286
|
+
}
|
|
3287
|
+
getContext(root) {
|
|
3288
|
+
const key = cacheKeyForRoot(path8.resolve(root));
|
|
3289
|
+
return this.contexts.get(key);
|
|
3290
|
+
}
|
|
3291
|
+
async dispose() {
|
|
3292
|
+
if (this.disposed) return;
|
|
3293
|
+
this.disposed = true;
|
|
3294
|
+
const contexts = [...this.contexts.values()];
|
|
3295
|
+
this.contexts.clear();
|
|
3296
|
+
await Promise.all(contexts.map((context) => this.disposeContext(context)));
|
|
3297
|
+
}
|
|
3298
|
+
reconfigure(options) {
|
|
3299
|
+
if (options.debounceMs !== void 0) this.options.debounceMs = Math.max(20, options.debounceMs);
|
|
3300
|
+
if (options.watch === void 0 || options.watch === this.options.watch) return;
|
|
3301
|
+
this.options.watch = options.watch;
|
|
3302
|
+
for (const context of this.contexts.values()) {
|
|
3303
|
+
if (options.watch) this.attachWatcher(context);
|
|
3304
|
+
else {
|
|
3305
|
+
if (context.debounceTimer) clearTimeout(context.debounceTimer);
|
|
3306
|
+
context.debounceTimer = null;
|
|
3307
|
+
const watcher = context.watcher;
|
|
3308
|
+
context.watcher = null;
|
|
3309
|
+
void watcher?.close();
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
async refresh(context, force = false) {
|
|
3314
|
+
if (context.refresh) {
|
|
3315
|
+
const index = await context.refresh;
|
|
3316
|
+
return force ? this.refresh(context, true) : index;
|
|
3317
|
+
}
|
|
3318
|
+
const generation = context.generation;
|
|
3319
|
+
const dirty = force ? /* @__PURE__ */ new Set(["*"]) : new Set(context.dirtyFiles);
|
|
3320
|
+
context.dirtyFiles.clear();
|
|
3321
|
+
const promise = buildIndexWithCache(context.root, this.options.indexOptions(), void 0, dirty).then((index) => {
|
|
3322
|
+
if (!context.disposed && context.generation === generation) {
|
|
3323
|
+
context.index = index;
|
|
3324
|
+
context.lastRefreshAt = Date.now();
|
|
3325
|
+
this.options.onRefresh?.(context.root, index);
|
|
2768
3326
|
}
|
|
2769
3327
|
return index;
|
|
2770
3328
|
}).finally(() => {
|
|
2771
|
-
if (
|
|
3329
|
+
if (context.refresh === promise) context.refresh = null;
|
|
3330
|
+
if (!context.disposed && context.dirtyFiles.size > 0) this.scheduleRefresh(context);
|
|
2772
3331
|
});
|
|
2773
|
-
|
|
3332
|
+
context.refresh = promise;
|
|
2774
3333
|
return promise;
|
|
2775
3334
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
const
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
3335
|
+
attachWatcher(context) {
|
|
3336
|
+
if (!this.options.watch) return;
|
|
3337
|
+
const options = this.options.indexOptions();
|
|
3338
|
+
const ignored = (absolute) => {
|
|
3339
|
+
const rel = path8.relative(context.root, absolute);
|
|
3340
|
+
return rel === ".." || rel.startsWith(`..${path8.sep}`) || path8.isAbsolute(rel) || ignoredRelativePath(rel, options) || isGitIgnoredPath(context.root, rel, context.ignoreMatcherCache);
|
|
3341
|
+
};
|
|
3342
|
+
const watcher = chokidar.watch(context.root, {
|
|
3343
|
+
ignored,
|
|
3344
|
+
ignoreInitial: true,
|
|
3345
|
+
persistent: true,
|
|
3346
|
+
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 20 },
|
|
3347
|
+
followSymlinks: false
|
|
3348
|
+
});
|
|
3349
|
+
context.watcher = watcher;
|
|
3350
|
+
watcher.on("all", (event, absolute) => {
|
|
3351
|
+
if (context.disposed || !["add", "change", "unlink"].includes(event)) return;
|
|
3352
|
+
const rel = path8.relative(context.root, absolute).split(path8.sep).join("/");
|
|
3353
|
+
if (!rel || ignoredRelativePath(rel, this.options.indexOptions()) || isGitIgnoredPath(context.root, rel, context.ignoreMatcherCache)) return;
|
|
3354
|
+
if (!SUPPORTED_EXTS.has(path8.extname(rel).toLowerCase())) return;
|
|
3355
|
+
context.dirtyFiles.add(rel);
|
|
3356
|
+
this.scheduleRefresh(context);
|
|
3357
|
+
});
|
|
3358
|
+
watcher.on("error", (error) => {
|
|
3359
|
+
if (!context.disposed) console.warn(`[dsh-code-index] watcher failed for ${context.root}: ${String(error)}`);
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
scheduleRefresh(context) {
|
|
3363
|
+
if (context.disposed || this.disposed) return;
|
|
3364
|
+
if (context.debounceTimer) clearTimeout(context.debounceTimer);
|
|
3365
|
+
context.debounceTimer = setTimeout(() => {
|
|
3366
|
+
context.debounceTimer = null;
|
|
3367
|
+
void this.refresh(context).catch((error) => {
|
|
3368
|
+
if (!context.disposed) console.warn(`[dsh-code-index] refresh failed for ${context.root}: ${String(error)}`);
|
|
3369
|
+
});
|
|
3370
|
+
}, this.options.debounceMs);
|
|
3371
|
+
}
|
|
3372
|
+
evictInactive() {
|
|
3373
|
+
while (this.contexts.size > this.options.maxContexts) {
|
|
3374
|
+
const oldest = this.contexts.entries().next().value;
|
|
3375
|
+
if (!oldest) return;
|
|
3376
|
+
this.contexts.delete(oldest[0]);
|
|
3377
|
+
void this.disposeContext(oldest[1]);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
async disposeContext(context) {
|
|
3381
|
+
context.disposed = true;
|
|
3382
|
+
context.generation++;
|
|
3383
|
+
if (context.debounceTimer) clearTimeout(context.debounceTimer);
|
|
3384
|
+
context.debounceTimer = null;
|
|
3385
|
+
const watcher = context.watcher;
|
|
3386
|
+
context.watcher = null;
|
|
3387
|
+
if (watcher) await watcher.close();
|
|
3388
|
+
await context.refresh?.catch(() => void 0);
|
|
3389
|
+
context.dirtyFiles.clear();
|
|
3390
|
+
context.ignoreMatcherCache.clear();
|
|
3391
|
+
context.index = null;
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
|
|
3395
|
+
// src/tools.ts
|
|
3396
|
+
var activeRepoContexts = null;
|
|
3397
|
+
function activateRepoContextManager(manager) {
|
|
3398
|
+
activeRepoContexts = manager;
|
|
3399
|
+
}
|
|
3400
|
+
function clearRepoContextManager(manager) {
|
|
3401
|
+
if (activeRepoContexts === manager) activeRepoContexts = null;
|
|
2803
3402
|
}
|
|
2804
|
-
var indexCache = createIndexCache((root) => buildIndexWithCache(root, indexOptions()));
|
|
2805
3403
|
function getIndex(root, force = false) {
|
|
2806
|
-
return
|
|
3404
|
+
return activeRepoContexts ? activeRepoContexts.get(root, force) : buildIndexWithCache(root, indexOptions(), void 0, force ? /* @__PURE__ */ new Set(["*"]) : void 0);
|
|
2807
3405
|
}
|
|
2808
3406
|
function invalidateIndexCache() {
|
|
2809
|
-
indexCache.invalidate();
|
|
2810
3407
|
}
|
|
2811
3408
|
async function resolveRoot(arg, exec) {
|
|
2812
|
-
const cwd = exec.agent?.session?.header?.cwd;
|
|
3409
|
+
const cwd = exec.agent?.session?.header?.cwd ?? exec.session?.header?.cwd ?? exec.cwd;
|
|
2813
3410
|
const base = arg ?? cwd ?? process.cwd();
|
|
2814
3411
|
const root = await findRepoRoot(base);
|
|
2815
|
-
if (!root) throw new Error(`no git repository found from ${
|
|
3412
|
+
if (!root) throw new Error(`no git repository found from ${path9.resolve(base)}`);
|
|
2816
3413
|
return root;
|
|
2817
3414
|
}
|
|
2818
3415
|
function hitsToSearchMeta(hits) {
|
|
@@ -2827,7 +3424,7 @@ function hitsToSearchMeta(hits) {
|
|
|
2827
3424
|
}
|
|
2828
3425
|
return {
|
|
2829
3426
|
shape: "matches",
|
|
2830
|
-
files: [...byFile.entries()].map(([
|
|
3427
|
+
files: [...byFile.entries()].map(([path10, matches]) => ({ path: path10, matches })),
|
|
2831
3428
|
truncated: false,
|
|
2832
3429
|
total: hits.length
|
|
2833
3430
|
};
|
|
@@ -3155,6 +3752,60 @@ var tools = [
|
|
|
3155
3752
|
}
|
|
3156
3753
|
}
|
|
3157
3754
|
}),
|
|
3755
|
+
defineTool({
|
|
3756
|
+
name: "code_context",
|
|
3757
|
+
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.",
|
|
3758
|
+
parameters: {
|
|
3759
|
+
task: {
|
|
3760
|
+
type: "string",
|
|
3761
|
+
required: true,
|
|
3762
|
+
description: "The code task or question the agent is currently trying to complete."
|
|
3763
|
+
},
|
|
3764
|
+
budgetChars: {
|
|
3765
|
+
type: "number",
|
|
3766
|
+
description: "Hard output budget in characters (default 5000)."
|
|
3767
|
+
},
|
|
3768
|
+
maxFiles: {
|
|
3769
|
+
type: "number",
|
|
3770
|
+
description: "Maximum relevant files to include (default 12)."
|
|
3771
|
+
},
|
|
3772
|
+
maxSymbols: {
|
|
3773
|
+
type: "number",
|
|
3774
|
+
description: "Maximum primary symbols to include (default 10)."
|
|
3775
|
+
},
|
|
3776
|
+
repoRoot: {
|
|
3777
|
+
type: "string",
|
|
3778
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
3779
|
+
}
|
|
3780
|
+
},
|
|
3781
|
+
output: {
|
|
3782
|
+
schema: { type: "string" },
|
|
3783
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
3784
|
+
},
|
|
3785
|
+
presentCall: (args) => ({
|
|
3786
|
+
card: "generic",
|
|
3787
|
+
title: "Task context",
|
|
3788
|
+
kind: "search",
|
|
3789
|
+
rawInput: { task: args.task, budgetChars: args.budgetChars }
|
|
3790
|
+
}),
|
|
3791
|
+
async execute(args, exec) {
|
|
3792
|
+
try {
|
|
3793
|
+
if (typeof args.task !== "string" || args.task.trim().length === 0) {
|
|
3794
|
+
throw new Error("task must be a non-empty string");
|
|
3795
|
+
}
|
|
3796
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
3797
|
+
const index = await getIndex(root);
|
|
3798
|
+
const result = await buildTaskContext(index, args.task, {
|
|
3799
|
+
budgetChars: args.budgetChars,
|
|
3800
|
+
maxFiles: args.maxFiles,
|
|
3801
|
+
maxSymbols: args.maxSymbols
|
|
3802
|
+
});
|
|
3803
|
+
return renderTaskContext(result);
|
|
3804
|
+
} catch (error) {
|
|
3805
|
+
return `code_context: ${error.message ?? String(error)}`;
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
}),
|
|
3158
3809
|
defineTool({
|
|
3159
3810
|
name: "code_health",
|
|
3160
3811
|
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).",
|
|
@@ -3196,24 +3847,32 @@ var tools = [
|
|
|
3196
3847
|
];
|
|
3197
3848
|
|
|
3198
3849
|
// src/settings.ts
|
|
3199
|
-
import
|
|
3850
|
+
import Schema2 from "@deepseek-ai/schemastery";
|
|
3200
3851
|
var SETTINGS_NAMESPACE = "code-index";
|
|
3201
|
-
var settingsSchema =
|
|
3202
|
-
excludeDirs:
|
|
3203
|
-
mapTopFiles:
|
|
3204
|
-
mapMaxChars:
|
|
3205
|
-
mapTtlMs:
|
|
3206
|
-
autoInject:
|
|
3207
|
-
codeHealth:
|
|
3852
|
+
var settingsSchema = Schema2.object({
|
|
3853
|
+
excludeDirs: Schema2.array(Schema2.string()).default([]),
|
|
3854
|
+
mapTopFiles: Schema2.number().default(24),
|
|
3855
|
+
mapMaxChars: Schema2.number().default(3200),
|
|
3856
|
+
mapTtlMs: Schema2.number().default(6e4),
|
|
3857
|
+
autoInject: Schema2.boolean().default(true),
|
|
3858
|
+
codeHealth: Schema2.boolean().default(false),
|
|
3859
|
+
toolSurface: Schema2.string().default("full"),
|
|
3860
|
+
externalWatch: Schema2.boolean().default(true),
|
|
3861
|
+
watchDebounceMs: Schema2.number().default(120)
|
|
3208
3862
|
});
|
|
3209
|
-
function registerSettings(getService, pluginConfig, onResolved) {
|
|
3863
|
+
function registerSettings(getService, pluginConfig, onResolved, fiber) {
|
|
3210
3864
|
const service = getService("settings");
|
|
3211
|
-
if (!service) return;
|
|
3865
|
+
if (!service) return void 0;
|
|
3866
|
+
if (service.configure) {
|
|
3867
|
+
const dispose = service.configure({ auto: false }, fiber);
|
|
3868
|
+
onResolved(pluginConfig ?? {});
|
|
3869
|
+
return typeof dispose === "function" ? dispose : void 0;
|
|
3870
|
+
}
|
|
3212
3871
|
const scope = service.register(SETTINGS_NAMESPACE, settingsSchema, {
|
|
3213
3872
|
base: pluginConfig ?? {}
|
|
3214
3873
|
});
|
|
3215
3874
|
onResolved(scope.get());
|
|
3216
|
-
scope.watch((next) => {
|
|
3875
|
+
return scope.watch((next) => {
|
|
3217
3876
|
onResolved(next);
|
|
3218
3877
|
});
|
|
3219
3878
|
}
|
|
@@ -3226,29 +3885,51 @@ function apply(ctx, pluginConfig) {
|
|
|
3226
3885
|
invalidateIndexCache();
|
|
3227
3886
|
ctx.effect(() => {
|
|
3228
3887
|
const disposers = [];
|
|
3888
|
+
const repoContexts = new RepoContextManager({
|
|
3889
|
+
indexOptions,
|
|
3890
|
+
watch: getConfig().externalWatch,
|
|
3891
|
+
debounceMs: getConfig().watchDebounceMs
|
|
3892
|
+
});
|
|
3893
|
+
activateRepoContextManager(repoContexts);
|
|
3894
|
+
const updateConfig = (next) => {
|
|
3895
|
+
applyConfig(next);
|
|
3896
|
+
repoContexts.reconfigure({ watch: getConfig().externalWatch, debounceMs: getConfig().watchDebounceMs });
|
|
3897
|
+
};
|
|
3229
3898
|
console.log("[dsh-code-index] plugin loaded");
|
|
3230
3899
|
if (ctx.inject) {
|
|
3231
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
3232
|
-
registerSettings(
|
|
3900
|
+
const injected = ctx.inject(["settings"], (settingsCtx) => {
|
|
3901
|
+
const dispose = registerSettings(
|
|
3233
3902
|
(name2) => settingsCtx.get(name2),
|
|
3234
3903
|
pluginConfig,
|
|
3235
|
-
(resolved) =>
|
|
3904
|
+
(resolved) => updateConfig(resolved),
|
|
3905
|
+
settingsCtx.fiber
|
|
3236
3906
|
);
|
|
3907
|
+
if (dispose) disposers.push(dispose);
|
|
3237
3908
|
});
|
|
3909
|
+
if (typeof injected === "function") disposers.push(injected);
|
|
3238
3910
|
}
|
|
3239
|
-
|
|
3911
|
+
const removeVolatileListener = ctx.on?.("loader/volatile-update", () => updateConfig(pluginConfig));
|
|
3912
|
+
if (removeVolatileListener) disposers.push(removeVolatileListener);
|
|
3913
|
+
const visibleTools = getConfig().toolSurface === "compact" ? tools.filter((tool) => ["code_index", "code_context", "code_health"].includes(tool.name)) : tools;
|
|
3914
|
+
for (const tool of visibleTools) {
|
|
3240
3915
|
if (tool.name === "code_health" && !getConfig().codeHealth) continue;
|
|
3241
3916
|
disposers.push(ctx.tools.register(tool));
|
|
3242
3917
|
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
3243
3918
|
}
|
|
3244
|
-
|
|
3245
|
-
|
|
3919
|
+
const cachedMaps = /* @__PURE__ */ new Map();
|
|
3920
|
+
const saveMap = (key, value) => {
|
|
3921
|
+
cachedMaps.delete(key);
|
|
3922
|
+
cachedMaps.set(key, value);
|
|
3923
|
+
while (cachedMaps.size > 4) cachedMaps.delete(cachedMaps.keys().next().value);
|
|
3924
|
+
};
|
|
3925
|
+
async function warmMap(base) {
|
|
3246
3926
|
const now = Date.now();
|
|
3247
3927
|
const cfg = getConfig();
|
|
3928
|
+
const key = cacheKeyForRoot(base);
|
|
3248
3929
|
try {
|
|
3249
|
-
const root = await findRepoRoot(
|
|
3930
|
+
const root = await findRepoRoot(base);
|
|
3250
3931
|
if (!root) {
|
|
3251
|
-
|
|
3932
|
+
saveMap(key, { root: "", at: now, text: "" });
|
|
3252
3933
|
return;
|
|
3253
3934
|
}
|
|
3254
3935
|
const index = await getIndex(root);
|
|
@@ -3257,27 +3938,34 @@ function apply(ctx, pluginConfig) {
|
|
|
3257
3938
|
{ maxChars: cfg.mapMaxChars }
|
|
3258
3939
|
);
|
|
3259
3940
|
const stats = symbolCount(index);
|
|
3260
|
-
|
|
3941
|
+
saveMap(key, {
|
|
3261
3942
|
root: index.root,
|
|
3262
3943
|
at: Date.now(),
|
|
3263
|
-
text: text ?
|
|
3944
|
+
text: text ? `Project: ${index.root}
|
|
3945
|
+
${text}
|
|
3264
3946
|
|
|
3265
3947
|
(summary: ${index.files.length} files, ${stats} symbols)` : ""
|
|
3266
|
-
};
|
|
3948
|
+
});
|
|
3267
3949
|
} catch {
|
|
3268
|
-
|
|
3950
|
+
saveMap(key, { root: "", at: now, text: "" });
|
|
3269
3951
|
}
|
|
3270
3952
|
}
|
|
3271
|
-
void warmMap();
|
|
3272
3953
|
if (getConfig().autoInject) {
|
|
3273
3954
|
disposers.push(ctx.systemPrompt.section({
|
|
3274
3955
|
name: "code-index:repo-map",
|
|
3275
3956
|
order: 60,
|
|
3276
3957
|
// before tool guidance (100–199), after persona (0)
|
|
3277
|
-
text: () => {
|
|
3958
|
+
text: (rawContext) => {
|
|
3959
|
+
const context = rawContext;
|
|
3960
|
+
const base = context.agent?.session?.header?.cwd ?? context.session?.header?.cwd ?? process.cwd();
|
|
3961
|
+
const key = cacheKeyForRoot(base);
|
|
3278
3962
|
const now = Date.now();
|
|
3279
|
-
|
|
3280
|
-
|
|
3963
|
+
const cached = cachedMaps.get(key);
|
|
3964
|
+
if (cached && now - cached.at < getConfig().mapTtlMs) {
|
|
3965
|
+
saveMap(key, cached);
|
|
3966
|
+
return cached.text;
|
|
3967
|
+
}
|
|
3968
|
+
void warmMap(base);
|
|
3281
3969
|
return cached?.text ?? "";
|
|
3282
3970
|
}
|
|
3283
3971
|
}));
|
|
@@ -3291,6 +3979,9 @@ function apply(ctx, pluginConfig) {
|
|
|
3291
3979
|
errors.push(error);
|
|
3292
3980
|
}
|
|
3293
3981
|
}
|
|
3982
|
+
clearRepoContextManager(repoContexts);
|
|
3983
|
+
void repoContexts.dispose();
|
|
3984
|
+
cachedMaps.clear();
|
|
3294
3985
|
console.log("[dsh-code-index] plugin unloaded");
|
|
3295
3986
|
if (errors.length > 0) throw new AggregateError(errors, "failed to unload dsh-code-index");
|
|
3296
3987
|
};
|
|
@@ -3298,6 +3989,7 @@ function apply(ctx, pluginConfig) {
|
|
|
3298
3989
|
}
|
|
3299
3990
|
export {
|
|
3300
3991
|
CACHE_DIR_NAME,
|
|
3992
|
+
Config,
|
|
3301
3993
|
DEFAULT_EXCLUDED_DIRS,
|
|
3302
3994
|
SUPPORTED_EXTS,
|
|
3303
3995
|
apply,
|
|
@@ -3306,6 +3998,7 @@ export {
|
|
|
3306
3998
|
buildIndexWithCache,
|
|
3307
3999
|
buildModuleGraph,
|
|
3308
4000
|
buildSymbolTable,
|
|
4001
|
+
buildTaskContext,
|
|
3309
4002
|
callerCounts,
|
|
3310
4003
|
changeContextInputFromArgs,
|
|
3311
4004
|
defaultCachePath,
|
|
@@ -3326,6 +4019,8 @@ export {
|
|
|
3326
4019
|
renderChangeContext,
|
|
3327
4020
|
renderHit,
|
|
3328
4021
|
renderRepoMap,
|
|
4022
|
+
renderTaskContext,
|
|
4023
|
+
routeTask,
|
|
3329
4024
|
saveIndex,
|
|
3330
4025
|
scanRepo,
|
|
3331
4026
|
scoreFile,
|