kojee-mcp 0.5.13 → 0.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LVL25VLO.js → chunk-77HWBSRH.js} +0 -5
- package/dist/cli.js +2 -2
- package/dist/plugins/hermes/__init__.py +21 -0
- package/dist/plugins/hermes/adapter.py +481 -0
- package/dist/plugins/hermes/kojee_tandem_core.py +344 -0
- package/dist/plugins/hermes/plugin.yaml +58 -0
- package/dist/{runtimes-CO43XUUK.js → runtimes-GG7EOFZH.js} +1 -3
- package/dist/wizard-3FDEWEYO.js +904 -0
- package/package.json +2 -2
- package/dist/wizard-L4MYRLJI.js +0 -382
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearRuntimeRecord,
|
|
3
|
+
readRecordedRuntime,
|
|
4
|
+
recordRuntime
|
|
5
|
+
} from "./chunk-EW72ZNQL.js";
|
|
6
|
+
import {
|
|
7
|
+
buildCodexMcpServerTable,
|
|
8
|
+
buildCodexStopHookBlock,
|
|
9
|
+
removeCodexConfig,
|
|
10
|
+
writeCodexConfig
|
|
11
|
+
} from "./chunk-65KRRDHP.js";
|
|
12
|
+
import {
|
|
13
|
+
kojeeHomeDir
|
|
14
|
+
} from "./chunk-SQL56SEB.js";
|
|
15
|
+
import {
|
|
16
|
+
WIZARD_RUNTIMES,
|
|
17
|
+
isWizardRuntime
|
|
18
|
+
} from "./chunk-77HWBSRH.js";
|
|
19
|
+
import {
|
|
20
|
+
resolveSignatureEmission,
|
|
21
|
+
resolveWebhookConfig
|
|
22
|
+
} from "./chunk-V5VZPYMZ.js";
|
|
23
|
+
import {
|
|
24
|
+
secureFile
|
|
25
|
+
} from "./chunk-BLEGIR35.js";
|
|
26
|
+
import {
|
|
27
|
+
CODEX_LISTEN_CAP_MS,
|
|
28
|
+
buildWebhookReceiverNote
|
|
29
|
+
} from "./chunk-X672ZN7V.js";
|
|
30
|
+
|
|
31
|
+
// src/wizard/wizard.ts
|
|
32
|
+
import crypto2 from "crypto";
|
|
33
|
+
import fs5 from "fs";
|
|
34
|
+
import path6 from "path";
|
|
35
|
+
import { fileURLToPath } from "url";
|
|
36
|
+
|
|
37
|
+
// src/wizard/registry.ts
|
|
38
|
+
var installers = /* @__PURE__ */ new Map();
|
|
39
|
+
function register(installer) {
|
|
40
|
+
installers.set(installer.id, installer);
|
|
41
|
+
}
|
|
42
|
+
function getInstaller(id) {
|
|
43
|
+
return installers.get(id);
|
|
44
|
+
}
|
|
45
|
+
function runtimeUsesWebhook(id) {
|
|
46
|
+
return getInstaller(id)?.capabilities.includes("webhook") ?? false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/wizard/capabilities/webhook-secret.ts
|
|
50
|
+
import crypto from "crypto";
|
|
51
|
+
import fs from "fs";
|
|
52
|
+
import path from "path";
|
|
53
|
+
function generateSecret() {
|
|
54
|
+
return crypto.randomBytes(32).toString("hex");
|
|
55
|
+
}
|
|
56
|
+
function readSecretFromEnv(envPath) {
|
|
57
|
+
let body;
|
|
58
|
+
try {
|
|
59
|
+
body = fs.readFileSync(envPath, "utf8");
|
|
60
|
+
} catch {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
const m = body.match(/^(?:export\s+)?KOJEE_WEBHOOK_SECRET=(.*)$/m);
|
|
64
|
+
if (!m) return void 0;
|
|
65
|
+
const raw = m[1].trim().replace(/^['"]|['"]$/g, "");
|
|
66
|
+
return raw.length > 0 ? raw : void 0;
|
|
67
|
+
}
|
|
68
|
+
function shellSingleQuote(value) {
|
|
69
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
70
|
+
}
|
|
71
|
+
function upsertEnvFile(filePath, vars, opts = {}) {
|
|
72
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
73
|
+
let lines = [];
|
|
74
|
+
try {
|
|
75
|
+
lines = fs.readFileSync(filePath, "utf8").split("\n");
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
if (opts.header && !lines.some((l) => l === opts.header)) lines.unshift(opts.header);
|
|
79
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
80
|
+
const pre = opts.exportPrefix ? "export " : "";
|
|
81
|
+
for (const [k, v] of vars) {
|
|
82
|
+
const line = `${pre}${k}=${shellSingleQuote(v)}`;
|
|
83
|
+
const idx = lines.findIndex((l) => l.startsWith(`${k}=`) || l.startsWith(`export ${k}=`));
|
|
84
|
+
if (idx >= 0) lines[idx] = line;
|
|
85
|
+
else lines.push(line);
|
|
86
|
+
}
|
|
87
|
+
fs.writeFileSync(filePath, lines.join("\n") + "\n", { mode: 384 });
|
|
88
|
+
}
|
|
89
|
+
function writeWebhookSecretBothSides(opts) {
|
|
90
|
+
const existing = opts.existingSecret ?? readSecretFromEnv(opts.daemonEnvPath) ?? readSecretFromEnv(opts.adapterEnvPath);
|
|
91
|
+
const secret = existing ?? generateSecret();
|
|
92
|
+
const reused = existing !== void 0;
|
|
93
|
+
const daemonVars = [
|
|
94
|
+
["KOJEE_RUNTIME", opts.runtime],
|
|
95
|
+
["KOJEE_WEBHOOK_URL", opts.webhookUrl],
|
|
96
|
+
["KOJEE_WEBHOOK_SECRET", secret],
|
|
97
|
+
...opts.signatureEnv ?? []
|
|
98
|
+
];
|
|
99
|
+
upsertEnvFile(opts.daemonEnvPath, daemonVars, {
|
|
100
|
+
exportPrefix: true,
|
|
101
|
+
header: `# kojee daemon env for runtime=${opts.runtime} (source this before starting the daemon)`
|
|
102
|
+
});
|
|
103
|
+
secureFile(opts.daemonEnvPath);
|
|
104
|
+
const adapterVars = [
|
|
105
|
+
["KOJEE_WEBHOOK_SECRET", secret],
|
|
106
|
+
...opts.mcpDir ? [["KOJEE_MCP_DIR", opts.mcpDir]] : [],
|
|
107
|
+
...opts.allowAllUsers ? [["KOJEE_TANDEM_ALLOW_ALL_USERS", "true"]] : []
|
|
108
|
+
];
|
|
109
|
+
upsertEnvFile(opts.adapterEnvPath, adapterVars);
|
|
110
|
+
secureFile(opts.adapterEnvPath);
|
|
111
|
+
return { secret, reused, daemonEnvPath: opts.daemonEnvPath, adapterEnvPath: opts.adapterEnvPath };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/wizard/installers/hermes.ts
|
|
115
|
+
import path4 from "path";
|
|
116
|
+
|
|
117
|
+
// src/wizard/plugin-payload.ts
|
|
118
|
+
import fs2 from "fs";
|
|
119
|
+
import path2 from "path";
|
|
120
|
+
var HERMES_PAYLOAD_FILES = [
|
|
121
|
+
"__init__.py",
|
|
122
|
+
"adapter.py",
|
|
123
|
+
"kojee_tandem_core.py",
|
|
124
|
+
"plugin.yaml"
|
|
125
|
+
];
|
|
126
|
+
function stagePluginPayload(opts) {
|
|
127
|
+
fs2.mkdirSync(opts.destDir, { recursive: true });
|
|
128
|
+
const written = [];
|
|
129
|
+
for (const file of opts.files) {
|
|
130
|
+
const src = path2.join(opts.srcDir, file);
|
|
131
|
+
if (!fs2.existsSync(src)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`stagePluginPayload: missing payload file '${file}' in ${opts.srcDir}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const dest = path2.join(opts.destDir, file);
|
|
137
|
+
fs2.copyFileSync(src, dest);
|
|
138
|
+
written.push(dest);
|
|
139
|
+
}
|
|
140
|
+
return written;
|
|
141
|
+
}
|
|
142
|
+
function resolveBundledPayloadDir(runtime, baseDir) {
|
|
143
|
+
const dir = path2.join(baseDir, "plugins", runtime);
|
|
144
|
+
if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`resolveBundledPayloadDir: no bundled payload for '${runtime}' at ${dir} \u2014 run the build (npm run build) so dist/plugins is staged.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return dir;
|
|
150
|
+
}
|
|
151
|
+
function missingPayloadFiles(runtime, baseDir, files) {
|
|
152
|
+
const dir = path2.join(baseDir, "plugins", runtime);
|
|
153
|
+
const dirOk = fs2.existsSync(dir) && fs2.statSync(dir).isDirectory();
|
|
154
|
+
if (!dirOk) return [...files];
|
|
155
|
+
return files.filter((f) => !fs2.existsSync(path2.join(dir, f)));
|
|
156
|
+
}
|
|
157
|
+
function installBundledPayload(opts) {
|
|
158
|
+
const srcDir = resolveBundledPayloadDir(opts.runtime, opts.baseDir);
|
|
159
|
+
return stagePluginPayload({ srcDir, destDir: opts.destDir, files: opts.files });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/wizard/service.ts
|
|
163
|
+
import fs3 from "fs";
|
|
164
|
+
import path3 from "path";
|
|
165
|
+
function systemdUnit(spec) {
|
|
166
|
+
return [
|
|
167
|
+
"[Unit]",
|
|
168
|
+
`Description=kojee-mcp daemon (${spec.runtime} runtime) \u2014 Tandem wake sidecar`,
|
|
169
|
+
"After=network-online.target",
|
|
170
|
+
"Wants=network-online.target",
|
|
171
|
+
"",
|
|
172
|
+
"[Service]",
|
|
173
|
+
"Type=simple",
|
|
174
|
+
`EnvironmentFile=${spec.envFile}`,
|
|
175
|
+
`Environment=KOJEE_RUNTIME=${spec.runtime}`,
|
|
176
|
+
`ExecStart=${spec.binPath}`,
|
|
177
|
+
"Restart=always",
|
|
178
|
+
"RestartSec=5",
|
|
179
|
+
"StandardOutput=journal",
|
|
180
|
+
"StandardError=journal",
|
|
181
|
+
"",
|
|
182
|
+
"[Install]",
|
|
183
|
+
"WantedBy=default.target",
|
|
184
|
+
""
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
function darwinLabel(serviceName) {
|
|
188
|
+
return `net.kojee.${serviceName}`;
|
|
189
|
+
}
|
|
190
|
+
function launchdPlist(spec, label) {
|
|
191
|
+
return [
|
|
192
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
193
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
194
|
+
'<plist version="1.0">',
|
|
195
|
+
"<dict>",
|
|
196
|
+
" <key>Label</key>",
|
|
197
|
+
` <string>${label}</string>`,
|
|
198
|
+
" <key>ProgramArguments</key>",
|
|
199
|
+
` <array><string>${spec.binPath}</string></array>`,
|
|
200
|
+
" <key>EnvironmentVariables</key>",
|
|
201
|
+
` <dict><key>KOJEE_RUNTIME</key><string>${spec.runtime}</string></dict>`,
|
|
202
|
+
" <key>RunAtLoad</key><true/>",
|
|
203
|
+
" <key>KeepAlive</key><true/>",
|
|
204
|
+
"</dict>",
|
|
205
|
+
"</plist>",
|
|
206
|
+
""
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
function planService(platform, spec) {
|
|
210
|
+
if (platform === "linux") {
|
|
211
|
+
const unitPath = path3.join(
|
|
212
|
+
spec.homeDir,
|
|
213
|
+
".config",
|
|
214
|
+
"systemd",
|
|
215
|
+
"user",
|
|
216
|
+
`${spec.serviceName}.service`
|
|
217
|
+
);
|
|
218
|
+
return {
|
|
219
|
+
supported: true,
|
|
220
|
+
unitPath,
|
|
221
|
+
unitContent: systemdUnit(spec),
|
|
222
|
+
activateCmd: `systemctl --user daemon-reload && systemctl --user enable --now ${spec.serviceName}`,
|
|
223
|
+
deactivateCmd: `systemctl --user disable --now ${spec.serviceName}`
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (platform === "darwin") {
|
|
227
|
+
const label = darwinLabel(spec.serviceName);
|
|
228
|
+
const unitPath = path3.join(spec.homeDir, "Library", "LaunchAgents", `${label}.plist`);
|
|
229
|
+
return {
|
|
230
|
+
supported: true,
|
|
231
|
+
unitPath,
|
|
232
|
+
unitContent: launchdPlist(spec, label),
|
|
233
|
+
activateCmd: `launchctl load -w ${unitPath}`,
|
|
234
|
+
deactivateCmd: `launchctl unload -w ${unitPath}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const manual = `schtasks /create /tn ${spec.serviceName} /sc onlogon /tr "${spec.binPath}" /f (wrap it to set KOJEE_RUNTIME=${spec.runtime} and load ${spec.envFile}), or run directly: KOJEE_RUNTIME=${spec.runtime} ${spec.binPath}`;
|
|
238
|
+
return {
|
|
239
|
+
supported: false,
|
|
240
|
+
activateCmd: manual,
|
|
241
|
+
deactivateCmd: `schtasks /delete /tn ${spec.serviceName} /f`,
|
|
242
|
+
note: "Auto-service install isn't supported on this platform yet; run the command above to keep the daemon up."
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function writeService(platform, spec) {
|
|
246
|
+
const plan = planService(platform, spec);
|
|
247
|
+
if (plan.supported && plan.unitPath && plan.unitContent !== void 0) {
|
|
248
|
+
fs3.mkdirSync(path3.dirname(plan.unitPath), { recursive: true });
|
|
249
|
+
fs3.writeFileSync(plan.unitPath, plan.unitContent, { mode: 420 });
|
|
250
|
+
}
|
|
251
|
+
return plan;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/wizard/installers/hermes.ts
|
|
255
|
+
function installHermes(inp) {
|
|
256
|
+
const daemonEnv = path4.join(inp.homeDir, ".kojee", "hermes.env");
|
|
257
|
+
const adapterEnv = path4.join(inp.homeDir, ".hermes", ".env");
|
|
258
|
+
const pluginDir = path4.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
|
|
259
|
+
if (!inp.skipPayload) {
|
|
260
|
+
const missing = missingPayloadFiles("hermes", inp.payloadBaseDir, HERMES_PAYLOAD_FILES);
|
|
261
|
+
if (missing.length > 0) {
|
|
262
|
+
return {
|
|
263
|
+
runtime: "hermes",
|
|
264
|
+
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.`,
|
|
265
|
+
exitCode: 2,
|
|
266
|
+
secret: "",
|
|
267
|
+
secretReused: false
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const sec = writeWebhookSecretBothSides({
|
|
272
|
+
daemonEnvPath: daemonEnv,
|
|
273
|
+
adapterEnvPath: adapterEnv,
|
|
274
|
+
webhookUrl: inp.webhookUrl,
|
|
275
|
+
runtime: "hermes",
|
|
276
|
+
...inp.mcpDir ? { mcpDir: inp.mcpDir } : {},
|
|
277
|
+
...inp.allowAllUsers ? { allowAllUsers: true } : {},
|
|
278
|
+
...inp.signatureEnv && inp.signatureEnv.length > 0 ? { signatureEnv: inp.signatureEnv } : {},
|
|
279
|
+
...inp.webhookSecret ? { existingSecret: inp.webhookSecret } : {}
|
|
280
|
+
});
|
|
281
|
+
const staged = inp.skipPayload ? [] : installBundledPayload({
|
|
282
|
+
runtime: "hermes",
|
|
283
|
+
baseDir: inp.payloadBaseDir,
|
|
284
|
+
destDir: pluginDir,
|
|
285
|
+
files: [...HERMES_PAYLOAD_FILES]
|
|
286
|
+
});
|
|
287
|
+
const svc = writeService(inp.platform, {
|
|
288
|
+
serviceName: "kojee-hermes",
|
|
289
|
+
binPath: inp.binPath,
|
|
290
|
+
envFile: daemonEnv,
|
|
291
|
+
runtime: "hermes",
|
|
292
|
+
homeDir: inp.homeDir
|
|
293
|
+
});
|
|
294
|
+
const lines = [
|
|
295
|
+
"Configured runtime: hermes (complete install)",
|
|
296
|
+
` secret: ${sec.reused ? "reused existing" : "generated"} (matched on daemon + adapter env)`,
|
|
297
|
+
` daemon env: ${daemonEnv}`,
|
|
298
|
+
` adapter env: ${adapterEnv}`,
|
|
299
|
+
inp.skipPayload ? ` plugin: SKIPPED \u2014 no bundled payload (run \`npm run build\` to stage dist/plugins/hermes)` : ` plugin: ${pluginDir} (${staged.length} files)`,
|
|
300
|
+
svc.supported ? ` service: ${svc.unitPath}` : ` service: manual \u2014 ${svc.note ?? "unsupported platform"}`,
|
|
301
|
+
// NB: the "Receiver contract" section is intentionally NOT printed here.
|
|
302
|
+
// configureHermes prints buildDaemonEnvBlock (the single contract owner for
|
|
303
|
+
// every daemon runtime) on the same URL-present path before this output, so
|
|
304
|
+
// emitting it here too would print the contract twice.
|
|
305
|
+
"",
|
|
306
|
+
"Next:",
|
|
307
|
+
` - start the daemon service: ${svc.activateCmd}`,
|
|
308
|
+
" - reload the Hermes gateway to load the plugin: systemctl --user restart hermes-gateway (or: hermes gateway restart)",
|
|
309
|
+
" - join a tandem once: tandem_join <tandem_id>",
|
|
310
|
+
" - verify: kojee-mcp doctor"
|
|
311
|
+
];
|
|
312
|
+
return {
|
|
313
|
+
runtime: "hermes",
|
|
314
|
+
output: lines.join("\n"),
|
|
315
|
+
exitCode: 0,
|
|
316
|
+
secret: sec.secret,
|
|
317
|
+
secretReused: sec.reused
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/wizard/capabilities/openclaw-channel-config.ts
|
|
322
|
+
import fs4 from "fs";
|
|
323
|
+
import path5 from "path";
|
|
324
|
+
var CHANNEL_ID = "kojee-tandem";
|
|
325
|
+
function mergeOpenclawChannelConfig(existing, block) {
|
|
326
|
+
const prevChannels = existing.channels && typeof existing.channels === "object" ? existing.channels : {};
|
|
327
|
+
return {
|
|
328
|
+
...existing,
|
|
329
|
+
channels: {
|
|
330
|
+
...prevChannels,
|
|
331
|
+
[CHANNEL_ID]: { ...block }
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function readOpenclawConfigState(configPath) {
|
|
336
|
+
let raw;
|
|
337
|
+
try {
|
|
338
|
+
raw = fs4.readFileSync(configPath, "utf8");
|
|
339
|
+
} catch {
|
|
340
|
+
return { cfg: {}, unparseable: false };
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const parsed = JSON.parse(raw);
|
|
344
|
+
return {
|
|
345
|
+
cfg: parsed && typeof parsed === "object" ? parsed : {},
|
|
346
|
+
unparseable: false
|
|
347
|
+
};
|
|
348
|
+
} catch {
|
|
349
|
+
return { cfg: {}, unparseable: true };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function atomicWrite(filePath, content, secret) {
|
|
353
|
+
fs4.mkdirSync(path5.dirname(filePath), { recursive: true });
|
|
354
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
355
|
+
fs4.writeFileSync(tmp, content, { ...secret ? { mode: 384 } : {} });
|
|
356
|
+
if (secret) secureFile(tmp);
|
|
357
|
+
fs4.renameSync(tmp, filePath);
|
|
358
|
+
if (secret) secureFile(filePath);
|
|
359
|
+
}
|
|
360
|
+
function writeOpenclawChannelConfig(configPath, block, opts = {}) {
|
|
361
|
+
const { cfg, unparseable } = readOpenclawConfigState(configPath);
|
|
362
|
+
let backedUp;
|
|
363
|
+
if (unparseable) {
|
|
364
|
+
const stamp = opts.timestamp ?? corruptStamp();
|
|
365
|
+
backedUp = `${configPath}.corrupt-${stamp}`;
|
|
366
|
+
fs4.copyFileSync(configPath, backedUp);
|
|
367
|
+
}
|
|
368
|
+
const merged = mergeOpenclawChannelConfig(cfg, block);
|
|
369
|
+
atomicWrite(configPath, JSON.stringify(merged, null, 2) + "\n", Boolean(block.credential));
|
|
370
|
+
return backedUp ? { backedUp } : {};
|
|
371
|
+
}
|
|
372
|
+
function corruptStamp() {
|
|
373
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
374
|
+
}
|
|
375
|
+
function removeOpenclawChannel(configPath) {
|
|
376
|
+
let raw;
|
|
377
|
+
try {
|
|
378
|
+
raw = fs4.readFileSync(configPath, "utf8");
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
let cfg;
|
|
383
|
+
try {
|
|
384
|
+
cfg = JSON.parse(raw);
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const channels = cfg.channels && typeof cfg.channels === "object" ? cfg.channels : void 0;
|
|
389
|
+
if (!channels || !(CHANNEL_ID in channels)) return false;
|
|
390
|
+
const { [CHANNEL_ID]: _removed, ...rest } = channels;
|
|
391
|
+
const next = { ...cfg, channels: rest };
|
|
392
|
+
atomicWrite(configPath, JSON.stringify(next, null, 2) + "\n", false);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/wizard/installers/openclaw.ts
|
|
397
|
+
function installOpenclaw(inp) {
|
|
398
|
+
const gatewayUrl = inp.url ? inp.url.replace(/\/+$/, "") : void 0;
|
|
399
|
+
const block = {
|
|
400
|
+
enabled: true,
|
|
401
|
+
...gatewayUrl ? { gatewayUrl } : {},
|
|
402
|
+
// Token-mode → credential lands here (config-first). Paired-mode → omitted.
|
|
403
|
+
...inp.token ? { credential: inp.token } : {},
|
|
404
|
+
tandems: [],
|
|
405
|
+
selfPrincipals: [],
|
|
406
|
+
wake: { severities: [], mentionsOnly: false }
|
|
407
|
+
};
|
|
408
|
+
const stamp = (inp.now ? inp.now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
409
|
+
const { backedUp } = writeOpenclawChannelConfig(inp.openclawConfigPath, block, { timestamp: stamp });
|
|
410
|
+
const installCmd = inp.pluginSourceDir ? `openclaw plugins install -l ${inp.pluginSourceDir}` : `openclaw plugins install npm:openclaw-channel-kojee-tandem (available once published)`;
|
|
411
|
+
const lines = [];
|
|
412
|
+
if (backedUp) {
|
|
413
|
+
lines.push(
|
|
414
|
+
`WARNING: ${inp.openclawConfigPath} was present but could not be parsed as JSON.`,
|
|
415
|
+
` The original was backed up to ${backedUp} before writing the kojee-tandem block.`,
|
|
416
|
+
` If it held other channels, recover them from that backup and re-merge by hand.`,
|
|
417
|
+
""
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
lines.push(
|
|
421
|
+
"Configured runtime: openclaw (in-process channel plugin)",
|
|
422
|
+
"Wake mode: native OpenClaw channel \u2014 the plugin streams Tandem events while the gateway is up.",
|
|
423
|
+
` channel config: ${inp.openclawConfigPath} (channels.${CHANNEL_ID})`,
|
|
424
|
+
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`,
|
|
425
|
+
"",
|
|
426
|
+
"Next steps (openclaw):",
|
|
427
|
+
` - Install the plugin via OpenClaw's own plugin manager: ${installCmd}`,
|
|
428
|
+
" - Reload the gateway to load the plugin: openclaw gateway restart",
|
|
429
|
+
" - Verify: openclaw plugins inspect kojee-tandem / openclaw channels status"
|
|
430
|
+
);
|
|
431
|
+
return {
|
|
432
|
+
runtime: "openclaw",
|
|
433
|
+
output: lines.join("\n"),
|
|
434
|
+
exitCode: 0,
|
|
435
|
+
configPath: inp.openclawConfigPath
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function uninstallOpenclaw(inp) {
|
|
439
|
+
const removed = removeOpenclawChannel(inp.openclawConfigPath);
|
|
440
|
+
const lines = ["Uninstalling runtime: openclaw (in-process channel plugin)"];
|
|
441
|
+
lines.push(
|
|
442
|
+
removed ? ` removed channels.${CHANNEL_ID} from ${inp.openclawConfigPath} (sibling channels preserved)` : ` no channels.${CHANNEL_ID} block found in ${inp.openclawConfigPath} \u2014 nothing to remove`
|
|
443
|
+
);
|
|
444
|
+
lines.push("");
|
|
445
|
+
lines.push("Next steps (openclaw):");
|
|
446
|
+
lines.push(" - Remove the plugin via OpenClaw's own plugin manager: openclaw plugins uninstall kojee-tandem");
|
|
447
|
+
lines.push(" - Reload the gateway: openclaw gateway restart");
|
|
448
|
+
return {
|
|
449
|
+
runtime: "openclaw",
|
|
450
|
+
output: lines.join("\n"),
|
|
451
|
+
exitCode: 0,
|
|
452
|
+
configPath: inp.openclawConfigPath
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/wizard/wizard.ts
|
|
457
|
+
var DEFAULT_BROKER_URL = "https://rosie-staging.kojee.net";
|
|
458
|
+
function generateWebhookSecret() {
|
|
459
|
+
return crypto2.randomBytes(32).toString("hex");
|
|
460
|
+
}
|
|
461
|
+
async function resolveRuntime(opts) {
|
|
462
|
+
if (opts.runtime !== void 0) {
|
|
463
|
+
if (!isWizardRuntime(opts.runtime) && getInstaller(opts.runtime) === void 0) {
|
|
464
|
+
return {
|
|
465
|
+
error: `Unknown --runtime "${opts.runtime}". Expected one of: ${WIZARD_RUNTIMES.join(", ")}.`
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return { runtime: opts.runtime };
|
|
469
|
+
}
|
|
470
|
+
if (opts.interactive && opts.promptRuntime) {
|
|
471
|
+
const picked = await opts.promptRuntime();
|
|
472
|
+
if (!isWizardRuntime(picked) && getInstaller(picked) === void 0) {
|
|
473
|
+
return { error: `Unknown runtime "${picked}". Expected one of: ${WIZARD_RUNTIMES.join(", ")}.` };
|
|
474
|
+
}
|
|
475
|
+
return { runtime: picked };
|
|
476
|
+
}
|
|
477
|
+
return { runtime: "claude-code" };
|
|
478
|
+
}
|
|
479
|
+
function resolveWizardWebhook(opts) {
|
|
480
|
+
const env = opts.env ?? process.env;
|
|
481
|
+
const url = (opts.webhookUrl ?? env["KOJEE_WEBHOOK_URL"] ?? "").trim();
|
|
482
|
+
let secret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
|
|
483
|
+
if (url && !secret) secret = generateWebhookSecret();
|
|
484
|
+
const sigFormat = (opts.webhookSignatureFormat ?? env["KOJEE_WEBHOOK_SIGNATURE_FORMAT"] ?? "").trim();
|
|
485
|
+
const sigHeader = (opts.webhookSignatureHeader ?? env["KOJEE_WEBHOOK_SIGNATURE_HEADER"] ?? "").trim();
|
|
486
|
+
const sigPrefix = opts.webhookSignaturePrefix ?? env["KOJEE_WEBHOOK_SIGNATURE_PREFIX"];
|
|
487
|
+
const signatureEnv = [];
|
|
488
|
+
if (sigFormat) signatureEnv.push(["KOJEE_WEBHOOK_SIGNATURE_FORMAT", sigFormat]);
|
|
489
|
+
if (sigHeader) signatureEnv.push(["KOJEE_WEBHOOK_SIGNATURE_HEADER", sigHeader]);
|
|
490
|
+
if (sigPrefix !== void 0) signatureEnv.push(["KOJEE_WEBHOOK_SIGNATURE_PREFIX", sigPrefix]);
|
|
491
|
+
const emission = resolveSignatureEmission(Object.fromEntries(signatureEnv));
|
|
492
|
+
const resolution = resolveWebhookConfig({
|
|
493
|
+
KOJEE_WEBHOOK_URL: url,
|
|
494
|
+
KOJEE_WEBHOOK_SECRET: secret,
|
|
495
|
+
...env["KOJEE_WEBHOOK_TIMEOUT_MS"] !== void 0 ? { KOJEE_WEBHOOK_TIMEOUT_MS: env["KOJEE_WEBHOOK_TIMEOUT_MS"] } : {},
|
|
496
|
+
...env["KOJEE_WEBHOOK_MAX_RETRIES"] !== void 0 ? { KOJEE_WEBHOOK_MAX_RETRIES: env["KOJEE_WEBHOOK_MAX_RETRIES"] } : {},
|
|
497
|
+
...Object.fromEntries(signatureEnv)
|
|
498
|
+
});
|
|
499
|
+
const warning = resolution.warning ?? (emission.warnings.length > 0 ? emission.warnings.join("; ") : void 0);
|
|
500
|
+
if (resolution.error) {
|
|
501
|
+
return {
|
|
502
|
+
url,
|
|
503
|
+
secret,
|
|
504
|
+
redactedSummary: "",
|
|
505
|
+
signatureEnv,
|
|
506
|
+
signatureHeader: emission.header,
|
|
507
|
+
signaturePrefix: emission.prefix,
|
|
508
|
+
error: resolution.error
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
url,
|
|
513
|
+
secret,
|
|
514
|
+
redactedSummary: resolution.config?.redactedSummary ?? `url=${url} secret=<redacted>`,
|
|
515
|
+
signatureEnv,
|
|
516
|
+
signatureHeader: emission.header,
|
|
517
|
+
signaturePrefix: emission.prefix,
|
|
518
|
+
...warning !== void 0 ? { warning } : {}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
var CODEX_UNVERIFIED_NOTE = "NOTE: live Codex verification (hook fires, MCP server connects, bounded listen works) has not been run on this build \u2014 confirm in a real Codex session. This is the owner morning step.";
|
|
522
|
+
async function gatherGuidedInputs(runtime, opts) {
|
|
523
|
+
const preamble = [];
|
|
524
|
+
const next = { ...opts };
|
|
525
|
+
if (opts.promptUrl) {
|
|
526
|
+
const answered = (await opts.promptUrl(opts.url ?? DEFAULT_BROKER_URL)).trim();
|
|
527
|
+
next.url = (answered.length > 0 ? answered : opts.url ?? DEFAULT_BROKER_URL).replace(/\/+$/, "");
|
|
528
|
+
}
|
|
529
|
+
const brokerUrl = (next.url ?? DEFAULT_BROKER_URL).replace(/\/+$/, "");
|
|
530
|
+
if (opts.promptAuth) {
|
|
531
|
+
const mode = await opts.promptAuth();
|
|
532
|
+
if (mode === "token") {
|
|
533
|
+
const token = opts.promptToken ? (await opts.promptToken()).trim() : "";
|
|
534
|
+
if (!token) return { error: "No token entered. Re-run `kojee-mcp init` and paste a token, or choose pair mode." };
|
|
535
|
+
next.token = token;
|
|
536
|
+
next.url = brokerUrl;
|
|
537
|
+
preamble.push("Auth: token mode \u2014 the written MCP config will launch the proxy with --token/--url");
|
|
538
|
+
preamble.push(" (it enrolls its own per-token keystore on first boot).");
|
|
539
|
+
} else {
|
|
540
|
+
const code = opts.promptPairCode ? (await opts.promptPairCode()).trim() : "";
|
|
541
|
+
if (!code) return { error: "No pair code entered. Re-run `kojee-mcp init` and enter a pair code, or choose token mode." };
|
|
542
|
+
const pair = opts.runPair ?? (async (a) => {
|
|
543
|
+
const { runPair } = await import("./pair-P4ILCMT7.js");
|
|
544
|
+
const { pairedConfigPath } = await import("./paired-config-JTFLHMZ2.js");
|
|
545
|
+
const { defaultPairedKeystorePath } = await import("./keystore-XLEV3FL5.js");
|
|
546
|
+
return runPair({ code: a.code, url: a.url, keystorePath: defaultPairedKeystorePath(), configPath: pairedConfigPath() });
|
|
547
|
+
});
|
|
548
|
+
try {
|
|
549
|
+
const { message } = await pair({ code, url: brokerUrl });
|
|
550
|
+
preamble.push(`Auth: pair mode \u2014 ${message}`);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
return { error: `Pairing failed: ${err.message}` };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (runtimeUsesWebhook(runtime) && opts.promptWebhookUrl && opts.webhookUrl === void 0) {
|
|
557
|
+
const wh = (await opts.promptWebhookUrl()).trim();
|
|
558
|
+
if (wh.length > 0) next.webhookUrl = wh;
|
|
559
|
+
}
|
|
560
|
+
return { opts: next, preamble };
|
|
561
|
+
}
|
|
562
|
+
async function runWizard(opts) {
|
|
563
|
+
const resolved = await resolveRuntime(opts);
|
|
564
|
+
if ("error" in resolved) {
|
|
565
|
+
return { runtime: "claude-code", output: resolved.error, exitCode: 2 };
|
|
566
|
+
}
|
|
567
|
+
const runtime = resolved.runtime;
|
|
568
|
+
if (opts.uninstall) {
|
|
569
|
+
return runWizardUninstall(runtime, opts);
|
|
570
|
+
}
|
|
571
|
+
let effective = opts;
|
|
572
|
+
let preamble = [];
|
|
573
|
+
if (opts.interactive && (opts.promptUrl || opts.promptAuth || opts.promptWebhookUrl)) {
|
|
574
|
+
const gathered = await gatherGuidedInputs(runtime, opts);
|
|
575
|
+
if ("error" in gathered) {
|
|
576
|
+
return { runtime, output: gathered.error, exitCode: 2 };
|
|
577
|
+
}
|
|
578
|
+
effective = gathered.opts;
|
|
579
|
+
preamble = gathered.preamble;
|
|
580
|
+
}
|
|
581
|
+
const installer = getInstaller(runtime);
|
|
582
|
+
const result = installer ? await installer.install({ opts: effective }) : { runtime, output: `No installer registered for runtime '${runtime}'`, exitCode: 2 };
|
|
583
|
+
if (preamble.length > 0 && result.exitCode === 0) {
|
|
584
|
+
return {
|
|
585
|
+
runtime,
|
|
586
|
+
output: [...preamble, "", result.output, "", whatHappensNext(runtime, effective)].join("\n"),
|
|
587
|
+
exitCode: result.exitCode
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return { runtime, output: result.output, exitCode: result.exitCode };
|
|
591
|
+
}
|
|
592
|
+
function whatHappensNext(runtime, opts) {
|
|
593
|
+
const lines = ["What happens next:"];
|
|
594
|
+
lines.push(" - Restart your runtime so it picks up the new kojee config.");
|
|
595
|
+
if (opts.token) {
|
|
596
|
+
lines.push(" - First boot enrolls this token's own keystore (~/.kojee/keypair-<hash>.json).");
|
|
597
|
+
} else {
|
|
598
|
+
lines.push(" - The proxy uses your paired credentials (~/.kojee/config.json + keypair.json).");
|
|
599
|
+
}
|
|
600
|
+
lines.push(" - Verify with: kojee-mcp doctor");
|
|
601
|
+
return lines.join("\n");
|
|
602
|
+
}
|
|
603
|
+
async function configureClaudeCode(opts) {
|
|
604
|
+
const { runInit } = await import("./install-LJY2CHKG.js");
|
|
605
|
+
const report = runInit({
|
|
606
|
+
...opts.configPath ? { configPath: opts.configPath } : {},
|
|
607
|
+
...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
|
|
608
|
+
// Token mode threads --token/--url into the written args (per-token
|
|
609
|
+
// keystore). Paired mode leaves both unset ⇒ args stay `["kojee-mcp"]`.
|
|
610
|
+
...opts.token && opts.url ? { token: opts.token, url: opts.url } : {}
|
|
611
|
+
});
|
|
612
|
+
recordRuntime("claude-code");
|
|
613
|
+
return { runtime: "claude-code", output: formatClaudeInit(report), exitCode: 0 };
|
|
614
|
+
}
|
|
615
|
+
function formatClaudeInit(report) {
|
|
616
|
+
const tick = (s) => {
|
|
617
|
+
if (s === "added") return "\u2713 added";
|
|
618
|
+
if (s === "already-installed") return "\u21BB already installed";
|
|
619
|
+
if (s === "preserved-different") return "\u26A0 preserved (existing entry differs \u2014 left untouched)";
|
|
620
|
+
if (s === "not-found") return "\u2014 not found";
|
|
621
|
+
return s ?? "\u2014";
|
|
622
|
+
};
|
|
623
|
+
const lines = ["Configured runtime: claude-code", "", "Installing kojee for Claude:"];
|
|
624
|
+
for (const t of report.targets) {
|
|
625
|
+
const label = t.kind === "cli" ? "CLI" : "Claude.app";
|
|
626
|
+
lines.push("");
|
|
627
|
+
lines.push(` ${t.path} (${label})`);
|
|
628
|
+
lines.push(` mcpServers.kojee ${tick(t.mcpServer)}`);
|
|
629
|
+
if (t.kind === "cli") {
|
|
630
|
+
if (t.hooksPath) lines.push(` ${t.hooksPath} (hooks)`);
|
|
631
|
+
lines.push(` hooks.Stop ${tick(t.stopHook)}`);
|
|
632
|
+
lines.push(` hooks.UserPromptSubmit ${tick(t.userPromptSubmitHook)}`);
|
|
633
|
+
} else {
|
|
634
|
+
lines.push(` (hooks not applicable for Claude.app agent mode)`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
lines.push("");
|
|
638
|
+
if (report.targets.some((t) => t.kind === "desktop" && t.mcpServer === "added")) {
|
|
639
|
+
lines.push(
|
|
640
|
+
"Existing Claude.app agent-mode sessions snapshotted the previous config",
|
|
641
|
+
"and won't pick up this change automatically. Start a NEW agent-mode",
|
|
642
|
+
"session (not a resumed one) to use the updated kojee config.",
|
|
643
|
+
""
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
lines.push("To verify: in any new CC session, run /mcp and confirm `kojee` is listed.");
|
|
647
|
+
lines.push(" run /hooks and confirm both Stop and UserPromptSubmit show the kojee entries.");
|
|
648
|
+
lines.push("To remove: `kojee-mcp init --uninstall`");
|
|
649
|
+
return lines.join("\n");
|
|
650
|
+
}
|
|
651
|
+
function configureCodex(opts) {
|
|
652
|
+
const wh = resolveWizardWebhook(opts);
|
|
653
|
+
if (wh.error) {
|
|
654
|
+
return { runtime: "codex", output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
|
|
655
|
+
}
|
|
656
|
+
const url = wh.url || "https://YOUR-CODEX-WEBHOOK-RECEIVER.local/kojee";
|
|
657
|
+
const secret = wh.secret || generateWebhookSecret();
|
|
658
|
+
const tokenArgs = opts.token && opts.url ? { token: opts.token, url: opts.url } : {};
|
|
659
|
+
writeCodexConfig({
|
|
660
|
+
...opts.configPath ? { configPath: opts.configPath } : {},
|
|
661
|
+
...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
|
|
662
|
+
webhookUrl: url,
|
|
663
|
+
webhookSecret: secret,
|
|
664
|
+
...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
|
|
665
|
+
...tokenArgs
|
|
666
|
+
});
|
|
667
|
+
recordRuntime("codex");
|
|
668
|
+
const lines = [];
|
|
669
|
+
lines.push("Configured runtime: codex");
|
|
670
|
+
lines.push("Wake mode: webhook-sink + stop-hook peek (Codex has no channel injection).");
|
|
671
|
+
lines.push("");
|
|
672
|
+
lines.push("Wrote [mcp_servers.kojee] to ~/.codex/config.toml:");
|
|
673
|
+
lines.push(indent(buildCodexMcpServerTable({
|
|
674
|
+
webhookUrl: url,
|
|
675
|
+
webhookSecret: "<redacted>",
|
|
676
|
+
...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
|
|
677
|
+
// Redact the gateway token in the human-readable printed copy (it ends up
|
|
678
|
+
// in result.output → cli.ts console.error). The WRITTEN config above keeps
|
|
679
|
+
// the real token; only this printed report is redacted, like webhookSecret.
|
|
680
|
+
...tokenArgs.token ? { token: "<redacted>", url: tokenArgs.url } : {}
|
|
681
|
+
})));
|
|
682
|
+
if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
|
|
683
|
+
lines.push("");
|
|
684
|
+
lines.push("Wrote the Codex Stop hook (~/.codex/hooks.json; inline TOML form):");
|
|
685
|
+
lines.push(indent(buildCodexStopHookBlock()));
|
|
686
|
+
lines.push("");
|
|
687
|
+
lines.push(`webhook: ${wh.url ? wh.redactedSummary : "(receiver URL not set \u2014 fill KOJEE_WEBHOOK_URL above)"}`);
|
|
688
|
+
lines.push(`bounded-listen cap: ${CODEX_LISTEN_CAP_MS}ms (the model picks listen vs drain vs ignore).`);
|
|
689
|
+
lines.push("");
|
|
690
|
+
lines.push("Next steps (codex):");
|
|
691
|
+
lines.push(" Restart Codex (or start a new `codex` / `codex exec` session) to load the MCP server and hook.");
|
|
692
|
+
lines.push(" Stand up your webhook receiver at KOJEE_WEBHOOK_URL \u2014 contract:");
|
|
693
|
+
lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
|
|
694
|
+
lines.push(" Verify: kojee-mcp doctor");
|
|
695
|
+
lines.push("");
|
|
696
|
+
lines.push(CODEX_UNVERIFIED_NOTE);
|
|
697
|
+
return { runtime: "codex", output: lines.join("\n"), exitCode: 0 };
|
|
698
|
+
}
|
|
699
|
+
function buildDaemonEnvBlock(runtime, wh, envFile) {
|
|
700
|
+
const lines = [];
|
|
701
|
+
if (wh.url && envFile) {
|
|
702
|
+
lines.push("Export these before starting the daemon (also written to a source-able file):");
|
|
703
|
+
lines.push(` export KOJEE_RUNTIME=${shellSingleQuote(runtime)}`);
|
|
704
|
+
lines.push(` export KOJEE_WEBHOOK_URL=${shellSingleQuote(wh.url)}`);
|
|
705
|
+
lines.push(` export KOJEE_WEBHOOK_SECRET=<generated; in ${envFile}>`);
|
|
706
|
+
for (const [k, v] of wh.signatureEnv) lines.push(` export ${k}=${shellSingleQuote(v)}`);
|
|
707
|
+
lines.push(` (validated: ${wh.redactedSummary})`);
|
|
708
|
+
lines.push(` source ${envFile}`);
|
|
709
|
+
} else {
|
|
710
|
+
lines.push("Set the daemon env (no receiver URL supplied yet):");
|
|
711
|
+
lines.push(` export KOJEE_RUNTIME=${shellSingleQuote(runtime)}`);
|
|
712
|
+
lines.push(` export KOJEE_WEBHOOK_URL="https://YOUR-RECEIVER.local/kojee"`);
|
|
713
|
+
lines.push(` export KOJEE_WEBHOOK_SECRET=<generate a hex secret>`);
|
|
714
|
+
for (const [k, v] of wh.signatureEnv) lines.push(` export ${k}=${shellSingleQuote(v)}`);
|
|
715
|
+
}
|
|
716
|
+
lines.push("");
|
|
717
|
+
lines.push("Receiver contract:");
|
|
718
|
+
lines.push(indent(buildWebhookReceiverNote({ header: wh.signatureHeader, prefix: wh.signaturePrefix })));
|
|
719
|
+
return lines;
|
|
720
|
+
}
|
|
721
|
+
function distDir() {
|
|
722
|
+
return path6.dirname(fileURLToPath(import.meta.url));
|
|
723
|
+
}
|
|
724
|
+
function resolveBinPath() {
|
|
725
|
+
const entry = process.argv[1];
|
|
726
|
+
return entry && entry.length > 0 ? entry : "kojee-mcp";
|
|
727
|
+
}
|
|
728
|
+
function configureHermes(opts) {
|
|
729
|
+
const runtime = "hermes";
|
|
730
|
+
const wh = resolveWizardWebhook(opts);
|
|
731
|
+
if (wh.error) {
|
|
732
|
+
return { runtime, output: `webhook env ERROR: ${wh.error}`, exitCode: 2 };
|
|
733
|
+
}
|
|
734
|
+
const lines = [];
|
|
735
|
+
lines.push(`Configured runtime: ${runtime}`);
|
|
736
|
+
lines.push("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
|
|
737
|
+
lines.push("");
|
|
738
|
+
if (wh.warning) lines.push(`webhook WARNING: ${wh.warning}`);
|
|
739
|
+
if (wh.url) {
|
|
740
|
+
const home = kojeeHomeDir();
|
|
741
|
+
let skipPayload = false;
|
|
742
|
+
const base = distDir();
|
|
743
|
+
try {
|
|
744
|
+
resolveBundledPayloadDir("hermes", base);
|
|
745
|
+
} catch {
|
|
746
|
+
skipPayload = true;
|
|
747
|
+
}
|
|
748
|
+
const env = opts.env ?? process.env;
|
|
749
|
+
const suppliedSecret = (opts.webhookSecret ?? env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
|
|
750
|
+
const install = installHermes({
|
|
751
|
+
homeDir: home,
|
|
752
|
+
payloadBaseDir: base,
|
|
753
|
+
platform: process.platform,
|
|
754
|
+
binPath: resolveBinPath(),
|
|
755
|
+
webhookUrl: wh.url,
|
|
756
|
+
...suppliedSecret ? { webhookSecret: suppliedSecret } : {},
|
|
757
|
+
...wh.signatureEnv.length > 0 ? { signatureEnv: wh.signatureEnv } : {},
|
|
758
|
+
signatureHeader: wh.signatureHeader,
|
|
759
|
+
signaturePrefix: wh.signaturePrefix,
|
|
760
|
+
skipPayload
|
|
761
|
+
});
|
|
762
|
+
if (install.exitCode !== 0) {
|
|
763
|
+
lines.push(install.output);
|
|
764
|
+
return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
|
|
765
|
+
}
|
|
766
|
+
recordRuntime(runtime);
|
|
767
|
+
const envFile = path6.join(home, ".kojee", "hermes.env");
|
|
768
|
+
lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
|
|
769
|
+
lines.push("");
|
|
770
|
+
lines.push(install.output);
|
|
771
|
+
return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
|
|
772
|
+
}
|
|
773
|
+
recordRuntime(runtime);
|
|
774
|
+
lines.push(...buildDaemonEnvBlock(runtime, wh, void 0));
|
|
775
|
+
lines.push("");
|
|
776
|
+
lines.push(`Next steps (${runtime}):`);
|
|
777
|
+
lines.push(" Re-run with --webhook-url once your receiver is up to complete the install");
|
|
778
|
+
lines.push(" (writes both env files, copies the plugin, installs the daemon service).");
|
|
779
|
+
lines.push(" Verify: kojee-mcp doctor (after the daemon is up).");
|
|
780
|
+
return { runtime, output: lines.join("\n"), exitCode: 0 };
|
|
781
|
+
}
|
|
782
|
+
function openclawConfigPath(opts) {
|
|
783
|
+
return opts.openclawConfigPath ?? path6.join(kojeeHomeDir(), ".openclaw", "config.json");
|
|
784
|
+
}
|
|
785
|
+
function resolveOpenclawPluginSourceDir() {
|
|
786
|
+
const candidates = [
|
|
787
|
+
path6.resolve(distDir(), "..", "..", "integrations", "openclaw-plugin"),
|
|
788
|
+
path6.resolve(distDir(), "..", "..", "..", "integrations", "openclaw-plugin")
|
|
789
|
+
];
|
|
790
|
+
for (const dir of candidates) {
|
|
791
|
+
if (fs5.existsSync(path6.join(dir, "openclaw.plugin.json"))) return dir;
|
|
792
|
+
}
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
function configureOpenclaw(opts) {
|
|
796
|
+
const runtime = "openclaw";
|
|
797
|
+
const env = opts.env ?? process.env;
|
|
798
|
+
const token = (opts.token ?? "").trim();
|
|
799
|
+
const url = (opts.url ?? env["KOJEE_GATEWAY_URL"] ?? "").trim();
|
|
800
|
+
const pluginSourceDir = resolveOpenclawPluginSourceDir();
|
|
801
|
+
const install = installOpenclaw({
|
|
802
|
+
homeDir: kojeeHomeDir(),
|
|
803
|
+
openclawConfigPath: openclawConfigPath(opts),
|
|
804
|
+
...url ? { url } : {},
|
|
805
|
+
...token ? { token } : {},
|
|
806
|
+
...pluginSourceDir ? { pluginSourceDir } : {}
|
|
807
|
+
});
|
|
808
|
+
recordRuntime(runtime);
|
|
809
|
+
return { runtime, output: install.output, exitCode: install.exitCode };
|
|
810
|
+
}
|
|
811
|
+
async function runWizardUninstall(runtime, opts) {
|
|
812
|
+
const effective = opts.runtime !== void 0 ? runtime : readRecordedRuntime() ?? runtime;
|
|
813
|
+
const lines = [`Uninstalling runtime: ${effective}`];
|
|
814
|
+
if (effective === "claude-code") {
|
|
815
|
+
const { runUninstall } = await import("./install-LJY2CHKG.js");
|
|
816
|
+
const report = runUninstall({
|
|
817
|
+
...opts.configPath ? { configPath: opts.configPath } : {},
|
|
818
|
+
...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
|
|
819
|
+
});
|
|
820
|
+
lines.push("Removing kojee from Claude:");
|
|
821
|
+
for (const t of report.targets) {
|
|
822
|
+
const label = t.kind === "cli" ? "CLI" : "Claude.app";
|
|
823
|
+
lines.push("");
|
|
824
|
+
lines.push(` ${t.path} (${label})`);
|
|
825
|
+
lines.push(` mcpServers.kojee ${t.mcpServer ? "\u2713 removed" : "\u2014 not found"}`);
|
|
826
|
+
if (t.kind === "cli") {
|
|
827
|
+
if (t.hooksPath) lines.push(` ${t.hooksPath} (hooks)`);
|
|
828
|
+
lines.push(` hook entries ${t.hooks ? "\u2713 removed" : "\u2014 not found"}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
} else if (effective === "codex") {
|
|
832
|
+
const removed = removeCodexConfig({
|
|
833
|
+
...opts.configPath ? { configPath: opts.configPath } : {},
|
|
834
|
+
...opts.hooksPath ? { hooksPath: opts.hooksPath } : {}
|
|
835
|
+
});
|
|
836
|
+
lines.push(` config.toml [mcp_servers.kojee]: ${removed.mcpServer ? "removed" : "not found"}`);
|
|
837
|
+
lines.push(` hooks.json Stop: ${removed.stopHook ? "removed" : "not found"}`);
|
|
838
|
+
} else if (effective === "openclaw") {
|
|
839
|
+
const un = uninstallOpenclaw({ openclawConfigPath: openclawConfigPath(opts) });
|
|
840
|
+
lines.push(un.output);
|
|
841
|
+
} else {
|
|
842
|
+
lines.push(" (hermes writes no MCP-config or hooks \u2014 nothing to tear down.");
|
|
843
|
+
lines.push(" Stop the daemon and unset KOJEE_WEBHOOK_URL/SECRET to disable the sink.)");
|
|
844
|
+
const envPath = path6.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
|
|
845
|
+
try {
|
|
846
|
+
fs5.unlinkSync(envPath);
|
|
847
|
+
lines.push(` removed ${envPath}`);
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
clearRuntimeRecord();
|
|
852
|
+
return { runtime: effective, output: lines.join("\n"), exitCode: 0 };
|
|
853
|
+
}
|
|
854
|
+
function indent(s) {
|
|
855
|
+
return s.split("\n").map((l) => " " + l).join("\n");
|
|
856
|
+
}
|
|
857
|
+
function registerBuiltinInstaller(id, menuLabel, capabilities, doInstall) {
|
|
858
|
+
register({
|
|
859
|
+
id,
|
|
860
|
+
menuLabel,
|
|
861
|
+
capabilities,
|
|
862
|
+
install: async (ctx) => doInstall(ctx.opts),
|
|
863
|
+
uninstall: async (ctx) => runWizardUninstall(id, ctx.opts),
|
|
864
|
+
verify: async () => ({ runtime: id, ok: true, checks: [] })
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
registerBuiltinInstaller(
|
|
868
|
+
"claude-code",
|
|
869
|
+
"Claude Code",
|
|
870
|
+
["pair-credential", "mcp-config-write", "hooks-write"],
|
|
871
|
+
(o) => configureClaudeCode(o)
|
|
872
|
+
);
|
|
873
|
+
registerBuiltinInstaller(
|
|
874
|
+
"codex",
|
|
875
|
+
"Codex",
|
|
876
|
+
["pair-credential", "mcp-config-write", "hooks-write", "webhook"],
|
|
877
|
+
(o) => configureCodex(o)
|
|
878
|
+
);
|
|
879
|
+
registerBuiltinInstaller(
|
|
880
|
+
"hermes",
|
|
881
|
+
"Hermes",
|
|
882
|
+
[
|
|
883
|
+
"pair-credential",
|
|
884
|
+
"webhook",
|
|
885
|
+
"webhook-secret-both-sides",
|
|
886
|
+
"plugin-payload-copy",
|
|
887
|
+
"service-install"
|
|
888
|
+
],
|
|
889
|
+
(o) => configureHermes(o)
|
|
890
|
+
);
|
|
891
|
+
registerBuiltinInstaller(
|
|
892
|
+
"openclaw",
|
|
893
|
+
"OpenClaw",
|
|
894
|
+
["pair-credential", "in-process-plugin"],
|
|
895
|
+
(o) => configureOpenclaw(o)
|
|
896
|
+
);
|
|
897
|
+
export {
|
|
898
|
+
CODEX_UNVERIFIED_NOTE,
|
|
899
|
+
DEFAULT_BROKER_URL,
|
|
900
|
+
generateWebhookSecret,
|
|
901
|
+
resolveRuntime,
|
|
902
|
+
resolveWizardWebhook,
|
|
903
|
+
runWizard
|
|
904
|
+
};
|