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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +27 -6
  2. package/dist/{cc-session-id-RURNIHHC.js → cc-session-id-5HHQJ7VC.js} +3 -1
  3. package/dist/{chunk-ZSHCHOPL.js → chunk-676WQSWZ.js} +163 -7
  4. package/dist/{chunk-FJUAMJHU.js → chunk-6NPMVZD7.js} +38 -3
  5. package/dist/{chunk-247WFMCJ.js → chunk-AGZ7ZGLY.js} +30 -3
  6. package/dist/chunk-AXBJQJKA.js +65 -0
  7. package/dist/{chunk-TCWIXG5C.js → chunk-CCVRXJJ2.js} +1 -1
  8. package/dist/{chunk-WBXU27BF.js → chunk-FNMKP34Y.js} +1 -1
  9. package/dist/chunk-GEGUWQYT.js +90 -0
  10. package/dist/{chunk-NE2F5CKS.js → chunk-LKGKDZSO.js} +9 -5
  11. package/dist/{chunk-XPIW4N55.js → chunk-N2YAZRAD.js} +178 -43
  12. package/dist/{chunk-LSUB6QMP.js → chunk-NIIBVKLP.js} +3 -0
  13. package/dist/chunk-P64UR3SE.js +86 -0
  14. package/dist/{chunk-A4IOKD4Z.js → chunk-QYZP6QT2.js} +2 -4
  15. package/dist/{chunk-KNEJTD6G.js → chunk-SZLNNMIZ.js} +6 -2
  16. package/dist/{chunk-CUOJHP2S.js → chunk-UGEBGCYU.js} +48 -12
  17. package/dist/{chunk-6XWTUDWW.js → chunk-WDWVBPXA.js} +7 -5
  18. package/dist/{chunk-TNZITJAB.js → chunk-WVQU3QQ7.js} +14 -4
  19. package/dist/cli.js +42 -23
  20. package/dist/{codex-prompt-submit-hook-J5PZEJSK.js → codex-prompt-submit-hook-UQJI6A54.js} +2 -2
  21. package/dist/{codex-stop-hook-32W4BIOM.js → codex-stop-hook-GTGRQNVV.js} +2 -2
  22. package/dist/{connect-handler-NZINEMG3.js → connect-handler-GTFXNZUC.js} +68 -9
  23. package/dist/{doctor-WUU5BVPT.js → doctor-54S7JGOE.js} +64 -5
  24. package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-6H4OOS6O.js} +84 -10
  25. package/dist/doctor-hermes-KMHM27Y3.js +597 -0
  26. package/dist/{doctor-openclaw-LL6NSPLC.js → doctor-openclaw-B5KADVHD.js} +5 -1
  27. package/dist/{event-log-2NBJEIEP.js → event-log-M22X3FAI.js} +1 -1
  28. package/dist/{event-stream-ATKYXDBV.js → event-stream-5ZEL7FXN.js} +5 -1
  29. package/dist/flap-log-D4KG7RK4.js +23 -0
  30. package/dist/{gateway-client-CbM2OC_w.d.ts → gateway-client-CLmQghON.d.ts} +11 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +6 -7
  33. package/dist/{install-JQNDGAAQ.js → install-GJVZEWLI.js} +8 -7
  34. package/dist/lib.d.ts +30 -1
  35. package/dist/lib.js +3 -3
  36. package/dist/plugins/hermes/adapter.py +7 -2
  37. package/dist/{registry-XJ67EO22.js → registry-MCJAVXJW.js} +36 -24
  38. package/dist/{send-cli-45RLGYIC.js → send-cli-WTY7H3BR.js} +2 -2
  39. package/dist/{server-DU3LFS32.js → server-VM3ZIGSZ.js} +2 -3
  40. package/dist/session-start-hook-VL3KYDTT.js +25 -0
  41. package/dist/{setup-handler-44ASXYMS.js → setup-handler-GNAIWAYU.js} +7 -7
  42. package/dist/{stop-hook-N6TX4YQT.js → stop-hook-BLPYRE7T.js} +5 -5
  43. package/dist/stream-watchdog-K2AWNCQW.js +64 -0
  44. package/dist/{tail-stream-G4WGTXDS.js → tail-stream-PQXEY3TI.js} +120 -9
  45. package/dist/{user-prompt-submit-hook-DTXXFDSD.js → user-prompt-submit-hook-YQXGE3JU.js} +3 -3
  46. package/package.json +3 -2
  47. package/dist/chunk-5DHIUN73.js +0 -25
  48. package/dist/chunk-TMCNB4JH.js +0 -33
@@ -0,0 +1,597 @@
1
+ import {
2
+ HERMES_INSTANCE_KEY,
3
+ HERMES_MCP_SERVER_NAME
4
+ } from "./chunk-GEGUWQYT.js";
5
+ import {
6
+ buildFlapDoctorCheck
7
+ } from "./chunk-P64UR3SE.js";
8
+ import "./chunk-SQL56SEB.js";
9
+
10
+ // src/doctor-hermes.ts
11
+ import fs from "fs";
12
+ import os from "os";
13
+ import path from "path";
14
+ import crypto from "crypto";
15
+ import { execFileSync } from "child_process";
16
+ import { parse } from "yaml";
17
+
18
+ // src/reconcile/index.ts
19
+ var FindingCollector = class {
20
+ items = [];
21
+ /** Append a pre-built Finding. */
22
+ add(finding) {
23
+ this.items.push(finding);
24
+ }
25
+ /** Append a Finding from its parts (the common call site). */
26
+ push(id, severity, detail) {
27
+ this.items.push({ id, severity, detail });
28
+ }
29
+ /** The findings in insertion order (read-only view). */
30
+ get findings() {
31
+ return this.items;
32
+ }
33
+ /**
34
+ * Roll the findings up into the doctor verdict: any `broken` ⇒ broken; else any
35
+ * `warn` ⇒ degraded; else healthy. `unknown` never fails the verdict (it is an
36
+ * "I couldn't determine this" signal, same as the doctor's `?` marks).
37
+ */
38
+ verdict() {
39
+ if (this.items.some((f) => f.severity === "broken")) return "broken";
40
+ if (this.items.some((f) => f.severity === "warn")) return "degraded";
41
+ return "healthy";
42
+ }
43
+ };
44
+
45
+ // src/doctor-hermes.ts
46
+ var WIZARD_RERUN = "re-run `kojee-mcp init --runtime hermes`";
47
+ var DEFAULT_LISTEN_HOST = "127.0.0.1";
48
+ var DEFAULT_LISTEN_PORT = "8645";
49
+ var DEFAULT_LISTEN_PATH = "/kojee-tandem";
50
+ function serviceUnitPath(homeDir, platform) {
51
+ if (platform === "darwin") {
52
+ return path.join(homeDir, "Library", "LaunchAgents", "net.kojee.kojee-hermes.plist");
53
+ }
54
+ return path.join(homeDir, ".config", "systemd", "user", "kojee-hermes.service");
55
+ }
56
+ function hermesEnvPathFor(homeDir) {
57
+ return path.join(homeDir, ".kojee", "hermes.env");
58
+ }
59
+ function hermesArtifactsPresent(deps = {}) {
60
+ const homeDir = deps.homeDir ?? os.homedir();
61
+ const platform = deps.platform ?? process.platform;
62
+ const exists = deps.exists ?? ((p) => fs.existsSync(p));
63
+ return exists(hermesEnvPathFor(homeDir)) || exists(serviceUnitPath(homeDir, platform));
64
+ }
65
+ function unquoteEnvValue(raw) {
66
+ let v = raw.trim();
67
+ if (v.length >= 2 && (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"'))) {
68
+ v = v.slice(1, -1);
69
+ }
70
+ return v.replace(/'\\''/g, "'");
71
+ }
72
+ function parseEnvFile(text) {
73
+ const vars = /* @__PURE__ */ new Map();
74
+ const exportKeys = [];
75
+ for (const rawLine of text.split("\n")) {
76
+ const line = rawLine.trim();
77
+ if (line === "" || line.startsWith("#")) continue;
78
+ const m = line.match(/^(export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
79
+ if (!m) continue;
80
+ const key = m[2];
81
+ if (m[1] !== void 0) exportKeys.push(key);
82
+ vars.set(key, unquoteEnvValue(m[3]));
83
+ }
84
+ return { vars, exportKeys };
85
+ }
86
+ function readChildEnv(configText) {
87
+ if (configText === null) return null;
88
+ let doc;
89
+ try {
90
+ doc = parse(configText);
91
+ } catch {
92
+ return null;
93
+ }
94
+ if (!doc || typeof doc !== "object") return null;
95
+ const servers = doc["mcp_servers"];
96
+ if (!servers || typeof servers !== "object") return null;
97
+ const kojee = servers[HERMES_MCP_SERVER_NAME];
98
+ if (!kojee || typeof kojee !== "object") return null;
99
+ const env = kojee["env"];
100
+ if (!env || typeof env !== "object") return null;
101
+ const out = {};
102
+ for (const [k, v] of Object.entries(env)) {
103
+ if (typeof v === "string") out[k] = v;
104
+ }
105
+ return out;
106
+ }
107
+ function sha256(value) {
108
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
109
+ }
110
+ function readPluginEnabled(configText) {
111
+ if (configText === null) return null;
112
+ let doc;
113
+ try {
114
+ doc = parse(configText);
115
+ } catch {
116
+ return null;
117
+ }
118
+ if (!doc || typeof doc !== "object") return null;
119
+ const plugins = doc["plugins"];
120
+ if (!plugins || typeof plugins !== "object") return false;
121
+ const enabled = plugins["enabled"];
122
+ if (!Array.isArray(enabled)) return false;
123
+ return enabled.includes("kojee-tandem");
124
+ }
125
+ function parseExecTargets(unitText) {
126
+ if (unitText === null) return null;
127
+ const m = unitText.match(/exec\s+([^'"\n<]+)/);
128
+ if (!m) return null;
129
+ return m[1].trim().split(/\s+/).filter((tok) => tok.startsWith("/"));
130
+ }
131
+ function defaultReadText(filePath) {
132
+ try {
133
+ return fs.readFileSync(filePath, "utf8");
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ function defaultProbeService(platform) {
139
+ return () => {
140
+ try {
141
+ if (platform === "darwin") {
142
+ const out2 = execFileSync("launchctl", ["list"], { encoding: "utf8" });
143
+ return { active: out2.includes("net.kojee.kojee-hermes") };
144
+ }
145
+ const out = execFileSync("systemctl", ["--user", "is-active", "kojee-hermes"], {
146
+ encoding: "utf8"
147
+ }).trim();
148
+ return { active: out === "active" };
149
+ } catch (err) {
150
+ const stdout = err.stdout;
151
+ if (stdout !== void 0) {
152
+ const s = (typeof stdout === "string" ? stdout : stdout.toString()).trim();
153
+ if (s.length > 0) return { active: s === "active" };
154
+ }
155
+ return null;
156
+ }
157
+ };
158
+ }
159
+ function defaultProbeReceiver(platform) {
160
+ return (_host, port) => {
161
+ try {
162
+ if (platform === "darwin") {
163
+ try {
164
+ execFileSync("lsof", [`-iTCP:${port}`, "-sTCP:LISTEN", "-n", "-P"], { encoding: "utf8" });
165
+ return true;
166
+ } catch (err) {
167
+ if (err.status === 1) return false;
168
+ return null;
169
+ }
170
+ }
171
+ const out = execFileSync("ss", ["-ltnH"], { encoding: "utf8" });
172
+ const re = new RegExp(`:${port}(\\s|$)`);
173
+ return out.split("\n").some((l) => re.test(l));
174
+ } catch {
175
+ return null;
176
+ }
177
+ };
178
+ }
179
+ function defaultReadDaemonEnv(platform) {
180
+ return () => {
181
+ if (platform !== "linux") return null;
182
+ try {
183
+ const pidStr = execFileSync(
184
+ "systemctl",
185
+ ["--user", "show", "-p", "MainPID", "--value", "kojee-hermes"],
186
+ { encoding: "utf8" }
187
+ ).trim();
188
+ const pid = Number.parseInt(pidStr, 10);
189
+ if (!Number.isFinite(pid) || pid <= 0) return null;
190
+ const raw = fs.readFileSync(`/proc/${pid}/environ`, "utf8");
191
+ const out = {};
192
+ for (const pair of raw.split("\0")) {
193
+ const eq = pair.indexOf("=");
194
+ if (eq <= 0) continue;
195
+ out[pair.slice(0, eq)] = pair.slice(eq + 1);
196
+ }
197
+ return out;
198
+ } catch {
199
+ return null;
200
+ }
201
+ };
202
+ }
203
+ function okFromSeverity(sev) {
204
+ return sev === "ok" ? true : sev === "warn" ? "warn" : sev === "broken" ? false : "unknown";
205
+ }
206
+ function emit(checks, collector, id, severity, detail) {
207
+ checks.push({ name: id, ok: okFromSeverity(severity), detail });
208
+ collector.push(id, severity, detail);
209
+ }
210
+ function collectHermesDoctorReport(deps = {}) {
211
+ const homeDir = deps.homeDir ?? os.homedir();
212
+ const platform = deps.platform ?? process.platform;
213
+ const readText = deps.readText ?? defaultReadText;
214
+ const exists = deps.exists ?? ((p) => fs.existsSync(p));
215
+ const probeService = deps.probeService ?? defaultProbeService(platform);
216
+ const probeReceiver = deps.probeReceiver ?? defaultProbeReceiver(platform);
217
+ const readDaemonEnv = deps.readDaemonEnv ?? defaultReadDaemonEnv(platform);
218
+ const hermesEnvPath = hermesEnvPathFor(homeDir);
219
+ const adapterEnvPath = path.join(homeDir, ".hermes", ".env");
220
+ const configYamlPath = path.join(homeDir, ".hermes", "config.yaml");
221
+ const unitPath = serviceUnitPath(homeDir, platform);
222
+ const daemonEnvText = readText(hermesEnvPath);
223
+ const adapterEnvText = readText(adapterEnvPath);
224
+ const unitText = readText(unitPath);
225
+ const configText = readText(configYamlPath);
226
+ const daemonEnv = daemonEnvText !== null ? parseEnvFile(daemonEnvText) : null;
227
+ const adapterEnv = adapterEnvText !== null ? parseEnvFile(adapterEnvText) : null;
228
+ const childEnv = readChildEnv(configText);
229
+ const pluginEnabled = readPluginEnabled(configText);
230
+ const checks = [];
231
+ const collector = new FindingCollector();
232
+ if (daemonEnv === null) {
233
+ emit(checks, collector, "webhook.env-format", "unknown", `hermes.env unreadable at ${hermesEnvPath}`);
234
+ } else if (daemonEnv.exportKeys.length > 0) {
235
+ const unitUsesEnvFile = unitText !== null && /^\s*EnvironmentFile=/m.test(unitText);
236
+ const keys = daemonEnv.exportKeys.join(", ");
237
+ if (unitUsesEnvFile) {
238
+ emit(
239
+ checks,
240
+ collector,
241
+ "webhook.env-format",
242
+ "broken",
243
+ `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).`
244
+ );
245
+ } else {
246
+ emit(
247
+ checks,
248
+ collector,
249
+ "webhook.env-format",
250
+ "warn",
251
+ `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.`
252
+ );
253
+ }
254
+ } else {
255
+ emit(
256
+ checks,
257
+ collector,
258
+ "webhook.env-format",
259
+ "ok",
260
+ "hermes.env is bare single-quoted (no export-prefixed lines) \u2014 sources cleanly."
261
+ );
262
+ }
263
+ const daemonUrl = (daemonEnv?.vars.get("KOJEE_WEBHOOK_URL") ?? "").trim();
264
+ const listenHost = adapterEnv?.vars.get("KOJEE_TANDEM_LISTEN_HOST") ?? DEFAULT_LISTEN_HOST;
265
+ const listenPort = adapterEnv?.vars.get("KOJEE_TANDEM_LISTEN_PORT") ?? DEFAULT_LISTEN_PORT;
266
+ const listenPath = adapterEnv?.vars.get("KOJEE_TANDEM_LISTEN_PATH") ?? DEFAULT_LISTEN_PATH;
267
+ if (!daemonUrl) {
268
+ emit(
269
+ checks,
270
+ collector,
271
+ "webhook.url-receiver-align",
272
+ "broken",
273
+ `daemon KOJEE_WEBHOOK_URL is EMPTY \u2014 no event stream (null delivery); the daemon will not POST wakes. Set the receiver URL: ${WIZARD_RERUN}.`
274
+ );
275
+ } else {
276
+ let parsed = null;
277
+ try {
278
+ parsed = new URL(daemonUrl);
279
+ } catch {
280
+ parsed = null;
281
+ }
282
+ if (parsed === null) {
283
+ emit(
284
+ checks,
285
+ collector,
286
+ "webhook.url-receiver-align",
287
+ "broken",
288
+ `daemon KOJEE_WEBHOOK_URL is not a valid URL \u2014 ${WIZARD_RERUN}.`
289
+ );
290
+ } else {
291
+ const urlPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
292
+ const aligned = parsed.hostname === listenHost && urlPort === listenPort && parsed.pathname === listenPath;
293
+ emit(
294
+ checks,
295
+ collector,
296
+ "webhook.url-receiver-align",
297
+ aligned ? "ok" : "broken",
298
+ 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.`
299
+ );
300
+ }
301
+ }
302
+ const daemonSecret = daemonEnv?.vars.get("KOJEE_WEBHOOK_SECRET");
303
+ const adapterSecret = adapterEnv?.vars.get("KOJEE_WEBHOOK_SECRET");
304
+ if (!daemonSecret || !adapterSecret) {
305
+ const which = [!daemonSecret ? "daemon (hermes.env)" : null, !adapterSecret ? "adapter (~/.hermes/.env)" : null].filter((s) => s !== null).join(" and ");
306
+ emit(
307
+ checks,
308
+ collector,
309
+ "webhook.secret-match",
310
+ "broken",
311
+ `KOJEE_WEBHOOK_SECRET missing on ${which} \u2014 the receiver can't verify signed POSTs (401). ${WIZARD_RERUN}.`
312
+ );
313
+ } else {
314
+ const same = sha256(daemonSecret) === sha256(adapterSecret);
315
+ emit(
316
+ checks,
317
+ collector,
318
+ "webhook.secret-match",
319
+ same ? "ok" : "broken",
320
+ 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).`
321
+ );
322
+ }
323
+ const execTargets = parseExecTargets(unitText);
324
+ if (unitText === null) {
325
+ emit(checks, collector, "webhook.daemon-bin", "unknown", `service unit unreadable at ${unitPath}.`);
326
+ } else if (execTargets === null || execTargets.length === 0) {
327
+ emit(
328
+ checks,
329
+ collector,
330
+ "webhook.daemon-bin",
331
+ "warn",
332
+ "could not parse the ExecStart exec target from the unit \u2014 cannot verify the daemon binary."
333
+ );
334
+ } else {
335
+ const missing = execTargets.filter((p) => !exists(p));
336
+ const ephemeral = execTargets.find((p) => /[\\/]_npx[\\/]/.test(p));
337
+ if (missing.length > 0) {
338
+ emit(
339
+ checks,
340
+ collector,
341
+ "webhook.daemon-bin",
342
+ "broken",
343
+ `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\`.`
344
+ );
345
+ } else if (ephemeral) {
346
+ emit(
347
+ checks,
348
+ collector,
349
+ "webhook.daemon-bin",
350
+ "warn",
351
+ `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\`.`
352
+ );
353
+ } else {
354
+ emit(
355
+ checks,
356
+ collector,
357
+ "webhook.daemon-bin",
358
+ "ok",
359
+ `daemon ExecStart targets exist (${execTargets.join(" ")}) \u2014 node + CLI resolve without PATH.`
360
+ );
361
+ }
362
+ }
363
+ const svc = probeService();
364
+ if (svc === null) {
365
+ emit(
366
+ checks,
367
+ collector,
368
+ "webhook.pusher-up",
369
+ "unknown",
370
+ "could not determine kojee-hermes service state (systemctl/launchctl unavailable)."
371
+ );
372
+ } else if (svc.active) {
373
+ emit(checks, collector, "webhook.pusher-up", "ok", "kojee-hermes service is active.");
374
+ } else {
375
+ const why = execTargets && execTargets.some((p) => !exists(p)) ? " (likely the missing ExecStart binary above \u2014 exit 127)" : "";
376
+ emit(
377
+ checks,
378
+ collector,
379
+ "webhook.pusher-up",
380
+ "broken",
381
+ `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.`
382
+ );
383
+ }
384
+ if (pluginEnabled === null) {
385
+ emit(
386
+ checks,
387
+ collector,
388
+ "webhook.plugin-enabled",
389
+ "unknown",
390
+ "could not read plugins.enabled from ~/.hermes/config.yaml."
391
+ );
392
+ } else if (pluginEnabled) {
393
+ emit(
394
+ checks,
395
+ collector,
396
+ "webhook.plugin-enabled",
397
+ "ok",
398
+ "kojee-tandem receiver plugin is enabled in Hermes (plugins.enabled)."
399
+ );
400
+ } else {
401
+ emit(
402
+ checks,
403
+ collector,
404
+ "webhook.plugin-enabled",
405
+ "broken",
406
+ "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`."
407
+ );
408
+ }
409
+ const receiver = probeReceiver(listenHost, Number.parseInt(listenPort, 10));
410
+ if (receiver === null) {
411
+ emit(
412
+ checks,
413
+ collector,
414
+ "webhook.receiver-up",
415
+ "unknown",
416
+ `could not probe the receiver bind ${listenHost}:${listenPort} (ss/lsof unavailable).`
417
+ );
418
+ } else if (receiver) {
419
+ emit(
420
+ checks,
421
+ collector,
422
+ "webhook.receiver-up",
423
+ "ok",
424
+ `receiver is listening on ${listenHost}:${listenPort}.`
425
+ );
426
+ } else {
427
+ 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";
428
+ emit(
429
+ checks,
430
+ collector,
431
+ "webhook.receiver-up",
432
+ "warn",
433
+ `nothing is listening on ${listenHost}:${listenPort} \u2014 ${fix}.`
434
+ );
435
+ }
436
+ if (childEnv === null) {
437
+ emit(
438
+ checks,
439
+ collector,
440
+ "webhook.no-duplicate",
441
+ "unknown",
442
+ `could not read mcp_servers.${HERMES_MCP_SERVER_NAME}.env from ~/.hermes/config.yaml.`
443
+ );
444
+ } else {
445
+ const childUrl = (childEnv["KOJEE_WEBHOOK_URL"] ?? "").trim();
446
+ emit(
447
+ checks,
448
+ collector,
449
+ "webhook.no-duplicate",
450
+ childUrl === "" ? "ok" : "broken",
451
+ 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).`
452
+ );
453
+ }
454
+ const daemonInstance = (daemonEnv?.vars.get("KOJEE_INSTANCE") ?? "").trim();
455
+ const childInstance = (childEnv?.["KOJEE_INSTANCE"] ?? "").trim();
456
+ if (!daemonInstance) {
457
+ emit(
458
+ checks,
459
+ collector,
460
+ "webhook.instance-align",
461
+ "broken",
462
+ `daemon KOJEE_INSTANCE is unset \u2014 daemon + MCP child may resolve to DIFFERENT sessions (split-brain seat). Expected '${HERMES_INSTANCE_KEY}'. ${WIZARD_RERUN}.`
463
+ );
464
+ } else if (childInstance && daemonInstance !== childInstance) {
465
+ emit(
466
+ checks,
467
+ collector,
468
+ "webhook.instance-align",
469
+ "broken",
470
+ `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}.`
471
+ );
472
+ } else if (daemonInstance !== HERMES_INSTANCE_KEY) {
473
+ emit(
474
+ checks,
475
+ collector,
476
+ "webhook.instance-align",
477
+ "warn",
478
+ `daemon KOJEE_INSTANCE='${daemonInstance}' != expected '${HERMES_INSTANCE_KEY}' (non-standard but self-consistent with the child).`
479
+ );
480
+ } else {
481
+ emit(
482
+ checks,
483
+ collector,
484
+ "webhook.instance-align",
485
+ "ok",
486
+ `daemon + mcp child share KOJEE_INSTANCE='${HERMES_INSTANCE_KEY}' \u2014 one backend seat.`
487
+ );
488
+ }
489
+ if (platform !== "linux") {
490
+ emit(
491
+ checks,
492
+ collector,
493
+ "webhook.env-reaches-daemon",
494
+ "unknown",
495
+ "n/a \u2014 reads /proc/<pid>/environ (Linux only)."
496
+ );
497
+ } else {
498
+ const liveEnv = readDaemonEnv();
499
+ if (liveEnv === null) {
500
+ emit(
501
+ checks,
502
+ collector,
503
+ "webhook.env-reaches-daemon",
504
+ "unknown",
505
+ "daemon not running (no MainPID) or its /proc environ is unreadable."
506
+ );
507
+ } else {
508
+ const missing = ["KOJEE_WEBHOOK_URL", "KOJEE_WEBHOOK_SECRET", "KOJEE_INSTANCE"].filter(
509
+ (k) => !((liveEnv[k] ?? "").length > 0)
510
+ );
511
+ emit(
512
+ checks,
513
+ collector,
514
+ "webhook.env-reaches-daemon",
515
+ missing.length === 0 ? "ok" : "broken",
516
+ 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.`
517
+ );
518
+ }
519
+ }
520
+ const flapCheck = buildFlapDoctorCheck({
521
+ name: "webhook.daemon-flapping",
522
+ // hermes namespaces every check `webhook.*`
523
+ ...deps.readFlaps ? { readFlaps: deps.readFlaps } : {},
524
+ ...deps.now ? { now: deps.now } : {}
525
+ });
526
+ emit(
527
+ checks,
528
+ collector,
529
+ flapCheck.name,
530
+ flapCheck.ok === "warn" ? "warn" : "ok",
531
+ flapCheck.detail
532
+ );
533
+ return { checks, findings: collector.findings, verdict: collector.verdict() };
534
+ }
535
+ var CHECK_GROUP = {
536
+ "webhook.env-format": "Config",
537
+ "webhook.url-receiver-align": "Config",
538
+ "webhook.secret-match": "Config",
539
+ "webhook.no-duplicate": "Config",
540
+ "webhook.instance-align": "Config",
541
+ "webhook.plugin-enabled": "Receiver",
542
+ "webhook.receiver-up": "Receiver",
543
+ "webhook.daemon-bin": "Pusher",
544
+ "webhook.pusher-up": "Pusher",
545
+ "webhook.env-reaches-daemon": "Pusher"
546
+ };
547
+ function formatHermesDoctorReport(report) {
548
+ const mark = (ok) => ok === true ? "\u2713" : ok === "warn" ? "\u26A0" : ok === "unknown" ? "?" : "\u2717";
549
+ const shortName = (name) => name.replace(/^webhook\./, "");
550
+ const nOk = report.checks.filter((c) => c.ok === true).length;
551
+ const nWarn = report.checks.filter((c) => c.ok === "warn").length;
552
+ const nBad = report.checks.filter((c) => c.ok === false).length;
553
+ const nUnknown = report.checks.filter((c) => c.ok === "unknown").length;
554
+ const verdictMark = report.verdict === "healthy" ? "\u2713" : report.verdict === "degraded" ? "\u26A0" : "\u2717";
555
+ const lines = [];
556
+ lines.push(`kojee-mcp doctor \xB7 hermes wake-path \xB7 ${verdictMark} ${report.verdict.toUpperCase()}`);
557
+ const tally = [
558
+ `${nOk} ok`,
559
+ nWarn ? `${nWarn} warning${nWarn > 1 ? "s" : ""}` : null,
560
+ nBad ? `${nBad} problem${nBad > 1 ? "s" : ""}` : null,
561
+ nUnknown ? `${nUnknown} unknown` : null
562
+ ].filter(Boolean).join(", ");
563
+ lines.push(` ${tally}`);
564
+ lines.push(" path: daemon (SSE \u2192 HMAC POST) \u2192 :8645 receiver plugin \u2192 wakes the agent");
565
+ lines.push("");
566
+ for (const group of ["Config", "Receiver", "Pusher"]) {
567
+ const groupChecks = report.checks.filter((c) => CHECK_GROUP[c.name] === group);
568
+ if (groupChecks.length === 0) continue;
569
+ lines.push(` ${group}:`);
570
+ for (const c of groupChecks) {
571
+ lines.push(` ${mark(c.ok)} ${shortName(c.name)}: ${c.detail}`);
572
+ }
573
+ lines.push("");
574
+ }
575
+ const actionable = [
576
+ ...report.checks.filter((c) => c.ok === false),
577
+ ...report.checks.filter((c) => c.ok === "warn")
578
+ ];
579
+ if (actionable.length === 0) {
580
+ lines.push("\u2192 Wake-path is healthy. Nothing to do.");
581
+ } else {
582
+ lines.push("\u2192 Next steps (in order):");
583
+ actionable.forEach((c, i) => {
584
+ lines.push(` ${i + 1}. [${shortName(c.name)}] ${c.detail}`);
585
+ });
586
+ }
587
+ lines.push("");
588
+ lines.push(
589
+ " (read-only. Enabling/restarting the daemon, plugin, or gateway is an owner step \u2014 shared infra needs the principal's word.)"
590
+ );
591
+ return lines.join("\n");
592
+ }
593
+ export {
594
+ collectHermesDoctorReport,
595
+ formatHermesDoctorReport,
596
+ hermesArtifactsPresent
597
+ };
@@ -5,7 +5,10 @@ import {
5
5
  import "./chunk-D6JKFJ6A.js";
6
6
  import {
7
7
  resolveOpenclawWakeTarget
8
- } from "./chunk-NE2F5CKS.js";
8
+ } from "./chunk-LKGKDZSO.js";
9
+ import {
10
+ buildFlapDoctorCheck
11
+ } from "./chunk-P64UR3SE.js";
9
12
  import "./chunk-SQL56SEB.js";
10
13
  import {
11
14
  loadPairedConfig,
@@ -60,6 +63,7 @@ function collectOpenclawDoctorReport(deps = {}) {
60
63
  ok: paired !== null,
61
64
  detail: paired ? `resolves \u2014 source: ${paired.source} paired slot (token value never printed)` : `NONE resolves (no ~/.kojee/agents/openclaw/config.json or ~/.kojee/config.json token) \u2014 ${WIZARD_RERUN}`
62
65
  });
66
+ checks.push(buildFlapDoctorCheck({ ...deps.readFlaps ? { readFlaps: deps.readFlaps } : {}, ...deps.now ? { now: deps.now } : {} }));
63
67
  const verdict = checks.some((c) => c.ok === false) ? "broken" : checks.some((c) => c.ok === "warn") ? "degraded" : "healthy";
64
68
  return { checks, verdict };
65
69
  }
@@ -6,7 +6,7 @@ import {
6
6
  startEventLog,
7
7
  statusLogPath,
8
8
  sweepStaleEventLogs
9
- } from "./chunk-TNZITJAB.js";
9
+ } from "./chunk-WVQU3QQ7.js";
10
10
  import "./chunk-67F67AQ6.js";
11
11
  import "./chunk-U5HHHRXA.js";
12
12
  export {
@@ -1,7 +1,9 @@
1
1
  import {
2
+ CONNECT_TIMEOUT_MS,
2
3
  LOG_HEARTBEAT_INTERVAL_MS,
3
4
  UNARMED_FALLBACK_MS,
4
5
  UNDICI_DEFAULT_BODY_TIMEOUT_MS,
6
+ buildStreamUrl,
5
7
  createAdaptiveWatchdog,
6
8
  createBackoffController,
7
9
  normalizeBackendEvent,
@@ -10,14 +12,16 @@ import {
10
12
  startEventStream,
11
13
  statusReason,
12
14
  undiciFetch
13
- } from "./chunk-CUOJHP2S.js";
15
+ } from "./chunk-UGEBGCYU.js";
14
16
  import "./chunk-Z5LPNJQ6.js";
15
17
  import "./chunk-MIEI4PLB.js";
16
18
  export {
19
+ CONNECT_TIMEOUT_MS,
17
20
  LOG_HEARTBEAT_INTERVAL_MS,
18
21
  UNARMED_FALLBACK_MS,
19
22
  UNDICI_DEFAULT_BODY_TIMEOUT_MS,
20
23
  undiciFetch as _sseStreamFetch,
24
+ buildStreamUrl,
21
25
  createAdaptiveWatchdog,
22
26
  createBackoffController,
23
27
  normalizeBackendEvent,
@@ -0,0 +1,23 @@
1
+ import {
2
+ FLAP_WARN_COUNT,
3
+ FLAP_WINDOW_MS,
4
+ MAX_FLAP_ENTRIES,
5
+ buildFlapDoctorCheck,
6
+ defaultFlapLogPath,
7
+ flapRateExceeds,
8
+ readFlaps,
9
+ recentFlapCount,
10
+ recordFlap
11
+ } from "./chunk-P64UR3SE.js";
12
+ import "./chunk-SQL56SEB.js";
13
+ export {
14
+ FLAP_WARN_COUNT,
15
+ FLAP_WINDOW_MS,
16
+ MAX_FLAP_ENTRIES,
17
+ buildFlapDoctorCheck,
18
+ defaultFlapLogPath,
19
+ flapRateExceeds,
20
+ readFlaps,
21
+ recentFlapCount,
22
+ recordFlap
23
+ };
@@ -111,6 +111,17 @@ declare class GatewayClient {
111
111
  sendRest(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<Response>;
112
112
  private trackNonce;
113
113
  private tryParseErrorBody;
114
+ /**
115
+ * Read + JSON-parse a response body, decompressing gzip EXPLICITLY. The gateway
116
+ * may return `Content-Encoding: gzip`; transparent decompression by the HTTP
117
+ * client proved unreliable in the long-lived in-session context (two undici
118
+ * versions in play), so we detect the gzip magic (0x1f 0x8b) on the raw bytes
119
+ * and gunzip ourselves. Plain (already-decompressed or identity) bodies pass
120
+ * through untouched — valid JSON never starts with 0x1f, so there's no false
121
+ * positive. Version-agnostic: correct whether or not the client transparently
122
+ * decompressed.
123
+ */
124
+ private readJsonBody;
114
125
  }
115
126
 
116
127
  export { GatewayClient as G, type KeystoreData as K, type LoadedKeyPair as L, type McpToolDefinition as M, type ProxyConfig as P, type ToolCallResult as T };