kojee-mcp 0.7.5-staging.2 → 0.7.5-staging.20
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-5HHQJ7VC.js} +3 -1
- package/dist/{chunk-ZSHCHOPL.js → chunk-676WQSWZ.js} +163 -7
- package/dist/{chunk-FJUAMJHU.js → chunk-6NPMVZD7.js} +38 -3
- package/dist/{chunk-247WFMCJ.js → chunk-AGZ7ZGLY.js} +30 -3
- package/dist/chunk-AXBJQJKA.js +65 -0
- package/dist/{chunk-TCWIXG5C.js → chunk-CCVRXJJ2.js} +1 -1
- package/dist/{chunk-WBXU27BF.js → chunk-FNMKP34Y.js} +1 -1
- package/dist/chunk-GEGUWQYT.js +90 -0
- package/dist/{chunk-NE2F5CKS.js → chunk-LKGKDZSO.js} +9 -5
- package/dist/{chunk-XPIW4N55.js → chunk-N2YAZRAD.js} +178 -43
- package/dist/{chunk-LSUB6QMP.js → chunk-NIIBVKLP.js} +3 -0
- package/dist/chunk-P64UR3SE.js +86 -0
- package/dist/{chunk-A4IOKD4Z.js → chunk-QYZP6QT2.js} +2 -4
- package/dist/{chunk-KNEJTD6G.js → chunk-SZLNNMIZ.js} +6 -2
- package/dist/{chunk-CUOJHP2S.js → chunk-UGEBGCYU.js} +48 -12
- package/dist/{chunk-6XWTUDWW.js → chunk-WDWVBPXA.js} +7 -5
- package/dist/{chunk-TNZITJAB.js → chunk-WVQU3QQ7.js} +14 -4
- package/dist/cli.js +42 -23
- package/dist/{codex-prompt-submit-hook-J5PZEJSK.js → codex-prompt-submit-hook-UQJI6A54.js} +2 -2
- package/dist/{codex-stop-hook-32W4BIOM.js → codex-stop-hook-GTGRQNVV.js} +2 -2
- package/dist/{connect-handler-NZINEMG3.js → connect-handler-GTFXNZUC.js} +68 -9
- package/dist/{doctor-WUU5BVPT.js → doctor-54S7JGOE.js} +64 -5
- package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-6H4OOS6O.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-log-2NBJEIEP.js → event-log-M22X3FAI.js} +1 -1
- package/dist/{event-stream-ATKYXDBV.js → event-stream-5ZEL7FXN.js} +5 -1
- package/dist/flap-log-D4KG7RK4.js +23 -0
- package/dist/{gateway-client-CbM2OC_w.d.ts → gateway-client-CLmQghON.d.ts} +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -7
- package/dist/{install-JQNDGAAQ.js → install-GJVZEWLI.js} +8 -7
- package/dist/lib.d.ts +30 -1
- package/dist/lib.js +3 -3
- package/dist/plugins/hermes/adapter.py +7 -2
- package/dist/{registry-XJ67EO22.js → registry-MCJAVXJW.js} +36 -24
- package/dist/{send-cli-45RLGYIC.js → send-cli-WTY7H3BR.js} +2 -2
- package/dist/{server-DU3LFS32.js → server-VM3ZIGSZ.js} +2 -3
- package/dist/session-start-hook-VL3KYDTT.js +25 -0
- package/dist/{setup-handler-44ASXYMS.js → setup-handler-GNAIWAYU.js} +7 -7
- package/dist/{stop-hook-N6TX4YQT.js → stop-hook-BLPYRE7T.js} +5 -5
- package/dist/stream-watchdog-K2AWNCQW.js +64 -0
- package/dist/{tail-stream-G4WGTXDS.js → tail-stream-PQXEY3TI.js} +120 -9
- package/dist/{user-prompt-submit-hook-DTXXFDSD.js → user-prompt-submit-hook-YQXGE3JU.js} +3 -3
- 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-SZLNNMIZ.js";
|
|
6
7
|
export {
|
|
7
8
|
SESSION_ID_ENV_VARS,
|
|
9
|
+
STABLE_RUNTIMES,
|
|
8
10
|
resolveInstanceKey,
|
|
9
11
|
resolveSharedSessionId
|
|
10
12
|
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
claudeCodeAdapter
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CCVRXJJ2.js";
|
|
4
4
|
import {
|
|
5
5
|
GatewayClient,
|
|
6
6
|
applyStableSessionId
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AGZ7ZGLY.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-QYZP6QT2.js";
|
|
15
15
|
import {
|
|
16
16
|
findClaudeAncestorPid
|
|
17
17
|
} from "./chunk-XJEBJIQE.js";
|
|
@@ -22,6 +22,10 @@ import os from "os";
|
|
|
22
22
|
import path from "path";
|
|
23
23
|
|
|
24
24
|
// src/tool-registry.ts
|
|
25
|
+
import { readFileSync, statSync } from "fs";
|
|
26
|
+
import { basename } from "path";
|
|
27
|
+
var UPLOAD_TOOL = "tandem_upload_attachment";
|
|
28
|
+
var MAX_UPLOAD_FILE_BYTES = 25 * 1024 * 1024;
|
|
25
29
|
var ToolRegistry = class {
|
|
26
30
|
constructor(gateway) {
|
|
27
31
|
this.gateway = gateway;
|
|
@@ -68,8 +72,87 @@ var ToolRegistry = class {
|
|
|
68
72
|
}
|
|
69
73
|
this.tools.set(tool.name, tool);
|
|
70
74
|
}
|
|
75
|
+
this.augmentUploadTool();
|
|
71
76
|
console.error(`[tools] Registered ${this.tools.size} tools from gateway`);
|
|
72
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Teach the gateway's `tandem_upload_attachment` tool about a `file_path` arg
|
|
80
|
+
* that THIS proxy resolves in-process (see {@link resolveUploadFilePath}): the
|
|
81
|
+
* model emits a local path, the proxy reads the bytes and forwards them as
|
|
82
|
+
* `data_base64`, so a multi-MB image never has to be generated into the model's
|
|
83
|
+
* tool call. Purely additive — the original `data_base64` shape still works. A
|
|
84
|
+
* no-op if the gateway doesn't expose the tool (older backend).
|
|
85
|
+
*/
|
|
86
|
+
augmentUploadTool() {
|
|
87
|
+
const tool = this.tools.get(UPLOAD_TOOL);
|
|
88
|
+
if (!tool) return;
|
|
89
|
+
const schema = { ...tool.inputSchema ?? {} };
|
|
90
|
+
const properties = { ...schema.properties ?? {} };
|
|
91
|
+
if ("file_path" in properties) return;
|
|
92
|
+
properties.file_path = {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Absolute path to a local image file. Your local kojee-mcp-server reads the bytes and uploads them for you (downscaled server-side), so the image is NEVER placed in your prompt/tool-call. Prefer this over data_base64 for photos. Provide file_path OR data_base64, not both; filename is derived from the path when omitted."
|
|
95
|
+
};
|
|
96
|
+
schema.properties = properties;
|
|
97
|
+
if (Array.isArray(schema.required)) {
|
|
98
|
+
schema.required = schema.required.filter(
|
|
99
|
+
(k) => k !== "data_base64" && k !== "filename"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const dependent = {
|
|
103
|
+
...schema.dependentRequired ?? {},
|
|
104
|
+
data_base64: ["filename"]
|
|
105
|
+
};
|
|
106
|
+
schema.dependentRequired = dependent;
|
|
107
|
+
this.tools.set(UPLOAD_TOOL, { ...tool, inputSchema: schema });
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a `tandem_upload_attachment` call that used `file_path`: read the file
|
|
111
|
+
* locally, base64-encode the RAW bytes (Buffer→base64 is byte-exact — no text
|
|
112
|
+
* round-trip, so no U+FFFD corruption), and rewrite the args to the `data_base64`
|
|
113
|
+
* shape the gateway expects. Returns the args to forward, or throws with a clear
|
|
114
|
+
* message the caller surfaces as an isError result.
|
|
115
|
+
*/
|
|
116
|
+
resolveUploadFilePath(args) {
|
|
117
|
+
const filePath = args.file_path;
|
|
118
|
+
const hasFilePath = typeof filePath === "string" && filePath.length > 0;
|
|
119
|
+
const hasBase64 = typeof args.data_base64 === "string" && args.data_base64.length > 0;
|
|
120
|
+
const hasFilename = typeof args.filename === "string" && args.filename.trim().length > 0;
|
|
121
|
+
if (!hasFilePath) {
|
|
122
|
+
if (hasBase64 && !hasFilename) {
|
|
123
|
+
throw new Error("filename is required when uploading with data_base64");
|
|
124
|
+
}
|
|
125
|
+
return args;
|
|
126
|
+
}
|
|
127
|
+
if (hasBase64) {
|
|
128
|
+
throw new Error("provide file_path OR data_base64, not both");
|
|
129
|
+
}
|
|
130
|
+
let size;
|
|
131
|
+
try {
|
|
132
|
+
const st = statSync(filePath);
|
|
133
|
+
if (!st.isFile()) throw new Error("not a regular file");
|
|
134
|
+
size = st.size;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`cannot read file_path '${filePath}': ${err?.message ?? String(err)}`);
|
|
137
|
+
}
|
|
138
|
+
if (size > MAX_UPLOAD_FILE_BYTES) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`file_path is ${size} bytes; exceeds the ${MAX_UPLOAD_FILE_BYTES}-byte upload cap`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const buf = readFileSync(filePath);
|
|
144
|
+
if (buf.length > MAX_UPLOAD_FILE_BYTES) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`file_path is ${buf.length} bytes; exceeds the ${MAX_UPLOAD_FILE_BYTES}-byte upload cap`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const { file_path: _drop, ...rest } = args;
|
|
150
|
+
return {
|
|
151
|
+
...rest,
|
|
152
|
+
data_base64: buf.toString("base64"),
|
|
153
|
+
filename: hasFilename ? args.filename : basename(filePath)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
73
156
|
/**
|
|
74
157
|
* Return all registered tools for the MCP ListTools response — gateway tools
|
|
75
158
|
* first (minus any a local tool shadows), then local tools.
|
|
@@ -114,9 +197,20 @@ var ToolRegistry = class {
|
|
|
114
197
|
isError: true
|
|
115
198
|
};
|
|
116
199
|
}
|
|
200
|
+
let outgoing = args;
|
|
201
|
+
if (name === UPLOAD_TOOL) {
|
|
202
|
+
try {
|
|
203
|
+
outgoing = this.resolveUploadFilePath(args);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: err?.message ?? String(err) }],
|
|
207
|
+
isError: true
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
117
211
|
return this.gateway.sendRpc("tools/call", {
|
|
118
212
|
name,
|
|
119
|
-
arguments:
|
|
213
|
+
arguments: outgoing
|
|
120
214
|
});
|
|
121
215
|
}
|
|
122
216
|
/** Total number of registered tools (gateway + local, shadowed names once). */
|
|
@@ -295,9 +389,11 @@ async function startProxy(config) {
|
|
|
295
389
|
}
|
|
296
390
|
console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
|
|
297
391
|
let server;
|
|
298
|
-
const
|
|
392
|
+
const isHeadlessDaemon = !!process.env["KOJEE_HEADLESS"] || !!process.env["KOJEE_WEBHOOK_EXPECTED"];
|
|
393
|
+
const { selectDelivery } = await import("./registry-MCJAVXJW.js");
|
|
299
394
|
const delivery = selectDelivery(adapter.runtime, {
|
|
300
395
|
supportsChannels: adapter.supportsChannels,
|
|
396
|
+
headless: isHeadlessDaemon,
|
|
301
397
|
// Per-window delivered mirror for the tandem_pending tool (codex only —
|
|
302
398
|
// other runtimes' deliveries ignore this even when passed).
|
|
303
399
|
...pendingState ? { onDelivered: (e) => pendingState.noteDelivered(e.tandem_id, e.cursor) } : {}
|
|
@@ -331,10 +427,21 @@ async function startProxy(config) {
|
|
|
331
427
|
}
|
|
332
428
|
for (const step of started.teardown) teardownSteps.push(step);
|
|
333
429
|
} else {
|
|
430
|
+
if (process.env["KOJEE_WEBHOOK_EXPECTED"]) {
|
|
431
|
+
console.error(
|
|
432
|
+
"[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>'`)."
|
|
433
|
+
);
|
|
434
|
+
}
|
|
334
435
|
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
335
436
|
}
|
|
336
|
-
|
|
337
|
-
|
|
437
|
+
if (!isHeadlessDaemon) {
|
|
438
|
+
process.stdin.on("end", () => shutdown("stdin end"));
|
|
439
|
+
process.stdin.on("close", () => shutdown("stdin close"));
|
|
440
|
+
} else {
|
|
441
|
+
console.error(
|
|
442
|
+
"[kojee-mcp] headless daemon (KOJEE_HEADLESS / KOJEE_WEBHOOK_EXPECTED): stdin EOF ignored; lifecycle owned by the service manager (SIGTERM/SIGINT/SIGHUP)."
|
|
443
|
+
);
|
|
444
|
+
}
|
|
338
445
|
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
339
446
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
340
447
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -351,6 +458,55 @@ async function startProxy(config) {
|
|
|
351
458
|
"[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
459
|
);
|
|
353
460
|
}
|
|
461
|
+
if (isHeadlessDaemon && activeStreamHandle) {
|
|
462
|
+
const { createStreamWatchdog } = await import("./stream-watchdog-K2AWNCQW.js");
|
|
463
|
+
const { recordFlap } = await import("./flap-log-D4KG7RK4.js");
|
|
464
|
+
const handleForWatchdog = activeStreamHandle;
|
|
465
|
+
const bootAt = Date.now();
|
|
466
|
+
const streamWatchdog = createStreamWatchdog({
|
|
467
|
+
getState: () => {
|
|
468
|
+
const s = handleForWatchdog.getState();
|
|
469
|
+
return { connected: s.connected, lastEventAt: s.lastEventAt };
|
|
470
|
+
},
|
|
471
|
+
onUnhealthy: ({ disconnectedForMs, reason }) => {
|
|
472
|
+
recordFlap({
|
|
473
|
+
ts: Date.now(),
|
|
474
|
+
instanceKey,
|
|
475
|
+
reason,
|
|
476
|
+
disconnectedForMs,
|
|
477
|
+
uptimeMs: Date.now() - bootAt
|
|
478
|
+
});
|
|
479
|
+
console.error(
|
|
480
|
+
`[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).`
|
|
481
|
+
);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
streamWatchdog.start();
|
|
486
|
+
teardownSteps.push(() => streamWatchdog.stop());
|
|
487
|
+
}
|
|
488
|
+
if (!isHeadlessDaemon && activeStreamHandle) {
|
|
489
|
+
const { createStreamWatchdog } = await import("./stream-watchdog-K2AWNCQW.js");
|
|
490
|
+
const handleForWatchdog = activeStreamHandle;
|
|
491
|
+
const streamWatchdog = createStreamWatchdog({
|
|
492
|
+
getState: () => {
|
|
493
|
+
const s = handleForWatchdog.getState();
|
|
494
|
+
return { connected: s.connected, lastEventAt: s.lastEventAt };
|
|
495
|
+
},
|
|
496
|
+
reArmAfterUnhealthy: true,
|
|
497
|
+
onUnhealthy: ({ reason }) => {
|
|
498
|
+
console.error(
|
|
499
|
+
`[kojee-mcp] stream-liveness watchdog: ${reason} \u2014 reconnecting in place (no supervisor on the stdio path; recovering the wedged stream).`
|
|
500
|
+
);
|
|
501
|
+
try {
|
|
502
|
+
handleForWatchdog.reconnect();
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
streamWatchdog.start();
|
|
508
|
+
teardownSteps.push(() => streamWatchdog.stop());
|
|
509
|
+
}
|
|
354
510
|
await startMcpServer(server);
|
|
355
511
|
}
|
|
356
512
|
async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
@@ -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")`;
|
|
@@ -15,26 +40,36 @@ function buildReplyRecipe(event) {
|
|
|
15
40
|
function buildCatchUpNote() {
|
|
16
41
|
return "Cursors are allocated per-Tandem, and the event-log interleaves every subscribed room into one stream \u2014 so track the last `cursor=<n>` you saw keyed by the line's `tandem=<id>` field, and only treat a jump WITHIN the same tandem_id as a gap (a jump between lines from different rooms is normal interleaving, not a missed message). If the cursor jumps within one tandem, or the log resets (the proxy caps and truncates the messages log in place on overflow \u2014 a `status=rotated` line is recorded in the status sibling), you may have missed messages: catch up with tandem_messages(tandem_id, since=<last cursor you saw for THAT tandem>).";
|
|
17
42
|
}
|
|
43
|
+
function buildSessionStartReminder(source) {
|
|
44
|
+
const freshProcess = source === "startup" || source === "resume";
|
|
45
|
+
const catchUp = "Catch up on anything sent while your context was away: " + buildCatchUpNote();
|
|
46
|
+
if (!freshProcess) {
|
|
47
|
+
return "[kojee] Context reset \u2014 your kojee Tandem connection and wake watcher from this process are still running; do NOT spawn another watcher (a second one would double-deliver every wake). " + catchUp;
|
|
48
|
+
}
|
|
49
|
+
return "[kojee] New session \u2014 reconnect to Tandem before anything else. FIRST ACTION this turn, before other work: ensure your kojee wake watcher is running \u2014 if you don't already have one, spawn it now by running the exact Monitor(command=`\u2026 kojee-mcp tail \u2026`, persistent=true) call given in your kojee MCP server instructions (they carry the version-pinned command + your live event-log path). Without it you receive no Tandem wakes. THEN " + catchUp;
|
|
50
|
+
}
|
|
18
51
|
function buildCondensedTandemRules() {
|
|
19
52
|
return "Tandem ground rules: (1) catch up before you speak \u2014 fetch messages since your last cursor (cursors are per-Tandem); never answer from a stale view. (2) close every loop \u2014 taking / done (with evidence) / blocked, said out loud; silence is a bug on a team. (3) your seat_name IS your room identity \u2014 keep it stable across sessions and reconnects. (4) plan your wake path per task and set your wake filter deliberately (heads-down \u2192 mentions; waiting on one reply \u2192 listen; co-working \u2192 all), and tell the room your posture WHEN IT CHANGES \u2014 skip the check-in ceremony when you are merely re-joining/reconnecting with the same posture. (5) send all replies and check-ins as kind=message \u2014 never kind=status; status is lifecycle-only (joined/left, system-generated).";
|
|
20
53
|
}
|
|
21
54
|
function buildMonitorNudge(logPath) {
|
|
22
55
|
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
56
|
}
|
|
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? }";
|
|
57
|
+
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
58
|
function buildWebhookReceiverNote(sig) {
|
|
26
59
|
const header = sig?.header ?? "X-Kojee-Signature";
|
|
27
60
|
const prefix = sig?.prefix ?? "";
|
|
28
61
|
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.`;
|
|
62
|
+
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
63
|
}
|
|
31
64
|
var CODEX_LISTEN_CAP_MS = 8e3;
|
|
32
65
|
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
66
|
|
|
34
67
|
export {
|
|
68
|
+
VERSION,
|
|
35
69
|
buildMonitorSpawn,
|
|
36
70
|
buildReplyRecipe,
|
|
37
71
|
buildCatchUpNote,
|
|
72
|
+
buildSessionStartReminder,
|
|
38
73
|
buildCondensedTandemRules,
|
|
39
74
|
buildMonitorNudge,
|
|
40
75
|
buildWebhookReceiverNote,
|
|
@@ -13,10 +13,13 @@ import {
|
|
|
13
13
|
} from "./chunk-PPTKGWFF.js";
|
|
14
14
|
import {
|
|
15
15
|
resolveInstanceKey
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-SZLNNMIZ.js";
|
|
17
17
|
|
|
18
18
|
// src/gateway-client.ts
|
|
19
19
|
import crypto from "crypto";
|
|
20
|
+
import zlib from "zlib";
|
|
21
|
+
import { promisify } from "util";
|
|
22
|
+
var gunzipAsync = promisify(zlib.gunzip);
|
|
20
23
|
var GatewayClient = class {
|
|
21
24
|
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
22
25
|
this.brokerUrl = brokerUrl;
|
|
@@ -117,7 +120,7 @@ var GatewayClient = class {
|
|
|
117
120
|
isError: true
|
|
118
121
|
};
|
|
119
122
|
}
|
|
120
|
-
const rpcResponse = await
|
|
123
|
+
const rpcResponse = await this.readJsonBody(response);
|
|
121
124
|
if (rpcResponse.error) {
|
|
122
125
|
return translateJsonRpcError(rpcResponse.error);
|
|
123
126
|
}
|
|
@@ -154,6 +157,14 @@ var GatewayClient = class {
|
|
|
154
157
|
);
|
|
155
158
|
return {
|
|
156
159
|
"Content-Type": "application/json",
|
|
160
|
+
// Accept gzip (bandwidth win) but DECOMPRESS EXPLICITLY on read (readJsonBody),
|
|
161
|
+
// rather than relying on the HTTP client's transparent decompression — that
|
|
162
|
+
// was inconsistent across the two undici versions in play (Node-builtin fetch
|
|
163
|
+
// here vs the npm undici used by the SSE stream), which let raw gzip bytes
|
|
164
|
+
// (`1f 8b 08…`) reach JSON.parse and wedge every in-session read (incident
|
|
165
|
+
// 2026-09-22, hot-fixed first with `identity`). We request ONLY gzip (never
|
|
166
|
+
// br/deflate) so readJsonBody's gzip-magic check fully covers what's on the wire.
|
|
167
|
+
"Accept-Encoding": "gzip",
|
|
157
168
|
Authorization: `DPoP ${this.token}`,
|
|
158
169
|
DPoP: proof,
|
|
159
170
|
"Mcp-Session-Id": getSessionId()
|
|
@@ -187,11 +198,27 @@ var GatewayClient = class {
|
|
|
187
198
|
}
|
|
188
199
|
async tryParseErrorBody(response) {
|
|
189
200
|
try {
|
|
190
|
-
return await
|
|
201
|
+
return await this.readJsonBody(response);
|
|
191
202
|
} catch {
|
|
192
203
|
return null;
|
|
193
204
|
}
|
|
194
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Read + JSON-parse a response body, decompressing gzip EXPLICITLY. The gateway
|
|
208
|
+
* may return `Content-Encoding: gzip`; transparent decompression by the HTTP
|
|
209
|
+
* client proved unreliable in the long-lived in-session context (two undici
|
|
210
|
+
* versions in play), so we detect the gzip magic (0x1f 0x8b) on the raw bytes
|
|
211
|
+
* and gunzip ourselves. Plain (already-decompressed or identity) bodies pass
|
|
212
|
+
* through untouched — valid JSON never starts with 0x1f, so there's no false
|
|
213
|
+
* positive. Version-agnostic: correct whether or not the client transparently
|
|
214
|
+
* decompressed.
|
|
215
|
+
*/
|
|
216
|
+
async readJsonBody(response) {
|
|
217
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
218
|
+
const isGzip = buf.length >= 2 && buf[0] === 31 && buf[1] === 139;
|
|
219
|
+
const text = (isGzip ? await gunzipAsync(buf) : buf).toString("utf8");
|
|
220
|
+
return JSON.parse(text);
|
|
221
|
+
}
|
|
195
222
|
};
|
|
196
223
|
|
|
197
224
|
// src/runtime/stable-session.ts
|
|
@@ -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|session-start|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
|
+
};
|
|
@@ -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
|
};
|