@astrofoundry/grimoire 3.31.1 → 3.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{admin-ENGUPLQ6.js → admin-MA5SI5CH.js} +123 -98
- package/dist/admin-MA5SI5CH.js.map +7 -0
- package/dist/{chunk-R46N6C3C.js → chunk-VTTJCQRQ.js} +37 -2
- package/dist/chunk-VTTJCQRQ.js.map +7 -0
- package/dist/cli.js +126 -18
- package/dist/cli.js.map +2 -2
- package/package.json +1 -1
- package/dist/admin-ENGUPLQ6.js.map +0 -7
- package/dist/chunk-R46N6C3C.js.map +0 -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-
|
|
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 {
|
|
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 {
|
|
45
|
+
return {
|
|
46
|
+
apiUrl: envUrl,
|
|
47
|
+
apiKey: envKey,
|
|
48
|
+
...envReranker ? { rerankerUrl: envReranker } : {}
|
|
49
|
+
};
|
|
38
50
|
}
|
|
39
51
|
const fileConfig = await loadConsumerConfig();
|
|
40
|
-
if (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:
|
|
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
|
|
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,41 @@ 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
|
-
Range 0-1 (higher = better)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
307
|
+
Range 0-1 (higher = better), always descending with rank.
|
|
308
|
+
"No results found." + exit 0 = clean miss.
|
|
309
|
+
Without a reranker the score is hybrid-fusion strength relative to the
|
|
310
|
+
top result (top result is always 1.0); judge by rank and content, not by
|
|
311
|
+
absolute score. With RERANKER_URL set the score is the local reranker's
|
|
312
|
+
relevance: >0.85 strong match, 0.6-0.85 relevant, <0.6 usually too weak
|
|
313
|
+
to cite.
|
|
228
314
|
|
|
229
315
|
MANAGEMENT
|
|
230
316
|
grimoire list [--names] Show indexed sources
|
|
231
317
|
grimoire stats Index statistics
|
|
232
318
|
grimoire init Configure API connection (first-time setup)
|
|
319
|
+
grimoire rerank Show reranker status (env, config, effective)
|
|
320
|
+
grimoire rerank <url> Enable local reranking (saved to config)
|
|
321
|
+
grimoire rerank off Disable local reranking (removes from config)
|
|
233
322
|
grimoire update Update grimoire itself
|
|
234
323
|
grimoire --version Print CLI version
|
|
235
324
|
|
|
236
325
|
ENVIRONMENT
|
|
237
326
|
GRIMOIRE_API_URL API endpoint URL
|
|
238
327
|
GRIMOIRE_API_KEY API key
|
|
328
|
+
RERANKER_URL Optional. Local-network reranker endpoint; when
|
|
329
|
+
set, search fetches a 50-result pool and reranks
|
|
330
|
+
client-side. Overrides "rerankerUrl" in
|
|
331
|
+
~/.grimoire/config.json. If the reranker is
|
|
332
|
+
unreachable, search warns on stderr and falls
|
|
333
|
+
back to the API ranking after a 15s timeout.
|
|
239
334
|
`);
|
|
240
335
|
} else {
|
|
241
336
|
console.log(`
|
|
242
|
-
grimoire
|
|
337
|
+
grimoire - Documentation RAG System (admin)
|
|
243
338
|
|
|
244
339
|
Commands:
|
|
245
340
|
add <name> --url <url> Add a new documentation source
|
|
@@ -251,6 +346,8 @@ Commands:
|
|
|
251
346
|
refresh <source> --full Purge chunks + caches, re-scrape from scratch
|
|
252
347
|
refresh <source> --skip-store Dry run: scrape/convert/chunk only,
|
|
253
348
|
no embedding, no Firestore writes
|
|
349
|
+
refresh <source> --allow-shrink Override the shrink/growth guards when a
|
|
350
|
+
large chunk-count change is expected
|
|
254
351
|
refresh --all Refresh all sources
|
|
255
352
|
delete <source> Delete source and all its chunks
|
|
256
353
|
scrape-urls <source> Fetch missing pages from urls.json
|
|
@@ -287,6 +384,10 @@ async function main() {
|
|
|
287
384
|
await cmdInit();
|
|
288
385
|
return;
|
|
289
386
|
}
|
|
387
|
+
if (command === "rerank") {
|
|
388
|
+
await cmdRerank(process.argv[3]);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
290
391
|
if (ADMIN_ONLY_COMMANDS.includes(command)) {
|
|
291
392
|
console.error(`The '${command}' command is only available in admin mode.`);
|
|
292
393
|
process.exit(1);
|
|
@@ -302,18 +403,25 @@ async function main() {
|
|
|
302
403
|
source: { type: "string" },
|
|
303
404
|
top: { type: "string" },
|
|
304
405
|
candidates: { type: "string" },
|
|
305
|
-
compact: { type: "boolean", default: false }
|
|
406
|
+
compact: { type: "boolean", default: false },
|
|
407
|
+
"no-rerank": { type: "boolean", default: false }
|
|
306
408
|
},
|
|
307
409
|
allowPositionals: true
|
|
308
410
|
});
|
|
309
411
|
const query = args.positionals[0];
|
|
310
412
|
if (!query) {
|
|
311
|
-
console.error('Usage: grimoire search "<query>" [--source <name>] [--top <n>] [--candidates <n>] [--compact]');
|
|
413
|
+
console.error('Usage: grimoire search "<query>" [--source <name>] [--top <n>] [--candidates <n>] [--compact] [--no-rerank]');
|
|
312
414
|
process.exit(1);
|
|
313
415
|
}
|
|
314
416
|
const topN = args.values.top ? parseInt(args.values.top, 10) : void 0;
|
|
315
417
|
const candidates = args.values.candidates ? parseInt(args.values.candidates, 10) : void 0;
|
|
316
|
-
await cmdConsumerSearch(config, query, {
|
|
418
|
+
await cmdConsumerSearch(config, query, {
|
|
419
|
+
source: args.values.source,
|
|
420
|
+
topN,
|
|
421
|
+
candidates,
|
|
422
|
+
compact: args.values.compact,
|
|
423
|
+
noRerank: args.values["no-rerank"]
|
|
424
|
+
});
|
|
317
425
|
} else if (command === "list") {
|
|
318
426
|
const args = parseArgs({
|
|
319
427
|
args: process.argv.slice(3),
|
|
@@ -333,7 +441,7 @@ async function main() {
|
|
|
333
441
|
await cmdInit();
|
|
334
442
|
return;
|
|
335
443
|
}
|
|
336
|
-
const { ADMIN_COMMANDS } = await import("./admin-
|
|
444
|
+
const { ADMIN_COMMANDS } = await import("./admin-MA5SI5CH.js");
|
|
337
445
|
const handler = ADMIN_COMMANDS[command];
|
|
338
446
|
if (!handler) {
|
|
339
447
|
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": "
|
|
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), always descending with rank.\n \"No results found.\" + exit 0 = clean miss.\n Without a reranker the score is hybrid-fusion strength relative to the\n top result (top result is always 1.0); judge by rank and content, not by\n absolute score. With RERANKER_URL set the score is the local reranker's\n relevance: >0.85 strong match, 0.6-0.85 relevant, <0.6 usually too weak\n to cite.\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;AAAA,CA8Df;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
|
}
|