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.
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/chat.js +32 -11
- package/src/commands/export.js +124 -0
- package/src/commands/import.js +195 -0
- package/src/commands/index-workspace.js +239 -0
- package/src/commands/mcp-server.js +113 -3
- package/src/commands/playground.js +111 -3
- package/src/lib/export/contexts/benchmark-export.js +27 -0
- package/src/lib/export/contexts/chat-export.js +41 -0
- package/src/lib/export/contexts/explore-export.js +22 -0
- package/src/lib/export/contexts/search-export.js +54 -0
- package/src/lib/export/contexts/workflow-export.js +80 -0
- package/src/lib/export/formats/clipboard-export.js +29 -0
- package/src/lib/export/formats/csv-export.js +45 -0
- package/src/lib/export/formats/json-export.js +50 -0
- package/src/lib/export/formats/markdown-export.js +189 -0
- package/src/lib/export/formats/mermaid-export.js +274 -0
- package/src/lib/export/formats/pdf-export.js +117 -0
- package/src/lib/export/formats/png-export.js +96 -0
- package/src/lib/export/formats/svg-export.js +116 -0
- package/src/lib/export/index.js +175 -0
- package/src/lib/workflow.js +206 -27
- package/src/mcp/install.js +280 -7
- package/src/mcp/schemas/index.js +40 -0
- package/src/mcp/server.js +2 -0
- package/src/mcp/tools/workspace.js +463 -0
- package/src/playground/announcements.md +52 -5
- package/src/playground/index.html +11125 -7796
- package/src/playground/vendor/mermaid.min.js +2811 -0
- package/src/workflows/rag-chat.json +165 -0
- package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
- package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
- package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
- package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
- package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
- package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
- package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
- package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
- package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
- package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
- package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
- package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
- package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
- package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
- package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
- package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
- package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
- package/src/playground/assets/announcements/appstore.jpg +0 -0
- package/src/playground/assets/announcements/circuits.jpg +0 -0
- package/src/playground/assets/announcements/csvingest.jpg +0 -0
- package/src/playground/assets/announcements/green-wave.jpg +0 -0
package/package.json
CHANGED
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', `
|
package/src/commands/chat.js
CHANGED
|
@@ -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
|
|
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] || '
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
console.log(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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 };
|