kojee-mcp 0.7.5-beta.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 +21 -3
- 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-4ATSAH7H.js → chunk-GLHA3DHU.js} +33 -5
- 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-CPOK642I.js → chunk-V34FLRC3.js} +89 -26
- package/dist/{chunk-TCWIXG5C.js → chunk-VEZJSFTM.js} +1 -1
- package/dist/cli.js +26 -15
- 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-NCHC6WPU.js → connect-handler-UHPBAN4A.js} +67 -9
- package/dist/{doctor-RKLXZR2B.js → doctor-CFDHPSJ5.js} +57 -4
- package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-YOLALC4O.js} +84 -10
- package/dist/{doctor-hermes-WLDI4R6P.js → doctor-hermes-KMHM27Y3.js} +17 -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 +4 -5
- package/dist/{install-JQNDGAAQ.js → install-NG366AOQ.js} +4 -4
- package/dist/lib.js +1 -1
- package/dist/{registry-DKWVVXJM.js → registry-4S4YRGJY.js} +23 -20
- package/dist/{server-DU3LFS32.js → server-F32TVL4J.js} +2 -3
- package/dist/{setup-handler-DA4465VH.js → setup-handler-WLW3G2Z5.js} +6 -7
- package/dist/{stop-hook-4MNOSDCB.js → stop-hook-BFXALOVO.js} +1 -1
- package/dist/stream-watchdog-CLRS2VMV.js +59 -0
- package/dist/{tail-stream-G4WGTXDS.js → tail-stream-A2LA5NF4.js} +106 -6
- package/package.json +1 -1
- package/dist/chunk-5DHIUN73.js +0 -25
- package/dist/chunk-TMCNB4JH.js +0 -33
package/README.md
CHANGED
|
@@ -431,12 +431,14 @@ constant beside it):
|
|
|
431
431
|
```
|
|
432
432
|
{ type, id, tandem_id, cursor, time,
|
|
433
433
|
from{ member_id, principal, agent_id?, session_id?, displayname },
|
|
434
|
-
kind, content{ body, format? }, mentions?, reply_to?, severity? }
|
|
434
|
+
kind, content{ body, format? }, mentions?, reply_to?, severity?, wake?, wake_reason? }
|
|
435
435
|
```
|
|
436
436
|
|
|
437
437
|
`from.session_id` and `severity` are present only when the wire carried them.
|
|
438
|
-
|
|
439
|
-
`
|
|
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`.
|
|
440
442
|
- **Verify** the `X-Kojee-Signature` header: it is the hex-encoded SHA-256 HMAC
|
|
441
443
|
of the **raw request-body bytes**, keyed by `KOJEE_WEBHOOK_SECRET`. Recompute
|
|
442
444
|
over the received bytes (before any re-serialization) and timing-safe compare;
|
|
@@ -574,6 +576,22 @@ npm run dev:stub
|
|
|
574
576
|
# Stub listens on http://localhost:8765
|
|
575
577
|
```
|
|
576
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
|
+
|
|
577
595
|
## How Approvals Work
|
|
578
596
|
|
|
579
597
|
Some tools are governed by approval policies configured in the Kojee dashboard. When an agent calls a governed tool:
|
|
@@ -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
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
claudeCodeAdapter
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VEZJSFTM.js";
|
|
4
4
|
import {
|
|
5
5
|
GatewayClient,
|
|
6
6
|
applyStableSessionId
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
createMcpServer,
|
|
13
13
|
startMcpServer
|
|
14
|
-
} from "./chunk-
|
|
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) } : {}
|
|
@@ -338,13 +340,12 @@ async function startProxy(config) {
|
|
|
338
340
|
}
|
|
339
341
|
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
340
342
|
}
|
|
341
|
-
const isHeadlessDaemon = !!process.env["KOJEE_WEBHOOK_EXPECTED"];
|
|
342
343
|
if (!isHeadlessDaemon) {
|
|
343
344
|
process.stdin.on("end", () => shutdown("stdin end"));
|
|
344
345
|
process.stdin.on("close", () => shutdown("stdin close"));
|
|
345
346
|
} else {
|
|
346
347
|
console.error(
|
|
347
|
-
"[kojee-mcp] headless daemon (KOJEE_WEBHOOK_EXPECTED
|
|
348
|
+
"[kojee-mcp] headless daemon (KOJEE_HEADLESS / KOJEE_WEBHOOK_EXPECTED): stdin EOF ignored; lifecycle owned by the service manager (SIGTERM/SIGINT/SIGHUP)."
|
|
348
349
|
);
|
|
349
350
|
}
|
|
350
351
|
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
@@ -363,6 +364,33 @@ async function startProxy(config) {
|
|
|
363
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."
|
|
364
365
|
);
|
|
365
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
|
+
}
|
|
366
394
|
await startMcpServer(server);
|
|
367
395
|
}
|
|
368
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,
|