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.
- package/README.md +101 -1
- package/dist/{chunk-67F67AQ6.js → chunk-3N4YYIOJ.js} +28 -0
- package/dist/{chunk-WBXU27BF.js → chunk-7J5CSXGS.js} +1 -1
- package/dist/{chunk-QNGMMBOD.js → chunk-BO52SILE.js} +21 -125
- package/dist/{chunk-TCWIXG5C.js → chunk-D5VPUCPY.js} +1 -1
- package/dist/{chunk-CUOJHP2S.js → chunk-GP7XNGUR.js} +10 -2
- package/dist/{chunk-TNZITJAB.js → chunk-HMS4PZLC.js} +1 -1
- package/dist/chunk-JRCHS6CP.js +14 -0
- package/dist/chunk-NMHM6SII.js +118 -0
- package/dist/{chunk-A4IOKD4Z.js → chunk-P2R3KWYK.js} +1 -1
- package/dist/{chunk-FJUAMJHU.js → chunk-QLARIAUJ.js} +26 -2
- package/dist/{chunk-ZSHCHOPL.js → chunk-TXGO2W5I.js} +6 -14
- package/dist/{send-cli-45RLGYIC.js → chunk-VGZ4M3FP.js} +8 -11
- package/dist/{chunk-PKOBR7ZN.js → chunk-WZP7HXXN.js} +9 -0
- package/dist/cli.js +75 -13
- package/dist/{codex-prompt-submit-hook-J5PZEJSK.js → codex-prompt-submit-hook-6QUCHZB2.js} +1 -1
- package/dist/{codex-stop-hook-32W4BIOM.js → codex-stop-hook-5LVFGGFE.js} +1 -1
- package/dist/{connect-handler-D3VQMUIS.js → connect-handler-5NACO67R.js} +2 -2
- package/dist/cowork-E46Z2QLU.js +370 -0
- package/dist/cowork-worker-RSXWOYUQ.js +316 -0
- package/dist/{doctor-WUU5BVPT.js → doctor-DK72HJI3.js} +4 -4
- package/dist/{doctor-codex-PJFWIABF.js → doctor-codex-MECEBXJC.js} +1 -1
- package/dist/{event-log-2NBJEIEP.js → event-log-QLWB22KX.js} +2 -2
- package/dist/{event-stream-ATKYXDBV.js → event-stream-L432EP4U.js} +1 -1
- package/dist/{hook-server-CWBH6PFL.js → hook-server-6EVSFLNW.js} +1 -1
- package/dist/index.js +5 -4
- package/dist/lib.d.ts +37 -1
- package/dist/lib.js +1 -1
- package/dist/messages-cli-KKFCKV55.js +24 -0
- package/dist/{registry-XJ67EO22.js → registry-E2DW73RE.js} +14 -14
- package/dist/send-cli-NPFQWSBM.js +18 -0
- package/dist/{server-DU3LFS32.js → server-D5VAMAQW.js} +2 -2
- package/dist/{session-discovery-NHCZ2XWA.js → session-discovery-4LZNLBPN.js} +3 -1
- package/dist/{setup-handler-5Q47JYPQ.js → setup-handler-ANXNXK6I.js} +2 -2
- package/dist/{stop-hook-N6TX4YQT.js → stop-hook-QZRUUX22.js} +4 -4
- package/dist/{tail-stream-G4WGTXDS.js → tail-stream-EK66YLNF.js} +103 -25
- package/dist/{user-prompt-submit-hook-DTXXFDSD.js → user-prompt-submit-hook-O7KIT3RR.js} +2 -2
- 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
|
|
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
|
};
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
CODEX_LISTEN_CAP_MS,
|
|
50
50
|
buildCondensedTandemRules,
|
|
51
51
|
buildWebhookReceiverNote
|
|
52
|
-
} from "./chunk-
|
|
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
|
|
139
|
-
import
|
|
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
|
|
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 =
|
|
597
|
-
const adapterEnv =
|
|
598
|
-
const pluginDir =
|
|
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 ${
|
|
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
|
|
654
|
-
"
|
|
655
|
-
" - verify
|
|
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
|
|
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 =
|
|
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 ??
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
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
|
);
|
|
@@ -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
|
-
|
|
283
|
+
const channelServer = opts.server;
|
|
284
|
+
if (opts.adapter.supportsChannels && channelServer) {
|
|
277
285
|
try {
|
|
278
286
|
const channel = opts.adapter.formatTandemEvent(parsed);
|
|
279
|
-
await
|
|
287
|
+
await channelServer.notification({
|
|
280
288
|
method: "notifications/claude/channel",
|
|
281
289
|
params: channel
|
|
282
290
|
});
|
|
@@ -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
|
+
};
|
|
@@ -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
|
|
12
|
+
return `npx ${KOJEE_PACKAGE} tail "${logPath}"`;
|
|
5
13
|
}
|
|
6
14
|
function buildMonitorSpawn(logPath) {
|
|
7
|
-
return
|
|
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
|