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