kojee-mcp 0.7.5-staging.2 → 0.7.5-staging.21

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 (48) hide show
  1. package/README.md +27 -6
  2. package/dist/{cc-session-id-RURNIHHC.js → cc-session-id-5HHQJ7VC.js} +3 -1
  3. package/dist/{chunk-ZSHCHOPL.js → chunk-676WQSWZ.js} +163 -7
  4. package/dist/{chunk-FJUAMJHU.js → chunk-6NPMVZD7.js} +38 -3
  5. package/dist/{chunk-247WFMCJ.js → chunk-AGZ7ZGLY.js} +30 -3
  6. package/dist/chunk-AXBJQJKA.js +65 -0
  7. package/dist/{chunk-TCWIXG5C.js → chunk-CCVRXJJ2.js} +1 -1
  8. package/dist/{chunk-WBXU27BF.js → chunk-FNMKP34Y.js} +1 -1
  9. package/dist/chunk-GEGUWQYT.js +90 -0
  10. package/dist/{chunk-NE2F5CKS.js → chunk-LKGKDZSO.js} +9 -5
  11. package/dist/{chunk-XPIW4N55.js → chunk-N2YAZRAD.js} +178 -43
  12. package/dist/{chunk-LSUB6QMP.js → chunk-NIIBVKLP.js} +3 -0
  13. package/dist/chunk-P64UR3SE.js +86 -0
  14. package/dist/{chunk-A4IOKD4Z.js → chunk-QYZP6QT2.js} +2 -4
  15. package/dist/{chunk-KNEJTD6G.js → chunk-SZLNNMIZ.js} +6 -2
  16. package/dist/{chunk-CUOJHP2S.js → chunk-UGEBGCYU.js} +48 -12
  17. package/dist/{chunk-6XWTUDWW.js → chunk-WDWVBPXA.js} +7 -5
  18. package/dist/{chunk-TNZITJAB.js → chunk-WVQU3QQ7.js} +14 -4
  19. package/dist/cli.js +42 -23
  20. package/dist/{codex-prompt-submit-hook-J5PZEJSK.js → codex-prompt-submit-hook-UQJI6A54.js} +2 -2
  21. package/dist/{codex-stop-hook-32W4BIOM.js → codex-stop-hook-GTGRQNVV.js} +2 -2
  22. package/dist/{connect-handler-NZINEMG3.js → connect-handler-GTFXNZUC.js} +68 -9
  23. package/dist/{doctor-WUU5BVPT.js → doctor-54S7JGOE.js} +64 -5
  24. package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-6H4OOS6O.js} +84 -10
  25. package/dist/doctor-hermes-KMHM27Y3.js +597 -0
  26. package/dist/{doctor-openclaw-LL6NSPLC.js → doctor-openclaw-B5KADVHD.js} +5 -1
  27. package/dist/{event-log-2NBJEIEP.js → event-log-M22X3FAI.js} +1 -1
  28. package/dist/{event-stream-ATKYXDBV.js → event-stream-5ZEL7FXN.js} +5 -1
  29. package/dist/flap-log-D4KG7RK4.js +23 -0
  30. package/dist/{gateway-client-CbM2OC_w.d.ts → gateway-client-CLmQghON.d.ts} +11 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +6 -7
  33. package/dist/{install-JQNDGAAQ.js → install-GJVZEWLI.js} +8 -7
  34. package/dist/lib.d.ts +30 -1
  35. package/dist/lib.js +3 -3
  36. package/dist/plugins/hermes/adapter.py +7 -2
  37. package/dist/{registry-XJ67EO22.js → registry-MCJAVXJW.js} +36 -24
  38. package/dist/{send-cli-45RLGYIC.js → send-cli-WTY7H3BR.js} +2 -2
  39. package/dist/{server-DU3LFS32.js → server-VM3ZIGSZ.js} +2 -3
  40. package/dist/session-start-hook-VL3KYDTT.js +25 -0
  41. package/dist/{setup-handler-44ASXYMS.js → setup-handler-GNAIWAYU.js} +7 -7
  42. package/dist/{stop-hook-N6TX4YQT.js → stop-hook-BLPYRE7T.js} +5 -5
  43. package/dist/stream-watchdog-K2AWNCQW.js +64 -0
  44. package/dist/{tail-stream-G4WGTXDS.js → tail-stream-PQXEY3TI.js} +120 -9
  45. package/dist/{user-prompt-submit-hook-DTXXFDSD.js → user-prompt-submit-hook-YQXGE3JU.js} +3 -3
  46. package/package.json +3 -2
  47. package/dist/chunk-5DHIUN73.js +0 -25
  48. package/dist/chunk-TMCNB4JH.js +0 -33
@@ -11,6 +11,7 @@ var STALE_FLOOR_MS = 9e4;
11
11
  var STALE_INTERVAL_MULTIPLIER = 3;
12
12
  var STALE_CHECK_INTERVAL_MS = 5e3;
13
13
  var UNARMED_FALLBACK_MS = 12 * 6e4;
14
+ var CONNECT_TIMEOUT_MS = 3e4;
14
15
  var UNDICI_DEFAULT_BODY_TIMEOUT_MS = 3e5;
15
16
  function createAdaptiveWatchdog(options = {}) {
16
17
  const floorMs = options.floorMs ?? STALE_FLOOR_MS;
@@ -79,7 +80,18 @@ async function startEventStream(opts) {
79
80
  let stopped = false;
80
81
  const streamDispatcher = new Agent({
81
82
  bodyTimeout: UNDICI_DEFAULT_BODY_TIMEOUT_MS,
82
- headersTimeout: UNDICI_DEFAULT_BODY_TIMEOUT_MS
83
+ headersTimeout: UNDICI_DEFAULT_BODY_TIMEOUT_MS,
84
+ // FORCE HTTP/1.1 FOR THE SSE STREAM. undici 8.x defaults `allowH2: true`, so on a
85
+ // TLS connection ALPN negotiates HTTP/2 to the ALB. The ALB then RSTs the long-lived
86
+ // SSE stream with NGHTTP_INTERNAL_ERROR (H2 idle / per-stream lifetime limits), which
87
+ // undici surfaces as ERR_HTTP2_STREAM_ERROR — a mid-connection stream reset the
88
+ // reconnect loop can't recover from: it wedges (connected=false for 90s) → the
89
+ // liveness watchdog self-exits(1) → systemd restarts → a fresh session that may
90
+ // subscribe to a partial room-set. Observed fleet-wide (inst-clauron/henry/
91
+ // shakshuka-dev), 25 recorded flaps all with this signature. HTTP/1.1 keeps the SSE
92
+ // connection open indefinitely (nginx/ALB 3600s idle timeout + our heartbeats).
93
+ // Root-caused by Backend-Dev, 2026-08-04.
94
+ allowH2: false
83
95
  });
84
96
  const cursors = /* @__PURE__ */ new Map();
85
97
  let currentController = new AbortController();
@@ -165,10 +177,16 @@ async function startEventStream(opts) {
165
177
  }
166
178
  async function connectAndConsume(opts, cursors, controller, isReconnect, state, dispatcher, onCursor, onOpen) {
167
179
  const mapSince = cursors.size > 0 ? serializeCursorMap(cursors) : null;
168
- let res = await openStream(opts, controller, mapSince, dispatcher);
169
- if (res.status === 400 && mapSince !== null) {
170
- console.error("[event-stream] per-room ?since rejected (400) \u2014 falling back to bare cursor");
171
- res = await openStream(opts, controller, String(Math.min(...cursors.values())), dispatcher);
180
+ const connectTimer = setTimeout(() => controller.abort(), CONNECT_TIMEOUT_MS);
181
+ let res;
182
+ try {
183
+ res = await openStream(opts, controller, mapSince, dispatcher);
184
+ if (res.status === 400 && mapSince !== null) {
185
+ console.error("[event-stream] per-room ?since rejected (400) \u2014 falling back to bare cursor");
186
+ res = await openStream(opts, controller, String(Math.min(...cursors.values())), dispatcher);
187
+ }
188
+ } finally {
189
+ clearTimeout(connectTimer);
172
190
  }
173
191
  if (!res.ok) throw new Error(`SSE connect failed: ${res.status}`);
174
192
  if (!res.body) throw new Error("SSE response has no body");
@@ -182,10 +200,15 @@ async function connectAndConsume(opts, cursors, controller, isReconnect, state,
182
200
  await consumeSse(res.body, opts, controller, state, onCursor);
183
201
  return true;
184
202
  }
185
- async function openStream(opts, controller, sinceValue, dispatcher) {
203
+ function buildStreamUrl(brokerUrl, sinceValue, seatName) {
186
204
  const params = new URLSearchParams();
187
205
  if (sinceValue !== null) params.set("since", sinceValue);
188
- const url = `${opts.brokerUrl}/api/v2/tandems/stream${params.toString() ? "?" + params.toString() : ""}`;
206
+ if (seatName) params.set("seat_name", seatName);
207
+ const qs = params.toString();
208
+ return `${brokerUrl}/api/v2/tandems/stream/${qs ? "?" + qs : ""}`;
209
+ }
210
+ async function openStream(opts, controller, sinceValue, dispatcher) {
211
+ const url = buildStreamUrl(opts.brokerUrl, sinceValue, opts.seatName);
189
212
  const proof = await createDPoPProof(
190
213
  opts.gateway.getPrivateKey(),
191
214
  opts.gateway.getKid(),
@@ -250,12 +273,19 @@ async function consumeSse(body, opts, controller, state, onCursor) {
250
273
  }
251
274
  continue;
252
275
  }
253
- if (evt.event === "message" || evt.event === "state_change") {
276
+ {
254
277
  try {
255
278
  const raw = JSON.parse(evt.data);
256
279
  const parsed = normalizeBackendEvent(raw, evt.event);
257
- onCursor(parsed.tandem_id, parsed.cursor);
258
- if (parsed.wake === false) continue;
280
+ if (typeof parsed.cursor === "number") {
281
+ onCursor(parsed.tandem_id, parsed.cursor);
282
+ }
283
+ if (parsed.wake !== true) continue;
284
+ if (evt.event === "message_update" && parsed.edited_at === void 0) {
285
+ console.error(
286
+ "[event-stream] message_update missing edited_at \u2014 the edit will dedupe against the original and NOT wake (backend contract regression?)"
287
+ );
288
+ }
259
289
  if (opts.delivery) {
260
290
  try {
261
291
  await opts.delivery.deliver(parsed);
@@ -393,6 +423,8 @@ function normalizeBackendEvent(raw, sseEventType) {
393
423
  const wake = typeof rawWake === "boolean" ? rawWake : void 0;
394
424
  const rawWakeReason = obj["wake_reason"];
395
425
  const wakeReason = typeof rawWakeReason === "string" && rawWakeReason.trim() ? sanitizeDisplayname(rawWakeReason) : void 0;
426
+ const rawEditedAt = obj["edited_at"];
427
+ const editedAt = typeof rawEditedAt === "string" && rawEditedAt.trim() ? rawEditedAt : void 0;
396
428
  const senderPresent = typeof obj === "object" && obj !== null && "sender" in obj;
397
429
  const maybeFrom = obj["from"];
398
430
  if (!senderPresent && maybeFrom && typeof maybeFrom["principal"] === "string") {
@@ -406,7 +438,8 @@ function normalizeBackendEvent(raw, sseEventType) {
406
438
  displayname: resolveDisplayname(maybeFrom["displayname"], canonicalPrincipal)
407
439
  },
408
440
  ...wake !== void 0 ? { wake } : {},
409
- ...wakeReason !== void 0 ? { wake_reason: wakeReason } : {}
441
+ ...wakeReason !== void 0 ? { wake_reason: wakeReason } : {},
442
+ ...editedAt !== void 0 ? { edited_at: editedAt } : {}
410
443
  };
411
444
  }
412
445
  const sender = obj["sender"] ?? {};
@@ -441,18 +474,21 @@ function normalizeBackendEvent(raw, sseEventType) {
441
474
  ...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {},
442
475
  ...severity ? { severity } : {},
443
476
  ...wake !== void 0 ? { wake } : {},
444
- ...wakeReason !== void 0 ? { wake_reason: wakeReason } : {}
477
+ ...wakeReason !== void 0 ? { wake_reason: wakeReason } : {},
478
+ ...editedAt !== void 0 ? { edited_at: editedAt } : {}
445
479
  };
446
480
  }
447
481
 
448
482
  export {
449
483
  undiciFetch,
450
484
  UNARMED_FALLBACK_MS,
485
+ CONNECT_TIMEOUT_MS,
451
486
  UNDICI_DEFAULT_BODY_TIMEOUT_MS,
452
487
  createAdaptiveWatchdog,
453
488
  createBackoffController,
454
489
  LOG_HEARTBEAT_INTERVAL_MS,
455
490
  startEventStream,
491
+ buildStreamUrl,
456
492
  statusReason,
457
493
  serializeCursorMap,
458
494
  sanitizeDisplayname,
@@ -1,11 +1,11 @@
1
+ import {
2
+ wrapNpxLauncher
3
+ } from "./chunk-D6JKFJ6A.js";
1
4
  import {
2
5
  buildHookCommand,
3
6
  isKojeeHookCommand,
4
7
  npxHookCommand
5
- } from "./chunk-TMCNB4JH.js";
6
- import {
7
- wrapNpxLauncher
8
- } from "./chunk-D6JKFJ6A.js";
8
+ } from "./chunk-AXBJQJKA.js";
9
9
  import {
10
10
  kojeeHomeDir
11
11
  } from "./chunk-SQL56SEB.js";
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-U5HHHRXA.js";
15
15
  import {
16
16
  VERSION
17
- } from "./chunk-5DHIUN73.js";
17
+ } from "./chunk-6NPMVZD7.js";
18
18
 
19
19
  // src/wizard/codex-config.ts
20
20
  import fs from "fs";
@@ -32,6 +32,7 @@ function defaultCodexConfigPath() {
32
32
  function defaultCodexHooksPath() {
33
33
  return win32DriveQualified(path.join(kojeeHomeDir(), ".codex", "hooks.json"), ".codex", "hooks.json");
34
34
  }
35
+ var CODEX_TRUST_FIX_STEPS = 'to trust it, run `codex`, then type `/hooks` and press `t` on the entry \u2014 Codex only prints a "hooks need review" warning at startup, it does not auto-prompt';
35
36
  var CODEX_STOP_HOOK_COMMAND = npxHookCommand("codex-stop");
36
37
  function codexStopHookCommand() {
37
38
  return buildHookCommand("codex-stop");
@@ -345,6 +346,7 @@ function writeFile600(p, content) {
345
346
  export {
346
347
  defaultCodexConfigPath,
347
348
  defaultCodexHooksPath,
349
+ CODEX_TRUST_FIX_STEPS,
348
350
  buildCodexMcpServerTable,
349
351
  buildCodexStopHookBlock,
350
352
  buildCodexPromptSubmitHookBlock,
@@ -15,6 +15,7 @@ var MAX_BODY_CHARS = 200;
15
15
  var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
16
16
  var DEFAULT_STATUS_MAX_BYTES = 1 * 1024 * 1024;
17
17
  var DEFAULT_MIN_AGE_MS = 6e4;
18
+ var DEDUP_MAX_KEYS = 2e4;
18
19
  var STATUS_LINE_PREFIX = "kojee-status";
19
20
  function statusLogPath(eventLogPath) {
20
21
  const dir = path.dirname(eventLogPath);
@@ -49,6 +50,7 @@ function startEventLog(opts) {
49
50
  const dir = opts.dir ?? DEFAULT_DIR;
50
51
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
51
52
  const statusMaxBytes = opts.statusMaxBytes ?? DEFAULT_STATUS_MAX_BYTES;
53
+ const dedupMaxKeys = opts.dedupMaxKeys ?? DEDUP_MAX_KEYS;
52
54
  fs.mkdirSync(dir, { recursive: true });
53
55
  const filePath = resolveEventLogPath(opts);
54
56
  const statusPath = statusLogPath(filePath);
@@ -57,6 +59,14 @@ function startEventLog(opts) {
57
59
  fs.writeFileSync(statusPath, "", { mode: 384 });
58
60
  secureFile(statusPath);
59
61
  let writtenIds = /* @__PURE__ */ new Set();
62
+ function rememberDedupKey(key) {
63
+ writtenIds.add(key);
64
+ while (writtenIds.size > dedupMaxKeys) {
65
+ const oldest = writtenIds.values().next().value;
66
+ if (oldest === void 0) break;
67
+ writtenIds.delete(oldest);
68
+ }
69
+ }
60
70
  let bytesWritten = 0;
61
71
  let linesWritten = 0;
62
72
  let statusBytesWritten = 0;
@@ -90,7 +100,6 @@ function startEventLog(opts) {
90
100
  await fs.promises.truncate(filePath, 0);
91
101
  bytesWritten = 0;
92
102
  linesWritten = 0;
93
- writtenIds = /* @__PURE__ */ new Set();
94
103
  void enqueueStatus(() => writeStatusRaw(`status=rotated dropped=${dropped}`)).catch(() => {
95
104
  });
96
105
  } catch (err) {
@@ -105,15 +114,16 @@ function startEventLog(opts) {
105
114
  path: filePath,
106
115
  statusPath,
107
116
  async append(event) {
108
- if (event.id && writtenIds.has(event.id)) return;
109
- if (event.id) writtenIds.add(event.id);
117
+ const dedupKey = event.id ? event.edited_at ? `${event.id}\0${event.edited_at}` : event.id : void 0;
118
+ if (dedupKey && writtenIds.has(dedupKey)) return;
119
+ if (dedupKey) rememberDedupKey(dedupKey);
110
120
  const line = formatLine(event);
111
121
  await enqueueMsg(async () => {
112
122
  try {
113
123
  await writeMsgRaw(line);
114
124
  } catch (err) {
115
125
  console.error("[event-log] append failed:", err.message);
116
- if (event.id) writtenIds.delete(event.id);
126
+ if (dedupKey) writtenIds.delete(dedupKey);
117
127
  }
118
128
  });
119
129
  },
package/dist/cli.js CHANGED
@@ -4,12 +4,12 @@ import {
4
4
  } from "./chunk-EIAUW6KO.js";
5
5
  import {
6
6
  startProxy
7
- } from "./chunk-ZSHCHOPL.js";
8
- import "./chunk-TCWIXG5C.js";
7
+ } from "./chunk-676WQSWZ.js";
8
+ import "./chunk-CCVRXJJ2.js";
9
9
  import {
10
10
  pairedConfigPath
11
11
  } from "./chunk-5SZHXYPK.js";
12
- import "./chunk-247WFMCJ.js";
12
+ import "./chunk-AGZ7ZGLY.js";
13
13
  import "./chunk-Z5LPNJQ6.js";
14
14
  import "./chunk-I67C2HYA.js";
15
15
  import "./chunk-MIEI4PLB.js";
@@ -18,14 +18,13 @@ import {
18
18
  deriveKeystorePath
19
19
  } from "./chunk-6G6YYST6.js";
20
20
  import "./chunk-U5HHHRXA.js";
21
- import "./chunk-A4IOKD4Z.js";
21
+ import "./chunk-QYZP6QT2.js";
22
22
  import {
23
23
  VERSION
24
- } from "./chunk-5DHIUN73.js";
25
- import "./chunk-FJUAMJHU.js";
24
+ } from "./chunk-6NPMVZD7.js";
26
25
  import "./chunk-PPTKGWFF.js";
27
26
  import "./chunk-XJEBJIQE.js";
28
- import "./chunk-KNEJTD6G.js";
27
+ import "./chunk-SZLNNMIZ.js";
29
28
 
30
29
  // src/cli.ts
31
30
  import { Command } from "commander";
@@ -48,7 +47,7 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
48
47
  program.command("connect <code>").description(
49
48
  "Connect this runtime to Kojee with a per-agent pair code from the dashboard (claude-code | codex | openclaw | hermes). claude-code/codex/openclaw write a per-runtime paired slot (~/.kojee/agents/<runtime>/config.json) and point the runtime launcher at it via --paired-config; hermes writes the global ~/.kojee/config.json (its daemon + `kojee-mcp send` read that). Idempotent."
50
49
  ).requiredOption("--runtime <id>", "Target runtime: claude-code | codex | openclaw | hermes").option("--url <url>", "Broker base URL (default: the canonical staging broker)").action(async (code, opts) => {
51
- const { runConnect } = await import("./connect-handler-NZINEMG3.js");
50
+ const { runConnect } = await import("./connect-handler-GTFXNZUC.js");
52
51
  const result = await runConnect({
53
52
  code,
54
53
  runtime: opts.runtime,
@@ -58,45 +57,59 @@ program.command("connect <code>").description(
58
57
  console.error(result.output);
59
58
  process.exit(result.exitCode);
60
59
  });
60
+ program.command("reconnect").description(
61
+ "Re-run configure for an already-paired runtime, KEEPING the token + enrolled key (no re-pair). Re-writes configs/hooks to this install and refreshes the agent instructions."
62
+ ).option("--runtime <id>", "Target runtime (defaults to the recorded runtime from the last connect/init)").action(async (opts) => {
63
+ const { runReconnect } = await import("./connect-handler-GTFXNZUC.js");
64
+ const result = await runReconnect({ ...opts.runtime ? { runtime: opts.runtime } : {} });
65
+ for (const m of result.messages) console.error(m);
66
+ console.error(result.output);
67
+ process.exit(result.exitCode);
68
+ });
61
69
  program.command("hook").description("Run a kojee MCP hook script (called by Claude Code via ~/.claude/settings.json)").requiredOption(
62
70
  "--type <type>",
63
- "Hook type: stop, user-prompt-submit, codex-stop, or codex-prompt-submit"
71
+ "Hook type: stop, user-prompt-submit, session-start, codex-stop, or codex-prompt-submit"
64
72
  ).action(async (opts) => {
65
73
  if (opts.type === "stop") {
66
- const { runStopHook } = await import("./stop-hook-N6TX4YQT.js");
74
+ const { runStopHook } = await import("./stop-hook-BLPYRE7T.js");
67
75
  await runStopHook();
68
76
  process.exit(0);
69
77
  } else if (opts.type === "user-prompt-submit") {
70
- const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-DTXXFDSD.js");
78
+ const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-YQXGE3JU.js");
71
79
  await runUserPromptSubmitHook();
72
80
  process.exit(0);
81
+ } else if (opts.type === "session-start") {
82
+ const { runSessionStartHook } = await import("./session-start-hook-VL3KYDTT.js");
83
+ await runSessionStartHook();
84
+ process.exit(0);
73
85
  } else if (opts.type === "codex-stop") {
74
- const { runCodexStopHook } = await import("./codex-stop-hook-32W4BIOM.js");
86
+ const { runCodexStopHook } = await import("./codex-stop-hook-GTGRQNVV.js");
75
87
  await runCodexStopHook();
76
88
  process.exit(0);
77
89
  } else if (opts.type === "codex-prompt-submit") {
78
- const { runCodexPromptSubmitHook } = await import("./codex-prompt-submit-hook-J5PZEJSK.js");
90
+ const { runCodexPromptSubmitHook } = await import("./codex-prompt-submit-hook-UQJI6A54.js");
79
91
  await runCodexPromptSubmitHook();
80
92
  process.exit(0);
81
93
  } else {
82
94
  console.error(
83
- `Unknown hook type: ${opts.type}. Expected 'stop', 'user-prompt-submit', 'codex-stop', or 'codex-prompt-submit'.`
95
+ `Unknown hook type: ${opts.type}. Expected 'stop', 'user-prompt-submit', 'session-start', 'codex-stop', or 'codex-prompt-submit'.`
84
96
  );
85
97
  process.exit(1);
86
98
  }
87
99
  });
88
- program.command("install-hooks").description("Install kojee Stop + UserPromptSubmit hooks in ~/.claude/settings.json (idempotent)").option("--hooks-path <path>", "Override default ~/.claude/settings.json").option("--uninstall", "Remove kojee hook entries instead of installing them").action(async (opts) => {
89
- const { installHooks, uninstallHooks } = await import("./install-JQNDGAAQ.js");
100
+ program.command("install-hooks").description("Install kojee Stop + UserPromptSubmit + SessionStart hooks in ~/.claude/settings.json (idempotent)").option("--hooks-path <path>", "Override default ~/.claude/settings.json").option("--uninstall", "Remove kojee hook entries instead of installing them").action(async (opts) => {
101
+ const { installHooks, uninstallHooks } = await import("./install-GJVZEWLI.js");
90
102
  if (opts.uninstall) {
91
103
  const removed = uninstallHooks({ hooksPath: opts.hooksPath });
92
104
  console.error(removed ? "Removed kojee hook entries." : "No kojee hook entries found.");
93
105
  } else {
94
- const { stop, ups } = installHooks({ hooksPath: opts.hooksPath });
106
+ const { stop, ups, sessionStart } = installHooks({ hooksPath: opts.hooksPath });
95
107
  const label = (s) => s === "added" ? "\u2713 added" : s === "updated" ? "\u21BB updated" : "\u21BB already installed";
96
108
  console.error(
97
109
  "Installed kojee hooks in " + (opts.hooksPath ?? "~/.claude/settings.json") + `:
98
110
  - Stop: ${label(stop)}
99
111
  - UserPromptSubmit: ${label(ups)}
112
+ - SessionStart: ${label(sessionStart)}
100
113
  Restart Claude Code for hooks to take effect.`
101
114
  );
102
115
  }
@@ -104,7 +117,7 @@ Restart Claude Code for hooks to take effect.`
104
117
  program.command("send <tandem_id>").description(
105
118
  "Send a Tandem message using this machine's paired credentials (~/.kojee). Prints one JSON envelope to stdout: {ok, message_id, cursor, text} on success, {ok:false, error:<typed code>, message} on failure (exit 1)."
106
119
  ).requiredOption("--body <text>", "Message body (required)").option("--reply-to <message_id>", "Message id this send replies to").option("--kind <kind>", "Message kind: message | status (default: backend default)").action(async (tandemId, opts) => {
107
- const { runSendCli } = await import("./send-cli-45RLGYIC.js");
120
+ const { runSendCli } = await import("./send-cli-WTY7H3BR.js");
108
121
  const { exitCode, envelope } = await runSendCli({
109
122
  tandemId,
110
123
  body: opts.body,
@@ -114,17 +127,23 @@ program.command("send <tandem_id>").description(
114
127
  process.stdout.write(JSON.stringify(envelope) + "\n");
115
128
  process.exit(exitCode);
116
129
  });
117
- program.command("tail <path>").description("Stream a file's contents and follow appends (portable replacement for `tail -F`)").action(async (filePath) => {
118
- const { runTail } = await import("./tail-stream-G4WGTXDS.js");
130
+ program.command("tail <path>").description("Stream a file's appends and follow (portable `tail -F`; follows from EOF \u2014 new events only)").option(
131
+ "--wake-only",
132
+ "Suppress routine heartbeat warn/recover telemetry; surface only a sustained stream outage"
133
+ ).option(
134
+ "--replay",
135
+ "Replay the whole file from the start before following (default: follow from EOF, no replay)"
136
+ ).action(async (filePath, opts) => {
137
+ const { runTail } = await import("./tail-stream-PQXEY3TI.js");
119
138
  try {
120
- await runTail(filePath);
139
+ await runTail(filePath, { wakeOnly: opts.wakeOnly === true, replay: opts.replay === true });
121
140
  } catch (err) {
122
141
  console.error("[kojee-mcp tail] Error:", err.message);
123
142
  process.exit(1);
124
143
  }
125
144
  });
126
145
  program.command("doctor").description("Diagnose the kojee wake path (proxy, hook-server, SSE stream, event log, Monitor) and print the exact wake recipe").action(async () => {
127
- const { runDoctor } = await import("./doctor-WUU5BVPT.js");
146
+ const { runDoctor } = await import("./doctor-54S7JGOE.js");
128
147
  const code = await runDoctor();
129
148
  process.exit(code);
130
149
  });
@@ -143,7 +162,7 @@ function addInstallOptions(cmd, runtimeHelp) {
143
162
  function makeInstallAction(verb) {
144
163
  return async (opts) => {
145
164
  const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
146
- const { runSetup, resolvePairCode } = await import("./setup-handler-44ASXYMS.js");
165
+ const { runSetup, resolvePairCode } = await import("./setup-handler-GNAIWAYU.js");
147
166
  const pairCode = resolvePairCode(opts);
148
167
  const result = await runSetup({
149
168
  verb,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  readHookStdin
3
- } from "./chunk-LSUB6QMP.js";
3
+ } from "./chunk-NIIBVKLP.js";
4
4
  import {
5
5
  listPending
6
6
  } from "./chunk-IZN7IZPW.js";
7
7
  import {
8
8
  CODEX_WAKE_BELL
9
- } from "./chunk-FJUAMJHU.js";
9
+ } from "./chunk-6NPMVZD7.js";
10
10
 
11
11
  // src/hooks/codex-prompt-submit-hook.ts
12
12
  function buildCodexPromptSubmitOutput() {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readHookStdin
3
- } from "./chunk-LSUB6QMP.js";
3
+ } from "./chunk-NIIBVKLP.js";
4
4
  import {
5
5
  drainedRoomsPath,
6
6
  listPending,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-IZN7IZPW.js";
9
9
  import {
10
10
  CODEX_WAKE_BELL
11
- } from "./chunk-FJUAMJHU.js";
11
+ } from "./chunk-6NPMVZD7.js";
12
12
 
13
13
  // src/hooks/codex-stop-hook.ts
14
14
  import fs from "fs";
@@ -2,16 +2,19 @@ import {
2
2
  DEFAULT_BROKER_URL,
3
3
  reconcileConnectPairSlot,
4
4
  runWizard
5
- } from "./chunk-XPIW4N55.js";
6
- import "./chunk-6XWTUDWW.js";
5
+ } from "./chunk-N2YAZRAD.js";
6
+ import "./chunk-WDWVBPXA.js";
7
7
  import "./chunk-E6WMFMM2.js";
8
- import "./chunk-77HWBSRH.js";
9
- import "./chunk-TMCNB4JH.js";
10
- import "./chunk-D6JKFJ6A.js";
8
+ import "./chunk-GEGUWQYT.js";
11
9
  import {
12
10
  CONNECT_RUNTIMES
13
11
  } from "./chunk-EIH2LNF4.js";
14
- import "./chunk-RDLF4NQC.js";
12
+ import {
13
+ readRecordedRuntime
14
+ } from "./chunk-RDLF4NQC.js";
15
+ import "./chunk-77HWBSRH.js";
16
+ import "./chunk-D6JKFJ6A.js";
17
+ import "./chunk-AXBJQJKA.js";
15
18
  import "./chunk-SQL56SEB.js";
16
19
  import "./chunk-V5VZPYMZ.js";
17
20
  import {
@@ -29,10 +32,10 @@ import {
29
32
  deriveKeystorePath
30
33
  } from "./chunk-6G6YYST6.js";
31
34
  import "./chunk-U5HHHRXA.js";
32
- import "./chunk-5DHIUN73.js";
33
- import "./chunk-FJUAMJHU.js";
35
+ import "./chunk-6NPMVZD7.js";
34
36
 
35
37
  // src/wizard/connect-handler.ts
38
+ import fs from "fs";
36
39
  import os from "os";
37
40
  import path from "path";
38
41
  function absConfigPath(runtime) {
@@ -139,7 +142,63 @@ Start (or restart) the hermes daemon to pick up the connection.`,
139
142
  exitCode: 0
140
143
  };
141
144
  }
145
+ async function runReconnect(opts = {}) {
146
+ const messages = [];
147
+ const runtime = opts.runtime ?? readRecordedRuntime() ?? null;
148
+ if (!runtime) {
149
+ return {
150
+ messages,
151
+ output: `reconnect: no runtime recorded on this box \u2014 pass --runtime (${CONNECT_RUNTIMES.join(" | ")} | hermes), or run \`kojee-mcp connect <code> --runtime <id>\` first.`,
152
+ exitCode: 2
153
+ };
154
+ }
155
+ const isHermes = runtime === "hermes";
156
+ if (!isHermes && !CONNECT_RUNTIMES.includes(runtime)) {
157
+ return {
158
+ messages,
159
+ output: `reconnect supports: ${CONNECT_RUNTIMES.join(", ")}, hermes. Unknown --runtime "${runtime}".`,
160
+ exitCode: 2
161
+ };
162
+ }
163
+ const configPath = isHermes ? pairedConfigPath() : absConfigPath(runtime);
164
+ const paired = loadPairedConfig(configPath);
165
+ if (!paired?.token) {
166
+ return {
167
+ messages,
168
+ output: `reconnect: no paired config at ${configPath} \u2014 nothing to keep. Run \`kojee-mcp connect <code> --runtime ${runtime}\` to pair first.`,
169
+ exitCode: 2
170
+ };
171
+ }
172
+ const keystorePath = isHermes ? defaultPairedKeystorePath() : deriveKeystorePath(paired.token);
173
+ if (!fs.existsSync(keystorePath)) {
174
+ messages.push(
175
+ `\u26A0 enrolled keystore MISSING at ${keystorePath} \u2014 configs will be refreshed, but the proxy cannot authenticate with a new key on this token (backend rejects double-enroll). If sends fail, re-pair with a fresh code.`
176
+ );
177
+ }
178
+ const wizard = opts.runWizard ?? runWizard;
179
+ const wiz = await wizard({
180
+ runtime,
181
+ ...isHermes ? {} : { pairedConfigPath: configPath },
182
+ interactive: false
183
+ });
184
+ if (wiz.exitCode !== 0) {
185
+ return { messages, output: wiz.output, exitCode: wiz.exitCode };
186
+ }
187
+ const label = identityLabel(paired);
188
+ return {
189
+ messages,
190
+ output: [
191
+ `\u21BB reconfigured ${runtime} as ${label} \u2014 token KEPT (no re-pair, no key re-enrollment).`,
192
+ "Configs + hooks re-written to this install; agent instructions refreshed below.",
193
+ `Restart ${runtime} to pick up the changes.`,
194
+ "",
195
+ wiz.output
196
+ ].join("\n"),
197
+ exitCode: 0
198
+ };
199
+ }
142
200
  export {
143
201
  runConnect,
144
- runConnectHermes
202
+ runConnectHermes,
203
+ runReconnect
145
204
  };
@@ -1,3 +1,7 @@
1
+ import {
2
+ classifyHookCommand,
3
+ isKojeeHookCommand
4
+ } from "./chunk-AXBJQJKA.js";
1
5
  import "./chunk-XLKGPGZT.js";
2
6
  import {
3
7
  loadControlToken
@@ -5,7 +9,7 @@ import {
5
9
  import {
6
10
  monitorHeartbeatPath,
7
11
  statusLogPath
8
- } from "./chunk-TNZITJAB.js";
12
+ } from "./chunk-WVQU3QQ7.js";
9
13
  import {
10
14
  discoveryPathForKey,
11
15
  readSessionDiscoveryByKey,
@@ -18,14 +22,14 @@ import "./chunk-U5HHHRXA.js";
18
22
  import {
19
23
  buildMonitorSpawn,
20
24
  buildReplyRecipe
21
- } from "./chunk-FJUAMJHU.js";
25
+ } from "./chunk-6NPMVZD7.js";
22
26
  import {
23
27
  deriveDiscoveryKey,
24
28
  findClaudeAncestorPid
25
29
  } from "./chunk-XJEBJIQE.js";
26
30
  import {
27
31
  resolveSharedSessionId
28
- } from "./chunk-KNEJTD6G.js";
32
+ } from "./chunk-SZLNNMIZ.js";
29
33
 
30
34
  // src/doctor.ts
31
35
  import fs from "fs";
@@ -149,6 +153,53 @@ async function resolveDefaultPairedScope() {
149
153
  }
150
154
  return { scope: resolution.scope, present, ...runtime ? { runtime } : {} };
151
155
  }
156
+ function defaultReadHookCommands() {
157
+ try {
158
+ const p = path.join(os.homedir(), ".claude", "settings.json");
159
+ const raw = fs.readFileSync(p, "utf8");
160
+ const cfg = JSON.parse(raw);
161
+ const out = [];
162
+ for (const event of ["Stop", "UserPromptSubmit"]) {
163
+ for (const matcher of cfg.hooks?.[event] ?? []) {
164
+ for (const h of matcher.hooks ?? []) {
165
+ if (typeof h.command === "string" && isKojeeHookCommand(h.command)) out.push(h.command);
166
+ }
167
+ }
168
+ }
169
+ return out;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ var HOOK_REPIN_FIX = "fix: `npm i -g kojee-mcp` then re-run `kojee-mcp setup` (re-pins in place, no re-pair)";
175
+ function hookPinCheck(commands) {
176
+ const name = "hook pin (settings.json Stop/UserPromptSubmit)";
177
+ if (commands === null) {
178
+ return { name, ok: "warn", detail: `no ~/.claude/settings.json readable \u2014 run \`kojee-mcp setup\` to install the wake hooks` };
179
+ }
180
+ if (commands.length === 0) {
181
+ return { name, ok: "warn", detail: `no kojee hooks installed \u2014 run \`kojee-mcp setup\` (the stop-hook is the Monitor self-heal)` };
182
+ }
183
+ const states = commands.map((c) => classifyHookCommand(c));
184
+ if (states.includes("pinned-broken")) {
185
+ return { name, ok: false, detail: `pinned cli.js is GONE \u2014 hooks are failing with exit 127 RIGHT NOW; ${HOOK_REPIN_FIX}` };
186
+ }
187
+ if (states.includes("pinned-ephemeral")) {
188
+ return {
189
+ name,
190
+ ok: false,
191
+ detail: `pinned INTO the npx cache (~/.npm/_npx/\u2026) \u2014 npx cache GC will silently kill the hooks (exit 127 every turn); ${HOOK_REPIN_FIX}`
192
+ };
193
+ }
194
+ if (states.includes("npx")) {
195
+ return {
196
+ name,
197
+ ok: "warn",
198
+ detail: `unpinned (npx -y) \u2014 pays npx resolution every turn and can stall on a registry fetch; ${HOOK_REPIN_FIX}`
199
+ };
200
+ }
201
+ return { name, ok: true, detail: `pinned to a durable binary (${commands.length} hook${commands.length === 1 ? "" : "s"})` };
202
+ }
152
203
  async function collectDoctorReport(deps = {}) {
153
204
  const fetchFn = deps.fetchFn ?? fetch;
154
205
  const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
@@ -183,6 +234,8 @@ async function collectDoctorReport(deps = {}) {
183
234
  detail: pairedScope.present ? "present (legacy global slot)" : "MISSING \u2014 run `kojee-mcp connect <code> --runtime <id>` (or `kojee-mcp pair`)"
184
235
  });
185
236
  }
237
+ const hookCommands = (deps.readHookCommands ?? defaultReadHookCommands)();
238
+ checks.push(hookPinCheck(hookCommands));
186
239
  let logPath = null;
187
240
  if (!discovery) {
188
241
  checks.push({
@@ -354,17 +407,23 @@ function formatDoctorReport(report) {
354
407
  async function runDoctor() {
355
408
  const { readRecordedRuntime } = await import("./runtime-record-OXRLTOLC.js");
356
409
  if (readRecordedRuntime() === "codex") {
357
- const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-PJFWIABF.js");
410
+ const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-6H4OOS6O.js");
358
411
  const report2 = collectCodexDoctorReport();
359
412
  console.error(formatCodexDoctorReport(report2));
360
413
  return report2.verdict === "broken" ? 1 : 0;
361
414
  }
362
415
  if (readRecordedRuntime() === "openclaw") {
363
- const { collectOpenclawDoctorReport, formatOpenclawDoctorReport } = await import("./doctor-openclaw-LL6NSPLC.js");
416
+ const { collectOpenclawDoctorReport, formatOpenclawDoctorReport } = await import("./doctor-openclaw-B5KADVHD.js");
364
417
  const report2 = collectOpenclawDoctorReport();
365
418
  console.error(formatOpenclawDoctorReport(report2));
366
419
  return report2.verdict === "broken" ? 1 : 0;
367
420
  }
421
+ const { hermesArtifactsPresent, collectHermesDoctorReport, formatHermesDoctorReport } = await import("./doctor-hermes-KMHM27Y3.js");
422
+ if (hermesArtifactsPresent()) {
423
+ const hermesReport = collectHermesDoctorReport();
424
+ console.error(formatHermesDoctorReport(hermesReport));
425
+ return hermesReport.verdict === "broken" ? 1 : 0;
426
+ }
368
427
  const report = await collectDoctorReport();
369
428
  console.error(formatDoctorReport(report));
370
429
  return report.verdict === "broken" ? 1 : 0;