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 Tandem channel plugins.** On those runtimes Tandem is a first-class channel (peer to Telegram/Discord), wired through a plugin that wraps the same gateway client — **not** MCP. See [Native gateway runtimes](#native-gateway-runtimes-openclaw--hermes).
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 fs6 from "fs";
139
- import path7 from "path";
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 path6 from "path";
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 = path6.join(inp.homeDir, ".kojee", "hermes.env");
513
- const adapterEnv = path6.join(inp.homeDir, ".hermes", ".env");
514
- const pluginDir = path6.join(inp.homeDir, ".hermes", "plugins", "kojee-tandem");
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 ${path6.join(inp.payloadBaseDir, "plugins", "hermes")}. No changes were written. Run \`npm run build\` to stage dist/plugins/hermes, then retry.`,
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: systemctl --user restart hermes-gateway (or: hermes gateway restart)",
565
- " - join a tandem once: tandem_join <tandem_id>",
566
- " - verify: kojee-mcp doctor"
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 path7.dirname(fileURLToPath2(import.meta.url));
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("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
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 = path7.join(home, ".kojee", "hermes.env");
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 ?? path7.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
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
- lines.push(" (hermes writes no MCP-config or hooks \u2014 nothing to tear down.");
1066
- lines.push(" Stop the daemon and unset KOJEE_WEBHOOK_URL/SECRET to disable the sink.)");
1067
- const envPath = path7.join(kojeeHomeDir(), ".kojee", `${effective}.env`);
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
- fs6.unlinkSync(envPath);
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-NZINEMG3.js");
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-44ASXYMS.js");
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,
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_BROKER_URL,
3
3
  reconcileConnectPairSlot,
4
4
  runWizard
5
- } from "./chunk-XPIW4N55.js";
5
+ } from "./chunk-QNGMMBOD.js";
6
6
  import "./chunk-6XWTUDWW.js";
7
7
  import "./chunk-E6WMFMM2.js";
8
8
  import "./chunk-77HWBSRH.js";
@@ -3,7 +3,7 @@ import {
3
3
  pairSlotFor,
4
4
  reconcileConnectPairSlot,
5
5
  runWizard
6
- } from "./chunk-XPIW4N55.js";
6
+ } from "./chunk-QNGMMBOD.js";
7
7
  import "./chunk-6XWTUDWW.js";
8
8
  import "./chunk-E6WMFMM2.js";
9
9
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.7.5-staging.2",
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": {