kojee-mcp 0.5.10 → 0.5.12

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.
Files changed (35) hide show
  1. package/README.md +24 -0
  2. package/dist/ancestry-ONFBQEP5.js +8 -0
  3. package/dist/{codex-stop-hook-SWA53ECG.js → chunk-35XBRG3V.js} +4 -3
  4. package/dist/{chunk-MKDMAAMN.js → chunk-5XP2UOFK.js} +12 -0
  5. package/dist/{chunk-DS26OORG.js → chunk-CO73VGWM.js} +41 -23
  6. package/dist/chunk-FQZCENSG.js +459 -0
  7. package/dist/{chunk-YKS6YZKM.js → chunk-PHXO5P25.js} +1 -4
  8. package/dist/chunk-WLMPCX7T.js +116 -0
  9. package/dist/chunk-XLKGPGZT.js +0 -0
  10. package/dist/chunk-XXFVWP6H.js +44 -0
  11. package/dist/{ensure-join-7AEDJMPE.js → chunk-YKW54DKF.js} +45 -15
  12. package/dist/cli.js +16 -13
  13. package/dist/codex-stop-hook-VY7DOMAG.js +16 -0
  14. package/dist/{doctor-XK335W7B.js → doctor-FVTALRQD.js} +110 -15
  15. package/dist/ensure-join-5Y5IJ7HN.js +8 -0
  16. package/dist/{event-log-B27VVEMK.js → event-log-VZD7NKYX.js} +1 -1
  17. package/dist/event-stream-FOT7MJZH.js +19 -0
  18. package/dist/{gateway-client-93P1E0CZ.d.ts → gateway-client-C6yx1mfM.d.ts} +6 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +7 -5
  21. package/dist/lib.d.ts +181 -3
  22. package/dist/lib.js +7 -7
  23. package/dist/{parent-watchdog-RZLHYP7T.js → parent-watchdog-TLU355FB.js} +1 -1
  24. package/dist/registry-A3VT6VJD.js +348 -0
  25. package/dist/server-77QRWKJM.js +14 -0
  26. package/dist/{stop-hook-OTCJGL6V.js → stop-hook-CUVDKXP7.js} +8 -7
  27. package/dist/{tail-stream-JNR4WFW3.js → tail-stream-VZ462ZON.js} +3 -2
  28. package/dist/{user-prompt-submit-hook-QXMC7EZU.js → user-prompt-submit-hook-PMBUPKUV.js} +6 -6
  29. package/dist/{webhook-sink-NWGCUDGY.js → webhook-sink-N6AUTFL3.js} +1 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-SCDWPGH3.js +0 -637
  32. package/dist/{chunk-BJMASMKX.js → chunk-VHKPWUX7.js} +0 -0
  33. package/dist/{doctor-codex-SMROUYGV.js → doctor-codex-PA3WO6LR.js} +1 -1
  34. package/dist/{send-cli-CN5EX7PO.js → send-cli-NZP5XE7T.js} +5 -5
  35. package/dist/{wizard-PLGHYCT3.js → wizard-L4MYRLJI.js} +11 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -1,637 +0,0 @@
1
- import {
2
- buildCatchUpNote,
3
- buildMonitorSpawn,
4
- buildReplyRecipe
5
- } from "./chunk-X672ZN7V.js";
6
- import {
7
- deriveDiscoveryKey,
8
- findClaudeAncestorPid
9
- } from "./chunk-BJMASMKX.js";
10
- import {
11
- AuthModule
12
- } from "./chunk-JXMVZEQ7.js";
13
- import {
14
- GatewayClient
15
- } from "./chunk-HSR3GXCL.js";
16
- import {
17
- startEventStream
18
- } from "./chunk-MKDMAAMN.js";
19
- import {
20
- translateToolCallResult
21
- } from "./chunk-LDZXU3DW.js";
22
-
23
- // src/index.ts
24
- import fs2 from "fs";
25
- import os from "os";
26
- import path2 from "path";
27
-
28
- // src/tool-registry.ts
29
- var ToolRegistry = class {
30
- constructor(gateway) {
31
- this.gateway = gateway;
32
- }
33
- gateway;
34
- /** Flat map: tool name → full tool definition */
35
- tools = /* @__PURE__ */ new Map();
36
- /**
37
- * Fetch all tools with full schemas from the gateway in a single RPC call.
38
- */
39
- async discoverTools() {
40
- console.error("[tools] Fetching tools with full schemas from gateway...");
41
- const result = await this.gateway.sendRpc("tools/list", {
42
- include_schema: true
43
- });
44
- const maybeError = result;
45
- if (maybeError.isError) {
46
- const msg = maybeError.content?.[0]?.text ?? "unknown error";
47
- throw new Error(`Gateway rejected tools/list: ${msg}`);
48
- }
49
- const toolList = result?.tools;
50
- if (!toolList || !Array.isArray(toolList)) {
51
- console.error("[tools] No tools returned from gateway");
52
- return;
53
- }
54
- for (const tool of toolList) {
55
- if (this.tools.has(tool.name)) {
56
- console.error(`[tools] Warning: duplicate tool name "${tool.name}" \u2014 overwriting`);
57
- }
58
- this.tools.set(tool.name, tool);
59
- }
60
- console.error(`[tools] Registered ${this.tools.size} tools from gateway`);
61
- }
62
- /**
63
- * Return all registered tools for the MCP ListTools response.
64
- */
65
- getAllTools() {
66
- return Array.from(this.tools.values());
67
- }
68
- /**
69
- * Call a tool through the gateway.
70
- */
71
- async callTool(name, args) {
72
- if (!this.tools.has(name)) {
73
- const available = Array.from(this.tools.keys()).slice(0, 10).join(", ");
74
- return {
75
- content: [
76
- {
77
- type: "text",
78
- text: `Tool '${name}' not found. Some available tools: ${available}${this.tools.size > 10 ? ", ..." : ""}`
79
- }
80
- ],
81
- isError: true
82
- };
83
- }
84
- return this.gateway.sendRpc("tools/call", {
85
- name,
86
- arguments: args
87
- });
88
- }
89
- /** Total number of registered tools. */
90
- get toolCount() {
91
- return this.tools.size;
92
- }
93
- };
94
-
95
- // src/server.ts
96
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
97
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
98
- import {
99
- ListToolsRequestSchema,
100
- CallToolRequestSchema
101
- } from "@modelcontextprotocol/sdk/types.js";
102
-
103
- // src/version.ts
104
- import fs from "fs";
105
- import path from "path";
106
- import { fileURLToPath } from "url";
107
- var FALLBACK_VERSION = "0.0.0-unknown";
108
- function resolveVersion() {
109
- try {
110
- const here = path.dirname(fileURLToPath(import.meta.url));
111
- const parsed = JSON.parse(
112
- fs.readFileSync(path.join(here, "..", "package.json"), "utf8")
113
- );
114
- return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
115
- } catch (err) {
116
- process.stderr.write(
117
- `kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
118
- `
119
- );
120
- return FALLBACK_VERSION;
121
- }
122
- }
123
- var VERSION = resolveVersion();
124
-
125
- // src/server.ts
126
- function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
127
- const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
128
-
129
- (1) If channel notifications are available, you'll see them as \`<channel source="kojee-mcp" ...>\` tags in your context. Each tag carries tandem_id, message_id, from_principal, kind, severity. To respond: ${buildReplyRecipe()}.
130
-
131
- `;
132
- const monitorSection = `(2) For sustained wake-from-idle WITHOUT channel notifications, the proxy writes one event line per Tandem message to ${eventLogPath}. At the start of every session, spawn ONE Monitor task: ${buildMonitorSpawn(eventLogPath)}. Each appended line will arrive as a separate wake notification, and carries msg=<id> and cursor=<n>. To respond: ${buildReplyRecipe()}. ${buildCatchUpNote()} (\`kojee-mcp tail\` is a portable line-streamer shipped with this proxy \u2014 works on macOS, Linux, and Windows. It follows BOTH the messages log above and a status sibling; status/heartbeat telemetry never wakes you \u2014 only real messages do.)
133
-
134
- `;
135
- const listenSection = "(3) If you want to BLOCK until any single reply lands (rather than receive a stream of events), call tandem_listen(tandem_id, since=cursor, timeout_ms=N) instead.";
136
- const advice = "\n\nPrefer (2) at session start \u2014 it's the default no-allowlist wake mechanism. (1) supplements it when channels are enabled; (3) is for one-shot blocking waits.";
137
- return intro + monitorSection + listenSection + advice;
138
- }
139
- async function executeToolCall(registry, name, args, hooks) {
140
- const rawResult = await registry.callTool(name, args);
141
- const result = translateToolCallResult(rawResult);
142
- if (name === "tandem_join" && !result.isError) {
143
- try {
144
- hooks?.onTandemJoin?.();
145
- } catch (err) {
146
- console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
147
- }
148
- }
149
- return result;
150
- }
151
- function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath, hooks) {
152
- const capabilities = { tools: {} };
153
- if (adapter.supportsChannels) {
154
- capabilities.experimental = { "claude/channel": {} };
155
- }
156
- const server = new Server(
157
- { name: "kojee-mcp", version: VERSION },
158
- {
159
- capabilities,
160
- ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
161
- }
162
- );
163
- server.setRequestHandler(ListToolsRequestSchema, async () => {
164
- const tools = registry.getAllTools().map((t) => ({
165
- name: t.name,
166
- description: t.description,
167
- inputSchema: t.inputSchema
168
- }));
169
- return { tools };
170
- });
171
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
172
- const { name, arguments: args } = request.params;
173
- const result = await executeToolCall(registry, name, args ?? {}, hooks);
174
- return { content: result.content, isError: result.isError };
175
- });
176
- return server;
177
- }
178
- async function startMcpServer(server) {
179
- const transport = new StdioServerTransport();
180
- await server.connect(transport);
181
- console.error("[mcp] Server started on stdio transport");
182
- }
183
-
184
- // src/runtime/detect.ts
185
- import psList from "ps-list";
186
- async function detectRuntime(env = process.env) {
187
- if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
188
- if (env["KOJEE_RUNTIME"] === "codex") return "codex";
189
- if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
190
- const ancestor = await detectRuntimeFromAncestry();
191
- if (ancestor) return ancestor;
192
- return "unknown";
193
- }
194
- async function detectRuntimeFromAncestry() {
195
- try {
196
- const processes = await psList();
197
- const byPid = new Map(processes.map((p) => [p.pid, p]));
198
- let pid = process.ppid;
199
- for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
200
- const row = byPid.get(pid);
201
- if (!row) return null;
202
- const haystack = `${row.name} ${row.cmd ?? ""}`;
203
- if (/(^|\/|\\)claude(\.app|\.exe)?(\/|$|\s|\\)/i.test(haystack) || /Claude Helper/i.test(haystack)) {
204
- return "claude-code";
205
- }
206
- if (/(^|\/|\\)codex(\.exe)?(\/|$|\s|\\)/i.test(haystack)) {
207
- return "codex";
208
- }
209
- pid = row.ppid;
210
- }
211
- } catch {
212
- }
213
- return null;
214
- }
215
-
216
- // src/adapters/claude-code.ts
217
- function computeSeverity(event) {
218
- if (event.type === "state_change") return "high";
219
- if (event.mentions && event.mentions.length > 0) {
220
- return "high";
221
- }
222
- return "normal";
223
- }
224
- function formatBody(event) {
225
- if (event.type === "state_change") {
226
- return `[Tandem: ${event.tandem_id}] ${event.from.displayname}: ${event.content.body}`;
227
- }
228
- return [
229
- `[Tandem: ${event.tandem_id}] ${event.from.displayname} (${event.from.principal}) \u2014 ${event.kind}:`,
230
- event.content.body,
231
- "",
232
- `> ${buildReplyRecipe({ tandem_id: event.tandem_id, message_id: event.id })}`
233
- ].join("\n");
234
- }
235
- var claudeCodeAdapter = {
236
- runtime: "claude-code",
237
- supportsChannels: true,
238
- formatTandemEvent(event) {
239
- const meta = {
240
- tandem_id: event.tandem_id,
241
- message_id: event.id,
242
- cursor: String(event.cursor),
243
- kind: event.kind,
244
- from_principal: event.from.principal,
245
- from_display: event.from.displayname,
246
- severity: computeSeverity(event)
247
- };
248
- if (event.wake_reason) meta.wake_reason = event.wake_reason;
249
- return { content: formatBody(event), meta };
250
- }
251
- };
252
-
253
- // src/adapters/codex.ts
254
- var codexAdapter = {
255
- runtime: "codex",
256
- supportsChannels: false,
257
- // Codex has NO Claude-style channel injection
258
- formatTandemEvent() {
259
- throw new Error(
260
- "codexAdapter.formatTandemEvent() is unreachable \u2014 Codex has no channel injection; server.ts gates this on supportsChannels. Codex receives events via the webhook sink + a model-chosen bounded tandem_listen."
261
- );
262
- }
263
- };
264
-
265
- // src/adapters/unknown.ts
266
- var unknownAdapter = {
267
- runtime: "unknown",
268
- supportsChannels: false,
269
- formatTandemEvent() {
270
- throw new Error(
271
- "unknownAdapter.formatTandemEvent() is unreachable \u2014 caller should gate on supportsChannels"
272
- );
273
- }
274
- };
275
-
276
- // src/index.ts
277
- var DEFAULT_KEYSTORE_PATH = path2.join(os.homedir(), ".kojee", "keypair.json");
278
- function isDPoPEnrollmentError(err) {
279
- const msg = String(err?.message ?? err ?? "").toLowerCase();
280
- if (msg.includes("invalid or expired") && msg.includes("token")) return false;
281
- if (msg.includes("generate a new")) return false;
282
- return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
283
- }
284
- async function selectAdapter() {
285
- const runtime = await detectRuntime();
286
- if (runtime === "claude-code") return claudeCodeAdapter;
287
- if (runtime === "codex") return codexAdapter;
288
- return unknownAdapter;
289
- }
290
- async function listTandemIds(gateway) {
291
- const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
292
- const maybeErr = result;
293
- if (maybeErr.isError) return null;
294
- const text = maybeErr.content?.[0]?.text;
295
- try {
296
- const parsed = text ? JSON.parse(text) : {};
297
- const list = Array.isArray(parsed.tandems) ? parsed.tandems : Array.isArray(parsed) ? parsed : null;
298
- if (!Array.isArray(list)) return [];
299
- return list.map((t) => {
300
- if (typeof t === "string") return t;
301
- const obj = t;
302
- if (obj?.my_membership?.is_member !== true) return void 0;
303
- return obj?.tandem_id ?? obj?.id;
304
- }).filter((id) => typeof id === "string" && id.length > 0);
305
- } catch {
306
- return null;
307
- }
308
- }
309
- function needsWebhookEventStream() {
310
- return (process.env["KOJEE_WEBHOOK_URL"] ?? "").trim().length > 0;
311
- }
312
- async function startProxy(config) {
313
- const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
314
- const adapter = await selectAdapter();
315
- console.error(`[kojee-mcp] Starting proxy for ${config.url} (runtime=${adapter.runtime})`);
316
- const { registry, gateway } = await enrollAndDiscover(config, keystorePath);
317
- console.error(
318
- `[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
319
- );
320
- let activeStreamHandle = null;
321
- const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-ARV6JIWK.js");
322
- const joinReconnect = createJoinReconnectScheduler({
323
- // BOOT-RACE (Bug B): report whether the stream handle was actually ready.
324
- // `false` ⇒ activeStreamHandle is still null (tandem_join fired before the
325
- // stream was set up) → the scheduler queues the reconnect and flushes it on
326
- // notifyReady() once the handle is assigned (see below), instead of silently
327
- // dropping it as the old `activeStreamHandle?.reconnect()` no-op did.
328
- reconnect: () => {
329
- if (!activeStreamHandle) return false;
330
- activeStreamHandle.reconnect();
331
- return true;
332
- }
333
- });
334
- const onTandemJoin = () => joinReconnect.requestReconnect();
335
- const teardownSteps = [];
336
- let shuttingDown = false;
337
- function shutdown(reason) {
338
- if (shuttingDown) return;
339
- shuttingDown = true;
340
- activeStreamHandle?.();
341
- for (const step of teardownSteps) {
342
- try {
343
- const maybe = step();
344
- if (maybe && typeof maybe.catch === "function") {
345
- maybe.catch((err) => {
346
- console.error("[kojee-mcp] async shutdown step failed:", err?.message ?? err);
347
- });
348
- }
349
- } catch (err) {
350
- console.error("[kojee-mcp] shutdown step failed:", err?.message ?? err);
351
- }
352
- }
353
- console.error(`[kojee-mcp] shutting down (${reason}), exiting`);
354
- process.exit(0);
355
- }
356
- const ccPid = await findClaudeAncestorPid();
357
- const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
358
- await ensureJoinTandems({
359
- gateway,
360
- env: process.env["KOJEE_TANDEMS"],
361
- listTandems: () => listTandemIds(gateway),
362
- onJoined: () => joinReconnect.requestReconnect()
363
- });
364
- let tandemMembershipCount = -1;
365
- try {
366
- const bootIds = await listTandemIds(gateway);
367
- tandemMembershipCount = bootIds === null ? -1 : bootIds.length;
368
- } catch (err) {
369
- console.error("[kojee-mcp] tandem_list probe failed:", err.message);
370
- }
371
- console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
372
- let server;
373
- if (adapter.supportsChannels) {
374
- const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
375
- const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
376
- const {
377
- writeDiscoveryByKey,
378
- cleanupDiscoveryByKey,
379
- sweepStaleDiscovery
380
- } = await import("./session-discovery-FNMJGFPM.js");
381
- const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
382
- const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
383
- const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
384
- const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
385
- sweepStaleDiscovery();
386
- sweepStaleEventLogs();
387
- const projectDir = process.env["CLAUDE_PROJECT_DIR"];
388
- const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
389
- const eventLog = startEventLog({ key: discoveryKey });
390
- const webhookResolution = resolveWebhookConfig();
391
- if (webhookResolution.error) {
392
- console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
393
- void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
394
- });
395
- }
396
- if (webhookResolution.warning) {
397
- console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
398
- void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
399
- });
400
- }
401
- const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
402
- // Route delivery/failure observability to the STATUS sink.
403
- log: (line) => {
404
- void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
405
- });
406
- }
407
- }) : null;
408
- if (webhookSink) {
409
- console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
410
- void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
411
- });
412
- }
413
- server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
414
- onTandemJoin
415
- });
416
- const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
417
- let controlToken = null;
418
- try {
419
- controlToken = issueControlToken();
420
- } catch (err) {
421
- console.error(
422
- "[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
423
- err.message
424
- );
425
- }
426
- const queue = new EventQueue();
427
- let streamHandle = null;
428
- const hookServer = await startHookServer({
429
- port: 0,
430
- queue,
431
- adapter,
432
- // 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
433
- // reads (GET /poll, GET /status). When token issuance failed both stay
434
- // available-but-degraded: /send answers 503, the reads stay open.
435
- ...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
436
- getStreamState: () => streamHandle ? streamHandle.getState() : {
437
- connected: false,
438
- connectedSince: null,
439
- lastEventAt: null,
440
- lastHeartbeatAt: null,
441
- cursors: {},
442
- reconnectCount: 0,
443
- // Adaptive: unknown until the watchdog observes ≥2 heartbeats.
444
- staleAfterMs: null
445
- }
446
- });
447
- writeDiscoveryByKey(discoveryKey, {
448
- schema: 2,
449
- discoveryKey,
450
- ccPid,
451
- projectDir: projectDir ?? null,
452
- proxyPid: process.pid,
453
- // Legacy `pid` mirrors `proxyPid` so the existing event-log sweep
454
- // (which reads `data.pid`) still considers this entry live.
455
- pid: process.pid,
456
- port: hookServer.port,
457
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
458
- brokerUrl: config.url,
459
- eventLogPath: eventLog.path,
460
- // Advertise where the POST /send bearer lives so local consumers
461
- // (native gateway plugins) can find it without guessing ~/.kojee.
462
- ...controlToken !== null ? { controlTokenPath: controlTokenPath() } : {},
463
- // Stamp the auth mode so `kojee-mcp doctor` renders the pairing check
464
- // honestly: a token-mode box has no ~/.kojee/config.json by design and
465
- // must not hard-fail on "paired config: MISSING". Defaults to "paired"
466
- // for back-compat with callers that don't set it.
467
- authMode: config.authMode ?? "paired"
468
- });
469
- const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
470
- process.on("exit", () => {
471
- cleanupDiscoveryFile();
472
- eventLog.cleanup();
473
- });
474
- teardownSteps.push(() => {
475
- void webhookSink?.stop();
476
- });
477
- teardownSteps.push(() => cleanupDiscoveryFile());
478
- teardownSteps.push(() => eventLog.cleanup());
479
- teardownSteps.push(() => hookServer.stop());
480
- streamHandle = await startEventStream({
481
- brokerUrl: config.url,
482
- token: config.token,
483
- gateway,
484
- adapter,
485
- server,
486
- queue,
487
- eventLog,
488
- // Generic webhook sink (null unless KOJEE_WEBHOOK_URL + _SECRET are set).
489
- // Wired LAST in the fan-out, fire-and-forget — can't delay a wake.
490
- ...webhookSink ? { webhookSink } : {},
491
- // Resubscribe-on-start (P0 #2): touch all memberships + write a
492
- // `status=subscribed n=<count>` line on every (re)connect, so a backend
493
- // restart / scope reset self-heals and the log is never ambiguously
494
- // empty. See resubscribe.ts for the unverified-touch caveat.
495
- //
496
- // MINOR 6: `listTandems` re-fetches the membership list per reconnect (a
497
- // mid-session join is touched next reconnect, not boot-frozen), each
498
- // touch is timeout-bounded + run with bounded concurrency, and the whole
499
- // routine runs concurrently with consumeSse (never blocks first-event
500
- // delivery). `listTandemIds` may return null (unknown) → treat as empty.
501
- // MINOR E: a shared debounce cursor damps a connect/drop flap storm — a
502
- // resubscribe within 30s of the last successful one is skipped.
503
- onConnected: /* @__PURE__ */ (() => {
504
- const debounceState = { lastRunAt: 0 };
505
- return async () => {
506
- await resubscribeMemberships({
507
- gateway,
508
- eventLog,
509
- listTandems: async () => await listTandemIds(gateway) ?? [],
510
- debounceState
511
- });
512
- };
513
- })()
514
- });
515
- activeStreamHandle = streamHandle;
516
- joinReconnect.notifyReady();
517
- } else if (needsWebhookEventStream()) {
518
- const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
519
- const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
520
- const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
521
- const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
522
- sweepStaleEventLogs();
523
- const projectDir = process.env["CLAUDE_PROJECT_DIR"];
524
- const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
525
- const eventLog = startEventLog({ key: discoveryKey });
526
- const webhookResolution = resolveWebhookConfig();
527
- if (webhookResolution.error) {
528
- console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
529
- void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
530
- });
531
- }
532
- if (webhookResolution.warning) {
533
- console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
534
- void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
535
- });
536
- }
537
- const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
538
- log: (line) => {
539
- void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
540
- });
541
- }
542
- }) : null;
543
- if (webhookSink) {
544
- console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
545
- void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
546
- });
547
- }
548
- server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
549
- onTandemJoin
550
- });
551
- process.on("exit", () => eventLog.cleanup());
552
- teardownSteps.push(() => {
553
- void webhookSink?.stop();
554
- });
555
- teardownSteps.push(() => eventLog.cleanup());
556
- const streamHandle = await startEventStream({
557
- brokerUrl: config.url,
558
- token: config.token,
559
- gateway,
560
- adapter,
561
- server,
562
- eventLog,
563
- ...webhookSink ? { webhookSink } : {},
564
- onConnected: /* @__PURE__ */ (() => {
565
- const debounceState = { lastRunAt: 0 };
566
- return async () => {
567
- await resubscribeMemberships({
568
- gateway,
569
- eventLog,
570
- listTandems: async () => await listTandemIds(gateway) ?? [],
571
- debounceState
572
- });
573
- };
574
- })()
575
- });
576
- activeStreamHandle = streamHandle;
577
- joinReconnect.notifyReady();
578
- } else {
579
- server = createMcpServer(registry, adapter, tandemMembershipCount);
580
- }
581
- process.stdin.on("end", () => shutdown("stdin end"));
582
- process.stdin.on("close", () => shutdown("stdin close"));
583
- process.on("SIGHUP", () => shutdown("SIGHUP"));
584
- process.on("SIGINT", () => shutdown("SIGINT"));
585
- process.on("SIGTERM", () => shutdown("SIGTERM"));
586
- if (ccPid !== null) {
587
- const { createParentWatchdog } = await import("./parent-watchdog-RZLHYP7T.js");
588
- const watchdog = createParentWatchdog({
589
- ccPid,
590
- onParentGone: () => shutdown("parent (Claude Code) gone")
591
- });
592
- watchdog.start();
593
- teardownSteps.push(() => watchdog.stop());
594
- } else {
595
- console.error(
596
- "[kojee-mcp] no Claude Code ancestor found \u2014 parent-liveness watchdog NOT armed (stdin/signal handlers still cover clean exits)"
597
- );
598
- }
599
- await startMcpServer(server);
600
- }
601
- async function enrollAndDiscover(config, keystorePath, isRetry = false) {
602
- const auth = new AuthModule(config.token, config.url, keystorePath);
603
- const keyPair = await auth.ensureEnrolled();
604
- const sessionId = GatewayClient.deriveSessionId(config.token);
605
- console.error(`[kojee-mcp] Session: ${sessionId}`);
606
- const gateway = new GatewayClient(
607
- config.url,
608
- config.token,
609
- keyPair.privateKey,
610
- keyPair.kid,
611
- sessionId
612
- );
613
- const registry = new ToolRegistry(gateway);
614
- try {
615
- await registry.discoverTools();
616
- return { registry, gateway };
617
- } catch (err) {
618
- if (isRetry || !isDPoPEnrollmentError(err)) {
619
- throw err;
620
- }
621
- console.error(
622
- "[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
623
- );
624
- try {
625
- if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
626
- } catch (unlinkErr) {
627
- console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
628
- }
629
- return enrollAndDiscover(config, keystorePath, true);
630
- }
631
- }
632
-
633
- export {
634
- VERSION,
635
- listTandemIds,
636
- startProxy
637
- };
File without changes
@@ -6,10 +6,10 @@ import "./chunk-SQL56SEB.js";
6
6
  import {
7
7
  resolveWebhookConfig
8
8
  } from "./chunk-V5VZPYMZ.js";
9
+ import "./chunk-BLEGIR35.js";
9
10
  import {
10
11
  CODEX_LISTEN_CAP_MS
11
12
  } from "./chunk-X672ZN7V.js";
12
- import "./chunk-BLEGIR35.js";
13
13
 
14
14
  // src/doctor-codex.ts
15
15
  import fs from "fs";
@@ -1,3 +1,8 @@
1
+ import {
2
+ executeSend,
3
+ parseSendRequest,
4
+ sendFailure
5
+ } from "./chunk-HIZ4NDWN.js";
1
6
  import {
2
7
  loadPairedConfig
3
8
  } from "./chunk-YH27B6SW.js";
@@ -9,11 +14,6 @@ import {
9
14
  loadKeystore
10
15
  } from "./chunk-CH32ELFX.js";
11
16
  import "./chunk-BLEGIR35.js";
12
- import {
13
- executeSend,
14
- parseSendRequest,
15
- sendFailure
16
- } from "./chunk-HIZ4NDWN.js";
17
17
  import "./chunk-LDZXU3DW.js";
18
18
 
19
19
  // src/tandem/send-cli.ts