vesper-wizard 2.0.3 → 2.0.5
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/wizard.js +253 -13
package/package.json
CHANGED
package/wizard.js
CHANGED
|
@@ -10,6 +10,9 @@ const path = require('path');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
12
|
const { execSync, spawnSync } = require('child_process');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const https = require('https');
|
|
15
|
+
const readline = require('readline');
|
|
13
16
|
|
|
14
17
|
// ── Paths ────────────────────────────────────────────────────
|
|
15
18
|
const HOME = os.homedir();
|
|
@@ -54,6 +57,209 @@ function yellow(text) { return `\x1b[33m${text}\x1b[0m`; }
|
|
|
54
57
|
function red(text) { return `\x1b[31m${text}\x1b[0m`; }
|
|
55
58
|
function magenta(text) { return `\x1b[35m${text}\x1b[0m`; }
|
|
56
59
|
|
|
60
|
+
// ── Vesper API URL resolution ────────────────────────────────
|
|
61
|
+
const VESPER_API_URL = process.env.VESPER_API_URL || '';
|
|
62
|
+
const DEFAULT_VESPER_API_CANDIDATES = [
|
|
63
|
+
'http://localhost:3000',
|
|
64
|
+
'http://127.0.0.1:3000',
|
|
65
|
+
'https://vesper.dev',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ── Device Auth Helpers ──────────────────────────────────────
|
|
69
|
+
function httpJson(method, url, body) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const parsed = new URL(url);
|
|
72
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
73
|
+
const opts = {
|
|
74
|
+
method,
|
|
75
|
+
hostname: parsed.hostname,
|
|
76
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
77
|
+
path: parsed.pathname + parsed.search,
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
};
|
|
80
|
+
const req = lib.request(opts, (res) => {
|
|
81
|
+
let data = '';
|
|
82
|
+
res.on('data', (chunk) => (data += chunk));
|
|
83
|
+
res.on('end', () => {
|
|
84
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
85
|
+
catch { resolve({ status: res.statusCode, body: data }); }
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
req.on('error', reject);
|
|
89
|
+
if (body) req.write(JSON.stringify(body));
|
|
90
|
+
req.end();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function probeDeviceAuth(baseUrl) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await httpJson('POST', `${baseUrl}/api/auth/device/start`);
|
|
97
|
+
if (res.status === 201 && !!res.body && !!res.body.code) {
|
|
98
|
+
return { baseUrl, status: 'ready', response: res.body };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (res.status === 503 && res.body && res.body.requiresSetup) {
|
|
102
|
+
return {
|
|
103
|
+
baseUrl,
|
|
104
|
+
status: 'setup-required',
|
|
105
|
+
response: res.body,
|
|
106
|
+
message: res.body.error || 'Auth storage is not initialized.',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
baseUrl,
|
|
112
|
+
status: 'unreachable',
|
|
113
|
+
response: res.body,
|
|
114
|
+
message: typeof res.body === 'string' ? res.body : JSON.stringify(res.body),
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
baseUrl,
|
|
119
|
+
status: 'unreachable',
|
|
120
|
+
message: error && error.message ? error.message : 'Request failed',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function resolveVesperApiBaseUrl() {
|
|
126
|
+
const candidates = VESPER_API_URL
|
|
127
|
+
? [VESPER_API_URL]
|
|
128
|
+
: DEFAULT_VESPER_API_CANDIDATES;
|
|
129
|
+
|
|
130
|
+
let setupRequiredProbe = null;
|
|
131
|
+
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
const probe = await probeDeviceAuth(candidate);
|
|
134
|
+
if (probe.status === 'ready') {
|
|
135
|
+
return probe;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!setupRequiredProbe && probe.status === 'setup-required') {
|
|
139
|
+
setupRequiredProbe = probe;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return setupRequiredProbe;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function openBrowser(url) {
|
|
147
|
+
try {
|
|
148
|
+
if (process.platform === 'win32') {
|
|
149
|
+
spawnSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
150
|
+
} else if (process.platform === 'darwin') {
|
|
151
|
+
spawnSync('open', [url], { stdio: 'ignore' });
|
|
152
|
+
} else {
|
|
153
|
+
spawnSync('xdg-open', [url], { stdio: 'ignore' });
|
|
154
|
+
}
|
|
155
|
+
} catch { /* browser open is best-effort */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function askYesNo(question) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
161
|
+
rl.question(` ${question} ${dim('[Y/n]')} `, (answer) => {
|
|
162
|
+
rl.close();
|
|
163
|
+
resolve(!answer || answer.toLowerCase().startsWith('y'));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function deviceAuthFlow() {
|
|
169
|
+
console.log(`\n ${cyan('■')} ${bold('Device Authentication')}`);
|
|
170
|
+
console.log(` ${dim('Link your CLI to a Vesper account for cloud features\n')}`);
|
|
171
|
+
|
|
172
|
+
const resolvedApiBaseUrl = await resolveVesperApiBaseUrl();
|
|
173
|
+
if (!resolvedApiBaseUrl) {
|
|
174
|
+
console.log(` ${red('✗')} ${red('Could not reach any Vesper auth endpoint.')}`);
|
|
175
|
+
console.log(` ${dim('Tried:')} ${dim((VESPER_API_URL ? [VESPER_API_URL] : DEFAULT_VESPER_API_CANDIDATES).join(', '))}`);
|
|
176
|
+
console.log(` ${dim('If your landing app is running locally, start it on http://localhost:3000 or set VESPER_API_URL.')}`);
|
|
177
|
+
console.log(` ${dim('Falling back to local-only mode.\n')}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (resolvedApiBaseUrl.status === 'setup-required') {
|
|
182
|
+
console.log(` ${yellow('!')} ${yellow('Reached Vesper auth endpoint, but local auth storage is not initialized.')}`);
|
|
183
|
+
console.log(` ${dim('Endpoint:')} ${dim(resolvedApiBaseUrl.baseUrl)}`);
|
|
184
|
+
console.log(` ${dim('Reason:')} ${dim(resolvedApiBaseUrl.message || 'Apply Supabase migrations first.')}`);
|
|
185
|
+
console.log(` ${dim('Run the SQL in supabase/migrations/001_device_auth.sql and 002_rate_limits.sql, then retry.')}`);
|
|
186
|
+
console.log(` ${dim('Falling back to local-only mode.\n')}`);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(` ${dim('Auth endpoint:')} ${dim(resolvedApiBaseUrl.baseUrl)}\n`);
|
|
191
|
+
|
|
192
|
+
// Step 1: Call /api/auth/device/start
|
|
193
|
+
process.stdout.write(` ${dim('Requesting device code...')}`);
|
|
194
|
+
let startRes;
|
|
195
|
+
try {
|
|
196
|
+
startRes = await httpJson('POST', `${resolvedApiBaseUrl.baseUrl}/api/auth/device/start`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.log(` ${red('✗')}`);
|
|
199
|
+
console.log(` ${red('Could not reach Vesper API at')} ${dim(resolvedApiBaseUrl.baseUrl)}`);
|
|
200
|
+
console.log(` ${dim('Falling back to local-only mode.\n')}`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (startRes.status !== 201 || !startRes.body.code) {
|
|
205
|
+
console.log(` ${red('✗')}`);
|
|
206
|
+
console.log(` ${red('Unexpected response:')} ${dim(JSON.stringify(startRes.body))}`);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { code, loginUrl } = startRes.body;
|
|
211
|
+
console.log(` ${green('✓')}\n`);
|
|
212
|
+
|
|
213
|
+
// Step 2: Display code and open browser
|
|
214
|
+
console.log(` ┌───────────────────────────────────────────────┐`);
|
|
215
|
+
console.log(` │ │`);
|
|
216
|
+
console.log(` │ ${bold('Your device code:')} ${cyan(bold(code))} │`);
|
|
217
|
+
console.log(` │ │`);
|
|
218
|
+
console.log(` │ ${dim('Open this URL to sign in:')} │`);
|
|
219
|
+
console.log(` │ ${cyan(loginUrl.padEnd(41))}│`);
|
|
220
|
+
console.log(` │ │`);
|
|
221
|
+
console.log(` └───────────────────────────────────────────────┘\n`);
|
|
222
|
+
|
|
223
|
+
openBrowser(loginUrl);
|
|
224
|
+
console.log(` ${dim('Browser opened automatically.')}`);
|
|
225
|
+
console.log(` ${dim('Waiting for you to sign in...')}\n`);
|
|
226
|
+
|
|
227
|
+
// Step 3: Poll until confirmed or expired
|
|
228
|
+
const POLL_INTERVAL = 3000; // 3 seconds
|
|
229
|
+
const MAX_POLLS = 200; // 10 min max (200 × 3s)
|
|
230
|
+
let polls = 0;
|
|
231
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
232
|
+
|
|
233
|
+
while (polls < MAX_POLLS) {
|
|
234
|
+
polls++;
|
|
235
|
+
const frame = spinner[polls % spinner.length];
|
|
236
|
+
process.stdout.write(`\r ${cyan(frame)} Polling... (${polls})`);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const pollRes = await httpJson('GET', `${resolvedApiBaseUrl.baseUrl}/api/auth/device/poll?code=${code}`);
|
|
240
|
+
|
|
241
|
+
if (pollRes.body.status === 'confirmed' && pollRes.body.apiKey) {
|
|
242
|
+
process.stdout.write(`\r ${green('✓')} Device authenticated! \n`);
|
|
243
|
+
console.log(` ${dim('Email:')} ${pollRes.body.email || 'linked'}`);
|
|
244
|
+
return pollRes.body.apiKey;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (pollRes.body.status === 'expired') {
|
|
248
|
+
process.stdout.write(`\r ${red('✗')} Device code expired. \n`);
|
|
249
|
+
console.log(` ${dim('Run the wizard again to get a new code.')}`);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Network hiccup — keep polling
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
process.stdout.write(`\r ${red('✗')} Timed out waiting for authentication.\n`);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
57
263
|
function printBanner() {
|
|
58
264
|
console.log(`
|
|
59
265
|
${dim('─────────────────────────────────────────────────')}
|
|
@@ -182,21 +388,50 @@ async function main() {
|
|
|
182
388
|
ensureDir(path.join(VESPER_DIR, 'datasets'));
|
|
183
389
|
console.log(` ${green('✓')}`);
|
|
184
390
|
|
|
185
|
-
// ─── Step 2:
|
|
186
|
-
|
|
391
|
+
// ─── Step 2: Authenticate (device flow or local key) ──────
|
|
392
|
+
console.log(`\n ${dim('[')}${cyan('2/6')}${dim(']')} Authentication`);
|
|
393
|
+
|
|
187
394
|
const existing = readToml(CONFIG_TOML);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
395
|
+
let localKey = existing.api_key || '';
|
|
396
|
+
let authMode = existing.auth_mode || '';
|
|
397
|
+
|
|
398
|
+
// If already has a cloud key, skip
|
|
399
|
+
if (localKey && !localKey.startsWith('vesper_sk_local_')) {
|
|
400
|
+
console.log(` ${green('✓')} Cloud API key already configured`);
|
|
401
|
+
console.log(` ${dim('Key:')} ${dim(localKey.slice(0, 24) + '...')} ${dim('→')} ${dim(CONFIG_TOML)}`);
|
|
402
|
+
} else {
|
|
403
|
+
// Offer device auth
|
|
404
|
+
const wantsDevice = await askYesNo(`${cyan('→')} Link to a Vesper account? (enables cloud sync & team features)`);
|
|
405
|
+
|
|
406
|
+
if (wantsDevice) {
|
|
407
|
+
const cloudKey = await deviceAuthFlow();
|
|
408
|
+
if (cloudKey) {
|
|
409
|
+
localKey = cloudKey;
|
|
410
|
+
authMode = 'cloud';
|
|
411
|
+
} else {
|
|
412
|
+
// Fall back to local key
|
|
413
|
+
if (!localKey) localKey = generateLocalKey();
|
|
414
|
+
authMode = 'local_unified';
|
|
415
|
+
console.log(`\n ${yellow('⚠')} Using local-only key. Run the wizard again anytime to link an account.`);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
if (!localKey) localKey = generateLocalKey();
|
|
419
|
+
authMode = 'local_unified';
|
|
420
|
+
console.log(` ${green('✓')} Local-only key generated`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const configData = { ...existing, api_key: localKey, auth_mode: authMode };
|
|
424
|
+
writeToml(CONFIG_TOML, configData);
|
|
425
|
+
console.log(` ${dim('Key:')} ${dim(localKey.slice(0, 24) + '...')} ${dim('→')} ${dim(CONFIG_TOML)}`);
|
|
426
|
+
}
|
|
193
427
|
|
|
194
428
|
// ─── Step 3: Local vault initialization ────────────────────
|
|
195
429
|
process.stdout.write(`\n ${dim('[')}${cyan('3/6')}${dim(']')} Initializing local credentials vault...`);
|
|
196
|
-
|
|
197
|
-
|
|
430
|
+
const vaultData = readToml(CONFIG_TOML);
|
|
431
|
+
if (!vaultData.auth_mode) vaultData.auth_mode = 'local_unified';
|
|
432
|
+
writeToml(CONFIG_TOML, vaultData);
|
|
198
433
|
console.log(` ${green('✓')}`);
|
|
199
|
-
console.log(` ${dim('Mode:')} ${dim('single local Vesper key (no external keys required)')}`);
|
|
434
|
+
console.log(` ${dim('Mode:')} ${dim(vaultData.auth_mode === 'cloud' ? 'cloud (linked to Vesper account)' : 'single local Vesper key (no external keys required)')}`);
|
|
200
435
|
|
|
201
436
|
// ─── Step 4: Install @vespermcp/mcp-server ─────────────────
|
|
202
437
|
console.log(`\n ${dim('[')}${cyan('4/6')}${dim(']')} Installing Vesper MCP server...`);
|
|
@@ -251,19 +486,24 @@ async function main() {
|
|
|
251
486
|
console.log(` ${configuredAgents.length > 0 ? green('✓') : yellow('⚠')} MCP agents ${dim(configuredAgents.length + ' configured')}`);
|
|
252
487
|
|
|
253
488
|
// ─── Final Summary ─────────────────────────────────────────
|
|
489
|
+
const finalConfig = readToml(CONFIG_TOML);
|
|
490
|
+
const isCloud = finalConfig.auth_mode === 'cloud';
|
|
254
491
|
console.log(`
|
|
255
492
|
${dim('═════════════════════════════════════════════════')}
|
|
256
493
|
|
|
257
494
|
${green(bold('✓ Vesper is ready!'))}
|
|
258
495
|
|
|
259
|
-
${bold('Your local API key:')}
|
|
260
|
-
${cyan(localKey)}
|
|
496
|
+
${bold(isCloud ? 'Your cloud API key:' : 'Your local API key:')}
|
|
497
|
+
${cyan(finalConfig.api_key || localKey)}
|
|
498
|
+
|
|
499
|
+
${bold('Auth mode:')}
|
|
500
|
+
${dim(isCloud ? '☁ Cloud (linked to Vesper account)' : '🔑 Local-only (key never leaves your machine)')}
|
|
261
501
|
|
|
262
502
|
${bold('Config file:')}
|
|
263
503
|
${dim(CONFIG_TOML)}
|
|
264
504
|
|
|
265
505
|
${bold('What just happened:')}
|
|
266
|
-
${dim('1.')} Generated a local API key (never leaves your machine)
|
|
506
|
+
${dim('1.')} ${isCloud ? 'Linked to your Vesper cloud account' : 'Generated a local API key (never leaves your machine)'}
|
|
267
507
|
${dim('2.')} Initialized local credentials vault
|
|
268
508
|
${dim('3.')} Auto-configured MCP for ${configuredAgents.length > 0 ? configuredAgents.join(', ') : 'detected agents'}
|
|
269
509
|
${dim('4.')} Vesper server ready on stdio transport
|