sentinel-mcp 0.1.2 → 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/bin/release-hook.sh +20 -0
- package/dist/index.js +76 -8
- package/dist/queue.d.ts +47 -0
- package/dist/queue.js +204 -0
- package/dist/types.d.ts +45 -1
- package/dist/ws-client.d.ts +1 -1
- package/dist/ws-client.js +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SubagentStop hook — writes a signal file for the sentinel-mcp file watcher.
|
|
3
|
+
# Called by Claude Code when a background subagent finishes.
|
|
4
|
+
# Reads hook input JSON from stdin, extracts the event ID, writes signal file.
|
|
5
|
+
|
|
6
|
+
COMPLETED_DIR="$HOME/.sentinel/completed"
|
|
7
|
+
mkdir -p "$COMPLETED_DIR"
|
|
8
|
+
|
|
9
|
+
# Read hook input from stdin
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
|
|
12
|
+
# Try to extract SENTINEL_EVENT_ID from the subagent's last message or transcript
|
|
13
|
+
EVENT_ID=$(echo "$INPUT" | grep -o 'SENTINEL_EVENT_ID=[^ "\\]*' | head -1 | cut -d= -f2)
|
|
14
|
+
|
|
15
|
+
if [ -z "$EVENT_ID" ]; then
|
|
16
|
+
EVENT_ID="unknown"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Write signal file — filename is the event ID
|
|
20
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$COMPLETED_DIR/$EVENT_ID"
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
4
5
|
import path from "path";
|
|
5
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { WsClient, decodeInstallationId } from "./ws-client.js";
|
|
10
|
+
import { EventQueue } from "./queue.js";
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
// Config from environment
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
@@ -36,6 +38,8 @@ try {
|
|
|
36
38
|
catch {
|
|
37
39
|
log("WARNING: Could not resolve repo name from git remote — events will not be filtered by repo");
|
|
38
40
|
}
|
|
41
|
+
const MAX_CONCURRENCY = parseInt(process.env.SENTINEL_MAX_CONCURRENCY ?? "3", 10);
|
|
42
|
+
const eventQueue = new EventQueue({ maxConcurrency: MAX_CONCURRENCY, recoverActive: true });
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
40
44
|
// MCP server setup
|
|
41
45
|
// ---------------------------------------------------------------------------
|
|
@@ -59,6 +63,21 @@ function pushNotification(content, meta) {
|
|
|
59
63
|
log(`Failed to push notification: ${err.message}`);
|
|
60
64
|
});
|
|
61
65
|
}
|
|
66
|
+
function updateEventStatus(usageId, status) {
|
|
67
|
+
if (!usageId)
|
|
68
|
+
return;
|
|
69
|
+
const httpUrl = RELAY_URL.replace("wss://", "https://").replace("ws://", "http://");
|
|
70
|
+
fetch(`${httpUrl}/api/dashboard/events/${usageId}/status`, {
|
|
71
|
+
method: "PUT",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
Authorization: `Bearer ${SENTINEL_TOKEN}`,
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({ status }),
|
|
77
|
+
}).catch((err) => {
|
|
78
|
+
log(`Failed to update event status: ${err.message}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
62
81
|
// ---------------------------------------------------------------------------
|
|
63
82
|
// GitHub token management
|
|
64
83
|
// ---------------------------------------------------------------------------
|
|
@@ -124,6 +143,22 @@ async function mcpToolCall(toolName, args) {
|
|
|
124
143
|
return rpcResponse.result;
|
|
125
144
|
}
|
|
126
145
|
// ---------------------------------------------------------------------------
|
|
146
|
+
// Auto-push — deliver next event from queue when capacity is available
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
function autoPush() {
|
|
149
|
+
while (eventQueue.hasCapacity() && eventQueue.queueLength() > 0) {
|
|
150
|
+
const active = eventQueue.activate();
|
|
151
|
+
if (!active)
|
|
152
|
+
break;
|
|
153
|
+
updateEventStatus(active.usageId, "processing");
|
|
154
|
+
if (active.notification) {
|
|
155
|
+
const content = `SENTINEL_EVENT_ID=${active.id}\n${active.notification.content}`;
|
|
156
|
+
pushNotification(content, active.notification.meta);
|
|
157
|
+
}
|
|
158
|
+
log(`Activated event ${active.id} (#${active.issueNumber} in ${active.repo})`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
127
162
|
// WebSocket client — receives events from relay, pushes as channel notifications
|
|
128
163
|
// ---------------------------------------------------------------------------
|
|
129
164
|
const wsClient = new WsClient({
|
|
@@ -131,15 +166,30 @@ const wsClient = new WsClient({
|
|
|
131
166
|
relayUrl: RELAY_URL,
|
|
132
167
|
repo: REPO_NAME || undefined,
|
|
133
168
|
log,
|
|
134
|
-
onEvent: (id, payload, notification) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
onEvent: (id, payload, notification, usageId) => {
|
|
170
|
+
const result = eventQueue.enqueue(id, usageId ?? null, payload, notification ?? null);
|
|
171
|
+
switch (result.action) {
|
|
172
|
+
case "duplicate":
|
|
173
|
+
log(`Duplicate event ${id}, skipping`);
|
|
174
|
+
break;
|
|
175
|
+
case "bypass":
|
|
176
|
+
// Comment/lifecycle for active issue — push directly, no slot consumed
|
|
177
|
+
updateEventStatus(usageId ?? null, "processing");
|
|
178
|
+
if (result.event.notification) {
|
|
179
|
+
pushNotification(`SENTINEL_EVENT_ID=${result.event.id}\n${result.event.notification.content}`, result.event.notification.meta);
|
|
180
|
+
}
|
|
181
|
+
log(`Bypass event ${id} (follow-up for active issue #${result.event.issueNumber})`);
|
|
182
|
+
break;
|
|
183
|
+
case "dequeue":
|
|
184
|
+
log(`Dequeued event ${result.removedId} — issue resolved before processing`);
|
|
185
|
+
break;
|
|
186
|
+
case "enqueued":
|
|
187
|
+
updateEventStatus(usageId ?? null, "pending");
|
|
188
|
+
log(`Queued event ${id} (${payload.type} #${payload.type === "pr_merged" ? payload.issue_number : payload.issue?.number}) — queue: ${eventQueue.queueLength()}`);
|
|
189
|
+
autoPush();
|
|
190
|
+
break;
|
|
142
191
|
}
|
|
192
|
+
// ACK immediately
|
|
143
193
|
wsClient.ack(id);
|
|
144
194
|
},
|
|
145
195
|
onDisplaced: (reason) => {
|
|
@@ -244,6 +294,23 @@ async function main() {
|
|
|
244
294
|
await refreshGithubToken();
|
|
245
295
|
tokenRefreshTimer = setInterval(refreshGithubToken, 50 * 60 * 1000);
|
|
246
296
|
wsClient.connect();
|
|
297
|
+
// Start file watcher for deterministic release
|
|
298
|
+
const completedDir = path.join(os.homedir(), ".sentinel", "completed");
|
|
299
|
+
eventQueue.startFileWatcher(completedDir, (eventId, usageId) => {
|
|
300
|
+
log(`Released event ${eventId} via hook signal`);
|
|
301
|
+
updateEventStatus(usageId, "delivered");
|
|
302
|
+
autoPush();
|
|
303
|
+
});
|
|
304
|
+
// Start timeout checker (every 60s, 30-min timeout)
|
|
305
|
+
setInterval(() => {
|
|
306
|
+
const released = eventQueue.checkTimeouts(30 * 60 * 1000);
|
|
307
|
+
for (const event of released) {
|
|
308
|
+
log(`Timeout: auto-released stale event ${event.id}`);
|
|
309
|
+
updateEventStatus(event.usageId, "delivered");
|
|
310
|
+
}
|
|
311
|
+
if (released.length > 0)
|
|
312
|
+
autoPush();
|
|
313
|
+
}, 60 * 1000);
|
|
247
314
|
log("Sentinel MCP bridge running");
|
|
248
315
|
}
|
|
249
316
|
main().catch((err) => {
|
|
@@ -257,6 +324,7 @@ function shutdown() {
|
|
|
257
324
|
log("Shutting down...");
|
|
258
325
|
if (tokenRefreshTimer)
|
|
259
326
|
clearInterval(tokenRefreshTimer);
|
|
327
|
+
eventQueue.stopFileWatcher();
|
|
260
328
|
wsClient.disconnect();
|
|
261
329
|
server.close().catch(() => { });
|
|
262
330
|
process.exit(0);
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ActiveEvent, SentinelEvent, EnqueueResult } from "./types.js";
|
|
2
|
+
export interface EventQueueOptions {
|
|
3
|
+
queueFilePath?: string;
|
|
4
|
+
maxConcurrency?: number;
|
|
5
|
+
recoverActive?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class EventQueue {
|
|
8
|
+
private state;
|
|
9
|
+
private filePath;
|
|
10
|
+
private watcher;
|
|
11
|
+
private completedDir;
|
|
12
|
+
constructor(opts?: EventQueueOptions);
|
|
13
|
+
enqueue(id: string, usageId: string | null, payload: SentinelEvent, notification: {
|
|
14
|
+
content: string;
|
|
15
|
+
meta: Record<string, string>;
|
|
16
|
+
} | null): EnqueueResult;
|
|
17
|
+
activate(): ActiveEvent | null;
|
|
18
|
+
releaseById(id: string): ActiveEvent | null;
|
|
19
|
+
hasCapacity(): boolean;
|
|
20
|
+
queueLength(): number;
|
|
21
|
+
status(): {
|
|
22
|
+
queueLength: number;
|
|
23
|
+
activeCount: number;
|
|
24
|
+
maxConcurrency: number;
|
|
25
|
+
queue: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
eventType: string;
|
|
28
|
+
repo: string;
|
|
29
|
+
issueNumber: number;
|
|
30
|
+
priority: string;
|
|
31
|
+
enqueuedAt: string;
|
|
32
|
+
}>;
|
|
33
|
+
active: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
repo: string;
|
|
36
|
+
issueNumber: number;
|
|
37
|
+
startedAt: string;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
getActiveEvents(): ActiveEvent[];
|
|
41
|
+
checkTimeouts(timeoutMs: number): ActiveEvent[];
|
|
42
|
+
startFileWatcher(completedDir: string, onRelease: (eventId: string, usageId: string | null) => void): void;
|
|
43
|
+
stopFileWatcher(): void;
|
|
44
|
+
private processSignalFiles;
|
|
45
|
+
private load;
|
|
46
|
+
private persist;
|
|
47
|
+
}
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
export class EventQueue {
|
|
5
|
+
state;
|
|
6
|
+
filePath;
|
|
7
|
+
watcher = null;
|
|
8
|
+
completedDir = "";
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.filePath = opts.queueFilePath ?? path.join(os.homedir(), ".sentinel", "queue.json");
|
|
11
|
+
const maxConcurrency = opts.maxConcurrency ?? 3;
|
|
12
|
+
this.state = this.load(maxConcurrency);
|
|
13
|
+
if (opts.recoverActive && this.state.active.length > 0) {
|
|
14
|
+
for (const active of this.state.active) {
|
|
15
|
+
this.state.queue.unshift({
|
|
16
|
+
id: active.id,
|
|
17
|
+
usageId: active.usageId,
|
|
18
|
+
priority: "high",
|
|
19
|
+
eventType: active.payload.type,
|
|
20
|
+
repo: active.repo,
|
|
21
|
+
issueNumber: active.issueNumber,
|
|
22
|
+
notification: active.notification,
|
|
23
|
+
payload: active.payload,
|
|
24
|
+
enqueuedAt: new Date().toISOString(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
this.state.active = [];
|
|
28
|
+
this.persist();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
enqueue(id, usageId, payload, notification) {
|
|
32
|
+
// Deduplicate
|
|
33
|
+
if (this.state.queue.some((e) => e.id === id) || this.state.active.some((e) => e.id === id)) {
|
|
34
|
+
return { action: "duplicate" };
|
|
35
|
+
}
|
|
36
|
+
const issueNumber = payload.type === "pr_merged" ? payload.issue_number : payload.issue.number;
|
|
37
|
+
const isActiveIssue = this.state.active.some((a) => a.issueNumber === issueNumber && a.repo === payload.repo);
|
|
38
|
+
const isQueuedIssue = this.state.queue.some((q) => q.issueNumber === issueNumber && q.repo === payload.repo);
|
|
39
|
+
const entry = {
|
|
40
|
+
id,
|
|
41
|
+
usageId,
|
|
42
|
+
priority: "normal",
|
|
43
|
+
eventType: payload.type,
|
|
44
|
+
repo: payload.repo,
|
|
45
|
+
issueNumber,
|
|
46
|
+
notification,
|
|
47
|
+
payload,
|
|
48
|
+
enqueuedAt: new Date().toISOString(),
|
|
49
|
+
};
|
|
50
|
+
// Bypass: comment/lifecycle events for already-active issues — push directly, no slot
|
|
51
|
+
if (isActiveIssue && (payload.type === "issue_comment" || payload.type === "pr_merged" || payload.type === "issue_closed")) {
|
|
52
|
+
return { action: "bypass", event: entry };
|
|
53
|
+
}
|
|
54
|
+
// Dequeue: pr_merged/issue_closed for queued-but-not-active issues — remove the queued event
|
|
55
|
+
if (isQueuedIssue && (payload.type === "pr_merged" || payload.type === "issue_closed")) {
|
|
56
|
+
const idx = this.state.queue.findIndex((q) => q.issueNumber === issueNumber && q.repo === payload.repo);
|
|
57
|
+
const removed = this.state.queue[idx];
|
|
58
|
+
this.state.queue.splice(idx, 1);
|
|
59
|
+
this.persist();
|
|
60
|
+
return { action: "dequeue", removedId: removed.id };
|
|
61
|
+
}
|
|
62
|
+
// High priority: comment for queued-but-not-active issue
|
|
63
|
+
if (isQueuedIssue && payload.type === "issue_comment") {
|
|
64
|
+
entry.priority = "high";
|
|
65
|
+
const firstNormal = this.state.queue.findIndex((e) => e.priority === "normal");
|
|
66
|
+
if (firstNormal === -1) {
|
|
67
|
+
this.state.queue.push(entry);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.state.queue.splice(firstNormal, 0, entry);
|
|
71
|
+
}
|
|
72
|
+
this.persist();
|
|
73
|
+
return { action: "enqueued", event: entry };
|
|
74
|
+
}
|
|
75
|
+
// Normal priority: new issue
|
|
76
|
+
this.state.queue.push(entry);
|
|
77
|
+
this.persist();
|
|
78
|
+
return { action: "enqueued", event: entry };
|
|
79
|
+
}
|
|
80
|
+
activate() {
|
|
81
|
+
if (this.state.queue.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
if (!this.hasCapacity())
|
|
84
|
+
return null;
|
|
85
|
+
const event = this.state.queue.shift();
|
|
86
|
+
const active = {
|
|
87
|
+
id: event.id,
|
|
88
|
+
usageId: event.usageId,
|
|
89
|
+
issueNumber: event.issueNumber,
|
|
90
|
+
repo: event.repo,
|
|
91
|
+
notification: event.notification,
|
|
92
|
+
payload: event.payload,
|
|
93
|
+
startedAt: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
this.state.active.push(active);
|
|
96
|
+
this.persist();
|
|
97
|
+
return active;
|
|
98
|
+
}
|
|
99
|
+
releaseById(id) {
|
|
100
|
+
// Fallback: "unknown" releases oldest active event (FIFO)
|
|
101
|
+
if (id === "unknown") {
|
|
102
|
+
if (this.state.active.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const released = this.state.active.shift();
|
|
105
|
+
this.persist();
|
|
106
|
+
return released;
|
|
107
|
+
}
|
|
108
|
+
const idx = this.state.active.findIndex((e) => e.id === id);
|
|
109
|
+
if (idx === -1)
|
|
110
|
+
return null;
|
|
111
|
+
const released = this.state.active.splice(idx, 1)[0];
|
|
112
|
+
this.persist();
|
|
113
|
+
return released;
|
|
114
|
+
}
|
|
115
|
+
hasCapacity() {
|
|
116
|
+
return this.state.active.length < this.state.maxConcurrency;
|
|
117
|
+
}
|
|
118
|
+
queueLength() {
|
|
119
|
+
return this.state.queue.length;
|
|
120
|
+
}
|
|
121
|
+
status() {
|
|
122
|
+
return {
|
|
123
|
+
queueLength: this.state.queue.length,
|
|
124
|
+
activeCount: this.state.active.length,
|
|
125
|
+
maxConcurrency: this.state.maxConcurrency,
|
|
126
|
+
queue: this.state.queue.map((e) => ({
|
|
127
|
+
id: e.id, eventType: e.eventType, repo: e.repo,
|
|
128
|
+
issueNumber: e.issueNumber, priority: e.priority, enqueuedAt: e.enqueuedAt,
|
|
129
|
+
})),
|
|
130
|
+
active: this.state.active.map((e) => ({
|
|
131
|
+
id: e.id, repo: e.repo, issueNumber: e.issueNumber, startedAt: e.startedAt,
|
|
132
|
+
})),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
getActiveEvents() {
|
|
136
|
+
return [...this.state.active];
|
|
137
|
+
}
|
|
138
|
+
checkTimeouts(timeoutMs) {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
const released = [];
|
|
141
|
+
const timedOut = this.state.active.filter((e) => now - new Date(e.startedAt).getTime() > timeoutMs);
|
|
142
|
+
for (const event of timedOut) {
|
|
143
|
+
const rel = this.releaseById(event.id);
|
|
144
|
+
if (rel)
|
|
145
|
+
released.push(rel);
|
|
146
|
+
}
|
|
147
|
+
return released;
|
|
148
|
+
}
|
|
149
|
+
startFileWatcher(completedDir, onRelease) {
|
|
150
|
+
this.completedDir = completedDir;
|
|
151
|
+
fs.mkdirSync(completedDir, { recursive: true });
|
|
152
|
+
// Process any existing signal files first
|
|
153
|
+
this.processSignalFiles(onRelease);
|
|
154
|
+
this.watcher = fs.watch(completedDir, () => {
|
|
155
|
+
this.processSignalFiles(onRelease);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
stopFileWatcher() {
|
|
159
|
+
if (this.watcher) {
|
|
160
|
+
this.watcher.close();
|
|
161
|
+
this.watcher = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
processSignalFiles(onRelease) {
|
|
165
|
+
let files;
|
|
166
|
+
try {
|
|
167
|
+
files = fs.readdirSync(this.completedDir);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const eventId = file;
|
|
174
|
+
const filePath = path.join(this.completedDir, file);
|
|
175
|
+
const released = this.releaseById(eventId);
|
|
176
|
+
try {
|
|
177
|
+
fs.unlinkSync(filePath);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// File may already be deleted; ignore
|
|
181
|
+
}
|
|
182
|
+
if (released) {
|
|
183
|
+
onRelease(eventId, released.usageId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
load(maxConcurrency) {
|
|
188
|
+
try {
|
|
189
|
+
const data = fs.readFileSync(this.filePath, "utf-8");
|
|
190
|
+
const parsed = JSON.parse(data);
|
|
191
|
+
return { ...parsed, maxConcurrency: parsed.maxConcurrency ?? maxConcurrency };
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { queue: [], active: [], maxConcurrency };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
persist() {
|
|
198
|
+
const dir = path.dirname(this.filePath);
|
|
199
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
200
|
+
const tmpPath = this.filePath + ".tmp";
|
|
201
|
+
fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2));
|
|
202
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface IssueEvent {
|
|
2
|
-
type: "issue_opened" | "issue_comment" | "issue_closed";
|
|
2
|
+
type: "issue_opened" | "issue_labeled" | "issue_comment" | "issue_closed";
|
|
3
3
|
repo: string;
|
|
4
4
|
issue: {
|
|
5
5
|
number: number;
|
|
@@ -29,6 +29,7 @@ export type ServerMessage = {
|
|
|
29
29
|
type: "event";
|
|
30
30
|
id: string;
|
|
31
31
|
payload: SentinelEvent;
|
|
32
|
+
usageId?: string;
|
|
32
33
|
notification?: {
|
|
33
34
|
content: string;
|
|
34
35
|
meta: Record<string, string>;
|
|
@@ -56,3 +57,46 @@ export type ClientMessage = {
|
|
|
56
57
|
issue: number;
|
|
57
58
|
session_id: string;
|
|
58
59
|
};
|
|
60
|
+
export interface QueuedEvent {
|
|
61
|
+
id: string;
|
|
62
|
+
usageId: string | null;
|
|
63
|
+
priority: "high" | "normal";
|
|
64
|
+
eventType: string;
|
|
65
|
+
repo: string;
|
|
66
|
+
issueNumber: number;
|
|
67
|
+
notification: {
|
|
68
|
+
content: string;
|
|
69
|
+
meta: Record<string, string>;
|
|
70
|
+
} | null;
|
|
71
|
+
payload: SentinelEvent;
|
|
72
|
+
enqueuedAt: string;
|
|
73
|
+
}
|
|
74
|
+
export interface ActiveEvent {
|
|
75
|
+
id: string;
|
|
76
|
+
usageId: string | null;
|
|
77
|
+
issueNumber: number;
|
|
78
|
+
repo: string;
|
|
79
|
+
notification: {
|
|
80
|
+
content: string;
|
|
81
|
+
meta: Record<string, string>;
|
|
82
|
+
} | null;
|
|
83
|
+
payload: SentinelEvent;
|
|
84
|
+
startedAt: string;
|
|
85
|
+
}
|
|
86
|
+
export interface QueueState {
|
|
87
|
+
queue: QueuedEvent[];
|
|
88
|
+
active: ActiveEvent[];
|
|
89
|
+
maxConcurrency: number;
|
|
90
|
+
}
|
|
91
|
+
export type EnqueueResult = {
|
|
92
|
+
action: "enqueued";
|
|
93
|
+
event: QueuedEvent;
|
|
94
|
+
} | {
|
|
95
|
+
action: "duplicate";
|
|
96
|
+
} | {
|
|
97
|
+
action: "bypass";
|
|
98
|
+
event: QueuedEvent;
|
|
99
|
+
} | {
|
|
100
|
+
action: "dequeue";
|
|
101
|
+
removedId: string;
|
|
102
|
+
};
|
package/dist/ws-client.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface WsClientOptions {
|
|
|
20
20
|
onEvent: (id: string, payload: any, notification?: {
|
|
21
21
|
content: string;
|
|
22
22
|
meta: Record<string, string>;
|
|
23
|
-
}) => void;
|
|
23
|
+
}, usageId?: string) => void;
|
|
24
24
|
onDisplaced: (reason: string) => void;
|
|
25
25
|
onLimitReached?: (message: string) => void;
|
|
26
26
|
log: (msg: string) => void;
|
package/dist/ws-client.js
CHANGED
|
@@ -89,7 +89,7 @@ export class WsClient {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
if (msg.type === "event") {
|
|
92
|
-
this.opts.onEvent(msg.id, msg.payload, msg.notification);
|
|
92
|
+
this.opts.onEvent(msg.id, msg.payload, msg.notification, msg.usageId);
|
|
93
93
|
}
|
|
94
94
|
else if (msg.type === "ping") {
|
|
95
95
|
this.send({ type: "pong" });
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinel-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Sentinel MCP server — connects GitHub issues to Claude Code sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"sentinel-mcp": "dist/index.js"
|
|
8
|
+
"sentinel-mcp": "dist/index.js",
|
|
9
|
+
"sentinel-mcp-release": "bin/release-hook.sh"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
|
-
"dist"
|
|
12
|
+
"dist",
|
|
13
|
+
"bin"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
14
16
|
"build": "tsc",
|