@gopherhole/cli 0.1.20 → 0.1.22

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 +137 -144
  2. package/package.json +1 -1
  3. package/src/index.ts +150 -144
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:' },
@@ -886,62 +895,46 @@ ${chalk_1.default.bold('Example:')}
886
895
  // Check if logged in
887
896
  if (!sessionId) {
888
897
  console.log(chalk_1.default.yellow('Not logged in yet.\n'));
889
- const { action } = await inquirer_1.default.prompt([
890
- {
891
- type: 'list',
892
- name: 'action',
893
- message: 'Choose:',
894
- choices: [
895
- { name: 'Create account', value: 'signup' },
896
- { name: 'Log in', value: 'login' },
897
- ],
898
- },
898
+ // OTP-based auth flow
899
+ const { email } = await inquirer_1.default.prompt([
900
+ { type: 'input', name: 'email', message: 'Email:', validate: (v) => v.includes('@') || 'Enter a valid email' },
899
901
  ]);
900
- if (action === 'signup') {
901
- const { name, email, password } = await inquirer_1.default.prompt([
902
- { type: 'input', name: 'name', message: 'Name:' },
903
- { type: 'input', name: 'email', message: 'Email:' },
904
- { type: 'password', name: 'password', message: 'Password:' },
905
- ]);
906
- const spinner = (0, ora_1.default)('Creating account...').start();
907
- const res = await fetch(`${API_URL}/auth/signup`, {
908
- method: 'POST',
909
- headers: { 'Content-Type': 'application/json' },
910
- body: JSON.stringify({ name, email, password }),
911
- });
912
- if (!res.ok) {
913
- spinner.fail('Signup failed');
914
- process.exit(1);
915
- }
916
- const data = await res.json();
917
- config.set('sessionId', data.sessionId);
918
- config.set('user', data.user);
919
- config.set('tenant', data.tenant);
920
- sessionId = data.sessionId;
921
- spinner.succeed('Account created!');
902
+ let spinner = (0, ora_1.default)('Sending verification code...').start();
903
+ const sendRes = await fetch(`${API_URL}/auth/send-code`, {
904
+ method: 'POST',
905
+ headers: { 'Content-Type': 'application/json' },
906
+ body: JSON.stringify({ email }),
907
+ });
908
+ if (!sendRes.ok) {
909
+ const err = await sendRes.json().catch(() => ({}));
910
+ spinner.fail(err.error || 'Failed to send code');
911
+ process.exit(1);
922
912
  }
923
- else {
924
- const { email, password } = await inquirer_1.default.prompt([
925
- { type: 'input', name: 'email', message: 'Email:' },
926
- { type: 'password', name: 'password', message: 'Password:' },
927
- ]);
928
- const spinner = (0, ora_1.default)('Logging in...').start();
929
- const res = await fetch(`${API_URL}/auth/login`, {
930
- method: 'POST',
931
- headers: { 'Content-Type': 'application/json' },
932
- body: JSON.stringify({ email, password }),
933
- });
934
- if (!res.ok) {
935
- spinner.fail('Login failed');
936
- process.exit(1);
937
- }
938
- const data = await res.json();
939
- config.set('sessionId', data.sessionId);
940
- config.set('user', data.user);
941
- config.set('tenant', data.tenant);
942
- sessionId = data.sessionId;
943
- spinner.succeed('Logged in!');
913
+ const sendData = await sendRes.json();
914
+ spinner.succeed('Code sent! Check your email.');
915
+ if (sendData.isNewUser) {
916
+ console.log(chalk_1.default.gray(' No account found - we\'ll create one for you.\n'));
944
917
  }
918
+ const { code } = await inquirer_1.default.prompt([
919
+ { type: 'input', name: 'code', message: 'Enter 6-digit code:', validate: (v) => /^\d{6}$/.test(v) || 'Enter 6 digits' },
920
+ ]);
921
+ spinner = (0, ora_1.default)('Verifying...').start();
922
+ const verifyRes = await fetch(`${API_URL}/auth/verify-code`, {
923
+ method: 'POST',
924
+ headers: { 'Content-Type': 'application/json' },
925
+ body: JSON.stringify({ email, code }),
926
+ });
927
+ if (!verifyRes.ok) {
928
+ const err = await verifyRes.json().catch(() => ({}));
929
+ spinner.fail(err.error || 'Verification failed');
930
+ process.exit(1);
931
+ }
932
+ const data = await verifyRes.json();
933
+ config.set('sessionId', data.sessionId);
934
+ config.set('user', data.user);
935
+ config.set('tenant', data.tenant);
936
+ sessionId = data.sessionId;
937
+ spinner.succeed(data.isNewUser ? 'Account created!' : 'Logged in!');
945
938
  }
946
939
  console.log('');
947
940
  // Create agent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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:' },
171
- ]);
172
141
 
173
- const spinner = ora('Logging in...').start();
174
- log('POST /auth/login', { email });
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
+ }
175
148
 
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
- });
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' },
151
+ ]);
182
152
 
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:' },
@@ -981,68 +999,56 @@ ${chalk.bold('Example:')}
981
999
  if (!sessionId) {
982
1000
  console.log(chalk.yellow('Not logged in yet.\n'));
983
1001
 
984
- const { action } = await inquirer.prompt([
985
- {
986
- type: 'list',
987
- name: 'action',
988
- message: 'Choose:',
989
- choices: [
990
- { name: 'Create account', value: 'signup' },
991
- { name: 'Log in', value: 'login' },
992
- ],
993
- },
1002
+ // OTP-based auth flow
1003
+ const { email } = await inquirer.prompt([
1004
+ { type: 'input', name: 'email', message: 'Email:', validate: (v: string) => v.includes('@') || 'Enter a valid email' },
994
1005
  ]);
995
1006
 
996
- if (action === 'signup') {
997
- const { name, email, password } = await inquirer.prompt([
998
- { type: 'input', name: 'name', message: 'Name:' },
999
- { type: 'input', name: 'email', message: 'Email:' },
1000
- { type: 'password', name: 'password', message: 'Password:' },
1001
- ]);
1007
+ let spinner = ora('Sending verification code...').start();
1002
1008
 
1003
- const spinner = ora('Creating account...').start();
1004
- const res = await fetch(`${API_URL}/auth/signup`, {
1005
- method: 'POST',
1006
- headers: { 'Content-Type': 'application/json' },
1007
- body: JSON.stringify({ name, email, password }),
1008
- });
1009
+ const sendRes = await fetch(`${API_URL}/auth/send-code`, {
1010
+ method: 'POST',
1011
+ headers: { 'Content-Type': 'application/json' },
1012
+ body: JSON.stringify({ email }),
1013
+ });
1009
1014
 
1010
- if (!res.ok) {
1011
- spinner.fail('Signup failed');
1012
- process.exit(1);
1013
- }
1015
+ if (!sendRes.ok) {
1016
+ const err = await sendRes.json().catch(() => ({}));
1017
+ spinner.fail(err.error || 'Failed to send code');
1018
+ process.exit(1);
1019
+ }
1014
1020
 
1015
- const data = await res.json();
1016
- config.set('sessionId', data.sessionId);
1017
- config.set('user', data.user);
1018
- config.set('tenant', data.tenant);
1019
- sessionId = data.sessionId;
1020
- spinner.succeed('Account created!');
1021
- } else {
1022
- const { email, password } = await inquirer.prompt([
1023
- { type: 'input', name: 'email', message: 'Email:' },
1024
- { type: 'password', name: 'password', message: 'Password:' },
1025
- ]);
1021
+ const sendData = await sendRes.json();
1022
+ spinner.succeed('Code sent! Check your email.');
1023
+
1024
+ if (sendData.isNewUser) {
1025
+ console.log(chalk.gray(' No account found - we\'ll create one for you.\n'));
1026
+ }
1026
1027
 
1027
- const spinner = ora('Logging in...').start();
1028
- const res = await fetch(`${API_URL}/auth/login`, {
1029
- method: 'POST',
1030
- headers: { 'Content-Type': 'application/json' },
1031
- body: JSON.stringify({ email, password }),
1032
- });
1028
+ const { code } = await inquirer.prompt([
1029
+ { type: 'input', name: 'code', message: 'Enter 6-digit code:', validate: (v: string) => /^\d{6}$/.test(v) || 'Enter 6 digits' },
1030
+ ]);
1033
1031
 
1034
- if (!res.ok) {
1035
- spinner.fail('Login failed');
1036
- process.exit(1);
1037
- }
1032
+ spinner = ora('Verifying...').start();
1038
1033
 
1039
- const data = await res.json();
1040
- config.set('sessionId', data.sessionId);
1041
- config.set('user', data.user);
1042
- config.set('tenant', data.tenant);
1043
- sessionId = data.sessionId;
1044
- spinner.succeed('Logged in!');
1034
+ const verifyRes = await fetch(`${API_URL}/auth/verify-code`, {
1035
+ method: 'POST',
1036
+ headers: { 'Content-Type': 'application/json' },
1037
+ body: JSON.stringify({ email, code }),
1038
+ });
1039
+
1040
+ if (!verifyRes.ok) {
1041
+ const err = await verifyRes.json().catch(() => ({}));
1042
+ spinner.fail(err.error || 'Verification failed');
1043
+ process.exit(1);
1045
1044
  }
1045
+
1046
+ const data = await verifyRes.json();
1047
+ config.set('sessionId', data.sessionId);
1048
+ config.set('user', data.user);
1049
+ config.set('tenant', data.tenant);
1050
+ sessionId = data.sessionId;
1051
+ spinner.succeed(data.isNewUser ? 'Account created!' : 'Logged in!');
1046
1052
  }
1047
1053
 
1048
1054
  console.log('');