kojee-mcp 0.7.5-beta.0 → 0.7.5-beta.1

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.
@@ -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"));
@@ -425,7 +425,8 @@ function installBundledPayload(opts) {
425
425
  import fs5 from "fs";
426
426
  import path5 from "path";
427
427
  function daemonSourceExecCommand(spec) {
428
- return `set -a; . ${spec.envFile}; set +a; exec ${spec.binPath}`;
428
+ const target = spec.nodePath ? `${spec.nodePath} ${spec.binPath}` : spec.binPath;
429
+ return `set -a; . ${spec.envFile}; set +a; exec ${target}`;
429
430
  }
430
431
  function xmlEscape(value) {
431
432
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -571,6 +572,7 @@ function installHermes(inp) {
571
572
  const svc = writeService(inp.platform, {
572
573
  serviceName: "kojee-hermes",
573
574
  binPath: inp.binPath,
575
+ ...inp.nodePath ? { nodePath: inp.nodePath } : {},
574
576
  envFile: daemonEnv,
575
577
  runtime: "hermes",
576
578
  homeDir: inp.homeDir
@@ -589,15 +591,18 @@ function installHermes(inp) {
589
591
  // every daemon runtime) on the same URL-present path before this output, so
590
592
  // emitting it here too would print the contract twice.
591
593
  "",
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.'
594
+ "Next \u2014 ACTIVATE (owner steps; the receiver never binds until you do these, in order):",
595
+ " 1. enable the Tandem receiver plugin (WITHOUT this, the gateway restart loads nothing):",
596
+ " hermes plugins enable kojee-tandem",
597
+ " 2. restart the gateway to bind the receiver (:8645) AND load the kojee MCP tools:",
598
+ " hermes gateway restart (or: systemctl --user restart hermes-gateway)",
599
+ ` 3. start the pusher daemon: ${svc.activateCmd}`,
600
+ " 4. verify end-to-end (should read HEALTHY): kojee-mcp doctor --runtime hermes",
601
+ "",
602
+ " Notes:",
603
+ " - the agent joins rooms from its own tools: mcp_kojee_tandem_join <tandem_id>",
604
+ " - if tools don't register: ensure Hermes has the MCP extra",
605
+ ' (cd ~/.hermes/hermes-agent && uv pip install -e ".[mcp]") and re-check connect_timeout.'
601
606
  ];
602
607
  return {
603
608
  runtime: "hermes",
@@ -975,7 +980,18 @@ function distDir() {
975
980
  }
976
981
  function resolveBinPath() {
977
982
  const entry = process.argv[1];
978
- return entry && entry.length > 0 ? entry : "kojee-mcp";
983
+ if (!entry || entry.length === 0) return "kojee-mcp";
984
+ try {
985
+ return fs6.realpathSync(entry);
986
+ } catch {
987
+ return entry;
988
+ }
989
+ }
990
+ function resolveDaemonExec() {
991
+ const nodePath = process.execPath;
992
+ const binPath = resolveBinPath();
993
+ const ephemeral = /[\\/]_npx[\\/]/.test(binPath);
994
+ return { nodePath, binPath, ephemeral };
979
995
  }
980
996
  function configureHermes(opts) {
981
997
  const runtime = "hermes";
@@ -1018,11 +1034,13 @@ function configureHermes(opts) {
1018
1034
  }
1019
1035
  const env = opts.env ?? process.env;
1020
1036
  const suppliedSecret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
1037
+ const daemonExec = resolveDaemonExec();
1021
1038
  const install = installHermes({
1022
1039
  homeDir: home,
1023
1040
  payloadBaseDir: base,
1024
1041
  platform: process.platform,
1025
- binPath: resolveBinPath(),
1042
+ binPath: daemonExec.binPath,
1043
+ nodePath: daemonExec.nodePath,
1026
1044
  webhookUrl: wh.url,
1027
1045
  ...suppliedSecret ? { webhookSecret: suppliedSecret } : {},
1028
1046
  ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
@@ -1035,6 +1053,15 @@ function configureHermes(opts) {
1035
1053
  return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
1036
1054
  }
1037
1055
  recordRuntime(runtime);
1056
+ if (daemonExec.ephemeral) {
1057
+ lines.push(
1058
+ "\u26A0 DURABILITY: this ran from an ephemeral npx cache, so the daemon points at a",
1059
+ " path npx will eventually delete (the daemon would then fail to start).",
1060
+ " For a durable daemon, install globally and re-run init:",
1061
+ " npm i -g kojee-mcp && kojee-mcp init --runtime hermes",
1062
+ ""
1063
+ );
1064
+ }
1038
1065
  const envFile = path7.join(home, ".kojee", "hermes.env");
1039
1066
  lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
1040
1067
  lines.push("");
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-EIAUW6KO.js";
5
5
  import {
6
6
  startProxy
7
- } from "./chunk-526UIEAL.js";
7
+ } from "./chunk-4ATSAH7H.js";
8
8
  import "./chunk-TCWIXG5C.js";
9
9
  import {
10
10
  pairedConfigPath
@@ -48,7 +48,7 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
48
48
  program.command("connect <code>").description(
49
49
  "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
50
  ).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");
51
+ const { runConnect } = await import("./connect-handler-NCHC6WPU.js");
52
52
  const result = await runConnect({
53
53
  code,
54
54
  runtime: opts.runtime,
@@ -124,7 +124,7 @@ program.command("tail <path>").description("Stream a file's contents and follow
124
124
  }
125
125
  });
126
126
  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");
127
+ const { runDoctor } = await import("./doctor-RKLXZR2B.js");
128
128
  const code = await runDoctor();
129
129
  process.exit(code);
130
130
  });
@@ -143,7 +143,7 @@ function addInstallOptions(cmd, runtimeHelp) {
143
143
  function makeInstallAction(verb) {
144
144
  return async (opts) => {
145
145
  const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
146
- const { runSetup, resolvePairCode } = await import("./setup-handler-6BDICX5F.js");
146
+ const { runSetup, resolvePairCode } = await import("./setup-handler-DA4465VH.js");
147
147
  const pairCode = resolvePairCode(opts);
148
148
  const result = await runSetup({
149
149
  verb,
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_BROKER_URL,
3
3
  reconcileConnectPairSlot,
4
4
  runWizard
5
- } from "./chunk-4E6WMRTM.js";
5
+ } from "./chunk-CPOK642I.js";
6
6
  import "./chunk-6XWTUDWW.js";
7
7
  import "./chunk-E6WMFMM2.js";
8
8
  import "./chunk-GEGUWQYT.js";
@@ -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));
@@ -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,7 +1,7 @@
1
1
  import {
2
2
  listTandemIds,
3
3
  startProxy
4
- } from "./chunk-526UIEAL.js";
4
+ } from "./chunk-4ATSAH7H.js";
5
5
  import "./chunk-TCWIXG5C.js";
6
6
  import "./chunk-TBVJOXIR.js";
7
7
  import "./chunk-Z5LPNJQ6.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(
@@ -3,7 +3,7 @@ import {
3
3
  pairSlotFor,
4
4
  reconcileConnectPairSlot,
5
5
  runWizard
6
- } from "./chunk-4E6WMRTM.js";
6
+ } from "./chunk-CPOK642I.js";
7
7
  import "./chunk-6XWTUDWW.js";
8
8
  import "./chunk-E6WMFMM2.js";
9
9
  import "./chunk-GEGUWQYT.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.7.5-beta.0",
3
+ "version": "0.7.5-beta.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {