@vectorasystems/cli 0.1.3 → 0.2.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/bin/vectora.js +105 -0
- package/package.json +1 -1
- package/src/commands/artifacts.js +13 -1
- package/src/commands/feedback.js +54 -0
- package/src/commands/handoff.js +84 -0
- package/src/commands/lock.js +74 -0
- package/src/commands/projects.js +18 -0
- package/src/commands/settings.js +94 -0
- package/src/commands/validate.js +84 -0
- package/src/lib/api-client.js +38 -0
- package/src/lib/constants.js +5 -2
- package/src/tui/components/ArtifactBrowser.js +14 -2
package/bin/vectora.js
CHANGED
|
@@ -94,6 +94,14 @@ projects
|
|
|
94
94
|
await m.remove(id);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
projects
|
|
98
|
+
.command('archive <id>')
|
|
99
|
+
.description('Archive a project')
|
|
100
|
+
.action(async (id) => {
|
|
101
|
+
const m = await import('../src/commands/projects.js');
|
|
102
|
+
await m.archive(id);
|
|
103
|
+
});
|
|
104
|
+
|
|
97
105
|
// ── Chat ──────────────────────────────────────────────────────────────────────
|
|
98
106
|
program
|
|
99
107
|
.command('chat')
|
|
@@ -202,6 +210,103 @@ config
|
|
|
202
210
|
await m.reset();
|
|
203
211
|
});
|
|
204
212
|
|
|
213
|
+
// ── Validate ─────────────────────────────────────────────────────────────────
|
|
214
|
+
program
|
|
215
|
+
.command('validate')
|
|
216
|
+
.description('Run 4-agent validation pipeline on the idea')
|
|
217
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
218
|
+
.option('-f, --format <fmt>', 'Output format: table or json')
|
|
219
|
+
.action(async (opts) => {
|
|
220
|
+
const m = await import('../src/commands/validate.js');
|
|
221
|
+
await m.validate(opts);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── Lock ─────────────────────────────────────────────────────────────────────
|
|
225
|
+
program
|
|
226
|
+
.command('lock')
|
|
227
|
+
.description('Lock the idea scope (requires 100% readiness)')
|
|
228
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
229
|
+
.action(async (opts) => {
|
|
230
|
+
const m = await import('../src/commands/lock.js');
|
|
231
|
+
await m.lock(opts);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── Feedback ─────────────────────────────────────────────────────────────────
|
|
235
|
+
program
|
|
236
|
+
.command('feedback')
|
|
237
|
+
.description('Submit feedback on a completed phase')
|
|
238
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
239
|
+
.requiredOption('--phase <phaseId>', 'Phase ID to give feedback on')
|
|
240
|
+
.requiredOption('--message <text>', 'Feedback message')
|
|
241
|
+
.option('--rating <n>', 'Rating 1-5')
|
|
242
|
+
.action(async (opts) => {
|
|
243
|
+
const m = await import('../src/commands/feedback.js');
|
|
244
|
+
await m.feedback(opts);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ── Handoff ──────────────────────────────────────────────────────────────────
|
|
248
|
+
const handoff = program.command('handoff').description('View or trigger handoff bundles');
|
|
249
|
+
|
|
250
|
+
handoff
|
|
251
|
+
.command('show')
|
|
252
|
+
.description('Show current handoff bundle')
|
|
253
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
254
|
+
.action(async (opts) => {
|
|
255
|
+
const m = await import('../src/commands/handoff.js');
|
|
256
|
+
await m.show(opts);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
handoff
|
|
260
|
+
.command('trigger')
|
|
261
|
+
.description('Generate a new handoff bundle')
|
|
262
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
263
|
+
.option('-t, --target <target>', 'Target: claudecode, codex, generic', 'generic')
|
|
264
|
+
.action(async (opts) => {
|
|
265
|
+
const m = await import('../src/commands/handoff.js');
|
|
266
|
+
await m.trigger(opts);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
270
|
+
const settings = program.command('settings').description('Manage API keys and preferences');
|
|
271
|
+
|
|
272
|
+
const settingsApiKeys = settings.command('api-keys').description('View or update API keys');
|
|
273
|
+
|
|
274
|
+
settingsApiKeys
|
|
275
|
+
.command('show')
|
|
276
|
+
.description('Show configured API keys (masked)')
|
|
277
|
+
.action(async () => {
|
|
278
|
+
const m = await import('../src/commands/settings.js');
|
|
279
|
+
await m.apiKeys();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
settingsApiKeys
|
|
283
|
+
.command('set')
|
|
284
|
+
.description('Set an API key for a provider')
|
|
285
|
+
.requiredOption('--provider <name>', 'Provider: anthropic, openai, google')
|
|
286
|
+
.requiredOption('--key <value>', 'API key (empty string to remove)')
|
|
287
|
+
.action(async (opts) => {
|
|
288
|
+
const m = await import('../src/commands/settings.js');
|
|
289
|
+
await m.apiKeysSet(opts);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const settingsKeyMode = settings.command('key-mode').description('View or set key mode');
|
|
293
|
+
|
|
294
|
+
settingsKeyMode
|
|
295
|
+
.command('show')
|
|
296
|
+
.description('Show current key mode preference')
|
|
297
|
+
.action(async () => {
|
|
298
|
+
const m = await import('../src/commands/settings.js');
|
|
299
|
+
await m.keyMode();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
settingsKeyMode
|
|
303
|
+
.command('set <mode>')
|
|
304
|
+
.description('Set key mode: own or server')
|
|
305
|
+
.action(async (mode) => {
|
|
306
|
+
const m = await import('../src/commands/settings.js');
|
|
307
|
+
await m.keyModeSet(mode);
|
|
308
|
+
});
|
|
309
|
+
|
|
205
310
|
// ── TUI ───────────────────────────────────────────────────────────────────────
|
|
206
311
|
program
|
|
207
312
|
.command('ui')
|
package/package.json
CHANGED
|
@@ -101,10 +101,22 @@ export async function download(id, opts) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Get presigned URL + suggested filename from API
|
|
104
|
-
const { url, filename } = await getArtifactDownloadUrl(id);
|
|
104
|
+
const { url, filename, inlineBase64 } = await getArtifactDownloadUrl(id);
|
|
105
105
|
|
|
106
106
|
const outPath = resolve(opts.out ?? filename);
|
|
107
107
|
|
|
108
|
+
// Local-storage fallback: API returns the file bytes inline when no
|
|
109
|
+
// presigned URL backend is configured.
|
|
110
|
+
if (inlineBase64) {
|
|
111
|
+
const buffer = Buffer.from(inlineBase64, 'base64');
|
|
112
|
+
await writeFile(outPath, buffer);
|
|
113
|
+
success(`Saved to ${chalk.bold(outPath)} ${chalk.dim(`(${(buffer.length / 1024).toFixed(1)} KB)`)}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!url) {
|
|
117
|
+
throw new Error('Artifact download URL was not provided by the API');
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
// Fetch directly from R2 via presigned URL
|
|
109
121
|
const res = await fetch(url, { signal: AbortSignal.timeout(60_000) });
|
|
110
122
|
if (!res.ok) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vectora/cli — feedback command: submit feedback on a phase
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { submitFeedback } from '../lib/api-client.js';
|
|
4
|
+
import { getConfigValue } from '../lib/config-store.js';
|
|
5
|
+
import { handleError } from '../lib/errors.js';
|
|
6
|
+
import { success, warn } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
const VALID_RATINGS = [1, 2, 3, 4, 5];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* vectora feedback --phase <phaseId> --message <text> [--rating <1-5>] [--project <id>]
|
|
12
|
+
*/
|
|
13
|
+
export async function feedback(opts) {
|
|
14
|
+
try {
|
|
15
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
16
|
+
if (!projectId) {
|
|
17
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!opts.phase) {
|
|
23
|
+
console.error(chalk.red('--phase <phaseId> is required'));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!opts.message) {
|
|
29
|
+
console.error(chalk.red('--message <text> is required'));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const body = {
|
|
35
|
+
phaseId: opts.phase,
|
|
36
|
+
feedbackText: opts.message,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (opts.rating !== undefined) {
|
|
40
|
+
const rating = Number(opts.rating);
|
|
41
|
+
if (!VALID_RATINGS.includes(rating)) {
|
|
42
|
+
console.error(chalk.red('--rating must be 1-5'));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
body.rating = rating;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await submitFeedback(projectId, body);
|
|
50
|
+
success(`Feedback submitted (artifact: ${result.artifact?.id ?? 'saved'})`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
handleError(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @vectora/cli — handoff command: view or trigger handoff bundles
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getHandoff, triggerHandoff } from '../lib/api-client.js';
|
|
5
|
+
import { getConfigValue } from '../lib/config-store.js';
|
|
6
|
+
import { handleError } from '../lib/errors.js';
|
|
7
|
+
import { success, warn, info, renderTime } from '../lib/output.js';
|
|
8
|
+
|
|
9
|
+
const VALID_TARGETS = ['claudecode', 'codex', 'generic'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* vectora handoff show [--project <id>]
|
|
13
|
+
*/
|
|
14
|
+
export async function show(opts) {
|
|
15
|
+
try {
|
|
16
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
17
|
+
if (!projectId) {
|
|
18
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = await getHandoff(projectId);
|
|
24
|
+
const bundle = result.bundle;
|
|
25
|
+
|
|
26
|
+
if (!bundle) {
|
|
27
|
+
warn('No handoff bundle found. Run: vectora handoff trigger');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(chalk.cyan.bold(' Handoff Bundle'));
|
|
33
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
34
|
+
console.log(` ${chalk.dim('ID:')} ${bundle.id}`);
|
|
35
|
+
console.log(` ${chalk.dim('Version:')} v${bundle.version}`);
|
|
36
|
+
console.log(` ${chalk.dim('Target:')} ${bundle.target}`);
|
|
37
|
+
console.log(` ${chalk.dim('Created:')} ${renderTime(bundle.createdAt)}`);
|
|
38
|
+
if (bundle.publicUrl) {
|
|
39
|
+
console.log(` ${chalk.dim('URL:')} ${chalk.cyan(bundle.publicUrl)}`);
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
handleError(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* vectora handoff trigger [--target <target>] [--project <id>]
|
|
49
|
+
*/
|
|
50
|
+
export async function trigger(opts) {
|
|
51
|
+
try {
|
|
52
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
53
|
+
if (!projectId) {
|
|
54
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const target = opts.target ?? 'generic';
|
|
60
|
+
if (!VALID_TARGETS.includes(target)) {
|
|
61
|
+
console.error(chalk.red(`Invalid target: ${target}. Valid: ${VALID_TARGETS.join(', ')}`));
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const spinner = ora({ text: `Triggering handoff (target: ${target})...`, color: 'cyan' }).start();
|
|
67
|
+
|
|
68
|
+
let result;
|
|
69
|
+
try {
|
|
70
|
+
result = await triggerHandoff(projectId, target);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
spinner.fail('Failed to trigger handoff');
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
spinner.succeed('Handoff triggered');
|
|
77
|
+
if (result.jobId) {
|
|
78
|
+
info(`Job ID: ${result.jobId}`);
|
|
79
|
+
info('Check progress with: vectora status');
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
handleError(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @vectora/cli — lock command: lock idea scope
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { runPhase, getProject } from '../lib/api-client.js';
|
|
5
|
+
import { getConfig, getConfigValue } from '../lib/config-store.js';
|
|
6
|
+
import { requireToken } from '../lib/auth-store.js';
|
|
7
|
+
import { streamPhaseProgress } from '../lib/sse-client.js';
|
|
8
|
+
import { handleError } from '../lib/errors.js';
|
|
9
|
+
import { warn, success, info } from '../lib/output.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* vectora lock [--project <id>]
|
|
13
|
+
*/
|
|
14
|
+
export async function lock(opts) {
|
|
15
|
+
try {
|
|
16
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
17
|
+
if (!projectId) {
|
|
18
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const token = await requireToken();
|
|
24
|
+
const { apiUrl } = getConfig();
|
|
25
|
+
|
|
26
|
+
const spinner = ora({ text: 'Locking idea scope...', color: 'cyan' }).start();
|
|
27
|
+
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
result = await runPhase(projectId, 'lock-idea');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
spinner.fail('Failed to start lock-idea');
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { jobId } = result;
|
|
37
|
+
spinner.text = `lock-idea queued (job: ${jobId?.slice(0, 8) ?? '?'})`;
|
|
38
|
+
|
|
39
|
+
// Stream SSE progress
|
|
40
|
+
try {
|
|
41
|
+
for await (const { event, data } of streamPhaseProgress(apiUrl, jobId, token)) {
|
|
42
|
+
switch (event) {
|
|
43
|
+
case 'job:status':
|
|
44
|
+
spinner.text = `lock-idea: ${data.status ?? 'processing'}`;
|
|
45
|
+
break;
|
|
46
|
+
case 'job:progress':
|
|
47
|
+
spinner.text = `lock-idea: ${data.step ?? data.message ?? ''}`;
|
|
48
|
+
break;
|
|
49
|
+
case 'job:completed':
|
|
50
|
+
spinner.succeed('Idea scope locked');
|
|
51
|
+
info('Scope is now frozen. Run validation next: vectora validate');
|
|
52
|
+
return;
|
|
53
|
+
case 'job:failed':
|
|
54
|
+
spinner.fail('lock-idea failed');
|
|
55
|
+
console.error(chalk.red(data.error ?? 'Unknown error'));
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
case 'job:timeout':
|
|
59
|
+
spinner.warn('lock-idea timed out');
|
|
60
|
+
info(`Check status with: vectora status --project ${projectId}`);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
spinner.warn('lock-idea — SSE stream ended');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
spinner.fail('lock-idea — lost connection');
|
|
68
|
+
info(`Phase may still be running. Check: vectora status --project ${projectId}`);
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
handleError(err);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/commands/projects.js
CHANGED
|
@@ -185,6 +185,24 @@ export async function select(id) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* vectora projects archive <id>
|
|
190
|
+
*/
|
|
191
|
+
export async function archive(id) {
|
|
192
|
+
try {
|
|
193
|
+
if (!id) {
|
|
194
|
+
console.error(chalk.red('Error — project ID is required'));
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const project = await patchProject(id, { status: 'archived' });
|
|
200
|
+
success(`Archived project ${chalk.bold(project.name)} ${chalk.dim(`(${project.id})`)}`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
handleError(err);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
/**
|
|
189
207
|
* vectora projects delete <id>
|
|
190
208
|
*/
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @vectora/cli — settings command: manage API keys and key mode
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getApiKeys, updateApiKeys, getKeyMode, updateKeyMode } from '../lib/api-client.js';
|
|
4
|
+
import { handleError } from '../lib/errors.js';
|
|
5
|
+
import { success, warn } from '../lib/output.js';
|
|
6
|
+
|
|
7
|
+
const VALID_PROVIDERS = ['anthropic', 'openai', 'google'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* vectora settings api-keys — show configured API keys (masked)
|
|
11
|
+
*/
|
|
12
|
+
export async function apiKeys() {
|
|
13
|
+
try {
|
|
14
|
+
const keys = await getApiKeys();
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(chalk.cyan.bold(' API Keys'));
|
|
17
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
18
|
+
for (const provider of VALID_PROVIDERS) {
|
|
19
|
+
const value = keys[provider];
|
|
20
|
+
const display = value ? chalk.green(value) : chalk.dim('not set');
|
|
21
|
+
console.log(` ${provider.padEnd(12)} ${display}`);
|
|
22
|
+
}
|
|
23
|
+
console.log();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
handleError(err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* vectora settings api-keys set --provider <name> --key <value>
|
|
31
|
+
*/
|
|
32
|
+
export async function apiKeysSet(opts) {
|
|
33
|
+
try {
|
|
34
|
+
if (!opts.provider) {
|
|
35
|
+
console.error(chalk.red('--provider is required (anthropic, openai, google)'));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!VALID_PROVIDERS.includes(opts.provider)) {
|
|
40
|
+
console.error(chalk.red(`Invalid provider. Valid: ${VALID_PROVIDERS.join(', ')}`));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!opts.key && opts.key !== '') {
|
|
45
|
+
console.error(chalk.red('--key is required (use empty string to delete)'));
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await updateApiKeys({ [opts.provider]: opts.key });
|
|
51
|
+
if (opts.key === '') {
|
|
52
|
+
success(`Removed ${opts.provider} API key`);
|
|
53
|
+
} else {
|
|
54
|
+
success(`Updated ${opts.provider} API key`);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
handleError(err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* vectora settings key-mode — show current key mode
|
|
63
|
+
*/
|
|
64
|
+
export async function keyMode() {
|
|
65
|
+
try {
|
|
66
|
+
const result = await getKeyMode();
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.cyan.bold(' Key Mode'));
|
|
69
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
70
|
+
console.log(` ${chalk.dim('Prefer own keys:')} ${result.preferOwnKeys ? chalk.green('yes') : chalk.dim('no')}`);
|
|
71
|
+
console.log(` ${chalk.dim('Has own keys:')} ${result.hasOwnKeys ? chalk.green('yes') : chalk.dim('no')}`);
|
|
72
|
+
console.log();
|
|
73
|
+
} catch (err) {
|
|
74
|
+
handleError(err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* vectora settings key-mode set <own|server>
|
|
80
|
+
*/
|
|
81
|
+
export async function keyModeSet(mode) {
|
|
82
|
+
try {
|
|
83
|
+
if (mode !== 'own' && mode !== 'server') {
|
|
84
|
+
console.error(chalk.red('Mode must be "own" or "server"'));
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const preferOwnKeys = mode === 'own';
|
|
89
|
+
await updateKeyMode(preferOwnKeys);
|
|
90
|
+
success(`Key mode set to: ${mode}`);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
handleError(err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @vectora/cli — validate command: run 4-agent validation pipeline
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { runValidation } from '../lib/api-client.js';
|
|
5
|
+
import { getConfigValue } from '../lib/config-store.js';
|
|
6
|
+
import { handleError } from '../lib/errors.js';
|
|
7
|
+
import { renderTable, warn, success, info } from '../lib/output.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* vectora validate [--project <id>] [--format <fmt>]
|
|
11
|
+
*/
|
|
12
|
+
export async function validate(opts) {
|
|
13
|
+
try {
|
|
14
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
15
|
+
if (!projectId) {
|
|
16
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spinner = ora({ text: 'Running validation pipeline (this may take a minute)...', color: 'cyan' }).start();
|
|
22
|
+
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
result = await runValidation(projectId);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spinner.fail('Validation failed');
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
spinner.succeed('Validation complete');
|
|
32
|
+
console.log();
|
|
33
|
+
|
|
34
|
+
// Summary
|
|
35
|
+
const recColor = result.recommendation === 'go' ? chalk.green :
|
|
36
|
+
result.recommendation === 'no-go' ? chalk.red : chalk.yellow;
|
|
37
|
+
console.log(` ${chalk.cyan.bold('Score:')} ${result.score}/100`);
|
|
38
|
+
console.log(` ${chalk.cyan.bold('Verdict:')} ${recColor(result.recommendation.toUpperCase())}`);
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
if (opts.format === 'json') {
|
|
42
|
+
console.log(JSON.stringify(result, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Report table
|
|
47
|
+
const report = result.report;
|
|
48
|
+
if (report) {
|
|
49
|
+
if (report.viabilityScore) {
|
|
50
|
+
console.log(chalk.cyan.bold(' Viability'));
|
|
51
|
+
console.log(` ${report.viabilityScore.reasoning}`);
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (report.stressTest?.criticalAssumptions?.length) {
|
|
56
|
+
console.log(chalk.cyan.bold(' Critical Assumptions'));
|
|
57
|
+
for (const a of report.stressTest.criticalAssumptions) {
|
|
58
|
+
console.log(` ${chalk.dim('•')} ${a}`);
|
|
59
|
+
}
|
|
60
|
+
console.log();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (report.viabilityScore?.topRisks?.length) {
|
|
64
|
+
console.log(chalk.cyan.bold(' Top Risks'));
|
|
65
|
+
for (const r of report.viabilityScore.topRisks) {
|
|
66
|
+
console.log(` ${chalk.dim('•')} ${r}`);
|
|
67
|
+
}
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (result.warnings?.length) {
|
|
73
|
+
for (const w of result.warnings) {
|
|
74
|
+
warn(w);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.artifactId) {
|
|
79
|
+
info(`Artifact saved: ${result.artifactId}`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
handleError(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/lib/api-client.js
CHANGED
|
@@ -125,8 +125,30 @@ export async function getArtifactDownloadUrl(artifactId) {
|
|
|
125
125
|
return authedFetch(`/v1/artifacts/${artifactId}/download`);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export async function runValidation(projectId) {
|
|
131
|
+
return authedFetch(`/v1/projects/${projectId}/validate`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body: {},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Feedback ───────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export async function submitFeedback(projectId, body) {
|
|
140
|
+
return authedFetch(`/v1/projects/${projectId}/feedback`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
body,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
// ─── Handoff ─────────────────────────────────────────────────────────────────
|
|
129
147
|
|
|
148
|
+
export async function getHandoff(projectId) {
|
|
149
|
+
return authedFetch(`/v1/projects/${projectId}/handoff`);
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
export async function triggerHandoff(projectId, target) {
|
|
131
153
|
return authedFetch(`/v1/projects/${projectId}/handoff`, {
|
|
132
154
|
method: 'POST',
|
|
@@ -159,6 +181,22 @@ export async function getApiKeys() {
|
|
|
159
181
|
return res.keys;
|
|
160
182
|
}
|
|
161
183
|
|
|
184
|
+
export async function updateApiKeys(keys) {
|
|
185
|
+
const res = await authedFetch('/v1/settings/api-keys', { method: 'PUT', body: keys });
|
|
186
|
+
return res.keys;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function getKeyMode() {
|
|
190
|
+
return authedFetch('/v1/settings/key-mode');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function updateKeyMode(preferOwnKeys) {
|
|
194
|
+
return authedFetch('/v1/settings/key-mode', {
|
|
195
|
+
method: 'PATCH',
|
|
196
|
+
body: { preferOwnKeys },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
162
200
|
// ─── Auth (unauthenticated or special) ───────────────────────────────────────
|
|
163
201
|
|
|
164
202
|
export async function getMe(token) {
|
package/src/lib/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vectora/cli — constants
|
|
2
2
|
// Inlined from @vectora/engine to avoid dependency in API-connected mode.
|
|
3
3
|
|
|
4
|
-
export const VERSION = '0.
|
|
4
|
+
export const VERSION = '0.2.0';
|
|
5
5
|
|
|
6
6
|
export const BRAND = {
|
|
7
7
|
name: 'VECTORA',
|
|
@@ -28,8 +28,11 @@ export const TEMPER_PHASES = [
|
|
|
28
28
|
'plan-platform',
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
/** Pre-pipeline phases (idea → validation → lock). */
|
|
32
|
+
export const PRE_PIPELINE_PHASES = ['lock-idea', 'run-validation'];
|
|
33
|
+
|
|
31
34
|
/** All valid phases for `vectora run`. */
|
|
32
|
-
export const VALID_PHASES = [...FORGE_PHASES, ...TEMPER_PHASES];
|
|
35
|
+
export const VALID_PHASES = [...PRE_PIPELINE_PHASES, ...FORGE_PHASES, ...TEMPER_PHASES];
|
|
33
36
|
|
|
34
37
|
/** Directories to skip during workspace scanning. */
|
|
35
38
|
export const WORKSPACE_IGNORED_DIRS = new Set([
|
|
@@ -43,11 +43,23 @@ export function ArtifactBrowser({ projectId, onBack }) {
|
|
|
43
43
|
setSavedPath(null);
|
|
44
44
|
setError(null);
|
|
45
45
|
try {
|
|
46
|
-
const { url, filename } = await getArtifactDownloadUrl(item.value);
|
|
46
|
+
const { url, filename, inlineBase64 } = await getArtifactDownloadUrl(item.value);
|
|
47
|
+
const outPath = resolve(filename);
|
|
48
|
+
|
|
49
|
+
// Local-storage fallback: the API can return inline bytes when no
|
|
50
|
+
// presigned URL backend is available.
|
|
51
|
+
if (inlineBase64) {
|
|
52
|
+
const buffer = Buffer.from(inlineBase64, 'base64');
|
|
53
|
+
await writeFile(outPath, buffer);
|
|
54
|
+
setSavedPath(outPath);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!url) throw new Error('Artifact download URL was not provided by the API');
|
|
59
|
+
|
|
47
60
|
const res = await fetch(url, { signal: AbortSignal.timeout(60_000) });
|
|
48
61
|
if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
|
|
49
62
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
50
|
-
const outPath = resolve(filename);
|
|
51
63
|
await writeFile(outPath, buffer);
|
|
52
64
|
setSavedPath(outPath);
|
|
53
65
|
} catch (err) {
|