@gopherhole/cli 0.1.22 → 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.
- package/dist/index.js +616 -17
- package/package.json +2 -1
- package/src/index.ts +599 -17
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,99 @@ const brand = {
|
|
|
19
20
|
greenDark: chalk_1.default.hex('#16a34a'), // gopher-600 - emphasis
|
|
20
21
|
};
|
|
21
22
|
// Version
|
|
22
|
-
const VERSION = '0.
|
|
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 makeAgentClient(apiKey) {
|
|
66
|
+
return new sdk_1.A2AClient({
|
|
67
|
+
apiKey,
|
|
68
|
+
baseUrl: (process.env.GOPHERHOLE_API_URL || 'https://hub.gopherhole.ai') + '/a2a',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Hub client for workspace/discovery operations (does not connect WebSocket) */
|
|
72
|
+
function makeHubClient(apiKey) {
|
|
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 sdk_1.GopherHole({ apiKey, hubUrl, autoReconnect: false });
|
|
76
|
+
}
|
|
77
|
+
/** Send a message and poll until terminal state, return response text.
|
|
78
|
+
* Matches the MCP client pattern: sendText → poll getTask. */
|
|
79
|
+
async function askAgent(client, agentId, text) {
|
|
80
|
+
const task = await client.sendText(agentId, text);
|
|
81
|
+
const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
|
|
82
|
+
let current = task;
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
const maxWait = 60_000;
|
|
85
|
+
const poll = 1_000;
|
|
86
|
+
while (!terminalStates.includes(current.status.state)) {
|
|
87
|
+
if (current.status.state === 'input-required') {
|
|
88
|
+
throw new Error('Agent requires additional input (not supported in CLI mode)');
|
|
89
|
+
}
|
|
90
|
+
if (current.status.state === 'auth-required') {
|
|
91
|
+
throw new Error('Agent requires authentication — check your API key or request access');
|
|
92
|
+
}
|
|
93
|
+
if (Date.now() - start > maxWait)
|
|
94
|
+
throw new Error('Timed out waiting for agent response');
|
|
95
|
+
await new Promise(r => setTimeout(r, poll));
|
|
96
|
+
current = await client.getTask(current.id);
|
|
97
|
+
}
|
|
98
|
+
if (current.status.state === 'failed') {
|
|
99
|
+
const msg = current.status.message?.parts?.[0];
|
|
100
|
+
throw new Error((msg && 'text' in msg ? msg.text : null) || 'Agent task failed');
|
|
101
|
+
}
|
|
102
|
+
// Extract text from artifacts first, then history
|
|
103
|
+
if (current.artifacts?.length) {
|
|
104
|
+
const texts = current.artifacts.flatMap(a => a.parts.filter(p => 'text' in p && p.text).map(p => p.text));
|
|
105
|
+
if (texts.length)
|
|
106
|
+
return texts.join('\n');
|
|
107
|
+
}
|
|
108
|
+
if (current.history?.length) {
|
|
109
|
+
const last = current.history[current.history.length - 1];
|
|
110
|
+
const texts = last.parts.filter(p => 'text' in p && p.text).map(p => p.text);
|
|
111
|
+
if (texts.length)
|
|
112
|
+
return texts.join('\n');
|
|
113
|
+
}
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
23
116
|
// ASCII art banner
|
|
24
117
|
function showBanner(context) {
|
|
25
118
|
const gopher = [
|
|
@@ -71,7 +164,7 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
71
164
|
${chalk_1.default.bold('Documentation:')}
|
|
72
165
|
https://docs.gopherhole.ai
|
|
73
166
|
`)
|
|
74
|
-
.version(
|
|
167
|
+
.version(VERSION)
|
|
75
168
|
.option('-v, --verbose', 'Enable verbose output for debugging')
|
|
76
169
|
.hook('preAction', (thisCommand) => {
|
|
77
170
|
verbose = thisCommand.opts().verbose || false;
|
|
@@ -285,7 +378,7 @@ ${chalk_1.default.bold('Example:')}
|
|
|
285
378
|
-H "Content-Type: application/json" \\
|
|
286
379
|
-d '{
|
|
287
380
|
"jsonrpc": "2.0",
|
|
288
|
-
"method": "
|
|
381
|
+
"method": "SendMessage",
|
|
289
382
|
"params": {
|
|
290
383
|
"message": { "role": "user", "parts": [{ "kind": "text", "text": "Hello!" }] },
|
|
291
384
|
"configuration": { "agentId": "echo" }
|
|
@@ -957,13 +1050,24 @@ ${chalk_1.default.bold('Example:')}
|
|
|
957
1050
|
}
|
|
958
1051
|
const data = await res.json();
|
|
959
1052
|
spinner.succeed('Agent created!');
|
|
960
|
-
// Write .env file
|
|
1053
|
+
// Write .env file (never overwrite existing)
|
|
961
1054
|
const fs = await import('fs');
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1055
|
+
const envLines = `GOPHERHOLE_API_KEY=${data.apiKey}\nGOPHERHOLE_AGENT_ID=${data.agent.id}`;
|
|
1056
|
+
if (fs.existsSync('.env')) {
|
|
1057
|
+
const existing = fs.readFileSync('.env', 'utf-8');
|
|
1058
|
+
if (existing.includes('GOPHERHOLE_API_KEY')) {
|
|
1059
|
+
console.log(chalk_1.default.gray('↳ .env already has GOPHERHOLE_API_KEY, skipping'));
|
|
1060
|
+
console.log(chalk_1.default.gray(` Your new key: ${data.apiKey}`));
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
fs.appendFileSync('.env', `\n# GopherHole Configuration\n${envLines}\n`);
|
|
1064
|
+
console.log(brand.green('✓ Updated .env (appended GopherHole keys)'));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
fs.writeFileSync('.env', `# GopherHole Configuration\n${envLines}\n`);
|
|
1069
|
+
console.log(brand.green('✓ Created .env'));
|
|
1070
|
+
}
|
|
967
1071
|
// Write example code
|
|
968
1072
|
const exampleCode = `import { GopherHole } from '@gopherhole/sdk';
|
|
969
1073
|
import 'dotenv/config';
|
|
@@ -992,8 +1096,13 @@ async function main() {
|
|
|
992
1096
|
|
|
993
1097
|
main().catch(console.error);
|
|
994
1098
|
`;
|
|
995
|
-
fs.
|
|
996
|
-
|
|
1099
|
+
if (!fs.existsSync('agent.ts')) {
|
|
1100
|
+
fs.writeFileSync('agent.ts', exampleCode);
|
|
1101
|
+
console.log(brand.green('✓ Created agent.ts'));
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
console.log(chalk_1.default.gray('↳ agent.ts already exists, skipping'));
|
|
1105
|
+
}
|
|
997
1106
|
// Create package.json if it doesn't exist
|
|
998
1107
|
if (!fs.existsSync('package.json')) {
|
|
999
1108
|
const pkg = {
|
|
@@ -1016,6 +1125,71 @@ main().catch(console.error);
|
|
|
1016
1125
|
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
|
1017
1126
|
console.log(brand.green('✓ Created package.json'));
|
|
1018
1127
|
}
|
|
1128
|
+
// Append GopherHole section to AGENTS.md (idempotent)
|
|
1129
|
+
const agentsMdPath = 'AGENTS.md';
|
|
1130
|
+
const gopherSection = `
|
|
1131
|
+
## GopherHole
|
|
1132
|
+
|
|
1133
|
+
This project is connected to the GopherHole agent hub.
|
|
1134
|
+
|
|
1135
|
+
### Auth
|
|
1136
|
+
|
|
1137
|
+
The \`gopher\` CLI resolves your API key in this order:
|
|
1138
|
+
1. \`--api-key <key>\` flag on any command
|
|
1139
|
+
2. \`GOPHERHOLE_API_KEY\` environment variable
|
|
1140
|
+
3. \`GOPHERHOLE_API_KEY\` in a \`.env\` file in the current directory
|
|
1141
|
+
|
|
1142
|
+
Never hard-code API keys. Use the env var or \`--api-key\` flag.
|
|
1143
|
+
|
|
1144
|
+
### Agent identity
|
|
1145
|
+
|
|
1146
|
+
- Agent ID: \`${data.agent.id}\`
|
|
1147
|
+
- Name: ${agentName}
|
|
1148
|
+
|
|
1149
|
+
### Commands
|
|
1150
|
+
|
|
1151
|
+
\`\`\`
|
|
1152
|
+
# Send a message to an agent
|
|
1153
|
+
gopher message <agent-id> "<text>"
|
|
1154
|
+
gopher message <agent-id> "<text>" --api-key <key>
|
|
1155
|
+
|
|
1156
|
+
# Agent memory (via memory agent)
|
|
1157
|
+
gopher memory recall "<query>"
|
|
1158
|
+
gopher memory store "<content>" [--tags tag1,tag2]
|
|
1159
|
+
gopher memory list [--limit 20]
|
|
1160
|
+
gopher memory forget "<query>" --confirm
|
|
1161
|
+
|
|
1162
|
+
# Shared workspaces
|
|
1163
|
+
gopher workspace list
|
|
1164
|
+
gopher workspace create <name>
|
|
1165
|
+
gopher workspace query <workspace-id> "<query>"
|
|
1166
|
+
gopher workspace store <workspace-id> "<content>" [--type fact|decision|preference|todo|context|reference]
|
|
1167
|
+
gopher workspace memories <workspace-id>
|
|
1168
|
+
gopher workspace forget <workspace-id> --id <memory-id>
|
|
1169
|
+
gopher workspace forget <workspace-id> --query "<query>"
|
|
1170
|
+
gopher workspace members list <workspace-id>
|
|
1171
|
+
gopher workspace members add <workspace-id> <agent-id> [--role read|write|admin]
|
|
1172
|
+
|
|
1173
|
+
# Discover agents (--api-key enables agent-mode auth)
|
|
1174
|
+
gopher discover search "<query>" --api-key <key>
|
|
1175
|
+
gopher discover search --category <category> --tag <tag>
|
|
1176
|
+
gopher discover nearby --lat <lat> --lng <lng> [--radius 10]
|
|
1177
|
+
gopher discover info <agent-id>
|
|
1178
|
+
gopher discover info <agent-id>
|
|
1179
|
+
|
|
1180
|
+
# Auth check
|
|
1181
|
+
gopher whoami
|
|
1182
|
+
\`\`\`
|
|
1183
|
+
`;
|
|
1184
|
+
const agentsMdExists = fs.existsSync(agentsMdPath);
|
|
1185
|
+
const existingContent = agentsMdExists ? fs.readFileSync(agentsMdPath, 'utf-8') : '';
|
|
1186
|
+
if (existingContent.includes('## GopherHole')) {
|
|
1187
|
+
console.log(chalk_1.default.gray('↳ AGENTS.md already has GopherHole section, skipping'));
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
fs.writeFileSync(agentsMdPath, existingContent + gopherSection);
|
|
1191
|
+
console.log(brand.green(agentsMdExists ? '✓ Updated AGENTS.md' : '✓ Created AGENTS.md'));
|
|
1192
|
+
}
|
|
1019
1193
|
console.log(chalk_1.default.bold('\n🎉 Project initialized!\n'));
|
|
1020
1194
|
console.log(chalk_1.default.bold('Next steps:'));
|
|
1021
1195
|
console.log(chalk_1.default.cyan(' npm install'));
|
|
@@ -1042,7 +1216,7 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1042
1216
|
process.exit(1);
|
|
1043
1217
|
}
|
|
1044
1218
|
const spinner = (0, ora_1.default)(`Sending to ${agentId}...`).start();
|
|
1045
|
-
log('POST /a2a
|
|
1219
|
+
log('POST /a2a SendMessage', { to: agentId, message });
|
|
1046
1220
|
try {
|
|
1047
1221
|
const res = await fetch(`${API_URL}/../a2a`, {
|
|
1048
1222
|
method: 'POST',
|
|
@@ -1052,7 +1226,7 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1052
1226
|
},
|
|
1053
1227
|
body: JSON.stringify({
|
|
1054
1228
|
jsonrpc: '2.0',
|
|
1055
|
-
method: '
|
|
1229
|
+
method: 'SendMessage',
|
|
1056
1230
|
params: {
|
|
1057
1231
|
message: { role: 'user', parts: [{ kind: 'text', text: message }] },
|
|
1058
1232
|
configuration: { agentId },
|
|
@@ -1113,6 +1287,7 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1113
1287
|
.option('-l, --limit <limit>', 'Number of results (max 50)', '10')
|
|
1114
1288
|
.option('-o, --offset <offset>', 'Pagination offset')
|
|
1115
1289
|
.option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
|
|
1290
|
+
.option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
|
|
1116
1291
|
.action(async (query, options) => {
|
|
1117
1292
|
const spinner = (0, ora_1.default)('Searching agents...').start();
|
|
1118
1293
|
try {
|
|
@@ -1142,10 +1317,15 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1142
1317
|
if (options.scope)
|
|
1143
1318
|
params.set('scope', options.scope);
|
|
1144
1319
|
log('GET /discover/agents?' + params.toString());
|
|
1320
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
1145
1321
|
const sessionId = config.get('sessionId');
|
|
1146
1322
|
const headers = {};
|
|
1147
|
-
if (
|
|
1323
|
+
if (apiKey) {
|
|
1324
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
1325
|
+
}
|
|
1326
|
+
else if (sessionId) {
|
|
1148
1327
|
headers['X-Session-ID'] = sessionId;
|
|
1328
|
+
}
|
|
1149
1329
|
const res = await fetch(`${API_URL}/discover/agents?${params}`, { headers });
|
|
1150
1330
|
const data = await res.json();
|
|
1151
1331
|
spinner.stop();
|
|
@@ -1260,9 +1440,11 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1260
1440
|
.option('-t, --tag <tag>', 'Filter by tag')
|
|
1261
1441
|
.option('-c, --category <category>', 'Filter by category')
|
|
1262
1442
|
.option('-l, --limit <limit>', 'Number of results (max 50)', '20')
|
|
1443
|
+
.option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
|
|
1263
1444
|
.action(async (options) => {
|
|
1264
1445
|
const spinner = (0, ora_1.default)('Searching nearby agents...').start();
|
|
1265
1446
|
try {
|
|
1447
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
1266
1448
|
const sessionId = config.get('sessionId');
|
|
1267
1449
|
const params = new URLSearchParams();
|
|
1268
1450
|
params.set('lat', options.lat);
|
|
@@ -1275,7 +1457,10 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1275
1457
|
params.set('limit', options.limit);
|
|
1276
1458
|
log('GET /discover/agents/nearby?' + params.toString());
|
|
1277
1459
|
const headers = {};
|
|
1278
|
-
if (
|
|
1460
|
+
if (apiKey) {
|
|
1461
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
1462
|
+
}
|
|
1463
|
+
else if (sessionId) {
|
|
1279
1464
|
headers['X-Session-ID'] = sessionId;
|
|
1280
1465
|
}
|
|
1281
1466
|
const res = await fetch(`${API_URL}/discover/agents/nearby?${params}`, { headers });
|
|
@@ -1311,14 +1496,20 @@ discover
|
|
|
1311
1496
|
${chalk_1.default.bold('Example:')}
|
|
1312
1497
|
$ gopherhole discover info agent-abc123
|
|
1313
1498
|
`)
|
|
1314
|
-
.
|
|
1499
|
+
.option('--api-key <key>', 'API key for agent-mode discovery (overrides session)')
|
|
1500
|
+
.action(async (agentId, options) => {
|
|
1315
1501
|
const spinner = (0, ora_1.default)('Fetching agent info...').start();
|
|
1316
1502
|
try {
|
|
1317
1503
|
log('GET /discover/agents/' + agentId);
|
|
1504
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
1318
1505
|
const sessionId = config.get('sessionId');
|
|
1319
1506
|
const headers = {};
|
|
1320
|
-
if (
|
|
1507
|
+
if (apiKey) {
|
|
1508
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
1509
|
+
}
|
|
1510
|
+
else if (sessionId) {
|
|
1321
1511
|
headers['X-Session-ID'] = sessionId;
|
|
1512
|
+
}
|
|
1322
1513
|
const res = await fetch(`${API_URL}/discover/agents/${agentId}`, { headers });
|
|
1323
1514
|
if (!res.ok) {
|
|
1324
1515
|
throw new Error('Agent not found');
|
|
@@ -2102,5 +2293,413 @@ program
|
|
|
2102
2293
|
console.log(chalk_1.default.gray(' Status: https://status.gopherhole.ai'));
|
|
2103
2294
|
console.log('');
|
|
2104
2295
|
});
|
|
2296
|
+
// ========== MESSAGE COMMAND (agent-to-agent, Bearer auth) ==========
|
|
2297
|
+
program
|
|
2298
|
+
.command('message <agentId> <text>')
|
|
2299
|
+
.description(`Send a message to an agent using your API key (agent-to-agent)
|
|
2300
|
+
|
|
2301
|
+
${chalk_1.default.bold('Auth (in order of precedence):')}
|
|
2302
|
+
--api-key <key> > GOPHERHOLE_API_KEY env > .env file
|
|
2303
|
+
--agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
|
|
2304
|
+
|
|
2305
|
+
${chalk_1.default.bold('Examples:')}
|
|
2306
|
+
$ gopher message agent-memory-official "What do you remember about me?"
|
|
2307
|
+
$ gopher message echo-agent "Hello!" --api-key gph_xxx
|
|
2308
|
+
$ GOPHERHOLE_API_KEY=gph_xxx gopher message search-agent "latest AI news"
|
|
2309
|
+
`)
|
|
2310
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2311
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2312
|
+
.action(async (agentId, text, options) => {
|
|
2313
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2314
|
+
if (!apiKey) {
|
|
2315
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2316
|
+
console.error(chalk_1.default.gray('Set GOPHERHOLE_API_KEY, pass --api-key, or run gopher init to create a .env'));
|
|
2317
|
+
process.exit(1);
|
|
2318
|
+
}
|
|
2319
|
+
const spinner = (0, ora_1.default)(`Messaging ${agentId}...`).start();
|
|
2320
|
+
try {
|
|
2321
|
+
const client = makeAgentClient(apiKey);
|
|
2322
|
+
const response = await askAgent(client, agentId, text);
|
|
2323
|
+
spinner.stop();
|
|
2324
|
+
console.log(response || chalk_1.default.gray('(no response)'));
|
|
2325
|
+
}
|
|
2326
|
+
catch (err) {
|
|
2327
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2328
|
+
process.exit(1);
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
// ========== MEMORY COMMANDS (agent-to-agent via memory agent) ==========
|
|
2332
|
+
const MEMORY_AGENT = process.env.GOPHERHOLE_MEMORY_AGENT || 'agent-memory-official';
|
|
2333
|
+
const memory = program
|
|
2334
|
+
.command('memory')
|
|
2335
|
+
.description(`Manage agent memory via the GopherHole memory agent
|
|
2336
|
+
|
|
2337
|
+
${chalk_1.default.bold('Auth (in order of precedence):')}
|
|
2338
|
+
--api-key <key> > GOPHERHOLE_API_KEY env > .env file
|
|
2339
|
+
--agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
|
|
2340
|
+
`);
|
|
2341
|
+
memory
|
|
2342
|
+
.command('recall <query>')
|
|
2343
|
+
.description('Search your agent memories semantically')
|
|
2344
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2345
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2346
|
+
.option('--limit <n>', 'Max results', '10')
|
|
2347
|
+
.action(async (query, options) => {
|
|
2348
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2349
|
+
if (!apiKey) {
|
|
2350
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2351
|
+
process.exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
const spinner = (0, ora_1.default)('Recalling...').start();
|
|
2354
|
+
try {
|
|
2355
|
+
const client = makeAgentClient(apiKey);
|
|
2356
|
+
const msg = `Search memories for: ${query} (limit: ${options.limit})`;
|
|
2357
|
+
const response = await askAgent(client, MEMORY_AGENT, msg);
|
|
2358
|
+
spinner.stop();
|
|
2359
|
+
console.log(response || chalk_1.default.gray('No memories found'));
|
|
2360
|
+
}
|
|
2361
|
+
catch (err) {
|
|
2362
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2363
|
+
process.exit(1);
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2366
|
+
memory
|
|
2367
|
+
.command('store <content>')
|
|
2368
|
+
.description('Store a new memory')
|
|
2369
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2370
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2371
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
2372
|
+
.action(async (content, options) => {
|
|
2373
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2374
|
+
if (!apiKey) {
|
|
2375
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2376
|
+
process.exit(1);
|
|
2377
|
+
}
|
|
2378
|
+
const spinner = (0, ora_1.default)('Storing memory...').start();
|
|
2379
|
+
try {
|
|
2380
|
+
const client = makeAgentClient(apiKey);
|
|
2381
|
+
const tags = options.tags ? `\n\nTags: ${options.tags}` : '';
|
|
2382
|
+
const response = await askAgent(client, MEMORY_AGENT, `Remember this:\n\n${content}${tags}`);
|
|
2383
|
+
spinner.stop();
|
|
2384
|
+
console.log(response || chalk_1.default.green('Memory stored'));
|
|
2385
|
+
}
|
|
2386
|
+
catch (err) {
|
|
2387
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2388
|
+
process.exit(1);
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
memory
|
|
2392
|
+
.command('forget <query>')
|
|
2393
|
+
.description('Delete memories matching a query')
|
|
2394
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2395
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2396
|
+
.option('--confirm', 'Required to confirm deletion')
|
|
2397
|
+
.action(async (query, options) => {
|
|
2398
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2399
|
+
if (!apiKey) {
|
|
2400
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2401
|
+
process.exit(1);
|
|
2402
|
+
}
|
|
2403
|
+
if (!options.confirm) {
|
|
2404
|
+
console.error(chalk_1.default.yellow('Add --confirm to delete memories'));
|
|
2405
|
+
process.exit(1);
|
|
2406
|
+
}
|
|
2407
|
+
const spinner = (0, ora_1.default)('Forgetting...').start();
|
|
2408
|
+
try {
|
|
2409
|
+
const client = makeAgentClient(apiKey);
|
|
2410
|
+
const response = await askAgent(client, MEMORY_AGENT, `Forget memories matching: ${query}`);
|
|
2411
|
+
spinner.stop();
|
|
2412
|
+
console.log(response || chalk_1.default.green('Done'));
|
|
2413
|
+
}
|
|
2414
|
+
catch (err) {
|
|
2415
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2416
|
+
process.exit(1);
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
memory
|
|
2420
|
+
.command('list')
|
|
2421
|
+
.description('List recent memories')
|
|
2422
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2423
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2424
|
+
.option('--limit <n>', 'Max results', '20')
|
|
2425
|
+
.option('--offset <n>', 'Pagination offset', '0')
|
|
2426
|
+
.action(async (options) => {
|
|
2427
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2428
|
+
if (!apiKey) {
|
|
2429
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2430
|
+
process.exit(1);
|
|
2431
|
+
}
|
|
2432
|
+
const spinner = (0, ora_1.default)('Listing memories...').start();
|
|
2433
|
+
try {
|
|
2434
|
+
const client = makeAgentClient(apiKey);
|
|
2435
|
+
const msg = `List my recent memories (limit: ${options.limit}, offset: ${options.offset})`;
|
|
2436
|
+
const response = await askAgent(client, MEMORY_AGENT, msg);
|
|
2437
|
+
spinner.stop();
|
|
2438
|
+
console.log(response || chalk_1.default.gray('No memories found'));
|
|
2439
|
+
}
|
|
2440
|
+
catch (err) {
|
|
2441
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
// ========== WORKSPACE COMMANDS (direct RPC, Bearer auth) ==========
|
|
2446
|
+
const ws = program
|
|
2447
|
+
.command('workspace')
|
|
2448
|
+
.alias('ws')
|
|
2449
|
+
.description(`Manage shared workspaces for multi-agent collaboration
|
|
2450
|
+
|
|
2451
|
+
${chalk_1.default.bold('Auth (in order of precedence):')}
|
|
2452
|
+
--api-key <key> > GOPHERHOLE_API_KEY env > .env file
|
|
2453
|
+
--agent-id <id> > GOPHERHOLE_AGENT_ID env > .env file
|
|
2454
|
+
`);
|
|
2455
|
+
ws
|
|
2456
|
+
.command('list')
|
|
2457
|
+
.description('List workspaces you are a member of')
|
|
2458
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2459
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2460
|
+
.action(async (options) => {
|
|
2461
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2462
|
+
if (!apiKey) {
|
|
2463
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2464
|
+
process.exit(1);
|
|
2465
|
+
}
|
|
2466
|
+
const spinner = (0, ora_1.default)('Loading workspaces...').start();
|
|
2467
|
+
try {
|
|
2468
|
+
const client = makeHubClient(apiKey);
|
|
2469
|
+
const result = await client.workspaceList();
|
|
2470
|
+
spinner.stop();
|
|
2471
|
+
if (!result.workspaces.length) {
|
|
2472
|
+
console.log(chalk_1.default.gray('No workspaces yet. Create one with: gopher workspace create <name>'));
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
result.workspaces.forEach(w => {
|
|
2476
|
+
console.log(`\n${chalk_1.default.bold(w.name)} ${chalk_1.default.gray(`(${w.id})`)}`);
|
|
2477
|
+
if (w.description)
|
|
2478
|
+
console.log(chalk_1.default.gray(` ${w.description}`));
|
|
2479
|
+
console.log(chalk_1.default.gray(` Role: ${w.my_role || '?'} | Members: ${w.member_count || '?'} | Memories: ${w.memory_count || '?'}`));
|
|
2480
|
+
});
|
|
2481
|
+
console.log('');
|
|
2482
|
+
}
|
|
2483
|
+
catch (err) {
|
|
2484
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2485
|
+
process.exit(1);
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
ws
|
|
2489
|
+
.command('create <name>')
|
|
2490
|
+
.description('Create a new workspace')
|
|
2491
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2492
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2493
|
+
.option('--description <text>', 'Workspace description')
|
|
2494
|
+
.action(async (name, options) => {
|
|
2495
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2496
|
+
if (!apiKey) {
|
|
2497
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2498
|
+
process.exit(1);
|
|
2499
|
+
}
|
|
2500
|
+
const spinner = (0, ora_1.default)('Creating workspace...').start();
|
|
2501
|
+
try {
|
|
2502
|
+
const client = makeHubClient(apiKey);
|
|
2503
|
+
const result = await client.workspaceCreate(name, options.description);
|
|
2504
|
+
spinner.succeed(`Workspace created: ${chalk_1.default.bold(result.workspace.name)}`);
|
|
2505
|
+
console.log(chalk_1.default.gray(` ID: ${result.workspace.id}`));
|
|
2506
|
+
}
|
|
2507
|
+
catch (err) {
|
|
2508
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2509
|
+
process.exit(1);
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
ws
|
|
2513
|
+
.command('query <workspaceId> <query>')
|
|
2514
|
+
.description('Search workspace memories semantically')
|
|
2515
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2516
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2517
|
+
.option('--type <type>', 'Filter by memory type (fact|decision|preference|todo|context|reference)')
|
|
2518
|
+
.option('--limit <n>', 'Max results', '10')
|
|
2519
|
+
.action(async (workspaceId, query, options) => {
|
|
2520
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2521
|
+
if (!apiKey) {
|
|
2522
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2523
|
+
process.exit(1);
|
|
2524
|
+
}
|
|
2525
|
+
const spinner = (0, ora_1.default)('Searching...').start();
|
|
2526
|
+
try {
|
|
2527
|
+
const client = makeHubClient(apiKey);
|
|
2528
|
+
const result = await client.workspaceQuery({
|
|
2529
|
+
workspace_id: workspaceId,
|
|
2530
|
+
query,
|
|
2531
|
+
type: options.type,
|
|
2532
|
+
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
2533
|
+
});
|
|
2534
|
+
spinner.stop();
|
|
2535
|
+
if (!result.memories.length) {
|
|
2536
|
+
console.log(chalk_1.default.gray('No memories found matching your query.'));
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
console.log(chalk_1.default.gray(`Found ${result.count} memories:\n`));
|
|
2540
|
+
result.memories.forEach(m => {
|
|
2541
|
+
const sim = m.similarity ? ` ${chalk_1.default.gray(`${(m.similarity * 100).toFixed(0)}%`)}` : '';
|
|
2542
|
+
console.log(`${chalk_1.default.bold(`[${m.type}]`)}${sim} ${m.content.substring(0, 200)}${m.content.length > 200 ? '…' : ''}`);
|
|
2543
|
+
if (m.tags.length)
|
|
2544
|
+
console.log(chalk_1.default.gray(` Tags: ${m.tags.join(', ')}`));
|
|
2545
|
+
console.log('');
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
catch (err) {
|
|
2549
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2550
|
+
process.exit(1);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
ws
|
|
2554
|
+
.command('store <workspaceId> <content>')
|
|
2555
|
+
.description('Store a memory in a workspace')
|
|
2556
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2557
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2558
|
+
.option('--type <type>', 'Memory type (fact|decision|preference|todo|context|reference)', 'fact')
|
|
2559
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
2560
|
+
.action(async (workspaceId, content, options) => {
|
|
2561
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2562
|
+
if (!apiKey) {
|
|
2563
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2564
|
+
process.exit(1);
|
|
2565
|
+
}
|
|
2566
|
+
const spinner = (0, ora_1.default)('Storing...').start();
|
|
2567
|
+
try {
|
|
2568
|
+
const client = makeHubClient(apiKey);
|
|
2569
|
+
const result = await client.workspaceStore({
|
|
2570
|
+
workspace_id: workspaceId,
|
|
2571
|
+
content,
|
|
2572
|
+
type: options.type,
|
|
2573
|
+
tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : undefined,
|
|
2574
|
+
});
|
|
2575
|
+
spinner.succeed(`Memory stored (${result.memory.id})`);
|
|
2576
|
+
}
|
|
2577
|
+
catch (err) {
|
|
2578
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2579
|
+
process.exit(1);
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
ws
|
|
2583
|
+
.command('memories <workspaceId>')
|
|
2584
|
+
.description('List all memories in a workspace (browse, non-semantic)')
|
|
2585
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2586
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2587
|
+
.option('--limit <n>', 'Max results', '20')
|
|
2588
|
+
.option('--offset <n>', 'Pagination offset', '0')
|
|
2589
|
+
.action(async (workspaceId, options) => {
|
|
2590
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2591
|
+
if (!apiKey) {
|
|
2592
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2593
|
+
process.exit(1);
|
|
2594
|
+
}
|
|
2595
|
+
const spinner = (0, ora_1.default)('Loading memories...').start();
|
|
2596
|
+
try {
|
|
2597
|
+
const client = makeHubClient(apiKey);
|
|
2598
|
+
const result = await client.workspaceMemories({
|
|
2599
|
+
workspace_id: workspaceId,
|
|
2600
|
+
limit: parseInt(options.limit),
|
|
2601
|
+
offset: parseInt(options.offset),
|
|
2602
|
+
});
|
|
2603
|
+
spinner.stop();
|
|
2604
|
+
if (!result.memories.length) {
|
|
2605
|
+
console.log(chalk_1.default.gray('No memories in this workspace.'));
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
console.log(chalk_1.default.gray(`Showing ${result.count} of ${result.total} memories:\n`));
|
|
2609
|
+
result.memories.forEach(m => {
|
|
2610
|
+
console.log(`${chalk_1.default.bold(`[${m.type}]`)} ${m.content.substring(0, 150)}${m.content.length > 150 ? '…' : ''}`);
|
|
2611
|
+
if (m.tags.length)
|
|
2612
|
+
console.log(chalk_1.default.gray(` Tags: ${m.tags.join(', ')}`));
|
|
2613
|
+
});
|
|
2614
|
+
console.log('');
|
|
2615
|
+
}
|
|
2616
|
+
catch (err) {
|
|
2617
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2618
|
+
process.exit(1);
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
ws
|
|
2622
|
+
.command('forget <workspaceId>')
|
|
2623
|
+
.description('Delete memories from a workspace by ID or semantic query')
|
|
2624
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2625
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2626
|
+
.option('--id <memoryId>', 'Delete a specific memory by ID')
|
|
2627
|
+
.option('--query <query>', 'Delete memories matching this query')
|
|
2628
|
+
.action(async (workspaceId, options) => {
|
|
2629
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2630
|
+
if (!apiKey) {
|
|
2631
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2632
|
+
process.exit(1);
|
|
2633
|
+
}
|
|
2634
|
+
if (!options.id && !options.query) {
|
|
2635
|
+
console.error(chalk_1.default.yellow('Provide --id <memoryId> or --query <query>'));
|
|
2636
|
+
process.exit(1);
|
|
2637
|
+
}
|
|
2638
|
+
const spinner = (0, ora_1.default)('Deleting...').start();
|
|
2639
|
+
try {
|
|
2640
|
+
const client = makeHubClient(apiKey);
|
|
2641
|
+
const result = await client.workspaceForget({
|
|
2642
|
+
workspace_id: workspaceId,
|
|
2643
|
+
id: options.id,
|
|
2644
|
+
query: options.query,
|
|
2645
|
+
});
|
|
2646
|
+
spinner.succeed(`Deleted ${result.deleted} memory/memories`);
|
|
2647
|
+
}
|
|
2648
|
+
catch (err) {
|
|
2649
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2650
|
+
process.exit(1);
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
const wsMembers = ws
|
|
2654
|
+
.command('members')
|
|
2655
|
+
.description('Manage workspace members');
|
|
2656
|
+
wsMembers
|
|
2657
|
+
.command('list <workspaceId>')
|
|
2658
|
+
.description('List workspace members')
|
|
2659
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2660
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2661
|
+
.action(async (workspaceId, options) => {
|
|
2662
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2663
|
+
if (!apiKey) {
|
|
2664
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2665
|
+
process.exit(1);
|
|
2666
|
+
}
|
|
2667
|
+
const spinner = (0, ora_1.default)('Loading members...').start();
|
|
2668
|
+
try {
|
|
2669
|
+
const client = makeHubClient(apiKey);
|
|
2670
|
+
const result = await client.workspaceMembersList(workspaceId);
|
|
2671
|
+
spinner.stop();
|
|
2672
|
+
result.members.forEach(m => {
|
|
2673
|
+
console.log(` ${m.agent_name || m.agent_id} ${chalk_1.default.gray(`(${m.role})`)}`);
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
catch (err) {
|
|
2677
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2678
|
+
process.exit(1);
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
wsMembers
|
|
2682
|
+
.command('add <workspaceId> <agentId>')
|
|
2683
|
+
.description('Add an agent to a workspace')
|
|
2684
|
+
.option('--api-key <key>', 'API key (overrides env / .env)')
|
|
2685
|
+
.option('--agent-id <id>', 'Your agent ID (overrides env / .env)')
|
|
2686
|
+
.option('--role <role>', 'Role: read | write | admin', 'write')
|
|
2687
|
+
.action(async (workspaceId, agentId, options) => {
|
|
2688
|
+
const apiKey = await resolveApiKey(options.apiKey);
|
|
2689
|
+
if (!apiKey) {
|
|
2690
|
+
console.error(chalk_1.default.red('No API key found.'));
|
|
2691
|
+
process.exit(1);
|
|
2692
|
+
}
|
|
2693
|
+
const spinner = (0, ora_1.default)('Adding member...').start();
|
|
2694
|
+
try {
|
|
2695
|
+
const client = makeHubClient(apiKey);
|
|
2696
|
+
await client.workspaceMembersAdd(workspaceId, agentId, options.role);
|
|
2697
|
+
spinner.succeed(`Added ${agentId} with role: ${options.role}`);
|
|
2698
|
+
}
|
|
2699
|
+
catch (err) {
|
|
2700
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
2701
|
+
process.exit(1);
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2105
2704
|
// Parse and run
|
|
2106
2705
|
program.parse();
|