@gopherhole/cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,7 @@ The CLI stores configuration in:
68
68
 
69
69
  - Website: https://gopherhole.ai
70
70
  - Dashboard: https://gopherhole.ai/dashboard
71
- - Docs: https://gopherhole.ai/docs
71
+ - Docs: https://docs.gopherhole.ai
72
72
  - GitHub: https://github.com/gopherhole
73
73
 
74
74
  ## License
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ ${chalk_1.default.bold('Examples:')}
69
69
  $ gopherhole agents list
70
70
 
71
71
  ${chalk_1.default.bold('Documentation:')}
72
- https://gopherhole.ai/docs
72
+ https://docs.gopherhole.ai
73
73
  `)
74
74
  .version('0.1.0')
75
75
  .option('-v, --verbose', 'Enable verbose output for debugging')
@@ -320,7 +320,7 @@ ${chalk_1.default.bold('Example:')}
320
320
  }
321
321
  console.log(chalk_1.default.bold('\nšŸ“š Next steps:\n'));
322
322
  console.log(` • Dashboard: ${chalk_1.default.cyan('https://gopherhole.ai/dashboard')}`);
323
- console.log(` • Docs: ${chalk_1.default.cyan('https://gopherhole.ai/docs')}`);
323
+ console.log(` • Docs: ${chalk_1.default.cyan('https://docs.gopherhole.ai')}`);
324
324
  console.log(` • Find agents: ${chalk_1.default.cyan('gopherhole discover search')}`);
325
325
  console.log(` • List yours: ${chalk_1.default.cyan('gopherhole agents list')}`);
326
326
  console.log('');
@@ -553,7 +553,7 @@ ${chalk_1.default.bold('Examples:')}
553
553
  console.log(chalk_1.default.bold('\n Quick test:'));
554
554
  console.log(chalk_1.default.white(` curl -H "Authorization: Bearer ${data.apiKey}" \\
555
555
  https://gopherhole.ai/a2a -d '{"jsonrpc":"2.0","method":"agent/info","id":1}'`));
556
- console.log(chalk_1.default.gray('\n Full docs: https://gopherhole.ai/docs'));
556
+ console.log(chalk_1.default.gray('\n Full docs: https://docs.gopherhole.ai'));
557
557
  console.log('');
558
558
  }
559
559
  catch (err) {
@@ -609,6 +609,124 @@ ${chalk_1.default.bold('Example:')}
609
609
  process.exit(1);
610
610
  }
611
611
  });
612
+ agents
613
+ .command('regenerate-key <agentId>')
614
+ .description(`Regenerate API key for an agent
615
+
616
+ ${chalk_1.default.bold('Example:')}
617
+ $ gopherhole agents regenerate-key agent-abc123
618
+
619
+ ${chalk_1.default.yellow('āš ļø Warning:')} This will invalidate the current key.
620
+ All connected agents using this key will stop working.
621
+ `)
622
+ .option('-f, --force', 'Skip confirmation')
623
+ .option('--json', 'Output as JSON')
624
+ .action(async (agentId, options) => {
625
+ const sessionId = config.get('sessionId');
626
+ if (!sessionId) {
627
+ console.log(chalk_1.default.yellow('Not logged in.'));
628
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
629
+ process.exit(1);
630
+ }
631
+ if (!options.force) {
632
+ const { confirm } = await inquirer_1.default.prompt([
633
+ {
634
+ type: 'confirm',
635
+ name: 'confirm',
636
+ message: `Regenerate API key for ${chalk_1.default.cyan(agentId)}? This will invalidate the current key.`,
637
+ default: false,
638
+ },
639
+ ]);
640
+ if (!confirm) {
641
+ console.log('Cancelled.');
642
+ return;
643
+ }
644
+ }
645
+ const spinner = (0, ora_1.default)('Regenerating API key...').start();
646
+ log('POST /agents/' + agentId + '/regenerate-key');
647
+ try {
648
+ const res = await fetch(`${API_URL}/agents/${agentId}/regenerate-key`, {
649
+ method: 'POST',
650
+ headers: {
651
+ 'Content-Type': 'application/json',
652
+ 'X-Session-ID': sessionId,
653
+ },
654
+ body: JSON.stringify({}),
655
+ });
656
+ if (!res.ok) {
657
+ const err = await res.json();
658
+ logError('regenerate-key', err);
659
+ throw new Error(err.error || 'Failed to regenerate key');
660
+ }
661
+ const data = await res.json();
662
+ spinner.succeed('API key regenerated');
663
+ if (options.json) {
664
+ console.log(JSON.stringify({ apiKey: data.apiKey }, null, 2));
665
+ }
666
+ else {
667
+ console.log('');
668
+ console.log(chalk_1.default.bold('New API Key:'));
669
+ console.log(chalk_1.default.green(data.apiKey));
670
+ console.log('');
671
+ console.log(chalk_1.default.yellow('āš ļø Copy this key now — it won\'t be shown again!'));
672
+ }
673
+ }
674
+ catch (err) {
675
+ spinner.fail(chalk_1.default.red(err.message));
676
+ process.exit(1);
677
+ }
678
+ });
679
+ agents
680
+ .command('sync-card <agentId>')
681
+ .description(`Sync agent card from its /.well-known/agent.json URL
682
+
683
+ ${chalk_1.default.bold('What it does:')}
684
+ Fetches the agent card from the agent's URL and updates
685
+ the GopherHole registry. Use after updating your agent's
686
+ skills, description, or capabilities.
687
+
688
+ ${chalk_1.default.bold('Examples:')}
689
+ $ gopherhole agents sync-card agent-abc123
690
+ $ gopherhole agents sync-card my-agent
691
+ `)
692
+ .action(async (agentId) => {
693
+ const sessionId = config.get('sessionId');
694
+ if (!sessionId) {
695
+ console.log(chalk_1.default.yellow('Not logged in.'));
696
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
697
+ process.exit(1);
698
+ }
699
+ const spinner = (0, ora_1.default)('Syncing agent card...').start();
700
+ log('POST /agents/' + agentId + '/sync-card');
701
+ try {
702
+ const res = await fetch(`${API_URL}/agents/${agentId}/sync-card`, {
703
+ method: 'POST',
704
+ headers: { 'X-Session-ID': sessionId },
705
+ });
706
+ if (!res.ok) {
707
+ const err = await res.json();
708
+ logError('sync-card', err);
709
+ throw new Error(err.error || 'Failed to sync card');
710
+ }
711
+ const data = await res.json();
712
+ spinner.succeed('Agent card synced!');
713
+ if (data.card) {
714
+ console.log(chalk_1.default.bold('\n Updated card:'));
715
+ console.log(` Name: ${brand.green(data.card.name)}`);
716
+ if (data.card.description) {
717
+ console.log(` Description: ${chalk_1.default.gray(data.card.description.slice(0, 60))}${data.card.description.length > 60 ? '...' : ''}`);
718
+ }
719
+ if (data.card.skills?.length) {
720
+ console.log(` Skills: ${data.card.skills.length} (${data.card.skills.map((s) => s.name).join(', ')})`);
721
+ }
722
+ console.log('');
723
+ }
724
+ }
725
+ catch (err) {
726
+ spinner.fail(chalk_1.default.red(err.message));
727
+ process.exit(1);
728
+ }
729
+ });
612
730
  // ========== INIT COMMAND ==========
613
731
  program
614
732
  .command('init')
@@ -773,7 +891,7 @@ main().catch(console.error);
773
891
  console.log(chalk_1.default.cyan(' npm start'));
774
892
  console.log('');
775
893
  console.log(chalk_1.default.gray(`Dashboard: https://gopherhole.ai/dashboard`));
776
- console.log(chalk_1.default.gray(`Docs: https://gopherhole.ai/docs`));
894
+ console.log(chalk_1.default.gray(`Docs: https://docs.gopherhole.ai`));
777
895
  console.log('');
778
896
  });
779
897
  // ========== SEND COMMAND ==========
@@ -1105,6 +1223,196 @@ ${chalk_1.default.bold('Example:')}
1105
1223
  process.exit(1);
1106
1224
  }
1107
1225
  });
1226
+ // ========== ACCESS COMMANDS ==========
1227
+ const access = program
1228
+ .command('access')
1229
+ .description(`Manage access requests to your agents
1230
+
1231
+ ${chalk_1.default.bold('Examples:')}
1232
+ $ gopherhole access list # List pending requests
1233
+ $ gopherhole access approve <id> # Approve a request
1234
+ $ gopherhole access reject <id> # Reject a request
1235
+ `);
1236
+ access
1237
+ .command('list')
1238
+ .description('List access requests to your agents')
1239
+ .option('--status <status>', 'Filter by status (pending, approved, rejected)', 'pending')
1240
+ .option('--json', 'Output as JSON')
1241
+ .action(async (options) => {
1242
+ const sessionId = config.get('sessionId');
1243
+ if (!sessionId) {
1244
+ console.log(chalk_1.default.yellow('Not logged in.'));
1245
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1246
+ process.exit(1);
1247
+ }
1248
+ const spinner = (0, ora_1.default)('Fetching access requests...').start();
1249
+ log('GET /access/inbound');
1250
+ try {
1251
+ const res = await fetch(`${API_URL}/access/inbound`, {
1252
+ headers: { 'X-Session-ID': sessionId },
1253
+ });
1254
+ if (!res.ok) {
1255
+ throw new Error('Failed to fetch access requests');
1256
+ }
1257
+ const data = await res.json();
1258
+ spinner.stop();
1259
+ // Filter by status
1260
+ const filtered = options.status === 'all'
1261
+ ? data.grants
1262
+ : data.grants.filter((g) => g.status === options.status);
1263
+ if (options.json) {
1264
+ console.log(JSON.stringify(filtered, null, 2));
1265
+ return;
1266
+ }
1267
+ if (filtered.length === 0) {
1268
+ console.log(chalk_1.default.gray(`\nNo ${options.status} access requests.`));
1269
+ return;
1270
+ }
1271
+ console.log(chalk_1.default.bold(`\nšŸ“‹ ${options.status.charAt(0).toUpperCase() + options.status.slice(1)} Access Requests:\n`));
1272
+ for (const grant of filtered) {
1273
+ const statusColor = grant.status === 'pending' ? chalk_1.default.yellow :
1274
+ grant.status === 'approved' ? chalk_1.default.green : chalk_1.default.red;
1275
+ console.log(` ${chalk_1.default.cyan(grant.id)}`);
1276
+ console.log(` From: ${grant.requester_agent_name || grant.requester_agent_id || 'Unknown'}`);
1277
+ console.log(` To: ${grant.target_agent_name || grant.target_agent_id}`);
1278
+ console.log(` Status: ${statusColor(grant.status)}`);
1279
+ if (grant.requested_reason) {
1280
+ console.log(` Reason: ${chalk_1.default.gray(grant.requested_reason)}`);
1281
+ }
1282
+ console.log(` Requested: ${new Date(grant.requested_at).toLocaleString()}`);
1283
+ console.log('');
1284
+ }
1285
+ if (options.status === 'pending' && filtered.length > 0) {
1286
+ console.log(chalk_1.default.gray(`Approve: gopherhole access approve <id>`));
1287
+ console.log(chalk_1.default.gray(`Reject: gopherhole access reject <id>`));
1288
+ }
1289
+ }
1290
+ catch (err) {
1291
+ spinner.fail(chalk_1.default.red(err.message));
1292
+ process.exit(1);
1293
+ }
1294
+ });
1295
+ access
1296
+ .command('approve <grantId>')
1297
+ .description('Approve an access request')
1298
+ .option('--price <amount>', 'Set custom price for this requester (e.g., 0.01)')
1299
+ .option('--currency <code>', 'Currency code (default: USD)', 'USD')
1300
+ .option('--unit <unit>', 'Price unit (request, message, task, month)', 'request')
1301
+ .option('--json', 'Output as JSON')
1302
+ .action(async (grantId, options) => {
1303
+ const sessionId = config.get('sessionId');
1304
+ if (!sessionId) {
1305
+ console.log(chalk_1.default.yellow('Not logged in.'));
1306
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1307
+ process.exit(1);
1308
+ }
1309
+ const spinner = (0, ora_1.default)('Approving access request...').start();
1310
+ log('PUT /access/' + grantId + '/approve');
1311
+ try {
1312
+ const body = {};
1313
+ if (options.price) {
1314
+ body.price_amount = parseFloat(options.price);
1315
+ body.price_currency = options.currency;
1316
+ body.price_unit = options.unit;
1317
+ }
1318
+ const res = await fetch(`${API_URL}/access/${grantId}/approve`, {
1319
+ method: 'PUT',
1320
+ headers: {
1321
+ 'Content-Type': 'application/json',
1322
+ 'X-Session-ID': sessionId,
1323
+ },
1324
+ body: JSON.stringify(body),
1325
+ });
1326
+ if (!res.ok) {
1327
+ const err = await res.json();
1328
+ throw new Error(err.error || 'Failed to approve');
1329
+ }
1330
+ spinner.succeed('Access request approved');
1331
+ if (options.price) {
1332
+ console.log(chalk_1.default.gray(` Custom pricing: ${options.price} ${options.currency}/${options.unit}`));
1333
+ }
1334
+ }
1335
+ catch (err) {
1336
+ spinner.fail(chalk_1.default.red(err.message));
1337
+ process.exit(1);
1338
+ }
1339
+ });
1340
+ access
1341
+ .command('reject <grantId>')
1342
+ .description('Reject an access request')
1343
+ .option('--reason <reason>', 'Reason for rejection')
1344
+ .action(async (grantId, options) => {
1345
+ const sessionId = config.get('sessionId');
1346
+ if (!sessionId) {
1347
+ console.log(chalk_1.default.yellow('Not logged in.'));
1348
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1349
+ process.exit(1);
1350
+ }
1351
+ const spinner = (0, ora_1.default)('Rejecting access request...').start();
1352
+ log('PUT /access/' + grantId + '/reject');
1353
+ try {
1354
+ const res = await fetch(`${API_URL}/access/${grantId}/reject`, {
1355
+ method: 'PUT',
1356
+ headers: {
1357
+ 'Content-Type': 'application/json',
1358
+ 'X-Session-ID': sessionId,
1359
+ },
1360
+ body: JSON.stringify({ reason: options.reason }),
1361
+ });
1362
+ if (!res.ok) {
1363
+ const err = await res.json();
1364
+ throw new Error(err.error || 'Failed to reject');
1365
+ }
1366
+ spinner.succeed('Access request rejected');
1367
+ }
1368
+ catch (err) {
1369
+ spinner.fail(chalk_1.default.red(err.message));
1370
+ process.exit(1);
1371
+ }
1372
+ });
1373
+ access
1374
+ .command('revoke <grantId>')
1375
+ .description('Revoke a previously approved access grant')
1376
+ .option('-f, --force', 'Skip confirmation')
1377
+ .action(async (grantId, options) => {
1378
+ const sessionId = config.get('sessionId');
1379
+ if (!sessionId) {
1380
+ console.log(chalk_1.default.yellow('Not logged in.'));
1381
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1382
+ process.exit(1);
1383
+ }
1384
+ if (!options.force) {
1385
+ const { confirm } = await inquirer_1.default.prompt([
1386
+ {
1387
+ type: 'confirm',
1388
+ name: 'confirm',
1389
+ message: `Revoke access grant ${chalk_1.default.cyan(grantId)}? The requester will lose access.`,
1390
+ default: false,
1391
+ },
1392
+ ]);
1393
+ if (!confirm) {
1394
+ console.log('Cancelled.');
1395
+ return;
1396
+ }
1397
+ }
1398
+ const spinner = (0, ora_1.default)('Revoking access...').start();
1399
+ log('DELETE /access/' + grantId);
1400
+ try {
1401
+ const res = await fetch(`${API_URL}/access/${grantId}`, {
1402
+ method: 'DELETE',
1403
+ headers: { 'X-Session-ID': sessionId },
1404
+ });
1405
+ if (!res.ok) {
1406
+ const err = await res.json();
1407
+ throw new Error(err.error || 'Failed to revoke');
1408
+ }
1409
+ spinner.succeed('Access revoked');
1410
+ }
1411
+ catch (err) {
1412
+ spinner.fail(chalk_1.default.red(err.message));
1413
+ process.exit(1);
1414
+ }
1415
+ });
1108
1416
  // ========== STATUS COMMAND ==========
1109
1417
  program
1110
1418
  .command('status')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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
@@ -76,7 +76,7 @@ ${chalk.bold('Examples:')}
76
76
  $ gopherhole agents list
77
77
 
78
78
  ${chalk.bold('Documentation:')}
79
- https://gopherhole.ai/docs
79
+ https://docs.gopherhole.ai
80
80
  `)
81
81
  .version('0.1.0')
82
82
  .option('-v, --verbose', 'Enable verbose output for debugging')
@@ -349,7 +349,7 @@ ${chalk.bold('Example:')}
349
349
 
350
350
  console.log(chalk.bold('\nšŸ“š Next steps:\n'));
351
351
  console.log(` • Dashboard: ${chalk.cyan('https://gopherhole.ai/dashboard')}`);
352
- console.log(` • Docs: ${chalk.cyan('https://gopherhole.ai/docs')}`);
352
+ console.log(` • Docs: ${chalk.cyan('https://docs.gopherhole.ai')}`);
353
353
  console.log(` • Find agents: ${chalk.cyan('gopherhole discover search')}`);
354
354
  console.log(` • List yours: ${chalk.cyan('gopherhole agents list')}`);
355
355
  console.log('');
@@ -618,7 +618,7 @@ ${chalk.bold('Examples:')}
618
618
  console.log(chalk.white(` curl -H "Authorization: Bearer ${data.apiKey}" \\
619
619
  https://gopherhole.ai/a2a -d '{"jsonrpc":"2.0","method":"agent/info","id":1}'`));
620
620
 
621
- console.log(chalk.gray('\n Full docs: https://gopherhole.ai/docs'));
621
+ console.log(chalk.gray('\n Full docs: https://docs.gopherhole.ai'));
622
622
  console.log('');
623
623
  } catch (err) {
624
624
  spinner.fail(chalk.red((err as Error).message));
@@ -680,6 +680,135 @@ ${chalk.bold('Example:')}
680
680
  }
681
681
  });
682
682
 
683
+ agents
684
+ .command('regenerate-key <agentId>')
685
+ .description(`Regenerate API key for an agent
686
+
687
+ ${chalk.bold('Example:')}
688
+ $ gopherhole agents regenerate-key agent-abc123
689
+
690
+ ${chalk.yellow('āš ļø Warning:')} This will invalidate the current key.
691
+ All connected agents using this key will stop working.
692
+ `)
693
+ .option('-f, --force', 'Skip confirmation')
694
+ .option('--json', 'Output as JSON')
695
+ .action(async (agentId, options) => {
696
+ const sessionId = config.get('sessionId') as string;
697
+ if (!sessionId) {
698
+ console.log(chalk.yellow('Not logged in.'));
699
+ console.log(chalk.gray('Run: gopherhole login'));
700
+ process.exit(1);
701
+ }
702
+
703
+ if (!options.force) {
704
+ const { confirm } = await inquirer.prompt([
705
+ {
706
+ type: 'confirm',
707
+ name: 'confirm',
708
+ message: `Regenerate API key for ${chalk.cyan(agentId)}? This will invalidate the current key.`,
709
+ default: false,
710
+ },
711
+ ]);
712
+
713
+ if (!confirm) {
714
+ console.log('Cancelled.');
715
+ return;
716
+ }
717
+ }
718
+
719
+ const spinner = ora('Regenerating API key...').start();
720
+ log('POST /agents/' + agentId + '/regenerate-key');
721
+
722
+ try {
723
+ const res = await fetch(`${API_URL}/agents/${agentId}/regenerate-key`, {
724
+ method: 'POST',
725
+ headers: {
726
+ 'Content-Type': 'application/json',
727
+ 'X-Session-ID': sessionId,
728
+ },
729
+ body: JSON.stringify({}),
730
+ });
731
+
732
+ if (!res.ok) {
733
+ const err = await res.json();
734
+ logError('regenerate-key', err);
735
+ throw new Error(err.error || 'Failed to regenerate key');
736
+ }
737
+
738
+ const data = await res.json();
739
+ spinner.succeed('API key regenerated');
740
+
741
+ if (options.json) {
742
+ console.log(JSON.stringify({ apiKey: data.apiKey }, null, 2));
743
+ } else {
744
+ console.log('');
745
+ console.log(chalk.bold('New API Key:'));
746
+ console.log(chalk.green(data.apiKey));
747
+ console.log('');
748
+ console.log(chalk.yellow('āš ļø Copy this key now — it won\'t be shown again!'));
749
+ }
750
+ } catch (err) {
751
+ spinner.fail(chalk.red((err as Error).message));
752
+ process.exit(1);
753
+ }
754
+ });
755
+
756
+ agents
757
+ .command('sync-card <agentId>')
758
+ .description(`Sync agent card from its /.well-known/agent.json URL
759
+
760
+ ${chalk.bold('What it does:')}
761
+ Fetches the agent card from the agent's URL and updates
762
+ the GopherHole registry. Use after updating your agent's
763
+ skills, description, or capabilities.
764
+
765
+ ${chalk.bold('Examples:')}
766
+ $ gopherhole agents sync-card agent-abc123
767
+ $ gopherhole agents sync-card my-agent
768
+ `)
769
+ .action(async (agentId) => {
770
+ const sessionId = config.get('sessionId') as string;
771
+ if (!sessionId) {
772
+ console.log(chalk.yellow('Not logged in.'));
773
+ console.log(chalk.gray('Run: gopherhole login'));
774
+ process.exit(1);
775
+ }
776
+
777
+ const spinner = ora('Syncing agent card...').start();
778
+ log('POST /agents/' + agentId + '/sync-card');
779
+
780
+ try {
781
+ const res = await fetch(`${API_URL}/agents/${agentId}/sync-card`, {
782
+ method: 'POST',
783
+ headers: { 'X-Session-ID': sessionId },
784
+ });
785
+
786
+ if (!res.ok) {
787
+ const err = await res.json();
788
+ logError('sync-card', err);
789
+ throw new Error(err.error || 'Failed to sync card');
790
+ }
791
+
792
+ const data = await res.json();
793
+ spinner.succeed('Agent card synced!');
794
+
795
+ if (data.card) {
796
+ console.log(chalk.bold('\n Updated card:'));
797
+ console.log(` Name: ${brand.green(data.card.name)}`);
798
+ if (data.card.description) {
799
+ console.log(` Description: ${chalk.gray(data.card.description.slice(0, 60))}${data.card.description.length > 60 ? '...' : ''}`);
800
+ }
801
+ if (data.card.skills?.length) {
802
+ console.log(` Skills: ${data.card.skills.length} (${data.card.skills.map((s: any) => s.name).join(', ')})`);
803
+ }
804
+ console.log('');
805
+ }
806
+ } catch (err) {
807
+ spinner.fail(chalk.red((err as Error).message));
808
+ process.exit(1);
809
+ }
810
+ });
811
+
683
812
  // ========== INIT COMMAND ==========
684
813
 
685
814
  program
@@ -864,7 +993,7 @@ main().catch(console.error);
864
993
  console.log(chalk.cyan(' npm start'));
865
994
  console.log('');
866
995
  console.log(chalk.gray(`Dashboard: https://gopherhole.ai/dashboard`));
867
- console.log(chalk.gray(`Docs: https://gopherhole.ai/docs`));
996
+ console.log(chalk.gray(`Docs: https://docs.gopherhole.ai`));
868
997
  console.log('');
869
998
  });
870
999
 
@@ -1231,6 +1360,224 @@ ${chalk.bold('Example:')}
1231
1360
  }
1232
1361
  });
1233
1362
 
1363
+ // ========== ACCESS COMMANDS ==========
1364
+
1365
+ const access = program
1366
+ .command('access')
1367
+ .description(`Manage access requests to your agents
1368
+
1369
+ ${chalk.bold('Examples:')}
1370
+ $ gopherhole access list # List pending requests
1371
+ $ gopherhole access approve <id> # Approve a request
1372
+ $ gopherhole access reject <id> # Reject a request
1373
+ `);
1374
+
1375
+ access
1376
+ .command('list')
1377
+ .description('List access requests to your agents')
1378
+ .option('--status <status>', 'Filter by status (pending, approved, rejected)', 'pending')
1379
+ .option('--json', 'Output as JSON')
1380
+ .action(async (options) => {
1381
+ const sessionId = config.get('sessionId') as string;
1382
+ if (!sessionId) {
1383
+ console.log(chalk.yellow('Not logged in.'));
1384
+ console.log(chalk.gray('Run: gopherhole login'));
1385
+ process.exit(1);
1386
+ }
1387
+
1388
+ const spinner = ora('Fetching access requests...').start();
1389
+ log('GET /access/inbound');
1390
+
1391
+ try {
1392
+ const res = await fetch(`${API_URL}/access/inbound`, {
1393
+ headers: { 'X-Session-ID': sessionId },
1394
+ });
1395
+
1396
+ if (!res.ok) {
1397
+ throw new Error('Failed to fetch access requests');
1398
+ }
1399
+
1400
+ const data = await res.json();
1401
+ spinner.stop();
1402
+
1403
+ // Filter by status
1404
+ const filtered = options.status === 'all'
1405
+ ? data.grants
1406
+ : data.grants.filter((g: any) => g.status === options.status);
1407
+
1408
+ if (options.json) {
1409
+ console.log(JSON.stringify(filtered, null, 2));
1410
+ return;
1411
+ }
1412
+
1413
+ if (filtered.length === 0) {
1414
+ console.log(chalk.gray(`\nNo ${options.status} access requests.`));
1415
+ return;
1416
+ }
1417
+
1418
+ console.log(chalk.bold(`\nšŸ“‹ ${options.status.charAt(0).toUpperCase() + options.status.slice(1)} Access Requests:\n`));
1419
+
1420
+ for (const grant of filtered) {
1421
+ const statusColor = grant.status === 'pending' ? chalk.yellow :
1422
+ grant.status === 'approved' ? chalk.green : chalk.red;
1423
+ console.log(` ${chalk.cyan(grant.id)}`);
1424
+ console.log(` From: ${grant.requester_agent_name || grant.requester_agent_id || 'Unknown'}`);
1425
+ console.log(` To: ${grant.target_agent_name || grant.target_agent_id}`);
1426
+ console.log(` Status: ${statusColor(grant.status)}`);
1427
+ if (grant.requested_reason) {
1428
+ console.log(` Reason: ${chalk.gray(grant.requested_reason)}`);
1429
+ }
1430
+ console.log(` Requested: ${new Date(grant.requested_at).toLocaleString()}`);
1431
+ console.log('');
1432
+ }
1433
+
1434
+ if (options.status === 'pending' && filtered.length > 0) {
1435
+ console.log(chalk.gray(`Approve: gopherhole access approve <id>`));
1436
+ console.log(chalk.gray(`Reject: gopherhole access reject <id>`));
1437
+ }
1438
+ } catch (err) {
1439
+ spinner.fail(chalk.red((err as Error).message));
1440
+ process.exit(1);
1441
+ }
1442
+ });
1443
+
1444
+ access
1445
+ .command('approve <grantId>')
1446
+ .description('Approve an access request')
1447
+ .option('--price <amount>', 'Set custom price for this requester (e.g., 0.01)')
1448
+ .option('--currency <code>', 'Currency code (default: USD)', 'USD')
1449
+ .option('--unit <unit>', 'Price unit (request, message, task, month)', 'request')
1450
+ .option('--json', 'Output as JSON')
1451
+ .action(async (grantId, options) => {
1452
+ const sessionId = config.get('sessionId') as string;
1453
+ if (!sessionId) {
1454
+ console.log(chalk.yellow('Not logged in.'));
1455
+ console.log(chalk.gray('Run: gopherhole login'));
1456
+ process.exit(1);
1457
+ }
1458
+
1459
+ const spinner = ora('Approving access request...').start();
1460
+ log('PUT /access/' + grantId + '/approve');
1461
+
1462
+ try {
1463
+ const body: any = {};
1464
+ if (options.price) {
1465
+ body.price_amount = parseFloat(options.price);
1466
+ body.price_currency = options.currency;
1467
+ body.price_unit = options.unit;
1468
+ }
1469
+
1470
+ const res = await fetch(`${API_URL}/access/${grantId}/approve`, {
1471
+ method: 'PUT',
1472
+ headers: {
1473
+ 'Content-Type': 'application/json',
1474
+ 'X-Session-ID': sessionId,
1475
+ },
1476
+ body: JSON.stringify(body),
1477
+ });
1478
+
1479
+ if (!res.ok) {
1480
+ const err = await res.json();
1481
+ throw new Error(err.error || 'Failed to approve');
1482
+ }
1483
+
1484
+ spinner.succeed('Access request approved');
1485
+
1486
+ if (options.price) {
1487
+ console.log(chalk.gray(` Custom pricing: ${options.price} ${options.currency}/${options.unit}`));
1488
+ }
1489
+ } catch (err) {
1490
+ spinner.fail(chalk.red((err as Error).message));
1491
+ process.exit(1);
1492
+ }
1493
+ });
1494
+
1495
+ access
1496
+ .command('reject <grantId>')
1497
+ .description('Reject an access request')
1498
+ .option('--reason <reason>', 'Reason for rejection')
1499
+ .action(async (grantId, options) => {
1500
+ const sessionId = config.get('sessionId') as string;
1501
+ if (!sessionId) {
1502
+ console.log(chalk.yellow('Not logged in.'));
1503
+ console.log(chalk.gray('Run: gopherhole login'));
1504
+ process.exit(1);
1505
+ }
1506
+
1507
+ const spinner = ora('Rejecting access request...').start();
1508
+ log('PUT /access/' + grantId + '/reject');
1509
+
1510
+ try {
1511
+ const res = await fetch(`${API_URL}/access/${grantId}/reject`, {
1512
+ method: 'PUT',
1513
+ headers: {
1514
+ 'Content-Type': 'application/json',
1515
+ 'X-Session-ID': sessionId,
1516
+ },
1517
+ body: JSON.stringify({ reason: options.reason }),
1518
+ });
1519
+
1520
+ if (!res.ok) {
1521
+ const err = await res.json();
1522
+ throw new Error(err.error || 'Failed to reject');
1523
+ }
1524
+
1525
+ spinner.succeed('Access request rejected');
1526
+ } catch (err) {
1527
+ spinner.fail(chalk.red((err as Error).message));
1528
+ process.exit(1);
1529
+ }
1530
+ });
1531
+
1532
+ access
1533
+ .command('revoke <grantId>')
1534
+ .description('Revoke a previously approved access grant')
1535
+ .option('-f, --force', 'Skip confirmation')
1536
+ .action(async (grantId, options) => {
1537
+ const sessionId = config.get('sessionId') as string;
1538
+ if (!sessionId) {
1539
+ console.log(chalk.yellow('Not logged in.'));
1540
+ console.log(chalk.gray('Run: gopherhole login'));
1541
+ process.exit(1);
1542
+ }
1543
+
1544
+ if (!options.force) {
1545
+ const { confirm } = await inquirer.prompt([
1546
+ {
1547
+ type: 'confirm',
1548
+ name: 'confirm',
1549
+ message: `Revoke access grant ${chalk.cyan(grantId)}? The requester will lose access.`,
1550
+ default: false,
1551
+ },
1552
+ ]);
1553
+
1554
+ if (!confirm) {
1555
+ console.log('Cancelled.');
1556
+ return;
1557
+ }
1558
+ }
1559
+
1560
+ const spinner = ora('Revoking access...').start();
1561
+ log('DELETE /access/' + grantId);
1562
+
1563
+ try {
1564
+ const res = await fetch(`${API_URL}/access/${grantId}`, {
1565
+ method: 'DELETE',
1566
+ headers: { 'X-Session-ID': sessionId },
1567
+ });
1568
+
1569
+ if (!res.ok) {
1570
+ const err = await res.json();
1571
+ throw new Error(err.error || 'Failed to revoke');
1572
+ }
1573
+
1574
+ spinner.succeed('Access revoked');
1575
+ } catch (err) {
1576
+ spinner.fail(chalk.red((err as Error).message));
1577
+ process.exit(1);
1578
+ }
1579
+ });
1580
+
1234
1581
  // ========== STATUS COMMAND ==========
1235
1582
 
1236
1583
  program