@gopherhole/cli 0.1.4 → 0.1.6

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 +281 -0
  2. package/package.json +1 -1
  3. package/src/index.ts +311 -0
package/dist/index.js CHANGED
@@ -1223,6 +1223,287 @@ ${chalk_1.default.bold('Example:')}
1223
1223
  process.exit(1);
1224
1224
  }
1225
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('--agent <agentId>', 'Filter by agent ID')
1240
+ .option('--status <status>', 'Filter by status (pending, approved, rejected, all)', 'pending')
1241
+ .option('--search <query>', 'Search by requester name or ID')
1242
+ .option('--limit <n>', 'Max results (default 50)', '50')
1243
+ .option('--offset <n>', 'Skip first N results', '0')
1244
+ .option('--json', 'Output as JSON')
1245
+ .action(async (options) => {
1246
+ const sessionId = config.get('sessionId');
1247
+ if (!sessionId) {
1248
+ console.log(chalk_1.default.yellow('Not logged in.'));
1249
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1250
+ process.exit(1);
1251
+ }
1252
+ const spinner = (0, ora_1.default)('Fetching access requests...').start();
1253
+ // Build query params
1254
+ const params = new URLSearchParams();
1255
+ if (options.agent)
1256
+ params.set('agent', options.agent);
1257
+ if (options.status && options.status !== 'all')
1258
+ params.set('status', options.status);
1259
+ if (options.search)
1260
+ params.set('search', options.search);
1261
+ params.set('limit', options.limit);
1262
+ params.set('offset', options.offset);
1263
+ log('GET /access/inbound?' + params.toString());
1264
+ try {
1265
+ const res = await fetch(`${API_URL}/access/inbound?${params}`, {
1266
+ headers: { 'X-Session-ID': sessionId },
1267
+ });
1268
+ if (!res.ok) {
1269
+ throw new Error('Failed to fetch access requests');
1270
+ }
1271
+ const data = await res.json();
1272
+ spinner.stop();
1273
+ if (options.json) {
1274
+ console.log(JSON.stringify(data.grants, null, 2));
1275
+ return;
1276
+ }
1277
+ if (data.grants.length === 0) {
1278
+ console.log(chalk_1.default.gray(`\nNo ${options.status === 'all' ? '' : options.status + ' '}access requests found.`));
1279
+ if (options.agent)
1280
+ console.log(chalk_1.default.gray(` Agent filter: ${options.agent}`));
1281
+ if (options.search)
1282
+ console.log(chalk_1.default.gray(` Search: "${options.search}"`));
1283
+ return;
1284
+ }
1285
+ console.log(chalk_1.default.bold(`\nšŸ“‹ Access Requests${options.agent ? ` for ${options.agent}` : ''}:\n`));
1286
+ for (const grant of data.grants) {
1287
+ const statusColor = grant.status === 'pending' ? chalk_1.default.yellow :
1288
+ grant.status === 'approved' ? chalk_1.default.green : chalk_1.default.red;
1289
+ console.log(` ${chalk_1.default.cyan(grant.id)}`);
1290
+ console.log(` From: ${grant.requester_agent_name || grant.requester_agent_id || 'Unknown'}`);
1291
+ console.log(` To: ${grant.target_agent_name || grant.target_agent_id}`);
1292
+ console.log(` Status: ${statusColor(grant.status)}`);
1293
+ if (grant.price_amount != null) {
1294
+ console.log(` Price: ${grant.price_amount} ${grant.price_currency}/${grant.price_unit}`);
1295
+ }
1296
+ if (grant.discount_percent != null) {
1297
+ console.log(` Discount: ${grant.discount_percent}%`);
1298
+ }
1299
+ if (grant.requested_reason) {
1300
+ console.log(` Reason: ${chalk_1.default.gray(grant.requested_reason)}`);
1301
+ }
1302
+ console.log(` Requested: ${new Date(grant.requested_at).toLocaleString()}`);
1303
+ console.log('');
1304
+ }
1305
+ // Pagination info
1306
+ if (data.grants.length >= parseInt(options.limit)) {
1307
+ console.log(chalk_1.default.gray(`Showing ${data.grants.length} results. Use --offset ${parseInt(options.offset) + parseInt(options.limit)} for next page.`));
1308
+ }
1309
+ if (options.status === 'pending' && data.grants.length > 0) {
1310
+ console.log(chalk_1.default.gray(`\nApprove: gopherhole access approve <id>`));
1311
+ console.log(chalk_1.default.gray(`Reject: gopherhole access reject <id>`));
1312
+ }
1313
+ }
1314
+ catch (err) {
1315
+ spinner.fail(chalk_1.default.red(err.message));
1316
+ process.exit(1);
1317
+ }
1318
+ });
1319
+ access
1320
+ .command('approve <grantId>')
1321
+ .description('Approve an access request')
1322
+ .option('--price <amount>', 'Set custom price (e.g., 0.01)')
1323
+ .option('--currency <code>', 'Currency code (default: USD)', 'USD')
1324
+ .option('--unit <unit>', 'Price unit (request, message, task, month)', 'request')
1325
+ .option('--discount <percent>', 'Discount off default price (e.g., 20 for 20% off)')
1326
+ .option('--skill-pricing <json>', 'Per-skill pricing as JSON (e.g., \'{"translate":{"amount":0.05,"currency":"USD","unit":"request"}}\')')
1327
+ .action(async (grantId, options) => {
1328
+ const sessionId = config.get('sessionId');
1329
+ if (!sessionId) {
1330
+ console.log(chalk_1.default.yellow('Not logged in.'));
1331
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1332
+ process.exit(1);
1333
+ }
1334
+ const spinner = (0, ora_1.default)('Approving access request...').start();
1335
+ log('PUT /access/' + grantId + '/approve');
1336
+ try {
1337
+ const body = {};
1338
+ if (options.price) {
1339
+ body.price_amount = parseFloat(options.price);
1340
+ body.price_currency = options.currency;
1341
+ body.price_unit = options.unit;
1342
+ }
1343
+ if (options.discount) {
1344
+ body.discount_percent = parseFloat(options.discount);
1345
+ }
1346
+ if (options.skillPricing) {
1347
+ body.skill_pricing = JSON.parse(options.skillPricing);
1348
+ }
1349
+ const res = await fetch(`${API_URL}/access/${grantId}/approve`, {
1350
+ method: 'PUT',
1351
+ headers: {
1352
+ 'Content-Type': 'application/json',
1353
+ 'X-Session-ID': sessionId,
1354
+ },
1355
+ body: JSON.stringify(body),
1356
+ });
1357
+ if (!res.ok) {
1358
+ const err = await res.json();
1359
+ throw new Error(err.error || 'Failed to approve');
1360
+ }
1361
+ spinner.succeed('Access request approved');
1362
+ if (options.price) {
1363
+ console.log(chalk_1.default.gray(` Custom price: ${options.price} ${options.currency}/${options.unit}`));
1364
+ }
1365
+ if (options.discount) {
1366
+ console.log(chalk_1.default.gray(` Discount: ${options.discount}% off`));
1367
+ }
1368
+ }
1369
+ catch (err) {
1370
+ spinner.fail(chalk_1.default.red(err.message));
1371
+ process.exit(1);
1372
+ }
1373
+ });
1374
+ access
1375
+ .command('edit <grantId>')
1376
+ .description('Edit pricing on an existing access grant')
1377
+ .option('--price <amount>', 'Set custom price (e.g., 0.01)')
1378
+ .option('--currency <code>', 'Currency code', 'USD')
1379
+ .option('--unit <unit>', 'Price unit (request, message, task, month)')
1380
+ .option('--discount <percent>', 'Discount off default price (e.g., 20 for 20% off)')
1381
+ .option('--clear-discount', 'Remove discount')
1382
+ .option('--skill-pricing <json>', 'Per-skill pricing as JSON')
1383
+ .option('--clear-skill-pricing', 'Remove per-skill pricing')
1384
+ .action(async (grantId, options) => {
1385
+ const sessionId = config.get('sessionId');
1386
+ if (!sessionId) {
1387
+ console.log(chalk_1.default.yellow('Not logged in.'));
1388
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1389
+ process.exit(1);
1390
+ }
1391
+ const spinner = (0, ora_1.default)('Updating access grant...').start();
1392
+ log('PATCH /access/' + grantId);
1393
+ try {
1394
+ const body = {};
1395
+ if (options.price) {
1396
+ body.price_amount = parseFloat(options.price);
1397
+ body.price_currency = options.currency;
1398
+ body.price_unit = options.unit;
1399
+ }
1400
+ if (options.discount) {
1401
+ body.discount_percent = parseFloat(options.discount);
1402
+ }
1403
+ if (options.clearDiscount) {
1404
+ body.discount_percent = null;
1405
+ }
1406
+ if (options.skillPricing) {
1407
+ body.skill_pricing = JSON.parse(options.skillPricing);
1408
+ }
1409
+ if (options.clearSkillPricing) {
1410
+ body.skill_pricing = null;
1411
+ }
1412
+ const res = await fetch(`${API_URL}/access/${grantId}`, {
1413
+ method: 'PATCH',
1414
+ headers: {
1415
+ 'Content-Type': 'application/json',
1416
+ 'X-Session-ID': sessionId,
1417
+ },
1418
+ body: JSON.stringify(body),
1419
+ });
1420
+ if (!res.ok) {
1421
+ const err = await res.json();
1422
+ throw new Error(err.error || 'Failed to update grant');
1423
+ }
1424
+ spinner.succeed('Access grant updated');
1425
+ }
1426
+ catch (err) {
1427
+ spinner.fail(chalk_1.default.red(err.message));
1428
+ process.exit(1);
1429
+ }
1430
+ });
1431
+ access
1432
+ .command('reject <grantId>')
1433
+ .description('Reject an access request')
1434
+ .option('--reason <reason>', 'Reason for rejection')
1435
+ .action(async (grantId, options) => {
1436
+ const sessionId = config.get('sessionId');
1437
+ if (!sessionId) {
1438
+ console.log(chalk_1.default.yellow('Not logged in.'));
1439
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1440
+ process.exit(1);
1441
+ }
1442
+ const spinner = (0, ora_1.default)('Rejecting access request...').start();
1443
+ log('PUT /access/' + grantId + '/reject');
1444
+ try {
1445
+ const res = await fetch(`${API_URL}/access/${grantId}/reject`, {
1446
+ method: 'PUT',
1447
+ headers: {
1448
+ 'Content-Type': 'application/json',
1449
+ 'X-Session-ID': sessionId,
1450
+ },
1451
+ body: JSON.stringify({ reason: options.reason }),
1452
+ });
1453
+ if (!res.ok) {
1454
+ const err = await res.json();
1455
+ throw new Error(err.error || 'Failed to reject');
1456
+ }
1457
+ spinner.succeed('Access request rejected');
1458
+ }
1459
+ catch (err) {
1460
+ spinner.fail(chalk_1.default.red(err.message));
1461
+ process.exit(1);
1462
+ }
1463
+ });
1464
+ access
1465
+ .command('revoke <grantId>')
1466
+ .description('Revoke a previously approved access grant')
1467
+ .option('-f, --force', 'Skip confirmation')
1468
+ .action(async (grantId, options) => {
1469
+ const sessionId = config.get('sessionId');
1470
+ if (!sessionId) {
1471
+ console.log(chalk_1.default.yellow('Not logged in.'));
1472
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1473
+ process.exit(1);
1474
+ }
1475
+ if (!options.force) {
1476
+ const { confirm } = await inquirer_1.default.prompt([
1477
+ {
1478
+ type: 'confirm',
1479
+ name: 'confirm',
1480
+ message: `Revoke access grant ${chalk_1.default.cyan(grantId)}? The requester will lose access.`,
1481
+ default: false,
1482
+ },
1483
+ ]);
1484
+ if (!confirm) {
1485
+ console.log('Cancelled.');
1486
+ return;
1487
+ }
1488
+ }
1489
+ const spinner = (0, ora_1.default)('Revoking access...').start();
1490
+ log('DELETE /access/' + grantId);
1491
+ try {
1492
+ const res = await fetch(`${API_URL}/access/${grantId}`, {
1493
+ method: 'DELETE',
1494
+ headers: { 'X-Session-ID': sessionId },
1495
+ });
1496
+ if (!res.ok) {
1497
+ const err = await res.json();
1498
+ throw new Error(err.error || 'Failed to revoke');
1499
+ }
1500
+ spinner.succeed('Access revoked');
1501
+ }
1502
+ catch (err) {
1503
+ spinner.fail(chalk_1.default.red(err.message));
1504
+ process.exit(1);
1505
+ }
1506
+ });
1226
1507
  // ========== STATUS COMMAND ==========
1227
1508
  program
1228
1509
  .command('status')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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
@@ -1360,6 +1360,317 @@ ${chalk.bold('Example:')}
1360
1360
  }
1361
1361
  });
1362
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('--agent <agentId>', 'Filter by agent ID')
1379
+ .option('--status <status>', 'Filter by status (pending, approved, rejected, all)', 'pending')
1380
+ .option('--search <query>', 'Search by requester name or ID')
1381
+ .option('--limit <n>', 'Max results (default 50)', '50')
1382
+ .option('--offset <n>', 'Skip first N results', '0')
1383
+ .option('--json', 'Output as JSON')
1384
+ .action(async (options) => {
1385
+ const sessionId = config.get('sessionId') as string;
1386
+ if (!sessionId) {
1387
+ console.log(chalk.yellow('Not logged in.'));
1388
+ console.log(chalk.gray('Run: gopherhole login'));
1389
+ process.exit(1);
1390
+ }
1391
+
1392
+ const spinner = ora('Fetching access requests...').start();
1393
+
1394
+ // Build query params
1395
+ const params = new URLSearchParams();
1396
+ if (options.agent) params.set('agent', options.agent);
1397
+ if (options.status && options.status !== 'all') params.set('status', options.status);
1398
+ if (options.search) params.set('search', options.search);
1399
+ params.set('limit', options.limit);
1400
+ params.set('offset', options.offset);
1401
+
1402
+ log('GET /access/inbound?' + params.toString());
1403
+
1404
+ try {
1405
+ const res = await fetch(`${API_URL}/access/inbound?${params}`, {
1406
+ headers: { 'X-Session-ID': sessionId },
1407
+ });
1408
+
1409
+ if (!res.ok) {
1410
+ throw new Error('Failed to fetch access requests');
1411
+ }
1412
+
1413
+ const data = await res.json();
1414
+ spinner.stop();
1415
+
1416
+ if (options.json) {
1417
+ console.log(JSON.stringify(data.grants, null, 2));
1418
+ return;
1419
+ }
1420
+
1421
+ if (data.grants.length === 0) {
1422
+ console.log(chalk.gray(`\nNo ${options.status === 'all' ? '' : options.status + ' '}access requests found.`));
1423
+ if (options.agent) console.log(chalk.gray(` Agent filter: ${options.agent}`));
1424
+ if (options.search) console.log(chalk.gray(` Search: "${options.search}"`));
1425
+ return;
1426
+ }
1427
+
1428
+ console.log(chalk.bold(`\nšŸ“‹ Access Requests${options.agent ? ` for ${options.agent}` : ''}:\n`));
1429
+
1430
+ for (const grant of data.grants) {
1431
+ const statusColor = grant.status === 'pending' ? chalk.yellow :
1432
+ grant.status === 'approved' ? chalk.green : chalk.red;
1433
+ console.log(` ${chalk.cyan(grant.id)}`);
1434
+ console.log(` From: ${grant.requester_agent_name || grant.requester_agent_id || 'Unknown'}`);
1435
+ console.log(` To: ${grant.target_agent_name || grant.target_agent_id}`);
1436
+ console.log(` Status: ${statusColor(grant.status)}`);
1437
+ if (grant.price_amount != null) {
1438
+ console.log(` Price: ${grant.price_amount} ${grant.price_currency}/${grant.price_unit}`);
1439
+ }
1440
+ if (grant.discount_percent != null) {
1441
+ console.log(` Discount: ${grant.discount_percent}%`);
1442
+ }
1443
+ if (grant.requested_reason) {
1444
+ console.log(` Reason: ${chalk.gray(grant.requested_reason)}`);
1445
+ }
1446
+ console.log(` Requested: ${new Date(grant.requested_at).toLocaleString()}`);
1447
+ console.log('');
1448
+ }
1449
+
1450
+ // Pagination info
1451
+ if (data.grants.length >= parseInt(options.limit)) {
1452
+ console.log(chalk.gray(`Showing ${data.grants.length} results. Use --offset ${parseInt(options.offset) + parseInt(options.limit)} for next page.`));
1453
+ }
1454
+
1455
+ if (options.status === 'pending' && data.grants.length > 0) {
1456
+ console.log(chalk.gray(`\nApprove: gopherhole access approve <id>`));
1457
+ console.log(chalk.gray(`Reject: gopherhole access reject <id>`));
1458
+ }
1459
+ } catch (err) {
1460
+ spinner.fail(chalk.red((err as Error).message));
1461
+ process.exit(1);
1462
+ }
1463
+ });
1464
+
1465
+ access
1466
+ .command('approve <grantId>')
1467
+ .description('Approve an access request')
1468
+ .option('--price <amount>', 'Set custom price (e.g., 0.01)')
1469
+ .option('--currency <code>', 'Currency code (default: USD)', 'USD')
1470
+ .option('--unit <unit>', 'Price unit (request, message, task, month)', 'request')
1471
+ .option('--discount <percent>', 'Discount off default price (e.g., 20 for 20% off)')
1472
+ .option('--skill-pricing <json>', 'Per-skill pricing as JSON (e.g., \'{"translate":{"amount":0.05,"currency":"USD","unit":"request"}}\')')
1473
+ .action(async (grantId, options) => {
1474
+ const sessionId = config.get('sessionId') as string;
1475
+ if (!sessionId) {
1476
+ console.log(chalk.yellow('Not logged in.'));
1477
+ console.log(chalk.gray('Run: gopherhole login'));
1478
+ process.exit(1);
1479
+ }
1480
+
1481
+ const spinner = ora('Approving access request...').start();
1482
+ log('PUT /access/' + grantId + '/approve');
1483
+
1484
+ try {
1485
+ const body: any = {};
1486
+ if (options.price) {
1487
+ body.price_amount = parseFloat(options.price);
1488
+ body.price_currency = options.currency;
1489
+ body.price_unit = options.unit;
1490
+ }
1491
+ if (options.discount) {
1492
+ body.discount_percent = parseFloat(options.discount);
1493
+ }
1494
+ if (options.skillPricing) {
1495
+ body.skill_pricing = JSON.parse(options.skillPricing);
1496
+ }
1497
+
1498
+ const res = await fetch(`${API_URL}/access/${grantId}/approve`, {
1499
+ method: 'PUT',
1500
+ headers: {
1501
+ 'Content-Type': 'application/json',
1502
+ 'X-Session-ID': sessionId,
1503
+ },
1504
+ body: JSON.stringify(body),
1505
+ });
1506
+
1507
+ if (!res.ok) {
1508
+ const err = await res.json();
1509
+ throw new Error(err.error || 'Failed to approve');
1510
+ }
1511
+
1512
+ spinner.succeed('Access request approved');
1513
+
1514
+ if (options.price) {
1515
+ console.log(chalk.gray(` Custom price: ${options.price} ${options.currency}/${options.unit}`));
1516
+ }
1517
+ if (options.discount) {
1518
+ console.log(chalk.gray(` Discount: ${options.discount}% off`));
1519
+ }
1520
+ } catch (err) {
1521
+ spinner.fail(chalk.red((err as Error).message));
1522
+ process.exit(1);
1523
+ }
1524
+ });
1525
+
1526
+ access
1527
+ .command('edit <grantId>')
1528
+ .description('Edit pricing on an existing access grant')
1529
+ .option('--price <amount>', 'Set custom price (e.g., 0.01)')
1530
+ .option('--currency <code>', 'Currency code', 'USD')
1531
+ .option('--unit <unit>', 'Price unit (request, message, task, month)')
1532
+ .option('--discount <percent>', 'Discount off default price (e.g., 20 for 20% off)')
1533
+ .option('--clear-discount', 'Remove discount')
1534
+ .option('--skill-pricing <json>', 'Per-skill pricing as JSON')
1535
+ .option('--clear-skill-pricing', 'Remove per-skill pricing')
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
+ const spinner = ora('Updating access grant...').start();
1545
+ log('PATCH /access/' + grantId);
1546
+
1547
+ try {
1548
+ const body: any = {};
1549
+ if (options.price) {
1550
+ body.price_amount = parseFloat(options.price);
1551
+ body.price_currency = options.currency;
1552
+ body.price_unit = options.unit;
1553
+ }
1554
+ if (options.discount) {
1555
+ body.discount_percent = parseFloat(options.discount);
1556
+ }
1557
+ if (options.clearDiscount) {
1558
+ body.discount_percent = null;
1559
+ }
1560
+ if (options.skillPricing) {
1561
+ body.skill_pricing = JSON.parse(options.skillPricing);
1562
+ }
1563
+ if (options.clearSkillPricing) {
1564
+ body.skill_pricing = null;
1565
+ }
1566
+
1567
+ const res = await fetch(`${API_URL}/access/${grantId}`, {
1568
+ method: 'PATCH',
1569
+ headers: {
1570
+ 'Content-Type': 'application/json',
1571
+ 'X-Session-ID': sessionId,
1572
+ },
1573
+ body: JSON.stringify(body),
1574
+ });
1575
+
1576
+ if (!res.ok) {
1577
+ const err = await res.json();
1578
+ throw new Error(err.error || 'Failed to update grant');
1579
+ }
1580
+
1581
+ spinner.succeed('Access grant updated');
1582
+ } catch (err) {
1583
+ spinner.fail(chalk.red((err as Error).message));
1584
+ process.exit(1);
1585
+ }
1586
+ });
1587
+
1588
+ access
1589
+ .command('reject <grantId>')
1590
+ .description('Reject an access request')
1591
+ .option('--reason <reason>', 'Reason for rejection')
1592
+ .action(async (grantId, options) => {
1593
+ const sessionId = config.get('sessionId') as string;
1594
+ if (!sessionId) {
1595
+ console.log(chalk.yellow('Not logged in.'));
1596
+ console.log(chalk.gray('Run: gopherhole login'));
1597
+ process.exit(1);
1598
+ }
1599
+
1600
+ const spinner = ora('Rejecting access request...').start();
1601
+ log('PUT /access/' + grantId + '/reject');
1602
+
1603
+ try {
1604
+ const res = await fetch(`${API_URL}/access/${grantId}/reject`, {
1605
+ method: 'PUT',
1606
+ headers: {
1607
+ 'Content-Type': 'application/json',
1608
+ 'X-Session-ID': sessionId,
1609
+ },
1610
+ body: JSON.stringify({ reason: options.reason }),
1611
+ });
1612
+
1613
+ if (!res.ok) {
1614
+ const err = await res.json();
1615
+ throw new Error(err.error || 'Failed to reject');
1616
+ }
1617
+
1618
+ spinner.succeed('Access request rejected');
1619
+ } catch (err) {
1620
+ spinner.fail(chalk.red((err as Error).message));
1621
+ process.exit(1);
1622
+ }
1623
+ });
1624
+
1625
+ access
1626
+ .command('revoke <grantId>')
1627
+ .description('Revoke a previously approved access grant')
1628
+ .option('-f, --force', 'Skip confirmation')
1629
+ .action(async (grantId, options) => {
1630
+ const sessionId = config.get('sessionId') as string;
1631
+ if (!sessionId) {
1632
+ console.log(chalk.yellow('Not logged in.'));
1633
+ console.log(chalk.gray('Run: gopherhole login'));
1634
+ process.exit(1);
1635
+ }
1636
+
1637
+ if (!options.force) {
1638
+ const { confirm } = await inquirer.prompt([
1639
+ {
1640
+ type: 'confirm',
1641
+ name: 'confirm',
1642
+ message: `Revoke access grant ${chalk.cyan(grantId)}? The requester will lose access.`,
1643
+ default: false,
1644
+ },
1645
+ ]);
1646
+
1647
+ if (!confirm) {
1648
+ console.log('Cancelled.');
1649
+ return;
1650
+ }
1651
+ }
1652
+
1653
+ const spinner = ora('Revoking access...').start();
1654
+ log('DELETE /access/' + grantId);
1655
+
1656
+ try {
1657
+ const res = await fetch(`${API_URL}/access/${grantId}`, {
1658
+ method: 'DELETE',
1659
+ headers: { 'X-Session-ID': sessionId },
1660
+ });
1661
+
1662
+ if (!res.ok) {
1663
+ const err = await res.json();
1664
+ throw new Error(err.error || 'Failed to revoke');
1665
+ }
1666
+
1667
+ spinner.succeed('Access revoked');
1668
+ } catch (err) {
1669
+ spinner.fail(chalk.red((err as Error).message));
1670
+ process.exit(1);
1671
+ }
1672
+ });
1673
+
1363
1674
  // ========== STATUS COMMAND ==========
1364
1675
 
1365
1676
  program