kojee-mcp 0.7.5-staging.5 → 0.7.5-staging.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.
package/README.md CHANGED
@@ -574,6 +574,22 @@ npm run dev:stub
574
574
  # Stub listens on http://localhost:8765
575
575
  ```
576
576
 
577
+ ## Releases & staging versioning
578
+
579
+ Two channels, and one rule that keeps them clean:
580
+
581
+ - **`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`).
582
+ - **`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`).
583
+
584
+ **How the staging version is computed** (`scripts/next-staging-version.mjs`):
585
+
586
+ 1. **base** = `package.json.version` with its **PATCH bumped once** — e.g. `0.7.4` → `0.7.5`.
587
+ 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`).
588
+
589
+ 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`.
590
+
591
+ > ⚠️ **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`.
592
+
577
593
  ## How Approvals Work
578
594
 
579
595
  Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
@@ -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")`;
@@ -32,6 +57,7 @@ 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,
@@ -5,7 +5,7 @@ import {
5
5
  isPlaceholderWebhookUrl,
6
6
  removeCodexConfig,
7
7
  writeCodexConfig
8
- } from "./chunk-6XWTUDWW.js";
8
+ } from "./chunk-T6QANUS2.js";
9
9
  import {
10
10
  removeOpenclawMcpServer,
11
11
  writeOpenclawMcpConfig
@@ -48,14 +48,12 @@ import {
48
48
  secureFile,
49
49
  unlinkSyncRetry
50
50
  } from "./chunk-U5HHHRXA.js";
51
- import {
52
- VERSION
53
- } from "./chunk-5DHIUN73.js";
54
51
  import {
55
52
  CODEX_LISTEN_CAP_MS,
53
+ VERSION,
56
54
  buildCondensedTandemRules,
57
55
  buildWebhookReceiverNote
58
- } from "./chunk-FJUAMJHU.js";
56
+ } from "./chunk-O4JQY7IF.js";
59
57
 
60
58
  // src/wizard/pair-slot.ts
61
59
  import fs from "fs";
@@ -425,7 +423,8 @@ function installBundledPayload(opts) {
425
423
  import fs5 from "fs";
426
424
  import path5 from "path";
427
425
  function daemonSourceExecCommand(spec) {
428
- return `set -a; . ${spec.envFile}; set +a; exec ${spec.binPath}`;
426
+ const target = spec.nodePath ? `${spec.nodePath} ${spec.binPath}` : spec.binPath;
427
+ return `set -a; . ${spec.envFile}; set +a; exec ${target}`;
429
428
  }
430
429
  function xmlEscape(value) {
431
430
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -571,6 +570,7 @@ function installHermes(inp) {
571
570
  const svc = writeService(inp.platform, {
572
571
  serviceName: "kojee-hermes",
573
572
  binPath: inp.binPath,
573
+ ...inp.nodePath ? { nodePath: inp.nodePath } : {},
574
574
  envFile: daemonEnv,
575
575
  runtime: "hermes",
576
576
  homeDir: inp.homeDir
@@ -589,15 +589,18 @@ function installHermes(inp) {
589
589
  // every daemon runtime) on the same URL-present path before this output, so
590
590
  // emitting it here too would print the contract twice.
591
591
  "",
592
- "Next:",
593
- ` - start the daemon service: ${svc.activateCmd}`,
594
- " - reload the Hermes gateway to load the channel plugin AND the kojee MCP server:",
595
- " systemctl --user restart hermes-gateway (or: hermes gateway restart, or /reload-mcp)",
596
- " - verify the MCP registered: hermes mcp test kojee (tools appear to the agent as mcp_kojee_*)",
597
- " - the agent can now join from its own tools: mcp_kojee_tandem_join <tandem_id>",
598
- " - verify the daemon/channel: kojee-mcp doctor",
599
- " - if tools don't register: ensure Hermes was installed with the MCP extra",
600
- ' (cd ~/.hermes/hermes-agent && uv pip install -e ".[mcp]") and re-check connect_timeout.'
592
+ "Next \u2014 ACTIVATE (owner steps; the receiver never binds until you do these, in order):",
593
+ " 1. enable the Tandem receiver plugin (WITHOUT this, the gateway restart loads nothing):",
594
+ " hermes plugins enable kojee-tandem",
595
+ " 2. restart the gateway to bind the receiver (:8645) AND load the kojee MCP tools:",
596
+ " hermes gateway restart (or: systemctl --user restart hermes-gateway)",
597
+ ` 3. start the pusher daemon: ${svc.activateCmd}`,
598
+ " 4. verify end-to-end (should read HEALTHY): kojee-mcp doctor --runtime hermes",
599
+ "",
600
+ " Notes:",
601
+ " - the agent joins rooms from its own tools: mcp_kojee_tandem_join <tandem_id>",
602
+ " - if tools don't register: ensure Hermes has the MCP extra",
603
+ ' (cd ~/.hermes/hermes-agent && uv pip install -e ".[mcp]") and re-check connect_timeout.'
601
604
  ];
602
605
  return {
603
606
  runtime: "hermes",
@@ -975,7 +978,18 @@ function distDir() {
975
978
  }
976
979
  function resolveBinPath() {
977
980
  const entry = process.argv[1];
978
- return entry && entry.length > 0 ? entry : "kojee-mcp";
981
+ if (!entry || entry.length === 0) return "kojee-mcp";
982
+ try {
983
+ return fs6.realpathSync(entry);
984
+ } catch {
985
+ return entry;
986
+ }
987
+ }
988
+ function resolveDaemonExec() {
989
+ const nodePath = process.execPath;
990
+ const binPath = resolveBinPath();
991
+ const ephemeral = /[\\/]_npx[\\/]/.test(binPath);
992
+ return { nodePath, binPath, ephemeral };
979
993
  }
980
994
  function configureHermes(opts) {
981
995
  const runtime = "hermes";
@@ -1018,11 +1032,13 @@ function configureHermes(opts) {
1018
1032
  }
1019
1033
  const env = opts.env ?? process.env;
1020
1034
  const suppliedSecret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
1035
+ const daemonExec = resolveDaemonExec();
1021
1036
  const install = installHermes({
1022
1037
  homeDir: home,
1023
1038
  payloadBaseDir: base,
1024
1039
  platform: process.platform,
1025
- binPath: resolveBinPath(),
1040
+ binPath: daemonExec.binPath,
1041
+ nodePath: daemonExec.nodePath,
1026
1042
  webhookUrl: wh.url,
1027
1043
  ...suppliedSecret ? { webhookSecret: suppliedSecret } : {},
1028
1044
  ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
@@ -1035,6 +1051,15 @@ function configureHermes(opts) {
1035
1051
  return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
1036
1052
  }
1037
1053
  recordRuntime(runtime);
1054
+ if (daemonExec.ephemeral) {
1055
+ lines.push(
1056
+ "\u26A0 DURABILITY: this ran from an ephemeral npx cache, so the daemon points at a",
1057
+ " path npx will eventually delete (the daemon would then fail to start).",
1058
+ " For a durable daemon, install globally and re-run init:",
1059
+ " npm i -g kojee-mcp && kojee-mcp init --runtime hermes",
1060
+ ""
1061
+ );
1062
+ }
1038
1063
  const envFile = path7.join(home, ".kojee", "hermes.env");
1039
1064
  lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
1040
1065
  lines.push("");
@@ -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-O4JQY7IF.js";
10
8
  import {
11
9
  translateToolCallResult
12
10
  } from "./chunk-PPTKGWFF.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-O4JQY7IF.js";
18
18
 
19
19
  // src/wizard/codex-config.ts
20
20
  import fs from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  claudeCodeAdapter
3
- } from "./chunk-TCWIXG5C.js";
3
+ } from "./chunk-XYDNRVOU.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-S35ZIVOL.js";
15
15
  import {
16
16
  findClaudeAncestorPid
17
17
  } from "./chunk-XJEBJIQE.js";
@@ -295,7 +295,7 @@ 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 { selectDelivery } = await import("./registry-ACVPVDCS.js");
299
299
  const delivery = selectDelivery(adapter.runtime, {
300
300
  supportsChannels: adapter.supportsChannels,
301
301
  // Per-window delivered mirror for the tandem_pending tool (codex only —
@@ -338,8 +338,15 @@ async function startProxy(config) {
338
338
  }
339
339
  server = createMcpServer(registry, adapter, tandemMembershipCount);
340
340
  }
341
- process.stdin.on("end", () => shutdown("stdin end"));
342
- process.stdin.on("close", () => shutdown("stdin close"));
341
+ const isHeadlessDaemon = !!process.env["KOJEE_WEBHOOK_EXPECTED"];
342
+ if (!isHeadlessDaemon) {
343
+ process.stdin.on("end", () => shutdown("stdin end"));
344
+ process.stdin.on("close", () => shutdown("stdin close"));
345
+ } else {
346
+ console.error(
347
+ "[kojee-mcp] headless daemon (KOJEE_WEBHOOK_EXPECTED=1): stdin EOF ignored; lifecycle owned by the service manager (SIGTERM/SIGINT/SIGHUP)."
348
+ );
349
+ }
343
350
  process.on("SIGHUP", () => shutdown("SIGHUP"));
344
351
  process.on("SIGINT", () => shutdown("SIGINT"));
345
352
  process.on("SIGTERM", () => shutdown("SIGTERM"));
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildReplyRecipe
3
- } from "./chunk-FJUAMJHU.js";
3
+ } from "./chunk-O4JQY7IF.js";
4
4
 
5
5
  // src/adapters/claude-code.ts
6
6
  function computeSeverity(event) {
package/dist/cli.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  } from "./chunk-EIAUW6KO.js";
5
5
  import {
6
6
  startProxy
7
- } from "./chunk-526UIEAL.js";
8
- import "./chunk-TCWIXG5C.js";
7
+ } from "./chunk-TUDC4GY5.js";
8
+ import "./chunk-XYDNRVOU.js";
9
9
  import {
10
10
  pairedConfigPath
11
11
  } from "./chunk-5SZHXYPK.js";
@@ -18,11 +18,10 @@ import {
18
18
  deriveKeystorePath
19
19
  } from "./chunk-6G6YYST6.js";
20
20
  import "./chunk-U5HHHRXA.js";
21
- import "./chunk-A4IOKD4Z.js";
21
+ import "./chunk-S35ZIVOL.js";
22
22
  import {
23
23
  VERSION
24
- } from "./chunk-5DHIUN73.js";
25
- import "./chunk-FJUAMJHU.js";
24
+ } from "./chunk-O4JQY7IF.js";
26
25
  import "./chunk-PPTKGWFF.js";
27
26
  import "./chunk-XJEBJIQE.js";
28
27
  import "./chunk-SRLD2UY7.js";
@@ -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-YYBLFRDP.js");
50
+ const { runConnect } = await import("./connect-handler-W7EAIU4R.js");
52
51
  const result = await runConnect({
53
52
  code,
54
53
  runtime: opts.runtime,
@@ -63,7 +62,7 @@ program.command("hook").description("Run a kojee MCP hook script (called by Clau
63
62
  "Hook type: stop, user-prompt-submit, codex-stop, or codex-prompt-submit"
64
63
  ).action(async (opts) => {
65
64
  if (opts.type === "stop") {
66
- const { runStopHook } = await import("./stop-hook-4MNOSDCB.js");
65
+ const { runStopHook } = await import("./stop-hook-Y34L3RCR.js");
67
66
  await runStopHook();
68
67
  process.exit(0);
69
68
  } else if (opts.type === "user-prompt-submit") {
@@ -71,11 +70,11 @@ program.command("hook").description("Run a kojee MCP hook script (called by Clau
71
70
  await runUserPromptSubmitHook();
72
71
  process.exit(0);
73
72
  } else if (opts.type === "codex-stop") {
74
- const { runCodexStopHook } = await import("./codex-stop-hook-32W4BIOM.js");
73
+ const { runCodexStopHook } = await import("./codex-stop-hook-M3LEWCNP.js");
75
74
  await runCodexStopHook();
76
75
  process.exit(0);
77
76
  } else if (opts.type === "codex-prompt-submit") {
78
- const { runCodexPromptSubmitHook } = await import("./codex-prompt-submit-hook-J5PZEJSK.js");
77
+ const { runCodexPromptSubmitHook } = await import("./codex-prompt-submit-hook-ACDZSHHC.js");
79
78
  await runCodexPromptSubmitHook();
80
79
  process.exit(0);
81
80
  } else {
@@ -114,17 +113,20 @@ program.command("send <tandem_id>").description(
114
113
  process.stdout.write(JSON.stringify(envelope) + "\n");
115
114
  process.exit(exitCode);
116
115
  });
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");
116
+ program.command("tail <path>").description("Stream a file's contents and follow appends (portable replacement for `tail -F`)").option(
117
+ "--wake-only",
118
+ "Suppress routine heartbeat warn/recover telemetry; surface only a sustained stream outage"
119
+ ).action(async (filePath, opts) => {
120
+ const { runTail } = await import("./tail-stream-HG4BKJOG.js");
119
121
  try {
120
- await runTail(filePath);
122
+ await runTail(filePath, { wakeOnly: opts.wakeOnly === true });
121
123
  } catch (err) {
122
124
  console.error("[kojee-mcp tail] Error:", err.message);
123
125
  process.exit(1);
124
126
  }
125
127
  });
126
128
  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-NHUPLM6O.js");
129
+ const { runDoctor } = await import("./doctor-O7MLJEMO.js");
128
130
  const code = await runDoctor();
129
131
  process.exit(code);
130
132
  });
@@ -143,7 +145,7 @@ function addInstallOptions(cmd, runtimeHelp) {
143
145
  function makeInstallAction(verb) {
144
146
  return async (opts) => {
145
147
  const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
146
- const { runSetup, resolvePairCode } = await import("./setup-handler-6BDICX5F.js");
148
+ const { runSetup, resolvePairCode } = await import("./setup-handler-6T7Q6DYQ.js");
147
149
  const pairCode = resolvePairCode(opts);
148
150
  const result = await runSetup({
149
151
  verb,
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-IZN7IZPW.js";
7
7
  import {
8
8
  CODEX_WAKE_BELL
9
- } from "./chunk-FJUAMJHU.js";
9
+ } from "./chunk-O4JQY7IF.js";
10
10
 
11
11
  // src/hooks/codex-prompt-submit-hook.ts
12
12
  function buildCodexPromptSubmitOutput() {
@@ -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-O4JQY7IF.js";
12
12
 
13
13
  // src/hooks/codex-stop-hook.ts
14
14
  import fs from "fs";
@@ -2,8 +2,8 @@ import {
2
2
  DEFAULT_BROKER_URL,
3
3
  reconcileConnectPairSlot,
4
4
  runWizard
5
- } from "./chunk-4E6WMRTM.js";
6
- import "./chunk-6XWTUDWW.js";
5
+ } from "./chunk-RTYRJAJO.js";
6
+ import "./chunk-T6QANUS2.js";
7
7
  import "./chunk-E6WMFMM2.js";
8
8
  import "./chunk-GEGUWQYT.js";
9
9
  import "./chunk-77HWBSRH.js";
@@ -30,8 +30,7 @@ import {
30
30
  deriveKeystorePath
31
31
  } from "./chunk-6G6YYST6.js";
32
32
  import "./chunk-U5HHHRXA.js";
33
- import "./chunk-5DHIUN73.js";
34
- import "./chunk-FJUAMJHU.js";
33
+ import "./chunk-O4JQY7IF.js";
35
34
 
36
35
  // src/wizard/connect-handler.ts
37
36
  import os from "os";
@@ -18,7 +18,7 @@ import "./chunk-U5HHHRXA.js";
18
18
  import {
19
19
  buildMonitorSpawn,
20
20
  buildReplyRecipe
21
- } from "./chunk-FJUAMJHU.js";
21
+ } from "./chunk-O4JQY7IF.js";
22
22
  import {
23
23
  deriveDiscoveryKey,
24
24
  findClaudeAncestorPid
@@ -354,7 +354,7 @@ function formatDoctorReport(report) {
354
354
  async function runDoctor() {
355
355
  const { readRecordedRuntime } = await import("./runtime-record-OXRLTOLC.js");
356
356
  if (readRecordedRuntime() === "codex") {
357
- const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-PJFWIABF.js");
357
+ const { collectCodexDoctorReport, formatCodexDoctorReport } = await import("./doctor-codex-D4BOWIAA.js");
358
358
  const report2 = collectCodexDoctorReport();
359
359
  console.error(formatCodexDoctorReport(report2));
360
360
  return report2.verdict === "broken" ? 1 : 0;
@@ -365,7 +365,7 @@ async function runDoctor() {
365
365
  console.error(formatOpenclawDoctorReport(report2));
366
366
  return report2.verdict === "broken" ? 1 : 0;
367
367
  }
368
- const { hermesArtifactsPresent, collectHermesDoctorReport, formatHermesDoctorReport } = await import("./doctor-hermes-7N7O422Y.js");
368
+ const { hermesArtifactsPresent, collectHermesDoctorReport, formatHermesDoctorReport } = await import("./doctor-hermes-WLDI4R6P.js");
369
369
  if (hermesArtifactsPresent()) {
370
370
  const hermesReport = collectHermesDoctorReport();
371
371
  console.error(formatHermesDoctorReport(hermesReport));
@@ -2,7 +2,7 @@ import {
2
2
  defaultCodexConfigPath,
3
3
  defaultCodexHooksPath,
4
4
  isPlaceholderWebhookUrl
5
- } from "./chunk-6XWTUDWW.js";
5
+ } from "./chunk-T6QANUS2.js";
6
6
  import "./chunk-TMCNB4JH.js";
7
7
  import "./chunk-D6JKFJ6A.js";
8
8
  import {
@@ -16,11 +16,9 @@ import {
16
16
  } from "./chunk-V5VZPYMZ.js";
17
17
  import "./chunk-U5HHHRXA.js";
18
18
  import {
19
+ CODEX_LISTEN_CAP_MS,
19
20
  VERSION
20
- } from "./chunk-5DHIUN73.js";
21
- import {
22
- CODEX_LISTEN_CAP_MS
23
- } from "./chunk-FJUAMJHU.js";
21
+ } from "./chunk-O4JQY7IF.js";
24
22
 
25
23
  // src/doctor-codex.ts
26
24
  import fs from "fs";
@@ -103,6 +103,27 @@ function readChildEnv(configText) {
103
103
  function sha256(value) {
104
104
  return crypto.createHash("sha256").update(value, "utf8").digest("hex");
105
105
  }
106
+ function readPluginEnabled(configText) {
107
+ if (configText === null) return null;
108
+ let doc;
109
+ try {
110
+ doc = parse(configText);
111
+ } catch {
112
+ return null;
113
+ }
114
+ if (!doc || typeof doc !== "object") return null;
115
+ const plugins = doc["plugins"];
116
+ if (!plugins || typeof plugins !== "object") return false;
117
+ const enabled = plugins["enabled"];
118
+ if (!Array.isArray(enabled)) return false;
119
+ return enabled.includes("kojee-tandem");
120
+ }
121
+ function parseExecTargets(unitText) {
122
+ if (unitText === null) return null;
123
+ const m = unitText.match(/exec\s+([^'"\n<]+)/);
124
+ if (!m) return null;
125
+ return m[1].trim().split(/\s+/).filter((tok) => tok.startsWith("/"));
126
+ }
106
127
  function defaultReadText(filePath) {
107
128
  try {
108
129
  return fs.readFileSync(filePath, "utf8");
@@ -186,6 +207,7 @@ function collectHermesDoctorReport(deps = {}) {
186
207
  const homeDir = deps.homeDir ?? os.homedir();
187
208
  const platform = deps.platform ?? process.platform;
188
209
  const readText = deps.readText ?? defaultReadText;
210
+ const exists = deps.exists ?? ((p) => fs.existsSync(p));
189
211
  const probeService = deps.probeService ?? defaultProbeService(platform);
190
212
  const probeReceiver = deps.probeReceiver ?? defaultProbeReceiver(platform);
191
213
  const readDaemonEnv = deps.readDaemonEnv ?? defaultReadDaemonEnv(platform);
@@ -200,6 +222,7 @@ function collectHermesDoctorReport(deps = {}) {
200
222
  const daemonEnv = daemonEnvText !== null ? parseEnvFile(daemonEnvText) : null;
201
223
  const adapterEnv = adapterEnvText !== null ? parseEnvFile(adapterEnvText) : null;
202
224
  const childEnv = readChildEnv(configText);
225
+ const pluginEnabled = readPluginEnabled(configText);
203
226
  const checks = [];
204
227
  const collector = new FindingCollector();
205
228
  if (daemonEnv === null) {
@@ -293,6 +316,46 @@ function collectHermesDoctorReport(deps = {}) {
293
316
  same ? "daemon + adapter secrets match (compared by sha256; values never printed)." : `daemon vs adapter KOJEE_WEBHOOK_SECRET DIFFER (compared by sha256; values never printed) \u2014 every POST is rejected 401. Resync one side: ${WIZARD_RERUN} (writes one secret both sides).`
294
317
  );
295
318
  }
319
+ const execTargets = parseExecTargets(unitText);
320
+ if (unitText === null) {
321
+ emit(checks, collector, "webhook.daemon-bin", "unknown", `service unit unreadable at ${unitPath}.`);
322
+ } else if (execTargets === null || execTargets.length === 0) {
323
+ emit(
324
+ checks,
325
+ collector,
326
+ "webhook.daemon-bin",
327
+ "warn",
328
+ "could not parse the ExecStart exec target from the unit \u2014 cannot verify the daemon binary."
329
+ );
330
+ } else {
331
+ const missing = execTargets.filter((p) => !exists(p));
332
+ const ephemeral = execTargets.find((p) => /[\\/]_npx[\\/]/.test(p));
333
+ if (missing.length > 0) {
334
+ emit(
335
+ checks,
336
+ collector,
337
+ "webhook.daemon-bin",
338
+ "broken",
339
+ `daemon ExecStart points at ${missing.join(", ")} which does NOT exist \u2014 the service fails to start (exit 127) and crash-loops. Install globally + ${WIZARD_RERUN}: \`npm i -g kojee-mcp && kojee-mcp init --runtime hermes\`.`
340
+ );
341
+ } else if (ephemeral) {
342
+ emit(
343
+ checks,
344
+ collector,
345
+ "webhook.daemon-bin",
346
+ "warn",
347
+ `daemon ExecStart runs from an EPHEMERAL npx cache (${ephemeral}) \u2014 it works now but npx will eventually delete it, then the daemon dies (exit 127). For durability: \`npm i -g kojee-mcp && kojee-mcp init --runtime hermes\`.`
348
+ );
349
+ } else {
350
+ emit(
351
+ checks,
352
+ collector,
353
+ "webhook.daemon-bin",
354
+ "ok",
355
+ `daemon ExecStart targets exist (${execTargets.join(" ")}) \u2014 node + CLI resolve without PATH.`
356
+ );
357
+ }
358
+ }
296
359
  const svc = probeService();
297
360
  if (svc === null) {
298
361
  emit(
@@ -305,12 +368,38 @@ function collectHermesDoctorReport(deps = {}) {
305
368
  } else if (svc.active) {
306
369
  emit(checks, collector, "webhook.pusher-up", "ok", "kojee-hermes service is active.");
307
370
  } else {
371
+ const why = execTargets && execTargets.some((p) => !exists(p)) ? " (likely the missing ExecStart binary above \u2014 exit 127)" : "";
308
372
  emit(
309
373
  checks,
310
374
  collector,
311
375
  "webhook.pusher-up",
312
376
  "broken",
313
- "kojee-hermes service is NOT active \u2014 no wakes are being pushed. Enable+start it (`systemctl --user enable --now kojee-hermes`) \u2014 shared infra, so the principal's call."
377
+ `kojee-hermes service is NOT active${why} \u2014 no wakes are being pushed. Enable+start it (\`systemctl --user enable --now kojee-hermes\`) \u2014 shared infra, so the principal's call.`
378
+ );
379
+ }
380
+ if (pluginEnabled === null) {
381
+ emit(
382
+ checks,
383
+ collector,
384
+ "webhook.plugin-enabled",
385
+ "unknown",
386
+ "could not read plugins.enabled from ~/.hermes/config.yaml."
387
+ );
388
+ } else if (pluginEnabled) {
389
+ emit(
390
+ checks,
391
+ collector,
392
+ "webhook.plugin-enabled",
393
+ "ok",
394
+ "kojee-tandem receiver plugin is enabled in Hermes (plugins.enabled)."
395
+ );
396
+ } else {
397
+ emit(
398
+ checks,
399
+ collector,
400
+ "webhook.plugin-enabled",
401
+ "broken",
402
+ "kojee-tandem receiver plugin is NOT enabled \u2014 the gateway loads no receiver, so wakes never arrive (a gateway restart alone can't fix this). Enable it, then restart the gateway: `hermes plugins enable kojee-tandem && hermes gateway restart`."
314
403
  );
315
404
  }
316
405
  const receiver = probeReceiver(listenHost, Number.parseInt(listenPort, 10));
@@ -331,12 +420,13 @@ function collectHermesDoctorReport(deps = {}) {
331
420
  `receiver is listening on ${listenHost}:${listenPort}.`
332
421
  );
333
422
  } else {
423
+ const fix = pluginEnabled === false ? "the kojee-tandem plugin is DISABLED (see webhook.plugin-enabled) \u2014 enable it, then restart the gateway" : "the Hermes gateway (kojee-tandem plugin) receiver is down \u2014 restart the gateway to load the plugin";
334
424
  emit(
335
425
  checks,
336
426
  collector,
337
427
  "webhook.receiver-up",
338
428
  "warn",
339
- `nothing is listening on ${listenHost}:${listenPort} \u2014 the Hermes gateway (kojee-tandem plugin) receiver may be down; restart the gateway to load the plugin.`
429
+ `nothing is listening on ${listenHost}:${listenPort} \u2014 ${fix}.`
340
430
  );
341
431
  }
342
432
  if (childEnv === null) {
@@ -425,20 +515,61 @@ function collectHermesDoctorReport(deps = {}) {
425
515
  }
426
516
  return { checks, findings: collector.findings, verdict: collector.verdict() };
427
517
  }
518
+ var CHECK_GROUP = {
519
+ "webhook.env-format": "Config",
520
+ "webhook.url-receiver-align": "Config",
521
+ "webhook.secret-match": "Config",
522
+ "webhook.no-duplicate": "Config",
523
+ "webhook.instance-align": "Config",
524
+ "webhook.plugin-enabled": "Receiver",
525
+ "webhook.receiver-up": "Receiver",
526
+ "webhook.daemon-bin": "Pusher",
527
+ "webhook.pusher-up": "Pusher",
528
+ "webhook.env-reaches-daemon": "Pusher"
529
+ };
428
530
  function formatHermesDoctorReport(report) {
429
531
  const mark = (ok) => ok === true ? "\u2713" : ok === "warn" ? "\u26A0" : ok === "unknown" ? "?" : "\u2717";
532
+ const shortName = (name) => name.replace(/^webhook\./, "");
533
+ const nOk = report.checks.filter((c) => c.ok === true).length;
534
+ const nWarn = report.checks.filter((c) => c.ok === "warn").length;
535
+ const nBad = report.checks.filter((c) => c.ok === false).length;
536
+ const nUnknown = report.checks.filter((c) => c.ok === "unknown").length;
537
+ const verdictMark = report.verdict === "healthy" ? "\u2713" : report.verdict === "degraded" ? "\u26A0" : "\u2717";
430
538
  const lines = [];
431
- lines.push(`kojee-mcp doctor (hermes) \u2014 verdict: ${report.verdict.toUpperCase()}`);
432
- lines.push("");
433
- lines.push(" Wake mode: kojee-hermes daemon \u2192 HMAC-signed webhook \u2192 loopback receiver plugin.");
434
- lines.push(" (Daemon streams the SSE + POSTs wakes; the tools-only MCP child never delivers.)");
539
+ lines.push(`kojee-mcp doctor \xB7 hermes wake-path \xB7 ${verdictMark} ${report.verdict.toUpperCase()}`);
540
+ const tally = [
541
+ `${nOk} ok`,
542
+ nWarn ? `${nWarn} warning${nWarn > 1 ? "s" : ""}` : null,
543
+ nBad ? `${nBad} problem${nBad > 1 ? "s" : ""}` : null,
544
+ nUnknown ? `${nUnknown} unknown` : null
545
+ ].filter(Boolean).join(", ");
546
+ lines.push(` ${tally}`);
547
+ lines.push(" path: daemon (SSE \u2192 HMAC POST) \u2192 :8645 receiver plugin \u2192 wakes the agent");
435
548
  lines.push("");
436
- for (const c of report.checks) {
437
- lines.push(` ${mark(c.ok)} ${c.name}: ${c.detail}`);
549
+ for (const group of ["Config", "Receiver", "Pusher"]) {
550
+ const groupChecks = report.checks.filter((c) => CHECK_GROUP[c.name] === group);
551
+ if (groupChecks.length === 0) continue;
552
+ lines.push(` ${group}:`);
553
+ for (const c of groupChecks) {
554
+ lines.push(` ${mark(c.ok)} ${shortName(c.name)}: ${c.detail}`);
555
+ }
556
+ lines.push("");
557
+ }
558
+ const actionable = [
559
+ ...report.checks.filter((c) => c.ok === false),
560
+ ...report.checks.filter((c) => c.ok === "warn")
561
+ ];
562
+ if (actionable.length === 0) {
563
+ lines.push("\u2192 Wake-path is healthy. Nothing to do.");
564
+ } else {
565
+ lines.push("\u2192 Next steps (in order):");
566
+ actionable.forEach((c, i) => {
567
+ lines.push(` ${i + 1}. [${shortName(c.name)}] ${c.detail}`);
568
+ });
438
569
  }
439
570
  lines.push("");
440
571
  lines.push(
441
- "NOTE: read-only. Enabling/bouncing the kojee-hermes service (or the gateway) is an owner step \u2014 shared infra needs the principal's word."
572
+ " (read-only. Enabling/restarting the daemon, plugin, or gateway is an owner step \u2014 shared infra needs the principal's word.)"
442
573
  );
443
574
  return lines.join("\n");
444
575
  }
package/dist/index.js CHANGED
@@ -1,17 +1,16 @@
1
1
  import {
2
2
  listTandemIds,
3
3
  startProxy
4
- } from "./chunk-526UIEAL.js";
5
- import "./chunk-TCWIXG5C.js";
4
+ } from "./chunk-TUDC4GY5.js";
5
+ import "./chunk-XYDNRVOU.js";
6
6
  import "./chunk-TBVJOXIR.js";
7
7
  import "./chunk-Z5LPNJQ6.js";
8
8
  import "./chunk-I67C2HYA.js";
9
9
  import "./chunk-MIEI4PLB.js";
10
10
  import "./chunk-6G6YYST6.js";
11
11
  import "./chunk-U5HHHRXA.js";
12
- import "./chunk-A4IOKD4Z.js";
13
- import "./chunk-5DHIUN73.js";
14
- import "./chunk-FJUAMJHU.js";
12
+ import "./chunk-S35ZIVOL.js";
13
+ import "./chunk-O4JQY7IF.js";
15
14
  import "./chunk-PPTKGWFF.js";
16
15
  import "./chunk-XJEBJIQE.js";
17
16
  import "./chunk-SRLD2UY7.js";
@@ -154,8 +154,13 @@ class KojeeTandemAdapter(BasePlatformAdapter):
154
154
 
155
155
  # ── Connection lifecycle ────────────────────────────────────────────
156
156
 
157
- async def connect(self) -> bool:
158
- """Start the loopback webhook listener the kojee-mcp daemon POSTs to."""
157
+ async def connect(self, *, is_reconnect: bool = False) -> bool:
158
+ """Start the loopback webhook listener the kojee-mcp daemon POSTs to.
159
+
160
+ ``is_reconnect`` is part of the BasePlatformAdapter.connect contract
161
+ (the gateway's reconnection watcher calls ``connect(is_reconnect=True)``);
162
+ this listener is stateless per-connect so the flag is accepted and ignored.
163
+ """
159
164
  if not self.webhook_secret:
160
165
  logger.error("kojee-tandem: KOJEE_WEBHOOK_SECRET must be set (signed-only inbound)")
161
166
  self._set_fatal_error(
@@ -13,12 +13,11 @@ import {
13
13
  } from "./chunk-IZN7IZPW.js";
14
14
  import {
15
15
  claudeCodeAdapter
16
- } from "./chunk-TCWIXG5C.js";
17
- import "./chunk-A4IOKD4Z.js";
16
+ } from "./chunk-XYDNRVOU.js";
17
+ import "./chunk-S35ZIVOL.js";
18
18
  import {
19
19
  VERSION
20
- } from "./chunk-5DHIUN73.js";
21
- import "./chunk-FJUAMJHU.js";
20
+ } from "./chunk-O4JQY7IF.js";
22
21
  import "./chunk-PPTKGWFF.js";
23
22
 
24
23
  // src/delivery/lib/fanout.ts
@@ -127,7 +126,7 @@ function createClaudeCodeDelivery() {
127
126
  const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
128
127
  const { createWebhookSink } = await import("./webhook-sink-N6AUTFL3.js");
129
128
  const { startEventStream } = await import("./event-stream-ATKYXDBV.js");
130
- const { createMcpServer } = await import("./server-DU3LFS32.js");
129
+ const { createMcpServer } = await import("./server-RK4UK6SP.js");
131
130
  const { deriveDiscoveryKey } = await import("./ancestry-A2F5KQ6A.js");
132
131
  const { resolveSharedSessionId } = await import("./cc-session-id-NNCZCHZZ.js");
133
132
  sweepStaleDiscovery();
@@ -324,7 +323,7 @@ function createWebhookDelivery(name, pendingLedger) {
324
323
  const { createWebhookSink } = await import("./webhook-sink-N6AUTFL3.js");
325
324
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
326
325
  const { startEventStream } = await import("./event-stream-ATKYXDBV.js");
327
- const { createMcpServer } = await import("./server-DU3LFS32.js");
326
+ const { createMcpServer } = await import("./server-RK4UK6SP.js");
328
327
  sweepStaleEventLogs();
329
328
  eventLog = startEventLog({
330
329
  key: ctx.instanceKey,
@@ -518,7 +517,7 @@ function createOpenclawDelivery(deps = {}) {
518
517
  const { startEventLog, sweepStaleEventLogs } = await import("./event-log-2NBJEIEP.js");
519
518
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
520
519
  const { startEventStream } = await import("./event-stream-ATKYXDBV.js");
521
- const { createMcpServer } = await import("./server-DU3LFS32.js");
520
+ const { createMcpServer } = await import("./server-RK4UK6SP.js");
522
521
  sweepStaleEventLogs();
523
522
  eventLog = startEventLog({
524
523
  key: ctx.instanceKey,
@@ -5,9 +5,8 @@ import {
5
5
  executeToolCall,
6
6
  extractDrainCursor,
7
7
  startMcpServer
8
- } from "./chunk-A4IOKD4Z.js";
9
- import "./chunk-5DHIUN73.js";
10
- import "./chunk-FJUAMJHU.js";
8
+ } from "./chunk-S35ZIVOL.js";
9
+ import "./chunk-O4JQY7IF.js";
11
10
  import "./chunk-PPTKGWFF.js";
12
11
  export {
13
12
  buildChannelInstructions,
@@ -3,8 +3,8 @@ import {
3
3
  pairSlotFor,
4
4
  reconcileConnectPairSlot,
5
5
  runWizard
6
- } from "./chunk-4E6WMRTM.js";
7
- import "./chunk-6XWTUDWW.js";
6
+ } from "./chunk-RTYRJAJO.js";
7
+ import "./chunk-T6QANUS2.js";
8
8
  import "./chunk-E6WMFMM2.js";
9
9
  import "./chunk-GEGUWQYT.js";
10
10
  import {
@@ -26,8 +26,7 @@ import "./chunk-I67C2HYA.js";
26
26
  import "./chunk-MIEI4PLB.js";
27
27
  import "./chunk-6G6YYST6.js";
28
28
  import "./chunk-U5HHHRXA.js";
29
- import "./chunk-5DHIUN73.js";
30
- import "./chunk-FJUAMJHU.js";
29
+ import "./chunk-O4JQY7IF.js";
31
30
 
32
31
  // src/wizard/setup-handler.ts
33
32
  var SETUP_SUPPORTED_RUNTIMES = ["claude-code", "codex"];
@@ -19,7 +19,7 @@ import "./chunk-67F67AQ6.js";
19
19
  import "./chunk-U5HHHRXA.js";
20
20
  import {
21
21
  buildMonitorNudge
22
- } from "./chunk-FJUAMJHU.js";
22
+ } from "./chunk-O4JQY7IF.js";
23
23
  import "./chunk-XJEBJIQE.js";
24
24
  import "./chunk-SRLD2UY7.js";
25
25
 
@@ -16,6 +16,7 @@ import "./chunk-U5HHHRXA.js";
16
16
 
17
17
  // src/tail-stream.ts
18
18
  import fs from "fs";
19
+ import { spawn } from "child_process";
19
20
  var STATUS_LINE_RE = new RegExp(`^\\[[^\\]]*\\] ${STATUS_LINE_PREFIX} (.*)$`);
20
21
  var HEARTBEAT_FIELDS_RE = /(^|\s)status=heartbeat(\s|$)/;
21
22
  var ROTATED_FIELDS_RE = /(^|\s)status=rotated(\s|$)/;
@@ -29,6 +30,7 @@ var TAIL_STALE_FLOOR_MS = envInt(
29
30
  );
30
31
  var STALE_CHECK_INTERVAL_MS = envInt("KOJEE_TAIL_CHECK_MS", 5e3);
31
32
  var TAIL_REWARN_AFTER_MS = 30 * 6e4;
33
+ var SUSTAINED_STALE_MS = envInt("KOJEE_TAIL_SUSTAINED_MS", TAIL_REWARN_AFTER_MS);
32
34
  var TAIL_HEALTHY_RESET_MS = 30 * 6e4;
33
35
  var TAIL_BACKOFF_CAP_CYCLES = 3;
34
36
  function envInt(name, fallback) {
@@ -99,9 +101,13 @@ function createStallDetector(startNow, options = {}) {
99
101
  }
100
102
  };
101
103
  }
102
- async function runTail(messagesPath) {
104
+ async function runTail(messagesPath, opts = {}) {
105
+ const wakeOnly = opts.wakeOnly === true;
103
106
  const statusPath = statusLogPath(messagesPath);
104
- const detector = createStallDetector(Date.now(), { floorMs: TAIL_STALE_FLOOR_MS });
107
+ const detector = createStallDetector(Date.now(), {
108
+ floorMs: TAIL_STALE_FLOOR_MS,
109
+ rewarnAfterMs: SUSTAINED_STALE_MS
110
+ });
105
111
  const heartbeatPath = monitorHeartbeatPath(messagesPath);
106
112
  const touchHeartbeat = () => {
107
113
  const now = /* @__PURE__ */ new Date();
@@ -116,7 +122,7 @@ async function runTail(messagesPath) {
116
122
  };
117
123
  let lastCursor = null;
118
124
  function onMessageLine(line) {
119
- if (detector.onLife(Date.now())?.kind === "recovered") {
125
+ if (detector.onLife(Date.now())?.kind === "recovered" && !wakeOnly) {
120
126
  process.stdout.write(
121
127
  "[kojee] stream recovered \u2014 events flowing again (recovery is automatic; no action needed).\n"
122
128
  );
@@ -133,7 +139,7 @@ async function runTail(messagesPath) {
133
139
  if (HEARTBEAT_FIELDS_RE.test(fields)) {
134
140
  detector.onHeartbeat(now);
135
141
  }
136
- if (detector.onLife(now)?.kind === "recovered") {
142
+ if (detector.onLife(now)?.kind === "recovered" && !wakeOnly) {
137
143
  process.stdout.write(
138
144
  "[kojee] stream recovered \u2014 heartbeats resumed (the stall was transient, likely idle-period timing or brief host load; no action needed).\n"
139
145
  );
@@ -149,7 +155,12 @@ async function runTail(messagesPath) {
149
155
  }
150
156
  }
151
157
  const followers = [
152
- makeFollower(messagesPath, onMessageLine, touchHeartbeat),
158
+ // MESSAGES (wake-critical) — prefer the OS `tail -F` follower where the
159
+ // platform has one; it delegates file-following to the battle-tested system
160
+ // binary instead of the Node fs.watch/poll tailer. STATUS (telemetry) stays
161
+ // on the Node follower: its startup-backlog seeding boundary (statusSeeded)
162
+ // is clean to detect only when we control the drain. See makeMessagesFollower.
163
+ makeMessagesFollower(messagesPath, onMessageLine, touchHeartbeat),
153
164
  makeFollower(statusPath, onStatusLine, touchHeartbeat)
154
165
  ];
155
166
  void Promise.all([
@@ -171,6 +182,7 @@ async function runTail(messagesPath) {
171
182
  );
172
183
  return;
173
184
  }
185
+ if (wakeOnly) return;
174
186
  const thresholdS = Math.round((signal.thresholdMs ?? 0) / 1e3);
175
187
  process.stdout.write(
176
188
  `[kojee] WARNING: no events or heartbeat for ${silentS}s (threshold ${thresholdS}s) \u2014 stream may be dead, but this is often benign (idle stream, or brief host load delaying heartbeats) and recovery is automatic; if it persists, run \`npx kojee-mcp doctor\`.
@@ -273,11 +285,99 @@ function makeFollower(filePath, onLine, onActivity) {
273
285
  }
274
286
  };
275
287
  }
288
+ function osTailAvailable() {
289
+ const override = (process.env.KOJEE_TAIL_IMPL ?? "").trim().toLowerCase();
290
+ if (override === "os") return true;
291
+ if (override === "node") return false;
292
+ return process.platform !== "win32";
293
+ }
294
+ function makeMessagesFollower(filePath, onLine, onActivity) {
295
+ if (!osTailAvailable()) return makeFollower(filePath, onLine, onActivity);
296
+ return makeTailFollower(
297
+ filePath,
298
+ onLine,
299
+ onActivity,
300
+ () => makeFollower(filePath, onLine, onActivity)
301
+ );
302
+ }
303
+ function makeTailFollower(filePath, onLine, onActivity, fallback) {
304
+ let child = null;
305
+ let heir = null;
306
+ let hbTimer = null;
307
+ let buffer = "";
308
+ let stopped = false;
309
+ let handedOff = false;
310
+ function emit(chunk) {
311
+ onActivity();
312
+ buffer += chunk;
313
+ let nl;
314
+ while ((nl = buffer.indexOf("\n")) !== -1) {
315
+ onLine(buffer.slice(0, nl));
316
+ buffer = buffer.slice(nl + 1);
317
+ }
318
+ }
319
+ function handOff() {
320
+ if (stopped || handedOff) return;
321
+ handedOff = true;
322
+ if (hbTimer) {
323
+ clearInterval(hbTimer);
324
+ hbTimer = null;
325
+ }
326
+ if (child) {
327
+ try {
328
+ child.kill("SIGTERM");
329
+ } catch {
330
+ }
331
+ child = null;
332
+ }
333
+ heir = fallback();
334
+ void heir.start().catch(() => {
335
+ });
336
+ }
337
+ return {
338
+ async start() {
339
+ try {
340
+ child = spawn("tail", ["-n", "+1", "-F", filePath], {
341
+ stdio: ["ignore", "pipe", "ignore"]
342
+ });
343
+ } catch {
344
+ handOff();
345
+ return;
346
+ }
347
+ child.on("error", () => handOff());
348
+ child.on("exit", () => {
349
+ if (!stopped) handOff();
350
+ });
351
+ child.stdout?.setEncoding("utf8");
352
+ child.stdout?.on("data", (d) => emit(d));
353
+ onActivity();
354
+ hbTimer = setInterval(onActivity, 1e3);
355
+ },
356
+ stop() {
357
+ stopped = true;
358
+ if (hbTimer) {
359
+ clearInterval(hbTimer);
360
+ hbTimer = null;
361
+ }
362
+ if (child) {
363
+ try {
364
+ child.kill("SIGTERM");
365
+ } catch {
366
+ }
367
+ child = null;
368
+ }
369
+ if (heir) heir.stop();
370
+ }
371
+ };
372
+ }
276
373
  function sleep(ms) {
277
374
  return new Promise((r) => setTimeout(r, ms));
278
375
  }
279
376
  export {
280
377
  createStallDetector,
281
378
  makeFollower,
379
+ makeMessagesFollower,
380
+ makeTailFollower,
381
+ osTailAvailable,
282
382
  runTail
283
383
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.7.5-staging.5",
3
+ "version": "0.7.5-staging.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -1,25 +0,0 @@
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
-
23
- export {
24
- VERSION
25
- };