kojee-mcp 0.7.5-staging.3 → 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +101 -1
  2. package/dist/{chunk-67F67AQ6.js → chunk-3N4YYIOJ.js} +28 -0
  3. package/dist/{chunk-WBXU27BF.js → chunk-7J5CSXGS.js} +1 -1
  4. package/dist/{chunk-QNGMMBOD.js → chunk-BO52SILE.js} +21 -125
  5. package/dist/{chunk-TCWIXG5C.js → chunk-D5VPUCPY.js} +1 -1
  6. package/dist/{chunk-CUOJHP2S.js → chunk-GP7XNGUR.js} +10 -2
  7. package/dist/{chunk-TNZITJAB.js → chunk-HMS4PZLC.js} +1 -1
  8. package/dist/chunk-JRCHS6CP.js +14 -0
  9. package/dist/chunk-NMHM6SII.js +118 -0
  10. package/dist/{chunk-A4IOKD4Z.js → chunk-P2R3KWYK.js} +1 -1
  11. package/dist/{chunk-FJUAMJHU.js → chunk-QLARIAUJ.js} +26 -2
  12. package/dist/{chunk-ZSHCHOPL.js → chunk-TXGO2W5I.js} +6 -14
  13. package/dist/{send-cli-45RLGYIC.js → chunk-VGZ4M3FP.js} +8 -11
  14. package/dist/{chunk-PKOBR7ZN.js → chunk-WZP7HXXN.js} +9 -0
  15. package/dist/cli.js +75 -13
  16. package/dist/{codex-prompt-submit-hook-J5PZEJSK.js → codex-prompt-submit-hook-6QUCHZB2.js} +1 -1
  17. package/dist/{codex-stop-hook-32W4BIOM.js → codex-stop-hook-5LVFGGFE.js} +1 -1
  18. package/dist/{connect-handler-D3VQMUIS.js → connect-handler-5NACO67R.js} +2 -2
  19. package/dist/cowork-E46Z2QLU.js +370 -0
  20. package/dist/cowork-worker-RSXWOYUQ.js +316 -0
  21. package/dist/{doctor-WUU5BVPT.js → doctor-DK72HJI3.js} +4 -4
  22. package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-MECEBXJC.js} +1 -1
  23. package/dist/{event-log-2NBJEIEP.js → event-log-QLWB22KX.js} +2 -2
  24. package/dist/{event-stream-ATKYXDBV.js → event-stream-L432EP4U.js} +1 -1
  25. package/dist/{hook-server-CWBH6PFL.js → hook-server-6EVSFLNW.js} +1 -1
  26. package/dist/index.js +5 -4
  27. package/dist/lib.d.ts +37 -1
  28. package/dist/lib.js +1 -1
  29. package/dist/messages-cli-KKFCKV55.js +24 -0
  30. package/dist/{registry-XJ67EO22.js → registry-E2DW73RE.js} +14 -14
  31. package/dist/send-cli-NPFQWSBM.js +18 -0
  32. package/dist/{server-DU3LFS32.js → server-D5VAMAQW.js} +2 -2
  33. package/dist/{session-discovery-NHCZ2XWA.js → session-discovery-4LZNLBPN.js} +3 -1
  34. package/dist/{setup-handler-5Q47JYPQ.js → setup-handler-ANXNXK6I.js} +2 -2
  35. package/dist/{stop-hook-N6TX4YQT.js → stop-hook-QZRUUX22.js} +4 -4
  36. package/dist/{tail-stream-G4WGTXDS.js → tail-stream-EK66YLNF.js} +103 -25
  37. package/dist/{user-prompt-submit-hook-DTXXFDSD.js → user-prompt-submit-hook-O7KIT3RR.js} +2 -2
  38. package/package.json +1 -2
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 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).
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).
15
15
 
16
16
  ## Mobile, Web & Desktop (Recommended)
17
17
 
@@ -555,6 +555,106 @@ literal URL in the body; de-fang or split it; this is **not** an auth failure),
555
555
  `approval_required`, `not_paired`, `not_enrolled`, `bad_request`,
556
556
  `unauthorized`, `send_unavailable`, `send_failed` (raw text preserved).
557
557
 
558
+ ## Claude Co-Work (paste-one-command wake)
559
+
560
+ **Claude Co-Work** is a cloud-sandbox harness with no MCP registration and no
561
+ restart available mid-session, so it can't use the `init`-wired MCP proxy above.
562
+ Instead, one line — generated from the Kojee dashboard and pasted into the
563
+ agent's first turn — enrolls the sandbox and gets it streaming Tandem wakes
564
+ immediately. This is a separate, parallel wake path from the MCP proxy; it does
565
+ not use `startProxy` at all.
566
+
567
+ ### The pasted command
568
+
569
+ ```bash
570
+ KOJEE_TOKEN=<token> npx -y kojee-mcp@<version> cowork --tandem <tandem_id> --url <broker>
571
+ ```
572
+
573
+ The dashboard fills in `<token>`, `<version>` (the currently published
574
+ `kojee-mcp` version), `<tandem_id>`, and `<broker>` when it generates this line
575
+ for you. Flags:
576
+
577
+ | Flag | Required | Description |
578
+ |---|---|---|
579
+ | `--tandem <id>` | yes, to launch | Tandem id to subscribe to. |
580
+ | `--url <url>` | yes, to launch | Broker base URL — the token does not encode it. |
581
+ | `--token <token>` | no (fallback) | Gateway token fallback. The canonical intake is the `KOJEE_TOKEN=` env prefix (keeps the secret off `/proc/<pid>/cmdline` and npm debug logs); `--token` costs a few seconds of argv exposure and is never forwarded to the worker process. |
582
+ | `--force` | no | Override the clobber guard — replace a different token/broker already planted in `~/.kojee/config.json`. Without it, `cowork` refuses to overwrite a different credential (protects against an accidental paste evicting another sandbox's DPoP key — see the warning below). |
583
+ | `--detach` | no | Host the worker as a detached background daemon (hidden `cowork-daemon` subcommand) instead of the default Monitor-hosted worker. |
584
+
585
+ Run once, in the foreground, `cowork` enrolls the token into the paired DPoP
586
+ slot, plants `~/.kojee/config.json` + `~/.kojee/keypair.json`, joins the
587
+ tandem, and runs a startup barrier read — so a bad or revoked token, or a
588
+ token that isn't a member of the tandem, fails loudly at paste time (nonzero
589
+ exit, no Monitor recipe printed) instead of silently starting deaf.
590
+
591
+ ### What the agent does with the printed output
592
+
593
+ On success `cowork` prints a recipe similar to:
594
+
595
+ ```
596
+ [kojee cowork] enrolled + subscribed to tandem_abc123 (seat cowork-9f2a1c7b4e05).
597
+ [kojee cowork] baseline cursor = 4821. NOTE: this command contains a secret token — don't re-share this transcript.
598
+
599
+ 1) Arm the wake watcher ONCE (persistent Monitor):
600
+ Monitor(command=`npx kojee-mcp@<version> cowork-stream --tandem tandem_abc123`, persistent=true, description="kojee Tandem events")
601
+ ...
602
+ ```
603
+
604
+ The agent should:
605
+
606
+ 1. **Arm the Monitor exactly once**, using the printed command — e.g.
607
+ `Monitor(command=\`npx kojee-mcp@<version> cowork-stream --tandem <tandem_id>\`, persistent=true, description="kojee Tandem events")`.
608
+ This spawns `cowork-stream`, which subscribes over SSE, writes the event
609
+ log, and fuses a tail of it onto its own stdout — each line is one Monitor
610
+ wake, carrying `msg=<id>` and `cursor=<n>`. If a watcher is already armed,
611
+ don't arm a second one.
612
+ 2. **Catch up once, right after arming** — `npx kojee-mcp@<version> messages <tandem_id> --since <baseline cursor>` — to cover the gap between paste and Monitor arm.
613
+ 3. **Reply via the `send` CLI, never MCP** (there are no MCP tools in this
614
+ session) — `npx kojee-mcp@<version> send <tandem_id> --body "..." --reply-to <msg>`.
615
+ One JSON envelope on stdout: `{ok:true, message_id, cursor}` (exit 0) or
616
+ `{ok:false, error, message}` (exit 1). Surface `content_blocked` /
617
+ `rate_limited` / `gateway_auth` distinctly — don't blind-retry.
618
+ 4. **Pull full, untruncated context on any gap** with the same `messages
619
+ --since <last cursor you saw>` command — e.g. after a cursor jump or a
620
+ `status=rotated` reset (event lines on the Monitor stream are truncated to
621
+ 200 characters).
622
+
623
+ Lines the worker prints that start with `[kojee]` (e.g. `[kojee] log=<path>`,
624
+ `[kojee] WARNING: ...`, `[kojee] WORKER EXITED ...`) are telemetry, not Tandem
625
+ messages — never reply to them.
626
+
627
+ ### Stopping it
628
+
629
+ ```bash
630
+ npx kojee-mcp@<version> cowork stop --tandem <tandem_id>
631
+ ```
632
+
633
+ Scans for live cowork workers on that tandem (omit `--tandem` to stop all live
634
+ cowork workers on the box), SIGTERMs each, and cleans up its discovery file.
635
+
636
+ **Merged (default) vs `--detach`:** by default the persistent Monitor *is* the
637
+ host for the worker (`cowork-stream`), so if the wake stream dies the harness
638
+ sees the Monitor's own process exit. `--detach` instead spawns a headless
639
+ `cowork-daemon` child in the background and has the agent's Monitor tail its
640
+ event-log file — use it only on a sandbox that won't keep a long-lived Monitor
641
+ child alive.
642
+
643
+ > **WARNING — mint a fresh token per sandbox.** Enrolling with `cowork` claims
644
+ > the token's single DPoP key slot: the Kojee backend keeps **exactly one**
645
+ > DPoP key per gateway token, and every enrollment **replaces** it. Paste the
646
+ > *same* token into a second sandbox and the first sandbox's credential is
647
+ > silently evicted — it goes offline. Mint a **fresh** cowork token per
648
+ > sandbox from the dashboard; never reuse one across sandboxes.
649
+ >
650
+ > The pasted command also **contains a secret token in plaintext**. It
651
+ > persists in the agent's transcript and shell history for as long as those
652
+ > are retained — don't re-share the transcript, and treat the token as
653
+ > compromised if it does leak out.
654
+
655
+ Keep the `@<version>` pin — seat coherence relies on a feature that only
656
+ exists in that version; do not drop it.
657
+
558
658
  ## Development
559
659
 
560
660
  Run tests:
@@ -73,6 +73,33 @@ function readSessionDiscoveryByKey(key) {
73
73
  return null;
74
74
  }
75
75
  }
76
+ function scanLiveCoworkWorkers(opts = {}) {
77
+ const dir = opts.dir ?? sessionDiscoveryDir();
78
+ const alive = opts.isAlive ?? isProcessAlive;
79
+ let names;
80
+ try {
81
+ names = fs.readdirSync(dir);
82
+ } catch {
83
+ return [];
84
+ }
85
+ const out = [];
86
+ for (const name of names) {
87
+ if (!name.startsWith("cc-") || !name.endsWith(".json")) continue;
88
+ const filePath = path.join(dir, name);
89
+ let entry = null;
90
+ try {
91
+ entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
92
+ } catch {
93
+ continue;
94
+ }
95
+ if (!entry || entry.origin !== "cowork") continue;
96
+ const pid = entry.proxyPid ?? entry.pid;
97
+ if (typeof pid !== "number" || !alive(pid)) continue;
98
+ const key = name.slice("cc-".length, -".json".length);
99
+ out.push({ key, entry, path: filePath, pid });
100
+ }
101
+ return out;
102
+ }
76
103
  var DEFAULT_SWEEP_MIN_AGE_MS = 6e4;
77
104
  function isProcessAlive(pid) {
78
105
  try {
@@ -143,5 +170,6 @@ export {
143
170
  writeDiscoveryByKey,
144
171
  cleanupDiscoveryByKey,
145
172
  readSessionDiscoveryByKey,
173
+ scanLiveCoworkWorkers,
146
174
  sweepStaleDiscovery
147
175
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readSessionDiscoveryByKey
3
- } from "./chunk-67F67AQ6.js";
3
+ } from "./chunk-3N4YYIOJ.js";
4
4
  import {
5
5
  deriveDiscoveryKey,
6
6
  findClaudeAncestorPid
@@ -49,7 +49,7 @@ import {
49
49
  CODEX_LISTEN_CAP_MS,
50
50
  buildCondensedTandemRules,
51
51
  buildWebhookReceiverNote
52
- } from "./chunk-FJUAMJHU.js";
52
+ } from "./chunk-QLARIAUJ.js";
53
53
 
54
54
  // src/wizard/pair-slot.ts
55
55
  import fs from "fs";
@@ -135,8 +135,8 @@ function pairSlotFor(runtime, code) {
135
135
 
136
136
  // src/wizard/wizard.ts
137
137
  import crypto3 from "crypto";
138
- import fs7 from "fs";
139
- import path8 from "path";
138
+ import fs6 from "fs";
139
+ import path7 from "path";
140
140
  import { fileURLToPath as fileURLToPath2 } from "url";
141
141
 
142
142
  // src/wizard/skill-install.ts
@@ -350,8 +350,7 @@ 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 ?? [],
354
- ...opts.daemonExtraEnv ?? []
353
+ ...opts.signatureEnv ?? []
355
354
  ];
356
355
  upsertEnvFile(opts.daemonEnvPath, daemonVars, {
357
356
  exportPrefix: true,
@@ -369,7 +368,7 @@ function writeWebhookSecretBothSides(opts) {
369
368
  }
370
369
 
371
370
  // src/wizard/installers/hermes.ts
372
- import path7 from "path";
371
+ import path6 from "path";
373
372
 
374
373
  // src/wizard/plugin-payload.ts
375
374
  import fs4 from "fs";
@@ -508,100 +507,17 @@ function writeService(platform, spec) {
508
507
  return plan;
509
508
  }
510
509
 
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
-
594
510
  // src/wizard/installers/hermes.ts
595
511
  function installHermes(inp) {
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");
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");
599
515
  if (!inp.skipPayload) {
600
516
  const missing = missingPayloadFiles("hermes", inp.payloadBaseDir, HERMES_PAYLOAD_FILES);
601
517
  if (missing.length > 0) {
602
518
  return {
603
519
  runtime: "hermes",
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.`,
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.`,
605
521
  exitCode: 2,
606
522
  secret: "",
607
523
  secretReused: false
@@ -613,14 +529,11 @@ function installHermes(inp) {
613
529
  adapterEnvPath: adapterEnv,
614
530
  webhookUrl: inp.webhookUrl,
615
531
  runtime: "hermes",
616
- daemonExtraEnv: [["KOJEE_INSTANCE", HERMES_INSTANCE_KEY]],
617
532
  ...inp.mcpDir ? { mcpDir: inp.mcpDir } : {},
618
533
  ...inp.allowAllUsers ? { allowAllUsers: true } : {},
619
534
  ...inp.signatureEnv && inp.signatureEnv.length > 0 ? { signatureEnv: inp.signatureEnv } : {},
620
535
  ...inp.webhookSecret ? { existingSecret: inp.webhookSecret } : {}
621
536
  });
622
- const hermesConfig = defaultHermesConfigPath(inp.homeDir);
623
- const mcp = writeHermesMcpConfig(hermesConfig, { binPath: inp.binPath });
624
537
  const staged = inp.skipPayload ? [] : installBundledPayload({
625
538
  runtime: "hermes",
626
539
  baseDir: inp.payloadBaseDir,
@@ -639,8 +552,6 @@ function installHermes(inp) {
639
552
  ` secret: ${sec.reused ? "reused existing" : "generated"} (matched on daemon + adapter env)`,
640
553
  ` daemon env: ${daemonEnv}`,
641
554
  ` 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.`] : [],
644
555
  inp.skipPayload ? ` plugin: SKIPPED \u2014 no bundled payload (run \`npm run build\` to stage dist/plugins/hermes)` : ` plugin: ${pluginDir} (${staged.length} files)`,
645
556
  svc.supported ? ` service: ${svc.unitPath}` : ` service: manual \u2014 ${svc.note ?? "unsupported platform"}`,
646
557
  // NB: the "Receiver contract" section is intentionally NOT printed here.
@@ -650,13 +561,9 @@ function installHermes(inp) {
650
561
  "",
651
562
  "Next:",
652
563
  ` - start the daemon service: ${svc.activateCmd}`,
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.'
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"
660
567
  ];
661
568
  return {
662
569
  runtime: "hermes",
@@ -1030,7 +937,7 @@ function buildDaemonEnvBlock(runtime, wh, envFile) {
1030
937
  return lines;
1031
938
  }
1032
939
  function distDir() {
1033
- return path8.dirname(fileURLToPath2(import.meta.url));
940
+ return path7.dirname(fileURLToPath2(import.meta.url));
1034
941
  }
1035
942
  function resolveBinPath() {
1036
943
  const entry = process.argv[1];
@@ -1048,10 +955,7 @@ function configureHermes(opts) {
1048
955
  opts = effectiveOpts;
1049
956
  const lines = [];
1050
957
  lines.push(`Configured runtime: ${runtime}`);
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
- );
958
+ lines.push("Wake mode: webhook sink (daemon-consumed). NO MCP-config file, NO hooks written.");
1055
959
  lines.push("");
1056
960
  if (!suppliedUrl) {
1057
961
  lines.push(
@@ -1094,7 +998,7 @@ function configureHermes(opts) {
1094
998
  return { runtime, output: lines.join("\n"), exitCode: install.exitCode };
1095
999
  }
1096
1000
  recordRuntime(runtime);
1097
- const envFile = path8.join(home, ".kojee", "hermes.env");
1001
+ const envFile = path7.join(home, ".kojee", "hermes.env");
1098
1002
  lines.push(...buildDaemonEnvBlock(runtime, wh, envFile));
1099
1003
  lines.push("");
1100
1004
  lines.push(install.output);
@@ -1116,7 +1020,7 @@ function configureHermes(opts) {
1116
1020
  return { runtime, output: lines.join("\n"), exitCode: 0 };
1117
1021
  }
1118
1022
  function openclawConfigPath(opts) {
1119
- return opts.openclawConfigPath ?? path8.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
1023
+ return opts.openclawConfigPath ?? path7.join(kojeeHomeDir(), ".openclaw", "openclaw.json");
1120
1024
  }
1121
1025
  function configureOpenclaw(opts) {
1122
1026
  const runtime = "openclaw";
@@ -1158,15 +1062,11 @@ async function runWizardUninstall(runtime, opts) {
1158
1062
  const un = uninstallOpenclaw({ openclawConfigPath: openclawConfigPath(opts) });
1159
1063
  lines.push(un.output);
1160
1064
  } else {
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`);
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`);
1168
1068
  try {
1169
- fs7.unlinkSync(envPath);
1069
+ fs6.unlinkSync(envPath);
1170
1070
  lines.push(` removed ${envPath}`);
1171
1071
  } catch {
1172
1072
  }
@@ -1209,11 +1109,7 @@ registerBuiltinInstaller(
1209
1109
  "webhook",
1210
1110
  "webhook-secret-both-sides",
1211
1111
  "plugin-payload-copy",
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"
1112
+ "service-install"
1217
1113
  ],
1218
1114
  (o) => configureHermes(o)
1219
1115
  );
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildReplyRecipe
3
- } from "./chunk-FJUAMJHU.js";
3
+ } from "./chunk-QLARIAUJ.js";
4
4
 
5
5
  // src/adapters/claude-code.ts
6
6
  function computeSeverity(event) {
@@ -82,6 +82,13 @@ async function startEventStream(opts) {
82
82
  headersTimeout: UNDICI_DEFAULT_BODY_TIMEOUT_MS
83
83
  });
84
84
  const cursors = /* @__PURE__ */ new Map();
85
+ if (opts.initialCursors) {
86
+ for (const [tandemId, cursor] of Object.entries(opts.initialCursors)) {
87
+ if (typeof cursor === "number" && Number.isFinite(cursor)) {
88
+ cursors.set(tandemId, cursor);
89
+ }
90
+ }
91
+ }
85
92
  let currentController = new AbortController();
86
93
  let connectGeneration = 0;
87
94
  const state = {
@@ -273,10 +280,11 @@ async function consumeSse(body, opts, controller, state, onCursor) {
273
280
  console.error("[event-stream] event-log append failed:", err);
274
281
  }
275
282
  }
276
- if (opts.adapter.supportsChannels) {
283
+ const channelServer = opts.server;
284
+ if (opts.adapter.supportsChannels && channelServer) {
277
285
  try {
278
286
  const channel = opts.adapter.formatTandemEvent(parsed);
279
- await opts.server.notification({
287
+ await channelServer.notification({
280
288
  method: "notifications/claude/channel",
281
289
  params: channel
282
290
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readSessionDiscoveryByKey,
3
3
  sessionDiscoveryDir
4
- } from "./chunk-67F67AQ6.js";
4
+ } from "./chunk-3N4YYIOJ.js";
5
5
  import {
6
6
  secureFile
7
7
  } from "./chunk-U5HHHRXA.js";
@@ -0,0 +1,14 @@
1
+ // src/adapters/unknown.ts
2
+ var unknownAdapter = {
3
+ runtime: "unknown",
4
+ supportsChannels: false,
5
+ formatTandemEvent() {
6
+ throw new Error(
7
+ "unknownAdapter.formatTandemEvent() is unreachable \u2014 caller should gate on supportsChannels"
8
+ );
9
+ }
10
+ };
11
+
12
+ export {
13
+ unknownAdapter
14
+ };
@@ -0,0 +1,118 @@
1
+ import {
2
+ createPairedGateway
3
+ } from "./chunk-VGZ4M3FP.js";
4
+ import {
5
+ classifySendFailure,
6
+ ensureJoined,
7
+ sendFailure
8
+ } from "./chunk-WZP7HXXN.js";
9
+ import {
10
+ translateToolCallResult
11
+ } from "./chunk-PPTKGWFF.js";
12
+
13
+ // src/tandem/messages-cli.ts
14
+ import os from "os";
15
+ import path from "path";
16
+ function parseMessagesRequest(input) {
17
+ const tandemId = input.tandem_id;
18
+ if (typeof tandemId !== "string" || tandemId.length === 0) {
19
+ return sendFailure("bad_request", "tandem_id is required and must be a non-empty string");
20
+ }
21
+ const request = { tandem_id: tandemId };
22
+ if (input.since !== void 0) {
23
+ if (typeof input.since !== "string" || input.since.length === 0) {
24
+ return sendFailure("bad_request", "--since must be a non-empty cursor when present");
25
+ }
26
+ request.since = input.since;
27
+ }
28
+ if (input.limit !== void 0) {
29
+ const raw = String(input.limit);
30
+ if (!/^\d+$/.test(raw)) {
31
+ return sendFailure("bad_request", "--limit must be a positive integer");
32
+ }
33
+ const n = Number.parseInt(raw, 10);
34
+ if (!Number.isFinite(n) || n <= 0) {
35
+ return sendFailure("bad_request", "--limit must be a positive integer");
36
+ }
37
+ request.limit = n;
38
+ }
39
+ return { ok: true, request };
40
+ }
41
+ function deriveMaxCursor(messages) {
42
+ let max = null;
43
+ for (const m of messages) {
44
+ if (m !== null && typeof m === "object") {
45
+ const c = m["cursor"];
46
+ if (typeof c === "number" && (max === null || c > max)) max = c;
47
+ }
48
+ }
49
+ return max;
50
+ }
51
+ async function executeRead(gateway, request) {
52
+ const sinceArg = request.since !== void 0 ? /^\d+$/.test(request.since) ? Number(request.since) : request.since : void 0;
53
+ const args = {
54
+ tandem_id: request.tandem_id,
55
+ ...sinceArg !== void 0 ? { since: sinceArg } : {},
56
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
57
+ };
58
+ let result;
59
+ try {
60
+ result = await gateway.sendRpc("tools/call", {
61
+ name: "tandem_messages",
62
+ arguments: args
63
+ });
64
+ } catch (err) {
65
+ return classifySendFailure(
66
+ `tandem_messages failed: ${err?.message ?? String(err)}`
67
+ );
68
+ }
69
+ const translated = translateToolCallResult(result);
70
+ const text = (translated.content ?? []).map((c) => typeof c?.text === "string" ? c.text : "").filter(Boolean).join("\n");
71
+ if (translated.isError) {
72
+ return classifySendFailure(text || "tandem_messages returned an error with no text");
73
+ }
74
+ let messages = [];
75
+ let cursor = null;
76
+ try {
77
+ const parsed = JSON.parse(text);
78
+ if (Array.isArray(parsed)) {
79
+ messages = parsed;
80
+ } else if (parsed !== null && typeof parsed === "object") {
81
+ const obj = parsed;
82
+ if (Array.isArray(obj["messages"])) messages = obj["messages"];
83
+ const rawCursor = obj["cursor"];
84
+ if (typeof rawCursor === "number") cursor = rawCursor;
85
+ }
86
+ if (cursor === null) cursor = deriveMaxCursor(messages);
87
+ } catch {
88
+ }
89
+ return { ok: true, tandem_id: request.tandem_id, messages, cursor, text };
90
+ }
91
+ async function runMessagesCli(args, deps = {}) {
92
+ const parsed = parseMessagesRequest({
93
+ tandem_id: args.tandemId,
94
+ ...args.since !== void 0 ? { since: args.since } : {},
95
+ ...args.limit !== void 0 ? { limit: args.limit } : {}
96
+ });
97
+ if (!parsed.ok) {
98
+ return { exitCode: 1, envelope: parsed };
99
+ }
100
+ const kojeeDir = args.kojeeDir ?? path.join(os.homedir(), ".kojee");
101
+ const createGateway = deps.createGateway ?? createPairedGateway;
102
+ const resolution = await createGateway(kojeeDir);
103
+ if (!resolution.ok) {
104
+ return { exitCode: 1, envelope: resolution };
105
+ }
106
+ const guard = await ensureJoined(resolution.gateway, parsed.request.tandem_id);
107
+ if (!guard.ok) {
108
+ return { exitCode: 1, envelope: guard.failure };
109
+ }
110
+ const envelope = await executeRead(resolution.gateway, parsed.request);
111
+ return { exitCode: envelope.ok ? 0 : 1, envelope };
112
+ }
113
+
114
+ export {
115
+ parseMessagesRequest,
116
+ executeRead,
117
+ runMessagesCli
118
+ };
@@ -6,7 +6,7 @@ import {
6
6
  buildCondensedTandemRules,
7
7
  buildMonitorSpawn,
8
8
  buildReplyRecipe
9
- } from "./chunk-FJUAMJHU.js";
9
+ } from "./chunk-QLARIAUJ.js";
10
10
  import {
11
11
  translateToolCallResult
12
12
  } from "./chunk-PPTKGWFF.js";
@@ -1,10 +1,18 @@
1
1
  // src/tandem/recipe.ts
2
2
  var SEND_BODY_PARAM = "body";
3
+ var KOJEE_PACKAGE = "kojee-mcp";
4
+ var MONITOR_DESCRIPTION = "kojee Tandem events";
5
+ function pinnedPackage(version) {
6
+ return `${KOJEE_PACKAGE}@${version}`;
7
+ }
8
+ function buildMonitorSpawnFor(command) {
9
+ return `Monitor(command=\`${command}\`, persistent=true, description="${MONITOR_DESCRIPTION}")`;
10
+ }
3
11
  function buildMonitorCommand(logPath) {
4
- return `npx kojee-mcp tail "${logPath}"`;
12
+ return `npx ${KOJEE_PACKAGE} tail "${logPath}"`;
5
13
  }
6
14
  function buildMonitorSpawn(logPath) {
7
- return `Monitor(command=\`${buildMonitorCommand(logPath)}\`, persistent=true, description="kojee Tandem events")`;
15
+ return buildMonitorSpawnFor(buildMonitorCommand(logPath));
8
16
  }
9
17
  function buildReplyRecipe(event) {
10
18
  if (event) {
@@ -21,6 +29,18 @@ function buildCondensedTandemRules() {
21
29
  function buildMonitorNudge(logPath) {
22
30
  return `[kojee] Tandem events are being logged but no Monitor is reading them \u2014 you may be missing wake notifications. Spawn the watcher once: ${buildMonitorSpawn(logPath)}. Then ${buildReplyRecipe()}.`;
23
31
  }
32
+ function buildCoworkMonitorSpawn(tandemId, version) {
33
+ return buildMonitorSpawnFor(
34
+ `npx ${pinnedPackage(version)} cowork-stream --tandem ${tandemId}`
35
+ );
36
+ }
37
+ function buildCliReplyRecipe(tandemId, version) {
38
+ return `npx ${pinnedPackage(version)} send ${tandemId} --${SEND_BODY_PARAM} "..." --reply-to <msg>`;
39
+ }
40
+ function buildCliCatchUpNote(tandemId, version, sinceCursor) {
41
+ const since = sinceCursor !== void 0 ? String(sinceCursor) : "<cursor>";
42
+ return `npx ${pinnedPackage(version)} messages ${tandemId} --since ${since}`;
43
+ }
24
44
  var WEBHOOK_BODY_SHAPE = "{ type, id, tandem_id, cursor, time, from{ member_id, principal, agent_id?, session_id?, displayname }, kind, content{ body, format? }, mentions?, reply_to?, severity? }";
25
45
  function buildWebhookReceiverNote(sig) {
26
46
  const header = sig?.header ?? "X-Kojee-Signature";
@@ -32,11 +52,15 @@ var CODEX_LISTEN_CAP_MS = 8e3;
32
52
  var CODEX_WAKE_BELL = "[kojee] Tandem events may be pending. Call tandem_pending now and drain each room it lists (tandem_messages(id, since=cursor)), then reply in the room with a normal message (kind=message \u2014 never status). If it returns none for you, ignore this.";
33
53
 
34
54
  export {
55
+ pinnedPackage,
35
56
  buildMonitorSpawn,
36
57
  buildReplyRecipe,
37
58
  buildCatchUpNote,
38
59
  buildCondensedTandemRules,
39
60
  buildMonitorNudge,
61
+ buildCoworkMonitorSpawn,
62
+ buildCliReplyRecipe,
63
+ buildCliCatchUpNote,
40
64
  buildWebhookReceiverNote,
41
65
  CODEX_LISTEN_CAP_MS,
42
66
  CODEX_WAKE_BELL