@gopherhole/cli 0.1.19 → 0.1.21

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 (3) hide show
  1. package/dist/index.js +125 -97
  2. package/package.json +1 -1
  3. package/src/index.ts +136 -98
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ program
60
60
 
61
61
  ${chalk_1.default.bold('Quick Start:')}
62
62
  $ gopherhole quickstart # Interactive setup wizard
63
- $ gopherhole signup # Create an account
63
+ $ gopherhole login # Log in or create account
64
64
  $ gopherhole agents create # Create your first agent
65
65
 
66
66
  ${chalk_1.default.bold('Examples:')}
@@ -106,80 +106,55 @@ ${chalk_1.default.bold('Example:')}
106
106
  console.log(brand.green(`✓ Already logged in as ${user?.email}\n`));
107
107
  }
108
108
  else {
109
- console.log(chalk_1.default.bold('Step 1: Create an account\n'));
110
- const { action } = await inquirer_1.default.prompt([
111
- {
112
- type: 'list',
113
- name: 'action',
114
- message: 'Do you have a GopherHole account?',
115
- choices: [
116
- { name: 'No, create one for me', value: 'signup' },
117
- { name: 'Yes, log me in', value: 'login' },
118
- ],
119
- },
109
+ console.log(chalk_1.default.bold('Step 1: Sign in\n'));
110
+ // OTP-based auth flow
111
+ const { email } = await inquirer_1.default.prompt([
112
+ { type: 'input', name: 'email', message: 'Email:', validate: (v) => v.includes('@') || 'Enter a valid email' },
120
113
  ]);
121
- if (action === 'signup') {
122
- const { name, email, password } = await inquirer_1.default.prompt([
123
- { type: 'input', name: 'name', message: 'Your name:' },
124
- { type: 'input', name: 'email', message: 'Email:' },
125
- { type: 'password', name: 'password', message: 'Password (min 8 chars):' },
126
- ]);
127
- const spinner = (0, ora_1.default)('Creating your account...').start();
128
- log('POST /auth/signup', { email });
129
- try {
130
- const res = await fetch(`${API_URL}/auth/signup`, {
131
- method: 'POST',
132
- headers: { 'Content-Type': 'application/json' },
133
- body: JSON.stringify({ name, email, password }),
134
- });
135
- if (!res.ok) {
136
- const err = await res.json();
137
- logError('signup', err);
138
- throw new Error(err.error || 'Signup failed');
139
- }
140
- const data = await res.json();
141
- config.set('sessionId', data.sessionId);
142
- config.set('user', data.user);
143
- config.set('tenant', data.tenant);
144
- sessionId = data.sessionId;
145
- spinner.succeed('Account created!');
146
- log('Session ID stored');
114
+ let spinner = (0, ora_1.default)('Sending verification code...').start();
115
+ log('POST /auth/send-code', { email });
116
+ try {
117
+ const sendRes = await fetch(`${API_URL}/auth/send-code`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ email }),
121
+ });
122
+ if (!sendRes.ok) {
123
+ const err = await sendRes.json();
124
+ logError('send-code', err);
125
+ throw new Error(err.error || 'Failed to send code');
147
126
  }
148
- catch (err) {
149
- spinner.fail(chalk_1.default.red(err.message));
150
- process.exit(1);
127
+ const sendData = await sendRes.json();
128
+ spinner.succeed('Code sent! Check your email.');
129
+ if (sendData.isNewUser) {
130
+ console.log(chalk_1.default.gray(' No account found - we\'ll create one for you.\n'));
151
131
  }
152
- }
153
- else {
154
- const { email, password } = await inquirer_1.default.prompt([
155
- { type: 'input', name: 'email', message: 'Email:' },
156
- { type: 'password', name: 'password', message: 'Password:' },
132
+ const { code } = await inquirer_1.default.prompt([
133
+ { type: 'input', name: 'code', message: 'Enter 6-digit code:', validate: (v) => /^\d{6}$/.test(v) || 'Enter 6 digits' },
157
134
  ]);
158
- const spinner = (0, ora_1.default)('Logging in...').start();
159
- log('POST /auth/login', { email });
160
- try {
161
- const res = await fetch(`${API_URL}/auth/login`, {
162
- method: 'POST',
163
- headers: { 'Content-Type': 'application/json' },
164
- body: JSON.stringify({ email, password }),
165
- });
166
- if (!res.ok) {
167
- const err = await res.json();
168
- logError('login', err);
169
- throw new Error(err.error || 'Login failed');
170
- }
171
- const data = await res.json();
172
- config.set('sessionId', data.sessionId);
173
- config.set('user', data.user);
174
- config.set('tenant', data.tenant);
175
- sessionId = data.sessionId;
176
- spinner.succeed('Logged in!');
177
- log('Session ID stored');
178
- }
179
- catch (err) {
180
- spinner.fail(chalk_1.default.red(err.message));
181
- process.exit(1);
135
+ spinner = (0, ora_1.default)('Verifying...').start();
136
+ log('POST /auth/verify-code', { email, code });
137
+ const verifyRes = await fetch(`${API_URL}/auth/verify-code`, {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json' },
140
+ body: JSON.stringify({ email, code }),
141
+ });
142
+ if (!verifyRes.ok) {
143
+ const err = await verifyRes.json();
144
+ logError('verify-code', err);
145
+ throw new Error(err.error || 'Verification failed');
182
146
  }
147
+ const data = await verifyRes.json();
148
+ config.set('sessionId', data.sessionId);
149
+ config.set('user', data.user);
150
+ config.set('tenant', data.tenant);
151
+ sessionId = data.sessionId;
152
+ spinner.succeed(data.isNewUser ? 'Account created!' : 'Logged in!');
153
+ log('Session ID stored');
154
+ }
155
+ catch (err) {
156
+ spinner.fail(chalk_1.default.red(err.message));
157
+ process.exit(1);
183
158
  }
184
159
  }
185
160
  // Step 2: Create agent
@@ -333,52 +308,86 @@ ${chalk_1.default.bold('Example:')}
333
308
  // ========== AUTH COMMANDS ==========
334
309
  program
335
310
  .command('login')
336
- .description(`Log in to GopherHole
311
+ .description(`Log in to GopherHole (creates account if needed)
337
312
 
338
313
  ${chalk_1.default.bold('Example:')}
339
314
  $ gopherhole login
340
315
  `)
341
316
  .action(async () => {
342
- const { email, password } = await inquirer_1.default.prompt([
343
- { type: 'input', name: 'email', message: 'Email:' },
344
- { type: 'password', name: 'password', message: 'Password:' },
317
+ const { email } = await inquirer_1.default.prompt([
318
+ { type: 'input', name: 'email', message: 'Email:', validate: (v) => v.includes('@') || 'Enter a valid email' },
345
319
  ]);
346
- const spinner = (0, ora_1.default)('Logging in...').start();
347
- log('POST /auth/login', { email });
320
+ let spinner = (0, ora_1.default)('Sending verification code...').start();
321
+ log('POST /auth/send-code', { email });
348
322
  try {
349
- const res = await fetch(`${API_URL}/auth/login`, {
323
+ const sendRes = await fetch(`${API_URL}/auth/send-code`, {
350
324
  method: 'POST',
351
325
  headers: { 'Content-Type': 'application/json' },
352
- body: JSON.stringify({ email, password }),
326
+ body: JSON.stringify({ email }),
353
327
  });
354
- if (!res.ok) {
355
- const err = await res.json();
356
- logError('login', err);
357
- throw new Error(err.error || 'Login failed');
328
+ if (!sendRes.ok) {
329
+ const err = await sendRes.json();
330
+ logError('send-code', err);
331
+ throw new Error(err.error || 'Failed to send code');
332
+ }
333
+ const sendData = await sendRes.json();
334
+ spinner.succeed('Code sent! Check your email.');
335
+ if (sendData.isNewUser) {
336
+ console.log(chalk_1.default.gray(' No account found - we\'ll create one for you.\n'));
337
+ }
338
+ const { code } = await inquirer_1.default.prompt([
339
+ { type: 'input', name: 'code', message: 'Enter 6-digit code:', validate: (v) => /^\d{6}$/.test(v) || 'Enter 6 digits' },
340
+ ]);
341
+ spinner = (0, ora_1.default)('Verifying...').start();
342
+ log('POST /auth/verify-code', { email, code });
343
+ const verifyRes = await fetch(`${API_URL}/auth/verify-code`, {
344
+ method: 'POST',
345
+ headers: { 'Content-Type': 'application/json' },
346
+ body: JSON.stringify({ email, code }),
347
+ });
348
+ if (!verifyRes.ok) {
349
+ const err = await verifyRes.json();
350
+ logError('verify-code', err);
351
+ throw new Error(err.error || 'Verification failed');
358
352
  }
359
- const data = await res.json();
353
+ const data = await verifyRes.json();
360
354
  config.set('sessionId', data.sessionId);
361
355
  config.set('user', data.user);
362
356
  config.set('tenant', data.tenant);
363
- spinner.succeed(`Logged in as ${brand.green(data.user.email)}`);
357
+ if (data.isNewUser) {
358
+ spinner.succeed(brand.green('Account created!'));
359
+ console.log(`\n${brand.green('✨ Welcome to GopherHole!')}\n`);
360
+ console.log('Next steps:');
361
+ console.log(chalk_1.default.gray(' $ gopherhole agents create # Create your first agent'));
362
+ console.log(chalk_1.default.gray(' $ gopherhole discover search # Find agents to talk to\n'));
363
+ }
364
+ else {
365
+ spinner.succeed(`Logged in as ${brand.green(data.user.email)}`);
366
+ }
364
367
  log('Session stored in:', config.path);
365
368
  }
366
369
  catch (err) {
367
370
  spinner.fail(chalk_1.default.red(err.message));
368
371
  console.log(chalk_1.default.gray('\nTroubleshooting:'));
369
- console.log(chalk_1.default.gray(' • Check your email and password'));
370
- console.log(chalk_1.default.gray(' • Try: gopherhole signup (to create account)'));
372
+ console.log(chalk_1.default.gray(' • Check your email for the code'));
373
+ console.log(chalk_1.default.gray(' • Codes expire after 10 minutes'));
371
374
  console.log(chalk_1.default.gray(' • Run with --verbose for more details'));
372
375
  process.exit(1);
373
376
  }
374
377
  });
378
+ // Alias for backward compatibility
375
379
  program
376
380
  .command('signup')
377
- .description(`Create a new GopherHole account
378
-
379
- ${chalk_1.default.bold('Example:')}
380
- $ gopherhole signup
381
- `)
381
+ .description(`Create a new GopherHole account (alias for login)`)
382
+ .action(async () => {
383
+ console.log(chalk_1.default.yellow('Note: signup and login are now unified. Running login...\n'));
384
+ // Re-run login command
385
+ await program.parseAsync(['node', 'gopherhole', 'login']);
386
+ });
387
+ // Legacy signup code removed - kept as hidden command for reference
388
+ program
389
+ .command('signup-legacy', { hidden: true })
390
+ .description(`[DEPRECATED] Create account with password`)
382
391
  .action(async () => {
383
392
  const { name, email, password } = await inquirer_1.default.prompt([
384
393
  { type: 'input', name: 'name', message: 'Name:' },
@@ -421,17 +430,36 @@ program
421
430
  program
422
431
  .command('whoami')
423
432
  .description('Show current logged in user')
424
- .action(() => {
425
- const user = config.get('user');
426
- const tenant = config.get('tenant');
427
- if (!user) {
433
+ .action(async () => {
434
+ const sessionId = config.get('sessionId');
435
+ const cachedUser = config.get('user');
436
+ const cachedTenant = config.get('tenant');
437
+ if (!sessionId || !cachedUser) {
428
438
  console.log(chalk_1.default.yellow('Not logged in.'));
429
439
  console.log(chalk_1.default.gray('\nTo log in: gopherhole login'));
430
440
  console.log(chalk_1.default.gray('To sign up: gopherhole signup'));
431
441
  process.exit(1);
432
442
  }
433
- console.log(`\n ${chalk_1.default.bold('User:')} ${user.name} <${user.email}>`);
434
- console.log(` ${chalk_1.default.bold('Org:')} ${tenant?.name || 'Personal'}`);
443
+ // Validate session with server
444
+ try {
445
+ const res = await fetch(`${API_URL}/auth/me`, {
446
+ headers: { 'X-Session-ID': sessionId },
447
+ });
448
+ if (!res.ok) {
449
+ const data = await res.json().catch(() => ({}));
450
+ if (data.error === 'Session expired' || res.status === 401) {
451
+ console.log(chalk_1.default.red('Session expired.'));
452
+ console.log(chalk_1.default.gray('\nPlease log in again: gopherhole login'));
453
+ process.exit(1);
454
+ }
455
+ }
456
+ }
457
+ catch {
458
+ // Network error - show cached info with warning
459
+ console.log(chalk_1.default.yellow('⚠ Could not verify session (offline?)\n'));
460
+ }
461
+ console.log(`\n ${chalk_1.default.bold('User:')} ${cachedUser.name} <${cachedUser.email}>`);
462
+ console.log(` ${chalk_1.default.bold('Org:')} ${cachedTenant?.name || 'Personal'}`);
435
463
  console.log(` ${chalk_1.default.bold('Config:')} ${config.path}\n`);
436
464
  });
437
465
  // ========== AGENT COMMANDS ==========
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "GopherHole CLI - Connect AI agents to the world",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -67,7 +67,7 @@ program
67
67
 
68
68
  ${chalk.bold('Quick Start:')}
69
69
  $ gopherhole quickstart # Interactive setup wizard
70
- $ gopherhole signup # Create an account
70
+ $ gopherhole login # Log in or create account
71
71
  $ gopherhole agents create # Create your first agent
72
72
 
73
73
  ${chalk.bold('Examples:')}
@@ -116,87 +116,65 @@ ${chalk.bold('Example:')}
116
116
  const user = config.get('user') as { email: string } | undefined;
117
117
  console.log(brand.green(`✓ Already logged in as ${user?.email}\n`));
118
118
  } else {
119
- console.log(chalk.bold('Step 1: Create an account\n'));
119
+ console.log(chalk.bold('Step 1: Sign in\n'));
120
120
 
121
- const { action } = await inquirer.prompt([
122
- {
123
- type: 'list',
124
- name: 'action',
125
- message: 'Do you have a GopherHole account?',
126
- choices: [
127
- { name: 'No, create one for me', value: 'signup' },
128
- { name: 'Yes, log me in', value: 'login' },
129
- ],
130
- },
121
+ // OTP-based auth flow
122
+ const { email } = await inquirer.prompt([
123
+ { type: 'input', name: 'email', message: 'Email:', validate: (v: string) => v.includes('@') || 'Enter a valid email' },
131
124
  ]);
132
125
 
133
- if (action === 'signup') {
134
- const { name, email, password } = await inquirer.prompt([
135
- { type: 'input', name: 'name', message: 'Your name:' },
136
- { type: 'input', name: 'email', message: 'Email:' },
137
- { type: 'password', name: 'password', message: 'Password (min 8 chars):' },
138
- ]);
126
+ let spinner = ora('Sending verification code...').start();
127
+ log('POST /auth/send-code', { email });
139
128
 
140
- const spinner = ora('Creating your account...').start();
141
- log('POST /auth/signup', { email });
142
-
143
- try {
144
- const res = await fetch(`${API_URL}/auth/signup`, {
145
- method: 'POST',
146
- headers: { 'Content-Type': 'application/json' },
147
- body: JSON.stringify({ name, email, password }),
148
- });
149
-
150
- if (!res.ok) {
151
- const err = await res.json();
152
- logError('signup', err);
153
- throw new Error(err.error || 'Signup failed');
154
- }
129
+ try {
130
+ const sendRes = await fetch(`${API_URL}/auth/send-code`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({ email }),
134
+ });
155
135
 
156
- const data = await res.json();
157
- config.set('sessionId', data.sessionId);
158
- config.set('user', data.user);
159
- config.set('tenant', data.tenant);
160
- sessionId = data.sessionId;
161
- spinner.succeed('Account created!');
162
- log('Session ID stored');
163
- } catch (err) {
164
- spinner.fail(chalk.red((err as Error).message));
165
- process.exit(1);
136
+ if (!sendRes.ok) {
137
+ const err = await sendRes.json();
138
+ logError('send-code', err);
139
+ throw new Error(err.error || 'Failed to send code');
166
140
  }
167
- } else {
168
- const { email, password } = await inquirer.prompt([
169
- { type: 'input', name: 'email', message: 'Email:' },
170
- { type: 'password', name: 'password', message: 'Password:' },
141
+
142
+ const sendData = await sendRes.json();
143
+ spinner.succeed('Code sent! Check your email.');
144
+
145
+ if (sendData.isNewUser) {
146
+ console.log(chalk.gray(' No account found - we\'ll create one for you.\n'));
147
+ }
148
+
149
+ const { code } = await inquirer.prompt([
150
+ { type: 'input', name: 'code', message: 'Enter 6-digit code:', validate: (v: string) => /^\d{6}$/.test(v) || 'Enter 6 digits' },
171
151
  ]);
172
152
 
173
- const spinner = ora('Logging in...').start();
174
- log('POST /auth/login', { email });
175
-
176
- try {
177
- const res = await fetch(`${API_URL}/auth/login`, {
178
- method: 'POST',
179
- headers: { 'Content-Type': 'application/json' },
180
- body: JSON.stringify({ email, password }),
181
- });
182
-
183
- if (!res.ok) {
184
- const err = await res.json();
185
- logError('login', err);
186
- throw new Error(err.error || 'Login failed');
187
- }
153
+ spinner = ora('Verifying...').start();
154
+ log('POST /auth/verify-code', { email, code });
188
155
 
189
- const data = await res.json();
190
- config.set('sessionId', data.sessionId);
191
- config.set('user', data.user);
192
- config.set('tenant', data.tenant);
193
- sessionId = data.sessionId;
194
- spinner.succeed('Logged in!');
195
- log('Session ID stored');
196
- } catch (err) {
197
- spinner.fail(chalk.red((err as Error).message));
198
- process.exit(1);
156
+ const verifyRes = await fetch(`${API_URL}/auth/verify-code`, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({ email, code }),
160
+ });
161
+
162
+ if (!verifyRes.ok) {
163
+ const err = await verifyRes.json();
164
+ logError('verify-code', err);
165
+ throw new Error(err.error || 'Verification failed');
199
166
  }
167
+
168
+ const data = await verifyRes.json();
169
+ config.set('sessionId', data.sessionId);
170
+ config.set('user', data.user);
171
+ config.set('tenant', data.tenant);
172
+ sessionId = data.sessionId;
173
+ spinner.succeed(data.isNewUser ? 'Account created!' : 'Logged in!');
174
+ log('Session ID stored');
175
+ } catch (err) {
176
+ spinner.fail(chalk.red((err as Error).message));
177
+ process.exit(1);
200
178
  }
201
179
  }
202
180
 
@@ -364,57 +342,97 @@ ${chalk.bold('Example:')}
364
342
 
365
343
  program
366
344
  .command('login')
367
- .description(`Log in to GopherHole
345
+ .description(`Log in to GopherHole (creates account if needed)
368
346
 
369
347
  ${chalk.bold('Example:')}
370
348
  $ gopherhole login
371
349
  `)
372
350
  .action(async () => {
373
- const { email, password } = await inquirer.prompt([
374
- { type: 'input', name: 'email', message: 'Email:' },
375
- { type: 'password', name: 'password', message: 'Password:' },
351
+ const { email } = await inquirer.prompt([
352
+ { type: 'input', name: 'email', message: 'Email:', validate: (v: string) => v.includes('@') || 'Enter a valid email' },
376
353
  ]);
377
354
 
378
- const spinner = ora('Logging in...').start();
379
- log('POST /auth/login', { email });
355
+ let spinner = ora('Sending verification code...').start();
356
+ log('POST /auth/send-code', { email });
380
357
 
381
358
  try {
382
- const res = await fetch(`${API_URL}/auth/login`, {
359
+ const sendRes = await fetch(`${API_URL}/auth/send-code`, {
383
360
  method: 'POST',
384
361
  headers: { 'Content-Type': 'application/json' },
385
- body: JSON.stringify({ email, password }),
362
+ body: JSON.stringify({ email }),
386
363
  });
387
364
 
388
- if (!res.ok) {
389
- const err = await res.json();
390
- logError('login', err);
391
- throw new Error(err.error || 'Login failed');
365
+ if (!sendRes.ok) {
366
+ const err = await sendRes.json();
367
+ logError('send-code', err);
368
+ throw new Error(err.error || 'Failed to send code');
392
369
  }
393
370
 
394
- const data = await res.json();
371
+ const sendData = await sendRes.json();
372
+ spinner.succeed('Code sent! Check your email.');
373
+
374
+ if (sendData.isNewUser) {
375
+ console.log(chalk.gray(' No account found - we\'ll create one for you.\n'));
376
+ }
377
+
378
+ const { code } = await inquirer.prompt([
379
+ { type: 'input', name: 'code', message: 'Enter 6-digit code:', validate: (v: string) => /^\d{6}$/.test(v) || 'Enter 6 digits' },
380
+ ]);
381
+
382
+ spinner = ora('Verifying...').start();
383
+ log('POST /auth/verify-code', { email, code });
384
+
385
+ const verifyRes = await fetch(`${API_URL}/auth/verify-code`, {
386
+ method: 'POST',
387
+ headers: { 'Content-Type': 'application/json' },
388
+ body: JSON.stringify({ email, code }),
389
+ });
390
+
391
+ if (!verifyRes.ok) {
392
+ const err = await verifyRes.json();
393
+ logError('verify-code', err);
394
+ throw new Error(err.error || 'Verification failed');
395
+ }
396
+
397
+ const data = await verifyRes.json();
395
398
  config.set('sessionId', data.sessionId);
396
399
  config.set('user', data.user);
397
400
  config.set('tenant', data.tenant);
398
401
 
399
- spinner.succeed(`Logged in as ${brand.green(data.user.email)}`);
402
+ if (data.isNewUser) {
403
+ spinner.succeed(brand.green('Account created!'));
404
+ console.log(`\n${brand.green('✨ Welcome to GopherHole!')}\n`);
405
+ console.log('Next steps:');
406
+ console.log(chalk.gray(' $ gopherhole agents create # Create your first agent'));
407
+ console.log(chalk.gray(' $ gopherhole discover search # Find agents to talk to\n'));
408
+ } else {
409
+ spinner.succeed(`Logged in as ${brand.green(data.user.email)}`);
410
+ }
400
411
  log('Session stored in:', config.path);
401
412
  } catch (err) {
402
413
  spinner.fail(chalk.red((err as Error).message));
403
414
  console.log(chalk.gray('\nTroubleshooting:'));
404
- console.log(chalk.gray(' • Check your email and password'));
405
- console.log(chalk.gray(' • Try: gopherhole signup (to create account)'));
415
+ console.log(chalk.gray(' • Check your email for the code'));
416
+ console.log(chalk.gray(' • Codes expire after 10 minutes'));
406
417
  console.log(chalk.gray(' • Run with --verbose for more details'));
407
418
  process.exit(1);
408
419
  }
409
420
  });
410
421
 
422
+ // Alias for backward compatibility
411
423
  program
412
424
  .command('signup')
413
- .description(`Create a new GopherHole account
425
+ .description(`Create a new GopherHole account (alias for login)`)
426
+ .action(async () => {
427
+ console.log(chalk.yellow('Note: signup and login are now unified. Running login...\n'));
428
+ // Re-run login command
429
+ await program.parseAsync(['node', 'gopherhole', 'login']);
430
+ });
414
431
 
415
- ${chalk.bold('Example:')}
416
- $ gopherhole signup
417
- `)
432
+ // Legacy signup code removed - kept as hidden command for reference
433
+ program
434
+ .command('signup-legacy', { hidden: true })
435
+ .description(`[DEPRECATED] Create account with password`)
418
436
  .action(async () => {
419
437
  const { name, email, password } = await inquirer.prompt([
420
438
  { type: 'input', name: 'name', message: 'Name:' },
@@ -463,19 +481,39 @@ program
463
481
  program
464
482
  .command('whoami')
465
483
  .description('Show current logged in user')
466
- .action(() => {
467
- const user = config.get('user') as { email: string; name: string } | undefined;
468
- const tenant = config.get('tenant') as { name: string } | undefined;
484
+ .action(async () => {
485
+ const sessionId = config.get('sessionId') as string | undefined;
486
+ const cachedUser = config.get('user') as { email: string; name: string } | undefined;
487
+ const cachedTenant = config.get('tenant') as { name: string } | undefined;
469
488
 
470
- if (!user) {
489
+ if (!sessionId || !cachedUser) {
471
490
  console.log(chalk.yellow('Not logged in.'));
472
491
  console.log(chalk.gray('\nTo log in: gopherhole login'));
473
492
  console.log(chalk.gray('To sign up: gopherhole signup'));
474
493
  process.exit(1);
475
494
  }
476
495
 
477
- console.log(`\n ${chalk.bold('User:')} ${user.name} <${user.email}>`);
478
- console.log(` ${chalk.bold('Org:')} ${tenant?.name || 'Personal'}`);
496
+ // Validate session with server
497
+ try {
498
+ const res = await fetch(`${API_URL}/auth/me`, {
499
+ headers: { 'X-Session-ID': sessionId },
500
+ });
501
+
502
+ if (!res.ok) {
503
+ const data = await res.json().catch(() => ({}));
504
+ if (data.error === 'Session expired' || res.status === 401) {
505
+ console.log(chalk.red('Session expired.'));
506
+ console.log(chalk.gray('\nPlease log in again: gopherhole login'));
507
+ process.exit(1);
508
+ }
509
+ }
510
+ } catch {
511
+ // Network error - show cached info with warning
512
+ console.log(chalk.yellow('⚠ Could not verify session (offline?)\n'));
513
+ }
514
+
515
+ console.log(`\n ${chalk.bold('User:')} ${cachedUser.name} <${cachedUser.email}>`);
516
+ console.log(` ${chalk.bold('Org:')} ${cachedTenant?.name || 'Personal'}`);
479
517
  console.log(` ${chalk.bold('Config:')} ${config.path}\n`);
480
518
  });
481
519