kojee-mcp 0.5.11 → 0.5.13

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 (37) hide show
  1. package/README.md +24 -0
  2. package/dist/ancestry-ONFBQEP5.js +8 -0
  3. package/dist/{chunk-LDZXU3DW.js → chunk-2OLXXOT3.js} +1 -0
  4. package/dist/{codex-stop-hook-SWA53ECG.js → chunk-35XBRG3V.js} +4 -3
  5. package/dist/chunk-3H3TL34J.js +401 -0
  6. package/dist/{chunk-MKDMAAMN.js → chunk-74XFVX6Z.js} +16 -4
  7. package/dist/chunk-CM3EKMDD.js +116 -0
  8. package/dist/{chunk-DS26OORG.js → chunk-CO73VGWM.js} +41 -23
  9. package/dist/{chunk-HIZ4NDWN.js → chunk-DXJ6QLSJ.js} +22 -2
  10. package/dist/{chunk-HSR3GXCL.js → chunk-IMOEZ4NJ.js} +83 -6
  11. package/dist/{chunk-2MIISF2W.js → chunk-NR4Y54OL.js} +14 -1
  12. package/dist/chunk-XLKGPGZT.js +0 -0
  13. package/dist/chunk-XXFVWP6H.js +44 -0
  14. package/dist/cli.js +17 -15
  15. package/dist/codex-stop-hook-VY7DOMAG.js +16 -0
  16. package/dist/{doctor-XK335W7B.js → doctor-FVTALRQD.js} +110 -15
  17. package/dist/{event-log-B27VVEMK.js → event-log-VZD7NKYX.js} +1 -1
  18. package/dist/event-stream-XX5EZ6HN.js +19 -0
  19. package/dist/{gateway-client-93P1E0CZ.d.ts → gateway-client-C6yx1mfM.d.ts} +6 -1
  20. package/dist/{hook-server-37E2LUKJ.js → hook-server-T2Z444OV.js} +2 -2
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +8 -7
  23. package/dist/lib.d.ts +224 -3
  24. package/dist/lib.js +16 -11
  25. package/dist/{parent-watchdog-RZLHYP7T.js → parent-watchdog-TLU355FB.js} +1 -1
  26. package/dist/registry-TGALQP6M.js +348 -0
  27. package/dist/{send-cli-CN5EX7PO.js → send-cli-RH7D4JDP.js} +18 -10
  28. package/dist/server-LBVEDIXP.js +14 -0
  29. package/dist/{stop-hook-GEJF47SN.js → stop-hook-CUVDKXP7.js} +7 -6
  30. package/dist/{tail-stream-JNR4WFW3.js → tail-stream-5DAVRQYK.js} +4 -3
  31. package/dist/{user-prompt-submit-hook-DGRRFHOB.js → user-prompt-submit-hook-PMBUPKUV.js} +5 -5
  32. package/dist/{webhook-sink-NWGCUDGY.js → webhook-sink-N6AUTFL3.js} +1 -1
  33. package/package.json +1 -1
  34. package/dist/chunk-D42PZX2I.js +0 -784
  35. package/dist/{chunk-BJMASMKX.js → chunk-VHKPWUX7.js} +0 -0
  36. package/dist/{doctor-codex-SMROUYGV.js → doctor-codex-PA3WO6LR.js} +1 -1
  37. package/dist/{wizard-PLGHYCT3.js → wizard-L4MYRLJI.js} +11 -11
@@ -1,784 +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
- secureDir,
21
- secureFile
22
- } from "./chunk-BLEGIR35.js";
23
- import {
24
- parseTandemsConfig
25
- } from "./chunk-YKW54DKF.js";
26
- import {
27
- translateToolCallResult
28
- } from "./chunk-LDZXU3DW.js";
29
-
30
- // src/index.ts
31
- import fs4 from "fs";
32
- import os2 from "os";
33
- import path3 from "path";
34
-
35
- // src/tool-registry.ts
36
- var ToolRegistry = class {
37
- constructor(gateway) {
38
- this.gateway = gateway;
39
- }
40
- gateway;
41
- /** Flat map: tool name → full tool definition */
42
- tools = /* @__PURE__ */ new Map();
43
- /**
44
- * Fetch all tools with full schemas from the gateway in a single RPC call.
45
- */
46
- async discoverTools() {
47
- console.error("[tools] Fetching tools with full schemas from gateway...");
48
- const result = await this.gateway.sendRpc("tools/list", {
49
- include_schema: true
50
- });
51
- const maybeError = result;
52
- if (maybeError.isError) {
53
- const msg = maybeError.content?.[0]?.text ?? "unknown error";
54
- throw new Error(`Gateway rejected tools/list: ${msg}`);
55
- }
56
- const toolList = result?.tools;
57
- if (!toolList || !Array.isArray(toolList)) {
58
- console.error("[tools] No tools returned from gateway");
59
- return;
60
- }
61
- for (const tool of toolList) {
62
- if (this.tools.has(tool.name)) {
63
- console.error(`[tools] Warning: duplicate tool name "${tool.name}" \u2014 overwriting`);
64
- }
65
- this.tools.set(tool.name, tool);
66
- }
67
- console.error(`[tools] Registered ${this.tools.size} tools from gateway`);
68
- }
69
- /**
70
- * Return all registered tools for the MCP ListTools response.
71
- */
72
- getAllTools() {
73
- return Array.from(this.tools.values());
74
- }
75
- /**
76
- * Call a tool through the gateway.
77
- */
78
- async callTool(name, args) {
79
- if (!this.tools.has(name)) {
80
- const available = Array.from(this.tools.keys()).slice(0, 10).join(", ");
81
- return {
82
- content: [
83
- {
84
- type: "text",
85
- text: `Tool '${name}' not found. Some available tools: ${available}${this.tools.size > 10 ? ", ..." : ""}`
86
- }
87
- ],
88
- isError: true
89
- };
90
- }
91
- return this.gateway.sendRpc("tools/call", {
92
- name,
93
- arguments: args
94
- });
95
- }
96
- /** Total number of registered tools. */
97
- get toolCount() {
98
- return this.tools.size;
99
- }
100
- };
101
-
102
- // src/server.ts
103
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
104
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
105
- import {
106
- ListToolsRequestSchema,
107
- CallToolRequestSchema
108
- } from "@modelcontextprotocol/sdk/types.js";
109
-
110
- // src/version.ts
111
- import fs from "fs";
112
- import path from "path";
113
- import { fileURLToPath } from "url";
114
- var FALLBACK_VERSION = "0.0.0-unknown";
115
- function resolveVersion() {
116
- try {
117
- const here = path.dirname(fileURLToPath(import.meta.url));
118
- const parsed = JSON.parse(
119
- fs.readFileSync(path.join(here, "..", "package.json"), "utf8")
120
- );
121
- return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
122
- } catch (err) {
123
- process.stderr.write(
124
- `kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
125
- `
126
- );
127
- return FALLBACK_VERSION;
128
- }
129
- }
130
- var VERSION = resolveVersion();
131
-
132
- // src/server.ts
133
- function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
134
- const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
135
-
136
- (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()}.
137
-
138
- `;
139
- 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.)
140
-
141
- `;
142
- 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.";
143
- 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.";
144
- return intro + monitorSection + listenSection + advice;
145
- }
146
- function tandemIdArg(args) {
147
- return typeof args["tandem_id"] === "string" ? args["tandem_id"] : null;
148
- }
149
- async function executeToolCall(registry, name, args, hooks) {
150
- const rawResult = await registry.callTool(name, args);
151
- const result = translateToolCallResult(rawResult);
152
- if (!result.isError) {
153
- if (name === "tandem_join") {
154
- try {
155
- hooks?.onTandemJoin?.(tandemIdArg(args));
156
- } catch (err) {
157
- console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
158
- }
159
- } else if (name === "tandem_leave") {
160
- try {
161
- hooks?.onTandemLeave?.(tandemIdArg(args));
162
- } catch (err) {
163
- console.error("[mcp] onTandemLeave hook failed:", err?.message ?? String(err));
164
- }
165
- }
166
- }
167
- return result;
168
- }
169
- function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath, hooks) {
170
- const capabilities = { tools: {} };
171
- if (adapter.supportsChannels) {
172
- capabilities.experimental = { "claude/channel": {} };
173
- }
174
- const server = new Server(
175
- { name: "kojee-mcp", version: VERSION },
176
- {
177
- capabilities,
178
- ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
179
- }
180
- );
181
- server.setRequestHandler(ListToolsRequestSchema, async () => {
182
- const tools = registry.getAllTools().map((t) => ({
183
- name: t.name,
184
- description: t.description,
185
- inputSchema: t.inputSchema
186
- }));
187
- return { tools };
188
- });
189
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
190
- const { name, arguments: args } = request.params;
191
- const result = await executeToolCall(registry, name, args ?? {}, hooks);
192
- return { content: result.content, isError: result.isError };
193
- });
194
- return server;
195
- }
196
- async function startMcpServer(server) {
197
- const transport = new StdioServerTransport();
198
- await server.connect(transport);
199
- console.error("[mcp] Server started on stdio transport");
200
- }
201
-
202
- // src/runtime/detect.ts
203
- import psList from "ps-list";
204
- async function detectRuntime(env = process.env) {
205
- if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
206
- if (env["KOJEE_RUNTIME"] === "codex") return "codex";
207
- if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
208
- const ancestor = await detectRuntimeFromAncestry();
209
- if (ancestor) return ancestor;
210
- return "unknown";
211
- }
212
- async function detectRuntimeFromAncestry() {
213
- try {
214
- const processes = await psList();
215
- const byPid = new Map(processes.map((p) => [p.pid, p]));
216
- let pid = process.ppid;
217
- for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
218
- const row = byPid.get(pid);
219
- if (!row) return null;
220
- const haystack = `${row.name} ${row.cmd ?? ""}`;
221
- if (/(^|\/|\\)claude(\.app|\.exe)?(\/|$|\s|\\)/i.test(haystack) || /Claude Helper/i.test(haystack)) {
222
- return "claude-code";
223
- }
224
- if (/(^|\/|\\)codex(\.exe)?(\/|$|\s|\\)/i.test(haystack)) {
225
- return "codex";
226
- }
227
- pid = row.ppid;
228
- }
229
- } catch {
230
- }
231
- return null;
232
- }
233
-
234
- // src/adapters/claude-code.ts
235
- function computeSeverity(event) {
236
- if (event.type === "state_change") return "high";
237
- if (event.mentions && event.mentions.length > 0) {
238
- return "high";
239
- }
240
- return "normal";
241
- }
242
- function formatBody(event) {
243
- if (event.type === "state_change") {
244
- return `[Tandem: ${event.tandem_id}] ${event.from.displayname}: ${event.content.body}`;
245
- }
246
- return [
247
- `[Tandem: ${event.tandem_id}] ${event.from.displayname} (${event.from.principal}) \u2014 ${event.kind}:`,
248
- event.content.body,
249
- "",
250
- `> ${buildReplyRecipe({ tandem_id: event.tandem_id, message_id: event.id })}`
251
- ].join("\n");
252
- }
253
- var claudeCodeAdapter = {
254
- runtime: "claude-code",
255
- supportsChannels: true,
256
- formatTandemEvent(event) {
257
- const meta = {
258
- tandem_id: event.tandem_id,
259
- message_id: event.id,
260
- cursor: String(event.cursor),
261
- kind: event.kind,
262
- from_principal: event.from.principal,
263
- from_display: event.from.displayname,
264
- severity: computeSeverity(event)
265
- };
266
- if (event.wake_reason) meta.wake_reason = event.wake_reason;
267
- return { content: formatBody(event), meta };
268
- }
269
- };
270
-
271
- // src/adapters/codex.ts
272
- var codexAdapter = {
273
- runtime: "codex",
274
- supportsChannels: false,
275
- // Codex has NO Claude-style channel injection
276
- formatTandemEvent() {
277
- throw new Error(
278
- "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."
279
- );
280
- }
281
- };
282
-
283
- // src/adapters/unknown.ts
284
- var unknownAdapter = {
285
- runtime: "unknown",
286
- supportsChannels: false,
287
- formatTandemEvent() {
288
- throw new Error(
289
- "unknownAdapter.formatTandemEvent() is unreachable \u2014 caller should gate on supportsChannels"
290
- );
291
- }
292
- };
293
-
294
- // src/runtime/cc-session-id.ts
295
- import fs2 from "fs";
296
- import { execFileSync } from "child_process";
297
- import { createHash } from "crypto";
298
- var CC_SESSION_ENV = "CLAUDE_CODE_SESSION_ID";
299
- function extractCcSessionId(raw) {
300
- if (raw.includes("\0")) {
301
- for (const entry of raw.split("\0")) {
302
- if (entry.startsWith(`${CC_SESSION_ENV}=`)) {
303
- const v = entry.slice(CC_SESSION_ENV.length + 1);
304
- return v.length > 0 ? v : null;
305
- }
306
- }
307
- return null;
308
- }
309
- const re = new RegExp(`(?:^|\\s)${CC_SESSION_ENV}=([^\\s]+)`, "g");
310
- let last = null;
311
- let m;
312
- while ((m = re.exec(raw)) !== null) last = m[1];
313
- return last;
314
- }
315
- function defaultReadProcessEnvRaw(pid, platform) {
316
- try {
317
- if (platform === "linux") {
318
- return fs2.readFileSync(`/proc/${pid}/environ`, "utf8");
319
- }
320
- if (platform === "darwin") {
321
- return execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], {
322
- encoding: "utf8",
323
- timeout: 2e3
324
- });
325
- }
326
- } catch {
327
- return null;
328
- }
329
- return null;
330
- }
331
- function sanitizeKey(value) {
332
- return value.replace(/[^A-Za-z0-9_-]/g, "");
333
- }
334
- function resolveInstanceKey(deps = {}) {
335
- const env = deps.env ?? process.env;
336
- const platform = deps.platform ?? process.platform;
337
- const ccPid = deps.ccPid ?? null;
338
- if (ccPid !== null) {
339
- const reader = deps.readProcessEnvRaw ?? defaultReadProcessEnvRaw;
340
- const raw = reader(ccPid, platform);
341
- if (raw) {
342
- const sid = sanitizeKey(extractCcSessionId(raw) ?? "");
343
- if (sid) return sid;
344
- }
345
- }
346
- const explicit = sanitizeKey((env.KOJEE_INSTANCE ?? "").trim());
347
- if (explicit) return `inst-${explicit}`;
348
- const base = deps.projectDir ?? env.CLAUDE_PROJECT_DIR ?? deps.cwd ?? process.cwd() ?? "";
349
- const hash = createHash("sha256").update(base).digest("hex").slice(0, 12);
350
- return `wd-${hash}`;
351
- }
352
-
353
- // src/tandem/room-memory.ts
354
- import fs3 from "fs";
355
- import os from "os";
356
- import path2 from "path";
357
- function defaultKojeeDir() {
358
- return path2.join(os.homedir(), ".kojee");
359
- }
360
- function seatedRoomsPath(key, dir = defaultKojeeDir()) {
361
- return path2.join(dir, `seated-rooms-cc-${key}.json`);
362
- }
363
- function readSeatedRooms(key, dir = defaultKojeeDir()) {
364
- let raw;
365
- try {
366
- raw = fs3.readFileSync(seatedRoomsPath(key, dir), "utf8");
367
- } catch {
368
- return [];
369
- }
370
- try {
371
- const parsed = JSON.parse(raw);
372
- return Array.isArray(parsed.rooms) ? parsed.rooms.filter((r) => typeof r === "string") : [];
373
- } catch {
374
- return [];
375
- }
376
- }
377
- function hasSeatedRoomsFile(key, dir = defaultKojeeDir()) {
378
- return fs3.existsSync(seatedRoomsPath(key, dir));
379
- }
380
- function seedSeatedRooms(key, rooms, dir = defaultKojeeDir()) {
381
- writeSeatedRooms(key, [...new Set(rooms)], dir);
382
- }
383
- function writeSeatedRooms(key, rooms, dir) {
384
- fs3.mkdirSync(dir, { recursive: true, mode: 448 });
385
- secureDir(dir);
386
- const filePath = seatedRoomsPath(key, dir);
387
- const body = { schema: 1, rooms, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
388
- fs3.writeFileSync(filePath, JSON.stringify(body, null, 2), { mode: 384 });
389
- secureFile(filePath);
390
- }
391
- function addSeatedRoom(key, tandemId, dir = defaultKojeeDir()) {
392
- const rooms = readSeatedRooms(key, dir);
393
- if (rooms.includes(tandemId)) return;
394
- writeSeatedRooms(key, [...rooms, tandemId], dir);
395
- }
396
- function removeSeatedRoom(key, tandemId, dir = defaultKojeeDir()) {
397
- const rooms = readSeatedRooms(key, dir);
398
- if (!rooms.includes(tandemId)) return;
399
- writeSeatedRooms(key, rooms.filter((r) => r !== tandemId), dir);
400
- }
401
-
402
- // src/index.ts
403
- var DEFAULT_KEYSTORE_PATH = path3.join(os2.homedir(), ".kojee", "keypair.json");
404
- function isDPoPEnrollmentError(err) {
405
- const msg = String(err?.message ?? err ?? "").toLowerCase();
406
- if (msg.includes("invalid or expired") && msg.includes("token")) return false;
407
- if (msg.includes("generate a new")) return false;
408
- return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
409
- }
410
- async function selectAdapter() {
411
- const runtime = await detectRuntime();
412
- if (runtime === "claude-code") return claudeCodeAdapter;
413
- if (runtime === "codex") return codexAdapter;
414
- return unknownAdapter;
415
- }
416
- async function listTandemIds(gateway) {
417
- const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
418
- const maybeErr = result;
419
- if (maybeErr.isError) return null;
420
- const text = maybeErr.content?.[0]?.text;
421
- try {
422
- const parsed = text ? JSON.parse(text) : {};
423
- const list = Array.isArray(parsed.tandems) ? parsed.tandems : Array.isArray(parsed) ? parsed : null;
424
- if (!Array.isArray(list)) return [];
425
- return list.map((t) => {
426
- if (typeof t === "string") return t;
427
- const obj = t;
428
- if (obj?.my_membership?.is_member !== true) return void 0;
429
- return obj?.tandem_id ?? obj?.id;
430
- }).filter((id) => typeof id === "string" && id.length > 0);
431
- } catch {
432
- return null;
433
- }
434
- }
435
- function needsWebhookEventStream() {
436
- return (process.env["KOJEE_WEBHOOK_URL"] ?? "").trim().length > 0;
437
- }
438
- async function startProxy(config) {
439
- const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
440
- const adapter = await selectAdapter();
441
- console.error(`[kojee-mcp] Starting proxy for ${config.url} (runtime=${adapter.runtime})`);
442
- const { registry, gateway } = await enrollAndDiscover(config, keystorePath);
443
- console.error(
444
- `[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
445
- );
446
- const ccPid = await findClaudeAncestorPid();
447
- const instanceKey = resolveInstanceKey({ ccPid });
448
- const roomMemory = {
449
- hasMemory: () => hasSeatedRoomsFile(instanceKey),
450
- read: () => readSeatedRooms(instanceKey),
451
- seed: (rooms) => seedSeatedRooms(instanceKey, rooms)
452
- };
453
- const recordRooms = parseTandemsConfig(process.env["KOJEE_TANDEMS"]).mode === "auto-local";
454
- if (recordRooms && instanceKey.startsWith("wd-")) {
455
- console.error(
456
- "[kojee-mcp] #28: no Claude Code session id available \u2014 room-memory keyed on project-dir hash; concurrent windows in the same dir will share it. Set KOJEE_INSTANCE=<unique-per-window> to keep them session-faithful."
457
- );
458
- }
459
- let activeStreamHandle = null;
460
- const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-ARV6JIWK.js");
461
- const joinReconnect = createJoinReconnectScheduler({
462
- // BOOT-RACE (Bug B): report whether the stream handle was actually ready.
463
- // `false` ⇒ activeStreamHandle is still null (tandem_join fired before the
464
- // stream was set up) → the scheduler queues the reconnect and flushes it on
465
- // notifyReady() once the handle is assigned (see below), instead of silently
466
- // dropping it as the old `activeStreamHandle?.reconnect()` no-op did.
467
- reconnect: () => {
468
- if (!activeStreamHandle) return false;
469
- activeStreamHandle.reconnect();
470
- return true;
471
- }
472
- });
473
- const onTandemJoin = (tandemId) => {
474
- joinReconnect.requestReconnect();
475
- if (recordRooms && tandemId) addSeatedRoom(instanceKey, tandemId);
476
- };
477
- const onTandemLeave = (tandemId) => {
478
- if (recordRooms && tandemId) removeSeatedRoom(instanceKey, tandemId);
479
- };
480
- const teardownSteps = [];
481
- let shuttingDown = false;
482
- function shutdown(reason) {
483
- if (shuttingDown) return;
484
- shuttingDown = true;
485
- activeStreamHandle?.();
486
- for (const step of teardownSteps) {
487
- try {
488
- const maybe = step();
489
- if (maybe && typeof maybe.catch === "function") {
490
- maybe.catch((err) => {
491
- console.error("[kojee-mcp] async shutdown step failed:", err?.message ?? err);
492
- });
493
- }
494
- } catch (err) {
495
- console.error("[kojee-mcp] shutdown step failed:", err?.message ?? err);
496
- }
497
- }
498
- console.error(`[kojee-mcp] shutting down (${reason}), exiting`);
499
- process.exit(0);
500
- }
501
- const { ensureJoinTandems } = await import("./ensure-join-5Y5IJ7HN.js");
502
- await ensureJoinTandems({
503
- gateway,
504
- env: process.env["KOJEE_TANDEMS"],
505
- listTandems: () => listTandemIds(gateway),
506
- roomMemory,
507
- onJoined: () => joinReconnect.requestReconnect()
508
- });
509
- let tandemMembershipCount = -1;
510
- try {
511
- const bootIds = await listTandemIds(gateway);
512
- tandemMembershipCount = bootIds === null ? -1 : bootIds.length;
513
- } catch (err) {
514
- console.error("[kojee-mcp] tandem_list probe failed:", err.message);
515
- }
516
- console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
517
- let server;
518
- if (adapter.supportsChannels) {
519
- const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
520
- const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
521
- const {
522
- writeDiscoveryByKey,
523
- cleanupDiscoveryByKey,
524
- sweepStaleDiscovery
525
- } = await import("./session-discovery-FNMJGFPM.js");
526
- const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
527
- const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
528
- const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
529
- const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
530
- sweepStaleDiscovery();
531
- sweepStaleEventLogs();
532
- const projectDir = process.env["CLAUDE_PROJECT_DIR"];
533
- const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
534
- const eventLog = startEventLog({ key: discoveryKey });
535
- const webhookResolution = resolveWebhookConfig();
536
- if (webhookResolution.error) {
537
- console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
538
- void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
539
- });
540
- }
541
- if (webhookResolution.warning) {
542
- console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
543
- void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
544
- });
545
- }
546
- const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
547
- // Route delivery/failure observability to the STATUS sink.
548
- log: (line) => {
549
- void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
550
- });
551
- }
552
- }) : null;
553
- if (webhookSink) {
554
- console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
555
- void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
556
- });
557
- }
558
- server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
559
- onTandemJoin,
560
- onTandemLeave
561
- });
562
- const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
563
- let controlToken = null;
564
- try {
565
- controlToken = issueControlToken();
566
- } catch (err) {
567
- console.error(
568
- "[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
569
- err.message
570
- );
571
- }
572
- const queue = new EventQueue();
573
- let streamHandle = null;
574
- const hookServer = await startHookServer({
575
- port: 0,
576
- queue,
577
- adapter,
578
- // 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
579
- // reads (GET /poll, GET /status). When token issuance failed both stay
580
- // available-but-degraded: /send answers 503, the reads stay open.
581
- ...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
582
- getStreamState: () => streamHandle ? streamHandle.getState() : {
583
- connected: false,
584
- connectedSince: null,
585
- lastEventAt: null,
586
- lastHeartbeatAt: null,
587
- cursors: {},
588
- reconnectCount: 0,
589
- // Adaptive: unknown until the watchdog observes ≥2 heartbeats.
590
- staleAfterMs: null
591
- }
592
- });
593
- writeDiscoveryByKey(discoveryKey, {
594
- schema: 2,
595
- discoveryKey,
596
- ccPid,
597
- projectDir: projectDir ?? null,
598
- proxyPid: process.pid,
599
- // Legacy `pid` mirrors `proxyPid` so the existing event-log sweep
600
- // (which reads `data.pid`) still considers this entry live.
601
- pid: process.pid,
602
- port: hookServer.port,
603
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
604
- brokerUrl: config.url,
605
- eventLogPath: eventLog.path,
606
- // Advertise where the POST /send bearer lives so local consumers
607
- // (native gateway plugins) can find it without guessing ~/.kojee.
608
- ...controlToken !== null ? { controlTokenPath: controlTokenPath() } : {},
609
- // Stamp the auth mode so `kojee-mcp doctor` renders the pairing check
610
- // honestly: a token-mode box has no ~/.kojee/config.json by design and
611
- // must not hard-fail on "paired config: MISSING". Defaults to "paired"
612
- // for back-compat with callers that don't set it.
613
- authMode: config.authMode ?? "paired"
614
- });
615
- const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
616
- process.on("exit", () => {
617
- cleanupDiscoveryFile();
618
- eventLog.cleanup();
619
- });
620
- teardownSteps.push(() => {
621
- void webhookSink?.stop();
622
- });
623
- teardownSteps.push(() => cleanupDiscoveryFile());
624
- teardownSteps.push(() => eventLog.cleanup());
625
- teardownSteps.push(() => hookServer.stop());
626
- streamHandle = await startEventStream({
627
- brokerUrl: config.url,
628
- token: config.token,
629
- gateway,
630
- adapter,
631
- server,
632
- queue,
633
- eventLog,
634
- // Generic webhook sink (null unless KOJEE_WEBHOOK_URL + _SECRET are set).
635
- // Wired LAST in the fan-out, fire-and-forget — can't delay a wake.
636
- ...webhookSink ? { webhookSink } : {},
637
- // Resubscribe-on-start (P0 #2): touch all memberships + write a
638
- // `status=subscribed n=<count>` line on every (re)connect, so a backend
639
- // restart / scope reset self-heals and the log is never ambiguously
640
- // empty. See resubscribe.ts for the unverified-touch caveat.
641
- //
642
- // MINOR 6: `listTandems` re-fetches the membership list per reconnect (a
643
- // mid-session join is touched next reconnect, not boot-frozen), each
644
- // touch is timeout-bounded + run with bounded concurrency, and the whole
645
- // routine runs concurrently with consumeSse (never blocks first-event
646
- // delivery). `listTandemIds` may return null (unknown) → treat as empty.
647
- // MINOR E: a shared debounce cursor damps a connect/drop flap storm — a
648
- // resubscribe within 30s of the last successful one is skipped.
649
- onConnected: /* @__PURE__ */ (() => {
650
- const debounceState = { lastRunAt: 0 };
651
- return async () => {
652
- await resubscribeMemberships({
653
- gateway,
654
- eventLog,
655
- listTandems: async () => await listTandemIds(gateway) ?? [],
656
- debounceState
657
- });
658
- };
659
- })()
660
- });
661
- activeStreamHandle = streamHandle;
662
- joinReconnect.notifyReady();
663
- } else if (needsWebhookEventStream()) {
664
- const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
665
- const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
666
- const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
667
- const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
668
- sweepStaleEventLogs();
669
- const projectDir = process.env["CLAUDE_PROJECT_DIR"];
670
- const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
671
- const eventLog = startEventLog({ key: discoveryKey });
672
- const webhookResolution = resolveWebhookConfig();
673
- if (webhookResolution.error) {
674
- console.error(`[kojee-mcp] webhook sink ERROR: ${webhookResolution.error}`);
675
- void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
676
- });
677
- }
678
- if (webhookResolution.warning) {
679
- console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
680
- void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
681
- });
682
- }
683
- const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
684
- log: (line) => {
685
- void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
686
- });
687
- }
688
- }) : null;
689
- if (webhookSink) {
690
- console.error(`[kojee-mcp] webhook sink ENABLED (${webhookSink.configSummary()})`);
691
- void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
692
- });
693
- }
694
- server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
695
- onTandemJoin,
696
- onTandemLeave
697
- });
698
- process.on("exit", () => eventLog.cleanup());
699
- teardownSteps.push(() => {
700
- void webhookSink?.stop();
701
- });
702
- teardownSteps.push(() => eventLog.cleanup());
703
- const streamHandle = await startEventStream({
704
- brokerUrl: config.url,
705
- token: config.token,
706
- gateway,
707
- adapter,
708
- server,
709
- eventLog,
710
- ...webhookSink ? { webhookSink } : {},
711
- onConnected: /* @__PURE__ */ (() => {
712
- const debounceState = { lastRunAt: 0 };
713
- return async () => {
714
- await resubscribeMemberships({
715
- gateway,
716
- eventLog,
717
- listTandems: async () => await listTandemIds(gateway) ?? [],
718
- debounceState
719
- });
720
- };
721
- })()
722
- });
723
- activeStreamHandle = streamHandle;
724
- joinReconnect.notifyReady();
725
- } else {
726
- server = createMcpServer(registry, adapter, tandemMembershipCount);
727
- }
728
- process.stdin.on("end", () => shutdown("stdin end"));
729
- process.stdin.on("close", () => shutdown("stdin close"));
730
- process.on("SIGHUP", () => shutdown("SIGHUP"));
731
- process.on("SIGINT", () => shutdown("SIGINT"));
732
- process.on("SIGTERM", () => shutdown("SIGTERM"));
733
- if (ccPid !== null) {
734
- const { createParentWatchdog } = await import("./parent-watchdog-RZLHYP7T.js");
735
- const watchdog = createParentWatchdog({
736
- ccPid,
737
- onParentGone: () => shutdown("parent (Claude Code) gone")
738
- });
739
- watchdog.start();
740
- teardownSteps.push(() => watchdog.stop());
741
- } else {
742
- console.error(
743
- "[kojee-mcp] no Claude Code ancestor found \u2014 parent-liveness watchdog NOT armed (stdin/signal handlers still cover clean exits)"
744
- );
745
- }
746
- await startMcpServer(server);
747
- }
748
- async function enrollAndDiscover(config, keystorePath, isRetry = false) {
749
- const auth = new AuthModule(config.token, config.url, keystorePath);
750
- const keyPair = await auth.ensureEnrolled();
751
- const sessionId = GatewayClient.deriveSessionId(config.token);
752
- console.error(`[kojee-mcp] Session: ${sessionId}`);
753
- const gateway = new GatewayClient(
754
- config.url,
755
- config.token,
756
- keyPair.privateKey,
757
- keyPair.kid,
758
- sessionId
759
- );
760
- const registry = new ToolRegistry(gateway);
761
- try {
762
- await registry.discoverTools();
763
- return { registry, gateway };
764
- } catch (err) {
765
- if (isRetry || !isDPoPEnrollmentError(err)) {
766
- throw err;
767
- }
768
- console.error(
769
- "[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
770
- );
771
- try {
772
- if (fs4.existsSync(keystorePath)) fs4.unlinkSync(keystorePath);
773
- } catch (unlinkErr) {
774
- console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
775
- }
776
- return enrollAndDiscover(config, keystorePath, true);
777
- }
778
- }
779
-
780
- export {
781
- VERSION,
782
- listTandemIds,
783
- startProxy
784
- };