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/lib/tunnel.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoCo Tunnel — Cross-machine Bus Proxy Module
|
|
3
|
+
*
|
|
4
|
+
* Export functions, no process.exit, accepts busDir as parameter.
|
|
5
|
+
* Used by: coco-cli.js, tunnel.js (thin CLI wrapper)
|
|
6
|
+
*/
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
function loadAgents(busDir) {
|
|
13
|
+
const p = path.join(busDir, 'agents.json');
|
|
14
|
+
if (!fs.existsSync(p)) return {};
|
|
15
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hashSecret(secret) {
|
|
19
|
+
return crypto.createHash('sha256').update(secret).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function authenticate(req, secret) {
|
|
23
|
+
if (!secret) return true;
|
|
24
|
+
const provided = req.headers['x-coco-token'];
|
|
25
|
+
return provided && hashSecret(provided) === hashSecret(secret);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readBusFile(busDir, filename) {
|
|
29
|
+
const p = path.join(busDir, filename);
|
|
30
|
+
if (!fs.existsSync(p)) return null;
|
|
31
|
+
try { return fs.readFileSync(p, 'utf-8'); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeBusFile(busDir, filename, data) {
|
|
35
|
+
const p = path.join(busDir, filename);
|
|
36
|
+
const dir = path.dirname(p);
|
|
37
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(p, data, 'utf-8');
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Start tunnel server
|
|
44
|
+
*/
|
|
45
|
+
function startServer(opts = {}) {
|
|
46
|
+
const busDir = opts.busDir || path.join(process.cwd(), '.bus');
|
|
47
|
+
const port = opts.port || 9090;
|
|
48
|
+
const secret = opts.secret || null;
|
|
49
|
+
|
|
50
|
+
console.log(`\n CoCo Tunnel — Server (receiving end)`);
|
|
51
|
+
console.log(` Listening on :${port}`);
|
|
52
|
+
console.log(` Auth: ${secret ? 'enabled' : 'disabled (INSECURE)'}`);
|
|
53
|
+
console.log(` Bus: ${busDir}\n`);
|
|
54
|
+
|
|
55
|
+
const server = http.createServer((req, res) => {
|
|
56
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
57
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
58
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Coco-Token');
|
|
59
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
60
|
+
if (secret && !authenticate(req, secret)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
61
|
+
|
|
62
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
63
|
+
const pathname = url.pathname;
|
|
64
|
+
|
|
65
|
+
if (req.method === 'GET' && pathname === '/health') {
|
|
66
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
67
|
+
res.end(JSON.stringify({ service: 'coco-tunnel', role: 'server', bus: busDir, agents: Object.keys(loadAgents(busDir)).length, uptime: process.uptime() }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (req.method === 'GET' && pathname === '/bus/agents') {
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
73
|
+
res.end(JSON.stringify(loadAgents(busDir)));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fileMatch = pathname.match(/^\/bus\/file\/(.+)$/);
|
|
78
|
+
if (req.method === 'GET' && fileMatch) {
|
|
79
|
+
const content = readBusFile(busDir, fileMatch[1]);
|
|
80
|
+
if (content === null) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; }
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(content);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (req.method === 'POST' && pathname === '/bus/send') {
|
|
86
|
+
let body = '';
|
|
87
|
+
req.on('data', c => body += c);
|
|
88
|
+
req.on('end', () => {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(body);
|
|
91
|
+
if (!data.to || !data.message) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing "to" or "message"' })); return; }
|
|
92
|
+
const msg = { id: `${Date.now()}_${crypto.randomBytes(4).toString('hex')}`, from: data.from || 'tunnel', to: data.to, message: data.message, metadata: { source: 'tunnel', tunnel_host: req.headers['host'] || 'unknown' }, timestamp: new Date().toISOString() };
|
|
93
|
+
const inboxDir = path.join(busDir, 'messages', data.to);
|
|
94
|
+
if (!fs.existsSync(inboxDir)) fs.mkdirSync(inboxDir, { recursive: true });
|
|
95
|
+
fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
|
|
96
|
+
res.writeHead(200); res.end(JSON.stringify({ sent: true, message_id: msg.id }));
|
|
97
|
+
} catch (err) { res.writeHead(400); res.end(JSON.stringify({ error: err.message })); }
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (req.method === 'POST' && pathname === '/bus/write') {
|
|
103
|
+
let body = '';
|
|
104
|
+
req.on('data', c => body += c);
|
|
105
|
+
req.on('end', () => {
|
|
106
|
+
try {
|
|
107
|
+
const data = JSON.parse(body);
|
|
108
|
+
if (!data.filepath || data.content === undefined) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing "filepath" or "content"' })); return; }
|
|
109
|
+
writeBusFile(busDir, data.filepath, typeof data.content === 'string' ? data.content : JSON.stringify(data.content, null, 2));
|
|
110
|
+
res.writeHead(200); res.end(JSON.stringify({ written: true, filepath: data.filepath }));
|
|
111
|
+
} catch (err) { res.writeHead(400); res.end(JSON.stringify({ error: err.message })); }
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (req.method === 'POST' && pathname === '/bus/register') {
|
|
117
|
+
let body = '';
|
|
118
|
+
req.on('data', c => body += c);
|
|
119
|
+
req.on('end', () => {
|
|
120
|
+
try {
|
|
121
|
+
const data = JSON.parse(body);
|
|
122
|
+
if (!data.name) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing "name"' })); return; }
|
|
123
|
+
const agents = loadAgents(busDir);
|
|
124
|
+
agents[data.name] = { name: data.name, description: data.description || 'Remote tunnel agent', capabilities: data.capabilities || [], tags: ['tunnel', 'remote'], status: 'idle', version: '1.0.0', last_seen: new Date().toISOString(), registered_at: agents[data.name]?.registered_at || new Date().toISOString() };
|
|
125
|
+
fs.writeFileSync(path.join(busDir, 'agents.json'), JSON.stringify(agents, null, 2), 'utf-8');
|
|
126
|
+
res.writeHead(200); res.end(JSON.stringify({ registered: true, agent: data.name }));
|
|
127
|
+
} catch (err) { res.writeHead(400); res.end(JSON.stringify({ error: err.message })); }
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' }));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
server.listen(port);
|
|
136
|
+
return server;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// HTTP fetch helper
|
|
140
|
+
function httpFetch(url, opts = {}) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const u = new URL(url);
|
|
143
|
+
const options = {
|
|
144
|
+
hostname: u.hostname, port: u.port, path: u.pathname + u.search,
|
|
145
|
+
method: opts.method || 'GET', headers: opts.headers || {}, timeout: 10000,
|
|
146
|
+
};
|
|
147
|
+
const req = http.request(options, (res) => {
|
|
148
|
+
let data = '';
|
|
149
|
+
res.on('data', c => data += c);
|
|
150
|
+
res.on('end', () => {
|
|
151
|
+
if (res.statusCode >= 200 && res.statusCode < 300) { try { resolve(JSON.parse(data)); } catch { resolve(data); } }
|
|
152
|
+
else reject(new Error(`HTTP ${res.statusCode}: ${data.substring(0, 100)}`));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
req.on('error', reject);
|
|
156
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
157
|
+
if (opts.body) req.write(opts.body);
|
|
158
|
+
req.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Start tunnel client
|
|
164
|
+
*/
|
|
165
|
+
async function startClient(opts = {}) {
|
|
166
|
+
const busDir = opts.busDir || path.join(process.cwd(), '.bus');
|
|
167
|
+
const host = opts.host || 'localhost';
|
|
168
|
+
const port = opts.port || 9090;
|
|
169
|
+
const secret = opts.secret || null;
|
|
170
|
+
const interval = opts.interval || 10000;
|
|
171
|
+
const baseUrl = `http://${host}:${port}`;
|
|
172
|
+
const headers = {};
|
|
173
|
+
if (secret) headers['X-Coco-Token'] = secret;
|
|
174
|
+
|
|
175
|
+
console.log(`\n CoCo Tunnel — Client (sending end)`);
|
|
176
|
+
console.log(` Remote: ${baseUrl}`);
|
|
177
|
+
console.log(` Sync: Every ${interval}ms\n`);
|
|
178
|
+
|
|
179
|
+
// Test connection
|
|
180
|
+
try {
|
|
181
|
+
const health = await httpFetch(`${baseUrl}/health`, { headers });
|
|
182
|
+
console.log(` Connection: ✅ remote bus has ${health.agents} agent(s)`);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(` Connection: ❌ ${err.message}`);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Register this machine
|
|
189
|
+
const hostname = require('os').hostname();
|
|
190
|
+
try {
|
|
191
|
+
await httpFetch(`${baseUrl}/bus/register`, {
|
|
192
|
+
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ name: `tunnel:${hostname}`, description: `Tunnel client from ${hostname}:${busDir}`, capabilities: ['tunnel', 'remote-sync'] }),
|
|
194
|
+
});
|
|
195
|
+
console.log(` Registered as "tunnel:${hostname}" on remote bus`);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.log(` Register: ❌ ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Sync loop
|
|
201
|
+
const syncInterval = setInterval(async () => {
|
|
202
|
+
try {
|
|
203
|
+
const localAgents = loadAgents(busDir);
|
|
204
|
+
for (const [name, profile] of Object.entries(localAgents)) {
|
|
205
|
+
await httpFetch(`${baseUrl}/bus/register`, {
|
|
206
|
+
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
|
|
207
|
+
body: JSON.stringify({ name, description: profile.description, capabilities: profile.capabilities }),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const msgsDir = path.join(busDir, 'messages');
|
|
212
|
+
if (fs.existsSync(msgsDir)) {
|
|
213
|
+
for (const inbox of fs.readdirSync(msgsDir).filter(d => fs.statSync(path.join(msgsDir, d)).isDirectory() && !d.endsWith('_outbox'))) {
|
|
214
|
+
for (const f of fs.readdirSync(path.join(msgsDir, inbox)).filter(f => f.endsWith('.json')).slice(-5)) {
|
|
215
|
+
try {
|
|
216
|
+
const msg = JSON.parse(fs.readFileSync(path.join(msgsDir, inbox, f), 'utf-8'));
|
|
217
|
+
if (msg.tunnel_pushed) continue;
|
|
218
|
+
await httpFetch(`${baseUrl}/bus/send`, {
|
|
219
|
+
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ from: msg.from, to: msg.to, message: msg.message }),
|
|
221
|
+
});
|
|
222
|
+
msg.tunnel_pushed = true;
|
|
223
|
+
fs.writeFileSync(path.join(msgsDir, inbox, f), JSON.stringify(msg, null, 2), 'utf-8');
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {}
|
|
229
|
+
}, interval);
|
|
230
|
+
|
|
231
|
+
return { syncInterval, stop: () => clearInterval(syncInterval) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Bidirectional sync
|
|
236
|
+
*/
|
|
237
|
+
async function startSync(opts = {}) {
|
|
238
|
+
const busDir = opts.busDir || path.join(process.cwd(), '.bus');
|
|
239
|
+
const remote = opts.remote || 'http://localhost:9090';
|
|
240
|
+
const secret = opts.secret || null;
|
|
241
|
+
const interval = opts.interval || 10000;
|
|
242
|
+
const headers = {};
|
|
243
|
+
if (secret) headers['X-Coco-Token'] = secret;
|
|
244
|
+
|
|
245
|
+
console.log(`\n CoCo Tunnel — Bidirectional Sync`);
|
|
246
|
+
console.log(` Remote: ${remote}`);
|
|
247
|
+
console.log(` Local: ${busDir}`);
|
|
248
|
+
console.log(` Sync: Every ${interval}ms\n`);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const health = await httpFetch(`${remote}/health`, { headers });
|
|
252
|
+
console.log(` Remote: ✅ ${health.agents} agent(s)`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.log(` Remote: ❌ ${err.message}`);
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function mergeAgents(local, remote) {
|
|
259
|
+
const merged = { ...local };
|
|
260
|
+
for (const [name, profile] of Object.entries(remote)) {
|
|
261
|
+
if (!merged[name]) merged[name] = profile;
|
|
262
|
+
else {
|
|
263
|
+
const l = new Date(merged[name].last_seen || 0).getTime();
|
|
264
|
+
const r = new Date(profile.last_seen || 0).getTime();
|
|
265
|
+
if (r > l) merged[name] = profile;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return merged;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const sync = setInterval(async () => {
|
|
272
|
+
try {
|
|
273
|
+
const remoteAgents = await httpFetch(`${remote}/bus/agents`, { headers });
|
|
274
|
+
const merged = mergeAgents(loadAgents(busDir), remoteAgents);
|
|
275
|
+
fs.writeFileSync(path.join(busDir, 'agents.json'), JSON.stringify(merged, null, 2), 'utf-8');
|
|
276
|
+
for (const [name, profile] of Object.entries(loadAgents(busDir))) {
|
|
277
|
+
if (!remoteAgents[name] || remoteAgents[name].last_seen !== profile.last_seen) {
|
|
278
|
+
await httpFetch(`${remote}/bus/register`, {
|
|
279
|
+
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' },
|
|
280
|
+
body: JSON.stringify({ name, description: profile.description, capabilities: profile.capabilities }),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {}
|
|
285
|
+
}, interval);
|
|
286
|
+
|
|
287
|
+
return { sync, stop: () => clearInterval(sync) };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* SSH tunnel instructions
|
|
292
|
+
*/
|
|
293
|
+
function printSSHHelp(opts = {}) {
|
|
294
|
+
const remote = opts.remote || 'user@remote-host';
|
|
295
|
+
const port = opts.port || 9090;
|
|
296
|
+
|
|
297
|
+
console.log(`\n╔═══════════════════════════════════════════════╗\n║ CoCo Tunnel — SSH Port Forwarding ║\n╚═══════════════════════════════════════════════╝\n`);
|
|
298
|
+
console.log(`To expose your local CoCo bus to a remote machine:\n`);
|
|
299
|
+
console.log(`1. On THIS machine, start the tunnel server:`);
|
|
300
|
+
console.log(` node tunnel.js server --port ${port} --secret ***\n`);
|
|
301
|
+
console.log(`2. On the REMOTE machine, create an SSH reverse tunnel:`);
|
|
302
|
+
console.log(` ssh -R ${port}:localhost:${port} ${remote}\n`);
|
|
303
|
+
console.log(`3. On the REMOTE machine, connect as client:`);
|
|
304
|
+
console.log(` node tunnel.js client --host localhost --port ${port} --secret ***\n`);
|
|
305
|
+
console.log(`Alternative: forward the other way:\n`);
|
|
306
|
+
console.log(`1. On the REMOTE machine, start tunnel server:`);
|
|
307
|
+
console.log(` node tunnel.js server --port ${port} --secret ***\n`);
|
|
308
|
+
console.log(`2. On THIS machine:`);
|
|
309
|
+
console.log(` ssh -L ${port}:localhost:${port} ${remote}\n`);
|
|
310
|
+
console.log(`3. On THIS machine, connect as client:`);
|
|
311
|
+
console.log(` node tunnel.js client --host localhost --port ${port} --secret ***\n`);
|
|
312
|
+
console.log(`To keep the tunnel alive:`);
|
|
313
|
+
console.log(` autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" \\`);
|
|
314
|
+
console.log(` -R ${port}:localhost:${port} ${remote}\n`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = { startServer, startClient, startSync, printSSHHelp };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"servers": {
|
|
3
|
+
"coco": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "node",
|
|
6
|
+
"args": ["C:\\Users\\Administrator\\.openclaw\\workspace\\mcp-coco\\index.js"]
|
|
7
|
+
},
|
|
8
|
+
"hermes": {
|
|
9
|
+
"type": "stdio",
|
|
10
|
+
"command": "hermes",
|
|
11
|
+
"args": ["mcp", "serve", "--accept-hooks"]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bus-agent",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Universal Agent Communication Hub — Connect any AI agent to any other. MCP bus with messaging, channels, memory, scheduling, workflows, and diagnostics.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coco": "./coco-cli.js",
|
|
8
|
+
"bus-agent": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"daemon": "node index.js --daemon",
|
|
13
|
+
"health": "node index.js --health",
|
|
14
|
+
"doctor": "node doctor.js",
|
|
15
|
+
"doctor:fix": "node doctor.js --fix",
|
|
16
|
+
"backup": "node backup.js",
|
|
17
|
+
"backup:list": "node backup.js --list",
|
|
18
|
+
"tunnel": "node tunnel.js server",
|
|
19
|
+
"memory": "node coco-cli.js memory"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/ClewCode/mcp-coco.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/ClewCode/mcp-coco/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/ClewCode/mcp-coco#readme",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"agent-bus",
|
|
36
|
+
"agent-communication",
|
|
37
|
+
"multi-agent",
|
|
38
|
+
"hermes",
|
|
39
|
+
"openclaw",
|
|
40
|
+
"ai-agents",
|
|
41
|
+
"message-bus",
|
|
42
|
+
"scheduler",
|
|
43
|
+
"workflow",
|
|
44
|
+
"memory",
|
|
45
|
+
"vector-search"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"files": [
|
|
49
|
+
"bin/",
|
|
50
|
+
"lib/",
|
|
51
|
+
"clients/",
|
|
52
|
+
"index.js",
|
|
53
|
+
"coco-cli.js",
|
|
54
|
+
"coco-tool.js",
|
|
55
|
+
"doctor.js",
|
|
56
|
+
"backup.js",
|
|
57
|
+
"tunnel.js",
|
|
58
|
+
"bridge.js",
|
|
59
|
+
"webhook-gateway.js",
|
|
60
|
+
"hermes-forwarder.js",
|
|
61
|
+
"setup.js",
|
|
62
|
+
"coco.js",
|
|
63
|
+
".env.coco",
|
|
64
|
+
"AGENTS.md",
|
|
65
|
+
"README.md",
|
|
66
|
+
"SKILL.md",
|
|
67
|
+
"LICENSE",
|
|
68
|
+
"claude-mcp.json",
|
|
69
|
+
"cursor-mcp.json",
|
|
70
|
+
"opencode-mcp.json",
|
|
71
|
+
"mcporter.example.json",
|
|
72
|
+
"hermes.example.json",
|
|
73
|
+
"coco-aliases.sh",
|
|
74
|
+
"scripts/"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# MCP CoCo — Windows Install Script
|
|
2
|
+
# Run as Administrator to install Scheduled Task + mcporter config
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
$RepoDir = Split-Path -Parent $PSScriptRoot
|
|
6
|
+
|
|
7
|
+
Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Cyan
|
|
8
|
+
Write-Host "║ MCP CoCo — Windows Install ║" -ForegroundColor Cyan
|
|
9
|
+
Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Cyan
|
|
10
|
+
Write-Host ""
|
|
11
|
+
|
|
12
|
+
# 1. Verify Hermes is installed
|
|
13
|
+
Write-Host "🔍 Checking Hermes..." -ForegroundColor Yellow
|
|
14
|
+
try {
|
|
15
|
+
$hermesVer = & hermes --version 2>&1
|
|
16
|
+
Write-Host " ✅ Hermes $hermesVer" -ForegroundColor Green
|
|
17
|
+
} catch {
|
|
18
|
+
Write-Host " ❌ Hermes not found. Install with: pip install hermes-agent" -ForegroundColor Red
|
|
19
|
+
exit 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# 2. Verify Node.js
|
|
23
|
+
Write-Host "🔍 Checking Node.js..." -ForegroundColor Yellow
|
|
24
|
+
try {
|
|
25
|
+
$nodeVer = node --version
|
|
26
|
+
Write-Host " ✅ Node.js $nodeVer" -ForegroundColor Green
|
|
27
|
+
} catch {
|
|
28
|
+
Write-Host " ❌ Node.js not found." -ForegroundColor Red
|
|
29
|
+
exit 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# 3. Test MCP CoCo
|
|
33
|
+
Write-Host "🧪 Testing MCP CoCo..." -ForegroundColor Yellow
|
|
34
|
+
try {
|
|
35
|
+
$result = node "$RepoDir\bin\cli.js" --health 2>&1
|
|
36
|
+
Write-Host " ✅ MCP CoCo works" -ForegroundColor Green
|
|
37
|
+
} catch {
|
|
38
|
+
Write-Host " ⚠️ First run - testing..." -ForegroundColor Yellow
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 4. Stop any existing daemon
|
|
42
|
+
Write-Host "🛑 Stopping existing daemon..." -ForegroundColor Yellow
|
|
43
|
+
try {
|
|
44
|
+
node "$RepoDir\index.js" --daemon 2>&1 | Out-Null
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
# 5. Register Scheduled Task for auto-start
|
|
48
|
+
$taskName = "MCP_CoCo_Daemon"
|
|
49
|
+
$taskExists = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
50
|
+
|
|
51
|
+
Write-Host "⚙️ Scheduled Task: $taskName" -ForegroundColor Yellow
|
|
52
|
+
if ($taskExists) {
|
|
53
|
+
Write-Host " ⚠️ Already exists, updating..." -ForegroundColor Yellow
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$action = New-ScheduledTaskAction -Execute "node.exe" `
|
|
57
|
+
-Argument "$RepoDir\index.js --daemon" `
|
|
58
|
+
-WorkingDirectory $RepoDir
|
|
59
|
+
|
|
60
|
+
$trigger = New-ScheduledTaskTrigger -AtStartup
|
|
61
|
+
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
62
|
+
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
|
66
|
+
Write-Host " ✅ Scheduled Task registered" -ForegroundColor Green
|
|
67
|
+
} catch {
|
|
68
|
+
Write-Host " ❌ Failed to register task: $_" -ForegroundColor Red
|
|
69
|
+
Write-Host " Run this script as Administrator" -ForegroundColor Red
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 6. Start daemon
|
|
73
|
+
Write-Host "🚀 Starting MCP CoCo daemon..." -ForegroundColor Yellow
|
|
74
|
+
try {
|
|
75
|
+
Start-ScheduledTask -TaskName $taskName
|
|
76
|
+
Write-Host " ✅ Daemon started via Scheduled Task" -ForegroundColor Green
|
|
77
|
+
} catch {
|
|
78
|
+
Write-Host " ⚠️ Could not start task: $_" -ForegroundColor Yellow
|
|
79
|
+
Write-Host " Start manually: node `"$RepoDir\index.js --daemon`"" -ForegroundColor Yellow
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# 7. Verify
|
|
83
|
+
Start-Sleep -Seconds 2
|
|
84
|
+
$health = & node "$RepoDir\bin\cli.js" --health 2>&1
|
|
85
|
+
if ($LASTEXITCODE -eq 0) {
|
|
86
|
+
Write-Host " ✅ Daemon health check passed" -ForegroundColor Green
|
|
87
|
+
} else {
|
|
88
|
+
Write-Host " ⚠️ Daemon may still be starting..." -ForegroundColor Yellow
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Write-Host ""
|
|
92
|
+
Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Cyan
|
|
93
|
+
Write-Host "║ MCP CoCo Installation Done ║" -ForegroundColor Cyan
|
|
94
|
+
Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Cyan
|
|
95
|
+
Write-Host ""
|
|
96
|
+
Write-Host "To use from OpenClaw, add to mcporter config:" -ForegroundColor White
|
|
97
|
+
Write-Host " mcporter add coco --stdio `"node $RepoDir\index.js`"" -ForegroundColor Gray
|
|
98
|
+
Write-Host ""
|
|
99
|
+
Write-Host "Then call:" -ForegroundColor White
|
|
100
|
+
Write-Host " mcporter call coco.ask_hermes prompt=`"Hello`"" -ForegroundColor Gray
|