@gopherhole/cli 0.3.0 → 0.4.0

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 +191 -3
  2. package/package.json +1 -1
  3. package/src/index.ts +182 -3
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ const brand = {
20
20
  greenDark: chalk_1.default.hex('#16a34a'), // gopher-600 - emphasis
21
21
  };
22
22
  // Version
23
- const VERSION = '0.2.0';
23
+ const VERSION = '0.3.0';
24
24
  // ========== API KEY RESOLUTION ==========
25
25
  // Precedence: --api-key flag > GOPHERHOLE_API_KEY env var > .env file in cwd
26
26
  async function resolveApiKey(flagValue) {
@@ -91,6 +91,7 @@ async function askAgent(client, agentId, text) {
91
91
  const start = Date.now();
92
92
  const maxWait = 60_000;
93
93
  const poll = 1_000;
94
+ let printedQueued = false;
94
95
  while (!terminalStates.includes(current.status.state)) {
95
96
  if (current.status.state === 'input-required') {
96
97
  throw new Error('Agent requires additional input (not supported in CLI mode)');
@@ -98,6 +99,10 @@ async function askAgent(client, agentId, text) {
98
99
  if (current.status.state === 'auth-required') {
99
100
  throw new Error('Agent requires authentication — check your API key or request access');
100
101
  }
102
+ if (current.status.state === 'submitted' && !printedQueued) {
103
+ console.log(chalk_1.default.yellow('⏳ Message queued — recipient is offline. Waiting for delivery...'));
104
+ printedQueued = true;
105
+ }
101
106
  if (Date.now() - start > maxWait)
102
107
  throw new Error('Timed out waiting for agent response');
103
108
  await new Promise(r => setTimeout(r, poll));
@@ -1216,8 +1221,11 @@ program
1216
1221
  ${chalk_1.default.bold('Examples:')}
1217
1222
  $ gopherhole send echo "Hello!"
1218
1223
  $ gopherhole send agent-abc123 "What's the weather?"
1224
+ $ gopherhole send agent-abc123 "Free now?" --ttl 0 # fail if offline
1225
+ $ gopherhole send agent-abc123 "Review this" --ttl 3600 # queue up to 1h
1219
1226
  `)
1220
- .action(async (agentId, message) => {
1227
+ .option('--ttl <seconds>', 'Message time-to-live in seconds (0 = no queue, omit = 30 day default)', parseInt)
1228
+ .action(async (agentId, message, cmdOpts) => {
1221
1229
  const sessionId = config.get('sessionId');
1222
1230
  if (!sessionId) {
1223
1231
  console.log(chalk_1.default.yellow('Not logged in.'));
@@ -1238,7 +1246,10 @@ ${chalk_1.default.bold('Examples:')}
1238
1246
  method: 'SendMessage',
1239
1247
  params: {
1240
1248
  message: { role: 'user', parts: [{ kind: 'text', text: message }] },
1241
- configuration: { agentId },
1249
+ configuration: {
1250
+ agentId,
1251
+ ...(cmdOpts.ttl !== undefined ? { 'x-ttl': cmdOpts.ttl } : {}),
1252
+ },
1242
1253
  },
1243
1254
  id: 1,
1244
1255
  }),
@@ -1263,6 +1274,183 @@ ${chalk_1.default.bold('Examples:')}
1263
1274
  process.exit(1);
1264
1275
  }
1265
1276
  });
1277
+ // ========== TASK COMMANDS ==========
1278
+ const taskCmd = program
1279
+ .command('task')
1280
+ .description(`Manage tasks (queued messages, pending responses)
1281
+
1282
+ ${chalk_1.default.bold('Examples:')}
1283
+ $ gopherhole task status task-abc123
1284
+ $ gopherhole task pending
1285
+ $ gopherhole task cancel task-abc123
1286
+ $ gopherhole task cancel-all
1287
+ `);
1288
+ taskCmd
1289
+ .command('status <taskId>')
1290
+ .description('Check the status of a task and get the response if completed')
1291
+ .action(async (taskId) => {
1292
+ const sessionId = config.get('sessionId');
1293
+ if (!sessionId) {
1294
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
1295
+ process.exit(1);
1296
+ }
1297
+ const spinner = (0, ora_1.default)(`Checking task ${taskId}...`).start();
1298
+ try {
1299
+ const res = await fetch(`${API_URL}/../a2a`, {
1300
+ method: 'POST',
1301
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1302
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'GetTask', params: { id: taskId }, id: 1 }),
1303
+ });
1304
+ const data = await res.json();
1305
+ if (data.error)
1306
+ throw new Error(data.error.message || 'Failed');
1307
+ const task = data.result;
1308
+ const state = task?.status?.state || 'unknown';
1309
+ spinner.stop();
1310
+ console.log(`\n${chalk_1.default.bold('Task:')} ${taskId}`);
1311
+ console.log(`${chalk_1.default.bold('State:')} ${stateColor(state)(state)}`);
1312
+ if (task?.status?.timestamp)
1313
+ console.log(`${chalk_1.default.bold('Time:')} ${task.status.timestamp}`);
1314
+ if (state === 'completed' && task?.artifacts?.length) {
1315
+ const texts = task.artifacts.flatMap((a) => a.parts?.filter((p) => p.kind === 'text').map((p) => p.text) || []);
1316
+ if (texts.length)
1317
+ console.log(`\n${chalk_1.default.bold('Response:')}\n${texts.join('\n')}`);
1318
+ }
1319
+ else if (state === 'submitted') {
1320
+ console.log(chalk_1.default.yellow('\n⏳ Queued — recipient hasn\'t come online yet.'));
1321
+ }
1322
+ else if (state === 'working') {
1323
+ console.log(chalk_1.default.blue('\n⚙️ Delivered — waiting for response.'));
1324
+ }
1325
+ else if (state === 'failed') {
1326
+ const msg = task?.status?.message;
1327
+ console.log(chalk_1.default.red(`\n❌ ${typeof msg === 'string' ? msg : 'Unknown error'}`));
1328
+ }
1329
+ }
1330
+ catch (err) {
1331
+ spinner.fail(chalk_1.default.red(err.message));
1332
+ process.exit(1);
1333
+ }
1334
+ });
1335
+ taskCmd
1336
+ .command('pending')
1337
+ .description('List all queued/pending tasks')
1338
+ .option('-l, --limit <n>', 'Max tasks to show', parseInt, 20)
1339
+ .action(async (opts) => {
1340
+ const sessionId = config.get('sessionId');
1341
+ if (!sessionId) {
1342
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
1343
+ process.exit(1);
1344
+ }
1345
+ const spinner = (0, ora_1.default)('Fetching pending tasks...').start();
1346
+ try {
1347
+ const res = await fetch(`${API_URL}/../a2a`, {
1348
+ method: 'POST',
1349
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1350
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'ListTasks', params: { status: 'submitted', pageSize: opts.limit || 20 }, id: 1 }),
1351
+ });
1352
+ const data = await res.json();
1353
+ if (data.error)
1354
+ throw new Error(data.error.message || 'Failed');
1355
+ const tasks = data.result?.tasks || [];
1356
+ spinner.stop();
1357
+ if (tasks.length === 0) {
1358
+ console.log(chalk_1.default.green('✅ No pending tasks.'));
1359
+ return;
1360
+ }
1361
+ console.log(chalk_1.default.bold(`\n${tasks.length} pending task(s):\n`));
1362
+ for (const t of tasks) {
1363
+ const age = Date.now() - new Date(t.status?.timestamp || 0).getTime();
1364
+ const ageMins = Math.round(age / 60000);
1365
+ console.log(` ${chalk_1.default.gray(t.id)} → ${t.serverAgentId || 'unknown'} ${chalk_1.default.yellow(`(${ageMins}m ago)`)}`);
1366
+ }
1367
+ }
1368
+ catch (err) {
1369
+ spinner.fail(chalk_1.default.red(err.message));
1370
+ process.exit(1);
1371
+ }
1372
+ });
1373
+ taskCmd
1374
+ .command('cancel <taskId>')
1375
+ .description('Cancel a specific task and purge its queued messages')
1376
+ .action(async (taskId) => {
1377
+ const sessionId = config.get('sessionId');
1378
+ if (!sessionId) {
1379
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
1380
+ process.exit(1);
1381
+ }
1382
+ const spinner = (0, ora_1.default)(`Canceling task ${taskId}...`).start();
1383
+ try {
1384
+ const res = await fetch(`${API_URL}/../a2a`, {
1385
+ method: 'POST',
1386
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1387
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'CancelTask', params: { id: taskId }, id: 1 }),
1388
+ });
1389
+ const data = await res.json();
1390
+ if (data.error)
1391
+ throw new Error(data.error.message || 'Failed');
1392
+ spinner.succeed(`Task ${taskId} canceled. Queued messages purged.`);
1393
+ }
1394
+ catch (err) {
1395
+ spinner.fail(chalk_1.default.red(err.message));
1396
+ process.exit(1);
1397
+ }
1398
+ });
1399
+ taskCmd
1400
+ .command('cancel-all')
1401
+ .description('Cancel ALL pending tasks and purge all queued messages')
1402
+ .action(async () => {
1403
+ const sessionId = config.get('sessionId');
1404
+ if (!sessionId) {
1405
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
1406
+ process.exit(1);
1407
+ }
1408
+ const spinner = (0, ora_1.default)('Fetching pending tasks...').start();
1409
+ try {
1410
+ const res = await fetch(`${API_URL}/../a2a`, {
1411
+ method: 'POST',
1412
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1413
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'ListTasks', params: { status: 'submitted', pageSize: 100 }, id: 1 }),
1414
+ });
1415
+ const data = await res.json();
1416
+ if (data.error)
1417
+ throw new Error(data.error.message || 'Failed');
1418
+ const tasks = data.result?.tasks || [];
1419
+ if (tasks.length === 0) {
1420
+ spinner.succeed('No pending tasks to cancel.');
1421
+ return;
1422
+ }
1423
+ spinner.text = `Canceling ${tasks.length} task(s)...`;
1424
+ let canceled = 0;
1425
+ for (const t of tasks) {
1426
+ try {
1427
+ await fetch(`${API_URL}/../a2a`, {
1428
+ method: 'POST',
1429
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1430
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'CancelTask', params: { id: t.id }, id: 1 }),
1431
+ });
1432
+ canceled++;
1433
+ }
1434
+ catch { /* skip */ }
1435
+ }
1436
+ spinner.succeed(`Canceled ${canceled}/${tasks.length} task(s). Queued messages purged.`);
1437
+ }
1438
+ catch (err) {
1439
+ spinner.fail(chalk_1.default.red(err.message));
1440
+ process.exit(1);
1441
+ }
1442
+ });
1443
+ function stateColor(state) {
1444
+ switch (state) {
1445
+ case 'completed': return chalk_1.default.green;
1446
+ case 'submitted': return chalk_1.default.yellow;
1447
+ case 'working': return chalk_1.default.blue;
1448
+ case 'failed':
1449
+ case 'canceled':
1450
+ case 'rejected': return chalk_1.default.red;
1451
+ default: return chalk_1.default.gray;
1452
+ }
1453
+ }
1266
1454
  // ========== DISCOVER COMMANDS ==========
1267
1455
  const discover = program
1268
1456
  .command('discover')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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
@@ -20,7 +20,7 @@ const brand = {
20
20
  };
21
21
 
22
22
  // Version
23
- const VERSION = '0.2.0';
23
+ const VERSION = '0.3.0';
24
24
 
25
25
  // ========== API KEY RESOLUTION ==========
26
26
  // Precedence: --api-key flag > GOPHERHOLE_API_KEY env var > .env file in cwd
@@ -96,6 +96,7 @@ async function askAgent(client: A2AClient, agentId: string, text: string): Promi
96
96
  const maxWait = 60_000;
97
97
  const poll = 1_000;
98
98
 
99
+ let printedQueued = false;
99
100
  while (!terminalStates.includes(current.status.state)) {
100
101
  if (current.status.state === 'input-required') {
101
102
  throw new Error('Agent requires additional input (not supported in CLI mode)');
@@ -103,6 +104,10 @@ async function askAgent(client: A2AClient, agentId: string, text: string): Promi
103
104
  if (current.status.state === 'auth-required') {
104
105
  throw new Error('Agent requires authentication — check your API key or request access');
105
106
  }
107
+ if (current.status.state === 'submitted' && !printedQueued) {
108
+ console.log(chalk.yellow('⏳ Message queued — recipient is offline. Waiting for delivery...'));
109
+ printedQueued = true;
110
+ }
106
111
  if (Date.now() - start > maxWait) throw new Error('Timed out waiting for agent response');
107
112
  await new Promise(r => setTimeout(r, poll));
108
113
  current = await client.getTask(current.id);
@@ -1347,8 +1352,11 @@ program
1347
1352
  ${chalk.bold('Examples:')}
1348
1353
  $ gopherhole send echo "Hello!"
1349
1354
  $ gopherhole send agent-abc123 "What's the weather?"
1355
+ $ gopherhole send agent-abc123 "Free now?" --ttl 0 # fail if offline
1356
+ $ gopherhole send agent-abc123 "Review this" --ttl 3600 # queue up to 1h
1350
1357
  `)
1351
- .action(async (agentId, message) => {
1358
+ .option('--ttl <seconds>', 'Message time-to-live in seconds (0 = no queue, omit = 30 day default)', parseInt)
1359
+ .action(async (agentId, message, cmdOpts) => {
1352
1360
  const sessionId = config.get('sessionId') as string;
1353
1361
  if (!sessionId) {
1354
1362
  console.log(chalk.yellow('Not logged in.'));
@@ -1371,7 +1379,10 @@ ${chalk.bold('Examples:')}
1371
1379
  method: 'SendMessage',
1372
1380
  params: {
1373
1381
  message: { role: 'user', parts: [{ kind: 'text', text: message }] },
1374
- configuration: { agentId },
1382
+ configuration: {
1383
+ agentId,
1384
+ ...(cmdOpts.ttl !== undefined ? { 'x-ttl': cmdOpts.ttl } : {}),
1385
+ },
1375
1386
  },
1376
1387
  id: 1,
1377
1388
  }),
@@ -1401,6 +1412,174 @@ ${chalk.bold('Examples:')}
1401
1412
  }
1402
1413
  });
1403
1414
 
1415
+ // ========== TASK COMMANDS ==========
1416
+
1417
+ const taskCmd = program
1418
+ .command('task')
1419
+ .description(`Manage tasks (queued messages, pending responses)
1420
+
1421
+ ${chalk.bold('Examples:')}
1422
+ $ gopherhole task status task-abc123
1423
+ $ gopherhole task pending
1424
+ $ gopherhole task cancel task-abc123
1425
+ $ gopherhole task cancel-all
1426
+ `);
1427
+
1428
+ taskCmd
1429
+ .command('status <taskId>')
1430
+ .description('Check the status of a task and get the response if completed')
1431
+ .action(async (taskId) => {
1432
+ const sessionId = config.get('sessionId') as string;
1433
+ if (!sessionId) { console.log(chalk.yellow('Not logged in. Run: gopherhole login')); process.exit(1); }
1434
+
1435
+ const spinner = ora(`Checking task ${taskId}...`).start();
1436
+ try {
1437
+ const res = await fetch(`${API_URL}/../a2a`, {
1438
+ method: 'POST',
1439
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1440
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'GetTask', params: { id: taskId }, id: 1 }),
1441
+ });
1442
+ const data = await res.json() as any;
1443
+ if (data.error) throw new Error(data.error.message || 'Failed');
1444
+
1445
+ const task = data.result;
1446
+ const state = task?.status?.state || 'unknown';
1447
+ spinner.stop();
1448
+
1449
+ console.log(`\n${chalk.bold('Task:')} ${taskId}`);
1450
+ console.log(`${chalk.bold('State:')} ${stateColor(state)(state)}`);
1451
+ if (task?.status?.timestamp) console.log(`${chalk.bold('Time:')} ${task.status.timestamp}`);
1452
+
1453
+ if (state === 'completed' && task?.artifacts?.length) {
1454
+ const texts = task.artifacts.flatMap((a: any) => a.parts?.filter((p: any) => p.kind === 'text').map((p: any) => p.text) || []);
1455
+ if (texts.length) console.log(`\n${chalk.bold('Response:')}\n${texts.join('\n')}`);
1456
+ } else if (state === 'submitted') {
1457
+ console.log(chalk.yellow('\n⏳ Queued — recipient hasn\'t come online yet.'));
1458
+ } else if (state === 'working') {
1459
+ console.log(chalk.blue('\n⚙️ Delivered — waiting for response.'));
1460
+ } else if (state === 'failed') {
1461
+ const msg = task?.status?.message;
1462
+ console.log(chalk.red(`\n❌ ${typeof msg === 'string' ? msg : 'Unknown error'}`));
1463
+ }
1464
+ } catch (err) {
1465
+ spinner.fail(chalk.red((err as Error).message));
1466
+ process.exit(1);
1467
+ }
1468
+ });
1469
+
1470
+ taskCmd
1471
+ .command('pending')
1472
+ .description('List all queued/pending tasks')
1473
+ .option('-l, --limit <n>', 'Max tasks to show', parseInt, 20)
1474
+ .action(async (opts) => {
1475
+ const sessionId = config.get('sessionId') as string;
1476
+ if (!sessionId) { console.log(chalk.yellow('Not logged in. Run: gopherhole login')); process.exit(1); }
1477
+
1478
+ const spinner = ora('Fetching pending tasks...').start();
1479
+ try {
1480
+ const res = await fetch(`${API_URL}/../a2a`, {
1481
+ method: 'POST',
1482
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1483
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'ListTasks', params: { status: 'submitted', pageSize: opts.limit || 20 }, id: 1 }),
1484
+ });
1485
+ const data = await res.json() as any;
1486
+ if (data.error) throw new Error(data.error.message || 'Failed');
1487
+
1488
+ const tasks = data.result?.tasks || [];
1489
+ spinner.stop();
1490
+
1491
+ if (tasks.length === 0) {
1492
+ console.log(chalk.green('✅ No pending tasks.'));
1493
+ return;
1494
+ }
1495
+
1496
+ console.log(chalk.bold(`\n${tasks.length} pending task(s):\n`));
1497
+ for (const t of tasks) {
1498
+ const age = Date.now() - new Date(t.status?.timestamp || 0).getTime();
1499
+ const ageMins = Math.round(age / 60000);
1500
+ console.log(` ${chalk.gray(t.id)} → ${t.serverAgentId || 'unknown'} ${chalk.yellow(`(${ageMins}m ago)`)}`);
1501
+ }
1502
+ } catch (err) {
1503
+ spinner.fail(chalk.red((err as Error).message));
1504
+ process.exit(1);
1505
+ }
1506
+ });
1507
+
1508
+ taskCmd
1509
+ .command('cancel <taskId>')
1510
+ .description('Cancel a specific task and purge its queued messages')
1511
+ .action(async (taskId) => {
1512
+ const sessionId = config.get('sessionId') as string;
1513
+ if (!sessionId) { console.log(chalk.yellow('Not logged in. Run: gopherhole login')); process.exit(1); }
1514
+
1515
+ const spinner = ora(`Canceling task ${taskId}...`).start();
1516
+ try {
1517
+ const res = await fetch(`${API_URL}/../a2a`, {
1518
+ method: 'POST',
1519
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1520
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'CancelTask', params: { id: taskId }, id: 1 }),
1521
+ });
1522
+ const data = await res.json() as any;
1523
+ if (data.error) throw new Error(data.error.message || 'Failed');
1524
+ spinner.succeed(`Task ${taskId} canceled. Queued messages purged.`);
1525
+ } catch (err) {
1526
+ spinner.fail(chalk.red((err as Error).message));
1527
+ process.exit(1);
1528
+ }
1529
+ });
1530
+
1531
+ taskCmd
1532
+ .command('cancel-all')
1533
+ .description('Cancel ALL pending tasks and purge all queued messages')
1534
+ .action(async () => {
1535
+ const sessionId = config.get('sessionId') as string;
1536
+ if (!sessionId) { console.log(chalk.yellow('Not logged in. Run: gopherhole login')); process.exit(1); }
1537
+
1538
+ const spinner = ora('Fetching pending tasks...').start();
1539
+ try {
1540
+ const res = await fetch(`${API_URL}/../a2a`, {
1541
+ method: 'POST',
1542
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1543
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'ListTasks', params: { status: 'submitted', pageSize: 100 }, id: 1 }),
1544
+ });
1545
+ const data = await res.json() as any;
1546
+ if (data.error) throw new Error(data.error.message || 'Failed');
1547
+
1548
+ const tasks = data.result?.tasks || [];
1549
+ if (tasks.length === 0) {
1550
+ spinner.succeed('No pending tasks to cancel.');
1551
+ return;
1552
+ }
1553
+
1554
+ spinner.text = `Canceling ${tasks.length} task(s)...`;
1555
+ let canceled = 0;
1556
+ for (const t of tasks) {
1557
+ try {
1558
+ await fetch(`${API_URL}/../a2a`, {
1559
+ method: 'POST',
1560
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
1561
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'CancelTask', params: { id: t.id }, id: 1 }),
1562
+ });
1563
+ canceled++;
1564
+ } catch { /* skip */ }
1565
+ }
1566
+ spinner.succeed(`Canceled ${canceled}/${tasks.length} task(s). Queued messages purged.`);
1567
+ } catch (err) {
1568
+ spinner.fail(chalk.red((err as Error).message));
1569
+ process.exit(1);
1570
+ }
1571
+ });
1572
+
1573
+ function stateColor(state: string) {
1574
+ switch (state) {
1575
+ case 'completed': return chalk.green;
1576
+ case 'submitted': return chalk.yellow;
1577
+ case 'working': return chalk.blue;
1578
+ case 'failed': case 'canceled': case 'rejected': return chalk.red;
1579
+ default: return chalk.gray;
1580
+ }
1581
+ }
1582
+
1404
1583
  // ========== DISCOVER COMMANDS ==========
1405
1584
 
1406
1585
  const discover = program