kojee-mcp 0.5.19 → 0.7.0
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 +13 -8
- package/dist/ancestry-A2F5KQ6A.js +10 -0
- package/dist/cc-session-id-RURNIHHC.js +10 -0
- package/dist/{chunk-U4MNICQI.js → chunk-247WFMCJ.js} +12 -22
- package/dist/{chunk-PSVVRGJC.js → chunk-5H75AEZ2.js} +22 -9
- package/dist/{chunk-YH27B6SW.js → chunk-5SZHXYPK.js} +16 -3
- package/dist/{chunk-DO42NPNR.js → chunk-67F67AQ6.js} +14 -3
- package/dist/{chunk-CH32ELFX.js → chunk-6G6YYST6.js} +1 -1
- package/dist/chunk-D6JKFJ6A.js +11 -0
- package/dist/chunk-E6WMFMM2.js +100 -0
- package/dist/chunk-EIAUW6KO.js +82 -0
- package/dist/chunk-EIH2LNF4.js +45 -0
- package/dist/{chunk-74XFVX6Z.js → chunk-FSKGQ6GT.js} +47 -10
- package/dist/{chunk-GI2CKKBL.js → chunk-GKXHKR3B.js} +1 -1
- package/dist/{wizard-Y3ULSO2C.js → chunk-GVWYW7MY.js} +247 -138
- package/dist/{chunk-JXMVZEQ7.js → chunk-I67C2HYA.js} +1 -1
- package/dist/chunk-KNEJTD6G.js +31 -0
- package/dist/{chunk-NR4Y54OL.js → chunk-MIEI4PLB.js} +1 -19
- package/dist/chunk-NE2F5CKS.js +113 -0
- package/dist/{chunk-XXFVWP6H.js → chunk-OB5T6P24.js} +1 -1
- package/dist/{chunk-65KRRDHP.js → chunk-OBIJ6WQP.js} +82 -27
- package/dist/{chunk-DXJ6QLSJ.js → chunk-PKOBR7ZN.js} +1 -1
- package/dist/{chunk-2OLXXOT3.js → chunk-PPTKGWFF.js} +69 -3
- package/dist/{chunk-X672ZN7V.js → chunk-QEJUNP3X.js} +3 -2
- package/dist/chunk-QJFMU4QC.js +33 -0
- package/dist/{chunk-EW72ZNQL.js → chunk-RDLF4NQC.js} +1 -1
- package/dist/{chunk-XIDCYUTK.js → chunk-SGRVG4HW.js} +9 -4
- package/dist/{chunk-DURRQDYI.js → chunk-TNZITJAB.js} +45 -15
- package/dist/chunk-U5HHHRXA.js +103 -0
- package/dist/chunk-WBXU27BF.js +35 -0
- package/dist/chunk-WIU5WGDF.js +10 -0
- package/dist/chunk-XJEBJIQE.js +52 -0
- package/dist/chunk-Z5LPNJQ6.js +20 -0
- package/dist/cli.js +121 -89
- package/dist/{chunk-35XBRG3V.js → codex-stop-hook-EOMQD3J4.js} +11 -15
- package/dist/connect-handler-HIRM624N.js +137 -0
- package/dist/{control-token-4BUCTYQB.js → control-token-KXSZ3GBY.js} +2 -2
- package/dist/{doctor-U3MTB4GY.js → doctor-PLKLHTJM.js} +48 -22
- package/dist/{doctor-codex-PA3WO6LR.js → doctor-codex-HARAAH4Y.js} +23 -6
- package/dist/doctor-openclaw-LL6NSPLC.js +86 -0
- package/dist/{event-log-HOBO3ECB.js → event-log-2NBJEIEP.js} +5 -3
- package/dist/{event-stream-XX5EZ6HN.js → event-stream-KRYWEYWO.js} +7 -4
- package/dist/{hook-server-XK2NHLJV.js → hook-server-CWBH6PFL.js} +2 -2
- package/dist/index.js +13 -11
- package/dist/{install-LJY2CHKG.js → install-V7LSQCYZ.js} +37 -22
- package/dist/{keystore-XLEV3FL5.js → keystore-LQNFDHGO.js} +2 -2
- package/dist/lib.d.ts +4 -1
- package/dist/lib.js +12 -9
- package/dist/pair-HTCVSMR6.js +11 -0
- package/dist/{paired-config-JTFLHMZ2.js → paired-config-GUDDO2CH.js} +4 -2
- package/dist/{parent-watchdog-TLU355FB.js → parent-watchdog-LTUNF7PD.js} +24 -5
- package/dist/{registry-CGLE5X54.js → registry-VRFHKOPP.js} +195 -26
- package/dist/resolve-slot-BFIB46Y5.js +13 -0
- package/dist/{runtime-record-WO4IECM6.js → runtime-record-OXRLTOLC.js} +2 -2
- package/dist/{send-cli-IPADRDD2.js → send-cli-45RLGYIC.js} +24 -12
- package/dist/{server-LTFG4GMV.js → server-YTHOMA4H.js} +5 -3
- package/dist/{session-discovery-FNMJGFPM.js → session-discovery-NHCZ2XWA.js} +2 -2
- package/dist/setup-handler-UHVKSX7E.js +159 -0
- package/dist/{stop-hook-QZTFH5WV.js → stop-hook-YI4KRKF2.js} +17 -20
- package/dist/{tail-stream-CEJZGSIR.js → tail-stream-NBGHHBS4.js} +61 -22
- package/dist/{user-prompt-submit-hook-FQ43NIHC.js → user-prompt-submit-hook-DTXXFDSD.js} +11 -14
- package/package.json +2 -1
- package/dist/ancestry-ONFBQEP5.js +0 -8
- package/dist/chunk-BLEGIR35.js +0 -43
- package/dist/chunk-CLKCNV2A.js +0 -100
- package/dist/chunk-OGHDTFAX.js +0 -50
- package/dist/chunk-VHKPWUX7.js +0 -41
- package/dist/codex-stop-hook-VY7DOMAG.js +0 -16
- package/dist/doctor-openclaw-SS2TMQOX.js +0 -95
- package/dist/pair-P4ILCMT7.js +0 -10
package/README.md
CHANGED
|
@@ -162,8 +162,9 @@ npx kojee-mcp init --runtime claude-code --token gw_abc... --url https://rosie-s
|
|
|
162
162
|
npx kojee-mcp init --runtime codex --webhook-url https://your-receiver/kojee
|
|
163
163
|
# Codex: writes ~/.codex/config.toml + a codex-stop hook
|
|
164
164
|
npx kojee-mcp init --runtime hermes --webhook-url https://your-receiver/kojee
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
# hermes daemon: print/record the webhook-sink env to export
|
|
166
|
+
npx kojee-mcp connect ABCD-1234 --runtime openclaw
|
|
167
|
+
# OpenClaw: writes mcp.servers.kojee + hooks into ~/.openclaw/openclaw.json
|
|
167
168
|
```
|
|
168
169
|
|
|
169
170
|
Auth flags (all runtimes): `--token <gw_…>` + `--url <broker>` (token mode), or
|
|
@@ -180,12 +181,16 @@ Per-runtime config-path / hooks-path overrides: `--config-path`, `--hooks-path`.
|
|
|
180
181
|
one. Codex has no Claude-style channel injection, so its wake is the **webhook
|
|
181
182
|
sink + a stop-hook fast PEEK + a model-chosen bounded `tandem_listen` (cap 8s,
|
|
182
183
|
never a blanket long-poll)**. See `docs/RUNTIMES.md`.
|
|
183
|
-
- **
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
- **openclaw** — `connect --runtime openclaw` writes `mcp.servers.kojee` (OpenClaw
|
|
185
|
+
spawns kojee-mcp AS its MCP server, threading the per-runtime paired slot via
|
|
186
|
+
`--paired-config`) **and** enables the gateway `hooks` (`hooks.enabled=true` +
|
|
187
|
+
a `hooks.token`) into `~/.openclaw/openclaw.json`. Wake is a POST to
|
|
188
|
+
`<gateway>/hooks/wake` (Bearer `hooks.token`) from the openclaw `WakeDelivery`
|
|
189
|
+
injector — no webhook receiver, no `--webhook-url`.
|
|
190
|
+
- **hermes** — a **daemon** feeding a webhook receiver. `init --runtime hermes`
|
|
191
|
+
(or `connect --runtime hermes`) validates the webhook env and prints + records
|
|
192
|
+
the env to export (secret redacted) into a source-able `~/.kojee/hermes.env`.
|
|
193
|
+
`--webhook-url` is optional (defaults to the hermes-plugin loopback receiver).
|
|
189
194
|
|
|
190
195
|
`kojee-mcp init --uninstall` is runtime-aware (uses the recorded runtime when
|
|
191
196
|
`--runtime` is omitted). `kojee-mcp doctor` is runtime-aware too.
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createDPoPProof,
|
|
3
2
|
deriveStableSessionId,
|
|
4
3
|
getSessionId,
|
|
5
4
|
setStableSessionId
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-Z5LPNJQ6.js";
|
|
6
|
+
import {
|
|
7
|
+
createDPoPProof
|
|
8
|
+
} from "./chunk-MIEI4PLB.js";
|
|
7
9
|
import {
|
|
8
10
|
translateHttpError,
|
|
9
11
|
translateJsonRpcError,
|
|
10
12
|
translateNetworkError
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-PPTKGWFF.js";
|
|
14
|
+
import {
|
|
15
|
+
resolveInstanceKey
|
|
16
|
+
} from "./chunk-KNEJTD6G.js";
|
|
12
17
|
|
|
13
18
|
// src/gateway-client.ts
|
|
14
19
|
import crypto from "crypto";
|
|
@@ -104,8 +109,11 @@ var GatewayClient = class {
|
|
|
104
109
|
const body = await this.tryParseErrorBody(response);
|
|
105
110
|
const translated = translateHttpError(response.status, body?.error);
|
|
106
111
|
if (translated) return translated;
|
|
112
|
+
const detail = body?.error_description ?? body?.detail ?? body?.error ?? "<no detail>";
|
|
107
113
|
return {
|
|
108
|
-
content: [
|
|
114
|
+
content: [
|
|
115
|
+
{ type: "text", text: `Gateway error ${response.status}: ${detail}` }
|
|
116
|
+
],
|
|
109
117
|
isError: true
|
|
110
118
|
};
|
|
111
119
|
}
|
|
@@ -186,24 +194,6 @@ var GatewayClient = class {
|
|
|
186
194
|
}
|
|
187
195
|
};
|
|
188
196
|
|
|
189
|
-
// src/runtime/cc-session-id.ts
|
|
190
|
-
import { randomUUID } from "crypto";
|
|
191
|
-
var SESSION_ID_ENV_VARS = ["CLAUDE_CODE_SESSION_ID"];
|
|
192
|
-
function sanitizeKey(value) {
|
|
193
|
-
return value.replace(/[^A-Za-z0-9_-]/g, "");
|
|
194
|
-
}
|
|
195
|
-
function resolveInstanceKey(deps = {}) {
|
|
196
|
-
const env = deps.env ?? process.env;
|
|
197
|
-
for (const name of SESSION_ID_ENV_VARS) {
|
|
198
|
-
const sid = sanitizeKey((env[name] ?? "").trim());
|
|
199
|
-
if (sid) return sid;
|
|
200
|
-
}
|
|
201
|
-
const explicit = sanitizeKey((env.KOJEE_INSTANCE ?? "").trim());
|
|
202
|
-
if (explicit) return `inst-${explicit}`;
|
|
203
|
-
const mint = deps.randomUUID ?? randomUUID;
|
|
204
|
-
return `mint-${mint()}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
197
|
// src/runtime/stable-session.ts
|
|
208
198
|
async function applyStableSessionId(token, deps = {}) {
|
|
209
199
|
let instanceKey = deps.instanceKey;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
claudeCodeAdapter
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OB5T6P24.js";
|
|
4
4
|
import {
|
|
5
5
|
GatewayClient,
|
|
6
6
|
applyStableSessionId
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-247WFMCJ.js";
|
|
8
8
|
import {
|
|
9
9
|
AuthModule
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-I67C2HYA.js";
|
|
11
11
|
import {
|
|
12
12
|
createMcpServer,
|
|
13
13
|
startMcpServer
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-SGRVG4HW.js";
|
|
15
15
|
import {
|
|
16
16
|
findClaudeAncestorPid
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-XJEBJIQE.js";
|
|
18
18
|
|
|
19
19
|
// src/index.ts
|
|
20
20
|
import fs from "fs";
|
|
@@ -93,6 +93,7 @@ import psList from "ps-list";
|
|
|
93
93
|
async function detectRuntime(env = process.env) {
|
|
94
94
|
if (env["KOJEE_RUNTIME"] === "claude-code") return "claude-code";
|
|
95
95
|
if (env["KOJEE_RUNTIME"] === "codex") return "codex";
|
|
96
|
+
if (env["KOJEE_RUNTIME"] === "openclaw") return "openclaw";
|
|
96
97
|
if (env["CLAUDE_CODE_SESSION_ID"]) return "claude-code";
|
|
97
98
|
const ancestor = await detectRuntimeFromAncestry();
|
|
98
99
|
if (ancestor) return ancestor;
|
|
@@ -132,6 +133,17 @@ var codexAdapter = {
|
|
|
132
133
|
}
|
|
133
134
|
};
|
|
134
135
|
|
|
136
|
+
// src/adapters/openclaw.ts
|
|
137
|
+
var openclawAdapter = {
|
|
138
|
+
runtime: "openclaw",
|
|
139
|
+
supportsChannels: false,
|
|
140
|
+
formatTandemEvent() {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"openclawAdapter.formatTandemEvent() is unreachable \u2014 OpenClaw receives events via the /wake injector delivery, not channels; server.ts gates this on supportsChannels."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
135
147
|
// src/adapters/unknown.ts
|
|
136
148
|
var unknownAdapter = {
|
|
137
149
|
runtime: "unknown",
|
|
@@ -155,6 +167,7 @@ async function selectAdapter() {
|
|
|
155
167
|
const runtime = await detectRuntime();
|
|
156
168
|
if (runtime === "claude-code") return claudeCodeAdapter;
|
|
157
169
|
if (runtime === "codex") return codexAdapter;
|
|
170
|
+
if (runtime === "openclaw") return openclawAdapter;
|
|
158
171
|
return unknownAdapter;
|
|
159
172
|
}
|
|
160
173
|
async function listTandemIds(gateway) {
|
|
@@ -238,7 +251,7 @@ async function startProxy(config) {
|
|
|
238
251
|
}
|
|
239
252
|
console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
|
|
240
253
|
let server;
|
|
241
|
-
const { selectDelivery } = await import("./registry-
|
|
254
|
+
const { selectDelivery } = await import("./registry-VRFHKOPP.js");
|
|
242
255
|
const delivery = selectDelivery(adapter.runtime, {
|
|
243
256
|
supportsChannels: adapter.supportsChannels
|
|
244
257
|
});
|
|
@@ -274,8 +287,8 @@ async function startProxy(config) {
|
|
|
274
287
|
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
275
288
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
276
289
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
const { createParentWatchdog, shouldArmWatchdog } = await import("./parent-watchdog-LTUNF7PD.js");
|
|
291
|
+
if (shouldArmWatchdog(ccPid)) {
|
|
279
292
|
const watchdog = createParentWatchdog({
|
|
280
293
|
ccPid,
|
|
281
294
|
onParentGone: () => shutdown("parent (Claude Code) gone")
|
|
@@ -284,7 +297,7 @@ async function startProxy(config) {
|
|
|
284
297
|
teardownSteps.push(() => watchdog.stop());
|
|
285
298
|
} else {
|
|
286
299
|
console.error(
|
|
287
|
-
"[kojee-mcp] no Claude
|
|
300
|
+
"[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."
|
|
288
301
|
);
|
|
289
302
|
}
|
|
290
303
|
await startMcpServer(server);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createSecureFileSync,
|
|
3
|
+
renameSyncRetry,
|
|
2
4
|
secureDir,
|
|
3
5
|
secureFile
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-U5HHHRXA.js";
|
|
5
7
|
|
|
6
8
|
// src/auth/paired-config.ts
|
|
7
9
|
import fs from "fs";
|
|
@@ -10,6 +12,9 @@ import path from "path";
|
|
|
10
12
|
function pairedConfigPath() {
|
|
11
13
|
return path.join(os.homedir(), ".kojee", "config.json");
|
|
12
14
|
}
|
|
15
|
+
function perRuntimeConfigPath(runtime) {
|
|
16
|
+
return path.join(os.homedir(), ".kojee", "agents", runtime, "config.json");
|
|
17
|
+
}
|
|
13
18
|
function loadPairedConfig(filePath = pairedConfigPath()) {
|
|
14
19
|
try {
|
|
15
20
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -21,13 +26,21 @@ function loadPairedConfig(filePath = pairedConfigPath()) {
|
|
|
21
26
|
function savePairedConfig(filePath, config) {
|
|
22
27
|
const dir = path.dirname(filePath);
|
|
23
28
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
29
|
+
const kojeeRoot = path.join(os.homedir(), ".kojee");
|
|
30
|
+
if (fs.existsSync(kojeeRoot)) secureDir(kojeeRoot);
|
|
31
|
+
const agentsRoot = path.join(kojeeRoot, "agents");
|
|
32
|
+
if (fs.existsSync(agentsRoot)) secureDir(agentsRoot);
|
|
24
33
|
secureDir(dir);
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
const tmp = filePath + ".tmp";
|
|
35
|
+
createSecureFileSync(tmp);
|
|
36
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
37
|
+
secureFile(tmp);
|
|
38
|
+
renameSyncRetry(tmp, filePath);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export {
|
|
30
42
|
pairedConfigPath,
|
|
43
|
+
perRuntimeConfigPath,
|
|
31
44
|
loadPairedConfig,
|
|
32
45
|
savePairedConfig
|
|
33
46
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
secureDir,
|
|
3
3
|
secureFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-U5HHHRXA.js";
|
|
5
5
|
|
|
6
6
|
// src/tandem/session-discovery.ts
|
|
7
7
|
import fs from "fs";
|
|
@@ -50,8 +50,18 @@ function writeDiscoveryByKey(key, entry) {
|
|
|
50
50
|
secureFile(filePath);
|
|
51
51
|
}
|
|
52
52
|
function cleanupDiscoveryByKey(key) {
|
|
53
|
+
const filePath = discoveryPathForKey(key);
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
56
|
+
const data = JSON.parse(raw);
|
|
57
|
+
const owner = data.proxyPid ?? data.pid;
|
|
58
|
+
if (typeof owner === "number" && owner !== process.pid && isProcessAlive(owner)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
53
63
|
try {
|
|
54
|
-
fs.unlinkSync(
|
|
64
|
+
fs.unlinkSync(filePath);
|
|
55
65
|
} catch {
|
|
56
66
|
}
|
|
57
67
|
}
|
|
@@ -69,7 +79,8 @@ function isProcessAlive(pid) {
|
|
|
69
79
|
process.kill(pid, 0);
|
|
70
80
|
return true;
|
|
71
81
|
} catch (err) {
|
|
72
|
-
|
|
82
|
+
const code = err.code;
|
|
83
|
+
if (code === "EPERM" || code === "EACCES") return true;
|
|
73
84
|
return false;
|
|
74
85
|
}
|
|
75
86
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/runtime/npx-launcher.ts
|
|
2
|
+
function wrapNpxLauncher(command, args) {
|
|
3
|
+
if (process.platform === "win32" && command === "npx") {
|
|
4
|
+
return { command: "cmd", args: ["/c", "npx", ...args] };
|
|
5
|
+
}
|
|
6
|
+
return { command, args };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
wrapNpxLauncher
|
|
11
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
wrapNpxLauncher
|
|
3
|
+
} from "./chunk-D6JKFJ6A.js";
|
|
4
|
+
import {
|
|
5
|
+
kojeeHomeDir
|
|
6
|
+
} from "./chunk-SQL56SEB.js";
|
|
7
|
+
import {
|
|
8
|
+
secureFile
|
|
9
|
+
} from "./chunk-U5HHHRXA.js";
|
|
10
|
+
|
|
11
|
+
// src/wizard/capabilities/openclaw-mcp-config.ts
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import os from "os";
|
|
16
|
+
function defaultOpenclawConfigPath() {
|
|
17
|
+
const p = path.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
|
|
18
|
+
if (process.platform === "win32" && !/^[a-zA-Z]:/.test(p)) {
|
|
19
|
+
return path.win32.resolve(path.win32.join(os.homedir(), ".openclaw", "openclaw.json"));
|
|
20
|
+
}
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
var MCP_SERVER_NAME = "kojee";
|
|
24
|
+
function buildKojeeMcpBlock(configPath, pairedConfigPath) {
|
|
25
|
+
const base = ["-y", "kojee-mcp", ...pairedConfigPath ? ["--paired-config", pairedConfigPath] : []];
|
|
26
|
+
const { command, args } = wrapNpxLauncher("npx", base);
|
|
27
|
+
return { command, args, env: { KOJEE_RUNTIME: "openclaw", KOJEE_OPENCLAW_CONFIG: configPath } };
|
|
28
|
+
}
|
|
29
|
+
function readState(configPath) {
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
return { cfg: {}, unparseable: false };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const p = JSON.parse(raw);
|
|
38
|
+
return { cfg: p && typeof p === "object" ? p : {}, unparseable: false };
|
|
39
|
+
} catch {
|
|
40
|
+
return { cfg: {}, unparseable: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function atomicWrite(filePath, content) {
|
|
44
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
45
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
46
|
+
fs.writeFileSync(tmp, content, { mode: 384 });
|
|
47
|
+
secureFile(tmp);
|
|
48
|
+
fs.renameSync(tmp, filePath);
|
|
49
|
+
secureFile(filePath);
|
|
50
|
+
}
|
|
51
|
+
function writeOpenclawMcpConfig(configPath, opts) {
|
|
52
|
+
const { cfg, unparseable } = readState(configPath);
|
|
53
|
+
let backedUp;
|
|
54
|
+
if (unparseable) {
|
|
55
|
+
const stamp = (opts.now ? opts.now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
56
|
+
backedUp = `${configPath}.corrupt-${stamp}`;
|
|
57
|
+
fs.copyFileSync(configPath, backedUp);
|
|
58
|
+
secureFile(backedUp);
|
|
59
|
+
}
|
|
60
|
+
const next = { ...cfg };
|
|
61
|
+
const prevMcp = next.mcp && typeof next.mcp === "object" ? next.mcp : {};
|
|
62
|
+
const prevServers = prevMcp.servers && typeof prevMcp.servers === "object" ? prevMcp.servers : {};
|
|
63
|
+
next.mcp = {
|
|
64
|
+
...prevMcp,
|
|
65
|
+
servers: { ...prevServers, [MCP_SERVER_NAME]: buildKojeeMcpBlock(configPath, opts.pairedConfigPath) }
|
|
66
|
+
};
|
|
67
|
+
const prevHooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
68
|
+
const existingToken = typeof prevHooks.token === "string" && prevHooks.token.trim() ? prevHooks.token.trim() : "";
|
|
69
|
+
const hooksTokenGenerated = existingToken === "";
|
|
70
|
+
const wakeToken = existingToken || crypto.randomBytes(32).toString("hex");
|
|
71
|
+
next.hooks = { ...prevHooks, enabled: true, token: wakeToken };
|
|
72
|
+
atomicWrite(configPath, JSON.stringify(next, null, 2) + "\n");
|
|
73
|
+
return { hooksTokenGenerated, wakeToken, ...backedUp ? { backedUp } : {} };
|
|
74
|
+
}
|
|
75
|
+
function removeOpenclawMcpServer(configPath) {
|
|
76
|
+
const { cfg, unparseable } = readState(configPath);
|
|
77
|
+
if (unparseable) return false;
|
|
78
|
+
const mcp = cfg.mcp;
|
|
79
|
+
const servers = mcp?.servers;
|
|
80
|
+
if (!servers || !(MCP_SERVER_NAME in servers)) return false;
|
|
81
|
+
const { [MCP_SERVER_NAME]: _drop, ...restServers } = servers;
|
|
82
|
+
const next = { ...cfg };
|
|
83
|
+
if (Object.keys(restServers).length > 0) {
|
|
84
|
+
next.mcp = { ...mcp, servers: restServers };
|
|
85
|
+
} else {
|
|
86
|
+
const nextMcp = { ...mcp };
|
|
87
|
+
delete nextMcp.servers;
|
|
88
|
+
if (Object.keys(nextMcp).length > 0) next.mcp = nextMcp;
|
|
89
|
+
else delete next.mcp;
|
|
90
|
+
}
|
|
91
|
+
atomicWrite(configPath, JSON.stringify(next, null, 2) + "\n");
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
defaultOpenclawConfigPath,
|
|
97
|
+
MCP_SERVER_NAME,
|
|
98
|
+
writeOpenclawMcpConfig,
|
|
99
|
+
removeOpenclawMcpServer
|
|
100
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadPairedConfig,
|
|
3
|
+
savePairedConfig
|
|
4
|
+
} from "./chunk-5SZHXYPK.js";
|
|
5
|
+
import {
|
|
6
|
+
AuthModule
|
|
7
|
+
} from "./chunk-I67C2HYA.js";
|
|
8
|
+
import {
|
|
9
|
+
createDPoPProof
|
|
10
|
+
} from "./chunk-MIEI4PLB.js";
|
|
11
|
+
import {
|
|
12
|
+
loadKeystore
|
|
13
|
+
} from "./chunk-6G6YYST6.js";
|
|
14
|
+
import {
|
|
15
|
+
renameSyncRetry
|
|
16
|
+
} from "./chunk-U5HHHRXA.js";
|
|
17
|
+
|
|
18
|
+
// src/tandem/pair.ts
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
async function fetchDisplayIdentity(url, code, keystorePath) {
|
|
21
|
+
try {
|
|
22
|
+
const ks = await loadKeystore(keystorePath, url).catch(() => null);
|
|
23
|
+
if (!ks) return {};
|
|
24
|
+
const meUrl = `${url}/api/v1/users/me/`;
|
|
25
|
+
const doFetch = async (nonce) => {
|
|
26
|
+
const proof = await createDPoPProof(ks.privateKey, ks.kid, "GET", meUrl, nonce, code);
|
|
27
|
+
return fetch(meUrl, { headers: { Authorization: `DPoP ${code}`, DPoP: proof } });
|
|
28
|
+
};
|
|
29
|
+
let res = await doFetch();
|
|
30
|
+
if (res.status === 401) {
|
|
31
|
+
const nonce = res.headers.get("DPoP-Nonce");
|
|
32
|
+
if (nonce) res = await doFetch(nonce);
|
|
33
|
+
}
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const body = await res.json();
|
|
36
|
+
return {
|
|
37
|
+
...body.principal_id ? { principal_id: body.principal_id } : {},
|
|
38
|
+
...body.agent_id ? { agent_id: body.agent_id } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
async function runPair(opts) {
|
|
46
|
+
if (!opts.allowOverwrite && loadPairedConfig(opts.configPath) !== null) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Already paired (config exists at ${opts.configPath}). To re-pair this slot, delete the config file first. For a second account, use --keystore-path /custom/keypair.json.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const existing = await loadKeystore(opts.keystorePath, opts.url).catch(() => null);
|
|
52
|
+
if (!existing) {
|
|
53
|
+
const tmpKeystore = `${opts.keystorePath}.tmp-${process.pid}`;
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(tmpKeystore);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
const auth = new AuthModule(opts.code, opts.url, tmpKeystore);
|
|
59
|
+
await auth.ensureEnrolled();
|
|
60
|
+
renameSyncRetry(tmpKeystore, opts.keystorePath);
|
|
61
|
+
}
|
|
62
|
+
const { principal_id, agent_id } = await fetchDisplayIdentity(
|
|
63
|
+
opts.url,
|
|
64
|
+
opts.code,
|
|
65
|
+
opts.keystorePath
|
|
66
|
+
);
|
|
67
|
+
const config = {
|
|
68
|
+
token: opts.code,
|
|
69
|
+
broker_url: opts.url,
|
|
70
|
+
paired_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71
|
+
...opts.runtime ? { runtime: opts.runtime } : {},
|
|
72
|
+
...principal_id ? { principal_id } : {},
|
|
73
|
+
...agent_id ? { agent_id } : {}
|
|
74
|
+
};
|
|
75
|
+
savePairedConfig(opts.configPath, config);
|
|
76
|
+
const message = principal_id && agent_id ? `Paired as ${principal_id} (${agent_id}). Keypair: ${opts.keystorePath}. Config: ${opts.configPath}.` : `Paired. Keypair: ${opts.keystorePath}. Config: ${opts.configPath}.`;
|
|
77
|
+
return { message };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
runPair
|
|
82
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadPairedConfig
|
|
3
|
+
} from "./chunk-5SZHXYPK.js";
|
|
4
|
+
import {
|
|
5
|
+
deriveKeystorePath
|
|
6
|
+
} from "./chunk-6G6YYST6.js";
|
|
7
|
+
|
|
8
|
+
// src/auth/resolve-slot.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var CONNECT_RUNTIMES = ["claude-code", "codex", "openclaw"];
|
|
13
|
+
function isConnectRuntime(v) {
|
|
14
|
+
return v !== void 0 && CONNECT_RUNTIMES.includes(v);
|
|
15
|
+
}
|
|
16
|
+
function resolvePairedSlot(opts) {
|
|
17
|
+
if (opts.pairedConfig) {
|
|
18
|
+
return { configPath: path.resolve(opts.pairedConfig), scope: "paired" };
|
|
19
|
+
}
|
|
20
|
+
const dir = opts.kojeeDir ?? path.join(os.homedir(), ".kojee");
|
|
21
|
+
if (isConnectRuntime(opts.runtime)) {
|
|
22
|
+
const perRuntime = path.join(dir, "agents", opts.runtime, "config.json");
|
|
23
|
+
if (fs.existsSync(perRuntime)) return { configPath: perRuntime, scope: "paired" };
|
|
24
|
+
}
|
|
25
|
+
return { configPath: path.join(dir, "config.json"), scope: "legacy" };
|
|
26
|
+
}
|
|
27
|
+
function resolveLaunchCreds(opts) {
|
|
28
|
+
const { configPath } = resolvePairedSlot({ pairedConfig: opts.pairedConfig });
|
|
29
|
+
const cfg = loadPairedConfig(configPath);
|
|
30
|
+
if (!cfg || typeof cfg.token !== "string" || cfg.token === "") {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
configPath,
|
|
34
|
+
error: `Not connected for this runtime (paired-config: ${configPath}) \u2014 run \`kojee-mcp connect <code> --runtime <id>\`.`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const url = (opts.url ?? cfg.broker_url).replace(/\/+$/, "");
|
|
38
|
+
return { ok: true, token: cfg.token, url, keystorePath: deriveKeystorePath(cfg.token), configPath };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
CONNECT_RUNTIMES,
|
|
43
|
+
resolvePairedSlot,
|
|
44
|
+
resolveLaunchCreds
|
|
45
|
+
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createDPoPProof,
|
|
3
2
|
getSessionId
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Z5LPNJQ6.js";
|
|
4
|
+
import {
|
|
5
|
+
createDPoPProof
|
|
6
|
+
} from "./chunk-MIEI4PLB.js";
|
|
5
7
|
|
|
6
8
|
// src/tandem/event-stream.ts
|
|
9
|
+
import { Agent } from "undici";
|
|
7
10
|
var STALE_FLOOR_MS = 9e4;
|
|
8
11
|
var STALE_INTERVAL_MULTIPLIER = 3;
|
|
9
12
|
var STALE_CHECK_INTERVAL_MS = 5e3;
|
|
@@ -71,6 +74,10 @@ function createBackoffController(options = {}) {
|
|
|
71
74
|
var LOG_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
72
75
|
async function startEventStream(opts) {
|
|
73
76
|
let stopped = false;
|
|
77
|
+
const streamDispatcher = new Agent({
|
|
78
|
+
bodyTimeout: UNDICI_DEFAULT_BODY_TIMEOUT_MS,
|
|
79
|
+
headersTimeout: UNDICI_DEFAULT_BODY_TIMEOUT_MS
|
|
80
|
+
});
|
|
74
81
|
const cursors = /* @__PURE__ */ new Map();
|
|
75
82
|
let currentController = new AbortController();
|
|
76
83
|
let connectGeneration = 0;
|
|
@@ -95,6 +102,7 @@ async function startEventStream(opts) {
|
|
|
95
102
|
currentController,
|
|
96
103
|
isReconnect,
|
|
97
104
|
state,
|
|
105
|
+
streamDispatcher,
|
|
98
106
|
(tandemId, cursor) => {
|
|
99
107
|
const prev = cursors.get(tandemId);
|
|
100
108
|
if (prev === void 0 || cursor > prev) cursors.set(tandemId, cursor);
|
|
@@ -110,7 +118,13 @@ async function startEventStream(opts) {
|
|
|
110
118
|
if (!opened && stopped) return;
|
|
111
119
|
} catch (err) {
|
|
112
120
|
if (stopped) return;
|
|
113
|
-
console.error(
|
|
121
|
+
console.error(
|
|
122
|
+
"[event-stream] disconnect:",
|
|
123
|
+
err?.message,
|
|
124
|
+
// Node/undici stash the real transport error here (errno/TLS); the
|
|
125
|
+
// top-level message is a generic "fetch failed" that drops it.
|
|
126
|
+
err?.cause ?? ""
|
|
127
|
+
);
|
|
114
128
|
state.connected = false;
|
|
115
129
|
await emitStatus(opts, `status=disconnected reason=${statusReason(err)}`);
|
|
116
130
|
}
|
|
@@ -125,6 +139,8 @@ async function startEventStream(opts) {
|
|
|
125
139
|
stopped = true;
|
|
126
140
|
state.connected = false;
|
|
127
141
|
currentController.abort();
|
|
142
|
+
void streamDispatcher.close().catch(() => {
|
|
143
|
+
});
|
|
128
144
|
});
|
|
129
145
|
handle.reconnect = () => {
|
|
130
146
|
if (stopped) return;
|
|
@@ -144,12 +160,12 @@ async function startEventStream(opts) {
|
|
|
144
160
|
});
|
|
145
161
|
return handle;
|
|
146
162
|
}
|
|
147
|
-
async function connectAndConsume(opts, cursors, controller, isReconnect, state, onCursor, onOpen) {
|
|
163
|
+
async function connectAndConsume(opts, cursors, controller, isReconnect, state, dispatcher, onCursor, onOpen) {
|
|
148
164
|
const mapSince = cursors.size > 0 ? serializeCursorMap(cursors) : null;
|
|
149
|
-
let res = await openStream(opts, controller, mapSince);
|
|
165
|
+
let res = await openStream(opts, controller, mapSince, dispatcher);
|
|
150
166
|
if (res.status === 400 && mapSince !== null) {
|
|
151
167
|
console.error("[event-stream] per-room ?since rejected (400) \u2014 falling back to bare cursor");
|
|
152
|
-
res = await openStream(opts, controller, String(Math.min(...cursors.values())));
|
|
168
|
+
res = await openStream(opts, controller, String(Math.min(...cursors.values())), dispatcher);
|
|
153
169
|
}
|
|
154
170
|
if (!res.ok) throw new Error(`SSE connect failed: ${res.status}`);
|
|
155
171
|
if (!res.body) throw new Error("SSE response has no body");
|
|
@@ -163,7 +179,7 @@ async function connectAndConsume(opts, cursors, controller, isReconnect, state,
|
|
|
163
179
|
await consumeSse(res.body, opts, controller, state, onCursor);
|
|
164
180
|
return true;
|
|
165
181
|
}
|
|
166
|
-
async function openStream(opts, controller, sinceValue) {
|
|
182
|
+
async function openStream(opts, controller, sinceValue, dispatcher) {
|
|
167
183
|
const params = new URLSearchParams();
|
|
168
184
|
if (sinceValue !== null) params.set("since", sinceValue);
|
|
169
185
|
const url = `${opts.brokerUrl}/api/v2/tandems/stream${params.toString() ? "?" + params.toString() : ""}`;
|
|
@@ -183,7 +199,11 @@ async function openStream(opts, controller, sinceValue) {
|
|
|
183
199
|
"Mcp-Session-Id": getSessionId(),
|
|
184
200
|
Accept: "text/event-stream"
|
|
185
201
|
},
|
|
186
|
-
signal: controller.signal
|
|
202
|
+
signal: controller.signal,
|
|
203
|
+
// Isolate this long-lived stream onto its own connection pool so it can
|
|
204
|
+
// never block tool-call POSTs on the shared global pool. The dispatcher is
|
|
205
|
+
// owned by startEventStream (per-stream lifecycle) and closed on teardown.
|
|
206
|
+
dispatcher
|
|
187
207
|
});
|
|
188
208
|
}
|
|
189
209
|
async function consumeSse(body, opts, controller, state, onCursor) {
|
|
@@ -288,8 +308,24 @@ async function emitStatus(opts, fields) {
|
|
|
288
308
|
}
|
|
289
309
|
}
|
|
290
310
|
function statusReason(err) {
|
|
291
|
-
const
|
|
292
|
-
|
|
311
|
+
const sanitize = (s) => s.replace(/\s+/g, "_");
|
|
312
|
+
const base = sanitize(err?.message ?? String(err));
|
|
313
|
+
const cause = err?.cause;
|
|
314
|
+
let token = base;
|
|
315
|
+
if (cause != null) {
|
|
316
|
+
let causePart;
|
|
317
|
+
if (typeof cause === "string") {
|
|
318
|
+
causePart = cause;
|
|
319
|
+
} else {
|
|
320
|
+
const code = cause.code;
|
|
321
|
+
const message = cause.message;
|
|
322
|
+
if (typeof code === "string" && code) causePart = code;
|
|
323
|
+
else if (typeof message === "string" && message) causePart = message;
|
|
324
|
+
else causePart = String(cause);
|
|
325
|
+
}
|
|
326
|
+
token = `${base}:${sanitize(causePart)}`;
|
|
327
|
+
}
|
|
328
|
+
return token.slice(0, 80);
|
|
293
329
|
}
|
|
294
330
|
function serializeCursorMap(cursors) {
|
|
295
331
|
return [...cursors.entries()].map(([tandemId, cursor]) => `${tandemId}:${cursor}`).join(",");
|
|
@@ -411,6 +447,7 @@ export {
|
|
|
411
447
|
createAdaptiveWatchdog,
|
|
412
448
|
createBackoffController,
|
|
413
449
|
startEventStream,
|
|
450
|
+
statusReason,
|
|
414
451
|
serializeCursorMap,
|
|
415
452
|
sanitizeDisplayname,
|
|
416
453
|
normalizeBackendEvent
|