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