kojee-mcp 0.7.5-beta.1 → 0.7.5-staging.10

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 (33) hide show
  1. package/README.md +21 -3
  2. package/dist/{chunk-CUOJHP2S.js → chunk-5XOO52G5.js} +17 -7
  3. package/dist/{chunk-FJUAMJHU.js → chunk-BAPFHC36.js} +29 -3
  4. package/dist/chunk-COM6435C.js +65 -0
  5. package/dist/{chunk-4ATSAH7H.js → chunk-GLHA3DHU.js} +33 -5
  6. package/dist/{chunk-NE2F5CKS.js → chunk-LKGKDZSO.js} +9 -5
  7. package/dist/{chunk-A4IOKD4Z.js → chunk-MVGKYVZT.js} +2 -4
  8. package/dist/chunk-P64UR3SE.js +86 -0
  9. package/dist/{chunk-6XWTUDWW.js → chunk-RZ5H7JWH.js} +7 -5
  10. package/dist/{chunk-CPOK642I.js → chunk-V34FLRC3.js} +89 -26
  11. package/dist/{chunk-TCWIXG5C.js → chunk-VEZJSFTM.js} +1 -1
  12. package/dist/cli.js +26 -15
  13. package/dist/{codex-prompt-submit-hook-J5PZEJSK.js → codex-prompt-submit-hook-QHVVPAJB.js} +1 -1
  14. package/dist/{codex-stop-hook-32W4BIOM.js → codex-stop-hook-P7A7UY7L.js} +1 -1
  15. package/dist/{connect-handler-NCHC6WPU.js → connect-handler-UHPBAN4A.js} +67 -9
  16. package/dist/{doctor-RKLXZR2B.js → doctor-CFDHPSJ5.js} +57 -4
  17. package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-YOLALC4O.js} +84 -10
  18. package/dist/{doctor-hermes-WLDI4R6P.js → doctor-hermes-KMHM27Y3.js} +17 -0
  19. package/dist/{doctor-openclaw-LL6NSPLC.js → doctor-openclaw-B5KADVHD.js} +5 -1
  20. package/dist/{event-stream-ATKYXDBV.js → event-stream-FMZOFBXT.js} +3 -1
  21. package/dist/flap-log-D4KG7RK4.js +23 -0
  22. package/dist/index.js +4 -5
  23. package/dist/{install-JQNDGAAQ.js → install-NG366AOQ.js} +4 -4
  24. package/dist/lib.js +1 -1
  25. package/dist/{registry-DKWVVXJM.js → registry-4S4YRGJY.js} +23 -20
  26. package/dist/{server-DU3LFS32.js → server-F32TVL4J.js} +2 -3
  27. package/dist/{setup-handler-DA4465VH.js → setup-handler-WLW3G2Z5.js} +6 -7
  28. package/dist/{stop-hook-4MNOSDCB.js → stop-hook-BFXALOVO.js} +1 -1
  29. package/dist/stream-watchdog-CLRS2VMV.js +59 -0
  30. package/dist/{tail-stream-G4WGTXDS.js → tail-stream-A2LA5NF4.js} +106 -6
  31. package/package.json +1 -1
  32. package/dist/chunk-5DHIUN73.js +0 -25
  33. package/dist/chunk-TMCNB4JH.js +0 -33
package/README.md CHANGED
@@ -431,12 +431,14 @@ constant beside it):
431
431
  ```
432
432
  { type, id, tandem_id, cursor, time,
433
433
  from{ member_id, principal, agent_id?, session_id?, displayname },
434
- kind, content{ body, format? }, mentions?, reply_to?, severity? }
434
+ kind, content{ body, format? }, mentions?, reply_to?, severity?, wake?, wake_reason? }
435
435
  ```
436
436
 
437
437
  `from.session_id` and `severity` are present only when the wire carried them.
438
- There is no `sender` object — the body is fully normalized and carries
439
- `from.principal`.
438
+ `wake` is the backend's per-subscriber wake stamp (always `true` here — the sink
439
+ fires only for delivered wakes) and `wake_reason` names why it woke (e.g.
440
+ `direct`/`mention`), present when the backend stamped it. There is no `sender`
441
+ object — the body is fully normalized and carries `from.principal`.
440
442
  - **Verify** the `X-Kojee-Signature` header: it is the hex-encoded SHA-256 HMAC
441
443
  of the **raw request-body bytes**, keyed by `KOJEE_WEBHOOK_SECRET`. Recompute
442
444
  over the received bytes (before any re-serialization) and timing-safe compare;
@@ -574,6 +576,22 @@ npm run dev:stub
574
576
  # Stub listens on http://localhost:8765
575
577
  ```
576
578
 
579
+ ## Releases & staging versioning
580
+
581
+ Two channels, and one rule that keeps them clean:
582
+
583
+ - **`latest` (production)** — a **manual** release off `main` (`npm publish` → `latest`, or promote a verified staging build via `npm dist-tag add kojee-mcp@<ver> latest`).
584
+ - **`staging` (pre-release)** — **automatic** on every merge to the `staging` branch, via `.github/workflows/publish-staging.yml`, published under the `staging` dist-tag (never touches `latest`).
585
+
586
+ **How the staging version is computed** (`scripts/next-staging-version.mjs`):
587
+
588
+ 1. **base** = `package.json.version` with its **PATCH bumped once** — e.g. `0.7.4` → `0.7.5`.
589
+ 2. **counter** = `1 + highest existing <base>-staging.N on npm`. This is **per-base**, so it **RESETS to 1 automatically when the base progresses** (a brand-new base has no prior `-staging.N`).
590
+
591
+ So a run yields e.g. `0.7.5-staging.6`, and once `0.7.5` ships to `latest` and `package.json` moves to `0.7.5`, the next staging build resets to `0.7.6-staging.1`.
592
+
593
+ > ⚠️ **THE RULE: `package.json.version` must hold the LAST STABLE version — never a `-staging`/`-beta` pre-release.** The CI bumps the patch itself; if the file already carries the *next* version (or a `-beta` whose stable part is the next version), the base **double-bumps**. That is exactly how a `0.7.5-beta.1` base once published `0.7.6-staging.6` instead of the intended `0.7.5-staging.6`. Cutting a stable release is the *only* time `package.json.version` advances, and it advances to the version you just shipped to `latest`.
594
+
577
595
  ## How Approvals Work
578
596
 
579
597
  Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
@@ -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;
@@ -165,10 +166,16 @@ async function startEventStream(opts) {
165
166
  }
166
167
  async function connectAndConsume(opts, cursors, controller, isReconnect, state, dispatcher, onCursor, onOpen) {
167
168
  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);
169
+ const connectTimer = setTimeout(() => controller.abort(), CONNECT_TIMEOUT_MS);
170
+ let res;
171
+ try {
172
+ res = await openStream(opts, controller, mapSince, dispatcher);
173
+ if (res.status === 400 && mapSince !== null) {
174
+ console.error("[event-stream] per-room ?since rejected (400) \u2014 falling back to bare cursor");
175
+ res = await openStream(opts, controller, String(Math.min(...cursors.values())), dispatcher);
176
+ }
177
+ } finally {
178
+ clearTimeout(connectTimer);
172
179
  }
173
180
  if (!res.ok) throw new Error(`SSE connect failed: ${res.status}`);
174
181
  if (!res.body) throw new Error("SSE response has no body");
@@ -250,12 +257,14 @@ async function consumeSse(body, opts, controller, state, onCursor) {
250
257
  }
251
258
  continue;
252
259
  }
253
- if (evt.event === "message" || evt.event === "state_change") {
260
+ {
254
261
  try {
255
262
  const raw = JSON.parse(evt.data);
256
263
  const parsed = normalizeBackendEvent(raw, evt.event);
257
- onCursor(parsed.tandem_id, parsed.cursor);
258
- if (parsed.wake === false) continue;
264
+ if (typeof parsed.cursor === "number") {
265
+ onCursor(parsed.tandem_id, parsed.cursor);
266
+ }
267
+ if (parsed.wake !== true) continue;
259
268
  if (opts.delivery) {
260
269
  try {
261
270
  await opts.delivery.deliver(parsed);
@@ -448,6 +457,7 @@ function normalizeBackendEvent(raw, sseEventType) {
448
457
  export {
449
458
  undiciFetch,
450
459
  UNARMED_FALLBACK_MS,
460
+ CONNECT_TIMEOUT_MS,
451
461
  UNDICI_DEFAULT_BODY_TIMEOUT_MS,
452
462
  createAdaptiveWatchdog,
453
463
  createBackoffController,
@@ -1,7 +1,32 @@
1
+ // src/version.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ var FALLBACK_VERSION = "0.0.0-unknown";
6
+ function resolveVersion() {
7
+ try {
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const parsed = JSON.parse(
10
+ fs.readFileSync(path.join(here, "..", "package.json"), "utf8")
11
+ );
12
+ return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
13
+ } catch (err) {
14
+ process.stderr.write(
15
+ `kojee-mcp: could not resolve version from package.json, falling back to ${FALLBACK_VERSION}: ${String(err)}
16
+ `
17
+ );
18
+ return FALLBACK_VERSION;
19
+ }
20
+ }
21
+ var VERSION = resolveVersion();
22
+
1
23
  // src/tandem/recipe.ts
2
24
  var SEND_BODY_PARAM = "body";
25
+ function monitorPackageSpec() {
26
+ return VERSION && VERSION !== "0.0.0-unknown" ? `kojee-mcp@${VERSION}` : "kojee-mcp";
27
+ }
3
28
  function buildMonitorCommand(logPath) {
4
- return `npx kojee-mcp tail "${logPath}"`;
29
+ return `npx ${monitorPackageSpec()} tail --wake-only "${logPath}"`;
5
30
  }
6
31
  function buildMonitorSpawn(logPath) {
7
32
  return `Monitor(command=\`${buildMonitorCommand(logPath)}\`, persistent=true, description="kojee Tandem events")`;
@@ -21,17 +46,18 @@ function buildCondensedTandemRules() {
21
46
  function buildMonitorNudge(logPath) {
22
47
  return `[kojee] Tandem events are being logged but no Monitor is reading them \u2014 you may be missing wake notifications. Spawn the watcher once: ${buildMonitorSpawn(logPath)}. Then ${buildReplyRecipe()}.`;
23
48
  }
24
- var WEBHOOK_BODY_SHAPE = "{ type, id, tandem_id, cursor, time, from{ member_id, principal, agent_id?, session_id?, displayname }, kind, content{ body, format? }, mentions?, reply_to?, severity? }";
49
+ var WEBHOOK_BODY_SHAPE = "{ type, id, tandem_id, cursor, time, from{ member_id, principal, agent_id?, session_id?, displayname }, kind, content{ body, format? }, mentions?, reply_to?, severity?, wake?, wake_reason? }";
25
50
  function buildWebhookReceiverNote(sig) {
26
51
  const header = sig?.header ?? "X-Kojee-Signature";
27
52
  const prefix = sig?.prefix ?? "";
28
53
  const digestDesc = prefix ? `the literal prefix \`${prefix}\` followed by the hex-encoded SHA-256 HMAC` : "the hex-encoded SHA-256 HMAC";
29
- return "Webhook sink (optional, OFF unless KOJEE_WEBHOOK_URL + KOJEE_WEBHOOK_SECRET are set): the proxy POSTs every Tandem event as JSON to your endpoint. The body is the canonical normalized TandemEvent \u2014 " + WEBHOOK_BODY_SHAPE + ` \u2014 where from.session_id and severity are present only when the wire carried them (the body is fully normalized: it carries from.principal, never the raw backend sender envelope). To build a receiver: (1) verify the ${header} header \u2014 it is ${digestDesc} of the RAW request body bytes keyed by your KOJEE_WEBHOOK_SECRET; recompute over the received bytes and timing-safe compare, reject mismatches. (2) Dedupe by message_id \u2014 the body's \`id\`, also in the X-Kojee-Delivery header: delivery is AT-LEAST-ONCE (the proxy replays backlog from the cursor on restart), so the same event may arrive more than once \u2014 there is no exactly-once promise.`;
54
+ return "Webhook sink (optional, OFF unless KOJEE_WEBHOOK_URL + KOJEE_WEBHOOK_SECRET are set): the proxy POSTs every Tandem event as JSON to your endpoint. The body is the canonical normalized TandemEvent \u2014 " + WEBHOOK_BODY_SHAPE + ` \u2014 where from.session_id and severity are present only when the wire carried them (the body is fully normalized: it carries from.principal, never the raw backend sender envelope). \`wake\` is the backend's per-subscriber wake stamp (always true here \u2014 the sink fires only for delivered wakes) and \`wake_reason\` names WHY it woke (e.g. direct/mention), present when the backend stamped it. To build a receiver: (1) verify the ${header} header \u2014 it is ${digestDesc} of the RAW request body bytes keyed by your KOJEE_WEBHOOK_SECRET; recompute over the received bytes and timing-safe compare, reject mismatches. (2) Dedupe by message_id \u2014 the body's \`id\`, also in the X-Kojee-Delivery header: delivery is AT-LEAST-ONCE (the proxy replays backlog from the cursor on restart), so the same event may arrive more than once \u2014 there is no exactly-once promise.`;
30
55
  }
31
56
  var CODEX_LISTEN_CAP_MS = 8e3;
32
57
  var CODEX_WAKE_BELL = "[kojee] Tandem events may be pending. Call tandem_pending now and drain each room it lists (tandem_messages(id, since=cursor)), then reply in the room with a normal message (kind=message \u2014 never status). If it returns none for you, ignore this.";
33
58
 
34
59
  export {
60
+ VERSION,
35
61
  buildMonitorSpawn,
36
62
  buildReplyRecipe,
37
63
  buildCatchUpNote,
@@ -0,0 +1,65 @@
1
+ // src/hooks/hook-command.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ function npxHookCommand(type) {
6
+ return `npx -y kojee-mcp hook --type=${type}`;
7
+ }
8
+ var NPX_CACHE_RE = /[\\/]_npx[\\/]/;
9
+ function resolveCliEntry(moduleUrl = import.meta.url) {
10
+ try {
11
+ const here = fileURLToPath(moduleUrl);
12
+ const candidate = path.join(path.dirname(here), "cli.js");
13
+ if (!fs.existsSync(candidate)) return null;
14
+ if (NPX_CACHE_RE.test(candidate)) return null;
15
+ return candidate;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function stableExecPath(execPath = process.execPath) {
21
+ const m = /^(.*)\/Cellar\/node(?:@[^/]+)?\/[^/]+\/bin\/node$/.exec(execPath);
22
+ if (!m || !m[1]) return execPath;
23
+ const alias = path.join(m[1], "bin", "node");
24
+ try {
25
+ if (!fs.existsSync(alias)) return execPath;
26
+ const real = fs.realpathSync(alias);
27
+ return /[\\/]Cellar[\\/]node(@[^/]+)?[\\/]/.test(real) ? alias : execPath;
28
+ } catch {
29
+ return execPath;
30
+ }
31
+ }
32
+ function buildHookCommand(type, opts = {}) {
33
+ const cliEntry = opts.cliEntry !== void 0 ? opts.cliEntry : resolveCliEntry();
34
+ if (!cliEntry) return npxHookCommand(type);
35
+ const execPath = stableExecPath(opts.execPath ?? process.execPath);
36
+ return `"${execPath}" "${cliEntry}" hook --type=${type}`;
37
+ }
38
+ var PINNED_HOOK_RE = /^"[^"]+" "[^"]+" hook --type=(stop|user-prompt-submit|codex-stop|codex-prompt-submit)$/;
39
+ function isKojeeHookCommand(command) {
40
+ if (command.startsWith("npx -y kojee-mcp hook --type=")) return true;
41
+ return PINNED_HOOK_RE.test(command);
42
+ }
43
+ function classifyHookCommand(command, deps = {}) {
44
+ if (command.startsWith("npx -y kojee-mcp hook --type=")) return "npx";
45
+ if (!PINNED_HOOK_RE.test(command)) return "foreign";
46
+ const paths = [...command.matchAll(/"([^"]+)"/g)].map((m) => m[1] ?? "");
47
+ const cliPath = paths[1] ?? "";
48
+ if (NPX_CACHE_RE.test(cliPath)) return "pinned-ephemeral";
49
+ const exists = deps.exists ?? ((p) => {
50
+ try {
51
+ return fs.existsSync(p);
52
+ } catch {
53
+ return true;
54
+ }
55
+ });
56
+ if (cliPath && !exists(cliPath)) return "pinned-broken";
57
+ return "pinned-durable";
58
+ }
59
+
60
+ export {
61
+ npxHookCommand,
62
+ buildHookCommand,
63
+ isKojeeHookCommand,
64
+ classifyHookCommand
65
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  claudeCodeAdapter
3
- } from "./chunk-TCWIXG5C.js";
3
+ } from "./chunk-VEZJSFTM.js";
4
4
  import {
5
5
  GatewayClient,
6
6
  applyStableSessionId
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  createMcpServer,
13
13
  startMcpServer
14
- } from "./chunk-A4IOKD4Z.js";
14
+ } from "./chunk-MVGKYVZT.js";
15
15
  import {
16
16
  findClaudeAncestorPid
17
17
  } from "./chunk-XJEBJIQE.js";
@@ -295,9 +295,11 @@ async function startProxy(config) {
295
295
  }
296
296
  console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
297
297
  let server;
298
- const { selectDelivery } = await import("./registry-DKWVVXJM.js");
298
+ const isHeadlessDaemon = !!process.env["KOJEE_HEADLESS"] || !!process.env["KOJEE_WEBHOOK_EXPECTED"];
299
+ const { selectDelivery } = await import("./registry-4S4YRGJY.js");
299
300
  const delivery = selectDelivery(adapter.runtime, {
300
301
  supportsChannels: adapter.supportsChannels,
302
+ headless: isHeadlessDaemon,
301
303
  // Per-window delivered mirror for the tandem_pending tool (codex only —
302
304
  // other runtimes' deliveries ignore this even when passed).
303
305
  ...pendingState ? { onDelivered: (e) => pendingState.noteDelivered(e.tandem_id, e.cursor) } : {}
@@ -338,13 +340,12 @@ async function startProxy(config) {
338
340
  }
339
341
  server = createMcpServer(registry, adapter, tandemMembershipCount);
340
342
  }
341
- const isHeadlessDaemon = !!process.env["KOJEE_WEBHOOK_EXPECTED"];
342
343
  if (!isHeadlessDaemon) {
343
344
  process.stdin.on("end", () => shutdown("stdin end"));
344
345
  process.stdin.on("close", () => shutdown("stdin close"));
345
346
  } else {
346
347
  console.error(
347
- "[kojee-mcp] headless daemon (KOJEE_WEBHOOK_EXPECTED=1): stdin EOF ignored; lifecycle owned by the service manager (SIGTERM/SIGINT/SIGHUP)."
348
+ "[kojee-mcp] headless daemon (KOJEE_HEADLESS / KOJEE_WEBHOOK_EXPECTED): stdin EOF ignored; lifecycle owned by the service manager (SIGTERM/SIGINT/SIGHUP)."
348
349
  );
349
350
  }
350
351
  process.on("SIGHUP", () => shutdown("SIGHUP"));
@@ -363,6 +364,33 @@ async function startProxy(config) {
363
364
  "[kojee-mcp] no resolvable Claude ancestor (Windows fallback) \u2014 parent-liveness watchdog not armed; stdin close/end + SIGHUP/SIGINT/SIGTERM cover shutdown, and per-window wake is preserved via the session-id discovery key."
364
365
  );
365
366
  }
367
+ if (isHeadlessDaemon && activeStreamHandle) {
368
+ const { createStreamWatchdog } = await import("./stream-watchdog-CLRS2VMV.js");
369
+ const { recordFlap } = await import("./flap-log-D4KG7RK4.js");
370
+ const handleForWatchdog = activeStreamHandle;
371
+ const bootAt = Date.now();
372
+ const streamWatchdog = createStreamWatchdog({
373
+ getState: () => {
374
+ const s = handleForWatchdog.getState();
375
+ return { connected: s.connected, lastEventAt: s.lastEventAt };
376
+ },
377
+ onUnhealthy: ({ disconnectedForMs, reason }) => {
378
+ recordFlap({
379
+ ts: Date.now(),
380
+ instanceKey,
381
+ reason,
382
+ disconnectedForMs,
383
+ uptimeMs: Date.now() - bootAt
384
+ });
385
+ console.error(
386
+ `[kojee-mcp] stream-liveness watchdog: ${reason} \u2014 self-exiting(1); the service manager (Restart=always) will start a fresh process. Repeated exits are recorded and surfaced by \`kojee-mcp doctor\` (flap detection).`
387
+ );
388
+ process.exit(1);
389
+ }
390
+ });
391
+ streamWatchdog.start();
392
+ teardownSteps.push(() => streamWatchdog.stop());
393
+ }
366
394
  await startMcpServer(server);
367
395
  }
368
396
  async function enrollAndDiscover(config, keystorePath, isRetry = false) {
@@ -87,18 +87,22 @@ function createCachedOpenclawWakeResolver(opts = {}) {
87
87
  return resolution;
88
88
  };
89
89
  }
90
- async function postOpenclawWake(target, body, deps = {}) {
90
+ function openclawAgentUrl(wakeUrl) {
91
+ return wakeUrl.replace(/\/wake$/, "/agent");
92
+ }
93
+ async function postOpenclawAgent(target, body, deps = {}) {
91
94
  const doFetch = deps.fetch ?? fetch;
95
+ const url = openclawAgentUrl(target.url);
92
96
  const controller = new AbortController();
93
97
  const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? 4e3);
94
98
  try {
95
- const res = await doFetch(target.url, {
99
+ const res = await doFetch(url, {
96
100
  method: "POST",
97
101
  headers: { "content-type": "application/json", authorization: `Bearer ${target.token}` },
98
- body: JSON.stringify(body),
102
+ body: JSON.stringify({ name: "Kojee", wakeMode: "now", ...body }),
99
103
  signal: controller.signal
100
104
  });
101
- return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status, error: `wake POST ${res.status}` };
105
+ return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status, error: `agent POST ${res.status}` };
102
106
  } catch (err) {
103
107
  return { ok: false, error: err.message };
104
108
  } finally {
@@ -109,5 +113,5 @@ async function postOpenclawWake(target, body, deps = {}) {
109
113
  export {
110
114
  resolveOpenclawWakeTarget,
111
115
  createCachedOpenclawWakeResolver,
112
- postOpenclawWake
116
+ postOpenclawAgent
113
117
  };
@@ -1,12 +1,10 @@
1
1
  import {
2
- VERSION
3
- } from "./chunk-5DHIUN73.js";
4
- import {
2
+ VERSION,
5
3
  buildCatchUpNote,
6
4
  buildCondensedTandemRules,
7
5
  buildMonitorSpawn,
8
6
  buildReplyRecipe
9
- } from "./chunk-FJUAMJHU.js";
7
+ } from "./chunk-BAPFHC36.js";
10
8
  import {
11
9
  translateToolCallResult
12
10
  } from "./chunk-PPTKGWFF.js";
@@ -0,0 +1,86 @@
1
+ import {
2
+ kojeeHomeDir
3
+ } from "./chunk-SQL56SEB.js";
4
+
5
+ // src/runtime/flap-log.ts
6
+ import fs from "fs";
7
+ import path from "path";
8
+ var MAX_FLAP_ENTRIES = 25;
9
+ function defaultFlapLogPath() {
10
+ return path.join(kojeeHomeDir(), ".kojee", "daemon-flaps.json");
11
+ }
12
+ function realRead(p) {
13
+ try {
14
+ return fs.readFileSync(p, "utf8");
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function realWrite(p, content) {
20
+ fs.mkdirSync(path.dirname(p), { recursive: true });
21
+ fs.writeFileSync(p, content, "utf8");
22
+ }
23
+ function readFlaps(deps = {}) {
24
+ const p = deps.filePath ?? defaultFlapLogPath();
25
+ const raw = (deps.readFile ?? realRead)(p);
26
+ if (raw === null) return [];
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ if (!Array.isArray(parsed)) return [];
30
+ return parsed.filter(
31
+ (e) => !!e && typeof e.ts === "number" && typeof e.instanceKey === "string"
32
+ );
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+ function recordFlap(entry, deps = {}) {
38
+ try {
39
+ const p = deps.filePath ?? defaultFlapLogPath();
40
+ const existing = readFlaps(deps);
41
+ const next = [...existing, entry].slice(-MAX_FLAP_ENTRIES);
42
+ (deps.writeFile ?? realWrite)(p, JSON.stringify(next, null, 2));
43
+ } catch {
44
+ }
45
+ }
46
+ function flapRateExceeds(flaps, opts) {
47
+ const cutoff = opts.now - opts.windowMs;
48
+ return flaps.filter((f) => f.ts >= cutoff).length >= opts.count;
49
+ }
50
+ function recentFlapCount(flaps, windowMs, now) {
51
+ const cutoff = now - windowMs;
52
+ return flaps.filter((f) => f.ts >= cutoff).length;
53
+ }
54
+ var FLAP_WINDOW_MS = 15 * 6e4;
55
+ var FLAP_WARN_COUNT = 3;
56
+ function buildFlapDoctorCheck(deps = {}) {
57
+ const name = deps.name ?? "daemon flapping";
58
+ const history = (deps.readFlaps ?? readFlaps)();
59
+ const now = deps.now?.() ?? Date.now();
60
+ const recent = recentFlapCount(history, FLAP_WINDOW_MS, now);
61
+ if (flapRateExceeds(history, { count: FLAP_WARN_COUNT, windowMs: FLAP_WINDOW_MS, now })) {
62
+ const last = history[history.length - 1];
63
+ return {
64
+ name,
65
+ ok: "warn",
66
+ detail: `the stream-liveness watchdog self-exited ${recent} times in the last 15min \u2014 the SSE reconnect loop is repeatedly wedging. This is a CHRONIC failure being masked by auto-restart; root-cause it (last reason: "${last?.reason ?? "unknown"}").`
67
+ };
68
+ }
69
+ return {
70
+ name,
71
+ ok: true,
72
+ detail: recent === 0 ? "no stream-liveness self-exits recently (healthy)" : `${recent} self-exit(s) in the last 15min \u2014 below the ${FLAP_WARN_COUNT}-in-15min chronic threshold`
73
+ };
74
+ }
75
+
76
+ export {
77
+ MAX_FLAP_ENTRIES,
78
+ defaultFlapLogPath,
79
+ readFlaps,
80
+ recordFlap,
81
+ flapRateExceeds,
82
+ recentFlapCount,
83
+ FLAP_WINDOW_MS,
84
+ FLAP_WARN_COUNT,
85
+ buildFlapDoctorCheck
86
+ };
@@ -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-COM6435C.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-BAPFHC36.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,