kojee-mcp 0.7.5-staging.4 → 0.7.5-staging.6
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 +21 -2
- package/dist/{chunk-O2GELC3V.js → chunk-4ATSAH7H.js} +14 -2
- package/dist/{chunk-QNGMMBOD.js → chunk-CPOK642I.js} +83 -115
- package/dist/chunk-GEGUWQYT.js +90 -0
- package/dist/cli.js +4 -4
- package/dist/{connect-handler-D3VQMUIS.js → connect-handler-NCHC6WPU.js} +2 -1
- package/dist/{doctor-ZHV362IF.js → doctor-RKLXZR2B.js} +6 -0
- package/dist/doctor-hermes-WLDI4R6P.js +580 -0
- package/dist/index.js +1 -1
- package/dist/plugins/hermes/adapter.py +7 -2
- package/dist/{setup-handler-5Q47JYPQ.js → setup-handler-DA4465VH.js} +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,8 +189,11 @@ Per-runtime config-path / hooks-path overrides: `--config-path`, `--hooks-path`.
|
|
|
189
189
|
injector — no webhook receiver, no `--webhook-url`.
|
|
190
190
|
- **hermes** — a **daemon** feeding a webhook receiver. `init --runtime hermes`
|
|
191
191
|
(or `connect --runtime hermes`) validates the webhook env and prints + records
|
|
192
|
-
the env
|
|
193
|
-
|
|
192
|
+
the env (secret redacted) into a single-quoted `~/.kojee/hermes.env`, loaded
|
|
193
|
+
with `set -a; . ~/.kojee/hermes.env; set +a` (the installed service unit's
|
|
194
|
+
`ExecStart` uses this exact wrapper so every var is exported to the daemon on
|
|
195
|
+
both Linux and macOS). `--webhook-url` is optional (defaults to the
|
|
196
|
+
hermes-plugin loopback receiver).
|
|
194
197
|
|
|
195
198
|
`kojee-mcp init --uninstall` is runtime-aware (uses the recorded runtime when
|
|
196
199
|
`--runtime` is omitted). `kojee-mcp doctor` is runtime-aware too.
|
|
@@ -571,6 +574,22 @@ npm run dev:stub
|
|
|
571
574
|
# Stub listens on http://localhost:8765
|
|
572
575
|
```
|
|
573
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
|
+
|
|
574
593
|
## How Approvals Work
|
|
575
594
|
|
|
576
595
|
Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
|
|
@@ -331,10 +331,22 @@ async function startProxy(config) {
|
|
|
331
331
|
}
|
|
332
332
|
for (const step of started.teardown) teardownSteps.push(step);
|
|
333
333
|
} else {
|
|
334
|
+
if (process.env["KOJEE_WEBHOOK_EXPECTED"]) {
|
|
335
|
+
console.error(
|
|
336
|
+
"[kojee-mcp] webhook expected (KOJEE_WEBHOOK_EXPECTED=1) but no delivery configured \u2014 env injection likely failed; check the daemon env file (~/.kojee/hermes.env) / service wiring (ExecStart should be `/bin/sh -c 'set -a; . <envFile>; set +a; exec <bin>'`)."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
334
339
|
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
335
340
|
}
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}
|
|
338
350
|
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
339
351
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
340
352
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
removeOpenclawMcpServer,
|
|
11
11
|
writeOpenclawMcpConfig
|
|
12
12
|
} from "./chunk-E6WMFMM2.js";
|
|
13
|
+
import {
|
|
14
|
+
HERMES_INSTANCE_KEY,
|
|
15
|
+
defaultHermesConfigPath,
|
|
16
|
+
removeHermesMcpServer,
|
|
17
|
+
writeHermesMcpConfig
|
|
18
|
+
} from "./chunk-GEGUWQYT.js";
|
|
13
19
|
import {
|
|
14
20
|
WIZARD_RUNTIMES,
|
|
15
21
|
isWizardRuntime
|
|
@@ -135,8 +141,8 @@ function pairSlotFor(runtime, code) {
|
|
|
135
141
|
|
|
136
142
|
// src/wizard/wizard.ts
|
|
137
143
|
import crypto3 from "crypto";
|
|
138
|
-
import
|
|
139
|
-
import
|
|
144
|
+
import fs6 from "fs";
|
|
145
|
+
import path7 from "path";
|
|
140
146
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
141
147
|
|
|
142
148
|
// src/wizard/skill-install.ts
|
|
@@ -354,8 +360,7 @@ function writeWebhookSecretBothSides(opts) {
|
|
|
354
360
|
...opts.daemonExtraEnv ?? []
|
|
355
361
|
];
|
|
356
362
|
upsertEnvFile(opts.daemonEnvPath, daemonVars, {
|
|
357
|
-
|
|
358
|
-
header: `# kojee daemon env for runtime=${opts.runtime} (source this before starting the daemon)`
|
|
363
|
+
header: `# kojee daemon env for runtime=${opts.runtime} (load with: set -a; . <this file>; set +a)`
|
|
359
364
|
});
|
|
360
365
|
secureFile(opts.daemonEnvPath);
|
|
361
366
|
const adapterVars = [
|
|
@@ -369,7 +374,7 @@ function writeWebhookSecretBothSides(opts) {
|
|
|
369
374
|
}
|
|
370
375
|
|
|
371
376
|
// src/wizard/installers/hermes.ts
|
|
372
|
-
import
|
|
377
|
+
import path6 from "path";
|
|
373
378
|
|
|
374
379
|
// src/wizard/plugin-payload.ts
|
|
375
380
|
import fs4 from "fs";
|
|
@@ -419,6 +424,13 @@ function installBundledPayload(opts) {
|
|
|
419
424
|
// src/wizard/service.ts
|
|
420
425
|
import fs5 from "fs";
|
|
421
426
|
import path5 from "path";
|
|
427
|
+
function daemonSourceExecCommand(spec) {
|
|
428
|
+
const target = spec.nodePath ? `${spec.nodePath} ${spec.binPath}` : spec.binPath;
|
|
429
|
+
return `set -a; . ${spec.envFile}; set +a; exec ${target}`;
|
|
430
|
+
}
|
|
431
|
+
function xmlEscape(value) {
|
|
432
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
433
|
+
}
|
|
422
434
|
function systemdUnit(spec) {
|
|
423
435
|
return [
|
|
424
436
|
"[Unit]",
|
|
@@ -428,9 +440,14 @@ function systemdUnit(spec) {
|
|
|
428
440
|
"",
|
|
429
441
|
"[Service]",
|
|
430
442
|
"Type=simple",
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
443
|
+
// §2b DISCRIMINATOR: KOJEE_WEBHOOK_EXPECTED lives in the WRAPPER env (this
|
|
444
|
+
// Environment= line), NOT in the sourced env file — a same-class drop would
|
|
445
|
+
// otherwise kill the marker too. It lets startProxy tell the "webhook was
|
|
446
|
+
// meant to be here" daemon apart from the tools-only child (which never sets
|
|
447
|
+
// it) when the sourced URL is absent → null delivery.
|
|
448
|
+
`Environment=KOJEE_RUNTIME=${spec.runtime} KOJEE_WEBHOOK_EXPECTED=1`,
|
|
449
|
+
// §2a: source the env file inside a shell wrapper (replaces EnvironmentFile=).
|
|
450
|
+
`ExecStart=/bin/sh -c '${daemonSourceExecCommand(spec)}'`,
|
|
434
451
|
"Restart=always",
|
|
435
452
|
"RestartSec=5",
|
|
436
453
|
"StandardOutput=journal",
|
|
@@ -451,11 +468,19 @@ function launchdPlist(spec, label) {
|
|
|
451
468
|
'<plist version="1.0">',
|
|
452
469
|
"<dict>",
|
|
453
470
|
" <key>Label</key>",
|
|
454
|
-
` <string>${label}</string>`,
|
|
471
|
+
` <string>${xmlEscape(label)}</string>`,
|
|
472
|
+
// §2a: same source-wrapper as the systemd unit so macOS actually loads the
|
|
473
|
+
// env file (the plist historically never did). The secret is never inlined
|
|
474
|
+
// here — it stays in the 0o600 sourced file.
|
|
455
475
|
" <key>ProgramArguments</key>",
|
|
456
|
-
|
|
476
|
+
" <array>",
|
|
477
|
+
" <string>/bin/sh</string>",
|
|
478
|
+
" <string>-c</string>",
|
|
479
|
+
` <string>${xmlEscape(daemonSourceExecCommand(spec))}</string>`,
|
|
480
|
+
" </array>",
|
|
481
|
+
// §2b: KOJEE_WEBHOOK_EXPECTED in the wrapper env (NOT the sourced file).
|
|
457
482
|
" <key>EnvironmentVariables</key>",
|
|
458
|
-
` <dict><key>KOJEE_RUNTIME</key><string>${spec.runtime}</string></dict>`,
|
|
483
|
+
` <dict><key>KOJEE_RUNTIME</key><string>${xmlEscape(spec.runtime)}</string><key>KOJEE_WEBHOOK_EXPECTED</key><string>1</string></dict>`,
|
|
459
484
|
" <key>RunAtLoad</key><true/>",
|
|
460
485
|
" <key>KeepAlive</key><true/>",
|
|
461
486
|
"</dict>",
|
|
@@ -508,100 +533,17 @@ function writeService(platform, spec) {
|
|
|
508
533
|
return plan;
|
|
509
534
|
}
|
|
510
535
|
|
|
511
|
-
// src/wizard/capabilities/hermes-mcp-config.ts
|
|
512
|
-
import fs6 from "fs";
|
|
513
|
-
import path6 from "path";
|
|
514
|
-
import { parseDocument, isMap } from "yaml";
|
|
515
|
-
var HERMES_MCP_SERVER_NAME = "kojee";
|
|
516
|
-
var HERMES_INSTANCE_KEY = "hermes-gateway";
|
|
517
|
-
var HERMES_MCP_CONNECT_TIMEOUT_S = 120;
|
|
518
|
-
function defaultHermesConfigPath(homeDir) {
|
|
519
|
-
return path6.join(homeDir, ".hermes", "config.yaml");
|
|
520
|
-
}
|
|
521
|
-
function buildKojeeEntry(binPath) {
|
|
522
|
-
return {
|
|
523
|
-
command: binPath,
|
|
524
|
-
args: [],
|
|
525
|
-
env: {
|
|
526
|
-
KOJEE_RUNTIME: "hermes",
|
|
527
|
-
KOJEE_INSTANCE: HERMES_INSTANCE_KEY,
|
|
528
|
-
// Vanilla tools-only proxy: NEVER a second webhook delivery (see header).
|
|
529
|
-
KOJEE_WEBHOOK_URL: ""
|
|
530
|
-
},
|
|
531
|
-
connect_timeout: HERMES_MCP_CONNECT_TIMEOUT_S
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
function resolveMode(filePath) {
|
|
535
|
-
try {
|
|
536
|
-
return fs6.statSync(filePath).mode & 511;
|
|
537
|
-
} catch {
|
|
538
|
-
return 384;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
function atomicWrite(filePath, content, mode) {
|
|
542
|
-
fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
543
|
-
const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
544
|
-
fs6.writeFileSync(tmp, content, { mode });
|
|
545
|
-
fs6.renameSync(tmp, filePath);
|
|
546
|
-
}
|
|
547
|
-
function backupSuffix(now) {
|
|
548
|
-
return (now ? now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
549
|
-
}
|
|
550
|
-
function writeHermesMcpConfig(configPath, opts) {
|
|
551
|
-
let raw = null;
|
|
552
|
-
try {
|
|
553
|
-
raw = fs6.readFileSync(configPath, "utf8");
|
|
554
|
-
} catch {
|
|
555
|
-
raw = null;
|
|
556
|
-
}
|
|
557
|
-
let backedUp;
|
|
558
|
-
let doc = parseDocument("");
|
|
559
|
-
if (raw !== null && raw.trim() !== "") {
|
|
560
|
-
const parsed = parseDocument(raw);
|
|
561
|
-
if (parsed.errors.length > 0) {
|
|
562
|
-
backedUp = `${configPath}.corrupt-${backupSuffix(opts.now)}`;
|
|
563
|
-
fs6.copyFileSync(configPath, backedUp);
|
|
564
|
-
} else {
|
|
565
|
-
doc = parsed;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (doc.hasIn(["mcp_servers"]) && !isMap(doc.getIn(["mcp_servers"], true))) {
|
|
569
|
-
doc.deleteIn(["mcp_servers"]);
|
|
570
|
-
}
|
|
571
|
-
doc.setIn(["mcp_servers", HERMES_MCP_SERVER_NAME], buildKojeeEntry(opts.binPath));
|
|
572
|
-
atomicWrite(configPath, String(doc), resolveMode(configPath));
|
|
573
|
-
return { configPath, ...backedUp ? { backedUp } : {} };
|
|
574
|
-
}
|
|
575
|
-
function removeHermesMcpServer(configPath) {
|
|
576
|
-
let raw;
|
|
577
|
-
try {
|
|
578
|
-
raw = fs6.readFileSync(configPath, "utf8");
|
|
579
|
-
} catch {
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
const doc = parseDocument(raw);
|
|
583
|
-
if (doc.errors.length > 0) return false;
|
|
584
|
-
if (!doc.hasIn(["mcp_servers", HERMES_MCP_SERVER_NAME])) return false;
|
|
585
|
-
doc.deleteIn(["mcp_servers", HERMES_MCP_SERVER_NAME]);
|
|
586
|
-
const servers = doc.getIn(["mcp_servers"], true);
|
|
587
|
-
if (isMap(servers) && servers.items.length === 0) {
|
|
588
|
-
doc.deleteIn(["mcp_servers"]);
|
|
589
|
-
}
|
|
590
|
-
atomicWrite(configPath, String(doc), resolveMode(configPath));
|
|
591
|
-
return true;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
536
|
// src/wizard/installers/hermes.ts
|
|
595
537
|
function installHermes(inp) {
|
|
596
|
-
const daemonEnv =
|
|
597
|
-
const adapterEnv =
|
|
598
|
-
const pluginDir =
|
|
538
|
+
const daemonEnv = path6.join(inp.homeDir, ".kojee", "hermes.env");
|
|
539
|
+
const adapterEnv = path6.join(inp.homeDir, ".hermes", ".env");
|
|
540
|
+
const pluginDir = path6.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
|
|
599
541
|
if (!inp.skipPayload) {
|
|
600
542
|
const missing = missingPayloadFiles("hermes", inp.payloadBaseDir, HERMES_PAYLOAD_FILES);
|
|
601
543
|
if (missing.length > 0) {
|
|
602
544
|
return {
|
|
603
545
|
runtime: "hermes",
|
|
604
|
-
output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${
|
|
546
|
+
output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${path6.join(inp.payloadBaseDir, "plugins", "hermes")}. No changes were written. Run \`npm run build\` to stage dist/plugins/hermes, then retry.`,
|
|
605
547
|
exitCode: 2,
|
|
606
548
|
secret: "",
|
|
607
549
|
secretReused: false
|
|
@@ -630,6 +572,7 @@ function installHermes(inp) {
|
|
|
630
572
|
const svc = writeService(inp.platform, {
|
|
631
573
|
serviceName: "kojee-hermes",
|
|
632
574
|
binPath: inp.binPath,
|
|
575
|
+
...inp.nodePath ? { nodePath: inp.nodePath } : {},
|
|
633
576
|
envFile: daemonEnv,
|
|
634
577
|
runtime: "hermes",
|
|
635
578
|
homeDir: inp.homeDir
|
|
@@ -648,15 +591,18 @@ function installHermes(inp) {
|
|
|
648
591
|
// every daemon runtime) on the same URL-present path before this output, so
|
|
649
592
|
// emitting it here too would print the contract twice.
|
|
650
593
|
"",
|
|
651
|
-
"Next:",
|
|
652
|
-
|
|
653
|
-
"
|
|
654
|
-
"
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
|
|
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.'
|
|
660
606
|
];
|
|
661
607
|
return {
|
|
662
608
|
runtime: "hermes",
|
|
@@ -1016,7 +962,7 @@ function buildDaemonEnvBlock(runtime, wh, envFile) {
|
|
|
1016
962
|
lines.push(` export KOJEE_WEBHOOK_SECRET=<generated; in ${envFile}>`);
|
|
1017
963
|
for (const [k, v] of wh.signatureEnv) lines.push(` export ${k}=${shellSingleQuote(v)}`);
|
|
1018
964
|
lines.push(` (validated: ${wh.redactedSummary})`);
|
|
1019
|
-
lines.push(`
|
|
965
|
+
lines.push(` set -a; . ${envFile}; set +a`);
|
|
1020
966
|
} else {
|
|
1021
967
|
lines.push("Set the daemon env (no receiver URL supplied yet):");
|
|
1022
968
|
lines.push(` export KOJEE_RUNTIME=${shellSingleQuote(runtime)}`);
|
|
@@ -1030,11 +976,22 @@ function buildDaemonEnvBlock(runtime, wh, envFile) {
|
|
|
1030
976
|
return lines;
|
|
1031
977
|
}
|
|
1032
978
|
function distDir() {
|
|
1033
|
-
return
|
|
979
|
+
return path7.dirname(fileURLToPath2(import.meta.url));
|
|
1034
980
|
}
|
|
1035
981
|
function resolveBinPath() {
|
|
1036
982
|
const entry = process.argv[1];
|
|
1037
|
-
|
|
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 };
|
|
1038
995
|
}
|
|
1039
996
|
function configureHermes(opts) {
|
|
1040
997
|
const runtime = "hermes";
|
|
@@ -1077,11 +1034,13 @@ function configureHermes(opts) {
|
|
|
1077
1034
|
}
|
|
1078
1035
|
const env = opts.env ?? process.env;
|
|
1079
1036
|
const suppliedSecret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
|
|
1037
|
+
const daemonExec = resolveDaemonExec();
|
|
1080
1038
|
const install = installHermes({
|
|
1081
1039
|
homeDir: home,
|
|
1082
1040
|
payloadBaseDir: base,
|
|
1083
1041
|
platform: process.platform,
|
|
1084
|
-
binPath:
|
|
1042
|
+
binPath: daemonExec.binPath,
|
|
1043
|
+
nodePath: daemonExec.nodePath,
|
|
1085
1044
|
webhookUrl: wh.url,
|
|
1086
1045
|
...suppliedSecret ? { webhookSecret: suppliedSecret } : {},
|
|
1087
1046
|
...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
|
|
@@ -1094,7 +1053,16 @@ function configureHermes(opts) {
|
|
|
1094
1053
|
return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
|
|
1095
1054
|
}
|
|
1096
1055
|
recordRuntime(runtime);
|
|
1097
|
-
|
|
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
|
+
}
|
|
1065
|
+
const envFile = path7.join(home, ".kojee", "hermes.env");
|
|
1098
1066
|
lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
|
|
1099
1067
|
lines.push("");
|
|
1100
1068
|
lines.push(install.output);
|
|
@@ -1116,7 +1084,7 @@ function configureHermes(opts) {
|
|
|
1116
1084
|
return { runtime, output: lines.join("\n"), exitCode: 0 };
|
|
1117
1085
|
}
|
|
1118
1086
|
function openclawConfigPath(opts) {
|
|
1119
|
-
return opts.openclawConfigPath ??
|
|
1087
|
+
return opts.openclawConfigPath ?? path7.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
|
|
1120
1088
|
}
|
|
1121
1089
|
function configureOpenclaw(opts) {
|
|
1122
1090
|
const runtime = "openclaw";
|
|
@@ -1164,9 +1132,9 @@ async function runWizardUninstall(runtime, opts) {
|
|
|
1164
1132
|
removedMcp ? ` removed mcp_servers.kojee from ${hermesConfig} (operator config preserved)` : ` no mcp_servers.kojee in ${hermesConfig} \u2014 nothing to remove`
|
|
1165
1133
|
);
|
|
1166
1134
|
lines.push(" (stop the daemon service + reload the gateway to disable the webhook wake path.)");
|
|
1167
|
-
const envPath =
|
|
1135
|
+
const envPath = path7.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
|
|
1168
1136
|
try {
|
|
1169
|
-
|
|
1137
|
+
fs6.unlinkSync(envPath);
|
|
1170
1138
|
lines.push(` removed ${envPath}`);
|
|
1171
1139
|
} catch {
|
|
1172
1140
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/wizard/capabilities/hermes-mcp-config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { parseDocument, isMap } from "yaml";
|
|
5
|
+
var HERMES_MCP_SERVER_NAME = "kojee";
|
|
6
|
+
var HERMES_INSTANCE_KEY = "hermes-gateway";
|
|
7
|
+
var HERMES_MCP_CONNECT_TIMEOUT_S = 120;
|
|
8
|
+
function defaultHermesConfigPath(homeDir) {
|
|
9
|
+
return path.join(homeDir, ".hermes", "config.yaml");
|
|
10
|
+
}
|
|
11
|
+
function buildKojeeEntry(binPath) {
|
|
12
|
+
return {
|
|
13
|
+
command: binPath,
|
|
14
|
+
args: [],
|
|
15
|
+
env: {
|
|
16
|
+
KOJEE_RUNTIME: "hermes",
|
|
17
|
+
KOJEE_INSTANCE: HERMES_INSTANCE_KEY,
|
|
18
|
+
// Vanilla tools-only proxy: NEVER a second webhook delivery (see header).
|
|
19
|
+
KOJEE_WEBHOOK_URL: ""
|
|
20
|
+
},
|
|
21
|
+
connect_timeout: HERMES_MCP_CONNECT_TIMEOUT_S
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function resolveMode(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.statSync(filePath).mode & 511;
|
|
27
|
+
} catch {
|
|
28
|
+
return 384;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function atomicWrite(filePath, content, mode) {
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
34
|
+
fs.writeFileSync(tmp, content, { mode });
|
|
35
|
+
fs.renameSync(tmp, filePath);
|
|
36
|
+
}
|
|
37
|
+
function backupSuffix(now) {
|
|
38
|
+
return (now ? now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
39
|
+
}
|
|
40
|
+
function writeHermesMcpConfig(configPath, opts) {
|
|
41
|
+
let raw = null;
|
|
42
|
+
try {
|
|
43
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
44
|
+
} catch {
|
|
45
|
+
raw = null;
|
|
46
|
+
}
|
|
47
|
+
let backedUp;
|
|
48
|
+
let doc = parseDocument("");
|
|
49
|
+
if (raw !== null && raw.trim() !== "") {
|
|
50
|
+
const parsed = parseDocument(raw);
|
|
51
|
+
if (parsed.errors.length > 0) {
|
|
52
|
+
backedUp = `${configPath}.corrupt-${backupSuffix(opts.now)}`;
|
|
53
|
+
fs.copyFileSync(configPath, backedUp);
|
|
54
|
+
} else {
|
|
55
|
+
doc = parsed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (doc.hasIn(["mcp_servers"]) && !isMap(doc.getIn(["mcp_servers"], true))) {
|
|
59
|
+
doc.deleteIn(["mcp_servers"]);
|
|
60
|
+
}
|
|
61
|
+
doc.setIn(["mcp_servers", HERMES_MCP_SERVER_NAME], buildKojeeEntry(opts.binPath));
|
|
62
|
+
atomicWrite(configPath, String(doc), resolveMode(configPath));
|
|
63
|
+
return { configPath, ...backedUp ? { backedUp } : {} };
|
|
64
|
+
}
|
|
65
|
+
function removeHermesMcpServer(configPath) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const doc = parseDocument(raw);
|
|
73
|
+
if (doc.errors.length > 0) return false;
|
|
74
|
+
if (!doc.hasIn(["mcp_servers", HERMES_MCP_SERVER_NAME])) return false;
|
|
75
|
+
doc.deleteIn(["mcp_servers", HERMES_MCP_SERVER_NAME]);
|
|
76
|
+
const servers = doc.getIn(["mcp_servers"], true);
|
|
77
|
+
if (isMap(servers) && servers.items.length === 0) {
|
|
78
|
+
doc.deleteIn(["mcp_servers"]);
|
|
79
|
+
}
|
|
80
|
+
atomicWrite(configPath, String(doc), resolveMode(configPath));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
HERMES_MCP_SERVER_NAME,
|
|
86
|
+
HERMES_INSTANCE_KEY,
|
|
87
|
+
defaultHermesConfigPath,
|
|
88
|
+
writeHermesMcpConfig,
|
|
89
|
+
removeHermesMcpServer
|
|
90
|
+
};
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,9 +2,10 @@ import {
|
|
|
2
2
|
DEFAULT_BROKER_URL,
|
|
3
3
|
reconcileConnectPairSlot,
|
|
4
4
|
runWizard
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CPOK642I.js";
|
|
6
6
|
import "./chunk-6XWTUDWW.js";
|
|
7
7
|
import "./chunk-E6WMFMM2.js";
|
|
8
|
+
import "./chunk-GEGUWQYT.js";
|
|
8
9
|
import "./chunk-77HWBSRH.js";
|
|
9
10
|
import "./chunk-TMCNB4JH.js";
|
|
10
11
|
import "./chunk-D6JKFJ6A.js";
|
|
@@ -365,6 +365,12 @@ 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-WLDI4R6P.js");
|
|
369
|
+
if (hermesArtifactsPresent()) {
|
|
370
|
+
const hermesReport = collectHermesDoctorReport();
|
|
371
|
+
console.error(formatHermesDoctorReport(hermesReport));
|
|
372
|
+
return hermesReport.verdict === "broken" ? 1 : 0;
|
|
373
|
+
}
|
|
368
374
|
const report = await collectDoctorReport();
|
|
369
375
|
console.error(formatDoctorReport(report));
|
|
370
376
|
return report.verdict === "broken" ? 1 : 0;
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HERMES_INSTANCE_KEY,
|
|
3
|
+
HERMES_MCP_SERVER_NAME
|
|
4
|
+
} from "./chunk-GEGUWQYT.js";
|
|
5
|
+
|
|
6
|
+
// src/doctor-hermes.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import { execFileSync } from "child_process";
|
|
12
|
+
import { parse } from "yaml";
|
|
13
|
+
|
|
14
|
+
// src/reconcile/index.ts
|
|
15
|
+
var FindingCollector = class {
|
|
16
|
+
items = [];
|
|
17
|
+
/** Append a pre-built Finding. */
|
|
18
|
+
add(finding) {
|
|
19
|
+
this.items.push(finding);
|
|
20
|
+
}
|
|
21
|
+
/** Append a Finding from its parts (the common call site). */
|
|
22
|
+
push(id, severity, detail) {
|
|
23
|
+
this.items.push({ id, severity, detail });
|
|
24
|
+
}
|
|
25
|
+
/** The findings in insertion order (read-only view). */
|
|
26
|
+
get findings() {
|
|
27
|
+
return this.items;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Roll the findings up into the doctor verdict: any `broken` ⇒ broken; else any
|
|
31
|
+
* `warn` ⇒ degraded; else healthy. `unknown` never fails the verdict (it is an
|
|
32
|
+
* "I couldn't determine this" signal, same as the doctor's `?` marks).
|
|
33
|
+
*/
|
|
34
|
+
verdict() {
|
|
35
|
+
if (this.items.some((f) => f.severity === "broken")) return "broken";
|
|
36
|
+
if (this.items.some((f) => f.severity === "warn")) return "degraded";
|
|
37
|
+
return "healthy";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/doctor-hermes.ts
|
|
42
|
+
var WIZARD_RERUN = "re-run `kojee-mcp init --runtime hermes`";
|
|
43
|
+
var DEFAULT_LISTEN_HOST = "127.0.0.1";
|
|
44
|
+
var DEFAULT_LISTEN_PORT = "8645";
|
|
45
|
+
var DEFAULT_LISTEN_PATH = "/kojee-tandem";
|
|
46
|
+
function serviceUnitPath(homeDir, platform) {
|
|
47
|
+
if (platform === "darwin") {
|
|
48
|
+
return path.join(homeDir, "Library", "LaunchAgents", "net.kojee.kojee-hermes.plist");
|
|
49
|
+
}
|
|
50
|
+
return path.join(homeDir, ".config", "systemd", "user", "kojee-hermes.service");
|
|
51
|
+
}
|
|
52
|
+
function hermesEnvPathFor(homeDir) {
|
|
53
|
+
return path.join(homeDir, ".kojee", "hermes.env");
|
|
54
|
+
}
|
|
55
|
+
function hermesArtifactsPresent(deps = {}) {
|
|
56
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
57
|
+
const platform = deps.platform ?? process.platform;
|
|
58
|
+
const exists = deps.exists ?? ((p) => fs.existsSync(p));
|
|
59
|
+
return exists(hermesEnvPathFor(homeDir)) || exists(serviceUnitPath(homeDir, platform));
|
|
60
|
+
}
|
|
61
|
+
function unquoteEnvValue(raw) {
|
|
62
|
+
let v = raw.trim();
|
|
63
|
+
if (v.length >= 2 && (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"'))) {
|
|
64
|
+
v = v.slice(1, -1);
|
|
65
|
+
}
|
|
66
|
+
return v.replace(/'\\''/g, "'");
|
|
67
|
+
}
|
|
68
|
+
function parseEnvFile(text) {
|
|
69
|
+
const vars = /* @__PURE__ */ new Map();
|
|
70
|
+
const exportKeys = [];
|
|
71
|
+
for (const rawLine of text.split("\n")) {
|
|
72
|
+
const line = rawLine.trim();
|
|
73
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
74
|
+
const m = line.match(/^(export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
75
|
+
if (!m) continue;
|
|
76
|
+
const key = m[2];
|
|
77
|
+
if (m[1] !== void 0) exportKeys.push(key);
|
|
78
|
+
vars.set(key, unquoteEnvValue(m[3]));
|
|
79
|
+
}
|
|
80
|
+
return { vars, exportKeys };
|
|
81
|
+
}
|
|
82
|
+
function readChildEnv(configText) {
|
|
83
|
+
if (configText === null) return null;
|
|
84
|
+
let doc;
|
|
85
|
+
try {
|
|
86
|
+
doc = parse(configText);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!doc || typeof doc !== "object") return null;
|
|
91
|
+
const servers = doc["mcp_servers"];
|
|
92
|
+
if (!servers || typeof servers !== "object") return null;
|
|
93
|
+
const kojee = servers[HERMES_MCP_SERVER_NAME];
|
|
94
|
+
if (!kojee || typeof kojee !== "object") return null;
|
|
95
|
+
const env = kojee["env"];
|
|
96
|
+
if (!env || typeof env !== "object") return null;
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(env)) {
|
|
99
|
+
if (typeof v === "string") out[k] = v;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function sha256(value) {
|
|
104
|
+
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
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
|
+
}
|
|
127
|
+
function defaultReadText(filePath) {
|
|
128
|
+
try {
|
|
129
|
+
return fs.readFileSync(filePath, "utf8");
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function defaultProbeService(platform) {
|
|
135
|
+
return () => {
|
|
136
|
+
try {
|
|
137
|
+
if (platform === "darwin") {
|
|
138
|
+
const out2 = execFileSync("launchctl", ["list"], { encoding: "utf8" });
|
|
139
|
+
return { active: out2.includes("net.kojee.kojee-hermes") };
|
|
140
|
+
}
|
|
141
|
+
const out = execFileSync("systemctl", ["--user", "is-active", "kojee-hermes"], {
|
|
142
|
+
encoding: "utf8"
|
|
143
|
+
}).trim();
|
|
144
|
+
return { active: out === "active" };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const stdout = err.stdout;
|
|
147
|
+
if (stdout !== void 0) {
|
|
148
|
+
const s = (typeof stdout === "string" ? stdout : stdout.toString()).trim();
|
|
149
|
+
if (s.length > 0) return { active: s === "active" };
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function defaultProbeReceiver(platform) {
|
|
156
|
+
return (_host, port) => {
|
|
157
|
+
try {
|
|
158
|
+
if (platform === "darwin") {
|
|
159
|
+
try {
|
|
160
|
+
execFileSync("lsof", [`-iTCP:${port}`, "-sTCP:LISTEN", "-n", "-P"], { encoding: "utf8" });
|
|
161
|
+
return true;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err.status === 1) return false;
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const out = execFileSync("ss", ["-ltnH"], { encoding: "utf8" });
|
|
168
|
+
const re = new RegExp(`:${port}(\\s|$)`);
|
|
169
|
+
return out.split("\n").some((l) => re.test(l));
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function defaultReadDaemonEnv(platform) {
|
|
176
|
+
return () => {
|
|
177
|
+
if (platform !== "linux") return null;
|
|
178
|
+
try {
|
|
179
|
+
const pidStr = execFileSync(
|
|
180
|
+
"systemctl",
|
|
181
|
+
["--user", "show", "-p", "MainPID", "--value", "kojee-hermes"],
|
|
182
|
+
{ encoding: "utf8" }
|
|
183
|
+
).trim();
|
|
184
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
185
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
186
|
+
const raw = fs.readFileSync(`/proc/${pid}/environ`, "utf8");
|
|
187
|
+
const out = {};
|
|
188
|
+
for (const pair of raw.split("\0")) {
|
|
189
|
+
const eq = pair.indexOf("=");
|
|
190
|
+
if (eq <= 0) continue;
|
|
191
|
+
out[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function okFromSeverity(sev) {
|
|
200
|
+
return sev === "ok" ? true : sev === "warn" ? "warn" : sev === "broken" ? false : "unknown";
|
|
201
|
+
}
|
|
202
|
+
function emit(checks, collector, id, severity, detail) {
|
|
203
|
+
checks.push({ name: id, ok: okFromSeverity(severity), detail });
|
|
204
|
+
collector.push(id, severity, detail);
|
|
205
|
+
}
|
|
206
|
+
function collectHermesDoctorReport(deps = {}) {
|
|
207
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
208
|
+
const platform = deps.platform ?? process.platform;
|
|
209
|
+
const readText = deps.readText ?? defaultReadText;
|
|
210
|
+
const exists = deps.exists ?? ((p) => fs.existsSync(p));
|
|
211
|
+
const probeService = deps.probeService ?? defaultProbeService(platform);
|
|
212
|
+
const probeReceiver = deps.probeReceiver ?? defaultProbeReceiver(platform);
|
|
213
|
+
const readDaemonEnv = deps.readDaemonEnv ?? defaultReadDaemonEnv(platform);
|
|
214
|
+
const hermesEnvPath = hermesEnvPathFor(homeDir);
|
|
215
|
+
const adapterEnvPath = path.join(homeDir, ".hermes", ".env");
|
|
216
|
+
const configYamlPath = path.join(homeDir, ".hermes", "config.yaml");
|
|
217
|
+
const unitPath = serviceUnitPath(homeDir, platform);
|
|
218
|
+
const daemonEnvText = readText(hermesEnvPath);
|
|
219
|
+
const adapterEnvText = readText(adapterEnvPath);
|
|
220
|
+
const unitText = readText(unitPath);
|
|
221
|
+
const configText = readText(configYamlPath);
|
|
222
|
+
const daemonEnv = daemonEnvText !== null ? parseEnvFile(daemonEnvText) : null;
|
|
223
|
+
const adapterEnv = adapterEnvText !== null ? parseEnvFile(adapterEnvText) : null;
|
|
224
|
+
const childEnv = readChildEnv(configText);
|
|
225
|
+
const pluginEnabled = readPluginEnabled(configText);
|
|
226
|
+
const checks = [];
|
|
227
|
+
const collector = new FindingCollector();
|
|
228
|
+
if (daemonEnv === null) {
|
|
229
|
+
emit(checks, collector, "webhook.env-format", "unknown", `hermes.env unreadable at ${hermesEnvPath}`);
|
|
230
|
+
} else if (daemonEnv.exportKeys.length > 0) {
|
|
231
|
+
const unitUsesEnvFile = unitText !== null && /^\s*EnvironmentFile=/m.test(unitText);
|
|
232
|
+
const keys = daemonEnv.exportKeys.join(", ");
|
|
233
|
+
if (unitUsesEnvFile) {
|
|
234
|
+
emit(
|
|
235
|
+
checks,
|
|
236
|
+
collector,
|
|
237
|
+
"webhook.env-format",
|
|
238
|
+
"broken",
|
|
239
|
+
`hermes.env has export-prefixed line(s) [${keys}] AND the service unit uses EnvironmentFile= \u2014 systemd DROPS them, so the daemon boots with those vars UNSET (silent webhook-off + seat churn). ${WIZARD_RERUN} to rewrite the unit (source wrapper) + env (bare).`
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
emit(
|
|
243
|
+
checks,
|
|
244
|
+
collector,
|
|
245
|
+
"webhook.env-format",
|
|
246
|
+
"warn",
|
|
247
|
+
`hermes.env has legacy export-prefixed line(s) [${keys}] \u2014 harmless under the source-wrapper unit (set -a exports them) but ${WIZARD_RERUN} to normalize to bare form.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
emit(
|
|
252
|
+
checks,
|
|
253
|
+
collector,
|
|
254
|
+
"webhook.env-format",
|
|
255
|
+
"ok",
|
|
256
|
+
"hermes.env is bare single-quoted (no export-prefixed lines) \u2014 sources cleanly."
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const daemonUrl = (daemonEnv?.vars.get("KOJEE_WEBHOOK_URL") ?? "").trim();
|
|
260
|
+
const listenHost = adapterEnv?.vars.get("KOJEE_TANDEM_LISTEN_HOST") ?? DEFAULT_LISTEN_HOST;
|
|
261
|
+
const listenPort = adapterEnv?.vars.get("KOJEE_TANDEM_LISTEN_PORT") ?? DEFAULT_LISTEN_PORT;
|
|
262
|
+
const listenPath = adapterEnv?.vars.get("KOJEE_TANDEM_LISTEN_PATH") ?? DEFAULT_LISTEN_PATH;
|
|
263
|
+
if (!daemonUrl) {
|
|
264
|
+
emit(
|
|
265
|
+
checks,
|
|
266
|
+
collector,
|
|
267
|
+
"webhook.url-receiver-align",
|
|
268
|
+
"broken",
|
|
269
|
+
`daemon KOJEE_WEBHOOK_URL is EMPTY \u2014 no event stream (null delivery); the daemon will not POST wakes. Set the receiver URL: ${WIZARD_RERUN}.`
|
|
270
|
+
);
|
|
271
|
+
} else {
|
|
272
|
+
let parsed = null;
|
|
273
|
+
try {
|
|
274
|
+
parsed = new URL(daemonUrl);
|
|
275
|
+
} catch {
|
|
276
|
+
parsed = null;
|
|
277
|
+
}
|
|
278
|
+
if (parsed === null) {
|
|
279
|
+
emit(
|
|
280
|
+
checks,
|
|
281
|
+
collector,
|
|
282
|
+
"webhook.url-receiver-align",
|
|
283
|
+
"broken",
|
|
284
|
+
`daemon KOJEE_WEBHOOK_URL is not a valid URL \u2014 ${WIZARD_RERUN}.`
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
const urlPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
288
|
+
const aligned = parsed.hostname === listenHost && urlPort === listenPort && parsed.pathname === listenPath;
|
|
289
|
+
emit(
|
|
290
|
+
checks,
|
|
291
|
+
collector,
|
|
292
|
+
"webhook.url-receiver-align",
|
|
293
|
+
aligned ? "ok" : "broken",
|
|
294
|
+
aligned ? `daemon URL host/port/path aligns with the receiver bind (${listenHost}:${listenPort}${listenPath}).` : `daemon URL (${parsed.hostname}:${urlPort}${parsed.pathname}) != receiver bind (${listenHost}:${listenPort}${listenPath}) \u2014 POSTs miss the listener. Align them.`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const daemonSecret = daemonEnv?.vars.get("KOJEE_WEBHOOK_SECRET");
|
|
299
|
+
const adapterSecret = adapterEnv?.vars.get("KOJEE_WEBHOOK_SECRET");
|
|
300
|
+
if (!daemonSecret || !adapterSecret) {
|
|
301
|
+
const which = [!daemonSecret ? "daemon (hermes.env)" : null, !adapterSecret ? "adapter (~/.hermes/.env)" : null].filter((s) => s !== null).join(" and ");
|
|
302
|
+
emit(
|
|
303
|
+
checks,
|
|
304
|
+
collector,
|
|
305
|
+
"webhook.secret-match",
|
|
306
|
+
"broken",
|
|
307
|
+
`KOJEE_WEBHOOK_SECRET missing on ${which} \u2014 the receiver can't verify signed POSTs (401). ${WIZARD_RERUN}.`
|
|
308
|
+
);
|
|
309
|
+
} else {
|
|
310
|
+
const same = sha256(daemonSecret) === sha256(adapterSecret);
|
|
311
|
+
emit(
|
|
312
|
+
checks,
|
|
313
|
+
collector,
|
|
314
|
+
"webhook.secret-match",
|
|
315
|
+
same ? "ok" : "broken",
|
|
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).`
|
|
317
|
+
);
|
|
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
|
+
}
|
|
359
|
+
const svc = probeService();
|
|
360
|
+
if (svc === null) {
|
|
361
|
+
emit(
|
|
362
|
+
checks,
|
|
363
|
+
collector,
|
|
364
|
+
"webhook.pusher-up",
|
|
365
|
+
"unknown",
|
|
366
|
+
"could not determine kojee-hermes service state (systemctl/launchctl unavailable)."
|
|
367
|
+
);
|
|
368
|
+
} else if (svc.active) {
|
|
369
|
+
emit(checks, collector, "webhook.pusher-up", "ok", "kojee-hermes service is active.");
|
|
370
|
+
} else {
|
|
371
|
+
const why = execTargets && execTargets.some((p) => !exists(p)) ? " (likely the missing ExecStart binary above \u2014 exit 127)" : "";
|
|
372
|
+
emit(
|
|
373
|
+
checks,
|
|
374
|
+
collector,
|
|
375
|
+
"webhook.pusher-up",
|
|
376
|
+
"broken",
|
|
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`."
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const receiver = probeReceiver(listenHost, Number.parseInt(listenPort, 10));
|
|
406
|
+
if (receiver === null) {
|
|
407
|
+
emit(
|
|
408
|
+
checks,
|
|
409
|
+
collector,
|
|
410
|
+
"webhook.receiver-up",
|
|
411
|
+
"unknown",
|
|
412
|
+
`could not probe the receiver bind ${listenHost}:${listenPort} (ss/lsof unavailable).`
|
|
413
|
+
);
|
|
414
|
+
} else if (receiver) {
|
|
415
|
+
emit(
|
|
416
|
+
checks,
|
|
417
|
+
collector,
|
|
418
|
+
"webhook.receiver-up",
|
|
419
|
+
"ok",
|
|
420
|
+
`receiver is listening on ${listenHost}:${listenPort}.`
|
|
421
|
+
);
|
|
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";
|
|
424
|
+
emit(
|
|
425
|
+
checks,
|
|
426
|
+
collector,
|
|
427
|
+
"webhook.receiver-up",
|
|
428
|
+
"warn",
|
|
429
|
+
`nothing is listening on ${listenHost}:${listenPort} \u2014 ${fix}.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (childEnv === null) {
|
|
433
|
+
emit(
|
|
434
|
+
checks,
|
|
435
|
+
collector,
|
|
436
|
+
"webhook.no-duplicate",
|
|
437
|
+
"unknown",
|
|
438
|
+
`could not read mcp_servers.${HERMES_MCP_SERVER_NAME}.env from ~/.hermes/config.yaml.`
|
|
439
|
+
);
|
|
440
|
+
} else {
|
|
441
|
+
const childUrl = (childEnv["KOJEE_WEBHOOK_URL"] ?? "").trim();
|
|
442
|
+
emit(
|
|
443
|
+
checks,
|
|
444
|
+
collector,
|
|
445
|
+
"webhook.no-duplicate",
|
|
446
|
+
childUrl === "" ? "ok" : "broken",
|
|
447
|
+
childUrl === "" ? "mcp child KOJEE_WEBHOOK_URL is empty (hard-off) \u2014 no duplicate delivery." : `mcp_servers.${HERMES_MCP_SERVER_NAME} has a NON-EMPTY KOJEE_WEBHOOK_URL \u2014 the tools-only child would DOUBLE-DELIVER wakes for the seat. Clear it (keep the empty hard-off).`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const daemonInstance = (daemonEnv?.vars.get("KOJEE_INSTANCE") ?? "").trim();
|
|
451
|
+
const childInstance = (childEnv?.["KOJEE_INSTANCE"] ?? "").trim();
|
|
452
|
+
if (!daemonInstance) {
|
|
453
|
+
emit(
|
|
454
|
+
checks,
|
|
455
|
+
collector,
|
|
456
|
+
"webhook.instance-align",
|
|
457
|
+
"broken",
|
|
458
|
+
`daemon KOJEE_INSTANCE is unset \u2014 daemon + MCP child may resolve to DIFFERENT sessions (split-brain seat). Expected '${HERMES_INSTANCE_KEY}'. ${WIZARD_RERUN}.`
|
|
459
|
+
);
|
|
460
|
+
} else if (childInstance && daemonInstance !== childInstance) {
|
|
461
|
+
emit(
|
|
462
|
+
checks,
|
|
463
|
+
collector,
|
|
464
|
+
"webhook.instance-align",
|
|
465
|
+
"broken",
|
|
466
|
+
`daemon KOJEE_INSTANCE='${daemonInstance}' != mcp child KOJEE_INSTANCE='${childInstance}' \u2014 split-brain: a tandem_send from the agent lands on a different seat than the daemon streams wakes for. ${WIZARD_RERUN}.`
|
|
467
|
+
);
|
|
468
|
+
} else if (daemonInstance !== HERMES_INSTANCE_KEY) {
|
|
469
|
+
emit(
|
|
470
|
+
checks,
|
|
471
|
+
collector,
|
|
472
|
+
"webhook.instance-align",
|
|
473
|
+
"warn",
|
|
474
|
+
`daemon KOJEE_INSTANCE='${daemonInstance}' != expected '${HERMES_INSTANCE_KEY}' (non-standard but self-consistent with the child).`
|
|
475
|
+
);
|
|
476
|
+
} else {
|
|
477
|
+
emit(
|
|
478
|
+
checks,
|
|
479
|
+
collector,
|
|
480
|
+
"webhook.instance-align",
|
|
481
|
+
"ok",
|
|
482
|
+
`daemon + mcp child share KOJEE_INSTANCE='${HERMES_INSTANCE_KEY}' \u2014 one backend seat.`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (platform !== "linux") {
|
|
486
|
+
emit(
|
|
487
|
+
checks,
|
|
488
|
+
collector,
|
|
489
|
+
"webhook.env-reaches-daemon",
|
|
490
|
+
"unknown",
|
|
491
|
+
"n/a \u2014 reads /proc/<pid>/environ (Linux only)."
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
const liveEnv = readDaemonEnv();
|
|
495
|
+
if (liveEnv === null) {
|
|
496
|
+
emit(
|
|
497
|
+
checks,
|
|
498
|
+
collector,
|
|
499
|
+
"webhook.env-reaches-daemon",
|
|
500
|
+
"unknown",
|
|
501
|
+
"daemon not running (no MainPID) or its /proc environ is unreadable."
|
|
502
|
+
);
|
|
503
|
+
} else {
|
|
504
|
+
const missing = ["KOJEE_WEBHOOK_URL", "KOJEE_WEBHOOK_SECRET", "KOJEE_INSTANCE"].filter(
|
|
505
|
+
(k) => !((liveEnv[k] ?? "").length > 0)
|
|
506
|
+
);
|
|
507
|
+
emit(
|
|
508
|
+
checks,
|
|
509
|
+
collector,
|
|
510
|
+
"webhook.env-reaches-daemon",
|
|
511
|
+
missing.length === 0 ? "ok" : "broken",
|
|
512
|
+
missing.length === 0 ? "live daemon env carries KOJEE_WEBHOOK_URL/SECRET/INSTANCE (injection reached the process; secret value never read)." : `live daemon env is MISSING ${missing.join(", ")} \u2014 env injection did NOT reach the running daemon (source wrapper not applied / stale unit). ${WIZARD_RERUN}, then restart the service.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return { checks, findings: collector.findings, verdict: collector.verdict() };
|
|
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
|
+
};
|
|
530
|
+
function formatHermesDoctorReport(report) {
|
|
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";
|
|
538
|
+
const lines = [];
|
|
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");
|
|
548
|
+
lines.push("");
|
|
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
|
+
});
|
|
569
|
+
}
|
|
570
|
+
lines.push("");
|
|
571
|
+
lines.push(
|
|
572
|
+
" (read-only. Enabling/restarting the daemon, plugin, or gateway is an owner step \u2014 shared infra needs the principal's word.)"
|
|
573
|
+
);
|
|
574
|
+
return lines.join("\n");
|
|
575
|
+
}
|
|
576
|
+
export {
|
|
577
|
+
collectHermesDoctorReport,
|
|
578
|
+
formatHermesDoctorReport,
|
|
579
|
+
hermesArtifactsPresent
|
|
580
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -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,9 +3,10 @@ import {
|
|
|
3
3
|
pairSlotFor,
|
|
4
4
|
reconcileConnectPairSlot,
|
|
5
5
|
runWizard
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CPOK642I.js";
|
|
7
7
|
import "./chunk-6XWTUDWW.js";
|
|
8
8
|
import "./chunk-E6WMFMM2.js";
|
|
9
|
+
import "./chunk-GEGUWQYT.js";
|
|
9
10
|
import {
|
|
10
11
|
isWizardRuntime
|
|
11
12
|
} from "./chunk-77HWBSRH.js";
|