dsh-code-index 0.7.0 → 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 +23 -0
- package/README.md +25 -5
- package/README.zh.md +25 -5
- package/dist/client.js +74 -19
- package/dist/client.js.map +1 -1
- package/dist/index.js +333 -103
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
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;
|
|
@@ -1794,13 +1835,13 @@ function findOrphanModules(index) {
|
|
|
1794
1835
|
index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
|
|
1795
1836
|
);
|
|
1796
1837
|
return nodes.filter(
|
|
1797
|
-
(
|
|
1838
|
+
(path10) => withSymbols.has(path10) && (incoming.get(path10) ?? 0) === 0 && (edges.get(path10)?.size ?? 0) === 0 && !ENTRY_RE.test(path10) && !TEST_RE.test(path10)
|
|
1798
1839
|
);
|
|
1799
1840
|
}
|
|
1800
1841
|
|
|
1801
1842
|
// src/git-diff.ts
|
|
1802
1843
|
import { execFile } from "child_process";
|
|
1803
|
-
import { readFile as
|
|
1844
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1804
1845
|
import path6 from "path";
|
|
1805
1846
|
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
1806
1847
|
var HEADER_PATH_PREFIX_RE = /^[ab]\//;
|
|
@@ -2013,7 +2054,7 @@ async function readWorkingTreeDiff(root, baseRef) {
|
|
|
2013
2054
|
const normalized = rawPath.replace(/\\/g, "/");
|
|
2014
2055
|
if (normalized === ".dsh-code-index" || normalized.startsWith(".dsh-code-index/")) continue;
|
|
2015
2056
|
try {
|
|
2016
|
-
const content = await
|
|
2057
|
+
const content = await readFile5(path6.resolve(root, normalized), "utf8");
|
|
2017
2058
|
const lines = content.length === 0 ? 0 : content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
|
|
2018
2059
|
untrackedFiles.push({ path: normalized, content, lines });
|
|
2019
2060
|
} catch {
|
|
@@ -2888,9 +2929,9 @@ function collectRelationships(graph, primary) {
|
|
|
2888
2929
|
(a, b) => b.priority - a.priority || a.resolution.localeCompare(b.resolution) || a.text.localeCompare(b.text)
|
|
2889
2930
|
);
|
|
2890
2931
|
}
|
|
2891
|
-
function addFile(files,
|
|
2892
|
-
const previous = files.get(
|
|
2893
|
-
if (!previous || score > previous.score) files.set(
|
|
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 });
|
|
2894
2935
|
}
|
|
2895
2936
|
function gatherFiles(index, route, primary, change, maxFiles) {
|
|
2896
2937
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -3119,9 +3160,21 @@ function renderTaskContext(result) {
|
|
|
3119
3160
|
|
|
3120
3161
|
// src/tools.ts
|
|
3121
3162
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3122
|
-
import
|
|
3163
|
+
import path9 from "path";
|
|
3123
3164
|
|
|
3124
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
|
+
});
|
|
3125
3178
|
var DEFAULTS = {
|
|
3126
3179
|
excludeDirs: [],
|
|
3127
3180
|
mapTopFiles: 24,
|
|
@@ -3129,14 +3182,17 @@ var DEFAULTS = {
|
|
|
3129
3182
|
mapTtlMs: 6e4,
|
|
3130
3183
|
autoInject: true,
|
|
3131
3184
|
codeHealth: false,
|
|
3132
|
-
toolSurface: "full"
|
|
3185
|
+
toolSurface: "full",
|
|
3186
|
+
externalWatch: true,
|
|
3187
|
+
watchDebounceMs: 120
|
|
3133
3188
|
};
|
|
3134
3189
|
var state = { current: { ...DEFAULTS } };
|
|
3135
3190
|
function applyConfig(partial) {
|
|
3191
|
+
const resolved = partial ? Object.fromEntries(Object.entries(partial).map(([key, value]) => [key, unwrapVolatile(value)])) : void 0;
|
|
3136
3192
|
state.current = {
|
|
3137
3193
|
...DEFAULTS,
|
|
3138
|
-
...
|
|
3139
|
-
excludeDirs: [...DEFAULTS.excludeDirs, ...
|
|
3194
|
+
...resolved ?? {},
|
|
3195
|
+
excludeDirs: [...DEFAULTS.excludeDirs, ...resolved?.excludeDirs ?? []]
|
|
3140
3196
|
};
|
|
3141
3197
|
if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {
|
|
3142
3198
|
state.current.mapTopFiles = DEFAULTS.mapTopFiles;
|
|
@@ -3150,6 +3206,16 @@ function applyConfig(partial) {
|
|
|
3150
3206
|
if (state.current.toolSurface !== "full" && state.current.toolSurface !== "compact") {
|
|
3151
3207
|
state.current.toolSurface = DEFAULTS.toolSurface;
|
|
3152
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();
|
|
3153
3219
|
}
|
|
3154
3220
|
function getConfig() {
|
|
3155
3221
|
return state.current;
|
|
@@ -3158,67 +3224,192 @@ function indexOptions() {
|
|
|
3158
3224
|
return { excludeDirs: state.current.excludeDirs };
|
|
3159
3225
|
}
|
|
3160
3226
|
|
|
3161
|
-
// src/
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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);
|
|
3174
3326
|
}
|
|
3175
3327
|
return index;
|
|
3176
3328
|
}).finally(() => {
|
|
3177
|
-
if (
|
|
3329
|
+
if (context.refresh === promise) context.refresh = null;
|
|
3330
|
+
if (!context.disposed && context.dirtyFiles.size > 0) this.scheduleRefresh(context);
|
|
3178
3331
|
});
|
|
3179
|
-
|
|
3332
|
+
context.refresh = promise;
|
|
3180
3333
|
return promise;
|
|
3181
3334
|
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
const
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
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;
|
|
3209
3402
|
}
|
|
3210
|
-
var indexCache = createIndexCache((root) => buildIndexWithCache(root, indexOptions()));
|
|
3211
3403
|
function getIndex(root, force = false) {
|
|
3212
|
-
return
|
|
3404
|
+
return activeRepoContexts ? activeRepoContexts.get(root, force) : buildIndexWithCache(root, indexOptions(), void 0, force ? /* @__PURE__ */ new Set(["*"]) : void 0);
|
|
3213
3405
|
}
|
|
3214
3406
|
function invalidateIndexCache() {
|
|
3215
|
-
indexCache.invalidate();
|
|
3216
3407
|
}
|
|
3217
3408
|
async function resolveRoot(arg, exec) {
|
|
3218
|
-
const cwd = exec.agent?.session?.header?.cwd;
|
|
3409
|
+
const cwd = exec.agent?.session?.header?.cwd ?? exec.session?.header?.cwd ?? exec.cwd;
|
|
3219
3410
|
const base = arg ?? cwd ?? process.cwd();
|
|
3220
3411
|
const root = await findRepoRoot(base);
|
|
3221
|
-
if (!root) throw new Error(`no git repository found from ${
|
|
3412
|
+
if (!root) throw new Error(`no git repository found from ${path9.resolve(base)}`);
|
|
3222
3413
|
return root;
|
|
3223
3414
|
}
|
|
3224
3415
|
function hitsToSearchMeta(hits) {
|
|
@@ -3233,7 +3424,7 @@ function hitsToSearchMeta(hits) {
|
|
|
3233
3424
|
}
|
|
3234
3425
|
return {
|
|
3235
3426
|
shape: "matches",
|
|
3236
|
-
files: [...byFile.entries()].map(([
|
|
3427
|
+
files: [...byFile.entries()].map(([path10, matches]) => ({ path: path10, matches })),
|
|
3237
3428
|
truncated: false,
|
|
3238
3429
|
total: hits.length
|
|
3239
3430
|
};
|
|
@@ -3656,25 +3847,32 @@ var tools = [
|
|
|
3656
3847
|
];
|
|
3657
3848
|
|
|
3658
3849
|
// src/settings.ts
|
|
3659
|
-
import
|
|
3850
|
+
import Schema2 from "@deepseek-ai/schemastery";
|
|
3660
3851
|
var SETTINGS_NAMESPACE = "code-index";
|
|
3661
|
-
var settingsSchema =
|
|
3662
|
-
excludeDirs:
|
|
3663
|
-
mapTopFiles:
|
|
3664
|
-
mapMaxChars:
|
|
3665
|
-
mapTtlMs:
|
|
3666
|
-
autoInject:
|
|
3667
|
-
codeHealth:
|
|
3668
|
-
toolSurface:
|
|
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)
|
|
3669
3862
|
});
|
|
3670
|
-
function registerSettings(getService, pluginConfig, onResolved) {
|
|
3863
|
+
function registerSettings(getService, pluginConfig, onResolved, fiber) {
|
|
3671
3864
|
const service = getService("settings");
|
|
3672
|
-
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
|
+
}
|
|
3673
3871
|
const scope = service.register(SETTINGS_NAMESPACE, settingsSchema, {
|
|
3674
3872
|
base: pluginConfig ?? {}
|
|
3675
3873
|
});
|
|
3676
3874
|
onResolved(scope.get());
|
|
3677
|
-
scope.watch((next) => {
|
|
3875
|
+
return scope.watch((next) => {
|
|
3678
3876
|
onResolved(next);
|
|
3679
3877
|
});
|
|
3680
3878
|
}
|
|
@@ -3687,30 +3885,51 @@ function apply(ctx, pluginConfig) {
|
|
|
3687
3885
|
invalidateIndexCache();
|
|
3688
3886
|
ctx.effect(() => {
|
|
3689
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
|
+
};
|
|
3690
3898
|
console.log("[dsh-code-index] plugin loaded");
|
|
3691
3899
|
if (ctx.inject) {
|
|
3692
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
3693
|
-
registerSettings(
|
|
3900
|
+
const injected = ctx.inject(["settings"], (settingsCtx) => {
|
|
3901
|
+
const dispose = registerSettings(
|
|
3694
3902
|
(name2) => settingsCtx.get(name2),
|
|
3695
3903
|
pluginConfig,
|
|
3696
|
-
(resolved) =>
|
|
3904
|
+
(resolved) => updateConfig(resolved),
|
|
3905
|
+
settingsCtx.fiber
|
|
3697
3906
|
);
|
|
3907
|
+
if (dispose) disposers.push(dispose);
|
|
3698
3908
|
});
|
|
3909
|
+
if (typeof injected === "function") disposers.push(injected);
|
|
3699
3910
|
}
|
|
3911
|
+
const removeVolatileListener = ctx.on?.("loader/volatile-update", () => updateConfig(pluginConfig));
|
|
3912
|
+
if (removeVolatileListener) disposers.push(removeVolatileListener);
|
|
3700
3913
|
const visibleTools = getConfig().toolSurface === "compact" ? tools.filter((tool) => ["code_index", "code_context", "code_health"].includes(tool.name)) : tools;
|
|
3701
3914
|
for (const tool of visibleTools) {
|
|
3702
3915
|
if (tool.name === "code_health" && !getConfig().codeHealth) continue;
|
|
3703
3916
|
disposers.push(ctx.tools.register(tool));
|
|
3704
3917
|
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
3705
3918
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
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) {
|
|
3708
3926
|
const now = Date.now();
|
|
3709
3927
|
const cfg = getConfig();
|
|
3928
|
+
const key = cacheKeyForRoot(base);
|
|
3710
3929
|
try {
|
|
3711
|
-
const root = await findRepoRoot(
|
|
3930
|
+
const root = await findRepoRoot(base);
|
|
3712
3931
|
if (!root) {
|
|
3713
|
-
|
|
3932
|
+
saveMap(key, { root: "", at: now, text: "" });
|
|
3714
3933
|
return;
|
|
3715
3934
|
}
|
|
3716
3935
|
const index = await getIndex(root);
|
|
@@ -3719,27 +3938,34 @@ function apply(ctx, pluginConfig) {
|
|
|
3719
3938
|
{ maxChars: cfg.mapMaxChars }
|
|
3720
3939
|
);
|
|
3721
3940
|
const stats = symbolCount(index);
|
|
3722
|
-
|
|
3941
|
+
saveMap(key, {
|
|
3723
3942
|
root: index.root,
|
|
3724
3943
|
at: Date.now(),
|
|
3725
|
-
text: text ?
|
|
3944
|
+
text: text ? `Project: ${index.root}
|
|
3945
|
+
${text}
|
|
3726
3946
|
|
|
3727
3947
|
(summary: ${index.files.length} files, ${stats} symbols)` : ""
|
|
3728
|
-
};
|
|
3948
|
+
});
|
|
3729
3949
|
} catch {
|
|
3730
|
-
|
|
3950
|
+
saveMap(key, { root: "", at: now, text: "" });
|
|
3731
3951
|
}
|
|
3732
3952
|
}
|
|
3733
|
-
void warmMap();
|
|
3734
3953
|
if (getConfig().autoInject) {
|
|
3735
3954
|
disposers.push(ctx.systemPrompt.section({
|
|
3736
3955
|
name: "code-index:repo-map",
|
|
3737
3956
|
order: 60,
|
|
3738
3957
|
// before tool guidance (100–199), after persona (0)
|
|
3739
|
-
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);
|
|
3740
3962
|
const now = Date.now();
|
|
3741
|
-
|
|
3742
|
-
|
|
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);
|
|
3743
3969
|
return cached?.text ?? "";
|
|
3744
3970
|
}
|
|
3745
3971
|
}));
|
|
@@ -3753,6 +3979,9 @@ function apply(ctx, pluginConfig) {
|
|
|
3753
3979
|
errors.push(error);
|
|
3754
3980
|
}
|
|
3755
3981
|
}
|
|
3982
|
+
clearRepoContextManager(repoContexts);
|
|
3983
|
+
void repoContexts.dispose();
|
|
3984
|
+
cachedMaps.clear();
|
|
3756
3985
|
console.log("[dsh-code-index] plugin unloaded");
|
|
3757
3986
|
if (errors.length > 0) throw new AggregateError(errors, "failed to unload dsh-code-index");
|
|
3758
3987
|
};
|
|
@@ -3760,6 +3989,7 @@ function apply(ctx, pluginConfig) {
|
|
|
3760
3989
|
}
|
|
3761
3990
|
export {
|
|
3762
3991
|
CACHE_DIR_NAME,
|
|
3992
|
+
Config,
|
|
3763
3993
|
DEFAULT_EXCLUDED_DIRS,
|
|
3764
3994
|
SUPPORTED_EXTS,
|
|
3765
3995
|
apply,
|