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.
Files changed (70) hide show
  1. package/README.md +13 -8
  2. package/dist/ancestry-A2F5KQ6A.js +10 -0
  3. package/dist/cc-session-id-RURNIHHC.js +10 -0
  4. package/dist/{chunk-U4MNICQI.js → chunk-247WFMCJ.js} +12 -22
  5. package/dist/{chunk-PSVVRGJC.js → chunk-5H75AEZ2.js} +22 -9
  6. package/dist/{chunk-YH27B6SW.js → chunk-5SZHXYPK.js} +16 -3
  7. package/dist/{chunk-DO42NPNR.js → chunk-67F67AQ6.js} +14 -3
  8. package/dist/{chunk-CH32ELFX.js → chunk-6G6YYST6.js} +1 -1
  9. package/dist/chunk-D6JKFJ6A.js +11 -0
  10. package/dist/chunk-E6WMFMM2.js +100 -0
  11. package/dist/chunk-EIAUW6KO.js +82 -0
  12. package/dist/chunk-EIH2LNF4.js +45 -0
  13. package/dist/{chunk-74XFVX6Z.js → chunk-FSKGQ6GT.js} +47 -10
  14. package/dist/{chunk-GI2CKKBL.js → chunk-GKXHKR3B.js} +1 -1
  15. package/dist/{wizard-Y3ULSO2C.js → chunk-GVWYW7MY.js} +247 -138
  16. package/dist/{chunk-JXMVZEQ7.js → chunk-I67C2HYA.js} +1 -1
  17. package/dist/chunk-KNEJTD6G.js +31 -0
  18. package/dist/{chunk-NR4Y54OL.js → chunk-MIEI4PLB.js} +1 -19
  19. package/dist/chunk-NE2F5CKS.js +113 -0
  20. package/dist/{chunk-XXFVWP6H.js → chunk-OB5T6P24.js} +1 -1
  21. package/dist/{chunk-65KRRDHP.js → chunk-OBIJ6WQP.js} +82 -27
  22. package/dist/{chunk-DXJ6QLSJ.js → chunk-PKOBR7ZN.js} +1 -1
  23. package/dist/{chunk-2OLXXOT3.js → chunk-PPTKGWFF.js} +69 -3
  24. package/dist/{chunk-X672ZN7V.js → chunk-QEJUNP3X.js} +3 -2
  25. package/dist/chunk-QJFMU4QC.js +33 -0
  26. package/dist/{chunk-EW72ZNQL.js → chunk-RDLF4NQC.js} +1 -1
  27. package/dist/{chunk-XIDCYUTK.js → chunk-SGRVG4HW.js} +9 -4
  28. package/dist/{chunk-DURRQDYI.js → chunk-TNZITJAB.js} +45 -15
  29. package/dist/chunk-U5HHHRXA.js +103 -0
  30. package/dist/chunk-WBXU27BF.js +35 -0
  31. package/dist/chunk-WIU5WGDF.js +10 -0
  32. package/dist/chunk-XJEBJIQE.js +52 -0
  33. package/dist/chunk-Z5LPNJQ6.js +20 -0
  34. package/dist/cli.js +121 -89
  35. package/dist/{chunk-35XBRG3V.js → codex-stop-hook-EOMQD3J4.js} +11 -15
  36. package/dist/connect-handler-HIRM624N.js +137 -0
  37. package/dist/{control-token-4BUCTYQB.js → control-token-KXSZ3GBY.js} +2 -2
  38. package/dist/{doctor-U3MTB4GY.js → doctor-PLKLHTJM.js} +48 -22
  39. package/dist/{doctor-codex-PA3WO6LR.js → doctor-codex-HARAAH4Y.js} +23 -6
  40. package/dist/doctor-openclaw-LL6NSPLC.js +86 -0
  41. package/dist/{event-log-HOBO3ECB.js → event-log-2NBJEIEP.js} +5 -3
  42. package/dist/{event-stream-XX5EZ6HN.js → event-stream-KRYWEYWO.js} +7 -4
  43. package/dist/{hook-server-XK2NHLJV.js → hook-server-CWBH6PFL.js} +2 -2
  44. package/dist/index.js +13 -11
  45. package/dist/{install-LJY2CHKG.js → install-V7LSQCYZ.js} +37 -22
  46. package/dist/{keystore-XLEV3FL5.js → keystore-LQNFDHGO.js} +2 -2
  47. package/dist/lib.d.ts +4 -1
  48. package/dist/lib.js +12 -9
  49. package/dist/pair-HTCVSMR6.js +11 -0
  50. package/dist/{paired-config-JTFLHMZ2.js → paired-config-GUDDO2CH.js} +4 -2
  51. package/dist/{parent-watchdog-TLU355FB.js → parent-watchdog-LTUNF7PD.js} +24 -5
  52. package/dist/{registry-CGLE5X54.js → registry-VRFHKOPP.js} +195 -26
  53. package/dist/resolve-slot-BFIB46Y5.js +13 -0
  54. package/dist/{runtime-record-WO4IECM6.js → runtime-record-OXRLTOLC.js} +2 -2
  55. package/dist/{send-cli-IPADRDD2.js → send-cli-45RLGYIC.js} +24 -12
  56. package/dist/{server-LTFG4GMV.js → server-YTHOMA4H.js} +5 -3
  57. package/dist/{session-discovery-FNMJGFPM.js → session-discovery-NHCZ2XWA.js} +2 -2
  58. package/dist/setup-handler-UHVKSX7E.js +159 -0
  59. package/dist/{stop-hook-QZTFH5WV.js → stop-hook-YI4KRKF2.js} +17 -20
  60. package/dist/{tail-stream-CEJZGSIR.js → tail-stream-NBGHHBS4.js} +61 -22
  61. package/dist/{user-prompt-submit-hook-FQ43NIHC.js → user-prompt-submit-hook-DTXXFDSD.js} +11 -14
  62. package/package.json +2 -1
  63. package/dist/ancestry-ONFBQEP5.js +0 -8
  64. package/dist/chunk-BLEGIR35.js +0 -43
  65. package/dist/chunk-CLKCNV2A.js +0 -100
  66. package/dist/chunk-OGHDTFAX.js +0 -50
  67. package/dist/chunk-VHKPWUX7.js +0 -41
  68. package/dist/codex-stop-hook-VY7DOMAG.js +0 -16
  69. package/dist/doctor-openclaw-SS2TMQOX.js +0 -95
  70. 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
- npx kojee-mcp init --runtime openclaw --webhook-url https://your-receiver/kojee
166
- # daemon runtimes: print/record the webhook-sink env to export
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
- - **hermes / openclaw** — these runtimes have **native channel plugins** (see
184
- [Native gateway runtimes](#native-gateway-runtimes-openclaw--hermes)). When you
185
- instead run the proxy as a **daemon** feeding a webhook receiver, `init --runtime
186
- hermes|openclaw` writes no MCP-config/hooks of ours, validates the webhook env,
187
- and prints + records the env to export (secret redacted) into a source-able
188
- `~/.kojee/<runtime>.env`.
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.
@@ -0,0 +1,10 @@
1
+ import {
2
+ deriveDiscoveryKey,
3
+ findClaudeAncestorPid,
4
+ pidLooksLikeClaude
5
+ } from "./chunk-XJEBJIQE.js";
6
+ export {
7
+ deriveDiscoveryKey,
8
+ findClaudeAncestorPid,
9
+ pidLooksLikeClaude
10
+ };
@@ -0,0 +1,10 @@
1
+ import {
2
+ SESSION_ID_ENV_VARS,
3
+ resolveInstanceKey,
4
+ resolveSharedSessionId
5
+ } from "./chunk-KNEJTD6G.js";
6
+ export {
7
+ SESSION_ID_ENV_VARS,
8
+ resolveInstanceKey,
9
+ resolveSharedSessionId
10
+ };
@@ -1,14 +1,19 @@
1
1
  import {
2
- createDPoPProof,
3
2
  deriveStableSessionId,
4
3
  getSessionId,
5
4
  setStableSessionId
6
- } from "./chunk-NR4Y54OL.js";
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-2OLXXOT3.js";
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: [{ type: "text", text: `Gateway error: ${response.status}` }],
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-XXFVWP6H.js";
3
+ } from "./chunk-OB5T6P24.js";
4
4
  import {
5
5
  GatewayClient,
6
6
  applyStableSessionId
7
- } from "./chunk-U4MNICQI.js";
7
+ } from "./chunk-247WFMCJ.js";
8
8
  import {
9
9
  AuthModule
10
- } from "./chunk-JXMVZEQ7.js";
10
+ } from "./chunk-I67C2HYA.js";
11
11
  import {
12
12
  createMcpServer,
13
13
  startMcpServer
14
- } from "./chunk-XIDCYUTK.js";
14
+ } from "./chunk-SGRVG4HW.js";
15
15
  import {
16
16
  findClaudeAncestorPid
17
- } from "./chunk-VHKPWUX7.js";
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-CGLE5X54.js");
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
- if (ccPid !== null) {
278
- const { createParentWatchdog } = await import("./parent-watchdog-TLU355FB.js");
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 Code ancestor found \u2014 parent-liveness watchdog NOT armed (stdin/signal handlers still cover clean exits)"
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-BLEGIR35.js";
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
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
26
- secureFile(filePath);
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-BLEGIR35.js";
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(discoveryPathForKey(key));
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
- if (err.code === "EPERM") return true;
82
+ const code = err.code;
83
+ if (code === "EPERM" || code === "EACCES") return true;
73
84
  return false;
74
85
  }
75
86
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  secureDir,
3
3
  secureFile
4
- } from "./chunk-BLEGIR35.js";
4
+ } from "./chunk-U5HHHRXA.js";
5
5
 
6
6
  // src/auth/keystore.ts
7
7
  import { importJWK, exportJWK, generateKeyPair } from "jose";
@@ -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-NR4Y54OL.js";
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("[event-stream] disconnect:", err.message);
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 msg = err?.message ?? String(err);
292
- return msg.replace(/\s+/g, "_").slice(0, 60);
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