unrag 0.2.6 → 0.2.8
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/cli/index.js +2748 -63
- package/package.json +7 -3
- package/registry/connectors/google-drive/_api-types.ts +60 -0
- package/registry/connectors/google-drive/client.ts +99 -38
- package/registry/connectors/google-drive/sync.ts +97 -69
- package/registry/connectors/google-drive/types.ts +76 -37
- package/registry/connectors/notion/client.ts +12 -3
- package/registry/connectors/notion/render.ts +62 -23
- package/registry/connectors/notion/sync.ts +30 -23
- package/registry/core/assets.ts +11 -10
- package/registry/core/config.ts +11 -25
- package/registry/core/context-engine.ts +28 -0
- package/registry/core/deep-merge.ts +45 -0
- package/registry/core/index.ts +1 -0
- package/registry/core/ingest.ts +117 -44
- package/registry/core/rerank.ts +158 -0
- package/registry/core/types.ts +168 -0
- package/registry/embedding/_shared.ts +6 -1
- package/registry/embedding/ai.ts +2 -3
- package/registry/embedding/azure.ts +11 -2
- package/registry/embedding/bedrock.ts +11 -2
- package/registry/embedding/cohere.ts +11 -2
- package/registry/embedding/google.ts +11 -2
- package/registry/embedding/mistral.ts +11 -2
- package/registry/embedding/ollama.ts +18 -3
- package/registry/embedding/openai.ts +11 -2
- package/registry/embedding/openrouter.ts +53 -11
- package/registry/embedding/together.ts +15 -5
- package/registry/embedding/vertex.ts +11 -2
- package/registry/embedding/voyage.ts +16 -6
- package/registry/extractors/audio-transcribe/index.ts +39 -23
- package/registry/extractors/file-docx/index.ts +8 -1
- package/registry/extractors/file-pptx/index.ts +22 -1
- package/registry/extractors/file-xlsx/index.ts +24 -1
- package/registry/extractors/image-caption-llm/index.ts +8 -3
- package/registry/extractors/image-ocr/index.ts +9 -4
- package/registry/extractors/pdf-llm/index.ts +9 -4
- package/registry/extractors/pdf-text-layer/index.ts +128 -17
- package/registry/extractors/pdf-text-layer/pdfjs-dist-legacy.d.ts +6 -0
- package/registry/extractors/video-frames/index.ts +8 -3
- package/registry/extractors/video-transcribe/index.ts +40 -24
- package/registry/manifest.json +22 -6
- package/registry/rerank/cohere.ts +120 -0
- package/registry/rerank/custom.ts +60 -0
- package/registry/rerank/index.ts +39 -0
- package/registry/rerank/types.ts +58 -0
- package/registry/store/drizzle-postgres-pgvector/store.ts +24 -7
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
20
|
|
|
3
21
|
// cli/run.ts
|
|
4
|
-
import { intro, outro as
|
|
22
|
+
import { intro, outro as outro5 } from "@clack/prompts";
|
|
5
23
|
|
|
6
24
|
// cli/commands/init.ts
|
|
7
25
|
import {
|
|
@@ -358,6 +376,10 @@ async function copyRegistryFiles(selection) {
|
|
|
358
376
|
src: path2.join(selection.registryRoot, "core/config.ts"),
|
|
359
377
|
dest: path2.join(installBaseAbs, "core/config.ts")
|
|
360
378
|
},
|
|
379
|
+
{
|
|
380
|
+
src: path2.join(selection.registryRoot, "core/deep-merge.ts"),
|
|
381
|
+
dest: path2.join(installBaseAbs, "core/deep-merge.ts")
|
|
382
|
+
},
|
|
361
383
|
{
|
|
362
384
|
src: path2.join(selection.registryRoot, "core/context-engine.ts"),
|
|
363
385
|
dest: path2.join(installBaseAbs, "core/context-engine.ts")
|
|
@@ -579,6 +601,52 @@ async function copyExtractorFiles(selection) {
|
|
|
579
601
|
await writeText(dest, raw);
|
|
580
602
|
}
|
|
581
603
|
}
|
|
604
|
+
async function copyBatteryFiles(selection) {
|
|
605
|
+
const toAbs = (projectRelative) => path2.join(selection.projectRoot, projectRelative);
|
|
606
|
+
const installBaseAbs = toAbs(selection.installDir);
|
|
607
|
+
const batteryRegistryDir = selection.battery === "reranker" ? "rerank" : selection.battery;
|
|
608
|
+
const batteryRegistryAbs = path2.join(selection.registryRoot, batteryRegistryDir);
|
|
609
|
+
if (!await exists(batteryRegistryAbs)) {
|
|
610
|
+
throw new Error(`Unknown battery registry: ${path2.relative(selection.registryRoot, batteryRegistryAbs)}`);
|
|
611
|
+
}
|
|
612
|
+
const batteryFiles = await listFilesRecursive(batteryRegistryAbs);
|
|
613
|
+
const destRootAbs = path2.join(installBaseAbs, batteryRegistryDir);
|
|
614
|
+
const nonInteractive = Boolean(selection.yes) || !process.stdin.isTTY;
|
|
615
|
+
const overwritePolicy = selection.overwrite ?? "skip";
|
|
616
|
+
const shouldWrite = async (src, dest) => {
|
|
617
|
+
if (!await exists(dest))
|
|
618
|
+
return true;
|
|
619
|
+
if (overwritePolicy === "force")
|
|
620
|
+
return true;
|
|
621
|
+
if (nonInteractive)
|
|
622
|
+
return false;
|
|
623
|
+
try {
|
|
624
|
+
const [srcRaw, destRaw] = await Promise.all([readText(src), readText(dest)]);
|
|
625
|
+
if (srcRaw === destRaw)
|
|
626
|
+
return false;
|
|
627
|
+
} catch {}
|
|
628
|
+
const answer = await confirm({
|
|
629
|
+
message: `Overwrite ${path2.relative(selection.projectRoot, dest)}?`,
|
|
630
|
+
initialValue: false
|
|
631
|
+
});
|
|
632
|
+
if (isCancel(answer)) {
|
|
633
|
+
cancel("Cancelled.");
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
return Boolean(answer);
|
|
637
|
+
};
|
|
638
|
+
for (const src of batteryFiles) {
|
|
639
|
+
if (!await exists(src)) {
|
|
640
|
+
throw new Error(`Registry file missing: ${src}`);
|
|
641
|
+
}
|
|
642
|
+
const rel = path2.relative(batteryRegistryAbs, src);
|
|
643
|
+
const dest = path2.join(destRootAbs, rel);
|
|
644
|
+
if (!await shouldWrite(src, dest))
|
|
645
|
+
continue;
|
|
646
|
+
const raw = await readText(src);
|
|
647
|
+
await writeText(dest, raw);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
582
650
|
|
|
583
651
|
// cli/lib/json.ts
|
|
584
652
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -675,6 +743,7 @@ async function fetchPreset(input) {
|
|
|
675
743
|
// cli/lib/packageJson.ts
|
|
676
744
|
import path4 from "node:path";
|
|
677
745
|
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
746
|
+
import { spawn } from "node:child_process";
|
|
678
747
|
async function detectPackageManager(projectRoot) {
|
|
679
748
|
if (await exists(path4.join(projectRoot, "bun.lock")))
|
|
680
749
|
return "bun";
|
|
@@ -803,6 +872,15 @@ function depsForEmbeddingProvider(provider) {
|
|
|
803
872
|
deps["voyage-ai-provider"] = "^3.0.0";
|
|
804
873
|
return { deps, devDeps };
|
|
805
874
|
}
|
|
875
|
+
function depsForBattery(battery) {
|
|
876
|
+
const deps = {};
|
|
877
|
+
const devDeps = {};
|
|
878
|
+
if (battery === "reranker") {
|
|
879
|
+
deps["ai"] = "^6.0.3";
|
|
880
|
+
deps["@ai-sdk/cohere"] = "^3.0.1";
|
|
881
|
+
}
|
|
882
|
+
return { deps, devDeps };
|
|
883
|
+
}
|
|
806
884
|
function installCmd(pm) {
|
|
807
885
|
if (pm === "bun")
|
|
808
886
|
return "bun install";
|
|
@@ -812,6 +890,32 @@ function installCmd(pm) {
|
|
|
812
890
|
return "yarn";
|
|
813
891
|
return "npm install";
|
|
814
892
|
}
|
|
893
|
+
function installSpawnSpec(pm) {
|
|
894
|
+
if (pm === "bun")
|
|
895
|
+
return { cmd: "bun", args: ["install"] };
|
|
896
|
+
if (pm === "pnpm")
|
|
897
|
+
return { cmd: "pnpm", args: ["install"] };
|
|
898
|
+
if (pm === "yarn")
|
|
899
|
+
return { cmd: "yarn", args: [] };
|
|
900
|
+
return { cmd: "npm", args: ["install"] };
|
|
901
|
+
}
|
|
902
|
+
async function installDependencies(projectRoot) {
|
|
903
|
+
const pm = await detectPackageManager(projectRoot);
|
|
904
|
+
const { cmd, args } = installSpawnSpec(pm);
|
|
905
|
+
await new Promise((resolve, reject) => {
|
|
906
|
+
const child = spawn(cmd, args, {
|
|
907
|
+
cwd: projectRoot,
|
|
908
|
+
stdio: "inherit",
|
|
909
|
+
env: process.env
|
|
910
|
+
});
|
|
911
|
+
child.on("error", (err) => reject(err));
|
|
912
|
+
child.on("exit", (code, signal) => {
|
|
913
|
+
if (code === 0)
|
|
914
|
+
return resolve();
|
|
915
|
+
reject(new Error(`Dependency installation failed (${installCmd(pm)}). Exit code: ${code ?? "null"}, signal: ${signal ?? "null"}`));
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
}
|
|
815
919
|
|
|
816
920
|
// cli/lib/tsconfig.ts
|
|
817
921
|
import path5 from "node:path";
|
|
@@ -956,6 +1060,10 @@ var parseInitArgs = (args) => {
|
|
|
956
1060
|
}
|
|
957
1061
|
continue;
|
|
958
1062
|
}
|
|
1063
|
+
if (a === "--no-install") {
|
|
1064
|
+
out.noInstall = true;
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
959
1067
|
}
|
|
960
1068
|
return out;
|
|
961
1069
|
};
|
|
@@ -987,6 +1095,7 @@ async function initCommand(args) {
|
|
|
987
1095
|
const defaultRichMediaExtractors = extractorOptions.filter((o) => o.defaultSelected).map((o) => o.value).sort();
|
|
988
1096
|
const existing = await readJsonFile(path6.join(root, CONFIG_FILE));
|
|
989
1097
|
const parsed = parseInitArgs(args);
|
|
1098
|
+
const noInstall = Boolean(parsed.noInstall) || process.env.UNRAG_SKIP_INSTALL === "1";
|
|
990
1099
|
const preset = parsed.preset ? await fetchPreset(parsed.preset) : null;
|
|
991
1100
|
if (preset) {
|
|
992
1101
|
const hasOtherChoices = Boolean(parsed.installDir) || Boolean(parsed.storeAdapter) || Boolean(parsed.aliasBase) || typeof parsed.richMedia === "boolean" || (parsed.extractors ?? []).length > 0;
|
|
@@ -1185,6 +1294,9 @@ async function initCommand(args) {
|
|
|
1185
1294
|
const merged = mergeDeps(pkg, { ...deps, ...embeddingDeps.deps, ...extractorDeps, ...connectorDeps }, { ...devDeps, ...embeddingDeps.devDeps, ...extractorDevDeps, ...connectorDevDeps });
|
|
1186
1295
|
if (merged.changes.length > 0) {
|
|
1187
1296
|
await writePackageJson(root, merged.pkg);
|
|
1297
|
+
if (!noInstall) {
|
|
1298
|
+
await installDependencies(root);
|
|
1299
|
+
}
|
|
1188
1300
|
}
|
|
1189
1301
|
const config = {
|
|
1190
1302
|
installDir,
|
|
@@ -1200,7 +1312,7 @@ async function initCommand(args) {
|
|
|
1200
1312
|
};
|
|
1201
1313
|
await writeJsonFile(path6.join(root, CONFIG_FILE), config);
|
|
1202
1314
|
const pm = await detectPackageManager(root);
|
|
1203
|
-
const installLine = merged.changes.length
|
|
1315
|
+
const installLine = merged.changes.length === 0 ? "Dependencies already satisfied." : noInstall ? `Next: run \`${installCmd(pm)}\`` : "Dependencies installed.";
|
|
1204
1316
|
const isNext = Boolean((merged.pkg.dependencies ?? {})["next"]) || Boolean((merged.pkg.devDependencies ?? {})["next"]);
|
|
1205
1317
|
const tsconfigResult = isNext ? await patchTsconfigPaths({ projectRoot: root, installDir, aliasBase }) : { changed: false };
|
|
1206
1318
|
const envHint = (() => {
|
|
@@ -1339,16 +1451,24 @@ var parseAddArgs = (args) => {
|
|
|
1339
1451
|
out.yes = true;
|
|
1340
1452
|
continue;
|
|
1341
1453
|
}
|
|
1454
|
+
if (a === "--no-install") {
|
|
1455
|
+
out.noInstall = true;
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1342
1458
|
if (!out.kind && a && !a.startsWith("-")) {
|
|
1343
1459
|
if (a === "extractor") {
|
|
1344
1460
|
out.kind = "extractor";
|
|
1345
1461
|
continue;
|
|
1346
1462
|
}
|
|
1463
|
+
if (a === "battery") {
|
|
1464
|
+
out.kind = "battery";
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1347
1467
|
out.kind = "connector";
|
|
1348
1468
|
out.name = a;
|
|
1349
1469
|
continue;
|
|
1350
1470
|
}
|
|
1351
|
-
if (out.kind === "extractor" && !out.name && a && !a.startsWith("-")) {
|
|
1471
|
+
if ((out.kind === "extractor" || out.kind === "battery") && !out.name && a && !a.startsWith("-")) {
|
|
1352
1472
|
out.name = a;
|
|
1353
1473
|
continue;
|
|
1354
1474
|
}
|
|
@@ -1363,6 +1483,7 @@ async function addCommand(args) {
|
|
|
1363
1483
|
const parsed = parseAddArgs(args);
|
|
1364
1484
|
const kind = parsed.kind ?? "connector";
|
|
1365
1485
|
const name = parsed.name;
|
|
1486
|
+
const noInstall = Boolean(parsed.noInstall) || process.env.UNRAG_SKIP_INSTALL === "1";
|
|
1366
1487
|
const configPath = path7.join(root, CONFIG_FILE2);
|
|
1367
1488
|
const config = await readJsonFile(configPath);
|
|
1368
1489
|
if (!config?.installDir) {
|
|
@@ -1376,20 +1497,76 @@ async function addCommand(args) {
|
|
|
1376
1497
|
const manifest = await readRegistryManifest(registryRoot);
|
|
1377
1498
|
const availableExtractors = new Set(manifest.extractors.map((e) => e.id));
|
|
1378
1499
|
const availableConnectors = new Set(manifest.connectors.filter((c) => c.status === "available").map((c) => c.id));
|
|
1500
|
+
const availableBatteries = new Set((manifest.batteries ?? []).filter((b) => b.status === "available").map((b) => b.id));
|
|
1379
1501
|
if (!name) {
|
|
1380
1502
|
outro2([
|
|
1381
1503
|
"Usage:",
|
|
1382
1504
|
" unrag add <connector>",
|
|
1383
1505
|
" unrag add extractor <name>",
|
|
1506
|
+
" unrag add battery <name>",
|
|
1384
1507
|
"",
|
|
1385
1508
|
`Available connectors: ${Array.from(availableConnectors).join(", ")}`,
|
|
1386
|
-
`Available extractors: ${Array.from(availableExtractors).join(", ")}
|
|
1509
|
+
`Available extractors: ${Array.from(availableExtractors).join(", ")}`,
|
|
1510
|
+
`Available batteries: ${Array.from(availableBatteries).join(", ")}`
|
|
1387
1511
|
].join(`
|
|
1388
1512
|
`));
|
|
1389
1513
|
return;
|
|
1390
1514
|
}
|
|
1391
1515
|
const nonInteractive = parsed.yes || !process.stdin.isTTY;
|
|
1392
1516
|
const pkg = await readPackageJson(root);
|
|
1517
|
+
if (kind === "battery") {
|
|
1518
|
+
const battery = name;
|
|
1519
|
+
if (!battery || !availableBatteries.has(battery)) {
|
|
1520
|
+
outro2(`Unknown battery: ${name}
|
|
1521
|
+
|
|
1522
|
+
Available batteries: ${Array.from(availableBatteries).join(", ")}`);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
await copyBatteryFiles({
|
|
1526
|
+
projectRoot: root,
|
|
1527
|
+
registryRoot,
|
|
1528
|
+
installDir: config.installDir,
|
|
1529
|
+
battery,
|
|
1530
|
+
yes: nonInteractive
|
|
1531
|
+
});
|
|
1532
|
+
const { deps: deps2, devDeps: devDeps2 } = depsForBattery(battery);
|
|
1533
|
+
const merged2 = mergeDeps(pkg, deps2, devDeps2);
|
|
1534
|
+
if (merged2.changes.length > 0) {
|
|
1535
|
+
await writePackageJson(root, merged2.pkg);
|
|
1536
|
+
if (!noInstall) {
|
|
1537
|
+
await installDependencies(root);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const batteries = Array.from(new Set([...config.batteries ?? [], battery])).sort();
|
|
1541
|
+
await writeJsonFile(configPath, { ...config, batteries });
|
|
1542
|
+
const wiringSnippet = battery === "reranker" ? [
|
|
1543
|
+
"",
|
|
1544
|
+
"Next steps:",
|
|
1545
|
+
"1. Import the reranker in unrag.config.ts:",
|
|
1546
|
+
` import { createCohereReranker } from "./${config.installDir}/rerank";`,
|
|
1547
|
+
"",
|
|
1548
|
+
"2. Add reranker to your engine config:",
|
|
1549
|
+
" const reranker = createCohereReranker();",
|
|
1550
|
+
" return unrag.createEngine({ store, reranker });",
|
|
1551
|
+
"",
|
|
1552
|
+
"3. Use reranking in your retrieval flow:",
|
|
1553
|
+
" const retrieved = await engine.retrieve({ query, topK: 30 });",
|
|
1554
|
+
" const reranked = await engine.rerank({ query, candidates: retrieved.chunks, topK: 8 });",
|
|
1555
|
+
"",
|
|
1556
|
+
"Env: COHERE_API_KEY (required for Cohere rerank-v3.5)"
|
|
1557
|
+
] : [];
|
|
1558
|
+
outro2([
|
|
1559
|
+
`Installed battery: ${battery}.`,
|
|
1560
|
+
"",
|
|
1561
|
+
`- Code: ${path7.join(config.installDir, battery === "reranker" ? "rerank" : battery)}`,
|
|
1562
|
+
"",
|
|
1563
|
+
merged2.changes.length > 0 ? `Added deps: ${merged2.changes.map((c) => c.name).join(", ")}` : "Added deps: none",
|
|
1564
|
+
merged2.changes.length > 0 && !noInstall ? "Dependencies installed." : merged2.changes.length > 0 && noInstall ? "Dependencies not installed (skipped)." : "",
|
|
1565
|
+
...wiringSnippet
|
|
1566
|
+
].filter(Boolean).join(`
|
|
1567
|
+
`));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1393
1570
|
if (kind === "connector") {
|
|
1394
1571
|
const connector = name;
|
|
1395
1572
|
if (!connector || !availableConnectors.has(connector)) {
|
|
@@ -1409,6 +1586,9 @@ Available connectors: ${Array.from(availableConnectors).join(", ")}`);
|
|
|
1409
1586
|
const merged2 = mergeDeps(pkg, deps2, devDeps2);
|
|
1410
1587
|
if (merged2.changes.length > 0) {
|
|
1411
1588
|
await writePackageJson(root, merged2.pkg);
|
|
1589
|
+
if (!noInstall) {
|
|
1590
|
+
await installDependencies(root);
|
|
1591
|
+
}
|
|
1412
1592
|
}
|
|
1413
1593
|
const connectors = Array.from(new Set([...config.connectors ?? [], connector])).sort();
|
|
1414
1594
|
await writeJsonFile(configPath, { ...config, connectors });
|
|
@@ -1419,6 +1599,7 @@ Available connectors: ${Array.from(availableConnectors).join(", ")}`);
|
|
|
1419
1599
|
`- Docs: ${docsUrl(`/docs/connectors/${connector}`)}`,
|
|
1420
1600
|
"",
|
|
1421
1601
|
merged2.changes.length > 0 ? `Added deps: ${merged2.changes.map((c) => c.name).join(", ")}` : "Added deps: none",
|
|
1602
|
+
merged2.changes.length > 0 && !noInstall ? "Dependencies installed." : merged2.changes.length > 0 && noInstall ? "Dependencies not installed (skipped)." : "",
|
|
1422
1603
|
nonInteractive ? "" : connector === "notion" ? "Tip: keep NOTION_TOKEN server-side only (env var)." : connector === "google-drive" ? "Tip: keep Google OAuth refresh tokens and service account keys server-side only." : ""
|
|
1423
1604
|
].filter(Boolean).join(`
|
|
1424
1605
|
`));
|
|
@@ -1442,6 +1623,9 @@ Available extractors: ${Array.from(availableExtractors).join(", ")}`);
|
|
|
1442
1623
|
const merged = mergeDeps(pkg, deps, devDeps);
|
|
1443
1624
|
if (merged.changes.length > 0) {
|
|
1444
1625
|
await writePackageJson(root, merged.pkg);
|
|
1626
|
+
if (!noInstall) {
|
|
1627
|
+
await installDependencies(root);
|
|
1628
|
+
}
|
|
1445
1629
|
}
|
|
1446
1630
|
const extractors = Array.from(new Set([...config.extractors ?? [], extractor])).sort();
|
|
1447
1631
|
await writeJsonFile(configPath, { ...config, extractors });
|
|
@@ -1451,76 +1635,2577 @@ Available extractors: ${Array.from(availableExtractors).join(", ")}`);
|
|
|
1451
1635
|
`- Code: ${path7.join(config.installDir, "extractors", extractor)}`,
|
|
1452
1636
|
"",
|
|
1453
1637
|
merged.changes.length > 0 ? `Added deps: ${merged.changes.map((c) => c.name).join(", ")}` : "Added deps: none",
|
|
1638
|
+
merged.changes.length > 0 && !noInstall ? "Dependencies installed." : merged.changes.length > 0 && noInstall ? "Dependencies not installed (skipped)." : "",
|
|
1454
1639
|
"",
|
|
1455
1640
|
`Next: import the extractor and pass it to createContextEngine({ extractors: [...] }).`
|
|
1456
1641
|
].filter(Boolean).join(`
|
|
1457
1642
|
`));
|
|
1458
1643
|
}
|
|
1459
1644
|
|
|
1460
|
-
// cli/
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1645
|
+
// cli/commands/doctor.ts
|
|
1646
|
+
import { outro as outro4, spinner as spinner2 } from "@clack/prompts";
|
|
1647
|
+
|
|
1648
|
+
// cli/lib/doctor/infer.ts
|
|
1649
|
+
import path8 from "node:path";
|
|
1650
|
+
import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
|
|
1651
|
+
var CONFIG_FILE3 = "unrag.json";
|
|
1652
|
+
var CONFIG_TS_FILE = "unrag.config.ts";
|
|
1653
|
+
var DEFAULT_INSTALL_DIRS = ["lib/unrag", "src/lib/unrag", "src/unrag"];
|
|
1654
|
+
async function inferInstallState(options) {
|
|
1655
|
+
const warnings = [];
|
|
1656
|
+
const projectRoot = options.projectRootOverride ?? await tryFindProjectRoot(process.cwd()) ?? process.cwd();
|
|
1657
|
+
const unragJsonPath = path8.join(projectRoot, CONFIG_FILE3);
|
|
1658
|
+
const unragJsonExists = await exists(unragJsonPath);
|
|
1659
|
+
let unragJson = null;
|
|
1660
|
+
let unragJsonParseable = false;
|
|
1661
|
+
if (unragJsonExists) {
|
|
1662
|
+
unragJson = await readJsonFile(unragJsonPath);
|
|
1663
|
+
unragJsonParseable = unragJson !== null;
|
|
1664
|
+
if (!unragJsonParseable) {
|
|
1665
|
+
warnings.push(`${CONFIG_FILE3} exists but could not be parsed.`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
let installDir = null;
|
|
1669
|
+
if (options.installDirOverride) {
|
|
1670
|
+
installDir = options.installDirOverride;
|
|
1671
|
+
} else if (unragJson?.installDir) {
|
|
1672
|
+
installDir = unragJson.installDir;
|
|
1673
|
+
} else {
|
|
1674
|
+
installDir = await inferInstallDirFromFilesystem(projectRoot);
|
|
1675
|
+
if (!installDir && !unragJsonExists) {
|
|
1676
|
+
warnings.push("Could not find unrag.json or infer installDir from filesystem.");
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const installDirFull = installDir ? path8.join(projectRoot, installDir) : null;
|
|
1680
|
+
const installDirExists = installDirFull ? await exists(installDirFull) : false;
|
|
1681
|
+
const configFileExists = await exists(path8.join(projectRoot, CONFIG_TS_FILE));
|
|
1682
|
+
let storeAdapter = null;
|
|
1683
|
+
if (unragJson?.storeAdapter) {
|
|
1684
|
+
storeAdapter = unragJson.storeAdapter;
|
|
1685
|
+
} else if (installDirFull && installDirExists) {
|
|
1686
|
+
storeAdapter = await inferStoreAdapterFromFilesystem(installDirFull);
|
|
1687
|
+
}
|
|
1688
|
+
let embeddingProvider = null;
|
|
1689
|
+
if (unragJson?.embeddingProvider) {
|
|
1690
|
+
embeddingProvider = unragJson.embeddingProvider;
|
|
1691
|
+
} else if (installDirFull && installDirExists) {
|
|
1692
|
+
embeddingProvider = await inferEmbeddingProviderFromFilesystem(installDirFull);
|
|
1693
|
+
}
|
|
1694
|
+
let installedExtractors = [];
|
|
1695
|
+
if (unragJson?.extractors && Array.isArray(unragJson.extractors)) {
|
|
1696
|
+
installedExtractors = unragJson.extractors;
|
|
1697
|
+
}
|
|
1698
|
+
if (installDirFull && installDirExists) {
|
|
1699
|
+
const fsExtractors = await inferExtractorsFromFilesystem(installDirFull);
|
|
1700
|
+
installedExtractors = Array.from(new Set([...installedExtractors, ...fsExtractors])).sort();
|
|
1701
|
+
}
|
|
1702
|
+
let installedConnectors = [];
|
|
1703
|
+
if (unragJson?.connectors && Array.isArray(unragJson.connectors)) {
|
|
1704
|
+
installedConnectors = unragJson.connectors;
|
|
1705
|
+
}
|
|
1706
|
+
if (installDirFull && installDirExists) {
|
|
1707
|
+
const fsConnectors = await inferConnectorsFromFilesystem(installDirFull);
|
|
1708
|
+
installedConnectors = Array.from(new Set([...installedConnectors, ...fsConnectors])).sort();
|
|
1709
|
+
}
|
|
1710
|
+
let inferredDbEnvVar = null;
|
|
1711
|
+
if (configFileExists) {
|
|
1712
|
+
inferredDbEnvVar = await inferDbEnvVarFromConfig(projectRoot);
|
|
1713
|
+
}
|
|
1714
|
+
let inferenceConfidence = "low";
|
|
1715
|
+
if (unragJsonExists && unragJsonParseable && installDirExists) {
|
|
1716
|
+
inferenceConfidence = "high";
|
|
1717
|
+
} else if (installDirExists && (storeAdapter || configFileExists)) {
|
|
1718
|
+
inferenceConfidence = "medium";
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
projectRoot,
|
|
1722
|
+
installDir,
|
|
1723
|
+
installDirExists,
|
|
1724
|
+
unragJsonExists,
|
|
1725
|
+
unragJsonParseable,
|
|
1726
|
+
unragJson,
|
|
1727
|
+
configFileExists,
|
|
1728
|
+
storeAdapter,
|
|
1729
|
+
embeddingProvider,
|
|
1730
|
+
installedExtractors,
|
|
1731
|
+
installedConnectors,
|
|
1732
|
+
inferredDbEnvVar,
|
|
1733
|
+
inferenceConfidence,
|
|
1734
|
+
warnings
|
|
1735
|
+
};
|
|
1507
1736
|
}
|
|
1508
|
-
async function
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1737
|
+
async function inferInstallDirFromFilesystem(projectRoot) {
|
|
1738
|
+
for (const candidate of DEFAULT_INSTALL_DIRS) {
|
|
1739
|
+
const full = path8.join(projectRoot, candidate);
|
|
1740
|
+
if (!await exists(full))
|
|
1741
|
+
continue;
|
|
1742
|
+
const hasUnragMd = await exists(path8.join(full, "unrag.md"));
|
|
1743
|
+
const hasStore = await exists(path8.join(full, "store"));
|
|
1744
|
+
const hasCore = await exists(path8.join(full, "core"));
|
|
1745
|
+
if (hasUnragMd || hasStore || hasCore) {
|
|
1746
|
+
return candidate;
|
|
1747
|
+
}
|
|
1514
1748
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
async function inferStoreAdapterFromFilesystem(installDir) {
|
|
1752
|
+
const storeDir = path8.join(installDir, "store");
|
|
1753
|
+
if (!await exists(storeDir))
|
|
1754
|
+
return null;
|
|
1755
|
+
try {
|
|
1756
|
+
const entries = await readdir2(storeDir, { withFileTypes: true });
|
|
1757
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1758
|
+
for (const dir of dirs) {
|
|
1759
|
+
if (dir.includes("drizzle"))
|
|
1760
|
+
return "drizzle";
|
|
1761
|
+
if (dir.includes("prisma"))
|
|
1762
|
+
return "prisma";
|
|
1763
|
+
if (dir.includes("raw-sql"))
|
|
1764
|
+
return "raw-sql";
|
|
1765
|
+
}
|
|
1766
|
+
} catch {}
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
async function inferEmbeddingProviderFromFilesystem(installDir) {
|
|
1770
|
+
const embeddingDir = path8.join(installDir, "embedding");
|
|
1771
|
+
if (!await exists(embeddingDir))
|
|
1772
|
+
return null;
|
|
1773
|
+
const providers = [
|
|
1774
|
+
"ai",
|
|
1775
|
+
"openai",
|
|
1776
|
+
"google",
|
|
1777
|
+
"openrouter",
|
|
1778
|
+
"azure",
|
|
1779
|
+
"vertex",
|
|
1780
|
+
"bedrock",
|
|
1781
|
+
"cohere",
|
|
1782
|
+
"mistral",
|
|
1783
|
+
"together",
|
|
1784
|
+
"ollama",
|
|
1785
|
+
"voyage"
|
|
1786
|
+
];
|
|
1787
|
+
try {
|
|
1788
|
+
const entries = await readdir2(embeddingDir);
|
|
1789
|
+
for (const provider of providers) {
|
|
1790
|
+
if (entries.includes(`${provider}.ts`)) {
|
|
1791
|
+
return provider;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
} catch {}
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
async function inferExtractorsFromFilesystem(installDir) {
|
|
1798
|
+
const extractorsDir = path8.join(installDir, "extractors");
|
|
1799
|
+
if (!await exists(extractorsDir))
|
|
1800
|
+
return [];
|
|
1801
|
+
try {
|
|
1802
|
+
const entries = await readdir2(extractorsDir, { withFileTypes: true });
|
|
1803
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => {
|
|
1804
|
+
if (name === "_shared")
|
|
1805
|
+
return false;
|
|
1806
|
+
if (name.startsWith("_"))
|
|
1807
|
+
return false;
|
|
1808
|
+
return true;
|
|
1809
|
+
});
|
|
1810
|
+
} catch {
|
|
1811
|
+
return [];
|
|
1518
1812
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1813
|
+
}
|
|
1814
|
+
async function inferConnectorsFromFilesystem(installDir) {
|
|
1815
|
+
const connectorsDir = path8.join(installDir, "connectors");
|
|
1816
|
+
if (!await exists(connectorsDir))
|
|
1817
|
+
return [];
|
|
1818
|
+
try {
|
|
1819
|
+
const entries = await readdir2(connectorsDir, { withFileTypes: true });
|
|
1820
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1821
|
+
} catch {
|
|
1822
|
+
return [];
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
async function inferDbEnvVarFromConfig(projectRoot) {
|
|
1826
|
+
const configPath = path8.join(projectRoot, CONFIG_TS_FILE);
|
|
1827
|
+
if (!await exists(configPath))
|
|
1828
|
+
return null;
|
|
1829
|
+
try {
|
|
1830
|
+
const content = await readFile6(configPath, "utf8");
|
|
1831
|
+
const candidates = extractDbEnvVarCandidates(content);
|
|
1832
|
+
if (candidates.length === 0) {
|
|
1833
|
+
const importedFiles = extractLocalImports(content);
|
|
1834
|
+
for (const importPath of importedFiles) {
|
|
1835
|
+
const resolved = await resolveImportPath(projectRoot, importPath);
|
|
1836
|
+
if (resolved) {
|
|
1837
|
+
const importContent = await readFile6(resolved, "utf8").catch(() => null);
|
|
1838
|
+
if (importContent) {
|
|
1839
|
+
candidates.push(...extractDbEnvVarCandidates(importContent));
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (candidates.length === 0)
|
|
1845
|
+
return null;
|
|
1846
|
+
if (candidates.length === 1)
|
|
1847
|
+
return candidates[0];
|
|
1848
|
+
const preferred = candidates.find((c) => c === "DATABASE_URL" || c.endsWith("_DATABASE_URL") || c.endsWith("_DB_URL"));
|
|
1849
|
+
return preferred ?? candidates[0];
|
|
1850
|
+
} catch {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function extractDbEnvVarCandidates(source) {
|
|
1855
|
+
const candidates = [];
|
|
1856
|
+
const connectionStringPattern = /connectionString\s*[:=]\s*process\.env\.([A-Z_][A-Z0-9_]*)/gi;
|
|
1857
|
+
let match;
|
|
1858
|
+
while ((match = connectionStringPattern.exec(source)) !== null) {
|
|
1859
|
+
if (match[1])
|
|
1860
|
+
candidates.push(match[1]);
|
|
1861
|
+
}
|
|
1862
|
+
const dbUrlPattern = /process\.env\.([A-Z_]*(?:DATABASE|DB)_?(?:URL|URI|CONNECTION)[A-Z_]*)/gi;
|
|
1863
|
+
while ((match = dbUrlPattern.exec(source)) !== null) {
|
|
1864
|
+
if (match[1] && !candidates.includes(match[1])) {
|
|
1865
|
+
candidates.push(match[1]);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const poolPattern = /new\s+Pool\s*\(\s*\{[^}]*process\.env\.([A-Z_][A-Z0-9_]*)/gi;
|
|
1869
|
+
while ((match = poolPattern.exec(source)) !== null) {
|
|
1870
|
+
if (match[1] && !candidates.includes(match[1])) {
|
|
1871
|
+
candidates.push(match[1]);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return candidates;
|
|
1875
|
+
}
|
|
1876
|
+
function extractLocalImports(source) {
|
|
1877
|
+
const imports = [];
|
|
1878
|
+
const importPattern = /from\s+["']([.@][^"']+)["']/g;
|
|
1879
|
+
let match;
|
|
1880
|
+
while ((match = importPattern.exec(source)) !== null) {
|
|
1881
|
+
if (match[1])
|
|
1882
|
+
imports.push(match[1]);
|
|
1883
|
+
}
|
|
1884
|
+
return imports;
|
|
1885
|
+
}
|
|
1886
|
+
async function resolveImportPath(projectRoot, importPath) {
|
|
1887
|
+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
1888
|
+
const configDir = projectRoot;
|
|
1889
|
+
const resolved = path8.resolve(configDir, importPath);
|
|
1890
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ""]) {
|
|
1891
|
+
const candidate = resolved + ext;
|
|
1892
|
+
if (await exists(candidate))
|
|
1893
|
+
return candidate;
|
|
1894
|
+
}
|
|
1895
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
1896
|
+
const candidate = path8.join(resolved, `index${ext}`);
|
|
1897
|
+
if (await exists(candidate))
|
|
1898
|
+
return candidate;
|
|
1899
|
+
}
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
if (importPath.startsWith("@/") || importPath.startsWith("@")) {
|
|
1903
|
+
const tsconfigPath = path8.join(projectRoot, "tsconfig.json");
|
|
1904
|
+
if (await exists(tsconfigPath)) {
|
|
1905
|
+
try {
|
|
1906
|
+
const tsconfig = await readJsonFile(tsconfigPath);
|
|
1907
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
1908
|
+
if (paths) {
|
|
1909
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
1910
|
+
const aliasBase = alias.replace("/*", "");
|
|
1911
|
+
if (importPath.startsWith(aliasBase)) {
|
|
1912
|
+
const rest = importPath.slice(aliasBase.length).replace(/^\//, "");
|
|
1913
|
+
for (const target of targets) {
|
|
1914
|
+
const targetBase = target.replace("/*", "").replace(/^\.\//, "");
|
|
1915
|
+
const resolved = path8.join(projectRoot, targetBase, rest);
|
|
1916
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ""]) {
|
|
1917
|
+
const candidate = resolved + ext;
|
|
1918
|
+
if (await exists(candidate))
|
|
1919
|
+
return candidate;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
} catch {}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
async function inferTableNames(installDir, storeAdapter) {
|
|
1931
|
+
const defaults = {
|
|
1932
|
+
documents: "documents",
|
|
1933
|
+
chunks: "chunks",
|
|
1934
|
+
embeddings: "embeddings"
|
|
1935
|
+
};
|
|
1936
|
+
if (!storeAdapter || !installDir)
|
|
1937
|
+
return defaults;
|
|
1938
|
+
const storeDir = path8.join(installDir, "store");
|
|
1939
|
+
if (!await exists(storeDir))
|
|
1940
|
+
return defaults;
|
|
1941
|
+
try {
|
|
1942
|
+
const entries = await readdir2(storeDir, { withFileTypes: true });
|
|
1943
|
+
const adapterDir = entries.find((e) => e.isDirectory() && e.name.includes(storeAdapter));
|
|
1944
|
+
if (!adapterDir)
|
|
1945
|
+
return defaults;
|
|
1946
|
+
const adapterPath = path8.join(storeDir, adapterDir.name);
|
|
1947
|
+
if (storeAdapter === "drizzle") {
|
|
1948
|
+
const schemaPath = path8.join(adapterPath, "schema.ts");
|
|
1949
|
+
if (await exists(schemaPath)) {
|
|
1950
|
+
const content = await readFile6(schemaPath, "utf8");
|
|
1951
|
+
const tableNames = extractDrizzleTableNames(content);
|
|
1952
|
+
return { ...defaults, ...tableNames };
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const storePath = path8.join(adapterPath, "store.ts");
|
|
1956
|
+
if (await exists(storePath)) {
|
|
1957
|
+
const content = await readFile6(storePath, "utf8");
|
|
1958
|
+
const tableNames = extractSqlTableNames(content);
|
|
1959
|
+
return { ...defaults, ...tableNames };
|
|
1960
|
+
}
|
|
1961
|
+
} catch {}
|
|
1962
|
+
return defaults;
|
|
1963
|
+
}
|
|
1964
|
+
function extractDrizzleTableNames(source) {
|
|
1965
|
+
const result = {};
|
|
1966
|
+
const tablePattern = /export\s+const\s+(documents|chunks|embeddings)\s*=\s*pgTable\s*\(\s*["']([^"']+)["']/g;
|
|
1967
|
+
let match;
|
|
1968
|
+
while ((match = tablePattern.exec(source)) !== null) {
|
|
1969
|
+
const varName = match[1];
|
|
1970
|
+
const tableName = match[2];
|
|
1971
|
+
if (tableName)
|
|
1972
|
+
result[varName] = tableName;
|
|
1973
|
+
}
|
|
1974
|
+
return result;
|
|
1975
|
+
}
|
|
1976
|
+
function extractSqlTableNames(source) {
|
|
1977
|
+
const result = {};
|
|
1978
|
+
const patterns = [
|
|
1979
|
+
/(?:from|into|update|delete\s+from)\s+([a-z_][a-z0-9_]*)\s/gi,
|
|
1980
|
+
/references\s+([a-z_][a-z0-9_]*)\s*\(/gi
|
|
1981
|
+
];
|
|
1982
|
+
const found = new Set;
|
|
1983
|
+
for (const pattern of patterns) {
|
|
1984
|
+
let match;
|
|
1985
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
1986
|
+
if (match[1])
|
|
1987
|
+
found.add(match[1].toLowerCase());
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
for (const name of found) {
|
|
1991
|
+
if (name.includes("document") && !name.includes("chunk")) {
|
|
1992
|
+
result.documents = name;
|
|
1993
|
+
} else if (name.includes("chunk")) {
|
|
1994
|
+
result.chunks = name;
|
|
1995
|
+
} else if (name.includes("embedding")) {
|
|
1996
|
+
result.embeddings = name;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return result;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// cli/lib/doctor/staticChecks.ts
|
|
2003
|
+
import path9 from "node:path";
|
|
2004
|
+
|
|
2005
|
+
// cli/lib/doctor/types.ts
|
|
2006
|
+
var EMBEDDING_PROVIDER_ENV_VARS = {
|
|
2007
|
+
ai: {
|
|
2008
|
+
required: ["AI_GATEWAY_API_KEY"],
|
|
2009
|
+
optional: ["AI_GATEWAY_MODEL"]
|
|
2010
|
+
},
|
|
2011
|
+
openai: {
|
|
2012
|
+
required: ["OPENAI_API_KEY"],
|
|
2013
|
+
optional: ["OPENAI_EMBEDDING_MODEL"]
|
|
2014
|
+
},
|
|
2015
|
+
google: {
|
|
2016
|
+
required: ["GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
2017
|
+
optional: ["GOOGLE_GENERATIVE_AI_EMBEDDING_MODEL"]
|
|
2018
|
+
},
|
|
2019
|
+
openrouter: {
|
|
2020
|
+
required: ["OPENROUTER_API_KEY"],
|
|
2021
|
+
optional: ["OPENROUTER_EMBEDDING_MODEL"]
|
|
2022
|
+
},
|
|
2023
|
+
azure: {
|
|
2024
|
+
required: ["AZURE_OPENAI_API_KEY", "AZURE_RESOURCE_NAME"],
|
|
2025
|
+
optional: ["AZURE_EMBEDDING_MODEL"]
|
|
2026
|
+
},
|
|
2027
|
+
vertex: {
|
|
2028
|
+
required: [],
|
|
2029
|
+
optional: ["GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_VERTEX_EMBEDDING_MODEL"]
|
|
2030
|
+
},
|
|
2031
|
+
bedrock: {
|
|
2032
|
+
required: ["AWS_REGION"],
|
|
2033
|
+
optional: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "BEDROCK_EMBEDDING_MODEL"]
|
|
2034
|
+
},
|
|
2035
|
+
cohere: {
|
|
2036
|
+
required: ["COHERE_API_KEY"],
|
|
2037
|
+
optional: ["COHERE_EMBEDDING_MODEL"]
|
|
2038
|
+
},
|
|
2039
|
+
mistral: {
|
|
2040
|
+
required: ["MISTRAL_API_KEY"],
|
|
2041
|
+
optional: ["MISTRAL_EMBEDDING_MODEL"]
|
|
2042
|
+
},
|
|
2043
|
+
together: {
|
|
2044
|
+
required: ["TOGETHER_AI_API_KEY"],
|
|
2045
|
+
optional: ["TOGETHER_AI_EMBEDDING_MODEL"]
|
|
2046
|
+
},
|
|
2047
|
+
ollama: {
|
|
2048
|
+
required: [],
|
|
2049
|
+
optional: ["OLLAMA_EMBEDDING_MODEL"]
|
|
2050
|
+
},
|
|
2051
|
+
voyage: {
|
|
2052
|
+
required: ["VOYAGE_API_KEY"],
|
|
2053
|
+
optional: ["VOYAGE_MODEL"]
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
var STORE_ADAPTER_DEPS = {
|
|
2057
|
+
drizzle: {
|
|
2058
|
+
required: ["drizzle-orm", "pg"],
|
|
2059
|
+
devRequired: ["@types/pg"]
|
|
2060
|
+
},
|
|
2061
|
+
prisma: {
|
|
2062
|
+
required: ["@prisma/client"],
|
|
2063
|
+
devRequired: ["prisma"]
|
|
2064
|
+
},
|
|
2065
|
+
"raw-sql": {
|
|
2066
|
+
required: ["pg"],
|
|
2067
|
+
devRequired: ["@types/pg"]
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
var EXTRACTOR_CONFIG_FLAGS = {
|
|
2071
|
+
"pdf-llm": ["assetProcessing.pdf.llmExtraction.enabled"],
|
|
2072
|
+
"pdf-text-layer": ["assetProcessing.pdf.textLayer.enabled"],
|
|
2073
|
+
"pdf-ocr": ["assetProcessing.pdf.ocr.enabled"],
|
|
2074
|
+
"image-ocr": ["assetProcessing.image.ocr.enabled"],
|
|
2075
|
+
"image-caption-llm": ["assetProcessing.image.captionLlm.enabled"],
|
|
2076
|
+
"audio-transcribe": ["assetProcessing.audio.transcription.enabled"],
|
|
2077
|
+
"video-transcribe": ["assetProcessing.video.transcription.enabled"],
|
|
2078
|
+
"video-frames": ["assetProcessing.video.frames.enabled"],
|
|
2079
|
+
"file-text": ["assetProcessing.file.text.enabled"],
|
|
2080
|
+
"file-docx": ["assetProcessing.file.docx.enabled"],
|
|
2081
|
+
"file-pptx": ["assetProcessing.file.pptx.enabled"],
|
|
2082
|
+
"file-xlsx": ["assetProcessing.file.xlsx.enabled"]
|
|
2083
|
+
};
|
|
2084
|
+
var EXTRACTOR_FACTORIES = {
|
|
2085
|
+
"pdf-llm": "createPdfLlmExtractor",
|
|
2086
|
+
"pdf-text-layer": "createPdfTextLayerExtractor",
|
|
2087
|
+
"pdf-ocr": "createPdfOcrExtractor",
|
|
2088
|
+
"image-ocr": "createImageOcrExtractor",
|
|
2089
|
+
"image-caption-llm": "createImageCaptionLlmExtractor",
|
|
2090
|
+
"audio-transcribe": "createAudioTranscribeExtractor",
|
|
2091
|
+
"video-transcribe": "createVideoTranscribeExtractor",
|
|
2092
|
+
"video-frames": "createVideoFramesExtractor",
|
|
2093
|
+
"file-text": "createFileTextExtractor",
|
|
2094
|
+
"file-docx": "createFileDocxExtractor",
|
|
2095
|
+
"file-pptx": "createFilePptxExtractor",
|
|
2096
|
+
"file-xlsx": "createFileXlsxExtractor"
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
// cli/lib/doctor/staticChecks.ts
|
|
2100
|
+
async function runStaticChecks(state) {
|
|
2101
|
+
const install = await runInstallChecks(state);
|
|
2102
|
+
const env = await runEnvChecks(state);
|
|
2103
|
+
const modules = await runModuleChecks(state);
|
|
2104
|
+
return { install, env, modules };
|
|
2105
|
+
}
|
|
2106
|
+
async function runInstallChecks(state) {
|
|
2107
|
+
const results = [];
|
|
2108
|
+
results.push(state.unragJsonExists && state.unragJsonParseable ? {
|
|
2109
|
+
id: "unrag-json",
|
|
2110
|
+
title: "unrag.json",
|
|
2111
|
+
status: "pass",
|
|
2112
|
+
summary: "Configuration file found and parseable.",
|
|
2113
|
+
meta: { version: state.unragJson?.version }
|
|
2114
|
+
} : state.unragJsonExists ? {
|
|
2115
|
+
id: "unrag-json",
|
|
2116
|
+
title: "unrag.json",
|
|
2117
|
+
status: "warn",
|
|
2118
|
+
summary: "Configuration file exists but could not be parsed.",
|
|
2119
|
+
fixHints: [
|
|
2120
|
+
"Check for JSON syntax errors in unrag.json",
|
|
2121
|
+
"Try running `unrag init` again to regenerate"
|
|
2122
|
+
]
|
|
2123
|
+
} : {
|
|
2124
|
+
id: "unrag-json",
|
|
2125
|
+
title: "unrag.json",
|
|
2126
|
+
status: "warn",
|
|
2127
|
+
summary: "Configuration file not found.",
|
|
2128
|
+
details: [
|
|
2129
|
+
"Doctor is inferring configuration from filesystem.",
|
|
2130
|
+
`Inference confidence: ${state.inferenceConfidence}`
|
|
2131
|
+
],
|
|
2132
|
+
fixHints: ["Run `unrag init` to create unrag.json"],
|
|
2133
|
+
docsLink: docsUrl("/docs/reference/cli")
|
|
2134
|
+
});
|
|
2135
|
+
results.push(state.configFileExists ? {
|
|
2136
|
+
id: "config-ts",
|
|
2137
|
+
title: "unrag.config.ts",
|
|
2138
|
+
status: "pass",
|
|
2139
|
+
summary: "Engine configuration file found."
|
|
2140
|
+
} : {
|
|
2141
|
+
id: "config-ts",
|
|
2142
|
+
title: "unrag.config.ts",
|
|
2143
|
+
status: "fail",
|
|
2144
|
+
summary: "Engine configuration file not found.",
|
|
2145
|
+
details: [
|
|
2146
|
+
"unrag.config.ts is required to create and configure the Unrag engine."
|
|
2147
|
+
],
|
|
2148
|
+
fixHints: ["Run `unrag init` to create unrag.config.ts"],
|
|
2149
|
+
docsLink: docsUrl("/docs/getting-started/quickstart")
|
|
2150
|
+
});
|
|
2151
|
+
if (state.installDir) {
|
|
2152
|
+
const installDirFull = path9.join(state.projectRoot, state.installDir);
|
|
2153
|
+
const coreExists = await exists(path9.join(installDirFull, "core"));
|
|
2154
|
+
const storeExists = await exists(path9.join(installDirFull, "store"));
|
|
2155
|
+
const embeddingExists = await exists(path9.join(installDirFull, "embedding"));
|
|
2156
|
+
const unragMdExists = await exists(path9.join(installDirFull, "unrag.md"));
|
|
2157
|
+
const missingDirs = [];
|
|
2158
|
+
if (!coreExists)
|
|
2159
|
+
missingDirs.push("core/");
|
|
2160
|
+
if (!storeExists)
|
|
2161
|
+
missingDirs.push("store/");
|
|
2162
|
+
if (!embeddingExists)
|
|
2163
|
+
missingDirs.push("embedding/");
|
|
2164
|
+
if (state.installDirExists && missingDirs.length === 0) {
|
|
2165
|
+
results.push({
|
|
2166
|
+
id: "install-dir",
|
|
2167
|
+
title: "Install directory",
|
|
2168
|
+
status: "pass",
|
|
2169
|
+
summary: `Install directory found at ${state.installDir}`,
|
|
2170
|
+
details: [
|
|
2171
|
+
`core/: ${coreExists ? "✓" : "✗"}`,
|
|
2172
|
+
`store/: ${storeExists ? "✓" : "✗"}`,
|
|
2173
|
+
`embedding/: ${embeddingExists ? "✓" : "✗"}`,
|
|
2174
|
+
`unrag.md: ${unragMdExists ? "✓" : "✗"}`
|
|
2175
|
+
]
|
|
2176
|
+
});
|
|
2177
|
+
} else if (state.installDirExists) {
|
|
2178
|
+
results.push({
|
|
2179
|
+
id: "install-dir",
|
|
2180
|
+
title: "Install directory",
|
|
2181
|
+
status: "warn",
|
|
2182
|
+
summary: `Install directory exists but is incomplete.`,
|
|
2183
|
+
details: [`Missing: ${missingDirs.join(", ")}`],
|
|
2184
|
+
fixHints: ["Run `unrag init` to reinstall missing files"]
|
|
2185
|
+
});
|
|
2186
|
+
} else {
|
|
2187
|
+
results.push({
|
|
2188
|
+
id: "install-dir",
|
|
2189
|
+
title: "Install directory",
|
|
2190
|
+
status: "fail",
|
|
2191
|
+
summary: `Install directory not found at ${state.installDir}`,
|
|
2192
|
+
fixHints: [
|
|
2193
|
+
"Run `unrag init` to install Unrag files",
|
|
2194
|
+
`Or use --install-dir to specify a different location`
|
|
2195
|
+
]
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
} else {
|
|
2199
|
+
results.push({
|
|
2200
|
+
id: "install-dir",
|
|
2201
|
+
title: "Install directory",
|
|
2202
|
+
status: "fail",
|
|
2203
|
+
summary: "Could not determine install directory.",
|
|
2204
|
+
fixHints: [
|
|
2205
|
+
"Run `unrag init` to install Unrag",
|
|
2206
|
+
"Or use --install-dir to specify the location"
|
|
2207
|
+
]
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
if (state.storeAdapter) {
|
|
2211
|
+
results.push({
|
|
2212
|
+
id: "store-adapter",
|
|
2213
|
+
title: "Store adapter",
|
|
2214
|
+
status: "pass",
|
|
2215
|
+
summary: `Using ${state.storeAdapter} adapter.`
|
|
2216
|
+
});
|
|
2217
|
+
} else if (state.installDirExists) {
|
|
2218
|
+
results.push({
|
|
2219
|
+
id: "store-adapter",
|
|
2220
|
+
title: "Store adapter",
|
|
2221
|
+
status: "warn",
|
|
2222
|
+
summary: "Could not determine store adapter.",
|
|
2223
|
+
details: ["Expected to find drizzle, prisma, or raw-sql adapter folder."]
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
const depResults = await checkDependencies(state);
|
|
2227
|
+
results.push(...depResults);
|
|
2228
|
+
return results;
|
|
2229
|
+
}
|
|
2230
|
+
async function checkDependencies(state) {
|
|
2231
|
+
const results = [];
|
|
2232
|
+
const pkgJsonPath = path9.join(state.projectRoot, "package.json");
|
|
2233
|
+
if (!await exists(pkgJsonPath)) {
|
|
2234
|
+
results.push({
|
|
2235
|
+
id: "deps-check",
|
|
2236
|
+
title: "Dependencies",
|
|
2237
|
+
status: "skip",
|
|
2238
|
+
summary: "No package.json found."
|
|
2239
|
+
});
|
|
2240
|
+
return results;
|
|
2241
|
+
}
|
|
2242
|
+
const pkgJson = await readJsonFile(pkgJsonPath);
|
|
2243
|
+
if (!pkgJson) {
|
|
2244
|
+
results.push({
|
|
2245
|
+
id: "deps-check",
|
|
2246
|
+
title: "Dependencies",
|
|
2247
|
+
status: "warn",
|
|
2248
|
+
summary: "Could not parse package.json."
|
|
2249
|
+
});
|
|
2250
|
+
return results;
|
|
2251
|
+
}
|
|
2252
|
+
const allDeps = {
|
|
2253
|
+
...pkgJson.dependencies ?? {},
|
|
2254
|
+
...pkgJson.devDependencies ?? {}
|
|
2255
|
+
};
|
|
2256
|
+
if (state.storeAdapter) {
|
|
2257
|
+
const adapterDeps = STORE_ADAPTER_DEPS[state.storeAdapter];
|
|
2258
|
+
const missingDeps = [];
|
|
2259
|
+
for (const dep of adapterDeps.required) {
|
|
2260
|
+
if (!allDeps[dep])
|
|
2261
|
+
missingDeps.push(dep);
|
|
2262
|
+
}
|
|
2263
|
+
if (missingDeps.length === 0) {
|
|
2264
|
+
results.push({
|
|
2265
|
+
id: "deps-store",
|
|
2266
|
+
title: `${state.storeAdapter} dependencies`,
|
|
2267
|
+
status: "pass",
|
|
2268
|
+
summary: "All required dependencies installed."
|
|
2269
|
+
});
|
|
2270
|
+
} else {
|
|
2271
|
+
results.push({
|
|
2272
|
+
id: "deps-store",
|
|
2273
|
+
title: `${state.storeAdapter} dependencies`,
|
|
2274
|
+
status: "fail",
|
|
2275
|
+
summary: `Missing dependencies: ${missingDeps.join(", ")}`,
|
|
2276
|
+
fixHints: [`Run: npm install ${missingDeps.join(" ")}`]
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (!allDeps["ai"]) {
|
|
2281
|
+
results.push({
|
|
2282
|
+
id: "deps-ai",
|
|
2283
|
+
title: "AI SDK",
|
|
2284
|
+
status: "fail",
|
|
2285
|
+
summary: "Missing required dependency: ai",
|
|
2286
|
+
fixHints: ["Run: npm install ai"]
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
return results;
|
|
2290
|
+
}
|
|
2291
|
+
async function runEnvChecks(state) {
|
|
2292
|
+
const results = [];
|
|
2293
|
+
const dbUrl = process.env.DATABASE_URL || (state.inferredDbEnvVar ? process.env[state.inferredDbEnvVar] : undefined);
|
|
2294
|
+
const dbEnvVarName = state.inferredDbEnvVar ?? "DATABASE_URL";
|
|
2295
|
+
const actualEnvVar = process.env.DATABASE_URL ? "DATABASE_URL" : state.inferredDbEnvVar && process.env[state.inferredDbEnvVar] ? state.inferredDbEnvVar : null;
|
|
2296
|
+
if (dbUrl) {
|
|
2297
|
+
const isValidFormat = dbUrl.startsWith("postgres://") || dbUrl.startsWith("postgresql://");
|
|
2298
|
+
results.push({
|
|
2299
|
+
id: "env-database-url",
|
|
2300
|
+
title: "Database URL",
|
|
2301
|
+
status: isValidFormat ? "pass" : "warn",
|
|
2302
|
+
summary: isValidFormat ? `${actualEnvVar} is set and appears to be a valid Postgres URL.` : `${actualEnvVar} is set but doesn't look like a Postgres URL.`,
|
|
2303
|
+
details: isValidFormat ? undefined : ["Expected format: postgres://user:pass@host:port/db"],
|
|
2304
|
+
meta: { envVar: actualEnvVar }
|
|
2305
|
+
});
|
|
2306
|
+
} else {
|
|
2307
|
+
results.push({
|
|
2308
|
+
id: "env-database-url",
|
|
2309
|
+
title: "Database URL",
|
|
2310
|
+
status: "warn",
|
|
2311
|
+
summary: `${dbEnvVarName} is not set.`,
|
|
2312
|
+
details: state.inferredDbEnvVar && state.inferredDbEnvVar !== "DATABASE_URL" ? [
|
|
2313
|
+
`Inferred env var from config: ${state.inferredDbEnvVar}`,
|
|
2314
|
+
"Neither DATABASE_URL nor the inferred var is set."
|
|
2315
|
+
] : undefined,
|
|
2316
|
+
fixHints: [
|
|
2317
|
+
`Set ${dbEnvVarName} to your Postgres connection string`,
|
|
2318
|
+
"Example: postgresql://user:pass@localhost:5432/mydb"
|
|
2319
|
+
],
|
|
2320
|
+
docsLink: docsUrl("/docs/getting-started/database")
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
if (state.embeddingProvider) {
|
|
2324
|
+
const providerEnv = EMBEDDING_PROVIDER_ENV_VARS[state.embeddingProvider];
|
|
2325
|
+
if (providerEnv) {
|
|
2326
|
+
const missingRequired = [];
|
|
2327
|
+
const missingOptional = [];
|
|
2328
|
+
for (const envVar of providerEnv.required) {
|
|
2329
|
+
if (!process.env[envVar])
|
|
2330
|
+
missingRequired.push(envVar);
|
|
2331
|
+
}
|
|
2332
|
+
for (const envVar of providerEnv.optional) {
|
|
2333
|
+
if (!process.env[envVar])
|
|
2334
|
+
missingOptional.push(envVar);
|
|
2335
|
+
}
|
|
2336
|
+
if (missingRequired.length === 0) {
|
|
2337
|
+
results.push({
|
|
2338
|
+
id: "env-embedding",
|
|
2339
|
+
title: `${state.embeddingProvider} provider`,
|
|
2340
|
+
status: "pass",
|
|
2341
|
+
summary: "Required environment variables are set.",
|
|
2342
|
+
details: missingOptional.length > 0 ? [`Optional (not set): ${missingOptional.join(", ")}`] : undefined
|
|
2343
|
+
});
|
|
2344
|
+
} else {
|
|
2345
|
+
results.push({
|
|
2346
|
+
id: "env-embedding",
|
|
2347
|
+
title: `${state.embeddingProvider} provider`,
|
|
2348
|
+
status: "fail",
|
|
2349
|
+
summary: `Missing required env vars: ${missingRequired.join(", ")}`,
|
|
2350
|
+
fixHints: missingRequired.map((v) => `Set ${v} in your environment`),
|
|
2351
|
+
docsLink: docsUrl(`/docs/providers/${state.embeddingProvider}`)
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
} else {
|
|
2356
|
+
results.push({
|
|
2357
|
+
id: "env-embedding",
|
|
2358
|
+
title: "Embedding provider",
|
|
2359
|
+
status: "skip",
|
|
2360
|
+
summary: "Could not determine embedding provider."
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
for (const connector of state.installedConnectors) {
|
|
2364
|
+
const connectorEnvResults = await checkConnectorEnvVars(connector);
|
|
2365
|
+
results.push(...connectorEnvResults);
|
|
2366
|
+
}
|
|
2367
|
+
return results;
|
|
2368
|
+
}
|
|
2369
|
+
async function checkConnectorEnvVars(connector) {
|
|
2370
|
+
const results = [];
|
|
2371
|
+
const connectorEnvVars = {
|
|
2372
|
+
notion: {
|
|
2373
|
+
required: ["NOTION_TOKEN"],
|
|
2374
|
+
optional: []
|
|
2375
|
+
},
|
|
2376
|
+
"google-drive": {
|
|
2377
|
+
required: [],
|
|
2378
|
+
optional: [
|
|
2379
|
+
"GOOGLE_SERVICE_ACCOUNT_JSON",
|
|
2380
|
+
"GOOGLE_CLIENT_ID",
|
|
2381
|
+
"GOOGLE_CLIENT_SECRET"
|
|
2382
|
+
]
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
const envVars = connectorEnvVars[connector];
|
|
2386
|
+
if (!envVars)
|
|
2387
|
+
return results;
|
|
2388
|
+
const missingRequired = [];
|
|
2389
|
+
for (const envVar of envVars.required) {
|
|
2390
|
+
if (!process.env[envVar])
|
|
2391
|
+
missingRequired.push(envVar);
|
|
2392
|
+
}
|
|
2393
|
+
if (envVars.required.length === 0) {
|
|
2394
|
+
const hasAny = envVars.optional.some((v) => process.env[v]);
|
|
2395
|
+
results.push({
|
|
2396
|
+
id: `env-connector-${connector}`,
|
|
2397
|
+
title: `${connector} connector`,
|
|
2398
|
+
status: hasAny ? "pass" : "warn",
|
|
2399
|
+
summary: hasAny ? "Connector credentials configured." : "No credentials found (may be configured at runtime).",
|
|
2400
|
+
docsLink: docsUrl(`/docs/connectors/${connector}`)
|
|
2401
|
+
});
|
|
2402
|
+
} else if (missingRequired.length === 0) {
|
|
2403
|
+
results.push({
|
|
2404
|
+
id: `env-connector-${connector}`,
|
|
2405
|
+
title: `${connector} connector`,
|
|
2406
|
+
status: "pass",
|
|
2407
|
+
summary: "Required environment variables are set."
|
|
2408
|
+
});
|
|
2409
|
+
} else {
|
|
2410
|
+
results.push({
|
|
2411
|
+
id: `env-connector-${connector}`,
|
|
2412
|
+
title: `${connector} connector`,
|
|
2413
|
+
status: "fail",
|
|
2414
|
+
summary: `Missing required env vars: ${missingRequired.join(", ")}`,
|
|
2415
|
+
fixHints: missingRequired.map((v) => `Set ${v} in your environment`),
|
|
2416
|
+
docsLink: docsUrl(`/docs/connectors/${connector}`)
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
return results;
|
|
2420
|
+
}
|
|
2421
|
+
async function runModuleChecks(state) {
|
|
2422
|
+
const results = [];
|
|
2423
|
+
if (!state.installDir || !state.installDirExists) {
|
|
2424
|
+
return results;
|
|
2425
|
+
}
|
|
2426
|
+
const installDirFull = path9.join(state.projectRoot, state.installDir);
|
|
2427
|
+
for (const extractor of state.installedExtractors) {
|
|
2428
|
+
const extractorDir = path9.join(installDirFull, "extractors", extractor);
|
|
2429
|
+
const extractorExists = await exists(extractorDir);
|
|
2430
|
+
const hasIndex = extractorExists && await exists(path9.join(extractorDir, "index.ts"));
|
|
2431
|
+
if (extractorExists && hasIndex) {
|
|
2432
|
+
results.push({
|
|
2433
|
+
id: `module-extractor-${extractor}`,
|
|
2434
|
+
title: `Extractor: ${extractor}`,
|
|
2435
|
+
status: "pass",
|
|
2436
|
+
summary: "Module files present."
|
|
2437
|
+
});
|
|
2438
|
+
} else if (extractorExists) {
|
|
2439
|
+
results.push({
|
|
2440
|
+
id: `module-extractor-${extractor}`,
|
|
2441
|
+
title: `Extractor: ${extractor}`,
|
|
2442
|
+
status: "warn",
|
|
2443
|
+
summary: "Module directory exists but may be incomplete.",
|
|
2444
|
+
details: ["Expected index.ts not found."]
|
|
2445
|
+
});
|
|
2446
|
+
} else {
|
|
2447
|
+
results.push({
|
|
2448
|
+
id: `module-extractor-${extractor}`,
|
|
2449
|
+
title: `Extractor: ${extractor}`,
|
|
2450
|
+
status: "fail",
|
|
2451
|
+
summary: "Listed in unrag.json but directory not found.",
|
|
2452
|
+
fixHints: [`Run: unrag add extractor ${extractor}`]
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
for (const connector of state.installedConnectors) {
|
|
2457
|
+
const connectorDir = path9.join(installDirFull, "connectors", connector);
|
|
2458
|
+
const connectorExists = await exists(connectorDir);
|
|
2459
|
+
const hasIndex = connectorExists && await exists(path9.join(connectorDir, "index.ts"));
|
|
2460
|
+
if (connectorExists && hasIndex) {
|
|
2461
|
+
results.push({
|
|
2462
|
+
id: `module-connector-${connector}`,
|
|
2463
|
+
title: `Connector: ${connector}`,
|
|
2464
|
+
status: "pass",
|
|
2465
|
+
summary: "Module files present."
|
|
2466
|
+
});
|
|
2467
|
+
} else if (connectorExists) {
|
|
2468
|
+
results.push({
|
|
2469
|
+
id: `module-connector-${connector}`,
|
|
2470
|
+
title: `Connector: ${connector}`,
|
|
2471
|
+
status: "warn",
|
|
2472
|
+
summary: "Module directory exists but may be incomplete."
|
|
2473
|
+
});
|
|
2474
|
+
} else {
|
|
2475
|
+
results.push({
|
|
2476
|
+
id: `module-connector-${connector}`,
|
|
2477
|
+
title: `Connector: ${connector}`,
|
|
2478
|
+
status: "fail",
|
|
2479
|
+
summary: "Listed in unrag.json but directory not found.",
|
|
2480
|
+
fixHints: [`Run: unrag add ${connector}`]
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
const depResults = await checkExtractorDependencies(state);
|
|
2485
|
+
results.push(...depResults);
|
|
2486
|
+
return results;
|
|
2487
|
+
}
|
|
2488
|
+
async function checkExtractorDependencies(state) {
|
|
2489
|
+
const results = [];
|
|
2490
|
+
const pkgJsonPath = path9.join(state.projectRoot, "package.json");
|
|
2491
|
+
if (!await exists(pkgJsonPath))
|
|
2492
|
+
return results;
|
|
2493
|
+
const pkgJson = await readJsonFile(pkgJsonPath);
|
|
2494
|
+
if (!pkgJson)
|
|
2495
|
+
return results;
|
|
2496
|
+
const allDeps = {
|
|
2497
|
+
...pkgJson.dependencies ?? {},
|
|
2498
|
+
...pkgJson.devDependencies ?? {}
|
|
2499
|
+
};
|
|
2500
|
+
const extractorDeps = {
|
|
2501
|
+
"pdf-text-layer": ["pdfjs-dist"],
|
|
2502
|
+
"file-docx": ["mammoth"],
|
|
2503
|
+
"file-pptx": ["jszip"],
|
|
2504
|
+
"file-xlsx": ["xlsx"]
|
|
2505
|
+
};
|
|
2506
|
+
for (const extractor of state.installedExtractors) {
|
|
2507
|
+
const deps = extractorDeps[extractor];
|
|
2508
|
+
if (!deps || deps.length === 0)
|
|
2509
|
+
continue;
|
|
2510
|
+
const missingDeps = deps.filter((d) => !allDeps[d]);
|
|
2511
|
+
if (missingDeps.length > 0) {
|
|
2512
|
+
results.push({
|
|
2513
|
+
id: `deps-extractor-${extractor}`,
|
|
2514
|
+
title: `${extractor} dependencies`,
|
|
2515
|
+
status: "fail",
|
|
2516
|
+
summary: `Missing: ${missingDeps.join(", ")}`,
|
|
2517
|
+
fixHints: [`Run: npm install ${missingDeps.join(" ")}`]
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
return results;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// cli/lib/doctor/configScan.ts
|
|
2525
|
+
import path10 from "node:path";
|
|
2526
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2527
|
+
var CONFIG_TS_FILE2 = "unrag.config.ts";
|
|
2528
|
+
async function runConfigCoherenceChecks(state) {
|
|
2529
|
+
const results = [];
|
|
2530
|
+
if (!state.configFileExists) {
|
|
2531
|
+
results.push({
|
|
2532
|
+
id: "config-coherence",
|
|
2533
|
+
title: "Config coherence",
|
|
2534
|
+
status: "skip",
|
|
2535
|
+
summary: "unrag.config.ts not found; skipping coherence checks."
|
|
2536
|
+
});
|
|
2537
|
+
return results;
|
|
2538
|
+
}
|
|
2539
|
+
const configPath = path10.join(state.projectRoot, CONFIG_TS_FILE2);
|
|
2540
|
+
const scanResult = await scanConfigFile(configPath);
|
|
2541
|
+
if (!scanResult.configContent) {
|
|
2542
|
+
results.push({
|
|
2543
|
+
id: "config-coherence",
|
|
2544
|
+
title: "Config coherence",
|
|
2545
|
+
status: "warn",
|
|
2546
|
+
summary: "Could not read unrag.config.ts for coherence analysis."
|
|
2547
|
+
});
|
|
2548
|
+
return results;
|
|
2549
|
+
}
|
|
2550
|
+
if (scanResult.parseWarnings.length > 0) {
|
|
2551
|
+
results.push({
|
|
2552
|
+
id: "config-scan-quality",
|
|
2553
|
+
title: "Config analysis",
|
|
2554
|
+
status: "warn",
|
|
2555
|
+
summary: `Static analysis confidence: ${scanResult.confidence}`,
|
|
2556
|
+
details: scanResult.parseWarnings
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
for (const extractor of state.installedExtractors) {
|
|
2560
|
+
const extractorResult = checkExtractorCoherence(extractor, scanResult, state.installDir);
|
|
2561
|
+
results.push(extractorResult);
|
|
2562
|
+
}
|
|
2563
|
+
if (state.installedExtractors.length === 0) {
|
|
2564
|
+
results.push({
|
|
2565
|
+
id: "config-extractors-summary",
|
|
2566
|
+
title: "Extractor configuration",
|
|
2567
|
+
status: "pass",
|
|
2568
|
+
summary: "No extractors installed; nothing to check."
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
return results;
|
|
2572
|
+
}
|
|
2573
|
+
async function scanConfigFile(configPath) {
|
|
2574
|
+
const result = {
|
|
2575
|
+
configExists: false,
|
|
2576
|
+
configContent: null,
|
|
2577
|
+
registeredExtractors: [],
|
|
2578
|
+
enabledFlags: [],
|
|
2579
|
+
confidence: "low",
|
|
2580
|
+
parseWarnings: []
|
|
2581
|
+
};
|
|
2582
|
+
if (!await exists(configPath)) {
|
|
2583
|
+
return result;
|
|
2584
|
+
}
|
|
2585
|
+
result.configExists = true;
|
|
2586
|
+
try {
|
|
2587
|
+
result.configContent = await readFile7(configPath, "utf8");
|
|
2588
|
+
} catch {
|
|
2589
|
+
result.parseWarnings.push("Could not read config file.");
|
|
2590
|
+
return result;
|
|
2591
|
+
}
|
|
2592
|
+
const content = result.configContent;
|
|
2593
|
+
result.registeredExtractors = detectRegisteredExtractors(content);
|
|
2594
|
+
result.enabledFlags = detectEnabledFlags(content);
|
|
2595
|
+
if (content.includes("defineUnragConfig")) {
|
|
2596
|
+
result.confidence = "high";
|
|
2597
|
+
} else if (content.includes("createUnragEngine") || content.includes("createContextEngine")) {
|
|
2598
|
+
result.confidence = "medium";
|
|
2599
|
+
} else {
|
|
2600
|
+
result.confidence = "low";
|
|
2601
|
+
result.parseWarnings.push("Config file doesn't appear to use standard Unrag patterns.");
|
|
2602
|
+
}
|
|
2603
|
+
if (content.includes("...extractors") || content.includes("spread")) {
|
|
2604
|
+
result.parseWarnings.push("Config uses spread operator for extractors; some may not be detected.");
|
|
2605
|
+
result.confidence = "medium";
|
|
2606
|
+
}
|
|
2607
|
+
const enabledUsesEnv = /enabled\s*:\s*[^,\n}]*process\.env\./i.test(content) || /enabled\s*:\s*[^,\n}]*\bprocess\.env\[[^\]]+\]/i.test(content);
|
|
2608
|
+
if (enabledUsesEnv) {
|
|
2609
|
+
result.parseWarnings.push("Config uses environment variables for assetProcessing enabled flags; runtime values may differ.");
|
|
2610
|
+
result.confidence = "medium";
|
|
2611
|
+
}
|
|
2612
|
+
return result;
|
|
2613
|
+
}
|
|
2614
|
+
function detectRegisteredExtractors(content) {
|
|
2615
|
+
const found = [];
|
|
2616
|
+
for (const [extractorId, factoryName] of Object.entries(EXTRACTOR_FACTORIES)) {
|
|
2617
|
+
const pattern = new RegExp(`${factoryName}\\s*\\(`, "i");
|
|
2618
|
+
if (pattern.test(content)) {
|
|
2619
|
+
found.push(extractorId);
|
|
2620
|
+
}
|
|
2621
|
+
const importPattern = new RegExp(`import\\s*\\{[^}]*${factoryName}[^}]*\\}`, "i");
|
|
2622
|
+
if (importPattern.test(content) && !found.includes(extractorId)) {
|
|
2623
|
+
const usagePattern = new RegExp(`\\b${factoryName}\\b`, "g");
|
|
2624
|
+
const matches = content.match(usagePattern);
|
|
2625
|
+
if (matches && matches.length > 1) {
|
|
2626
|
+
found.push(extractorId);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return found;
|
|
2631
|
+
}
|
|
2632
|
+
function detectEnabledFlags(content) {
|
|
2633
|
+
const enabled = [];
|
|
2634
|
+
const flagPatterns = {
|
|
2635
|
+
"assetProcessing.pdf.llmExtraction.enabled": [
|
|
2636
|
+
/pdf\s*:\s*\{[^}]*llmExtraction\s*:\s*\{[^}]*enabled\s*:\s*true/is,
|
|
2637
|
+
/llmExtraction\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2638
|
+
],
|
|
2639
|
+
"assetProcessing.pdf.textLayer.enabled": [
|
|
2640
|
+
/pdf\s*:\s*\{[^}]*textLayer\s*:\s*\{[^}]*enabled\s*:\s*true/is,
|
|
2641
|
+
/textLayer\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2642
|
+
],
|
|
2643
|
+
"assetProcessing.pdf.ocr.enabled": [
|
|
2644
|
+
/pdf\s*:\s*\{[^}]*ocr\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2645
|
+
],
|
|
2646
|
+
"assetProcessing.image.ocr.enabled": [
|
|
2647
|
+
/image\s*:\s*\{[^}]*ocr\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2648
|
+
],
|
|
2649
|
+
"assetProcessing.image.captionLlm.enabled": [
|
|
2650
|
+
/image\s*:\s*\{[^}]*captionLlm\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2651
|
+
],
|
|
2652
|
+
"assetProcessing.audio.transcription.enabled": [
|
|
2653
|
+
/audio\s*:\s*\{[^}]*transcription\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2654
|
+
],
|
|
2655
|
+
"assetProcessing.video.transcription.enabled": [
|
|
2656
|
+
/video\s*:\s*\{[^}]*transcription\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2657
|
+
],
|
|
2658
|
+
"assetProcessing.video.frames.enabled": [
|
|
2659
|
+
/video\s*:\s*\{[^}]*frames\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2660
|
+
],
|
|
2661
|
+
"assetProcessing.file.text.enabled": [
|
|
2662
|
+
/file\s*:\s*\{[^}]*text\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2663
|
+
],
|
|
2664
|
+
"assetProcessing.file.docx.enabled": [
|
|
2665
|
+
/file\s*:\s*\{[^}]*docx\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2666
|
+
],
|
|
2667
|
+
"assetProcessing.file.pptx.enabled": [
|
|
2668
|
+
/file\s*:\s*\{[^}]*pptx\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2669
|
+
],
|
|
2670
|
+
"assetProcessing.file.xlsx.enabled": [
|
|
2671
|
+
/file\s*:\s*\{[^}]*xlsx\s*:\s*\{[^}]*enabled\s*:\s*true/is
|
|
2672
|
+
]
|
|
2673
|
+
};
|
|
2674
|
+
for (const [flagPath, patterns] of Object.entries(flagPatterns)) {
|
|
2675
|
+
for (const pattern of patterns) {
|
|
2676
|
+
if (pattern.test(content)) {
|
|
2677
|
+
enabled.push(flagPath);
|
|
2678
|
+
break;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return enabled;
|
|
2683
|
+
}
|
|
2684
|
+
function checkExtractorCoherence(extractorId, scanResult, installDir) {
|
|
2685
|
+
const isRegistered = scanResult.registeredExtractors.includes(extractorId);
|
|
2686
|
+
const requiredFlags = EXTRACTOR_CONFIG_FLAGS[extractorId] ?? [];
|
|
2687
|
+
const enabledRequiredFlags = requiredFlags.filter((f) => scanResult.enabledFlags.includes(f));
|
|
2688
|
+
const missingFlags = requiredFlags.filter((f) => !scanResult.enabledFlags.includes(f));
|
|
2689
|
+
const factoryName = EXTRACTOR_FACTORIES[extractorId];
|
|
2690
|
+
const importPath = installDir ? `@unrag/extractors/${extractorId}` : `./lib/unrag/extractors/${extractorId}`;
|
|
2691
|
+
if (isRegistered && missingFlags.length === 0) {
|
|
2692
|
+
return {
|
|
2693
|
+
id: `coherence-${extractorId}`,
|
|
2694
|
+
title: `Extractor: ${extractorId}`,
|
|
2695
|
+
status: "pass",
|
|
2696
|
+
summary: "Installed, registered, and enabled.",
|
|
2697
|
+
meta: {
|
|
2698
|
+
registered: isRegistered,
|
|
2699
|
+
enabledFlags: enabledRequiredFlags
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
if (!isRegistered) {
|
|
2704
|
+
return {
|
|
2705
|
+
id: `coherence-${extractorId}`,
|
|
2706
|
+
title: `Extractor: ${extractorId}`,
|
|
2707
|
+
status: "warn",
|
|
2708
|
+
summary: "Installed but not registered in config.",
|
|
2709
|
+
details: [
|
|
2710
|
+
`Factory ${factoryName}() not found in unrag.config.ts`,
|
|
2711
|
+
"Assets of this type will be skipped during ingestion."
|
|
2712
|
+
],
|
|
2713
|
+
fixHints: [
|
|
2714
|
+
`Import and add to extractors array:`,
|
|
2715
|
+
` import { ${factoryName} } from "${importPath}";`,
|
|
2716
|
+
` engine: { extractors: [${factoryName}()] }`
|
|
2717
|
+
],
|
|
2718
|
+
docsLink: docsUrl("/docs/reference/cli#registering-extractors-manually"),
|
|
2719
|
+
meta: {
|
|
2720
|
+
registered: false,
|
|
2721
|
+
confidence: scanResult.confidence
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
if (missingFlags.length > 0) {
|
|
2726
|
+
return {
|
|
2727
|
+
id: `coherence-${extractorId}`,
|
|
2728
|
+
title: `Extractor: ${extractorId}`,
|
|
2729
|
+
status: "warn",
|
|
2730
|
+
summary: "Registered but required flags are disabled.",
|
|
2731
|
+
details: [
|
|
2732
|
+
`Missing enabled flags: ${missingFlags.join(", ")}`,
|
|
2733
|
+
"The extractor is wired but assets won't be processed."
|
|
2734
|
+
],
|
|
2735
|
+
fixHints: missingFlags.map((f) => `Set ${f.replace("assetProcessing.", "engine.assetProcessing.")}: true`),
|
|
2736
|
+
meta: {
|
|
2737
|
+
registered: true,
|
|
2738
|
+
missingFlags,
|
|
2739
|
+
confidence: scanResult.confidence
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
id: `coherence-${extractorId}`,
|
|
2745
|
+
title: `Extractor: ${extractorId}`,
|
|
2746
|
+
status: "pass",
|
|
2747
|
+
summary: "Configuration appears correct."
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// cli/lib/doctor/dbChecks.ts
|
|
2752
|
+
import path11 from "node:path";
|
|
2753
|
+
async function runDbChecks(state, options) {
|
|
2754
|
+
const results = [];
|
|
2755
|
+
const dbUrlResult = resolveDbUrl(state, options);
|
|
2756
|
+
if (!dbUrlResult.url) {
|
|
2757
|
+
results.push({
|
|
2758
|
+
id: "db-url",
|
|
2759
|
+
title: "Database URL",
|
|
2760
|
+
status: "fail",
|
|
2761
|
+
summary: "Could not determine database connection string.",
|
|
2762
|
+
details: dbUrlResult.details,
|
|
2763
|
+
fixHints: [
|
|
2764
|
+
"Set DATABASE_URL environment variable",
|
|
2765
|
+
"Or use --database-url <url> flag",
|
|
2766
|
+
"Or use --database-url-env <VAR_NAME> flag"
|
|
2767
|
+
],
|
|
2768
|
+
docsLink: docsUrl("/docs/getting-started/database")
|
|
2769
|
+
});
|
|
2770
|
+
return results;
|
|
2771
|
+
}
|
|
2772
|
+
results.push({
|
|
2773
|
+
id: "db-url",
|
|
2774
|
+
title: "Database URL",
|
|
2775
|
+
status: "pass",
|
|
2776
|
+
summary: `Using ${dbUrlResult.source}`,
|
|
2777
|
+
details: [redactConnectionString(dbUrlResult.url)]
|
|
2778
|
+
});
|
|
2779
|
+
let client = null;
|
|
2780
|
+
try {
|
|
2781
|
+
const pg = await import("pg");
|
|
2782
|
+
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
2783
|
+
const pool = new Pool({ connectionString: dbUrlResult.url });
|
|
2784
|
+
client = {
|
|
2785
|
+
query: (sql, params) => pool.query(sql, params),
|
|
2786
|
+
end: () => pool.end()
|
|
2787
|
+
};
|
|
2788
|
+
const connectivityResult = await checkConnectivity(client);
|
|
2789
|
+
results.push(connectivityResult);
|
|
2790
|
+
if (connectivityResult.status === "fail") {
|
|
2791
|
+
return results;
|
|
2792
|
+
}
|
|
2793
|
+
const pgvectorResult = await checkPgvector(client);
|
|
2794
|
+
results.push(pgvectorResult);
|
|
2795
|
+
const installDirFull = state.installDir ? path11.join(state.projectRoot, state.installDir) : null;
|
|
2796
|
+
const tableNames = await inferTableNames(installDirFull ?? "", state.storeAdapter);
|
|
2797
|
+
const schemaResults = await checkSchema(client, options.schema, tableNames);
|
|
2798
|
+
results.push(...schemaResults);
|
|
2799
|
+
const indexResults = await checkIndexes(client, options.schema, tableNames);
|
|
2800
|
+
results.push(...indexResults);
|
|
2801
|
+
const dimensionResults = await checkDimensionConsistency(client, options.schema, tableNames, options.scope);
|
|
2802
|
+
results.push(...dimensionResults);
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2805
|
+
results.push({
|
|
2806
|
+
id: "db-connection",
|
|
2807
|
+
title: "Database connection",
|
|
2808
|
+
status: "fail",
|
|
2809
|
+
summary: `Connection failed: ${message}`,
|
|
2810
|
+
fixHints: [
|
|
2811
|
+
"Check that DATABASE_URL is correct",
|
|
2812
|
+
"Ensure the database server is running",
|
|
2813
|
+
"Check network connectivity and firewall rules"
|
|
2814
|
+
]
|
|
2815
|
+
});
|
|
2816
|
+
} finally {
|
|
2817
|
+
if (client) {
|
|
2818
|
+
await client.end().catch(() => {});
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return results;
|
|
2822
|
+
}
|
|
2823
|
+
function resolveDbUrl(state, options) {
|
|
2824
|
+
const details = [];
|
|
2825
|
+
if (options.databaseUrl) {
|
|
2826
|
+
return {
|
|
2827
|
+
url: options.databaseUrl,
|
|
2828
|
+
source: "--database-url flag",
|
|
2829
|
+
details: ["Using explicitly provided connection string."]
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
if (options.databaseUrlEnv) {
|
|
2833
|
+
const value = process.env[options.databaseUrlEnv];
|
|
2834
|
+
if (value) {
|
|
2835
|
+
return {
|
|
2836
|
+
url: value,
|
|
2837
|
+
source: `${options.databaseUrlEnv} (via --database-url-env)`,
|
|
2838
|
+
details: [`Using env var specified by flag: ${options.databaseUrlEnv}`]
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
details.push(`${options.databaseUrlEnv} is not set (specified via --database-url-env).`);
|
|
2842
|
+
}
|
|
2843
|
+
if (state.inferredDbEnvVar) {
|
|
2844
|
+
const value = process.env[state.inferredDbEnvVar];
|
|
2845
|
+
if (value) {
|
|
2846
|
+
return {
|
|
2847
|
+
url: value,
|
|
2848
|
+
source: `${state.inferredDbEnvVar} (inferred from config)`,
|
|
2849
|
+
details: [`Found ${state.inferredDbEnvVar} in your database configuration.`]
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
details.push(`${state.inferredDbEnvVar} inferred from config but not set.`);
|
|
2853
|
+
}
|
|
2854
|
+
if (process.env.DATABASE_URL) {
|
|
2855
|
+
return {
|
|
2856
|
+
url: process.env.DATABASE_URL,
|
|
2857
|
+
source: "DATABASE_URL",
|
|
2858
|
+
details: ["Using standard DATABASE_URL environment variable."]
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
details.push("DATABASE_URL is not set.");
|
|
2862
|
+
return { url: null, source: "", details };
|
|
2863
|
+
}
|
|
2864
|
+
function redactConnectionString(url) {
|
|
2865
|
+
try {
|
|
2866
|
+
const parsed = new URL(url);
|
|
2867
|
+
if (parsed.password) {
|
|
2868
|
+
parsed.password = "****";
|
|
2869
|
+
}
|
|
2870
|
+
return parsed.toString().replace(/\*\*\*\*@/, "****@");
|
|
2871
|
+
} catch {
|
|
2872
|
+
return url.replace(/:([^:@]+)@/, ":****@");
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
async function checkConnectivity(client) {
|
|
2876
|
+
try {
|
|
2877
|
+
const versionResult = await client.query("SELECT version()");
|
|
2878
|
+
const version = versionResult.rows[0]?.version ?? "unknown";
|
|
2879
|
+
const userResult = await client.query("SELECT current_user");
|
|
2880
|
+
const user = userResult.rows[0]?.current_user ?? "unknown";
|
|
2881
|
+
const dbResult = await client.query("SELECT current_database()");
|
|
2882
|
+
const database = dbResult.rows[0]?.current_database ?? "unknown";
|
|
2883
|
+
const versionMatch = version.match(/PostgreSQL (\d+\.\d+)/);
|
|
2884
|
+
const pgVersion = versionMatch ? versionMatch[1] : version.slice(0, 50);
|
|
2885
|
+
return {
|
|
2886
|
+
id: "db-connectivity",
|
|
2887
|
+
title: "Database connectivity",
|
|
2888
|
+
status: "pass",
|
|
2889
|
+
summary: "Successfully connected to PostgreSQL.",
|
|
2890
|
+
details: [
|
|
2891
|
+
`Version: PostgreSQL ${pgVersion}`,
|
|
2892
|
+
`Database: ${database}`,
|
|
2893
|
+
`User: ${user}`
|
|
2894
|
+
]
|
|
2895
|
+
};
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2898
|
+
return {
|
|
2899
|
+
id: "db-connectivity",
|
|
2900
|
+
title: "Database connectivity",
|
|
2901
|
+
status: "fail",
|
|
2902
|
+
summary: `Connection test failed: ${message}`
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
async function checkPgvector(client) {
|
|
2907
|
+
try {
|
|
2908
|
+
const extResult = await client.query("SELECT extname, extversion FROM pg_extension WHERE extname = 'vector'");
|
|
2909
|
+
if (extResult.rows.length === 0) {
|
|
2910
|
+
return {
|
|
2911
|
+
id: "db-pgvector",
|
|
2912
|
+
title: "pgvector extension",
|
|
2913
|
+
status: "fail",
|
|
2914
|
+
summary: "pgvector extension is not installed.",
|
|
2915
|
+
fixHints: [
|
|
2916
|
+
"Run: CREATE EXTENSION IF NOT EXISTS vector;",
|
|
2917
|
+
"You may need superuser privileges or extension preinstalled."
|
|
2918
|
+
],
|
|
2919
|
+
docsLink: docsUrl("/docs/getting-started/database#enabling-pgvector")
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
const version = extResult.rows[0]?.extversion ?? "unknown";
|
|
2923
|
+
try {
|
|
2924
|
+
await client.query("SELECT '[1,2,3]'::vector <=> '[1,2,3]'::vector");
|
|
2925
|
+
} catch {
|
|
2926
|
+
return {
|
|
2927
|
+
id: "db-pgvector",
|
|
2928
|
+
title: "pgvector extension",
|
|
2929
|
+
status: "warn",
|
|
2930
|
+
summary: `pgvector ${version} installed but operator test failed.`,
|
|
2931
|
+
details: ["The <=> (cosine distance) operator may not be available."]
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
let hasHnsw = false;
|
|
2935
|
+
try {
|
|
2936
|
+
const amResult = await client.query("SELECT amname FROM pg_am WHERE amname = 'hnsw'");
|
|
2937
|
+
hasHnsw = amResult.rows.length > 0;
|
|
2938
|
+
} catch {}
|
|
2939
|
+
return {
|
|
2940
|
+
id: "db-pgvector",
|
|
2941
|
+
title: "pgvector extension",
|
|
2942
|
+
status: "pass",
|
|
2943
|
+
summary: `pgvector ${version} installed and working.`,
|
|
2944
|
+
details: hasHnsw ? ["HNSW index support available."] : ["HNSW index support not detected (requires pgvector 0.5.0+)."],
|
|
2945
|
+
meta: { version, hasHnsw }
|
|
2946
|
+
};
|
|
2947
|
+
} catch (err) {
|
|
2948
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2949
|
+
return {
|
|
2950
|
+
id: "db-pgvector",
|
|
2951
|
+
title: "pgvector extension",
|
|
2952
|
+
status: "fail",
|
|
2953
|
+
summary: `pgvector check failed: ${message}`
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
async function checkSchema(client, schema, tableNames) {
|
|
2958
|
+
const results = [];
|
|
2959
|
+
for (const [logicalName, tableName] of Object.entries(tableNames)) {
|
|
2960
|
+
const tableResult = await checkTable(client, schema, tableName, logicalName);
|
|
2961
|
+
results.push(tableResult);
|
|
2962
|
+
}
|
|
2963
|
+
const fkResult = await checkForeignKeys(client, schema, tableNames);
|
|
2964
|
+
results.push(fkResult);
|
|
2965
|
+
return results;
|
|
2966
|
+
}
|
|
2967
|
+
async function checkTable(client, schema, tableName, logicalName) {
|
|
2968
|
+
const expectedColumns = {
|
|
2969
|
+
documents: ["id", "source_id", "content", "metadata"],
|
|
2970
|
+
chunks: ["id", "document_id", "source_id", "idx", "content", "token_count", "metadata"],
|
|
2971
|
+
embeddings: ["chunk_id", "embedding", "embedding_dimension"]
|
|
2972
|
+
};
|
|
2973
|
+
try {
|
|
2974
|
+
const tableResult = await client.query(`SELECT EXISTS (
|
|
2975
|
+
SELECT FROM information_schema.tables
|
|
2976
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
2977
|
+
)`, [schema, tableName]);
|
|
2978
|
+
if (!tableResult.rows[0]?.exists) {
|
|
2979
|
+
return {
|
|
2980
|
+
id: `db-table-${logicalName}`,
|
|
2981
|
+
title: `Table: ${tableName}`,
|
|
2982
|
+
status: "fail",
|
|
2983
|
+
summary: `Table ${schema}.${tableName} does not exist.`,
|
|
2984
|
+
fixHints: [
|
|
2985
|
+
"Run the schema migration to create Unrag tables.",
|
|
2986
|
+
"See lib/unrag/unrag.md for the required schema."
|
|
2987
|
+
],
|
|
2988
|
+
docsLink: docsUrl("/docs/getting-started/database#creating-the-schema")
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
const columnResult = await client.query(`SELECT column_name FROM information_schema.columns
|
|
2992
|
+
WHERE table_schema = $1 AND table_name = $2`, [schema, tableName]);
|
|
2993
|
+
const actualColumns = columnResult.rows.map((r) => r.column_name);
|
|
2994
|
+
const expected = expectedColumns[logicalName] ?? [];
|
|
2995
|
+
const missingColumns = expected.filter((c) => !actualColumns.includes(c));
|
|
2996
|
+
if (missingColumns.length > 0) {
|
|
2997
|
+
return {
|
|
2998
|
+
id: `db-table-${logicalName}`,
|
|
2999
|
+
title: `Table: ${tableName}`,
|
|
3000
|
+
status: "warn",
|
|
3001
|
+
summary: `Table exists but missing columns: ${missingColumns.join(", ")}`,
|
|
3002
|
+
details: [
|
|
3003
|
+
`Expected: ${expected.join(", ")}`,
|
|
3004
|
+
`Found: ${actualColumns.join(", ")}`
|
|
3005
|
+
]
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
return {
|
|
3009
|
+
id: `db-table-${logicalName}`,
|
|
3010
|
+
title: `Table: ${tableName}`,
|
|
3011
|
+
status: "pass",
|
|
3012
|
+
summary: "Table exists with expected columns.",
|
|
3013
|
+
meta: { columns: actualColumns }
|
|
3014
|
+
};
|
|
3015
|
+
} catch (err) {
|
|
3016
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3017
|
+
return {
|
|
3018
|
+
id: `db-table-${logicalName}`,
|
|
3019
|
+
title: `Table: ${tableName}`,
|
|
3020
|
+
status: "fail",
|
|
3021
|
+
summary: `Schema check failed: ${message}`
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
async function checkForeignKeys(client, schema, tableNames) {
|
|
3026
|
+
try {
|
|
3027
|
+
const fkResult = await client.query(`SELECT
|
|
3028
|
+
tc.constraint_name,
|
|
3029
|
+
tc.table_name,
|
|
3030
|
+
kcu.column_name,
|
|
3031
|
+
ccu.table_name AS foreign_table_name,
|
|
3032
|
+
rc.delete_rule
|
|
3033
|
+
FROM information_schema.table_constraints tc
|
|
3034
|
+
JOIN information_schema.key_column_usage kcu
|
|
3035
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
3036
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
3037
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
3038
|
+
JOIN information_schema.referential_constraints rc
|
|
3039
|
+
ON tc.constraint_name = rc.constraint_name
|
|
3040
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
3041
|
+
AND tc.table_schema = $1
|
|
3042
|
+
AND tc.table_name IN ($2, $3, $4)`, [schema, tableNames.documents, tableNames.chunks, tableNames.embeddings]);
|
|
3043
|
+
const fks = fkResult.rows;
|
|
3044
|
+
const issues = [];
|
|
3045
|
+
const chunksFk = fks.find((f) => f.table_name === tableNames.chunks && f.column_name === "document_id");
|
|
3046
|
+
if (!chunksFk) {
|
|
3047
|
+
issues.push(`${tableNames.chunks}.document_id: FK not found`);
|
|
3048
|
+
} else if (chunksFk.delete_rule !== "CASCADE") {
|
|
3049
|
+
issues.push(`${tableNames.chunks}.document_id: delete rule is ${chunksFk.delete_rule}, expected CASCADE`);
|
|
3050
|
+
}
|
|
3051
|
+
const embeddingsFk = fks.find((f) => f.table_name === tableNames.embeddings && f.column_name === "chunk_id");
|
|
3052
|
+
if (!embeddingsFk) {
|
|
3053
|
+
issues.push(`${tableNames.embeddings}.chunk_id: FK not found`);
|
|
3054
|
+
} else if (embeddingsFk.delete_rule !== "CASCADE") {
|
|
3055
|
+
issues.push(`${tableNames.embeddings}.chunk_id: delete rule is ${embeddingsFk.delete_rule}, expected CASCADE`);
|
|
3056
|
+
}
|
|
3057
|
+
if (issues.length === 0) {
|
|
3058
|
+
return {
|
|
3059
|
+
id: "db-foreign-keys",
|
|
3060
|
+
title: "Foreign key constraints",
|
|
3061
|
+
status: "pass",
|
|
3062
|
+
summary: "CASCADE delete constraints are properly configured."
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
return {
|
|
3066
|
+
id: "db-foreign-keys",
|
|
3067
|
+
title: "Foreign key constraints",
|
|
3068
|
+
status: "warn",
|
|
3069
|
+
summary: "Some foreign key constraints may be misconfigured.",
|
|
3070
|
+
details: issues,
|
|
3071
|
+
fixHints: [
|
|
3072
|
+
"Ensure FK constraints use ON DELETE CASCADE",
|
|
3073
|
+
"This ensures chunks/embeddings are deleted when documents are removed."
|
|
3074
|
+
]
|
|
3075
|
+
};
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3078
|
+
return {
|
|
3079
|
+
id: "db-foreign-keys",
|
|
3080
|
+
title: "Foreign key constraints",
|
|
3081
|
+
status: "warn",
|
|
3082
|
+
summary: `Could not verify FK constraints: ${message}`
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function checkIndexes(client, schema, tableNames) {
|
|
3087
|
+
const results = [];
|
|
3088
|
+
try {
|
|
3089
|
+
const indexResult = await client.query(`SELECT tablename, indexname, indexdef
|
|
3090
|
+
FROM pg_indexes
|
|
3091
|
+
WHERE schemaname = $1
|
|
3092
|
+
AND tablename IN ($2, $3, $4)`, [schema, tableNames.documents, tableNames.chunks, tableNames.embeddings]);
|
|
3093
|
+
const indexes = indexResult.rows;
|
|
3094
|
+
const chunksSourceIdx = indexes.find((i) => i.tablename === tableNames.chunks && i.indexdef.toLowerCase().includes("source_id"));
|
|
3095
|
+
results.push({
|
|
3096
|
+
id: "db-index-chunks-source",
|
|
3097
|
+
title: `Index: ${tableNames.chunks}(source_id)`,
|
|
3098
|
+
status: chunksSourceIdx ? "pass" : "warn",
|
|
3099
|
+
summary: chunksSourceIdx ? "Index exists for source_id filtering." : "Recommended index on source_id not found.",
|
|
3100
|
+
fixHints: chunksSourceIdx ? undefined : [
|
|
3101
|
+
`CREATE INDEX IF NOT EXISTS ${tableNames.chunks}_source_id_idx ON ${tableNames.chunks}(source_id);`
|
|
3102
|
+
]
|
|
3103
|
+
});
|
|
3104
|
+
const docsSourceIdx = indexes.find((i) => i.tablename === tableNames.documents && i.indexdef.toLowerCase().includes("source_id"));
|
|
3105
|
+
results.push({
|
|
3106
|
+
id: "db-index-docs-source",
|
|
3107
|
+
title: `Index: ${tableNames.documents}(source_id)`,
|
|
3108
|
+
status: docsSourceIdx ? "pass" : "warn",
|
|
3109
|
+
summary: docsSourceIdx ? "Index exists for source_id filtering." : "Recommended index on source_id not found.",
|
|
3110
|
+
fixHints: docsSourceIdx ? undefined : [
|
|
3111
|
+
`CREATE INDEX IF NOT EXISTS ${tableNames.documents}_source_id_idx ON ${tableNames.documents}(source_id);`
|
|
3112
|
+
]
|
|
3113
|
+
});
|
|
3114
|
+
const vectorIdx = indexes.find((i) => i.tablename === tableNames.embeddings && (i.indexdef.toLowerCase().includes("hnsw") || i.indexdef.toLowerCase().includes("ivfflat")));
|
|
3115
|
+
const countResult = await client.query(`SELECT COUNT(*) as count FROM ${schema}.${tableNames.embeddings}`);
|
|
3116
|
+
const rowCount = parseInt(countResult.rows[0]?.count ?? "0", 10);
|
|
3117
|
+
if (vectorIdx) {
|
|
3118
|
+
results.push({
|
|
3119
|
+
id: "db-index-vector",
|
|
3120
|
+
title: "Vector index",
|
|
3121
|
+
status: "pass",
|
|
3122
|
+
summary: "Vector index found for similarity search.",
|
|
3123
|
+
details: [vectorIdx.indexdef],
|
|
3124
|
+
meta: { rowCount }
|
|
3125
|
+
});
|
|
3126
|
+
} else if (rowCount > 50000) {
|
|
3127
|
+
results.push({
|
|
3128
|
+
id: "db-index-vector",
|
|
3129
|
+
title: "Vector index",
|
|
3130
|
+
status: "warn",
|
|
3131
|
+
summary: `No vector index found (${rowCount.toLocaleString()} embeddings).`,
|
|
3132
|
+
details: [
|
|
3133
|
+
"Large datasets benefit significantly from HNSW indexing.",
|
|
3134
|
+
"Without an index, similarity search scans all rows."
|
|
3135
|
+
],
|
|
3136
|
+
fixHints: [
|
|
3137
|
+
`CREATE INDEX IF NOT EXISTS ${tableNames.embeddings}_hnsw_idx`,
|
|
3138
|
+
`ON ${tableNames.embeddings} USING hnsw (embedding vector_cosine_ops);`
|
|
3139
|
+
],
|
|
3140
|
+
docsLink: docsUrl("/docs/concepts/performance"),
|
|
3141
|
+
meta: { rowCount }
|
|
3142
|
+
});
|
|
3143
|
+
} else {
|
|
3144
|
+
results.push({
|
|
3145
|
+
id: "db-index-vector",
|
|
3146
|
+
title: "Vector index",
|
|
3147
|
+
status: "pass",
|
|
3148
|
+
summary: `No vector index (${rowCount.toLocaleString()} embeddings).`,
|
|
3149
|
+
details: [
|
|
3150
|
+
"Current dataset size is small enough for sequential scan.",
|
|
3151
|
+
"Consider adding HNSW index when exceeding ~50k embeddings."
|
|
3152
|
+
],
|
|
3153
|
+
meta: { rowCount }
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
} catch (err) {
|
|
3157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3158
|
+
results.push({
|
|
3159
|
+
id: "db-indexes",
|
|
3160
|
+
title: "Index validation",
|
|
3161
|
+
status: "warn",
|
|
3162
|
+
summary: `Could not check indexes: ${message}`
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
return results;
|
|
3166
|
+
}
|
|
3167
|
+
async function checkDimensionConsistency(client, schema, tableNames, scope) {
|
|
3168
|
+
const results = [];
|
|
3169
|
+
try {
|
|
3170
|
+
const nullDimSql = scope ? `SELECT COUNT(*) as count
|
|
3171
|
+
FROM ${schema}.${tableNames.embeddings} e
|
|
3172
|
+
JOIN ${schema}.${tableNames.chunks} c ON e.chunk_id = c.id
|
|
3173
|
+
WHERE c.source_id LIKE $1
|
|
3174
|
+
AND (e.embedding_dimension IS NULL OR e.embedding_dimension = 0)` : `SELECT COUNT(*) as count
|
|
3175
|
+
FROM ${schema}.${tableNames.embeddings} e
|
|
3176
|
+
WHERE (e.embedding_dimension IS NULL OR e.embedding_dimension = 0)`;
|
|
3177
|
+
const nullDimResult = await client.query(nullDimSql, scope ? [scope + "%"] : []);
|
|
3178
|
+
const nullCount = parseInt(nullDimResult.rows[0]?.count ?? "0", 10);
|
|
3179
|
+
if (nullCount > 0) {
|
|
3180
|
+
results.push({
|
|
3181
|
+
id: "db-dim-nulls",
|
|
3182
|
+
title: "Embedding dimensions",
|
|
3183
|
+
status: "warn",
|
|
3184
|
+
summary: `${nullCount} embeddings have NULL or 0 dimension.`,
|
|
3185
|
+
details: [
|
|
3186
|
+
"This may indicate incomplete ingestion or migration issues.",
|
|
3187
|
+
scope ? `Scope: ${scope}*` : "Checking all embeddings."
|
|
3188
|
+
]
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
const dimSql = scope ? `SELECT e.embedding_dimension, COUNT(*) as count
|
|
3192
|
+
FROM ${schema}.${tableNames.embeddings} e
|
|
3193
|
+
JOIN ${schema}.${tableNames.chunks} c ON e.chunk_id = c.id
|
|
3194
|
+
WHERE c.source_id LIKE $1
|
|
3195
|
+
AND e.embedding_dimension IS NOT NULL AND e.embedding_dimension > 0
|
|
3196
|
+
GROUP BY e.embedding_dimension
|
|
3197
|
+
ORDER BY count DESC` : `SELECT e.embedding_dimension, COUNT(*) as count
|
|
3198
|
+
FROM ${schema}.${tableNames.embeddings} e
|
|
3199
|
+
WHERE e.embedding_dimension IS NOT NULL AND e.embedding_dimension > 0
|
|
3200
|
+
GROUP BY e.embedding_dimension
|
|
3201
|
+
ORDER BY count DESC`;
|
|
3202
|
+
const dimResult = await client.query(dimSql, scope ? [scope + "%"] : []);
|
|
3203
|
+
const dimensions = dimResult.rows;
|
|
3204
|
+
if (dimensions.length === 0) {
|
|
3205
|
+
results.push({
|
|
3206
|
+
id: "db-dim-consistency",
|
|
3207
|
+
title: "Dimension consistency",
|
|
3208
|
+
status: "pass",
|
|
3209
|
+
summary: "No embeddings found to check.",
|
|
3210
|
+
details: scope ? [`Scope: ${scope}*`] : undefined
|
|
3211
|
+
});
|
|
3212
|
+
} else if (dimensions.length === 1) {
|
|
3213
|
+
const dim = dimensions[0];
|
|
3214
|
+
results.push({
|
|
3215
|
+
id: "db-dim-consistency",
|
|
3216
|
+
title: "Dimension consistency",
|
|
3217
|
+
status: "pass",
|
|
3218
|
+
summary: `All embeddings use ${dim.embedding_dimension} dimensions.`,
|
|
3219
|
+
details: [
|
|
3220
|
+
`Total: ${parseInt(dim.count, 10).toLocaleString()} embeddings`,
|
|
3221
|
+
scope ? `Scope: ${scope}*` : "All embeddings checked."
|
|
3222
|
+
],
|
|
3223
|
+
meta: { dimension: dim.embedding_dimension, count: parseInt(dim.count, 10) }
|
|
3224
|
+
});
|
|
3225
|
+
} else {
|
|
3226
|
+
const details = dimensions.map((d) => `${d.embedding_dimension} dimensions: ${parseInt(d.count, 10).toLocaleString()} embeddings`);
|
|
3227
|
+
results.push({
|
|
3228
|
+
id: "db-dim-consistency",
|
|
3229
|
+
title: "Dimension consistency",
|
|
3230
|
+
status: "warn",
|
|
3231
|
+
summary: `Mixed dimensions found (${dimensions.length} different values).`,
|
|
3232
|
+
details: [
|
|
3233
|
+
...details,
|
|
3234
|
+
"",
|
|
3235
|
+
"Mixed dimensions can cause retrieval errors.",
|
|
3236
|
+
"This typically happens when changing embedding models."
|
|
3237
|
+
],
|
|
3238
|
+
fixHints: [
|
|
3239
|
+
"Use --scope to isolate different embedding sets",
|
|
3240
|
+
"Re-ingest documents with the current model",
|
|
3241
|
+
"Or separate content by sourceId prefix"
|
|
3242
|
+
],
|
|
3243
|
+
docsLink: docsUrl("/docs/concepts/architecture"),
|
|
3244
|
+
meta: {
|
|
3245
|
+
dimensions: dimensions.map((d) => ({
|
|
3246
|
+
dimension: d.embedding_dimension,
|
|
3247
|
+
count: parseInt(d.count, 10)
|
|
3248
|
+
}))
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
try {
|
|
3253
|
+
const sampleResult = await client.query(`SELECT
|
|
3254
|
+
chunk_id,
|
|
3255
|
+
embedding_dimension as stored_dim,
|
|
3256
|
+
vector_dims(embedding) as actual_dim
|
|
3257
|
+
FROM ${schema}.${tableNames.embeddings}
|
|
3258
|
+
WHERE embedding IS NOT NULL
|
|
3259
|
+
LIMIT 100`);
|
|
3260
|
+
const mismatches = sampleResult.rows.filter((r) => r.stored_dim !== r.actual_dim);
|
|
3261
|
+
if (mismatches.length > 0) {
|
|
3262
|
+
results.push({
|
|
3263
|
+
id: "db-dim-mismatch",
|
|
3264
|
+
title: "Dimension metadata",
|
|
3265
|
+
status: "warn",
|
|
3266
|
+
summary: `${mismatches.length} sampled rows have dimension mismatch.`,
|
|
3267
|
+
details: [
|
|
3268
|
+
"embedding_dimension column doesn't match actual vector length.",
|
|
3269
|
+
"This may cause issues with some operations."
|
|
3270
|
+
]
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
} catch {}
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3276
|
+
results.push({
|
|
3277
|
+
id: "db-dimensions",
|
|
3278
|
+
title: "Dimension checks",
|
|
3279
|
+
status: "warn",
|
|
3280
|
+
summary: `Could not check dimensions: ${message}`
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
return results;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// cli/lib/doctor/output.ts
|
|
3287
|
+
var STATUS_ICONS = {
|
|
3288
|
+
pass: "✓",
|
|
3289
|
+
warn: "⚠",
|
|
3290
|
+
fail: "✗",
|
|
3291
|
+
skip: "○"
|
|
3292
|
+
};
|
|
3293
|
+
var STATUS_COLORS = {
|
|
3294
|
+
pass: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
3295
|
+
warn: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
3296
|
+
fail: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
3297
|
+
skip: (s) => `\x1B[90m${s}\x1B[0m`
|
|
3298
|
+
};
|
|
3299
|
+
function formatReport(report, options = {}) {
|
|
3300
|
+
const lines = [];
|
|
3301
|
+
lines.push("");
|
|
3302
|
+
for (const group of report.groups) {
|
|
3303
|
+
if (group.results.length === 0)
|
|
3304
|
+
continue;
|
|
3305
|
+
lines.push(`${group.title}`);
|
|
3306
|
+
lines.push("─".repeat(group.title.length));
|
|
3307
|
+
for (const result of group.results) {
|
|
3308
|
+
lines.push(formatResult(result));
|
|
3309
|
+
}
|
|
3310
|
+
lines.push("");
|
|
3311
|
+
}
|
|
3312
|
+
lines.push("Summary");
|
|
3313
|
+
lines.push("─".repeat(7));
|
|
3314
|
+
const { pass, warn, fail, skip, total } = report.summary;
|
|
3315
|
+
const summaryParts = [];
|
|
3316
|
+
if (pass > 0)
|
|
3317
|
+
summaryParts.push(STATUS_COLORS.pass(`${pass} passed`));
|
|
3318
|
+
if (warn > 0)
|
|
3319
|
+
summaryParts.push(STATUS_COLORS.warn(`${warn} warnings`));
|
|
3320
|
+
if (fail > 0)
|
|
3321
|
+
summaryParts.push(STATUS_COLORS.fail(`${fail} failed`));
|
|
3322
|
+
if (skip > 0)
|
|
3323
|
+
summaryParts.push(STATUS_COLORS.skip(`${skip} skipped`));
|
|
3324
|
+
lines.push(`${summaryParts.join(", ")} (${total} total)`);
|
|
3325
|
+
if (fail > 0) {
|
|
3326
|
+
lines.push("");
|
|
3327
|
+
lines.push(STATUS_COLORS.fail("Some checks failed. See details above."));
|
|
3328
|
+
} else if (warn > 0) {
|
|
3329
|
+
lines.push("");
|
|
3330
|
+
lines.push(STATUS_COLORS.warn("All checks passed with warnings. Review recommended."));
|
|
3331
|
+
} else {
|
|
3332
|
+
lines.push("");
|
|
3333
|
+
lines.push(STATUS_COLORS.pass("All checks passed!"));
|
|
3334
|
+
}
|
|
3335
|
+
if (options.showDbHint) {
|
|
3336
|
+
lines.push("");
|
|
3337
|
+
lines.push(STATUS_COLORS.skip("Tip: Run `unrag doctor --db` to also check database connectivity and schema."));
|
|
3338
|
+
}
|
|
3339
|
+
return lines.join(`
|
|
3340
|
+
`);
|
|
3341
|
+
}
|
|
3342
|
+
function formatResult(result) {
|
|
3343
|
+
const lines = [];
|
|
3344
|
+
const icon = STATUS_ICONS[result.status];
|
|
3345
|
+
const color = STATUS_COLORS[result.status];
|
|
3346
|
+
lines.push(` ${color(icon)} ${result.title}: ${result.summary}`);
|
|
3347
|
+
if (result.details && result.details.length > 0) {
|
|
3348
|
+
for (const detail of result.details) {
|
|
3349
|
+
if (detail === "") {
|
|
3350
|
+
lines.push("");
|
|
3351
|
+
} else {
|
|
3352
|
+
lines.push(` ${detail}`);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
if (result.fixHints && result.fixHints.length > 0) {
|
|
3357
|
+
lines.push(` Fix:`);
|
|
3358
|
+
for (const hint of result.fixHints) {
|
|
3359
|
+
lines.push(` ${hint}`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
if (result.docsLink) {
|
|
3363
|
+
lines.push(` Docs: ${result.docsLink}`);
|
|
3364
|
+
}
|
|
3365
|
+
return lines.join(`
|
|
3366
|
+
`);
|
|
3367
|
+
}
|
|
3368
|
+
function formatJson(report) {
|
|
3369
|
+
return JSON.stringify(report, null, 2);
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// cli/lib/doctor/env.ts
|
|
3373
|
+
import path12 from "node:path";
|
|
3374
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
3375
|
+
async function loadEnvFilesFromList(options) {
|
|
3376
|
+
const loadedFiles = [];
|
|
3377
|
+
const loadedKeys = [];
|
|
3378
|
+
const skippedKeys = [];
|
|
3379
|
+
const warnings = [];
|
|
3380
|
+
for (const rel of options.files) {
|
|
3381
|
+
if (!rel.trim())
|
|
3382
|
+
continue;
|
|
3383
|
+
const abs = path12.isAbsolute(rel) ? rel : path12.join(options.projectRoot, rel);
|
|
3384
|
+
if (!await exists(abs))
|
|
3385
|
+
continue;
|
|
3386
|
+
try {
|
|
3387
|
+
const raw = await readFile8(abs, "utf8");
|
|
3388
|
+
const parsed = parseDotenv(raw);
|
|
3389
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
3390
|
+
if (process.env[k] !== undefined) {
|
|
3391
|
+
skippedKeys.push(k);
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
process.env[k] = v;
|
|
3395
|
+
loadedKeys.push(k);
|
|
3396
|
+
}
|
|
3397
|
+
loadedFiles.push(rel);
|
|
3398
|
+
} catch (err) {
|
|
3399
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3400
|
+
warnings.push(`Failed to read ${rel}: ${msg}`);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
return {
|
|
3404
|
+
loadedFiles,
|
|
3405
|
+
loadedKeys,
|
|
3406
|
+
skippedKeys,
|
|
3407
|
+
warnings
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
function parseDotenv(raw) {
|
|
3411
|
+
const out = {};
|
|
3412
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3413
|
+
const trimmed = line.trim();
|
|
3414
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
3415
|
+
continue;
|
|
3416
|
+
const l = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
3417
|
+
const eq = l.indexOf("=");
|
|
3418
|
+
if (eq <= 0)
|
|
3419
|
+
continue;
|
|
3420
|
+
const key = l.slice(0, eq).trim();
|
|
3421
|
+
if (!key)
|
|
3422
|
+
continue;
|
|
3423
|
+
let value = l.slice(eq + 1).trim();
|
|
3424
|
+
if (!value.startsWith('"') && !value.startsWith("'")) {
|
|
3425
|
+
const hash = value.indexOf(" #");
|
|
3426
|
+
if (hash >= 0)
|
|
3427
|
+
value = value.slice(0, hash).trim();
|
|
3428
|
+
}
|
|
3429
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3430
|
+
value = value.slice(1, -1);
|
|
3431
|
+
}
|
|
3432
|
+
if (trimmed.includes('"')) {
|
|
3433
|
+
value = value.replace(/\\n/g, `
|
|
3434
|
+
`).replace(/\\r/g, "\r").replace(/\\t/g, "\t");
|
|
3435
|
+
}
|
|
3436
|
+
out[key] = value;
|
|
3437
|
+
}
|
|
3438
|
+
return out;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// cli/lib/doctor/doctorConfig.ts
|
|
3442
|
+
import path13 from "node:path";
|
|
3443
|
+
var DOCTOR_CONFIG_VERSION = 1;
|
|
3444
|
+
var DEFAULT_ENV_LOAD_FILES = [
|
|
3445
|
+
".env",
|
|
3446
|
+
".env.local",
|
|
3447
|
+
".env.${NODE_ENV}",
|
|
3448
|
+
".env.${NODE_ENV}.local"
|
|
3449
|
+
];
|
|
3450
|
+
async function readDoctorConfig(configPath) {
|
|
3451
|
+
if (!await exists(configPath)) {
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
const raw = await readJsonFile(configPath);
|
|
3455
|
+
if (!raw || typeof raw !== "object") {
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
if (raw.version !== undefined && typeof raw.version !== "number") {
|
|
3459
|
+
return null;
|
|
3460
|
+
}
|
|
3461
|
+
return raw;
|
|
3462
|
+
}
|
|
3463
|
+
function mergeDoctorArgsWithConfig(args, config, projectRoot) {
|
|
3464
|
+
if (!config)
|
|
3465
|
+
return args;
|
|
3466
|
+
const merged = { ...args };
|
|
3467
|
+
if (!merged.installDir && config.installDir) {
|
|
3468
|
+
merged.installDir = config.installDir;
|
|
3469
|
+
}
|
|
3470
|
+
if (!merged.schema && config.db?.schema) {
|
|
3471
|
+
merged.schema = config.db.schema;
|
|
3472
|
+
}
|
|
3473
|
+
if (merged.scope === undefined && config.defaults?.scope !== undefined) {
|
|
3474
|
+
merged.scope = config.defaults.scope ?? undefined;
|
|
3475
|
+
}
|
|
3476
|
+
if (merged.strict === undefined && config.defaults?.strict !== undefined) {
|
|
3477
|
+
merged.strict = config.defaults.strict;
|
|
3478
|
+
}
|
|
3479
|
+
if (!merged.databaseUrlEnv && config.env?.databaseUrlEnv) {
|
|
3480
|
+
merged.databaseUrlEnv = config.env.databaseUrlEnv;
|
|
3481
|
+
}
|
|
3482
|
+
return merged;
|
|
3483
|
+
}
|
|
3484
|
+
function getEnvFilesToLoad(config, extraEnvFile) {
|
|
3485
|
+
const files = config?.env?.loadFiles ?? DEFAULT_ENV_LOAD_FILES;
|
|
3486
|
+
const nodeEnv = "development".trim();
|
|
3487
|
+
const interpolated = files.map((f) => f.replace(/\$\{NODE_ENV\}/g, nodeEnv));
|
|
3488
|
+
if (extraEnvFile) {
|
|
3489
|
+
return [extraEnvFile, ...interpolated.filter((f) => f !== extraEnvFile)];
|
|
3490
|
+
}
|
|
3491
|
+
return interpolated;
|
|
3492
|
+
}
|
|
3493
|
+
function resolveConfigPath(projectRoot, configPath) {
|
|
3494
|
+
if (path13.isAbsolute(configPath)) {
|
|
3495
|
+
return configPath;
|
|
3496
|
+
}
|
|
3497
|
+
return path13.join(projectRoot, configPath);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// cli/commands/doctor-setup.ts
|
|
3501
|
+
import path14 from "node:path";
|
|
3502
|
+
import {
|
|
3503
|
+
cancel as cancel3,
|
|
3504
|
+
confirm as confirm3,
|
|
3505
|
+
isCancel as isCancel3,
|
|
3506
|
+
multiselect,
|
|
3507
|
+
outro as outro3,
|
|
3508
|
+
select as select2,
|
|
3509
|
+
spinner,
|
|
3510
|
+
text as text2
|
|
3511
|
+
} from "@clack/prompts";
|
|
3512
|
+
var DEFAULT_CONFIG_PATH = ".unrag/doctor.json";
|
|
3513
|
+
function parseSetupArgs(args) {
|
|
3514
|
+
const out = {};
|
|
3515
|
+
for (let i = 0;i < args.length; i++) {
|
|
3516
|
+
const a = args[i];
|
|
3517
|
+
if (a === "--yes" || a === "-y") {
|
|
3518
|
+
out.yes = true;
|
|
3519
|
+
continue;
|
|
3520
|
+
}
|
|
3521
|
+
if (a === "--project-root") {
|
|
3522
|
+
const v = args[i + 1];
|
|
3523
|
+
if (v && !v.startsWith("-")) {
|
|
3524
|
+
out.projectRoot = v;
|
|
3525
|
+
i++;
|
|
3526
|
+
}
|
|
3527
|
+
continue;
|
|
3528
|
+
}
|
|
3529
|
+
if (a === "--config") {
|
|
3530
|
+
const v = args[i + 1];
|
|
3531
|
+
if (v && !v.startsWith("-")) {
|
|
3532
|
+
out.configPath = v;
|
|
3533
|
+
i++;
|
|
3534
|
+
}
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
return out;
|
|
3539
|
+
}
|
|
3540
|
+
function renderSetupHelp() {
|
|
3541
|
+
return [
|
|
3542
|
+
"unrag doctor setup — configure doctor for your project",
|
|
3543
|
+
"",
|
|
3544
|
+
"Usage:",
|
|
3545
|
+
" unrag doctor setup [options]",
|
|
3546
|
+
"",
|
|
3547
|
+
"Options:",
|
|
3548
|
+
" --yes, -y Non-interactive; accept defaults",
|
|
3549
|
+
" --project-root <path> Override project root directory",
|
|
3550
|
+
" --config <path> Output config file path (default: .unrag/doctor.json)",
|
|
3551
|
+
"",
|
|
3552
|
+
"This command will:",
|
|
3553
|
+
" 1. Detect your Unrag installation settings",
|
|
3554
|
+
" 2. Create a doctor config file with your project-specific settings",
|
|
3555
|
+
" 3. Add convenient npm scripts to your package.json:",
|
|
3556
|
+
" - unrag:doctor Run static checks",
|
|
3557
|
+
" - unrag:doctor:db Run with database checks",
|
|
3558
|
+
" - unrag:doctor:ci Run strict mode with JSON output for CI",
|
|
3559
|
+
"",
|
|
3560
|
+
"Examples:",
|
|
3561
|
+
" unrag doctor setup Interactive setup",
|
|
3562
|
+
" unrag doctor setup --yes Use detected defaults",
|
|
3563
|
+
" unrag doctor setup --config .doctor.json Custom config location",
|
|
3564
|
+
"",
|
|
3565
|
+
"Docs:",
|
|
3566
|
+
` ${docsUrl("/docs/reference/cli")}`
|
|
3567
|
+
].join(`
|
|
3568
|
+
`);
|
|
3569
|
+
}
|
|
3570
|
+
async function doctorSetupCommand(args) {
|
|
3571
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3572
|
+
outro3(renderSetupHelp());
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
const parsed = parseSetupArgs(args);
|
|
3576
|
+
const nonInteractive = Boolean(parsed.yes) || !process.stdin.isTTY;
|
|
3577
|
+
const projectRoot = parsed.projectRoot ?? await tryFindProjectRoot(process.cwd()) ?? process.cwd();
|
|
3578
|
+
const s = spinner();
|
|
3579
|
+
s.start("Detecting project configuration...");
|
|
3580
|
+
const state = await inferInstallState({
|
|
3581
|
+
projectRootOverride: projectRoot
|
|
3582
|
+
});
|
|
3583
|
+
const tableNames = state.installDir ? await inferTableNames(path14.join(projectRoot, state.installDir), state.storeAdapter) : { documents: "documents", chunks: "chunks", embeddings: "embeddings" };
|
|
3584
|
+
s.stop("Configuration detected.");
|
|
3585
|
+
const configPathAnswer = parsed.configPath ? parsed.configPath : nonInteractive ? DEFAULT_CONFIG_PATH : await text2({
|
|
3586
|
+
message: "Config file path",
|
|
3587
|
+
initialValue: DEFAULT_CONFIG_PATH,
|
|
3588
|
+
validate: (v) => {
|
|
3589
|
+
if (!v.trim())
|
|
3590
|
+
return "Config path is required";
|
|
3591
|
+
if (!v.endsWith(".json"))
|
|
3592
|
+
return "Config file must be .json";
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
if (isCancel3(configPathAnswer)) {
|
|
3597
|
+
cancel3("Cancelled.");
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
const configPath = String(configPathAnswer).trim();
|
|
3601
|
+
const configFullPath = path14.isAbsolute(configPath) ? configPath : path14.join(projectRoot, configPath);
|
|
3602
|
+
if (await exists(configFullPath)) {
|
|
3603
|
+
if (nonInteractive) {} else {
|
|
3604
|
+
const overwrite = await confirm3({
|
|
3605
|
+
message: `Config file ${configPath} already exists. Overwrite?`,
|
|
3606
|
+
initialValue: false
|
|
3607
|
+
});
|
|
3608
|
+
if (isCancel3(overwrite)) {
|
|
3609
|
+
cancel3("Cancelled.");
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
if (!overwrite) {
|
|
3613
|
+
outro3("Keeping existing config file.");
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
const installDirAnswer = nonInteractive ? state.installDir ?? "lib/unrag" : await text2({
|
|
3619
|
+
message: "Unrag install directory",
|
|
3620
|
+
initialValue: state.installDir ?? "lib/unrag",
|
|
3621
|
+
validate: (v) => {
|
|
3622
|
+
if (!v.trim())
|
|
3623
|
+
return "Install directory is required";
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
});
|
|
3627
|
+
if (isCancel3(installDirAnswer)) {
|
|
3628
|
+
cancel3("Cancelled.");
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
const installDir = String(installDirAnswer).trim();
|
|
3632
|
+
const envFilesAnswer = nonInteractive ? DEFAULT_ENV_LOAD_FILES : await multiselect({
|
|
3633
|
+
message: "Which .env files should doctor load? (space to toggle)",
|
|
3634
|
+
options: [
|
|
3635
|
+
{ value: ".env", label: ".env", hint: "base env" },
|
|
3636
|
+
{ value: ".env.local", label: ".env.local", hint: "local overrides" },
|
|
3637
|
+
{
|
|
3638
|
+
value: ".env.${NODE_ENV}",
|
|
3639
|
+
label: ".env.${NODE_ENV}",
|
|
3640
|
+
hint: "e.g. .env.development"
|
|
3641
|
+
},
|
|
3642
|
+
{
|
|
3643
|
+
value: ".env.${NODE_ENV}.local",
|
|
3644
|
+
label: ".env.${NODE_ENV}.local",
|
|
3645
|
+
hint: "e.g. .env.development.local"
|
|
3646
|
+
}
|
|
3647
|
+
],
|
|
3648
|
+
initialValues: DEFAULT_ENV_LOAD_FILES,
|
|
3649
|
+
required: false
|
|
3650
|
+
});
|
|
3651
|
+
if (isCancel3(envFilesAnswer)) {
|
|
3652
|
+
cancel3("Cancelled.");
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
const envFiles = envFilesAnswer;
|
|
3656
|
+
const dbEnvVarDefault = state.inferredDbEnvVar ?? "DATABASE_URL";
|
|
3657
|
+
const dbEnvVarAnswer = nonInteractive ? dbEnvVarDefault : await text2({
|
|
3658
|
+
message: "Database URL environment variable name",
|
|
3659
|
+
initialValue: dbEnvVarDefault,
|
|
3660
|
+
validate: (v) => {
|
|
3661
|
+
if (!v.trim())
|
|
3662
|
+
return "Env var name is required";
|
|
3663
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(v))
|
|
3664
|
+
return "Invalid env var name";
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
});
|
|
3668
|
+
if (isCancel3(dbEnvVarAnswer)) {
|
|
3669
|
+
cancel3("Cancelled.");
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
const databaseUrlEnv = String(dbEnvVarAnswer).trim();
|
|
3673
|
+
const schemaAnswer = nonInteractive ? "public" : await text2({
|
|
3674
|
+
message: "Database schema name",
|
|
3675
|
+
initialValue: "public"
|
|
3676
|
+
});
|
|
3677
|
+
if (isCancel3(schemaAnswer)) {
|
|
3678
|
+
cancel3("Cancelled.");
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
const schema = String(schemaAnswer).trim() || "public";
|
|
3682
|
+
const documentsTableAnswer = nonInteractive ? tableNames.documents : await text2({
|
|
3683
|
+
message: "Documents table name",
|
|
3684
|
+
initialValue: tableNames.documents
|
|
3685
|
+
});
|
|
3686
|
+
if (isCancel3(documentsTableAnswer)) {
|
|
3687
|
+
cancel3("Cancelled.");
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
const documentsTable = String(documentsTableAnswer).trim() || "documents";
|
|
3691
|
+
const chunksTableAnswer = nonInteractive ? tableNames.chunks : await text2({
|
|
3692
|
+
message: "Chunks table name",
|
|
3693
|
+
initialValue: tableNames.chunks
|
|
3694
|
+
});
|
|
3695
|
+
if (isCancel3(chunksTableAnswer)) {
|
|
3696
|
+
cancel3("Cancelled.");
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
const chunksTable = String(chunksTableAnswer).trim() || "chunks";
|
|
3700
|
+
const embeddingsTableAnswer = nonInteractive ? tableNames.embeddings : await text2({
|
|
3701
|
+
message: "Embeddings table name",
|
|
3702
|
+
initialValue: tableNames.embeddings
|
|
3703
|
+
});
|
|
3704
|
+
if (isCancel3(embeddingsTableAnswer)) {
|
|
3705
|
+
cancel3("Cancelled.");
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
const embeddingsTable = String(embeddingsTableAnswer).trim() || "embeddings";
|
|
3709
|
+
const scopeAnswer = nonInteractive ? "" : await text2({
|
|
3710
|
+
message: "Default scope prefix for dimension checks (optional, press enter to skip)",
|
|
3711
|
+
initialValue: ""
|
|
3712
|
+
});
|
|
3713
|
+
if (isCancel3(scopeAnswer)) {
|
|
3714
|
+
cancel3("Cancelled.");
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
const defaultScope = String(scopeAnswer).trim() || null;
|
|
3718
|
+
const strictAnswer = nonInteractive ? false : await confirm3({
|
|
3719
|
+
message: "Enable strict mode by default? (treat warnings as failures)",
|
|
3720
|
+
initialValue: false
|
|
3721
|
+
});
|
|
3722
|
+
if (isCancel3(strictAnswer)) {
|
|
3723
|
+
cancel3("Cancelled.");
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
const strictDefault = Boolean(strictAnswer);
|
|
3727
|
+
const ciIncludeDbAnswer = nonInteractive ? true : await confirm3({
|
|
3728
|
+
message: "Should CI script include database checks (--db)?",
|
|
3729
|
+
initialValue: true
|
|
3730
|
+
});
|
|
3731
|
+
if (isCancel3(ciIncludeDbAnswer)) {
|
|
3732
|
+
cancel3("Cancelled.");
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
const ciIncludeDb = Boolean(ciIncludeDbAnswer);
|
|
3736
|
+
const ciStrictAnswer = nonInteractive ? true : await confirm3({
|
|
3737
|
+
message: "Should CI script use strict mode (--strict)?",
|
|
3738
|
+
initialValue: true
|
|
3739
|
+
});
|
|
3740
|
+
if (isCancel3(ciStrictAnswer)) {
|
|
3741
|
+
cancel3("Cancelled.");
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
const ciStrict = Boolean(ciStrictAnswer);
|
|
3745
|
+
const config = {
|
|
3746
|
+
version: DOCTOR_CONFIG_VERSION,
|
|
3747
|
+
installDir,
|
|
3748
|
+
env: {
|
|
3749
|
+
loadFiles: envFiles,
|
|
3750
|
+
databaseUrlEnv
|
|
3751
|
+
},
|
|
3752
|
+
db: {
|
|
3753
|
+
schema,
|
|
3754
|
+
tables: {
|
|
3755
|
+
documents: documentsTable,
|
|
3756
|
+
chunks: chunksTable,
|
|
3757
|
+
embeddings: embeddingsTable
|
|
3758
|
+
}
|
|
3759
|
+
},
|
|
3760
|
+
defaults: {
|
|
3761
|
+
scope: defaultScope,
|
|
3762
|
+
strict: strictDefault
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
const configDir = path14.dirname(configFullPath);
|
|
3766
|
+
if (!await exists(configDir)) {
|
|
3767
|
+
await ensureDir(configDir);
|
|
3768
|
+
}
|
|
3769
|
+
await writeJsonFile(configFullPath, config);
|
|
3770
|
+
const relConfigPath = path14.relative(projectRoot, configFullPath);
|
|
3771
|
+
const scripts = {
|
|
3772
|
+
"unrag:doctor": `unrag doctor --config ${relConfigPath}`,
|
|
3773
|
+
"unrag:doctor:db": `unrag doctor --config ${relConfigPath} --db`,
|
|
3774
|
+
"unrag:doctor:ci": buildCiScript(relConfigPath, ciIncludeDb, ciStrict)
|
|
3775
|
+
};
|
|
3776
|
+
const pkgPath = path14.join(projectRoot, "package.json");
|
|
3777
|
+
const pkg = await readJsonFile(pkgPath);
|
|
3778
|
+
if (!pkg) {
|
|
3779
|
+
outro3([
|
|
3780
|
+
`Created ${relConfigPath}`,
|
|
3781
|
+
"",
|
|
3782
|
+
"Could not find package.json to add scripts.",
|
|
3783
|
+
"Add these scripts manually:",
|
|
3784
|
+
...Object.entries(scripts).map(([k, v]) => ` "${k}": "${v}"`)
|
|
3785
|
+
].join(`
|
|
3786
|
+
`));
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
const existingScripts = pkg.scripts ?? {};
|
|
3790
|
+
const conflictingScripts = Object.keys(scripts).filter((k) => (k in existingScripts));
|
|
3791
|
+
let scriptsToAdd = scripts;
|
|
3792
|
+
if (conflictingScripts.length > 0 && !nonInteractive) {
|
|
3793
|
+
for (const scriptName of conflictingScripts) {
|
|
3794
|
+
const action = await select2({
|
|
3795
|
+
message: `Script "${scriptName}" already exists. What would you like to do?`,
|
|
3796
|
+
options: [
|
|
3797
|
+
{ value: "keep", label: "Keep existing", hint: existingScripts[scriptName] },
|
|
3798
|
+
{ value: "overwrite", label: "Overwrite", hint: scripts[scriptName] },
|
|
3799
|
+
{
|
|
3800
|
+
value: "rename",
|
|
3801
|
+
label: "Add with different name",
|
|
3802
|
+
hint: `${scriptName}:new`
|
|
3803
|
+
}
|
|
3804
|
+
],
|
|
3805
|
+
initialValue: "keep"
|
|
3806
|
+
});
|
|
3807
|
+
if (isCancel3(action)) {
|
|
3808
|
+
cancel3("Cancelled.");
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
if (action === "keep") {
|
|
3812
|
+
delete scriptsToAdd[scriptName];
|
|
3813
|
+
} else if (action === "rename") {
|
|
3814
|
+
const newName = await text2({
|
|
3815
|
+
message: `New script name for ${scriptName}`,
|
|
3816
|
+
initialValue: `${scriptName}:new`,
|
|
3817
|
+
validate: (v) => {
|
|
3818
|
+
if (!v.trim())
|
|
3819
|
+
return "Script name is required";
|
|
3820
|
+
if (v in existingScripts || v in scriptsToAdd)
|
|
3821
|
+
return "Script name already exists";
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
});
|
|
3825
|
+
if (isCancel3(newName)) {
|
|
3826
|
+
cancel3("Cancelled.");
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
const value = scriptsToAdd[scriptName];
|
|
3830
|
+
delete scriptsToAdd[scriptName];
|
|
3831
|
+
scriptsToAdd[String(newName)] = value;
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
} else if (conflictingScripts.length > 0 && nonInteractive) {
|
|
3835
|
+
for (const scriptName of conflictingScripts) {
|
|
3836
|
+
delete scriptsToAdd[scriptName];
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
if (Object.keys(scriptsToAdd).length > 0) {
|
|
3840
|
+
pkg.scripts = {
|
|
3841
|
+
...existingScripts,
|
|
3842
|
+
...scriptsToAdd
|
|
3843
|
+
};
|
|
3844
|
+
await writeJsonFile(pkgPath, pkg);
|
|
3845
|
+
}
|
|
3846
|
+
const addedScripts = Object.keys(scriptsToAdd);
|
|
3847
|
+
const skippedScripts = conflictingScripts.filter((k) => !(k in scriptsToAdd) && !addedScripts.some((a) => a !== k && scripts[k]));
|
|
3848
|
+
outro3([
|
|
3849
|
+
`Created ${relConfigPath}`,
|
|
3850
|
+
"",
|
|
3851
|
+
addedScripts.length > 0 ? `Added scripts to package.json:
|
|
3852
|
+
${addedScripts.map((k) => ` ${k}: ${scriptsToAdd[k]}`).join(`
|
|
3853
|
+
`)}` : "No new scripts added.",
|
|
3854
|
+
skippedScripts.length > 0 ? `
|
|
3855
|
+
Kept existing scripts: ${skippedScripts.join(", ")}` : "",
|
|
3856
|
+
"",
|
|
3857
|
+
"Usage:",
|
|
3858
|
+
" npm run unrag:doctor # Static checks",
|
|
3859
|
+
" npm run unrag:doctor:db # Include database checks",
|
|
3860
|
+
" npm run unrag:doctor:ci # CI mode (JSON output)",
|
|
3861
|
+
"",
|
|
3862
|
+
"Or run directly:",
|
|
3863
|
+
` unrag doctor --config ${relConfigPath}`
|
|
3864
|
+
].filter(Boolean).join(`
|
|
3865
|
+
`));
|
|
3866
|
+
}
|
|
3867
|
+
function buildCiScript(configPath, includeDb, strict) {
|
|
3868
|
+
const parts = ["unrag doctor", `--config ${configPath}`];
|
|
3869
|
+
if (includeDb)
|
|
3870
|
+
parts.push("--db");
|
|
3871
|
+
if (strict)
|
|
3872
|
+
parts.push("--strict");
|
|
3873
|
+
parts.push("--json");
|
|
3874
|
+
return parts.join(" ");
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// cli/commands/doctor.ts
|
|
3878
|
+
function parseDoctorArgs(args) {
|
|
3879
|
+
const out = {};
|
|
3880
|
+
for (let i = 0;i < args.length; i++) {
|
|
3881
|
+
const a = args[i];
|
|
3882
|
+
if (a === "--db") {
|
|
3883
|
+
out.db = true;
|
|
3884
|
+
continue;
|
|
3885
|
+
}
|
|
3886
|
+
if (a === "--json") {
|
|
3887
|
+
out.json = true;
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
if (a === "--strict") {
|
|
3891
|
+
out.strict = true;
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
if (a === "--config") {
|
|
3895
|
+
const v = args[i + 1];
|
|
3896
|
+
if (v && !v.startsWith("-")) {
|
|
3897
|
+
out.config = v;
|
|
3898
|
+
i++;
|
|
3899
|
+
}
|
|
3900
|
+
continue;
|
|
3901
|
+
}
|
|
3902
|
+
if (a === "--project-root") {
|
|
3903
|
+
const v = args[i + 1];
|
|
3904
|
+
if (v && !v.startsWith("-")) {
|
|
3905
|
+
out.projectRoot = v;
|
|
3906
|
+
i++;
|
|
3907
|
+
}
|
|
3908
|
+
continue;
|
|
3909
|
+
}
|
|
3910
|
+
if (a === "--install-dir" || a === "--dir") {
|
|
3911
|
+
const v = args[i + 1];
|
|
3912
|
+
if (v && !v.startsWith("-")) {
|
|
3913
|
+
out.installDir = v;
|
|
3914
|
+
i++;
|
|
3915
|
+
}
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
if (a === "--schema") {
|
|
3919
|
+
const v = args[i + 1];
|
|
3920
|
+
if (v && !v.startsWith("-")) {
|
|
3921
|
+
out.schema = v;
|
|
3922
|
+
i++;
|
|
3923
|
+
}
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3926
|
+
if (a === "--scope") {
|
|
3927
|
+
const v = args[i + 1];
|
|
3928
|
+
if (v && !v.startsWith("-")) {
|
|
3929
|
+
out.scope = v;
|
|
3930
|
+
i++;
|
|
3931
|
+
}
|
|
3932
|
+
continue;
|
|
3933
|
+
}
|
|
3934
|
+
if (a === "--database-url") {
|
|
3935
|
+
const v = args[i + 1];
|
|
3936
|
+
if (v && !v.startsWith("-")) {
|
|
3937
|
+
out.databaseUrl = v;
|
|
3938
|
+
i++;
|
|
3939
|
+
}
|
|
3940
|
+
continue;
|
|
3941
|
+
}
|
|
3942
|
+
if (a === "--database-url-env") {
|
|
3943
|
+
const v = args[i + 1];
|
|
3944
|
+
if (v && !v.startsWith("-")) {
|
|
3945
|
+
out.databaseUrlEnv = v;
|
|
3946
|
+
i++;
|
|
3947
|
+
}
|
|
3948
|
+
continue;
|
|
3949
|
+
}
|
|
3950
|
+
if (a === "--env-file") {
|
|
3951
|
+
const v = args[i + 1];
|
|
3952
|
+
if (v && !v.startsWith("-")) {
|
|
3953
|
+
out.envFile = v;
|
|
3954
|
+
i++;
|
|
3955
|
+
}
|
|
3956
|
+
continue;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
return out;
|
|
3960
|
+
}
|
|
3961
|
+
function renderDoctorHelp() {
|
|
3962
|
+
return [
|
|
3963
|
+
"unrag doctor — validate your Unrag installation",
|
|
3964
|
+
"",
|
|
3965
|
+
"Usage:",
|
|
3966
|
+
" unrag doctor [options]",
|
|
3967
|
+
" unrag doctor setup [options]",
|
|
3968
|
+
"",
|
|
3969
|
+
"Subcommands:",
|
|
3970
|
+
" setup Generate project-specific doctor config and package.json scripts",
|
|
3971
|
+
"",
|
|
3972
|
+
"Options:",
|
|
3973
|
+
" --config <path> Load settings from a doctor config file",
|
|
3974
|
+
" --db Run database checks (connectivity, pgvector, schema, indexes)",
|
|
3975
|
+
" --json Output results as JSON (for CI)",
|
|
3976
|
+
" --strict Treat warnings as failures",
|
|
3977
|
+
" --project-root <path> Override project root directory",
|
|
3978
|
+
" --install-dir <path> Override Unrag install directory",
|
|
3979
|
+
" --schema <name> Database schema name (default: public)",
|
|
3980
|
+
" --scope <prefix> Limit dimension checks to sourceId prefix",
|
|
3981
|
+
" --database-url <url> Database connection string (overrides env)",
|
|
3982
|
+
" --database-url-env <name> Env var name for database URL",
|
|
3983
|
+
" --env-file <path> Load env vars from a specific .env file (optional)",
|
|
3984
|
+
"",
|
|
3985
|
+
"Static checks (default):",
|
|
3986
|
+
" - Project/install integrity (unrag.json, config, install dir)",
|
|
3987
|
+
" - Environment variables (DATABASE_URL, provider keys)",
|
|
3988
|
+
" - Dependencies (pg, drizzle-orm, etc.)",
|
|
3989
|
+
" - Module presence (extractors, connectors)",
|
|
3990
|
+
" - Config coherence (extractors wired and enabled)",
|
|
3991
|
+
"",
|
|
3992
|
+
"Database checks (--db):",
|
|
3993
|
+
" - PostgreSQL connectivity",
|
|
3994
|
+
" - pgvector extension installed",
|
|
3995
|
+
" - Schema/table validation",
|
|
3996
|
+
" - Index recommendations",
|
|
3997
|
+
" - Embedding dimension consistency",
|
|
3998
|
+
"",
|
|
3999
|
+
"Exit codes:",
|
|
4000
|
+
" 0 All checks passed (warnings allowed unless --strict)",
|
|
4001
|
+
" 1 One or more checks failed",
|
|
4002
|
+
" 2 Doctor could not run (internal error)",
|
|
4003
|
+
"",
|
|
4004
|
+
"Examples:",
|
|
4005
|
+
" unrag doctor Run static checks",
|
|
4006
|
+
" unrag doctor --db Include database checks",
|
|
4007
|
+
" unrag doctor --db --strict Fail on warnings too",
|
|
4008
|
+
" unrag doctor --json Output JSON for CI",
|
|
4009
|
+
" unrag doctor --config .unrag/doctor.json Use project config",
|
|
4010
|
+
" unrag doctor setup Generate config and scripts",
|
|
4011
|
+
"",
|
|
4012
|
+
"Docs:",
|
|
4013
|
+
` ${docsUrl("/docs/reference/cli")}`
|
|
4014
|
+
].join(`
|
|
4015
|
+
`);
|
|
4016
|
+
}
|
|
4017
|
+
async function doctorCommand(args) {
|
|
4018
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4019
|
+
outro4(renderDoctorHelp());
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
if (args[0] === "setup") {
|
|
4023
|
+
await doctorSetupCommand(args.slice(1));
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
const parsed = parseDoctorArgs(args);
|
|
4027
|
+
const s = spinner2();
|
|
4028
|
+
s.start("Running doctor checks...");
|
|
4029
|
+
try {
|
|
4030
|
+
const projectRoot = parsed.projectRoot ?? await tryFindProjectRoot(process.cwd()) ?? process.cwd();
|
|
4031
|
+
let doctorConfig = null;
|
|
4032
|
+
if (parsed.config) {
|
|
4033
|
+
const configPath = resolveConfigPath(projectRoot, parsed.config);
|
|
4034
|
+
doctorConfig = await readDoctorConfig(configPath);
|
|
4035
|
+
if (!doctorConfig) {
|
|
4036
|
+
s.stop("Doctor failed.");
|
|
4037
|
+
outro4(`Error: Could not read config file: ${parsed.config}`);
|
|
4038
|
+
process.exitCode = 2;
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
const mergedArgs = mergeDoctorArgsWithConfig(parsed, doctorConfig, projectRoot);
|
|
4043
|
+
const state = await inferInstallState({
|
|
4044
|
+
projectRootOverride: projectRoot,
|
|
4045
|
+
installDirOverride: mergedArgs.installDir
|
|
4046
|
+
});
|
|
4047
|
+
const envFilesToLoad = getEnvFilesToLoad(doctorConfig, mergedArgs.envFile);
|
|
4048
|
+
await loadEnvFilesFromList({
|
|
4049
|
+
projectRoot: state.projectRoot,
|
|
4050
|
+
files: envFilesToLoad
|
|
4051
|
+
});
|
|
4052
|
+
const staticResults = await runStaticChecks(state);
|
|
4053
|
+
const coherenceResults = await runConfigCoherenceChecks(state);
|
|
4054
|
+
let dbResults = [];
|
|
4055
|
+
if (mergedArgs.db) {
|
|
4056
|
+
dbResults = await runDbChecks(state, {
|
|
4057
|
+
databaseUrl: mergedArgs.databaseUrl,
|
|
4058
|
+
databaseUrlEnv: mergedArgs.databaseUrlEnv ?? doctorConfig?.env?.databaseUrlEnv,
|
|
4059
|
+
schema: mergedArgs.schema ?? doctorConfig?.db?.schema ?? "public",
|
|
4060
|
+
scope: mergedArgs.scope
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
s.stop("Doctor checks complete.");
|
|
4064
|
+
const groups = [
|
|
4065
|
+
{
|
|
4066
|
+
id: "install",
|
|
4067
|
+
title: "Installation",
|
|
4068
|
+
results: staticResults.install
|
|
4069
|
+
},
|
|
4070
|
+
{
|
|
4071
|
+
id: "env",
|
|
4072
|
+
title: "Environment",
|
|
4073
|
+
results: staticResults.env
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
id: "modules",
|
|
4077
|
+
title: "Modules",
|
|
4078
|
+
results: staticResults.modules
|
|
4079
|
+
},
|
|
4080
|
+
{
|
|
4081
|
+
id: "config",
|
|
4082
|
+
title: "Configuration",
|
|
4083
|
+
results: coherenceResults
|
|
4084
|
+
}
|
|
4085
|
+
];
|
|
4086
|
+
if (mergedArgs.db) {
|
|
4087
|
+
groups.push({
|
|
4088
|
+
id: "database",
|
|
4089
|
+
title: "Database",
|
|
4090
|
+
results: dbResults
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
const allResults = groups.flatMap((g) => g.results);
|
|
4094
|
+
const summary = {
|
|
4095
|
+
total: allResults.length,
|
|
4096
|
+
pass: allResults.filter((r) => r.status === "pass").length,
|
|
4097
|
+
warn: allResults.filter((r) => r.status === "warn").length,
|
|
4098
|
+
fail: allResults.filter((r) => r.status === "fail").length,
|
|
4099
|
+
skip: allResults.filter((r) => r.status === "skip").length
|
|
4100
|
+
};
|
|
4101
|
+
const report = { groups, summary };
|
|
4102
|
+
if (mergedArgs.json) {
|
|
4103
|
+
console.log(formatJson(report));
|
|
4104
|
+
} else {
|
|
4105
|
+
outro4(formatReport(report, { showDbHint: !mergedArgs.db }));
|
|
4106
|
+
}
|
|
4107
|
+
const hasFails = summary.fail > 0;
|
|
4108
|
+
const hasWarns = summary.warn > 0;
|
|
4109
|
+
if (hasFails) {
|
|
4110
|
+
process.exitCode = 1;
|
|
4111
|
+
} else if (mergedArgs.strict && hasWarns) {
|
|
4112
|
+
process.exitCode = 1;
|
|
4113
|
+
}
|
|
4114
|
+
} catch (err) {
|
|
4115
|
+
s.stop("Doctor failed.");
|
|
4116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4117
|
+
outro4(`Error: ${message}`);
|
|
4118
|
+
process.exitCode = 2;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
// cli/run.ts
|
|
4123
|
+
function renderHelp() {
|
|
4124
|
+
return [
|
|
4125
|
+
"unrag — vendor-in RAG primitives (ingest/retrieve + adapters) into your repo.",
|
|
4126
|
+
"",
|
|
4127
|
+
"Usage:",
|
|
4128
|
+
" bunx unrag <command> [options]",
|
|
4129
|
+
" npx unrag <command> [options]",
|
|
4130
|
+
"",
|
|
4131
|
+
"Commands:",
|
|
4132
|
+
" init Install core files (config + store adapter templates)",
|
|
4133
|
+
" add <connector> Install a connector (notion, google-drive)",
|
|
4134
|
+
" add extractor <n> Install an extractor (pdf-llm, image-ocr, etc.)",
|
|
4135
|
+
" add battery <name> Install a battery module (reranker, etc.)",
|
|
4136
|
+
" doctor Validate installation and configuration",
|
|
4137
|
+
" doctor setup Generate project-specific doctor config and scripts",
|
|
4138
|
+
" help Show this help",
|
|
4139
|
+
"",
|
|
4140
|
+
"Global options:",
|
|
4141
|
+
" -h, --help Show help",
|
|
4142
|
+
" -y, --yes Non-interactive; accept defaults",
|
|
4143
|
+
"",
|
|
4144
|
+
"init options:",
|
|
4145
|
+
" --store <adapter> drizzle | prisma | raw-sql",
|
|
4146
|
+
" --dir <path> Install directory (alias: --install-dir)",
|
|
4147
|
+
" --alias <@name> Import alias base (e.g. @unrag)",
|
|
4148
|
+
" --preset <id|url> Install from a web-generated preset (non-interactive)",
|
|
4149
|
+
" --overwrite <mode> skip | force (when files already exist)",
|
|
4150
|
+
" --rich-media Enable rich media setup (also enables multimodal embeddings)",
|
|
4151
|
+
" --no-rich-media Disable rich media setup",
|
|
4152
|
+
" --extractors <list> Comma-separated extractors (implies --rich-media)",
|
|
4153
|
+
" --no-install Skip automatic dependency installation",
|
|
4154
|
+
"",
|
|
4155
|
+
"add options:",
|
|
4156
|
+
" --no-install Skip automatic dependency installation",
|
|
4157
|
+
"",
|
|
4158
|
+
"doctor options:",
|
|
4159
|
+
" --config <path> Load settings from a doctor config file",
|
|
4160
|
+
" --db Run database checks (connectivity, schema, indexes)",
|
|
4161
|
+
" --json Output JSON for CI",
|
|
4162
|
+
" --strict Treat warnings as failures",
|
|
4163
|
+
"",
|
|
4164
|
+
"Examples:",
|
|
4165
|
+
" bunx unrag@latest init",
|
|
4166
|
+
" bunx unrag@latest init --yes --store drizzle --dir lib/unrag --alias @unrag",
|
|
4167
|
+
" bunx unrag@latest init --yes --rich-media",
|
|
4168
|
+
" bunx unrag@latest init --yes --extractors pdf-text-layer,file-text",
|
|
4169
|
+
" bunx unrag add notion --yes",
|
|
4170
|
+
" bunx unrag add battery reranker --yes",
|
|
4171
|
+
" bunx unrag doctor",
|
|
4172
|
+
" bunx unrag doctor --db",
|
|
4173
|
+
" bunx unrag doctor setup",
|
|
4174
|
+
" bunx unrag doctor --config .unrag/doctor.json",
|
|
4175
|
+
"",
|
|
4176
|
+
"Docs:",
|
|
4177
|
+
` - Quickstart: ${docsUrl("/docs/getting-started/quickstart")}`,
|
|
4178
|
+
` - CLI: ${docsUrl("/docs/reference/cli")}`,
|
|
4179
|
+
` - Notion: ${docsUrl("/docs/connectors/notion")}`,
|
|
4180
|
+
"",
|
|
4181
|
+
"Repo:",
|
|
4182
|
+
` ${UNRAG_GITHUB_REPO_URL}`,
|
|
4183
|
+
"",
|
|
4184
|
+
"Tip:",
|
|
4185
|
+
" After `init`, open the generated unrag.md for schema + env vars (DATABASE_URL)."
|
|
4186
|
+
].join(`
|
|
4187
|
+
`);
|
|
4188
|
+
}
|
|
4189
|
+
async function run(argv) {
|
|
4190
|
+
const [, , command, ...rest] = argv;
|
|
4191
|
+
intro("unrag");
|
|
4192
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
4193
|
+
outro5(renderHelp());
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
if (command === "init") {
|
|
4197
|
+
await initCommand(rest);
|
|
4198
|
+
return;
|
|
4199
|
+
}
|
|
4200
|
+
if (command === "add") {
|
|
4201
|
+
await addCommand(rest);
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
if (command === "doctor") {
|
|
4205
|
+
await doctorCommand(rest);
|
|
1521
4206
|
return;
|
|
1522
4207
|
}
|
|
1523
|
-
|
|
4208
|
+
outro5([`Unknown command: ${command}`, "", renderHelp()].join(`
|
|
1524
4209
|
`));
|
|
1525
4210
|
process.exitCode = 1;
|
|
1526
4211
|
}
|