kojee-mcp 0.7.5-staging.2 → 0.7.5-staging.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ There are three ways to connect Kojee to an MCP-capable agent:
|
|
|
11
11
|
|
|
12
12
|
1. **Mobile, web & desktop (recommended for chat clients)** — paste the Kojee MCP URL into the app's "Add custom connector" dialog. The app handles OAuth login and consent. No local install. Works on Claude (web/desktop/iOS/Android) and ChatGPT (web/desktop/iOS/Android with Developer Mode enabled).
|
|
13
13
|
2. **Claude Code / Codex / Cursor — the local stdio proxy.** Run `kojee-mcp` locally; it holds a `gw_` gateway token + ES256 keypair and signs every request with DPoP (RFC 9449). The runtime-aware [`init` wizard](#quick-start-claude-code--tandem) wires it into your harness and sets up the wake path so an idle agent is woken by Tandem messages between turns. **This is the recommended path for agentic runtimes** and the focus of this README.
|
|
14
|
-
3. **OpenClaw / Hermes — native
|
|
14
|
+
3. **OpenClaw / Hermes — native gateway runtimes.** On those runtimes kojee is registered as the agent's MCP server (so the agent can explore + call kojee tools), and Tandem is a first-class channel wired through the gateway (an in-gateway MCP wake injector on OpenClaw; a sidecar daemon + channel plugin on Hermes). See [Native gateway runtimes](#native-gateway-runtimes-openclaw--hermes).
|
|
15
15
|
|
|
16
16
|
## Mobile, Web & Desktop (Recommended)
|
|
17
17
|
|
|
@@ -135,8 +135,8 @@ function pairSlotFor(runtime, code) {
|
|
|
135
135
|
|
|
136
136
|
// src/wizard/wizard.ts
|
|
137
137
|
import crypto3 from "crypto";
|
|
138
|
-
import
|
|
139
|
-
import
|
|
138
|
+
import fs7 from "fs";
|
|
139
|
+
import path8 from "path";
|
|
140
140
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
141
141
|
|
|
142
142
|
// src/wizard/skill-install.ts
|
|
@@ -350,7 +350,8 @@ function writeWebhookSecretBothSides(opts) {
|
|
|
350
350
|
["KOJEE_RUNTIME", opts.runtime],
|
|
351
351
|
["KOJEE_WEBHOOK_URL", opts.webhookUrl],
|
|
352
352
|
["KOJEE_WEBHOOK_SECRET", secret],
|
|
353
|
-
...opts.signatureEnv ?? []
|
|
353
|
+
...opts.signatureEnv ?? [],
|
|
354
|
+
...opts.daemonExtraEnv ?? []
|
|
354
355
|
];
|
|
355
356
|
upsertEnvFile(opts.daemonEnvPath, daemonVars, {
|
|
356
357
|
exportPrefix: true,
|
|
@@ -368,7 +369,7 @@ function writeWebhookSecretBothSides(opts) {
|
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
// src/wizard/installers/hermes.ts
|
|
371
|
-
import
|
|
372
|
+
import path7 from "path";
|
|
372
373
|
|
|
373
374
|
// src/wizard/plugin-payload.ts
|
|
374
375
|
import fs4 from "fs";
|
|
@@ -507,17 +508,100 @@ function writeService(platform, spec) {
|
|
|
507
508
|
return plan;
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
// src/wizard/capabilities/hermes-mcp-config.ts
|
|
512
|
+
import fs6 from "fs";
|
|
513
|
+
import path6 from "path";
|
|
514
|
+
import { parseDocument, isMap } from "yaml";
|
|
515
|
+
var HERMES_MCP_SERVER_NAME = "kojee";
|
|
516
|
+
var HERMES_INSTANCE_KEY = "hermes-gateway";
|
|
517
|
+
var HERMES_MCP_CONNECT_TIMEOUT_S = 120;
|
|
518
|
+
function defaultHermesConfigPath(homeDir) {
|
|
519
|
+
return path6.join(homeDir, ".hermes", "config.yaml");
|
|
520
|
+
}
|
|
521
|
+
function buildKojeeEntry(binPath) {
|
|
522
|
+
return {
|
|
523
|
+
command: binPath,
|
|
524
|
+
args: [],
|
|
525
|
+
env: {
|
|
526
|
+
KOJEE_RUNTIME: "hermes",
|
|
527
|
+
KOJEE_INSTANCE: HERMES_INSTANCE_KEY,
|
|
528
|
+
// Vanilla tools-only proxy: NEVER a second webhook delivery (see header).
|
|
529
|
+
KOJEE_WEBHOOK_URL: ""
|
|
530
|
+
},
|
|
531
|
+
connect_timeout: HERMES_MCP_CONNECT_TIMEOUT_S
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function resolveMode(filePath) {
|
|
535
|
+
try {
|
|
536
|
+
return fs6.statSync(filePath).mode & 511;
|
|
537
|
+
} catch {
|
|
538
|
+
return 384;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function atomicWrite(filePath, content, mode) {
|
|
542
|
+
fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
543
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
544
|
+
fs6.writeFileSync(tmp, content, { mode });
|
|
545
|
+
fs6.renameSync(tmp, filePath);
|
|
546
|
+
}
|
|
547
|
+
function backupSuffix(now) {
|
|
548
|
+
return (now ? now() : /* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
549
|
+
}
|
|
550
|
+
function writeHermesMcpConfig(configPath, opts) {
|
|
551
|
+
let raw = null;
|
|
552
|
+
try {
|
|
553
|
+
raw = fs6.readFileSync(configPath, "utf8");
|
|
554
|
+
} catch {
|
|
555
|
+
raw = null;
|
|
556
|
+
}
|
|
557
|
+
let backedUp;
|
|
558
|
+
let doc = parseDocument("");
|
|
559
|
+
if (raw !== null && raw.trim() !== "") {
|
|
560
|
+
const parsed = parseDocument(raw);
|
|
561
|
+
if (parsed.errors.length > 0) {
|
|
562
|
+
backedUp = `${configPath}.corrupt-${backupSuffix(opts.now)}`;
|
|
563
|
+
fs6.copyFileSync(configPath, backedUp);
|
|
564
|
+
} else {
|
|
565
|
+
doc = parsed;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (doc.hasIn(["mcp_servers"]) && !isMap(doc.getIn(["mcp_servers"], true))) {
|
|
569
|
+
doc.deleteIn(["mcp_servers"]);
|
|
570
|
+
}
|
|
571
|
+
doc.setIn(["mcp_servers", HERMES_MCP_SERVER_NAME], buildKojeeEntry(opts.binPath));
|
|
572
|
+
atomicWrite(configPath, String(doc), resolveMode(configPath));
|
|
573
|
+
return { configPath, ...backedUp ? { backedUp } : {} };
|
|
574
|
+
}
|
|
575
|
+
function removeHermesMcpServer(configPath) {
|
|
576
|
+
let raw;
|
|
577
|
+
try {
|
|
578
|
+
raw = fs6.readFileSync(configPath, "utf8");
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const doc = parseDocument(raw);
|
|
583
|
+
if (doc.errors.length > 0) return false;
|
|
584
|
+
if (!doc.hasIn(["mcp_servers", HERMES_MCP_SERVER_NAME])) return false;
|
|
585
|
+
doc.deleteIn(["mcp_servers", HERMES_MCP_SERVER_NAME]);
|
|
586
|
+
const servers = doc.getIn(["mcp_servers"], true);
|
|
587
|
+
if (isMap(servers) && servers.items.length === 0) {
|
|
588
|
+
doc.deleteIn(["mcp_servers"]);
|
|
589
|
+
}
|
|
590
|
+
atomicWrite(configPath, String(doc), resolveMode(configPath));
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
510
594
|
// src/wizard/installers/hermes.ts
|
|
511
595
|
function installHermes(inp) {
|
|
512
|
-
const daemonEnv =
|
|
513
|
-
const adapterEnv =
|
|
514
|
-
const pluginDir =
|
|
596
|
+
const daemonEnv = path7.join(inp.homeDir, ".kojee", "hermes.env");
|
|
597
|
+
const adapterEnv = path7.join(inp.homeDir, ".hermes", ".env");
|
|
598
|
+
const pluginDir = path7.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
|
|
515
599
|
if (!inp.skipPayload) {
|
|
516
600
|
const missing = missingPayloadFiles("hermes", inp.payloadBaseDir, HERMES_PAYLOAD_FILES);
|
|
517
601
|
if (missing.length > 0) {
|
|
518
602
|
return {
|
|
519
603
|
runtime: "hermes",
|
|
520
|
-
output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${
|
|
604
|
+
output: `hermes install ERROR: the bundled plugin payload is incomplete \u2014 missing ${missing.join(", ")} under ${path7.join(inp.payloadBaseDir, "plugins", "hermes")}. No changes were written. Run \`npm run build\` to stage dist/plugins/hermes, then retry.`,
|
|
521
605
|
exitCode: 2,
|
|
522
606
|
secret: "",
|
|
523
607
|
secretReused: false
|
|
@@ -529,11 +613,14 @@ function installHermes(inp) {
|
|
|
529
613
|
adapterEnvPath: adapterEnv,
|
|
530
614
|
webhookUrl: inp.webhookUrl,
|
|
531
615
|
runtime: "hermes",
|
|
616
|
+
daemonExtraEnv: [["KOJEE_INSTANCE", HERMES_INSTANCE_KEY]],
|
|
532
617
|
...inp.mcpDir ? { mcpDir: inp.mcpDir } : {},
|
|
533
618
|
...inp.allowAllUsers ? { allowAllUsers: true } : {},
|
|
534
619
|
...inp.signatureEnv && inp.signatureEnv.length > 0 ? { signatureEnv: inp.signatureEnv } : {},
|
|
535
620
|
...inp.webhookSecret ? { existingSecret: inp.webhookSecret } : {}
|
|
536
621
|
});
|
|
622
|
+
const hermesConfig = defaultHermesConfigPath(inp.homeDir);
|
|
623
|
+
const mcp = writeHermesMcpConfig(hermesConfig, { binPath: inp.binPath });
|
|
537
624
|
const staged = inp.skipPayload ? [] : installBundledPayload({
|
|
538
625
|
runtime: "hermes",
|
|
539
626
|
baseDir: inp.payloadBaseDir,
|
|
@@ -552,6 +639,8 @@ function installHermes(inp) {
|
|
|
552
639
|
` secret: ${sec.reused ? "reused existing" : "generated"} (matched on daemon + adapter env)`,
|
|
553
640
|
` daemon env: ${daemonEnv}`,
|
|
554
641
|
` adapter env: ${adapterEnv}`,
|
|
642
|
+
` mcp config: ${hermesConfig} (mcp_servers.kojee \u2014 agent explores kojee via mcp_kojee_*)`,
|
|
643
|
+
...mcp.backedUp ? [` NOTE: prior ${hermesConfig} was unparseable; backed up to ${mcp.backedUp} before writing.`] : [],
|
|
555
644
|
inp.skipPayload ? ` plugin: SKIPPED \u2014 no bundled payload (run \`npm run build\` to stage dist/plugins/hermes)` : ` plugin: ${pluginDir} (${staged.length} files)`,
|
|
556
645
|
svc.supported ? ` service: ${svc.unitPath}` : ` service: manual \u2014 ${svc.note ?? "unsupported platform"}`,
|
|
557
646
|
// NB: the "Receiver contract" section is intentionally NOT printed here.
|
|
@@ -561,9 +650,13 @@ function installHermes(inp) {
|
|
|
561
650
|
"",
|
|
562
651
|
"Next:",
|
|
563
652
|
` - start the daemon service: ${svc.activateCmd}`,
|
|
564
|
-
" - reload the Hermes gateway to load the plugin
|
|
565
|
-
"
|
|
566
|
-
" - verify:
|
|
653
|
+
" - reload the Hermes gateway to load the channel plugin AND the kojee MCP server:",
|
|
654
|
+
" systemctl --user restart hermes-gateway (or: hermes gateway restart, or /reload-mcp)",
|
|
655
|
+
" - verify the MCP registered: hermes mcp test kojee (tools appear to the agent as mcp_kojee_*)",
|
|
656
|
+
" - the agent can now join from its own tools: mcp_kojee_tandem_join <tandem_id>",
|
|
657
|
+
" - verify the daemon/channel: kojee-mcp doctor",
|
|
658
|
+
" - if tools don't register: ensure Hermes was installed with the MCP extra",
|
|
659
|
+
' (cd ~/.hermes/hermes-agent && uv pip install -e ".[mcp]") and re-check connect_timeout.'
|
|
567
660
|
];
|
|
568
661
|
return {
|
|
569
662
|
runtime: "hermes",
|
|
@@ -937,7 +1030,7 @@ function buildDaemonEnvBlock(runtime, wh, envFile) {
|
|
|
937
1030
|
return lines;
|
|
938
1031
|
}
|
|
939
1032
|
function distDir() {
|
|
940
|
-
return
|
|
1033
|
+
return path8.dirname(fileURLToPath2(import.meta.url));
|
|
941
1034
|
}
|
|
942
1035
|
function resolveBinPath() {
|
|
943
1036
|
const entry = process.argv[1];
|
|
@@ -955,7 +1048,10 @@ function configureHermes(opts) {
|
|
|
955
1048
|
opts = effectiveOpts;
|
|
956
1049
|
const lines = [];
|
|
957
1050
|
lines.push(`Configured runtime: ${runtime}`);
|
|
958
|
-
lines.push(
|
|
1051
|
+
lines.push(
|
|
1052
|
+
"Wake mode: webhook sink (daemon-consumed). ALSO registers kojee as the Hermes agent's",
|
|
1053
|
+
"MCP server (mcp_servers.kojee) so the agent can explore kojee tools. NO hooks written."
|
|
1054
|
+
);
|
|
959
1055
|
lines.push("");
|
|
960
1056
|
if (!suppliedUrl) {
|
|
961
1057
|
lines.push(
|
|
@@ -998,7 +1094,7 @@ function configureHermes(opts) {
|
|
|
998
1094
|
return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
|
|
999
1095
|
}
|
|
1000
1096
|
recordRuntime(runtime);
|
|
1001
|
-
const envFile =
|
|
1097
|
+
const envFile = path8.join(home, ".kojee", "hermes.env");
|
|
1002
1098
|
lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
|
|
1003
1099
|
lines.push("");
|
|
1004
1100
|
lines.push(install.output);
|
|
@@ -1020,7 +1116,7 @@ function configureHermes(opts) {
|
|
|
1020
1116
|
return { runtime, output: lines.join("\n"), exitCode: 0 };
|
|
1021
1117
|
}
|
|
1022
1118
|
function openclawConfigPath(opts) {
|
|
1023
|
-
return opts.openclawConfigPath ??
|
|
1119
|
+
return opts.openclawConfigPath ?? path8.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
|
|
1024
1120
|
}
|
|
1025
1121
|
function configureOpenclaw(opts) {
|
|
1026
1122
|
const runtime = "openclaw";
|
|
@@ -1062,11 +1158,15 @@ async function runWizardUninstall(runtime, opts) {
|
|
|
1062
1158
|
const un = uninstallOpenclaw({ openclawConfigPath: openclawConfigPath(opts) });
|
|
1063
1159
|
lines.push(un.output);
|
|
1064
1160
|
} else {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1161
|
+
const hermesConfig = defaultHermesConfigPath(kojeeHomeDir());
|
|
1162
|
+
const removedMcp = removeHermesMcpServer(hermesConfig);
|
|
1163
|
+
lines.push(
|
|
1164
|
+
removedMcp ? ` removed mcp_servers.kojee from ${hermesConfig} (operator config preserved)` : ` no mcp_servers.kojee in ${hermesConfig} \u2014 nothing to remove`
|
|
1165
|
+
);
|
|
1166
|
+
lines.push(" (stop the daemon service + reload the gateway to disable the webhook wake path.)");
|
|
1167
|
+
const envPath = path8.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
|
|
1068
1168
|
try {
|
|
1069
|
-
|
|
1169
|
+
fs7.unlinkSync(envPath);
|
|
1070
1170
|
lines.push(` removed ${envPath}`);
|
|
1071
1171
|
} catch {
|
|
1072
1172
|
}
|
|
@@ -1109,7 +1209,11 @@ registerBuiltinInstaller(
|
|
|
1109
1209
|
"webhook",
|
|
1110
1210
|
"webhook-secret-both-sides",
|
|
1111
1211
|
"plugin-payload-copy",
|
|
1112
|
-
"service-install"
|
|
1212
|
+
"service-install",
|
|
1213
|
+
// kojee registered as the Hermes agent's MCP server (mcp_servers.kojee in
|
|
1214
|
+
// ~/.hermes/config.yaml) so the agent can EXPLORE + call kojee tools —
|
|
1215
|
+
// additive to the channel plugin, does not change the webhook wake path.
|
|
1216
|
+
"mcp-config-write"
|
|
1113
1217
|
],
|
|
1114
1218
|
(o) => configureHermes(o)
|
|
1115
1219
|
);
|
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
|
|
|
48
48
|
program.command("connect <code>").description(
|
|
49
49
|
"Connect this runtime to Kojee with a per-agent pair code from the dashboard (claude-code | codex | openclaw | hermes). claude-code/codex/openclaw write a per-runtime paired slot (~/.kojee/agents/<runtime>/config.json) and point the runtime launcher at it via --paired-config; hermes writes the global ~/.kojee/config.json (its daemon + `kojee-mcp send` read that). Idempotent."
|
|
50
50
|
).requiredOption("--runtime <id>", "Target runtime: claude-code | codex | openclaw | hermes").option("--url <url>", "Broker base URL (default: the canonical staging broker)").action(async (code, opts) => {
|
|
51
|
-
const { runConnect } = await import("./connect-handler-
|
|
51
|
+
const { runConnect } = await import("./connect-handler-D3VQMUIS.js");
|
|
52
52
|
const result = await runConnect({
|
|
53
53
|
code,
|
|
54
54
|
runtime: opts.runtime,
|
|
@@ -143,7 +143,7 @@ function addInstallOptions(cmd, runtimeHelp) {
|
|
|
143
143
|
function makeInstallAction(verb) {
|
|
144
144
|
return async (opts) => {
|
|
145
145
|
const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
|
|
146
|
-
const { runSetup, resolvePairCode } = await import("./setup-handler-
|
|
146
|
+
const { runSetup, resolvePairCode } = await import("./setup-handler-5Q47JYPQ.js");
|
|
147
147
|
const pairCode = resolvePairCode(opts);
|
|
148
148
|
const result = await runSetup({
|
|
149
149
|
verb,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kojee-mcp",
|
|
3
|
-
"version": "0.7.5-staging.
|
|
3
|
+
"version": "0.7.5-staging.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"ps-list": "^8.1.1",
|
|
36
36
|
"ulidx": "^2.3.0",
|
|
37
37
|
"undici": "^8.5.0",
|
|
38
|
+
"yaml": "^2.9.0",
|
|
38
39
|
"zod": "^3.22.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|