bus-agent 2.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/.env.coco +11 -0
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/SKILL.md +314 -0
- package/backup.js +57 -0
- package/bin/cli.js +41 -0
- package/bridge.js +325 -0
- package/claude-mcp.json +10 -0
- package/clients/coco-client.ts +245 -0
- package/clients/coco_client.py +216 -0
- package/coco-aliases.sh +10 -0
- package/coco-cli.js +1002 -0
- package/coco-tool.js +177 -0
- package/coco.js +26 -0
- package/cursor-mcp.json +3 -0
- package/doctor.js +24 -0
- package/hermes-forwarder.js +152 -0
- package/hermes.example.json +9 -0
- package/index.js +52 -0
- package/lib/backup.js +256 -0
- package/lib/bus.js +516 -0
- package/lib/daemon.js +96 -0
- package/lib/doctor.js +333 -0
- package/lib/hermes.js +162 -0
- package/lib/mcp.js +730 -0
- package/lib/memory.js +667 -0
- package/lib/orchestrator.js +426 -0
- package/lib/scheduler.js +259 -0
- package/lib/tunnel.js +317 -0
- package/mcporter.example.json +14 -0
- package/opencode-mcp.json +10 -0
- package/package.json +76 -0
- package/scripts/install.bat +5 -0
- package/scripts/install.ps1 +100 -0
- package/setup.js +320 -0
- package/tunnel.js +66 -0
- package/webhook-gateway.js +420 -0
package/coco-cli.js
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CoCo CLI v2.1 — Direct bus interface for CLI agents
|
|
4
|
+
*
|
|
5
|
+
* No MCP needed. Direct file access to the CoCo Agent Bus.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node coco-cli.js agents # List all agents (with filters)
|
|
9
|
+
* node coco-cli.js whoami # Show your agent identity
|
|
10
|
+
* node coco-cli.js inbox [agent] # Show inbox messages
|
|
11
|
+
* node coco-cli.js send <to> <message> # Send a message
|
|
12
|
+
* node coco-cli.js reply <msg-id> <message> # Reply to a message
|
|
13
|
+
* node coco-cli.js profile [agent] # Show profile
|
|
14
|
+
* node coco-cli.js profile edit --status busy # Edit your profile
|
|
15
|
+
* node coco-cli.js search <query> # Search agents
|
|
16
|
+
* node coco-cli.js status # Show CoCo + your agent status
|
|
17
|
+
* node coco-cli.js subscribe <channel> # Listen to channel messages
|
|
18
|
+
* node coco-cli.js channel list|send|history # Channel operations
|
|
19
|
+
* node coco-cli.js events [since] # View system events
|
|
20
|
+
* node coco-cli.js watch [agent] # Watch for new messages
|
|
21
|
+
*/
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
const BUS_DIR = path.join(__dirname, '.bus');
|
|
26
|
+
const AGENTS_FILE = path.join(BUS_DIR, 'agents.json');
|
|
27
|
+
const MSGS_DIR = path.join(BUS_DIR, 'messages');
|
|
28
|
+
const CHANNELS_DIR = path.join(BUS_DIR, 'channels');
|
|
29
|
+
const EVENTS_DIR = path.join(BUS_DIR, 'events');
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const cmd = args[0] || 'help';
|
|
33
|
+
|
|
34
|
+
function main() {
|
|
35
|
+
switch (cmd) {
|
|
36
|
+
case 'whoami':
|
|
37
|
+
return cmdWhoami();
|
|
38
|
+
case 'agents':
|
|
39
|
+
case 'ls':
|
|
40
|
+
return cmdAgents(args.slice(1));
|
|
41
|
+
case 'inbox':
|
|
42
|
+
case 'messages':
|
|
43
|
+
return cmdInbox(args[1], args.slice(2));
|
|
44
|
+
case 'send':
|
|
45
|
+
return cmdSend(args[1], args.slice(2).join(' '));
|
|
46
|
+
case 'reply':
|
|
47
|
+
return cmdReply(args[1], args.slice(2).join(' '));
|
|
48
|
+
case 'profile':
|
|
49
|
+
return cmdProfile(args.slice(1));
|
|
50
|
+
case 'search':
|
|
51
|
+
return cmdSearch(args.slice(1).join(' '));
|
|
52
|
+
case 'status':
|
|
53
|
+
return cmdStatus();
|
|
54
|
+
case 'subscribe':
|
|
55
|
+
return cmdSubscribe(args[1]);
|
|
56
|
+
case 'channel':
|
|
57
|
+
return cmdChannel(args.slice(1));
|
|
58
|
+
case 'doctor':
|
|
59
|
+
return cmdDoctor(args.slice(1));
|
|
60
|
+
case 'tunnel':
|
|
61
|
+
return cmdTunnel(args.slice(1));
|
|
62
|
+
case 'backup':
|
|
63
|
+
return cmdBackup(args.slice(1));
|
|
64
|
+
case 'memory':
|
|
65
|
+
cmdMemory(args.slice(1)).catch(e => console.error('Error:', e.message));
|
|
66
|
+
return;
|
|
67
|
+
case 'events':
|
|
68
|
+
return cmdEvents(args[1]);
|
|
69
|
+
case 'watch':
|
|
70
|
+
return cmdWatch(args[1]);
|
|
71
|
+
case 'help':
|
|
72
|
+
default:
|
|
73
|
+
return cmdHelp();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Whoami ──
|
|
78
|
+
|
|
79
|
+
function cmdWhoami() {
|
|
80
|
+
const agents = loadAgents();
|
|
81
|
+
const name = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
82
|
+
const seen = agents[name];
|
|
83
|
+
const profile = seen || {};
|
|
84
|
+
console.log(`\n👤 Your Agent Identity:`);
|
|
85
|
+
console.log(` Name: ${name}`);
|
|
86
|
+
console.log(` Registered: ${seen ? '✅ yes' : '❌ no'}`);
|
|
87
|
+
console.log(` Status: ${profile.status || 'unknown'}`);
|
|
88
|
+
console.log(` Capabilities:${(profile.capabilities || []).join(', ') || 'none'}`);
|
|
89
|
+
console.log(` Model: ${profile.model ? `${profile.model.provider}/${profile.model.name}` : 'not set'}`);
|
|
90
|
+
if (profile.last_seen) console.log(` Last seen: ${profile.last_seen}`);
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Agents (enhanced with filters) ──
|
|
95
|
+
|
|
96
|
+
function cmdAgents(filterArgs) {
|
|
97
|
+
const agents = loadAgents();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const cutoff = now - 300000;
|
|
100
|
+
let list = Object.entries(agents);
|
|
101
|
+
|
|
102
|
+
// Parse filter args
|
|
103
|
+
const filters = {};
|
|
104
|
+
for (let i = 0; i < filterArgs.length; i++) {
|
|
105
|
+
if (filterArgs[i] === '--online') filters.online = true;
|
|
106
|
+
else if (filterArgs[i] === '--capability' || filterArgs[i] === '-c') filters.capability = filterArgs[++i];
|
|
107
|
+
else if (filterArgs[i] === '--tag' || filterArgs[i] === '-t') filters.tag = filterArgs[++i];
|
|
108
|
+
else if (filterArgs[i] === '--status' || filterArgs[i] === '-s') filters.status = filterArgs[++i];
|
|
109
|
+
else if (filterArgs[i] === '--search') filters.search = filterArgs[++i];
|
|
110
|
+
else if (filterArgs[i] === '--model' || filterArgs[i] === '-m') filters.model = filterArgs[++i];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (filters.online) list = list.filter(([, a]) => new Date(a.last_seen).getTime() > cutoff);
|
|
114
|
+
if (filters.status) list = list.filter(([, a]) => a.status === filters.status);
|
|
115
|
+
if (filters.capability) list = list.filter(([, a]) => (a.capabilities || []).some(c => c.toLowerCase().includes(filters.capability.toLowerCase())));
|
|
116
|
+
if (filters.tag) list = list.filter(([, a]) => (a.tags || []).some(t => t.toLowerCase() === filters.tag.toLowerCase()));
|
|
117
|
+
if (filters.model) list = list.filter(([, a]) => {
|
|
118
|
+
const m = a.model?.name || a.model?.provider || '';
|
|
119
|
+
return m.toLowerCase().includes(filters.model.toLowerCase());
|
|
120
|
+
});
|
|
121
|
+
if (filters.search) list = list.filter(([, a]) =>
|
|
122
|
+
a.name.toLowerCase().includes(filters.search.toLowerCase()) ||
|
|
123
|
+
(a.description || '').toLowerCase().includes(filters.search.toLowerCase())
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
console.log(`\n🤖 Agents on CoCo Bus (${list.length} total):`);
|
|
127
|
+
console.log('─'.repeat(70));
|
|
128
|
+
|
|
129
|
+
list.sort((a, b) => new Date(b[1].last_seen) - new Date(a[1].last_seen));
|
|
130
|
+
|
|
131
|
+
for (const [name, info] of list) {
|
|
132
|
+
const online = new Date(info.last_seen).getTime() > cutoff;
|
|
133
|
+
const statusIcon = online ? '🟢' : '⚫';
|
|
134
|
+
const statusLabel = info.status || (online ? 'idle' : 'offline');
|
|
135
|
+
const ago = msAgo(new Date(info.last_seen).getTime());
|
|
136
|
+
const caps = info.capabilities && info.capabilities.length > 0
|
|
137
|
+
? ` [${info.capabilities.slice(0, 3).join(', ')}${info.capabilities.length > 3 ? '...' : ''}]`
|
|
138
|
+
: '';
|
|
139
|
+
const model = info.model ? ` (${info.model.provider || '?'}/${info.model.name || '?'})` : '';
|
|
140
|
+
console.log(` ${statusIcon} ${name.padEnd(18)} ${statusLabel.padEnd(8)}${model}${caps}`);
|
|
141
|
+
if (info.description) console.log(` ${' '.repeat(18)} ${info.description.substring(0, 50)}`);
|
|
142
|
+
console.log(` ${' '.repeat(18)} ${ago}`);
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Inbox (enhanced with --from filter) ──
|
|
148
|
+
|
|
149
|
+
function cmdInbox(agentName, extra) {
|
|
150
|
+
const agent = agentName || process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
151
|
+
const dir = path.join(MSGS_DIR, agent);
|
|
152
|
+
|
|
153
|
+
const opts = { from: null, unread: false };
|
|
154
|
+
for (let i = 0; i < (extra || []).length; i++) {
|
|
155
|
+
if (extra[i] === '--from') opts.from = extra[++i];
|
|
156
|
+
if (extra[i] === '--unread') opts.unread = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!fs.existsSync(dir)) {
|
|
160
|
+
console.log(`📭 No messages for "${agent}"`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let files = fs.readdirSync(dir).filter(f => f.endsWith('.json')).sort();
|
|
164
|
+
|
|
165
|
+
if (opts.from) {
|
|
166
|
+
files = files.filter(f => {
|
|
167
|
+
try {
|
|
168
|
+
const m = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
169
|
+
return m.from === opts.from;
|
|
170
|
+
} catch { return false; }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (files.length === 0) {
|
|
175
|
+
console.log(`📭 No messages for "${agent}"`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const showCount = opts.unread ? undefined : files.length;
|
|
180
|
+
const showFiles = opts.unread ? files.slice(-10) : files;
|
|
181
|
+
|
|
182
|
+
console.log(`\n📬 Inbox for "${agent}" (${files.length} total):`);
|
|
183
|
+
console.log('─'.repeat(70));
|
|
184
|
+
for (const f of showFiles) {
|
|
185
|
+
try {
|
|
186
|
+
const msg = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
187
|
+
const ago = msAgo(new Date(msg.timestamp).getTime());
|
|
188
|
+
const tag = msg.metadata?.type === 'system_event' ? '🔔' :
|
|
189
|
+
msg.metadata?.type === 'system_welcome' ? '👋' :
|
|
190
|
+
msg.metadata?.channel ? '📢' : '📩';
|
|
191
|
+
console.log(` ${tag} [${msg.id.substring(0, 16)}] from:${msg.from}`);
|
|
192
|
+
console.log(` ${msg.message.substring(0, 100)}`);
|
|
193
|
+
console.log(` ${ago}`);
|
|
194
|
+
if (msg.metadata?.channel) console.log(` (via channel: ${msg.metadata.channel})`);
|
|
195
|
+
console.log();
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Send / Reply (unchanged) ──
|
|
201
|
+
|
|
202
|
+
function cmdSend(to, text) {
|
|
203
|
+
if (!to || !text) {
|
|
204
|
+
console.log('Usage: node coco-cli.js send <to> <message>');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const from = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
208
|
+
const msg = {
|
|
209
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
210
|
+
from,
|
|
211
|
+
to,
|
|
212
|
+
message: text,
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
};
|
|
215
|
+
const inboxDir = path.join(MSGS_DIR, to);
|
|
216
|
+
if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
|
|
217
|
+
fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
|
|
218
|
+
console.log(`✅ Message sent to "${to}" (id: ${msg.id})`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cmdReply(msgId, text) {
|
|
222
|
+
if (!msgId || !text) {
|
|
223
|
+
console.log('Usage: node coco-cli.js reply <msg-id> <message>');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
const from = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
227
|
+
|
|
228
|
+
let original = null;
|
|
229
|
+
if (fs.existsSync(MSGS_DIR)) {
|
|
230
|
+
const dirs = fs.readdirSync(MSGS_DIR);
|
|
231
|
+
for (const d of dirs) {
|
|
232
|
+
const attempts = [`${msgId}.json`, msgId];
|
|
233
|
+
for (const a of attempts) {
|
|
234
|
+
const p = path.join(MSGS_DIR, d, a);
|
|
235
|
+
if (fs.existsSync(p)) {
|
|
236
|
+
try { original = JSON.parse(fs.readFileSync(p, 'utf-8')); break; } catch {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (original) break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!original) {
|
|
243
|
+
console.log(`❌ Message "${msgId}" not found`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const reply = {
|
|
248
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
249
|
+
from,
|
|
250
|
+
to: original.from,
|
|
251
|
+
message: text,
|
|
252
|
+
in_reply_to: msgId,
|
|
253
|
+
timestamp: new Date().toISOString(),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const inboxDir = path.join(MSGS_DIR, original.from);
|
|
257
|
+
if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
|
|
258
|
+
fs.writeFileSync(path.join(inboxDir, `${reply.id}.json`), JSON.stringify(reply, null, 2), 'utf-8');
|
|
259
|
+
console.log(`✅ Reply sent to "${original.from}" (id: ${reply.id})`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Profile (new) ──
|
|
263
|
+
|
|
264
|
+
function cmdProfile(subArgs) {
|
|
265
|
+
if (subArgs[0] === 'edit') {
|
|
266
|
+
return cmdProfileEdit(subArgs.slice(1));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const agentName = subArgs[0] || process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
270
|
+
const agents = loadAgents();
|
|
271
|
+
const profile = agents[agentName];
|
|
272
|
+
|
|
273
|
+
if (!profile) {
|
|
274
|
+
console.log(`❌ Agent "${agentName}" not found on bus`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(`\n👤 Profile: ${agentName}`);
|
|
279
|
+
console.log('─'.repeat(50));
|
|
280
|
+
console.log(` Description: ${profile.description || '(none)'}`);
|
|
281
|
+
console.log(` Status: ${profile.status || 'unknown'}`);
|
|
282
|
+
console.log(` Version: ${profile.version || '?'}`);
|
|
283
|
+
console.log(` Model: ${profile.model ? `${profile.model.provider}/${profile.model.name}` : 'not set'}`);
|
|
284
|
+
if (profile.model?.context_window) console.log(` Context: ${profile.model.context_window} tokens`);
|
|
285
|
+
console.log(` Capabilities: ${(profile.capabilities || []).join(', ') || '(none)'}`);
|
|
286
|
+
console.log(` Tags: ${(profile.tags || []).join(', ') || '(none)'}`);
|
|
287
|
+
console.log(` Tools: ${(profile.tools || []).slice(0, 10).join(', ') || '(none)'}`);
|
|
288
|
+
if (profile.endpoints) {
|
|
289
|
+
const eps = Object.entries(profile.endpoints).filter(([, v]) => v).map(([k, v]) => `${k}:${v}`);
|
|
290
|
+
if (eps.length) console.log(` Endpoints: ${eps.join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
console.log(` Last seen: ${profile.last_seen || 'never'}`);
|
|
293
|
+
console.log(` Registered: ${profile.registered_at || 'never'}`);
|
|
294
|
+
console.log();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cmdProfileEdit(edits) {
|
|
298
|
+
const name = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
299
|
+
const agents = loadAgents();
|
|
300
|
+
if (!agents[name]) {
|
|
301
|
+
console.log(`❌ Not registered. Register first with: send or whoami`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const profile = agents[name];
|
|
306
|
+
for (let i = 0; i < edits.length; i++) {
|
|
307
|
+
switch (edits[i]) {
|
|
308
|
+
case '--status':
|
|
309
|
+
profile.status = edits[++i];
|
|
310
|
+
break;
|
|
311
|
+
case '--description':
|
|
312
|
+
case '--desc':
|
|
313
|
+
profile.description = edits[++i];
|
|
314
|
+
break;
|
|
315
|
+
case '--capabilities':
|
|
316
|
+
case '--caps':
|
|
317
|
+
profile.capabilities = edits[++i].split(',');
|
|
318
|
+
break;
|
|
319
|
+
case '--tags':
|
|
320
|
+
profile.tags = edits[++i].split(',');
|
|
321
|
+
break;
|
|
322
|
+
case '--version':
|
|
323
|
+
case '-v':
|
|
324
|
+
profile.version = edits[++i];
|
|
325
|
+
break;
|
|
326
|
+
case '--model':
|
|
327
|
+
const modelStr = edits[++i];
|
|
328
|
+
const parts = modelStr.split('/');
|
|
329
|
+
profile.model = { provider: parts[0] || 'unknown', name: parts[1] || parts[0] || 'unknown' };
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
profile.last_seen = new Date().toISOString();
|
|
335
|
+
fs.writeFileSync(AGENTS_FILE, JSON.stringify(agents, null, 2), 'utf-8');
|
|
336
|
+
console.log(`✅ Profile updated for "${name}"`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Search (new) ──
|
|
340
|
+
|
|
341
|
+
function cmdSearch(query) {
|
|
342
|
+
if (!query) {
|
|
343
|
+
console.log('Usage: node coco-cli.js search <query>');
|
|
344
|
+
console.log('Searches agent names, descriptions, capabilities, and tags');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const agents = loadAgents();
|
|
349
|
+
const q = query.toLowerCase();
|
|
350
|
+
const results = Object.entries(agents).filter(([name, info]) =>
|
|
351
|
+
name.toLowerCase().includes(q) ||
|
|
352
|
+
(info.description || '').toLowerCase().includes(q) ||
|
|
353
|
+
(info.capabilities || []).some(c => c.toLowerCase().includes(q)) ||
|
|
354
|
+
(info.tags || []).some(t => t.toLowerCase().includes(q))
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
console.log(`\n🔍 Search results for "${query}" (${results.length} matches):`);
|
|
358
|
+
console.log('─'.repeat(50));
|
|
359
|
+
for (const [name, info] of results) {
|
|
360
|
+
const caps = (info.capabilities || []).slice(0, 3).join(', ');
|
|
361
|
+
console.log(` ${name.padEnd(18)} ${caps ? `[${caps}]` : ''}`);
|
|
362
|
+
if (info.description) console.log(` ${' '.repeat(18)} ${info.description.substring(0, 60)}`);
|
|
363
|
+
}
|
|
364
|
+
if (results.length === 0) console.log(' No matches found.');
|
|
365
|
+
console.log();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Status (new) ──
|
|
369
|
+
|
|
370
|
+
function cmdStatus() {
|
|
371
|
+
const agents = loadAgents();
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
const cutoff = now - 300000;
|
|
374
|
+
|
|
375
|
+
const online = Object.entries(agents).filter(([, a]) => new Date(a.last_seen).getTime() > cutoff);
|
|
376
|
+
const offline = Object.entries(agents).filter(([, a]) => new Date(a.last_seen).getTime() <= cutoff);
|
|
377
|
+
|
|
378
|
+
const myName = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
379
|
+
const me = agents[myName];
|
|
380
|
+
const myOnline = me && new Date(me.last_seen).getTime() > cutoff;
|
|
381
|
+
|
|
382
|
+
// Count bus files
|
|
383
|
+
const msgCount = fs.existsSync(MSGS_DIR)
|
|
384
|
+
? fs.readdirSync(MSGS_DIR).filter(d => !d.endsWith('_outbox')).reduce((sum, d) => {
|
|
385
|
+
const p = path.join(MSGS_DIR, d);
|
|
386
|
+
return sum + (fs.existsSync(p) ? fs.readdirSync(p).filter(f => f.endsWith('.json')).length : 0);
|
|
387
|
+
}, 0) : 0;
|
|
388
|
+
|
|
389
|
+
const chCount = fs.existsSync(CHANNELS_DIR)
|
|
390
|
+
? fs.readdirSync(CHANNELS_DIR).filter(f => f.endsWith('.json')).length : 0;
|
|
391
|
+
|
|
392
|
+
console.log(`
|
|
393
|
+
╔══════════════════════════════════════════╗
|
|
394
|
+
║ MCP CoCo — Agent Bus Status ║
|
|
395
|
+
╚══════════════════════════════════════════╝`);
|
|
396
|
+
console.log(` You: ${myName} ${myOnline ? '🟢 online' : '⚫ offline'}`);
|
|
397
|
+
console.log(` Bus agents: ${Object.keys(agents).length} total (${online.length} online, ${offline.length} offline)`);
|
|
398
|
+
console.log(` Messages: ${msgCount} unread across all agents`);
|
|
399
|
+
console.log(` Channels: ${chCount}`);
|
|
400
|
+
console.log(` Bus directory: ${BUS_DIR}`);
|
|
401
|
+
console.log();
|
|
402
|
+
|
|
403
|
+
// Show online agents
|
|
404
|
+
if (online.length > 0) {
|
|
405
|
+
console.log(' 🟢 Online agents:');
|
|
406
|
+
for (const [name, info] of online) {
|
|
407
|
+
const ago = msAgo(new Date(info.last_seen).getTime());
|
|
408
|
+
const caps = info.capabilities && info.capabilities.length > 0
|
|
409
|
+
? ` [${info.capabilities.slice(0, 2).join(', ')}${info.capabilities.length > 2 ? '...' : ''}]`
|
|
410
|
+
: '';
|
|
411
|
+
const model = info.model ? ` (${info.model.provider || '?'}/${info.model.name || '?'})` : '';
|
|
412
|
+
console.log(` ${name.padEnd(18)}${model}${caps} (${ago})`);
|
|
413
|
+
}
|
|
414
|
+
console.log();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Subscribe (new) ──
|
|
419
|
+
|
|
420
|
+
function cmdSubscribe(channelId) {
|
|
421
|
+
if (!channelId) {
|
|
422
|
+
console.log('Usage: node coco-cli.js subscribe <channel>');
|
|
423
|
+
console.log('Listens to channel messages in real-time.');
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const chPath = path.join(CHANNELS_DIR, `${channelId}.json`);
|
|
428
|
+
if (!fs.existsSync(chPath)) {
|
|
429
|
+
console.log(`❌ Channel "${channelId}" not found. Create it first.`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const logDir = path.join(CHANNELS_DIR, channelId, 'log');
|
|
434
|
+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
435
|
+
|
|
436
|
+
const myName = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
437
|
+
|
|
438
|
+
// Join if not already
|
|
439
|
+
const ch = JSON.parse(fs.readFileSync(chPath, 'utf-8'));
|
|
440
|
+
if (!ch.members.includes(myName)) {
|
|
441
|
+
ch.members.push(myName);
|
|
442
|
+
fs.writeFileSync(chPath, JSON.stringify(ch, null, 2), 'utf-8');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Get initial history
|
|
446
|
+
const files = fs.readdirSync(logDir).filter(f => f.endsWith('.json')).sort();
|
|
447
|
+
const seenIds = new Set(files);
|
|
448
|
+
|
|
449
|
+
console.log(`\n👂 Subscribed to #${channelId} as "${myName}"`);
|
|
450
|
+
console.log(` ${ch.members.length} members: ${ch.members.join(', ')}`);
|
|
451
|
+
console.log(` Waiting for new messages... (Ctrl+C to stop)\n`);
|
|
452
|
+
|
|
453
|
+
// Show last few messages
|
|
454
|
+
const recent = files.slice(-5);
|
|
455
|
+
if (recent.length > 0) {
|
|
456
|
+
console.log(' Recent messages:');
|
|
457
|
+
for (const f of recent) {
|
|
458
|
+
try {
|
|
459
|
+
const msg = JSON.parse(fs.readFileSync(path.join(logDir, f), 'utf-8'));
|
|
460
|
+
const ago = msAgo(new Date(msg.timestamp).getTime());
|
|
461
|
+
console.log(` [${msg.from}] ${msg.message.substring(0, 100)} (${ago})`);
|
|
462
|
+
} catch {}
|
|
463
|
+
}
|
|
464
|
+
console.log();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const interval = setInterval(() => {
|
|
468
|
+
const currentFiles = fs.readdirSync(logDir).filter(f => f.endsWith('.json'));
|
|
469
|
+
for (const f of currentFiles) {
|
|
470
|
+
if (!seenIds.has(f)) {
|
|
471
|
+
seenIds.add(f);
|
|
472
|
+
try {
|
|
473
|
+
const msg = JSON.parse(fs.readFileSync(path.join(logDir, f), 'utf-8'));
|
|
474
|
+
const ago = msAgo(new Date(msg.timestamp).getTime());
|
|
475
|
+
process.stdout.write(` 💬 [${msg.from}] ${msg.message.substring(0, 120)}\n`);
|
|
476
|
+
process.stdout.write(` ${ago}\n\n`);
|
|
477
|
+
} catch {}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}, 2000);
|
|
481
|
+
|
|
482
|
+
// Also watch DM inbox for channel messages
|
|
483
|
+
const inboxDir = path.join(MSGS_DIR, myName);
|
|
484
|
+
if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
|
|
485
|
+
let lastMsgCount = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json')).length;
|
|
486
|
+
|
|
487
|
+
const msgInterval = setInterval(() => {
|
|
488
|
+
const currentMsgs = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json'));
|
|
489
|
+
if (currentMsgs.length > lastMsgCount) {
|
|
490
|
+
const newMsgs = currentMsgs.slice(lastMsgCount - currentMsgs.length);
|
|
491
|
+
for (const f of newMsgs) {
|
|
492
|
+
try {
|
|
493
|
+
const msg = JSON.parse(fs.readFileSync(path.join(inboxDir, f), 'utf-8'));
|
|
494
|
+
if (msg.metadata?.channel === channelId) {
|
|
495
|
+
process.stdout.write(` 💬 [${msg.from}] ${msg.message.replace(`[${channelId}] `, '')}\n`);
|
|
496
|
+
}
|
|
497
|
+
} catch {}
|
|
498
|
+
}
|
|
499
|
+
lastMsgCount = currentMsgs.length;
|
|
500
|
+
}
|
|
501
|
+
}, 2000);
|
|
502
|
+
|
|
503
|
+
process.on('SIGINT', () => {
|
|
504
|
+
clearInterval(interval);
|
|
505
|
+
clearInterval(msgInterval);
|
|
506
|
+
process.stdout.write('👋 Unsubscribed.\n');
|
|
507
|
+
process.exit(0);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Events (new) ──
|
|
512
|
+
|
|
513
|
+
function cmdEvents(since) {
|
|
514
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
515
|
+
const eventsFile = path.join(EVENTS_DIR, `${today}.jsonl`);
|
|
516
|
+
if (!fs.existsSync(eventsFile)) {
|
|
517
|
+
console.log('No events today.');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const lines = fs.readFileSync(eventsFile, 'utf-8').split('\n').filter(Boolean);
|
|
522
|
+
let events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
523
|
+
|
|
524
|
+
if (since) {
|
|
525
|
+
const sinceTs = new Date(since).getTime();
|
|
526
|
+
events = events.filter(e => new Date(e.timestamp).getTime() >= sinceTs);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
events = events.slice(-50);
|
|
530
|
+
|
|
531
|
+
if (events.length === 0) {
|
|
532
|
+
console.log('No matching events.');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
console.log(`\n📋 System Events (${events.length} shown):`);
|
|
537
|
+
console.log('─'.repeat(60));
|
|
538
|
+
for (const ev of events) {
|
|
539
|
+
const ago = msAgo(new Date(ev.timestamp).getTime());
|
|
540
|
+
const icon = ev.type === 'agent_joined' ? '🟢' :
|
|
541
|
+
ev.type === 'agent_offline' ? '⚫' :
|
|
542
|
+
ev.type === 'welcome_sent' ? '👋' :
|
|
543
|
+
ev.type === 'channel_created' ? '📢' :
|
|
544
|
+
ev.type === 'profile_updated' ? '✏️' :
|
|
545
|
+
ev.type === 'status_change' ? '🔄' : '📌';
|
|
546
|
+
console.log(` ${icon} ${ev.type.padEnd(20)} ${ev.data?.agent || ev.data?.channel || ''} (${ago})`);
|
|
547
|
+
if (ev.data?.reason) console.log(` reason: ${ev.data.reason}`);
|
|
548
|
+
}
|
|
549
|
+
console.log();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── Channel ──
|
|
553
|
+
|
|
554
|
+
function cmdChannel(subArgs) {
|
|
555
|
+
const sub = subArgs[0] || 'list';
|
|
556
|
+
|
|
557
|
+
if (sub === 'list') {
|
|
558
|
+
if (!fs.existsSync(CHANNELS_DIR)) {
|
|
559
|
+
console.log('No channels');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const files = fs.readdirSync(CHANNELS_DIR).filter(f => f.endsWith('.json'));
|
|
563
|
+
console.log('\n📢 Channels:');
|
|
564
|
+
console.log('─'.repeat(50));
|
|
565
|
+
for (const f of files) {
|
|
566
|
+
const ch = JSON.parse(fs.readFileSync(path.join(CHANNELS_DIR, f), 'utf-8'));
|
|
567
|
+
console.log(` ${ch.id.padEnd(20)} ${ch.topic || ''} (${ch.members.length} members)`);
|
|
568
|
+
}
|
|
569
|
+
console.log();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (sub === 'create') {
|
|
574
|
+
const chId = subArgs[1];
|
|
575
|
+
const topic = subArgs.slice(2).join(' ') || '';
|
|
576
|
+
if (!chId) { console.log('Usage: node coco-cli.js channel create <name> [topic]'); return; }
|
|
577
|
+
const ch = { id: chId, topic, created_by: process.env.COCO_AGENT || 'cli', created_at: new Date().toISOString(), members: [] };
|
|
578
|
+
fs.writeFileSync(path.join(CHANNELS_DIR, `${chId}.json`), JSON.stringify(ch, null, 2), 'utf-8');
|
|
579
|
+
console.log(`✅ Channel "#${chId}" created`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (sub === 'join') {
|
|
584
|
+
const chId = subArgs[1];
|
|
585
|
+
const agentName = subArgs[2] || process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
586
|
+
if (!chId) { console.log('Usage: node coco-cli.js channel join <name> [agent]'); return; }
|
|
587
|
+
const chPath = path.join(CHANNELS_DIR, `${chId}.json`);
|
|
588
|
+
if (!fs.existsSync(chPath)) { console.log(`❌ Channel "${chId}" not found`); return; }
|
|
589
|
+
const ch = JSON.parse(fs.readFileSync(chPath, 'utf-8'));
|
|
590
|
+
if (!ch.members.includes(agentName)) ch.members.push(agentName);
|
|
591
|
+
fs.writeFileSync(chPath, JSON.stringify(ch, null, 2), 'utf-8');
|
|
592
|
+
console.log(`✅ Joined channel "#${chId}" as "${agentName}"`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (sub === 'send') {
|
|
597
|
+
const channelId = subArgs[1];
|
|
598
|
+
const text = subArgs.slice(2).join(' ');
|
|
599
|
+
if (!channelId || !text) {
|
|
600
|
+
console.log('Usage: node coco-cli.js channel send <channel> <message>');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const from = process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
604
|
+
const chPath = path.join(CHANNELS_DIR, `${channelId}.json`);
|
|
605
|
+
if (!fs.existsSync(chPath)) {
|
|
606
|
+
console.log(`❌ Channel "${channelId}" not found`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const channel = JSON.parse(fs.readFileSync(chPath, 'utf-8'));
|
|
611
|
+
const msg = {
|
|
612
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
613
|
+
channel: channelId,
|
|
614
|
+
from,
|
|
615
|
+
message: text,
|
|
616
|
+
timestamp: new Date().toISOString(),
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const logDir = path.join(CHANNELS_DIR, channelId, 'log');
|
|
620
|
+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
621
|
+
fs.writeFileSync(path.join(logDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
|
|
622
|
+
|
|
623
|
+
for (const member of channel.members) {
|
|
624
|
+
if (member !== from) {
|
|
625
|
+
const memberInbox = path.join(MSGS_DIR, member);
|
|
626
|
+
if (!fs.existsSync(memberInbox)) fs.mkdirSync(memberInbox, { recursive: true });
|
|
627
|
+
const dm = { ...msg, to: member, metadata: { channel: channelId } };
|
|
628
|
+
fs.writeFileSync(path.join(memberInbox, `${msg.id}.json`), JSON.stringify(dm, null, 2), 'utf-8');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
console.log(`✅ Sent to channel "#${channelId}" (${channel.members.length - 1} members notified)`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (sub === 'log' || sub === 'history') {
|
|
636
|
+
const channelId = subArgs[1];
|
|
637
|
+
const logDir = path.join(CHANNELS_DIR, channelId, 'log');
|
|
638
|
+
if (!fs.existsSync(logDir)) {
|
|
639
|
+
console.log(`No messages in channel "#${channelId}"`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const files = fs.readdirSync(logDir).filter(f => f.endsWith('.json')).sort().slice(-20);
|
|
643
|
+
console.log(`\n📢 Channel "#${channelId}" history:`);
|
|
644
|
+
console.log('─'.repeat(60));
|
|
645
|
+
for (const f of files) {
|
|
646
|
+
const msg = JSON.parse(fs.readFileSync(path.join(logDir, f), 'utf-8'));
|
|
647
|
+
const ago = msAgo(new Date(msg.timestamp).getTime());
|
|
648
|
+
console.log(` [${msg.from}] ${msg.message.substring(0, 100)} (${ago})`);
|
|
649
|
+
}
|
|
650
|
+
console.log();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ── Watch ──
|
|
656
|
+
|
|
657
|
+
function cmdWatch(agentName) {
|
|
658
|
+
const agent = agentName || process.env.COCO_AGENT || process.env.USER || 'anonymous';
|
|
659
|
+
const dir = path.join(MSGS_DIR, agent);
|
|
660
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
661
|
+
|
|
662
|
+
console.log(`👀 Watching for messages for "${agent}"... (Ctrl+C to stop)\n`);
|
|
663
|
+
|
|
664
|
+
let lastCount = fs.readdirSync(dir).filter(f => f.endsWith('.json')).length;
|
|
665
|
+
|
|
666
|
+
const interval = setInterval(() => {
|
|
667
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
668
|
+
if (files.length > lastCount) {
|
|
669
|
+
const newFiles = files.slice(lastCount - files.length);
|
|
670
|
+
for (const f of newFiles) {
|
|
671
|
+
try {
|
|
672
|
+
const msg = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
673
|
+
const tag = msg.metadata?.channel ? `[#${msg.metadata.channel}]` : '';
|
|
674
|
+
console.log(`\n📩 ${tag} [${msg.from}] ${msg.message}`);
|
|
675
|
+
console.log(` Reply: node coco-cli.js reply ${msg.id} "your message"\n`);
|
|
676
|
+
} catch {}
|
|
677
|
+
}
|
|
678
|
+
lastCount = files.length;
|
|
679
|
+
}
|
|
680
|
+
}, 2000);
|
|
681
|
+
|
|
682
|
+
process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ── Doctor ──
|
|
686
|
+
|
|
687
|
+
function cmdDoctor(args) {
|
|
688
|
+
const { runDiagnostics } = require('./lib/doctor');
|
|
689
|
+
const opts = {
|
|
690
|
+
busDir: BUS_DIR,
|
|
691
|
+
quick: args.includes('--quick'),
|
|
692
|
+
fix: args.includes('--fix'),
|
|
693
|
+
report: args.includes('--report'),
|
|
694
|
+
watch: args.includes('--watch'),
|
|
695
|
+
};
|
|
696
|
+
if (opts.watch) {
|
|
697
|
+
const { watchDiagnostics } = require('./lib/doctor');
|
|
698
|
+
watchDiagnostics(opts);
|
|
699
|
+
} else {
|
|
700
|
+
runDiagnostics(opts);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ── Tunnel ──
|
|
705
|
+
|
|
706
|
+
function cmdTunnel(args) {
|
|
707
|
+
const { startServer, startClient, startSync, printSSHHelp } = require('./lib/tunnel');
|
|
708
|
+
const mode = args[0];
|
|
709
|
+
if (!mode || mode === '--help') {
|
|
710
|
+
console.log('Usage: coco tunnel server|client|sync|ssh [options]');
|
|
711
|
+
console.log(' coco tunnel server --port 9090 --secret <token>');
|
|
712
|
+
console.log(' coco tunnel client --host <ip> --port 9090 --secret <token>');
|
|
713
|
+
console.log(' coco tunnel sync --remote http://<ip>:9090');
|
|
714
|
+
console.log(' coco tunnel ssh --remote user@host');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const opts = { busDir: BUS_DIR };
|
|
718
|
+
for (let i = 1; i < args.length; i++) {
|
|
719
|
+
switch (args[i]) {
|
|
720
|
+
case '--port': opts.port = parseInt(args[++i], 10); break;
|
|
721
|
+
case '--secret': opts.secret = args[++i]; break;
|
|
722
|
+
case '--host': opts.host = args[++i]; break;
|
|
723
|
+
case '--remote': opts.remote = args[++i]; break;
|
|
724
|
+
case '--interval': opts.interval = parseInt(args[++i], 10); break;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
switch (mode) {
|
|
728
|
+
case 'server': startServer(opts); break;
|
|
729
|
+
case 'client': startClient(opts).catch(e => console.error('Error:', e.message)); break;
|
|
730
|
+
case 'sync': startSync(opts).catch(e => console.error('Error:', e.message)); break;
|
|
731
|
+
case 'ssh': printSSHHelp(opts); break;
|
|
732
|
+
default: console.log('Unknown tunnel mode:', mode);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ── Memory ──
|
|
737
|
+
|
|
738
|
+
async function cmdMemory(args) {
|
|
739
|
+
const { MemoryStore } = require('./lib/memory');
|
|
740
|
+
const mem = new MemoryStore(BUS_DIR);
|
|
741
|
+
const sub = args[0];
|
|
742
|
+
if (!sub) { console.log('Usage: coco memory store|search|recall|list|forget|stats|archive|clear|configure|rebuild <args>'); return; }
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
switch (sub) {
|
|
746
|
+
case 'store': {
|
|
747
|
+
const agent = args[1];
|
|
748
|
+
const content = args.slice(2).filter(a => !a.startsWith('--')).join(' ');
|
|
749
|
+
const key = args.includes('--key') ? args[args.indexOf('--key') + 1] : undefined;
|
|
750
|
+
const namespace = args.includes('--namespace') ? args[args.indexOf('--namespace') + 1] : undefined;
|
|
751
|
+
const ttl = args.includes('--ttl') ? parseInt(args[args.indexOf('--ttl') + 1], 10) : undefined;
|
|
752
|
+
if (!agent || !content) { console.log('Usage: coco memory store <agent> <content> [--key] [--namespace] [--ttl]'); return; }
|
|
753
|
+
const id = await mem.store(agent, { content, key, namespace, ttl });
|
|
754
|
+
console.log(' ✅ Memory stored: ' + id);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case 'search': {
|
|
758
|
+
const agent = args[1];
|
|
759
|
+
const query = args.slice(2).filter(a => !a.startsWith('--')).join(' ');
|
|
760
|
+
const limit = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1], 10) : 10;
|
|
761
|
+
const namespace = args.includes('--namespace') ? args[args.indexOf('--namespace') + 1] : undefined;
|
|
762
|
+
if (!agent || !query) { console.log('Usage: coco memory search <agent> <query> [--limit] [--namespace]'); return; }
|
|
763
|
+
const results = await mem.search(agent, query, { limit, namespace });
|
|
764
|
+
if (results.length === 0) { console.log(' No results found.'); return; }
|
|
765
|
+
console.log('');
|
|
766
|
+
console.log(' Memory Search - ' + results.length + ' result(s) for "' + query + '":');
|
|
767
|
+
console.log('');
|
|
768
|
+
for (const r of results) {
|
|
769
|
+
const tag = r.search_type === 'vector' ? '[V]' : '[K]';
|
|
770
|
+
console.log(' ' + tag + ' [' + r.score.toFixed(2) + '] ' + r.id.substring(0, 20) + '... ' + r.content.substring(0, 80));
|
|
771
|
+
if (r.key) console.log(' key: ' + r.key);
|
|
772
|
+
console.log('');
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
case 'recall': {
|
|
777
|
+
const agent = args[1]; const id = args[2];
|
|
778
|
+
if (!agent || !id) { console.log('Usage: coco memory recall <agent> <id>'); return; }
|
|
779
|
+
const e = mem.recall(agent, id);
|
|
780
|
+
if (!e) { console.log(' Not found.'); return; }
|
|
781
|
+
console.log('');
|
|
782
|
+
console.log(' Memory: ' + e.id);
|
|
783
|
+
if (e.key) console.log(' Key: ' + e.key);
|
|
784
|
+
console.log(' Namespace: ' + e.namespace);
|
|
785
|
+
console.log(' Content: ' + e.content);
|
|
786
|
+
console.log(' Stored: ' + e.metadata.stored_at);
|
|
787
|
+
console.log('');
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case 'list': {
|
|
791
|
+
const agent = args[1];
|
|
792
|
+
if (!agent) { console.log('Usage: coco memory list <agent> [--namespace] [--limit] [--offset]'); return; }
|
|
793
|
+
const limit = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1], 10) : 50;
|
|
794
|
+
const offset = args.includes('--offset') ? parseInt(args[args.indexOf('--offset') + 1], 10) : 0;
|
|
795
|
+
const namespace = args.includes('--namespace') ? args[args.indexOf('--namespace') + 1] : undefined;
|
|
796
|
+
const list = mem.list(agent, { namespace, limit, offset });
|
|
797
|
+
if (list.length === 0) { console.log(' No memories.'); return; }
|
|
798
|
+
console.log('');
|
|
799
|
+
console.log(' Memories for ' + agent + ' (' + list.length + '):');
|
|
800
|
+
console.log('');
|
|
801
|
+
for (const m of list) console.log(' ' + m.id.substring(0, 24) + ' [' + m.namespace + '] ' + m.content.substring(0, 70));
|
|
802
|
+
console.log('');
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case 'forget': {
|
|
806
|
+
const agent = args[1]; const id = args[2];
|
|
807
|
+
if (!agent || !id) { console.log('Usage: coco memory forget <agent> <id>'); return; }
|
|
808
|
+
const ok = mem.forget(agent, id);
|
|
809
|
+
console.log(ok ? ' Deleted.' : ' Not found.');
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
case 'stats': {
|
|
813
|
+
const agent = args[1];
|
|
814
|
+
if (!agent) { console.log('Usage: coco memory stats <agent>'); return; }
|
|
815
|
+
const s = mem.stats(agent);
|
|
816
|
+
console.log('');
|
|
817
|
+
console.log(' Memory Stats - ' + agent + ':');
|
|
818
|
+
console.log('');
|
|
819
|
+
console.log(' Total: ' + s.total);
|
|
820
|
+
console.log(' Namespaces: ' + JSON.stringify(s.namespaces));
|
|
821
|
+
console.log(' Size: ' + (s.size_bytes / 1024).toFixed(2) + ' KB');
|
|
822
|
+
console.log(' Vectors: ' + s.vectors + ' (dim ' + s.vector_dimension + ')');
|
|
823
|
+
console.log(' Embedding: ' + s.embed_provider);
|
|
824
|
+
console.log(' Oldest: ' + (s.oldest || 'N/A'));
|
|
825
|
+
console.log(' Newest: ' + (s.newest || 'N/A'));
|
|
826
|
+
console.log('');
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
case 'archive': {
|
|
830
|
+
const agent = args[1];
|
|
831
|
+
if (!agent) { console.log('Usage: coco memory archive <agent> [--max-age-days] [--no-delete]'); return; }
|
|
832
|
+
const maxAge = args.includes('--max-age-days') ? parseInt(args[args.indexOf('--max-age-days') + 1], 10) : 7;
|
|
833
|
+
const deleteAfter = !args.includes('--no-delete');
|
|
834
|
+
const r = await mem.archive(agent, { maxAgeDays: maxAge, deleteAfter });
|
|
835
|
+
console.log(' Archived ' + r.archived + ' message(s) for ' + agent + (deleteAfter ? ' (deleted originals)' : ''));
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case 'clear': {
|
|
839
|
+
const agent = args[1];
|
|
840
|
+
const ns = args.includes('--namespace') ? args[args.indexOf('--namespace') + 1] : undefined;
|
|
841
|
+
if (!agent) { console.log('Usage: coco memory clear <agent> [--namespace]'); return; }
|
|
842
|
+
const count = mem.clear(agent, ns);
|
|
843
|
+
console.log(' Cleared ' + count + ' entries' + (ns ? ' in namespace "' + ns + '"' : ''));
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case 'configure': {
|
|
847
|
+
const provider = args.includes('--provider') ? args[args.indexOf('--provider') + 1] : undefined;
|
|
848
|
+
const endpoint = args.includes('--endpoint') ? args[args.indexOf('--endpoint') + 1] : undefined;
|
|
849
|
+
const model = args.includes('--model') ? args[args.indexOf('--model') + 1] : undefined;
|
|
850
|
+
const apiKey = args.includes('--api-key') ? args[args.indexOf('--api-key') + 1] : undefined;
|
|
851
|
+
if (!provider) { console.log('Usage: coco memory configure --provider <keyword|ollama|openai|custom> [--endpoint] [--model] [--api-key]'); return; }
|
|
852
|
+
const cfg = mem.configure({ provider, endpoint, model, api_key: apiKey });
|
|
853
|
+
console.log(' Configured: ' + cfg.provider);
|
|
854
|
+
if (cfg.model) console.log(' Model: ' + cfg.model);
|
|
855
|
+
if (cfg.endpoint) console.log(' Endpoint: ' + cfg.endpoint);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case 'rebuild': {
|
|
859
|
+
const agent = args[1];
|
|
860
|
+
if (!agent) { console.log('Usage: coco memory rebuild <agent>'); return; }
|
|
861
|
+
const r = await mem.rebuildVectors(agent);
|
|
862
|
+
console.log(' Rebuilt ' + r.rebuilt + ' vectors.');
|
|
863
|
+
if (r.failed > 0) console.log(' Failed: ' + r.failed);
|
|
864
|
+
if (r.reason) console.log(' ' + r.reason);
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
default:
|
|
868
|
+
console.log('Usage: coco memory store|search|recall|list|forget|stats|archive|clear|configure|rebuild');
|
|
869
|
+
}
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.error('Error:', err.message);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ── Backup ──
|
|
876
|
+
|
|
877
|
+
function cmdBackup(args) {
|
|
878
|
+
const { createBackup, restoreBackup, listBackups, backupInfo, diffBackup, cleanupBackups, autoBackup, watchMode } = require('./lib/backup');
|
|
879
|
+
const cmd = args[0];
|
|
880
|
+
try {
|
|
881
|
+
if (!cmd) { createBackup(BUS_DIR); return; }
|
|
882
|
+
switch (cmd) {
|
|
883
|
+
case '--list': listBackups(BUS_DIR); break;
|
|
884
|
+
case '--info': backupInfo(path.resolve(args[1])); break;
|
|
885
|
+
case '--restore': restoreBackup(path.resolve(args[1]), BUS_DIR); break;
|
|
886
|
+
case '--diff': diffBackup(path.resolve(args[1]), BUS_DIR); break;
|
|
887
|
+
case '--cleanup': cleanupBackups(BUS_DIR, parseInt(args[1], 10) || 30); break;
|
|
888
|
+
case '--auto': autoBackup(BUS_DIR); break;
|
|
889
|
+
case '--watch': watchMode(BUS_DIR, parseInt(args[1], 10) || 30); break;
|
|
890
|
+
default: console.log('Usage: coco backup [--list|--info|--restore|--diff|--cleanup|--auto|--watch]');
|
|
891
|
+
}
|
|
892
|
+
} catch (err) {
|
|
893
|
+
console.error('Error:', err.message);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ── Help ──
|
|
898
|
+
|
|
899
|
+
function cmdHelp() {
|
|
900
|
+
console.log(`
|
|
901
|
+
╔═══════════════════════════════════════════════╗
|
|
902
|
+
║ MCP CoCo v2.1 — CLI Agent Bus Interface ║
|
|
903
|
+
╚═══════════════════════════════════════════════╝
|
|
904
|
+
|
|
905
|
+
Usage:
|
|
906
|
+
node coco-cli.js <command> [args]
|
|
907
|
+
|
|
908
|
+
Commands:
|
|
909
|
+
agents [filters] List all agents on the bus
|
|
910
|
+
filters: --online, --status, --capability, --tag, --model, --search
|
|
911
|
+
|
|
912
|
+
whoami Show your agent identity
|
|
913
|
+
inbox [agent] [--from X] Check messages (default: \$COCO_AGENT)
|
|
914
|
+
send <to> <message> Send a message to another agent
|
|
915
|
+
reply <msg-id> <message> Reply to an incoming message
|
|
916
|
+
profile [agent] Show agent profile details
|
|
917
|
+
profile edit --status busy Edit your profile fields
|
|
918
|
+
search <query> Search agents by name/capability/description
|
|
919
|
+
status Show CoCo bus health & online agents
|
|
920
|
+
subscribe <channel> Listen to channel messages in real-time
|
|
921
|
+
|
|
922
|
+
channel list List all channels
|
|
923
|
+
channel create <name> [topic] Create a channel
|
|
924
|
+
channel join <name> [agent] Join a channel
|
|
925
|
+
channel send <name> <message> Send to a channel
|
|
926
|
+
channel history <name> Read channel history
|
|
927
|
+
events [since] View system events (agent join/leave, etc.)
|
|
928
|
+
watch [agent] Watch for new messages (poll every 2s)
|
|
929
|
+
|
|
930
|
+
doctor [--quick] [--fix] Run bus diagnostics & health check
|
|
931
|
+
doctor --report Save report to .bus/diagnostic-report.json
|
|
932
|
+
doctor --watch Watch mode (check every 30s)
|
|
933
|
+
|
|
934
|
+
tunnel server [options] Start tunnel server (receiving end)
|
|
935
|
+
--port, --secret
|
|
936
|
+
tunnel client [options] Start tunnel client (sending end)
|
|
937
|
+
--host, --port, --secret, --interval
|
|
938
|
+
tunnel sync [options] Bidirectional bus sync
|
|
939
|
+
--remote, --secret, --interval
|
|
940
|
+
tunnel ssh [options] Show SSH tunnel instructions
|
|
941
|
+
--remote, --port
|
|
942
|
+
|
|
943
|
+
backup Create timestamped backup
|
|
944
|
+
backup --list List available backups
|
|
945
|
+
backup --info <file> Show backup metadata
|
|
946
|
+
backup --restore <file> Restore bus from backup
|
|
947
|
+
backup --diff <file> Compare backup with current state
|
|
948
|
+
backup --cleanup [days] Remove backups older than N days
|
|
949
|
+
backup --auto Backup only if changes detected
|
|
950
|
+
backup --watch <minutes> Auto-backup every N minutes
|
|
951
|
+
|
|
952
|
+
memory store <agent> <content> Store a memory for an agent
|
|
953
|
+
--key <key> Optional lookup key
|
|
954
|
+
--namespace <ns> Namespace (default: default)
|
|
955
|
+
memory search <agent> <query> Search agent memories
|
|
956
|
+
--limit <n>, --namespace <ns>
|
|
957
|
+
memory recall <agent> <id> Recall a memory by ID or key
|
|
958
|
+
memory list <agent> List memories
|
|
959
|
+
--namespace <ns>, --limit <n>, --offset <n>
|
|
960
|
+
memory forget <agent> <id> Delete a memory
|
|
961
|
+
memory stats <agent> Show memory statistics
|
|
962
|
+
memory archive <agent> Archive old messages to memory
|
|
963
|
+
--max-age-days <n>, --no-delete
|
|
964
|
+
memory clear <agent> Clear all memories for an agent
|
|
965
|
+
--namespace <ns>
|
|
966
|
+
memory configure Configure embedding provider for vector search
|
|
967
|
+
--provider <keyword|ollama|openai|custom> [--endpoint] [--model] [--api-key]
|
|
968
|
+
memory rebuild <agent> Rebuild vector embeddings for all memories
|
|
969
|
+
|
|
970
|
+
Environment:
|
|
971
|
+
COCO_AGENT=opencode Set your agent name for inbox/send
|
|
972
|
+
|
|
973
|
+
Examples:
|
|
974
|
+
node coco-cli.js agents --online --capability code-review
|
|
975
|
+
node coco-cli.js profile opencode
|
|
976
|
+
node coco-cli.js search web
|
|
977
|
+
node coco-cli.js status
|
|
978
|
+
node coco-cli.js subscribe dev-chat
|
|
979
|
+
node coco-cli.js channel create dev-chat "Development chat"
|
|
980
|
+
`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// ── Utils ──
|
|
984
|
+
|
|
985
|
+
function loadAgents() {
|
|
986
|
+
try {
|
|
987
|
+
if (fs.existsSync(AGENTS_FILE)) {
|
|
988
|
+
return JSON.parse(fs.readFileSync(AGENTS_FILE, 'utf-8'));
|
|
989
|
+
}
|
|
990
|
+
} catch {}
|
|
991
|
+
return {};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function msAgo(ts) {
|
|
995
|
+
const diff = Date.now() - ts;
|
|
996
|
+
if (diff < 60000) return 'just now';
|
|
997
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
998
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
999
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
main();
|