opencode-rag-plugin 1.3.6 → 1.4.2

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