vesper-wizard 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/wizard.js +216 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vesper-wizard",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Zero-friction setup wizard for Vesper — local MCP server, unified dataset API, and agent auto-config in 60 seconds",
5
5
  "bin": {
6
6
  "vesper-wizard": "wizard.js"
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,171 @@ 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 canReachDeviceAuth(baseUrl) {
95
+ try {
96
+ const res = await httpJson('POST', `${baseUrl}/api/auth/device/start`);
97
+ return res.status === 201 && !!res.body && !!res.body.code;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ async function resolveVesperApiBaseUrl() {
104
+ const candidates = VESPER_API_URL
105
+ ? [VESPER_API_URL]
106
+ : DEFAULT_VESPER_API_CANDIDATES;
107
+
108
+ for (const candidate of candidates) {
109
+ if (await canReachDeviceAuth(candidate)) {
110
+ return candidate;
111
+ }
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ function openBrowser(url) {
118
+ try {
119
+ if (process.platform === 'win32') {
120
+ spawnSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
121
+ } else if (process.platform === 'darwin') {
122
+ spawnSync('open', [url], { stdio: 'ignore' });
123
+ } else {
124
+ spawnSync('xdg-open', [url], { stdio: 'ignore' });
125
+ }
126
+ } catch { /* browser open is best-effort */ }
127
+ }
128
+
129
+ function askYesNo(question) {
130
+ return new Promise((resolve) => {
131
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
132
+ rl.question(` ${question} ${dim('[Y/n]')} `, (answer) => {
133
+ rl.close();
134
+ resolve(!answer || answer.toLowerCase().startsWith('y'));
135
+ });
136
+ });
137
+ }
138
+
139
+ async function deviceAuthFlow() {
140
+ console.log(`\n ${cyan('■')} ${bold('Device Authentication')}`);
141
+ console.log(` ${dim('Link your CLI to a Vesper account for cloud features\n')}`);
142
+
143
+ const resolvedApiBaseUrl = await resolveVesperApiBaseUrl();
144
+ if (!resolvedApiBaseUrl) {
145
+ console.log(` ${red('✗')} ${red('Could not reach any Vesper auth endpoint.')}`);
146
+ console.log(` ${dim('Tried:')} ${dim((VESPER_API_URL ? [VESPER_API_URL] : DEFAULT_VESPER_API_CANDIDATES).join(', '))}`);
147
+ console.log(` ${dim('If your landing app is running locally, start it on http://localhost:3000 or set VESPER_API_URL.')}`);
148
+ console.log(` ${dim('Falling back to local-only mode.\n')}`);
149
+ return null;
150
+ }
151
+
152
+ console.log(` ${dim('Auth endpoint:')} ${dim(resolvedApiBaseUrl)}\n`);
153
+
154
+ // Step 1: Call /api/auth/device/start
155
+ process.stdout.write(` ${dim('Requesting device code...')}`);
156
+ let startRes;
157
+ try {
158
+ startRes = await httpJson('POST', `${resolvedApiBaseUrl}/api/auth/device/start`);
159
+ } catch (err) {
160
+ console.log(` ${red('✗')}`);
161
+ console.log(` ${red('Could not reach Vesper API at')} ${dim(resolvedApiBaseUrl)}`);
162
+ console.log(` ${dim('Falling back to local-only mode.\n')}`);
163
+ return null;
164
+ }
165
+
166
+ if (startRes.status !== 201 || !startRes.body.code) {
167
+ console.log(` ${red('✗')}`);
168
+ console.log(` ${red('Unexpected response:')} ${dim(JSON.stringify(startRes.body))}`);
169
+ return null;
170
+ }
171
+
172
+ const { code, loginUrl } = startRes.body;
173
+ console.log(` ${green('✓')}\n`);
174
+
175
+ // Step 2: Display code and open browser
176
+ console.log(` ┌───────────────────────────────────────────────┐`);
177
+ console.log(` │ │`);
178
+ console.log(` │ ${bold('Your device code:')} ${cyan(bold(code))} │`);
179
+ console.log(` │ │`);
180
+ console.log(` │ ${dim('Open this URL to sign in:')} │`);
181
+ console.log(` │ ${cyan(loginUrl.padEnd(41))}│`);
182
+ console.log(` │ │`);
183
+ console.log(` └───────────────────────────────────────────────┘\n`);
184
+
185
+ openBrowser(loginUrl);
186
+ console.log(` ${dim('Browser opened automatically.')}`);
187
+ console.log(` ${dim('Waiting for you to sign in...')}\n`);
188
+
189
+ // Step 3: Poll until confirmed or expired
190
+ const POLL_INTERVAL = 3000; // 3 seconds
191
+ const MAX_POLLS = 200; // 10 min max (200 × 3s)
192
+ let polls = 0;
193
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
194
+
195
+ while (polls < MAX_POLLS) {
196
+ polls++;
197
+ const frame = spinner[polls % spinner.length];
198
+ process.stdout.write(`\r ${cyan(frame)} Polling... (${polls})`);
199
+
200
+ try {
201
+ const pollRes = await httpJson('GET', `${resolvedApiBaseUrl}/api/auth/device/poll?code=${code}`);
202
+
203
+ if (pollRes.body.status === 'confirmed' && pollRes.body.apiKey) {
204
+ process.stdout.write(`\r ${green('✓')} Device authenticated! \n`);
205
+ console.log(` ${dim('Email:')} ${pollRes.body.email || 'linked'}`);
206
+ return pollRes.body.apiKey;
207
+ }
208
+
209
+ if (pollRes.body.status === 'expired') {
210
+ process.stdout.write(`\r ${red('✗')} Device code expired. \n`);
211
+ console.log(` ${dim('Run the wizard again to get a new code.')}`);
212
+ return null;
213
+ }
214
+ } catch {
215
+ // Network hiccup — keep polling
216
+ }
217
+
218
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
219
+ }
220
+
221
+ process.stdout.write(`\r ${red('✗')} Timed out waiting for authentication.\n`);
222
+ return null;
223
+ }
224
+
57
225
  function printBanner() {
58
226
  console.log(`
59
227
  ${dim('─────────────────────────────────────────────────')}
@@ -182,21 +350,50 @@ async function main() {
182
350
  ensureDir(path.join(VESPER_DIR, 'datasets'));
183
351
  console.log(` ${green('✓')}`);
184
352
 
185
- // ─── Step 2: Generate local API key ────────────────────────
186
- process.stdout.write(` ${dim('[')}${cyan('2/6')}${dim(']')} Generating local API key...`);
353
+ // ─── Step 2: Authenticate (device flow or local key) ──────
354
+ console.log(`\n ${dim('[')}${cyan('2/6')}${dim(']')} Authentication`);
355
+
187
356
  const existing = readToml(CONFIG_TOML);
188
- const localKey = existing.api_key || generateLocalKey();
189
- const configData = { ...existing, api_key: localKey };
190
- writeToml(CONFIG_TOML, configData);
191
- console.log(` ${green('✓')}`);
192
- console.log(` ${dim('Key:')} ${dim(localKey.slice(0, 20) + '...')} ${dim('')} ${dim(CONFIG_TOML)}`);
357
+ let localKey = existing.api_key || '';
358
+ let authMode = existing.auth_mode || '';
359
+
360
+ // If already has a cloud key, skip
361
+ if (localKey && !localKey.startsWith('vesper_sk_local_')) {
362
+ console.log(` ${green('✓')} Cloud API key already configured`);
363
+ console.log(` ${dim('Key:')} ${dim(localKey.slice(0, 24) + '...')} ${dim('→')} ${dim(CONFIG_TOML)}`);
364
+ } else {
365
+ // Offer device auth
366
+ const wantsDevice = await askYesNo(`${cyan('→')} Link to a Vesper account? (enables cloud sync & team features)`);
367
+
368
+ if (wantsDevice) {
369
+ const cloudKey = await deviceAuthFlow();
370
+ if (cloudKey) {
371
+ localKey = cloudKey;
372
+ authMode = 'cloud';
373
+ } else {
374
+ // Fall back to local key
375
+ if (!localKey) localKey = generateLocalKey();
376
+ authMode = 'local_unified';
377
+ console.log(`\n ${yellow('⚠')} Using local-only key. Run the wizard again anytime to link an account.`);
378
+ }
379
+ } else {
380
+ if (!localKey) localKey = generateLocalKey();
381
+ authMode = 'local_unified';
382
+ console.log(` ${green('✓')} Local-only key generated`);
383
+ }
384
+
385
+ const configData = { ...existing, api_key: localKey, auth_mode: authMode };
386
+ writeToml(CONFIG_TOML, configData);
387
+ console.log(` ${dim('Key:')} ${dim(localKey.slice(0, 24) + '...')} ${dim('→')} ${dim(CONFIG_TOML)}`);
388
+ }
193
389
 
194
390
  // ─── Step 3: Local vault initialization ────────────────────
195
391
  process.stdout.write(`\n ${dim('[')}${cyan('3/6')}${dim(']')} Initializing local credentials vault...`);
196
- configData.auth_mode = configData.auth_mode || 'local_unified';
197
- writeToml(CONFIG_TOML, configData);
392
+ const vaultData = readToml(CONFIG_TOML);
393
+ if (!vaultData.auth_mode) vaultData.auth_mode = 'local_unified';
394
+ writeToml(CONFIG_TOML, vaultData);
198
395
  console.log(` ${green('✓')}`);
199
- console.log(` ${dim('Mode:')} ${dim('single local Vesper key (no external keys required)')}`);
396
+ console.log(` ${dim('Mode:')} ${dim(vaultData.auth_mode === 'cloud' ? 'cloud (linked to Vesper account)' : 'single local Vesper key (no external keys required)')}`);
200
397
 
201
398
  // ─── Step 4: Install @vespermcp/mcp-server ─────────────────
202
399
  console.log(`\n ${dim('[')}${cyan('4/6')}${dim(']')} Installing Vesper MCP server...`);
@@ -208,7 +405,7 @@ async function main() {
208
405
  });
209
406
  console.log(` ${green('✓')} @vespermcp/mcp-server installed`);
210
407
  } catch {
211
- console.log(` ${yellow('⚠')} Could not auto-install — run manually: npx @vespermcp/mcp-server --setup`);
408
+ console.log(` ${yellow('⚠')} Could not auto-install — run manually: npx -y @vespermcp/mcp-server@latest --setup`);
212
409
  }
213
410
 
214
411
  // ─── Step 5: Auto-configure all detected IDEs ──────────────
@@ -251,19 +448,24 @@ async function main() {
251
448
  console.log(` ${configuredAgents.length > 0 ? green('✓') : yellow('⚠')} MCP agents ${dim(configuredAgents.length + ' configured')}`);
252
449
 
253
450
  // ─── Final Summary ─────────────────────────────────────────
451
+ const finalConfig = readToml(CONFIG_TOML);
452
+ const isCloud = finalConfig.auth_mode === 'cloud';
254
453
  console.log(`
255
454
  ${dim('═════════════════════════════════════════════════')}
256
455
 
257
456
  ${green(bold('✓ Vesper is ready!'))}
258
457
 
259
- ${bold('Your local API key:')}
260
- ${cyan(localKey)}
458
+ ${bold(isCloud ? 'Your cloud API key:' : 'Your local API key:')}
459
+ ${cyan(finalConfig.api_key || localKey)}
460
+
461
+ ${bold('Auth mode:')}
462
+ ${dim(isCloud ? '☁ Cloud (linked to Vesper account)' : '🔑 Local-only (key never leaves your machine)')}
261
463
 
262
464
  ${bold('Config file:')}
263
465
  ${dim(CONFIG_TOML)}
264
466
 
265
467
  ${bold('What just happened:')}
266
- ${dim('1.')} Generated a local API key (never leaves your machine)
468
+ ${dim('1.')} ${isCloud ? 'Linked to your Vesper cloud account' : 'Generated a local API key (never leaves your machine)'}
267
469
  ${dim('2.')} Initialized local credentials vault
268
470
  ${dim('3.')} Auto-configured MCP for ${configuredAgents.length > 0 ? configuredAgents.join(', ') : 'detected agents'}
269
471
  ${dim('4.')} Vesper server ready on stdio transport