@gopherhole/cli 0.1.23 → 0.3.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 +623 -14
  2. package/package.json +2 -1
  3. package/src/index.ts +608 -14
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const chalk_1 = __importDefault(require("chalk"));
9
9
  const conf_1 = __importDefault(require("conf"));
10
10
  const inquirer_1 = __importDefault(require("inquirer"));
11
11
  const ora_1 = __importDefault(require("ora"));
12
+ const sdk_1 = require("@gopherhole/sdk");
12
13
  const config = new conf_1.default({ projectName: 'gopherhole' });
13
14
  const API_URL = 'https://gopherhole.ai/api';
14
15
  const WS_URL = 'wss://gopherhole.helixdata.workers.dev/ws';
@@ -19,7 +20,107 @@ const brand = {
19
20
  greenDark: chalk_1.default.hex('#16a34a'), // gopher-600 - emphasis
20
21
  };
21
22
  // Version
22
- const VERSION = '0.1.23';
23
+ const VERSION = '0.2.0';
24
+ // ========== API KEY RESOLUTION ==========
25
+ // Precedence: --api-key flag > GOPHERHOLE_API_KEY env var > .env file in cwd
26
+ async function resolveApiKey(flagValue) {
27
+ if (flagValue)
28
+ return flagValue;
29
+ if (process.env.GOPHERHOLE_API_KEY)
30
+ return process.env.GOPHERHOLE_API_KEY;
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)
39
+ return match[1].trim().replace(/^["']|["']$/g, '');
40
+ }
41
+ }
42
+ catch { /* ignore */ }
43
+ return null;
44
+ }
45
+ /** HTTP client for sending A2A messages */
46
+ async function resolveAgentId(flagValue) {
47
+ if (flagValue)
48
+ return flagValue;
49
+ if (process.env.GOPHERHOLE_AGENT_ID)
50
+ return process.env.GOPHERHOLE_AGENT_ID;
51
+ try {
52
+ const fs = await import('fs');
53
+ const path = await import('path');
54
+ const envPath = path.join(process.cwd(), '.env');
55
+ if (fs.existsSync(envPath)) {
56
+ const content = fs.readFileSync(envPath, 'utf-8');
57
+ const match = content.match(/^GOPHERHOLE_AGENT_ID=(.+)$/m);
58
+ if (match)
59
+ return match[1].trim().replace(/^["']|["']$/g, '');
60
+ }
61
+ }
62
+ catch { /* ignore */ }
63
+ return null;
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
+ }
73
+ function makeAgentClient(apiKey) {
74
+ return new sdk_1.A2AClient({
75
+ apiKey,
76
+ baseUrl: (process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai') + '/a2a',
77
+ });
78
+ }
79
+ /** Hub client for workspace/discovery operations (does not connect WebSocket) */
80
+ function makeHubClient(apiKey, transport) {
81
+ const apiUrl = process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai';
82
+ const hubUrl = apiUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
83
+ return new sdk_1.GopherHole({ apiKey, hubUrl, autoReconnect: false, transport: transport || 'http' });
84
+ }
85
+ /** Send a message and poll until terminal state, return response text.
86
+ * Matches the MCP client pattern: sendText → poll getTask. */
87
+ async function askAgent(client, agentId, text) {
88
+ const task = await client.sendText(agentId, text);
89
+ const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
90
+ let current = task;
91
+ const start = Date.now();
92
+ const maxWait = 60_000;
93
+ const poll = 1_000;
94
+ while (!terminalStates.includes(current.status.state)) {
95
+ if (current.status.state === 'input-required') {
96
+ throw new Error('Agent requires additional input (not supported in CLI mode)');
97
+ }
98
+ if (current.status.state === 'auth-required') {
99
+ throw new Error('Agent requires authentication — check your API key or request access');
100
+ }
101
+ if (Date.now() - start > maxWait)
102
+ throw new Error('Timed out waiting for agent response');
103
+ await new Promise(r => setTimeout(r, poll));
104
+ current = await client.getTask(current.id);
105
+ }
106
+ if (current.status.state === 'failed') {
107
+ const msg = current.status.message?.parts?.[0];
108
+ throw new Error((msg && 'text' in msg ? msg.text : null) || 'Agent task failed');
109
+ }
110
+ // Extract text from artifacts first, then history
111
+ if (current.artifacts?.length) {
112
+ const texts = current.artifacts.flatMap(a => a.parts.filter(p => 'text' in p && p.text).map(p => p.text));
113
+ if (texts.length)
114
+ return texts.join('\n');
115
+ }
116
+ if (current.history?.length) {
117
+ const last = current.history[current.history.length - 1];
118
+ const texts = last.parts.filter(p => 'text' in p && p.text).map(p => p.text);
119
+ if (texts.length)
120
+ return texts.join('\n');
121
+ }
122
+ return '';
123
+ }
23
124
  // ASCII art banner
24
125
  function showBanner(context) {
25
126
  const gopher = [
@@ -71,8 +172,9 @@ ${chalk_1.default.bold('Examples:')}
71
172
  ${chalk_1.default.bold('Documentation:')}
72
173
  https://docs.gopherhole.ai
73
174
  `)
74
- .version('0.1.0')
175
+ .version(VERSION)
75
176
  .option('-v, --verbose', 'Enable verbose output for debugging')
177
+ .option('-t, --transport <mode>', 'Transport mode: http | ws | auto (default: http)')
76
178
  .hook('preAction', (thisCommand) => {
77
179
  verbose = thisCommand.opts().verbose || false;
78
180
  if (verbose) {
@@ -957,13 +1059,24 @@ ${chalk_1.default.bold('Example:')}
957
1059
  }
958
1060
  const data = await res.json();
959
1061
  spinner.succeed('Agent created!');
960
- // Write .env file
1062
+ // Write .env file (never overwrite existing)
961
1063
  const fs = await import('fs');
962
- fs.writeFileSync('.env', `# GopherHole Configuration
963
- GOPHERHOLE_API_KEY=${data.apiKey}
964
- GOPHERHOLE_AGENT_ID=${data.agent.id}
965
- `);
966
- console.log(brand.green(' Created .env'));
1064
+ const envLines = `GOPHERHOLE_API_KEY=${data.apiKey}\nGOPHERHOLE_AGENT_ID=${data.agent.id}`;
1065
+ if (fs.existsSync('.env')) {
1066
+ const existing = fs.readFileSync('.env', 'utf-8');
1067
+ if (existing.includes('GOPHERHOLE_API_KEY')) {
1068
+ console.log(chalk_1.default.gray(' .env already has GOPHERHOLE_API_KEY, skipping'));
1069
+ console.log(chalk_1.default.gray(` Your new key: ${data.apiKey}`));
1070
+ }
1071
+ else {
1072
+ fs.appendFileSync('.env', `\n# GopherHole Configuration\n${envLines}\n`);
1073
+ console.log(brand.green('✓ Updated .env (appended GopherHole keys)'));
1074
+ }
1075
+ }
1076
+ else {
1077
+ fs.writeFileSync('.env', `# GopherHole Configuration\n${envLines}\n`);
1078
+ console.log(brand.green('✓ Created .env'));
1079
+ }
967
1080
  // Write example code
968
1081
  const exampleCode = `import { GopherHole } from '@gopherhole/sdk';
969
1082
  import 'dotenv/config';
@@ -992,8 +1105,13 @@ async function main() {
992
1105
 
993
1106
  main().catch(console.error);
994
1107
  `;
995
- fs.writeFileSync('agent.ts', exampleCode);
996
- console.log(brand.green('✓ Created agent.ts'));
1108
+ if (!fs.existsSync('agent.ts')) {
1109
+ fs.writeFileSync('agent.ts', exampleCode);
1110
+ console.log(brand.green('✓ Created agent.ts'));
1111
+ }
1112
+ else {
1113
+ console.log(chalk_1.default.gray('↳ agent.ts already exists, skipping'));
1114
+ }
997
1115
  // Create package.json if it doesn't exist
998
1116
  if (!fs.existsSync('package.json')) {
999
1117
  const pkg = {
@@ -1016,6 +1134,71 @@ main().catch(console.error);
1016
1134
  fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
1017
1135
  console.log(brand.green('✓ Created package.json'));
1018
1136
  }
1137
+ // Append GopherHole section to AGENTS.md (idempotent)
1138
+ const agentsMdPath = 'AGENTS.md';
1139
+ const gopherSection = `
1140
+ ## GopherHole
1141
+
1142
+ This project is connected to the GopherHole agent hub.
1143
+
1144
+ ### Auth
1145
+
1146
+ The \`gopher\` CLI resolves your API key in this order:
1147
+ 1. \`--api-key <key>\` flag on any command
1148
+ 2. \`GOPHERHOLE_API_KEY\` environment variable
1149
+ 3. \`GOPHERHOLE_API_KEY\` in a \`.env\` file in the current directory
1150
+
1151
+ Never hard-code API keys. Use the env var or \`--api-key\` flag.
1152
+
1153
+ ### Agent identity
1154
+
1155
+ - Agent ID: \`${data.agent.id}\`
1156
+ - Name: ${agentName}
1157
+
1158
+ ### Commands
1159
+
1160
+ \`\`\`
1161
+ # Send a message to an agent
1162
+ gopher message <agent-id> "<text>"
1163
+ gopher message <agent-id> "<text>" --api-key <key>
1164
+
1165
+ # Agent memory (via memory agent)
1166
+ gopher memory recall "<query>"
1167
+ gopher memory store "<content>" [--tags tag1,tag2]
1168
+ gopher memory list [--limit 20]
1169
+ gopher memory forget "<query>" --confirm
1170
+
1171
+ # Shared workspaces
1172
+ gopher workspace list
1173
+ gopher workspace create <name>
1174
+ gopher workspace query <workspace-id> "<query>"
1175
+ gopher workspace store <workspace-id> "<content>" [--type fact|decision|preference|todo|context|reference]
1176
+ gopher workspace memories <workspace-id>
1177
+ gopher workspace forget <workspace-id> --id <memory-id>
1178
+ gopher workspace forget <workspace-id> --query "<query>"
1179
+ gopher workspace members list <workspace-id>
1180
+ gopher workspace members add <workspace-id> <agent-id> [--role read|write|admin]
1181
+
1182
+ # Discover agents (--api-key enables agent-mode auth)
1183
+ gopher discover search "<query>" --api-key <key>
1184
+ gopher discover search --category <category> --tag <tag>
1185
+ gopher discover nearby --lat <lat> --lng <lng> [--radius 10]
1186
+ gopher discover info <agent-id>
1187
+ gopher discover info <agent-id>
1188
+
1189
+ # Auth check
1190
+ gopher whoami
1191
+ \`\`\`
1192
+ `;
1193
+ const agentsMdExists = fs.existsSync(agentsMdPath);
1194
+ const existingContent = agentsMdExists ? fs.readFileSync(agentsMdPath, 'utf-8') : '';
1195
+ if (existingContent.includes('## GopherHole')) {
1196
+ console.log(chalk_1.default.gray('↳ AGENTS.md already has GopherHole section, skipping'));
1197
+ }
1198
+ else {
1199
+ fs.writeFileSync(agentsMdPath, existingContent + gopherSection);
1200
+ console.log(brand.green(agentsMdExists ? '✓ Updated AGENTS.md' : '✓ Created AGENTS.md'));
1201
+ }
1019
1202
  console.log(chalk_1.default.bold('\n🎉 Project initialized!\n'));
1020
1203
  console.log(chalk_1.default.bold('Next steps:'));
1021
1204
  console.log(chalk_1.default.cyan(' npm install'));
@@ -1113,6 +1296,7 @@ ${chalk_1.default.bold('Examples:')}
1113
1296
  .option('-l, --limit <limit>', 'Number of results (max 50)', '10')
1114
1297
  .option('-o, --offset <offset>', 'Pagination offset')
1115
1298
  .option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
1299
+ .option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
1116
1300
  .action(async (query, options) => {
1117
1301
  const spinner = (0, ora_1.default)('Searching agents...').start();
1118
1302
  try {
@@ -1142,10 +1326,15 @@ ${chalk_1.default.bold('Examples:')}
1142
1326
  if (options.scope)
1143
1327
  params.set('scope', options.scope);
1144
1328
  log('GET /discover/agents?' + params.toString());
1329
+ const apiKey = await resolveApiKey(options.apiKey);
1145
1330
  const sessionId = config.get('sessionId');
1146
1331
  const headers = {};
1147
- if (sessionId)
1332
+ if (apiKey) {
1333
+ headers['Authorization'] = `Bearer ${apiKey}`;
1334
+ }
1335
+ else if (sessionId) {
1148
1336
  headers['X-Session-ID'] = sessionId;
1337
+ }
1149
1338
  const res = await fetch(`${API_URL}/discover/agents?${params}`, { headers });
1150
1339
  const data = await res.json();
1151
1340
  spinner.stop();
@@ -1260,9 +1449,11 @@ ${chalk_1.default.bold('Examples:')}
1260
1449
  .option('-t, --tag <tag>', 'Filter by tag')
1261
1450
  .option('-c, --category <category>', 'Filter by category')
1262
1451
  .option('-l, --limit <limit>', 'Number of results (max 50)', '20')
1452
+ .option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
1263
1453
  .action(async (options) => {
1264
1454
  const spinner = (0, ora_1.default)('Searching nearby agents...').start();
1265
1455
  try {
1456
+ const apiKey = await resolveApiKey(options.apiKey);
1266
1457
  const sessionId = config.get('sessionId');
1267
1458
  const params = new URLSearchParams();
1268
1459
  params.set('lat', options.lat);
@@ -1275,7 +1466,10 @@ ${chalk_1.default.bold('Examples:')}
1275
1466
  params.set('limit', options.limit);
1276
1467
  log('GET /discover/agents/nearby?' + params.toString());
1277
1468
  const headers = {};
1278
- if (sessionId) {
1469
+ if (apiKey) {
1470
+ headers['Authorization'] = `Bearer ${apiKey}`;
1471
+ }
1472
+ else if (sessionId) {
1279
1473
  headers['X-Session-ID'] = sessionId;
1280
1474
  }
1281
1475
  const res = await fetch(`${API_URL}/discover/agents/nearby?${params}`, { headers });
@@ -1311,14 +1505,20 @@ discover
1311
1505
  ${chalk_1.default.bold('Example:')}
1312
1506
  $ gopherhole discover info agent-abc123
1313
1507
  `)
1314
- .action(async (agentId) => {
1508
+ .option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
1509
+ .action(async (agentId, options) => {
1315
1510
  const spinner = (0, ora_1.default)('Fetching agent info...').start();
1316
1511
  try {
1317
1512
  log('GET /discover/agents/' + agentId);
1513
+ const apiKey = await resolveApiKey(options.apiKey);
1318
1514
  const sessionId = config.get('sessionId');
1319
1515
  const headers = {};
1320
- if (sessionId)
1516
+ if (apiKey) {
1517
+ headers['Authorization'] = `Bearer ${apiKey}`;
1518
+ }
1519
+ else if (sessionId) {
1321
1520
  headers['X-Session-ID'] = sessionId;
1521
+ }
1322
1522
  const res = await fetch(`${API_URL}/discover/agents/${agentId}`, { headers });
1323
1523
  if (!res.ok) {
1324
1524
  throw new Error('Agent not found');
@@ -2102,5 +2302,414 @@ program
2102
2302
  console.log(chalk_1.default.gray(' Status: https://status.gopherhole.ai'));
2103
2303
  console.log('');
2104
2304
  });
2305
+ // ========== MESSAGE COMMAND (agent-to-agent, Bearer auth) ==========
2306
+ program
2307
+ .command('message <agentId> <text>')
2308
+ .description(`Send a message to an agent using your API key (agent-to-agent)
2309
+
2310
+ ${chalk_1.default.bold('Auth (in order of precedence):')}
2311
+ --api-key <key> > GOPHERHOLE_API_KEY env > .env file
2312
+ --agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
2313
+
2314
+ ${chalk_1.default.bold('Examples:')}
2315
+ $ gopher message agent-memory-official "What do you remember about me?"
2316
+ $ gopher message echo-agent "Hello!" --api-key gph_xxx
2317
+ $ GOPHERHOLE_API_KEY=gph_xxx gopher message search-agent "latest AI news"
2318
+ `)
2319
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2320
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2321
+ .action(async (agentId, text, options) => {
2322
+ const apiKey = await resolveApiKey(options.apiKey);
2323
+ if (!apiKey) {
2324
+ console.error(chalk_1.default.red('No API key found.'));
2325
+ console.error(chalk_1.default.gray('Set GOPHERHOLE_API_KEY, pass --api-key, or run gopher init to create a .env'));
2326
+ process.exit(1);
2327
+ }
2328
+ log(`Transport: http (A2AClient is HTTP-only)`);
2329
+ const spinner = (0, ora_1.default)(`Messaging ${agentId}...`).start();
2330
+ try {
2331
+ const client = makeAgentClient(apiKey);
2332
+ const response = await askAgent(client, agentId, text);
2333
+ spinner.stop();
2334
+ console.log(response || chalk_1.default.gray('(no response)'));
2335
+ }
2336
+ catch (err) {
2337
+ spinner.fail(chalk_1.default.red(err.message));
2338
+ process.exit(1);
2339
+ }
2340
+ });
2341
+ // ========== MEMORY COMMANDS (agent-to-agent via memory agent) ==========
2342
+ const MEMORY_AGENT = process.env.GOPHERHOLE_MEMORY_AGENT || 'agent-memory-official';
2343
+ const memory = program
2344
+ .command('memory')
2345
+ .description(`Manage agent memory via the GopherHole memory agent
2346
+
2347
+ ${chalk_1.default.bold('Auth (in order of precedence):')}
2348
+ --api-key <key> > GOPHERHOLE_API_KEY env > .env file
2349
+ --agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
2350
+ `);
2351
+ memory
2352
+ .command('recall <query>')
2353
+ .description('Search your agent memories semantically')
2354
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2355
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2356
+ .option('--limit <n>', 'Max results', '10')
2357
+ .action(async (query, options) => {
2358
+ const apiKey = await resolveApiKey(options.apiKey);
2359
+ if (!apiKey) {
2360
+ console.error(chalk_1.default.red('No API key found.'));
2361
+ process.exit(1);
2362
+ }
2363
+ const spinner = (0, ora_1.default)('Recalling...').start();
2364
+ try {
2365
+ const client = makeAgentClient(apiKey);
2366
+ const msg = `Search memories for: ${query} (limit: ${options.limit})`;
2367
+ const response = await askAgent(client, MEMORY_AGENT, msg);
2368
+ spinner.stop();
2369
+ console.log(response || chalk_1.default.gray('No memories found'));
2370
+ }
2371
+ catch (err) {
2372
+ spinner.fail(chalk_1.default.red(err.message));
2373
+ process.exit(1);
2374
+ }
2375
+ });
2376
+ memory
2377
+ .command('store <content>')
2378
+ .description('Store a new memory')
2379
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2380
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2381
+ .option('--tags <tags>', 'Comma-separated tags')
2382
+ .action(async (content, options) => {
2383
+ const apiKey = await resolveApiKey(options.apiKey);
2384
+ if (!apiKey) {
2385
+ console.error(chalk_1.default.red('No API key found.'));
2386
+ process.exit(1);
2387
+ }
2388
+ const spinner = (0, ora_1.default)('Storing memory...').start();
2389
+ try {
2390
+ const client = makeAgentClient(apiKey);
2391
+ const tags = options.tags ? `\n\nTags: ${options.tags}` : '';
2392
+ const response = await askAgent(client, MEMORY_AGENT, `Remember this:\n\n${content}${tags}`);
2393
+ spinner.stop();
2394
+ console.log(response || chalk_1.default.green('Memory stored'));
2395
+ }
2396
+ catch (err) {
2397
+ spinner.fail(chalk_1.default.red(err.message));
2398
+ process.exit(1);
2399
+ }
2400
+ });
2401
+ memory
2402
+ .command('forget <query>')
2403
+ .description('Delete memories matching a query')
2404
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2405
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2406
+ .option('--confirm', 'Required to confirm deletion')
2407
+ .action(async (query, options) => {
2408
+ const apiKey = await resolveApiKey(options.apiKey);
2409
+ if (!apiKey) {
2410
+ console.error(chalk_1.default.red('No API key found.'));
2411
+ process.exit(1);
2412
+ }
2413
+ if (!options.confirm) {
2414
+ console.error(chalk_1.default.yellow('Add --confirm to delete memories'));
2415
+ process.exit(1);
2416
+ }
2417
+ const spinner = (0, ora_1.default)('Forgetting...').start();
2418
+ try {
2419
+ const client = makeAgentClient(apiKey);
2420
+ const response = await askAgent(client, MEMORY_AGENT, `Forget memories matching: ${query}`);
2421
+ spinner.stop();
2422
+ console.log(response || chalk_1.default.green('Done'));
2423
+ }
2424
+ catch (err) {
2425
+ spinner.fail(chalk_1.default.red(err.message));
2426
+ process.exit(1);
2427
+ }
2428
+ });
2429
+ memory
2430
+ .command('list')
2431
+ .description('List recent memories')
2432
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2433
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2434
+ .option('--limit <n>', 'Max results', '20')
2435
+ .option('--offset <n>', 'Pagination offset', '0')
2436
+ .action(async (options) => {
2437
+ const apiKey = await resolveApiKey(options.apiKey);
2438
+ if (!apiKey) {
2439
+ console.error(chalk_1.default.red('No API key found.'));
2440
+ process.exit(1);
2441
+ }
2442
+ const spinner = (0, ora_1.default)('Listing memories...').start();
2443
+ try {
2444
+ const client = makeAgentClient(apiKey);
2445
+ const msg = `List my recent memories (limit: ${options.limit}, offset: ${options.offset})`;
2446
+ const response = await askAgent(client, MEMORY_AGENT, msg);
2447
+ spinner.stop();
2448
+ console.log(response || chalk_1.default.gray('No memories found'));
2449
+ }
2450
+ catch (err) {
2451
+ spinner.fail(chalk_1.default.red(err.message));
2452
+ process.exit(1);
2453
+ }
2454
+ });
2455
+ // ========== WORKSPACE COMMANDS (direct RPC, Bearer auth) ==========
2456
+ const ws = program
2457
+ .command('workspace')
2458
+ .alias('ws')
2459
+ .description(`Manage shared workspaces for multi-agent collaboration
2460
+
2461
+ ${chalk_1.default.bold('Auth (in order of precedence):')}
2462
+ --api-key <key> > GOPHERHOLE_API_KEY env > .env file
2463
+ --agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
2464
+ `);
2465
+ ws
2466
+ .command('list')
2467
+ .description('List workspaces you are a member of')
2468
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2469
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2470
+ .action(async (options) => {
2471
+ const apiKey = await resolveApiKey(options.apiKey);
2472
+ if (!apiKey) {
2473
+ console.error(chalk_1.default.red('No API key found.'));
2474
+ process.exit(1);
2475
+ }
2476
+ const spinner = (0, ora_1.default)('Loading workspaces...').start();
2477
+ try {
2478
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2479
+ const result = await client.workspaceList();
2480
+ spinner.stop();
2481
+ if (!result.workspaces.length) {
2482
+ console.log(chalk_1.default.gray('No workspaces yet. Create one with: gopher workspace create <name>'));
2483
+ return;
2484
+ }
2485
+ result.workspaces.forEach(w => {
2486
+ console.log(`\n${chalk_1.default.bold(w.name)} ${chalk_1.default.gray(`(${w.id})`)}`);
2487
+ if (w.description)
2488
+ console.log(chalk_1.default.gray(` ${w.description}`));
2489
+ console.log(chalk_1.default.gray(` Role: ${w.my_role || '?'} | Members: ${w.member_count || '?'} | Memories: ${w.memory_count || '?'}`));
2490
+ });
2491
+ console.log('');
2492
+ }
2493
+ catch (err) {
2494
+ spinner.fail(chalk_1.default.red(err.message));
2495
+ process.exit(1);
2496
+ }
2497
+ });
2498
+ ws
2499
+ .command('create <name>')
2500
+ .description('Create a new workspace')
2501
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2502
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2503
+ .option('--description <text>', 'Workspace description')
2504
+ .action(async (name, options) => {
2505
+ const apiKey = await resolveApiKey(options.apiKey);
2506
+ if (!apiKey) {
2507
+ console.error(chalk_1.default.red('No API key found.'));
2508
+ process.exit(1);
2509
+ }
2510
+ const spinner = (0, ora_1.default)('Creating workspace...').start();
2511
+ try {
2512
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2513
+ const result = await client.workspaceCreate(name, options.description);
2514
+ spinner.succeed(`Workspace created: ${chalk_1.default.bold(result.workspace.name)}`);
2515
+ console.log(chalk_1.default.gray(` ID: ${result.workspace.id}`));
2516
+ }
2517
+ catch (err) {
2518
+ spinner.fail(chalk_1.default.red(err.message));
2519
+ process.exit(1);
2520
+ }
2521
+ });
2522
+ ws
2523
+ .command('query <workspaceId> <query>')
2524
+ .description('Search workspace memories semantically')
2525
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2526
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2527
+ .option('--type <type>', 'Filter by memory type (fact|decision|preference|todo|context|reference)')
2528
+ .option('--limit <n>', 'Max results', '10')
2529
+ .action(async (workspaceId, query, options) => {
2530
+ const apiKey = await resolveApiKey(options.apiKey);
2531
+ if (!apiKey) {
2532
+ console.error(chalk_1.default.red('No API key found.'));
2533
+ process.exit(1);
2534
+ }
2535
+ const spinner = (0, ora_1.default)('Searching...').start();
2536
+ try {
2537
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2538
+ const result = await client.workspaceQuery({
2539
+ workspace_id: workspaceId,
2540
+ query,
2541
+ type: options.type,
2542
+ limit: options.limit ? parseInt(options.limit) : undefined,
2543
+ });
2544
+ spinner.stop();
2545
+ if (!result.memories.length) {
2546
+ console.log(chalk_1.default.gray('No memories found matching your query.'));
2547
+ return;
2548
+ }
2549
+ console.log(chalk_1.default.gray(`Found ${result.count} memories:\n`));
2550
+ result.memories.forEach(m => {
2551
+ const sim = m.similarity ? ` ${chalk_1.default.gray(`${(m.similarity * 100).toFixed(0)}%`)}` : '';
2552
+ console.log(`${chalk_1.default.bold(`[${m.type}]`)}${sim} ${m.content.substring(0, 200)}${m.content.length > 200 ? '…' : ''}`);
2553
+ if (m.tags.length)
2554
+ console.log(chalk_1.default.gray(` Tags: ${m.tags.join(', ')}`));
2555
+ console.log('');
2556
+ });
2557
+ }
2558
+ catch (err) {
2559
+ spinner.fail(chalk_1.default.red(err.message));
2560
+ process.exit(1);
2561
+ }
2562
+ });
2563
+ ws
2564
+ .command('store <workspaceId> <content>')
2565
+ .description('Store a memory in a workspace')
2566
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2567
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2568
+ .option('--type <type>', 'Memory type (fact|decision|preference|todo|context|reference)', 'fact')
2569
+ .option('--tags <tags>', 'Comma-separated tags')
2570
+ .action(async (workspaceId, content, options) => {
2571
+ const apiKey = await resolveApiKey(options.apiKey);
2572
+ if (!apiKey) {
2573
+ console.error(chalk_1.default.red('No API key found.'));
2574
+ process.exit(1);
2575
+ }
2576
+ const spinner = (0, ora_1.default)('Storing...').start();
2577
+ try {
2578
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2579
+ const result = await client.workspaceStore({
2580
+ workspace_id: workspaceId,
2581
+ content,
2582
+ type: options.type,
2583
+ tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : undefined,
2584
+ });
2585
+ spinner.succeed(`Memory stored (${result.memory.id})`);
2586
+ }
2587
+ catch (err) {
2588
+ spinner.fail(chalk_1.default.red(err.message));
2589
+ process.exit(1);
2590
+ }
2591
+ });
2592
+ ws
2593
+ .command('memories <workspaceId>')
2594
+ .description('List all memories in a workspace (browse, non-semantic)')
2595
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2596
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2597
+ .option('--limit <n>', 'Max results', '20')
2598
+ .option('--offset <n>', 'Pagination offset', '0')
2599
+ .action(async (workspaceId, options) => {
2600
+ const apiKey = await resolveApiKey(options.apiKey);
2601
+ if (!apiKey) {
2602
+ console.error(chalk_1.default.red('No API key found.'));
2603
+ process.exit(1);
2604
+ }
2605
+ const spinner = (0, ora_1.default)('Loading memories...').start();
2606
+ try {
2607
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2608
+ const result = await client.workspaceMemories({
2609
+ workspace_id: workspaceId,
2610
+ limit: parseInt(options.limit),
2611
+ offset: parseInt(options.offset),
2612
+ });
2613
+ spinner.stop();
2614
+ if (!result.memories.length) {
2615
+ console.log(chalk_1.default.gray('No memories in this workspace.'));
2616
+ return;
2617
+ }
2618
+ console.log(chalk_1.default.gray(`Showing ${result.count} of ${result.total} memories:\n`));
2619
+ result.memories.forEach(m => {
2620
+ console.log(`${chalk_1.default.bold(`[${m.type}]`)} ${m.content.substring(0, 150)}${m.content.length > 150 ? '…' : ''}`);
2621
+ if (m.tags.length)
2622
+ console.log(chalk_1.default.gray(` Tags: ${m.tags.join(', ')}`));
2623
+ });
2624
+ console.log('');
2625
+ }
2626
+ catch (err) {
2627
+ spinner.fail(chalk_1.default.red(err.message));
2628
+ process.exit(1);
2629
+ }
2630
+ });
2631
+ ws
2632
+ .command('forget <workspaceId>')
2633
+ .description('Delete memories from a workspace by ID or semantic query')
2634
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2635
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2636
+ .option('--id <memoryId>', 'Delete a specific memory by ID')
2637
+ .option('--query <query>', 'Delete memories matching this query')
2638
+ .action(async (workspaceId, options) => {
2639
+ const apiKey = await resolveApiKey(options.apiKey);
2640
+ if (!apiKey) {
2641
+ console.error(chalk_1.default.red('No API key found.'));
2642
+ process.exit(1);
2643
+ }
2644
+ if (!options.id && !options.query) {
2645
+ console.error(chalk_1.default.yellow('Provide --id <memoryId> or --query <query>'));
2646
+ process.exit(1);
2647
+ }
2648
+ const spinner = (0, ora_1.default)('Deleting...').start();
2649
+ try {
2650
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2651
+ const result = await client.workspaceForget({
2652
+ workspace_id: workspaceId,
2653
+ id: options.id,
2654
+ query: options.query,
2655
+ });
2656
+ spinner.succeed(`Deleted ${result.deleted} memory/memories`);
2657
+ }
2658
+ catch (err) {
2659
+ spinner.fail(chalk_1.default.red(err.message));
2660
+ process.exit(1);
2661
+ }
2662
+ });
2663
+ const wsMembers = ws
2664
+ .command('members')
2665
+ .description('Manage workspace members');
2666
+ wsMembers
2667
+ .command('list <workspaceId>')
2668
+ .description('List workspace members')
2669
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2670
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2671
+ .action(async (workspaceId, options) => {
2672
+ const apiKey = await resolveApiKey(options.apiKey);
2673
+ if (!apiKey) {
2674
+ console.error(chalk_1.default.red('No API key found.'));
2675
+ process.exit(1);
2676
+ }
2677
+ const spinner = (0, ora_1.default)('Loading members...').start();
2678
+ try {
2679
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2680
+ const result = await client.workspaceMembersList(workspaceId);
2681
+ spinner.stop();
2682
+ result.members.forEach(m => {
2683
+ console.log(` ${m.agent_name || m.agent_id} ${chalk_1.default.gray(`(${m.role})`)}`);
2684
+ });
2685
+ }
2686
+ catch (err) {
2687
+ spinner.fail(chalk_1.default.red(err.message));
2688
+ process.exit(1);
2689
+ }
2690
+ });
2691
+ wsMembers
2692
+ .command('add <workspaceId> <agentId>')
2693
+ .description('Add an agent to a workspace')
2694
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2695
+ .option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
2696
+ .option('--role <role>', 'Role: read | write | admin', 'write')
2697
+ .action(async (workspaceId, agentId, options) => {
2698
+ const apiKey = await resolveApiKey(options.apiKey);
2699
+ if (!apiKey) {
2700
+ console.error(chalk_1.default.red('No API key found.'));
2701
+ process.exit(1);
2702
+ }
2703
+ const spinner = (0, ora_1.default)('Adding member...').start();
2704
+ try {
2705
+ const client = makeHubClient(apiKey, resolveTransport(program.opts().transport));
2706
+ await client.workspaceMembersAdd(workspaceId, agentId, options.role);
2707
+ spinner.succeed(`Added ${agentId} with role: ${options.role}`);
2708
+ }
2709
+ catch (err) {
2710
+ spinner.fail(chalk_1.default.red(err.message));
2711
+ process.exit(1);
2712
+ }
2713
+ });
2105
2714
  // Parse and run
2106
2715
  program.parse();