@oomkapwn/enquire-mcp 2.5.0 → 2.6.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/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
- interface ServeOptions {
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { FtsIndex } from "./fts5.js";
4
+ import { Vault } from "./vault.js";
5
+ import { VaultWatcher } from "./watcher.js";
6
+ export interface ServeOptions {
3
7
  vault: string;
4
8
  enableWrite?: boolean;
5
9
  maxFileBytes?: string;
@@ -17,7 +21,47 @@ interface ServeOptions {
17
21
  diagnosticSearchTools?: boolean;
18
22
  }
19
23
  declare function main(): Promise<void>;
24
+ /**
25
+ * Heavyweight resources shared across every MCP-server instance: the vault
26
+ * (parsed-note cache + privacy filter), the FTS5 index handle, the optional
27
+ * filesystem watcher. v2.6.0 split this out so the HTTP transport can spin up
28
+ * a fresh `McpServer` per session over the SAME vault/index — opening the
29
+ * SQLite handle once and reusing it across thousands of remote-MCP calls.
30
+ *
31
+ * `warningTracker` is a single-fire latch for the `--disabled-tools` /
32
+ * `--enabled-tools` typo warnings: stdio prints them once at boot; HTTP
33
+ * prints them on the first session build, then never again.
34
+ */
35
+ export interface ServerDeps {
36
+ vault: Vault;
37
+ ftsIndex: FtsIndex | null;
38
+ watcher: VaultWatcher | null;
39
+ disabledTools: Set<string>;
40
+ enabledTools: Set<string>;
41
+ warningTracker: {
42
+ printed: boolean;
43
+ };
44
+ }
45
+ /**
46
+ * One-time bootstrap of the heavy deps (vault open + FTS5 sync + watcher).
47
+ * Idempotent on a per-call basis but NOT designed to be called multiple
48
+ * times in one process — the FTS5 sync would double-index. Stdio + HTTP
49
+ * each call this exactly once at startup.
50
+ */
51
+ export declare function prepareServerDeps(opts: ServeOptions): Promise<ServerDeps>;
52
+ /**
53
+ * Build a fresh `McpServer` over already-prepared deps. Cheap (just
54
+ * registers tool handlers — no I/O, no SQLite open). Stdio calls this once;
55
+ * HTTP calls it per session.
56
+ */
57
+ export declare function buildMcpServer(deps: ServerDeps, opts: ServeOptions): McpServer;
20
58
  declare function startServer(opts: ServeOptions): Promise<void>;
59
+ /**
60
+ * Shared "ready" banner used by stdio + HTTP startup paths so the runtime
61
+ * configuration summary is identical regardless of transport. Transport
62
+ * suffix is appended by the caller.
63
+ */
64
+ export declare function formatReadyBanner(deps: ServerDeps): string;
21
65
  declare function parsePositiveInt(raw: string, flag: string): number;
22
66
  export { main, parsePositiveInt, startServer };
23
67
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA4DA,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA2MnC;AAED,iBAAe,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA2K5D;AAotDD,iBAAS,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3D;AAsCD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAMtF,OAAO,EAAkC,QAAQ,EAAE,MAAM,WAAW,CAAC;AAsCrE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAW5C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAcD,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA0SnC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,cAAc,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,CA8C/E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,GAAG,SAAS,CAqF9E;AAED,iBAAe,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD5D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAY1D;AAotDD,iBAAS,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3D;AAsCD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { chunkContent, defaultIndexFile, FtsIndex } from "./fts5.js";
12
12
  import { appendToNote, archiveNote, chatThreadAppend, chatThreadRead, contextPack, createNote, dataviewQuery, embeddingsSearch, findPath, findSimilar, frontmatterGet, frontmatterSearch, frontmatterSet, getBacklinks, getNoteNeighbors, getOpenQuestions, getOutboundLinks, getRecentEdits, getUnresolvedWikilinks, getVaultStats, lintWiki, listCanvases, listNotes, listTags, openInUi, paperAudit, readCanvas, readNote, renameNote, replaceInNotes, resolveWikilink, searchHybrid, searchText, semanticSearch, validateNoteProposal } from "./tools.js";
13
13
  import { Vault } from "./vault.js";
14
14
  import { VaultWatcher } from "./watcher.js";
15
- const VERSION = "2.5.0";
15
+ const VERSION = "2.6.0";
16
16
  /** Default location for the persistent embedding index, alongside .fts5.db. */
17
17
  function embedDbPath(vaultRoot) {
18
18
  // Match the FTS5 location convention by stripping the .fts5.db extension
@@ -46,6 +46,79 @@ async function main() {
46
46
  .action(async (opts) => {
47
47
  await startServer(opts);
48
48
  });
49
+ // v2.6.0 — remote-MCP HTTP transport. Mirrors `serve` flags + adds HTTP
50
+ // surface (bearer auth, rate-limit, CORS). See docs/http-transport.md.
51
+ program
52
+ .command("serve-http")
53
+ .description("Start the MCP server over HTTP (Streamable HTTP transport). For remote-MCP use with claude.ai web, ChatGPT, Cursor HTTP mode, mobile clients. Requires --bearer-token (or --bearer-token-env). Bind to 127.0.0.1 by default — front with Tailscale Funnel / Cloudflare Tunnel for remote access.")
54
+ .requiredOption("--vault <path>", "Path to the Obsidian vault root")
55
+ .option("--port <n>", "TCP port (default 3000)", "3000")
56
+ .option("--host <host>", "Bind host (default 127.0.0.1 — explicit because 0.0.0.0 must be opt-in for remote-MCP)", "127.0.0.1")
57
+ .option("--bearer-token <token>", "Bearer token clients must present in the Authorization header. Generate with `enquire-mcp gen-token`. Required.")
58
+ .option("--bearer-token-env <name>", "Read the bearer token from this env var instead of --bearer-token (cleaner for systemd / .env / process listings). Either flag is required.")
59
+ .option("--mcp-path <path>", "URL path for the MCP endpoint (default /mcp)", "/mcp")
60
+ .option("--rate-limit <n>", "Max requests per minute per bearer token (default 120). Pass 0 to disable.", "120")
61
+ .option("--cors-origin <origin...>", "CORS allowlist (repeatable). Default empty — no Access-Control-Allow-Origin sent. Use '*' as a single entry to allow any origin (not compatible with credentialed Bearer requests; you almost always want explicit origins like https://claude.ai).")
62
+ .option("--health-path <path>", "URL path for the unauthenticated health probe (default /health)", "/health")
63
+ .option("--enable-write", "Enable the write tools (gated identically to stdio mode). Off by default.")
64
+ .option("--max-file-bytes <n>", "Max bytes for any single file read/write (default 5MB)")
65
+ .option("--cache-size <n>", "Max parsed-note cache entries (default 1024)")
66
+ .option("--persistent-cache", "Persist parsed-note cache to disk so cold starts skip re-parsing")
67
+ .option("--cache-file <path>", "Override the persistent-cache file location")
68
+ .option("--persistent-index", "Maintain a SQLite FTS5 inverted index for sub-100ms BM25-ranked search")
69
+ .option("--index-file <path>", "Override the FTS5 index file location")
70
+ .option("--tokenize <mode>", "FTS5 tokenize mode: 'unicode61' (default) or 'trigram'")
71
+ .option("--exclude-glob <pattern...>", "Privacy denylist (same semantics as `serve`).")
72
+ .option("--read-paths <pattern...>", "Privacy allowlist (same semantics as `serve`).")
73
+ .option("--watch", "Watch the vault for .md changes and refresh indexes incrementally.")
74
+ .option("--disabled-tools <name...>", "Skip registration of specific tools by name.")
75
+ .option("--enabled-tools <name...>", "Strict allowlist — when set, ONLY listed tools register.")
76
+ .option("--diagnostic-search-tools", "Register the four single-ranker search tools alongside obsidian_search.")
77
+ .action(async (opts) => {
78
+ const tokenFromArg = typeof opts.bearerToken === "string" ? opts.bearerToken.trim() : "";
79
+ const tokenFromEnv = typeof opts.bearerTokenEnv === "string" ? (process.env[opts.bearerTokenEnv] ?? "").trim() : "";
80
+ const bearerToken = tokenFromArg.length > 0 ? tokenFromArg : tokenFromEnv;
81
+ if (!bearerToken) {
82
+ process.stderr.write("enquire serve-http: --bearer-token (or --bearer-token-env <name>) is required.\n" +
83
+ " Generate one with: enquire-mcp gen-token\n");
84
+ process.exit(1);
85
+ }
86
+ // --port accepts 0 as "kernel-assigned ephemeral" — useful for tests
87
+ // and for scenarios where the user binds via a tunnel and doesn't
88
+ // care which local port. So we use a non-negative-integer check
89
+ // here, NOT parsePositiveInt (which would reject 0).
90
+ const portNum = Number(opts.port ?? "3000");
91
+ if (!Number.isFinite(portNum) || !Number.isInteger(portNum) || portNum < 0 || portNum > 65535) {
92
+ throw new Error(`--port must be an integer in [0, 65535]; got "${opts.port}"`);
93
+ }
94
+ const httpOpts = {
95
+ ...opts,
96
+ port: portNum,
97
+ host: opts.host ?? "127.0.0.1",
98
+ bearerToken,
99
+ mcpPath: opts.mcpPath ?? "/mcp",
100
+ rateLimitPerMinute: opts.rateLimit !== undefined ? Number(opts.rateLimit) : 120,
101
+ corsOrigins: opts.corsOrigin ?? [],
102
+ healthPath: opts.healthPath ?? "/health"
103
+ };
104
+ if (!Number.isFinite(httpOpts.rateLimitPerMinute) ||
105
+ httpOpts.rateLimitPerMinute < 0 ||
106
+ !Number.isInteger(httpOpts.rateLimitPerMinute)) {
107
+ throw new Error(`--rate-limit must be a non-negative integer; got "${opts.rateLimit}"`);
108
+ }
109
+ const { startHttpServer } = await import("./http-transport.js");
110
+ await startHttpServer(httpOpts);
111
+ });
112
+ // v2.6.0 — convenience helper. Same as `node -e
113
+ // 'console.log(require("crypto").randomBytes(32).toString("base64url"))'`
114
+ // but discoverable in --help.
115
+ program
116
+ .command("gen-token")
117
+ .description("Generate a fresh 32-byte base64url bearer token suitable for `serve-http --bearer-token`.")
118
+ .action(async () => {
119
+ const { generateBearerToken } = await import("./http-transport.js");
120
+ process.stdout.write(`${generateBearerToken()}\n`);
121
+ });
49
122
  program
50
123
  .command("clear-cache")
51
124
  .description("Delete the persistent-cache file for a given vault")
@@ -169,7 +242,13 @@ async function main() {
169
242
  });
170
243
  await program.parseAsync(process.argv);
171
244
  }
172
- async function startServer(opts) {
245
+ /**
246
+ * One-time bootstrap of the heavy deps (vault open + FTS5 sync + watcher).
247
+ * Idempotent on a per-call basis but NOT designed to be called multiple
248
+ * times in one process — the FTS5 sync would double-index. Stdio + HTTP
249
+ * each call this exactly once at startup.
250
+ */
251
+ export async function prepareServerDeps(opts) {
173
252
  const vault = new Vault(opts.vault, {
174
253
  enableWrite: !!opts.enableWrite,
175
254
  maxFileBytes: opts.maxFileBytes !== undefined ? parsePositiveInt(opts.maxFileBytes, "--max-file-bytes") : undefined,
@@ -198,6 +277,28 @@ async function startServer(opts) {
198
277
  throw err;
199
278
  }
200
279
  }
280
+ // Optional watcher — only when --watch is passed. Starts AFTER the initial
281
+ // FTS5 sync so we don't double-index files during boot.
282
+ let watcher = null;
283
+ if (opts.watch) {
284
+ watcher = new VaultWatcher({ vault, ftsIndex });
285
+ await watcher.start();
286
+ }
287
+ return {
288
+ vault,
289
+ ftsIndex,
290
+ watcher,
291
+ disabledTools: new Set(opts.disabledTools ?? []),
292
+ enabledTools: new Set(opts.enabledTools ?? []),
293
+ warningTracker: { printed: false }
294
+ };
295
+ }
296
+ /**
297
+ * Build a fresh `McpServer` over already-prepared deps. Cheap (just
298
+ * registers tool handlers — no I/O, no SQLite open). Stdio calls this once;
299
+ * HTTP calls it per session.
300
+ */
301
+ export function buildMcpServer(deps, opts) {
201
302
  const server = new McpServer({
202
303
  name: "enquire",
203
304
  version: VERSION
@@ -217,62 +318,75 @@ async function startServer(opts) {
217
318
  // unknown — typo or stale doc reference. Pre-fix, a typo in
218
319
  // `--disabled-tools obsidan_search` (note the missing `i`) silently
219
320
  // disabled nothing; now we log a warning so the user can correct it.
220
- const disabledTools = new Set(opts.disabledTools ?? []);
221
- const enabledTools = new Set(opts.enabledTools ?? []);
222
321
  const usedDisabled = new Set();
223
322
  const usedEnabled = new Set();
224
323
  const registeredNames = new Set();
225
- if (disabledTools.size > 0 || enabledTools.size > 0) {
324
+ // v2.6.0: only print skip-logging on the first build (stdio: once at boot;
325
+ // HTTP: once on first session). Subsequent HTTP sessions reuse the same
326
+ // gating decisions silently — no need to spam logs per request.
327
+ const verbose = !deps.warningTracker.printed;
328
+ if (deps.disabledTools.size > 0 || deps.enabledTools.size > 0) {
226
329
  const origRegisterTool = server.registerTool.bind(server);
227
330
  server.registerTool = (name, ...rest) => {
228
331
  registeredNames.add(name);
229
- if (enabledTools.size > 0) {
230
- if (enabledTools.has(name)) {
332
+ if (deps.enabledTools.size > 0) {
333
+ if (deps.enabledTools.has(name)) {
231
334
  usedEnabled.add(name);
232
335
  }
233
336
  else {
234
- process.stderr.write(`enquire: skipping tool ${name} (not in --enabled-tools allowlist)\n`);
337
+ if (verbose)
338
+ process.stderr.write(`enquire: skipping tool ${name} (not in --enabled-tools allowlist)\n`);
235
339
  return undefined;
236
340
  }
237
341
  }
238
- if (disabledTools.has(name)) {
342
+ if (deps.disabledTools.has(name)) {
239
343
  usedDisabled.add(name);
240
- process.stderr.write(`enquire: skipping tool ${name} (disabled by --disabled-tools)\n`);
344
+ if (verbose)
345
+ process.stderr.write(`enquire: skipping tool ${name} (disabled by --disabled-tools)\n`);
241
346
  return undefined;
242
347
  }
243
348
  return origRegisterTool(name, ...rest);
244
349
  };
245
350
  }
246
- registerReadTools(server, vault, ftsIndex, opts.diagnosticSearchTools ?? false);
247
- if (vault.writeEnabled)
248
- registerWriteTools(server, vault);
249
- if (ftsIndex && opts.diagnosticSearchTools)
250
- registerFtsTools(server, ftsIndex, vault);
251
- registerResources(server, vault);
252
- if (ftsIndex)
253
- registerChunkResource(server, ftsIndex, vault);
351
+ registerReadTools(server, deps.vault, deps.ftsIndex, opts.diagnosticSearchTools ?? false);
352
+ if (deps.vault.writeEnabled)
353
+ registerWriteTools(server, deps.vault);
354
+ if (deps.ftsIndex && opts.diagnosticSearchTools)
355
+ registerFtsTools(server, deps.ftsIndex, deps.vault);
356
+ registerResources(server, deps.vault);
357
+ if (deps.ftsIndex)
358
+ registerChunkResource(server, deps.ftsIndex, deps.vault);
254
359
  registerPrompts(server);
255
360
  // v2.0.0-beta.1: warn on unknown names AFTER all tools are registered.
256
361
  // We can't validate at parse time because the canonical list depends on
257
362
  // runtime config (e.g. --persistent-index gates obsidian_full_text_search,
258
363
  // --enable-write gates the 5 write tools). So we wait until everything is
259
364
  // registered, then diff the user's lists against what was actually seen.
260
- for (const name of disabledTools) {
261
- if (!usedDisabled.has(name)) {
262
- const hint = registeredNames.has(name)
263
- ? "" // shouldn't happen — would have been used
264
- : ` (no such tool registered; check spelling; available: ${[...registeredNames].sort().join(", ")})`;
265
- process.stderr.write(`enquire: warning --disabled-tools "${name}" did not match any tool${hint}\n`);
365
+ if (verbose) {
366
+ for (const name of deps.disabledTools) {
367
+ if (!usedDisabled.has(name)) {
368
+ const hint = registeredNames.has(name)
369
+ ? "" // shouldn't happen would have been used
370
+ : ` (no such tool registered; check spelling; available: ${[...registeredNames].sort().join(", ")})`;
371
+ process.stderr.write(`enquire: warning — --disabled-tools "${name}" did not match any tool${hint}\n`);
372
+ }
266
373
  }
267
- }
268
- for (const name of enabledTools) {
269
- if (!usedEnabled.has(name)) {
270
- const hint = registeredNames.has(name)
271
- ? ""
272
- : ` (no such tool; check spelling; available: ${[...registeredNames].sort().join(", ")})`;
273
- process.stderr.write(`enquire: warning — --enabled-tools "${name}" did not match any tool${hint}\n`);
374
+ for (const name of deps.enabledTools) {
375
+ if (!usedEnabled.has(name)) {
376
+ const hint = registeredNames.has(name)
377
+ ? ""
378
+ : ` (no such tool; check spelling; available: ${[...registeredNames].sort().join(", ")})`;
379
+ process.stderr.write(`enquire: warning --enabled-tools "${name}" did not match any tool${hint}\n`);
380
+ }
274
381
  }
382
+ deps.warningTracker.printed = true;
275
383
  }
384
+ return server;
385
+ }
386
+ async function startServer(opts) {
387
+ const deps = await prepareServerDeps(opts);
388
+ const { vault, ftsIndex, watcher } = deps;
389
+ const server = buildMcpServer(deps, opts);
276
390
  const transport = new StdioServerTransport();
277
391
  await server.connect(transport);
278
392
  if (vault.persistentCacheEnabled) {
@@ -305,12 +419,7 @@ async function startServer(opts) {
305
419
  void flush();
306
420
  });
307
421
  }
308
- // Optional watcher — only when --watch is passed. Starts AFTER the initial
309
- // FTS5 sync so we don't double-index files during boot.
310
- let watcher = null;
311
- if (opts.watch) {
312
- watcher = new VaultWatcher({ vault, ftsIndex });
313
- await watcher.start();
422
+ if (watcher) {
314
423
  const closeWatcher = () => {
315
424
  void watcher?.close();
316
425
  };
@@ -318,6 +427,21 @@ async function startServer(opts) {
318
427
  process.once("SIGTERM", closeWatcher);
319
428
  process.on("beforeExit", closeWatcher);
320
429
  }
430
+ process.stderr.write(`${formatReadyBanner(deps)} (transport=stdio)\n`);
431
+ if (ftsIndex) {
432
+ const closeFts = () => ftsIndex?.close();
433
+ process.once("SIGINT", closeFts);
434
+ process.once("SIGTERM", closeFts);
435
+ process.on("beforeExit", closeFts);
436
+ }
437
+ }
438
+ /**
439
+ * Shared "ready" banner used by stdio + HTTP startup paths so the runtime
440
+ * configuration summary is identical regardless of transport. Transport
441
+ * suffix is appended by the caller.
442
+ */
443
+ export function formatReadyBanner(deps) {
444
+ const { vault, ftsIndex, watcher, disabledTools, enabledTools } = deps;
321
445
  const writeMode = vault.writeEnabled ? "WRITE-ENABLED" : "read-only";
322
446
  const cacheMode = vault.persistentCacheEnabled ? `, persistent-cache=${vault.cacheFile}` : "";
323
447
  const ftsMode = ftsIndex ? `, fts5-index (${ftsIndex.totalFiles()} files / ${ftsIndex.totalChunks()} chunks)` : "";
@@ -327,13 +451,7 @@ async function startServer(opts) {
327
451
  const watchMode = watcher ? ", watch=on" : "";
328
452
  const disabledMode = disabledTools.size > 0 ? `, disabled-tools=${disabledTools.size}` : "";
329
453
  const enabledMode = enabledTools.size > 0 ? `, enabled-tools=${enabledTools.size}` : "";
330
- process.stderr.write(`enquire ${VERSION} ready (${writeMode}, vault=${vault.root}${cacheMode}${ftsMode}${privacyMode}${watchMode}${disabledMode}${enabledMode})\n`);
331
- if (ftsIndex) {
332
- const closeFts = () => ftsIndex?.close();
333
- process.once("SIGINT", closeFts);
334
- process.once("SIGTERM", closeFts);
335
- process.on("beforeExit", closeFts);
336
- }
454
+ return `enquire ${VERSION} ready (${writeMode}, vault=${vault.root}${cacheMode}${ftsMode}${privacyMode}${watchMode}${disabledMode}${enabledMode})`;
337
455
  }
338
456
  // v2.0 alpha — sync the persistent embedding index. Same incremental-rebuild
339
457
  // pattern as syncFtsIndex (mtime tracked in source_state); we only re-embed