kojee-mcp 0.4.0 → 0.5.2
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/README.md +98 -10
- package/dist/chunk-2TUAFAIW.js +244 -0
- package/dist/{chunk-36DMIXH7.js → chunk-BJMASMKX.js} +13 -23
- package/dist/chunk-BLEGIR35.js +43 -0
- package/dist/chunk-C6GZ2L2W.js +38 -0
- package/dist/{chunk-VZVGTHGF.js → chunk-DO42NPNR.js} +11 -17
- package/dist/chunk-EW72ZNQL.js +39 -0
- package/dist/chunk-F7L25L2J.js +60 -0
- package/dist/{chunk-WHTH6WBP.js → chunk-LSUB6QMP.js} +3 -0
- package/dist/chunk-LVL25VLO.js +22 -0
- package/dist/chunk-SQL56SEB.js +14 -0
- package/dist/chunk-WBMX4CHB.js +378 -0
- package/dist/{chunk-ZGVUM4AG.js → chunk-YEC7IHIG.js} +276 -318
- package/dist/{chunk-E7TE4QZD.js → chunk-YH27B6SW.js} +9 -9
- package/dist/chunk-ZW4SW7LJ.js +225 -0
- package/dist/cli.js +70 -78
- package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
- package/dist/doctor-TSHOMT5X.js +237 -0
- package/dist/doctor-codex-BMI5JOO6.js +130 -0
- package/dist/event-log-RSTM4PLL.js +18 -0
- package/dist/{hook-server-43QS7L7P.js → hook-server-QF5JVUHV.js} +28 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -2
- package/dist/{install-WV25CRU2.js → install-WBIUVBZW.js} +9 -7
- package/dist/{paired-config-OAR3O3XY.js → paired-config-JTFLHMZ2.js} +2 -1
- package/dist/resubscribe-SLZNA76S.js +59 -0
- package/dist/runtime-record-WO4IECM6.js +14 -0
- package/dist/runtimes-CO43XUUK.js +12 -0
- package/dist/{session-discovery-WSHLR4OV.js → session-discovery-FNMJGFPM.js} +2 -1
- package/dist/stop-hook-SEPWWETV.js +119 -0
- package/dist/tail-stream-BYKO4DW6.js +162 -0
- package/dist/{user-prompt-submit-hook-WSRIJVF4.js → user-prompt-submit-hook-ARPEO6FF.js} +5 -4
- package/dist/webhook-config-5TLLX7RA.js +10 -0
- package/dist/webhook-sink-7OYZBWXA.js +163 -0
- package/dist/wizard-7KHD5JT4.js +265 -0
- package/package.json +9 -7
- package/dist/event-log-ETWR6PPY.js +0 -112
- package/dist/stop-hook-5XU3EQAE.js +0 -76
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/tandem/webhook-config.ts
|
|
2
|
+
var WEBHOOK_DEFAULT_TIMEOUT_MS = 5e3;
|
|
3
|
+
var WEBHOOK_DEFAULT_MAX_RETRIES = 4;
|
|
4
|
+
function redactUrlUserinfo(rawUrl, parsed) {
|
|
5
|
+
if (!parsed.username && !parsed.password) return rawUrl;
|
|
6
|
+
parsed.username = "";
|
|
7
|
+
parsed.password = "";
|
|
8
|
+
return parsed.toString();
|
|
9
|
+
}
|
|
10
|
+
function parsePositiveInt(raw, fallback) {
|
|
11
|
+
if (raw === void 0) return fallback;
|
|
12
|
+
const n = Number(raw);
|
|
13
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) return fallback;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
function resolveWebhookConfig(env = process.env) {
|
|
17
|
+
const url = (env["KOJEE_WEBHOOK_URL"] ?? "").trim();
|
|
18
|
+
if (!url) {
|
|
19
|
+
return { enabled: false, config: null };
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(url);
|
|
24
|
+
} catch {
|
|
25
|
+
return {
|
|
26
|
+
enabled: false,
|
|
27
|
+
config: null,
|
|
28
|
+
error: `KOJEE_WEBHOOK_URL is not a valid URL \u2014 webhook sink DISABLED`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
32
|
+
return {
|
|
33
|
+
enabled: false,
|
|
34
|
+
config: null,
|
|
35
|
+
error: `KOJEE_WEBHOOK_URL must be http(s) (got ${parsed.protocol}) \u2014 webhook sink DISABLED`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const secret = (env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
|
|
39
|
+
if (!secret) {
|
|
40
|
+
return {
|
|
41
|
+
enabled: false,
|
|
42
|
+
config: null,
|
|
43
|
+
error: "KOJEE_WEBHOOK_URL is set but KOJEE_WEBHOOK_SECRET is missing \u2014 webhook sink DISABLED (the proxy NEVER sends unsigned webhooks)"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const timeoutMs = parsePositiveInt(env["KOJEE_WEBHOOK_TIMEOUT_MS"], WEBHOOK_DEFAULT_TIMEOUT_MS);
|
|
47
|
+
const maxRetries = parsePositiveInt(env["KOJEE_WEBHOOK_MAX_RETRIES"], WEBHOOK_DEFAULT_MAX_RETRIES);
|
|
48
|
+
const safeUrl = redactUrlUserinfo(url, parsed);
|
|
49
|
+
const redactedSummary = `url=${safeUrl} secret=<redacted> timeoutMs=${timeoutMs} maxRetries=${maxRetries}`;
|
|
50
|
+
return {
|
|
51
|
+
enabled: true,
|
|
52
|
+
config: { url, secret, timeoutMs, maxRetries, redactedSummary }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
WEBHOOK_DEFAULT_TIMEOUT_MS,
|
|
58
|
+
WEBHOOK_DEFAULT_MAX_RETRIES,
|
|
59
|
+
resolveWebhookConfig
|
|
60
|
+
};
|
|
@@ -5,6 +5,7 @@ var NULL_RESULT = {
|
|
|
5
5
|
sessionId: null,
|
|
6
6
|
transcriptPath: null,
|
|
7
7
|
hookEventName: null,
|
|
8
|
+
stopHookActive: false,
|
|
8
9
|
raw: ""
|
|
9
10
|
};
|
|
10
11
|
async function readHookStdin() {
|
|
@@ -21,6 +22,7 @@ async function readHookStdin() {
|
|
|
21
22
|
sessionId: stringOrNull(parsed["session_id"]),
|
|
22
23
|
transcriptPath: stringOrNull(parsed["transcript_path"]),
|
|
23
24
|
hookEventName: stringOrNull(parsed["hook_event_name"]),
|
|
25
|
+
stopHookActive: parsed["stop_hook_active"] === true,
|
|
24
26
|
raw
|
|
25
27
|
};
|
|
26
28
|
} catch {
|
|
@@ -28,6 +30,7 @@ async function readHookStdin() {
|
|
|
28
30
|
sessionId: null,
|
|
29
31
|
transcriptPath: null,
|
|
30
32
|
hookEventName: null,
|
|
33
|
+
stopHookActive: false,
|
|
31
34
|
raw
|
|
32
35
|
};
|
|
33
36
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/wizard/runtimes.ts
|
|
2
|
+
var WIZARD_RUNTIMES = ["claude-code", "hermes", "openclaw", "codex"];
|
|
3
|
+
var WEBHOOK_RUNTIMES = /* @__PURE__ */ new Set([
|
|
4
|
+
"hermes",
|
|
5
|
+
"openclaw"
|
|
6
|
+
]);
|
|
7
|
+
function isWizardRuntime(value) {
|
|
8
|
+
return WIZARD_RUNTIMES.includes(value);
|
|
9
|
+
}
|
|
10
|
+
var RUNTIME_MENU = [
|
|
11
|
+
{ index: 1, runtime: "claude-code" },
|
|
12
|
+
{ index: 2, runtime: "hermes" },
|
|
13
|
+
{ index: 3, runtime: "openclaw" },
|
|
14
|
+
{ index: 4, runtime: "codex" }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
WIZARD_RUNTIMES,
|
|
19
|
+
WEBHOOK_RUNTIMES,
|
|
20
|
+
isWizardRuntime,
|
|
21
|
+
RUNTIME_MENU
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/wizard/home.ts
|
|
2
|
+
import os from "os";
|
|
3
|
+
function kojeeHomeDir() {
|
|
4
|
+
const env = process.env;
|
|
5
|
+
const homeKey = "HOME";
|
|
6
|
+
const profileKey = "USERPROFILE";
|
|
7
|
+
const fromEnv = env[homeKey] ?? env[profileKey];
|
|
8
|
+
if (fromEnv && fromEnv.length > 0) return fromEnv;
|
|
9
|
+
return os.homedir();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
kojeeHomeDir
|
|
14
|
+
};
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// src/auth/dpop.ts
|
|
2
|
+
import { SignJWT, base64url } from "jose";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
async function createDPoPProof(privateKey, kid, method, url, nonce, accessToken) {
|
|
5
|
+
const payload = {
|
|
6
|
+
htm: method,
|
|
7
|
+
htu: url,
|
|
8
|
+
jti: crypto.randomUUID()
|
|
9
|
+
};
|
|
10
|
+
if (nonce) {
|
|
11
|
+
payload.nonce = nonce;
|
|
12
|
+
}
|
|
13
|
+
if (accessToken) {
|
|
14
|
+
payload.ath = computeAth(accessToken);
|
|
15
|
+
}
|
|
16
|
+
const header = {
|
|
17
|
+
typ: "dpop+jwt",
|
|
18
|
+
alg: "ES256",
|
|
19
|
+
jwk: { kid }
|
|
20
|
+
};
|
|
21
|
+
return new SignJWT(payload).setProtectedHeader(header).setIssuedAt().sign(privateKey);
|
|
22
|
+
}
|
|
23
|
+
function computeAth(accessToken) {
|
|
24
|
+
const hash = crypto.createHash("sha256").update(accessToken).digest();
|
|
25
|
+
return base64url.encode(hash);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/tandem/session-id.ts
|
|
29
|
+
import { ulid } from "ulidx";
|
|
30
|
+
var MCP_SESSION_ID = ulid();
|
|
31
|
+
|
|
32
|
+
// src/tandem/event-stream.ts
|
|
33
|
+
var STALE_FLOOR_MS = 9e4;
|
|
34
|
+
var STALE_INTERVAL_MULTIPLIER = 3;
|
|
35
|
+
var STALE_CHECK_INTERVAL_MS = 5e3;
|
|
36
|
+
var UNARMED_FALLBACK_MS = 12 * 6e4;
|
|
37
|
+
function createAdaptiveWatchdog(options = {}) {
|
|
38
|
+
const floorMs = options.floorMs ?? STALE_FLOOR_MS;
|
|
39
|
+
const multiplier = options.multiplier ?? STALE_INTERVAL_MULTIPLIER;
|
|
40
|
+
const unarmedFallbackMs = options.unarmedFallbackMs ?? UNARMED_FALLBACK_MS;
|
|
41
|
+
let prevHeartbeatAt = null;
|
|
42
|
+
let lastIntervalMs = null;
|
|
43
|
+
function thresholdMs() {
|
|
44
|
+
if (lastIntervalMs === null) return null;
|
|
45
|
+
return Math.max(lastIntervalMs * multiplier, floorMs);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
onHeartbeat(now) {
|
|
49
|
+
if (prevHeartbeatAt !== null) {
|
|
50
|
+
const interval = now - prevHeartbeatAt;
|
|
51
|
+
if (interval > 0) lastIntervalMs = interval;
|
|
52
|
+
}
|
|
53
|
+
prevHeartbeatAt = now;
|
|
54
|
+
},
|
|
55
|
+
onByte(_now) {
|
|
56
|
+
},
|
|
57
|
+
shouldAbort(lastByteAt, now) {
|
|
58
|
+
const t = thresholdMs();
|
|
59
|
+
if (t === null) {
|
|
60
|
+
return now - lastByteAt >= unarmedFallbackMs;
|
|
61
|
+
}
|
|
62
|
+
return now - lastByteAt >= t;
|
|
63
|
+
},
|
|
64
|
+
armedThresholdMs() {
|
|
65
|
+
return thresholdMs();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
70
|
+
var BACKOFF_CEILING_MS = 3e4;
|
|
71
|
+
var STABLE_CONNECTION_MS = 1e4;
|
|
72
|
+
function createBackoffController(options = {}) {
|
|
73
|
+
const baseMs = options.baseMs ?? BACKOFF_BASE_MS;
|
|
74
|
+
const ceilingMs = options.ceilingMs ?? BACKOFF_CEILING_MS;
|
|
75
|
+
const stableMs = options.stableMs ?? STABLE_CONNECTION_MS;
|
|
76
|
+
let backoffMs = baseMs;
|
|
77
|
+
let connectedAt = null;
|
|
78
|
+
return {
|
|
79
|
+
currentBackoffMs() {
|
|
80
|
+
return backoffMs;
|
|
81
|
+
},
|
|
82
|
+
markConnected(now) {
|
|
83
|
+
connectedAt = now;
|
|
84
|
+
},
|
|
85
|
+
markDisconnected(now) {
|
|
86
|
+
const lasted = connectedAt !== null ? now - connectedAt : 0;
|
|
87
|
+
connectedAt = null;
|
|
88
|
+
if (lasted >= stableMs) {
|
|
89
|
+
backoffMs = baseMs;
|
|
90
|
+
} else {
|
|
91
|
+
backoffMs = Math.min(backoffMs * 2, ceilingMs);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
var LOG_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
97
|
+
async function startEventStream(opts) {
|
|
98
|
+
let stopped = false;
|
|
99
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
100
|
+
let currentController = new AbortController();
|
|
101
|
+
let connectGeneration = 0;
|
|
102
|
+
const state = {
|
|
103
|
+
connected: false,
|
|
104
|
+
connectedSince: null,
|
|
105
|
+
lastEventAt: null,
|
|
106
|
+
lastHeartbeatAt: null,
|
|
107
|
+
reconnectCount: 0,
|
|
108
|
+
// null until the adaptive watchdog learns a cadence (≥2 heartbeats).
|
|
109
|
+
staleAfterMs: null
|
|
110
|
+
};
|
|
111
|
+
void (async function loop() {
|
|
112
|
+
const backoff = createBackoffController();
|
|
113
|
+
while (!stopped) {
|
|
114
|
+
currentController = new AbortController();
|
|
115
|
+
const isReconnect = connectGeneration > 0;
|
|
116
|
+
try {
|
|
117
|
+
const opened = await connectAndConsume(
|
|
118
|
+
opts,
|
|
119
|
+
cursors,
|
|
120
|
+
currentController,
|
|
121
|
+
isReconnect,
|
|
122
|
+
state,
|
|
123
|
+
(tandemId, cursor) => {
|
|
124
|
+
const prev = cursors.get(tandemId);
|
|
125
|
+
if (prev === void 0 || cursor > prev) cursors.set(tandemId, cursor);
|
|
126
|
+
},
|
|
127
|
+
() => {
|
|
128
|
+
backoff.markConnected(Date.now());
|
|
129
|
+
connectGeneration += 1;
|
|
130
|
+
state.connected = true;
|
|
131
|
+
state.connectedSince = Date.now();
|
|
132
|
+
state.reconnectCount = connectGeneration - 1;
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
if (!opened && stopped) return;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (stopped) return;
|
|
138
|
+
console.error("[event-stream] disconnect:", err.message);
|
|
139
|
+
state.connected = false;
|
|
140
|
+
await emitStatus(opts, `status=disconnected reason=${statusReason(err)}`);
|
|
141
|
+
}
|
|
142
|
+
if (stopped) return;
|
|
143
|
+
state.connected = false;
|
|
144
|
+
backoff.markDisconnected(Date.now());
|
|
145
|
+
const jitter = Math.random() * backoff.currentBackoffMs();
|
|
146
|
+
await sleep(jitter);
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
const handle = (() => {
|
|
150
|
+
stopped = true;
|
|
151
|
+
state.connected = false;
|
|
152
|
+
currentController.abort();
|
|
153
|
+
});
|
|
154
|
+
handle.getState = () => ({
|
|
155
|
+
connected: state.connected,
|
|
156
|
+
connectedSince: state.connectedSince,
|
|
157
|
+
lastEventAt: state.lastEventAt,
|
|
158
|
+
lastHeartbeatAt: state.lastHeartbeatAt,
|
|
159
|
+
cursors: Object.fromEntries(cursors),
|
|
160
|
+
reconnectCount: state.reconnectCount,
|
|
161
|
+
staleAfterMs: state.staleAfterMs
|
|
162
|
+
});
|
|
163
|
+
return handle;
|
|
164
|
+
}
|
|
165
|
+
async function connectAndConsume(opts, cursors, controller, isReconnect, state, onCursor, onOpen) {
|
|
166
|
+
const mapSince = cursors.size > 0 ? serializeCursorMap(cursors) : null;
|
|
167
|
+
let res = await openStream(opts, controller, mapSince);
|
|
168
|
+
if (res.status === 400 && mapSince !== null) {
|
|
169
|
+
console.error("[event-stream] per-room ?since rejected (400) \u2014 falling back to bare cursor");
|
|
170
|
+
res = await openStream(opts, controller, String(Math.min(...cursors.values())));
|
|
171
|
+
}
|
|
172
|
+
if (!res.ok) throw new Error(`SSE connect failed: ${res.status}`);
|
|
173
|
+
if (!res.body) throw new Error("SSE response has no body");
|
|
174
|
+
onOpen();
|
|
175
|
+
await emitStatus(opts, isReconnect ? "status=reconnected" : "status=connected");
|
|
176
|
+
if (opts.onConnected) {
|
|
177
|
+
void Promise.resolve().then(() => opts.onConnected()).catch((err) => {
|
|
178
|
+
console.error("[event-stream] onConnected (resubscribe) failed:", err.message);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
await consumeSse(res.body, opts, controller, state, onCursor);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
async function openStream(opts, controller, sinceValue) {
|
|
185
|
+
const params = new URLSearchParams();
|
|
186
|
+
if (sinceValue !== null) params.set("since", sinceValue);
|
|
187
|
+
const url = `${opts.brokerUrl}/api/v2/tandems/stream${params.toString() ? "?" + params.toString() : ""}`;
|
|
188
|
+
const proof = await createDPoPProof(
|
|
189
|
+
opts.gateway.getPrivateKey(),
|
|
190
|
+
opts.gateway.getKid(),
|
|
191
|
+
"GET",
|
|
192
|
+
url,
|
|
193
|
+
void 0,
|
|
194
|
+
opts.token
|
|
195
|
+
);
|
|
196
|
+
return fetch(url, {
|
|
197
|
+
method: "GET",
|
|
198
|
+
headers: {
|
|
199
|
+
Authorization: `DPoP ${opts.token}`,
|
|
200
|
+
DPoP: proof,
|
|
201
|
+
"Mcp-Session-Id": MCP_SESSION_ID,
|
|
202
|
+
Accept: "text/event-stream"
|
|
203
|
+
},
|
|
204
|
+
signal: controller.signal
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function consumeSse(body, opts, controller, state, onCursor) {
|
|
208
|
+
const reader = body.getReader();
|
|
209
|
+
const decoder = new TextDecoder();
|
|
210
|
+
let buffer = "";
|
|
211
|
+
let lastByteAt = Date.now();
|
|
212
|
+
const watchdog = createAdaptiveWatchdog();
|
|
213
|
+
let lastLogHeartbeatAt = 0;
|
|
214
|
+
const watchdogTimer = setInterval(() => {
|
|
215
|
+
if (watchdog.shouldAbort(lastByteAt, Date.now())) {
|
|
216
|
+
const armed = watchdog.armedThresholdMs();
|
|
217
|
+
const reason = armed !== null ? `no bytes for \u2265${armed}ms, cadence learned` : `no bytes for \u2265${UNARMED_FALLBACK_MS}ms, UNARMED fallback (no cadence learned \u2014 presumed wedged)`;
|
|
218
|
+
console.error(`[event-stream] stale connection (${reason}) \u2014 aborting to reconnect`);
|
|
219
|
+
controller.abort();
|
|
220
|
+
}
|
|
221
|
+
}, STALE_CHECK_INTERVAL_MS);
|
|
222
|
+
try {
|
|
223
|
+
while (true) {
|
|
224
|
+
const { value, done } = await reader.read();
|
|
225
|
+
if (done) return;
|
|
226
|
+
lastByteAt = Date.now();
|
|
227
|
+
watchdog.onByte(lastByteAt);
|
|
228
|
+
state.lastEventAt = lastByteAt;
|
|
229
|
+
buffer += decoder.decode(value, { stream: true });
|
|
230
|
+
const events = drainSseEvents(buffer);
|
|
231
|
+
buffer = events.remaining;
|
|
232
|
+
for (const evt of events.events) {
|
|
233
|
+
if (evt.event === "stream_revoked") {
|
|
234
|
+
throw new Error("stream_revoked \u2014 reconnect needed");
|
|
235
|
+
}
|
|
236
|
+
if (evt.event === "heartbeat") {
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
state.lastHeartbeatAt = now;
|
|
239
|
+
watchdog.onHeartbeat(now);
|
|
240
|
+
state.staleAfterMs = watchdog.armedThresholdMs();
|
|
241
|
+
if (now - lastLogHeartbeatAt >= LOG_HEARTBEAT_INTERVAL_MS) {
|
|
242
|
+
lastLogHeartbeatAt = now;
|
|
243
|
+
void opts.eventLog?.appendStatus("status=heartbeat").catch(() => {
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (evt.event === "message" || evt.event === "state_change") {
|
|
249
|
+
try {
|
|
250
|
+
const raw = JSON.parse(evt.data);
|
|
251
|
+
const parsed = normalizeBackendEvent(raw, evt.event);
|
|
252
|
+
onCursor(parsed.tandem_id, parsed.cursor);
|
|
253
|
+
opts.queue?.push(parsed);
|
|
254
|
+
if (opts.eventLog) {
|
|
255
|
+
try {
|
|
256
|
+
await opts.eventLog.append(parsed);
|
|
257
|
+
opts.queue?.markMonitorDelivered(parsed.id);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error("[event-stream] event-log append failed:", err);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (opts.adapter.supportsChannels) {
|
|
263
|
+
try {
|
|
264
|
+
const channel = opts.adapter.formatTandemEvent(parsed);
|
|
265
|
+
await opts.server.notification({
|
|
266
|
+
method: "notifications/claude/channel",
|
|
267
|
+
params: channel
|
|
268
|
+
});
|
|
269
|
+
opts.queue?.markChannelDelivered(parsed.id);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error("[event-stream] channel notification failed:", err);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
opts.webhookSink?.enqueue(parsed);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error("[event-stream] webhook enqueue failed:", err);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error("[event-stream] failed to handle event:", err);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} finally {
|
|
286
|
+
clearInterval(watchdogTimer);
|
|
287
|
+
try {
|
|
288
|
+
reader.releaseLock();
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function emitStatus(opts, fields) {
|
|
294
|
+
try {
|
|
295
|
+
await opts.eventLog?.appendStatus(fields);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function statusReason(err) {
|
|
300
|
+
const msg = err?.message ?? String(err);
|
|
301
|
+
return msg.replace(/\s+/g, "_").slice(0, 60);
|
|
302
|
+
}
|
|
303
|
+
function serializeCursorMap(cursors) {
|
|
304
|
+
return [...cursors.entries()].map(([tandemId, cursor]) => `${tandemId}:${cursor}`).join(",");
|
|
305
|
+
}
|
|
306
|
+
function drainSseEvents(input) {
|
|
307
|
+
const events = [];
|
|
308
|
+
const parts = input.split("\n\n");
|
|
309
|
+
const remaining = parts.pop() ?? "";
|
|
310
|
+
for (const block of parts) {
|
|
311
|
+
let id;
|
|
312
|
+
let event = "message";
|
|
313
|
+
const dataLines = [];
|
|
314
|
+
for (const line of block.split("\n")) {
|
|
315
|
+
if (line.startsWith("id: ")) id = line.slice(4);
|
|
316
|
+
else if (line.startsWith("event: ")) event = line.slice(7);
|
|
317
|
+
else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
|
|
318
|
+
}
|
|
319
|
+
if (dataLines.length > 0) events.push({ id, event, data: dataLines.join("\n") });
|
|
320
|
+
}
|
|
321
|
+
return { events, remaining };
|
|
322
|
+
}
|
|
323
|
+
function sleep(ms) {
|
|
324
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
325
|
+
}
|
|
326
|
+
var MAX_DISPLAYNAME_CHARS = 64;
|
|
327
|
+
function sanitizeDisplayname(name) {
|
|
328
|
+
return name.replace(/[\x00-\x1f\x7f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_DISPLAYNAME_CHARS);
|
|
329
|
+
}
|
|
330
|
+
function normalizeBackendEvent(raw, sseEventType) {
|
|
331
|
+
const obj = raw ?? {};
|
|
332
|
+
const maybeFrom = obj["from"];
|
|
333
|
+
if (maybeFrom && typeof maybeFrom["principal"] === "string") {
|
|
334
|
+
return raw;
|
|
335
|
+
}
|
|
336
|
+
const sender = obj["sender"] ?? {};
|
|
337
|
+
const principal = sender["principal_id"] ?? "";
|
|
338
|
+
const agentId = sender["agent_id"];
|
|
339
|
+
const rawSessionId = sender["session_id"];
|
|
340
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.trim() ? rawSessionId : void 0;
|
|
341
|
+
const rawSeverity = obj["severity"];
|
|
342
|
+
const severity = typeof rawSeverity === "string" && rawSeverity.trim() ? rawSeverity : void 0;
|
|
343
|
+
const rawDisplay = sender["display"];
|
|
344
|
+
const trimmedDisplay = typeof rawDisplay === "string" ? rawDisplay.trim() : "";
|
|
345
|
+
const safeDisplay = trimmedDisplay ? sanitizeDisplayname(trimmedDisplay) : "";
|
|
346
|
+
const displayname = safeDisplay ? safeDisplay : principal ? `principal:${principal.slice(0, 8)}` : "unknown";
|
|
347
|
+
const type = sseEventType === "state_change" ? "state_change" : "message";
|
|
348
|
+
const kind = obj["kind"] ?? "message";
|
|
349
|
+
return {
|
|
350
|
+
type,
|
|
351
|
+
id: obj["message_id"] ?? obj["id"] ?? "",
|
|
352
|
+
tandem_id: obj["tandem_id"] ?? "",
|
|
353
|
+
cursor: obj["cursor"] ?? 0,
|
|
354
|
+
time: obj["time"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
355
|
+
from: {
|
|
356
|
+
member_id: "",
|
|
357
|
+
principal,
|
|
358
|
+
...agentId ? { agent_id: agentId } : {},
|
|
359
|
+
...sessionId ? { session_id: sessionId } : {},
|
|
360
|
+
displayname
|
|
361
|
+
},
|
|
362
|
+
kind,
|
|
363
|
+
content: {
|
|
364
|
+
body: obj["body"] ?? "",
|
|
365
|
+
...typeof obj["format"] === "string" ? { format: obj["format"] } : {}
|
|
366
|
+
},
|
|
367
|
+
...Array.isArray(obj["mentions"]) ? { mentions: obj["mentions"] } : {},
|
|
368
|
+
...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {},
|
|
369
|
+
...severity ? { severity } : {}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export {
|
|
374
|
+
createDPoPProof,
|
|
375
|
+
MCP_SESSION_ID,
|
|
376
|
+
createAdaptiveWatchdog,
|
|
377
|
+
startEventStream
|
|
378
|
+
};
|