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/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();