@gopherhole/cli 0.1.23 → 0.2.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 +613 -14
  2. package/package.json +2 -1
  3. package/src/index.ts +596 -14
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import Conf from 'conf';
5
5
  import inquirer from 'inquirer';
6
6
  import ora from 'ora';
7
7
  import open from 'open';
8
+ import { A2AClient, GopherHole } from '@gopherhole/sdk';
8
9
 
9
10
  const config = new Conf({ projectName: 'gopherhole' });
10
11
  const API_URL = 'https://gopherhole.ai/api';
@@ -18,7 +19,102 @@ const brand = {
18
19
  };
19
20
 
20
21
  // Version
21
- const VERSION = '0.1.23';
22
+ const VERSION = '0.2.0';
23
+
24
+ // ========== API KEY RESOLUTION ==========
25
+ // Precedence: --api-key flag > GOPHERHOLE_API_KEY env var > .env file in cwd
26
+
27
+ async function resolveApiKey(flagValue?: string): Promise<string | null> {
28
+ if (flagValue) return flagValue;
29
+ if (process.env.GOPHERHOLE_API_KEY) return process.env.GOPHERHOLE_API_KEY;
30
+
31
+ try {
32
+ const fs = await import('fs');
33
+ const path = await import('path');
34
+ const envPath = path.join(process.cwd(), '.env');
35
+ if (fs.existsSync(envPath)) {
36
+ const content = fs.readFileSync(envPath, 'utf-8');
37
+ const match = content.match(/^GOPHERHOLE_API_KEY=(.+)$/m);
38
+ if (match) return match[1].trim().replace(/^["']|["']$/g, '');
39
+ }
40
+ } catch { /* ignore */ }
41
+
42
+ return null;
43
+ }
44
+
45
+ /** HTTP client for sending A2A messages */
46
+ async function resolveAgentId(flagValue?: string): Promise<string | null> {
47
+ if (flagValue) return flagValue;
48
+ if (process.env.GOPHERHOLE_AGENT_ID) return process.env.GOPHERHOLE_AGENT_ID;
49
+
50
+ try {
51
+ const fs = await import('fs');
52
+ const path = await import('path');
53
+ const envPath = path.join(process.cwd(), '.env');
54
+ if (fs.existsSync(envPath)) {
55
+ const content = fs.readFileSync(envPath, 'utf-8');
56
+ const match = content.match(/^GOPHERHOLE_AGENT_ID=(.+)$/m);
57
+ if (match) return match[1].trim().replace(/^["']|["']$/g, '');
58
+ }
59
+ } catch { /* ignore */ }
60
+
61
+ return null;
62
+ }
63
+
64
+ function makeAgentClient(apiKey: string): A2AClient {
65
+ return new A2AClient({
66
+ apiKey,
67
+ baseUrl: (process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai') + '/a2a',
68
+ });
69
+ }
70
+
71
+ /** Hub client for workspace/discovery operations (does not connect WebSocket) */
72
+ function makeHubClient(apiKey: string): GopherHole {
73
+ const apiUrl = process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai';
74
+ const hubUrl = apiUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
75
+ return new GopherHole({ apiKey, hubUrl, autoReconnect: false });
76
+ }
77
+
78
+ /** Send a message and poll until terminal state, return response text.
79
+ * Matches the MCP client pattern: sendText → poll getTask. */
80
+ async function askAgent(client: A2AClient, agentId: string, text: string): Promise<string> {
81
+ const task = await client.sendText(agentId, text);
82
+
83
+ const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
84
+ let current = task;
85
+ const start = Date.now();
86
+ const maxWait = 60_000;
87
+ const poll = 1_000;
88
+
89
+ while (!terminalStates.includes(current.status.state)) {
90
+ if (current.status.state === 'input-required') {
91
+ throw new Error('Agent requires additional input (not supported in CLI mode)');
92
+ }
93
+ if (current.status.state === 'auth-required') {
94
+ throw new Error('Agent requires authentication — check your API key or request access');
95
+ }
96
+ if (Date.now() - start > maxWait) throw new Error('Timed out waiting for agent response');
97
+ await new Promise(r => setTimeout(r, poll));
98
+ current = await client.getTask(current.id);
99
+ }
100
+
101
+ if (current.status.state === 'failed') {
102
+ const msg = current.status.message?.parts?.[0];
103
+ throw new Error((msg && 'text' in msg ? msg.text : null) || 'Agent task failed');
104
+ }
105
+
106
+ // Extract text from artifacts first, then history
107
+ if (current.artifacts?.length) {
108
+ const texts = current.artifacts.flatMap(a => a.parts.filter(p => 'text' in p && p.text).map(p => (p as { text: string }).text));
109
+ if (texts.length) return texts.join('\n');
110
+ }
111
+ if (current.history?.length) {
112
+ const last = current.history[current.history.length - 1];
113
+ const texts = last.parts.filter(p => 'text' in p && p.text).map(p => (p as { text: string }).text);
114
+ if (texts.length) return texts.join('\n');
115
+ }
116
+ return '';
117
+ }
22
118
 
23
119
  // ASCII art banner
24
120
  function showBanner(context?: string) {
@@ -78,7 +174,7 @@ ${chalk.bold('Examples:')}
78
174
  ${chalk.bold('Documentation:')}
79
175
  https://docs.gopherhole.ai
80
176
  `)
81
- .version('0.1.0')
177
+ .version(VERSION)
82
178
  .option('-v, --verbose', 'Enable verbose output for debugging')
83
179
  .hook('preAction', (thisCommand) => {
84
180
  verbose = thisCommand.opts().verbose || false;
@@ -1078,13 +1174,23 @@ ${chalk.bold('Example:')}
1078
1174
  const data = await res.json();
1079
1175
  spinner.succeed('Agent created!');
1080
1176
 
1081
- // Write .env file
1177
+ // Write .env file (never overwrite existing)
1082
1178
  const fs = await import('fs');
1083
- fs.writeFileSync('.env', `# GopherHole Configuration
1084
- GOPHERHOLE_API_KEY=${data.apiKey}
1085
- GOPHERHOLE_AGENT_ID=${data.agent.id}
1086
- `);
1087
- console.log(brand.green('✓ Created .env'));
1179
+ const envLines = `GOPHERHOLE_API_KEY=${data.apiKey}\nGOPHERHOLE_AGENT_ID=${data.agent.id}`;
1180
+
1181
+ if (fs.existsSync('.env')) {
1182
+ const existing = fs.readFileSync('.env', 'utf-8');
1183
+ if (existing.includes('GOPHERHOLE_API_KEY')) {
1184
+ console.log(chalk.gray('↳ .env already has GOPHERHOLE_API_KEY, skipping'));
1185
+ console.log(chalk.gray(` Your new key: ${data.apiKey}`));
1186
+ } else {
1187
+ fs.appendFileSync('.env', `\n# GopherHole Configuration\n${envLines}\n`);
1188
+ console.log(brand.green('✓ Updated .env (appended GopherHole keys)'));
1189
+ }
1190
+ } else {
1191
+ fs.writeFileSync('.env', `# GopherHole Configuration\n${envLines}\n`);
1192
+ console.log(brand.green('✓ Created .env'));
1193
+ }
1088
1194
 
1089
1195
  // Write example code
1090
1196
  const exampleCode = `import { GopherHole } from '@gopherhole/sdk';
@@ -1114,8 +1220,12 @@ async function main() {
1114
1220
 
1115
1221
  main().catch(console.error);
1116
1222
  `;
1117
- fs.writeFileSync('agent.ts', exampleCode);
1118
- console.log(brand.green('✓ Created agent.ts'));
1223
+ if (!fs.existsSync('agent.ts')) {
1224
+ fs.writeFileSync('agent.ts', exampleCode);
1225
+ console.log(brand.green('✓ Created agent.ts'));
1226
+ } else {
1227
+ console.log(chalk.gray('↳ agent.ts already exists, skipping'));
1228
+ }
1119
1229
 
1120
1230
  // Create package.json if it doesn't exist
1121
1231
  if (!fs.existsSync('package.json')) {
@@ -1140,6 +1250,73 @@ main().catch(console.error);
1140
1250
  console.log(brand.green('✓ Created package.json'));
1141
1251
  }
1142
1252
 
1253
+ // Append GopherHole section to AGENTS.md (idempotent)
1254
+ const agentsMdPath = 'AGENTS.md';
1255
+ const gopherSection = `
1256
+ ## GopherHole
1257
+
1258
+ This project is connected to the GopherHole agent hub.
1259
+
1260
+ ### Auth
1261
+
1262
+ The \`gopher\` CLI resolves your API key in this order:
1263
+ 1. \`--api-key <key>\` flag on any command
1264
+ 2. \`GOPHERHOLE_API_KEY\` environment variable
1265
+ 3. \`GOPHERHOLE_API_KEY\` in a \`.env\` file in the current directory
1266
+
1267
+ Never hard-code API keys. Use the env var or \`--api-key\` flag.
1268
+
1269
+ ### Agent identity
1270
+
1271
+ - Agent ID: \`${data.agent.id}\`
1272
+ - Name: ${agentName}
1273
+
1274
+ ### Commands
1275
+
1276
+ \`\`\`
1277
+ # Send a message to an agent
1278
+ gopher message <agent-id> "<text>"
1279
+ gopher message <agent-id> "<text>" --api-key <key>
1280
+
1281
+ # Agent memory (via memory agent)
1282
+ gopher memory recall "<query>"
1283
+ gopher memory store "<content>" [--tags tag1,tag2]
1284
+ gopher memory list [--limit 20]
1285
+ gopher memory forget "<query>" --confirm
1286
+
1287
+ # Shared workspaces
1288
+ gopher workspace list
1289
+ gopher workspace create <name>
1290
+ gopher workspace query <workspace-id> "<query>"
1291
+ gopher workspace store <workspace-id> "<content>" [--type fact|decision|preference|todo|context|reference]
1292
+ gopher workspace memories <workspace-id>
1293
+ gopher workspace forget <workspace-id> --id <memory-id>
1294
+ gopher workspace forget <workspace-id> --query "<query>"
1295
+ gopher workspace members list <workspace-id>
1296
+ gopher workspace members add <workspace-id> <agent-id> [--role read|write|admin]
1297
+
1298
+ # Discover agents (--api-key enables agent-mode auth)
1299
+ gopher discover search "<query>" --api-key <key>
1300
+ gopher discover search --category <category> --tag <tag>
1301
+ gopher discover nearby --lat <lat> --lng <lng> [--radius 10]
1302
+ gopher discover info <agent-id>
1303
+ gopher discover info <agent-id>
1304
+
1305
+ # Auth check
1306
+ gopher whoami
1307
+ \`\`\`
1308
+ `;
1309
+
1310
+ const agentsMdExists = fs.existsSync(agentsMdPath);
1311
+ const existingContent = agentsMdExists ? fs.readFileSync(agentsMdPath, 'utf-8') : '';
1312
+
1313
+ if (existingContent.includes('## GopherHole')) {
1314
+ console.log(chalk.gray('↳ AGENTS.md already has GopherHole section, skipping'));
1315
+ } else {
1316
+ fs.writeFileSync(agentsMdPath, existingContent + gopherSection);
1317
+ console.log(brand.green(agentsMdExists ? '✓ Updated AGENTS.md' : '✓ Created AGENTS.md'));
1318
+ }
1319
+
1143
1320
  console.log(chalk.bold('\n🎉 Project initialized!\n'));
1144
1321
  console.log(chalk.bold('Next steps:'));
1145
1322
  console.log(chalk.cyan(' npm install'));
@@ -1248,6 +1425,7 @@ ${chalk.bold('Examples:')}
1248
1425
  .option('-l, --limit <limit>', 'Number of results (max 50)', '10')
1249
1426
  .option('-o, --offset <offset>', 'Pagination offset')
1250
1427
  .option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
1428
+ .option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
1251
1429
  .action(async (query, options) => {
1252
1430
  const spinner = ora('Searching agents...').start();
1253
1431
 
@@ -1267,9 +1445,14 @@ ${chalk.bold('Examples:')}
1267
1445
  if (options.scope) params.set('scope', options.scope);
1268
1446
 
1269
1447
  log('GET /discover/agents?' + params.toString());
1448
+ const apiKey = await resolveApiKey(options.apiKey);
1270
1449
  const sessionId = config.get('sessionId') as string | undefined;
1271
1450
  const headers: Record<string, string> = {};
1272
- if (sessionId) headers['X-Session-ID'] = sessionId;
1451
+ if (apiKey) {
1452
+ headers['Authorization'] = `Bearer ${apiKey}`;
1453
+ } else if (sessionId) {
1454
+ headers['X-Session-ID'] = sessionId;
1455
+ }
1273
1456
  const res = await fetch(`${API_URL}/discover/agents?${params}`, { headers });
1274
1457
  const data = await res.json();
1275
1458
  spinner.stop();
@@ -1391,10 +1574,12 @@ ${chalk.bold('Examples:')}
1391
1574
  .option('-t, --tag <tag>', 'Filter by tag')
1392
1575
  .option('-c, --category <category>', 'Filter by category')
1393
1576
  .option('-l, --limit <limit>', 'Number of results (max 50)', '20')
1577
+ .option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
1394
1578
  .action(async (options) => {
1395
1579
  const spinner = ora('Searching nearby agents...').start();
1396
1580
 
1397
1581
  try {
1582
+ const apiKey = await resolveApiKey(options.apiKey);
1398
1583
  const sessionId = config.get('sessionId') as string;
1399
1584
  const params = new URLSearchParams();
1400
1585
  params.set('lat', options.lat);
@@ -1406,7 +1591,9 @@ ${chalk.bold('Examples:')}
1406
1591
 
1407
1592
  log('GET /discover/agents/nearby?' + params.toString());
1408
1593
  const headers: Record<string, string> = {};
1409
- if (sessionId) {
1594
+ if (apiKey) {
1595
+ headers['Authorization'] = `Bearer ${apiKey}`;
1596
+ } else if (sessionId) {
1410
1597
  headers['X-Session-ID'] = sessionId;
1411
1598
  }
1412
1599
  const res = await fetch(`${API_URL}/discover/agents/nearby?${params}`, { headers });
@@ -1447,14 +1634,20 @@ discover
1447
1634
  ${chalk.bold('Example:')}
1448
1635
  $ gopherhole discover info agent-abc123
1449
1636
  `)
1450
- .action(async (agentId) => {
1637
+ .option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
1638
+ .action(async (agentId, options) => {
1451
1639
  const spinner = ora('Fetching agent info...').start();
1452
1640
 
1453
1641
  try {
1454
1642
  log('GET /discover/agents/' + agentId);
1643
+ const apiKey = await resolveApiKey(options.apiKey);
1455
1644
  const sessionId = config.get('sessionId') as string | undefined;
1456
1645
  const headers: Record<string, string> = {};
1457
- if (sessionId) headers['X-Session-ID'] = sessionId;
1646
+ if (apiKey) {
1647
+ headers['Authorization'] = `Bearer ${apiKey}`;
1648
+ } else if (sessionId) {
1649
+ headers['X-Session-ID'] = sessionId;
1650
+ }
1458
1651
  const res = await fetch(`${API_URL}/discover/agents/${agentId}`, { headers });
1459
1652
  if (!res.ok) {
1460
1653
  throw new Error('Agent not found');
@@ -2332,5 +2525,394 @@ program
2332
2525
  console.log('');
2333
2526
  });
2334
2527
 
2528
+ // ========== MESSAGE COMMAND (agent-to-agent, Bearer auth) ==========
2529
+
2530
+ program
2531
+ .command('message <agentId> <text>')
2532
+ .description(`Send a message to an agent using your API key (agent-to-agent)
2533
+
2534
+ ${chalk.bold('Auth (in order of precedence):')}
2535
+ --api-key <key> > GOPHERHOLE_API_KEY env > .env file
2536
+ --agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
2537
+
2538
+ ${chalk.bold('Examples:')}
2539
+ $ gopher message agent-memory-official "What do you remember about me?"
2540
+ $ gopher message echo-agent "Hello!" --api-key gph_xxx
2541
+ $ GOPHERHOLE_API_KEY=gph_xxx gopher message search-agent "latest AI news"
2542
+ `)
2543
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2544
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2545
+ .action(async (agentId, text, options) => {
2546
+ const apiKey = await resolveApiKey(options.apiKey);
2547
+ if (!apiKey) {
2548
+ console.error(chalk.red('No API key found.'));
2549
+ console.error(chalk.gray('Set GOPHERHOLE_API_KEY, pass --api-key, or run gopher init to create a .env'));
2550
+ process.exit(1);
2551
+ }
2552
+
2553
+ const spinner = ora(`Messaging ${agentId}...`).start();
2554
+ try {
2555
+ const client = makeAgentClient(apiKey);
2556
+ const response = await askAgent(client, agentId, text);
2557
+ spinner.stop();
2558
+ console.log(response || chalk.gray('(no response)'));
2559
+ } catch (err) {
2560
+ spinner.fail(chalk.red((err as Error).message));
2561
+ process.exit(1);
2562
+ }
2563
+ });
2564
+
2565
+ // ========== MEMORY COMMANDS (agent-to-agent via memory agent) ==========
2566
+
2567
+ const MEMORY_AGENT = process.env.GOPHERHOLE_MEMORY_AGENT || 'agent-memory-official';
2568
+
2569
+ const memory = program
2570
+ .command('memory')
2571
+ .description(`Manage agent memory via the GopherHole memory agent
2572
+
2573
+ ${chalk.bold('Auth (in order of precedence):')}
2574
+ --api-key <key> > GOPHERHOLE_API_KEY env > .env file
2575
+ --agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
2576
+ `);
2577
+
2578
+ memory
2579
+ .command('recall <query>')
2580
+ .description('Search your agent memories semantically')
2581
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2582
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2583
+ .option('--limit <n>', 'Max results', '10')
2584
+ .action(async (query, options) => {
2585
+ const apiKey = await resolveApiKey(options.apiKey);
2586
+ if (!apiKey) {
2587
+ console.error(chalk.red('No API key found.')); process.exit(1);
2588
+ }
2589
+ const spinner = ora('Recalling...').start();
2590
+ try {
2591
+ const client = makeAgentClient(apiKey);
2592
+ const msg = `Search memories for: ${query} (limit: ${options.limit})`;
2593
+ const response = await askAgent(client, MEMORY_AGENT, msg);
2594
+ spinner.stop();
2595
+ console.log(response || chalk.gray('No memories found'));
2596
+ } catch (err) {
2597
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2598
+ }
2599
+ });
2600
+
2601
+ memory
2602
+ .command('store <content>')
2603
+ .description('Store a new memory')
2604
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2605
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2606
+ .option('--tags <tags>', 'Comma-separated tags')
2607
+ .action(async (content, options) => {
2608
+ const apiKey = await resolveApiKey(options.apiKey);
2609
+ if (!apiKey) {
2610
+ console.error(chalk.red('No API key found.')); process.exit(1);
2611
+ }
2612
+ const spinner = ora('Storing memory...').start();
2613
+ try {
2614
+ const client = makeAgentClient(apiKey);
2615
+ const tags = options.tags ? `\n\nTags: ${options.tags}` : '';
2616
+ const response = await askAgent(client, MEMORY_AGENT, `Remember this:\n\n${content}${tags}`);
2617
+ spinner.stop();
2618
+ console.log(response || chalk.green('Memory stored'));
2619
+ } catch (err) {
2620
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2621
+ }
2622
+ });
2623
+
2624
+ memory
2625
+ .command('forget <query>')
2626
+ .description('Delete memories matching a query')
2627
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2628
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2629
+ .option('--confirm', 'Required to confirm deletion')
2630
+ .action(async (query, options) => {
2631
+ const apiKey = await resolveApiKey(options.apiKey);
2632
+ if (!apiKey) {
2633
+ console.error(chalk.red('No API key found.')); process.exit(1);
2634
+ }
2635
+ if (!options.confirm) {
2636
+ console.error(chalk.yellow('Add --confirm to delete memories'));
2637
+ process.exit(1);
2638
+ }
2639
+ const spinner = ora('Forgetting...').start();
2640
+ try {
2641
+ const client = makeAgentClient(apiKey);
2642
+ const response = await askAgent(client, MEMORY_AGENT, `Forget memories matching: ${query}`);
2643
+ spinner.stop();
2644
+ console.log(response || chalk.green('Done'));
2645
+ } catch (err) {
2646
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2647
+ }
2648
+ });
2649
+
2650
+ memory
2651
+ .command('list')
2652
+ .description('List recent memories')
2653
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2654
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2655
+ .option('--limit <n>', 'Max results', '20')
2656
+ .option('--offset <n>', 'Pagination offset', '0')
2657
+ .action(async (options) => {
2658
+ const apiKey = await resolveApiKey(options.apiKey);
2659
+ if (!apiKey) {
2660
+ console.error(chalk.red('No API key found.')); process.exit(1);
2661
+ }
2662
+ const spinner = ora('Listing memories...').start();
2663
+ try {
2664
+ const client = makeAgentClient(apiKey);
2665
+ const msg = `List my recent memories (limit: ${options.limit}, offset: ${options.offset})`;
2666
+ const response = await askAgent(client, MEMORY_AGENT, msg);
2667
+ spinner.stop();
2668
+ console.log(response || chalk.gray('No memories found'));
2669
+ } catch (err) {
2670
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2671
+ }
2672
+ });
2673
+
2674
+ // ========== WORKSPACE COMMANDS (direct RPC, Bearer auth) ==========
2675
+
2676
+ const ws = program
2677
+ .command('workspace')
2678
+ .alias('ws')
2679
+ .description(`Manage shared workspaces for multi-agent collaboration
2680
+
2681
+ ${chalk.bold('Auth (in order of precedence):')}
2682
+ --api-key <key> > GOPHERHOLE_API_KEY env > .env file
2683
+ --agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
2684
+ `);
2685
+
2686
+ ws
2687
+ .command('list')
2688
+ .description('List workspaces you are a member of')
2689
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2690
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2691
+ .action(async (options) => {
2692
+ const apiKey = await resolveApiKey(options.apiKey);
2693
+ if (!apiKey) {
2694
+ console.error(chalk.red('No API key found.')); process.exit(1);
2695
+ }
2696
+ const spinner = ora('Loading workspaces...').start();
2697
+ try {
2698
+ const client = makeHubClient(apiKey);
2699
+ const result = await client.workspaceList();
2700
+ spinner.stop();
2701
+ if (!result.workspaces.length) {
2702
+ console.log(chalk.gray('No workspaces yet. Create one with: gopher workspace create <name>'));
2703
+ return;
2704
+ }
2705
+ result.workspaces.forEach(w => {
2706
+ console.log(`\n${chalk.bold(w.name)} ${chalk.gray(`(${w.id})`)}`);
2707
+ if (w.description) console.log(chalk.gray(` ${w.description}`));
2708
+ console.log(chalk.gray(` Role: ${w.my_role || '?'} | Members: ${w.member_count || '?'} | Memories: ${w.memory_count || '?'}`));
2709
+ });
2710
+ console.log('');
2711
+ } catch (err) {
2712
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2713
+ }
2714
+ });
2715
+
2716
+ ws
2717
+ .command('create <name>')
2718
+ .description('Create a new workspace')
2719
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2720
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2721
+ .option('--description <text>', 'Workspace description')
2722
+ .action(async (name, options) => {
2723
+ const apiKey = await resolveApiKey(options.apiKey);
2724
+ if (!apiKey) {
2725
+ console.error(chalk.red('No API key found.')); process.exit(1);
2726
+ }
2727
+ const spinner = ora('Creating workspace...').start();
2728
+ try {
2729
+ const client = makeHubClient(apiKey);
2730
+ const result = await client.workspaceCreate(name, options.description);
2731
+ spinner.succeed(`Workspace created: ${chalk.bold(result.workspace.name)}`);
2732
+ console.log(chalk.gray(` ID: ${result.workspace.id}`));
2733
+ } catch (err) {
2734
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2735
+ }
2736
+ });
2737
+
2738
+ ws
2739
+ .command('query <workspaceId> <query>')
2740
+ .description('Search workspace memories semantically')
2741
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2742
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2743
+ .option('--type <type>', 'Filter by memory type (fact|decision|preference|todo|context|reference)')
2744
+ .option('--limit <n>', 'Max results', '10')
2745
+ .action(async (workspaceId, query, options) => {
2746
+ const apiKey = await resolveApiKey(options.apiKey);
2747
+ if (!apiKey) {
2748
+ console.error(chalk.red('No API key found.')); process.exit(1);
2749
+ }
2750
+ const spinner = ora('Searching...').start();
2751
+ try {
2752
+ const client = makeHubClient(apiKey);
2753
+ const result = await client.workspaceQuery({
2754
+ workspace_id: workspaceId,
2755
+ query,
2756
+ type: options.type,
2757
+ limit: options.limit ? parseInt(options.limit) : undefined,
2758
+ });
2759
+ spinner.stop();
2760
+ if (!result.memories.length) {
2761
+ console.log(chalk.gray('No memories found matching your query.'));
2762
+ return;
2763
+ }
2764
+ console.log(chalk.gray(`Found ${result.count} memories:\n`));
2765
+ result.memories.forEach(m => {
2766
+ const sim = m.similarity ? ` ${chalk.gray(`${(m.similarity * 100).toFixed(0)}%`)}` : '';
2767
+ console.log(`${chalk.bold(`[${m.type}]`)}${sim} ${m.content.substring(0, 200)}${m.content.length > 200 ? '…' : ''}`);
2768
+ if (m.tags.length) console.log(chalk.gray(` Tags: ${m.tags.join(', ')}`));
2769
+ console.log('');
2770
+ });
2771
+ } catch (err) {
2772
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2773
+ }
2774
+ });
2775
+
2776
+ ws
2777
+ .command('store <workspaceId> <content>')
2778
+ .description('Store a memory in a workspace')
2779
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2780
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2781
+ .option('--type <type>', 'Memory type (fact|decision|preference|todo|context|reference)', 'fact')
2782
+ .option('--tags <tags>', 'Comma-separated tags')
2783
+ .action(async (workspaceId, content, options) => {
2784
+ const apiKey = await resolveApiKey(options.apiKey);
2785
+ if (!apiKey) {
2786
+ console.error(chalk.red('No API key found.')); process.exit(1);
2787
+ }
2788
+ const spinner = ora('Storing...').start();
2789
+ try {
2790
+ const client = makeHubClient(apiKey);
2791
+ const result = await client.workspaceStore({
2792
+ workspace_id: workspaceId,
2793
+ content,
2794
+ type: options.type,
2795
+ tags: options.tags ? options.tags.split(',').map((t: string) => t.trim()) : undefined,
2796
+ });
2797
+ spinner.succeed(`Memory stored (${result.memory.id})`);
2798
+ } catch (err) {
2799
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2800
+ }
2801
+ });
2802
+
2803
+ ws
2804
+ .command('memories <workspaceId>')
2805
+ .description('List all memories in a workspace (browse, non-semantic)')
2806
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2807
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2808
+ .option('--limit <n>', 'Max results', '20')
2809
+ .option('--offset <n>', 'Pagination offset', '0')
2810
+ .action(async (workspaceId, options) => {
2811
+ const apiKey = await resolveApiKey(options.apiKey);
2812
+ if (!apiKey) {
2813
+ console.error(chalk.red('No API key found.')); process.exit(1);
2814
+ }
2815
+ const spinner = ora('Loading memories...').start();
2816
+ try {
2817
+ const client = makeHubClient(apiKey);
2818
+ const result = await client.workspaceMemories({
2819
+ workspace_id: workspaceId,
2820
+ limit: parseInt(options.limit),
2821
+ offset: parseInt(options.offset),
2822
+ });
2823
+ spinner.stop();
2824
+ if (!result.memories.length) {
2825
+ console.log(chalk.gray('No memories in this workspace.'));
2826
+ return;
2827
+ }
2828
+ console.log(chalk.gray(`Showing ${result.count} of ${result.total} memories:\n`));
2829
+ result.memories.forEach(m => {
2830
+ console.log(`${chalk.bold(`[${m.type}]`)} ${m.content.substring(0, 150)}${m.content.length > 150 ? '…' : ''}`);
2831
+ if (m.tags.length) console.log(chalk.gray(` Tags: ${m.tags.join(', ')}`));
2832
+ });
2833
+ console.log('');
2834
+ } catch (err) {
2835
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2836
+ }
2837
+ });
2838
+
2839
+ ws
2840
+ .command('forget <workspaceId>')
2841
+ .description('Delete memories from a workspace by ID or semantic query')
2842
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2843
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2844
+ .option('--id <memoryId>', 'Delete a specific memory by ID')
2845
+ .option('--query <query>', 'Delete memories matching this query')
2846
+ .action(async (workspaceId, options) => {
2847
+ const apiKey = await resolveApiKey(options.apiKey);
2848
+ if (!apiKey) {
2849
+ console.error(chalk.red('No API key found.')); process.exit(1);
2850
+ }
2851
+ if (!options.id && !options.query) {
2852
+ console.error(chalk.yellow('Provide --id <memoryId> or --query <query>'));
2853
+ process.exit(1);
2854
+ }
2855
+ const spinner = ora('Deleting...').start();
2856
+ try {
2857
+ const client = makeHubClient(apiKey);
2858
+ const result = await client.workspaceForget({
2859
+ workspace_id: workspaceId,
2860
+ id: options.id,
2861
+ query: options.query,
2862
+ });
2863
+ spinner.succeed(`Deleted ${result.deleted} memory/memories`);
2864
+ } catch (err) {
2865
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2866
+ }
2867
+ });
2868
+
2869
+ const wsMembers = ws
2870
+ .command('members')
2871
+ .description('Manage workspace members');
2872
+
2873
+ wsMembers
2874
+ .command('list <workspaceId>')
2875
+ .description('List workspace members')
2876
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2877
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2878
+ .action(async (workspaceId, options) => {
2879
+ const apiKey = await resolveApiKey(options.apiKey);
2880
+ if (!apiKey) {
2881
+ console.error(chalk.red('No API key found.')); process.exit(1);
2882
+ }
2883
+ const spinner = ora('Loading members...').start();
2884
+ try {
2885
+ const client = makeHubClient(apiKey);
2886
+ const result = await client.workspaceMembersList(workspaceId);
2887
+ spinner.stop();
2888
+ result.members.forEach(m => {
2889
+ console.log(` ${m.agent_name || m.agent_id} ${chalk.gray(`(${m.role})`)}`);
2890
+ });
2891
+ } catch (err) {
2892
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2893
+ }
2894
+ });
2895
+
2896
+ wsMembers
2897
+ .command('add <workspaceId> <agentId>')
2898
+ .description('Add an agent to a workspace')
2899
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2900
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2901
+ .option('--role <role>', 'Role: read | write | admin', 'write')
2902
+ .action(async (workspaceId, agentId, options) => {
2903
+ const apiKey = await resolveApiKey(options.apiKey);
2904
+ if (!apiKey) {
2905
+ console.error(chalk.red('No API key found.')); process.exit(1);
2906
+ }
2907
+ const spinner = ora('Adding member...').start();
2908
+ try {
2909
+ const client = makeHubClient(apiKey);
2910
+ await client.workspaceMembersAdd(workspaceId, agentId, options.role);
2911
+ spinner.succeed(`Added ${agentId} with role: ${options.role}`);
2912
+ } catch (err) {
2913
+ spinner.fail(chalk.red((err as Error).message)); process.exit(1);
2914
+ }
2915
+ });
2916
+
2335
2917
  // Parse and run
2336
2918
  program.parse();