github-webhook-mcp 0.2.0 → 0.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/manifest.json +1 -1
- package/package.json +1 -1
- package/server/event-store.js +181 -152
- package/server/index.js +215 -107
package/manifest.json
CHANGED
package/package.json
CHANGED
package/server/event-store.js
CHANGED
|
@@ -1,152 +1,181 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { resolve, dirname } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import iconv from "iconv-lite";
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const DEFAULT_DATA_FILE = resolve(__dirname, "..", "..", "events.json");
|
|
8
|
-
const PRIMARY_ENCODING = "utf-8";
|
|
9
|
-
const LEGACY_ENCODINGS = ["utf-8", "cp932", "shift_jis"];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Try
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
payload.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import iconv from "iconv-lite";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DEFAULT_DATA_FILE = resolve(__dirname, "..", "..", "events.json");
|
|
8
|
+
const PRIMARY_ENCODING = "utf-8";
|
|
9
|
+
const LEGACY_ENCODINGS = ["utf-8", "cp932", "shift_jis"];
|
|
10
|
+
const DEFAULT_PURGE_DAYS = 1;
|
|
11
|
+
|
|
12
|
+
function purgeDays() {
|
|
13
|
+
const env = process.env.PURGE_AFTER_DAYS;
|
|
14
|
+
if (env !== undefined) {
|
|
15
|
+
const n = Number(env);
|
|
16
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
17
|
+
}
|
|
18
|
+
return DEFAULT_PURGE_DAYS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function dataFilePath() {
|
|
22
|
+
return process.env.EVENTS_JSON_PATH || DEFAULT_DATA_FILE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Load / Save ─────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export function load() {
|
|
28
|
+
const filePath = dataFilePath();
|
|
29
|
+
if (!existsSync(filePath)) return [];
|
|
30
|
+
|
|
31
|
+
const raw = readFileSync(filePath);
|
|
32
|
+
|
|
33
|
+
// Try UTF-8 first (with BOM stripping)
|
|
34
|
+
try {
|
|
35
|
+
let text = raw.toString("utf-8");
|
|
36
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
37
|
+
const events = JSON.parse(text);
|
|
38
|
+
return events;
|
|
39
|
+
} catch {
|
|
40
|
+
// fall through to legacy encodings
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try legacy encodings
|
|
44
|
+
for (const encoding of LEGACY_ENCODINGS) {
|
|
45
|
+
try {
|
|
46
|
+
const text = iconv.decode(raw, encoding);
|
|
47
|
+
const events = JSON.parse(text);
|
|
48
|
+
// Migrate to UTF-8
|
|
49
|
+
save(events);
|
|
50
|
+
return events;
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(`Unable to decode event store: ${filePath}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function save(events) {
|
|
60
|
+
const filePath = dataFilePath();
|
|
61
|
+
writeFileSync(filePath, JSON.stringify(events, null, 2), PRIMARY_ENCODING);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Purge ──────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function purgeProcessed(events) {
|
|
67
|
+
const days = purgeDays();
|
|
68
|
+
if (days < 0) return { kept: events, purged: 0 };
|
|
69
|
+
const cutoff = Date.now() - days * 86_400_000;
|
|
70
|
+
const before = events.length;
|
|
71
|
+
const kept = events.filter((e) => {
|
|
72
|
+
if (!e.processed) return true;
|
|
73
|
+
const ts = Date.parse(e.received_at);
|
|
74
|
+
return Number.isNaN(ts) || ts > cutoff;
|
|
75
|
+
});
|
|
76
|
+
return { kept, purged: before - kept.length };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Query ───────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export function getPending() {
|
|
82
|
+
return load().filter((e) => !e.processed);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getEvent(eventId) {
|
|
86
|
+
for (const event of load()) {
|
|
87
|
+
if (event.id === eventId) return event;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getPendingStatus() {
|
|
93
|
+
const pending = getPending();
|
|
94
|
+
const types = {};
|
|
95
|
+
for (const event of pending) {
|
|
96
|
+
types[event.type] = (types[event.type] || 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
pending_count: pending.length,
|
|
100
|
+
latest_received_at: pending.length > 0 ? pending[pending.length - 1].received_at : null,
|
|
101
|
+
types,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function eventNumber(payload) {
|
|
108
|
+
return (
|
|
109
|
+
payload.number ??
|
|
110
|
+
payload.issue?.number ??
|
|
111
|
+
payload.pull_request?.number ??
|
|
112
|
+
null
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function eventTitle(payload) {
|
|
117
|
+
return (
|
|
118
|
+
payload.issue?.title ??
|
|
119
|
+
payload.pull_request?.title ??
|
|
120
|
+
payload.discussion?.title ??
|
|
121
|
+
payload.check_run?.name ??
|
|
122
|
+
payload.workflow_run?.name ??
|
|
123
|
+
payload.workflow_job?.name ??
|
|
124
|
+
null
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function eventUrl(payload) {
|
|
129
|
+
return (
|
|
130
|
+
payload.issue?.html_url ??
|
|
131
|
+
payload.pull_request?.html_url ??
|
|
132
|
+
payload.discussion?.html_url ??
|
|
133
|
+
payload.check_run?.html_url ??
|
|
134
|
+
payload.workflow_run?.html_url ??
|
|
135
|
+
null
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function summarizeEvent(event) {
|
|
140
|
+
const payload = event.payload || {};
|
|
141
|
+
return {
|
|
142
|
+
id: event.id,
|
|
143
|
+
type: event.type,
|
|
144
|
+
received_at: event.received_at,
|
|
145
|
+
processed: event.processed,
|
|
146
|
+
trigger_status: event.trigger_status ?? null,
|
|
147
|
+
last_triggered_at: event.last_triggered_at ?? null,
|
|
148
|
+
action: payload.action ?? null,
|
|
149
|
+
repo: payload.repository?.full_name ?? null,
|
|
150
|
+
sender: payload.sender?.login ?? null,
|
|
151
|
+
number: eventNumber(payload),
|
|
152
|
+
title: eventTitle(payload),
|
|
153
|
+
url: eventUrl(payload),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getPendingSummaries(limit = 20) {
|
|
158
|
+
let pending = getPending();
|
|
159
|
+
if (limit > 0) {
|
|
160
|
+
pending = pending.slice(-limit);
|
|
161
|
+
}
|
|
162
|
+
return pending.map(summarizeEvent);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Mutation ────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
export function markDone(eventId) {
|
|
168
|
+
const events = load();
|
|
169
|
+
let found = false;
|
|
170
|
+
for (const event of events) {
|
|
171
|
+
if (event.id === eventId) {
|
|
172
|
+
event.processed = true;
|
|
173
|
+
found = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!found) return { success: false, purged: 0 };
|
|
178
|
+
const { kept, purged } = purgeProcessed(events);
|
|
179
|
+
save(kept);
|
|
180
|
+
return { success: true, purged };
|
|
181
|
+
}
|
package/server/index.js
CHANGED
|
@@ -1,107 +1,215 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { watch } from "node:fs";
|
|
9
|
+
import {
|
|
10
|
+
getPendingStatus,
|
|
11
|
+
getPendingSummaries,
|
|
12
|
+
getEvent,
|
|
13
|
+
getPending,
|
|
14
|
+
markDone,
|
|
15
|
+
summarizeEvent,
|
|
16
|
+
dataFilePath,
|
|
17
|
+
} from "./event-store.js";
|
|
18
|
+
|
|
19
|
+
const CHANNEL_ENABLED = process.env.WEBHOOK_CHANNEL !== "0";
|
|
20
|
+
|
|
21
|
+
const capabilities = {
|
|
22
|
+
tools: {},
|
|
23
|
+
};
|
|
24
|
+
if (CHANNEL_ENABLED) {
|
|
25
|
+
capabilities.experimental = { "claude/channel": {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const server = new Server(
|
|
29
|
+
{ name: "github-webhook-mcp", version: "0.3.0" },
|
|
30
|
+
{
|
|
31
|
+
capabilities,
|
|
32
|
+
instructions: CHANNEL_ENABLED
|
|
33
|
+
? "GitHub webhook events arrive as <channel source=\"github-webhook-mcp\" ...>. They are one-way: read them and act, no reply expected."
|
|
34
|
+
: undefined,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// ── Tools ───────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const TOOLS = [
|
|
41
|
+
{
|
|
42
|
+
name: "get_pending_status",
|
|
43
|
+
description:
|
|
44
|
+
"Get a lightweight snapshot of pending GitHub webhook events. Use this for periodic polling before requesting details.",
|
|
45
|
+
inputSchema: { type: "object", properties: {} },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "list_pending_events",
|
|
49
|
+
description:
|
|
50
|
+
"List lightweight summaries for pending GitHub webhook events. Returns metadata only, without full payloads.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
limit: {
|
|
55
|
+
type: "number",
|
|
56
|
+
description: "Maximum number of pending events to return (1-100, default 20)",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_event",
|
|
63
|
+
description: "Get the full payload for a single webhook event by ID.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
event_id: { type: "string", description: "The event ID to retrieve" },
|
|
68
|
+
},
|
|
69
|
+
required: ["event_id"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "get_webhook_events",
|
|
74
|
+
description:
|
|
75
|
+
"Get pending (unprocessed) GitHub webhook events with full payloads. Prefer get_pending_status or list_pending_events for polling.",
|
|
76
|
+
inputSchema: { type: "object", properties: {} },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "mark_processed",
|
|
80
|
+
description: "Mark a webhook event as processed so it won't appear again.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
event_id: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "The event ID to mark as processed",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["event_id"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
95
|
+
|
|
96
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
97
|
+
const { name, arguments: args } = req.params;
|
|
98
|
+
|
|
99
|
+
switch (name) {
|
|
100
|
+
case "get_pending_status": {
|
|
101
|
+
const status = getPendingStatus();
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case "list_pending_events": {
|
|
107
|
+
const limit = Math.max(1, Math.min(100, Number(args?.limit) || 20));
|
|
108
|
+
const summaries = getPendingSummaries(limit);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify(summaries, null, 2) }],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
case "get_event": {
|
|
114
|
+
const event = getEvent(args.event_id);
|
|
115
|
+
if (event === null) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: JSON.stringify({ error: "not_found", event_id: args.event_id }),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
case "get_webhook_events": {
|
|
130
|
+
const events = getPending();
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
case "mark_processed": {
|
|
136
|
+
const result = markDone(args.event_id);
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify({
|
|
142
|
+
success: result.success,
|
|
143
|
+
event_id: args.event_id,
|
|
144
|
+
purged: result.purged,
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
throw new Error(`unknown tool: ${name}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── Start ───────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
const transport = new StdioServerTransport();
|
|
158
|
+
await server.connect(transport);
|
|
159
|
+
|
|
160
|
+
// ── File watcher (after connect) ─────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
if (CHANNEL_ENABLED) {
|
|
163
|
+
const seenIds = new Set(getPending().map((e) => e.id));
|
|
164
|
+
let debounce = null;
|
|
165
|
+
|
|
166
|
+
function onFileChange() {
|
|
167
|
+
if (debounce) return;
|
|
168
|
+
debounce = setTimeout(() => {
|
|
169
|
+
debounce = null;
|
|
170
|
+
try {
|
|
171
|
+
const pending = getPending();
|
|
172
|
+
for (const event of pending) {
|
|
173
|
+
if (seenIds.has(event.id)) continue;
|
|
174
|
+
seenIds.add(event.id);
|
|
175
|
+
const summary = summarizeEvent(event);
|
|
176
|
+
const lines = [
|
|
177
|
+
`[${summary.type}] ${summary.repo ?? ""}`,
|
|
178
|
+
summary.action ? `action: ${summary.action}` : null,
|
|
179
|
+
summary.title ? `#${summary.number ?? ""} ${summary.title}` : null,
|
|
180
|
+
summary.sender ? `by ${summary.sender}` : null,
|
|
181
|
+
summary.url ?? null,
|
|
182
|
+
].filter(Boolean);
|
|
183
|
+
server.notification({
|
|
184
|
+
method: "notifications/claude/channel",
|
|
185
|
+
params: {
|
|
186
|
+
content: lines.join("\n"),
|
|
187
|
+
meta: {
|
|
188
|
+
chat_id: "github",
|
|
189
|
+
message_id: event.id,
|
|
190
|
+
user: summary.sender ?? "github",
|
|
191
|
+
ts: summary.received_at,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// file may be mid-write; ignore and retry on next change
|
|
198
|
+
}
|
|
199
|
+
}, 500);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
watch(dataFilePath(), onFileChange);
|
|
204
|
+
} catch {
|
|
205
|
+
// events.json may not exist yet; start polling fallback
|
|
206
|
+
const poll = setInterval(() => {
|
|
207
|
+
try {
|
|
208
|
+
watch(dataFilePath(), onFileChange);
|
|
209
|
+
clearInterval(poll);
|
|
210
|
+
} catch {
|
|
211
|
+
// keep waiting
|
|
212
|
+
}
|
|
213
|
+
}, 5000);
|
|
214
|
+
}
|
|
215
|
+
}
|