@esankhan3/code-search-mcp 0.1.0 → 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 +174 -19
- 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/backends/types.js +0 -0
- package/dist/cli/index.js +231 -0
- package/dist/core/config.js +372 -0
- 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/server.js +4 -0
- package/dist/transports/http-transport.js +38 -0
- package/package.json +18 -6
- package/scripts/smoke/run-smoke.sh +167 -0
package/README.md
CHANGED
|
@@ -39,9 +39,43 @@ That's the whole setup. Claude Desktop now knows your codebase.
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
## Three
|
|
42
|
+
## Three bin entries (one install)
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
The npm package ships three binaries. Pick the one that matches the
|
|
45
|
+
job — none of them need a separate install.
|
|
46
|
+
|
|
47
|
+
| Bin | Use case |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `code-search-mcp` | The MCP-server entry. Three operating modes below. |
|
|
50
|
+
| `code-search` | Standalone CLI: `index` / `query` / `status` / `reset` / `daemon` / `serve` / `mcp`. |
|
|
51
|
+
| `code-search-daemon` | Long-running indexer with file-watcher and JSON-RPC socket. |
|
|
52
|
+
|
|
53
|
+
### Standalone CLI
|
|
54
|
+
```sh
|
|
55
|
+
code-search index . # index current dir
|
|
56
|
+
code-search query "where do we verify JWTs" # hybrid by default
|
|
57
|
+
code-search query "createPet" --mode bm25 --top-k 5 # exact-identifier mode
|
|
58
|
+
code-search status --project petshop # JSON report
|
|
59
|
+
code-search reset --project petshop # drop the index
|
|
60
|
+
code-search --print-config # resolved config (redacted)
|
|
61
|
+
```
|
|
62
|
+
Every `CodeSearchConfig` leaf is settable as `--<dotted-path>` —
|
|
63
|
+
e.g. `--embedding.provider codestral`, `--retrieval.max-chunks 12`,
|
|
64
|
+
`--no-indexing.auto-index`.
|
|
65
|
+
|
|
66
|
+
### Daemon mode (long-running indexer)
|
|
67
|
+
```sh
|
|
68
|
+
code-search-daemon --workspace /path/to/repos
|
|
69
|
+
```
|
|
70
|
+
Builds the index once, then watches the workspace and debounce-
|
|
71
|
+
reindexes on file events. Listens on a UDS socket at
|
|
72
|
+
`<dataDir>/daemon/<project>.sock` (named pipe on Windows). The CLI
|
|
73
|
+
and MCP server automatically prefer the daemon when it's alive,
|
|
74
|
+
falling back to in-process when it isn't.
|
|
75
|
+
|
|
76
|
+
### MCP — three modes (`code-search-mcp` argv)
|
|
77
|
+
|
|
78
|
+
**Remote proxy (default)**
|
|
45
79
|
```sh
|
|
46
80
|
code-search-mcp # default — proxies to remote
|
|
47
81
|
code-search-mcp --remote URL
|
|
@@ -51,7 +85,7 @@ Zero local setup, zero local index — perfect for hosted
|
|
|
51
85
|
deployments where the index lives in the cloud and dev machines
|
|
52
86
|
stay light. Auth via `CODE_SEARCH_API_KEY`.
|
|
53
87
|
|
|
54
|
-
|
|
88
|
+
**Local**
|
|
55
89
|
```sh
|
|
56
90
|
code-search-mcp --local /path/to/repos
|
|
57
91
|
code-search-mcp --local github:my-org/my-pattern
|
|
@@ -60,14 +94,15 @@ Discovers every repo under a path (or clones a GitHub org), builds
|
|
|
60
94
|
the knowledge base, and serves over stdio. Works fully offline if
|
|
61
95
|
your embedder + reranker are local (Ollama).
|
|
62
96
|
|
|
63
|
-
|
|
97
|
+
**Serve**
|
|
64
98
|
```sh
|
|
65
99
|
code-search-mcp --serve --port 4000 --auth api-key
|
|
66
100
|
```
|
|
67
101
|
Boots an HTTP server (Streamable HTTP transport, SSE optional)
|
|
68
|
-
with `/mcp`, `/health`, `/
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
with `/mcp`, `/health`, `/ready`, `/version`, `/status`,
|
|
103
|
+
`/metrics` (Prometheus), `/admin/api/status`, and an admin
|
|
104
|
+
`POST /index`. Use this to host one index for a whole team — every
|
|
105
|
+
dev points their client at the same URL.
|
|
71
106
|
|
|
72
107
|
---
|
|
73
108
|
|
|
@@ -142,6 +177,45 @@ repos*. So does `impact_analysis`.
|
|
|
142
177
|
|
|
143
178
|
---
|
|
144
179
|
|
|
180
|
+
## Observability
|
|
181
|
+
|
|
182
|
+
`code-search-mcp --serve` exposes these endpoints out of the box —
|
|
183
|
+
no scrape config or APM agent required.
|
|
184
|
+
|
|
185
|
+
| Endpoint | Returns |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `GET /health` | Liveness ping with project + uptime |
|
|
188
|
+
| `GET /ready` | `200` only after first index completes |
|
|
189
|
+
| `GET /version` | Package + Node + platform |
|
|
190
|
+
| `GET /status` | Current indexing phase + last-success history |
|
|
191
|
+
| `GET /metrics` | Prometheus text format (queries, latency histogram, chunks, embedding/LLM calls, errors) |
|
|
192
|
+
| `GET /admin/api/status` | JSON status dashboard for the admin UI |
|
|
193
|
+
|
|
194
|
+
Set `CODE_SEARCH_STRUCTURED_LOGS=1` for one-JSON-line-per-event
|
|
195
|
+
stderr logs, and `CODE_SEARCH_TELEMETRY_RECORD_QUERIES=1` to opt
|
|
196
|
+
into recording query text in spans (off by default — queries are
|
|
197
|
+
PII-shaped).
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Multi-project / multi-tenant
|
|
202
|
+
|
|
203
|
+
Drop project entries under `<dataDir>/projects/<name>/project.yaml`:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
workspace: /Users/me/repos/petshop
|
|
207
|
+
scopes: [team-shop]
|
|
208
|
+
quotas:
|
|
209
|
+
max_queries_per_minute: 60
|
|
210
|
+
max_embedding_cost_usd: 5
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`ProjectRegistry` reads them at boot. `projectAccessAllowed`
|
|
214
|
+
gates by scope (`*` is admin). `checkProjectQuota` enforces a
|
|
215
|
+
sliding 1-minute window per `(identity, project)`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
145
219
|
## Server features
|
|
146
220
|
|
|
147
221
|
### Auth, three flavors
|
|
@@ -221,21 +295,64 @@ LanceDB store, same graph files.
|
|
|
221
295
|
|
|
222
296
|
---
|
|
223
297
|
|
|
224
|
-
## Configuration
|
|
298
|
+
## Configuration — defaults → file → env → CLI flags
|
|
299
|
+
|
|
300
|
+
Single resolver in `src/core/config.ts`. Five precedence layers
|
|
301
|
+
(lowest → highest):
|
|
302
|
+
|
|
303
|
+
1. compiled-in `DEFAULTS`
|
|
304
|
+
2. `~/.code-search/config.yaml` (user-global)
|
|
305
|
+
3. `<workspaceDir>/.code-search.yaml` (per-workspace overlay)
|
|
306
|
+
4. `CODE_SEARCH_*` env vars
|
|
307
|
+
5. CLI flags via `--<dotted-path>` (e.g. `--embedding.provider codestral`)
|
|
225
308
|
|
|
226
|
-
|
|
227
|
-
|
|
309
|
+
Run `code-search --print-config` or `code-search-mcp --print-config`
|
|
310
|
+
any time to see the resolved struct (secrets redacted).
|
|
228
311
|
|
|
229
|
-
| Var |
|
|
312
|
+
| Var (`CODE_SEARCH_*`) | Effect |
|
|
230
313
|
|---|---|
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
314
|
+
| `SERVER` | Remote URL (proxy mode) |
|
|
315
|
+
| `API_KEY` | API key for proxy or serve modes |
|
|
316
|
+
| `DATA_DIR` | Override the index storage root |
|
|
317
|
+
| `PORT` / `HOST` / `TRANSPORT` | HTTP server bind |
|
|
318
|
+
| `AUTH_MODE` / `AUTH_API_KEYS` / `AUTH_JWT_SECRET` | Server auth |
|
|
319
|
+
| `REINDEX_INTERVAL` | `30m` / `1h` / `6h` / `0` |
|
|
320
|
+
| `EMBEDDING_PROVIDER` | `auto` / `codestral` / `voyage` / `openai` / `ollama` / `openai-compatible` / `custom` / `gemini-oauth` |
|
|
321
|
+
| `EMBEDDING_MODEL` / `EMBEDDING_DIMENSIONS` | Model + dims |
|
|
322
|
+
| `EMBEDDING_API_KEY` / `EMBEDDING_BASE_URL` | Credentials |
|
|
238
323
|
| `OLLAMA_HOST` | Default `http://localhost:11434` |
|
|
324
|
+
| `RERANKER_PROVIDER` | `ollama` (default) / `cohere` / `voyage` / `openai-compatible` / `custom` / `none` |
|
|
325
|
+
| `RERANKER_MODEL` / `RERANKER_API_KEY` / `RERANKER_BASE_URL` | Reranker config |
|
|
326
|
+
| `RETRIEVAL_MAX_CHUNKS` / `RETRIEVAL_MAX_TOKENS` | Retrieval tuning |
|
|
327
|
+
| `AUTO_INDEX` | `false` to disable startup auto-index |
|
|
328
|
+
| `DAEMON_DISABLED` / `DAEMON_AUTO_SPAWN` | Daemon backend control |
|
|
329
|
+
| `LLM_MODE` / `LLM_PROVIDER` / `LLM_MODEL` / `LLM_API_KEY` / `LLM_BASE_URL` | Profiling + service-mesh LLM. Pre-promoted to `ANVIL_LLM_*` so the agent-core integration is silent. |
|
|
330
|
+
| `INDEXING_DEBOUNCE_MS` / `CHUNKING_MAX_TOKENS` | Indexer knobs |
|
|
331
|
+
| `METRICS_ENABLED` / `STRUCTURED_LOGS` / `TELEMETRY_RECORD_QUERIES` | Observability |
|
|
332
|
+
|
|
333
|
+
### Per-workspace config
|
|
334
|
+
|
|
335
|
+
Drop a `.code-search.yaml` at the root of your workspace:
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
embedding:
|
|
339
|
+
provider: codestral
|
|
340
|
+
model: codestral-embed-2505
|
|
341
|
+
dimensions: 1024
|
|
342
|
+
reranker:
|
|
343
|
+
provider: ollama
|
|
344
|
+
model: qwen2.5-coder:7b # the default that actually answers
|
|
345
|
+
retrieval:
|
|
346
|
+
max_chunks: 12
|
|
347
|
+
hybrid_weights: { vector: 0.55, bm25: 0.3, graph: 0.15 }
|
|
348
|
+
indexing:
|
|
349
|
+
auto_index: true
|
|
350
|
+
debounce_ms: 500
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
CLI flags override env, env overrides this file, this file overrides
|
|
354
|
+
the user-global `~/.code-search/config.yaml`, that overrides
|
|
355
|
+
`DEFAULTS`.
|
|
239
356
|
|
|
240
357
|
---
|
|
241
358
|
|
|
@@ -262,11 +379,49 @@ logs a warning instead of pretending it's fine.
|
|
|
262
379
|
|
|
263
380
|
---
|
|
264
381
|
|
|
382
|
+
## End-to-end smoke test (T1–T8)
|
|
383
|
+
|
|
384
|
+
The eight-case smoke that pins the standalone product contract lives
|
|
385
|
+
at `scripts/smoke/run-smoke.sh`. It spins up a tiny `pet-shop` git
|
|
386
|
+
fixture under `/tmp/cs-smoke/`, then runs:
|
|
387
|
+
|
|
388
|
+
| # | Verifies |
|
|
389
|
+
|---|---|
|
|
390
|
+
| **T1** | `--print-config` env + CLI flag layering, redaction |
|
|
391
|
+
| **T2** | `code-search index` produces a real Ollama-backed index; no stale legacy-env warning |
|
|
392
|
+
| **T3** | `code-search query --mode vector` and `--mode bm25` both return the right file |
|
|
393
|
+
| **T4** | `code-search status` reports provider + chunk count |
|
|
394
|
+
| **T5** | Issue-#6 reproduction: `CODE_SEARCH_EMBEDDING_PROVIDER=openai` actually constructs the OpenAI embedder (skipped when `OPENAI_API_KEY` is unset) |
|
|
395
|
+
| **T6** | Vector-space mismatch guard hard-errors on cross-provider query |
|
|
396
|
+
| **T7** | Daemon round-trip: spawn, UDS socket, query through RPC, file-watcher debounce-reindex |
|
|
397
|
+
| **T8** | HTTP serve: `/health` `/ready` `/version` `/metrics` `/admin/api/status` all 200; Prom text format |
|
|
398
|
+
|
|
399
|
+
Run it locally:
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
npm -w @esankhan3/code-search-mcp run build # required first
|
|
403
|
+
npm -w @esankhan3/code-search-mcp run smoke # T1–T8
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Requirements: Ollama on `$OLLAMA_HOST` (default `localhost:11434`)
|
|
407
|
+
with `bge-m3` pulled. Pass `OPENAI_API_KEY=...` to exercise the T5
|
|
408
|
+
provider-switch path against the real OpenAI endpoint.
|
|
409
|
+
|
|
410
|
+
The smoke runs **outside CI** — it requires Ollama + real models —
|
|
411
|
+
so it's a hand-rolled local check, not a release gate. The release
|
|
412
|
+
workflow does enforce a static guard that every bin entry
|
|
413
|
+
(`code-search-mcp` / `code-search` / `code-search-daemon`) is in the
|
|
414
|
+
built tarball before publishing.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
265
418
|
## Status
|
|
266
419
|
|
|
267
420
|
Stable. The retrieval and graph layers move with
|
|
268
421
|
`@anvil/knowledge-core`; the MCP surface is locked. New tools
|
|
269
|
-
land additively.
|
|
422
|
+
land additively. P0–P8 of the standalone-product plan
|
|
423
|
+
(`docs/CODE-SEARCH-MCP-STANDALONE-PLAN.md`) have landed; F1–F4
|
|
424
|
+
follow-up fixes verified by `scripts/smoke/run-smoke.sh`.
|
|
270
425
|
|
|
271
426
|
---
|
|
272
427
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir, homedir } from "node:os";
|
|
6
|
+
import {
|
|
7
|
+
resolveCodeSearchConfig,
|
|
8
|
+
parseCliFlags,
|
|
9
|
+
toKnowledgeConfig,
|
|
10
|
+
redactSecrets,
|
|
11
|
+
DEFAULTS
|
|
12
|
+
} from "../core/config.js";
|
|
13
|
+
const ENV_KEYS = [
|
|
14
|
+
"CODE_SEARCH_EMBEDDING_PROVIDER",
|
|
15
|
+
"CODE_SEARCH_EMBEDDING_MODEL",
|
|
16
|
+
"CODE_SEARCH_EMBEDDING_DIMENSIONS",
|
|
17
|
+
"CODE_SEARCH_EMBEDDING_API_KEY",
|
|
18
|
+
"CODE_SEARCH_RERANKER_PROVIDER",
|
|
19
|
+
"CODE_SEARCH_PORT",
|
|
20
|
+
"CODE_SEARCH_AUTH_MODE",
|
|
21
|
+
"CODE_SEARCH_AUTH_API_KEYS",
|
|
22
|
+
"CODE_SEARCH_DATA_DIR",
|
|
23
|
+
"CODE_SEARCH_REINDEX_INTERVAL"
|
|
24
|
+
];
|
|
25
|
+
let tmp = "";
|
|
26
|
+
const STASH = {};
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
for (const k of ENV_KEYS) {
|
|
29
|
+
STASH[k] = process.env[k];
|
|
30
|
+
delete process.env[k];
|
|
31
|
+
}
|
|
32
|
+
tmp = mkdtempSync(join(tmpdir(), "cs-cfg-"));
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
36
|
+
for (const k of ENV_KEYS) {
|
|
37
|
+
if (STASH[k] === void 0) delete process.env[k];
|
|
38
|
+
else process.env[k] = STASH[k];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
describe("resolveCodeSearchConfig precedence (P3)", () => {
|
|
42
|
+
it("returns DEFAULTS when no file / env / CLI is present", () => {
|
|
43
|
+
const cfg = resolveCodeSearchConfig({ configPath: join(tmp, "missing.yaml") });
|
|
44
|
+
assert.equal(cfg.embedding.provider, "auto");
|
|
45
|
+
assert.equal(cfg.server.port, 3100);
|
|
46
|
+
assert.equal(cfg.indexing.autoIndex, true);
|
|
47
|
+
});
|
|
48
|
+
it("env overrides defaults", () => {
|
|
49
|
+
process.env.CODE_SEARCH_EMBEDDING_PROVIDER = "codestral";
|
|
50
|
+
process.env.CODE_SEARCH_EMBEDDING_MODEL = "codestral-embed-2505";
|
|
51
|
+
process.env.CODE_SEARCH_EMBEDDING_API_KEY = "sk-abc";
|
|
52
|
+
process.env.CODE_SEARCH_PORT = "4242";
|
|
53
|
+
const cfg = resolveCodeSearchConfig({ configPath: join(tmp, "missing.yaml") });
|
|
54
|
+
assert.equal(cfg.embedding.provider, "codestral");
|
|
55
|
+
assert.equal(cfg.embedding.model, "codestral-embed-2505");
|
|
56
|
+
assert.equal(cfg.embedding.apiKey, "sk-abc");
|
|
57
|
+
assert.equal(cfg.server.port, 4242);
|
|
58
|
+
});
|
|
59
|
+
it("YAML config file overrides defaults", () => {
|
|
60
|
+
const configPath = join(tmp, "config.yaml");
|
|
61
|
+
writeFileSync(
|
|
62
|
+
configPath,
|
|
63
|
+
`server:
|
|
64
|
+
port: 5500
|
|
65
|
+
embedding:
|
|
66
|
+
provider: voyage
|
|
67
|
+
model: voyage-code-3
|
|
68
|
+
`,
|
|
69
|
+
"utf-8"
|
|
70
|
+
);
|
|
71
|
+
const cfg = resolveCodeSearchConfig({ configPath });
|
|
72
|
+
assert.equal(cfg.server.port, 5500);
|
|
73
|
+
assert.equal(cfg.embedding.provider, "voyage");
|
|
74
|
+
assert.equal(cfg.embedding.model, "voyage-code-3");
|
|
75
|
+
});
|
|
76
|
+
it("CLI flags override env vars", () => {
|
|
77
|
+
process.env.CODE_SEARCH_EMBEDDING_PROVIDER = "voyage";
|
|
78
|
+
const { patch } = parseCliFlags(["--embedding.provider", "codestral", "--server.port", "6000"]);
|
|
79
|
+
const cfg = resolveCodeSearchConfig({ configPath: join(tmp, "missing.yaml"), cli: patch });
|
|
80
|
+
assert.equal(cfg.embedding.provider, "codestral");
|
|
81
|
+
assert.equal(cfg.server.port, 6e3);
|
|
82
|
+
});
|
|
83
|
+
it("per-workspace .code-search.yaml overrides ~/.code-search/config.yaml", () => {
|
|
84
|
+
const globalPath = join(tmp, "global.yaml");
|
|
85
|
+
writeFileSync(globalPath, `embedding:
|
|
86
|
+
provider: voyage
|
|
87
|
+
`, "utf-8");
|
|
88
|
+
const workspace = join(tmp, "workspace");
|
|
89
|
+
mkdirSync(workspace, { recursive: true });
|
|
90
|
+
writeFileSync(join(workspace, ".code-search.yaml"), `embedding:
|
|
91
|
+
provider: openai
|
|
92
|
+
`, "utf-8");
|
|
93
|
+
const cfg = resolveCodeSearchConfig({ configPath: globalPath, workspaceDir: workspace });
|
|
94
|
+
assert.equal(cfg.embedding.provider, "openai");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("toKnowledgeConfig (P3 adapter)", () => {
|
|
98
|
+
it("maps unified config to KnowledgeConfig shape", () => {
|
|
99
|
+
const cfg = resolveCodeSearchConfig({ configPath: join(tmp, "missing.yaml") });
|
|
100
|
+
const k = toKnowledgeConfig(cfg);
|
|
101
|
+
assert.equal(k.embedding.provider, cfg.embedding.provider);
|
|
102
|
+
assert.equal(k.retrieval.maxChunks, cfg.retrieval.maxChunks);
|
|
103
|
+
assert.deepEqual(k.chunking, cfg.indexing.chunking);
|
|
104
|
+
const r = typeof k.retrieval.reranker === "string" ? { provider: k.retrieval.reranker } : k.retrieval.reranker;
|
|
105
|
+
assert.equal(r.provider, cfg.reranker.provider);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("redactSecrets (P3 --print-config)", () => {
|
|
109
|
+
it("redacts apiKey/jwtSecret/token fields, preserves shape", () => {
|
|
110
|
+
const input = {
|
|
111
|
+
embedding: { apiKey: "sk-super-secret", model: "x" },
|
|
112
|
+
auth: { apiKeys: ["key-1", "key-2"], jwtSecret: "jwt-super-secret" },
|
|
113
|
+
github: { token: "gh-token" },
|
|
114
|
+
server: { port: 3100 }
|
|
115
|
+
};
|
|
116
|
+
const out = redactSecrets(input);
|
|
117
|
+
assert.match(out.embedding.apiKey, /redacted/);
|
|
118
|
+
assert.equal(out.embedding.model, "x");
|
|
119
|
+
assert.match(out.auth.apiKeys[0], /redacted/);
|
|
120
|
+
assert.match(out.auth.jwtSecret, /redacted/);
|
|
121
|
+
assert.match(out.github.token, /redacted/);
|
|
122
|
+
assert.equal(out.server.port, 3100);
|
|
123
|
+
});
|
|
124
|
+
it("keeps DEFAULTS shape redaction-safe (no secrets exposed)", () => {
|
|
125
|
+
const redacted = redactSecrets(DEFAULTS);
|
|
126
|
+
const json = JSON.stringify(redacted);
|
|
127
|
+
assert.equal(/sk-[a-zA-Z0-9]+/.test(json), false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("parseCliFlags", () => {
|
|
131
|
+
it("handles dotted paths and value coercion", () => {
|
|
132
|
+
const { patch } = parseCliFlags([
|
|
133
|
+
"--embedding.provider",
|
|
134
|
+
"voyage",
|
|
135
|
+
"--retrieval.max-chunks",
|
|
136
|
+
"20",
|
|
137
|
+
"--telemetry.metrics-enabled",
|
|
138
|
+
"true"
|
|
139
|
+
]);
|
|
140
|
+
assert.equal(patch.embedding.provider, "voyage");
|
|
141
|
+
assert.equal(patch.retrieval.maxChunks, 20);
|
|
142
|
+
assert.equal(patch.telemetry.metricsEnabled, true);
|
|
143
|
+
});
|
|
144
|
+
it("treats reserved subcommand flags as rest", () => {
|
|
145
|
+
const { rest } = parseCliFlags(["--local", "/some/path", "--embedding.provider", "codestral"]);
|
|
146
|
+
assert.ok(rest.includes("--local"));
|
|
147
|
+
});
|
|
148
|
+
it("--no-x.y sets boolean false", () => {
|
|
149
|
+
const { patch } = parseCliFlags(["--no-indexing.auto-index"]);
|
|
150
|
+
assert.equal(patch.indexing.autoIndex, false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
void homedir;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { RpcServer } from "../daemon/rpc-server.js";
|
|
7
|
+
import { DaemonBackend } from "../backends/daemon-client.js";
|
|
8
|
+
const tmp = mkdtempSync(join(tmpdir(), "cs-rpc-"));
|
|
9
|
+
const socketPath = join(tmp, "test.sock");
|
|
10
|
+
let server;
|
|
11
|
+
before(async () => {
|
|
12
|
+
server = new RpcServer({
|
|
13
|
+
socketPath,
|
|
14
|
+
handlers: {
|
|
15
|
+
"search.code": async (p) => ({ query: p.query, totalTokens: 0, chunks: [] }),
|
|
16
|
+
"index.status": async () => ({
|
|
17
|
+
totalChunks: 0,
|
|
18
|
+
repos: [],
|
|
19
|
+
embeddingProvider: "stub",
|
|
20
|
+
lastIndexedAt: null,
|
|
21
|
+
watching: true,
|
|
22
|
+
queueDepth: 0,
|
|
23
|
+
uptimeSec: 1
|
|
24
|
+
}),
|
|
25
|
+
"index.force": async () => ({
|
|
26
|
+
totalChunks: 0,
|
|
27
|
+
repos: [],
|
|
28
|
+
embeddingProvider: "stub",
|
|
29
|
+
lastIndexedAt: null,
|
|
30
|
+
watching: true,
|
|
31
|
+
queueDepth: 0,
|
|
32
|
+
uptimeSec: 1
|
|
33
|
+
}),
|
|
34
|
+
"index.invalidate": async () => ({ ok: true }),
|
|
35
|
+
"health": () => ({ ok: true, uptime: 1 })
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
await server.start();
|
|
39
|
+
});
|
|
40
|
+
after(async () => {
|
|
41
|
+
await server.stop();
|
|
42
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
describe("daemon RPC roundtrip", { skip: process.platform === "win32" }, () => {
|
|
45
|
+
it("ping returns true for a live daemon", async () => {
|
|
46
|
+
const client = new DaemonBackend({
|
|
47
|
+
project: "test",
|
|
48
|
+
workspaceDir: null,
|
|
49
|
+
knowledge: {},
|
|
50
|
+
preferDaemon: true,
|
|
51
|
+
socketPath
|
|
52
|
+
});
|
|
53
|
+
assert.equal(await client.ping(), true);
|
|
54
|
+
await client.close();
|
|
55
|
+
});
|
|
56
|
+
it("search returns the typed payload", async () => {
|
|
57
|
+
const client = new DaemonBackend({
|
|
58
|
+
project: "test",
|
|
59
|
+
workspaceDir: null,
|
|
60
|
+
knowledge: {},
|
|
61
|
+
preferDaemon: true,
|
|
62
|
+
socketPath
|
|
63
|
+
});
|
|
64
|
+
const r = await client.search("hello", { mode: "hybrid", maxResults: 5 });
|
|
65
|
+
assert.equal(r.query, "hello");
|
|
66
|
+
assert.equal(Array.isArray(r.chunks), true);
|
|
67
|
+
await client.close();
|
|
68
|
+
});
|
|
69
|
+
it("status / invalidate / forceIndex roundtrip", async () => {
|
|
70
|
+
const client = new DaemonBackend({
|
|
71
|
+
project: "test",
|
|
72
|
+
workspaceDir: null,
|
|
73
|
+
knowledge: {},
|
|
74
|
+
preferDaemon: true,
|
|
75
|
+
socketPath
|
|
76
|
+
});
|
|
77
|
+
const s = await client.status();
|
|
78
|
+
assert.equal(s.embeddingProvider, "stub");
|
|
79
|
+
assert.equal(s.watching, true);
|
|
80
|
+
await client.invalidate(["/some/path.ts"]);
|
|
81
|
+
const after2 = await client.forceIndex({ force: true });
|
|
82
|
+
assert.equal(after2.embeddingProvider, "stub");
|
|
83
|
+
await client.close();
|
|
84
|
+
});
|
|
85
|
+
it("unknown method returns RPC error", async () => {
|
|
86
|
+
const client = new DaemonBackend({
|
|
87
|
+
project: "test",
|
|
88
|
+
workspaceDir: null,
|
|
89
|
+
knowledge: {},
|
|
90
|
+
preferDaemon: true,
|
|
91
|
+
socketPath
|
|
92
|
+
});
|
|
93
|
+
await assert.rejects(
|
|
94
|
+
// @ts-expect-error — calling a private method on purpose
|
|
95
|
+
() => client.rpc("notARealMethod", {}, 500),
|
|
96
|
+
/Method not found/
|
|
97
|
+
);
|
|
98
|
+
await client.close();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Counter, Gauge, Histogram, registry, metrics } from "../observability/metrics.js";
|
|
4
|
+
import { Logger } from "../observability/logger.js";
|
|
5
|
+
describe("Prometheus collectors (P7)", () => {
|
|
6
|
+
it("Counter renders with labels", () => {
|
|
7
|
+
const c = new Counter("test_counter", "test counter");
|
|
8
|
+
c.inc({ kind: "a" });
|
|
9
|
+
c.inc({ kind: "a" });
|
|
10
|
+
c.inc({ kind: "b" });
|
|
11
|
+
const out = c.render();
|
|
12
|
+
assert.match(out, /# HELP test_counter test counter/);
|
|
13
|
+
assert.match(out, /# TYPE test_counter counter/);
|
|
14
|
+
assert.match(out, /test_counter\{kind="a"\} 2/);
|
|
15
|
+
assert.match(out, /test_counter\{kind="b"\} 1/);
|
|
16
|
+
});
|
|
17
|
+
it("Gauge updates last-write per label set", () => {
|
|
18
|
+
const g = new Gauge("test_gauge", "test gauge");
|
|
19
|
+
g.set(5, { repo: "a" });
|
|
20
|
+
g.set(7, { repo: "a" });
|
|
21
|
+
assert.match(g.render(), /test_gauge\{repo="a"\} 7/);
|
|
22
|
+
});
|
|
23
|
+
it("Histogram tracks buckets, sum, count", () => {
|
|
24
|
+
const h = new Histogram("test_hist", "test", [0.1, 0.5, 1]);
|
|
25
|
+
h.observe(0.05);
|
|
26
|
+
h.observe(0.3);
|
|
27
|
+
h.observe(0.9);
|
|
28
|
+
const out = h.render();
|
|
29
|
+
assert.match(out, /test_hist_count\{?\}? 3/);
|
|
30
|
+
assert.match(out, /test_hist_sum\{?\}? 1.25/);
|
|
31
|
+
assert.match(out, /test_hist_bucket\{le="0.1"\} 1/);
|
|
32
|
+
assert.match(out, /test_hist_bucket\{le="\+Inf"\} 3/);
|
|
33
|
+
});
|
|
34
|
+
it("registry renders the prelude for all pre-declared metrics", () => {
|
|
35
|
+
metrics.queriesTotal.inc({ mode: "hybrid" });
|
|
36
|
+
const out = registry.render();
|
|
37
|
+
assert.match(out, /code_search_queries_total/);
|
|
38
|
+
assert.match(out, /code_search_query_duration_seconds/);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("Logger (P7)", () => {
|
|
42
|
+
it("emits structured JSON when enabled", () => {
|
|
43
|
+
const lines = [];
|
|
44
|
+
const log = new Logger({ structured: true, write: (l) => lines.push(l) });
|
|
45
|
+
log.info("hello", { query: "auth" });
|
|
46
|
+
assert.equal(lines.length, 1);
|
|
47
|
+
const parsed = JSON.parse(lines[0]);
|
|
48
|
+
assert.equal(parsed.msg, "hello");
|
|
49
|
+
assert.equal(parsed.query, "auth");
|
|
50
|
+
assert.equal(parsed.level, "info");
|
|
51
|
+
assert.ok(parsed.ts);
|
|
52
|
+
});
|
|
53
|
+
it("respects level threshold", () => {
|
|
54
|
+
const lines = [];
|
|
55
|
+
const log = new Logger({ structured: true, level: "warn", write: (l) => lines.push(l) });
|
|
56
|
+
log.info("skipped");
|
|
57
|
+
log.warn("kept");
|
|
58
|
+
assert.equal(lines.length, 1);
|
|
59
|
+
assert.match(lines[0], /"kept"/);
|
|
60
|
+
});
|
|
61
|
+
it("falls back to text mode when structured=false", () => {
|
|
62
|
+
const lines = [];
|
|
63
|
+
const log = new Logger({ structured: false, write: (l) => lines.push(l) });
|
|
64
|
+
log.error("boom");
|
|
65
|
+
assert.equal(lines.length, 1);
|
|
66
|
+
assert.match(lines[0], /ERROR/);
|
|
67
|
+
assert.match(lines[0], /boom/);
|
|
68
|
+
});
|
|
69
|
+
});
|