opencode-rag-plugin 1.3.6 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/ReadMe.md +51 -1
  2. package/dist/cli.d.ts +0 -5
  3. package/dist/cli.js +250 -103
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/config.d.ts +18 -1
  6. package/dist/core/config.js +16 -0
  7. package/dist/core/config.js.map +1 -1
  8. package/dist/core/interfaces.d.ts +6 -1
  9. package/dist/core/manifest.d.ts +2 -0
  10. package/dist/core/manifest.js +5 -2
  11. package/dist/core/manifest.js.map +1 -1
  12. package/dist/core/provider-defaults.d.ts +9 -0
  13. package/dist/core/provider-defaults.js +84 -0
  14. package/dist/core/provider-defaults.js.map +1 -0
  15. package/dist/core/resolve-api-key.d.ts +2 -0
  16. package/dist/core/resolve-api-key.js +71 -0
  17. package/dist/core/resolve-api-key.js.map +1 -0
  18. package/dist/core/runtime-overrides.d.ts +36 -0
  19. package/dist/core/runtime-overrides.js +106 -0
  20. package/dist/core/runtime-overrides.js.map +1 -0
  21. package/dist/describer/anthropic.d.ts +10 -0
  22. package/dist/describer/anthropic.js +167 -0
  23. package/dist/describer/anthropic.js.map +1 -0
  24. package/dist/describer/describer.d.ts +12 -0
  25. package/dist/describer/describer.js +177 -0
  26. package/dist/describer/describer.js.map +1 -0
  27. package/dist/describer/factory.d.ts +3 -0
  28. package/dist/describer/factory.js +19 -0
  29. package/dist/describer/factory.js.map +1 -0
  30. package/dist/describer/gemini.d.ts +10 -0
  31. package/dist/describer/gemini.js +177 -0
  32. package/dist/describer/gemini.js.map +1 -0
  33. package/dist/embedder/cohere.d.ts +12 -0
  34. package/dist/embedder/cohere.js +35 -0
  35. package/dist/embedder/cohere.js.map +1 -0
  36. package/dist/embedder/factory.d.ts +1 -1
  37. package/dist/embedder/factory.js +19 -12
  38. package/dist/embedder/factory.js.map +1 -1
  39. package/dist/embedder/ollama.d.ts +1 -1
  40. package/dist/embedder/ollama.js +1 -1
  41. package/dist/embedder/ollama.js.map +1 -1
  42. package/dist/embedder/openai.d.ts +8 -1
  43. package/dist/embedder/openai.js +30 -2
  44. package/dist/embedder/openai.js.map +1 -1
  45. package/dist/index.d.ts +3 -2
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/indexer.d.ts +2 -1
  49. package/dist/indexer.js +38 -1
  50. package/dist/indexer.js.map +1 -1
  51. package/dist/opencode/create-read-tool.js +2 -2
  52. package/dist/opencode/create-read-tool.js.map +1 -1
  53. package/dist/plugin.d.ts +2 -1
  54. package/dist/plugin.js +67 -18
  55. package/dist/plugin.js.map +1 -1
  56. package/dist/retriever/retriever.d.ts +1 -0
  57. package/dist/retriever/retriever.js +2 -1
  58. package/dist/retriever/retriever.js.map +1 -1
  59. package/dist/tui.js +474 -11
  60. package/dist/tui.js.map +1 -1
  61. package/dist/vectorstore/lancedb.d.ts +19 -0
  62. package/dist/vectorstore/lancedb.js +162 -41
  63. package/dist/vectorstore/lancedb.js.map +1 -1
  64. package/dist/watcher.d.ts +6 -1
  65. package/dist/watcher.js +72 -26
  66. package/dist/watcher.js.map +1 -1
  67. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -6,13 +6,35 @@ import os from "node:os";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import chokidar from "chokidar";
9
+ import pc from "picocolors";
9
10
  import { loadConfig, DEFAULT_CONFIG, resolveLogConfig } from "./core/config.js";
11
+ import { resolveApiKey } from "./core/resolve-api-key.js";
10
12
  import { loadChunkersFromConfig } from "./chunker/loader.js";
11
13
  import { createEmbedder } from "./embedder/factory.js";
14
+ import { createDescriptionProvider } from "./describer/factory.js";
12
15
  import { retrieve } from "./retriever/retriever.js";
13
16
  import { createWatchPassScheduler, createWatchIgnore, getIndexStatusSummary, runIndexPass, } from "./indexer.js";
17
+ const c = {
18
+ heading: (s) => pc.bold(pc.cyan(s)),
19
+ label: (s) => pc.dim(s),
20
+ dim: (s) => pc.dim(s),
21
+ value: (s) => pc.green(s),
22
+ num: (s) => pc.green(String(s)),
23
+ file: (s) => pc.yellow(s),
24
+ lang: (s) => pc.cyan(s),
25
+ score: (s) => pc.magenta(s),
26
+ desc: (s) => pc.dim(s),
27
+ success: (s) => pc.green(s),
28
+ warn: (s) => pc.yellow(s),
29
+ error: (s) => pc.red(s),
30
+ enabled: (s) => pc.green(s),
31
+ disabled: (s) => pc.yellow(s),
32
+ created: (s) => pc.green(s),
33
+ updated: (s) => pc.yellow(s),
34
+ exists: (s) => pc.dim(s),
35
+ };
14
36
  function logCliError(logFilePath, scope, message, error) {
15
- console.error(message);
37
+ console.error(c.error(message));
16
38
  //appendDebugLog(logFilePath, { scope, message, error });
17
39
  }
18
40
  function logCliInfo(logFilePath, scope, message) {
@@ -20,50 +42,53 @@ function logCliInfo(logFilePath, scope, message) {
20
42
  //appendDebugLog(logFilePath, { scope, message });
21
43
  }
22
44
  async function resolveConfig(opt, logFilePath) {
45
+ const worktree = process.cwd();
23
46
  if (opt.config) {
24
47
  try {
25
48
  const configPath = path.resolve(opt.config);
26
49
  const cfg = loadConfig(configPath);
50
+ resolveApiKey(cfg, worktree);
27
51
  await loadChunkersFromConfig(cfg, path.dirname(configPath));
28
- logCliInfo(logFilePath, "config", `Config: ${configPath}`);
52
+ logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(configPath)}`);
29
53
  return logConfigDetails(logFilePath, cfg);
30
54
  }
31
55
  catch (err) {
32
56
  logCliError(logFilePath, "config", `Could not load config from ${opt.config}, using defaults`, err);
33
- console.error(`Could not load config from ${opt.config}, using defaults`);
57
+ console.error(c.warn(`Could not load config from ${opt.config}, using defaults`));
34
58
  }
35
59
  }
36
60
  for (const loc of ["opencode-rag.json", ".opencode/opencode-rag.json", ".opencode/rag.json"]) {
37
61
  const configPath = path.resolve(loc);
38
62
  try {
39
63
  const cfg = loadConfig(configPath);
64
+ resolveApiKey(cfg, worktree);
40
65
  await loadChunkersFromConfig(cfg, path.dirname(configPath));
41
- logCliInfo(logFilePath, "config", `Config: ${configPath}`);
66
+ logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(configPath)}`);
42
67
  return logConfigDetails(logFilePath, cfg);
43
68
  }
44
69
  catch (err) {
45
70
  logCliError(logFilePath, "config", `Failed to load config from ${configPath}`, err);
46
71
  }
47
72
  }
48
- logCliInfo(logFilePath, "config", `Config: using defaults (no opencode-rag.json found)`);
73
+ logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.dim("using defaults (no opencode-rag.json found)")}`);
49
74
  return logConfigDetails(logFilePath, DEFAULT_CONFIG);
50
75
  }
51
76
  async function loadCliKeywordIndex(storePath, logFilePath) {
52
77
  const { KeywordIndex } = await import("./retriever/keyword-index.js");
53
78
  try {
54
79
  const index = await KeywordIndex.load(storePath);
55
- logCliInfo(logFilePath, "keyword-index", `Keyword index loaded (${index.count()} chunks)`);
80
+ logCliInfo(logFilePath, "keyword-index", `${c.label("Keyword index loaded")} (${c.num(index.count())} chunks)`);
56
81
  return index;
57
82
  }
58
83
  catch {
59
- logCliInfo(logFilePath, "keyword-index", "Creating keyword index");
84
+ logCliInfo(logFilePath, "keyword-index", c.warn("Creating keyword index"));
60
85
  return new KeywordIndex(storePath);
61
86
  }
62
87
  }
63
88
  function logConfigDetails(logFilePath, config) {
64
- logCliInfo(logFilePath, "config", ` Embedding provider: ${config.embedding.provider}`);
65
- logCliInfo(logFilePath, "config", ` Embedding model: ${config.embedding.model}`);
66
- logCliInfo(logFilePath, "config", ` Vector store: ${config.vectorStore.path}`);
89
+ logCliInfo(logFilePath, "config", ` ${c.label("Embedding provider:")} ${c.value(config.embedding.provider)}`);
90
+ logCliInfo(logFilePath, "config", ` ${c.label("Embedding model:")} ${c.value(config.embedding.model)}`);
91
+ logCliInfo(logFilePath, "config", ` ${c.label("Vector store:")} ${c.file(config.vectorStore.path)}`);
67
92
  return config;
68
93
  }
69
94
  function formatTimestamp(timestamp) {
@@ -72,14 +97,14 @@ function formatTimestamp(timestamp) {
72
97
  return new Date(timestamp).toLocaleString();
73
98
  }
74
99
  function logIndexSummary(logFilePath, stats) {
75
- logCliInfo(logFilePath, "index", ` New: ${stats.newFiles}`);
76
- logCliInfo(logFilePath, "index", ` Modified: ${stats.modifiedFiles}`);
77
- logCliInfo(logFilePath, "index", ` Unchanged: ${stats.unchangedFiles}`);
78
- logCliInfo(logFilePath, "index", ` Deleted: ${stats.deletedFiles}`);
79
- logCliInfo(logFilePath, "index", ` Removed: ${stats.removedFiles}`);
80
- logCliInfo(logFilePath, "index", ` Empty skipped: ${stats.skippedEmptyFiles}`);
81
- logCliInfo(logFilePath, "index", ` Small skipped: ${stats.skippedSmallFiles}`);
82
- logCliInfo(logFilePath, "index", ` Chunks written: ${stats.totalChunks}`);
100
+ logCliInfo(logFilePath, "index", ` ${c.label("New:")} ${c.num(stats.newFiles)}`);
101
+ logCliInfo(logFilePath, "index", ` ${c.label("Modified:")} ${c.num(stats.modifiedFiles)}`);
102
+ logCliInfo(logFilePath, "index", ` ${c.label("Unchanged:")} ${c.num(stats.unchangedFiles)}`);
103
+ logCliInfo(logFilePath, "index", ` ${c.label("Deleted:")} ${c.num(stats.deletedFiles)}`);
104
+ logCliInfo(logFilePath, "index", ` ${c.label("Removed:")} ${c.num(stats.removedFiles)}`);
105
+ logCliInfo(logFilePath, "index", ` ${c.label("Empty skipped:")} ${c.num(stats.skippedEmptyFiles)}`);
106
+ logCliInfo(logFilePath, "index", ` ${c.label("Small skipped:")} ${c.num(stats.skippedSmallFiles)}`);
107
+ logCliInfo(logFilePath, "index", ` ${c.label("Chunks written:")} ${c.num(stats.totalChunks)}`);
83
108
  }
84
109
  function formatDuration(ms) {
85
110
  const seconds = (ms / 1000).toFixed(1);
@@ -251,7 +276,7 @@ function installWorkspaceDependencies(opencodeDir) {
251
276
  let lastError;
252
277
  for (const attempt of attempts) {
253
278
  if (attempt.retry) {
254
- console.log(" Retrying dependency install without native module compilation...");
279
+ console.log(c.warn(" Retrying dependency install without native module compilation..."));
255
280
  }
256
281
  const result = process.platform === "win32"
257
282
  ? spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `npm ${attempt.args.map(quoteForCmd).join(" ")}`], {
@@ -289,19 +314,27 @@ program
289
314
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
290
315
  const config = await resolveConfig(options, logFilePath);
291
316
  logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
292
- logCliInfo(logFilePath, "index", "\nIndexing workspace...");
317
+ logCliInfo(logFilePath, "index", `\n${c.heading("Indexing workspace...")}`);
293
318
  const embedder = createEmbedder(config);
294
319
  // Detect actual vector dimension from the model
295
- const probe = await embedder.embed(["dimension-probe"]);
320
+ const probe = await embedder.embed(["dimension-probe"], "query");
296
321
  let vectorDimension = 384;
297
322
  if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
298
323
  vectorDimension = probe[0].length;
299
324
  }
300
- logCliInfo(logFilePath, "index", ` Vector dimension: ${vectorDimension}`);
325
+ logCliInfo(logFilePath, "index", ` ${c.label("Vector dimension:")} ${c.num(vectorDimension)}`);
301
326
  const { LanceDBStore } = await import("./vectorstore/lancedb.js");
302
327
  const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path), vectorDimension);
303
328
  const keywordIndex = await loadCliKeywordIndex(path.resolve(cwd, config.vectorStore.path), logFilePath);
304
- logCliInfo(logFilePath, "index", `Scanning: ${cwd}`);
329
+ // Create description provider (enabled by default)
330
+ const descriptionConfig = config.description ?? { enabled: true, provider: "ollama", baseUrl: "http://127.0.0.1:11434/api", model: "qwen2.5:3b", systemPrompt: "" };
331
+ const descriptionProvider = descriptionConfig.enabled
332
+ ? createDescriptionProvider(descriptionConfig)
333
+ : undefined;
334
+ if (descriptionProvider) {
335
+ logCliInfo(logFilePath, "index", ` ${c.label("Description LLM:")} ${c.value(descriptionConfig.model)} (${descriptionConfig.provider})`);
336
+ }
337
+ logCliInfo(logFilePath, "index", `${c.label("Scanning:")} ${c.file(cwd)}`);
305
338
  const runPass = async (watchTriggered = false) => {
306
339
  const passStarted = Date.now();
307
340
  const stats = await runIndexPass({
@@ -311,6 +344,7 @@ program
311
344
  store,
312
345
  embedder,
313
346
  keywordIndex,
347
+ descriptionProvider,
314
348
  force: Boolean(options.force && !watchTriggered),
315
349
  logger: {
316
350
  info: (message) => logCliInfo(logFilePath, "index", message),
@@ -318,17 +352,16 @@ program
318
352
  },
319
353
  });
320
354
  logIndexSummary(logFilePath, stats);
321
- logCliInfo(logFilePath, "index", `\nIndexing complete. ${stats.finalCount} chunks stored (${formatDuration(Date.now() - passStarted)}).`);
355
+ logCliInfo(logFilePath, "index", `\n${c.success("Indexing complete.")} ${c.num(stats.finalCount)} chunks stored (${formatDuration(Date.now() - passStarted)}).`);
322
356
  };
323
357
  await runPass(false);
324
358
  if (!options.watch) {
325
359
  return;
326
360
  }
327
- logCliInfo(logFilePath, "index", "\nWatching for changes...");
361
+ logCliInfo(logFilePath, "index", `\n${c.heading("Watching for changes...")}`);
328
362
  const scheduler = createWatchPassScheduler(() => runPass(true), (error) => {
329
363
  const message = error.message || String(error);
330
- logCliError(logFilePath, "watch", `Watch reindex failed: ${message}`, error);
331
- console.error(`\nWatch reindex failed: ${message}`);
364
+ logCliError(logFilePath, "watch", `\nWatch reindex failed: ${message}`, error);
332
365
  }, 300);
333
366
  const watcher = chokidar.watch(cwd, {
334
367
  ignored: createWatchIgnore(cwd, config, path.resolve(cwd, config.vectorStore.path)),
@@ -343,7 +376,7 @@ program
343
376
  watcher.on("addDir", handleChange);
344
377
  watcher.on("error", (error) => {
345
378
  logCliError(logFilePath, "watch", `Watcher error: ${error.message}`, error);
346
- console.error(`\nWatcher error: ${error.message}`);
379
+ console.error(c.error(`\nWatcher error: ${error.message}`));
347
380
  });
348
381
  const shutdown = async () => {
349
382
  scheduler.close();
@@ -353,15 +386,14 @@ program
353
386
  process.once("SIGINT", () => void shutdown());
354
387
  process.once("SIGTERM", () => void shutdown());
355
388
  const duration = formatDuration(Date.now() - started);
356
- logCliInfo(logFilePath, "index", `Watcher ready (${duration} startup). Press Ctrl+C to stop.`);
389
+ logCliInfo(logFilePath, "index", `${c.success("Watcher ready")} (${duration} startup). Press Ctrl+C to stop.`);
357
390
  }
358
391
  catch (err) {
359
392
  const message = err.message || String(err);
360
393
  const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
361
- logCliError(logFilePath, "index", `Indexing failed: ${message}`, err);
362
- console.error(`\nIndexing failed: ${message}`);
394
+ logCliError(logFilePath, "index", `\nIndexing failed: ${message}`, err);
363
395
  if (message.toLowerCase().includes("fetch") || message.toLowerCase().includes("econnrefused")) {
364
- console.error("Hint: Is your embedding provider running?");
396
+ console.error(c.warn("Hint: Is your embedding provider running?"));
365
397
  }
366
398
  process.exit(1);
367
399
  }
@@ -379,41 +411,41 @@ program
379
411
  let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
380
412
  const config = await resolveConfig(options, logFilePath);
381
413
  logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
382
- logCliInfo(logFilePath, "query", `\nQuerying: "${query}"`);
383
- logCliInfo(logFilePath, "query", `Top-K: ${parseInt(options.topK ?? "10", 10)}`);
414
+ logCliInfo(logFilePath, "query", `\n${c.heading("Querying:")} "${query}"`);
415
+ logCliInfo(logFilePath, "query", `${c.label("Top-K:")} ${c.num(parseInt(options.topK ?? "10", 10))}`);
384
416
  const embedder = createEmbedder(config);
385
417
  const { LanceDBStore } = await import("./vectorstore/lancedb.js");
386
418
  const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
387
419
  const indexedCount = await store.count();
388
420
  if (indexedCount === 0) {
389
- logCliInfo(logFilePath, "query", "No indexed chunks found. Run 'opencode-rag index' first.");
421
+ logCliInfo(logFilePath, "query", `${c.warn("No indexed chunks found.")} Run 'opencode-rag index' first.`);
390
422
  return;
391
423
  }
392
- logCliInfo(logFilePath, "query", `Searching ${indexedCount} indexed chunks...`);
424
+ logCliInfo(logFilePath, "query", `${c.label("Searching")} ${c.num(indexedCount)} indexed chunks...`);
393
425
  const topK = parseInt(options.topK ?? "10", 10);
394
426
  const minScore = config.retrieval.minScore;
395
427
  const keywordIndex = await loadCliKeywordIndex(path.resolve(cwd, config.vectorStore.path), logFilePath);
396
428
  const hybridCfg = config.retrieval.hybridSearch;
397
- const results = await retrieve(query, embedder, store, { topK, minScore, keywordIndex, keywordWeight: hybridCfg?.keywordWeight });
429
+ const rawResults = await retrieve(query, embedder, store, { topK, minScore, keywordIndex, keywordWeight: hybridCfg?.keywordWeight, queryPrefix: config.embedding.queryPrefix });
430
+ const results = dedupeResults(rawResults);
398
431
  if (results.length === 0) {
399
- logCliInfo(logFilePath, "query", "No results found.");
432
+ logCliInfo(logFilePath, "query", c.warn("No results found."));
400
433
  return;
401
434
  }
402
435
  const duration = formatDuration(Date.now() - started);
403
- logCliInfo(logFilePath, "query", `\n${results.length} result(s) in ${duration}:\n`);
436
+ logCliInfo(logFilePath, "query", `\n${c.num(results.length)} result(s) in ${duration}:\n`);
404
437
  for (const r of results) {
405
- logCliInfo(logFilePath, "query", ` ${r.chunk.metadata.filePath}:${r.chunk.metadata.startLine}-${r.chunk.metadata.endLine}`);
406
- logCliInfo(logFilePath, "query", ` Score: ${r.score.toFixed(4)}`);
407
- logCliInfo(logFilePath, "query", ` ${r.chunk.content.slice(0, 200).replace(/\n/g, "\n ")}`);
438
+ logCliInfo(logFilePath, "query", ` ${c.file(r.chunk.metadata.filePath)}:${c.value(String(r.chunk.metadata.startLine))}-${c.value(String(r.chunk.metadata.endLine))}`);
439
+ logCliInfo(logFilePath, "query", ` ${c.label("Score:")} ${c.score(r.score.toFixed(4))}`);
440
+ logCliInfo(logFilePath, "query", ` ${pc.dim(r.chunk.content.slice(0, 200).replace(/\n/g, "\n "))}`);
408
441
  }
409
442
  }
410
443
  catch (err) {
411
444
  const message = err.message || String(err);
412
445
  const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
413
- logCliError(logFilePath, "query", `Query failed: ${message}`, err);
414
- console.error(`\nQuery failed: ${message}`);
446
+ logCliError(logFilePath, "query", `\nQuery failed: ${message}`, err);
415
447
  if (message.toLowerCase().includes("fetch") || message.toLowerCase().includes("econnrefused")) {
416
- console.error("Hint: Is your embedding provider running?");
448
+ console.error(c.warn("Hint: Is your embedding provider running?"));
417
449
  }
418
450
  process.exit(1);
419
451
  }
@@ -432,20 +464,18 @@ program
432
464
  const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
433
465
  const prevCount = await store.count();
434
466
  if (prevCount === 0) {
435
- logCliInfo(logFilePath, "clear", "No indexed data to clear.");
436
- return;
467
+ logCliInfo(logFilePath, "clear", c.warn("No indexed data to clear."));
468
+ }
469
+ else {
470
+ logCliInfo(logFilePath, "clear", `${c.label("Clearing")} ${c.num(prevCount)} indexed chunks...`);
437
471
  }
438
- logCliInfo(logFilePath, "clear", `Clearing ${prevCount} indexed chunks...`);
439
- await store.clear();
440
- const { KeywordIndex } = await import("./retriever/keyword-index.js");
441
- await KeywordIndex.clearFile(path.resolve(cwd, config.vectorStore.path));
442
- logCliInfo(logFilePath, "clear", `Done. ${prevCount} chunks removed, keyword index cleared.`);
472
+ await store.dropDatabase();
473
+ logCliInfo(logFilePath, "clear", `${c.success("Done.")} vector database directory removed.`);
443
474
  }
444
475
  catch (err) {
445
476
  const message = err.message || String(err);
446
477
  const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
447
- logCliError(logFilePath, "clear", `Clear failed: ${message}`, err);
448
- console.error(`\nClear failed: ${message}`);
478
+ logCliError(logFilePath, "clear", `\nClear failed: ${message}`, err);
449
479
  process.exit(1);
450
480
  }
451
481
  });
@@ -463,33 +493,132 @@ program
463
493
  const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
464
494
  const count = await store.count();
465
495
  const summary = await getIndexStatusSummary(cwd, path.resolve(cwd, config.vectorStore.path), config, store);
466
- logCliInfo(logFilePath, "status", `\nIndexed chunks: ${count}`);
467
- logCliInfo(logFilePath, "status", `Store path: ${path.resolve(cwd, config.vectorStore.path)}`);
468
- logCliInfo(logFilePath, "status", `Embedding provider: ${config.embedding.provider}`);
469
- logCliInfo(logFilePath, "status", `Embedding model: ${config.embedding.model}`);
470
- logCliInfo(logFilePath, "status", `File extensions: ${config.indexing.includeExtensions.join(", ")}`);
471
- logCliInfo(logFilePath, "status", `Excluded dirs: ${config.indexing.excludeDirs.join(", ")}`);
472
- logCliInfo(logFilePath, "status", `Default top-K: ${config.retrieval.topK}`);
473
- logCliInfo(logFilePath, "status", `Plugin enabled: ${config.openCode.enabled}`);
474
- logCliInfo(logFilePath, "status", `Manifest status: ${summary.manifestStatus}`);
475
- logCliInfo(logFilePath, "status", `Manifest entries: ${summary.manifestEntries}`);
476
- logCliInfo(logFilePath, "status", `Last indexed: ${formatTimestamp(summary.lastIndexedAt)}`);
477
- logCliInfo(logFilePath, "status", `Up-to-date files: ${summary.upToDateFiles}`);
478
- logCliInfo(logFilePath, "status", `Pending files: ${summary.pendingFiles}`);
479
- logCliInfo(logFilePath, "status", `Watch mode: off`);
496
+ logCliInfo(logFilePath, "status", `\n${c.heading("Indexed chunks:")} ${c.num(count)}`);
497
+ logCliInfo(logFilePath, "status", `${c.label("Store path:")} ${c.file(path.resolve(cwd, config.vectorStore.path))}`);
498
+ logCliInfo(logFilePath, "status", `${c.label("Embedding provider:")} ${c.value(config.embedding.provider)}`);
499
+ logCliInfo(logFilePath, "status", `${c.label("Embedding model:")} ${c.value(config.embedding.model)}`);
500
+ logCliInfo(logFilePath, "status", `${c.label("File extensions:")} ${config.indexing.includeExtensions.join(", ")}`);
501
+ logCliInfo(logFilePath, "status", `${c.label("Excluded dirs:")} ${config.indexing.excludeDirs.join(", ")}`);
502
+ logCliInfo(logFilePath, "status", `${c.label("Default top-K:")} ${c.num(config.retrieval.topK)}`);
503
+ logCliInfo(logFilePath, "status", `${c.label("Plugin enabled:")} ${config.openCode.enabled ? c.enabled("yes") : c.disabled("no")}`);
504
+ logCliInfo(logFilePath, "status", `${c.label("Manifest status:")} ${summary.manifestStatus}`);
505
+ logCliInfo(logFilePath, "status", `${c.label("Manifest entries:")} ${c.num(summary.manifestEntries)}`);
506
+ logCliInfo(logFilePath, "status", `${c.label("Last indexed:")} ${c.value(formatTimestamp(summary.lastIndexedAt))}`);
507
+ logCliInfo(logFilePath, "status", `${c.label("Up-to-date files:")} ${c.num(summary.upToDateFiles)}`);
508
+ logCliInfo(logFilePath, "status", `${c.label("Pending files:")} ${c.num(summary.pendingFiles)}`);
509
+ logCliInfo(logFilePath, "status", `${c.label("Watch mode:")} ${c.dim("off")}`);
480
510
  const kiCount = config.retrieval.hybridSearch?.enabled
481
511
  ? (await loadCliKeywordIndex(path.resolve(cwd, config.vectorStore.path), logFilePath))?.count() ?? 0
482
512
  : 0;
483
- logCliInfo(logFilePath, "status", `Keyword index: ${config.retrieval.hybridSearch?.enabled ? "enabled" : "disabled"} (${kiCount} chunks)`);
513
+ logCliInfo(logFilePath, "status", `${c.label("Keyword index:")} ${config.retrieval.hybridSearch?.enabled ? c.enabled("enabled") : c.disabled("disabled")} (${c.num(kiCount)} chunks)`);
484
514
  if (summary.rebuildRequired) {
485
- logCliInfo(logFilePath, "status", `Rebuild required: yes (manifest missing/corrupt)`);
515
+ logCliInfo(logFilePath, "status", `${c.label("Rebuild required:")} ${c.warn("yes")} (manifest missing/corrupt)`);
516
+ }
517
+ }
518
+ catch (err) {
519
+ const message = err.message || String(err);
520
+ const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
521
+ logCliError(logFilePath, "status", `\nStatus check failed: ${message}`, err);
522
+ process.exit(1);
523
+ }
524
+ });
525
+ program
526
+ .command("list")
527
+ .description("List all indexed files with chunk counts")
528
+ .option("-c, --config <path>", "path to config file")
529
+ .action(async (options) => {
530
+ try {
531
+ const cwd = process.cwd();
532
+ let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
533
+ const config = await resolveConfig(options, logFilePath);
534
+ logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
535
+ const { LanceDBStore } = await import("./vectorstore/lancedb.js");
536
+ const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
537
+ const files = await store.listFiles();
538
+ if (files.length === 0) {
539
+ logCliInfo(logFilePath, "list", `${c.warn("No indexed files found.")} Run 'opencode-rag index' first.`);
540
+ return;
541
+ }
542
+ logCliInfo(logFilePath, "list", `\n${c.num(files.length)} file(s) indexed:\n`);
543
+ for (const f of files) {
544
+ logCliInfo(logFilePath, "list", ` ${c.file(f.filePath)} ${c.label("(")}${c.lang(f.language)}${c.label(", ")}${c.num(f.chunkCount)} chunk${f.chunkCount === 1 ? "" : "s"}${c.label(")")}`);
486
545
  }
487
546
  }
488
547
  catch (err) {
489
548
  const message = err.message || String(err);
490
549
  const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
491
- logCliError(logFilePath, "status", `Status check failed: ${message}`, err);
492
- console.error(`\nStatus check failed: ${message}`);
550
+ logCliError(logFilePath, "list", `\nList failed: ${message}`, err);
551
+ process.exit(1);
552
+ }
553
+ });
554
+ program
555
+ .command("show <file>")
556
+ .description("Show chunks for a specific file")
557
+ .option("-c, --config <path>", "path to config file")
558
+ .action(async (file, options) => {
559
+ try {
560
+ const cwd = process.cwd();
561
+ let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
562
+ const config = await resolveConfig(options, logFilePath);
563
+ logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
564
+ const { LanceDBStore } = await import("./vectorstore/lancedb.js");
565
+ const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
566
+ const chunks = await store.getChunksByFilePath(file);
567
+ if (chunks.length === 0) {
568
+ logCliInfo(logFilePath, "show", `${c.warn(`No chunks found for '${file}'.`)}`);
569
+ return;
570
+ }
571
+ logCliInfo(logFilePath, "show", `\n${c.num(chunks.length)} chunk(s) for ${c.file(file)}:\n`);
572
+ for (const chunk of chunks) {
573
+ logCliInfo(logFilePath, "show", ` ${c.label("[")}${c.value(String(chunk.metadata.startLine))}${c.label("-")}${c.value(String(chunk.metadata.endLine))}${c.label("]")} ${c.label("(")}${c.lang(chunk.metadata.language)}${c.label(")")} ${pc.dim(chunk.id)}`);
574
+ if (chunk.description) {
575
+ logCliInfo(logFilePath, "show", ` ${c.desc(">")} ${c.desc(chunk.description)}`);
576
+ }
577
+ logCliInfo(logFilePath, "show", ` ${chunk.content}\n`);
578
+ }
579
+ }
580
+ catch (err) {
581
+ const message = err.message || String(err);
582
+ const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
583
+ logCliError(logFilePath, "show", `\nShow failed: ${message}`, err);
584
+ process.exit(1);
585
+ }
586
+ });
587
+ program
588
+ .command("dump")
589
+ .description("Dump all indexed chunks")
590
+ .option("-c, --config <path>", "path to config file")
591
+ .option("--offset <number>", "start at chunk offset", "0")
592
+ .option("--limit <number>", "max number of chunks to dump", "25")
593
+ .action(async (options) => {
594
+ try {
595
+ const cwd = process.cwd();
596
+ let logFilePath = path.resolve(cwd, ".opencode", "opencode-rag.log");
597
+ const config = await resolveConfig(options, logFilePath);
598
+ logFilePath = path.resolve(cwd, resolveLogConfig(config).logFilePath);
599
+ const { LanceDBStore } = await import("./vectorstore/lancedb.js");
600
+ const store = new LanceDBStore(path.resolve(cwd, config.vectorStore.path));
601
+ const total = await store.count();
602
+ if (total === 0) {
603
+ logCliInfo(logFilePath, "dump", `${c.warn("No indexed chunks found.")} Run 'opencode-rag index' first.`);
604
+ return;
605
+ }
606
+ const offset = parseInt(options.offset ?? "0", 10);
607
+ const limit = parseInt(options.limit ?? "25", 10);
608
+ const chunks = await store.getChunks(offset, limit);
609
+ logCliInfo(logFilePath, "dump", `\n${c.heading("Chunks")} ${c.value(String(offset + 1))}${c.label("-")}${c.value(String(offset + chunks.length))} of ${c.num(total)}:\n`);
610
+ for (const chunk of chunks) {
611
+ logCliInfo(logFilePath, "dump", ` ${c.file(chunk.filePath)}:${c.value(String(chunk.startLine))}${c.label("-")}${c.value(String(chunk.endLine))} ${c.label("(")}${c.lang(chunk.language)}${c.label(")")}`);
612
+ logCliInfo(logFilePath, "dump", ` ${chunk.content}\n`);
613
+ }
614
+ if (offset + limit < total) {
615
+ logCliInfo(logFilePath, "dump", ` ${c.dim(`... ${total - offset - limit} more (use --offset ${offset + limit} to continue)`)}`);
616
+ }
617
+ }
618
+ catch (err) {
619
+ const message = err.message || String(err);
620
+ const logFilePath = path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
621
+ logCliError(logFilePath, "dump", `\nDump failed: ${message}`, err);
493
622
  process.exit(1);
494
623
  }
495
624
  });
@@ -557,125 +686,143 @@ program
557
686
  const tuiPluginEntryPath = path.join(pluginsDir, "rag-tui.js");
558
687
  const tuiConfigPath = path.join(opencodeDir, "tui.json");
559
688
  const opencodePackagePath = path.join(opencodeDir, "package.json");
560
- console.log("Initializing OpenCodeRAG in workspace...\n");
689
+ console.log(`\n${c.heading("Initializing OpenCodeRAG in workspace...")}\n`);
561
690
  if (!existsSync(opencodeDir)) {
562
691
  mkdirSync(opencodeDir, { recursive: true });
563
- console.log(" Created: .opencode/");
692
+ console.log(` ${c.created("Created:")} .opencode/`);
564
693
  }
565
694
  else {
566
- console.log(" Exists: .opencode/");
695
+ console.log(` ${c.exists("Exists:")} .opencode/`);
567
696
  }
568
697
  if (!existsSync(pluginsDir)) {
569
698
  mkdirSync(pluginsDir, { recursive: true });
570
- console.log(" Created: .opencode/plugins/");
699
+ console.log(` ${c.created("Created:")} .opencode/plugins/`);
571
700
  }
572
701
  else {
573
- console.log(" Exists: .opencode/plugins/");
702
+ console.log(` ${c.exists("Exists:")} .opencode/plugins/`);
574
703
  }
575
704
  const gitignoreExists = existsSync(gitignorePath);
576
705
  const nextGitignoreContent = mergeGitignoreContent(gitignoreExists ? readFileSync(gitignorePath, "utf-8") : undefined);
577
706
  if (!gitignoreExists || options.force || readFileSync(gitignorePath, "utf-8") !== nextGitignoreContent) {
578
707
  writeFileSync(gitignorePath, nextGitignoreContent, "utf-8");
579
- console.log(` ${gitignoreExists ? "Updated" : "Created"}: .opencode/.gitignore`);
708
+ console.log(` ${gitignoreExists ? c.updated("Updated:") : c.created("Created:")} .opencode/.gitignore`);
580
709
  }
581
710
  else {
582
- console.log(" Exists: .opencode/.gitignore");
711
+ console.log(` ${c.exists("Exists:")} .opencode/.gitignore`);
583
712
  }
584
713
  const opencodeConfigExists = existsSync(opencodeConfigPath);
585
714
  const nextOpencodeConfig = buildOpencodeConfig(readJsonObject(opencodeConfigPath));
586
715
  if (!opencodeConfigExists || options.force) {
587
716
  writeJsonFile(opencodeConfigPath, nextOpencodeConfig);
588
- console.log(` ${opencodeConfigExists ? "Updated" : "Created"}: .opencode/opencode.json`);
717
+ console.log(` ${opencodeConfigExists ? c.updated("Updated:") : c.created("Created:")} .opencode/opencode.json`);
589
718
  }
590
719
  else if (JSON.stringify(readJsonObject(opencodeConfigPath)) !== JSON.stringify(nextOpencodeConfig)) {
591
720
  writeJsonFile(opencodeConfigPath, nextOpencodeConfig);
592
- console.log(" Updated: .opencode/opencode.json");
721
+ console.log(` ${c.updated("Updated:")} .opencode/opencode.json`);
593
722
  }
594
723
  else {
595
- console.log(" Exists: .opencode/opencode.json");
724
+ console.log(` ${c.exists("Exists:")} .opencode/opencode.json`);
596
725
  }
597
726
  const pluginEntryExists = existsSync(pluginEntryPath);
598
727
  const pluginEntryContent = generateWorkspacePluginFile(packageMetadata.name);
599
728
  if (!pluginEntryExists || options.force) {
600
729
  writeFileSync(pluginEntryPath, pluginEntryContent, "utf-8");
601
- console.log(` ${pluginEntryExists ? "Updated" : "Created"}: .opencode/plugins/rag-plugin.js`);
730
+ console.log(` ${pluginEntryExists ? c.updated("Updated:") : c.created("Created:")} .opencode/plugins/rag-plugin.js`);
602
731
  }
603
732
  else if (readFileSync(pluginEntryPath, "utf-8") !== pluginEntryContent) {
604
733
  writeFileSync(pluginEntryPath, pluginEntryContent, "utf-8");
605
- console.log(" Updated: .opencode/plugins/rag-plugin.js");
734
+ console.log(` ${c.updated("Updated:")} .opencode/plugins/rag-plugin.js`);
606
735
  }
607
736
  else {
608
- console.log(" Exists: .opencode/plugins/rag-plugin.js");
737
+ console.log(` ${c.exists("Exists:")} .opencode/plugins/rag-plugin.js`);
609
738
  }
610
739
  const tuiPluginEntryExists = existsSync(tuiPluginEntryPath);
611
740
  const tuiPluginEntryContent = generateWorkspaceTuiPluginFile(packageMetadata.name);
612
741
  if (!tuiPluginEntryExists || options.force) {
613
742
  writeFileSync(tuiPluginEntryPath, tuiPluginEntryContent, "utf-8");
614
- console.log(` ${tuiPluginEntryExists ? "Updated" : "Created"}: .opencode/plugins/rag-tui.js`);
743
+ console.log(` ${tuiPluginEntryExists ? c.updated("Updated:") : c.created("Created:")} .opencode/plugins/rag-tui.js`);
615
744
  }
616
745
  else if (readFileSync(tuiPluginEntryPath, "utf-8") !== tuiPluginEntryContent) {
617
746
  writeFileSync(tuiPluginEntryPath, tuiPluginEntryContent, "utf-8");
618
- console.log(" Updated: .opencode/plugins/rag-tui.js");
747
+ console.log(` ${c.updated("Updated:")} .opencode/plugins/rag-tui.js`);
619
748
  }
620
749
  else {
621
- console.log(" Exists: .opencode/plugins/rag-tui.js");
750
+ console.log(` ${c.exists("Exists:")} .opencode/plugins/rag-tui.js`);
622
751
  }
623
752
  const tuiConfigExists = existsSync(tuiConfigPath);
624
753
  const nextTuiConfig = { plugin: ["./plugins/rag-tui.js"] };
625
754
  if (!tuiConfigExists || options.force) {
626
755
  writeJsonFile(tuiConfigPath, nextTuiConfig);
627
- console.log(` ${tuiConfigExists ? "Updated" : "Created"}: .opencode/tui.json`);
756
+ console.log(` ${tuiConfigExists ? c.updated("Updated:") : c.created("Created:")} .opencode/tui.json`);
628
757
  }
629
758
  else if (JSON.stringify(readJsonObject(tuiConfigPath)) !== JSON.stringify(nextTuiConfig)) {
630
759
  writeJsonFile(tuiConfigPath, nextTuiConfig);
631
- console.log(" Updated: .opencode/tui.json");
760
+ console.log(` ${c.updated("Updated:")} .opencode/tui.json`);
632
761
  }
633
762
  else {
634
- console.log(" Exists: .opencode/tui.json");
763
+ console.log(` ${c.exists("Exists:")} .opencode/tui.json`);
635
764
  }
636
765
  const workspacePackageExists = existsSync(opencodePackagePath);
637
766
  const nextWorkspacePackage = buildWorkspacePackageJson(readJsonObject(opencodePackagePath), packageMetadata, opencodeDir);
638
767
  if (!workspacePackageExists || options.force) {
639
768
  writeJsonFile(opencodePackagePath, nextWorkspacePackage);
640
- console.log(` ${workspacePackageExists ? "Updated" : "Created"}: .opencode/package.json`);
769
+ console.log(` ${workspacePackageExists ? c.updated("Updated:") : c.created("Created:")} .opencode/package.json`);
641
770
  }
642
771
  else if (JSON.stringify(readJsonObject(opencodePackagePath)) !== JSON.stringify(nextWorkspacePackage)) {
643
772
  writeJsonFile(opencodePackagePath, nextWorkspacePackage);
644
- console.log(" Updated: .opencode/package.json");
773
+ console.log(` ${c.updated("Updated:")} .opencode/package.json`);
645
774
  }
646
775
  else {
647
- console.log(" Exists: .opencode/package.json");
776
+ console.log(` ${c.exists("Exists:")} .opencode/package.json`);
648
777
  }
649
778
  const configExists = existsSync(configPath);
650
779
  if (!configExists || options.force) {
651
780
  writeFileSync(configPath, generateDefaultConfigJson(), "utf-8");
652
- console.log(` ${configExists ? "Updated" : "Created"}: opencode-rag.json`);
781
+ console.log(` ${configExists ? c.updated("Updated:") : c.created("Created:")} opencode-rag.json`);
653
782
  }
654
783
  else {
655
- console.log(" Exists: opencode-rag.json");
784
+ console.log(` ${c.exists("Exists:")} opencode-rag.json`);
656
785
  }
657
786
  if (!options.skipInstall) {
658
- console.log("\nInstalling workspace-local plugin dependencies...\n");
787
+ console.log(`\n${c.heading("Installing workspace-local plugin dependencies...")}\n`);
659
788
  installWorkspaceDependencies(opencodeDir);
660
- console.log("\n Installed: .opencode/node_modules/");
789
+ console.log(`\n ${c.success("Installed:")} .opencode/node_modules/`);
661
790
  const updatedGlobalConfigs = removeStaleGlobalPluginRegistrations(os.homedir(), packageMetadata.name);
662
791
  if (updatedGlobalConfigs.length > 0) {
663
792
  for (const configPath of updatedGlobalConfigs) {
664
- console.log(` Removed stale plugin registration from ${configPath}`);
793
+ console.log(` ${c.warn("Removed stale plugin registration from")} ${configPath}`);
665
794
  }
666
795
  }
667
- console.log(" OpenCode loads the plugin from .opencode/plugins/rag-plugin.js; no global plugin registration is required.");
796
+ console.log(` ${c.dim("OpenCode loads the plugin from .opencode/plugins/rag-plugin.js; no global plugin registration is required.")}`);
668
797
  }
669
798
  else {
670
- console.log("\n Skipped: dependency installation (--skip-install)");
799
+ console.log(`\n ${c.exists("Skipped:")} dependency installation (--skip-install)`);
671
800
  }
672
- console.log("\nDone. Restart OpenCode if it is running, then run `opencode-rag index` in this workspace.");
801
+ console.log(`\n${c.success("Done.")} Restart OpenCode if it is running, then run ${c.file("'opencode-rag index'")} in this workspace.`);
673
802
  });
674
803
  /**
675
804
  * Determine whether the CLI should auto-run for the current module.
676
805
  * Resolves the first argv entry so symlinked binaries compare against the
677
806
  * real file path, and returns false if the path cannot be resolved.
678
807
  */
808
+ function dedupeResults(results) {
809
+ const seen = new Set();
810
+ const deduped = [];
811
+ for (const result of results) {
812
+ const chunk = result.chunk;
813
+ const key = [
814
+ chunk.metadata.filePath,
815
+ chunk.metadata.startLine,
816
+ chunk.metadata.endLine,
817
+ chunk.content,
818
+ ].join(":");
819
+ if (seen.has(key))
820
+ continue;
821
+ seen.add(key);
822
+ deduped.push(result);
823
+ }
824
+ return deduped;
825
+ }
679
826
  export function shouldAutoRunCli(moduleUrl, argv1) {
680
827
  if (!argv1) {
681
828
  return false;
@@ -695,7 +842,7 @@ if (shouldAutoRunCli(import.meta.url, process.argv[1])) {
695
842
  else {
696
843
  // Fallback: if the module appears to be running as a CLI (has argv with commands like 'init', 'index', etc.)
697
844
  // and not being imported as a library, parse the arguments anyway
698
- const commands = ['init', 'index', 'query', 'clear', 'status'];
845
+ const commands = ['init', 'index', 'query', 'clear', 'status', 'list', 'show', 'dump'];
699
846
  const cmd = process.argv[2];
700
847
  if (process.argv.length > 2 && cmd && commands.includes(cmd.toLowerCase())) {
701
848
  void program.parseAsync(process.argv);