@esankhan3/code-search-mcp 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -248
- package/dist/__tests__/config-resolver.test.js +153 -0
- package/dist/__tests__/daemon-rpc.test.js +100 -0
- package/dist/__tests__/observability.test.js +69 -0
- package/dist/__tests__/projects.test.js +141 -0
- package/dist/backends/daemon-client.js +87 -0
- package/dist/backends/in-process.js +104 -0
- package/dist/backends/index.js +29 -0
- package/dist/cli/index.js +231 -0
- package/dist/core/config.js +362 -49
- package/dist/core/env-config.js +45 -43
- package/dist/daemon/index.js +119 -0
- package/dist/daemon/rpc-server.js +99 -0
- package/dist/daemon/watcher.js +105 -0
- package/dist/index.js +7 -0
- package/dist/observability/logger.js +66 -0
- package/dist/observability/metrics.js +138 -0
- package/dist/projects/registry.js +146 -0
- package/dist/resources/resources.js +3 -3
- package/dist/server.js +6 -2
- package/dist/tools/graph.js +1 -1
- package/dist/tools/index-tools.js +1 -1
- package/dist/tools/profile.js +2 -2
- package/dist/tools/search.js +1 -1
- package/dist/transports/http-transport.js +38 -0
- package/package.json +31 -12
- package/scripts/smoke/run-smoke.sh +167 -0
- package/dist/core/ast-graph-builder.js +0 -800
- package/dist/core/chunker.js +0 -368
- package/dist/core/claude-runner.js +0 -173
- package/dist/core/cross-repo-detector.js +0 -887
- package/dist/core/embedder.js +0 -334
- package/dist/core/file-walker.js +0 -186
- package/dist/core/git-diff.js +0 -160
- package/dist/core/graph-metrics.js +0 -233
- package/dist/core/indexer.js +0 -552
- package/dist/core/project-graph-builder.js +0 -202
- package/dist/core/query-classifier.js +0 -144
- package/dist/core/query-router.js +0 -210
- package/dist/core/rag-evaluator.js +0 -213
- package/dist/core/repo-profiler.js +0 -383
- package/dist/core/reranker.js +0 -218
- package/dist/core/retriever.js +0 -242
- package/dist/core/semantic-edge-detector.js +0 -147
- package/dist/core/service-mesh-inferrer.js +0 -241
- package/dist/core/structural-hasher.js +0 -316
- package/dist/core/tree-sitter-parser.js +0 -539
- package/dist/core/vector-store.js +0 -259
- package/dist/core/workspace-detector.js +0 -481
- /package/dist/{core → backends}/types.js +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
resolveCodeSearchConfig,
|
|
7
|
+
parseCliFlags,
|
|
8
|
+
printConfig,
|
|
9
|
+
toKnowledgeConfig
|
|
10
|
+
} from "../core/config.js";
|
|
11
|
+
import {
|
|
12
|
+
indexFromPath,
|
|
13
|
+
KnowledgeIndexer,
|
|
14
|
+
getKnowledgeBasePath,
|
|
15
|
+
loadProfile
|
|
16
|
+
} from "@esankhan3/anvil-knowledge-core";
|
|
17
|
+
import { pickBackend, daemonSocketPath } from "../backends/index.js";
|
|
18
|
+
const HELP_TEXT = `code-search \u2014 multi-repo code intelligence
|
|
19
|
+
|
|
20
|
+
USAGE:
|
|
21
|
+
code-search <command> [options]
|
|
22
|
+
code-search --print-config print resolved configuration (secrets redacted)
|
|
23
|
+
|
|
24
|
+
COMMANDS:
|
|
25
|
+
index [path] index a directory (default: cwd)
|
|
26
|
+
query <text> hybrid search; print top-K
|
|
27
|
+
status show index + daemon health
|
|
28
|
+
reset [--project <name>] drop the project's index
|
|
29
|
+
daemon --workspace <path> spawn the long-running daemon
|
|
30
|
+
serve [--port N] [--auth mode] HTTP MCP server
|
|
31
|
+
mcp stdio MCP server (Claude Desktop)
|
|
32
|
+
|
|
33
|
+
QUERY OPTIONS:
|
|
34
|
+
--mode hybrid|vector|bm25 retrieval mode (default: hybrid)
|
|
35
|
+
--top-k N max results (default: 10)
|
|
36
|
+
--repo <name> restrict to a repo (repeatable)
|
|
37
|
+
--format text|json|jsonl output format (default: text)
|
|
38
|
+
--project <name> project namespace (default: cwd basename)
|
|
39
|
+
|
|
40
|
+
CONFIG:
|
|
41
|
+
Every leaf in CodeSearchConfig is settable via --<dotted-path>.
|
|
42
|
+
Examples:
|
|
43
|
+
--embedding.provider codestral
|
|
44
|
+
--retrieval.max-chunks 20
|
|
45
|
+
--no-indexing.auto-index
|
|
46
|
+
`;
|
|
47
|
+
async function main() {
|
|
48
|
+
const argv = process.argv.slice(2);
|
|
49
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
50
|
+
process.stdout.write(HELP_TEXT);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
if (argv[0] === "--print-config") {
|
|
54
|
+
const { patch } = parseCliFlags(argv.slice(1));
|
|
55
|
+
const cfg = resolveCodeSearchConfig({ cli: patch });
|
|
56
|
+
process.stdout.write(printConfig(cfg) + "\n");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
const sub = argv[0];
|
|
60
|
+
const rest = argv.slice(1);
|
|
61
|
+
switch (sub) {
|
|
62
|
+
case "index":
|
|
63
|
+
return await cmdIndex(rest);
|
|
64
|
+
case "query":
|
|
65
|
+
return await cmdQuery(rest);
|
|
66
|
+
case "status":
|
|
67
|
+
return await cmdStatus(rest);
|
|
68
|
+
case "reset":
|
|
69
|
+
return await cmdReset(rest);
|
|
70
|
+
case "daemon":
|
|
71
|
+
return await cmdDaemon(rest);
|
|
72
|
+
case "serve":
|
|
73
|
+
return await cmdServe(rest);
|
|
74
|
+
case "mcp":
|
|
75
|
+
return await cmdMcp(rest);
|
|
76
|
+
default:
|
|
77
|
+
process.stderr.write(`unknown subcommand: ${sub}
|
|
78
|
+
${HELP_TEXT}`);
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function takeFlag(rest, name) {
|
|
83
|
+
const idx = rest.indexOf(name);
|
|
84
|
+
if (idx < 0) return void 0;
|
|
85
|
+
return rest[idx + 1];
|
|
86
|
+
}
|
|
87
|
+
function projectFromPath(p) {
|
|
88
|
+
return resolvePath(p).split("/").filter(Boolean).pop() || "project";
|
|
89
|
+
}
|
|
90
|
+
async function cmdIndex(rest) {
|
|
91
|
+
const { patch, rest: positional } = parseCliFlags(rest);
|
|
92
|
+
const path = positional.find((a) => !a.startsWith("--")) ?? process.cwd();
|
|
93
|
+
const cfg = resolveCodeSearchConfig({ cli: patch, workspaceDir: path });
|
|
94
|
+
const project = takeFlag(positional, "--project") ?? projectFromPath(path);
|
|
95
|
+
const force = positional.includes("--force");
|
|
96
|
+
process.stderr.write(`[code-search] indexing ${path} as project "${project}"...
|
|
97
|
+
`);
|
|
98
|
+
const stats = await indexFromPath(project, resolvePath(path), {
|
|
99
|
+
force,
|
|
100
|
+
config: toKnowledgeConfig(cfg),
|
|
101
|
+
onProgress: (m) => process.stderr.write(`[code-search] ${m}
|
|
102
|
+
`)
|
|
103
|
+
});
|
|
104
|
+
process.stdout.write(JSON.stringify({
|
|
105
|
+
project,
|
|
106
|
+
chunks: stats.totalChunks,
|
|
107
|
+
repos: stats.repos.length,
|
|
108
|
+
crossRepoEdges: stats.crossRepoEdges,
|
|
109
|
+
durationMs: stats.indexDurationMs
|
|
110
|
+
}, null, 2) + "\n");
|
|
111
|
+
}
|
|
112
|
+
const VALID_MODES = /* @__PURE__ */ new Set(["hybrid", "vector", "bm25"]);
|
|
113
|
+
async function cmdQuery(rest) {
|
|
114
|
+
const { patch, rest: positional } = parseCliFlags(rest);
|
|
115
|
+
const queryText = positional.find((a) => !a.startsWith("--") && !a.startsWith("-"));
|
|
116
|
+
if (!queryText) {
|
|
117
|
+
process.stderr.write("code-search query: missing <text>\n");
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
const rawMode = takeFlag(positional, "--mode") ?? "hybrid";
|
|
121
|
+
if (!VALID_MODES.has(rawMode)) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`code-search query: invalid --mode "${rawMode}". Valid modes: ${[...VALID_MODES].join(", ")}.
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
128
|
+
const mode = rawMode;
|
|
129
|
+
const topK = parseInt(takeFlag(positional, "--top-k") ?? "10", 10);
|
|
130
|
+
const repos = positional.reduce((acc, a, i) => {
|
|
131
|
+
if (a === "--repo" && positional[i + 1]) acc.push(positional[i + 1]);
|
|
132
|
+
return acc;
|
|
133
|
+
}, []);
|
|
134
|
+
const format = takeFlag(positional, "--format") ?? "text";
|
|
135
|
+
const project = takeFlag(positional, "--project") ?? projectFromPath(process.cwd());
|
|
136
|
+
const cfg = resolveCodeSearchConfig({ cli: patch });
|
|
137
|
+
const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
|
|
138
|
+
const backend = await pickBackend({
|
|
139
|
+
project,
|
|
140
|
+
workspaceDir: process.cwd(),
|
|
141
|
+
knowledge: toKnowledgeConfig(cfg),
|
|
142
|
+
preferDaemon: !cfg.daemon.disabled,
|
|
143
|
+
socketPath
|
|
144
|
+
});
|
|
145
|
+
const result = await backend.search(queryText, {
|
|
146
|
+
mode,
|
|
147
|
+
maxResults: topK,
|
|
148
|
+
repos: repos.length ? repos : void 0
|
|
149
|
+
});
|
|
150
|
+
await backend.close();
|
|
151
|
+
printSearchResult(result, format);
|
|
152
|
+
}
|
|
153
|
+
function printSearchResult(result, format) {
|
|
154
|
+
if (format === "json") {
|
|
155
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (format === "jsonl") {
|
|
159
|
+
for (const c of result.chunks) process.stdout.write(JSON.stringify(c) + "\n");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (result.chunks.length === 0) {
|
|
163
|
+
process.stdout.write(`No results for "${result.query}"
|
|
164
|
+
`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let i = 0;
|
|
168
|
+
for (const c of result.chunks) {
|
|
169
|
+
i++;
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`### ${i}. ${c.repoName}/${c.filePath}:${c.startLine}-${c.endLine} (score: ${c.score.toFixed(3)}, source: ${c.source})
|
|
172
|
+
\`\`\`` + c.language + "\n" + c.content + "\n```\n\n"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function cmdStatus(rest) {
|
|
177
|
+
const { patch } = parseCliFlags(rest);
|
|
178
|
+
const cfg = resolveCodeSearchConfig({ cli: patch });
|
|
179
|
+
const project = takeFlag(rest, "--project") ?? projectFromPath(process.cwd());
|
|
180
|
+
const indexer = new KnowledgeIndexer();
|
|
181
|
+
const stats = await indexer.getStats(project);
|
|
182
|
+
const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
|
|
183
|
+
process.stdout.write(JSON.stringify({
|
|
184
|
+
project,
|
|
185
|
+
dataDir: cfg.storage.dataDir,
|
|
186
|
+
daemonSocket: socketPath,
|
|
187
|
+
daemonAlive: existsSync(socketPath),
|
|
188
|
+
totalChunks: stats.totalChunks,
|
|
189
|
+
embeddingProvider: stats.embeddingProvider,
|
|
190
|
+
lastIndexedAt: stats.lastIndexed || null,
|
|
191
|
+
repos: stats.repos,
|
|
192
|
+
profilesAvailable: stats.repos.map((r) => ({
|
|
193
|
+
name: r.name,
|
|
194
|
+
hasProfile: !!loadProfile(project, r.name)
|
|
195
|
+
}))
|
|
196
|
+
}, null, 2) + "\n");
|
|
197
|
+
}
|
|
198
|
+
async function cmdReset(rest) {
|
|
199
|
+
const project = takeFlag(rest, "--project") ?? projectFromPath(process.cwd());
|
|
200
|
+
const basePath = getKnowledgeBasePath(project);
|
|
201
|
+
if (existsSync(basePath)) {
|
|
202
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
203
|
+
process.stderr.write(`[code-search] removed ${basePath}
|
|
204
|
+
`);
|
|
205
|
+
} else {
|
|
206
|
+
process.stderr.write(`[code-search] nothing to remove at ${basePath}
|
|
207
|
+
`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function cmdDaemon(rest) {
|
|
211
|
+
const here = new URL(import.meta.url);
|
|
212
|
+
const daemonEntry = join(new URL("..", here).pathname, "daemon", "index.js");
|
|
213
|
+
const child = spawn(process.execPath, [daemonEntry, ...rest], {
|
|
214
|
+
stdio: "inherit",
|
|
215
|
+
detached: false
|
|
216
|
+
});
|
|
217
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
218
|
+
}
|
|
219
|
+
async function cmdServe(rest) {
|
|
220
|
+
process.argv = [process.argv[0], process.argv[1], "--serve", ...rest];
|
|
221
|
+
await import("../index.js");
|
|
222
|
+
}
|
|
223
|
+
async function cmdMcp(rest) {
|
|
224
|
+
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
225
|
+
await import("../index.js");
|
|
226
|
+
}
|
|
227
|
+
main().catch((err) => {
|
|
228
|
+
process.stderr.write(`code-search fatal: ${err instanceof Error ? err.stack : err}
|
|
229
|
+
`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
package/dist/core/config.js
CHANGED
|
@@ -1,59 +1,372 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
function defaultDataDir() {
|
|
5
|
+
return process.env.CODE_SEARCH_DATA_DIR ?? join(homedir(), ".code-search", "index");
|
|
6
|
+
}
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
server: {
|
|
9
|
+
transport: "stdio",
|
|
10
|
+
port: 3100,
|
|
11
|
+
host: "127.0.0.1"
|
|
12
|
+
},
|
|
13
|
+
auth: {
|
|
14
|
+
mode: "none",
|
|
15
|
+
apiKeys: [],
|
|
16
|
+
jwtSecret: void 0,
|
|
17
|
+
jwtIssuer: "code-search-mcp",
|
|
18
|
+
rateLimitPerMinute: 100
|
|
19
|
+
},
|
|
20
|
+
storage: {
|
|
21
|
+
dataDir: defaultDataDir()
|
|
22
|
+
},
|
|
23
|
+
embedding: {
|
|
24
|
+
provider: "auto",
|
|
25
|
+
dimensions: 1024
|
|
26
|
+
},
|
|
27
|
+
reranker: {
|
|
28
|
+
provider: "ollama"
|
|
29
|
+
},
|
|
7
30
|
retrieval: {
|
|
8
|
-
maxChunks:
|
|
31
|
+
maxChunks: 8,
|
|
9
32
|
maxTokens: 12e3,
|
|
10
|
-
hybridWeights: { vector: 0.5, bm25: 0.3, graph: 0.2 }
|
|
11
|
-
reranker: "ollama"
|
|
33
|
+
hybridWeights: { vector: 0.5, bm25: 0.3, graph: 0.2 }
|
|
12
34
|
},
|
|
13
|
-
|
|
35
|
+
indexing: {
|
|
36
|
+
autoIndex: true,
|
|
37
|
+
reindexIntervalMs: 0,
|
|
38
|
+
chunking: { maxTokens: 500, contextEnrichment: "structural" },
|
|
39
|
+
respectGitignore: true,
|
|
40
|
+
debounceMs: 500,
|
|
41
|
+
ignorePatterns: ["node_modules", ".git", "dist", "build", ".next", "__pycache__", ".venv", "vendor", "target"]
|
|
42
|
+
},
|
|
43
|
+
llm: {
|
|
44
|
+
mode: "none",
|
|
45
|
+
provider: "anthropic",
|
|
46
|
+
model: "sonnet",
|
|
47
|
+
apiKey: void 0,
|
|
48
|
+
baseUrl: void 0,
|
|
49
|
+
claudeBin: "claude"
|
|
50
|
+
},
|
|
51
|
+
github: {
|
|
52
|
+
token: void 0
|
|
53
|
+
},
|
|
54
|
+
telemetry: {
|
|
55
|
+
otelEndpoint: void 0,
|
|
56
|
+
otelHeaders: void 0,
|
|
57
|
+
metricsEnabled: false,
|
|
58
|
+
structuredLogs: false,
|
|
59
|
+
recordQueries: false
|
|
60
|
+
},
|
|
61
|
+
daemon: {
|
|
62
|
+
autoSpawn: false,
|
|
63
|
+
disabled: false
|
|
64
|
+
}
|
|
14
65
|
};
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
function isPlainObject(v) {
|
|
67
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
68
|
+
}
|
|
69
|
+
function deepMerge(base, ...patches) {
|
|
70
|
+
const out = { ...base };
|
|
71
|
+
for (const p of patches) {
|
|
72
|
+
if (!p) continue;
|
|
73
|
+
for (const [k, v] of Object.entries(p)) {
|
|
74
|
+
if (v === void 0) continue;
|
|
75
|
+
if (isPlainObject(v) && isPlainObject(out[k])) {
|
|
76
|
+
out[k] = deepMerge(out[k], v);
|
|
77
|
+
} else {
|
|
78
|
+
out[k] = v;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function parseYaml(src) {
|
|
85
|
+
const lines = src.split(/\r?\n/);
|
|
86
|
+
const root = {};
|
|
87
|
+
const stack = [{ obj: root, indent: -1 }];
|
|
88
|
+
for (const rawLine of lines) {
|
|
89
|
+
const line = rawLine.replace(/#.*$/, "").replace(/\s+$/, "");
|
|
90
|
+
if (!line.trim()) continue;
|
|
91
|
+
const indent = line.length - line.trimStart().length;
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
const colon = trimmed.indexOf(":");
|
|
94
|
+
if (colon < 0) continue;
|
|
95
|
+
const key = trimmed.slice(0, colon).trim();
|
|
96
|
+
const valRaw = trimmed.slice(colon + 1).trim();
|
|
97
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
98
|
+
const parent = stack[stack.length - 1].obj;
|
|
99
|
+
if (!valRaw) {
|
|
100
|
+
const child = {};
|
|
101
|
+
parent[key] = child;
|
|
102
|
+
stack.push({ obj: child, indent });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
parent[key] = coerce(valRaw);
|
|
106
|
+
}
|
|
107
|
+
return root;
|
|
108
|
+
}
|
|
109
|
+
function coerce(raw) {
|
|
110
|
+
let v = raw;
|
|
111
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
112
|
+
return v.slice(1, -1);
|
|
113
|
+
}
|
|
114
|
+
if (v === "true") return true;
|
|
115
|
+
if (v === "false") return false;
|
|
116
|
+
if (v === "null" || v === "~") return null;
|
|
117
|
+
if (/^-?\d+$/.test(v)) return parseInt(v, 10);
|
|
118
|
+
if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
|
|
119
|
+
if (v.startsWith("[") && v.endsWith("]")) {
|
|
120
|
+
return v.slice(1, -1).split(",").map((s) => coerce(s.trim()));
|
|
121
|
+
}
|
|
122
|
+
if (v.startsWith("{") && v.endsWith("}")) {
|
|
123
|
+
const o = {};
|
|
124
|
+
for (const part of v.slice(1, -1).split(",")) {
|
|
125
|
+
const [k, x] = part.split(":");
|
|
126
|
+
if (k && x !== void 0) o[k.trim()] = coerce(x.trim());
|
|
127
|
+
}
|
|
128
|
+
return o;
|
|
129
|
+
}
|
|
130
|
+
if (v.startsWith("~")) return v.replace(/^~/, homedir());
|
|
131
|
+
return v;
|
|
132
|
+
}
|
|
133
|
+
function snakeToCamel(obj) {
|
|
134
|
+
if (Array.isArray(obj)) return obj.map(snakeToCamel);
|
|
135
|
+
if (!isPlainObject(obj)) return obj;
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
138
|
+
const camel = k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
139
|
+
out[camel] = snakeToCamel(v);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function loadYamlFile(path) {
|
|
144
|
+
if (!existsSync(path)) return null;
|
|
145
|
+
try {
|
|
146
|
+
const src = readFileSync(path, "utf-8");
|
|
147
|
+
const raw = parseYaml(src);
|
|
148
|
+
return snakeToCamel(raw);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function parseHeaders(s) {
|
|
154
|
+
if (!s) return void 0;
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const pair of s.split(",")) {
|
|
157
|
+
const eq = pair.indexOf("=");
|
|
158
|
+
if (eq > 0) out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
159
|
+
}
|
|
160
|
+
return Object.keys(out).length ? out : void 0;
|
|
161
|
+
}
|
|
162
|
+
function parseReindexMs(raw) {
|
|
163
|
+
if (!raw || raw === "0" || raw === "none") return 0;
|
|
164
|
+
const m = raw.match(/^(\d+)(m|h)$/);
|
|
165
|
+
if (!m) {
|
|
166
|
+
const n = parseInt(raw, 10);
|
|
167
|
+
return Number.isFinite(n) ? n : 0;
|
|
168
|
+
}
|
|
169
|
+
return m[2] === "h" ? parseInt(m[1], 10) * 60 * 6e4 : parseInt(m[1], 10) * 6e4;
|
|
170
|
+
}
|
|
171
|
+
function envLayer() {
|
|
172
|
+
const env = (k) => process.env[`CODE_SEARCH_${k}`];
|
|
173
|
+
const authMode = env("AUTH_MODE");
|
|
174
|
+
const transport = env("TRANSPORT");
|
|
175
|
+
const port = env("PORT");
|
|
176
|
+
const rateLimit = env("RATE_LIMIT_PER_MINUTE");
|
|
177
|
+
const maxChunks = env("RETRIEVAL_MAX_CHUNKS");
|
|
178
|
+
const maxTokens = env("RETRIEVAL_MAX_TOKENS");
|
|
179
|
+
const chunkMaxTokens = env("CHUNKING_MAX_TOKENS");
|
|
180
|
+
const debounce = env("INDEXING_DEBOUNCE_MS");
|
|
181
|
+
const embedDims = env("EMBEDDING_DIMENSIONS");
|
|
182
|
+
return {
|
|
183
|
+
server: {
|
|
184
|
+
transport,
|
|
185
|
+
port: port ? parseInt(port, 10) : void 0,
|
|
186
|
+
host: env("HOST") ?? (authMode === "none" || !authMode ? void 0 : "0.0.0.0")
|
|
187
|
+
},
|
|
188
|
+
auth: {
|
|
189
|
+
mode: authMode,
|
|
190
|
+
apiKeys: env("AUTH_API_KEYS")?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
191
|
+
jwtSecret: env("AUTH_JWT_SECRET"),
|
|
192
|
+
jwtIssuer: env("AUTH_JWT_ISSUER"),
|
|
193
|
+
rateLimitPerMinute: rateLimit ? parseInt(rateLimit, 10) : void 0
|
|
194
|
+
},
|
|
195
|
+
storage: {
|
|
196
|
+
dataDir: env("DATA_DIR")
|
|
197
|
+
},
|
|
198
|
+
embedding: {
|
|
199
|
+
provider: env("EMBEDDING_PROVIDER"),
|
|
200
|
+
model: env("EMBEDDING_MODEL"),
|
|
201
|
+
dimensions: embedDims ? parseInt(embedDims, 10) : void 0,
|
|
202
|
+
apiKey: env("EMBEDDING_API_KEY"),
|
|
203
|
+
baseUrl: env("EMBEDDING_BASE_URL"),
|
|
204
|
+
ollamaHost: env("OLLAMA_HOST") ?? process.env.OLLAMA_HOST
|
|
205
|
+
},
|
|
206
|
+
reranker: {
|
|
207
|
+
provider: env("RERANKER_PROVIDER"),
|
|
208
|
+
model: env("RERANKER_MODEL"),
|
|
209
|
+
apiKey: env("RERANKER_API_KEY"),
|
|
210
|
+
baseUrl: env("RERANKER_BASE_URL")
|
|
211
|
+
},
|
|
212
|
+
retrieval: {
|
|
213
|
+
maxChunks: maxChunks ? parseInt(maxChunks, 10) : void 0,
|
|
214
|
+
maxTokens: maxTokens ? parseInt(maxTokens, 10) : void 0
|
|
215
|
+
},
|
|
216
|
+
indexing: {
|
|
217
|
+
autoIndex: env("AUTO_INDEX") !== void 0 ? env("AUTO_INDEX") !== "false" && env("AUTO_INDEX") !== "0" : void 0,
|
|
218
|
+
reindexIntervalMs: env("REINDEX_INTERVAL") !== void 0 ? parseReindexMs(env("REINDEX_INTERVAL")) : void 0,
|
|
219
|
+
chunking: {
|
|
220
|
+
maxTokens: chunkMaxTokens ? parseInt(chunkMaxTokens, 10) : void 0
|
|
221
|
+
},
|
|
222
|
+
debounceMs: debounce ? parseInt(debounce, 10) : void 0
|
|
223
|
+
},
|
|
224
|
+
llm: {
|
|
225
|
+
mode: env("LLM_MODE"),
|
|
226
|
+
provider: env("LLM_PROVIDER"),
|
|
227
|
+
model: env("LLM_MODEL"),
|
|
228
|
+
apiKey: env("LLM_API_KEY") ?? process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
|
|
229
|
+
baseUrl: env("LLM_BASE_URL"),
|
|
230
|
+
claudeBin: env("CLAUDE_BIN") ?? process.env.CLAUDE_BIN
|
|
231
|
+
},
|
|
232
|
+
github: {
|
|
233
|
+
token: env("GITHUB_TOKEN") ?? process.env.GITHUB_TOKEN
|
|
234
|
+
},
|
|
235
|
+
telemetry: {
|
|
236
|
+
otelEndpoint: env("OTEL_ENDPOINT") ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
237
|
+
otelHeaders: parseHeaders(env("OTEL_HEADERS") ?? process.env.OTEL_EXPORTER_OTLP_HEADERS),
|
|
238
|
+
metricsEnabled: env("METRICS_ENABLED") === "1" || env("METRICS_ENABLED") === "true" ? true : void 0,
|
|
239
|
+
structuredLogs: env("STRUCTURED_LOGS") === "1" || env("STRUCTURED_LOGS") === "true" ? true : void 0,
|
|
240
|
+
recordQueries: env("TELEMETRY_RECORD_QUERIES") === "1" || env("TELEMETRY_RECORD_QUERIES") === "true" ? true : void 0
|
|
241
|
+
},
|
|
242
|
+
daemon: {
|
|
243
|
+
autoSpawn: env("DAEMON_AUTO_SPAWN") === "1" || env("DAEMON_AUTO_SPAWN") === "true" ? true : void 0,
|
|
244
|
+
disabled: env("DAEMON_DISABLED") === "1" || env("DAEMON_DISABLED") === "true" ? true : void 0
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const RESERVED_FLAGS = /* @__PURE__ */ new Set([
|
|
249
|
+
"--local",
|
|
250
|
+
"--remote",
|
|
251
|
+
"--serve",
|
|
252
|
+
"--help",
|
|
253
|
+
"-h",
|
|
254
|
+
"--print-config",
|
|
255
|
+
"--api-key",
|
|
256
|
+
"--project",
|
|
257
|
+
"--token",
|
|
258
|
+
"--force",
|
|
259
|
+
"--workspace",
|
|
260
|
+
"--no-daemon",
|
|
261
|
+
"--auto-spawn-daemon",
|
|
262
|
+
"--config",
|
|
263
|
+
"--format",
|
|
264
|
+
"--top-k",
|
|
265
|
+
"--mode",
|
|
266
|
+
"--repo",
|
|
267
|
+
"--language"
|
|
268
|
+
]);
|
|
269
|
+
function parseCliFlags(argv) {
|
|
270
|
+
const patch = {};
|
|
271
|
+
const rest = [];
|
|
272
|
+
for (let i = 0; i < argv.length; i++) {
|
|
273
|
+
const arg = argv[i];
|
|
274
|
+
if (!arg.startsWith("--") || RESERVED_FLAGS.has(arg)) {
|
|
275
|
+
rest.push(arg);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
let key = arg.slice(2);
|
|
279
|
+
let negate = false;
|
|
280
|
+
if (key.startsWith("no-")) {
|
|
281
|
+
negate = true;
|
|
282
|
+
key = key.slice(3);
|
|
283
|
+
}
|
|
284
|
+
const path = key.split(".").map((seg) => seg.replace(/-([a-z])/g, (_, c) => c.toUpperCase()));
|
|
285
|
+
let val;
|
|
286
|
+
if (negate) {
|
|
287
|
+
val = false;
|
|
288
|
+
} else if (argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) {
|
|
289
|
+
val = coerce(argv[++i]);
|
|
290
|
+
} else {
|
|
291
|
+
val = true;
|
|
292
|
+
}
|
|
293
|
+
setDeep(patch, path, val);
|
|
294
|
+
}
|
|
295
|
+
return { patch, rest };
|
|
296
|
+
}
|
|
297
|
+
function setDeep(obj, path, value) {
|
|
298
|
+
let cur = obj;
|
|
299
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
300
|
+
const k = path[i];
|
|
301
|
+
if (!isPlainObject(cur[k])) cur[k] = {};
|
|
302
|
+
cur = cur[k];
|
|
303
|
+
}
|
|
304
|
+
cur[path[path.length - 1]] = value;
|
|
305
|
+
}
|
|
306
|
+
function resolveCodeSearchConfig(opts = {}) {
|
|
307
|
+
const globalConfigPath = opts.configPath ?? join(homedir(), ".code-search", "config.yaml");
|
|
308
|
+
const yaml1 = loadYamlFile(globalConfigPath) ?? {};
|
|
309
|
+
const yaml2 = opts.workspaceDir ? loadYamlFile(join(opts.workspaceDir, ".code-search.yaml")) ?? {} : {};
|
|
310
|
+
const env = envLayer();
|
|
311
|
+
const cli = opts.cli ?? {};
|
|
312
|
+
const cfg = deepMerge(DEFAULTS, yaml1, yaml2, env, cli);
|
|
313
|
+
bridgeLegacyEnvVars(cfg);
|
|
314
|
+
return cfg;
|
|
315
|
+
}
|
|
316
|
+
function bridgeLegacyEnvVars(cfg) {
|
|
317
|
+
process.env.ANVIL_LLM_MODE ??= process.env.CODE_SEARCH_LLM_MODE ?? cfg.llm.mode;
|
|
318
|
+
process.env.ANVIL_LLM_PROVIDER ??= process.env.CODE_SEARCH_LLM_PROVIDER ?? cfg.llm.provider;
|
|
319
|
+
process.env.ANVIL_LLM_MODEL ??= process.env.CODE_SEARCH_LLM_MODEL ?? cfg.llm.model;
|
|
320
|
+
if (cfg.llm.apiKey) process.env.ANVIL_LLM_API_KEY ??= cfg.llm.apiKey;
|
|
321
|
+
if (cfg.llm.baseUrl) process.env.ANVIL_LLM_BASE_URL ??= cfg.llm.baseUrl;
|
|
322
|
+
if (cfg.llm.claudeBin) process.env.ANVIL_CLAUDE_BIN ??= cfg.llm.claudeBin;
|
|
323
|
+
}
|
|
324
|
+
function toKnowledgeConfig(c) {
|
|
325
|
+
return {
|
|
326
|
+
embedding: { ...c.embedding },
|
|
327
|
+
chunking: { ...c.indexing.chunking },
|
|
328
|
+
retrieval: {
|
|
329
|
+
maxChunks: c.retrieval.maxChunks,
|
|
330
|
+
maxTokens: c.retrieval.maxTokens,
|
|
331
|
+
hybridWeights: { ...c.retrieval.hybridWeights },
|
|
332
|
+
reranker: { ...c.reranker }
|
|
333
|
+
},
|
|
334
|
+
autoIndex: c.indexing.autoIndex
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const SECRET_KEYS = /* @__PURE__ */ new Set([
|
|
338
|
+
"apiKey",
|
|
339
|
+
"jwtSecret",
|
|
340
|
+
"token",
|
|
341
|
+
"githubToken"
|
|
342
|
+
]);
|
|
343
|
+
function redactSecrets(value, parentKey = "") {
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
return value.map((v) => redactSecrets(v, parentKey));
|
|
346
|
+
}
|
|
347
|
+
if (isPlainObject(value)) {
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [k, v] of Object.entries(value)) {
|
|
350
|
+
if (SECRET_KEYS.has(k) && typeof v === "string" && v.length > 0) {
|
|
351
|
+
out[k] = `***redacted (${v.length} chars)`;
|
|
352
|
+
} else if (k === "apiKeys" && Array.isArray(v)) {
|
|
353
|
+
out[k] = v.map((s) => `***redacted (${s.length} chars)`);
|
|
354
|
+
} else {
|
|
355
|
+
out[k] = redactSecrets(v, k);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
function printConfig(c) {
|
|
363
|
+
return JSON.stringify(redactSecrets(c), null, 2);
|
|
54
364
|
}
|
|
55
365
|
export {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
366
|
+
DEFAULTS,
|
|
367
|
+
parseCliFlags,
|
|
368
|
+
printConfig,
|
|
369
|
+
redactSecrets,
|
|
370
|
+
resolveCodeSearchConfig,
|
|
371
|
+
toKnowledgeConfig
|
|
59
372
|
};
|