kojee-mcp 0.4.0 → 0.5.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.
@@ -1,11 +1,22 @@
1
+ import {
2
+ MCP_SESSION_ID,
3
+ createDPoPProof,
4
+ startEventStream
5
+ } from "./chunk-QB22PD6T.js";
6
+ import {
7
+ buildCatchUpNote,
8
+ buildMonitorSpawn,
9
+ buildReplyRecipe
10
+ } from "./chunk-E26AHU6J.js";
1
11
  import {
2
12
  deriveDiscoveryKey,
3
13
  findClaudeAncestorPid
4
- } from "./chunk-36DMIXH7.js";
14
+ } from "./chunk-BJMASMKX.js";
5
15
 
6
16
  // src/index.ts
7
- import fs2 from "fs";
8
- import path2 from "path";
17
+ import fs3 from "fs";
18
+ import os2 from "os";
19
+ import path3 from "path";
9
20
 
10
21
  // src/auth/auth-module.ts
11
22
  import { calculateJwkThumbprint } from "jose";
@@ -14,12 +25,9 @@ import crypto from "crypto";
14
25
  // src/auth/keystore.ts
15
26
  import { importJWK, exportJWK, generateKeyPair } from "jose";
16
27
  import fs from "fs";
28
+ import os from "os";
17
29
  import path from "path";
18
- var DEFAULT_PATH = path.join(
19
- process.env["HOME"] ?? "~",
20
- ".kojee",
21
- "keypair.json"
22
- );
30
+ var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
23
31
  async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
24
32
  if (!fs.existsSync(keystorePath)) {
25
33
  return null;
@@ -192,34 +200,7 @@ var AuthModule = class {
192
200
  };
193
201
 
194
202
  // src/gateway-client.ts
195
- import crypto3 from "crypto";
196
-
197
- // src/auth/dpop.ts
198
- import { SignJWT, base64url } from "jose";
199
203
  import crypto2 from "crypto";
200
- async function createDPoPProof(privateKey, kid, method, url, nonce, accessToken) {
201
- const payload = {
202
- htm: method,
203
- htu: url,
204
- jti: crypto2.randomUUID()
205
- };
206
- if (nonce) {
207
- payload.nonce = nonce;
208
- }
209
- if (accessToken) {
210
- payload.ath = computeAth(accessToken);
211
- }
212
- const header = {
213
- typ: "dpop+jwt",
214
- alg: "ES256",
215
- jwk: { kid }
216
- };
217
- return new SignJWT(payload).setProtectedHeader(header).setIssuedAt().sign(privateKey);
218
- }
219
- function computeAth(accessToken) {
220
- const hash = crypto2.createHash("sha256").update(accessToken).digest();
221
- return base64url.encode(hash);
222
- }
223
204
 
224
205
  // src/error-translator.ts
225
206
  function translateGovernanceResult(result) {
@@ -384,10 +365,6 @@ function makeError(text) {
384
365
  };
385
366
  }
386
367
 
387
- // src/tandem/session-id.ts
388
- import { ulid } from "ulidx";
389
- var MCP_SESSION_ID = ulid();
390
-
391
368
  // src/gateway-client.ts
392
369
  var STEP_UP_POLL_INTERVAL_MS = 5e3;
393
370
  var STEP_UP_MAX_TIMEOUT_MS = 3e5;
@@ -426,26 +403,33 @@ var GatewayClient = class {
426
403
  * session_id = sha256(token + "proxy").slice(0, 16)
427
404
  */
428
405
  static deriveSessionId(token) {
429
- const hash = crypto3.createHash("sha256").update(token + "proxy").digest("hex");
406
+ const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
430
407
  return hash.slice(0, 16);
431
408
  }
432
409
  /**
433
410
  * Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth,
434
411
  * nonce retry, and step-up retry transparently.
412
+ *
413
+ * `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
414
+ * underlying `fetch` option — NOT placed inside `params`/`arguments`. A
415
+ * caller with a per-call timeout budget (e.g. resubscribeMemberships) passes
416
+ * its controller's signal here so a hung backend aborts at the budget instead
417
+ * of hanging forever. Putting the signal in `arguments` (the round-2 bug) both
418
+ * left fetch un-aborted AND serialized a junk `{}` onto the wire body.
435
419
  */
436
- async sendRpc(method, params = {}) {
420
+ async sendRpc(method, params = {}, signal) {
437
421
  const rpcRequest = {
438
422
  jsonrpc: "2.0",
439
423
  id: ++this.requestCounter,
440
424
  method,
441
425
  params
442
426
  };
443
- return this.executeWithRetries(rpcRequest);
427
+ return this.executeWithRetries(rpcRequest, signal);
444
428
  }
445
- async executeWithRetries(rpcRequest) {
429
+ async executeWithRetries(rpcRequest, signal) {
446
430
  let response;
447
431
  try {
448
- response = await this.sendHttpRequest(rpcRequest);
432
+ response = await this.sendHttpRequest(rpcRequest, signal);
449
433
  } catch (err) {
450
434
  return translateNetworkError(err);
451
435
  }
@@ -455,7 +439,7 @@ var GatewayClient = class {
455
439
  if (body?.error === "use_dpop_nonce") {
456
440
  console.error("[gateway] Nonce expired, retrying with fresh nonce...");
457
441
  try {
458
- response = await this.sendHttpRequest(rpcRequest);
442
+ response = await this.sendHttpRequest(rpcRequest, signal);
459
443
  } catch (err) {
460
444
  return translateNetworkError(err);
461
445
  }
@@ -468,7 +452,7 @@ var GatewayClient = class {
468
452
  if (response.status === 403) {
469
453
  const body = await this.tryParseErrorBody(response);
470
454
  if (body?.error === "step_up_required") {
471
- return this.handleStepUp(rpcRequest, body.trigger);
455
+ return this.handleStepUp(rpcRequest, body.trigger, signal);
472
456
  }
473
457
  const translated = translateHttpError(403, body?.error, body?.trigger);
474
458
  if (translated) return translated;
@@ -489,7 +473,7 @@ var GatewayClient = class {
489
473
  const result = rpcResponse.result;
490
474
  return result ?? { content: [{ type: "text", text: "No result" }] };
491
475
  }
492
- async sendHttpRequest(rpcRequest) {
476
+ async sendHttpRequest(rpcRequest, signal) {
493
477
  const proof = await createDPoPProof(
494
478
  this.privateKey,
495
479
  this.kid,
@@ -506,14 +490,18 @@ var GatewayClient = class {
506
490
  DPoP: proof,
507
491
  "Mcp-Session-Id": MCP_SESSION_ID
508
492
  },
509
- body: JSON.stringify(rpcRequest)
493
+ body: JSON.stringify(rpcRequest),
494
+ // ROUND-3 MAJOR A: the caller's AbortSignal rides HERE (a real fetch
495
+ // option), never inside the JSON-RPC body. `undefined` is a valid value
496
+ // for the fetch `signal` option (no abort wired).
497
+ ...signal ? { signal } : {}
510
498
  });
511
499
  }
512
500
  /**
513
501
  * Handle step-up retry: poll with backoff until user approves
514
502
  * or timeout is reached.
515
503
  */
516
- async handleStepUp(rpcRequest, trigger) {
504
+ async handleStepUp(rpcRequest, trigger, signal) {
517
505
  console.error(
518
506
  `[gateway] Device re-authorization required (${trigger ?? "unknown"}). Waiting for user approval...`
519
507
  );
@@ -522,7 +510,7 @@ var GatewayClient = class {
522
510
  await sleep(STEP_UP_POLL_INTERVAL_MS);
523
511
  let response;
524
512
  try {
525
- response = await this.sendHttpRequest(rpcRequest);
513
+ response = await this.sendHttpRequest(rpcRequest, signal);
526
514
  } catch {
527
515
  continue;
528
516
  }
@@ -648,16 +636,41 @@ import {
648
636
  ListToolsRequestSchema,
649
637
  CallToolRequestSchema
650
638
  } from "@modelcontextprotocol/sdk/types.js";
651
- function buildChannelInstructions(tandemMembershipCount, eventLogPath) {
652
- const intro = 'Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:\n\n(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. Reply with tandem_send(tandem_id, text, reply_to=<message_id>); read more context with tandem_messages(tandem_id, since=cursor).\n\n';
653
- 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: Monitor(command="tail -n +1 -F ${eventLogPath}", persistent=true, description="kojee Tandem events"). Each appended line will arrive as a separate wake notification. Use the cursor in the line to call tandem_messages(tandem_id, since=cursor-1) for the full message body.
639
+
640
+ // src/version.ts
641
+ import fs2 from "fs";
642
+ import path2 from "path";
643
+ import { fileURLToPath } from "url";
644
+ var FALLBACK_VERSION = "0.0.0-unknown";
645
+ function resolveVersion() {
646
+ try {
647
+ const here = path2.dirname(fileURLToPath(import.meta.url));
648
+ const parsed = JSON.parse(
649
+ fs2.readFileSync(path2.join(here, "..", "package.json"), "utf8")
650
+ );
651
+ return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
652
+ } catch (err) {
653
+ process.stderr.write(
654
+ `kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
655
+ `
656
+ );
657
+ return FALLBACK_VERSION;
658
+ }
659
+ }
660
+ var VERSION = resolveVersion();
661
+
662
+ // src/server.ts
663
+ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
664
+ const intro = `Tandem events are delivered to you in three ways depending on which is active in your Claude Code session:
665
+
666
+ (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()}.
667
+
668
+ `;
669
+ 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.)
654
670
 
655
671
  `;
656
672
  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.";
657
673
  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.";
658
- if (tandemMembershipCount === 0) {
659
- return intro + listenSection;
660
- }
661
674
  return intro + monitorSection + listenSection + advice;
662
675
  }
663
676
  function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath) {
@@ -666,7 +679,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
666
679
  capabilities.experimental = { "claude/channel": {} };
667
680
  }
668
681
  const server = new Server(
669
- { name: "kojee-mcp", version: "0.3.0" },
682
+ { name: "kojee-mcp", version: VERSION },
670
683
  {
671
684
  capabilities,
672
685
  ...adapter.supportsChannels ? { instructions: buildChannelInstructions(tandemMembershipCount, eventLogPath ?? "") } : {}
@@ -695,31 +708,26 @@ async function startMcpServer(server) {
695
708
  }
696
709
 
697
710
  // src/runtime/detect.ts
698
- import childProcess from "child_process";
699
- function detectRuntime(env = process.env) {
711
+ import psList from "ps-list";
712
+ async function detectRuntime(env = process.env) {
700
713
  if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
701
714
  if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
702
- if (parentProcessLooksLikeClaude()) return "claude-code";
715
+ if (await parentProcessLooksLikeClaude()) return "claude-code";
703
716
  return "unknown";
704
717
  }
705
- function parentProcessLooksLikeClaude() {
706
- if (process.platform === "win32") return false;
718
+ async function parentProcessLooksLikeClaude() {
707
719
  try {
720
+ const processes = await psList();
721
+ const byPid = new Map(processes.map((p) => [p.pid, p]));
708
722
  let pid = process.ppid;
709
723
  for (let depth = 0; depth < 5 && pid && pid > 1; depth++) {
710
- const cmd = childProcess.execFileSync("ps", ["-p", String(pid), "-o", "command="], {
711
- encoding: "utf8",
712
- stdio: ["ignore", "pipe", "ignore"]
713
- }).trim();
714
- if (/(^|\/)claude(\.app)?(\/|$|\s|\\)/i.test(cmd) || /Claude Helper/i.test(cmd)) {
724
+ const row = byPid.get(pid);
725
+ if (!row) return false;
726
+ const haystack = `${row.name} ${row.cmd ?? ""}`;
727
+ if (/(^|\/|\\)claude(\.app|\.exe)?(\/|$|\s|\\)/i.test(haystack) || /Claude Helper/i.test(haystack)) {
715
728
  return true;
716
729
  }
717
- const ppidOut = childProcess.execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
718
- encoding: "utf8",
719
- stdio: ["ignore", "pipe", "ignore"]
720
- }).trim();
721
- pid = Number.parseInt(ppidOut, 10);
722
- if (!Number.isFinite(pid)) return false;
730
+ pid = row.ppid;
723
731
  }
724
732
  } catch {
725
733
  }
@@ -742,7 +750,7 @@ function formatBody(event) {
742
750
  `[Tandem: ${event.tandem_id}] ${event.from.displayname} (${event.from.principal}) \u2014 ${event.kind}:`,
743
751
  event.content.body,
744
752
  "",
745
- `> reply with tandem_send(tandem_id="${event.tandem_id}", text="...", reply_to="${event.id}")`
753
+ `> ${buildReplyRecipe({ tandem_id: event.tandem_id, message_id: event.id })}`
746
754
  ].join("\n");
747
755
  }
748
756
  var claudeCodeAdapter = {
@@ -773,176 +781,40 @@ var unknownAdapter = {
773
781
  }
774
782
  };
775
783
 
776
- // src/tandem/event-stream.ts
777
- async function startEventStream(opts) {
778
- let stopped = false;
779
- let lastCursor = null;
780
- const controller = new AbortController();
781
- void (async function loop() {
782
- let backoffMs = 1e3;
783
- while (!stopped) {
784
- try {
785
- await connectAndConsume(opts, lastCursor, controller, (cursor) => {
786
- lastCursor = cursor;
787
- backoffMs = 1e3;
788
- });
789
- } catch (err) {
790
- if (stopped) return;
791
- console.error("[event-stream] disconnect:", err.message);
792
- }
793
- if (stopped) return;
794
- const jitter = Math.random() * backoffMs;
795
- await sleep2(jitter);
796
- backoffMs = Math.min(backoffMs * 2, 3e4);
797
- }
798
- })();
799
- return () => {
800
- stopped = true;
801
- controller.abort();
802
- };
803
- }
804
- async function connectAndConsume(opts, sinceCursor, controller, onCursor) {
805
- const params = new URLSearchParams();
806
- if (sinceCursor !== null) params.set("since", String(sinceCursor));
807
- const url = `${opts.brokerUrl}/api/v2/tandems/stream${params.toString() ? "?" + params.toString() : ""}`;
808
- const proof = await createDPoPProof(
809
- opts.gateway.getPrivateKey(),
810
- opts.gateway.getKid(),
811
- "GET",
812
- url,
813
- void 0,
814
- opts.token
815
- );
816
- const res = await fetch(url, {
817
- method: "GET",
818
- headers: {
819
- Authorization: `DPoP ${opts.token}`,
820
- DPoP: proof,
821
- "Mcp-Session-Id": MCP_SESSION_ID,
822
- Accept: "text/event-stream"
823
- },
824
- signal: controller.signal
825
- });
826
- if (!res.ok) throw new Error(`SSE connect failed: ${res.status}`);
827
- if (!res.body) throw new Error("SSE response has no body");
828
- await consumeSse(res.body, opts, onCursor);
829
- }
830
- async function consumeSse(body, opts, onCursor) {
831
- const reader = body.getReader();
832
- const decoder = new TextDecoder();
833
- let buffer = "";
834
- while (true) {
835
- const { value, done } = await reader.read();
836
- if (done) return;
837
- buffer += decoder.decode(value, { stream: true });
838
- const events = drainSseEvents(buffer);
839
- buffer = events.remaining;
840
- for (const evt of events.events) {
841
- if (evt.event === "stream_revoked") {
842
- throw new Error("stream_revoked \u2014 reconnect needed");
843
- }
844
- if (evt.event === "heartbeat") continue;
845
- if (evt.event === "message" || evt.event === "state_change") {
846
- try {
847
- const raw = JSON.parse(evt.data);
848
- const parsed = normalizeBackendEvent(raw, evt.event);
849
- onCursor(parsed.cursor);
850
- opts.queue?.push(parsed);
851
- const channel = opts.adapter.formatTandemEvent(parsed);
852
- await opts.server.notification({
853
- method: "notifications/claude/channel",
854
- params: channel
855
- });
856
- opts.queue?.markChannelDelivered(parsed.id);
857
- if (opts.eventLog) {
858
- try {
859
- await opts.eventLog.append(parsed);
860
- opts.queue?.markMonitorDelivered(parsed.id);
861
- } catch (err) {
862
- console.error("[event-stream] event-log append failed:", err);
863
- }
864
- }
865
- } catch (err) {
866
- console.error("[event-stream] failed to handle event:", err);
867
- }
868
- }
869
- }
870
- }
871
- }
872
- function drainSseEvents(input) {
873
- const events = [];
874
- const parts = input.split("\n\n");
875
- const remaining = parts.pop() ?? "";
876
- for (const block of parts) {
877
- let id;
878
- let event = "message";
879
- const dataLines = [];
880
- for (const line of block.split("\n")) {
881
- if (line.startsWith("id: ")) id = line.slice(4);
882
- else if (line.startsWith("event: ")) event = line.slice(7);
883
- else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
884
- }
885
- if (dataLines.length > 0) events.push({ id, event, data: dataLines.join("\n") });
886
- }
887
- return { events, remaining };
888
- }
889
- function sleep2(ms) {
890
- return new Promise((r) => setTimeout(r, ms));
891
- }
892
- function normalizeBackendEvent(raw, sseEventType) {
893
- const obj = raw ?? {};
894
- const maybeFrom = obj["from"];
895
- if (maybeFrom && typeof maybeFrom["principal"] === "string") {
896
- return raw;
897
- }
898
- const sender = obj["sender"] ?? {};
899
- const principal = sender["principal_id"] ?? "";
900
- const agentId = sender["agent_id"];
901
- const displayname = principal ? `principal:${principal.slice(0, 8)}` : "unknown";
902
- const type = sseEventType === "state_change" ? "state_change" : "message";
903
- const kind = obj["kind"] ?? "message";
904
- return {
905
- type,
906
- id: obj["message_id"] ?? obj["id"] ?? "",
907
- tandem_id: obj["tandem_id"] ?? "",
908
- cursor: obj["cursor"] ?? 0,
909
- time: obj["time"] ?? (/* @__PURE__ */ new Date()).toISOString(),
910
- from: {
911
- member_id: "",
912
- principal,
913
- ...agentId ? { agent_id: agentId } : {},
914
- displayname
915
- },
916
- kind,
917
- content: {
918
- body: obj["body"] ?? "",
919
- ...typeof obj["format"] === "string" ? { format: obj["format"] } : {}
920
- },
921
- ...Array.isArray(obj["mentions"]) ? { mentions: obj["mentions"] } : {},
922
- ...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {}
923
- };
924
- }
925
-
926
784
  // src/index.ts
927
- var DEFAULT_KEYSTORE_PATH = path2.join(
928
- process.env["HOME"] ?? "~",
929
- ".kojee",
930
- "keypair.json"
931
- );
785
+ var DEFAULT_KEYSTORE_PATH = path3.join(os2.homedir(), ".kojee", "keypair.json");
932
786
  function isDPoPEnrollmentError(err) {
933
787
  const msg = String(err?.message ?? err ?? "").toLowerCase();
934
788
  if (msg.includes("invalid or expired") && msg.includes("token")) return false;
935
789
  if (msg.includes("generate a new")) return false;
936
790
  return msg.includes("401") || msg.includes("authentication failed") || msg.includes("dpop") || msg.includes("key enrollment") || msg.includes("kid");
937
791
  }
938
- function selectAdapter() {
939
- const runtime = detectRuntime();
792
+ async function selectAdapter() {
793
+ const runtime = await detectRuntime();
940
794
  if (runtime === "claude-code") return claudeCodeAdapter;
941
795
  return unknownAdapter;
942
796
  }
797
+ async function listTandemIds(gateway) {
798
+ const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
799
+ const maybeErr = result;
800
+ if (maybeErr.isError) return null;
801
+ const text = maybeErr.content?.[0]?.text;
802
+ try {
803
+ const parsed = text ? JSON.parse(text) : {};
804
+ const list = Array.isArray(parsed.tandems) ? parsed.tandems : Array.isArray(parsed) ? parsed : null;
805
+ if (!Array.isArray(list)) return [];
806
+ return list.map((t) => {
807
+ if (typeof t === "string") return t;
808
+ const obj = t;
809
+ return obj?.tandem_id ?? obj?.id;
810
+ }).filter((id) => typeof id === "string" && id.length > 0);
811
+ } catch {
812
+ return null;
813
+ }
814
+ }
943
815
  async function startProxy(config) {
944
816
  const keystorePath = config.keystorePath || DEFAULT_KEYSTORE_PATH;
945
- const adapter = selectAdapter();
817
+ const adapter = await selectAdapter();
946
818
  console.error(`[kojee-mcp] Starting proxy for ${config.url} (runtime=${adapter.runtime})`);
947
819
  const { registry, gateway } = await enrollAndDiscover(config, keystorePath);
948
820
  console.error(
@@ -950,22 +822,8 @@ async function startProxy(config) {
950
822
  );
951
823
  let tandemMembershipCount = -1;
952
824
  try {
953
- const result = await gateway.sendRpc("tools/call", { name: "tandem_list", arguments: {} });
954
- const maybeErr = result;
955
- if (!maybeErr.isError) {
956
- const text = maybeErr.content?.[0]?.text;
957
- try {
958
- const parsed = text ? JSON.parse(text) : {};
959
- if (Array.isArray(parsed.tandems)) {
960
- tandemMembershipCount = parsed.tandems.length;
961
- } else if (Array.isArray(parsed)) {
962
- tandemMembershipCount = parsed.length;
963
- } else {
964
- tandemMembershipCount = 0;
965
- }
966
- } catch {
967
- }
968
- }
825
+ const bootIds = await listTandemIds(gateway);
826
+ tandemMembershipCount = bootIds === null ? -1 : bootIds.length;
969
827
  } catch (err) {
970
828
  console.error("[kojee-mcp] tandem_list probe failed:", err.message);
971
829
  }
@@ -973,22 +831,38 @@ async function startProxy(config) {
973
831
  let server;
974
832
  if (adapter.supportsChannels) {
975
833
  const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
976
- const { startHookServer } = await import("./hook-server-43QS7L7P.js");
834
+ const { startHookServer } = await import("./hook-server-QF5JVUHV.js");
977
835
  const {
978
836
  writeDiscoveryByKey,
979
837
  cleanupDiscoveryByKey,
980
838
  sweepStaleDiscovery
981
- } = await import("./session-discovery-WSHLR4OV.js");
982
- const { startEventLog, sweepStaleEventLogs } = await import("./event-log-ETWR6PPY.js");
839
+ } = await import("./session-discovery-QE5TTAPS.js");
840
+ const { startEventLog, sweepStaleEventLogs } = await import("./event-log-R6VW6GAF.js");
841
+ const { resubscribeMemberships } = await import("./resubscribe-SLZNA76S.js");
983
842
  sweepStaleDiscovery();
984
843
  sweepStaleEventLogs();
985
- const ccPid = findClaudeAncestorPid();
844
+ const ccPid = await findClaudeAncestorPid();
986
845
  const projectDir = process.env["CLAUDE_PROJECT_DIR"];
987
846
  const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
988
847
  const eventLog = startEventLog({ key: discoveryKey });
989
848
  server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path);
990
849
  const queue = new EventQueue();
991
- const hookServer = await startHookServer({ port: 0, queue, adapter });
850
+ let streamHandle = null;
851
+ const hookServer = await startHookServer({
852
+ port: 0,
853
+ queue,
854
+ adapter,
855
+ getStreamState: () => streamHandle ? streamHandle.getState() : {
856
+ connected: false,
857
+ connectedSince: null,
858
+ lastEventAt: null,
859
+ lastHeartbeatAt: null,
860
+ cursors: {},
861
+ reconnectCount: 0,
862
+ // Adaptive: unknown until the watchdog observes ≥2 heartbeats.
863
+ staleAfterMs: null
864
+ }
865
+ });
992
866
  writeDiscoveryByKey(discoveryKey, {
993
867
  schema: 2,
994
868
  discoveryKey,
@@ -1000,7 +874,8 @@ async function startProxy(config) {
1000
874
  pid: process.pid,
1001
875
  port: hookServer.port,
1002
876
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1003
- brokerUrl: config.url
877
+ brokerUrl: config.url,
878
+ eventLogPath: eventLog.path
1004
879
  });
1005
880
  const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
1006
881
  process.on("exit", () => cleanupDiscoveryFile());
@@ -1013,15 +888,39 @@ async function startProxy(config) {
1013
888
  process.exit(0);
1014
889
  });
1015
890
  process.on("exit", () => eventLog.cleanup());
1016
- const cancelStream = await startEventStream({
891
+ streamHandle = await startEventStream({
1017
892
  brokerUrl: config.url,
1018
893
  token: config.token,
1019
894
  gateway,
1020
895
  adapter,
1021
896
  server,
1022
897
  queue,
1023
- eventLog
898
+ eventLog,
899
+ // Resubscribe-on-start (P0 #2): touch all memberships + write a
900
+ // `status=subscribed n=<count>` line on every (re)connect, so a backend
901
+ // restart / scope reset self-heals and the log is never ambiguously
902
+ // empty. See resubscribe.ts for the unverified-touch caveat.
903
+ //
904
+ // MINOR 6: `listTandems` re-fetches the membership list per reconnect (a
905
+ // mid-session join is touched next reconnect, not boot-frozen), each
906
+ // touch is timeout-bounded + run with bounded concurrency, and the whole
907
+ // routine runs concurrently with consumeSse (never blocks first-event
908
+ // delivery). `listTandemIds` may return null (unknown) → treat as empty.
909
+ // MINOR E: a shared debounce cursor damps a connect/drop flap storm — a
910
+ // resubscribe within 30s of the last successful one is skipped.
911
+ onConnected: /* @__PURE__ */ (() => {
912
+ const debounceState = { lastRunAt: 0 };
913
+ return async () => {
914
+ await resubscribeMemberships({
915
+ gateway,
916
+ eventLog,
917
+ listTandems: async () => await listTandemIds(gateway) ?? [],
918
+ debounceState
919
+ });
920
+ };
921
+ })()
1024
922
  });
923
+ const cancelStream = streamHandle;
1025
924
  process.stdin.on("end", () => {
1026
925
  cancelStream();
1027
926
  cleanupDiscoveryFile();
@@ -1064,7 +963,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
1064
963
  "[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
1065
964
  );
1066
965
  try {
1067
- if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
966
+ if (fs3.existsSync(keystorePath)) fs3.unlinkSync(keystorePath);
1068
967
  } catch (unlinkErr) {
1069
968
  console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
1070
969
  }
@@ -1074,5 +973,6 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
1074
973
 
1075
974
  export {
1076
975
  AuthModule,
976
+ VERSION,
1077
977
  startProxy
1078
978
  };
@@ -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
  }