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
@@ -1,19 +1,21 @@
1
1
  import {
2
- clearRuntimeRecord,
3
- readRecordedRuntime,
4
- recordRuntime
5
- } from "./chunk-EW72ZNQL.js";
2
+ removeOpenclawMcpServer,
3
+ writeOpenclawMcpConfig
4
+ } from "./chunk-E6WMFMM2.js";
6
5
  import {
7
6
  buildCodexMcpServerTable,
8
7
  buildCodexStopHookBlock,
9
8
  removeCodexConfig,
10
9
  writeCodexConfig
11
- } from "./chunk-65KRRDHP.js";
10
+ } from "./chunk-OBIJ6WQP.js";
12
11
  import {
13
- CHANNEL_ID,
14
- removeOpenclawChannel,
15
- writeOpenclawChannelConfig
16
- } from "./chunk-CLKCNV2A.js";
12
+ CONNECT_RUNTIMES
13
+ } from "./chunk-EIH2LNF4.js";
14
+ import {
15
+ clearRuntimeRecord,
16
+ readRecordedRuntime,
17
+ recordRuntime
18
+ } from "./chunk-RDLF4NQC.js";
17
19
  import {
18
20
  kojeeHomeDir
19
21
  } from "./chunk-SQL56SEB.js";
@@ -26,17 +28,109 @@ import {
26
28
  resolveWebhookConfig
27
29
  } from "./chunk-V5VZPYMZ.js";
28
30
  import {
29
- secureFile
30
- } from "./chunk-BLEGIR35.js";
31
+ loadPairedConfig,
32
+ pairedConfigPath,
33
+ perRuntimeConfigPath
34
+ } from "./chunk-5SZHXYPK.js";
35
+ import {
36
+ defaultPairedKeystorePath,
37
+ deriveKeystorePath
38
+ } from "./chunk-6G6YYST6.js";
39
+ import {
40
+ secureFile,
41
+ unlinkSyncRetry
42
+ } from "./chunk-U5HHHRXA.js";
31
43
  import {
32
44
  CODEX_LISTEN_CAP_MS,
33
45
  buildWebhookReceiverNote
34
- } from "./chunk-X672ZN7V.js";
46
+ } from "./chunk-QEJUNP3X.js";
47
+
48
+ // src/wizard/pair-slot.ts
49
+ import fs from "fs";
50
+ import os from "os";
51
+ import path from "path";
52
+ function absPerRuntimeConfigPath(runtime) {
53
+ const p = path.resolve(perRuntimeConfigPath(runtime));
54
+ if (process.platform === "win32" && !/^[a-zA-Z]:/.test(p)) {
55
+ return path.resolve(path.join(os.homedir(), ".kojee", "agents", runtime, "config.json"));
56
+ }
57
+ return p;
58
+ }
59
+ function reconcileConnectPairSlot(runtime, code) {
60
+ const messages = [];
61
+ for (const other of CONNECT_RUNTIMES) {
62
+ if (other === runtime) continue;
63
+ const sibling = loadPairedConfig(perRuntimeConfigPath(other));
64
+ if (sibling?.token === code) {
65
+ return {
66
+ ok: false,
67
+ output: `This pair code is already connected as ${other}; use a distinct per-agent code for ${runtime}. The dashboard mints one pair code per agent.`,
68
+ exitCode: 2
69
+ };
70
+ }
71
+ }
72
+ if (!CONNECT_RUNTIMES.includes(runtime)) {
73
+ return { ok: true, messages };
74
+ }
75
+ const legacy = loadPairedConfig(pairedConfigPath());
76
+ if (legacy && legacy.token === code) {
77
+ if (legacy.runtime === "hermes") {
78
+ return {
79
+ ok: false,
80
+ output: `This pair code backs the live hermes connection (${pairedConfigPath()}); retiring it would silently break the hermes daemon. Use a distinct per-agent code for ${runtime}. The dashboard mints one pair code per agent.`,
81
+ exitCode: 2
82
+ };
83
+ }
84
+ try {
85
+ unlinkSyncRetry(pairedConfigPath());
86
+ } catch (err) {
87
+ const e = err;
88
+ if (e.code !== "ENOENT") {
89
+ return {
90
+ ok: false,
91
+ output: `Aborted: could not retire the legacy global config at ${pairedConfigPath()} (${e.message}). Close any running kojee proxy and retry.`,
92
+ exitCode: 1
93
+ };
94
+ }
95
+ }
96
+ const legacyKeystore = defaultPairedKeystorePath();
97
+ const target = deriveKeystorePath(code);
98
+ try {
99
+ if (fs.existsSync(legacyKeystore) && !fs.existsSync(target)) {
100
+ fs.renameSync(legacyKeystore, target);
101
+ }
102
+ } catch {
103
+ }
104
+ messages.push("Migrated the legacy global slot into this per-runtime connection.");
105
+ }
106
+ return { ok: true, messages };
107
+ }
108
+ function pairSlotFor(runtime, code) {
109
+ if (CONNECT_RUNTIMES.includes(runtime)) {
110
+ const configPath = absPerRuntimeConfigPath(runtime);
111
+ return {
112
+ configPath,
113
+ keystorePath: deriveKeystorePath(code),
114
+ runtime,
115
+ allowOverwrite: true,
116
+ pairedConfigPath: configPath
117
+ };
118
+ }
119
+ return {
120
+ configPath: pairedConfigPath(),
121
+ keystorePath: defaultPairedKeystorePath(),
122
+ // F2: stamp the pairing runtime into the global PairedConfig so a later
123
+ // connect-runtime reconcile can recognize a live hermes slot and refuse to
124
+ // retire it (see reconcileConnectPairSlot).
125
+ runtime,
126
+ allowOverwrite: false
127
+ };
128
+ }
35
129
 
36
130
  // src/wizard/wizard.ts
37
131
  import crypto2 from "crypto";
38
- import fs4 from "fs";
39
- import path5 from "path";
132
+ import fs5 from "fs";
133
+ import path6 from "path";
40
134
  import { fileURLToPath } from "url";
41
135
 
42
136
  // src/wizard/registry.ts
@@ -53,15 +147,15 @@ function runtimeUsesWebhook(id) {
53
147
 
54
148
  // src/wizard/capabilities/webhook-secret.ts
55
149
  import crypto from "crypto";
56
- import fs from "fs";
57
- import path from "path";
150
+ import fs2 from "fs";
151
+ import path2 from "path";
58
152
  function generateSecret() {
59
153
  return crypto.randomBytes(32).toString("hex");
60
154
  }
61
155
  function readSecretFromEnv(envPath) {
62
156
  let body;
63
157
  try {
64
- body = fs.readFileSync(envPath, "utf8");
158
+ body = fs2.readFileSync(envPath, "utf8");
65
159
  } catch {
66
160
  return void 0;
67
161
  }
@@ -74,10 +168,10 @@ function shellSingleQuote(value) {
74
168
  return `'${value.replace(/'/g, `'\\''`)}'`;
75
169
  }
76
170
  function upsertEnvFile(filePath, vars, opts = {}) {
77
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
171
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
78
172
  let lines = [];
79
173
  try {
80
- lines = fs.readFileSync(filePath, "utf8").split("\n");
174
+ lines = fs2.readFileSync(filePath, "utf8").split("\n");
81
175
  } catch {
82
176
  }
83
177
  if (opts.header && !lines.some((l) => l === opts.header)) lines.unshift(opts.header);
@@ -89,7 +183,7 @@ function upsertEnvFile(filePath, vars, opts = {}) {
89
183
  if (idx >= 0) lines[idx] = line;
90
184
  else lines.push(line);
91
185
  }
92
- fs.writeFileSync(filePath, lines.join("\n") + "\n", { mode: 384 });
186
+ fs2.writeFileSync(filePath, lines.join("\n") + "\n", { mode: 384 });
93
187
  }
94
188
  function writeWebhookSecretBothSides(opts) {
95
189
  const existing = opts.existingSecret ?? readSecretFromEnv(opts.daemonEnvPath) ?? readSecretFromEnv(opts.adapterEnvPath);
@@ -117,11 +211,11 @@ function writeWebhookSecretBothSides(opts) {
117
211
  }
118
212
 
119
213
  // src/wizard/installers/hermes.ts
120
- import path4 from "path";
214
+ import path5 from "path";
121
215
 
122
216
  // src/wizard/plugin-payload.ts
123
- import fs2 from "fs";
124
- import path2 from "path";
217
+ import fs3 from "fs";
218
+ import path3 from "path";
125
219
  var HERMES_PAYLOAD_FILES = [
126
220
  "__init__.py",
127
221
  "adapter.py",
@@ -129,24 +223,24 @@ var HERMES_PAYLOAD_FILES = [
129
223
  "plugin.yaml"
130
224
  ];
131
225
  function stagePluginPayload(opts) {
132
- fs2.mkdirSync(opts.destDir, { recursive: true });
226
+ fs3.mkdirSync(opts.destDir, { recursive: true });
133
227
  const written = [];
134
228
  for (const file of opts.files) {
135
- const src = path2.join(opts.srcDir, file);
136
- if (!fs2.existsSync(src)) {
229
+ const src = path3.join(opts.srcDir, file);
230
+ if (!fs3.existsSync(src)) {
137
231
  throw new Error(
138
232
  `stagePluginPayload: missing payload file '${file}' in ${opts.srcDir}`
139
233
  );
140
234
  }
141
- const dest = path2.join(opts.destDir, file);
142
- fs2.copyFileSync(src, dest);
235
+ const dest = path3.join(opts.destDir, file);
236
+ fs3.copyFileSync(src, dest);
143
237
  written.push(dest);
144
238
  }
145
239
  return written;
146
240
  }
147
241
  function resolveBundledPayloadDir(runtime, baseDir) {
148
- const dir = path2.join(baseDir, "plugins", runtime);
149
- if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
242
+ const dir = path3.join(baseDir, "plugins", runtime);
243
+ if (!fs3.existsSync(dir) || !fs3.statSync(dir).isDirectory()) {
150
244
  throw new Error(
151
245
  `resolveBundledPayloadDir: no bundled payload for '${runtime}' at ${dir} \u2014 run the build (npm run build) so dist/plugins is staged.`
152
246
  );
@@ -154,10 +248,10 @@ function resolveBundledPayloadDir(runtime, baseDir) {
154
248
  return dir;
155
249
  }
156
250
  function missingPayloadFiles(runtime, baseDir, files) {
157
- const dir = path2.join(baseDir, "plugins", runtime);
158
- const dirOk = fs2.existsSync(dir) && fs2.statSync(dir).isDirectory();
251
+ const dir = path3.join(baseDir, "plugins", runtime);
252
+ const dirOk = fs3.existsSync(dir) && fs3.statSync(dir).isDirectory();
159
253
  if (!dirOk) return [...files];
160
- return files.filter((f) => !fs2.existsSync(path2.join(dir, f)));
254
+ return files.filter((f) => !fs3.existsSync(path3.join(dir, f)));
161
255
  }
162
256
  function installBundledPayload(opts) {
163
257
  const srcDir = resolveBundledPayloadDir(opts.runtime, opts.baseDir);
@@ -165,8 +259,8 @@ function installBundledPayload(opts) {
165
259
  }
166
260
 
167
261
  // src/wizard/service.ts
168
- import fs3 from "fs";
169
- import path3 from "path";
262
+ import fs4 from "fs";
263
+ import path4 from "path";
170
264
  function systemdUnit(spec) {
171
265
  return [
172
266
  "[Unit]",
@@ -213,7 +307,7 @@ function launchdPlist(spec, label) {
213
307
  }
214
308
  function planService(platform, spec) {
215
309
  if (platform === "linux") {
216
- const unitPath = path3.join(
310
+ const unitPath = path4.join(
217
311
  spec.homeDir,
218
312
  ".config",
219
313
  "systemd",
@@ -230,7 +324,7 @@ function planService(platform, spec) {
230
324
  }
231
325
  if (platform === "darwin") {
232
326
  const label = darwinLabel(spec.serviceName);
233
- const unitPath = path3.join(spec.homeDir, "Library", "LaunchAgents", `${label}.plist`);
327
+ const unitPath = path4.join(spec.homeDir, "Library", "LaunchAgents", `${label}.plist`);
234
328
  return {
235
329
  supported: true,
236
330
  unitPath,
@@ -250,23 +344,23 @@ function planService(platform, spec) {
250
344
  function writeService(platform, spec) {
251
345
  const plan = planService(platform, spec);
252
346
  if (plan.supported && plan.unitPath && plan.unitContent !== void 0) {
253
- fs3.mkdirSync(path3.dirname(plan.unitPath), { recursive: true });
254
- fs3.writeFileSync(plan.unitPath, plan.unitContent, { mode: 420 });
347
+ fs4.mkdirSync(path4.dirname(plan.unitPath), { recursive: true });
348
+ fs4.writeFileSync(plan.unitPath, plan.unitContent, { mode: 420 });
255
349
  }
256
350
  return plan;
257
351
  }
258
352
 
259
353
  // src/wizard/installers/hermes.ts
260
354
  function installHermes(inp) {
261
- const daemonEnv = path4.join(inp.homeDir, ".kojee", "hermes.env");
262
- const adapterEnv = path4.join(inp.homeDir, ".hermes", ".env");
263
- const pluginDir = path4.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
355
+ const daemonEnv = path5.join(inp.homeDir, ".kojee", "hermes.env");
356
+ const adapterEnv = path5.join(inp.homeDir, ".hermes", ".env");
357
+ const pluginDir = path5.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
264
358
  if (!inp.skipPayload) {
265
359
  const missing = missingPayloadFiles("hermes", inp.payloadBaseDir, HERMES_PAYLOAD_FILES);
266
360
  if (missing.length > 0) {
267
361
  return {
268
362
  runtime: "hermes",
269
- output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${path4.join(inp.payloadBaseDir, "plugins", "hermes")}. No changes were written. Run \`npm run build\` to stage dist/plugins/hermes, then retry.`,
363
+ output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${path5.join(inp.payloadBaseDir, "plugins", "hermes")}. No changes were written. Run \`npm run build\` to stage dist/plugins/hermes, then retry.`,
270
364
  exitCode: 2,
271
365
  secret: "",
272
366
  secretReused: false
@@ -325,66 +419,49 @@ function installHermes(inp) {
325
419
 
326
420
  // src/wizard/installers/openclaw.ts
327
421
  function installOpenclaw(inp) {
328
- const gatewayUrl = inp.url ? inp.url.replace(/\/+$/, "") : void 0;
329
- const block = {
330
- enabled: true,
331
- ...gatewayUrl ? { gatewayUrl } : {},
332
- // Token-mode → credential lands here (config-first). Paired-mode → omitted.
333
- ...inp.token ? { credential: inp.token } : {},
334
- tandems: [],
335
- selfPrincipals: [],
336
- wake: { severities: [], mentionsOnly: false }
337
- };
338
- const stamp = (inp.now ? inp.now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
339
- const { backedUp } = writeOpenclawChannelConfig(inp.openclawConfigPath, block, { timestamp: stamp });
340
- const installCmd = inp.pluginSourceDir ? `openclaw plugins install -l ${inp.pluginSourceDir}` : `openclaw plugins install npm:openclaw-channel-kojee-tandem (available once published)`;
422
+ const r = writeOpenclawMcpConfig(inp.openclawConfigPath, {
423
+ ...inp.pairedConfigPath ? { pairedConfigPath: inp.pairedConfigPath } : {},
424
+ ...inp.now ? { now: inp.now } : {}
425
+ });
341
426
  const lines = [];
342
- if (backedUp) {
427
+ if (r.backedUp) {
343
428
  lines.push(
344
- `WARNING: ${inp.openclawConfigPath} was present but could not be parsed as JSON.`,
345
- ` The original was backed up to ${backedUp} before writing the kojee-tandem block.`,
346
- ` If it held other channels, recover them from that backup and re-merge by hand.`,
429
+ `WARNING: ${inp.openclawConfigPath} was present but unparseable; backed up to ${r.backedUp} before writing.`,
347
430
  ""
348
431
  );
349
432
  }
350
433
  lines.push(
351
- "Configured runtime: openclaw (in-process channel plugin)",
352
- "Wake mode: native OpenClaw channel \u2014 the plugin streams Tandem events while the gateway is up.",
353
- ` channel config: ${inp.openclawConfigPath} (channels.${CHANNEL_ID})`,
354
- inp.token ? ` credential: written into the channel block (owner-only perms; the plugin's config-first path)` : ` credential: shared ~/.kojee/config.json (paired) \u2014 not duplicated into the channel block`,
434
+ "Configured runtime: openclaw (MCP server + /wake injector)",
435
+ "Wake mode: kojee-mcp runs as OpenClaw's MCP server; Tandem events POST to the local",
436
+ " OpenClaw gateway /hooks/wake (Bearer hooks.token) \u2192 the agent wakes.",
437
+ ` mcp config: ${inp.openclawConfigPath} (mcp.servers.kojee)`,
438
+ inp.pairedConfigPath ? ` credentials: per-runtime paired slot (${inp.pairedConfigPath})` : ` credentials: shared ~/.kojee/config.json (paired)`,
439
+ ` hooks token: ${r.hooksTokenGenerated ? "generated + hooks.enabled=true (new)" : "preserved existing"}`,
355
440
  "",
356
441
  "Next steps (openclaw):",
357
- ` - Install the plugin via OpenClaw's own plugin manager: ${installCmd}`,
358
- " - Reload the gateway to load the plugin: openclaw gateway restart",
359
- " - Verify: openclaw plugins inspect kojee-tandem / openclaw channels status"
442
+ " - Run OpenClaw as its GATEWAY DAEMON (its default mode) \u2014 the /wake injector only",
443
+ " wakes a running gateway. If the gateway is down, nudges are dropped until it restarts.",
444
+ " - Restart the OpenClaw gateway to spawn the kojee MCP server: openclaw gateway restart",
445
+ " - Verify: openclaw mcp list (kojee should be listed) / kojee-mcp doctor"
360
446
  );
361
- return {
362
- runtime: "openclaw",
363
- output: lines.join("\n"),
364
- exitCode: 0,
365
- configPath: inp.openclawConfigPath
366
- };
447
+ return { runtime: "openclaw", output: lines.join("\n"), exitCode: 0, configPath: inp.openclawConfigPath };
367
448
  }
368
449
  function uninstallOpenclaw(inp) {
369
- const removed = removeOpenclawChannel(inp.openclawConfigPath);
370
- const lines = ["Uninstalling runtime: openclaw (in-process channel plugin)"];
371
- lines.push(
372
- removed ? ` removed channels.${CHANNEL_ID} from ${inp.openclawConfigPath} (sibling channels preserved)` : ` no channels.${CHANNEL_ID} block found in ${inp.openclawConfigPath} \u2014 nothing to remove`
373
- );
374
- lines.push("");
375
- lines.push("Next steps (openclaw):");
376
- lines.push(" - Remove the plugin via OpenClaw's own plugin manager: openclaw plugins uninstall kojee-tandem");
377
- lines.push(" - Reload the gateway: openclaw gateway restart");
378
- return {
379
- runtime: "openclaw",
380
- output: lines.join("\n"),
381
- exitCode: 0,
382
- configPath: inp.openclawConfigPath
383
- };
450
+ const removed = removeOpenclawMcpServer(inp.openclawConfigPath);
451
+ const lines = [
452
+ "Uninstalling runtime: openclaw (MCP server + /wake injector)",
453
+ removed ? ` removed mcp.servers.kojee from ${inp.openclawConfigPath} (hooks + siblings preserved)` : ` no mcp.servers.kojee in ${inp.openclawConfigPath} \u2014 nothing to remove`,
454
+ "",
455
+ "Next steps (openclaw):",
456
+ " - Restart the OpenClaw gateway: openclaw gateway restart",
457
+ " - hooks.enabled/token were left intact (you may use hooks for other integrations)."
458
+ ];
459
+ return { runtime: "openclaw", output: lines.join("\n"), exitCode: 0, configPath: inp.openclawConfigPath };
384
460
  }
385
461
 
386
462
  // src/wizard/wizard.ts
387
463
  var DEFAULT_BROKER_URL = "https://rosie-staging.kojee.net";
464
+ var HERMES_DEFAULT_WEBHOOK_URL = "http://127.0.0.1:8645/kojee-tandem";
388
465
  function generateWebhookSecret() {
389
466
  return crypto2.randomBytes(32).toString("hex");
390
467
  }
@@ -469,15 +546,25 @@ async function gatherGuidedInputs(runtime, opts) {
469
546
  } else {
470
547
  const code = opts.promptPairCode ? (await opts.promptPairCode()).trim() : "";
471
548
  if (!code) return { error: "No pair code entered. Re-run `kojee-mcp init` and enter a pair code, or choose token mode." };
472
- const pair = opts.runPair ?? (async (a) => {
473
- const { runPair } = await import("./pair-P4ILCMT7.js");
474
- const { pairedConfigPath } = await import("./paired-config-JTFLHMZ2.js");
475
- const { defaultPairedKeystorePath } = await import("./keystore-XLEV3FL5.js");
476
- return runPair({ code: a.code, url: a.url, keystorePath: defaultPairedKeystorePath(), configPath: pairedConfigPath() });
549
+ const reconcile = reconcileConnectPairSlot(runtime, code);
550
+ if (!reconcile.ok) return { error: reconcile.output };
551
+ for (const m of reconcile.messages) preamble.push(m);
552
+ const slot = pairSlotFor(runtime, code);
553
+ const pair = opts.runPair ?? (async (o) => {
554
+ const { runPair } = await import("./pair-HTCVSMR6.js");
555
+ return runPair(o);
477
556
  });
478
557
  try {
479
- const { message } = await pair({ code, url: brokerUrl });
558
+ const { message } = await pair({
559
+ code,
560
+ url: brokerUrl,
561
+ keystorePath: slot.keystorePath,
562
+ configPath: slot.configPath,
563
+ ...slot.runtime ? { runtime: slot.runtime } : {},
564
+ ...slot.allowOverwrite ? { allowOverwrite: true } : {}
565
+ });
480
566
  preamble.push(`Auth: pair mode \u2014 ${message}`);
567
+ if (slot.pairedConfigPath) next.pairedConfigPath = slot.pairedConfigPath;
481
568
  } catch (err) {
482
569
  return { error: `Pairing failed: ${err.message}` };
483
570
  }
@@ -524,6 +611,8 @@ function whatHappensNext(runtime, opts) {
524
611
  lines.push(" - Restart your runtime so it picks up the new kojee config.");
525
612
  if (opts.token) {
526
613
  lines.push(" - First boot enrolls this token's own keystore (~/.kojee/keypair-<hash>.json).");
614
+ } else if (opts.pairedConfigPath) {
615
+ lines.push(` - The proxy uses your paired credentials (${opts.pairedConfigPath}).`);
527
616
  } else {
528
617
  lines.push(" - The proxy uses your paired credentials (~/.kojee/config.json + keypair.json).");
529
618
  }
@@ -531,13 +620,18 @@ function whatHappensNext(runtime, opts) {
531
620
  return lines.join("\n");
532
621
  }
533
622
  async function configureClaudeCode(opts) {
534
- const { runInit } = await import("./install-LJY2CHKG.js");
623
+ const { runInit } = await import("./install-V7LSQCYZ.js");
535
624
  const report = runInit({
536
625
  ...opts.configPath ? { configPath: opts.configPath } : {},
537
626
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
538
627
  // Token mode threads --token/--url into the written args (per-token
539
628
  // keystore). Paired mode leaves both unset ⇒ args stay `["kojee-mcp"]`.
540
- ...opts.token && opts.url ? { token: opts.token, url: opts.url } : {}
629
+ ...opts.token && opts.url ? { token: opts.token, url: opts.url } : {},
630
+ // Per-runtime paired mode (connect): embed --paired-config, overwrite a
631
+ // stale entry so a re-connect can never leave a launcher reading old creds,
632
+ // and ensureCli so a fresh box with no ~/.claude.json still gets the
633
+ // launcher WRITTEN (never a silent "not-found" that drops the wrong slot).
634
+ ...opts.pairedConfigPath ? { pairedConfigPath: opts.pairedConfigPath, overwrite: true, ensureCli: true } : {}
541
635
  });
542
636
  recordRuntime("claude-code");
543
637
  return { runtime: "claude-code", output: formatClaudeInit(report), exitCode: 0 };
@@ -546,6 +640,7 @@ function formatClaudeInit(report) {
546
640
  const tick = (s) => {
547
641
  if (s === "added") return "\u2713 added";
548
642
  if (s === "already-installed") return "\u21BB already installed";
643
+ if (s === "updated") return "\u21BB updated";
549
644
  if (s === "preserved-different") return "\u26A0 preserved (existing entry differs \u2014 left untouched)";
550
645
  if (s === "not-found") return "\u2014 not found";
551
646
  return s ?? "\u2014";
@@ -583,44 +678,60 @@ function configureCodex(opts) {
583
678
  if (wh.error) {
584
679
  return { runtime: "codex", output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
585
680
  }
586
- const url = wh.url || "https://YOUR-CODEX-WEBHOOK-RECEIVER.local/kojee";
587
- const secret = wh.secret || generateWebhookSecret();
681
+ const connectPairedMode = !!opts.pairedConfigPath && !wh.url && !wh.secret;
682
+ const hasRealUrl = !!wh.url;
683
+ const url = connectPairedMode || !hasRealUrl ? void 0 : wh.url;
684
+ const secret = connectPairedMode || !hasRealUrl ? void 0 : wh.secret || generateWebhookSecret();
588
685
  const tokenArgs = opts.token && opts.url ? { token: opts.token, url: opts.url } : {};
686
+ const pairedArgs = opts.pairedConfigPath ? { pairedConfigPath: opts.pairedConfigPath } : {};
589
687
  writeCodexConfig({
590
688
  ...opts.configPath ? { configPath: opts.configPath } : {},
591
689
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
592
- webhookUrl: url,
593
- webhookSecret: secret,
690
+ ...url !== void 0 ? { webhookUrl: url } : {},
691
+ ...secret !== void 0 ? { webhookSecret: secret } : {},
594
692
  ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
595
- ...tokenArgs
693
+ ...tokenArgs,
694
+ ...pairedArgs
596
695
  });
597
696
  recordRuntime("codex");
598
697
  const lines = [];
599
698
  lines.push("Configured runtime: codex");
600
- lines.push("Wake mode: webhook-sink + stop-hook peek (Codex has no channel injection).");
699
+ lines.push(
700
+ "Wake mode: self-contained (proxy-written pending marker) + stop-hook peek. Webhook OPTIONAL \u2014 an extra low-latency push path, never required."
701
+ );
601
702
  lines.push("");
602
703
  lines.push("Wrote [mcp_servers.kojee] to ~/.codex/config.toml:");
603
704
  lines.push(indent(buildCodexMcpServerTable({
604
- webhookUrl: url,
605
- webhookSecret: "<redacted>",
705
+ // In connectPairedMode the webhook env is left untouched — print neither key
706
+ // (they are preserved from the existing config, not owned this run).
707
+ ...url !== void 0 ? { webhookUrl: url } : {},
708
+ ...secret !== void 0 ? { webhookSecret: "<redacted>" } : {},
606
709
  ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
607
710
  // Redact the gateway token in the human-readable printed copy (it ends up
608
711
  // in result.output → cli.ts console.error). The WRITTEN config above keeps
609
712
  // the real token; only this printed report is redacted, like webhookSecret.
610
- ...tokenArgs.token ? { token: "<redacted>", url: tokenArgs.url } : {}
713
+ ...tokenArgs.token ? { token: "<redacted>", url: tokenArgs.url } : {},
714
+ // A paired-config path is not a secret — print it verbatim.
715
+ ...pairedArgs
611
716
  })));
612
717
  if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
613
718
  lines.push("");
614
719
  lines.push("Wrote the Codex Stop hook (~/.codex/hooks.json; inline TOML form):");
615
720
  lines.push(indent(buildCodexStopHookBlock()));
616
721
  lines.push("");
617
- lines.push(`webhook: ${wh.url ? wh.redactedSummary : "(receiver URL not set \u2014 fill KOJEE_WEBHOOK_URL above)"}`);
722
+ lines.push(
723
+ connectPairedMode ? "webhook: (unchanged \u2014 `connect` rewrites only the credential args, not the wake path)" : `webhook: ${wh.url ? wh.redactedSummary : "OPTIONAL \u2014 not configured (wake is self-contained; set KOJEE_WEBHOOK_URL only for an extra low-latency push path)"}`
724
+ );
618
725
  lines.push(`bounded-listen cap: ${CODEX_LISTEN_CAP_MS}ms (the model picks listen vs drain vs ignore).`);
619
726
  lines.push("");
620
727
  lines.push("Next steps (codex):");
621
728
  lines.push(" Restart Codex (or start a new `codex` / `codex exec` session) to load the MCP server and hook.");
622
- lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
623
- lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
729
+ if (wh.url) {
730
+ lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
731
+ lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
732
+ } else {
733
+ lines.push(" (Optional) For an extra low-latency push path, re-run with --webhook-url <receiver>; wake works without it.");
734
+ }
624
735
  lines.push(" Verify: kojee-mcp doctor");
625
736
  lines.push("");
626
737
  lines.push(CODEX_UNVERIFIED_NOTE);
@@ -649,7 +760,7 @@ function buildDaemonEnvBlock(runtime, wh, envFile) {
649
760
  return lines;
650
761
  }
651
762
  function distDir() {
652
- return path5.dirname(fileURLToPath(import.meta.url));
763
+ return path6.dirname(fileURLToPath(import.meta.url));
653
764
  }
654
765
  function resolveBinPath() {
655
766
  const entry = process.argv[1];
@@ -657,14 +768,30 @@ function resolveBinPath() {
657
768
  }
658
769
  function configureHermes(opts) {
659
770
  const runtime = "hermes";
660
- const wh = resolveWizardWebhook(opts);
771
+ const env0 = opts.env ?? process.env;
772
+ const suppliedUrl = (opts.webhookUrl ?? env0["KOJEE_WEBHOOK_URL"] ?? "").trim();
773
+ const effectiveOpts = suppliedUrl ? opts : { ...opts, webhookUrl: HERMES_DEFAULT_WEBHOOK_URL };
774
+ const wh = resolveWizardWebhook(effectiveOpts);
661
775
  if (wh.error) {
662
776
  return { runtime, output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
663
777
  }
778
+ opts = effectiveOpts;
664
779
  const lines = [];
665
780
  lines.push(`Configured runtime: ${runtime}`);
666
781
  lines.push("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
667
782
  lines.push("");
783
+ if (!suppliedUrl) {
784
+ lines.push(
785
+ "WARNING: no --webhook-url was supplied, so the webhook sink DEFAULTS to the fixed",
786
+ `loopback port ${HERMES_DEFAULT_WEBHOOK_URL} (the hermes-plugin listener). Every Tandem`,
787
+ "event body is POSTed there. On a multi-user host, another local user or process that",
788
+ "binds that port while the hermes plugin is down (before first start, or after a crash)",
789
+ "receives those event bodies plus valid signature/body pairs replayable against the real",
790
+ "receiver. If that risk applies to this host, re-run with an explicit --webhook-url",
791
+ "pointing at a receiver you control, and keep the hermes plugin running.",
792
+ ""
793
+ );
794
+ }
668
795
  if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
669
796
  if (wh.url) {
670
797
  const home = kojeeHomeDir();
@@ -694,7 +821,7 @@ function configureHermes(opts) {
694
821
  return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
695
822
  }
696
823
  recordRuntime(runtime);
697
- const envFile = path5.join(home, ".kojee", "hermes.env");
824
+ const envFile = path6.join(home, ".kojee", "hermes.env");
698
825
  lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
699
826
  lines.push("");
700
827
  lines.push(install.output);
@@ -710,30 +837,13 @@ function configureHermes(opts) {
710
837
  return { runtime, output: lines.join("\n"), exitCode: 0 };
711
838
  }
712
839
  function openclawConfigPath(opts) {
713
- return opts.openclawConfigPath ?? path5.join(kojeeHomeDir(), ".openclaw", "config.json");
714
- }
715
- function resolveOpenclawPluginSourceDir() {
716
- const candidates = [
717
- path5.resolve(distDir(), "..", "..", "integrations", "openclaw-plugin"),
718
- path5.resolve(distDir(), "..", "..", "..", "integrations", "openclaw-plugin")
719
- ];
720
- for (const dir of candidates) {
721
- if (fs4.existsSync(path5.join(dir, "openclaw.plugin.json"))) return dir;
722
- }
723
- return void 0;
840
+ return opts.openclawConfigPath ?? path6.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
724
841
  }
725
842
  function configureOpenclaw(opts) {
726
843
  const runtime = "openclaw";
727
- const env = opts.env ?? process.env;
728
- const token = (opts.token ?? "").trim();
729
- const url = (opts.url ?? env["KOJEE_GATEWAY_URL"] ?? "").trim();
730
- const pluginSourceDir = resolveOpenclawPluginSourceDir();
731
844
  const install = installOpenclaw({
732
- homeDir: kojeeHomeDir(),
733
845
  openclawConfigPath: openclawConfigPath(opts),
734
- ...url ? { url } : {},
735
- ...token ? { token } : {},
736
- ...pluginSourceDir ? { pluginSourceDir } : {}
846
+ ...opts.pairedConfigPath ? { pairedConfigPath: opts.pairedConfigPath } : {}
737
847
  });
738
848
  recordRuntime(runtime);
739
849
  return { runtime, output: install.output, exitCode: install.exitCode };
@@ -742,7 +852,7 @@ async function runWizardUninstall(runtime, opts) {
742
852
  const effective = opts.runtime !== void 0 ? runtime : readRecordedRuntime() ?? runtime;
743
853
  const lines = [`Uninstalling runtime: ${effective}`];
744
854
  if (effective === "claude-code") {
745
- const { runUninstall } = await import("./install-LJY2CHKG.js");
855
+ const { runUninstall } = await import("./install-V7LSQCYZ.js");
746
856
  const report = runUninstall({
747
857
  ...opts.configPath ? { configPath: opts.configPath } : {},
748
858
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
@@ -771,9 +881,9 @@ async function runWizardUninstall(runtime, opts) {
771
881
  } else {
772
882
  lines.push(" (hermes writes no MCP-config or hooks \u2014 nothing to tear down.");
773
883
  lines.push(" Stop the daemon and unset KOJEE_WEBHOOK_URL/SECRET to disable the sink.)");
774
- const envPath = path5.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
884
+ const envPath = path6.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
775
885
  try {
776
- fs4.unlinkSync(envPath);
886
+ fs5.unlinkSync(envPath);
777
887
  lines.push(` removed ${envPath}`);
778
888
  } catch {
779
889
  }
@@ -821,14 +931,13 @@ registerBuiltinInstaller(
821
931
  registerBuiltinInstaller(
822
932
  "openclaw",
823
933
  "OpenClaw",
824
- ["pair-credential", "in-process-plugin"],
934
+ ["pair-credential", "mcp-config-write"],
825
935
  (o) => configureOpenclaw(o)
826
936
  );
937
+
827
938
  export {
828
- CODEX_UNVERIFIED_NOTE,
939
+ reconcileConnectPairSlot,
940
+ pairSlotFor,
829
941
  DEFAULT_BROKER_URL,
830
- generateWebhookSecret,
831
- resolveRuntime,
832
- resolveWizardWebhook,
833
942
  runWizard
834
943
  };