@astrofoundry/grimoire 3.31.1 → 3.32.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.
@@ -30,11 +30,46 @@ var bold = (s) => isTTY ? `\x1B[1m${s}\x1B[0m` : s;
30
30
  var cyan = (s) => isTTY ? `\x1B[36m${s}\x1B[0m` : s;
31
31
  var yellow = (s) => isTTY ? `\x1B[33m${s}\x1B[0m` : s;
32
32
 
33
+ // src/reranker.ts
34
+ function getRerankerUrl() {
35
+ const url = process.env.RERANKER_URL;
36
+ if (!url) {
37
+ throw new Error("RERANKER_URL environment variable is not set");
38
+ }
39
+ return url;
40
+ }
41
+ var DEFAULT_TIMEOUT_MS = 3e4;
42
+ var RERANK_POOL_SIZE = 25;
43
+ var RERANK_DOC_MAX_CHARS = 800;
44
+ function rerankDocText(title, headingPath, content) {
45
+ const context = [title, ...headingPath].filter(Boolean).join(" > ");
46
+ const text = context ? `${context}
47
+
48
+ ${content}` : content;
49
+ return text.length > RERANK_DOC_MAX_CHARS ? text.slice(0, RERANK_DOC_MAX_CHARS) : text;
50
+ }
51
+ async function rerank(query, documents, topN = 5, baseUrl = getRerankerUrl(), timeoutMs = DEFAULT_TIMEOUT_MS) {
52
+ const response = await fetch(`${baseUrl}/v1/rerank`, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({ query, documents, top_n: topN }),
56
+ signal: AbortSignal.timeout(timeoutMs)
57
+ });
58
+ if (!response.ok) {
59
+ throw new Error(`Reranker request failed: ${response.status} ${response.statusText}`);
60
+ }
61
+ const data = await response.json();
62
+ return data.results;
63
+ }
64
+
33
65
  export {
34
66
  __commonJS,
35
67
  __toESM,
36
68
  bold,
37
69
  cyan,
38
- yellow
70
+ yellow,
71
+ RERANK_POOL_SIZE,
72
+ rerankDocText,
73
+ rerank
39
74
  };
40
- //# sourceMappingURL=chunk-R46N6C3C.js.map
75
+ //# sourceMappingURL=chunk-VTTJCQRQ.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/format.ts", "../src/reranker.ts"],
4
+ "sourcesContent": ["const isTTY = process.stdout.isTTY ?? false;\n\nexport const bold = (s: string): string => isTTY ? `\\x1b[1m${s}\\x1b[0m` : s;\nexport const cyan = (s: string): string => isTTY ? `\\x1b[36m${s}\\x1b[0m` : s;\nexport const yellow = (s: string): string => isTTY ? `\\x1b[33m${s}\\x1b[0m` : s;\nexport const red = (s: string): string => isTTY ? `\\x1b[31m${s}\\x1b[0m` : s;\n", "export interface RerankResult {\n index: number;\n relevance_score: number;\n}\n\ninterface LlamaCppRerankResponse {\n results: { index: number; relevance_score: number }[];\n}\n\nfunction getRerankerUrl(): string {\n const url = process.env.RERANKER_URL;\n if (!url) {\n throw new Error(\"RERANKER_URL environment variable is not set\");\n }\n return url;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n// The local 4B reranker processes ~300 tokens/s, so document size and pool\n// size bound search latency directly. The contextual head (title > path +\n// the first part of the chunk) carries the relevance signal; the tail of a\n// 500-token table chunk adds latency, not ranking quality.\nexport const RERANK_POOL_SIZE = 25;\nconst RERANK_DOC_MAX_CHARS = 800;\n\nexport function rerankDocText(\n title: string,\n headingPath: string[],\n content: string,\n): string {\n const context = [title, ...headingPath].filter(Boolean).join(\" > \");\n const text = context ? `${context}\\n\\n${content}` : content;\n return text.length > RERANK_DOC_MAX_CHARS ? text.slice(0, RERANK_DOC_MAX_CHARS) : text;\n}\n\nexport async function rerank(\n query: string,\n documents: string[],\n topN = 5,\n baseUrl = getRerankerUrl(),\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RerankResult[]> {\n const response = await fetch(`${baseUrl}/v1/rerank`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ query, documents, top_n: topN }),\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n if (!response.ok) {\n throw new Error(`Reranker request failed: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as LlamaCppRerankResponse;\n return data.results;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAM,QAAQ,QAAQ,OAAO,SAAS;AAE/B,IAAM,OAAO,CAAC,MAAsB,QAAQ,UAAU,CAAC,YAAY;AACnE,IAAM,OAAO,CAAC,MAAsB,QAAQ,WAAW,CAAC,YAAY;AACpE,IAAM,SAAS,CAAC,MAAsB,QAAQ,WAAW,CAAC,YAAY;;;ACK7E,SAAS,iBAAyB;AAChC,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,SAAO;AACT;AAEA,IAAM,qBAAqB;AAMpB,IAAM,mBAAmB;AAChC,IAAM,uBAAuB;AAEtB,SAAS,cACd,OACA,aACA,SACQ;AACR,QAAM,UAAU,CAAC,OAAO,GAAG,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,KAAK;AAClE,QAAM,OAAO,UAAU,GAAG,OAAO;AAAA;AAAA,EAAO,OAAO,KAAK;AACpD,SAAO,KAAK,SAAS,uBAAuB,KAAK,MAAM,GAAG,oBAAoB,IAAI;AACpF;AAEA,eAAsB,OACpB,OACA,WACA,OAAO,GACP,UAAU,eAAe,GACzB,YAAY,oBACa;AACzB,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,cAAc;AAAA,IACnD,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,WAAW,OAAO,KAAK,CAAC;AAAA,IACtD,QAAQ,YAAY,QAAQ,SAAS;AAAA,EACvC,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,4BAA4B,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EACtF;AAEA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,SAAO,KAAK;AACd;",
6
+ "names": []
7
+ }
package/dist/cli.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ RERANK_POOL_SIZE,
3
4
  bold,
4
5
  cyan,
6
+ rerank,
7
+ rerankDocText,
5
8
  yellow
6
- } from "./chunk-R46N6C3C.js";
9
+ } from "./chunk-VTTJCQRQ.js";
7
10
 
8
11
  // src/cli.ts
9
12
  import { parseArgs } from "node:util";
@@ -22,7 +25,11 @@ async function loadConsumerConfig() {
22
25
  if (!raw) return null;
23
26
  const data = JSON.parse(raw);
24
27
  if (typeof data.apiUrl === "string" && typeof data.apiKey === "string") {
25
- return { apiUrl: data.apiUrl, apiKey: data.apiKey };
28
+ return {
29
+ apiUrl: data.apiUrl,
30
+ apiKey: data.apiKey,
31
+ ...typeof data.rerankerUrl === "string" && data.rerankerUrl ? { rerankerUrl: data.rerankerUrl } : {}
32
+ };
26
33
  }
27
34
  return null;
28
35
  }
@@ -33,11 +40,18 @@ async function saveConsumerConfig(config) {
33
40
  async function resolveConsumerConfig() {
34
41
  const envUrl = process.env.GRIMOIRE_API_URL;
35
42
  const envKey = process.env.GRIMOIRE_API_KEY;
43
+ const envReranker = process.env.RERANKER_URL;
36
44
  if (envUrl && envKey) {
37
- return { apiUrl: envUrl, apiKey: envKey };
45
+ return {
46
+ apiUrl: envUrl,
47
+ apiKey: envKey,
48
+ ...envReranker ? { rerankerUrl: envReranker } : {}
49
+ };
38
50
  }
39
51
  const fileConfig = await loadConsumerConfig();
40
- if (fileConfig) return fileConfig;
52
+ if (fileConfig) {
53
+ return envReranker ? { ...fileConfig, rerankerUrl: envReranker } : fileConfig;
54
+ }
41
55
  throw new Error("Grimoire is not configured. Run 'grimoire init' to set up.");
42
56
  }
43
57
  async function detectConsumerMode() {
@@ -46,15 +60,53 @@ async function detectConsumerMode() {
46
60
  const config = await loadConsumerConfig();
47
61
  return config !== null;
48
62
  }
63
+ async function setConfigRerankerUrl(url) {
64
+ const existing = await loadConsumerConfig();
65
+ if (!existing) {
66
+ throw new Error("Grimoire is not configured. Run 'grimoire init' first.");
67
+ }
68
+ await saveConsumerConfig({
69
+ apiUrl: existing.apiUrl,
70
+ apiKey: existing.apiKey,
71
+ ...url ? { rerankerUrl: url } : {}
72
+ });
73
+ }
74
+ async function cmdRerank(arg) {
75
+ if (!arg) {
76
+ const envUrl = process.env.RERANKER_URL;
77
+ const fileConfig = await loadConsumerConfig();
78
+ const effective = envUrl ?? fileConfig?.rerankerUrl;
79
+ console.log(`Env RERANKER_URL: ${envUrl ?? "(not set)"}`);
80
+ console.log(`Config rerankerUrl: ${fileConfig?.rerankerUrl ?? "(not set)"}`);
81
+ console.log(`Effective: ${effective ?? "off"}`);
82
+ return;
83
+ }
84
+ if (arg === "off") {
85
+ await setConfigRerankerUrl(void 0);
86
+ console.log(`Reranker removed from ${CONFIG_FILE}.`);
87
+ if (process.env.RERANKER_URL) {
88
+ console.log("Note: RERANKER_URL is set in your environment and still takes precedence. Unset it too.");
89
+ }
90
+ return;
91
+ }
92
+ new URL(arg);
93
+ await setConfigRerankerUrl(arg);
94
+ console.log(`Reranker set to ${arg} in ${CONFIG_FILE}.`);
95
+ }
49
96
  async function cmdInit() {
50
97
  const rl = createInterface({ input: process.stdin, output: process.stdout });
51
98
  const ask = (q) => new Promise((resolve2) => rl.question(q, resolve2));
52
99
  const existing = await loadConsumerConfig();
53
100
  const apiUrl = await ask(`API URL${existing ? ` [${existing.apiUrl}]` : ""}: `);
54
101
  const apiKey = await ask(`API Key${existing ? " [****]" : ""}: `);
102
+ const rerankerUrl = await ask(
103
+ `Reranker URL (optional, local network)${existing?.rerankerUrl ? ` [${existing.rerankerUrl}]` : ""}: `
104
+ );
105
+ const resolvedReranker = rerankerUrl.trim() || existing?.rerankerUrl;
55
106
  const config = {
56
107
  apiUrl: apiUrl.trim() || existing?.apiUrl || "",
57
- apiKey: apiKey.trim() || existing?.apiKey || ""
108
+ apiKey: apiKey.trim() || existing?.apiKey || "",
109
+ ...resolvedReranker ? { rerankerUrl: resolvedReranker } : {}
58
110
  };
59
111
  rl.close();
60
112
  if (!config.apiUrl || !config.apiKey) {
@@ -66,6 +118,31 @@ Saved to ${CONFIG_FILE}`);
66
118
  }
67
119
 
68
120
  // src/consumer.ts
121
+ function contextualText(result) {
122
+ return rerankDocText(result.title, result.heading_path, result.content);
123
+ }
124
+ var RERANK_TIMEOUT_MS = 3e4;
125
+ async function rerankResults(rerankerUrl, query, results, topN) {
126
+ try {
127
+ const reranked = await rerank(
128
+ query,
129
+ results.map(contextualText),
130
+ topN,
131
+ rerankerUrl,
132
+ RERANK_TIMEOUT_MS
133
+ );
134
+ return reranked.map((r) => ({
135
+ ...results[r.index],
136
+ relevance_score: r.relevance_score
137
+ }));
138
+ } catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ console.error(
141
+ `Warning: reranker at ${rerankerUrl} unavailable (${message}); using API ranking. Use --no-rerank or 'grimoire rerank off' to silence this.`
142
+ );
143
+ return results.slice(0, topN);
144
+ }
145
+ }
69
146
  async function apiRequest(config, path, options) {
70
147
  const url = `${config.apiUrl.replace(/\/$/, "")}${path}`;
71
148
  let response;
@@ -90,12 +167,14 @@ async function apiRequest(config, path, options) {
90
167
  return response.json();
91
168
  }
92
169
  async function cmdConsumerSearch(config, query, options) {
170
+ const topN = options.topN ?? 5;
171
+ const rerankerUrl = options.noRerank ? void 0 : config.rerankerUrl;
93
172
  const data = await apiRequest(config, "/search", {
94
173
  method: "POST",
95
174
  body: JSON.stringify({
96
175
  query,
97
176
  source: options.source,
98
- topN: options.topN,
177
+ topN: rerankerUrl ? Math.max(topN, RERANK_POOL_SIZE) : topN,
99
178
  candidates: options.candidates
100
179
  })
101
180
  });
@@ -103,6 +182,9 @@ async function cmdConsumerSearch(config, query, options) {
103
182
  console.log("No results found.");
104
183
  return;
105
184
  }
185
+ if (rerankerUrl) {
186
+ data.results = await rerankResults(rerankerUrl, query, data.results, topN);
187
+ }
106
188
  if (options.compact) {
107
189
  for (const r of data.results) {
108
190
  console.log(`${r.relevance_score.toFixed(4)} | ${r.source} | ${r.title} | ${r.heading_path.join(" > ")} | ${r.url}`);
@@ -188,10 +270,10 @@ async function cmdUpdate() {
188
270
  function showHelp(isConsumer) {
189
271
  if (isConsumer) {
190
272
  console.log(`
191
- grimoire \u2014 Documentation RAG
273
+ grimoire - Documentation RAG
192
274
 
193
275
  USAGE
194
- grimoire search "<query>" [--source <name>] [--top <n>] [--candidates <n>] [--compact]
276
+ grimoire search "<query>" [--source <name>] [--top <n>] [--candidates <n>] [--compact] [--no-rerank]
195
277
 
196
278
  QUERY TIPS
197
279
  - Use the library's own terminology ("Firestore batched writes"
@@ -218,28 +300,40 @@ FLAGS
218
300
  --compact One line per result: score | source | title | heading | url
219
301
  Default (non-compact): multi-line block with title, URL,
220
302
  heading path, and content snippet.
303
+ --no-rerank Skip local reranking for this search even if a reranker
304
+ is configured. No effect when none is configured.
221
305
 
222
306
  RELEVANCE SCORES
223
307
  Range 0-1 (higher = better). >0.85 strong match, 0.6-0.85 relevant,
224
308
  <0.6 usually too weak to cite. "No results found." + exit 0 = clean miss.
225
309
  Results are ranked by hybrid vector + exact-identifier match; the score
226
310
  shown is vector similarity, so an exact identifier hit can rank first
227
- with a modest score.
311
+ with a modest score. With RERANKER_URL set, results are reranked locally
312
+ and the score is the reranker's relevance instead.
228
313
 
229
314
  MANAGEMENT
230
315
  grimoire list [--names] Show indexed sources
231
316
  grimoire stats Index statistics
232
317
  grimoire init Configure API connection (first-time setup)
318
+ grimoire rerank Show reranker status (env, config, effective)
319
+ grimoire rerank <url> Enable local reranking (saved to config)
320
+ grimoire rerank off Disable local reranking (removes from config)
233
321
  grimoire update Update grimoire itself
234
322
  grimoire --version Print CLI version
235
323
 
236
324
  ENVIRONMENT
237
325
  GRIMOIRE_API_URL API endpoint URL
238
326
  GRIMOIRE_API_KEY API key
327
+ RERANKER_URL Optional. Local-network reranker endpoint; when
328
+ set, search fetches a 50-result pool and reranks
329
+ client-side. Overrides "rerankerUrl" in
330
+ ~/.grimoire/config.json. If the reranker is
331
+ unreachable, search warns on stderr and falls
332
+ back to the API ranking after a 15s timeout.
239
333
  `);
240
334
  } else {
241
335
  console.log(`
242
- grimoire \u2014 Documentation RAG System (admin)
336
+ grimoire - Documentation RAG System (admin)
243
337
 
244
338
  Commands:
245
339
  add <name> --url <url> Add a new documentation source
@@ -251,6 +345,8 @@ Commands:
251
345
  refresh <source> --full Purge chunks + caches, re-scrape from scratch
252
346
  refresh <source> --skip-store Dry run: scrape/convert/chunk only,
253
347
  no embedding, no Firestore writes
348
+ refresh <source> --allow-shrink Override the shrink/growth guards when a
349
+ large chunk-count change is expected
254
350
  refresh --all Refresh all sources
255
351
  delete <source> Delete source and all its chunks
256
352
  scrape-urls <source> Fetch missing pages from urls.json
@@ -287,6 +383,10 @@ async function main() {
287
383
  await cmdInit();
288
384
  return;
289
385
  }
386
+ if (command === "rerank") {
387
+ await cmdRerank(process.argv[3]);
388
+ return;
389
+ }
290
390
  if (ADMIN_ONLY_COMMANDS.includes(command)) {
291
391
  console.error(`The '${command}' command is only available in admin mode.`);
292
392
  process.exit(1);
@@ -302,18 +402,25 @@ async function main() {
302
402
  source: { type: "string" },
303
403
  top: { type: "string" },
304
404
  candidates: { type: "string" },
305
- compact: { type: "boolean", default: false }
405
+ compact: { type: "boolean", default: false },
406
+ "no-rerank": { type: "boolean", default: false }
306
407
  },
307
408
  allowPositionals: true
308
409
  });
309
410
  const query = args.positionals[0];
310
411
  if (!query) {
311
- console.error('Usage: grimoire search "<query>" [--source <name>] [--top <n>] [--candidates <n>] [--compact]');
412
+ console.error('Usage: grimoire search "<query>" [--source <name>] [--top <n>] [--candidates <n>] [--compact] [--no-rerank]');
312
413
  process.exit(1);
313
414
  }
314
415
  const topN = args.values.top ? parseInt(args.values.top, 10) : void 0;
315
416
  const candidates = args.values.candidates ? parseInt(args.values.candidates, 10) : void 0;
316
- await cmdConsumerSearch(config, query, { source: args.values.source, topN, candidates, compact: args.values.compact });
417
+ await cmdConsumerSearch(config, query, {
418
+ source: args.values.source,
419
+ topN,
420
+ candidates,
421
+ compact: args.values.compact,
422
+ noRerank: args.values["no-rerank"]
423
+ });
317
424
  } else if (command === "list") {
318
425
  const args = parseArgs({
319
426
  args: process.argv.slice(3),
@@ -333,7 +440,7 @@ async function main() {
333
440
  await cmdInit();
334
441
  return;
335
442
  }
336
- const { ADMIN_COMMANDS } = await import("./admin-ENGUPLQ6.js");
443
+ const { ADMIN_COMMANDS } = await import("./admin-4C3QNVQE.js");
337
444
  const handler = ADMIN_COMMANDS[command];
338
445
  if (!handler) {
339
446
  console.error(`Unknown command: ${command}. Run "grimoire --help" for usage.`);
package/dist/cli.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/cli.ts", "../src/consumer-config.ts", "../src/consumer.ts"],
4
- "sourcesContent": ["import { parseArgs } from \"node:util\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { detectConsumerMode, resolveConsumerConfig, cmdInit } from \"./consumer-config.js\";\nimport { cmdConsumerSearch, cmdConsumerList, cmdConsumerStats } from \"./consumer.js\";\n\nconst PROJECT_ROOT = resolve(import.meta.dirname, \"..\");\n\nconst envPath = join(PROJECT_ROOT, \".env\");\nif (existsSync(envPath)) {\n for (const line of readFileSync(envPath, \"utf-8\").split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex);\n const value = trimmed.slice(eqIndex + 1);\n if (!process.env[key]) {\n process.env[key] = value;\n }\n }\n}\n\nconst ADMIN_ONLY_COMMANDS = [\"add\", \"refresh\", \"delete\", \"scrape-urls\", \"export\", \"apikey\"];\n\nasync function cmdUpdate(): Promise<void> {\n const { execSync } = await import(\"node:child_process\");\n const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, \"package.json\"), \"utf-8\"));\n console.log(`Current version: ${pkg.version}`);\n console.log(\"Checking for updates...\");\n execSync(\"npm install -g @astrofoundry/grimoire@latest\", { stdio: \"inherit\" });\n const updated = JSON.parse(readFileSync(join(PROJECT_ROOT, \"package.json\"), \"utf-8\"));\n if (updated.version === pkg.version) {\n console.log(\"Already on the latest version.\");\n } else {\n console.log(`Updated to ${updated.version}.`);\n }\n}\n\nfunction showHelp(isConsumer: boolean): void {\n if (isConsumer) {\n console.log(`\ngrimoire \u2014 Documentation RAG\n\nUSAGE\n grimoire search \"<query>\" [--source <name>] [--top <n>] [--candidates <n>] [--compact]\n\nQUERY TIPS\n - Use the library's own terminology (\"Firestore batched writes\"\n not \"how do I do multi-write in firestore\")\n - Exact identifiers (max_connections, ExportContext, snake_case,\n dotted.paths, camelCase) are matched literally - include them verbatim\n - Scope with --source when you know the area\n - Rephrase if first search misses\n - Prefer --compact for scanning; omit for full snippets\n - Cite the URL in your answer when precision matters\n\nEXAMPLES\n grimoire search \"Firestore batched writes\" --source firebase-firestore --compact\n grimoire search \"react server components\" --top 3\n grimoire search \"mysql max_connections default\" --source gcp-cloud-sql\n grimoire list --names\n\nFLAGS\n --source <name> Scope to one indexed source. Run \\`grimoire list --names\\`\n for the full list (sources are added regularly).\n --top <n> Max results. Default: 5.\n --candidates <n> Retrieval pool size before final ranking. Default: 50,\n max: 200. Raise for broad/ambiguous queries.\n --compact One line per result: score | source | title | heading | url\n Default (non-compact): multi-line block with title, URL,\n heading path, and content snippet.\n\nRELEVANCE SCORES\n Range 0-1 (higher = better). >0.85 strong match, 0.6-0.85 relevant,\n <0.6 usually too weak to cite. \"No results found.\" + exit 0 = clean miss.\n Results are ranked by hybrid vector + exact-identifier match; the score\n shown is vector similarity, so an exact identifier hit can rank first\n with a modest score.\n\nMANAGEMENT\n grimoire list [--names] Show indexed sources\n grimoire stats Index statistics\n grimoire init Configure API connection (first-time setup)\n grimoire update Update grimoire itself\n grimoire --version Print CLI version\n\nENVIRONMENT\n GRIMOIRE_API_URL API endpoint URL\n GRIMOIRE_API_KEY API key\n`);\n } else {\n console.log(`\ngrimoire \u2014 Documentation RAG System (admin)\n\nCommands:\n add <name> --url <url> Add a new documentation source\n refresh <source> Scrape, convert, then sync: only chunks\n whose content changed are re-embedded and\n written; removed chunks are deleted\n refresh <source> --from-html Re-convert from cached HTML, then sync\n refresh <source> --from-markdown Re-chunk from cached markdown, then sync\n refresh <source> --full Purge chunks + caches, re-scrape from scratch\n refresh <source> --skip-store Dry run: scrape/convert/chunk only,\n no embedding, no Firestore writes\n refresh --all Refresh all sources\n delete <source> Delete source and all its chunks\n scrape-urls <source> Fetch missing pages from urls.json\n update Update grimoire to the latest version\n search \"<query>\" [--source <n>] [--top <n>] [--candidates <n>] [--compact]\n Hybrid search (vector + exact identifiers)\n list [--names] List all sources\n stats Show chunk/source statistics\n export <source> Export source as JSON\n apikey create <name> Generate an API key\n apikey list List API keys\n apikey delete <name> Delete an API key\n`);\n }\n}\n\nasync function main(): Promise<void> {\n const command = process.argv[2];\n\n if (command === \"--version\" || command === \"-v\") {\n const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, \"package.json\"), \"utf-8\"));\n console.log(pkg.version);\n process.exit(0);\n }\n\n const isConsumer = await detectConsumerMode();\n\n if (!command || command === \"--help\" || command === \"-h\") {\n showHelp(isConsumer);\n process.exit(0);\n }\n\n if (command === \"update\") {\n await cmdUpdate();\n return;\n }\n\n if (isConsumer) {\n if (command === \"init\") {\n await cmdInit();\n return;\n }\n\n if (ADMIN_ONLY_COMMANDS.includes(command)) {\n console.error(`The '${command}' command is only available in admin mode.`);\n process.exit(1);\n }\n\n const config = await resolveConsumerConfig().catch(() => {\n console.error(\"Grimoire is not configured yet. Run 'grimoire init' to set up your API connection.\");\n process.exit(1);\n });\n\n if (command === \"search\") {\n const args = parseArgs({\n args: process.argv.slice(3),\n options: {\n source: { type: \"string\" },\n top: { type: \"string\" },\n candidates: { type: \"string\" },\n compact: { type: \"boolean\", default: false },\n },\n allowPositionals: true,\n });\n const query = args.positionals[0];\n if (!query) {\n console.error(\"Usage: grimoire search \\\"<query>\\\" [--source <name>] [--top <n>] [--candidates <n>] [--compact]\");\n process.exit(1);\n }\n const topN = args.values.top ? parseInt(args.values.top, 10) : undefined;\n const candidates = args.values.candidates ? parseInt(args.values.candidates, 10) : undefined;\n await cmdConsumerSearch(config, query, { source: args.values.source, topN, candidates, compact: args.values.compact });\n } else if (command === \"list\") {\n const args = parseArgs({\n args: process.argv.slice(3),\n options: { names: { type: \"boolean\", default: false } },\n allowPositionals: true,\n });\n await cmdConsumerList(config, { names: args.values.names });\n } else if (command === \"stats\") {\n await cmdConsumerStats(config);\n } else {\n console.error(`Unknown command: ${command}. Run \"grimoire --help\" for usage.`);\n process.exit(1);\n }\n return;\n }\n\n if (command === \"init\") {\n await cmdInit();\n return;\n }\n\n const { ADMIN_COMMANDS } = await import(\"./admin.js\");\n const handler = ADMIN_COMMANDS[command];\n if (!handler) {\n console.error(`Unknown command: ${command}. Run \"grimoire --help\" for usage.`);\n process.exit(1);\n }\n\n await handler();\n}\n\nconst GCP_AUTH_PATTERNS = [\n \"Unable to detect a Project Id\",\n \"Could not load the default credentials\",\n \"invalid_grant\",\n \"invalid_rapt\",\n \"UNAUTHENTICATED\",\n \"Getting metadata from plugin failed\",\n];\n\nmain().catch((err: Error) => {\n const msg = err.message ?? String(err);\n if (GCP_AUTH_PATTERNS.some((p) => msg.includes(p))) {\n console.error(\"Google Cloud authentication failed. Re-authenticate with:\\n\\n gcloud auth application-default login\\n\");\n } else {\n console.error(`Error: ${msg}`);\n }\n process.exit(1);\n});\n", "import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { createInterface } from \"node:readline\";\n\nconst CONFIG_DIR = join(homedir(), \".grimoire\");\nconst CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\nexport interface ConsumerConfig {\n apiUrl: string;\n apiKey: string;\n}\n\nexport async function loadConsumerConfig(): Promise<ConsumerConfig | null> {\n const raw = await readFile(CONFIG_FILE, \"utf-8\").catch(() => null);\n if (!raw) return null;\n const data = JSON.parse(raw);\n if (typeof data.apiUrl === \"string\" && typeof data.apiKey === \"string\") {\n return { apiUrl: data.apiUrl, apiKey: data.apiKey };\n }\n return null;\n}\n\nexport async function saveConsumerConfig(config: ConsumerConfig): Promise<void> {\n await mkdir(CONFIG_DIR, { recursive: true });\n await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\nexport async function resolveConsumerConfig(): Promise<ConsumerConfig> {\n const envUrl = process.env.GRIMOIRE_API_URL;\n const envKey = process.env.GRIMOIRE_API_KEY;\n\n if (envUrl && envKey) {\n return { apiUrl: envUrl, apiKey: envKey };\n }\n\n const fileConfig = await loadConsumerConfig();\n if (fileConfig) return fileConfig;\n\n throw new Error(\"Grimoire is not configured. Run 'grimoire init' to set up.\");\n}\n\nexport function isConsumerMode(): boolean {\n return !!process.env.GRIMOIRE_API_URL;\n}\n\nexport async function detectConsumerMode(): Promise<boolean> {\n if (process.env.GOOGLE_APPLICATION_CREDENTIALS) return false;\n if (process.env.GRIMOIRE_API_URL) return true;\n const config = await loadConsumerConfig();\n return config !== null;\n}\n\nexport async function cmdInit(): Promise<void> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const ask = (q: string): Promise<string> =>\n new Promise((resolve) => rl.question(q, resolve));\n\n const existing = await loadConsumerConfig();\n\n const apiUrl = await ask(`API URL${existing ? ` [${existing.apiUrl}]` : \"\"}: `);\n const apiKey = await ask(`API Key${existing ? \" [****]\" : \"\"}: `);\n\n const config: ConsumerConfig = {\n apiUrl: apiUrl.trim() || existing?.apiUrl || \"\",\n apiKey: apiKey.trim() || existing?.apiKey || \"\",\n };\n\n rl.close();\n\n if (!config.apiUrl || !config.apiKey) {\n throw new Error(\"Both API URL and API Key are required.\");\n }\n\n await saveConsumerConfig(config);\n console.log(`\\nSaved to ${CONFIG_FILE}`);\n}\n", "import type { SearchResult, SourceMeta } from \"./types.js\";\nimport type { ConsumerConfig } from \"./consumer-config.js\";\nimport { bold, cyan, yellow } from \"./format.js\";\n\nasync function apiRequest<T>(config: ConsumerConfig, path: string, options?: RequestInit): Promise<T> {\n const url = `${config.apiUrl.replace(/\\/$/, \"\")}${path}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": config.apiKey,\n ...options?.headers,\n },\n });\n } catch {\n throw new Error(`Cannot reach Grimoire API at ${config.apiUrl}. Check your GRIMOIRE_API_URL.`);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new Error(\"Invalid API key. Check your GRIMOIRE_API_KEY or run 'grimoire init'.\");\n }\n\n if (!response.ok) {\n throw new Error(`API error: ${response.status} ${response.statusText}`);\n }\n\n return response.json() as Promise<T>;\n}\n\nexport async function cmdConsumerSearch(\n config: ConsumerConfig,\n query: string,\n options: { source?: string; topN?: number; candidates?: number; compact?: boolean },\n): Promise<void> {\n const data = await apiRequest<{ results: SearchResult[] }>(config, \"/search\", {\n method: \"POST\",\n body: JSON.stringify({\n query,\n source: options.source,\n topN: options.topN,\n candidates: options.candidates,\n }),\n });\n\n if (data.results.length === 0) {\n console.log(\"No results found.\");\n return;\n }\n\n if (options.compact) {\n for (const r of data.results) {\n console.log(`${r.relevance_score.toFixed(4)} | ${r.source} | ${r.title} | ${r.heading_path.join(\" > \")} | ${r.url}`);\n }\n return;\n }\n\n for (let i = 0; i < data.results.length; i++) {\n const r = data.results[i];\n console.log(`\\n${bold(`[${i + 1}] ${r.title}`)} (${r.relevance_score.toFixed(4)})`);\n console.log(` ${cyan(r.url)}`);\n console.log(` ${yellow(r.heading_path.join(\" > \"))}`);\n console.log(` ${r.content.replace(/\\n/g, \" \")}`);\n }\n}\n\nexport async function cmdConsumerList(config: ConsumerConfig, options?: { names?: boolean }): Promise<void> {\n const data = await apiRequest<{ sources: SourceMeta[] }>(config, \"/list\");\n\n if (data.sources.length === 0) {\n console.log(\"No sources available.\");\n return;\n }\n\n if (options?.names) {\n for (const s of data.sources) {\n console.log(s.source);\n }\n return;\n }\n\n console.log(\"\\nSources:\\n\");\n for (const s of data.sources) {\n const ver = s.version ? ` v${s.version}` : \"\";\n console.log(` ${bold(s.source)}${ver}`);\n console.log(` ${s.chunk_count} chunks, ${s.url_count} URLs, last refreshed ${s.last_refreshed}`);\n }\n}\n\nexport async function cmdConsumerStats(config: ConsumerConfig): Promise<void> {\n const data = await apiRequest<{ sources: SourceMeta[]; totalChunks: number; totalUrls: number }>(config, \"/stats\");\n\n if (data.sources.length === 0) {\n console.log(\"No sources have been refreshed yet.\");\n return;\n }\n\n console.log(\"\\nSource Statistics:\\n\");\n for (const s of data.sources) {\n const ver = s.version ? ` v${s.version}` : \"\";\n console.log(` ${bold(s.source)}${ver}`);\n console.log(` Chunks: ${s.chunk_count}`);\n console.log(` URLs: ${s.url_count}`);\n console.log(` Last refreshed: ${s.last_refreshed}`);\n }\n\n console.log(`\\n Total: ${data.totalChunks} chunks across ${data.totalUrls} URLs from ${data.sources.length} sources`);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { parseArgs } from \"node:util\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { detectConsumerMode, resolveConsumerConfig, cmdInit, cmdRerank } from \"./consumer-config.js\";\nimport { cmdConsumerSearch, cmdConsumerList, cmdConsumerStats } from \"./consumer.js\";\n\nconst PROJECT_ROOT = resolve(import.meta.dirname, \"..\");\n\nconst envPath = join(PROJECT_ROOT, \".env\");\nif (existsSync(envPath)) {\n for (const line of readFileSync(envPath, \"utf-8\").split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex);\n const value = trimmed.slice(eqIndex + 1);\n if (!process.env[key]) {\n process.env[key] = value;\n }\n }\n}\n\nconst ADMIN_ONLY_COMMANDS = [\"add\", \"refresh\", \"delete\", \"scrape-urls\", \"export\", \"apikey\"];\n\nasync function cmdUpdate(): Promise<void> {\n const { execSync } = await import(\"node:child_process\");\n const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, \"package.json\"), \"utf-8\"));\n console.log(`Current version: ${pkg.version}`);\n console.log(\"Checking for updates...\");\n execSync(\"npm install -g @astrofoundry/grimoire@latest\", { stdio: \"inherit\" });\n const updated = JSON.parse(readFileSync(join(PROJECT_ROOT, \"package.json\"), \"utf-8\"));\n if (updated.version === pkg.version) {\n console.log(\"Already on the latest version.\");\n } else {\n console.log(`Updated to ${updated.version}.`);\n }\n}\n\nfunction showHelp(isConsumer: boolean): void {\n if (isConsumer) {\n console.log(`\ngrimoire - Documentation RAG\n\nUSAGE\n grimoire search \"<query>\" [--source <name>] [--top <n>] [--candidates <n>] [--compact] [--no-rerank]\n\nQUERY TIPS\n - Use the library's own terminology (\"Firestore batched writes\"\n not \"how do I do multi-write in firestore\")\n - Exact identifiers (max_connections, ExportContext, snake_case,\n dotted.paths, camelCase) are matched literally - include them verbatim\n - Scope with --source when you know the area\n - Rephrase if first search misses\n - Prefer --compact for scanning; omit for full snippets\n - Cite the URL in your answer when precision matters\n\nEXAMPLES\n grimoire search \"Firestore batched writes\" --source firebase-firestore --compact\n grimoire search \"react server components\" --top 3\n grimoire search \"mysql max_connections default\" --source gcp-cloud-sql\n grimoire list --names\n\nFLAGS\n --source <name> Scope to one indexed source. Run \\`grimoire list --names\\`\n for the full list (sources are added regularly).\n --top <n> Max results. Default: 5.\n --candidates <n> Retrieval pool size before final ranking. Default: 50,\n max: 200. Raise for broad/ambiguous queries.\n --compact One line per result: score | source | title | heading | url\n Default (non-compact): multi-line block with title, URL,\n heading path, and content snippet.\n --no-rerank Skip local reranking for this search even if a reranker\n is configured. No effect when none is configured.\n\nRELEVANCE SCORES\n Range 0-1 (higher = better). >0.85 strong match, 0.6-0.85 relevant,\n <0.6 usually too weak to cite. \"No results found.\" + exit 0 = clean miss.\n Results are ranked by hybrid vector + exact-identifier match; the score\n shown is vector similarity, so an exact identifier hit can rank first\n with a modest score. With RERANKER_URL set, results are reranked locally\n and the score is the reranker's relevance instead.\n\nMANAGEMENT\n grimoire list [--names] Show indexed sources\n grimoire stats Index statistics\n grimoire init Configure API connection (first-time setup)\n grimoire rerank Show reranker status (env, config, effective)\n grimoire rerank <url> Enable local reranking (saved to config)\n grimoire rerank off Disable local reranking (removes from config)\n grimoire update Update grimoire itself\n grimoire --version Print CLI version\n\nENVIRONMENT\n GRIMOIRE_API_URL API endpoint URL\n GRIMOIRE_API_KEY API key\n RERANKER_URL Optional. Local-network reranker endpoint; when\n set, search fetches a 50-result pool and reranks\n client-side. Overrides \"rerankerUrl\" in\n ~/.grimoire/config.json. If the reranker is\n unreachable, search warns on stderr and falls\n back to the API ranking after a 15s timeout.\n`);\n } else {\n console.log(`\ngrimoire - Documentation RAG System (admin)\n\nCommands:\n add <name> --url <url> Add a new documentation source\n refresh <source> Scrape, convert, then sync: only chunks\n whose content changed are re-embedded and\n written; removed chunks are deleted\n refresh <source> --from-html Re-convert from cached HTML, then sync\n refresh <source> --from-markdown Re-chunk from cached markdown, then sync\n refresh <source> --full Purge chunks + caches, re-scrape from scratch\n refresh <source> --skip-store Dry run: scrape/convert/chunk only,\n no embedding, no Firestore writes\n refresh <source> --allow-shrink Override the shrink/growth guards when a\n large chunk-count change is expected\n refresh --all Refresh all sources\n delete <source> Delete source and all its chunks\n scrape-urls <source> Fetch missing pages from urls.json\n update Update grimoire to the latest version\n search \"<query>\" [--source <n>] [--top <n>] [--candidates <n>] [--compact]\n Hybrid search (vector + exact identifiers)\n list [--names] List all sources\n stats Show chunk/source statistics\n export <source> Export source as JSON\n apikey create <name> Generate an API key\n apikey list List API keys\n apikey delete <name> Delete an API key\n`);\n }\n}\n\nasync function main(): Promise<void> {\n const command = process.argv[2];\n\n if (command === \"--version\" || command === \"-v\") {\n const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, \"package.json\"), \"utf-8\"));\n console.log(pkg.version);\n process.exit(0);\n }\n\n const isConsumer = await detectConsumerMode();\n\n if (!command || command === \"--help\" || command === \"-h\") {\n showHelp(isConsumer);\n process.exit(0);\n }\n\n if (command === \"update\") {\n await cmdUpdate();\n return;\n }\n\n if (isConsumer) {\n if (command === \"init\") {\n await cmdInit();\n return;\n }\n\n if (command === \"rerank\") {\n await cmdRerank(process.argv[3]);\n return;\n }\n\n if (ADMIN_ONLY_COMMANDS.includes(command)) {\n console.error(`The '${command}' command is only available in admin mode.`);\n process.exit(1);\n }\n\n const config = await resolveConsumerConfig().catch(() => {\n console.error(\"Grimoire is not configured yet. Run 'grimoire init' to set up your API connection.\");\n process.exit(1);\n });\n\n if (command === \"search\") {\n const args = parseArgs({\n args: process.argv.slice(3),\n options: {\n source: { type: \"string\" },\n top: { type: \"string\" },\n candidates: { type: \"string\" },\n compact: { type: \"boolean\", default: false },\n \"no-rerank\": { type: \"boolean\", default: false },\n },\n allowPositionals: true,\n });\n const query = args.positionals[0];\n if (!query) {\n console.error(\"Usage: grimoire search \\\"<query>\\\" [--source <name>] [--top <n>] [--candidates <n>] [--compact] [--no-rerank]\");\n process.exit(1);\n }\n const topN = args.values.top ? parseInt(args.values.top, 10) : undefined;\n const candidates = args.values.candidates ? parseInt(args.values.candidates, 10) : undefined;\n await cmdConsumerSearch(config, query, {\n source: args.values.source,\n topN,\n candidates,\n compact: args.values.compact,\n noRerank: args.values[\"no-rerank\"],\n });\n } else if (command === \"list\") {\n const args = parseArgs({\n args: process.argv.slice(3),\n options: { names: { type: \"boolean\", default: false } },\n allowPositionals: true,\n });\n await cmdConsumerList(config, { names: args.values.names });\n } else if (command === \"stats\") {\n await cmdConsumerStats(config);\n } else {\n console.error(`Unknown command: ${command}. Run \"grimoire --help\" for usage.`);\n process.exit(1);\n }\n return;\n }\n\n if (command === \"init\") {\n await cmdInit();\n return;\n }\n\n const { ADMIN_COMMANDS } = await import(\"./admin.js\");\n const handler = ADMIN_COMMANDS[command];\n if (!handler) {\n console.error(`Unknown command: ${command}. Run \"grimoire --help\" for usage.`);\n process.exit(1);\n }\n\n await handler();\n}\n\nconst GCP_AUTH_PATTERNS = [\n \"Unable to detect a Project Id\",\n \"Could not load the default credentials\",\n \"invalid_grant\",\n \"invalid_rapt\",\n \"UNAUTHENTICATED\",\n \"Getting metadata from plugin failed\",\n];\n\nmain().catch((err: Error) => {\n const msg = err.message ?? String(err);\n if (GCP_AUTH_PATTERNS.some((p) => msg.includes(p))) {\n console.error(\"Google Cloud authentication failed. Re-authenticate with:\\n\\n gcloud auth application-default login\\n\");\n } else {\n console.error(`Error: ${msg}`);\n }\n process.exit(1);\n});\n", "import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { createInterface } from \"node:readline\";\n\nconst CONFIG_DIR = join(homedir(), \".grimoire\");\nconst CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\nexport interface ConsumerConfig {\n apiUrl: string;\n apiKey: string;\n rerankerUrl?: string;\n}\n\nexport async function loadConsumerConfig(): Promise<ConsumerConfig | null> {\n const raw = await readFile(CONFIG_FILE, \"utf-8\").catch(() => null);\n if (!raw) return null;\n const data = JSON.parse(raw);\n if (typeof data.apiUrl === \"string\" && typeof data.apiKey === \"string\") {\n return {\n apiUrl: data.apiUrl,\n apiKey: data.apiKey,\n ...(typeof data.rerankerUrl === \"string\" && data.rerankerUrl\n ? { rerankerUrl: data.rerankerUrl }\n : {}),\n };\n }\n return null;\n}\n\nexport async function saveConsumerConfig(config: ConsumerConfig): Promise<void> {\n await mkdir(CONFIG_DIR, { recursive: true });\n await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// RERANKER_URL env always wins over the config file: the reranker is\n// network-specific, so the shell is the natural per-machine override.\nexport async function resolveConsumerConfig(): Promise<ConsumerConfig> {\n const envUrl = process.env.GRIMOIRE_API_URL;\n const envKey = process.env.GRIMOIRE_API_KEY;\n const envReranker = process.env.RERANKER_URL;\n\n if (envUrl && envKey) {\n return {\n apiUrl: envUrl,\n apiKey: envKey,\n ...(envReranker ? { rerankerUrl: envReranker } : {}),\n };\n }\n\n const fileConfig = await loadConsumerConfig();\n if (fileConfig) {\n return envReranker ? { ...fileConfig, rerankerUrl: envReranker } : fileConfig;\n }\n\n throw new Error(\"Grimoire is not configured. Run 'grimoire init' to set up.\");\n}\n\nexport function isConsumerMode(): boolean {\n return !!process.env.GRIMOIRE_API_URL;\n}\n\nexport async function detectConsumerMode(): Promise<boolean> {\n if (process.env.GOOGLE_APPLICATION_CREDENTIALS) return false;\n if (process.env.GRIMOIRE_API_URL) return true;\n const config = await loadConsumerConfig();\n return config !== null;\n}\n\nasync function setConfigRerankerUrl(url: string | undefined): Promise<void> {\n const existing = await loadConsumerConfig();\n if (!existing) {\n throw new Error(\"Grimoire is not configured. Run 'grimoire init' first.\");\n }\n await saveConsumerConfig({\n apiUrl: existing.apiUrl,\n apiKey: existing.apiKey,\n ...(url ? { rerankerUrl: url } : {}),\n });\n}\n\nexport async function cmdRerank(arg?: string): Promise<void> {\n if (!arg) {\n const envUrl = process.env.RERANKER_URL;\n const fileConfig = await loadConsumerConfig();\n const effective = envUrl ?? fileConfig?.rerankerUrl;\n console.log(`Env RERANKER_URL: ${envUrl ?? \"(not set)\"}`);\n console.log(`Config rerankerUrl: ${fileConfig?.rerankerUrl ?? \"(not set)\"}`);\n console.log(`Effective: ${effective ?? \"off\"}`);\n return;\n }\n\n if (arg === \"off\") {\n await setConfigRerankerUrl(undefined);\n console.log(`Reranker removed from ${CONFIG_FILE}.`);\n if (process.env.RERANKER_URL) {\n console.log(\"Note: RERANKER_URL is set in your environment and still takes precedence. Unset it too.\");\n }\n return;\n }\n\n new URL(arg);\n await setConfigRerankerUrl(arg);\n console.log(`Reranker set to ${arg} in ${CONFIG_FILE}.`);\n}\n\nexport async function cmdInit(): Promise<void> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const ask = (q: string): Promise<string> =>\n new Promise((resolve) => rl.question(q, resolve));\n\n const existing = await loadConsumerConfig();\n\n const apiUrl = await ask(`API URL${existing ? ` [${existing.apiUrl}]` : \"\"}: `);\n const apiKey = await ask(`API Key${existing ? \" [****]\" : \"\"}: `);\n const rerankerUrl = await ask(\n `Reranker URL (optional, local network)${existing?.rerankerUrl ? ` [${existing.rerankerUrl}]` : \"\"}: `,\n );\n\n const resolvedReranker = rerankerUrl.trim() || existing?.rerankerUrl;\n const config: ConsumerConfig = {\n apiUrl: apiUrl.trim() || existing?.apiUrl || \"\",\n apiKey: apiKey.trim() || existing?.apiKey || \"\",\n ...(resolvedReranker ? { rerankerUrl: resolvedReranker } : {}),\n };\n\n rl.close();\n\n if (!config.apiUrl || !config.apiKey) {\n throw new Error(\"Both API URL and API Key are required.\");\n }\n\n await saveConsumerConfig(config);\n console.log(`\\nSaved to ${CONFIG_FILE}`);\n}\n", "import type { SearchResult, SourceMeta } from \"./types.js\";\nimport type { ConsumerConfig } from \"./consumer-config.js\";\nimport { bold, cyan, yellow } from \"./format.js\";\nimport { rerank, rerankDocText, RERANK_POOL_SIZE } from \"./reranker.js\";\n\n// The API ranks by hybrid fusion server-side; when a reranker is reachable\n// (local network), fetch a larger pool and rerank client-side with the same\n// contextual text the admin pipeline uses.\nfunction contextualText(result: SearchResult): string {\n return rerankDocText(result.title, result.heading_path, result.content);\n}\n\nconst RERANK_TIMEOUT_MS = 30_000;\n\n// Reranking is an enhancement, not a dependency: on failure or timeout the\n// API's fused ranking is already in hand, so degrade to it with a warning.\nasync function rerankResults(\n rerankerUrl: string,\n query: string,\n results: SearchResult[],\n topN: number,\n): Promise<SearchResult[]> {\n try {\n const reranked = await rerank(\n query,\n results.map(contextualText),\n topN,\n rerankerUrl,\n RERANK_TIMEOUT_MS,\n );\n return reranked.map((r) => ({\n ...results[r.index],\n relevance_score: r.relevance_score,\n }));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(\n `Warning: reranker at ${rerankerUrl} unavailable (${message}); using API ranking. ` +\n \"Use --no-rerank or 'grimoire rerank off' to silence this.\",\n );\n return results.slice(0, topN);\n }\n}\n\nasync function apiRequest<T>(config: ConsumerConfig, path: string, options?: RequestInit): Promise<T> {\n const url = `${config.apiUrl.replace(/\\/$/, \"\")}${path}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": config.apiKey,\n ...options?.headers,\n },\n });\n } catch {\n throw new Error(`Cannot reach Grimoire API at ${config.apiUrl}. Check your GRIMOIRE_API_URL.`);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new Error(\"Invalid API key. Check your GRIMOIRE_API_KEY or run 'grimoire init'.\");\n }\n\n if (!response.ok) {\n throw new Error(`API error: ${response.status} ${response.statusText}`);\n }\n\n return response.json() as Promise<T>;\n}\n\nexport async function cmdConsumerSearch(\n config: ConsumerConfig,\n query: string,\n options: { source?: string; topN?: number; candidates?: number; compact?: boolean; noRerank?: boolean },\n): Promise<void> {\n const topN = options.topN ?? 5;\n const rerankerUrl = options.noRerank ? undefined : config.rerankerUrl;\n\n const data = await apiRequest<{ results: SearchResult[] }>(config, \"/search\", {\n method: \"POST\",\n body: JSON.stringify({\n query,\n source: options.source,\n topN: rerankerUrl ? Math.max(topN, RERANK_POOL_SIZE) : topN,\n candidates: options.candidates,\n }),\n });\n\n if (data.results.length === 0) {\n console.log(\"No results found.\");\n return;\n }\n\n if (rerankerUrl) {\n data.results = await rerankResults(rerankerUrl, query, data.results, topN);\n }\n\n if (options.compact) {\n for (const r of data.results) {\n console.log(`${r.relevance_score.toFixed(4)} | ${r.source} | ${r.title} | ${r.heading_path.join(\" > \")} | ${r.url}`);\n }\n return;\n }\n\n for (let i = 0; i < data.results.length; i++) {\n const r = data.results[i];\n console.log(`\\n${bold(`[${i + 1}] ${r.title}`)} (${r.relevance_score.toFixed(4)})`);\n console.log(` ${cyan(r.url)}`);\n console.log(` ${yellow(r.heading_path.join(\" > \"))}`);\n console.log(` ${r.content.replace(/\\n/g, \" \")}`);\n }\n}\n\nexport async function cmdConsumerList(config: ConsumerConfig, options?: { names?: boolean }): Promise<void> {\n const data = await apiRequest<{ sources: SourceMeta[] }>(config, \"/list\");\n\n if (data.sources.length === 0) {\n console.log(\"No sources available.\");\n return;\n }\n\n if (options?.names) {\n for (const s of data.sources) {\n console.log(s.source);\n }\n return;\n }\n\n console.log(\"\\nSources:\\n\");\n for (const s of data.sources) {\n const ver = s.version ? ` v${s.version}` : \"\";\n console.log(` ${bold(s.source)}${ver}`);\n console.log(` ${s.chunk_count} chunks, ${s.url_count} URLs, last refreshed ${s.last_refreshed}`);\n }\n}\n\nexport async function cmdConsumerStats(config: ConsumerConfig): Promise<void> {\n const data = await apiRequest<{ sources: SourceMeta[]; totalChunks: number; totalUrls: number }>(config, \"/stats\");\n\n if (data.sources.length === 0) {\n console.log(\"No sources have been refreshed yet.\");\n return;\n }\n\n console.log(\"\\nSource Statistics:\\n\");\n for (const s of data.sources) {\n const ver = s.version ? ` v${s.version}` : \"\";\n console.log(` ${bold(s.source)}${ver}`);\n console.log(` Chunks: ${s.chunk_count}`);\n console.log(` URLs: ${s.url_count}`);\n console.log(` Last refreshed: ${s.last_refreshed}`);\n }\n\n console.log(`\\n Total: ${data.totalChunks} chunks across ${data.totalUrls} URLs from ${data.sources.length} sources`);\n}\n"],
5
+ "mappings": ";;;;;;;;;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,cAAc,kBAAkB;AACzC,SAAS,QAAAA,OAAM,eAAe;;;ACF9B,SAAS,UAAU,WAAW,aAAa;AAC3C,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAEhC,IAAM,aAAa,KAAK,QAAQ,GAAG,WAAW;AAC9C,IAAM,cAAc,KAAK,YAAY,aAAa;AAQlD,eAAsB,qBAAqD;AACzE,QAAM,MAAM,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,MAAM,IAAI;AACjE,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,MAAI,OAAO,KAAK,WAAW,YAAY,OAAO,KAAK,WAAW,UAAU;AACtE,WAAO;AAAA,MACL,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,GAAI,OAAO,KAAK,gBAAgB,YAAY,KAAK,cAC7C,EAAE,aAAa,KAAK,YAAY,IAChC,CAAC;AAAA,IACP;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,mBAAmB,QAAuC;AAC9E,QAAM,MAAM,YAAY,EAAE,WAAW,KAAK,CAAC;AAC3C,QAAM,UAAU,aAAa,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,MAAM,OAAO;AAC9E;AAIA,eAAsB,wBAAiD;AACrE,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,cAAc,QAAQ,IAAI;AAEhC,MAAI,UAAU,QAAQ;AACpB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,GAAI,cAAc,EAAE,aAAa,YAAY,IAAI,CAAC;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,aAAa,MAAM,mBAAmB;AAC5C,MAAI,YAAY;AACd,WAAO,cAAc,EAAE,GAAG,YAAY,aAAa,YAAY,IAAI;AAAA,EACrE;AAEA,QAAM,IAAI,MAAM,4DAA4D;AAC9E;AAMA,eAAsB,qBAAuC;AAC3D,MAAI,QAAQ,IAAI,+BAAgC,QAAO;AACvD,MAAI,QAAQ,IAAI,iBAAkB,QAAO;AACzC,QAAM,SAAS,MAAM,mBAAmB;AACxC,SAAO,WAAW;AACpB;AAEA,eAAe,qBAAqB,KAAwC;AAC1E,QAAM,WAAW,MAAM,mBAAmB;AAC1C,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,QAAM,mBAAmB;AAAA,IACvB,QAAQ,SAAS;AAAA,IACjB,QAAQ,SAAS;AAAA,IACjB,GAAI,MAAM,EAAE,aAAa,IAAI,IAAI,CAAC;AAAA,EACpC,CAAC;AACH;AAEA,eAAsB,UAAU,KAA6B;AAC3D,MAAI,CAAC,KAAK;AACR,UAAM,SAAS,QAAQ,IAAI;AAC3B,UAAM,aAAa,MAAM,mBAAmB;AAC5C,UAAM,YAAY,UAAU,YAAY;AACxC,YAAQ,IAAI,uBAAuB,UAAU,WAAW,EAAE;AAC1D,YAAQ,IAAI,uBAAuB,YAAY,eAAe,WAAW,EAAE;AAC3E,YAAQ,IAAI,uBAAuB,aAAa,KAAK,EAAE;AACvD;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO;AACjB,UAAM,qBAAqB,MAAS;AACpC,YAAQ,IAAI,yBAAyB,WAAW,GAAG;AACnD,QAAI,QAAQ,IAAI,cAAc;AAC5B,cAAQ,IAAI,yFAAyF;AAAA,IACvG;AACA;AAAA,EACF;AAEA,MAAI,IAAI,GAAG;AACX,QAAM,qBAAqB,GAAG;AAC9B,UAAQ,IAAI,mBAAmB,GAAG,OAAO,WAAW,GAAG;AACzD;AAEA,eAAsB,UAAyB;AAC7C,QAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,QAAM,MAAM,CAAC,MACX,IAAI,QAAQ,CAACC,aAAY,GAAG,SAAS,GAAGA,QAAO,CAAC;AAElD,QAAM,WAAW,MAAM,mBAAmB;AAE1C,QAAM,SAAS,MAAM,IAAI,UAAU,WAAW,KAAK,SAAS,MAAM,MAAM,EAAE,IAAI;AAC9E,QAAM,SAAS,MAAM,IAAI,UAAU,WAAW,YAAY,EAAE,IAAI;AAChE,QAAM,cAAc,MAAM;AAAA,IACxB,yCAAyC,UAAU,cAAc,KAAK,SAAS,WAAW,MAAM,EAAE;AAAA,EACpG;AAEA,QAAM,mBAAmB,YAAY,KAAK,KAAK,UAAU;AACzD,QAAM,SAAyB;AAAA,IAC7B,QAAQ,OAAO,KAAK,KAAK,UAAU,UAAU;AAAA,IAC7C,QAAQ,OAAO,KAAK,KAAK,UAAU,UAAU;AAAA,IAC7C,GAAI,mBAAmB,EAAE,aAAa,iBAAiB,IAAI,CAAC;AAAA,EAC9D;AAEA,KAAG,MAAM;AAET,MAAI,CAAC,OAAO,UAAU,CAAC,OAAO,QAAQ;AACpC,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AAEA,QAAM,mBAAmB,MAAM;AAC/B,UAAQ,IAAI;AAAA,WAAc,WAAW,EAAE;AACzC;;;AC9HA,SAAS,eAAe,QAA8B;AACpD,SAAO,cAAc,OAAO,OAAO,OAAO,cAAc,OAAO,OAAO;AACxE;AAEA,IAAM,oBAAoB;AAI1B,eAAe,cACb,aACA,OACA,SACA,MACyB;AACzB,MAAI;AACF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA,QAAQ,IAAI,cAAc;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,WAAO,SAAS,IAAI,CAAC,OAAO;AAAA,MAC1B,GAAG,QAAQ,EAAE,KAAK;AAAA,MAClB,iBAAiB,EAAE;AAAA,IACrB,EAAE;AAAA,EACJ,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,YAAQ;AAAA,MACN,wBAAwB,WAAW,iBAAiB,OAAO;AAAA,IAE7D;AACA,WAAO,QAAQ,MAAM,GAAG,IAAI;AAAA,EAC9B;AACF;AAEA,eAAe,WAAc,QAAwB,MAAc,SAAmC;AACpG,QAAM,MAAM,GAAG,OAAO,OAAO,QAAQ,OAAO,EAAE,CAAC,GAAG,IAAI;AAEtD,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK;AAAA,MAC1B,GAAG;AAAA,MACH,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,aAAa,OAAO;AAAA,QACpB,GAAG,SAAS;AAAA,MACd;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,UAAM,IAAI,MAAM,gCAAgC,OAAO,MAAM,gCAAgC;AAAA,EAC/F;AAEA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACtD,UAAM,IAAI,MAAM,sEAAsE;AAAA,EACxF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,cAAc,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EACxE;AAEA,SAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,kBACpB,QACA,OACA,SACe;AACf,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,cAAc,QAAQ,WAAW,SAAY,OAAO;AAE1D,QAAM,OAAO,MAAM,WAAwC,QAAQ,WAAW;AAAA,IAC5E,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,QAAQ,QAAQ;AAAA,MAChB,MAAM,cAAc,KAAK,IAAI,MAAM,gBAAgB,IAAI;AAAA,MACvD,YAAY,QAAQ;AAAA,IACtB,CAAC;AAAA,EACH,CAAC;AAED,MAAI,KAAK,QAAQ,WAAW,GAAG;AAC7B,YAAQ,IAAI,mBAAmB;AAC/B;AAAA,EACF;AAEA,MAAI,aAAa;AACf,SAAK,UAAU,MAAM,cAAc,aAAa,OAAO,KAAK,SAAS,IAAI;AAAA,EAC3E;AAEA,MAAI,QAAQ,SAAS;AACnB,eAAW,KAAK,KAAK,SAAS;AAC5B,cAAQ,IAAI,GAAG,EAAE,gBAAgB,QAAQ,CAAC,CAAC,MAAM,EAAE,MAAM,MAAM,EAAE,KAAK,MAAM,EAAE,aAAa,KAAK,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE;AAAA,IACrH;AACA;AAAA,EACF;AAEA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,QAAQ,KAAK;AAC5C,UAAM,IAAI,KAAK,QAAQ,CAAC;AACxB,YAAQ,IAAI;AAAA,EAAK,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,gBAAgB,QAAQ,CAAC,CAAC,GAAG;AAClF,YAAQ,IAAI,OAAO,KAAK,EAAE,GAAG,CAAC,EAAE;AAChC,YAAQ,IAAI,OAAO,OAAO,EAAE,aAAa,KAAK,KAAK,CAAC,CAAC,EAAE;AACvD,YAAQ,IAAI,OAAO,EAAE,QAAQ,QAAQ,OAAO,GAAG,CAAC,EAAE;AAAA,EACpD;AACF;AAEA,eAAsB,gBAAgB,QAAwB,SAA8C;AAC1G,QAAM,OAAO,MAAM,WAAsC,QAAQ,OAAO;AAExE,MAAI,KAAK,QAAQ,WAAW,GAAG;AAC7B,YAAQ,IAAI,uBAAuB;AACnC;AAAA,EACF;AAEA,MAAI,SAAS,OAAO;AAClB,eAAW,KAAK,KAAK,SAAS;AAC5B,cAAQ,IAAI,EAAE,MAAM;AAAA,IACtB;AACA;AAAA,EACF;AAEA,UAAQ,IAAI,cAAc;AAC1B,aAAW,KAAK,KAAK,SAAS;AAC5B,UAAM,MAAM,EAAE,UAAU,KAAK,EAAE,OAAO,KAAK;AAC3C,YAAQ,IAAI,KAAK,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,EAAE;AACvC,YAAQ,IAAI,OAAO,EAAE,WAAW,YAAY,EAAE,SAAS,yBAAyB,EAAE,cAAc,EAAE;AAAA,EACpG;AACF;AAEA,eAAsB,iBAAiB,QAAuC;AAC5E,QAAM,OAAO,MAAM,WAA8E,QAAQ,QAAQ;AAEjH,MAAI,KAAK,QAAQ,WAAW,GAAG;AAC7B,YAAQ,IAAI,qCAAqC;AACjD;AAAA,EACF;AAEA,UAAQ,IAAI,wBAAwB;AACpC,aAAW,KAAK,KAAK,SAAS;AAC5B,UAAM,MAAM,EAAE,UAAU,KAAK,EAAE,OAAO,KAAK;AAC3C,YAAQ,IAAI,KAAK,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,EAAE;AACvC,YAAQ,IAAI,eAAe,EAAE,WAAW,EAAE;AAC1C,YAAQ,IAAI,aAAa,EAAE,SAAS,EAAE;AACtC,YAAQ,IAAI,uBAAuB,EAAE,cAAc,EAAE;AAAA,EACvD;AAEA,UAAQ,IAAI;AAAA,WAAc,KAAK,WAAW,kBAAkB,KAAK,SAAS,cAAc,KAAK,QAAQ,MAAM,UAAU;AACvH;;;AFtJA,IAAM,eAAe,QAAQ,YAAY,SAAS,IAAI;AAEtD,IAAM,UAAUC,MAAK,cAAc,MAAM;AACzC,IAAI,WAAW,OAAO,GAAG;AACvB,aAAW,QAAQ,aAAa,SAAS,OAAO,EAAE,MAAM,IAAI,GAAG;AAC7D,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AACzC,UAAM,UAAU,QAAQ,QAAQ,GAAG;AACnC,QAAI,YAAY,GAAI;AACpB,UAAM,MAAM,QAAQ,MAAM,GAAG,OAAO;AACpC,UAAM,QAAQ,QAAQ,MAAM,UAAU,CAAC;AACvC,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,cAAQ,IAAI,GAAG,IAAI;AAAA,IACrB;AAAA,EACF;AACF;AAEA,IAAM,sBAAsB,CAAC,OAAO,WAAW,UAAU,eAAe,UAAU,QAAQ;AAE1F,eAAe,YAA2B;AACxC,QAAM,EAAE,SAAS,IAAI,MAAM,OAAO,oBAAoB;AACtD,QAAM,MAAM,KAAK,MAAM,aAAaA,MAAK,cAAc,cAAc,GAAG,OAAO,CAAC;AAChF,UAAQ,IAAI,oBAAoB,IAAI,OAAO,EAAE;AAC7C,UAAQ,IAAI,yBAAyB;AACrC,WAAS,gDAAgD,EAAE,OAAO,UAAU,CAAC;AAC7E,QAAM,UAAU,KAAK,MAAM,aAAaA,MAAK,cAAc,cAAc,GAAG,OAAO,CAAC;AACpF,MAAI,QAAQ,YAAY,IAAI,SAAS;AACnC,YAAQ,IAAI,gCAAgC;AAAA,EAC9C,OAAO;AACL,YAAQ,IAAI,cAAc,QAAQ,OAAO,GAAG;AAAA,EAC9C;AACF;AAEA,SAAS,SAAS,YAA2B;AAC3C,MAAI,YAAY;AACd,YAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CA6Df;AAAA,EACC,OAAO;AACL,YAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CA2Bf;AAAA,EACC;AACF;AAEA,eAAe,OAAsB;AACnC,QAAM,UAAU,QAAQ,KAAK,CAAC;AAE9B,MAAI,YAAY,eAAe,YAAY,MAAM;AAC/C,UAAM,MAAM,KAAK,MAAM,aAAaA,MAAK,cAAc,cAAc,GAAG,OAAO,CAAC;AAChF,YAAQ,IAAI,IAAI,OAAO;AACvB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,aAAa,MAAM,mBAAmB;AAE5C,MAAI,CAAC,WAAW,YAAY,YAAY,YAAY,MAAM;AACxD,aAAS,UAAU;AACnB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,YAAY,UAAU;AACxB,UAAM,UAAU;AAChB;AAAA,EACF;AAEA,MAAI,YAAY;AACd,QAAI,YAAY,QAAQ;AACtB,YAAM,QAAQ;AACd;AAAA,IACF;AAEA,QAAI,YAAY,UAAU;AACxB,YAAM,UAAU,QAAQ,KAAK,CAAC,CAAC;AAC/B;AAAA,IACF;AAEA,QAAI,oBAAoB,SAAS,OAAO,GAAG;AACzC,cAAQ,MAAM,QAAQ,OAAO,4CAA4C;AACzE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS,MAAM,sBAAsB,EAAE,MAAM,MAAM;AACvD,cAAQ,MAAM,oFAAoF;AAClG,cAAQ,KAAK,CAAC;AAAA,IAChB,CAAC;AAED,QAAI,YAAY,UAAU;AACxB,YAAM,OAAO,UAAU;AAAA,QACrB,MAAM,QAAQ,KAAK,MAAM,CAAC;AAAA,QAC1B,SAAS;AAAA,UACP,QAAQ,EAAE,MAAM,SAAS;AAAA,UACzB,KAAK,EAAE,MAAM,SAAS;AAAA,UACtB,YAAY,EAAE,MAAM,SAAS;AAAA,UAC7B,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,UAC3C,aAAa,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACjD;AAAA,QACA,kBAAkB;AAAA,MACpB,CAAC;AACD,YAAM,QAAQ,KAAK,YAAY,CAAC;AAChC,UAAI,CAAC,OAAO;AACV,gBAAQ,MAAM,6GAA+G;AAC7H,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA,YAAM,OAAO,KAAK,OAAO,MAAM,SAAS,KAAK,OAAO,KAAK,EAAE,IAAI;AAC/D,YAAM,aAAa,KAAK,OAAO,aAAa,SAAS,KAAK,OAAO,YAAY,EAAE,IAAI;AACnF,YAAM,kBAAkB,QAAQ,OAAO;AAAA,QACrC,QAAQ,KAAK,OAAO;AAAA,QACpB;AAAA,QACA;AAAA,QACA,SAAS,KAAK,OAAO;AAAA,QACrB,UAAU,KAAK,OAAO,WAAW;AAAA,MACnC,CAAC;AAAA,IACH,WAAW,YAAY,QAAQ;AAC7B,YAAM,OAAO,UAAU;AAAA,QACrB,MAAM,QAAQ,KAAK,MAAM,CAAC;AAAA,QAC1B,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,SAAS,MAAM,EAAE;AAAA,QACtD,kBAAkB;AAAA,MACpB,CAAC;AACD,YAAM,gBAAgB,QAAQ,EAAE,OAAO,KAAK,OAAO,MAAM,CAAC;AAAA,IAC5D,WAAW,YAAY,SAAS;AAC9B,YAAM,iBAAiB,MAAM;AAAA,IAC/B,OAAO;AACL,cAAQ,MAAM,oBAAoB,OAAO,oCAAoC;AAC7E,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA;AAAA,EACF;AAEA,MAAI,YAAY,QAAQ;AACtB,UAAM,QAAQ;AACd;AAAA,EACF;AAEA,QAAM,EAAE,eAAe,IAAI,MAAM,OAAO,qBAAY;AACpD,QAAM,UAAU,eAAe,OAAO;AACtC,MAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,oBAAoB,OAAO,oCAAoC;AAC7E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,QAAQ;AAChB;AAEA,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAe;AAC3B,QAAM,MAAM,IAAI,WAAW,OAAO,GAAG;AACrC,MAAI,kBAAkB,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,GAAG;AAClD,YAAQ,MAAM,wGAAwG;AAAA,EACxH,OAAO;AACL,YAAQ,MAAM,UAAU,GAAG,EAAE;AAAA,EAC/B;AACA,UAAQ,KAAK,CAAC;AAChB,CAAC;",
6
6
  "names": ["join", "resolve", "join"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrofoundry/grimoire",
3
- "version": "3.31.1",
3
+ "version": "3.32.0",
4
4
  "description": "Documentation RAG System",
5
5
  "keywords": [],
6
6
  "author": "",