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
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CoCo Webhook Gateway v2.1 — Receive external webhooks and post to Agent Bus
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - GitHub push, PR, issues, Actions CI status
|
|
7
|
+
* - GitLab push/merge events
|
|
8
|
+
* - Slack messages
|
|
9
|
+
* - Telegram bot updates
|
|
10
|
+
* - Generic JSON webhooks
|
|
11
|
+
* - GitHub Actions CI Status endpoint
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node webhook-gateway.js [port]
|
|
15
|
+
* # Default port: 8080
|
|
16
|
+
*/
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const BUS_DIR = path.join(__dirname, '.bus');
|
|
22
|
+
const MSGS_DIR = path.join(BUS_DIR, 'messages');
|
|
23
|
+
const CHANNELS_DIR = path.join(BUS_DIR, 'channels');
|
|
24
|
+
|
|
25
|
+
const PORT = parseInt(process.argv[2], 10) || 8080;
|
|
26
|
+
const AGENT_NAME = 'webhook';
|
|
27
|
+
|
|
28
|
+
// ── Bus Interface ──────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function ensureDir(dir) {
|
|
31
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getChannels() {
|
|
35
|
+
if (!fs.existsSync(CHANNELS_DIR)) return [];
|
|
36
|
+
return fs.readdirSync(CHANNELS_DIR)
|
|
37
|
+
.filter(f => f.endsWith('.json'))
|
|
38
|
+
.map(f => f.replace(/\.json$/, ''));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function postToChannel(channelId, from, message, metadata = {}) {
|
|
42
|
+
const chPath = path.join(CHANNELS_DIR, `${channelId}.json`);
|
|
43
|
+
if (!fs.existsSync(chPath)) return { ok: false, error: 'Channel not found' };
|
|
44
|
+
|
|
45
|
+
const ch = JSON.parse(fs.readFileSync(chPath, 'utf-8'));
|
|
46
|
+
const msg = {
|
|
47
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
48
|
+
channel: channelId,
|
|
49
|
+
from: from || AGENT_NAME,
|
|
50
|
+
message,
|
|
51
|
+
metadata: { ...metadata, source: 'webhook' },
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const logDir = path.join(CHANNELS_DIR, channelId, 'log');
|
|
56
|
+
ensureDir(logDir);
|
|
57
|
+
fs.writeFileSync(path.join(logDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
|
|
58
|
+
|
|
59
|
+
// DM all members
|
|
60
|
+
for (const member of ch.members) {
|
|
61
|
+
if (member !== (from || AGENT_NAME)) {
|
|
62
|
+
const inboxDir = path.join(MSGS_DIR, member);
|
|
63
|
+
ensureDir(inboxDir);
|
|
64
|
+
const dm = { ...msg, to: member };
|
|
65
|
+
fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify(dm, null, 2), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: true, message_id: msg.id, channel: channelId, recipients: ch.members.length - 1 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function postToAgent(agentName, from, message, metadata = {}) {
|
|
73
|
+
const msg = {
|
|
74
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
75
|
+
from: from || AGENT_NAME,
|
|
76
|
+
to: agentName,
|
|
77
|
+
message,
|
|
78
|
+
metadata: { ...metadata, source: 'webhook' },
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const inboxDir = path.join(MSGS_DIR, agentName);
|
|
83
|
+
ensureDir(inboxDir);
|
|
84
|
+
fs.writeFileSync(path.join(inboxDir, `${msg.id}.json`), JSON.stringify(msg, null, 2), 'utf-8');
|
|
85
|
+
return { ok: true, message_id: msg.id, to: agentName };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Webhook Handlers ───────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function formatBody(body, source) {
|
|
91
|
+
switch (source) {
|
|
92
|
+
case 'github': return formatGitHub(body);
|
|
93
|
+
case 'github-actions': return formatGitHubActions(body);
|
|
94
|
+
case 'gitlab': return formatGitLab(body);
|
|
95
|
+
case 'slack': return formatSlack(body);
|
|
96
|
+
case 'telegram': return formatTelegram(body);
|
|
97
|
+
default: return formatGeneric(body, source);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatGitHub(body) {
|
|
102
|
+
const ref = (body.ref || '').replace('refs/heads/', '');
|
|
103
|
+
const repo = body.repository?.full_name || body.repository?.name || 'unknown';
|
|
104
|
+
const sender = body.sender?.login || body.pusher?.name || 'unknown';
|
|
105
|
+
|
|
106
|
+
// Pull request
|
|
107
|
+
if (body.pull_request) {
|
|
108
|
+
const pr = body.pull_request;
|
|
109
|
+
const action = body.action || 'updated';
|
|
110
|
+
return `🔀 [${repo}] PR #${pr.number} ${action}: ${pr.title}\n by ${pr.user?.login} · ${pr.html_url}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Issue
|
|
114
|
+
if (body.issue && !body.comment) {
|
|
115
|
+
const issue = body.issue;
|
|
116
|
+
const action = body.action || 'updated';
|
|
117
|
+
const labels = (issue.labels || []).map(l => l.name).join(', ');
|
|
118
|
+
return `🎫 [${repo}] Issue #${issue.number} ${action}: ${issue.title}\n by ${issue.user?.login}${labels ? ` [${labels}]` : ''}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Issue comment
|
|
122
|
+
if (body.issue && body.comment) {
|
|
123
|
+
const issue = body.issue;
|
|
124
|
+
const comment = body.comment;
|
|
125
|
+
return `💬 [${repo}] Comment on #${issue.number}: ${issue.title}\n by ${comment.user?.login}: ${comment.body?.substring(0, 200)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Commits
|
|
129
|
+
const commits = body.commits || [];
|
|
130
|
+
if (commits.length > 0) {
|
|
131
|
+
const commitMessages = commits.slice(0, 5).map(c => ` • ${c.message?.split('\n')[0]} (${c.id?.substring(0, 7)})`).join('\n');
|
|
132
|
+
const extra = commits.length > 5 ? `\n ... and ${commits.length - 5} more` : '';
|
|
133
|
+
return `📦 [${repo}:${ref}] ${commits.length} commit(s) by ${sender}\n${commitMessages}${extra}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Push
|
|
137
|
+
if (body.ref) {
|
|
138
|
+
return `📡 [${repo}:${ref}] push by ${sender}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return `🔔 [${repo}] ${body.action || 'event'} from ${sender}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatGitHubActions(body) {
|
|
145
|
+
const workflow = body.workflow || body.workflow_run?.name || 'Workflow';
|
|
146
|
+
const repo = body.repository?.full_name || 'unknown';
|
|
147
|
+
const branch = body.ref?.replace('refs/heads/', '') || body.workflow_run?.head_branch || 'unknown';
|
|
148
|
+
const status = body.action || body.workflow_run?.status || body.workflow_run?.conclusion || 'unknown';
|
|
149
|
+
const runId = body.workflow_run?.id || body.run_id || '?';
|
|
150
|
+
const actor = body.sender?.login || 'unknown';
|
|
151
|
+
|
|
152
|
+
let icon, statusText;
|
|
153
|
+
switch (status) {
|
|
154
|
+
case 'completed': {
|
|
155
|
+
const conclusion = body.workflow_run?.conclusion || body.conclusion || 'success';
|
|
156
|
+
if (conclusion === 'success') { icon = '✅'; statusText = 'passed'; }
|
|
157
|
+
else if (conclusion === 'failure') { icon = '❌'; statusText = 'failed'; }
|
|
158
|
+
else if (conclusion === 'cancelled') { icon = '🚫'; statusText = 'cancelled'; }
|
|
159
|
+
else { icon = '⚠️'; statusText = conclusion; }
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'in_progress': icon = '🔄'; statusText = 'in progress'; break;
|
|
163
|
+
case 'queued': icon = '⏳'; statusText = 'queued'; break;
|
|
164
|
+
case 'requested': icon = '🔁'; statusText = 'requested'; break;
|
|
165
|
+
default: icon = 'ℹ️'; statusText = status; break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const url = body.workflow_run?.html_url ||
|
|
169
|
+
`https://github.com/${repo}/actions/runs/${runId}`;
|
|
170
|
+
|
|
171
|
+
let msg = `${icon} [${repo}] ${workflow} ${statusText}`;
|
|
172
|
+
msg += `\n Branch: ${branch} · by ${actor}`;
|
|
173
|
+
msg += `\n ${url}`;
|
|
174
|
+
|
|
175
|
+
// Annotations/errors
|
|
176
|
+
if (body.workflow_run?.conclusion === 'failure' && body.workflow_run?.head_commit) {
|
|
177
|
+
msg += `\n Commit: ${body.workflow_run.head_commit.id?.substring(0, 7)} ${body.workflow_run.head_commit.message?.split('\n')[0] || ''}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return msg;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatGitLab(body) {
|
|
184
|
+
const ref = (body.ref || '').replace('refs/heads/', '');
|
|
185
|
+
const project = body.project?.name || 'unknown';
|
|
186
|
+
const user = body.user_username || body.user?.name || 'unknown';
|
|
187
|
+
|
|
188
|
+
// Merge request
|
|
189
|
+
if (body.object_attributes?.action && body.object_kind === 'merge_request') {
|
|
190
|
+
const mr = body.object_attributes;
|
|
191
|
+
return `🔀 [${project}] MR #${mr.iid} ${mr.action}: ${mr.title}\n by ${user}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const commits = body.commits || [];
|
|
195
|
+
const msg = commits.length > 0
|
|
196
|
+
? commits.slice(0, 3).map(c => ` • ${c.message?.split('\n')[0]}`).join('\n')
|
|
197
|
+
: '';
|
|
198
|
+
|
|
199
|
+
return `📦 [${project}] ${ref} — by ${user}\n${msg}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatSlack(body) {
|
|
203
|
+
const text = body.text || body.event?.text || body.message?.text || '(no text)';
|
|
204
|
+
const channel = body.event?.channel || body.channel_name || 'unknown';
|
|
205
|
+
const user = body.event?.user || body.user_name || 'unknown';
|
|
206
|
+
return `💬 [${channel}] ${user}: ${text}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatTelegram(body) {
|
|
210
|
+
const msg = body.message || body.edited_message || body.callback_query?.message || {};
|
|
211
|
+
const text = msg.text || msg.caption || '(no text)';
|
|
212
|
+
const from = msg.from?.first_name || msg.from?.username || 'unknown';
|
|
213
|
+
const chat = msg.chat?.title || msg.chat?.username || msg.chat?.id || 'unknown';
|
|
214
|
+
return `✈️ [${chat}] ${from}: ${text}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatGeneric(body, source) {
|
|
218
|
+
const text = body.text || body.message || body.content || body.msg || JSON.stringify(body);
|
|
219
|
+
return `[${source}] ${typeof text === 'string' ? text.substring(0, 500) : JSON.stringify(text).substring(0, 500)}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── HTTP Server ────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function getBody(req) {
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
let body = '';
|
|
227
|
+
req.on('data', chunk => body += chunk);
|
|
228
|
+
req.on('end', () => resolve(body));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function respondJson(res, code, data) {
|
|
233
|
+
res.writeHead(code, {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
'Access-Control-Allow-Origin': '*',
|
|
236
|
+
});
|
|
237
|
+
res.end(JSON.stringify(data, null, 2));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const server = http.createServer(async (req, res) => {
|
|
241
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
242
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
|
|
243
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-GitHub-Event, X-GitLab-Event');
|
|
244
|
+
|
|
245
|
+
if (req.method === 'OPTIONS') {
|
|
246
|
+
res.writeHead(204);
|
|
247
|
+
res.end();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
252
|
+
const pathname = url.pathname;
|
|
253
|
+
const method = req.method;
|
|
254
|
+
|
|
255
|
+
// ── GET endpoints ──
|
|
256
|
+
|
|
257
|
+
if (method === 'GET' && pathname === '/') {
|
|
258
|
+
respondJson(res, 200, {
|
|
259
|
+
service: 'bus-agent-webhook-gateway',
|
|
260
|
+
version: '2.1.0',
|
|
261
|
+
endpoints: {
|
|
262
|
+
'POST /webhook/github/:channel': 'GitHub webhook → bus channel',
|
|
263
|
+
'POST /webhook/github-actions/:channel': 'GitHub Actions CI webhook → bus channel',
|
|
264
|
+
'POST /webhook/gitlab/:channel': 'GitLab webhook → bus channel',
|
|
265
|
+
'POST /webhook/slack/:channel': 'Slack webhook → bus channel',
|
|
266
|
+
'POST /webhook/telegram/:channel': 'Telegram webhook → bus channel',
|
|
267
|
+
'POST /webhook/generic/:channel': 'Generic webhook → bus channel',
|
|
268
|
+
'POST /hook/:channel': 'Generic webhook (alias)',
|
|
269
|
+
'POST /api/send': 'Direct: send to agent or channel',
|
|
270
|
+
'GET /health': 'Health check',
|
|
271
|
+
'GET /api/channels': 'List channels',
|
|
272
|
+
'GET /api/agents': 'List agents',
|
|
273
|
+
},
|
|
274
|
+
channels: getChannels(),
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (method === 'GET' && pathname === '/health') {
|
|
280
|
+
respondJson(res, 200, {
|
|
281
|
+
status: 'ok',
|
|
282
|
+
uptime: process.uptime(),
|
|
283
|
+
channels: getChannels().length,
|
|
284
|
+
bus_dir: fs.existsSync(BUS_DIR),
|
|
285
|
+
bus_messages: fs.existsSync(MSGS_DIR)
|
|
286
|
+
? fs.readdirSync(MSGS_DIR).filter(d => fs.statSync(path.join(MSGS_DIR, d)).isDirectory()).length
|
|
287
|
+
: 0,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (method === 'GET' && pathname === '/api/channels') {
|
|
293
|
+
const channels = getChannels();
|
|
294
|
+
const details = channels.map(id => {
|
|
295
|
+
const p = path.join(CHANNELS_DIR, `${id}.json`);
|
|
296
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return { id }; }
|
|
297
|
+
});
|
|
298
|
+
respondJson(res, 200, { channels: details });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (method === 'GET' && pathname === '/api/agents') {
|
|
303
|
+
const agentsFile = path.join(BUS_DIR, 'agents.json');
|
|
304
|
+
const agents = fs.existsSync(agentsFile) ? JSON.parse(fs.readFileSync(agentsFile, 'utf-8')) : {};
|
|
305
|
+
respondJson(res, 200, { agents: Object.keys(agents), count: Object.keys(agents).length });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── POST /api/send — Direct message API ──
|
|
310
|
+
|
|
311
|
+
if (method === 'POST' && pathname === '/api/send') {
|
|
312
|
+
const body = await getBody(req);
|
|
313
|
+
try {
|
|
314
|
+
const data = JSON.parse(body);
|
|
315
|
+
const { to, message, from } = data;
|
|
316
|
+
|
|
317
|
+
if (!to || !message) {
|
|
318
|
+
respondJson(res, 400, { error: 'Missing "to" or "message"' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let result;
|
|
323
|
+
if (to.startsWith('#')) {
|
|
324
|
+
result = postToChannel(to.slice(1), from || 'api', message);
|
|
325
|
+
} else {
|
|
326
|
+
result = postToAgent(to, from || 'api', message);
|
|
327
|
+
}
|
|
328
|
+
respondJson(res, 200, { received: true, result });
|
|
329
|
+
} catch (err) {
|
|
330
|
+
respondJson(res, 400, { error: `Invalid JSON: ${err.message}` });
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── POST /webhook/:source/:channel or POST /hook/:channel ──
|
|
336
|
+
|
|
337
|
+
const match = pathname.match(/^\/(?:webhook\/(\w+)\/)?(\w+)$/);
|
|
338
|
+
if (!match || method !== 'POST') {
|
|
339
|
+
respondJson(res, 404, { error: 'Not found. See GET / for endpoints.' });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const source = match[1] || 'generic';
|
|
344
|
+
const channelId = match[2];
|
|
345
|
+
const body = await getBody(req);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// Detect GitHub Actions from event header
|
|
349
|
+
let effectiveSource = source;
|
|
350
|
+
const ghEvent = req.headers['x-github-event'];
|
|
351
|
+
if (ghEvent === 'workflow_job' || ghEvent === 'workflow_run') {
|
|
352
|
+
effectiveSource = 'github-actions';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const parsed = JSON.parse(body);
|
|
356
|
+
const message = formatBody(parsed, effectiveSource);
|
|
357
|
+
const result = postToChannel(channelId, `webhook:${effectiveSource}`, message);
|
|
358
|
+
|
|
359
|
+
if (result.ok) {
|
|
360
|
+
console.log(`[${new Date().toISOString()}] ${effectiveSource} → #${channelId}: ${message.substring(0, 80)}...`);
|
|
361
|
+
respondJson(res, 200, { received: true, source: effectiveSource, result });
|
|
362
|
+
} else {
|
|
363
|
+
respondJson(res, 404, { error: result.error, source: effectiveSource });
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(`[${new Date().toISOString()}] Error:`, err.message);
|
|
367
|
+
respondJson(res, 400, { error: `Invalid webhook: ${err.message}` });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ── Start ──────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
ensureDir(BUS_DIR);
|
|
374
|
+
ensureDir(path.join(MSGS_DIR, AGENT_NAME));
|
|
375
|
+
ensureDir(CHANNELS_DIR);
|
|
376
|
+
|
|
377
|
+
// Register webhook agent on bus with profile
|
|
378
|
+
const agentsFile = path.join(BUS_DIR, 'agents.json');
|
|
379
|
+
const agents = fs.existsSync(agentsFile) ? JSON.parse(fs.readFileSync(agentsFile, 'utf-8')) : {};
|
|
380
|
+
agents[AGENT_NAME] = {
|
|
381
|
+
name: AGENT_NAME,
|
|
382
|
+
description: 'Webhook Gateway — receives external webhooks and posts to channels',
|
|
383
|
+
capabilities: ['webhook', 'github', 'gitlab', 'slack', 'telegram', 'ci-cd'],
|
|
384
|
+
tags: ['webhook', 'integration', 'ci'],
|
|
385
|
+
status: 'idle',
|
|
386
|
+
version: '2.1.0',
|
|
387
|
+
last_seen: new Date().toISOString(),
|
|
388
|
+
registered_at: agents[AGENT_NAME]?.registered_at || new Date().toISOString(),
|
|
389
|
+
};
|
|
390
|
+
fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2), 'utf-8');
|
|
391
|
+
|
|
392
|
+
server.listen(PORT, () => {
|
|
393
|
+
console.log(`
|
|
394
|
+
╔══════════════════════════════════════════════╗
|
|
395
|
+
║ MCP CoCo — Webhook Gateway v2.1 ║
|
|
396
|
+
║ http://localhost:${PORT} ║
|
|
397
|
+
╚══════════════════════════════════════════════╝
|
|
398
|
+
|
|
399
|
+
Endpoints:
|
|
400
|
+
POST /webhook/github/:channel GitHub push/PR/issues
|
|
401
|
+
POST /webhook/github-actions/:channel GitHub Actions CI status
|
|
402
|
+
POST /webhook/gitlab/:channel GitLab push/MRs
|
|
403
|
+
POST /webhook/slack/:channel Slack messages
|
|
404
|
+
POST /webhook/telegram/:channel Telegram bot updates
|
|
405
|
+
POST /webhook/generic/:channel Generic JSON webhook
|
|
406
|
+
POST /hook/:channel Generic (alias)
|
|
407
|
+
POST /api/send Direct: {"to":"agent/#channel","message":"..."}
|
|
408
|
+
|
|
409
|
+
GET /health Health check
|
|
410
|
+
GET /api/channels List channels
|
|
411
|
+
GET /api/agents List agents
|
|
412
|
+
GET / This help
|
|
413
|
+
|
|
414
|
+
Scheduled tasks: ${getChannels().join(', ') || '(none)'}
|
|
415
|
+
`);
|
|
416
|
+
|
|
417
|
+
// Log incoming events to the bus as system events
|
|
418
|
+
const evDir = path.join(BUS_DIR, 'events');
|
|
419
|
+
ensureDir(evDir);
|
|
420
|
+
});
|