@gopherhole/cli 0.2.0 → 0.3.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 +34 -13
  2. package/package.json +2 -2
  3. package/src/index.ts +36 -13
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) {
@@ -62,6 +62,14 @@ async function resolveAgentId(flagValue) {
62
62
  catch { /* ignore */ }
63
63
  return null;
64
64
  }
65
+ function resolveTransport(flagValue) {
66
+ const value = flagValue || process.env.GOPHERHOLE_TRANSPORT || 'http';
67
+ if (!['http', 'ws', 'auto'].includes(value)) {
68
+ console.error(chalk_1.default.red(`Invalid transport: ${value}. Must be http, ws, or auto`));
69
+ process.exit(1);
70
+ }
71
+ return value;
72
+ }
65
73
  function makeAgentClient(apiKey) {
66
74
  return new sdk_1.A2AClient({
67
75
  apiKey,
@@ -69,10 +77,10 @@ function makeAgentClient(apiKey) {
69
77
  });
70
78
  }
71
79
  /** Hub client for workspace/discovery operations (does not connect WebSocket) */
72
- function makeHubClient(apiKey) {
80
+ function makeHubClient(apiKey, transport) {
73
81
  const apiUrl = process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai';
74
82
  const hubUrl = apiUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
75
- return new sdk_1.GopherHole({ apiKey, hubUrl, autoReconnect: false });
83
+ return new sdk_1.GopherHole({ apiKey, hubUrl, autoReconnect: false, transport: transport || 'http' });
76
84
  }
77
85
  /** Send a message and poll until terminal state, return response text.
78
86
  * Matches the MCP client pattern: sendText → poll getTask. */
@@ -83,6 +91,7 @@ async function askAgent(client, agentId, text) {
83
91
  const start = Date.now();
84
92
  const maxWait = 60_000;
85
93
  const poll = 1_000;
94
+ let printedQueued = false;
86
95
  while (!terminalStates.includes(current.status.state)) {
87
96
  if (current.status.state === 'input-required') {
88
97
  throw new Error('Agent requires additional input (not supported in CLI mode)');
@@ -90,6 +99,10 @@ async function askAgent(client, agentId, text) {
90
99
  if (current.status.state === 'auth-required') {
91
100
  throw new Error('Agent requires authentication — check your API key or request access');
92
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
+ }
93
106
  if (Date.now() - start > maxWait)
94
107
  throw new Error('Timed out waiting for agent response');
95
108
  await new Promise(r => setTimeout(r, poll));
@@ -166,6 +179,7 @@ ${chalk_1.default.bold('Documentation:')}
166
179
  `)
167
180
  .version(VERSION)
168
181
  .option('-v, --verbose', 'Enable verbose output for debugging')
182
+ .option('-t, --transport <mode>', 'Transport mode: http | ws | auto (default: http)')
169
183
  .hook('preAction', (thisCommand) => {
170
184
  verbose = thisCommand.opts().verbose || false;
171
185
  if (verbose) {
@@ -1207,8 +1221,11 @@ program
1207
1221
  ${chalk_1.default.bold('Examples:')}
1208
1222
  $ gopherhole send echo "Hello!"
1209
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
1210
1226
  `)
1211
- .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) => {
1212
1229
  const sessionId = config.get('sessionId');
1213
1230
  if (!sessionId) {
1214
1231
  console.log(chalk_1.default.yellow('Not logged in.'));
@@ -1229,7 +1246,10 @@ ${chalk_1.default.bold('Examples:')}
1229
1246
  method: 'SendMessage',
1230
1247
  params: {
1231
1248
  message: { role: 'user', parts: [{ kind: 'text', text: message }] },
1232
- configuration: { agentId },
1249
+ configuration: {
1250
+ agentId,
1251
+ ...(cmdOpts.ttl !== undefined ? { 'x-ttl': cmdOpts.ttl } : {}),
1252
+ },
1233
1253
  },
1234
1254
  id: 1,
1235
1255
  }),
@@ -2316,6 +2336,7 @@ ${chalk_1.default.bold('Examples:')}
2316
2336
  console.error(chalk_1.default.gray('Set GOPHERHOLE_API_KEY, pass --api-key, or run gopher init to create a .env'));
2317
2337
  process.exit(1);
2318
2338
  }
2339
+ log(`Transport: http (A2AClient is HTTP-only)`);
2319
2340
  const spinner = (0, ora_1.default)(`Messaging ${agentId}...`).start();
2320
2341
  try {
2321
2342
  const client = makeAgentClient(apiKey);
@@ -2465,7 +2486,7 @@ ws
2465
2486
  }
2466
2487
  const spinner = (0, ora_1.default)('Loading workspaces...').start();
2467
2488
  try {
2468
- const client = makeHubClient(apiKey);
2489
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2469
2490
  const result = await client.workspaceList();
2470
2491
  spinner.stop();
2471
2492
  if (!result.workspaces.length) {
@@ -2499,7 +2520,7 @@ ws
2499
2520
  }
2500
2521
  const spinner = (0, ora_1.default)('Creating workspace...').start();
2501
2522
  try {
2502
- const client = makeHubClient(apiKey);
2523
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2503
2524
  const result = await client.workspaceCreate(name, options.description);
2504
2525
  spinner.succeed(`Workspace created: ${chalk_1.default.bold(result.workspace.name)}`);
2505
2526
  console.log(chalk_1.default.gray(` ID: ${result.workspace.id}`));
@@ -2524,7 +2545,7 @@ ws
2524
2545
  }
2525
2546
  const spinner = (0, ora_1.default)('Searching...').start();
2526
2547
  try {
2527
- const client = makeHubClient(apiKey);
2548
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2528
2549
  const result = await client.workspaceQuery({
2529
2550
  workspace_id: workspaceId,
2530
2551
  query,
@@ -2565,7 +2586,7 @@ ws
2565
2586
  }
2566
2587
  const spinner = (0, ora_1.default)('Storing...').start();
2567
2588
  try {
2568
- const client = makeHubClient(apiKey);
2589
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2569
2590
  const result = await client.workspaceStore({
2570
2591
  workspace_id: workspaceId,
2571
2592
  content,
@@ -2594,7 +2615,7 @@ ws
2594
2615
  }
2595
2616
  const spinner = (0, ora_1.default)('Loading memories...').start();
2596
2617
  try {
2597
- const client = makeHubClient(apiKey);
2618
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2598
2619
  const result = await client.workspaceMemories({
2599
2620
  workspace_id: workspaceId,
2600
2621
  limit: parseInt(options.limit),
@@ -2637,7 +2658,7 @@ ws
2637
2658
  }
2638
2659
  const spinner = (0, ora_1.default)('Deleting...').start();
2639
2660
  try {
2640
- const client = makeHubClient(apiKey);
2661
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2641
2662
  const result = await client.workspaceForget({
2642
2663
  workspace_id: workspaceId,
2643
2664
  id: options.id,
@@ -2666,7 +2687,7 @@ wsMembers
2666
2687
  }
2667
2688
  const spinner = (0, ora_1.default)('Loading members...').start();
2668
2689
  try {
2669
- const client = makeHubClient(apiKey);
2690
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2670
2691
  const result = await client.workspaceMembersList(workspaceId);
2671
2692
  spinner.stop();
2672
2693
  result.members.forEach(m => {
@@ -2692,7 +2713,7 @@ wsMembers
2692
2713
  }
2693
2714
  const spinner = (0, ora_1.default)('Adding member...').start();
2694
2715
  try {
2695
- const client = makeHubClient(apiKey);
2716
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2696
2717
  await client.workspaceMembersAdd(workspaceId, agentId, options.role);
2697
2718
  spinner.succeed(`Added ${agentId} with role: ${options.role}`);
2698
2719
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "GopherHole CLI - Connect AI agents to the world",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "author": "GopherHole",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "@gopherhole/sdk": "^0.5.4",
25
+ "@gopherhole/sdk": "^0.6.0",
26
26
  "chalk": "^5.3.0",
27
27
  "commander": "^12.0.0",
28
28
  "conf": "^12.0.0",
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import inquirer from 'inquirer';
6
6
  import ora from 'ora';
7
7
  import open from 'open';
8
8
  import { A2AClient, GopherHole } from '@gopherhole/sdk';
9
+ import type { TransportMode } from '@gopherhole/sdk';
9
10
 
10
11
  const config = new Conf({ projectName: 'gopherhole' });
11
12
  const API_URL = 'https://gopherhole.ai/api';
@@ -19,7 +20,7 @@ const brand = {
19
20
  };
20
21
 
21
22
  // Version
22
- const VERSION = '0.2.0';
23
+ const VERSION = '0.3.0';
23
24
 
24
25
  // ========== API KEY RESOLUTION ==========
25
26
  // Precedence: --api-key flag > GOPHERHOLE_API_KEY env var > .env file in cwd
@@ -61,6 +62,15 @@ async function resolveAgentId(flagValue?: string): Promise<string | null> {
61
62
  return null;
62
63
  }
63
64
 
65
+ function resolveTransport(flagValue?: string): TransportMode {
66
+ const value = flagValue || process.env.GOPHERHOLE_TRANSPORT || 'http';
67
+ if (!['http', 'ws', 'auto'].includes(value)) {
68
+ console.error(chalk.red(`Invalid transport: ${value}. Must be http, ws, or auto`));
69
+ process.exit(1);
70
+ }
71
+ return value as TransportMode;
72
+ }
73
+
64
74
  function makeAgentClient(apiKey: string): A2AClient {
65
75
  return new A2AClient({
66
76
  apiKey,
@@ -69,10 +79,10 @@ function makeAgentClient(apiKey: string): A2AClient {
69
79
  }
70
80
 
71
81
  /** Hub client for workspace/discovery operations (does not connect WebSocket) */
72
- function makeHubClient(apiKey: string): GopherHole {
82
+ function makeHubClient(apiKey: string, transport?: TransportMode): GopherHole {
73
83
  const apiUrl = process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai';
74
84
  const hubUrl = apiUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
75
- return new GopherHole({ apiKey, hubUrl, autoReconnect: false });
85
+ return new GopherHole({ apiKey, hubUrl, autoReconnect: false, transport: transport || 'http' });
76
86
  }
77
87
 
78
88
  /** Send a message and poll until terminal state, return response text.
@@ -86,6 +96,7 @@ async function askAgent(client: A2AClient, agentId: string, text: string): Promi
86
96
  const maxWait = 60_000;
87
97
  const poll = 1_000;
88
98
 
99
+ let printedQueued = false;
89
100
  while (!terminalStates.includes(current.status.state)) {
90
101
  if (current.status.state === 'input-required') {
91
102
  throw new Error('Agent requires additional input (not supported in CLI mode)');
@@ -93,6 +104,10 @@ async function askAgent(client: A2AClient, agentId: string, text: string): Promi
93
104
  if (current.status.state === 'auth-required') {
94
105
  throw new Error('Agent requires authentication — check your API key or request access');
95
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
+ }
96
111
  if (Date.now() - start > maxWait) throw new Error('Timed out waiting for agent response');
97
112
  await new Promise(r => setTimeout(r, poll));
98
113
  current = await client.getTask(current.id);
@@ -176,6 +191,7 @@ ${chalk.bold('Documentation:')}
176
191
  `)
177
192
  .version(VERSION)
178
193
  .option('-v, --verbose', 'Enable verbose output for debugging')
194
+ .option('-t, --transport <mode>', 'Transport mode: http | ws | auto (default: http)')
179
195
  .hook('preAction', (thisCommand) => {
180
196
  verbose = thisCommand.opts().verbose || false;
181
197
  if (verbose) {
@@ -1336,8 +1352,11 @@ program
1336
1352
  ${chalk.bold('Examples:')}
1337
1353
  $ gopherhole send echo "Hello!"
1338
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
1339
1357
  `)
1340
- .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) => {
1341
1360
  const sessionId = config.get('sessionId') as string;
1342
1361
  if (!sessionId) {
1343
1362
  console.log(chalk.yellow('Not logged in.'));
@@ -1360,7 +1379,10 @@ ${chalk.bold('Examples:')}
1360
1379
  method: 'SendMessage',
1361
1380
  params: {
1362
1381
  message: { role: 'user', parts: [{ kind: 'text', text: message }] },
1363
- configuration: { agentId },
1382
+ configuration: {
1383
+ agentId,
1384
+ ...(cmdOpts.ttl !== undefined ? { 'x-ttl': cmdOpts.ttl } : {}),
1385
+ },
1364
1386
  },
1365
1387
  id: 1,
1366
1388
  }),
@@ -2550,6 +2572,7 @@ ${chalk.bold('Examples:')}
2550
2572
  process.exit(1);
2551
2573
  }
2552
2574
 
2575
+ log(`Transport: http (A2AClient is HTTP-only)`);
2553
2576
  const spinner = ora(`Messaging ${agentId}...`).start();
2554
2577
  try {
2555
2578
  const client = makeAgentClient(apiKey);
@@ -2695,7 +2718,7 @@ ws
2695
2718
  }
2696
2719
  const spinner = ora('Loading workspaces...').start();
2697
2720
  try {
2698
- const client = makeHubClient(apiKey);
2721
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2699
2722
  const result = await client.workspaceList();
2700
2723
  spinner.stop();
2701
2724
  if (!result.workspaces.length) {
@@ -2726,7 +2749,7 @@ ws
2726
2749
  }
2727
2750
  const spinner = ora('Creating workspace...').start();
2728
2751
  try {
2729
- const client = makeHubClient(apiKey);
2752
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2730
2753
  const result = await client.workspaceCreate(name, options.description);
2731
2754
  spinner.succeed(`Workspace created: ${chalk.bold(result.workspace.name)}`);
2732
2755
  console.log(chalk.gray(` ID: ${result.workspace.id}`));
@@ -2749,7 +2772,7 @@ ws
2749
2772
  }
2750
2773
  const spinner = ora('Searching...').start();
2751
2774
  try {
2752
- const client = makeHubClient(apiKey);
2775
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2753
2776
  const result = await client.workspaceQuery({
2754
2777
  workspace_id: workspaceId,
2755
2778
  query,
@@ -2787,7 +2810,7 @@ ws
2787
2810
  }
2788
2811
  const spinner = ora('Storing...').start();
2789
2812
  try {
2790
- const client = makeHubClient(apiKey);
2813
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2791
2814
  const result = await client.workspaceStore({
2792
2815
  workspace_id: workspaceId,
2793
2816
  content,
@@ -2814,7 +2837,7 @@ ws
2814
2837
  }
2815
2838
  const spinner = ora('Loading memories...').start();
2816
2839
  try {
2817
- const client = makeHubClient(apiKey);
2840
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2818
2841
  const result = await client.workspaceMemories({
2819
2842
  workspace_id: workspaceId,
2820
2843
  limit: parseInt(options.limit),
@@ -2854,7 +2877,7 @@ ws
2854
2877
  }
2855
2878
  const spinner = ora('Deleting...').start();
2856
2879
  try {
2857
- const client = makeHubClient(apiKey);
2880
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2858
2881
  const result = await client.workspaceForget({
2859
2882
  workspace_id: workspaceId,
2860
2883
  id: options.id,
@@ -2882,7 +2905,7 @@ wsMembers
2882
2905
  }
2883
2906
  const spinner = ora('Loading members...').start();
2884
2907
  try {
2885
- const client = makeHubClient(apiKey);
2908
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2886
2909
  const result = await client.workspaceMembersList(workspaceId);
2887
2910
  spinner.stop();
2888
2911
  result.members.forEach(m => {
@@ -2906,7 +2929,7 @@ wsMembers
2906
2929
  }
2907
2930
  const spinner = ora('Adding member...').start();
2908
2931
  try {
2909
- const client = makeHubClient(apiKey);
2932
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2910
2933
  await client.workspaceMembersAdd(workspaceId, agentId, options.role);
2911
2934
  spinner.succeed(`Added ${agentId} with role: ${options.role}`);
2912
2935
  } catch (err) {