kojee-mcp 0.6.0 → 0.7.1

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-KH5YEJIM.js → chunk-247WFMCJ.js} +7 -20
  5. package/dist/chunk-5DHIUN73.js +25 -0
  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-EBYUJM3H.js +14 -0
  12. package/dist/chunk-EIAUW6KO.js +82 -0
  13. package/dist/chunk-EIH2LNF4.js +45 -0
  14. package/dist/{chunk-BYV7CWNY.js → chunk-FBJCPRVH.js} +22 -9
  15. package/dist/{chunk-XN3IC4B5.js → chunk-FSKGQ6GT.js} +30 -5
  16. package/dist/{chunk-GI2CKKBL.js → chunk-GKXHKR3B.js} +1 -1
  17. package/dist/{chunk-ALZX3SGI.js → chunk-GNLCUJBK.js} +13 -27
  18. package/dist/{chunk-JXMVZEQ7.js → chunk-I67C2HYA.js} +1 -1
  19. package/dist/chunk-KNEJTD6G.js +31 -0
  20. package/dist/{chunk-65KRRDHP.js → chunk-KPMD72FY.js} +94 -28
  21. package/dist/{chunk-NR4Y54OL.js → chunk-MIEI4PLB.js} +1 -19
  22. package/dist/chunk-NE2F5CKS.js +113 -0
  23. package/dist/chunk-QJFMU4QC.js +33 -0
  24. package/dist/{chunk-EW72ZNQL.js → chunk-RDLF4NQC.js} +1 -1
  25. package/dist/{chunk-Q7T3WAST.js → chunk-SSW5AQSR.js} +6 -1
  26. package/dist/{chunk-DURRQDYI.js → chunk-TNZITJAB.js} +45 -15
  27. package/dist/chunk-U5HHHRXA.js +103 -0
  28. package/dist/{setup-handler-7HOFAD7C.js → chunk-UFHGZUST.js} +431 -253
  29. package/dist/{chunk-N35X22G7.js → chunk-UPJV7GBE.js} +1 -1
  30. package/dist/chunk-WBXU27BF.js +35 -0
  31. package/dist/chunk-XJEBJIQE.js +52 -0
  32. package/dist/chunk-Z5LPNJQ6.js +20 -0
  33. package/dist/cli.js +63 -25
  34. package/dist/codex-stop-hook-BMOJVM6O.js +96 -0
  35. package/dist/connect-handler-4DRTFMOB.js +138 -0
  36. package/dist/{control-token-4BUCTYQB.js → control-token-KXSZ3GBY.js} +2 -2
  37. package/dist/{doctor-WVX3WCYX.js → doctor-5QJ3HGNR.js} +48 -22
  38. package/dist/{doctor-codex-Z5M4SFBG.js → doctor-codex-VGJKTX2E.js} +33 -7
  39. package/dist/doctor-openclaw-LL6NSPLC.js +86 -0
  40. package/dist/{event-log-HOBO3ECB.js → event-log-2NBJEIEP.js} +5 -3
  41. package/dist/{event-stream-SYBUJ5EH.js → event-stream-KRYWEYWO.js} +7 -4
  42. package/dist/index.js +13 -10
  43. package/dist/{install-LJY2CHKG.js → install-V7LSQCYZ.js} +37 -22
  44. package/dist/{keystore-XLEV3FL5.js → keystore-LQNFDHGO.js} +2 -2
  45. package/dist/lib.d.ts +4 -1
  46. package/dist/lib.js +11 -8
  47. package/dist/pair-HTCVSMR6.js +11 -0
  48. package/dist/{paired-config-JTFLHMZ2.js → paired-config-GUDDO2CH.js} +4 -2
  49. package/dist/{parent-watchdog-TLU355FB.js → parent-watchdog-LTUNF7PD.js} +24 -5
  50. package/dist/{registry-YLOFZDI2.js → registry-ZZZ26WGA.js} +197 -24
  51. package/dist/resolve-slot-BFIB46Y5.js +13 -0
  52. package/dist/{runtime-record-WO4IECM6.js → runtime-record-OXRLTOLC.js} +2 -2
  53. package/dist/{send-cli-IYJF32PE.js → send-cli-45RLGYIC.js} +22 -10
  54. package/dist/{server-R24AVNTI.js → server-ITPFQVTK.js} +5 -2
  55. package/dist/{session-discovery-FNMJGFPM.js → session-discovery-NHCZ2XWA.js} +2 -2
  56. package/dist/setup-handler-JFM45NCN.js +160 -0
  57. package/dist/{stop-hook-TCPJFGFF.js → stop-hook-5ABGTC2O.js} +17 -20
  58. package/dist/{tail-stream-4IMXI44S.js → tail-stream-NBGHHBS4.js} +40 -22
  59. package/dist/{user-prompt-submit-hook-FQ43NIHC.js → user-prompt-submit-hook-DTXXFDSD.js} +11 -14
  60. package/package.json +3 -2
  61. package/skills/using-tandems/SKILL.md +52 -0
  62. package/dist/ancestry-ONFBQEP5.js +0 -8
  63. package/dist/chunk-BLEGIR35.js +0 -43
  64. package/dist/chunk-CLKCNV2A.js +0 -100
  65. package/dist/chunk-KFDZWNQO.js +0 -73
  66. package/dist/chunk-OGHDTFAX.js +0 -50
  67. package/dist/chunk-VHKPWUX7.js +0 -41
  68. package/dist/codex-stop-hook-RWQSZTST.js +0 -16
  69. package/dist/doctor-openclaw-SS2TMQOX.js +0 -95
  70. package/dist/pair-P4ILCMT7.js +0 -10
@@ -1,19 +1,22 @@
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,
8
+ isPlaceholderWebhookUrl,
9
9
  removeCodexConfig,
10
10
  writeCodexConfig
11
- } from "./chunk-65KRRDHP.js";
11
+ } from "./chunk-KPMD72FY.js";
12
12
  import {
13
- CHANNEL_ID,
14
- removeOpenclawChannel,
15
- writeOpenclawChannelConfig
16
- } from "./chunk-CLKCNV2A.js";
13
+ CONNECT_RUNTIMES
14
+ } from "./chunk-EIH2LNF4.js";
15
+ import {
16
+ clearRuntimeRecord,
17
+ readRecordedRuntime,
18
+ recordRuntime
19
+ } from "./chunk-RDLF4NQC.js";
17
20
  import {
18
21
  kojeeHomeDir
19
22
  } from "./chunk-SQL56SEB.js";
@@ -26,21 +29,265 @@ import {
26
29
  resolveWebhookConfig
27
30
  } from "./chunk-V5VZPYMZ.js";
28
31
  import {
29
- loadPairedConfig
30
- } from "./chunk-YH27B6SW.js";
32
+ loadPairedConfig,
33
+ pairedConfigPath,
34
+ perRuntimeConfigPath
35
+ } from "./chunk-5SZHXYPK.js";
36
+ import {
37
+ defaultPairedKeystorePath,
38
+ deriveKeystorePath
39
+ } from "./chunk-6G6YYST6.js";
40
+ import {
41
+ secureFile,
42
+ unlinkSyncRetry
43
+ } from "./chunk-U5HHHRXA.js";
31
44
  import {
32
- secureFile
33
- } from "./chunk-BLEGIR35.js";
45
+ VERSION
46
+ } from "./chunk-5DHIUN73.js";
34
47
  import {
35
48
  CODEX_LISTEN_CAP_MS,
49
+ buildCondensedTandemRules,
36
50
  buildWebhookReceiverNote
37
- } from "./chunk-Q7T3WAST.js";
51
+ } from "./chunk-SSW5AQSR.js";
52
+
53
+ // src/wizard/pair-slot.ts
54
+ import fs from "fs";
55
+ import os from "os";
56
+ import path from "path";
57
+ function absPerRuntimeConfigPath(runtime) {
58
+ const p = path.resolve(perRuntimeConfigPath(runtime));
59
+ if (process.platform === "win32" && !/^[a-zA-Z]:/.test(p)) {
60
+ return path.resolve(path.join(os.homedir(), ".kojee", "agents", runtime, "config.json"));
61
+ }
62
+ return p;
63
+ }
64
+ function reconcileConnectPairSlot(runtime, code) {
65
+ const messages = [];
66
+ for (const other of CONNECT_RUNTIMES) {
67
+ if (other === runtime) continue;
68
+ const sibling = loadPairedConfig(perRuntimeConfigPath(other));
69
+ if (sibling?.token === code) {
70
+ return {
71
+ ok: false,
72
+ 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.`,
73
+ exitCode: 2
74
+ };
75
+ }
76
+ }
77
+ if (!CONNECT_RUNTIMES.includes(runtime)) {
78
+ return { ok: true, messages };
79
+ }
80
+ const legacy = loadPairedConfig(pairedConfigPath());
81
+ if (legacy && legacy.token === code) {
82
+ if (legacy.runtime === "hermes") {
83
+ return {
84
+ ok: false,
85
+ 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.`,
86
+ exitCode: 2
87
+ };
88
+ }
89
+ try {
90
+ unlinkSyncRetry(pairedConfigPath());
91
+ } catch (err) {
92
+ const e = err;
93
+ if (e.code !== "ENOENT") {
94
+ return {
95
+ ok: false,
96
+ output: `Aborted: could not retire the legacy global config at ${pairedConfigPath()} (${e.message}). Close any running kojee proxy and retry.`,
97
+ exitCode: 1
98
+ };
99
+ }
100
+ }
101
+ const legacyKeystore = defaultPairedKeystorePath();
102
+ const target = deriveKeystorePath(code);
103
+ try {
104
+ if (fs.existsSync(legacyKeystore) && !fs.existsSync(target)) {
105
+ fs.renameSync(legacyKeystore, target);
106
+ }
107
+ } catch {
108
+ }
109
+ messages.push("Migrated the legacy global slot into this per-runtime connection.");
110
+ }
111
+ return { ok: true, messages };
112
+ }
113
+ function pairSlotFor(runtime, code) {
114
+ if (CONNECT_RUNTIMES.includes(runtime)) {
115
+ const configPath = absPerRuntimeConfigPath(runtime);
116
+ return {
117
+ configPath,
118
+ keystorePath: deriveKeystorePath(code),
119
+ runtime,
120
+ allowOverwrite: true,
121
+ pairedConfigPath: configPath
122
+ };
123
+ }
124
+ return {
125
+ configPath: pairedConfigPath(),
126
+ keystorePath: defaultPairedKeystorePath(),
127
+ // F2: stamp the pairing runtime into the global PairedConfig so a later
128
+ // connect-runtime reconcile can recognize a live hermes slot and refuse to
129
+ // retire it (see reconcileConnectPairSlot).
130
+ runtime,
131
+ allowOverwrite: false
132
+ };
133
+ }
38
134
 
39
135
  // src/wizard/wizard.ts
40
- import crypto2 from "crypto";
41
- import fs4 from "fs";
42
- import path5 from "path";
136
+ import crypto3 from "crypto";
137
+ import fs6 from "fs";
138
+ import path7 from "path";
139
+ import { fileURLToPath as fileURLToPath2 } from "url";
140
+
141
+ // src/wizard/skill-install.ts
142
+ import crypto from "crypto";
143
+ import fs2 from "fs";
144
+ import path2 from "path";
43
145
  import { fileURLToPath } from "url";
146
+ var SKILL_NAME = "using-tandems";
147
+ var SKILL_MARKER_PREFIX = "kojee-mcp:skill using-tandems";
148
+ function hashSkillBody(body) {
149
+ return crypto.createHash("sha256").update(body, "utf8").digest("hex").slice(0, 12);
150
+ }
151
+ function bundledSkillSourcePath() {
152
+ const here = path2.dirname(fileURLToPath(import.meta.url));
153
+ const candidates = [
154
+ path2.join(here, "..", "skills", SKILL_NAME, "SKILL.md"),
155
+ path2.join(here, "..", "..", "skills", SKILL_NAME, "SKILL.md")
156
+ ];
157
+ for (const c of candidates) {
158
+ if (fs2.existsSync(c)) return c;
159
+ }
160
+ throw new Error(
161
+ `bundled skill '${SKILL_NAME}' not found (looked at: ${candidates.join(", ")}) \u2014 the package is missing its skills/ payload; reinstall kojee-mcp.`
162
+ );
163
+ }
164
+ function readBundledSkill() {
165
+ const body = fs2.readFileSync(bundledSkillSourcePath(), "utf8").replace(/\s+$/, "") + "\n";
166
+ return { body, version: VERSION, hash: hashSkillBody(body) };
167
+ }
168
+ function markerLine(version, hash) {
169
+ return `<!-- ${SKILL_MARKER_PREFIX} v${version} sha256:${hash} \u2014 managed by kojee-mcp; your edits are preserved (a modified file is never auto-updated or auto-removed) -->`;
170
+ }
171
+ function renderInstalledSkill(b) {
172
+ return `${b.body}
173
+ ${markerLine(b.version, hashSkillBody(b.body))}
174
+ `;
175
+ }
176
+ var MARKER_RE = new RegExp(`<!-- ${SKILL_MARKER_PREFIX} v(\\S+) sha256:([0-9a-f]{12})[^>]*-->`);
177
+ function parseInstalledSkill(content) {
178
+ const m = MARKER_RE.exec(content);
179
+ if (!m) return null;
180
+ const before = content.slice(0, m.index);
181
+ const after = content.slice(m.index + m[0].length);
182
+ const body = (before + after).replace(/\s+$/, "") + "\n";
183
+ return { version: m[1], hash: m[2], body };
184
+ }
185
+ function compareVersions(a, b) {
186
+ const parse = (v) => (v.split("-")[0] ?? "").split(".").map((n) => parseInt(n, 10) || 0);
187
+ const x = parse(a);
188
+ const y = parse(b);
189
+ for (let i = 0; i < 3; i++) {
190
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
191
+ if (d !== 0) return d < 0 ? -1 : 1;
192
+ }
193
+ return 0;
194
+ }
195
+ function readDest(dest) {
196
+ try {
197
+ return { existing: fs2.readFileSync(dest, "utf8"), unreadable: false };
198
+ } catch (err) {
199
+ if (err.code === "ENOENT") return { existing: null, unreadable: false };
200
+ return { existing: null, unreadable: true };
201
+ }
202
+ }
203
+ function installSkillFile(skillsRoot, bundled = readBundledSkill()) {
204
+ const dir = path2.join(skillsRoot, SKILL_NAME);
205
+ const dest = path2.join(dir, "SKILL.md");
206
+ const { existing, unreadable } = readDest(dest);
207
+ if (unreadable) return { status: "skipped-unreadable", path: dest };
208
+ if (existing === null) {
209
+ fs2.mkdirSync(dir, { recursive: true });
210
+ fs2.writeFileSync(dest, renderInstalledSkill(bundled));
211
+ return { status: "installed", path: dest };
212
+ }
213
+ const parsed = parseInstalledSkill(existing);
214
+ if (!parsed) return { status: "skipped-unmanaged", path: dest };
215
+ if (hashSkillBody(parsed.body) !== parsed.hash) return { status: "skipped-modified", path: dest };
216
+ const cmp = compareVersions(bundled.version, parsed.version);
217
+ if (cmp < 0) return { status: "kept-newer", path: dest };
218
+ if (cmp === 0 && hashSkillBody(bundled.body) === parsed.hash)
219
+ return { status: "up-to-date", path: dest };
220
+ fs2.writeFileSync(dest, renderInstalledSkill(bundled));
221
+ return { status: "updated", path: dest };
222
+ }
223
+ function removeSkillFile(skillsRoot) {
224
+ const dir = path2.join(skillsRoot, SKILL_NAME);
225
+ const dest = path2.join(dir, "SKILL.md");
226
+ const { existing, unreadable } = readDest(dest);
227
+ if (unreadable) return { status: "skipped-unreadable", path: dest };
228
+ if (existing === null) return { status: "not-found", path: dest };
229
+ const parsed = parseInstalledSkill(existing);
230
+ if (!parsed) return { status: "skipped-unmanaged", path: dest };
231
+ if (hashSkillBody(parsed.body) !== parsed.hash) return { status: "skipped-modified", path: dest };
232
+ fs2.unlinkSync(dest);
233
+ try {
234
+ fs2.rmdirSync(dir);
235
+ } catch {
236
+ }
237
+ return { status: "removed", path: dest };
238
+ }
239
+ function skillsRootFor(runtime, homeDir = kojeeHomeDir()) {
240
+ if (runtime === "claude-code") return path2.join(homeDir, ".claude", "skills");
241
+ if (runtime === "codex") return path2.join(homeDir, ".agents", "skills");
242
+ return null;
243
+ }
244
+ var LABEL = `skill (${SKILL_NAME})`;
245
+ function installSkillForRuntime(runtime, skillsRootOverride) {
246
+ const root = skillsRootOverride ?? skillsRootFor(runtime);
247
+ if (!root) return null;
248
+ try {
249
+ const r = installSkillFile(root);
250
+ switch (r.status) {
251
+ case "installed":
252
+ return `${LABEL}: \u2713 installed \u2192 ${r.path}`;
253
+ case "updated":
254
+ return `${LABEL}: \u21BB updated to v${VERSION} \u2192 ${r.path}`;
255
+ case "up-to-date":
256
+ return `${LABEL}: \u21BB up to date (v${VERSION})`;
257
+ case "kept-newer":
258
+ return `${LABEL}: \u2014 kept (installed copy is newer than this proxy's v${VERSION})`;
259
+ case "skipped-unmanaged":
260
+ return `${LABEL}: \u26A0 skipped \u2014 ${r.path} exists without a kojee-mcp marker (not ours; left untouched)`;
261
+ case "skipped-modified":
262
+ return `${LABEL}: \u26A0 skipped \u2014 ${r.path} was modified locally (edits preserved; delete the file to receive updates)`;
263
+ case "skipped-unreadable":
264
+ return `${LABEL}: \u26A0 skipped \u2014 ${r.path} exists but could not be read (not verifying ownership \u2192 not touching it)`;
265
+ }
266
+ } catch (err) {
267
+ return `${LABEL}: WARNING \u2014 could not install (${err.message}); the runtime still works without it.`;
268
+ }
269
+ }
270
+ function removeSkillForRuntime(runtime, skillsRootOverride) {
271
+ const root = skillsRootOverride ?? skillsRootFor(runtime);
272
+ if (!root) return null;
273
+ try {
274
+ const r = removeSkillFile(root);
275
+ switch (r.status) {
276
+ case "removed":
277
+ return `${LABEL}: \u2713 removed ${r.path}`;
278
+ case "not-found":
279
+ return `${LABEL}: \u2014 not found`;
280
+ case "skipped-unmanaged":
281
+ return `${LABEL}: \u26A0 left in place \u2014 ${r.path} has no kojee-mcp marker (not ours)`;
282
+ case "skipped-modified":
283
+ return `${LABEL}: \u26A0 left in place \u2014 ${r.path} was modified locally (never deleting your edits; remove it manually)`;
284
+ case "skipped-unreadable":
285
+ return `${LABEL}: \u26A0 left in place \u2014 ${r.path} exists but could not be read (cannot verify it's ours)`;
286
+ }
287
+ } catch (err) {
288
+ return `${LABEL}: WARNING \u2014 could not remove (${err.message}).`;
289
+ }
290
+ }
44
291
 
45
292
  // src/wizard/registry.ts
46
293
  var installers = /* @__PURE__ */ new Map();
@@ -55,16 +302,16 @@ function runtimeUsesWebhook(id) {
55
302
  }
56
303
 
57
304
  // src/wizard/capabilities/webhook-secret.ts
58
- import crypto from "crypto";
59
- import fs from "fs";
60
- import path from "path";
305
+ import crypto2 from "crypto";
306
+ import fs3 from "fs";
307
+ import path3 from "path";
61
308
  function generateSecret() {
62
- return crypto.randomBytes(32).toString("hex");
309
+ return crypto2.randomBytes(32).toString("hex");
63
310
  }
64
311
  function readSecretFromEnv(envPath) {
65
312
  let body;
66
313
  try {
67
- body = fs.readFileSync(envPath, "utf8");
314
+ body = fs3.readFileSync(envPath, "utf8");
68
315
  } catch {
69
316
  return void 0;
70
317
  }
@@ -77,10 +324,10 @@ function shellSingleQuote(value) {
77
324
  return `'${value.replace(/'/g, `'\\''`)}'`;
78
325
  }
79
326
  function upsertEnvFile(filePath, vars, opts = {}) {
80
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
327
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
81
328
  let lines = [];
82
329
  try {
83
- lines = fs.readFileSync(filePath, "utf8").split("\n");
330
+ lines = fs3.readFileSync(filePath, "utf8").split("\n");
84
331
  } catch {
85
332
  }
86
333
  if (opts.header && !lines.some((l) => l === opts.header)) lines.unshift(opts.header);
@@ -92,7 +339,7 @@ function upsertEnvFile(filePath, vars, opts = {}) {
92
339
  if (idx >= 0) lines[idx] = line;
93
340
  else lines.push(line);
94
341
  }
95
- fs.writeFileSync(filePath, lines.join("\n") + "\n", { mode: 384 });
342
+ fs3.writeFileSync(filePath, lines.join("\n") + "\n", { mode: 384 });
96
343
  }
97
344
  function writeWebhookSecretBothSides(opts) {
98
345
  const existing = opts.existingSecret ?? readSecretFromEnv(opts.daemonEnvPath) ?? readSecretFromEnv(opts.adapterEnvPath);
@@ -120,11 +367,11 @@ function writeWebhookSecretBothSides(opts) {
120
367
  }
121
368
 
122
369
  // src/wizard/installers/hermes.ts
123
- import path4 from "path";
370
+ import path6 from "path";
124
371
 
125
372
  // src/wizard/plugin-payload.ts
126
- import fs2 from "fs";
127
- import path2 from "path";
373
+ import fs4 from "fs";
374
+ import path4 from "path";
128
375
  var HERMES_PAYLOAD_FILES = [
129
376
  "__init__.py",
130
377
  "adapter.py",
@@ -132,24 +379,24 @@ var HERMES_PAYLOAD_FILES = [
132
379
  "plugin.yaml"
133
380
  ];
134
381
  function stagePluginPayload(opts) {
135
- fs2.mkdirSync(opts.destDir, { recursive: true });
382
+ fs4.mkdirSync(opts.destDir, { recursive: true });
136
383
  const written = [];
137
384
  for (const file of opts.files) {
138
- const src = path2.join(opts.srcDir, file);
139
- if (!fs2.existsSync(src)) {
385
+ const src = path4.join(opts.srcDir, file);
386
+ if (!fs4.existsSync(src)) {
140
387
  throw new Error(
141
388
  `stagePluginPayload: missing payload file '${file}' in ${opts.srcDir}`
142
389
  );
143
390
  }
144
- const dest = path2.join(opts.destDir, file);
145
- fs2.copyFileSync(src, dest);
391
+ const dest = path4.join(opts.destDir, file);
392
+ fs4.copyFileSync(src, dest);
146
393
  written.push(dest);
147
394
  }
148
395
  return written;
149
396
  }
150
397
  function resolveBundledPayloadDir(runtime, baseDir) {
151
- const dir = path2.join(baseDir, "plugins", runtime);
152
- if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
398
+ const dir = path4.join(baseDir, "plugins", runtime);
399
+ if (!fs4.existsSync(dir) || !fs4.statSync(dir).isDirectory()) {
153
400
  throw new Error(
154
401
  `resolveBundledPayloadDir: no bundled payload for '${runtime}' at ${dir} \u2014 run the build (npm run build) so dist/plugins is staged.`
155
402
  );
@@ -157,10 +404,10 @@ function resolveBundledPayloadDir(runtime, baseDir) {
157
404
  return dir;
158
405
  }
159
406
  function missingPayloadFiles(runtime, baseDir, files) {
160
- const dir = path2.join(baseDir, "plugins", runtime);
161
- const dirOk = fs2.existsSync(dir) && fs2.statSync(dir).isDirectory();
407
+ const dir = path4.join(baseDir, "plugins", runtime);
408
+ const dirOk = fs4.existsSync(dir) && fs4.statSync(dir).isDirectory();
162
409
  if (!dirOk) return [...files];
163
- return files.filter((f) => !fs2.existsSync(path2.join(dir, f)));
410
+ return files.filter((f) => !fs4.existsSync(path4.join(dir, f)));
164
411
  }
165
412
  function installBundledPayload(opts) {
166
413
  const srcDir = resolveBundledPayloadDir(opts.runtime, opts.baseDir);
@@ -168,8 +415,8 @@ function installBundledPayload(opts) {
168
415
  }
169
416
 
170
417
  // src/wizard/service.ts
171
- import fs3 from "fs";
172
- import path3 from "path";
418
+ import fs5 from "fs";
419
+ import path5 from "path";
173
420
  function systemdUnit(spec) {
174
421
  return [
175
422
  "[Unit]",
@@ -216,7 +463,7 @@ function launchdPlist(spec, label) {
216
463
  }
217
464
  function planService(platform, spec) {
218
465
  if (platform === "linux") {
219
- const unitPath = path3.join(
466
+ const unitPath = path5.join(
220
467
  spec.homeDir,
221
468
  ".config",
222
469
  "systemd",
@@ -233,7 +480,7 @@ function planService(platform, spec) {
233
480
  }
234
481
  if (platform === "darwin") {
235
482
  const label = darwinLabel(spec.serviceName);
236
- const unitPath = path3.join(spec.homeDir, "Library", "LaunchAgents", `${label}.plist`);
483
+ const unitPath = path5.join(spec.homeDir, "Library", "LaunchAgents", `${label}.plist`);
237
484
  return {
238
485
  supported: true,
239
486
  unitPath,
@@ -253,23 +500,23 @@ function planService(platform, spec) {
253
500
  function writeService(platform, spec) {
254
501
  const plan = planService(platform, spec);
255
502
  if (plan.supported && plan.unitPath && plan.unitContent !== void 0) {
256
- fs3.mkdirSync(path3.dirname(plan.unitPath), { recursive: true });
257
- fs3.writeFileSync(plan.unitPath, plan.unitContent, { mode: 420 });
503
+ fs5.mkdirSync(path5.dirname(plan.unitPath), { recursive: true });
504
+ fs5.writeFileSync(plan.unitPath, plan.unitContent, { mode: 420 });
258
505
  }
259
506
  return plan;
260
507
  }
261
508
 
262
509
  // src/wizard/installers/hermes.ts
263
510
  function installHermes(inp) {
264
- const daemonEnv = path4.join(inp.homeDir, ".kojee", "hermes.env");
265
- const adapterEnv = path4.join(inp.homeDir, ".hermes", ".env");
266
- const pluginDir = path4.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
511
+ const daemonEnv = path6.join(inp.homeDir, ".kojee", "hermes.env");
512
+ const adapterEnv = path6.join(inp.homeDir, ".hermes", ".env");
513
+ const pluginDir = path6.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
267
514
  if (!inp.skipPayload) {
268
515
  const missing = missingPayloadFiles("hermes", inp.payloadBaseDir, HERMES_PAYLOAD_FILES);
269
516
  if (missing.length > 0) {
270
517
  return {
271
518
  runtime: "hermes",
272
- 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.`,
519
+ output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${path6.join(inp.payloadBaseDir, "plugins", "hermes")}. No changes were written. Run \`npm run build\` to stage dist/plugins/hermes, then retry.`,
273
520
  exitCode: 2,
274
521
  secret: "",
275
522
  secretReused: false
@@ -328,68 +575,59 @@ function installHermes(inp) {
328
575
 
329
576
  // src/wizard/installers/openclaw.ts
330
577
  function installOpenclaw(inp) {
331
- const gatewayUrl = inp.url ? inp.url.replace(/\/+$/, "") : void 0;
332
- const block = {
333
- enabled: true,
334
- ...gatewayUrl ? { gatewayUrl } : {},
335
- // Token-mode → credential lands here (config-first). Paired-mode → omitted.
336
- ...inp.token ? { credential: inp.token } : {},
337
- tandems: [],
338
- selfPrincipals: [],
339
- wake: { severities: [], mentionsOnly: false }
340
- };
341
- const stamp = (inp.now ? inp.now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
342
- const { backedUp } = writeOpenclawChannelConfig(inp.openclawConfigPath, block, { timestamp: stamp });
343
- const installCmd = inp.pluginSourceDir ? `openclaw plugins install -l ${inp.pluginSourceDir}` : `openclaw plugins install npm:openclaw-channel-kojee-tandem (available once published)`;
578
+ const r = writeOpenclawMcpConfig(inp.openclawConfigPath, {
579
+ ...inp.pairedConfigPath ? { pairedConfigPath: inp.pairedConfigPath } : {},
580
+ ...inp.now ? { now: inp.now } : {}
581
+ });
344
582
  const lines = [];
345
- if (backedUp) {
583
+ if (r.backedUp) {
346
584
  lines.push(
347
- `WARNING: ${inp.openclawConfigPath} was present but could not be parsed as JSON.`,
348
- ` The original was backed up to ${backedUp} before writing the kojee-tandem block.`,
349
- ` If it held other channels, recover them from that backup and re-merge by hand.`,
585
+ `WARNING: ${inp.openclawConfigPath} was present but unparseable; backed up to ${r.backedUp} before writing.`,
350
586
  ""
351
587
  );
352
588
  }
353
589
  lines.push(
354
- "Configured runtime: openclaw (in-process channel plugin)",
355
- "Wake mode: native OpenClaw channel \u2014 the plugin streams Tandem events while the gateway is up.",
356
- ` channel config: ${inp.openclawConfigPath} (channels.${CHANNEL_ID})`,
357
- 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`,
590
+ "Configured runtime: openclaw (MCP server + /wake injector)",
591
+ "Wake mode: kojee-mcp runs as OpenClaw's MCP server; Tandem events POST to the local",
592
+ " OpenClaw gateway /hooks/wake (Bearer hooks.token) \u2192 the agent wakes.",
593
+ ` mcp config: ${inp.openclawConfigPath} (mcp.servers.kojee)`,
594
+ inp.pairedConfigPath ? ` credentials: per-runtime paired slot (${inp.pairedConfigPath})` : ` credentials: shared ~/.kojee/config.json (paired)`,
595
+ ` hooks token: ${r.hooksTokenGenerated ? "generated + hooks.enabled=true (new)" : "preserved existing"}`,
358
596
  "",
359
597
  "Next steps (openclaw):",
360
- ` - Install the plugin via OpenClaw's own plugin manager: ${installCmd}`,
361
- " - Reload the gateway to load the plugin: openclaw gateway restart",
362
- " - Verify: openclaw plugins inspect kojee-tandem / openclaw channels status"
598
+ " - Run OpenClaw as its GATEWAY DAEMON (its default mode) \u2014 the /wake injector only",
599
+ " wakes a running gateway. If the gateway is down, nudges are dropped until it restarts.",
600
+ " - Restart the OpenClaw gateway to spawn the kojee MCP server: openclaw gateway restart",
601
+ " - Verify: openclaw mcp list (kojee should be listed) / kojee-mcp doctor",
602
+ "",
603
+ // Belt-and-suspenders: the same condensed rules ride the kojee MCP server's
604
+ // `instructions` field (buildNonChannelInstructions), but not every MCP host
605
+ // surfaces server instructions to the agent — print them here too so the
606
+ // operator can paste them into the agent's own instructions if needed.
607
+ "Agent ground rules (also delivered via the kojee MCP server's instructions;",
608
+ "add to your OpenClaw agent's instructions if it doesn't surface those):",
609
+ ...buildCondensedTandemRules().split("\n").map((l) => ` ${l}`)
363
610
  );
364
- return {
365
- runtime: "openclaw",
366
- output: lines.join("\n"),
367
- exitCode: 0,
368
- configPath: inp.openclawConfigPath
369
- };
611
+ return { runtime: "openclaw", output: lines.join("\n"), exitCode: 0, configPath: inp.openclawConfigPath };
370
612
  }
371
613
  function uninstallOpenclaw(inp) {
372
- const removed = removeOpenclawChannel(inp.openclawConfigPath);
373
- const lines = ["Uninstalling runtime: openclaw (in-process channel plugin)"];
374
- lines.push(
375
- removed ? ` removed channels.${CHANNEL_ID} from ${inp.openclawConfigPath} (sibling channels preserved)` : ` no channels.${CHANNEL_ID} block found in ${inp.openclawConfigPath} \u2014 nothing to remove`
376
- );
377
- lines.push("");
378
- lines.push("Next steps (openclaw):");
379
- lines.push(" - Remove the plugin via OpenClaw's own plugin manager: openclaw plugins uninstall kojee-tandem");
380
- lines.push(" - Reload the gateway: openclaw gateway restart");
381
- return {
382
- runtime: "openclaw",
383
- output: lines.join("\n"),
384
- exitCode: 0,
385
- configPath: inp.openclawConfigPath
386
- };
614
+ const removed = removeOpenclawMcpServer(inp.openclawConfigPath);
615
+ const lines = [
616
+ "Uninstalling runtime: openclaw (MCP server + /wake injector)",
617
+ removed ? ` removed mcp.servers.kojee from ${inp.openclawConfigPath} (hooks + siblings preserved)` : ` no mcp.servers.kojee in ${inp.openclawConfigPath} \u2014 nothing to remove`,
618
+ "",
619
+ "Next steps (openclaw):",
620
+ " - Restart the OpenClaw gateway: openclaw gateway restart",
621
+ " - hooks.enabled/token were left intact (you may use hooks for other integrations)."
622
+ ];
623
+ return { runtime: "openclaw", output: lines.join("\n"), exitCode: 0, configPath: inp.openclawConfigPath };
387
624
  }
388
625
 
389
626
  // src/wizard/wizard.ts
390
627
  var DEFAULT_BROKER_URL = "https://rosie-staging.kojee.net";
628
+ var HERMES_DEFAULT_WEBHOOK_URL = "http://127.0.0.1:8645/kojee-tandem";
391
629
  function generateWebhookSecret() {
392
- return crypto2.randomBytes(32).toString("hex");
630
+ return crypto3.randomBytes(32).toString("hex");
393
631
  }
394
632
  async function resolveRuntime(opts) {
395
633
  if (opts.runtime !== void 0) {
@@ -411,7 +649,8 @@ async function resolveRuntime(opts) {
411
649
  }
412
650
  function resolveWizardWebhook(opts) {
413
651
  const env = opts.env ?? process.env;
414
- const url = (opts.webhookUrl ?? env["KOJEE_WEBHOOK_URL"] ?? "").trim();
652
+ const envUrl = (env["KOJEE_WEBHOOK_URL"] ?? "").trim();
653
+ const url = (opts.webhookUrl ?? (isPlaceholderWebhookUrl(envUrl) ? "" : envUrl)).trim();
415
654
  let secret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
416
655
  if (url && !secret) secret = generateWebhookSecret();
417
656
  const sigFormat = (opts.webhookSignatureFormat ?? env["KOJEE_WEBHOOK_SIGNATURE_FORMAT"] ?? "").trim();
@@ -472,15 +711,25 @@ async function gatherGuidedInputs(runtime, opts) {
472
711
  } else {
473
712
  const code = opts.promptPairCode ? (await opts.promptPairCode()).trim() : "";
474
713
  if (!code) return { error: "No pair code entered. Re-run `kojee-mcp init` and enter a pair code, or choose token mode." };
475
- const pair = opts.runPair ?? (async (a) => {
476
- const { runPair } = await import("./pair-P4ILCMT7.js");
477
- const { pairedConfigPath } = await import("./paired-config-JTFLHMZ2.js");
478
- const { defaultPairedKeystorePath } = await import("./keystore-XLEV3FL5.js");
479
- return runPair({ code: a.code, url: a.url, keystorePath: defaultPairedKeystorePath(), configPath: pairedConfigPath() });
714
+ const reconcile = reconcileConnectPairSlot(runtime, code);
715
+ if (!reconcile.ok) return { error: reconcile.output };
716
+ for (const m of reconcile.messages) preamble.push(m);
717
+ const slot = pairSlotFor(runtime, code);
718
+ const pair = opts.runPair ?? (async (o) => {
719
+ const { runPair } = await import("./pair-HTCVSMR6.js");
720
+ return runPair(o);
480
721
  });
481
722
  try {
482
- const { message } = await pair({ code, url: brokerUrl });
723
+ const { message } = await pair({
724
+ code,
725
+ url: brokerUrl,
726
+ keystorePath: slot.keystorePath,
727
+ configPath: slot.configPath,
728
+ ...slot.runtime ? { runtime: slot.runtime } : {},
729
+ ...slot.allowOverwrite ? { allowOverwrite: true } : {}
730
+ });
483
731
  preamble.push(`Auth: pair mode \u2014 ${message}`);
732
+ if (slot.pairedConfigPath) next.pairedConfigPath = slot.pairedConfigPath;
484
733
  } catch (err) {
485
734
  return { error: `Pairing failed: ${err.message}` };
486
735
  }
@@ -513,20 +762,26 @@ async function runWizard(opts) {
513
762
  }
514
763
  const installer = getInstaller(runtime);
515
764
  const result = installer ? await installer.install({ opts: effective }) : { runtime, output: `No installer registered for runtime '${runtime}'`, exitCode: 2 };
765
+ const skillLine = result.exitCode === 0 ? installSkillForRuntime(runtime, effective.skillsRoot) : null;
766
+ const body = skillLine ? `${result.output}
767
+
768
+ ${skillLine}` : result.output;
516
769
  if (preamble.length > 0 && result.exitCode === 0) {
517
770
  return {
518
771
  runtime,
519
- output: [...preamble, "", result.output, "", whatHappensNext(runtime, effective)].join("\n"),
772
+ output: [...preamble, "", body, "", whatHappensNext(runtime, effective)].join("\n"),
520
773
  exitCode: result.exitCode
521
774
  };
522
775
  }
523
- return { runtime, output: result.output, exitCode: result.exitCode };
776
+ return { runtime, output: body, exitCode: result.exitCode };
524
777
  }
525
778
  function whatHappensNext(runtime, opts) {
526
779
  const lines = ["What happens next:"];
527
780
  lines.push(" - Restart your runtime so it picks up the new kojee config.");
528
781
  if (opts.token) {
529
782
  lines.push(" - First boot enrolls this token's own keystore (~/.kojee/keypair-<hash>.json).");
783
+ } else if (opts.pairedConfigPath) {
784
+ lines.push(` - The proxy uses your paired credentials (${opts.pairedConfigPath}).`);
530
785
  } else {
531
786
  lines.push(" - The proxy uses your paired credentials (~/.kojee/config.json + keypair.json).");
532
787
  }
@@ -534,13 +789,18 @@ function whatHappensNext(runtime, opts) {
534
789
  return lines.join("\n");
535
790
  }
536
791
  async function configureClaudeCode(opts) {
537
- const { runInit } = await import("./install-LJY2CHKG.js");
792
+ const { runInit } = await import("./install-V7LSQCYZ.js");
538
793
  const report = runInit({
539
794
  ...opts.configPath ? { configPath: opts.configPath } : {},
540
795
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
541
796
  // Token mode threads --token/--url into the written args (per-token
542
797
  // keystore). Paired mode leaves both unset ⇒ args stay `["kojee-mcp"]`.
543
- ...opts.token && opts.url ? { token: opts.token, url: opts.url } : {}
798
+ ...opts.token && opts.url ? { token: opts.token, url: opts.url } : {},
799
+ // Per-runtime paired mode (connect): embed --paired-config, overwrite a
800
+ // stale entry so a re-connect can never leave a launcher reading old creds,
801
+ // and ensureCli so a fresh box with no ~/.claude.json still gets the
802
+ // launcher WRITTEN (never a silent "not-found" that drops the wrong slot).
803
+ ...opts.pairedConfigPath ? { pairedConfigPath: opts.pairedConfigPath, overwrite: true, ensureCli: true } : {}
544
804
  });
545
805
  recordRuntime("claude-code");
546
806
  return { runtime: "claude-code", output: formatClaudeInit(report), exitCode: 0 };
@@ -549,6 +809,7 @@ function formatClaudeInit(report) {
549
809
  const tick = (s) => {
550
810
  if (s === "added") return "\u2713 added";
551
811
  if (s === "already-installed") return "\u21BB already installed";
812
+ if (s === "updated") return "\u21BB updated";
552
813
  if (s === "preserved-different") return "\u26A0 preserved (existing entry differs \u2014 left untouched)";
553
814
  if (s === "not-found") return "\u2014 not found";
554
815
  return s ?? "\u2014";
@@ -586,44 +847,60 @@ function configureCodex(opts) {
586
847
  if (wh.error) {
587
848
  return { runtime: "codex", output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
588
849
  }
589
- const url = wh.url || "https://YOUR-CODEX-WEBHOOK-RECEIVER.local/kojee";
590
- const secret = wh.secret || generateWebhookSecret();
850
+ const connectPairedMode = !!opts.pairedConfigPath && !wh.url && !wh.secret;
851
+ const hasRealUrl = !!wh.url && !isPlaceholderWebhookUrl(wh.url);
852
+ const url = connectPairedMode || !hasRealUrl ? void 0 : wh.url;
853
+ const secret = connectPairedMode || !hasRealUrl ? void 0 : wh.secret || generateWebhookSecret();
591
854
  const tokenArgs = opts.token && opts.url ? { token: opts.token, url: opts.url } : {};
855
+ const pairedArgs = opts.pairedConfigPath ? { pairedConfigPath: opts.pairedConfigPath } : {};
592
856
  writeCodexConfig({
593
857
  ...opts.configPath ? { configPath: opts.configPath } : {},
594
858
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
595
- webhookUrl: url,
596
- webhookSecret: secret,
859
+ ...url !== void 0 ? { webhookUrl: url } : {},
860
+ ...secret !== void 0 ? { webhookSecret: secret } : {},
597
861
  ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
598
- ...tokenArgs
862
+ ...tokenArgs,
863
+ ...pairedArgs
599
864
  });
600
865
  recordRuntime("codex");
601
866
  const lines = [];
602
867
  lines.push("Configured runtime: codex");
603
- lines.push("Wake mode: webhook-sink + stop-hook peek (Codex has no channel injection).");
868
+ lines.push(
869
+ "Wake mode: self-contained (proxy-written pending marker) + stop-hook peek. Webhook OPTIONAL \u2014 an extra low-latency push path, never required."
870
+ );
604
871
  lines.push("");
605
872
  lines.push("Wrote [mcp_servers.kojee] to ~/.codex/config.toml:");
606
873
  lines.push(indent(buildCodexMcpServerTable({
607
- webhookUrl: url,
608
- webhookSecret: "<redacted>",
874
+ // In connectPairedMode the webhook env is left untouched — print neither key
875
+ // (they are preserved from the existing config, not owned this run).
876
+ ...url !== void 0 ? { webhookUrl: url } : {},
877
+ ...secret !== void 0 ? { webhookSecret: "<redacted>" } : {},
609
878
  ...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
610
879
  // Redact the gateway token in the human-readable printed copy (it ends up
611
880
  // in result.output → cli.ts console.error). The WRITTEN config above keeps
612
881
  // the real token; only this printed report is redacted, like webhookSecret.
613
- ...tokenArgs.token ? { token: "<redacted>", url: tokenArgs.url } : {}
882
+ ...tokenArgs.token ? { token: "<redacted>", url: tokenArgs.url } : {},
883
+ // A paired-config path is not a secret — print it verbatim.
884
+ ...pairedArgs
614
885
  })));
615
886
  if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
616
887
  lines.push("");
617
888
  lines.push("Wrote the Codex Stop hook (~/.codex/hooks.json; inline TOML form):");
618
889
  lines.push(indent(buildCodexStopHookBlock()));
619
890
  lines.push("");
620
- lines.push(`webhook: ${wh.url ? wh.redactedSummary : "(receiver URL not set \u2014 fill KOJEE_WEBHOOK_URL above)"}`);
891
+ lines.push(
892
+ 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)"}`
893
+ );
621
894
  lines.push(`bounded-listen cap: ${CODEX_LISTEN_CAP_MS}ms (the model picks listen vs drain vs ignore).`);
622
895
  lines.push("");
623
896
  lines.push("Next steps (codex):");
624
897
  lines.push(" Restart Codex (or start a new `codex` / `codex exec` session) to load the MCP server and hook.");
625
- lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
626
- lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
898
+ if (wh.url) {
899
+ lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
900
+ lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
901
+ } else {
902
+ lines.push(" (Optional) For an extra low-latency push path, re-run with --webhook-url <receiver>; wake works without it.");
903
+ }
627
904
  lines.push(" Verify: kojee-mcp doctor");
628
905
  lines.push("");
629
906
  lines.push(CODEX_UNVERIFIED_NOTE);
@@ -652,7 +929,7 @@ function buildDaemonEnvBlock(runtime, wh, envFile) {
652
929
  return lines;
653
930
  }
654
931
  function distDir() {
655
- return path5.dirname(fileURLToPath(import.meta.url));
932
+ return path7.dirname(fileURLToPath2(import.meta.url));
656
933
  }
657
934
  function resolveBinPath() {
658
935
  const entry = process.argv[1];
@@ -660,14 +937,30 @@ function resolveBinPath() {
660
937
  }
661
938
  function configureHermes(opts) {
662
939
  const runtime = "hermes";
663
- const wh = resolveWizardWebhook(opts);
940
+ const env0 = opts.env ?? process.env;
941
+ const suppliedUrl = (opts.webhookUrl ?? env0["KOJEE_WEBHOOK_URL"] ?? "").trim();
942
+ const effectiveOpts = suppliedUrl ? opts : { ...opts, webhookUrl: HERMES_DEFAULT_WEBHOOK_URL };
943
+ const wh = resolveWizardWebhook(effectiveOpts);
664
944
  if (wh.error) {
665
945
  return { runtime, output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
666
946
  }
947
+ opts = effectiveOpts;
667
948
  const lines = [];
668
949
  lines.push(`Configured runtime: ${runtime}`);
669
950
  lines.push("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
670
951
  lines.push("");
952
+ if (!suppliedUrl) {
953
+ lines.push(
954
+ "WARNING: no --webhook-url was supplied, so the webhook sink DEFAULTS to the fixed",
955
+ `loopback port ${HERMES_DEFAULT_WEBHOOK_URL} (the hermes-plugin listener). Every Tandem`,
956
+ "event body is POSTed there. On a multi-user host, another local user or process that",
957
+ "binds that port while the hermes plugin is down (before first start, or after a crash)",
958
+ "receives those event bodies plus valid signature/body pairs replayable against the real",
959
+ "receiver. If that risk applies to this host, re-run with an explicit --webhook-url",
960
+ "pointing at a receiver you control, and keep the hermes plugin running.",
961
+ ""
962
+ );
963
+ }
671
964
  if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
672
965
  if (wh.url) {
673
966
  const home = kojeeHomeDir();
@@ -697,10 +990,13 @@ function configureHermes(opts) {
697
990
  return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
698
991
  }
699
992
  recordRuntime(runtime);
700
- const envFile = path5.join(home, ".kojee", "hermes.env");
993
+ const envFile = path7.join(home, ".kojee", "hermes.env");
701
994
  lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
702
995
  lines.push("");
703
996
  lines.push(install.output);
997
+ lines.push("");
998
+ lines.push("Agent ground rules (add to your hermes agent's instructions \u2014 hermes has no skill loader):");
999
+ lines.push(indent(buildCondensedTandemRules()));
704
1000
  return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
705
1001
  }
706
1002
  recordRuntime(runtime);
@@ -710,33 +1006,19 @@ function configureHermes(opts) {
710
1006
  lines.push(" Re-run with --webhook-url once your receiver is up to complete the install");
711
1007
  lines.push(" (writes both env files, copies the plugin, installs the daemon service).");
712
1008
  lines.push(" Verify: kojee-mcp doctor (after the daemon is up).");
1009
+ lines.push("");
1010
+ lines.push("Agent ground rules (add to your hermes agent's instructions \u2014 hermes has no skill loader):");
1011
+ lines.push(indent(buildCondensedTandemRules()));
713
1012
  return { runtime, output: lines.join("\n"), exitCode: 0 };
714
1013
  }
715
1014
  function openclawConfigPath(opts) {
716
- return opts.openclawConfigPath ?? path5.join(kojeeHomeDir(), ".openclaw", "config.json");
717
- }
718
- function resolveOpenclawPluginSourceDir() {
719
- const candidates = [
720
- path5.resolve(distDir(), "..", "..", "integrations", "openclaw-plugin"),
721
- path5.resolve(distDir(), "..", "..", "..", "integrations", "openclaw-plugin")
722
- ];
723
- for (const dir of candidates) {
724
- if (fs4.existsSync(path5.join(dir, "openclaw.plugin.json"))) return dir;
725
- }
726
- return void 0;
1015
+ return opts.openclawConfigPath ?? path7.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
727
1016
  }
728
1017
  function configureOpenclaw(opts) {
729
1018
  const runtime = "openclaw";
730
- const env = opts.env ?? process.env;
731
- const token = (opts.token ?? "").trim();
732
- const url = (opts.url ?? env["KOJEE_GATEWAY_URL"] ?? "").trim();
733
- const pluginSourceDir = resolveOpenclawPluginSourceDir();
734
1019
  const install = installOpenclaw({
735
- homeDir: kojeeHomeDir(),
736
1020
  openclawConfigPath: openclawConfigPath(opts),
737
- ...url ? { url } : {},
738
- ...token ? { token } : {},
739
- ...pluginSourceDir ? { pluginSourceDir } : {}
1021
+ ...opts.pairedConfigPath ? { pairedConfigPath: opts.pairedConfigPath } : {}
740
1022
  });
741
1023
  recordRuntime(runtime);
742
1024
  return { runtime, output: install.output, exitCode: install.exitCode };
@@ -745,7 +1027,7 @@ async function runWizardUninstall(runtime, opts) {
745
1027
  const effective = opts.runtime !== void 0 ? runtime : readRecordedRuntime() ?? runtime;
746
1028
  const lines = [`Uninstalling runtime: ${effective}`];
747
1029
  if (effective === "claude-code") {
748
- const { runUninstall } = await import("./install-LJY2CHKG.js");
1030
+ const { runUninstall } = await import("./install-V7LSQCYZ.js");
749
1031
  const report = runUninstall({
750
1032
  ...opts.configPath ? { configPath: opts.configPath } : {},
751
1033
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
@@ -774,13 +1056,15 @@ async function runWizardUninstall(runtime, opts) {
774
1056
  } else {
775
1057
  lines.push(" (hermes writes no MCP-config or hooks \u2014 nothing to tear down.");
776
1058
  lines.push(" Stop the daemon and unset KOJEE_WEBHOOK_URL/SECRET to disable the sink.)");
777
- const envPath = path5.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
1059
+ const envPath = path7.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
778
1060
  try {
779
- fs4.unlinkSync(envPath);
1061
+ fs6.unlinkSync(envPath);
780
1062
  lines.push(` removed ${envPath}`);
781
1063
  } catch {
782
1064
  }
783
1065
  }
1066
+ const skillLine = removeSkillForRuntime(effective, opts.skillsRoot);
1067
+ if (skillLine) lines.push(skillLine);
784
1068
  clearRuntimeRecord();
785
1069
  return { runtime: effective, output: lines.join("\n"), exitCode: 0 };
786
1070
  }
@@ -824,119 +1108,13 @@ registerBuiltinInstaller(
824
1108
  registerBuiltinInstaller(
825
1109
  "openclaw",
826
1110
  "OpenClaw",
827
- ["pair-credential", "in-process-plugin"],
1111
+ ["pair-credential", "mcp-config-write"],
828
1112
  (o) => configureOpenclaw(o)
829
1113
  );
830
1114
 
831
- // src/wizard/setup-handler.ts
832
- var SETUP_SUPPORTED_RUNTIMES = ["claude-code", "codex"];
833
- function isSetupRuntime(value) {
834
- return SETUP_SUPPORTED_RUNTIMES.includes(value);
835
- }
836
- function resolvePairCode(opts) {
837
- return opts.pair ?? opts.pairCode;
838
- }
839
- async function defaultRunPair(args) {
840
- const { runPair } = await import("./pair-P4ILCMT7.js");
841
- const { pairedConfigPath } = await import("./paired-config-JTFLHMZ2.js");
842
- const { defaultPairedKeystorePath } = await import("./keystore-XLEV3FL5.js");
843
- return runPair({
844
- code: args.code,
845
- url: args.url,
846
- keystorePath: defaultPairedKeystorePath(),
847
- configPath: pairedConfigPath()
848
- });
849
- }
850
- async function runSetup(opts) {
851
- const messages = [];
852
- const requested = opts.runtime ?? "claude-code";
853
- if (opts.verb === "setup" && opts.runtime !== void 0 && !isSetupRuntime(opts.runtime)) {
854
- const supported = SETUP_SUPPORTED_RUNTIMES.join(", ");
855
- const output = isWizardRuntime(opts.runtime) ? `\`setup\` does not support --runtime ${opts.runtime} yet (coming soon). Supported by \`setup\`: ${supported}. For ${opts.runtime}, use \`kojee-mcp init --runtime ${opts.runtime}\` for now.` : `Unknown --runtime "${opts.runtime}". \`setup\` supports: ${supported}. (For hermes/openclaw use \`kojee-mcp init\`.)`;
856
- return { runtime: requested, messages, output, exitCode: 2 };
857
- }
858
- if (opts.token !== void 0 && (opts.url === void 0 || opts.url.trim() === "") && !opts.uninstall) {
859
- return {
860
- runtime: requested,
861
- messages,
862
- output: "--token requires --url (the broker URL the Kojee UI gave you). Re-run with --url <broker-url>. Nothing was written.",
863
- exitCode: 2
864
- };
865
- }
866
- const url = opts.url ? opts.url.replace(/\/+$/, "") : void 0;
867
- if (opts.pairCode && !opts.uninstall) {
868
- const pair = opts.runPair ?? defaultRunPair;
869
- const pairUrl = url ?? DEFAULT_BROKER_URL;
870
- try {
871
- const r = await pair({ code: opts.pairCode, url: pairUrl });
872
- messages.push(r.message);
873
- } catch (err) {
874
- return {
875
- runtime: requested,
876
- messages,
877
- output: `Pairing failed: ${err.message}`,
878
- exitCode: 1
879
- };
880
- }
881
- }
882
- const loadCfg = opts.loadPairedConfig ?? loadPairedConfig;
883
- const hasCredential = loadCfg() !== null || opts.token !== void 0;
884
- if (!hasCredential && !opts.interactive && !opts.uninstall) {
885
- return {
886
- runtime: requested,
887
- messages,
888
- output: "Not paired. Run `kojee-mcp pair <code> --url <broker>` first, then re-run \u2014 or pass --pair <code> (or --token <T> --url <U>), or run in a terminal for the guided wizard.",
889
- exitCode: 1
890
- };
891
- }
892
- const result = await runWizard({
893
- ...opts.runtime !== void 0 ? { runtime: opts.runtime } : {},
894
- ...opts.uninstall ? { uninstall: true } : {},
895
- ...opts.configPath ? { configPath: opts.configPath } : {},
896
- ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
897
- // Token mode (token + url) threads --token/--url into the written config.
898
- ...opts.token ? { token: opts.token } : {},
899
- ...url ? { url } : {},
900
- ...opts.webhookUrl ? { webhookUrl: opts.webhookUrl } : {},
901
- ...opts.webhookSecret ? { webhookSecret: opts.webhookSecret } : {},
902
- ...opts.webhookSignatureFormat ? { webhookSignatureFormat: opts.webhookSignatureFormat } : {},
903
- ...opts.webhookSignatureHeader ? { webhookSignatureHeader: opts.webhookSignatureHeader } : {},
904
- ...opts.webhookSignaturePrefix !== void 0 ? { webhookSignaturePrefix: opts.webhookSignaturePrefix } : {},
905
- interactive: opts.interactive,
906
- ...opts.interactive && opts.prompts ? opts.prompts : {}
907
- });
908
- if (opts.verb === "setup" && result.exitCode === 0 && !opts.uninstall) {
909
- return {
910
- runtime: result.runtime,
911
- messages,
912
- output: crispSetupSummary(result.runtime, opts, url),
913
- exitCode: 0
914
- };
915
- }
916
- return { runtime: result.runtime, messages, output: result.output, exitCode: result.exitCode };
917
- }
918
- function crispSetupSummary(runtime, opts, url) {
919
- const authLine = opts.token ? ` Auth: token mode (--token/--url${url ? `, ${url}` : ""})` : " Auth: paired credentials (~/.kojee/config.json)";
920
- const lines = [];
921
- if (runtime === "codex") {
922
- lines.push("\u2713 kojee is set up for Codex.");
923
- lines.push(` MCP config: ${opts.configPath ?? "~/.codex/config.toml"} ([mcp_servers.kojee])`);
924
- lines.push(` Stop hook: ${opts.hooksPath ?? "~/.codex/hooks.json"} (codex-stop)`);
925
- lines.push(authLine);
926
- lines.push("");
927
- lines.push("Next: restart Codex to connect.");
928
- } else {
929
- lines.push("\u2713 kojee is set up for Claude Code.");
930
- lines.push(` MCP config: ${opts.configPath ?? "~/.claude.json"} (mcpServers.kojee)`);
931
- lines.push(` Hooks: ${opts.hooksPath ?? "~/.claude/settings.json"} (Stop + UserPromptSubmit)`);
932
- lines.push(authLine);
933
- lines.push("");
934
- lines.push("Next: restart Claude Code to connect.");
935
- }
936
- return lines.join("\n");
937
- }
938
1115
  export {
939
- SETUP_SUPPORTED_RUNTIMES,
940
- resolvePairCode,
941
- runSetup
1116
+ reconcileConnectPairSlot,
1117
+ pairSlotFor,
1118
+ DEFAULT_BROKER_URL,
1119
+ runWizard
942
1120
  };