@gopherhole/cli 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/dist/index.js +796 -78
  2. package/package.json +3 -2
  3. package/src/index.ts +863 -76
package/src/index.ts CHANGED
@@ -8,19 +8,332 @@ import open from 'open';
8
8
 
9
9
  const config = new Conf({ projectName: 'gopherhole' });
10
10
  const API_URL = 'https://gopherhole.ai/api';
11
+ const WS_URL = 'wss://gopherhole.helixdata.workers.dev/ws';
12
+
13
+ // Global verbose flag
14
+ let verbose = false;
15
+
16
+ function log(...args: unknown[]) {
17
+ if (verbose) {
18
+ console.log(chalk.gray('[verbose]'), ...args);
19
+ }
20
+ }
21
+
22
+ function logError(context: string, error: unknown) {
23
+ if (verbose) {
24
+ console.error(chalk.red(`[verbose:${context}]`), error);
25
+ }
26
+ }
11
27
 
12
28
  const program = new Command();
13
29
 
14
30
  program
15
31
  .name('gopherhole')
16
- .description('šŸæļø GopherHole CLI - Connect AI agents to the world')
17
- .version('0.1.0');
32
+ .description(`šŸæļø GopherHole CLI - Connect AI agents to the world
33
+
34
+ ${chalk.bold('Quick Start:')}
35
+ $ gopherhole quickstart # Interactive setup wizard
36
+ $ gopherhole signup # Create an account
37
+ $ gopherhole agents create # Create your first agent
38
+
39
+ ${chalk.bold('Examples:')}
40
+ $ gopherhole discover search "weather"
41
+ $ gopherhole send agent-abc123 "Hello!"
42
+ $ gopherhole agents list
43
+
44
+ ${chalk.bold('Documentation:')}
45
+ https://gopherhole.ai/docs
46
+ `)
47
+ .version('0.1.0')
48
+ .option('-v, --verbose', 'Enable verbose output for debugging')
49
+ .hook('preAction', (thisCommand) => {
50
+ verbose = thisCommand.opts().verbose || false;
51
+ if (verbose) {
52
+ console.log(chalk.gray('[verbose] Verbose mode enabled'));
53
+ console.log(chalk.gray(`[verbose] API: ${API_URL}`));
54
+ console.log(chalk.gray(`[verbose] WebSocket: ${WS_URL}`));
55
+ }
56
+ });
57
+
58
+ // ========== QUICKSTART COMMAND ==========
59
+
60
+ program
61
+ .command('quickstart')
62
+ .description(`Interactive setup wizard for new users
63
+
64
+ ${chalk.bold('What it does:')}
65
+ 1. Creates an account (or logs you in)
66
+ 2. Creates your first agent
67
+ 3. Generates starter code
68
+ 4. Shows next steps
69
+
70
+ ${chalk.bold('Example:')}
71
+ $ gopherhole quickstart
72
+ `)
73
+ .action(async () => {
74
+ console.log(chalk.bold('\nšŸæļø Welcome to GopherHole!\n'));
75
+ console.log('This wizard will help you set up your first agent.\n');
76
+
77
+ let sessionId = config.get('sessionId') as string;
78
+
79
+ // Step 1: Auth
80
+ if (sessionId) {
81
+ const user = config.get('user') as { email: string } | undefined;
82
+ console.log(chalk.green(`āœ“ Already logged in as ${user?.email}\n`));
83
+ } else {
84
+ console.log(chalk.bold('Step 1: Create an account\n'));
85
+
86
+ const { action } = await inquirer.prompt([
87
+ {
88
+ type: 'list',
89
+ name: 'action',
90
+ message: 'Do you have a GopherHole account?',
91
+ choices: [
92
+ { name: 'No, create one for me', value: 'signup' },
93
+ { name: 'Yes, log me in', value: 'login' },
94
+ ],
95
+ },
96
+ ]);
97
+
98
+ if (action === 'signup') {
99
+ const { name, email, password } = await inquirer.prompt([
100
+ { type: 'input', name: 'name', message: 'Your name:' },
101
+ { type: 'input', name: 'email', message: 'Email:' },
102
+ { type: 'password', name: 'password', message: 'Password (min 8 chars):' },
103
+ ]);
104
+
105
+ const spinner = ora('Creating your account...').start();
106
+ log('POST /auth/signup', { email });
107
+
108
+ try {
109
+ const res = await fetch(`${API_URL}/auth/signup`, {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({ name, email, password }),
113
+ });
114
+
115
+ if (!res.ok) {
116
+ const err = await res.json();
117
+ logError('signup', err);
118
+ throw new Error(err.error || 'Signup failed');
119
+ }
120
+
121
+ const data = await res.json();
122
+ config.set('sessionId', data.sessionId);
123
+ config.set('user', data.user);
124
+ config.set('tenant', data.tenant);
125
+ sessionId = data.sessionId;
126
+ spinner.succeed('Account created!');
127
+ log('Session ID stored');
128
+ } catch (err) {
129
+ spinner.fail(chalk.red((err as Error).message));
130
+ process.exit(1);
131
+ }
132
+ } else {
133
+ const { email, password } = await inquirer.prompt([
134
+ { type: 'input', name: 'email', message: 'Email:' },
135
+ { type: 'password', name: 'password', message: 'Password:' },
136
+ ]);
137
+
138
+ const spinner = ora('Logging in...').start();
139
+ log('POST /auth/login', { email });
140
+
141
+ try {
142
+ const res = await fetch(`${API_URL}/auth/login`, {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({ email, password }),
146
+ });
147
+
148
+ if (!res.ok) {
149
+ const err = await res.json();
150
+ logError('login', err);
151
+ throw new Error(err.error || 'Login failed');
152
+ }
153
+
154
+ const data = await res.json();
155
+ config.set('sessionId', data.sessionId);
156
+ config.set('user', data.user);
157
+ config.set('tenant', data.tenant);
158
+ sessionId = data.sessionId;
159
+ spinner.succeed('Logged in!');
160
+ log('Session ID stored');
161
+ } catch (err) {
162
+ spinner.fail(chalk.red((err as Error).message));
163
+ process.exit(1);
164
+ }
165
+ }
166
+ }
167
+
168
+ // Step 2: Create agent
169
+ console.log(chalk.bold('\nStep 2: Create your agent\n'));
170
+
171
+ const { agentName, agentDesc } = await inquirer.prompt([
172
+ {
173
+ type: 'input',
174
+ name: 'agentName',
175
+ message: 'Agent name:',
176
+ default: 'my-agent',
177
+ validate: (input) => input.length > 0 || 'Name is required',
178
+ },
179
+ {
180
+ type: 'input',
181
+ name: 'agentDesc',
182
+ message: 'What does your agent do? (optional):',
183
+ },
184
+ ]);
185
+
186
+ const spinner = ora('Creating agent...').start();
187
+ log('POST /agents', { name: agentName });
188
+
189
+ try {
190
+ const res = await fetch(`${API_URL}/agents`, {
191
+ method: 'POST',
192
+ headers: {
193
+ 'Content-Type': 'application/json',
194
+ 'X-Session-ID': sessionId,
195
+ },
196
+ body: JSON.stringify({ name: agentName, description: agentDesc }),
197
+ });
198
+
199
+ if (!res.ok) {
200
+ const err = await res.json();
201
+ logError('create agent', err);
202
+ throw new Error(err.error || 'Failed to create agent');
203
+ }
204
+
205
+ const data = await res.json();
206
+ spinner.succeed('Agent created!');
207
+
208
+ // Step 3: Show results
209
+ console.log(chalk.bold('\n✨ Your agent is ready!\n'));
210
+
211
+ console.log(chalk.bold(' Agent Details:'));
212
+ console.log(` Name: ${chalk.green(data.agent.name)}`);
213
+ console.log(` ID: ${chalk.cyan(data.agent.id)}`);
214
+ console.log(` API Key: ${chalk.yellow(data.apiKey)}`);
215
+
216
+ console.log(chalk.bold('\n Connection Info:'));
217
+ console.log(` WebSocket: ${chalk.cyan(WS_URL)}`);
218
+ console.log(` REST API: ${chalk.cyan('https://gopherhole.ai/a2a')}`);
219
+
220
+ console.log(chalk.red('\n āš ļø Save your API key now - it won\'t be shown again!\n'));
221
+
222
+ // Step 4: Generate code
223
+ console.log(chalk.bold('Step 3: Quick start code\n'));
224
+
225
+ const { language } = await inquirer.prompt([
226
+ {
227
+ type: 'list',
228
+ name: 'language',
229
+ message: 'Which language?',
230
+ choices: [
231
+ { name: 'TypeScript / JavaScript', value: 'ts' },
232
+ { name: 'Python', value: 'py' },
233
+ { name: 'Go', value: 'go' },
234
+ { name: 'Just show me the curl command', value: 'curl' },
235
+ ],
236
+ },
237
+ ]);
238
+
239
+ console.log('');
240
+
241
+ if (language === 'ts') {
242
+ console.log(chalk.gray(' # Install the SDK'));
243
+ console.log(chalk.cyan(' npm install @gopherhole/sdk\n'));
244
+ console.log(chalk.gray(' # agent.ts'));
245
+ console.log(chalk.white(` import { GopherHole } from '@gopherhole/sdk';
246
+
247
+ const client = new GopherHole('${data.apiKey}');
248
+
249
+ async function main() {
250
+ await client.connect();
251
+ console.log('šŸæļø Connected to GopherHole!');
252
+
253
+ // Handle incoming messages
254
+ client.on('message', async (msg) => {
255
+ console.log('From:', msg.from, 'Message:', msg.text);
256
+
257
+ // Reply back
258
+ await client.send(msg.from, 'Hello! I got your message.');
259
+ });
260
+ }
261
+
262
+ main();`));
263
+ } else if (language === 'py') {
264
+ console.log(chalk.gray(' # Install the SDK'));
265
+ console.log(chalk.cyan(' pip install gopherhole\n'));
266
+ console.log(chalk.gray(' # agent.py'));
267
+ console.log(chalk.white(` from gopherhole import GopherHole
268
+
269
+ client = GopherHole("${data.apiKey}")
270
+
271
+ @client.on_message
272
+ async def handle_message(msg):
273
+ print(f"From: {msg.from_agent}, Message: {msg.text}")
274
+ await client.send(msg.from_agent, "Hello! I got your message.")
275
+
276
+ if __name__ == "__main__":
277
+ client.run()`));
278
+ } else if (language === 'go') {
279
+ console.log(chalk.gray(' # Install the SDK'));
280
+ console.log(chalk.cyan(' go get github.com/gopherhole/gopherhole-go\n'));
281
+ console.log(chalk.gray(' // main.go'));
282
+ console.log(chalk.white(` package main
283
+
284
+ import (
285
+ "fmt"
286
+ gopherhole "github.com/gopherhole/gopherhole-go"
287
+ )
288
+
289
+ func main() {
290
+ client := gopherhole.New("${data.apiKey}")
291
+
292
+ client.OnMessage(func(msg *gopherhole.Message) {
293
+ fmt.Printf("From: %s, Message: %s\\n", msg.From, msg.Text)
294
+ client.Send(msg.From, "Hello! I got your message.")
295
+ })
296
+
297
+ client.Connect()
298
+ }`));
299
+ } else {
300
+ console.log(chalk.gray(' # Test with curl'));
301
+ console.log(chalk.white(` curl -X POST https://gopherhole.ai/a2a \\
302
+ -H "Authorization: Bearer ${data.apiKey}" \\
303
+ -H "Content-Type: application/json" \\
304
+ -d '{
305
+ "jsonrpc": "2.0",
306
+ "method": "message/send",
307
+ "params": {
308
+ "message": { "role": "user", "parts": [{ "kind": "text", "text": "Hello!" }] },
309
+ "configuration": { "agentId": "echo" }
310
+ },
311
+ "id": 1
312
+ }'`));
313
+ }
314
+
315
+ console.log(chalk.bold('\nšŸ“š Next steps:\n'));
316
+ console.log(` • Dashboard: ${chalk.cyan('https://gopherhole.ai/dashboard')}`);
317
+ console.log(` • Docs: ${chalk.cyan('https://gopherhole.ai/docs')}`);
318
+ console.log(` • Find agents: ${chalk.cyan('gopherhole discover search')}`);
319
+ console.log(` • List yours: ${chalk.cyan('gopherhole agents list')}`);
320
+ console.log('');
321
+
322
+ } catch (err) {
323
+ spinner.fail(chalk.red((err as Error).message));
324
+ process.exit(1);
325
+ }
326
+ });
18
327
 
19
328
  // ========== AUTH COMMANDS ==========
20
329
 
21
330
  program
22
331
  .command('login')
23
- .description('Log in to GopherHole')
332
+ .description(`Log in to GopherHole
333
+
334
+ ${chalk.bold('Example:')}
335
+ $ gopherhole login
336
+ `)
24
337
  .action(async () => {
25
338
  const { email, password } = await inquirer.prompt([
26
339
  { type: 'input', name: 'email', message: 'Email:' },
@@ -28,6 +341,7 @@ program
28
341
  ]);
29
342
 
30
343
  const spinner = ora('Logging in...').start();
344
+ log('POST /auth/login', { email });
31
345
 
32
346
  try {
33
347
  const res = await fetch(`${API_URL}/auth/login`, {
@@ -38,6 +352,7 @@ program
38
352
 
39
353
  if (!res.ok) {
40
354
  const err = await res.json();
355
+ logError('login', err);
41
356
  throw new Error(err.error || 'Login failed');
42
357
  }
43
358
 
@@ -47,23 +362,33 @@ program
47
362
  config.set('tenant', data.tenant);
48
363
 
49
364
  spinner.succeed(`Logged in as ${chalk.green(data.user.email)}`);
365
+ log('Session stored in:', config.path);
50
366
  } catch (err) {
51
367
  spinner.fail(chalk.red((err as Error).message));
368
+ console.log(chalk.gray('\nTroubleshooting:'));
369
+ console.log(chalk.gray(' • Check your email and password'));
370
+ console.log(chalk.gray(' • Try: gopherhole signup (to create account)'));
371
+ console.log(chalk.gray(' • Run with --verbose for more details'));
52
372
  process.exit(1);
53
373
  }
54
374
  });
55
375
 
56
376
  program
57
377
  .command('signup')
58
- .description('Create a new GopherHole account')
378
+ .description(`Create a new GopherHole account
379
+
380
+ ${chalk.bold('Example:')}
381
+ $ gopherhole signup
382
+ `)
59
383
  .action(async () => {
60
384
  const { name, email, password } = await inquirer.prompt([
61
385
  { type: 'input', name: 'name', message: 'Name:' },
62
386
  { type: 'input', name: 'email', message: 'Email:' },
63
- { type: 'password', name: 'password', message: 'Password:' },
387
+ { type: 'password', name: 'password', message: 'Password (min 8 chars):' },
64
388
  ]);
65
389
 
66
390
  const spinner = ora('Creating account...').start();
391
+ log('POST /auth/signup', { email });
67
392
 
68
393
  try {
69
394
  const res = await fetch(`${API_URL}/auth/signup`, {
@@ -74,6 +399,7 @@ program
74
399
 
75
400
  if (!res.ok) {
76
401
  const err = await res.json();
402
+ logError('signup', err);
77
403
  throw new Error(err.error || 'Signup failed');
78
404
  }
79
405
 
@@ -83,6 +409,7 @@ program
83
409
  config.set('tenant', data.tenant);
84
410
 
85
411
  spinner.succeed(`Account created! Logged in as ${chalk.green(data.user.email)}`);
412
+ console.log(chalk.gray('\nNext: gopherhole agents create'));
86
413
  } catch (err) {
87
414
  spinner.fail(chalk.red((err as Error).message));
88
415
  process.exit(1);
@@ -94,7 +421,8 @@ program
94
421
  .description('Log out of GopherHole')
95
422
  .action(() => {
96
423
  config.clear();
97
- console.log(chalk.green('Logged out successfully'));
424
+ console.log(chalk.green('āœ“ Logged out successfully'));
425
+ log('Config cleared:', config.path);
98
426
  });
99
427
 
100
428
  program
@@ -102,53 +430,83 @@ program
102
430
  .description('Show current logged in user')
103
431
  .action(() => {
104
432
  const user = config.get('user') as { email: string; name: string } | undefined;
433
+ const tenant = config.get('tenant') as { name: string } | undefined;
434
+
105
435
  if (!user) {
106
- console.log(chalk.yellow('Not logged in. Run: gopherhole login'));
436
+ console.log(chalk.yellow('Not logged in.'));
437
+ console.log(chalk.gray('\nTo log in: gopherhole login'));
438
+ console.log(chalk.gray('To sign up: gopherhole signup'));
107
439
  process.exit(1);
108
440
  }
109
- console.log(`Logged in as ${chalk.green(user.email)} (${user.name})`);
441
+
442
+ console.log(`\n ${chalk.bold('User:')} ${user.name} <${user.email}>`);
443
+ console.log(` ${chalk.bold('Org:')} ${tenant?.name || 'Personal'}`);
444
+ console.log(` ${chalk.bold('Config:')} ${config.path}\n`);
110
445
  });
111
446
 
112
447
  // ========== AGENT COMMANDS ==========
113
448
 
114
- const agents = program.command('agents').description('Manage agents');
449
+ const agents = program
450
+ .command('agents')
451
+ .description(`Manage your agents
452
+
453
+ ${chalk.bold('Examples:')}
454
+ $ gopherhole agents list
455
+ $ gopherhole agents create --name "my-bot"
456
+ $ gopherhole agents delete agent-abc123
457
+ `);
115
458
 
116
459
  agents
117
460
  .command('list')
118
461
  .description('List your agents')
119
- .action(async () => {
462
+ .option('--json', 'Output as JSON')
463
+ .action(async (options) => {
120
464
  const sessionId = config.get('sessionId') as string;
121
465
  if (!sessionId) {
122
- console.log(chalk.yellow('Not logged in. Run: gopherhole login'));
466
+ console.log(chalk.yellow('Not logged in.'));
467
+ console.log(chalk.gray('Run: gopherhole login'));
123
468
  process.exit(1);
124
469
  }
125
470
 
126
471
  const spinner = ora('Fetching agents...').start();
472
+ log('GET /agents');
127
473
 
128
474
  try {
129
475
  const res = await fetch(`${API_URL}/agents`, {
130
476
  headers: { 'X-Session-ID': sessionId },
131
477
  });
132
478
 
133
- if (!res.ok) throw new Error('Failed to fetch agents');
479
+ if (!res.ok) {
480
+ const err = await res.json();
481
+ logError('list', err);
482
+ throw new Error('Failed to fetch agents');
483
+ }
134
484
 
135
485
  const data = await res.json();
136
486
  spinner.stop();
137
487
 
488
+ if (options.json) {
489
+ console.log(JSON.stringify(data.agents, null, 2));
490
+ return;
491
+ }
492
+
138
493
  if (data.agents.length === 0) {
139
- console.log(chalk.yellow('No agents yet. Create one with: gopherhole agents create'));
494
+ console.log(chalk.yellow('\nNo agents yet.\n'));
495
+ console.log(chalk.gray('Create one: gopherhole agents create'));
496
+ console.log(chalk.gray('Or try: gopherhole quickstart\n'));
140
497
  return;
141
498
  }
142
499
 
143
- console.log(chalk.bold('\nYour Agents:\n'));
500
+ console.log(chalk.bold('\nšŸæļø Your Agents:\n'));
144
501
  for (const agent of data.agents) {
145
502
  const status = agent.status === 'active' ? chalk.green('ā—') : chalk.red('ā—');
146
- console.log(` ${status} ${chalk.bold(agent.name)} (${agent.id})`);
503
+ console.log(` ${status} ${chalk.bold(agent.name)}`);
504
+ console.log(` ID: ${chalk.cyan(agent.id)}`);
147
505
  if (agent.description) {
148
506
  console.log(` ${chalk.gray(agent.description)}`);
149
507
  }
508
+ console.log('');
150
509
  }
151
- console.log('');
152
510
  } catch (err) {
153
511
  spinner.fail(chalk.red((err as Error).message));
154
512
  process.exit(1);
@@ -157,13 +515,19 @@ agents
157
515
 
158
516
  agents
159
517
  .command('create')
160
- .description('Create a new agent')
518
+ .description(`Create a new agent
519
+
520
+ ${chalk.bold('Examples:')}
521
+ $ gopherhole agents create
522
+ $ gopherhole agents create --name "weather-bot" --description "Gets weather"
523
+ `)
161
524
  .option('-n, --name <name>', 'Agent name')
162
525
  .option('-d, --description <description>', 'Agent description')
163
526
  .action(async (options) => {
164
527
  const sessionId = config.get('sessionId') as string;
165
528
  if (!sessionId) {
166
- console.log(chalk.yellow('Not logged in. Run: gopherhole login'));
529
+ console.log(chalk.yellow('Not logged in.'));
530
+ console.log(chalk.gray('Run: gopherhole login'));
167
531
  process.exit(1);
168
532
  }
169
533
 
@@ -171,7 +535,12 @@ agents
171
535
 
172
536
  if (!name) {
173
537
  const answers = await inquirer.prompt([
174
- { type: 'input', name: 'name', message: 'Agent name:' },
538
+ {
539
+ type: 'input',
540
+ name: 'name',
541
+ message: 'Agent name:',
542
+ validate: (input) => input.length > 0 || 'Name is required',
543
+ },
175
544
  { type: 'input', name: 'description', message: 'Description (optional):' },
176
545
  ]);
177
546
  name = answers.name;
@@ -179,6 +548,7 @@ agents
179
548
  }
180
549
 
181
550
  const spinner = ora('Creating agent...').start();
551
+ log('POST /agents', { name, description });
182
552
 
183
553
  try {
184
554
  const res = await fetch(`${API_URL}/agents`, {
@@ -192,28 +562,29 @@ agents
192
562
 
193
563
  if (!res.ok) {
194
564
  const err = await res.json();
565
+ logError('create', err);
195
566
  throw new Error(err.error || 'Failed to create agent');
196
567
  }
197
568
 
198
569
  const data = await res.json();
199
570
  spinner.succeed(`Agent created: ${chalk.green(data.agent.name)}`);
200
- console.log('');
201
- console.log(` ${chalk.bold('Agent ID:')} ${data.agent.id}`);
202
- console.log(` ${chalk.bold('API Key:')} ${chalk.cyan(data.apiKey)}`);
203
- console.log('');
204
- console.log(chalk.gray('Save this API key - it won\'t be shown again!'));
205
- console.log('');
206
- console.log(chalk.bold('Quick start:'));
207
- console.log(chalk.gray(`
208
- import { GopherHole } from '@gopherhole/sdk';
571
+
572
+ console.log(chalk.bold('\n Agent Details:'));
573
+ console.log(` ID: ${chalk.cyan(data.agent.id)}`);
574
+ console.log(` API Key: ${chalk.yellow(data.apiKey)}`);
575
+
576
+ console.log(chalk.bold('\n Connection:'));
577
+ console.log(` WebSocket: ${chalk.cyan(WS_URL)}`);
578
+ console.log(` REST API: ${chalk.cyan('https://gopherhole.ai/a2a')}`);
209
579
 
210
- const hub = new GopherHole('${data.apiKey}');
211
- await hub.connect();
580
+ console.log(chalk.red('\n āš ļø Save your API key now - it won\'t be shown again!'));
212
581
 
213
- hub.on('message', (msg) => {
214
- console.log('Received:', msg);
215
- });
216
- `));
582
+ console.log(chalk.bold('\n Quick test:'));
583
+ console.log(chalk.white(` curl -H "Authorization: Bearer ${data.apiKey}" \\
584
+ https://gopherhole.ai/a2a -d '{"jsonrpc":"2.0","method":"agent/info","id":1}'`));
585
+
586
+ console.log(chalk.gray('\n Full docs: https://gopherhole.ai/docs'));
587
+ console.log('');
217
588
  } catch (err) {
218
589
  spinner.fail(chalk.red((err as Error).message));
219
590
  process.exit(1);
@@ -222,29 +593,38 @@ agents
222
593
 
223
594
  agents
224
595
  .command('delete <agentId>')
225
- .description('Delete an agent')
226
- .action(async (agentId) => {
596
+ .description(`Delete an agent
597
+
598
+ ${chalk.bold('Example:')}
599
+ $ gopherhole agents delete agent-abc123
600
+ `)
601
+ .option('-f, --force', 'Skip confirmation')
602
+ .action(async (agentId, options) => {
227
603
  const sessionId = config.get('sessionId') as string;
228
604
  if (!sessionId) {
229
- console.log(chalk.yellow('Not logged in. Run: gopherhole login'));
605
+ console.log(chalk.yellow('Not logged in.'));
606
+ console.log(chalk.gray('Run: gopherhole login'));
230
607
  process.exit(1);
231
608
  }
232
609
 
233
- const { confirm } = await inquirer.prompt([
234
- {
235
- type: 'confirm',
236
- name: 'confirm',
237
- message: `Are you sure you want to delete agent ${agentId}?`,
238
- default: false,
239
- },
240
- ]);
610
+ if (!options.force) {
611
+ const { confirm } = await inquirer.prompt([
612
+ {
613
+ type: 'confirm',
614
+ name: 'confirm',
615
+ message: `Delete agent ${chalk.cyan(agentId)}? This cannot be undone.`,
616
+ default: false,
617
+ },
618
+ ]);
241
619
 
242
- if (!confirm) {
243
- console.log('Cancelled');
244
- return;
620
+ if (!confirm) {
621
+ console.log('Cancelled.');
622
+ return;
623
+ }
245
624
  }
246
625
 
247
626
  const spinner = ora('Deleting agent...').start();
627
+ log('DELETE /agents/' + agentId);
248
628
 
249
629
  try {
250
630
  const res = await fetch(`${API_URL}/agents/${agentId}`, {
@@ -252,7 +632,11 @@ agents
252
632
  headers: { 'X-Session-ID': sessionId },
253
633
  });
254
634
 
255
- if (!res.ok) throw new Error('Failed to delete agent');
635
+ if (!res.ok) {
636
+ const err = await res.json();
637
+ logError('delete', err);
638
+ throw new Error(err.error || 'Failed to delete agent');
639
+ }
256
640
 
257
641
  spinner.succeed('Agent deleted');
258
642
  } catch (err) {
@@ -265,24 +649,34 @@ agents
265
649
 
266
650
  program
267
651
  .command('init')
268
- .description('Initialize GopherHole in current directory')
652
+ .description(`Initialize GopherHole in current directory
653
+
654
+ ${chalk.bold('What it does:')}
655
+ • Creates .env with your API key
656
+ • Generates starter code (agent.ts)
657
+ • Sets up package.json if needed
658
+
659
+ ${chalk.bold('Example:')}
660
+ $ mkdir my-agent && cd my-agent
661
+ $ gopherhole init
662
+ `)
269
663
  .action(async () => {
270
- console.log(chalk.bold('\nšŸæļø GopherHole Init\n'));
664
+ console.log(chalk.bold('\nšŸæļø Initialize GopherHole Project\n'));
271
665
 
272
666
  let sessionId = config.get('sessionId') as string;
273
667
 
274
668
  // Check if logged in
275
669
  if (!sessionId) {
276
- console.log('First, let\'s log you in (or create an account).\n');
670
+ console.log(chalk.yellow('Not logged in yet.\n'));
277
671
 
278
672
  const { action } = await inquirer.prompt([
279
673
  {
280
674
  type: 'list',
281
675
  name: 'action',
282
- message: 'Do you have a GopherHole account?',
676
+ message: 'Choose:',
283
677
  choices: [
284
- { name: 'Yes, log me in', value: 'login' },
285
- { name: 'No, create one', value: 'signup' },
678
+ { name: 'Create account', value: 'signup' },
679
+ { name: 'Log in', value: 'login' },
286
680
  ],
287
681
  },
288
682
  ]);
@@ -368,41 +762,73 @@ program
368
762
 
369
763
  // Write .env file
370
764
  const fs = await import('fs');
371
- fs.writeFileSync('.env', `GOPHERHOLE_API_KEY=${data.apiKey}\n`);
372
- console.log(chalk.green('āœ“ Created .env with API key'));
765
+ fs.writeFileSync('.env', `# GopherHole Configuration
766
+ GOPHERHOLE_API_KEY=${data.apiKey}
767
+ GOPHERHOLE_AGENT_ID=${data.agent.id}
768
+ `);
769
+ console.log(chalk.green('āœ“ Created .env'));
373
770
 
374
771
  // Write example code
375
772
  const exampleCode = `import { GopherHole } from '@gopherhole/sdk';
773
+ import 'dotenv/config';
376
774
 
377
- const hub = new GopherHole(process.env.GOPHERHOLE_API_KEY);
775
+ const client = new GopherHole(process.env.GOPHERHOLE_API_KEY!);
378
776
 
379
777
  async function main() {
380
- await hub.connect();
381
- console.log('šŸæļø Connected to GopherHole!');
778
+ await client.connect();
779
+ console.log('šŸæļø Connected to GopherHole as ${agentName}!');
382
780
 
383
- hub.on('message', async (msg) => {
384
- console.log(\`Message from \${msg.from}:\`, msg.payload);
781
+ // Handle incoming messages
782
+ client.on('message', async (msg) => {
783
+ console.log(\`šŸ“© Message from \${msg.from}:\`, msg.text);
385
784
 
386
- // Reply to the message
387
- await hub.sendText(msg.from, 'Hello back!');
785
+ // Echo back
786
+ await client.send(msg.from, \`You said: \${msg.text}\`);
787
+ });
788
+
789
+ // Keep alive
790
+ process.on('SIGINT', async () => {
791
+ console.log('\\nšŸ‘‹ Disconnecting...');
792
+ await client.disconnect();
793
+ process.exit(0);
388
794
  });
389
795
  }
390
796
 
391
797
  main().catch(console.error);
392
798
  `;
393
799
  fs.writeFileSync('agent.ts', exampleCode);
394
- console.log(chalk.green('āœ“ Created agent.ts example'));
800
+ console.log(chalk.green('āœ“ Created agent.ts'));
801
+
802
+ // Create package.json if it doesn't exist
803
+ if (!fs.existsSync('package.json')) {
804
+ const pkg = {
805
+ name: agentName,
806
+ version: '0.1.0',
807
+ type: 'module',
808
+ scripts: {
809
+ start: 'tsx agent.ts',
810
+ dev: 'tsx watch agent.ts',
811
+ },
812
+ dependencies: {
813
+ '@gopherhole/sdk': '^0.1.0',
814
+ dotenv: '^16.0.0',
815
+ },
816
+ devDependencies: {
817
+ tsx: '^4.0.0',
818
+ typescript: '^5.0.0',
819
+ },
820
+ };
821
+ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
822
+ console.log(chalk.green('āœ“ Created package.json'));
823
+ }
395
824
 
396
- console.log('');
825
+ console.log(chalk.bold('\nšŸŽ‰ Project initialized!\n'));
397
826
  console.log(chalk.bold('Next steps:'));
398
- console.log('');
399
- console.log(' 1. Install the SDK:');
400
- console.log(chalk.cyan(' npm install @gopherhole/sdk'));
401
- console.log('');
402
- console.log(' 2. Run your agent:');
403
- console.log(chalk.cyan(' npx ts-node agent.ts'));
827
+ console.log(chalk.cyan(' npm install'));
828
+ console.log(chalk.cyan(' npm start'));
404
829
  console.log('');
405
830
  console.log(chalk.gray(`Dashboard: https://gopherhole.ai/dashboard`));
831
+ console.log(chalk.gray(`Docs: https://gopherhole.ai/docs`));
406
832
  console.log('');
407
833
  });
408
834
 
@@ -410,15 +836,22 @@ main().catch(console.error);
410
836
 
411
837
  program
412
838
  .command('send <agentId> <message>')
413
- .description('Send a message to an agent (for testing)')
839
+ .description(`Send a message to an agent (for testing)
840
+
841
+ ${chalk.bold('Examples:')}
842
+ $ gopherhole send echo "Hello!"
843
+ $ gopherhole send agent-abc123 "What's the weather?"
844
+ `)
414
845
  .action(async (agentId, message) => {
415
846
  const sessionId = config.get('sessionId') as string;
416
847
  if (!sessionId) {
417
- console.log(chalk.yellow('Not logged in. Run: gopherhole login'));
848
+ console.log(chalk.yellow('Not logged in.'));
849
+ console.log(chalk.gray('Run: gopherhole login'));
418
850
  process.exit(1);
419
851
  }
420
852
 
421
- const spinner = ora('Sending message...').start();
853
+ const spinner = ora(`Sending to ${agentId}...`).start();
854
+ log('POST /a2a message/send', { to: agentId, message });
422
855
 
423
856
  try {
424
857
  const res = await fetch(`${API_URL}/../a2a`, {
@@ -439,13 +872,367 @@ program
439
872
  });
440
873
 
441
874
  const data = await res.json();
875
+
876
+ if (data.error) {
877
+ logError('send', data.error);
878
+ throw new Error(data.error.message || 'Send failed');
879
+ }
880
+
442
881
  spinner.succeed('Message sent!');
443
- console.log(chalk.gray(JSON.stringify(data, null, 2)));
882
+
883
+ if (data.result?.task) {
884
+ console.log(chalk.gray(`\nTask ID: ${data.result.task.id}`));
885
+ console.log(chalk.gray(`Status: ${data.result.task.status.state}`));
886
+ }
887
+
888
+ if (verbose) {
889
+ console.log(chalk.gray('\nFull response:'));
890
+ console.log(JSON.stringify(data, null, 2));
891
+ }
892
+ } catch (err) {
893
+ spinner.fail(chalk.red((err as Error).message));
894
+ process.exit(1);
895
+ }
896
+ });
897
+
898
+ // ========== DISCOVER COMMANDS ==========
899
+
900
+ const discover = program
901
+ .command('discover')
902
+ .description(`Discover public agents
903
+
904
+ ${chalk.bold('Examples:')}
905
+ $ gopherhole discover search "weather"
906
+ $ gopherhole discover top
907
+ $ gopherhole discover info agent-abc123
908
+ `);
909
+
910
+ discover
911
+ .command('search [query]')
912
+ .description(`Search for agents
913
+
914
+ ${chalk.bold('Examples:')}
915
+ $ gopherhole discover search
916
+ $ gopherhole discover search "email assistant"
917
+ $ gopherhole discover search --category productivity
918
+ $ gopherhole discover search --tag ai --sort popular
919
+ `)
920
+ .option('-c, --category <category>', 'Filter by category')
921
+ .option('-t, --tag <tag>', 'Filter by tag')
922
+ .option('-s, --sort <sort>', 'Sort by: rating, popular, recent', 'rating')
923
+ .option('-l, --limit <limit>', 'Number of results', '20')
924
+ .action(async (query, options) => {
925
+ const spinner = ora('Searching agents...').start();
926
+
927
+ try {
928
+ const params = new URLSearchParams();
929
+ if (query) params.set('q', query);
930
+ if (options.category) params.set('category', options.category);
931
+ if (options.tag) params.set('tag', options.tag);
932
+ if (options.sort) params.set('sort', options.sort);
933
+ if (options.limit) params.set('limit', options.limit);
934
+
935
+ log('GET /discover/agents?' + params.toString());
936
+ const res = await fetch(`${API_URL}/discover/agents?${params}`);
937
+ const data = await res.json();
938
+ spinner.stop();
939
+
940
+ if (data.agents.length === 0) {
941
+ console.log(chalk.yellow('\nNo agents found.'));
942
+ console.log(chalk.gray('Try: gopherhole discover search (without query)'));
943
+ return;
944
+ }
945
+
946
+ console.log(chalk.bold(`\nšŸ” Found ${data.agents.length} agents:\n`));
947
+
948
+ for (const agent of data.agents) {
949
+ const stars = 'ā˜…'.repeat(Math.round(agent.avgRating)) + 'ā˜†'.repeat(5 - Math.round(agent.avgRating));
950
+ const pricing = agent.pricing === 'free' ? chalk.green('FREE') :
951
+ agent.pricing === 'paid' ? chalk.blue('PAID') : chalk.gray('CONTACT');
952
+
953
+ console.log(` ${chalk.bold(agent.name)} ${chalk.yellow(stars)} (${agent.ratingCount})`);
954
+ console.log(` ${chalk.gray(agent.description || 'No description')}`);
955
+ console.log(` ${chalk.cyan(agent.id)} | ${pricing} | ${agent.category || 'uncategorized'}`);
956
+ console.log('');
957
+ }
958
+
959
+ console.log(chalk.gray(`Tip: gopherhole discover info <agent-id> for details\n`));
960
+ } catch (err) {
961
+ spinner.fail(chalk.red((err as Error).message));
962
+ process.exit(1);
963
+ }
964
+ });
965
+
966
+ discover
967
+ .command('categories')
968
+ .description('List available categories')
969
+ .action(async () => {
970
+ const spinner = ora('Fetching categories...').start();
971
+
972
+ try {
973
+ const res = await fetch(`${API_URL}/discover/categories`);
974
+ const data = await res.json();
975
+ spinner.stop();
976
+
977
+ if (data.categories.length === 0) {
978
+ console.log(chalk.yellow('No categories yet'));
979
+ return;
980
+ }
981
+
982
+ console.log(chalk.bold('\nšŸ“‚ Categories:\n'));
983
+ for (const cat of data.categories) {
984
+ console.log(` ${chalk.bold(cat.name)} (${cat.count} agents)`);
985
+ }
986
+ console.log(chalk.gray('\nSearch by category: gopherhole discover search --category <name>\n'));
987
+ } catch (err) {
988
+ spinner.fail(chalk.red((err as Error).message));
989
+ process.exit(1);
990
+ }
991
+ });
992
+
993
+ discover
994
+ .command('featured')
995
+ .description('Show featured agents')
996
+ .action(async () => {
997
+ const spinner = ora('Fetching featured agents...').start();
998
+
999
+ try {
1000
+ const res = await fetch(`${API_URL}/discover/featured`);
1001
+ const data = await res.json();
1002
+ spinner.stop();
1003
+
1004
+ if (data.featured.length === 0) {
1005
+ console.log(chalk.yellow('No featured agents yet'));
1006
+ return;
1007
+ }
1008
+
1009
+ console.log(chalk.bold('\n⭐ Featured Agents:\n'));
1010
+ for (const agent of data.featured) {
1011
+ const stars = 'ā˜…'.repeat(Math.round(agent.avgRating)) + 'ā˜†'.repeat(5 - Math.round(agent.avgRating));
1012
+ console.log(` ${chalk.bold(agent.name)} ${chalk.yellow(stars)}`);
1013
+ console.log(` ${chalk.gray(agent.description || 'No description')}`);
1014
+ console.log(` ${chalk.cyan(agent.id)}`);
1015
+ console.log('');
1016
+ }
1017
+ } catch (err) {
1018
+ spinner.fail(chalk.red((err as Error).message));
1019
+ process.exit(1);
1020
+ }
1021
+ });
1022
+
1023
+ discover
1024
+ .command('info <agentId>')
1025
+ .description(`Get detailed info about an agent
1026
+
1027
+ ${chalk.bold('Example:')}
1028
+ $ gopherhole discover info agent-abc123
1029
+ `)
1030
+ .action(async (agentId) => {
1031
+ const spinner = ora('Fetching agent info...').start();
1032
+
1033
+ try {
1034
+ log('GET /discover/agents/' + agentId);
1035
+ const res = await fetch(`${API_URL}/discover/agents/${agentId}`);
1036
+ if (!res.ok) {
1037
+ throw new Error('Agent not found');
1038
+ }
1039
+ const data = await res.json();
1040
+ spinner.stop();
1041
+
1042
+ const agent = data.agent;
1043
+ const stars = 'ā˜…'.repeat(Math.round(agent.stats.avgRating)) + 'ā˜†'.repeat(5 - Math.round(agent.stats.avgRating));
1044
+
1045
+ console.log(chalk.bold(`\nšŸæļø ${agent.name}\n`));
1046
+ console.log(` ${chalk.gray(agent.description || 'No description')}`);
1047
+ console.log('');
1048
+ console.log(` ${chalk.bold('Rating:')} ${chalk.yellow(stars)} ${agent.stats.avgRating.toFixed(1)}/5 (${agent.stats.ratingCount} reviews)`);
1049
+ console.log(` ${chalk.bold('Messages:')} ${agent.stats.totalMessages.toLocaleString()}`);
1050
+ console.log(` ${chalk.bold('Success:')} ${(agent.stats.successRate * 100).toFixed(1)}%`);
1051
+ console.log(` ${chalk.bold('Response:')} ${agent.stats.avgResponseTime}ms avg`);
1052
+ console.log(` ${chalk.bold('Pricing:')} ${agent.pricing}`);
1053
+ console.log(` ${chalk.bold('Category:')} ${agent.category || 'None'}`);
1054
+ console.log(` ${chalk.bold('By:')} ${agent.tenantName}`);
1055
+
1056
+ if (agent.tags.length > 0) {
1057
+ console.log(` ${chalk.bold('Tags:')} ${agent.tags.join(', ')}`);
1058
+ }
1059
+ if (agent.websiteUrl) {
1060
+ console.log(` ${chalk.bold('Website:')} ${chalk.cyan(agent.websiteUrl)}`);
1061
+ }
1062
+ if (agent.docsUrl) {
1063
+ console.log(` ${chalk.bold('Docs:')} ${chalk.cyan(agent.docsUrl)}`);
1064
+ }
1065
+
1066
+ if (agent.agentCard?.skills?.length) {
1067
+ console.log(chalk.bold('\n Skills:'));
1068
+ for (const skill of agent.agentCard.skills) {
1069
+ console.log(` • ${skill.name}${skill.description ? ` - ${chalk.gray(skill.description)}` : ''}`);
1070
+ }
1071
+ }
1072
+
1073
+ if (data.reviews.length > 0) {
1074
+ console.log(chalk.bold('\n Recent Reviews:'));
1075
+ for (const review of data.reviews.slice(0, 3)) {
1076
+ const reviewStars = 'ā˜…'.repeat(review.rating) + 'ā˜†'.repeat(5 - review.rating);
1077
+ console.log(` ${chalk.yellow(reviewStars)} "${review.review}"`);
1078
+ console.log(` ${chalk.gray(`- ${review.reviewer_name}`)}`);
1079
+ }
1080
+ }
1081
+
1082
+ console.log(chalk.bold('\n Quick start:'));
1083
+ console.log(chalk.cyan(` await client.send('${agentId}', 'Hello!')`));
1084
+ console.log('');
444
1085
  } catch (err) {
445
1086
  spinner.fail(chalk.red((err as Error).message));
446
1087
  process.exit(1);
447
1088
  }
448
1089
  });
449
1090
 
1091
+ discover
1092
+ .command('top')
1093
+ .description(`Show top-rated agents
1094
+
1095
+ ${chalk.bold('Example:')}
1096
+ $ gopherhole discover top
1097
+ $ gopherhole discover top --limit 5
1098
+ `)
1099
+ .option('-l, --limit <limit>', 'Number of results', '10')
1100
+ .action(async (options) => {
1101
+ const spinner = ora('Fetching top agents...').start();
1102
+
1103
+ try {
1104
+ const res = await fetch(`${API_URL}/discover/agents?sort=rating&limit=${options.limit}`);
1105
+ const data = await res.json();
1106
+ spinner.stop();
1107
+
1108
+ console.log(chalk.bold('\nšŸ† Top Rated Agents:\n'));
1109
+
1110
+ let rank = 1;
1111
+ for (const agent of data.agents) {
1112
+ const stars = 'ā˜…'.repeat(Math.round(agent.avgRating)) + 'ā˜†'.repeat(5 - Math.round(agent.avgRating));
1113
+ const medal = rank === 1 ? 'šŸ„‡' : rank === 2 ? '🄈' : rank === 3 ? 'šŸ„‰' : `#${rank}`;
1114
+ console.log(` ${medal} ${chalk.bold(agent.name)} ${chalk.yellow(stars)} (${agent.ratingCount})`);
1115
+ console.log(` ${chalk.gray(agent.description?.slice(0, 60) || 'No description')}${(agent.description?.length || 0) > 60 ? '...' : ''}`);
1116
+ console.log(` ${chalk.cyan(agent.id)}`);
1117
+ console.log('');
1118
+ rank++;
1119
+ }
1120
+ } catch (err) {
1121
+ spinner.fail(chalk.red((err as Error).message));
1122
+ process.exit(1);
1123
+ }
1124
+ });
1125
+
1126
+ discover
1127
+ .command('rate <agentId>')
1128
+ .description(`Rate an agent
1129
+
1130
+ ${chalk.bold('Example:')}
1131
+ $ gopherhole discover rate agent-abc123
1132
+ $ gopherhole discover rate agent-abc123 --rating 5 --message "Great bot!"
1133
+ `)
1134
+ .option('-r, --rating <rating>', 'Rating 1-5')
1135
+ .option('-m, --message <review>', 'Review message')
1136
+ .action(async (agentId, options) => {
1137
+ const sessionId = config.get('sessionId') as string;
1138
+ if (!sessionId) {
1139
+ console.log(chalk.yellow('Not logged in.'));
1140
+ console.log(chalk.gray('Run: gopherhole login'));
1141
+ process.exit(1);
1142
+ }
1143
+
1144
+ let { rating, message: review } = options;
1145
+
1146
+ if (!rating) {
1147
+ const answers = await inquirer.prompt([
1148
+ {
1149
+ type: 'list',
1150
+ name: 'rating',
1151
+ message: 'Your rating:',
1152
+ choices: [
1153
+ { name: 'ā˜…ā˜…ā˜…ā˜…ā˜… Excellent', value: 5 },
1154
+ { name: 'ā˜…ā˜…ā˜…ā˜…ā˜† Great', value: 4 },
1155
+ { name: 'ā˜…ā˜…ā˜…ā˜†ā˜† Good', value: 3 },
1156
+ { name: 'ā˜…ā˜…ā˜†ā˜†ā˜† Fair', value: 2 },
1157
+ { name: 'ā˜…ā˜†ā˜†ā˜†ā˜† Poor', value: 1 },
1158
+ ],
1159
+ },
1160
+ {
1161
+ type: 'input',
1162
+ name: 'review',
1163
+ message: 'Review (optional):',
1164
+ },
1165
+ ]);
1166
+ rating = answers.rating;
1167
+ review = answers.review;
1168
+ }
1169
+
1170
+ const spinner = ora('Submitting rating...').start();
1171
+
1172
+ try {
1173
+ const res = await fetch(`${API_URL}/discover/agents/${agentId}/rate`, {
1174
+ method: 'POST',
1175
+ headers: {
1176
+ 'Content-Type': 'application/json',
1177
+ 'X-Session-ID': sessionId,
1178
+ },
1179
+ body: JSON.stringify({
1180
+ rating: parseInt(rating),
1181
+ review: review || undefined,
1182
+ }),
1183
+ });
1184
+
1185
+ if (!res.ok) {
1186
+ const err = await res.json();
1187
+ throw new Error(err.error || 'Failed to rate');
1188
+ }
1189
+
1190
+ const data = await res.json();
1191
+ spinner.succeed(`Rated! New average: ${chalk.yellow(data.avgRating.toFixed(1))} (${data.ratingCount} reviews)`);
1192
+ } catch (err) {
1193
+ spinner.fail(chalk.red((err as Error).message));
1194
+ process.exit(1);
1195
+ }
1196
+ });
1197
+
1198
+ // ========== STATUS COMMAND ==========
1199
+
1200
+ program
1201
+ .command('status')
1202
+ .description('Show GopherHole service status and your session info')
1203
+ .action(async () => {
1204
+ console.log(chalk.bold('\nšŸæļø GopherHole Status\n'));
1205
+
1206
+ // Check API health
1207
+ const spinner = ora('Checking API...').start();
1208
+ try {
1209
+ const start = Date.now();
1210
+ const res = await fetch(`${API_URL.replace('/api', '')}/health`);
1211
+ const latency = Date.now() - start;
1212
+
1213
+ if (res.ok) {
1214
+ spinner.succeed(`API: ${chalk.green('Online')} (${latency}ms)`);
1215
+ } else {
1216
+ spinner.fail(`API: ${chalk.red('Degraded')}`);
1217
+ }
1218
+ } catch {
1219
+ spinner.fail(`API: ${chalk.red('Offline')}`);
1220
+ }
1221
+
1222
+ // Check login status
1223
+ const user = config.get('user') as { email: string } | undefined;
1224
+ if (user) {
1225
+ console.log(` Auth: ${chalk.green('Logged in')} as ${user.email}`);
1226
+ } else {
1227
+ console.log(` Auth: ${chalk.yellow('Not logged in')}`);
1228
+ }
1229
+
1230
+ console.log(` Config: ${config.path}`);
1231
+ console.log('');
1232
+ console.log(chalk.gray(' Dashboard: https://gopherhole.ai/dashboard'));
1233
+ console.log(chalk.gray(' Status: https://status.gopherhole.ai'));
1234
+ console.log('');
1235
+ });
1236
+
450
1237
  // Parse and run
451
1238
  program.parse();