kojee-mcp 0.5.4 → 0.5.7

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 (30) hide show
  1. package/README.md +24 -5
  2. package/dist/{chunk-36L3GCU3.js → chunk-3XDJOHMZ.js} +12 -2
  3. package/dist/chunk-6SK6ITFE.js +142 -0
  4. package/dist/{chunk-62KH6VNQ.js → chunk-GATXJ6UT.js} +122 -190
  5. package/dist/{control-token-TYDAL477.js → chunk-GI2CKKBL.js} +13 -2
  6. package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
  7. package/dist/{chunk-YVUXQ4Z2.js → chunk-UEGQGXPY.js} +53 -8
  8. package/dist/{chunk-OSKHA5DS.js → chunk-V5VZPYMZ.js} +2 -2
  9. package/dist/cli.js +19 -24
  10. package/dist/control-token-4BUCTYQB.js +13 -0
  11. package/dist/{doctor-TXWMMSRC.js → doctor-QCQDFLEH.js} +29 -16
  12. package/dist/{doctor-codex-3A7KYOVX.js → doctor-codex-NZ53ROQA.js} +3 -3
  13. package/dist/ensure-join-7AEDJMPE.js +96 -0
  14. package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
  15. package/dist/{hook-server-NDJSV22J.js → hook-server-37E2LUKJ.js} +6 -0
  16. package/dist/index.d.ts +18 -15
  17. package/dist/index.js +7 -4
  18. package/dist/lib.d.ts +427 -0
  19. package/dist/lib.js +44 -0
  20. package/dist/parent-watchdog-RZLHYP7T.js +65 -0
  21. package/dist/reconnect-scheduler-ARV6JIWK.js +36 -0
  22. package/dist/resubscribe-G5OGDZJD.js +6 -0
  23. package/dist/{send-cli-7QJ36YY7.js → send-cli-C2F4WTBN.js} +1 -1
  24. package/dist/{stop-hook-GO363SMD.js → stop-hook-46BJD55B.js} +15 -7
  25. package/dist/{tail-stream-U436QL2X.js → tail-stream-VUZBYKXS.js} +4 -4
  26. package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
  27. package/dist/{webhook-config-UKUSI2FE.js → webhook-config-O4WMQ532.js} +1 -1
  28. package/dist/{webhook-sink-GCLL2S6S.js → webhook-sink-NWGCUDGY.js} +17 -3
  29. package/dist/{wizard-Z5JA3YPV.js → wizard-UOXQYJLP.js} +7 -7
  30. package/package.json +11 -2
package/README.md CHANGED
@@ -218,8 +218,8 @@ in it is Hermes-specific — and it is **OFF by default**.
218
218
  |---|---|
219
219
  | `KOJEE_WEBHOOK_URL` | Receiver endpoint (http/https). **Unset ⇒ sink OFF** (zero behavior change). |
220
220
  | `KOJEE_WEBHOOK_SECRET` | HMAC-SHA256 key for the signature header. URL set but secret unset ⇒ sink **DISABLED with an error** (the proxy NEVER sends unsigned webhooks). |
221
- | `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `5000`). |
222
- | `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a retryable failure — network / 5xx / 408 / 429 (default `4`). |
221
+ | `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `30000`). A timed-out attempt is **never retried** — see the retry policy below. |
222
+ | `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a **retryable** failure — connection errors (refused / reset / DNS) / 5xx / 408 / 429 (default `2`). Timeouts are **not** in this class. |
223
223
  | `KOJEE_WEBHOOK_SIGNATURE_HEADER` | Header name carrying the signature (default `X-Kojee-Signature`). |
224
224
  | `KOJEE_WEBHOOK_SIGNATURE_PREFIX` | Literal string prepended to the hex digest (default empty — bare hex). |
225
225
  | `KOJEE_WEBHOOK_SIGNATURE_FORMAT` | Optional preset. `github` ⇒ header `X-Hub-Signature-256`, prefix `sha256=` (the GitHub-webhook convention). Explicit `_HEADER`/`_PREFIX` vars override the preset's corresponding value. Unknown values are **warned about once and ignored** — never fatal. |
@@ -275,10 +275,29 @@ constant beside it):
275
275
  cursor on restart, so the same event may arrive more than once. There is **no
276
276
  exactly-once** promise; the receiver's dedupe is what makes redelivery safe.
277
277
 
278
+ **Retry policy (0.5.6 — anti-storm).** A delivery attempt that **times out is
279
+ never retried**: the receiver may well have processed the event and just
280
+ answered slowly (the canonical receiver spawns an agent session *before*
281
+ responding), so a re-POST risks duplicate side effects by design. The sink logs
282
+ it as `delivered-unconfirmed (receiver slow)` and moves on. Retries happen
283
+ **only on genuine non-delivery**: connection errors (refused / reset / DNS —
284
+ the request never reached a receiver) and 5xx / 408 / 429 responses (the
285
+ receiver answered that it did *not* process the event), up to
286
+ `KOJEE_WEBHOOK_MAX_RETRIES` (default `2`). For those retried cases delivery is
287
+ still **at-least-once** and every redelivery carries the same
288
+ `X-Kojee-Delivery` id and identical body bytes — **dedupe by event id remains
289
+ the receiver's responsibility** (the `recipe.ts` contract). This fix removes
290
+ the timeout-driven storm, not the at-least-once semantics. If your receiver
291
+ does slow work, the robust pattern is still: verify the signature, dedupe,
292
+ **respond `202` immediately**, then process.
293
+
278
294
  The sink is isolated and fire-and-forget: a slow, hanging, or failing webhook can
279
- never delay or break the Monitor (event-log) or Channel wake paths. The status
280
- log redacts the secret and strips any basic-auth credentials embedded in
281
- `KOJEE_WEBHOOK_URL`.
295
+ never delay or break the Monitor (event-log) or Channel wake paths (those run
296
+ before the webhook push). A slow delivery only delays *later webhook events*
297
+ behind it in the sink's own FIFO, and that backlog is bounded: the queue caps at
298
+ 1000 (overflow logs + drops the newest; the resubscribe-replay redelivers after
299
+ a restart). The status log redacts the secret and strips any basic-auth
300
+ credentials embedded in `KOJEE_WEBHOOK_URL`.
282
301
 
283
302
  ## Wake Continuity (0.5.4)
284
303
 
@@ -14,10 +14,18 @@ import {
14
14
 
15
15
  // src/auth/keystore.ts
16
16
  import { importJWK, exportJWK, generateKeyPair } from "jose";
17
+ import crypto from "crypto";
17
18
  import fs from "fs";
18
19
  import os from "os";
19
20
  import path from "path";
20
21
  var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
22
+ function defaultPairedKeystorePath() {
23
+ return path.join(os.homedir(), ".kojee", "keypair.json");
24
+ }
25
+ function deriveKeystorePath(token) {
26
+ const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
27
+ return path.join(os.homedir(), ".kojee", `keypair-${hash}.json`);
28
+ }
21
29
  async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
22
30
  if (!fs.existsSync(keystorePath)) {
23
31
  return null;
@@ -61,7 +69,7 @@ async function generateES256KeyPair() {
61
69
  }
62
70
 
63
71
  // src/gateway-client.ts
64
- import crypto from "crypto";
72
+ import crypto2 from "crypto";
65
73
  var GatewayClient = class {
66
74
  constructor(brokerUrl, token, privateKey, kid, sessionId) {
67
75
  this.brokerUrl = brokerUrl;
@@ -97,7 +105,7 @@ var GatewayClient = class {
97
105
  * session_id = sha256(token + "proxy").slice(0, 16)
98
106
  */
99
107
  static deriveSessionId(token) {
100
- const hash = crypto.createHash("sha256").update(token + "proxy").digest("hex");
108
+ const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
101
109
  return hash.slice(0, 16);
102
110
  }
103
111
  /**
@@ -206,6 +214,8 @@ var GatewayClient = class {
206
214
  };
207
215
 
208
216
  export {
217
+ defaultPairedKeystorePath,
218
+ deriveKeystorePath,
209
219
  loadKeystore,
210
220
  saveKeystore,
211
221
  generateES256KeyPair,
@@ -0,0 +1,142 @@
1
+ import {
2
+ generateES256KeyPair,
3
+ loadKeystore,
4
+ saveKeystore
5
+ } from "./chunk-3XDJOHMZ.js";
6
+
7
+ // src/auth/auth-module.ts
8
+ import { calculateJwkThumbprint } from "jose";
9
+ import crypto from "crypto";
10
+
11
+ // src/auth/registration.ts
12
+ async function registerKey(brokerUrl, token, publicJwk) {
13
+ const url = `${brokerUrl}/api/v1/bots/keys/register/`;
14
+ const response = await fetch(url, {
15
+ method: "POST",
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ Authorization: `Bearer ${token}`
19
+ },
20
+ body: JSON.stringify({ public_jwk: publicJwk })
21
+ });
22
+ if (!response.ok) {
23
+ const body = await response.text();
24
+ throw new Error(
25
+ `Key registration failed (${response.status}): ${body}`
26
+ );
27
+ }
28
+ return await response.json();
29
+ }
30
+ async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
31
+ const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
32
+ const response = await fetch(url, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ Authorization: `Bearer ${token}`
37
+ },
38
+ body: JSON.stringify({
39
+ bot_key_id: botKeyId,
40
+ challenge,
41
+ signature
42
+ })
43
+ });
44
+ if (!response.ok) {
45
+ const body = await response.text();
46
+ throw new Error(
47
+ `Key confirmation failed (${response.status}): ${body}`
48
+ );
49
+ }
50
+ return await response.json();
51
+ }
52
+
53
+ // src/auth/auth-module.ts
54
+ async function signChallengeRaw(privateKey, data) {
55
+ const signer = crypto.createSign("SHA256");
56
+ signer.update(data);
57
+ signer.end();
58
+ const derSignature = signer.sign(
59
+ privateKey
60
+ );
61
+ return derSignature.toString("base64url");
62
+ }
63
+ var AuthModule = class {
64
+ constructor(token, brokerUrl, keystorePath) {
65
+ this.token = token;
66
+ this.brokerUrl = brokerUrl;
67
+ this.keystorePath = keystorePath;
68
+ }
69
+ token;
70
+ brokerUrl;
71
+ keystorePath;
72
+ privateKey = null;
73
+ publicJwk = null;
74
+ kid = null;
75
+ /**
76
+ * Ensure we have an enrolled keypair. Either loads from disk or
77
+ * performs the full enrollment flow.
78
+ */
79
+ async ensureEnrolled() {
80
+ const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
81
+ if (existing) {
82
+ this.privateKey = existing.privateKey;
83
+ this.publicJwk = existing.publicJwk;
84
+ this.kid = existing.kid;
85
+ console.error("[auth] Loaded existing keypair from keystore");
86
+ return {
87
+ privateKey: existing.privateKey,
88
+ publicJwk: existing.publicJwk,
89
+ kid: existing.kid
90
+ };
91
+ }
92
+ console.error("[auth] No valid keystore found, enrolling new keypair...");
93
+ const { privateKey, publicJwk } = await generateES256KeyPair();
94
+ const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
95
+ console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
96
+ const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
97
+ const challengeData = `${regResult.challenge}.${thumbprint}`;
98
+ const signature = await signChallengeRaw(privateKey, challengeData);
99
+ const confirmResult = await confirmKey(
100
+ this.brokerUrl,
101
+ this.token,
102
+ regResult.bot_key_id,
103
+ regResult.challenge,
104
+ signature
105
+ );
106
+ if (!confirmResult.key_confirmed) {
107
+ throw new Error("Key enrollment failed: confirmation was rejected");
108
+ }
109
+ console.error("[auth] Key enrollment confirmed");
110
+ await saveKeystore(
111
+ privateKey,
112
+ publicJwk,
113
+ regResult.bot_key_id,
114
+ this.brokerUrl,
115
+ this.keystorePath
116
+ );
117
+ this.privateKey = privateKey;
118
+ this.publicJwk = publicJwk;
119
+ this.kid = regResult.bot_key_id;
120
+ return {
121
+ privateKey,
122
+ publicJwk,
123
+ kid: regResult.bot_key_id
124
+ };
125
+ }
126
+ getPrivateKey() {
127
+ if (!this.privateKey) throw new Error("Not enrolled yet");
128
+ return this.privateKey;
129
+ }
130
+ getPublicJwk() {
131
+ if (!this.publicJwk) throw new Error("Not enrolled yet");
132
+ return this.publicJwk;
133
+ }
134
+ getKid() {
135
+ if (!this.kid) throw new Error("Not enrolled yet");
136
+ return this.kid;
137
+ }
138
+ };
139
+
140
+ export {
141
+ AuthModule
142
+ };
@@ -1,21 +1,21 @@
1
1
  import {
2
- startEventStream
3
- } from "./chunk-YVUXQ4Z2.js";
2
+ buildCatchUpNote,
3
+ buildMonitorSpawn,
4
+ buildReplyRecipe
5
+ } from "./chunk-X672ZN7V.js";
4
6
  import {
5
7
  deriveDiscoveryKey,
6
8
  findClaudeAncestorPid
7
9
  } from "./chunk-BJMASMKX.js";
8
10
  import {
9
- buildCatchUpNote,
10
- buildMonitorSpawn,
11
- buildReplyRecipe
12
- } from "./chunk-X672ZN7V.js";
11
+ AuthModule
12
+ } from "./chunk-6SK6ITFE.js";
13
13
  import {
14
- GatewayClient,
15
- generateES256KeyPair,
16
- loadKeystore,
17
- saveKeystore
18
- } from "./chunk-36L3GCU3.js";
14
+ GatewayClient
15
+ } from "./chunk-3XDJOHMZ.js";
16
+ import {
17
+ startEventStream
18
+ } from "./chunk-UEGQGXPY.js";
19
19
  import {
20
20
  translateToolCallResult
21
21
  } from "./chunk-LDZXU3DW.js";
@@ -25,139 +25,6 @@ import fs2 from "fs";
25
25
  import os from "os";
26
26
  import path2 from "path";
27
27
 
28
- // src/auth/auth-module.ts
29
- import { calculateJwkThumbprint } from "jose";
30
- import crypto from "crypto";
31
-
32
- // src/auth/registration.ts
33
- async function registerKey(brokerUrl, token, publicJwk) {
34
- const url = `${brokerUrl}/api/v1/bots/keys/register/`;
35
- const response = await fetch(url, {
36
- method: "POST",
37
- headers: {
38
- "Content-Type": "application/json",
39
- Authorization: `Bearer ${token}`
40
- },
41
- body: JSON.stringify({ public_jwk: publicJwk })
42
- });
43
- if (!response.ok) {
44
- const body = await response.text();
45
- throw new Error(
46
- `Key registration failed (${response.status}): ${body}`
47
- );
48
- }
49
- return await response.json();
50
- }
51
- async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
52
- const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
53
- const response = await fetch(url, {
54
- method: "POST",
55
- headers: {
56
- "Content-Type": "application/json",
57
- Authorization: `Bearer ${token}`
58
- },
59
- body: JSON.stringify({
60
- bot_key_id: botKeyId,
61
- challenge,
62
- signature
63
- })
64
- });
65
- if (!response.ok) {
66
- const body = await response.text();
67
- throw new Error(
68
- `Key confirmation failed (${response.status}): ${body}`
69
- );
70
- }
71
- return await response.json();
72
- }
73
-
74
- // src/auth/auth-module.ts
75
- async function signChallengeRaw(privateKey, data) {
76
- const signer = crypto.createSign("SHA256");
77
- signer.update(data);
78
- signer.end();
79
- const derSignature = signer.sign(
80
- privateKey
81
- );
82
- return derSignature.toString("base64url");
83
- }
84
- var AuthModule = class {
85
- constructor(token, brokerUrl, keystorePath) {
86
- this.token = token;
87
- this.brokerUrl = brokerUrl;
88
- this.keystorePath = keystorePath;
89
- }
90
- token;
91
- brokerUrl;
92
- keystorePath;
93
- privateKey = null;
94
- publicJwk = null;
95
- kid = null;
96
- /**
97
- * Ensure we have an enrolled keypair. Either loads from disk or
98
- * performs the full enrollment flow.
99
- */
100
- async ensureEnrolled() {
101
- const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
102
- if (existing) {
103
- this.privateKey = existing.privateKey;
104
- this.publicJwk = existing.publicJwk;
105
- this.kid = existing.kid;
106
- console.error("[auth] Loaded existing keypair from keystore");
107
- return {
108
- privateKey: existing.privateKey,
109
- publicJwk: existing.publicJwk,
110
- kid: existing.kid
111
- };
112
- }
113
- console.error("[auth] No valid keystore found, enrolling new keypair...");
114
- const { privateKey, publicJwk } = await generateES256KeyPair();
115
- const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
116
- console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
117
- const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
118
- const challengeData = `${regResult.challenge}.${thumbprint}`;
119
- const signature = await signChallengeRaw(privateKey, challengeData);
120
- const confirmResult = await confirmKey(
121
- this.brokerUrl,
122
- this.token,
123
- regResult.bot_key_id,
124
- regResult.challenge,
125
- signature
126
- );
127
- if (!confirmResult.key_confirmed) {
128
- throw new Error("Key enrollment failed: confirmation was rejected");
129
- }
130
- console.error("[auth] Key enrollment confirmed");
131
- await saveKeystore(
132
- privateKey,
133
- publicJwk,
134
- regResult.bot_key_id,
135
- this.brokerUrl,
136
- this.keystorePath
137
- );
138
- this.privateKey = privateKey;
139
- this.publicJwk = publicJwk;
140
- this.kid = regResult.bot_key_id;
141
- return {
142
- privateKey,
143
- publicJwk,
144
- kid: regResult.bot_key_id
145
- };
146
- }
147
- getPrivateKey() {
148
- if (!this.privateKey) throw new Error("Not enrolled yet");
149
- return this.privateKey;
150
- }
151
- getPublicJwk() {
152
- if (!this.publicJwk) throw new Error("Not enrolled yet");
153
- return this.publicJwk;
154
- }
155
- getKid() {
156
- if (!this.kid) throw new Error("Not enrolled yet");
157
- return this.kid;
158
- }
159
- };
160
-
161
28
  // src/tool-registry.ts
162
29
  var ToolRegistry = class {
163
30
  constructor(gateway) {
@@ -269,7 +136,19 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
269
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.";
270
137
  return intro + monitorSection + listenSection + advice;
271
138
  }
272
- function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath) {
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) {
273
152
  const capabilities = { tools: {} };
274
153
  if (adapter.supportsChannels) {
275
154
  capabilities.experimental = { "claude/channel": {} };
@@ -291,8 +170,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
291
170
  });
292
171
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
293
172
  const { name, arguments: args } = request.params;
294
- const rawResult = await registry.callTool(name, args ?? {});
295
- const result = translateToolCallResult(rawResult);
173
+ const result = await executeToolCall(registry, name, args ?? {}, hooks);
296
174
  return { content: result.content, isError: result.isError };
297
175
  });
298
176
  return server;
@@ -420,6 +298,7 @@ async function listTandemIds(gateway) {
420
298
  return list.map((t) => {
421
299
  if (typeof t === "string") return t;
422
300
  const obj = t;
301
+ if (obj?.my_membership?.is_member !== true) return void 0;
423
302
  return obj?.tandem_id ?? obj?.id;
424
303
  }).filter((id) => typeof id === "string" && id.length > 0);
425
304
  } catch {
@@ -437,6 +316,50 @@ async function startProxy(config) {
437
316
  console.error(
438
317
  `[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
439
318
  );
319
+ let activeStreamHandle = null;
320
+ const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-ARV6JIWK.js");
321
+ const joinReconnect = createJoinReconnectScheduler({
322
+ // BOOT-RACE (Bug B): report whether the stream handle was actually ready.
323
+ // `false` ⇒ activeStreamHandle is still null (tandem_join fired before the
324
+ // stream was set up) → the scheduler queues the reconnect and flushes it on
325
+ // notifyReady() once the handle is assigned (see below), instead of silently
326
+ // dropping it as the old `activeStreamHandle?.reconnect()` no-op did.
327
+ reconnect: () => {
328
+ if (!activeStreamHandle) return false;
329
+ activeStreamHandle.reconnect();
330
+ return true;
331
+ }
332
+ });
333
+ const onTandemJoin = () => joinReconnect.requestReconnect();
334
+ const teardownSteps = [];
335
+ let shuttingDown = false;
336
+ function shutdown(reason) {
337
+ if (shuttingDown) return;
338
+ shuttingDown = true;
339
+ activeStreamHandle?.();
340
+ for (const step of teardownSteps) {
341
+ try {
342
+ const maybe = step();
343
+ if (maybe && typeof maybe.catch === "function") {
344
+ maybe.catch((err) => {
345
+ console.error("[kojee-mcp] async shutdown step failed:", err?.message ?? err);
346
+ });
347
+ }
348
+ } catch (err) {
349
+ console.error("[kojee-mcp] shutdown step failed:", err?.message ?? err);
350
+ }
351
+ }
352
+ console.error(`[kojee-mcp] shutting down (${reason}), exiting`);
353
+ process.exit(0);
354
+ }
355
+ const ccPid = await findClaudeAncestorPid();
356
+ const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
357
+ await ensureJoinTandems({
358
+ gateway,
359
+ env: process.env["KOJEE_TANDEMS"],
360
+ listTandems: () => listTandemIds(gateway),
361
+ onJoined: () => joinReconnect.requestReconnect()
362
+ });
440
363
  let tandemMembershipCount = -1;
441
364
  try {
442
365
  const bootIds = await listTandemIds(gateway);
@@ -448,19 +371,18 @@ async function startProxy(config) {
448
371
  let server;
449
372
  if (adapter.supportsChannels) {
450
373
  const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
451
- const { startHookServer } = await import("./hook-server-NDJSV22J.js");
374
+ const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
452
375
  const {
453
376
  writeDiscoveryByKey,
454
377
  cleanupDiscoveryByKey,
455
378
  sweepStaleDiscovery
456
379
  } = await import("./session-discovery-FNMJGFPM.js");
457
380
  const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
458
- const { resubscribeMemberships } = await import("./resubscribe-SLZNA76S.js");
459
- const { resolveWebhookConfig } = await import("./webhook-config-UKUSI2FE.js");
460
- const { createWebhookSink } = await import("./webhook-sink-GCLL2S6S.js");
381
+ const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
382
+ const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
383
+ const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
461
384
  sweepStaleDiscovery();
462
385
  sweepStaleEventLogs();
463
- const ccPid = await findClaudeAncestorPid();
464
386
  const projectDir = process.env["CLAUDE_PROJECT_DIR"];
465
387
  const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
466
388
  const eventLog = startEventLog({ key: discoveryKey });
@@ -487,14 +409,16 @@ async function startProxy(config) {
487
409
  void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
488
410
  });
489
411
  }
490
- server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path);
491
- const { issueControlToken, controlTokenPath } = await import("./control-token-TYDAL477.js");
412
+ server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
413
+ onTandemJoin
414
+ });
415
+ const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
492
416
  let controlToken = null;
493
417
  try {
494
418
  controlToken = issueControlToken();
495
419
  } catch (err) {
496
420
  console.error(
497
- "[kojee-mcp] control token write failed \u2014 POST /send disabled:",
421
+ "[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
498
422
  err.message
499
423
  );
500
424
  }
@@ -504,7 +428,10 @@ async function startProxy(config) {
504
428
  port: 0,
505
429
  queue,
506
430
  adapter,
507
- ...controlToken !== null ? { send: { gateway, authToken: controlToken } } : {},
431
+ // 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
432
+ // reads (GET /poll, GET /status). When token issuance failed both stay
433
+ // available-but-degraded: /send answers 503, the reads stay open.
434
+ ...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
508
435
  getStreamState: () => streamHandle ? streamHandle.getState() : {
509
436
  connected: false,
510
437
  connectedSince: null,
@@ -539,16 +466,16 @@ async function startProxy(config) {
539
466
  authMode: config.authMode ?? "paired"
540
467
  });
541
468
  const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
542
- process.on("exit", () => cleanupDiscoveryFile());
543
- process.on("SIGINT", () => {
469
+ process.on("exit", () => {
544
470
  cleanupDiscoveryFile();
545
- process.exit(0);
471
+ eventLog.cleanup();
546
472
  });
547
- process.on("SIGTERM", () => {
548
- cleanupDiscoveryFile();
549
- process.exit(0);
473
+ teardownSteps.push(() => {
474
+ void webhookSink?.stop();
550
475
  });
551
- process.on("exit", () => eventLog.cleanup());
476
+ teardownSteps.push(() => cleanupDiscoveryFile());
477
+ teardownSteps.push(() => eventLog.cleanup());
478
+ teardownSteps.push(() => hookServer.stop());
552
479
  streamHandle = await startEventStream({
553
480
  brokerUrl: config.url,
554
481
  token: config.token,
@@ -584,24 +511,14 @@ async function startProxy(config) {
584
511
  };
585
512
  })()
586
513
  });
587
- const cancelStream = streamHandle;
588
- process.stdin.on("end", () => {
589
- cancelStream();
590
- void webhookSink?.stop();
591
- cleanupDiscoveryFile();
592
- eventLog.cleanup();
593
- hookServer.stop().finally(() => {
594
- console.error("[kojee-mcp] stdin closed, exiting");
595
- process.exit(0);
596
- });
597
- });
514
+ activeStreamHandle = streamHandle;
515
+ joinReconnect.notifyReady();
598
516
  } else if (needsWebhookEventStream()) {
599
517
  const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
600
- const { resolveWebhookConfig } = await import("./webhook-config-UKUSI2FE.js");
601
- const { createWebhookSink } = await import("./webhook-sink-GCLL2S6S.js");
602
- const { resubscribeMemberships } = await import("./resubscribe-SLZNA76S.js");
518
+ const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
519
+ const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
520
+ const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
603
521
  sweepStaleEventLogs();
604
- const ccPid = await findClaudeAncestorPid();
605
522
  const projectDir = process.env["CLAUDE_PROJECT_DIR"];
606
523
  const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
607
524
  const eventLog = startEventLog({ key: discoveryKey });
@@ -627,8 +544,14 @@ async function startProxy(config) {
627
544
  void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
628
545
  });
629
546
  }
630
- server = createMcpServer(registry, adapter, tandemMembershipCount);
547
+ server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
548
+ onTandemJoin
549
+ });
631
550
  process.on("exit", () => eventLog.cleanup());
551
+ teardownSteps.push(() => {
552
+ void webhookSink?.stop();
553
+ });
554
+ teardownSteps.push(() => eventLog.cleanup());
632
555
  const streamHandle = await startEventStream({
633
556
  brokerUrl: config.url,
634
557
  token: config.token,
@@ -649,19 +572,28 @@ async function startProxy(config) {
649
572
  };
650
573
  })()
651
574
  });
652
- process.stdin.on("end", () => {
653
- streamHandle();
654
- void webhookSink?.stop();
655
- eventLog.cleanup();
656
- console.error("[kojee-mcp] stdin closed, exiting");
657
- process.exit(0);
658
- });
575
+ activeStreamHandle = streamHandle;
576
+ joinReconnect.notifyReady();
659
577
  } else {
660
578
  server = createMcpServer(registry, adapter, tandemMembershipCount);
661
- process.stdin.on("end", () => {
662
- console.error("[kojee-mcp] stdin closed, exiting");
663
- process.exit(0);
579
+ }
580
+ process.stdin.on("end", () => shutdown("stdin end"));
581
+ process.stdin.on("close", () => shutdown("stdin close"));
582
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
583
+ process.on("SIGINT", () => shutdown("SIGINT"));
584
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
585
+ if (ccPid !== null) {
586
+ const { createParentWatchdog } = await import("./parent-watchdog-RZLHYP7T.js");
587
+ const watchdog = createParentWatchdog({
588
+ ccPid,
589
+ onParentGone: () => shutdown("parent (Claude Code) gone")
664
590
  });
591
+ watchdog.start();
592
+ teardownSteps.push(() => watchdog.stop());
593
+ } else {
594
+ console.error(
595
+ "[kojee-mcp] no Claude Code ancestor found \u2014 parent-liveness watchdog NOT armed (stdin/signal handlers still cover clean exits)"
596
+ );
665
597
  }
666
598
  await startMcpServer(server);
667
599
  }
@@ -698,7 +630,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
698
630
  }
699
631
 
700
632
  export {
701
- AuthModule,
702
633
  VERSION,
634
+ listTandemIds,
703
635
  startProxy
704
636
  };