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/bridge.js ADDED
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CoCo External Bridge — Connect external platforms to the Agent Bus
4
+ *
5
+ * Generic bridge pattern that can be adapted for:
6
+ * - Slack (Web API + Events API)
7
+ * - Discord (Webhook + Gateway)
8
+ * - Matrix, IRC, Telegram, etc.
9
+ *
10
+ * Each bridge runs as a bus agent, forwarding messages between
11
+ * the bus and the external platform.
12
+ *
13
+ * Usage:
14
+ * node bridge.js slack # Start Slack bridge (configured via .bridge.json)
15
+ * node bridge.js discord # Start Discord bridge
16
+ * node bridge.js generic # Start generic webhook bridge
17
+ * node bridge.js --help # Show config help
18
+ */
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const http = require('http');
22
+
23
+ const BUS_DIR = path.join(__dirname, '.bus');
24
+ const MSGS_DIR = path.join(BUS_DIR, 'messages');
25
+ const CHANNEL_MAP_FILE = path.join(BUS_DIR, 'channel-map.json');
26
+ const BRIDGE_CONFIG = path.join(__dirname, 'bridge-config.json');
27
+
28
+ // ── Bridge Registry ────────────────────────────────────
29
+
30
+ const bridges = {};
31
+
32
+ class GenericBridge {
33
+ constructor(type, config) {
34
+ this.type = type;
35
+ this.config = config;
36
+ this.agentName = `bridge:${type}`;
37
+ this.mappings = this._loadMappings();
38
+ this._running = false;
39
+ this._server = null;
40
+ }
41
+
42
+ _loadMappings() {
43
+ try {
44
+ if (fs.existsSync(CHANNEL_MAP_FILE)) {
45
+ return JSON.parse(fs.readFileSync(CHANNEL_MAP_FILE, 'utf-8'));
46
+ }
47
+ } catch {}
48
+ return { channels: {}, dms: {} };
49
+ }
50
+
51
+ _saveMappings() {
52
+ fs.writeFileSync(CHANNEL_MAP_FILE, JSON.stringify(this.mappings, null, 2), 'utf-8');
53
+ }
54
+
55
+ registerAgent(description) {
56
+ const agentsFile = path.join(BUS_DIR, 'agents.json');
57
+ const agents = fs.existsSync(agentsFile) ? JSON.parse(fs.readFileSync(agentsFile, 'utf-8')) : {};
58
+ agents[this.agentName] = {
59
+ name: this.agentName,
60
+ description,
61
+ capabilities: ['bridge', this.type, 'messaging'],
62
+ tags: ['bridge', this.type],
63
+ status: 'idle',
64
+ version: '1.0.0',
65
+ last_seen: new Date().toISOString(),
66
+ registered_at: agents[this.agentName]?.registered_at || new Date().toISOString(),
67
+ };
68
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2), 'utf-8');
69
+ console.log(` ✅ Registered as "${this.agentName}" on CoCo Bus`);
70
+ }
71
+
72
+ // Map external channel → bus channel
73
+ mapChannel(externalId, busChannelId) {
74
+ this.mappings.channels[externalId] = busChannelId;
75
+ this._saveMappings();
76
+ console.log(` 🔗 Mapped ${this.type}:${externalId} → #${busChannelId}`);
77
+ }
78
+
79
+ getBusChannel(externalId) {
80
+ return this.mappings.channels[externalId] || null;
81
+ }
82
+
83
+ // Send a message TO the bus
84
+ postToBus(channelId, from, message, metadata = {}) {
85
+ const chPath = path.join(CHANNELS_DIR, `${channelId}.json`);
86
+ if (!fs.existsSync(chPath)) return { ok: false, error: 'Channel not found' };
87
+
88
+ const ch = JSON.parse(fs.readFileSync(chPath, 'utf-8'));
89
+ const msg = {
90
+ id: `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
91
+ channel: channelId,
92
+ from: `${this.agentName}:${from}`,
93
+ message,
94
+ metadata: { ...metadata, source: this.type },
95
+ timestamp: new Date().toISOString(),
96
+ };
97
+
98
+ const logDir = path.join(CHANNELS_DIR, channelId, 'log');
99
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
100
+ fs.writeFileSync(path.join(logDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
101
+
102
+ for (const member of ch.members) {
103
+ if (member !== this.agentName) {
104
+ const inboxDir = path.join(MSGS_DIR, member);
105
+ if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
106
+ fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify({ ...msg, to: member }, null, 2), 'utf-8');
107
+ }
108
+ }
109
+
110
+ return { ok: true, message_id: msg.id };
111
+ }
112
+
113
+ // Watch bus channel → forward to external (override in subclasses)
114
+ start() {
115
+ this._running = true;
116
+ console.log(` 👂 Bridge "${this.type}" watching...`);
117
+ }
118
+
119
+ stop() {
120
+ this._running = false;
121
+ if (this._server) this._server.close();
122
+ }
123
+ }
124
+
125
+ // ── Slack Bridge (Web API via outgoing webhook) ────────
126
+
127
+ class SlackBridge extends GenericBridge {
128
+ constructor(config) {
129
+ super('slack', config);
130
+ this.webhookUrl = config.webhook_url || null;
131
+ this.signingSecret = config.signing_secret || '';
132
+ }
133
+
134
+ postToSlack(channel, text) {
135
+ if (!this.webhookUrl) return;
136
+ // In production, use @slack/web-api or fetch
137
+ console.log(` 📤 [Slack → ${channel}] ${text.substring(0, 60)}`);
138
+ }
139
+
140
+ postToBusFromSlack(channelId, userId, text) {
141
+ return this.postToBus(channelId, userId, text);
142
+ }
143
+ }
144
+
145
+ // ── Discord Bridge (Webhook) ───────────────────────────
146
+
147
+ class DiscordBridge extends GenericBridge {
148
+ constructor(config) {
149
+ super('discord', config);
150
+ this.webhookUrl = config.webhook_url || null;
151
+ this.botToken = config.bot_token || null;
152
+ }
153
+
154
+ postToDiscord(channel, text) {
155
+ if (!this.webhookUrl) return;
156
+ console.log(` 📤 [Discord → ${channel}] ${text.substring(0, 60)}`);
157
+ }
158
+ }
159
+
160
+ // ── HTTP Bridge Server ──────────────────────────────────
161
+
162
+ function startBridgeListener(bridge, port = 9090) {
163
+ const server = http.createServer((req, res) => {
164
+ res.setHeader('Access-Control-Allow-Origin', '*');
165
+
166
+ if (req.method === 'GET') {
167
+ res.writeHead(200, { 'Content-Type': 'application/json' });
168
+ res.end(JSON.stringify({
169
+ bridge: bridge.type,
170
+ mappings: bridge.mappings,
171
+ status: 'running',
172
+ }));
173
+ return;
174
+ }
175
+
176
+ if (req.method === 'POST') {
177
+ let body = '';
178
+ req.on('data', c => body += c);
179
+ req.on('end', () => {
180
+ try {
181
+ const data = JSON.parse(body);
182
+ const { action, ...params } = data;
183
+
184
+ switch (action) {
185
+ case 'map':
186
+ bridge.mapChannel(params.external_id, params.bus_channel);
187
+ res.writeHead(200);
188
+ res.end(JSON.stringify({ ok: true }));
189
+ break;
190
+
191
+ case 'send':
192
+ bridge.postToBusFromSlack
193
+ ? bridge.postToBusFromSlack(params.channel, params.user, params.text)
194
+ : bridge.postToBus(params.channel, params.user, params.text);
195
+ res.writeHead(200);
196
+ res.end(JSON.stringify({ ok: true }));
197
+ break;
198
+
199
+ default:
200
+ res.writeHead(400);
201
+ res.end(JSON.stringify({ error: 'Unknown action' }));
202
+ }
203
+ } catch (err) {
204
+ res.writeHead(400);
205
+ res.end(JSON.stringify({ error: err.message }));
206
+ }
207
+ });
208
+ return;
209
+ }
210
+ });
211
+
212
+ bridge._server = server;
213
+ server.listen(port, () => {
214
+ console.log(` 🌐 ${bridge.type} bridge listening on :${port}`);
215
+ });
216
+ }
217
+
218
+ // ── Main ──
219
+
220
+ const CHANNELS_DIR = path.join(BUS_DIR, 'channels');
221
+
222
+ function help() {
223
+ console.log(`
224
+ ╔═══════════════════════════════════════════════╗
225
+ ║ MCP CoCo — External Platform Bridge ║
226
+ ╚═══════════════════════════════════════════════╝
227
+
228
+ Usage:
229
+ node bridge.js slack [port] Start Slack webhook bridge
230
+ node bridge.js discord [port] Start Discord webhook bridge
231
+ node bridge.js generic [port] Start generic webhook bridge
232
+
233
+ Configuration (bridge-config.json):
234
+ {
235
+ "slack": {
236
+ "webhook_url": "https://hooks.slack.com/...",
237
+ "signing_secret": "..."
238
+ },
239
+ "discord": {
240
+ "webhook_url": "https://discord.com/api/webhooks/...",
241
+ "bot_token": "..."
242
+ }
243
+ }
244
+
245
+ Channel Mapping:
246
+ POST /api/map -d '{"action":"map","external_id":"C123","bus_channel":"dev"}'
247
+ POST /api/send -d '{"action":"send","channel":"dev","user":"john","text":"Hello"}"
248
+ `);
249
+ }
250
+
251
+ async function main() {
252
+ const type = process.argv[2];
253
+ const port = parseInt(process.argv[3], 10) || 9090;
254
+
255
+ if (!type || type === '--help') {
256
+ help();
257
+ return;
258
+ }
259
+
260
+ // Load config
261
+ const config = fs.existsSync(BRIDGE_CONFIG)
262
+ ? JSON.parse(fs.readFileSync(BRIDGE_CONFIG, 'utf-8'))
263
+ : {};
264
+
265
+ let bridge;
266
+ switch (type) {
267
+ case 'slack':
268
+ bridge = new SlackBridge(config.slack || {});
269
+ break;
270
+ case 'discord':
271
+ bridge = new DiscordBridge(config.discord || {});
272
+ break;
273
+ case 'generic':
274
+ bridge = new GenericBridge('generic', config.generic || {});
275
+ break;
276
+ default:
277
+ console.error(`Unknown bridge type: ${type}`);
278
+ help();
279
+ process.exit(1);
280
+ }
281
+
282
+ console.log(`
283
+ ╔══════════════════════════════════════════════╗
284
+ ║ MCP CoCo — ${type.padEnd(22)}║
285
+ ╚══════════════════════════════════════════════╝
286
+ `);
287
+
288
+ bridge.registerAgent(`${type} bridge — connects ${type} to CoCo Bus`);
289
+ startBridgeListener(bridge, port);
290
+ bridge.start();
291
+
292
+ // Watch bus for outgoing messages (poll)
293
+ const inboxDir = path.join(MSGS_DIR, bridge.agentName);
294
+ if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
295
+ let lastCount = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json')).length;
296
+
297
+ setInterval(() => {
298
+ if (!fs.existsSync(inboxDir)) return;
299
+ const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json'));
300
+ if (files.length > lastCount) {
301
+ const newFiles = files.slice(lastCount - files.length);
302
+ for (const f of newFiles) {
303
+ try {
304
+ const msg = JSON.parse(fs.readFileSync(path.join(inboxDir, f), 'utf-8'));
305
+ // Forward to external platform
306
+ if (bridge.type === 'slack') {
307
+ console.log(` 📬 [Bus → Slack/${msg.metadata?.channel || 'DM'}] ${msg.message.substring(0, 80)}`);
308
+ } else if (bridge.type === 'discord') {
309
+ console.log(` 📬 [Bus → Discord/${msg.metadata?.channel || 'DM'}] ${msg.message.substring(0, 80)}`);
310
+ } else {
311
+ console.log(` 📬 [Bus → External] ${msg.message.substring(0, 80)}`);
312
+ }
313
+ } catch {}
314
+ }
315
+ lastCount = files.length;
316
+ }
317
+ }, 3000);
318
+
319
+ console.log(`\n Press Ctrl+C to stop\n`);
320
+ }
321
+
322
+ main().catch(err => {
323
+ console.error('FATAL:', err.message);
324
+ process.exit(1);
325
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "coco": {
4
+ "command": "node",
5
+ "args": [
6
+ "E:\\_system\\.openclaw\\workspace\\repos\\mcp-coco\\index.js"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * CoCo Bus Client — TypeScript/JS SDK for Agent Bus
3
+ *
4
+ * Direct file-based access to the CoCo Agent Bus. No MCP needed.
5
+ * Works in Node.js. For browser, use the WebSocket/proxy approach.
6
+ *
7
+ * Usage:
8
+ * import { CoCoClient } from './coco-client.js';
9
+ *
10
+ * const bus = new CoCoClient('/path/to/.bus', 'my-agent');
11
+ * await bus.register({ description: 'My agent' });
12
+ * await bus.send('hermes', 'Hello!');
13
+ * const msgs = await bus.fetchMessages();
14
+ */
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import { randomUUID } from 'crypto';
19
+
20
+ interface AgentProfile {
21
+ name: string;
22
+ description?: string;
23
+ capabilities?: string[];
24
+ model?: { provider?: string; name?: string; context_window?: number };
25
+ tags?: string[];
26
+ status?: 'idle' | 'busy' | 'offline';
27
+ version?: string;
28
+ endpoints?: Record<string, string>;
29
+ tools?: string[];
30
+ metadata?: Record<string, unknown>;
31
+ last_seen?: string;
32
+ registered_at?: string;
33
+ }
34
+
35
+ interface BusMessage {
36
+ id: string;
37
+ from: string;
38
+ to?: string;
39
+ channel?: string;
40
+ message: string;
41
+ metadata?: Record<string, unknown>;
42
+ timestamp: string;
43
+ }
44
+
45
+ export class CoCoClient {
46
+ private busDir: string;
47
+ private msgsDir: string;
48
+ private agentsFile: string;
49
+ private channelsDir: string;
50
+ agentName: string;
51
+
52
+ constructor(busDir: string, agentName?: string) {
53
+ this.busDir = busDir;
54
+ this.msgsDir = path.join(busDir, 'messages');
55
+ this.agentsFile = path.join(busDir, 'agents.json');
56
+ this.channelsDir = path.join(busDir, 'channels');
57
+ this.agentName = agentName || process.env.COCO_AGENT || process.env.USER || 'ts-agent';
58
+ this._ensureDirs();
59
+ }
60
+
61
+ private _ensureDirs() {
62
+ for (const d of [this.busDir, this.msgsDir, this.channelsDir]) {
63
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
64
+ }
65
+ }
66
+
67
+ private _genId(): string {
68
+ return `${Date.now()}_${randomUUID().slice(0, 8)}`;
69
+ }
70
+
71
+ private _loadAgents(): Record<string, AgentProfile> {
72
+ try {
73
+ if (fs.existsSync(this.agentsFile)) {
74
+ return JSON.parse(fs.readFileSync(this.agentsFile, 'utf-8'));
75
+ }
76
+ } catch {}
77
+ return {};
78
+ }
79
+
80
+ private _saveAgents(agents: Record<string, AgentProfile>) {
81
+ fs.writeFileSync(this.agentsFile, JSON.stringify(agents, null, 2), 'utf-8');
82
+ }
83
+
84
+ // ── Registration ──
85
+
86
+ register(profile: {
87
+ description?: string;
88
+ capabilities?: string[];
89
+ model?: { provider?: string; name?: string };
90
+ tags?: string[];
91
+ version?: string;
92
+ status?: string;
93
+ }): { registered: boolean; is_new: boolean } {
94
+ const agents = this._loadAgents();
95
+ const existing = agents[this.agentName];
96
+ agents[this.agentName] = {
97
+ name: this.agentName,
98
+ description: profile.description || '',
99
+ capabilities: profile.capabilities || [],
100
+ model: profile.model || null as any,
101
+ tags: profile.tags || [],
102
+ status: (profile.status as any) || 'idle',
103
+ version: profile.version || '1.0.0',
104
+ endpoints: {},
105
+ tools: [],
106
+ metadata: {},
107
+ last_seen: new Date().toISOString(),
108
+ registered_at: existing?.registered_at || new Date().toISOString(),
109
+ };
110
+ this._saveAgents(agents);
111
+ return { registered: true, is_new: !existing };
112
+ }
113
+
114
+ heartbeat(): boolean {
115
+ const agents = this._loadAgents();
116
+ if (agents[this.agentName]) {
117
+ agents[this.agentName].last_seen = new Date().toISOString();
118
+ agents[this.agentName].status = 'idle';
119
+ this._saveAgents(agents);
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ setStatus(status: 'idle' | 'busy' | 'offline'): boolean {
126
+ const agents = this._loadAgents();
127
+ if (agents[this.agentName]) {
128
+ agents[this.agentName].status = status;
129
+ agents[this.agentName].last_seen = new Date().toISOString();
130
+ this._saveAgents(agents);
131
+ return true;
132
+ }
133
+ return false;
134
+ }
135
+
136
+ listAgents(onlineOnly = false): Record<string, AgentProfile> {
137
+ const agents = this._loadAgents();
138
+ if (onlineOnly) {
139
+ const cutoff = Date.now() - 300000;
140
+ return Object.fromEntries(
141
+ Object.entries(agents).filter(([, a]) => new Date(a.last_seen!).getTime() > cutoff)
142
+ );
143
+ }
144
+ return agents;
145
+ }
146
+
147
+ // ── Messaging ──
148
+
149
+ send(to: string, message: string, metadata?: Record<string, unknown>): string {
150
+ const msg: BusMessage = {
151
+ id: this._genId(),
152
+ from: this.agentName,
153
+ to,
154
+ message,
155
+ metadata: metadata || {},
156
+ timestamp: new Date().toISOString(),
157
+ };
158
+
159
+ const inboxDir = path.join(this.msgsDir, to);
160
+ if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
161
+ fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
162
+
163
+ // Outbox
164
+ const outboxDir = path.join(this.msgsDir, `${this.agentName}_outbox`);
165
+ if (!fs.existsSync(outboxDir)) fs.mkdirSync(outboxDir, { recursive: true });
166
+ fs.writeFileSync(path.join(outboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
167
+
168
+ return msg.id;
169
+ }
170
+
171
+ broadcast(message: string): Array<{ to: string; message_id: string }> {
172
+ const agents = this._loadAgents();
173
+ return Object.keys(agents)
174
+ .filter(name => name !== this.agentName)
175
+ .map(name => ({ to: name, message_id: this.send(name, message, { broadcast: true }) }));
176
+ }
177
+
178
+ fetchMessages(limit = 50, fromAgent?: string): BusMessage[] {
179
+ const inboxDir = path.join(this.msgsDir, this.agentName);
180
+ if (!fs.existsSync(inboxDir)) return [];
181
+
182
+ const files = fs.readdirSync(inboxDir)
183
+ .filter(f => f.endsWith('.json'))
184
+ .sort()
185
+ .slice(-limit);
186
+
187
+ return files
188
+ .map(f => {
189
+ try {
190
+ const msg: BusMessage = JSON.parse(fs.readFileSync(path.join(inboxDir, f), 'utf-8'));
191
+ if (fromAgent && msg.from !== fromAgent) return null;
192
+ return msg;
193
+ } catch { return null; }
194
+ })
195
+ .filter(Boolean) as BusMessage[];
196
+ }
197
+
198
+ reply(originalMsg: BusMessage, text: string): string {
199
+ return this.send(originalMsg.from, text, { in_reply_to: originalMsg.id });
200
+ }
201
+
202
+ // ── Channels ──
203
+
204
+ channelSend(channelId: string, message: string): string {
205
+ const chPath = path.join(this.channelsDir, `${channelId}.json`);
206
+ if (!fs.existsSync(chPath)) throw new Error(`Channel "${channelId}" not found`);
207
+
208
+ const msg: BusMessage = {
209
+ id: this._genId(),
210
+ channel: channelId,
211
+ from: this.agentName,
212
+ message,
213
+ timestamp: new Date().toISOString(),
214
+ };
215
+
216
+ const logDir = path.join(this.channelsDir, channelId, 'log');
217
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
218
+ fs.writeFileSync(path.join(logDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
219
+
220
+ // DM members
221
+ const channel = JSON.parse(fs.readFileSync(chPath, 'utf-8'));
222
+ for (const member of channel.members || []) {
223
+ if (member !== this.agentName) {
224
+ this.send(member, `[${channelId}] ${message}`, { channel: channelId });
225
+ }
226
+ }
227
+
228
+ return msg.id;
229
+ }
230
+
231
+ channelHistory(channelId: string, limit = 20): BusMessage[] {
232
+ const logDir = path.join(this.channelsDir, channelId, 'log');
233
+ if (!fs.existsSync(logDir)) return [];
234
+
235
+ return fs.readdirSync(logDir)
236
+ .filter(f => f.endsWith('.json'))
237
+ .sort()
238
+ .slice(-limit)
239
+ .map(f => {
240
+ try { return JSON.parse(fs.readFileSync(path.join(logDir, f), 'utf-8')); }
241
+ catch { return null; }
242
+ })
243
+ .filter(Boolean) as BusMessage[];
244
+ }
245
+ }