voyageai-cli 1.29.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/README.md +82 -8
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/benchmark.js +22 -8
- package/src/commands/chat.js +50 -11
- package/src/commands/chunk.js +10 -0
- package/src/commands/demo.js +4 -0
- package/src/commands/embed.js +13 -0
- package/src/commands/estimate.js +3 -0
- package/src/commands/eval.js +6 -0
- package/src/commands/explain.js +2 -0
- package/src/commands/export.js +124 -0
- package/src/commands/generate.js +2 -0
- package/src/commands/import.js +195 -0
- package/src/commands/index-workspace.js +239 -0
- package/src/commands/ingest.js +4 -0
- package/src/commands/init.js +2 -0
- package/src/commands/mcp-server.js +115 -3
- package/src/commands/models.js +2 -0
- package/src/commands/ping.js +7 -0
- package/src/commands/pipeline.js +15 -0
- package/src/commands/playground.js +163 -9
- package/src/commands/query.js +16 -0
- package/src/commands/rerank.js +12 -0
- package/src/commands/scaffold.js +2 -0
- package/src/commands/search.js +11 -0
- package/src/commands/similarity.js +9 -0
- package/src/commands/store.js +4 -0
- package/src/commands/workflow.js +286 -0
- package/src/lib/capability-report.js +134 -0
- package/src/lib/chat.js +32 -1
- package/src/lib/config.js +2 -0
- package/src/lib/cost-display.js +107 -0
- package/src/lib/explanations.js +6 -0
- 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/llm.js +125 -18
- package/src/lib/quality-audit.js +71 -0
- package/src/lib/security/blocked-domains.json +17 -0
- package/src/lib/security-audit.js +198 -0
- package/src/lib/telemetry.js +23 -1
- package/src/lib/workflow-scaffold.js +61 -0
- package/src/lib/workflow-test-runner.js +208 -0
- package/src/lib/workflow.js +333 -28
- 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 +56 -0
- package/src/playground/help/workflow-nodes.js +472 -0
- package/src/playground/index.html +13134 -8507
- 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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { renderJson, renderJsonl } = require('./formats/json-export');
|
|
4
|
+
const { renderCsv } = require('./formats/csv-export');
|
|
5
|
+
const { renderMarkdown } = require('./formats/markdown-export');
|
|
6
|
+
const { renderMermaid } = require('./formats/mermaid-export');
|
|
7
|
+
const { copyToClipboard } = require('./formats/clipboard-export');
|
|
8
|
+
const { renderSvgAsync } = require('./formats/svg-export');
|
|
9
|
+
const { renderPng } = require('./formats/png-export');
|
|
10
|
+
const { renderPdf } = require('./formats/pdf-export');
|
|
11
|
+
|
|
12
|
+
const { normalizeWorkflow, WORKFLOW_FORMATS } = require('./contexts/workflow-export');
|
|
13
|
+
const { normalizeSearch, SEARCH_FORMATS } = require('./contexts/search-export');
|
|
14
|
+
const { normalizeChat, CHAT_FORMATS } = require('./contexts/chat-export');
|
|
15
|
+
const { normalizeBenchmark, BENCHMARK_FORMATS } = require('./contexts/benchmark-export');
|
|
16
|
+
const { normalizeExplore, EXPLORE_FORMATS } = require('./contexts/explore-export');
|
|
17
|
+
|
|
18
|
+
// ════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Context → Formats mapping
|
|
20
|
+
// ════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
const FORMAT_MAP = {
|
|
23
|
+
workflow: WORKFLOW_FORMATS,
|
|
24
|
+
search: SEARCH_FORMATS,
|
|
25
|
+
chat: CHAT_FORMATS,
|
|
26
|
+
benchmark: BENCHMARK_FORMATS,
|
|
27
|
+
explore: EXPLORE_FORMATS,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TRANSFORMERS = {
|
|
31
|
+
workflow: normalizeWorkflow,
|
|
32
|
+
search: normalizeSearch,
|
|
33
|
+
chat: normalizeChat,
|
|
34
|
+
benchmark: normalizeBenchmark,
|
|
35
|
+
explore: normalizeExplore,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RENDERERS = {
|
|
39
|
+
json: renderJson,
|
|
40
|
+
jsonl: renderJsonl,
|
|
41
|
+
csv: renderCsv,
|
|
42
|
+
markdown: renderMarkdown,
|
|
43
|
+
mermaid: renderMermaid,
|
|
44
|
+
svg: renderSvgAsync,
|
|
45
|
+
png: renderPng,
|
|
46
|
+
pdf: renderPdf,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Formats that produce binary (Buffer) output
|
|
50
|
+
const BINARY_FORMATS = new Set(['png', 'pdf']);
|
|
51
|
+
|
|
52
|
+
const EXT_MAP = {
|
|
53
|
+
json: '.json',
|
|
54
|
+
jsonl: '.jsonl',
|
|
55
|
+
csv: '.csv',
|
|
56
|
+
markdown: '.md',
|
|
57
|
+
mermaid: '.mmd',
|
|
58
|
+
svg: '.svg',
|
|
59
|
+
png: '.png',
|
|
60
|
+
pdf: '.pdf',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ════════════════════════════════════════════════════════════════════
|
|
64
|
+
// Public API
|
|
65
|
+
// ════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get supported formats for a given context.
|
|
69
|
+
* @param {string} context
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
function getFormatsForContext(context) {
|
|
73
|
+
return FORMAT_MAP[context] || [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build a deterministic filename for the export.
|
|
78
|
+
* Pattern: {context}-{name}-{timestamp}.{ext}
|
|
79
|
+
* @param {string} context
|
|
80
|
+
* @param {object} data
|
|
81
|
+
* @param {string} format
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function buildFilename(context, data, format) {
|
|
85
|
+
const name = (data.name || data.sessionId || data.query || context)
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
88
|
+
.replace(/^-|-$/g, '')
|
|
89
|
+
.slice(0, 40);
|
|
90
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
91
|
+
const ext = EXT_MAP[format] || '.txt';
|
|
92
|
+
return `${context}-${name}-${ts}${ext}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Main export entry point.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} params
|
|
99
|
+
* @param {string} params.context - 'workflow' | 'search' | 'chat' | 'benchmark' | 'explore'
|
|
100
|
+
* @param {string} params.format - 'json' | 'jsonl' | 'csv' | 'markdown' | 'mermaid' | 'clipboard'
|
|
101
|
+
* @param {object} params.data - Raw source data
|
|
102
|
+
* @param {object} [params.options] - Format & context specific options
|
|
103
|
+
* @returns {{ content: string, mimeType: string, suggestedFilename: string, format: string }}
|
|
104
|
+
*/
|
|
105
|
+
async function exportArtifact({ context, format, data, options = {} }) {
|
|
106
|
+
// Handle clipboard as a meta-format: pick the best underlying format and copy
|
|
107
|
+
const isClipboard = format === 'clipboard';
|
|
108
|
+
const effectiveFormat = isClipboard ? pickClipboardFormat(context) : format;
|
|
109
|
+
|
|
110
|
+
// Validate
|
|
111
|
+
const supported = getFormatsForContext(context);
|
|
112
|
+
if (!supported.includes(format)) {
|
|
113
|
+
throw new ExportError(
|
|
114
|
+
`Format "${format}" not supported for ${context}. Supported: ${supported.join(', ')}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Transform
|
|
119
|
+
const transformer = TRANSFORMERS[context];
|
|
120
|
+
if (!transformer) {
|
|
121
|
+
throw new ExportError(`Unknown export context: "${context}"`);
|
|
122
|
+
}
|
|
123
|
+
const normalized = transformer(data, options);
|
|
124
|
+
|
|
125
|
+
// Render
|
|
126
|
+
const renderer = RENDERERS[effectiveFormat];
|
|
127
|
+
if (!renderer) {
|
|
128
|
+
throw new ExportError(`No renderer for format: "${effectiveFormat}"`);
|
|
129
|
+
}
|
|
130
|
+
const output = await renderer(normalized, options);
|
|
131
|
+
const isBinary = BINARY_FORMATS.has(effectiveFormat);
|
|
132
|
+
|
|
133
|
+
// Clipboard side-effect (text only — binary clipboard handled by Electron)
|
|
134
|
+
if (isClipboard) {
|
|
135
|
+
const ok = copyToClipboard(typeof output.content === 'string' ? output.content : output.content.toString());
|
|
136
|
+
if (!ok) {
|
|
137
|
+
throw new ExportError('Failed to copy to clipboard — unsupported platform or missing clipboard tool');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
content: output.content,
|
|
143
|
+
mimeType: output.mimeType,
|
|
144
|
+
suggestedFilename: buildFilename(context, data, effectiveFormat),
|
|
145
|
+
format: effectiveFormat,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Pick the best text format for clipboard based on context.
|
|
151
|
+
*/
|
|
152
|
+
function pickClipboardFormat(context) {
|
|
153
|
+
switch (context) {
|
|
154
|
+
case 'workflow': return 'mermaid';
|
|
155
|
+
case 'search': return 'json';
|
|
156
|
+
case 'chat': return 'markdown';
|
|
157
|
+
case 'benchmark': return 'json';
|
|
158
|
+
case 'explore': return 'json';
|
|
159
|
+
default: return 'json';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class ExportError extends Error {
|
|
164
|
+
constructor(message) {
|
|
165
|
+
super(message);
|
|
166
|
+
this.name = 'ExportError';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
exportArtifact,
|
|
172
|
+
getFormatsForContext,
|
|
173
|
+
buildFilename,
|
|
174
|
+
ExportError,
|
|
175
|
+
};
|
package/src/lib/llm.js
CHANGED
|
@@ -147,15 +147,24 @@ class AnthropicProvider {
|
|
|
147
147
|
const json = await res.json();
|
|
148
148
|
const text = json.content?.[0]?.text || '';
|
|
149
149
|
yield text;
|
|
150
|
+
// Yield usage sentinel
|
|
151
|
+
const usage = json.usage || {};
|
|
152
|
+
yield { __usage: { inputTokens: usage.input_tokens || 0, outputTokens: usage.output_tokens || 0 } };
|
|
150
153
|
return;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
// Manual SSE loop to capture usage from streaming events
|
|
157
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
158
|
+
for await (const chunk of parseSSEWithMeta(res.body)) {
|
|
159
|
+
if (chunk.__event === 'message_start' && chunk.__data?.message?.usage) {
|
|
160
|
+
usage.inputTokens = chunk.__data.message.usage.input_tokens || 0;
|
|
161
|
+
} else if (chunk.__event === 'message_delta' && chunk.__data?.usage) {
|
|
162
|
+
usage.outputTokens = chunk.__data.usage.output_tokens || 0;
|
|
163
|
+
} else if (chunk.__event === 'content_block_delta' && chunk.__data?.delta?.text) {
|
|
164
|
+
yield chunk.__data.delta.text;
|
|
156
165
|
}
|
|
157
|
-
|
|
158
|
-
}
|
|
166
|
+
}
|
|
167
|
+
yield { __usage: usage };
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
/**
|
|
@@ -163,7 +172,7 @@ class AnthropicProvider {
|
|
|
163
172
|
* @param {Array} messages - Conversation messages
|
|
164
173
|
* @param {Array} tools - Tool definitions in Anthropic format
|
|
165
174
|
* @param {object} [options]
|
|
166
|
-
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
|
|
175
|
+
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string, usage: object}>}
|
|
167
176
|
*/
|
|
168
177
|
async chatWithTools(messages, tools, options = {}) {
|
|
169
178
|
const model = options.model || this.model;
|
|
@@ -200,6 +209,8 @@ class AnthropicProvider {
|
|
|
200
209
|
|
|
201
210
|
const json = await res.json();
|
|
202
211
|
const stopReason = json.stop_reason || 'end_turn';
|
|
212
|
+
const apiUsage = json.usage || {};
|
|
213
|
+
const usage = { inputTokens: apiUsage.input_tokens || 0, outputTokens: apiUsage.output_tokens || 0 };
|
|
203
214
|
|
|
204
215
|
// Check for tool_use blocks
|
|
205
216
|
const toolBlocks = (json.content || []).filter(b => b.type === 'tool_use');
|
|
@@ -212,6 +223,7 @@ class AnthropicProvider {
|
|
|
212
223
|
arguments: b.input,
|
|
213
224
|
})),
|
|
214
225
|
stopReason,
|
|
226
|
+
usage,
|
|
215
227
|
_raw: json.content,
|
|
216
228
|
};
|
|
217
229
|
}
|
|
@@ -222,6 +234,7 @@ class AnthropicProvider {
|
|
|
222
234
|
type: 'text',
|
|
223
235
|
content: textBlocks.map(b => b.text).join(''),
|
|
224
236
|
stopReason,
|
|
237
|
+
usage,
|
|
225
238
|
};
|
|
226
239
|
}
|
|
227
240
|
|
|
@@ -324,6 +337,11 @@ class OpenAIProvider {
|
|
|
324
337
|
messages,
|
|
325
338
|
};
|
|
326
339
|
|
|
340
|
+
// Request usage data in streaming mode
|
|
341
|
+
if (stream) {
|
|
342
|
+
body.stream_options = { include_usage: true };
|
|
343
|
+
}
|
|
344
|
+
|
|
327
345
|
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
328
346
|
method: 'POST',
|
|
329
347
|
headers: {
|
|
@@ -342,14 +360,25 @@ class OpenAIProvider {
|
|
|
342
360
|
const json = await res.json();
|
|
343
361
|
const text = json.choices?.[0]?.message?.content || '';
|
|
344
362
|
yield text;
|
|
363
|
+
const apiUsage = json.usage || {};
|
|
364
|
+
yield { __usage: { inputTokens: apiUsage.prompt_tokens || 0, outputTokens: apiUsage.completion_tokens || 0 } };
|
|
345
365
|
return;
|
|
346
366
|
}
|
|
347
367
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
368
|
+
// Manual SSE loop to capture usage from final streaming chunk
|
|
369
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
370
|
+
for await (const chunk of parseSSEWithMeta(res.body)) {
|
|
371
|
+
const data = chunk.__data;
|
|
372
|
+
if (data === '[DONE]') continue;
|
|
373
|
+
// Final chunk with usage stats (stream_options: include_usage)
|
|
374
|
+
if (data?.usage) {
|
|
375
|
+
usage.inputTokens = data.usage.prompt_tokens || 0;
|
|
376
|
+
usage.outputTokens = data.usage.completion_tokens || 0;
|
|
377
|
+
}
|
|
378
|
+
const content = data?.choices?.[0]?.delta?.content;
|
|
379
|
+
if (content) yield content;
|
|
380
|
+
}
|
|
381
|
+
yield { __usage: usage };
|
|
353
382
|
}
|
|
354
383
|
|
|
355
384
|
/**
|
|
@@ -357,7 +386,7 @@ class OpenAIProvider {
|
|
|
357
386
|
* @param {Array} messages - Conversation messages
|
|
358
387
|
* @param {Array} tools - Tool definitions in OpenAI format
|
|
359
388
|
* @param {object} [options]
|
|
360
|
-
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
|
|
389
|
+
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string, usage: object}>}
|
|
361
390
|
*/
|
|
362
391
|
async chatWithTools(messages, tools, options = {}) {
|
|
363
392
|
const model = options.model || this.model;
|
|
@@ -389,6 +418,8 @@ class OpenAIProvider {
|
|
|
389
418
|
const choice = json.choices?.[0] || {};
|
|
390
419
|
const msg = choice.message || {};
|
|
391
420
|
const stopReason = choice.finish_reason || 'stop';
|
|
421
|
+
const apiUsage = json.usage || {};
|
|
422
|
+
const usage = { inputTokens: apiUsage.prompt_tokens || 0, outputTokens: apiUsage.completion_tokens || 0 };
|
|
392
423
|
|
|
393
424
|
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
394
425
|
return {
|
|
@@ -401,6 +432,7 @@ class OpenAIProvider {
|
|
|
401
432
|
: tc.function.arguments,
|
|
402
433
|
})),
|
|
403
434
|
stopReason,
|
|
435
|
+
usage,
|
|
404
436
|
_raw: msg,
|
|
405
437
|
};
|
|
406
438
|
}
|
|
@@ -409,6 +441,7 @@ class OpenAIProvider {
|
|
|
409
441
|
type: 'text',
|
|
410
442
|
content: msg.content || '',
|
|
411
443
|
stopReason,
|
|
444
|
+
usage,
|
|
412
445
|
};
|
|
413
446
|
}
|
|
414
447
|
|
|
@@ -502,14 +535,25 @@ class OllamaProvider {
|
|
|
502
535
|
const json = await res.json();
|
|
503
536
|
const text = json.choices?.[0]?.message?.content || '';
|
|
504
537
|
yield text;
|
|
538
|
+
// Ollama may not return usage, default to 0
|
|
539
|
+
const apiUsage = json.usage || {};
|
|
540
|
+
yield { __usage: { inputTokens: apiUsage.prompt_tokens || 0, outputTokens: apiUsage.completion_tokens || 0 } };
|
|
505
541
|
return;
|
|
506
542
|
}
|
|
507
543
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
544
|
+
// Manual SSE loop (Ollama may not support stream_options)
|
|
545
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
546
|
+
for await (const chunk of parseSSEWithMeta(res.body)) {
|
|
547
|
+
const data = chunk.__data;
|
|
548
|
+
if (data === '[DONE]') continue;
|
|
549
|
+
if (data?.usage) {
|
|
550
|
+
usage.inputTokens = data.usage.prompt_tokens || 0;
|
|
551
|
+
usage.outputTokens = data.usage.completion_tokens || 0;
|
|
552
|
+
}
|
|
553
|
+
const content = data?.choices?.[0]?.delta?.content;
|
|
554
|
+
if (content) yield content;
|
|
555
|
+
}
|
|
556
|
+
yield { __usage: usage };
|
|
513
557
|
}
|
|
514
558
|
|
|
515
559
|
/**
|
|
@@ -517,7 +561,7 @@ class OllamaProvider {
|
|
|
517
561
|
* @param {Array} messages - Conversation messages
|
|
518
562
|
* @param {Array} tools - Tool definitions in OpenAI format
|
|
519
563
|
* @param {object} [options]
|
|
520
|
-
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
|
|
564
|
+
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string, usage: object}>}
|
|
521
565
|
*/
|
|
522
566
|
async chatWithTools(messages, tools, options = {}) {
|
|
523
567
|
const model = options.model || this.model;
|
|
@@ -544,6 +588,9 @@ class OllamaProvider {
|
|
|
544
588
|
const choice = json.choices?.[0] || {};
|
|
545
589
|
const msg = choice.message || {};
|
|
546
590
|
const stopReason = choice.finish_reason || 'stop';
|
|
591
|
+
// Ollama may not return usage, default to 0
|
|
592
|
+
const apiUsage = json.usage || {};
|
|
593
|
+
const usage = { inputTokens: apiUsage.prompt_tokens || 0, outputTokens: apiUsage.completion_tokens || 0 };
|
|
547
594
|
|
|
548
595
|
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
549
596
|
return {
|
|
@@ -556,6 +603,7 @@ class OllamaProvider {
|
|
|
556
603
|
: tc.function.arguments,
|
|
557
604
|
})),
|
|
558
605
|
stopReason,
|
|
606
|
+
usage,
|
|
559
607
|
_raw: msg,
|
|
560
608
|
};
|
|
561
609
|
}
|
|
@@ -564,6 +612,7 @@ class OllamaProvider {
|
|
|
564
612
|
type: 'text',
|
|
565
613
|
content: msg.content || '',
|
|
566
614
|
stopReason,
|
|
615
|
+
usage,
|
|
567
616
|
};
|
|
568
617
|
}
|
|
569
618
|
|
|
@@ -622,6 +671,64 @@ class OllamaProvider {
|
|
|
622
671
|
// SSE Stream Parser
|
|
623
672
|
// ============================================
|
|
624
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Parse a Server-Sent Events stream, yielding raw event+data pairs.
|
|
676
|
+
* Unlike parseSSE, this preserves event types and full data objects
|
|
677
|
+
* so callers can extract both content and metadata (e.g. usage stats).
|
|
678
|
+
*
|
|
679
|
+
* @param {ReadableStream} body - Response body stream
|
|
680
|
+
* @yields {{ __event: string|null, __data: object|string }} Parsed SSE events
|
|
681
|
+
*/
|
|
682
|
+
async function* parseSSEWithMeta(body) {
|
|
683
|
+
const decoder = new TextDecoder();
|
|
684
|
+
let buffer = '';
|
|
685
|
+
let currentEvent = null;
|
|
686
|
+
|
|
687
|
+
for await (const chunk of body) {
|
|
688
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
689
|
+
|
|
690
|
+
const lines = buffer.split('\n');
|
|
691
|
+
buffer = lines.pop() || '';
|
|
692
|
+
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
if (line.startsWith('event: ')) {
|
|
695
|
+
currentEvent = line.slice(7).trim();
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (line.startsWith('data: ')) {
|
|
700
|
+
const rawData = line.slice(6);
|
|
701
|
+
|
|
702
|
+
if (rawData === '[DONE]') {
|
|
703
|
+
yield { __event: currentEvent, __data: '[DONE]' };
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let parsed;
|
|
708
|
+
try {
|
|
709
|
+
parsed = JSON.parse(rawData);
|
|
710
|
+
} catch {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
yield { __event: currentEvent, __data: parsed };
|
|
715
|
+
currentEvent = null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Process remaining buffer
|
|
721
|
+
if (buffer.trim() && buffer.startsWith('data: ')) {
|
|
722
|
+
const rawData = buffer.slice(6);
|
|
723
|
+
if (rawData !== '[DONE]') {
|
|
724
|
+
try {
|
|
725
|
+
const parsed = JSON.parse(rawData);
|
|
726
|
+
yield { __event: currentEvent, __data: parsed };
|
|
727
|
+
} catch { /* skip */ }
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
625
732
|
/**
|
|
626
733
|
* Parse a Server-Sent Events stream.
|
|
627
734
|
* @param {ReadableStream} body - Response body stream
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const CATEGORIES = ['retrieval', 'analysis', 'ingestion', 'domain-specific', 'utility', 'integration'];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run quality audit on a workflow definition and its package.
|
|
10
|
+
* @param {object} definition - Parsed workflow JSON
|
|
11
|
+
* @param {object} pkg - Parsed package.json
|
|
12
|
+
* @param {string} [packagePath] - Path to the package directory
|
|
13
|
+
* @returns {Array<{level: string, message: string}>}
|
|
14
|
+
*/
|
|
15
|
+
function qualityAudit(definition, pkg, packagePath) {
|
|
16
|
+
const issues = [];
|
|
17
|
+
|
|
18
|
+
// Package metadata checks
|
|
19
|
+
if (!pkg.description || pkg.description.length < 20) {
|
|
20
|
+
issues.push({ level: 'error', message: 'Package description too short (min 20 chars)' });
|
|
21
|
+
}
|
|
22
|
+
if (!pkg.author) {
|
|
23
|
+
issues.push({ level: 'error', message: 'Package must have an author' });
|
|
24
|
+
}
|
|
25
|
+
if (!pkg.license) {
|
|
26
|
+
issues.push({ level: 'warning', message: 'No license specified' });
|
|
27
|
+
}
|
|
28
|
+
if (!pkg.vai?.category || !CATEGORIES.includes(pkg.vai.category)) {
|
|
29
|
+
issues.push({ level: 'error', message: 'Invalid or missing vai.category' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// README checks
|
|
33
|
+
if (packagePath) {
|
|
34
|
+
const readmePath = path.join(packagePath, 'README.md');
|
|
35
|
+
if (!fs.existsSync(readmePath)) {
|
|
36
|
+
issues.push({ level: 'error', message: 'Missing README.md' });
|
|
37
|
+
} else {
|
|
38
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
39
|
+
if (readme.length < 200) {
|
|
40
|
+
issues.push({ level: 'warning', message: 'README is very short (< 200 chars)' });
|
|
41
|
+
}
|
|
42
|
+
if (!readme.includes('## Usage') && !readme.includes('## Install')) {
|
|
43
|
+
issues.push({ level: 'warning', message: 'README should include Usage or Install section' });
|
|
44
|
+
}
|
|
45
|
+
if (readme.includes('TODO')) {
|
|
46
|
+
issues.push({ level: 'warning', message: 'README contains TODO placeholders' });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Workflow definition quality
|
|
52
|
+
if (definition && Array.isArray(definition.steps)) {
|
|
53
|
+
if (definition.steps.length === 1) {
|
|
54
|
+
issues.push({ level: 'suggestion', message: 'Single-step workflows may not warrant a package — consider documenting as a CLI example instead' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Branding
|
|
59
|
+
if (!definition?.branding?.icon) {
|
|
60
|
+
issues.push({ level: 'suggestion', message: 'Consider adding branding.icon for store display' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Naming — should be descriptive, not generic
|
|
64
|
+
if (definition?.name && /^(test|my|workflow|demo|example)/i.test(definition.name)) {
|
|
65
|
+
issues.push({ level: 'warning', message: `Workflow name "${definition.name}" is too generic` });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { qualityAudit, CATEGORIES };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
"evil.com",
|
|
3
|
+
"malware.com",
|
|
4
|
+
"requestbin.com",
|
|
5
|
+
"webhook.site",
|
|
6
|
+
"pipedream.net",
|
|
7
|
+
"hookbin.com",
|
|
8
|
+
"requestcatcher.com",
|
|
9
|
+
"canarytokens.com",
|
|
10
|
+
"burpcollaborator.net",
|
|
11
|
+
"interact.sh",
|
|
12
|
+
"oastify.com",
|
|
13
|
+
"dnslog.cn",
|
|
14
|
+
"ceye.io",
|
|
15
|
+
"bxss.me",
|
|
16
|
+
"xss.ht"
|
|
17
|
+
]
|