@esankhan3/code-search-mcp 1.0.6 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -54
- package/dist/__tests__/_harness.js +194 -0
- package/dist/__tests__/config-cli.test.js +110 -0
- package/dist/__tests__/config-resolver.test.js +1 -1
- package/dist/__tests__/daemon-rpc-protocol.test.js +95 -0
- package/dist/__tests__/daemon-runtime.test.js +145 -0
- package/dist/__tests__/graph-tools-store.test.js +314 -0
- package/dist/__tests__/graph-tools.test.js +11 -4
- package/dist/__tests__/graph-traversal.test.js +262 -0
- package/dist/__tests__/http-auth.test.js +180 -0
- package/dist/__tests__/http-index.test.js +125 -0
- package/dist/__tests__/metrics-collect.test.js +48 -0
- package/dist/__tests__/observability.test.js +3 -4
- package/dist/__tests__/remote-proxy.test.js +105 -0
- package/dist/__tests__/server-cli-args.test.js +146 -0
- package/dist/__tests__/server-lifecycle.test.js +108 -0
- package/dist/__tests__/tool-inputs.test.js +178 -0
- package/dist/backends/in-process.js +5 -2
- package/dist/backends/index.js +2 -2
- package/dist/cli/index.js +143 -76
- package/dist/core/config.js +106 -35
- package/dist/core/env-config.js +3 -5
- package/dist/core/project-name.js +12 -0
- package/dist/core/version.js +5 -0
- package/dist/daemon/index.js +106 -65
- package/dist/daemon/rpc-server.js +38 -18
- package/dist/index.js +113 -89
- package/dist/middleware/auth.js +34 -66
- package/dist/middleware/rate-limit.js +37 -0
- package/dist/observability/logger.js +10 -2
- package/dist/observability/metrics.js +7 -6
- package/dist/projects/registry.js +13 -15
- package/dist/resources/resources.js +17 -0
- package/dist/server.js +216 -129
- package/dist/tools/graph.js +448 -165
- package/dist/tools/index-tools.js +6 -2
- package/dist/tools/profile.js +14 -8
- package/dist/tools/search.js +61 -51
- package/dist/tools/validate.js +48 -0
- package/dist/transports/http-transport.js +94 -55
- package/dist/transports/remote-proxy.js +58 -97
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, rmSync } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import { constants as osConstants } from "node:os";
|
|
5
|
+
import { dirname, join, resolve as resolvePath, basename } from "node:path";
|
|
5
6
|
import {
|
|
6
7
|
resolveCodeSearchConfig,
|
|
7
8
|
parseCliFlags,
|
|
8
9
|
printConfig,
|
|
9
|
-
|
|
10
|
+
redactSecrets,
|
|
11
|
+
toKnowledgeConfig,
|
|
12
|
+
exportDataDir
|
|
10
13
|
} from "../core/config.js";
|
|
14
|
+
import { validateProjectName } from "../core/project-name.js";
|
|
11
15
|
import {
|
|
12
16
|
indexFromPath,
|
|
13
17
|
KnowledgeIndexer,
|
|
18
|
+
getBlobStore,
|
|
14
19
|
getKnowledgeBasePath,
|
|
15
20
|
loadProfile
|
|
16
21
|
} from "@esankhan3/anvil-knowledge-core";
|
|
17
|
-
import { pickBackend, daemonSocketPath } from "../backends/index.js";
|
|
22
|
+
import { pickBackend, daemonSocketPath, DaemonBackend } from "../backends/index.js";
|
|
23
|
+
import { configureLogger, getComponentLogger } from "../observability/logger.js";
|
|
24
|
+
const log = getComponentLogger("code-search");
|
|
25
|
+
const FORMATS = /* @__PURE__ */ new Set(["text", "json", "jsonl"]);
|
|
18
26
|
const HELP_TEXT = `code-search \u2014 multi-repo code intelligence
|
|
19
27
|
|
|
20
28
|
USAGE:
|
|
@@ -26,9 +34,14 @@ COMMANDS:
|
|
|
26
34
|
query <text> hybrid search; print top-K
|
|
27
35
|
status show index + daemon health
|
|
28
36
|
reset [--project <name>] drop the project's index
|
|
29
|
-
daemon --workspace <path>
|
|
30
|
-
serve [--port N] [--auth mode] HTTP MCP server
|
|
31
|
-
mcp
|
|
37
|
+
daemon --workspace <path> run the long-running daemon
|
|
38
|
+
serve [path] [--port N] [--auth mode] HTTP MCP server
|
|
39
|
+
mcp [path] stdio MCP server over local repos (default: cwd)
|
|
40
|
+
mcp --remote <url> stdio proxy to a remote code-search server
|
|
41
|
+
|
|
42
|
+
INDEX OPTIONS:
|
|
43
|
+
--project <name> project namespace (default: path basename)
|
|
44
|
+
--force full re-index
|
|
32
45
|
|
|
33
46
|
QUERY OPTIONS:
|
|
34
47
|
--mode hybrid|vector|bm25 retrieval mode (default: hybrid)
|
|
@@ -36,6 +49,7 @@ QUERY OPTIONS:
|
|
|
36
49
|
--repo <name> restrict to a repo (repeatable)
|
|
37
50
|
--format text|json|jsonl output format (default: text)
|
|
38
51
|
--project <name> project namespace (default: cwd basename)
|
|
52
|
+
--no-daemon search in-process even if a daemon is running
|
|
39
53
|
|
|
40
54
|
CONFIG:
|
|
41
55
|
Every leaf in CodeSearchConfig is settable via --<dotted-path>.
|
|
@@ -44,6 +58,11 @@ CONFIG:
|
|
|
44
58
|
--retrieval.max-chunks 20
|
|
45
59
|
--no-indexing.auto-index
|
|
46
60
|
`;
|
|
61
|
+
function usageError(msg) {
|
|
62
|
+
process.stderr.write(`code-search: ${msg}
|
|
63
|
+
`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
47
66
|
async function main() {
|
|
48
67
|
const argv = process.argv.slice(2);
|
|
49
68
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -51,8 +70,7 @@ async function main() {
|
|
|
51
70
|
process.exit(0);
|
|
52
71
|
}
|
|
53
72
|
if (argv[0] === "--print-config") {
|
|
54
|
-
const {
|
|
55
|
-
const cfg = resolveCodeSearchConfig({ cli: patch });
|
|
73
|
+
const { cfg } = parseCommand("--print-config", argv.slice(1), {}, process.cwd());
|
|
56
74
|
process.stdout.write(printConfig(cfg) + "\n");
|
|
57
75
|
process.exit(0);
|
|
58
76
|
}
|
|
@@ -79,68 +97,100 @@ ${HELP_TEXT}`);
|
|
|
79
97
|
process.exit(2);
|
|
80
98
|
}
|
|
81
99
|
}
|
|
82
|
-
function
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
100
|
+
function parseCommand(cmd, argv, spec, workspaceDir) {
|
|
101
|
+
const { patch, rest, unknown } = parseCliFlags(argv);
|
|
102
|
+
if (unknown.length) usageError(`${cmd}: unknown flag(s): ${unknown.join(", ")}`);
|
|
103
|
+
const values = /* @__PURE__ */ new Map();
|
|
104
|
+
const bools = /* @__PURE__ */ new Set();
|
|
105
|
+
const positionals = [];
|
|
106
|
+
for (let i = 0; i < rest.length; i++) {
|
|
107
|
+
const a = rest[i];
|
|
108
|
+
if (a === "--help" || a === "-h") {
|
|
109
|
+
process.stdout.write(HELP_TEXT);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
if (spec.values?.includes(a)) {
|
|
113
|
+
const v = rest[i + 1];
|
|
114
|
+
if (v === void 0 || v.startsWith("--")) usageError(`${cmd}: ${a} needs a value`);
|
|
115
|
+
values.set(a, [...values.get(a) ?? [], v]);
|
|
116
|
+
i++;
|
|
117
|
+
} else if (spec.bools?.includes(a)) {
|
|
118
|
+
bools.add(a);
|
|
119
|
+
} else if (a.startsWith("-") && a.length > 1 && Number.isNaN(Number(a))) {
|
|
120
|
+
usageError(`${cmd}: unknown flag ${a}`);
|
|
121
|
+
} else {
|
|
122
|
+
positionals.push(a);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (spec.maxPositionals !== void 0 && positionals.length > spec.maxPositionals) {
|
|
126
|
+
usageError(`${cmd}: unexpected argument(s): ${positionals.slice(spec.maxPositionals).join(" ")}`);
|
|
127
|
+
}
|
|
128
|
+
const cfg = resolveCodeSearchConfig({ cli: patch, workspaceDir: workspaceDir ?? positionals[0] ?? process.cwd() });
|
|
129
|
+
configureLogger({ structured: cfg.telemetry.structuredLogs });
|
|
130
|
+
return { cfg, values, bools, positionals };
|
|
86
131
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
132
|
+
function flag(p, name) {
|
|
133
|
+
const v = p.values.get(name);
|
|
134
|
+
return v?.[v.length - 1];
|
|
135
|
+
}
|
|
136
|
+
function projectArg(p, fallbackDir) {
|
|
137
|
+
const name = flag(p, "--project") ?? basename(resolvePath(fallbackDir));
|
|
138
|
+
try {
|
|
139
|
+
return validateProjectName(name);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
usageError(err.message);
|
|
142
|
+
}
|
|
89
143
|
}
|
|
90
144
|
async function cmdIndex(rest) {
|
|
91
|
-
const
|
|
92
|
-
const path =
|
|
93
|
-
|
|
94
|
-
const project =
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
onProgress: (m) => process.stderr.write(`[code-search] ${m}
|
|
102
|
-
`)
|
|
145
|
+
const p = parseCommand("index", rest, { values: ["--project"], bools: ["--force"], maxPositionals: 1 });
|
|
146
|
+
const path = resolvePath(p.positionals[0] ?? process.cwd());
|
|
147
|
+
exportDataDir(p.cfg);
|
|
148
|
+
const project = projectArg(p, path);
|
|
149
|
+
const knowledge = toKnowledgeConfig(p.cfg);
|
|
150
|
+
log.info(`indexing ${path} as project "${project}"...`);
|
|
151
|
+
const stats = await indexFromPath(project, path, {
|
|
152
|
+
force: p.bools.has("--force"),
|
|
153
|
+
config: knowledge,
|
|
154
|
+
onProgress: (m) => log.info(m)
|
|
103
155
|
});
|
|
156
|
+
const rollup = await new KnowledgeIndexer().getStats(project, knowledge);
|
|
104
157
|
process.stdout.write(JSON.stringify({
|
|
105
158
|
project,
|
|
106
159
|
chunks: stats.totalChunks,
|
|
107
160
|
repos: stats.repos.length,
|
|
108
|
-
crossRepoEdges:
|
|
161
|
+
crossRepoEdges: rollup.crossRepoEdges,
|
|
109
162
|
durationMs: stats.indexDurationMs
|
|
110
163
|
}, null, 2) + "\n");
|
|
111
164
|
}
|
|
112
165
|
const VALID_MODES = /* @__PURE__ */ new Set(["hybrid", "vector", "bm25"]);
|
|
113
166
|
async function cmdQuery(rest) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const rawMode =
|
|
167
|
+
const p = parseCommand("query", rest, {
|
|
168
|
+
values: ["--project", "--mode", "--top-k", "--format", "--repo"],
|
|
169
|
+
bools: ["--no-daemon"]
|
|
170
|
+
}, process.cwd());
|
|
171
|
+
const queryText = p.positionals.join(" ");
|
|
172
|
+
if (!queryText) usageError("query: missing <text>");
|
|
173
|
+
const rawMode = flag(p, "--mode") ?? "hybrid";
|
|
121
174
|
if (!VALID_MODES.has(rawMode)) {
|
|
122
|
-
|
|
123
|
-
`code-search query: invalid --mode "${rawMode}". Valid modes: ${[...VALID_MODES].join(", ")}.
|
|
124
|
-
`
|
|
125
|
-
);
|
|
126
|
-
process.exit(2);
|
|
175
|
+
usageError(`query: invalid --mode "${rawMode}". Valid modes: ${[...VALID_MODES].join(", ")}.`);
|
|
127
176
|
}
|
|
128
177
|
const mode = rawMode;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const format =
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
178
|
+
const rawTopK = flag(p, "--top-k") ?? "10";
|
|
179
|
+
if (!/^\d+$/.test(rawTopK) || parseInt(rawTopK, 10) < 1) {
|
|
180
|
+
usageError(`query: invalid --top-k "${rawTopK}" (a positive integer).`);
|
|
181
|
+
}
|
|
182
|
+
const topK = parseInt(rawTopK, 10);
|
|
183
|
+
const format = flag(p, "--format") ?? "text";
|
|
184
|
+
if (!FORMATS.has(format)) usageError(`query: invalid --format "${format}". Valid formats: ${[...FORMATS].join(", ")}.`);
|
|
185
|
+
const repos = p.values.get("--repo") ?? [];
|
|
186
|
+
exportDataDir(p.cfg);
|
|
187
|
+
const project = projectArg(p, process.cwd());
|
|
138
188
|
const backend = await pickBackend({
|
|
139
189
|
project,
|
|
140
190
|
workspaceDir: process.cwd(),
|
|
141
|
-
knowledge: toKnowledgeConfig(cfg),
|
|
142
|
-
preferDaemon: !cfg.daemon.disabled,
|
|
143
|
-
socketPath
|
|
191
|
+
knowledge: toKnowledgeConfig(p.cfg),
|
|
192
|
+
preferDaemon: !p.cfg.daemon.disabled && !p.bools.has("--no-daemon"),
|
|
193
|
+
socketPath: daemonSocketPath(p.cfg.storage.dataDir, project)
|
|
144
194
|
});
|
|
145
195
|
const result = await backend.search(queryText, {
|
|
146
196
|
mode,
|
|
@@ -174,37 +224,44 @@ function printSearchResult(result, format) {
|
|
|
174
224
|
}
|
|
175
225
|
}
|
|
176
226
|
async function cmdStatus(rest) {
|
|
177
|
-
const {
|
|
178
|
-
|
|
179
|
-
const project =
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
);
|
|
227
|
+
const p = parseCommand("status", rest, { values: ["--project"], maxPositionals: 0 }, process.cwd());
|
|
228
|
+
exportDataDir(p.cfg);
|
|
229
|
+
const project = projectArg(p, process.cwd());
|
|
230
|
+
const knowledge = toKnowledgeConfig(p.cfg);
|
|
231
|
+
const socketPath = daemonSocketPath(p.cfg.storage.dataDir, project);
|
|
232
|
+
const daemonAlive = await new DaemonBackend({ project, workspaceDir: null, knowledge, preferDaemon: true, socketPath }).ping();
|
|
233
|
+
const blobs = getBlobStore(project, knowledge);
|
|
234
|
+
const indexed = await blobs.exists("index_stats.json") || await blobs.exists("lancedb");
|
|
235
|
+
const stats = indexed ? await new KnowledgeIndexer().getStats(project, knowledge) : null;
|
|
236
|
+
const profilesAvailable = stats ? await Promise.all(stats.repos.map(async (r) => ({ name: r.name, hasProfile: !!await loadProfile(project, r.name, knowledge) }))) : [];
|
|
186
237
|
process.stdout.write(JSON.stringify({
|
|
187
238
|
project,
|
|
188
|
-
dataDir: cfg.storage.dataDir,
|
|
239
|
+
dataDir: p.cfg.storage.dataDir,
|
|
240
|
+
...p.cfg.storage.backends ? { storageBackends: redactSecrets(p.cfg.storage.backends) } : { kbPath: getKnowledgeBasePath(project) },
|
|
241
|
+
indexed,
|
|
189
242
|
daemonSocket: socketPath,
|
|
190
|
-
daemonAlive
|
|
191
|
-
totalChunks: stats
|
|
192
|
-
embeddingProvider: stats
|
|
193
|
-
lastIndexedAt: stats
|
|
194
|
-
repos: stats
|
|
243
|
+
daemonAlive,
|
|
244
|
+
totalChunks: stats?.totalChunks ?? 0,
|
|
245
|
+
embeddingProvider: stats?.embeddingProvider ?? null,
|
|
246
|
+
lastIndexedAt: stats?.lastIndexed || null,
|
|
247
|
+
repos: stats?.repos ?? [],
|
|
195
248
|
profilesAvailable
|
|
196
249
|
}, null, 2) + "\n");
|
|
197
250
|
}
|
|
198
251
|
async function cmdReset(rest) {
|
|
199
|
-
const
|
|
200
|
-
|
|
252
|
+
const p = parseCommand("reset", rest, { values: ["--project"], maxPositionals: 0 }, process.cwd());
|
|
253
|
+
exportDataDir(p.cfg);
|
|
254
|
+
const project = projectArg(p, process.cwd());
|
|
255
|
+
const dataDir = p.cfg.storage.dataDir;
|
|
256
|
+
const basePath = resolvePath(getKnowledgeBasePath(project));
|
|
257
|
+
if (dirname(basePath) !== dataDir) {
|
|
258
|
+
usageError(`reset: refusing to remove ${basePath} \u2014 not a project directory inside ${dataDir}`);
|
|
259
|
+
}
|
|
201
260
|
if (existsSync(basePath)) {
|
|
202
261
|
rmSync(basePath, { recursive: true, force: true });
|
|
203
|
-
|
|
204
|
-
`);
|
|
262
|
+
log.info(`removed ${basePath}`);
|
|
205
263
|
} else {
|
|
206
|
-
|
|
207
|
-
`);
|
|
264
|
+
log.info(`nothing to remove at ${basePath}`);
|
|
208
265
|
}
|
|
209
266
|
}
|
|
210
267
|
async function cmdDaemon(rest) {
|
|
@@ -214,18 +271,28 @@ async function cmdDaemon(rest) {
|
|
|
214
271
|
stdio: "inherit",
|
|
215
272
|
detached: false
|
|
216
273
|
});
|
|
217
|
-
|
|
274
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
275
|
+
process.on(sig, () => {
|
|
276
|
+
try {
|
|
277
|
+
child.kill(sig);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
child.on("exit", (code, signal) => {
|
|
283
|
+
process.exit(code ?? 128 + (signal ? osConstants.signals[signal] ?? 0 : 0));
|
|
284
|
+
});
|
|
218
285
|
}
|
|
219
286
|
async function cmdServe(rest) {
|
|
220
287
|
process.argv = [process.argv[0], process.argv[1], "--serve", ...rest];
|
|
221
288
|
await import("../index.js");
|
|
222
289
|
}
|
|
223
290
|
async function cmdMcp(rest) {
|
|
224
|
-
|
|
291
|
+
const remote = rest.includes("--remote") || rest.some((a) => /^https?:\/\//.test(a));
|
|
292
|
+
process.argv = [process.argv[0], process.argv[1], ...remote ? [] : ["--local"], ...rest];
|
|
225
293
|
await import("../index.js");
|
|
226
294
|
}
|
|
227
295
|
main().catch((err) => {
|
|
228
|
-
|
|
229
|
-
`);
|
|
296
|
+
log.error(`fatal: ${err instanceof Error ? err.stack : err}`);
|
|
230
297
|
process.exit(1);
|
|
231
298
|
});
|
package/dist/core/config.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { Logger } from "../observability/logger.js";
|
|
4
5
|
function defaultDataDir() {
|
|
5
|
-
return process.env.CODE_SEARCH_DATA_DIR ?? join(homedir(), ".
|
|
6
|
+
return process.env.CODE_SEARCH_DATA_DIR ?? join(process.env.ANVIL_HOME || join(homedir(), ".anvil"), "knowledge-base");
|
|
6
7
|
}
|
|
7
8
|
const DEFAULTS = {
|
|
8
9
|
server: {
|
|
@@ -59,7 +60,6 @@ const DEFAULTS = {
|
|
|
59
60
|
recordQueries: false
|
|
60
61
|
},
|
|
61
62
|
daemon: {
|
|
62
|
-
autoSpawn: false,
|
|
63
63
|
disabled: false
|
|
64
64
|
}
|
|
65
65
|
};
|
|
@@ -159,14 +159,16 @@ function parseHeaders(s) {
|
|
|
159
159
|
}
|
|
160
160
|
return Object.keys(out).length ? out : void 0;
|
|
161
161
|
}
|
|
162
|
-
function
|
|
163
|
-
if (
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
function parseReindexInterval(raw) {
|
|
163
|
+
if (raw === void 0 || raw === null || raw === "" || raw === 0 || raw === "0" || raw === "none") return 0;
|
|
164
|
+
const s = String(raw).trim();
|
|
165
|
+
const unit = s.match(/^(\d+)(m|h)$/);
|
|
166
|
+
if (unit) return parseInt(unit[1], 10) * (unit[2] === "h" ? 36e5 : 6e4);
|
|
167
|
+
if (/^\d+$/.test(s)) {
|
|
168
|
+
const ms = parseInt(s, 10);
|
|
169
|
+
return ms >= 6e4 ? ms : null;
|
|
168
170
|
}
|
|
169
|
-
return
|
|
171
|
+
return null;
|
|
170
172
|
}
|
|
171
173
|
function envStorageBackends(env) {
|
|
172
174
|
const blobBackend = env("STORAGE_BLOB_BACKEND");
|
|
@@ -220,7 +222,7 @@ function envLayer() {
|
|
|
220
222
|
server: {
|
|
221
223
|
transport,
|
|
222
224
|
port: port ? parseInt(port, 10) : void 0,
|
|
223
|
-
host: env("HOST")
|
|
225
|
+
host: env("HOST")
|
|
224
226
|
},
|
|
225
227
|
auth: {
|
|
226
228
|
mode: authMode,
|
|
@@ -253,7 +255,8 @@ function envLayer() {
|
|
|
253
255
|
},
|
|
254
256
|
indexing: {
|
|
255
257
|
autoIndex: env("AUTO_INDEX") !== void 0 ? env("AUTO_INDEX") !== "false" && env("AUTO_INDEX") !== "0" : void 0,
|
|
256
|
-
|
|
258
|
+
// Raw string; normalised (and validated) by the resolver for every layer.
|
|
259
|
+
reindexIntervalMs: env("REINDEX_INTERVAL"),
|
|
257
260
|
chunking: {
|
|
258
261
|
maxTokens: chunkMaxTokens ? parseInt(chunkMaxTokens, 10) : void 0
|
|
259
262
|
},
|
|
@@ -278,7 +281,6 @@ function envLayer() {
|
|
|
278
281
|
recordQueries: env("TELEMETRY_RECORD_QUERIES") === "1" || env("TELEMETRY_RECORD_QUERIES") === "true" ? true : void 0
|
|
279
282
|
},
|
|
280
283
|
daemon: {
|
|
281
|
-
autoSpawn: env("DAEMON_AUTO_SPAWN") === "1" || env("DAEMON_AUTO_SPAWN") === "true" ? true : void 0,
|
|
282
284
|
disabled: env("DAEMON_DISABLED") === "1" || env("DAEMON_DISABLED") === "true" ? true : void 0
|
|
283
285
|
}
|
|
284
286
|
};
|
|
@@ -296,30 +298,64 @@ const RESERVED_FLAGS = /* @__PURE__ */ new Set([
|
|
|
296
298
|
"--force",
|
|
297
299
|
"--workspace",
|
|
298
300
|
"--no-daemon",
|
|
299
|
-
"--auto-spawn-daemon",
|
|
300
|
-
"--config",
|
|
301
301
|
"--format",
|
|
302
302
|
"--top-k",
|
|
303
303
|
"--mode",
|
|
304
|
-
"--repo"
|
|
305
|
-
"--language"
|
|
304
|
+
"--repo"
|
|
306
305
|
]);
|
|
306
|
+
const ROOT_ALIASES = {
|
|
307
|
+
"--port": ["server", "port"],
|
|
308
|
+
"--host": ["server", "host"],
|
|
309
|
+
"--transport": ["server", "transport"],
|
|
310
|
+
"--auth": ["auth", "mode"]
|
|
311
|
+
};
|
|
312
|
+
const OPTIONAL_LEAVES = [
|
|
313
|
+
"auth.jwtSecret",
|
|
314
|
+
"embedding.model",
|
|
315
|
+
"embedding.apiKey",
|
|
316
|
+
"embedding.baseUrl",
|
|
317
|
+
"embedding.ollamaHost",
|
|
318
|
+
"reranker.model",
|
|
319
|
+
"reranker.apiKey",
|
|
320
|
+
"reranker.baseUrl",
|
|
321
|
+
"reranker.timeoutMs",
|
|
322
|
+
"llm.apiKey",
|
|
323
|
+
"llm.baseUrl",
|
|
324
|
+
"github.token",
|
|
325
|
+
"telemetry.otelEndpoint"
|
|
326
|
+
];
|
|
327
|
+
const OPEN_SUBTREES = ["storage.backends.", "telemetry.otelHeaders."];
|
|
328
|
+
function leafPaths(obj, prefix, out) {
|
|
329
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
330
|
+
if (isPlainObject(v)) leafPaths(v, `${prefix}${k}.`, out);
|
|
331
|
+
else out.add(`${prefix}${k}`);
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
const KNOWN_LEAVES = leafPaths(DEFAULTS, "", new Set(OPTIONAL_LEAVES));
|
|
336
|
+
function isKnownPath(path) {
|
|
337
|
+
return KNOWN_LEAVES.has(path) || OPEN_SUBTREES.some((p) => path.startsWith(p));
|
|
338
|
+
}
|
|
307
339
|
function parseCliFlags(argv) {
|
|
308
340
|
const patch = {};
|
|
309
341
|
const rest = [];
|
|
342
|
+
const unknown = [];
|
|
310
343
|
for (let i = 0; i < argv.length; i++) {
|
|
311
344
|
const arg = argv[i];
|
|
312
345
|
if (!arg.startsWith("--") || RESERVED_FLAGS.has(arg)) {
|
|
313
346
|
rest.push(arg);
|
|
314
347
|
continue;
|
|
315
348
|
}
|
|
316
|
-
let
|
|
349
|
+
let path = ROOT_ALIASES[arg];
|
|
317
350
|
let negate = false;
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
if (!path) {
|
|
352
|
+
let key = arg.slice(2);
|
|
353
|
+
if (key.startsWith("no-")) {
|
|
354
|
+
negate = true;
|
|
355
|
+
key = key.slice(3);
|
|
356
|
+
}
|
|
357
|
+
path = key.split(".").map((seg) => seg.replace(/-([a-z])/g, (_, c) => c.toUpperCase()));
|
|
321
358
|
}
|
|
322
|
-
const path = key.split(".").map((seg) => seg.replace(/-([a-z])/g, (_, c) => c.toUpperCase()));
|
|
323
359
|
let val;
|
|
324
360
|
if (negate) {
|
|
325
361
|
val = false;
|
|
@@ -328,9 +364,13 @@ function parseCliFlags(argv) {
|
|
|
328
364
|
} else {
|
|
329
365
|
val = true;
|
|
330
366
|
}
|
|
367
|
+
if (!isKnownPath(path.join("."))) {
|
|
368
|
+
unknown.push(arg);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
331
371
|
setDeep(patch, path, val);
|
|
332
372
|
}
|
|
333
|
-
return { patch, rest };
|
|
373
|
+
return { patch, rest, unknown };
|
|
334
374
|
}
|
|
335
375
|
function setDeep(obj, path, value) {
|
|
336
376
|
let cur = obj;
|
|
@@ -347,10 +387,33 @@ function resolveCodeSearchConfig(opts = {}) {
|
|
|
347
387
|
const yaml2 = opts.workspaceDir ? loadYamlFile(join(opts.workspaceDir, ".code-search.yaml")) ?? {} : {};
|
|
348
388
|
const env = envLayer();
|
|
349
389
|
const cli = opts.cli ?? {};
|
|
350
|
-
const
|
|
390
|
+
const layers = [yaml1, yaml2, env, cli];
|
|
391
|
+
const cfg = deepMerge(
|
|
392
|
+
structuredClone(DEFAULTS),
|
|
393
|
+
...layers
|
|
394
|
+
);
|
|
395
|
+
if (cfg.auth.mode !== "none" && !layers.some((l) => l.server?.host !== void 0)) {
|
|
396
|
+
cfg.server.host = "0.0.0.0";
|
|
397
|
+
}
|
|
398
|
+
const rawInterval = cfg.indexing.reindexIntervalMs;
|
|
399
|
+
const intervalMs = parseReindexInterval(rawInterval);
|
|
400
|
+
if (intervalMs === null) {
|
|
401
|
+
warnOnce(cfg, `[code-search] invalid reindex interval ${JSON.stringify(String(rawInterval))} (use 30m, 1h, \u2026 \u2014 at least one minute \u2014 or 0); auto-reindex disabled`);
|
|
402
|
+
}
|
|
403
|
+
cfg.indexing.reindexIntervalMs = intervalMs ?? 0;
|
|
404
|
+
cfg.storage.dataDir = resolve(cfg.storage.dataDir);
|
|
351
405
|
bridgeLegacyEnvVars(cfg);
|
|
352
406
|
return cfg;
|
|
353
407
|
}
|
|
408
|
+
const warned = /* @__PURE__ */ new Set();
|
|
409
|
+
function warnOnce(cfg, msg) {
|
|
410
|
+
if (warned.has(msg)) return;
|
|
411
|
+
warned.add(msg);
|
|
412
|
+
new Logger({ structured: cfg.telemetry.structuredLogs }).warn(msg);
|
|
413
|
+
}
|
|
414
|
+
function exportDataDir(cfg) {
|
|
415
|
+
process.env.CODE_SEARCH_DATA_DIR = cfg.storage.dataDir;
|
|
416
|
+
}
|
|
354
417
|
function bridgeLegacyEnvVars(cfg) {
|
|
355
418
|
process.env.ANVIL_LLM_MODE ??= process.env.CODE_SEARCH_LLM_MODE ?? cfg.llm.mode;
|
|
356
419
|
process.env.ANVIL_LLM_PROVIDER ??= process.env.CODE_SEARCH_LLM_PROVIDER ?? cfg.llm.provider;
|
|
@@ -362,7 +425,7 @@ function bridgeLegacyEnvVars(cfg) {
|
|
|
362
425
|
function toKnowledgeConfig(c) {
|
|
363
426
|
return {
|
|
364
427
|
embedding: { ...c.embedding },
|
|
365
|
-
chunking: { ...c.indexing.chunking },
|
|
428
|
+
chunking: { ...c.indexing.chunking, ignorePatterns: c.indexing.ignorePatterns },
|
|
366
429
|
retrieval: {
|
|
367
430
|
maxChunks: c.retrieval.maxChunks,
|
|
368
431
|
maxTokens: c.retrieval.maxTokens,
|
|
@@ -373,23 +436,29 @@ function toKnowledgeConfig(c) {
|
|
|
373
436
|
...c.storage.backends ? { storage: c.storage.backends } : {}
|
|
374
437
|
};
|
|
375
438
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
]);
|
|
439
|
+
const SECRET_KEY = /pass(word)?|secret|token|authorization|api[-_]?keys?|credential/i;
|
|
440
|
+
const URI_USERINFO = /([a-z][a-z0-9+.-]*:\/\/)([^/\s@]+)@/gi;
|
|
441
|
+
function redactString(v) {
|
|
442
|
+
return `***redacted (${v.length} chars)`;
|
|
443
|
+
}
|
|
382
444
|
function redactSecrets(value, parentKey = "") {
|
|
445
|
+
if (typeof value === "string") {
|
|
446
|
+
return value.replace(URI_USERINFO, (_m, scheme, info) => {
|
|
447
|
+
const colon = info.indexOf(":");
|
|
448
|
+
return `${scheme}${colon >= 0 ? info.slice(0, colon + 1) : ""}***@`;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
383
451
|
if (Array.isArray(value)) {
|
|
384
452
|
return value.map((v) => redactSecrets(v, parentKey));
|
|
385
453
|
}
|
|
386
454
|
if (isPlainObject(value)) {
|
|
387
455
|
const out = {};
|
|
388
456
|
for (const [k, v] of Object.entries(value)) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
457
|
+
const secret = SECRET_KEY.test(k) || parentKey === "otelHeaders";
|
|
458
|
+
if (secret && typeof v === "string" && v.length > 0) {
|
|
459
|
+
out[k] = redactString(v);
|
|
460
|
+
} else if (secret && Array.isArray(v)) {
|
|
461
|
+
out[k] = v.map((s) => typeof s === "string" ? redactString(s) : redactSecrets(s, k));
|
|
393
462
|
} else {
|
|
394
463
|
out[k] = redactSecrets(v, k);
|
|
395
464
|
}
|
|
@@ -403,7 +472,9 @@ function printConfig(c) {
|
|
|
403
472
|
}
|
|
404
473
|
export {
|
|
405
474
|
DEFAULTS,
|
|
475
|
+
exportDataDir,
|
|
406
476
|
parseCliFlags,
|
|
477
|
+
parseReindexInterval,
|
|
407
478
|
printConfig,
|
|
408
479
|
redactSecrets,
|
|
409
480
|
resolveCodeSearchConfig,
|
package/dist/core/env-config.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { execSync as _execSync } from "node:child_process";
|
|
2
2
|
import {
|
|
3
3
|
resolveCodeSearchConfig,
|
|
4
|
-
parseCliFlags,
|
|
5
4
|
toKnowledgeConfig
|
|
6
5
|
} from "./config.js";
|
|
7
6
|
import { normalizeRerankerConfig } from "@esankhan3/anvil-knowledge-core";
|
|
8
7
|
let _config = null;
|
|
9
8
|
function resolveLlmMode(explicit, apiKey, claudeBin) {
|
|
10
|
-
if (explicit
|
|
9
|
+
if (explicit === "cli" || explicit === "api" || explicit === "none") {
|
|
11
10
|
if (explicit === "api" && !apiKey) {
|
|
12
11
|
console.error(
|
|
13
12
|
`[code-search-mcp] WARNING: LLM_MODE=api but no API key found. Set CODE_SEARCH_LLM_API_KEY or ANTHROPIC_API_KEY. Falling back to LLM_MODE=none.`
|
|
@@ -32,10 +31,9 @@ function resolvedKnowledgeConfig() {
|
|
|
32
31
|
}
|
|
33
32
|
function loadServerConfig(opts) {
|
|
34
33
|
if (_config) return _config;
|
|
35
|
-
const
|
|
36
|
-
const c = resolveCodeSearchConfig({ cli, workspaceDir: opts?.workspaceDir });
|
|
34
|
+
const c = resolveCodeSearchConfig({ cli: opts?.cli, workspaceDir: opts?.workspaceDir });
|
|
37
35
|
const rerankerStruct = normalizeRerankerConfig(c.reranker);
|
|
38
|
-
const llmMode = resolveLlmMode(c.llm.mode, c.llm.apiKey, c.llm.claudeBin);
|
|
36
|
+
const llmMode = resolveLlmMode(process.env.ANVIL_LLM_MODE ?? c.llm.mode, c.llm.apiKey, c.llm.claudeBin);
|
|
39
37
|
_config = {
|
|
40
38
|
port: c.server.port,
|
|
41
39
|
host: c.server.host,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const RESERVED = /* @__PURE__ */ new Set(["daemon", "projects"]);
|
|
2
|
+
function validateProjectName(name) {
|
|
3
|
+
if (!name || name === "." || name === ".." || /[/\\\0]/.test(name) || RESERVED.has(name)) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`Invalid project name ${JSON.stringify(name)}: a project name must be a single path segment (no "/" or "\\", not "." or "..") and not one of the reserved names: ${[...RESERVED].join(", ")}.`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
return name;
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
validateProjectName
|
|
12
|
+
};
|