opencode-rag-plugin 1.11.0 → 1.12.4

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.
Files changed (44) hide show
  1. package/ReadMe.md +14 -6
  2. package/dist/api.js +21 -82
  3. package/dist/api.js.map +1 -1
  4. package/dist/chunker/image.d.ts +14 -0
  5. package/dist/chunker/image.js +356 -0
  6. package/dist/chunker/image.js.map +1 -0
  7. package/dist/chunker/loader.js +5 -0
  8. package/dist/chunker/loader.js.map +1 -1
  9. package/dist/chunker/xml.js +1 -1
  10. package/dist/chunker/xml.js.map +1 -1
  11. package/dist/cli.js +123 -108
  12. package/dist/cli.js.map +1 -1
  13. package/dist/core/bootstrap.d.ts +18 -0
  14. package/dist/core/bootstrap.js +80 -0
  15. package/dist/core/bootstrap.js.map +1 -0
  16. package/dist/core/config.d.ts +15 -0
  17. package/dist/core/config.js +39 -3
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/interfaces.d.ts +1 -0
  20. package/dist/core/resolve-api-key.js +3 -0
  21. package/dist/core/resolve-api-key.js.map +1 -1
  22. package/dist/core/runtime-overrides.d.ts +5 -0
  23. package/dist/core/runtime-overrides.js +17 -0
  24. package/dist/core/runtime-overrides.js.map +1 -1
  25. package/dist/describer/describer.js +1 -1
  26. package/dist/describer/describer.js.map +1 -1
  27. package/dist/embedder/health.d.ts +25 -0
  28. package/dist/embedder/health.js +346 -0
  29. package/dist/embedder/health.js.map +1 -0
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/indexer.d.ts +1 -1
  34. package/dist/indexer.js +192 -22
  35. package/dist/indexer.js.map +1 -1
  36. package/dist/mcp/server.js +7 -61
  37. package/dist/mcp/server.js.map +1 -1
  38. package/dist/retriever/keyword-index.js +38 -5
  39. package/dist/retriever/keyword-index.js.map +1 -1
  40. package/dist/web/api.d.ts +8 -0
  41. package/dist/web/api.js +81 -0
  42. package/dist/web/api.js.map +1 -1
  43. package/dist/web/ui/index.html +503 -98
  44. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -7,12 +7,10 @@ import { spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import chokidar from "chokidar";
9
9
  import pc from "picocolors";
10
- import { loadConfig, DEFAULT_CONFIG, resolveLogConfig } from "./core/config.js";
11
- import { resolveApiKey } from "./core/resolve-api-key.js";
10
+ import { loadConfig, DEFAULT_CONFIG } from "./core/config.js";
11
+ import { resolveRagContext } from "./core/bootstrap.js";
12
12
  import { appendDebugLog } from "./core/fileLogger.js";
13
- import { loadChunkersFromConfig } from "./chunker/loader.js";
14
- import { createEmbedder } from "./embedder/factory.js";
15
- import { createDescriptionProvider } from "./describer/factory.js";
13
+ import { checkProviderHealth, pullOllamaModels } from "./embedder/health.js";
16
14
  import { retrieve } from "./retriever/retriever.js";
17
15
  import { createWatchPassScheduler, createWatchIgnore, getIndexStatusSummary, runIndexPass, } from "./indexer.js";
18
16
  const c = {
@@ -42,55 +40,16 @@ function logCliInfo(logFilePath, scope, message) {
42
40
  console.log(message);
43
41
  //appendDebugLog(logFilePath, { scope, message });
44
42
  }
45
- async function resolveConfig(opt, logFilePath) {
46
- const worktree = process.cwd();
47
- if (opt.config) {
48
- try {
49
- const configPath = path.resolve(opt.config);
50
- const cfg = loadConfig(configPath);
51
- resolveApiKey(cfg, worktree);
52
- await loadChunkersFromConfig(cfg, path.dirname(configPath));
53
- logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(configPath)}`);
54
- return logConfigDetails(logFilePath, cfg);
55
- }
56
- catch (err) {
57
- logCliError(logFilePath, "config", `Could not load config from ${opt.config}, using defaults`, err);
58
- console.error(c.warn(`Could not load config from ${opt.config}, using defaults`));
59
- }
60
- }
61
- for (const loc of ["opencode-rag.json", ".opencode/opencode-rag.json", ".opencode/rag.json"]) {
62
- const configPath = path.resolve(loc);
63
- try {
64
- const cfg = loadConfig(configPath);
65
- resolveApiKey(cfg, worktree);
66
- await loadChunkersFromConfig(cfg, path.dirname(configPath));
67
- logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(configPath)}`);
68
- return logConfigDetails(logFilePath, cfg);
69
- }
70
- catch (err) {
71
- logCliError(logFilePath, "config", `Failed to load config from ${configPath}`, err);
72
- }
73
- }
74
- logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.dim("using defaults (no opencode-rag.json found)")}`);
75
- return logConfigDetails(logFilePath, DEFAULT_CONFIG);
76
- }
77
- async function loadCliKeywordIndex(storePath, logFilePath) {
78
- const { KeywordIndex } = await import("./retriever/keyword-index.js");
79
- try {
80
- const index = await KeywordIndex.load(storePath);
81
- logCliInfo(logFilePath, "keyword-index", `${c.label("Keyword index loaded")} (${c.num(index.count())} chunks)`);
82
- return index;
83
- }
84
- catch {
85
- logCliInfo(logFilePath, "keyword-index", c.warn("Creating keyword index"));
86
- return new KeywordIndex(storePath);
87
- }
43
+ async function resolveCliContext(opt, logFilePath) {
44
+ const ctx = await resolveRagContext({ configPath: opt.config });
45
+ logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(ctx.logFilePath)}`);
46
+ logConfigDetails(logFilePath, ctx.config);
47
+ return ctx;
88
48
  }
89
49
  function logConfigDetails(logFilePath, config) {
90
50
  logCliInfo(logFilePath, "config", ` ${c.label("Embedding provider:")} ${c.value(config.embedding.provider)}`);
91
51
  logCliInfo(logFilePath, "config", ` ${c.label("Embedding model:")} ${c.value(config.embedding.model)}`);
92
52
  logCliInfo(logFilePath, "config", ` ${c.label("Vector store:")} ${c.file(config.vectorStore.path)}`);
93
- return config;
94
53
  }
95
54
  function formatTimestamp(timestamp) {
96
55
  if (!timestamp)
@@ -369,26 +328,13 @@ program
369
328
  try {
370
329
  const cwd = process.cwd();
371
330
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
372
- const config = await resolveConfig(options, logFilePath);
373
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
331
+ const ctx = await resolveCliContext(options, logFilePath);
332
+ const { config, embedder, store, storePath, keywordIndex, descriptionProvider, dimension } = ctx;
333
+ logFilePath = ctx.logFilePath;
374
334
  logCliInfo(logFilePath, "index", `\n${c.heading("Indexing workspace...")}`);
375
- const embedder = createEmbedder(config);
376
- // Detect actual vector dimension from the model
377
- const probe = await embedder.embed(["dimension-probe"], "query");
378
- let vectorDimension = 384;
379
- if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
380
- vectorDimension = probe[0].length;
381
- }
382
- logCliInfo(logFilePath, "index", ` ${c.label("Vector dimension:")} ${c.num(vectorDimension)}`);
383
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
384
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path), vectorDimension);
385
- const keywordIndex = await loadCliKeywordIndex(path.resolve(cwd, config.vectorStore.path), logFilePath);
386
- // Create description provider (enabled by default)
387
- const descriptionConfig = config.description ?? { enabled: true, provider: "ollama", baseUrl: "http://127.0.0.1:11434/api", model: "qwen2.5:3b", systemPrompt: "" };
388
- const descriptionProvider = descriptionConfig.enabled
389
- ? createDescriptionProvider(descriptionConfig)
390
- : undefined;
335
+ logCliInfo(logFilePath, "index", ` ${c.label("Vector dimension:")} ${c.num(dimension)}`);
391
336
  if (descriptionProvider) {
337
+ const descriptionConfig = config.description ?? { provider: "ollama", model: "qwen2.5:3b" };
392
338
  logCliInfo(logFilePath, "index", ` ${c.label("Description LLM:")} ${c.value(descriptionConfig.model)} (${descriptionConfig.provider})`);
393
339
  }
394
340
  logCliInfo(logFilePath, "index", `${c.label("Scanning:")} ${c.file(cwd)}`);
@@ -396,7 +342,7 @@ program
396
342
  const passStarted = Date.now();
397
343
  const stats = await runIndexPass({
398
344
  cwd,
399
- storePath: path.resolve(cwd, config.vectorStore.path),
345
+ storePath,
400
346
  config,
401
347
  store,
402
348
  embedder,
@@ -427,7 +373,7 @@ program
427
373
  logCliError(logFilePath, "watch", `\nWatch reindex failed: ${message}`, error);
428
374
  }, 300);
429
375
  const watcher = chokidar.watch(cwd, {
430
- ignored: createWatchIgnore(cwd, config, path.resolve(cwd, config.vectorStore.path)),
376
+ ignored: createWatchIgnore(cwd, config, storePath),
431
377
  ignoreInitial: true,
432
378
  persistent: true,
433
379
  });
@@ -477,13 +423,11 @@ program
477
423
  try {
478
424
  const cwd = process.cwd();
479
425
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
480
- const config = await resolveConfig(options, logFilePath);
481
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
426
+ const ctx = await resolveCliContext(options, logFilePath);
427
+ const { config, embedder, store, storePath, keywordIndex } = ctx;
428
+ logFilePath = ctx.logFilePath;
482
429
  logCliInfo(logFilePath, "query", `\n${c.heading("Querying:")} "${query}"`);
483
430
  logCliInfo(logFilePath, "query", `${c.label("Top-K:")} ${c.num(parseInt(options.topK ?? "10", 10))}`);
484
- const embedder = createEmbedder(config);
485
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
486
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
487
431
  const indexedCount = await store.count();
488
432
  if (indexedCount === 0) {
489
433
  logCliInfo(logFilePath, "query", `${c.warn("No indexed chunks found.")} Run 'opencode-rag index' first.`);
@@ -492,7 +436,6 @@ program
492
436
  logCliInfo(logFilePath, "query", `${c.label("Searching")} ${c.num(indexedCount)} indexed chunks...`);
493
437
  const topK = parseInt(options.topK ?? "10", 10);
494
438
  const minScore = config.retrieval.minScore;
495
- const keywordIndex = await loadCliKeywordIndex(path.resolve(cwd, config.vectorStore.path), logFilePath);
496
439
  const hybridCfg = config.retrieval.hybridSearch;
497
440
  const rawResults = await retrieve(query, embedder, store, {
498
441
  topK,
@@ -540,10 +483,9 @@ program
540
483
  try {
541
484
  const cwd = process.cwd();
542
485
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
543
- const config = await resolveConfig(options, logFilePath);
544
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
545
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
546
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
486
+ const ctx = await resolveCliContext(options, logFilePath);
487
+ const { store } = ctx;
488
+ logFilePath = ctx.logFilePath;
547
489
  const prevCount = await store.count();
548
490
  if (prevCount === 0) {
549
491
  logCliInfo(logFilePath, "clear", c.warn("No indexed data to clear."));
@@ -551,7 +493,7 @@ program
551
493
  else {
552
494
  logCliInfo(logFilePath, "clear", `${c.label("Clearing")} ${c.num(prevCount)} indexed chunks...`);
553
495
  }
554
- await store.dropDatabase();
496
+ await store.clear();
555
497
  logCliInfo(logFilePath, "clear", `${c.success("Done.")} vector database directory removed.`);
556
498
  }
557
499
  catch (err) {
@@ -569,14 +511,13 @@ program
569
511
  try {
570
512
  const cwd = process.cwd();
571
513
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
572
- const config = await resolveConfig(options, logFilePath);
573
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
574
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
575
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
514
+ const ctx = await resolveCliContext(options, logFilePath);
515
+ const { config, store, storePath, keywordIndex } = ctx;
516
+ logFilePath = ctx.logFilePath;
576
517
  const count = await store.count();
577
- const summary = await getIndexStatusSummary(cwd, path.resolve(cwd, config.vectorStore.path), config, store);
518
+ const summary = await getIndexStatusSummary(cwd, storePath, config, store);
578
519
  logCliInfo(logFilePath, "status", `\n${c.heading("Indexed chunks:")} ${c.num(count)}`);
579
- logCliInfo(logFilePath, "status", `${c.label("Store path:")} ${c.file(path.resolve(cwd, config.vectorStore.path))}`);
520
+ logCliInfo(logFilePath, "status", `${c.label("Store path:")} ${c.file(storePath)}`);
580
521
  logCliInfo(logFilePath, "status", `${c.label("Embedding provider:")} ${c.value(config.embedding.provider)}`);
581
522
  logCliInfo(logFilePath, "status", `${c.label("Embedding model:")} ${c.value(config.embedding.model)}`);
582
523
  logCliInfo(logFilePath, "status", `${c.label("File extensions:")} ${config.indexing.includeExtensions.join(", ")}`);
@@ -590,7 +531,7 @@ program
590
531
  logCliInfo(logFilePath, "status", `${c.label("Pending files:")} ${c.num(summary.pendingFiles)}`);
591
532
  logCliInfo(logFilePath, "status", `${c.label("Watch mode:")} ${c.dim("off")}`);
592
533
  const kiCount = config.retrieval.hybridSearch?.enabled
593
- ? (await loadCliKeywordIndex(path.resolve(cwd, config.vectorStore.path), logFilePath))?.count() ?? 0
534
+ ? keywordIndex?.count() ?? 0
594
535
  : 0;
595
536
  logCliInfo(logFilePath, "status", `${c.label("Keyword index:")} ${config.retrieval.hybridSearch?.enabled ? c.enabled("enabled") : c.disabled("disabled")} (${c.num(kiCount)} chunks)`);
596
537
  if (summary.rebuildRequired) {
@@ -612,10 +553,9 @@ program
612
553
  try {
613
554
  const cwd = process.cwd();
614
555
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
615
- const config = await resolveConfig(options, logFilePath);
616
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
617
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
618
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
556
+ const ctx = await resolveCliContext(options, logFilePath);
557
+ const { store } = ctx;
558
+ logFilePath = ctx.logFilePath;
619
559
  const files = await store.listFiles();
620
560
  if (files.length === 0) {
621
561
  logCliInfo(logFilePath, "list", `${c.warn("No indexed files found.")} Run 'opencode-rag index' first.`);
@@ -641,10 +581,9 @@ program
641
581
  try {
642
582
  const cwd = process.cwd();
643
583
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
644
- const config = await resolveConfig(options, logFilePath);
645
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
646
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
647
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
584
+ const ctx = await resolveCliContext(options, logFilePath);
585
+ const { store } = ctx;
586
+ logFilePath = ctx.logFilePath;
648
587
  const chunks = await store.getChunksByFilePath(file);
649
588
  if (chunks.length === 0) {
650
589
  logCliInfo(logFilePath, "show", `${c.warn(`No chunks found for '${file}'.`)}`);
@@ -676,10 +615,9 @@ program
676
615
  try {
677
616
  const cwd = process.cwd();
678
617
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
679
- const config = await resolveConfig(options, logFilePath);
680
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
681
- const { LanceDBStore } = await import("./vectorstore/lancedb.js");
682
- const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
618
+ const ctx = await resolveCliContext(options, logFilePath);
619
+ const { store, storePath } = ctx;
620
+ logFilePath = ctx.logFilePath;
683
621
  const total = await store.count();
684
622
  if (total === 0) {
685
623
  logCliInfo(logFilePath, "dump", `${c.warn("No indexed chunks found.")} Run 'opencode-rag index' first.`);
@@ -714,12 +652,13 @@ program
714
652
  try {
715
653
  const cwd = process.cwd();
716
654
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
717
- const config = await resolveConfig(options, logFilePath);
718
- logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
655
+ const ctx = await resolveCliContext(options, logFilePath);
656
+ const { config, storePath } = ctx;
657
+ logFilePath = ctx.logFilePath;
719
658
  const port = parseInt(options.port ?? String(config.ui?.port ?? 3210), 10);
720
659
  const openBrowser = options.open !== false && (config.ui?.openBrowser ?? true);
721
660
  const { startWebUi } = await import("./web/server.js");
722
- const server = await startWebUi(path.resolve(cwd, config.vectorStore.path), port);
661
+ const server = await startWebUi(storePath, port);
723
662
  const url = `http://127.0.0.1:${server.port}`;
724
663
  logCliInfo(logFilePath, "ui", `\n${c.heading("OpenCodeRAG Web UI")}`);
725
664
  logCliInfo(logFilePath, "ui", ` ${c.label("URL:")} ${c.value(url)}`);
@@ -783,8 +722,8 @@ program
783
722
  try {
784
723
  const cwd = process.cwd();
785
724
  const logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
786
- const config = await resolveConfig(options, logFilePath);
787
- const storePath = path.resolve(cwd, config.vectorStore.path);
725
+ const ctx = await resolveCliContext(options, logFilePath);
726
+ const { storePath } = ctx;
788
727
  const { listSessions } = await import("./eval/storage.js");
789
728
  const sessions = listSessions(storePath);
790
729
  if (sessions.length === 0) {
@@ -818,8 +757,8 @@ program
818
757
  try {
819
758
  const cwd = process.cwd();
820
759
  const logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
821
- const config = await resolveConfig(options, logFilePath);
822
- const storePath = path.resolve(cwd, config.vectorStore.path);
760
+ const ctx = await resolveCliContext(options, logFilePath);
761
+ const { storePath } = ctx;
823
762
  const { analyzeTokenUsage } = await import("./eval/token-analysis.js");
824
763
  const analysis = analyzeTokenUsage(storePath, sessionID);
825
764
  if (analysis.queryCount === 0) {
@@ -877,8 +816,8 @@ program
877
816
  try {
878
817
  const cwd = process.cwd();
879
818
  const logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
880
- const config = await resolveConfig(options, logFilePath);
881
- const storePath = path.resolve(cwd, config.vectorStore.path);
819
+ const ctx = await resolveCliContext(options, logFilePath);
820
+ const { storePath } = ctx;
882
821
  const { analyzeTokenUsage, compareTokenAnalyses, formatTokenReport } = await import("./eval/token-analysis.js");
883
822
  const a = analyzeTokenUsage(storePath, sessionA);
884
823
  const b = analyzeTokenUsage(storePath, sessionB);
@@ -940,6 +879,24 @@ function generateDefaultConfigJson() {
940
879
  contentType: DEFAULT_CONFIG.openCode.autoInject.contentType,
941
880
  },
942
881
  },
882
+ imageDescription: {
883
+ enabled: DEFAULT_CONFIG.imageDescription.enabled,
884
+ provider: DEFAULT_CONFIG.imageDescription.provider,
885
+ model: DEFAULT_CONFIG.imageDescription.model,
886
+ baseUrl: DEFAULT_CONFIG.imageDescription.baseUrl,
887
+ timeoutMs: DEFAULT_CONFIG.imageDescription.timeoutMs,
888
+ think: DEFAULT_CONFIG.imageDescription.think,
889
+ numCtx: DEFAULT_CONFIG.imageDescription.numCtx,
890
+ },
891
+ description: {
892
+ enabled: DEFAULT_CONFIG.description.enabled,
893
+ provider: DEFAULT_CONFIG.description.provider,
894
+ baseUrl: DEFAULT_CONFIG.description.baseUrl,
895
+ model: DEFAULT_CONFIG.description.model,
896
+ think: DEFAULT_CONFIG.description.think,
897
+ numCtx: DEFAULT_CONFIG.description.numCtx,
898
+ timeoutMs: DEFAULT_CONFIG.description.timeoutMs,
899
+ },
943
900
  mcp: {
944
901
  enabled: DEFAULT_CONFIG.mcp.enabled,
945
902
  },
@@ -957,6 +914,7 @@ program
957
914
  .description("Configure the current workspace for OpenCodeRAG")
958
915
  .option("-f, --force", "overwrite existing files")
959
916
  .option("--skip-install", "skip installing workspace-local plugin dependencies")
917
+ .option("--skip-health-check", "skip provider connectivity and model availability check")
960
918
  .action(async (options) => {
961
919
  const cwd = process.cwd();
962
920
  const packageMetadata = getPackageMetadata();
@@ -1086,6 +1044,63 @@ program
1086
1044
  else {
1087
1045
  console.log(` ${c.exists("Exists:")} opencode-rag.json`);
1088
1046
  }
1047
+ // ── Provider health check ──────────────────────────────────
1048
+ if (!options.skipHealthCheck) {
1049
+ console.log(`\n${c.heading("Checking provider connectivity...")}\n`);
1050
+ const logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
1051
+ const ragConfig = loadConfig(configPath);
1052
+ const results = await checkProviderHealth(ragConfig);
1053
+ for (const r of results) {
1054
+ const icon = r.status === "ok" ? c.success("✓") : r.status === "missing" ? c.warn("○") : c.error("✗");
1055
+ const typeLabel = r.type === "image_description" ? "image description" : r.type;
1056
+ const label = `${typeLabel} model`;
1057
+ console.log(` ${icon} ${c.value(r.model)} (${r.provider}) — ${label}: ${r.status}`);
1058
+ if (r.error)
1059
+ console.log(` ${c.dim(r.error)}`);
1060
+ }
1061
+ const missingOllama = results.filter((r) => r.status === "missing" && r.provider === "ollama");
1062
+ if (missingOllama.length > 0) {
1063
+ const pullEntries = missingOllama.map((r) => {
1064
+ if (r.type === "embedding") {
1065
+ return { model: r.model, baseUrl: ragConfig.embedding.baseUrl, proxy: ragConfig.embedding.proxy };
1066
+ }
1067
+ if (r.type === "description" && ragConfig.description) {
1068
+ return { model: r.model, baseUrl: ragConfig.description.baseUrl, proxy: ragConfig.description.proxy };
1069
+ }
1070
+ if (r.type === "image_description" && ragConfig.imageDescription) {
1071
+ return { model: r.model, baseUrl: ragConfig.imageDescription.baseUrl, proxy: ragConfig.imageDescription.proxy };
1072
+ }
1073
+ return { model: r.model, baseUrl: ragConfig.embedding.baseUrl, proxy: ragConfig.embedding.proxy };
1074
+ });
1075
+ console.log(`\n ${c.warn("Models not found:")} ${pullEntries.map((e) => e.model).join(", ")}`);
1076
+ const readline = await import("node:readline");
1077
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1078
+ const answer = await new Promise((resolve) => {
1079
+ rl.question(` Pull ${pullEntries.length === 1 ? "this model" : "these models"} now? (y/n) `, resolve);
1080
+ });
1081
+ rl.close();
1082
+ if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
1083
+ console.log();
1084
+ try {
1085
+ await pullOllamaModels(pullEntries, (model, line) => {
1086
+ console.log(` ${c.value(model)}: ${line}`);
1087
+ });
1088
+ console.log(`\n ${c.success("Models pulled successfully.")}`);
1089
+ }
1090
+ catch (err) {
1091
+ console.error(`\n ${c.error("Pull failed:")} ${err.message}`);
1092
+ console.log(` ${c.dim("Pull manually with: ollama pull <model>")}`);
1093
+ }
1094
+ }
1095
+ else {
1096
+ console.log(` ${c.dim("Skipped. Pull manually with: ollama pull <model>")}`);
1097
+ }
1098
+ }
1099
+ const hasErrors = results.some((r) => r.status === "error");
1100
+ if (hasErrors) {
1101
+ console.log(`\n ${c.warn("Some providers are not reachable.")} Check configuration and network, then run ${c.file("'opencode-rag index'")}.`);
1102
+ }
1103
+ }
1089
1104
  if (!options.skipInstall) {
1090
1105
  console.log(`\n${c.heading("Installing workspace-local plugin dependencies...")}\n`);
1091
1106
  installWorkspaceDependencies(opencodeDir);