voyageai-cli 1.30.0 → 1.30.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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +6 -0
  3. package/src/commands/chat.js +32 -11
  4. package/src/commands/export.js +124 -0
  5. package/src/commands/import.js +195 -0
  6. package/src/commands/index-workspace.js +239 -0
  7. package/src/commands/mcp-server.js +113 -3
  8. package/src/commands/playground.js +111 -3
  9. package/src/lib/export/contexts/benchmark-export.js +27 -0
  10. package/src/lib/export/contexts/chat-export.js +41 -0
  11. package/src/lib/export/contexts/explore-export.js +22 -0
  12. package/src/lib/export/contexts/search-export.js +54 -0
  13. package/src/lib/export/contexts/workflow-export.js +80 -0
  14. package/src/lib/export/formats/clipboard-export.js +29 -0
  15. package/src/lib/export/formats/csv-export.js +45 -0
  16. package/src/lib/export/formats/json-export.js +50 -0
  17. package/src/lib/export/formats/markdown-export.js +189 -0
  18. package/src/lib/export/formats/mermaid-export.js +274 -0
  19. package/src/lib/export/formats/pdf-export.js +117 -0
  20. package/src/lib/export/formats/png-export.js +96 -0
  21. package/src/lib/export/formats/svg-export.js +116 -0
  22. package/src/lib/export/index.js +175 -0
  23. package/src/lib/workflow.js +206 -27
  24. package/src/mcp/install.js +280 -7
  25. package/src/mcp/schemas/index.js +40 -0
  26. package/src/mcp/server.js +2 -0
  27. package/src/mcp/tools/workspace.js +463 -0
  28. package/src/playground/announcements.md +52 -5
  29. package/src/playground/index.html +11125 -7796
  30. package/src/playground/vendor/mermaid.min.js +2811 -0
  31. package/src/workflows/rag-chat.json +165 -0
  32. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  33. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  34. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  35. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  36. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  37. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  38. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  39. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  40. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  41. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  42. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  43. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  44. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  45. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  46. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  47. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  48. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  49. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  50. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  51. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
  52. package/src/playground/assets/announcements/appstore.jpg +0 -0
  53. package/src/playground/assets/announcements/circuits.jpg +0 -0
  54. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  55. package/src/playground/assets/announcements/green-wave.jpg +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.30.0",
3
+ "version": "1.30.1",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
package/src/cli.js CHANGED
@@ -38,6 +38,9 @@ const { registerBug } = require('./commands/bug');
38
38
  const { registerChat } = require('./commands/chat');
39
39
  const { registerMcpServer } = require('./commands/mcp-server');
40
40
  const { registerWorkflow } = require('./commands/workflow');
41
+ const { registerIndexWorkspace } = require('./commands/index-workspace');
42
+ const { registerExport } = require('./commands/export');
43
+ const { registerImport } = require('./commands/import');
41
44
  const { showBanner, showQuickStart, getVersion } = require('./lib/banner');
42
45
 
43
46
  const version = getVersion();
@@ -80,6 +83,9 @@ registerBug(program);
80
83
  registerChat(program);
81
84
  registerMcpServer(program);
82
85
  registerWorkflow(program);
86
+ registerIndexWorkspace(program);
87
+ registerExport(program);
88
+ registerImport(program);
83
89
 
84
90
  // Append disclaimer to all help output
85
91
  program.addHelpText('after', `
@@ -499,7 +499,7 @@ async function handleSlashCommand(input, ctx) {
499
499
  console.log(' /context Show retrieved context from last query');
500
500
  console.log(' /clear Clear conversation history');
501
501
  console.log(' /model Show or switch LLM model (/model <name>)');
502
- console.log(' /export Export conversation (markdown or json)');
502
+ console.log(' /export [format] [file] Export conversation (markdown, json, pdf)');
503
503
  if (isAgent) {
504
504
  console.log(' /tools Show tool calls from last response');
505
505
  console.log(' /export-workflow Export last tool sequence as workflow');
@@ -613,17 +613,38 @@ async function handleSlashCommand(input, ctx) {
613
613
  }
614
614
 
615
615
  case '/export': {
616
- const format = parts[1] || 'md';
617
- if (format === 'json') {
618
- const data = history.exportJSON();
619
- const filename = `chat-${history.sessionId.slice(0, 8)}.json`;
620
- fs.writeFileSync(filename, JSON.stringify(data, null, 2) + '\n');
621
- console.log(ui.success(`Exported to ${filename}`));
622
- } else {
623
- const md = history.exportMarkdown();
624
- const filename = `chat-${history.sessionId.slice(0, 8)}.md`;
625
- fs.writeFileSync(filename, md);
616
+ const format = parts[1] || 'markdown';
617
+ const outFile = parts[2] || null;
618
+ const validFormats = ['json', 'markdown', 'md', 'pdf'];
619
+
620
+ if (!validFormats.includes(format)) {
621
+ console.log(pc.dim(` Unknown format: ${format}. Use: ${validFormats.join(', ')}`));
622
+ return true;
623
+ }
624
+
625
+ try {
626
+ const { exportArtifact } = require('../lib/export');
627
+ const chatData = history.exportJSON();
628
+ const effectiveFormat = format === 'md' ? 'markdown' : format;
629
+
630
+ const result = await exportArtifact({
631
+ context: 'chat',
632
+ format: effectiveFormat,
633
+ data: chatData,
634
+ options: {},
635
+ });
636
+
637
+ const isBinary = Buffer.isBuffer(result.content);
638
+ const filename = outFile || result.suggestedFilename;
639
+
640
+ if (isBinary) {
641
+ fs.writeFileSync(filename, result.content);
642
+ } else {
643
+ fs.writeFileSync(filename, result.content, 'utf-8');
644
+ }
626
645
  console.log(ui.success(`Exported to ${filename}`));
646
+ } catch (err) {
647
+ console.log(pc.red(` Export failed: ${err.message}`));
627
648
  }
628
649
  return true;
629
650
  }
@@ -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,239 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const ora = require('ora');
5
+ const pc = require('picocolors');
6
+
7
+ /**
8
+ * Register the index-workspace command.
9
+ * @param {import('commander').Command} program
10
+ */
11
+ function registerIndexWorkspace(program) {
12
+ program
13
+ .command('index-workspace [path]')
14
+ .alias('index-ws')
15
+ .description('Index a workspace/codebase for semantic code search')
16
+ .option('--db <name>', 'MongoDB database name')
17
+ .option('--collection <name>', 'Collection to store indexed documents')
18
+ .option('--content-type <type>', 'Content type: code, docs, config, or all', 'code')
19
+ .option('--model <name>', 'Embedding model', 'voyage-code-3')
20
+ .option('--max-files <n>', 'Maximum files to index', (v) => parseInt(v, 10), 1000)
21
+ .option('--max-file-size <bytes>', 'Maximum file size in bytes', (v) => parseInt(v, 10), 100000)
22
+ .option('--chunk-size <n>', 'Target chunk size in characters', (v) => parseInt(v, 10), 512)
23
+ .option('--chunk-overlap <n>', 'Overlap between chunks', (v) => parseInt(v, 10), 50)
24
+ .option('--batch-size <n>', 'Files per batch', (v) => parseInt(v, 10), 10)
25
+ .option('--create-index', 'Create vector search index after indexing')
26
+ .option('--json', 'Output as JSON')
27
+ .action(async (workspacePath, opts) => {
28
+ const telemetry = require('../lib/telemetry');
29
+ telemetry.send('cli_index_workspace_run', { contentType: opts.contentType });
30
+
31
+ const { handleIndexWorkspace } = require('../mcp/tools/workspace');
32
+ const resolvedPath = workspacePath ? path.resolve(workspacePath) : process.cwd();
33
+
34
+ const spinner = ora(`Indexing ${resolvedPath}...`).start();
35
+
36
+ try {
37
+ const result = await handleIndexWorkspace({
38
+ path: resolvedPath,
39
+ db: opts.db,
40
+ collection: opts.collection,
41
+ contentType: opts.contentType,
42
+ model: opts.model,
43
+ maxFiles: opts.maxFiles,
44
+ maxFileSize: opts.maxFileSize,
45
+ chunkSize: opts.chunkSize,
46
+ chunkOverlap: opts.chunkOverlap,
47
+ batchSize: opts.batchSize,
48
+ });
49
+
50
+ spinner.stop();
51
+
52
+ if (opts.json) {
53
+ console.log(JSON.stringify(result.structuredContent, null, 2));
54
+ } else {
55
+ const stats = result.structuredContent;
56
+ console.log('\n' + pc.green('Workspace indexed successfully!') + '\n');
57
+ console.log(` Files found: ${stats.filesFound}`);
58
+ console.log(` Files indexed: ${stats.filesIndexed}`);
59
+ console.log(` Chunks created: ${stats.chunksCreated}`);
60
+ console.log(` Time: ${stats.timeMs}ms`);
61
+ console.log(` Collection: ${stats.db}.${stats.collection}`);
62
+ console.log(` Model: ${stats.model}`);
63
+
64
+ if (stats.errors?.length > 0) {
65
+ console.log('\n' + pc.yellow(`Errors (${stats.errors.length}):`));
66
+ for (const err of stats.errors.slice(0, 5)) {
67
+ console.log(` ${pc.dim(err.file)}: ${err.error}`);
68
+ }
69
+ if (stats.errors.length > 5) {
70
+ console.log(` ... and ${stats.errors.length - 5} more`);
71
+ }
72
+ }
73
+ }
74
+
75
+ // Create index if requested
76
+ if (opts.createIndex) {
77
+ const indexSpinner = ora('Creating vector search index...').start();
78
+ try {
79
+ const { createVectorIndex } = require('../lib/mongo');
80
+ await createVectorIndex(
81
+ result.structuredContent.db,
82
+ result.structuredContent.collection,
83
+ 'vector_index',
84
+ 'embedding'
85
+ );
86
+ indexSpinner.succeed('Vector search index created');
87
+ } catch (err) {
88
+ indexSpinner.fail(`Failed to create index: ${err.message}`);
89
+ }
90
+ }
91
+
92
+ } catch (err) {
93
+ spinner.fail(`Indexing failed: ${err.message}`);
94
+ process.exit(1);
95
+ }
96
+ });
97
+
98
+ // Search code command
99
+ program
100
+ .command('search-code <query>')
101
+ .alias('sc')
102
+ .description('Semantic code search across indexed codebase')
103
+ .option('--db <name>', 'MongoDB database name')
104
+ .option('--collection <name>', 'Collection with indexed code')
105
+ .option('--limit <n>', 'Maximum results', (v) => parseInt(v, 10), 10)
106
+ .option('--language <lang>', 'Filter by programming language (e.g., js, py, go)')
107
+ .option('--category <cat>', 'Filter by category: code, docs, config')
108
+ .option('--model <name>', 'Embedding model')
109
+ .option('--json', 'Output as JSON')
110
+ .action(async (query, opts) => {
111
+ const telemetry = require('../lib/telemetry');
112
+ telemetry.send('cli_search_code_run', { language: opts.language });
113
+
114
+ const { handleSearchCode } = require('../mcp/tools/workspace');
115
+ const spinner = ora('Searching...').start();
116
+
117
+ try {
118
+ const result = await handleSearchCode({
119
+ query,
120
+ db: opts.db,
121
+ collection: opts.collection,
122
+ limit: opts.limit,
123
+ language: opts.language,
124
+ category: opts.category,
125
+ model: opts.model,
126
+ });
127
+
128
+ spinner.stop();
129
+
130
+ if (opts.json) {
131
+ console.log(JSON.stringify(result.structuredContent, null, 2));
132
+ } else {
133
+ const results = result.structuredContent.results;
134
+ const meta = result.structuredContent.metadata;
135
+
136
+ console.log(`\n${pc.bold(`Found ${results.length} results`)} ${pc.dim(`(${meta.timeMs}ms)`)}\n`);
137
+
138
+ for (let i = 0; i < results.length; i++) {
139
+ const r = results[i];
140
+ const score = (r.score * 100).toFixed(1);
141
+
142
+ console.log(pc.cyan(`[${i + 1}] ${r.source}`) + pc.dim(` (${r.language || 'unknown'}) — ${score}%`));
143
+
144
+ if (r.symbols?.length > 0) {
145
+ console.log(pc.dim(` Symbols: ${r.symbols.slice(0, 5).join(', ')}`));
146
+ }
147
+
148
+ // Show snippet
149
+ const snippet = r.content.slice(0, 200).replace(/\n/g, '\n ');
150
+ console.log(pc.dim(' ' + snippet + (r.content.length > 200 ? '...' : '')));
151
+ console.log('');
152
+ }
153
+ }
154
+
155
+ } catch (err) {
156
+ spinner.fail(`Search failed: ${err.message}`);
157
+ process.exit(1);
158
+ }
159
+ });
160
+
161
+ // Explain code command (context retrieval for code)
162
+ program
163
+ .command('context-code')
164
+ .alias('ctx')
165
+ .description('Get contextual information for code from indexed documentation')
166
+ .option('--code <snippet>', 'Code snippet to explain')
167
+ .option('--file <path>', 'File containing code to explain')
168
+ .option('--language <lang>', 'Programming language')
169
+ .option('--db <name>', 'MongoDB database name')
170
+ .option('--collection <name>', 'Collection with indexed documentation')
171
+ .option('--context-limit <n>', 'Number of context documents', (v) => parseInt(v, 10), 5)
172
+ .option('--json', 'Output as JSON')
173
+ .action(async (opts) => {
174
+ const telemetry = require('../lib/telemetry');
175
+ telemetry.send('cli_explain_code_run');
176
+
177
+ const { handleExplainCode } = require('../mcp/tools/workspace');
178
+ const fs = require('fs');
179
+
180
+ let code = opts.code;
181
+
182
+ // Read from file if provided
183
+ if (opts.file && !code) {
184
+ try {
185
+ code = fs.readFileSync(opts.file, 'utf-8');
186
+ if (!opts.language) {
187
+ opts.language = path.extname(opts.file).slice(1);
188
+ }
189
+ } catch (err) {
190
+ console.error(`Failed to read file: ${err.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ // Read from stdin if no code provided
196
+ if (!code) {
197
+ console.error('Provide code via --code, --file, or pipe to stdin');
198
+ process.exit(1);
199
+ }
200
+
201
+ const spinner = ora('Finding relevant context...').start();
202
+
203
+ try {
204
+ const result = await handleExplainCode({
205
+ code,
206
+ language: opts.language,
207
+ db: opts.db,
208
+ collection: opts.collection,
209
+ contextLimit: opts.contextLimit,
210
+ });
211
+
212
+ spinner.stop();
213
+
214
+ if (opts.json) {
215
+ console.log(JSON.stringify(result.structuredContent, null, 2));
216
+ } else {
217
+ console.log('\n' + pc.bold('Code Context') + '\n');
218
+ console.log(pc.dim('─'.repeat(60)) + '\n');
219
+
220
+ const context = result.structuredContent.context || [];
221
+ if (context.length === 0) {
222
+ console.log(pc.yellow('No relevant context found. Try indexing more documentation.'));
223
+ } else {
224
+ for (const ctx of context) {
225
+ console.log(pc.cyan(`[${ctx.source}]`) + pc.dim(` — ${(ctx.score * 100).toFixed(1)}%`));
226
+ console.log(ctx.content.slice(0, 500));
227
+ console.log(pc.dim('─'.repeat(40)) + '\n');
228
+ }
229
+ }
230
+ }
231
+
232
+ } catch (err) {
233
+ spinner.fail(`Explain failed: ${err.message}`);
234
+ process.exit(1);
235
+ }
236
+ });
237
+ }
238
+
239
+ module.exports = { registerIndexWorkspace };