voyageai-cli 1.30.0 → 1.30.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 (82) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +8 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/chat.js +32 -11
  6. package/src/commands/code-search.js +751 -0
  7. package/src/commands/doctor.js +1 -1
  8. package/src/commands/export.js +124 -0
  9. package/src/commands/import.js +195 -0
  10. package/src/commands/index-workspace.js +243 -0
  11. package/src/commands/mcp-server.js +113 -3
  12. package/src/commands/playground.js +120 -4
  13. package/src/commands/quickstart.js +4 -4
  14. package/src/commands/workflow.js +132 -65
  15. package/src/lib/catalog.js +4 -2
  16. package/src/lib/code-search.js +315 -0
  17. package/src/lib/codegen.js +1 -1
  18. package/src/lib/explanations.js +3 -3
  19. package/src/lib/export/contexts/benchmark-export.js +27 -0
  20. package/src/lib/export/contexts/chat-export.js +41 -0
  21. package/src/lib/export/contexts/explore-export.js +22 -0
  22. package/src/lib/export/contexts/search-export.js +54 -0
  23. package/src/lib/export/contexts/workflow-export.js +80 -0
  24. package/src/lib/export/formats/clipboard-export.js +29 -0
  25. package/src/lib/export/formats/csv-export.js +45 -0
  26. package/src/lib/export/formats/json-export.js +50 -0
  27. package/src/lib/export/formats/markdown-export.js +189 -0
  28. package/src/lib/export/formats/mermaid-export.js +274 -0
  29. package/src/lib/export/formats/pdf-export.js +117 -0
  30. package/src/lib/export/formats/png-export.js +96 -0
  31. package/src/lib/export/formats/svg-export.js +116 -0
  32. package/src/lib/export/index.js +175 -0
  33. package/src/lib/github.js +226 -0
  34. package/src/lib/template-engine.js +154 -20
  35. package/src/lib/workflow-builder.js +753 -0
  36. package/src/lib/workflow-formatters.js +454 -0
  37. package/src/lib/workflow-input-cache.js +111 -0
  38. package/src/lib/workflow-scaffold.js +1 -1
  39. package/src/lib/workflow.js +297 -28
  40. package/src/mcp/install.js +280 -7
  41. package/src/mcp/schemas/index.js +170 -0
  42. package/src/mcp/server.js +19 -4
  43. package/src/mcp/tools/authoring.js +662 -0
  44. package/src/mcp/tools/code-search.js +620 -0
  45. package/src/mcp/tools/ingest.js +2 -5
  46. package/src/mcp/tools/retrieval.js +2 -15
  47. package/src/mcp/tools/workspace.js +452 -0
  48. package/src/mcp/utils.js +20 -0
  49. package/src/playground/announcements.md +52 -5
  50. package/src/playground/help/workflow-nodes.js +127 -2
  51. package/src/playground/index.html +17109 -12438
  52. package/src/playground/vendor/mermaid.min.js +2811 -0
  53. package/src/workflows/code-review.json +110 -0
  54. package/src/workflows/cost-analysis.json +5 -0
  55. package/src/workflows/rag-chat.json +165 -0
  56. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  57. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  58. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  59. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  60. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  61. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  62. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  63. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  64. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  65. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  66. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  67. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  68. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  69. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  70. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  71. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  72. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  73. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  74. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  75. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  76. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  77. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  78. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
  79. package/src/playground/assets/announcements/appstore.jpg +0 -0
  80. package/src/playground/assets/announcements/circuits.jpg +0 -0
  81. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  82. package/src/playground/assets/announcements/green-wave.jpg +0 -0
@@ -121,7 +121,7 @@ async function checkApiConnection() {
121
121
  });
122
122
 
123
123
  // Send minimal request body
124
- req.write(JSON.stringify({ model: 'voyage-3-lite', input: ['test'] }));
124
+ req.write(JSON.stringify({ model: 'voyage-4-lite', input: ['test'] }));
125
125
  req.end();
126
126
  });
127
127
 
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { exportArtifact, getFormatsForContext } = require('../lib/export');
6
+ const { copyToClipboard } = require('../lib/export/formats/clipboard-export');
7
+ const ui = require('../lib/ui');
8
+
9
+ /**
10
+ * Register the export command on a Commander program.
11
+ * @param {import('commander').Command} program
12
+ */
13
+ function registerExport(program) {
14
+ const exportCmd = program
15
+ .command('export')
16
+ .description('Export workflows, chat sessions, and search results in various formats');
17
+
18
+ // ── export workflow <file> ──
19
+ exportCmd
20
+ .command('workflow <file>')
21
+ .description('Export a workflow definition')
22
+ .option('-f, --format <fmt>', 'Output format (json, markdown, mermaid, svg, png, clipboard)', 'json')
23
+ .option('-o, --output <path>', 'Output file path (stdout if omitted)')
24
+ .option('--options <json>', 'Format-specific options as JSON string', '{}')
25
+ .option('--clipboard', 'Copy to system clipboard')
26
+ .action(async (file, opts) => {
27
+ await handleExport('workflow', file, opts);
28
+ });
29
+
30
+ // ── export chat <sessionId> ──
31
+ exportCmd
32
+ .command('chat <sessionId>')
33
+ .description('Export a chat session')
34
+ .option('-f, --format <fmt>', 'Output format (json, markdown, pdf, clipboard)', 'json')
35
+ .option('-o, --output <path>', 'Output file path (stdout if omitted)')
36
+ .option('--options <json>', 'Format-specific options as JSON string', '{}')
37
+ .option('--clipboard', 'Copy to system clipboard')
38
+ .action(async (sessionId, opts) => {
39
+ await handleExport('chat', sessionId, opts);
40
+ });
41
+
42
+ // ── export results <file> ──
43
+ exportCmd
44
+ .command('results <file>')
45
+ .description('Export saved search results')
46
+ .option('-f, --format <fmt>', 'Output format (json, jsonl, csv, markdown, clipboard)', 'json')
47
+ .option('-o, --output <path>', 'Output file path (stdout if omitted)')
48
+ .option('--options <json>', 'Format-specific options as JSON string', '{}')
49
+ .option('--clipboard', 'Copy to system clipboard')
50
+ .action(async (file, opts) => {
51
+ await handleExport('search', file, opts);
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Handle an export action.
57
+ */
58
+ async function handleExport(context, source, opts) {
59
+ try {
60
+ // Load source data
61
+ let data;
62
+ if (context === 'chat') {
63
+ // Chat sessions are loaded from MongoDB — for now, support JSON file input too
64
+ data = loadJsonFile(source);
65
+ } else {
66
+ data = loadJsonFile(source);
67
+ }
68
+
69
+ const format = opts.clipboard ? 'clipboard' : opts.format;
70
+ const formatOptions = parseOptions(opts.options);
71
+
72
+ const result = await exportArtifact({
73
+ context,
74
+ format,
75
+ data,
76
+ options: formatOptions,
77
+ });
78
+
79
+ const isBinary = Buffer.isBuffer(result.content);
80
+
81
+ // Output
82
+ if (opts.clipboard) {
83
+ console.log(ui.success ? ui.success('Copied to clipboard') : '✓ Copied to clipboard');
84
+ } else if (opts.output) {
85
+ const outPath = path.resolve(opts.output);
86
+ if (isBinary) {
87
+ fs.writeFileSync(outPath, result.content);
88
+ } else {
89
+ fs.writeFileSync(outPath, result.content, 'utf-8');
90
+ }
91
+ console.log(ui.success ? ui.success(`Saved to ${outPath}`) : `✓ Saved to ${outPath}`);
92
+ } else if (isBinary) {
93
+ // Binary to stdout requires --output
94
+ console.error(ui.error ? ui.error('Binary formats (png, pdf) require --output <path>') : '✗ Binary formats (png, pdf) require --output <path>');
95
+ process.exitCode = 1;
96
+ } else {
97
+ // stdout
98
+ process.stdout.write(result.content);
99
+ if (!result.content.endsWith('\n')) process.stdout.write('\n');
100
+ }
101
+ } catch (err) {
102
+ console.error(ui.error ? ui.error(err.message) : `✗ ${err.message}`);
103
+ process.exitCode = 1;
104
+ }
105
+ }
106
+
107
+ function loadJsonFile(filePath) {
108
+ const resolved = path.resolve(filePath);
109
+ if (!fs.existsSync(resolved)) {
110
+ throw new Error(`File not found: ${resolved}`);
111
+ }
112
+ const raw = fs.readFileSync(resolved, 'utf-8');
113
+ return JSON.parse(raw);
114
+ }
115
+
116
+ function parseOptions(jsonStr) {
117
+ try {
118
+ return JSON.parse(jsonStr || '{}');
119
+ } catch {
120
+ throw new Error(`Invalid --options JSON: ${jsonStr}`);
121
+ }
122
+ }
123
+
124
+ module.exports = { registerExport };
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ui = require('../lib/ui');
6
+ const { validateWorkflow } = require('../lib/workflow');
7
+
8
+ /**
9
+ * Register the import command on a Commander program.
10
+ * @param {import('commander').Command} program
11
+ */
12
+ function registerImport(program) {
13
+ program
14
+ .command('import <file>')
15
+ .description('Import a workflow definition or chat session exported by vai')
16
+ .option('--type <type>', 'Force type: workflow or chat (auto-detected if omitted)')
17
+ .option('--db <name>', 'Target database (for chat import)')
18
+ .option('--collection <name>', 'Target collection (for chat import)')
19
+ .option('--dry-run', 'Show what would be imported without writing')
20
+ .option('--json', 'Machine-readable JSON output')
21
+ .action(async (file, opts) => {
22
+ try {
23
+ const resolved = path.resolve(file);
24
+ if (!fs.existsSync(resolved)) {
25
+ throw new Error(`File not found: ${resolved}`);
26
+ }
27
+
28
+ const raw = fs.readFileSync(resolved, 'utf-8');
29
+ const data = JSON.parse(raw);
30
+
31
+ // Auto-detect type
32
+ const type = opts.type || detectType(data);
33
+ if (!type) {
34
+ throw new Error(
35
+ 'Could not auto-detect import type. Use --type workflow or --type chat.'
36
+ );
37
+ }
38
+
39
+ if (type === 'workflow') {
40
+ await importWorkflow(data, opts, resolved);
41
+ } else if (type === 'chat') {
42
+ await importChat(data, opts, resolved);
43
+ } else {
44
+ throw new Error(`Unknown import type: "${type}". Use "workflow" or "chat".`);
45
+ }
46
+ } catch (err) {
47
+ if (opts.json) {
48
+ console.log(JSON.stringify({ error: err.message }));
49
+ } else {
50
+ console.error(ui.error ? ui.error(err.message) : `✗ ${err.message}`);
51
+ }
52
+ process.exitCode = 1;
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Detect whether the JSON is a workflow or chat session.
59
+ */
60
+ function detectType(data) {
61
+ // Workflow: has steps array
62
+ if (Array.isArray(data.steps) && data.steps.length > 0) return 'workflow';
63
+ // Chat: has turns or session object with turns
64
+ if (data.turns || (data.session && data.session.turns)) return 'chat';
65
+ // Exported chat wrapper
66
+ if (data._context === 'chat') return 'chat';
67
+ if (data._context === 'workflow') return 'workflow';
68
+ // Workflow with name + steps
69
+ if (data.name && data.steps) return 'workflow';
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Import a workflow definition.
75
+ */
76
+ async function importWorkflow(data, opts, filePath) {
77
+ // Strip export metadata
78
+ const workflow = { ...data };
79
+ delete workflow._exportMeta;
80
+ delete workflow._metadata;
81
+ delete workflow._execution;
82
+ delete workflow._context;
83
+ delete workflow._dependencyMap;
84
+ delete workflow._executionLayers;
85
+
86
+ // Validate
87
+ const errors = validateWorkflow(workflow);
88
+ if (errors.length > 0) {
89
+ throw new Error(`Invalid workflow:\n ${errors.join('\n ')}`);
90
+ }
91
+
92
+ const stepCount = (workflow.steps || []).length;
93
+ const inputCount = Object.keys(workflow.inputs || {}).length;
94
+
95
+ if (opts.dryRun) {
96
+ const summary = {
97
+ type: 'workflow',
98
+ name: workflow.name,
99
+ version: workflow.version,
100
+ steps: stepCount,
101
+ inputs: inputCount,
102
+ valid: true,
103
+ };
104
+ if (opts.json) {
105
+ console.log(JSON.stringify({ dryRun: true, ...summary }));
106
+ } else {
107
+ console.log(ui.success ? ui.success('Dry run — workflow is valid') : '✓ Dry run — workflow is valid');
108
+ console.log(` Name: ${workflow.name}`);
109
+ console.log(` Version: ${workflow.version || '—'}`);
110
+ console.log(` Steps: ${stepCount}`);
111
+ console.log(` Inputs: ${inputCount}`);
112
+ }
113
+ return;
114
+ }
115
+
116
+ // Write cleaned workflow to a local file
117
+ const outName = `${workflow.name || 'imported-workflow'}.json`;
118
+ const outPath = path.resolve(outName);
119
+ fs.writeFileSync(outPath, JSON.stringify(workflow, null, 2) + '\n', 'utf-8');
120
+
121
+ if (opts.json) {
122
+ console.log(JSON.stringify({ imported: true, type: 'workflow', path: outPath, steps: stepCount }));
123
+ } else {
124
+ console.log(ui.success ? ui.success(`Imported workflow → ${outPath}`) : `✓ Imported workflow → ${outPath}`);
125
+ console.log(` Name: ${workflow.name} | Steps: ${stepCount} | Inputs: ${inputCount}`);
126
+ console.log(` Run with: vai workflow run ${outName}`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Import a chat session into MongoDB.
132
+ */
133
+ async function importChat(data, opts, filePath) {
134
+ // Unwrap export format
135
+ const session = data.session || data;
136
+ const turns = session.turns || [];
137
+
138
+ if (turns.length === 0) {
139
+ throw new Error('Chat session has no turns to import.');
140
+ }
141
+
142
+ const summary = {
143
+ type: 'chat',
144
+ sessionId: session.sessionId || session.id,
145
+ turns: turns.length,
146
+ provider: session.provider,
147
+ model: session.model,
148
+ };
149
+
150
+ if (opts.dryRun) {
151
+ if (opts.json) {
152
+ console.log(JSON.stringify({ dryRun: true, ...summary }));
153
+ } else {
154
+ console.log(ui.success ? ui.success('Dry run — chat session is valid') : '✓ Dry run — chat session is valid');
155
+ console.log(` Session: ${summary.sessionId || '—'}`);
156
+ console.log(` Turns: ${summary.turns}`);
157
+ console.log(` Provider: ${summary.provider || '—'} (${summary.model || '—'})`);
158
+ }
159
+ return;
160
+ }
161
+
162
+ // Import to MongoDB
163
+ const dbName = opts.db || 'vai';
164
+ const collName = opts.collection || 'chat_history';
165
+
166
+ const { getMongoCollection } = require('../lib/mongo');
167
+ const { client, collection } = await getMongoCollection(dbName, collName);
168
+
169
+ try {
170
+ const docs = turns.map((turn, i) => ({
171
+ sessionId: session.sessionId || session.id || `imported-${Date.now()}`,
172
+ role: turn.role,
173
+ content: turn.content,
174
+ timestamp: turn.timestamp ? new Date(turn.timestamp) : new Date(),
175
+ ...(turn.context ? { context: turn.context } : {}),
176
+ ...(turn.metadata ? { metadata: turn.metadata } : {}),
177
+ _imported: true,
178
+ _importedAt: new Date(),
179
+ _importSource: path.basename(filePath),
180
+ }));
181
+
182
+ const result = await collection.insertMany(docs);
183
+
184
+ if (opts.json) {
185
+ console.log(JSON.stringify({ imported: true, ...summary, inserted: result.insertedCount, db: dbName, collection: collName }));
186
+ } else {
187
+ console.log(ui.success ? ui.success(`Imported ${result.insertedCount} turns → ${dbName}.${collName}`) : `✓ Imported ${result.insertedCount} turns → ${dbName}.${collName}`);
188
+ console.log(` Session: ${summary.sessionId || '—'} | Provider: ${summary.provider || '—'}`);
189
+ }
190
+ } finally {
191
+ await client.close();
192
+ }
193
+ }
194
+
195
+ module.exports = { registerImport, detectType };
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ let ora;
5
+ async function getOra() {
6
+ if (!ora) { ora = (await import('ora')).default; }
7
+ return ora;
8
+ }
9
+ const pc = require('picocolors');
10
+
11
+ /**
12
+ * Register the index-workspace command.
13
+ * @param {import('commander').Command} program
14
+ */
15
+ function registerIndexWorkspace(program) {
16
+ program
17
+ .command('index-workspace [path]')
18
+ .alias('index-ws')
19
+ .description('Index a workspace/codebase for semantic code search')
20
+ .option('--db <name>', 'MongoDB database name')
21
+ .option('--collection <name>', 'Collection to store indexed documents')
22
+ .option('--content-type <type>', 'Content type: code, docs, config, or all', 'code')
23
+ .option('--model <name>', 'Embedding model', 'voyage-code-3')
24
+ .option('--max-files <n>', 'Maximum files to index', (v) => parseInt(v, 10), 1000)
25
+ .option('--max-file-size <bytes>', 'Maximum file size in bytes', (v) => parseInt(v, 10), 100000)
26
+ .option('--chunk-size <n>', 'Target chunk size in characters', (v) => parseInt(v, 10), 512)
27
+ .option('--chunk-overlap <n>', 'Overlap between chunks', (v) => parseInt(v, 10), 50)
28
+ .option('--batch-size <n>', 'Files per batch', (v) => parseInt(v, 10), 10)
29
+ .option('--create-index', 'Create vector search index after indexing')
30
+ .option('--json', 'Output as JSON')
31
+ .action(async (workspacePath, opts) => {
32
+ const telemetry = require('../lib/telemetry');
33
+ telemetry.send('cli_index_workspace_run', { contentType: opts.contentType });
34
+
35
+ const { handleIndexWorkspace } = require('../mcp/tools/workspace');
36
+ const resolvedPath = workspacePath ? path.resolve(workspacePath) : process.cwd();
37
+
38
+ const spinner = (await getOra())(`Indexing ${resolvedPath}...`).start();
39
+
40
+ try {
41
+ const result = await handleIndexWorkspace({
42
+ path: resolvedPath,
43
+ db: opts.db,
44
+ collection: opts.collection,
45
+ contentType: opts.contentType,
46
+ model: opts.model,
47
+ maxFiles: opts.maxFiles,
48
+ maxFileSize: opts.maxFileSize,
49
+ chunkSize: opts.chunkSize,
50
+ chunkOverlap: opts.chunkOverlap,
51
+ batchSize: opts.batchSize,
52
+ });
53
+
54
+ spinner.stop();
55
+
56
+ if (opts.json) {
57
+ console.log(JSON.stringify(result.structuredContent, null, 2));
58
+ } else {
59
+ const stats = result.structuredContent;
60
+ console.log('\n' + pc.green('Workspace indexed successfully!') + '\n');
61
+ console.log(` Files found: ${stats.filesFound}`);
62
+ console.log(` Files indexed: ${stats.filesIndexed}`);
63
+ console.log(` Chunks created: ${stats.chunksCreated}`);
64
+ console.log(` Time: ${stats.timeMs}ms`);
65
+ console.log(` Collection: ${stats.db}.${stats.collection}`);
66
+ console.log(` Model: ${stats.model}`);
67
+
68
+ if (stats.errors?.length > 0) {
69
+ console.log('\n' + pc.yellow(`Errors (${stats.errors.length}):`));
70
+ for (const err of stats.errors.slice(0, 5)) {
71
+ console.log(` ${pc.dim(err.file)}: ${err.error}`);
72
+ }
73
+ if (stats.errors.length > 5) {
74
+ console.log(` ... and ${stats.errors.length - 5} more`);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Create index if requested
80
+ if (opts.createIndex) {
81
+ const indexSpinner = (await getOra())('Creating vector search index...').start();
82
+ try {
83
+ const { createVectorIndex } = require('../lib/mongo');
84
+ await createVectorIndex(
85
+ result.structuredContent.db,
86
+ result.structuredContent.collection,
87
+ 'vector_index',
88
+ 'embedding'
89
+ );
90
+ indexSpinner.succeed('Vector search index created');
91
+ } catch (err) {
92
+ indexSpinner.fail(`Failed to create index: ${err.message}`);
93
+ }
94
+ }
95
+
96
+ } catch (err) {
97
+ spinner.fail(`Indexing failed: ${err.message}`);
98
+ process.exit(1);
99
+ }
100
+ });
101
+
102
+ // Search code command
103
+ program
104
+ .command('search-code <query>')
105
+ .alias('sc')
106
+ .description('Semantic code search across indexed codebase')
107
+ .option('--db <name>', 'MongoDB database name')
108
+ .option('--collection <name>', 'Collection with indexed code')
109
+ .option('--limit <n>', 'Maximum results', (v) => parseInt(v, 10), 10)
110
+ .option('--language <lang>', 'Filter by programming language (e.g., js, py, go)')
111
+ .option('--category <cat>', 'Filter by category: code, docs, config')
112
+ .option('--model <name>', 'Embedding model')
113
+ .option('--json', 'Output as JSON')
114
+ .action(async (query, opts) => {
115
+ const telemetry = require('../lib/telemetry');
116
+ telemetry.send('cli_search_code_run', { language: opts.language });
117
+
118
+ const { handleSearchCode } = require('../mcp/tools/workspace');
119
+ const spinner = (await getOra())('Searching...').start();
120
+
121
+ try {
122
+ const result = await handleSearchCode({
123
+ query,
124
+ db: opts.db,
125
+ collection: opts.collection,
126
+ limit: opts.limit,
127
+ language: opts.language,
128
+ category: opts.category,
129
+ model: opts.model,
130
+ });
131
+
132
+ spinner.stop();
133
+
134
+ if (opts.json) {
135
+ console.log(JSON.stringify(result.structuredContent, null, 2));
136
+ } else {
137
+ const results = result.structuredContent.results;
138
+ const meta = result.structuredContent.metadata;
139
+
140
+ console.log(`\n${pc.bold(`Found ${results.length} results`)} ${pc.dim(`(${meta.timeMs}ms)`)}\n`);
141
+
142
+ for (let i = 0; i < results.length; i++) {
143
+ const r = results[i];
144
+ const score = (r.score * 100).toFixed(1);
145
+
146
+ console.log(pc.cyan(`[${i + 1}] ${r.source}`) + pc.dim(` (${r.language || 'unknown'}) — ${score}%`));
147
+
148
+ if (r.symbols?.length > 0) {
149
+ console.log(pc.dim(` Symbols: ${r.symbols.slice(0, 5).join(', ')}`));
150
+ }
151
+
152
+ // Show snippet
153
+ const snippet = r.content.slice(0, 200).replace(/\n/g, '\n ');
154
+ console.log(pc.dim(' ' + snippet + (r.content.length > 200 ? '...' : '')));
155
+ console.log('');
156
+ }
157
+ }
158
+
159
+ } catch (err) {
160
+ spinner.fail(`Search failed: ${err.message}`);
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ // Explain code command (context retrieval for code)
166
+ program
167
+ .command('context-code')
168
+ .alias('ctx')
169
+ .description('Get contextual information for code from indexed documentation')
170
+ .option('--code <snippet>', 'Code snippet to explain')
171
+ .option('--file <path>', 'File containing code to explain')
172
+ .option('--language <lang>', 'Programming language')
173
+ .option('--db <name>', 'MongoDB database name')
174
+ .option('--collection <name>', 'Collection with indexed documentation')
175
+ .option('--context-limit <n>', 'Number of context documents', (v) => parseInt(v, 10), 5)
176
+ .option('--json', 'Output as JSON')
177
+ .action(async (opts) => {
178
+ const telemetry = require('../lib/telemetry');
179
+ telemetry.send('cli_explain_code_run');
180
+
181
+ const { handleExplainCode } = require('../mcp/tools/workspace');
182
+ const fs = require('fs');
183
+
184
+ let code = opts.code;
185
+
186
+ // Read from file if provided
187
+ if (opts.file && !code) {
188
+ try {
189
+ code = fs.readFileSync(opts.file, 'utf-8');
190
+ if (!opts.language) {
191
+ opts.language = path.extname(opts.file).slice(1);
192
+ }
193
+ } catch (err) {
194
+ console.error(`Failed to read file: ${err.message}`);
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ // Read from stdin if no code provided
200
+ if (!code) {
201
+ console.error('Provide code via --code, --file, or pipe to stdin');
202
+ process.exit(1);
203
+ }
204
+
205
+ const spinner = (await getOra())('Finding relevant context...').start();
206
+
207
+ try {
208
+ const result = await handleExplainCode({
209
+ code,
210
+ language: opts.language,
211
+ db: opts.db,
212
+ collection: opts.collection,
213
+ contextLimit: opts.contextLimit,
214
+ });
215
+
216
+ spinner.stop();
217
+
218
+ if (opts.json) {
219
+ console.log(JSON.stringify(result.structuredContent, null, 2));
220
+ } else {
221
+ console.log('\n' + pc.bold('Code Context') + '\n');
222
+ console.log(pc.dim('─'.repeat(60)) + '\n');
223
+
224
+ const context = result.structuredContent.context || [];
225
+ if (context.length === 0) {
226
+ console.log(pc.yellow('No relevant context found. Try indexing more documentation.'));
227
+ } else {
228
+ for (const ctx of context) {
229
+ console.log(pc.cyan(`[${ctx.source}]`) + pc.dim(` — ${(ctx.score * 100).toFixed(1)}%`));
230
+ console.log(ctx.content.slice(0, 500));
231
+ console.log(pc.dim('─'.repeat(40)) + '\n');
232
+ }
233
+ }
234
+ }
235
+
236
+ } catch (err) {
237
+ spinner.fail(`Explain failed: ${err.message}`);
238
+ process.exit(1);
239
+ }
240
+ });
241
+ }
242
+
243
+ module.exports = { registerIndexWorkspace };