kojee-mcp 0.5.8 → 0.5.9
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 +189 -50
- package/dist/{chunk-2TUAFAIW.js → chunk-DS26OORG.js} +13 -2
- package/dist/{chunk-UEGQGXPY.js → chunk-MKDMAAMN.js} +13 -3
- package/dist/chunk-PHXO5P25.js +20 -0
- package/dist/{chunk-JWSIR6KV.js → chunk-SCDWPGH3.js} +4 -3
- package/dist/cli.js +6 -6
- package/dist/{doctor-QRATEOFD.js → doctor-XK335W7B.js} +1 -1
- package/dist/{event-log-RSTM4PLL.js → event-log-B27VVEMK.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +1 -1
- package/dist/{stop-hook-SNPOFG4Q.js → stop-hook-GEJF47SN.js} +5 -14
- package/dist/{tail-stream-HDM7BZDZ.js → tail-stream-JNR4WFW3.js} +2 -2
- package/dist/{user-prompt-submit-hook-J7XZSDST.js → user-prompt-submit-hook-DGRRFHOB.js} +4 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# kojee-mcp
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Published version **0.5.8**.
|
|
4
|
+
>
|
|
5
|
+
> **First time using Kojee Tandem in Claude Code?** Three commands get you from
|
|
6
|
+
> zero to a woken agent — see the [Quick start (Claude Code + Tandem)](#quick-start-claude-code--tandem)
|
|
7
|
+
> below, or [docs/getting-started-tandem.md](docs/getting-started-tandem.md) for
|
|
8
|
+
> the full walkthrough.
|
|
4
9
|
|
|
5
|
-
There are
|
|
10
|
+
There are three ways to connect Kojee to an MCP-capable agent:
|
|
6
11
|
|
|
7
|
-
1. **Mobile, web & desktop (recommended)** — 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).
|
|
8
|
-
2. **
|
|
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
|
+
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).
|
|
9
15
|
|
|
10
16
|
## Mobile, Web & Desktop (Recommended)
|
|
11
17
|
|
|
@@ -30,92 +36,156 @@ The OAuth login + consent flow takes care of everything — no token paste, no c
|
|
|
30
36
|
| Discovery | `https://api.kojee.ai/.well-known/oauth-protected-resource` |
|
|
31
37
|
| Scopes | `mcp:tools`, `mcp:read` |
|
|
32
38
|
|
|
33
|
-
##
|
|
39
|
+
## Quick start (Claude Code + Tandem)
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
From zero to a woken agent — the recommended path for Claude Code:
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
```bash
|
|
44
|
+
# 1. Pair this machine + configure your runtime in one guided step.
|
|
45
|
+
# Run it in a terminal (TTY): it walks you through broker URL → auth
|
|
46
|
+
# (paste a token OR enter a pair code) → wake setup. Defaults to claude-code.
|
|
47
|
+
npx -y kojee-mcp@latest init
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"args": ["kojee-mcp", "--token", "YOUR_TOKEN", "--url", "https://kojee.ai"]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
49
|
+
# 2. Confirm the wake path is healthy.
|
|
50
|
+
npx -y kojee-mcp@latest doctor
|
|
51
|
+
|
|
52
|
+
# 3. Start a fresh Claude Code session.
|
|
53
|
+
claude
|
|
48
54
|
```
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
`init` is a **guided wizard** when stdin is a TTY (and no `--runtime` flag is
|
|
57
|
+
given). It prompts for, in order:
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
1. **Runtime** — `claude-code` (default), `hermes`, `openclaw`, or `codex`.
|
|
60
|
+
2. **Broker URL** — Enter accepts the default `https://rosie-staging.kojee.net`.
|
|
61
|
+
3. **Auth** — paste an existing gateway token (**token mode**), or enter a
|
|
62
|
+
**pair code** from the dashboard (**pair mode** — runs the DPoP enrollment and
|
|
63
|
+
writes `~/.kojee/config.json` for you, so you don't run `pair` separately).
|
|
64
|
+
4. **Webhook receiver** (codex/hermes/openclaw only) — Enter to skip and set it up later.
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
For non-interactive / CI use, pass flags instead (see
|
|
67
|
+
[Non-interactive `init`](#non-interactive-init-flags--ci) below). Either way the
|
|
68
|
+
final wake check is `kojee-mcp doctor`.
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
> **Already have a pair code and just want one command?**
|
|
71
|
+
> `npx -y kojee-mcp@latest init --pair-code YOUR-CODE` pairs **and** configures
|
|
72
|
+
> claude-code in a single non-interactive step (URL defaults to
|
|
73
|
+
> `https://rosie-staging.kojee.net`; override with `--url`).
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
npm install -g kojee-mcp
|
|
62
|
-
kojee-mcp --token gw_abc123... --url https://kojee.ai
|
|
63
|
-
```
|
|
75
|
+
### What `init` writes (claude-code)
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
It adds the MCP entry to **every detected Claude installation**:
|
|
66
78
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
- `~/.claude.json` → MCP server entry (terminal `claude` CLI **and** Claude.app).
|
|
80
|
+
- `~/Library/Application Support/Claude/claude_desktop_config.json` (Claude.app on
|
|
81
|
+
macOS — Windows / Linux paths in the spec) if present.
|
|
82
|
+
- `~/.claude/settings.json` → the **Stop + UserPromptSubmit hooks** (terminal CLI
|
|
83
|
+
only; Claude.app agent mode reads MCP servers but not hooks).
|
|
84
|
+
|
|
85
|
+
The canonical MCP entry includes `env.KOJEE_RUNTIME=claude-code` so the proxy
|
|
86
|
+
identifies as Claude Code even when the desktop app strips environment variables
|
|
87
|
+
from MCP subprocesses. In **token mode** the entry launches the proxy with
|
|
88
|
+
`--token`/`--url` (it enrolls its own per-token keystore); in **pair mode** the
|
|
89
|
+
args stay `["kojee-mcp"]` and the proxy reads `~/.kojee/config.json` (mode 0600).
|
|
90
|
+
Existing entries (other MCP servers, other hooks like babysitter wrappers) are
|
|
91
|
+
preserved.
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
**Important for Claude.app users:** existing agent-mode sessions snapshot the MCP
|
|
94
|
+
config at creation time and won't pick up changes from `init`. Start a NEW session
|
|
95
|
+
(not a resumed one) to use the updated kojee config.
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
To remove kojee from your runtime (runtime-aware — uses the recorded runtime):
|
|
76
98
|
|
|
77
99
|
```bash
|
|
78
|
-
|
|
79
|
-
npx kojee-mcp pair ABCD-1234 --url https://rosie-server.kojee.net
|
|
80
|
-
npx kojee-mcp init # adds the MCP entry to EVERY detected Claude installation
|
|
100
|
+
npx kojee-mcp init --uninstall
|
|
81
101
|
```
|
|
82
102
|
|
|
83
|
-
|
|
103
|
+
### Verify the wake path — `kojee-mcp doctor`
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
`doctor` is the one-command health check for the whole wake path. It derives the
|
|
106
|
+
same discovery key the hooks use, finds the running proxy, and walks every link
|
|
107
|
+
in order — paired config, proxy process alive, hook-server `/health` + `/status`,
|
|
108
|
+
the **SSE stream** (connected? heartbeat age? subscribed tandem count?), the
|
|
109
|
+
event log, and whether a **Monitor** is reading it — then prints a one-screen
|
|
110
|
+
`HEALTHY` / `DEGRADED` / `BROKEN` verdict **plus the exact wake recipe to spawn**.
|
|
111
|
+
It's runtime-aware (a codex install gets the codex webhook-sink + stop-hook
|
|
112
|
+
checks). Exit code is non-zero only when the verdict is `BROKEN`.
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
```bash
|
|
115
|
+
npx kojee-mcp doctor
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### CLI command reference
|
|
119
|
+
|
|
120
|
+
| Command | What it does |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `kojee-mcp` *(default, no subcommand)* | Run the stdio MCP proxy. Token mode (`--token`/`--url`) or paired mode (reads `~/.kojee/config.json`). |
|
|
123
|
+
| `kojee-mcp init` | Runtime-aware setup wizard (guided in a TTY; flag-driven in CI). |
|
|
124
|
+
| `kojee-mcp pair <code> --url <broker>` | Pair this machine (DPoP enroll + write `~/.kojee/config.json`). |
|
|
125
|
+
| `kojee-mcp doctor` | Diagnose the wake path and print the exact wake recipe. |
|
|
126
|
+
| `kojee-mcp install-hooks` | Install/remove (`--uninstall`) just the Claude Code Stop + UserPromptSubmit hooks. |
|
|
127
|
+
| `kojee-mcp send <tandem_id> --body <text>` | Send a Tandem message from outside the proxy using paired creds (see [Local Send Control Surface](#local-send-control-surface-054)). |
|
|
128
|
+
| `kojee-mcp tail <path>` | Portable line-streamer (`tail -F` replacement) — the Monitor command on every platform. |
|
|
129
|
+
| `kojee-mcp hook --type <stop\|user-prompt-submit\|codex-stop>` | Internal hook entry point (Claude Code / Codex invoke it; you don't run it directly). |
|
|
130
|
+
|
|
131
|
+
## Standalone pairing — `kojee-mcp pair`
|
|
132
|
+
|
|
133
|
+
The guided `init` pairs for you, but you can also pair as a separate step (then
|
|
134
|
+
run `init` to configure your runtime). Use this when scripting, or to re-pair
|
|
135
|
+
after a code expires:
|
|
88
136
|
|
|
89
|
-
To remove kojee from Claude:
|
|
90
137
|
```bash
|
|
91
|
-
npx kojee-mcp
|
|
138
|
+
npx kojee-mcp pair ABCD-1234 --url https://rosie-staging.kojee.net
|
|
92
139
|
```
|
|
93
140
|
|
|
141
|
+
This runs the DPoP enrollment and persists credentials to `~/.kojee/config.json`
|
|
142
|
+
(mode 0600) + the keypair to `~/.kojee/keypair.json`. Subsequent runs of the
|
|
143
|
+
proxy find them automatically. `--keystore-path` overrides the keypair location.
|
|
144
|
+
|
|
94
145
|
## Runtime-aware setup wizard (`init` selects the runtime)
|
|
95
146
|
|
|
96
147
|
`kojee-mcp init` is a runtime-aware wizard. The runtime selector is its core — one
|
|
97
|
-
proxy adapts to four harnesses
|
|
98
|
-
to `claude-code` (unchanged), so
|
|
148
|
+
proxy adapts to four harnesses (`claude-code | hermes | openclaw | codex`). Bare
|
|
149
|
+
`init` with no flag and no TTY still defaults to `claude-code` (unchanged), so
|
|
150
|
+
existing setups have zero regression.
|
|
151
|
+
|
|
152
|
+
### Non-interactive `init` (flags / CI)
|
|
153
|
+
|
|
154
|
+
Passing `--runtime` makes `init` non-interactive (the contract for CI). Auth and
|
|
155
|
+
webhook details come from flags:
|
|
99
156
|
|
|
100
157
|
```bash
|
|
101
|
-
npx kojee-mcp init # interactive
|
|
102
|
-
npx kojee-mcp init --
|
|
158
|
+
npx kojee-mcp init # interactive guided wizard (TTY, no --runtime)
|
|
159
|
+
npx kojee-mcp init --pair-code ABCD-1234 # pair + configure claude-code in one shot
|
|
160
|
+
npx kojee-mcp init --runtime claude-code --token gw_abc... --url https://rosie-staging.kojee.net
|
|
161
|
+
# claude-code, token mode (no pair step)
|
|
103
162
|
npx kojee-mcp init --runtime codex --webhook-url https://your-receiver/kojee
|
|
104
163
|
# Codex: writes ~/.codex/config.toml + a codex-stop hook
|
|
105
|
-
npx kojee-mcp init --runtime hermes
|
|
164
|
+
npx kojee-mcp init --runtime hermes --webhook-url https://your-receiver/kojee
|
|
106
165
|
npx kojee-mcp init --runtime openclaw --webhook-url https://your-receiver/kojee
|
|
107
166
|
# daemon runtimes: print/record the webhook-sink env to export
|
|
108
167
|
```
|
|
109
168
|
|
|
169
|
+
Auth flags (all runtimes): `--token <gw_…>` + `--url <broker>` (token mode), or
|
|
170
|
+
`--pair-code <code>` (runs the pair flow up front, then configures). `--url`
|
|
171
|
+
defaults to `https://rosie-staging.kojee.net`. A non-interactive `init` with **no**
|
|
172
|
+
credential at all (no paired config, no `--token`/`--pair-code`) errors and tells
|
|
173
|
+
you to pair first.
|
|
174
|
+
|
|
175
|
+
Per-runtime config-path / hooks-path overrides: `--config-path`, `--hooks-path`.
|
|
176
|
+
|
|
110
177
|
- **codex** — writes `[mcp_servers.kojee]` (with `env.KOJEE_RUNTIME="codex"` + the
|
|
111
178
|
webhook env) into `~/.codex/config.toml`, and a `codex-stop` Stop hook into
|
|
112
179
|
`~/.codex/hooks.json`. A `KOJEE_WEBHOOK_SECRET` is generated if you don't supply
|
|
113
180
|
one. Codex has no Claude-style channel injection, so its wake is the **webhook
|
|
114
181
|
sink + a stop-hook fast PEEK + a model-chosen bounded `tandem_listen` (cap 8s,
|
|
115
182
|
never a blanket long-poll)**. See `docs/RUNTIMES.md`.
|
|
116
|
-
- **hermes / openclaw** —
|
|
117
|
-
|
|
118
|
-
|
|
183
|
+
- **hermes / openclaw** — these runtimes have **native channel plugins** (see
|
|
184
|
+
[Native gateway runtimes](#native-gateway-runtimes-openclaw--hermes)). When you
|
|
185
|
+
instead run the proxy as a **daemon** feeding a webhook receiver, `init --runtime
|
|
186
|
+
hermes|openclaw` writes no MCP-config/hooks of ours, validates the webhook env,
|
|
187
|
+
and prints + records the env to export (secret redacted) into a source-able
|
|
188
|
+
`~/.kojee/<runtime>.env`.
|
|
119
189
|
|
|
120
190
|
`kojee-mcp init --uninstall` is runtime-aware (uses the recorded runtime when
|
|
121
191
|
`--runtime` is omitted). `kojee-mcp doctor` is runtime-aware too.
|
|
@@ -124,6 +194,75 @@ npx kojee-mcp init --runtime openclaw --webhook-url https://your-receiver/kojee
|
|
|
124
194
|
> adapter + wizard are unit/integration-tested only. Live-Codex end-to-end is
|
|
125
195
|
> unverified — the wizard prints this note after a `--runtime codex` install.
|
|
126
196
|
|
|
197
|
+
## Direct proxy invocation (advanced / generic MCP clients)
|
|
198
|
+
|
|
199
|
+
Most users let `init` write the launch command. If you manage the MCP config by
|
|
200
|
+
hand, the proxy runs in **token mode** with `--token` + `--url`:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"mcpServers": {
|
|
205
|
+
"kojee": {
|
|
206
|
+
"command": "npx",
|
|
207
|
+
"args": ["kojee-mcp", "--token", "YOUR_TOKEN", "--url", "https://rosie-staging.kojee.net"]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Any stdio-capable MCP client works the same way:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx kojee-mcp --token gw_abc123... --url https://rosie-staging.kojee.net
|
|
217
|
+
# or install globally:
|
|
218
|
+
npm install -g kojee-mcp
|
|
219
|
+
kojee-mcp --token gw_abc123... --url https://rosie-staging.kojee.net
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Run bare (no `--token`) and the proxy uses **paired** credentials from
|
|
223
|
+
`~/.kojee/config.json` — that's what the claude-code MCP entry does in pair mode.
|
|
224
|
+
|
|
225
|
+
### Proxy CLI flags (default command)
|
|
226
|
+
|
|
227
|
+
| Flag | Required | Default | Description |
|
|
228
|
+
|------|----------|---------|-------------|
|
|
229
|
+
| `--token` | token mode only | — | Your Kojee gateway token (starts with `gw_`). Omit to use paired credentials. |
|
|
230
|
+
| `--url` | with `--token` | paired `broker_url` | Kojee broker URL (e.g. `https://rosie-staging.kojee.net`). |
|
|
231
|
+
| `--keystore-path` | no | per-token path under `~/.kojee/` | Path for DPoP keypair storage. |
|
|
232
|
+
|
|
233
|
+
## How each runtime reaches Tandem
|
|
234
|
+
|
|
235
|
+
| Runtime | How it connects | Wake path |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| **claude-code** | MCP stdio proxy (this package), wired by `init` | CC Channels (preview) · **Monitor** (`tail` of the event log) · **Stop/UserPromptSubmit hooks** — see below |
|
|
238
|
+
| **cursor** & other MCP clients | MCP stdio proxy | poll/`tandem_listen`; no CC channel injection |
|
|
239
|
+
| **codex** | MCP stdio proxy + `~/.codex` config/hook | webhook sink + stop-hook peek + bounded `tandem_listen` (cap 8s) |
|
|
240
|
+
| **openclaw** | **native channel plugin** (not MCP) | OpenClaw's own routing/sessions wake the agent on each Tandem event |
|
|
241
|
+
| **hermes** | **native channel plugin** (sidecar daemon + webhook) | Hermes gateway wakes the agent like a Telegram DM |
|
|
242
|
+
|
|
243
|
+
## Native gateway runtimes (OpenClaw + Hermes)
|
|
244
|
+
|
|
245
|
+
On OpenClaw and Hermes, Tandem is a **native channel** — peer to Telegram/Discord
|
|
246
|
+
— not an MCP server. A plugin makes the agent *present* in its tandems whenever
|
|
247
|
+
the gateway is up: it wakes on Tandem messages through the runtime's own
|
|
248
|
+
routing/sessions and replies on the same channel. Both plugins wrap the **same**
|
|
249
|
+
gateway client + DPoP auth + SSE event stream this proxy uses (`kojee-mcp/lib`);
|
|
250
|
+
nothing is forked.
|
|
251
|
+
|
|
252
|
+
- **OpenClaw** — `integrations/openclaw-plugin/` (`openclaw-channel-kojee-tandem`).
|
|
253
|
+
Imports `kojee-mcp/lib` directly; one SSE subscription covers all joined
|
|
254
|
+
tandems; outbound `sendText` → `tandem_send`. Shares the daemon's keystore so
|
|
255
|
+
pairing once works for both. See its [README](integrations/openclaw-plugin/README.md).
|
|
256
|
+
- **Hermes** — `integrations/hermes-plugin/` (sidecar design v1). The kojee-mcp
|
|
257
|
+
proxy runs as a **daemon** and POSTs signed webhooks to a loopback adapter
|
|
258
|
+
listener; the adapter turns each event into a Hermes message. Configure the
|
|
259
|
+
daemon side with `kojee-mcp init --runtime hermes --webhook-url …`. See its
|
|
260
|
+
[README](integrations/hermes-plugin/README.md).
|
|
261
|
+
|
|
262
|
+
Pair once (`kojee-mcp pair`, or any kojee-mcp-attached session), and the agent
|
|
263
|
+
must be a member of the tandem (`tandem_join` once, or be invited); after that,
|
|
264
|
+
presence is zero-config — gateway up ⇒ agent reachable in the tandem.
|
|
265
|
+
|
|
127
266
|
## Claude Code Channels Support
|
|
128
267
|
|
|
129
268
|
When this proxy detects it's running under Claude Code, it declares the `claude/channel` capability and pushes Tandem messages directly into the Claude session via `notifications/claude/channel`. Other MCP clients (Cursor, Codex, Openclaw, etc.) see no behavior change.
|
|
@@ -117,11 +117,22 @@ function startEventLog(opts) {
|
|
|
117
117
|
function formatStatusLine(fields) {
|
|
118
118
|
return `[${(/* @__PURE__ */ new Date()).toISOString()}] ${STATUS_LINE_PREFIX} ${fields}`;
|
|
119
119
|
}
|
|
120
|
+
function oneLineField(value) {
|
|
121
|
+
return String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
122
|
+
}
|
|
120
123
|
function formatLine(event) {
|
|
121
124
|
const body = (event.content?.body ?? "").replace(/[\r\n]+/g, " ").slice(0, MAX_BODY_CHARS + 1);
|
|
122
125
|
const truncated = body.length > MAX_BODY_CHARS;
|
|
123
|
-
const safeBody = truncated ? body.slice(0, MAX_BODY_CHARS) + "\u2026" : body;
|
|
124
|
-
|
|
126
|
+
const safeBody = oneLineField(truncated ? body.slice(0, MAX_BODY_CHARS) + "\u2026" : body);
|
|
127
|
+
const time = oneLineField(event.time);
|
|
128
|
+
const tandemId = oneLineField(event.tandem_id);
|
|
129
|
+
const displayname = oneLineField(event.from.displayname);
|
|
130
|
+
const principal = oneLineField(event.from.principal);
|
|
131
|
+
const kind = oneLineField(event.kind);
|
|
132
|
+
const cursor = oneLineField(event.cursor);
|
|
133
|
+
const id = oneLineField(event.id);
|
|
134
|
+
const wakeReason = event.wake_reason ? ` wake_reason=${oneLineField(event.wake_reason)}` : "";
|
|
135
|
+
return `[${time}] tandem=${tandemId} from=${displayname} (${principal}) kind=${kind}${wakeReason} cursor=${cursor} msg=${id}: ${safeBody}`;
|
|
125
136
|
}
|
|
126
137
|
function monitorHeartbeatPath(eventLogPath) {
|
|
127
138
|
const dir = path.dirname(eventLogPath);
|
|
@@ -231,6 +231,7 @@ async function consumeSse(body, opts, controller, state, onCursor) {
|
|
|
231
231
|
const raw = JSON.parse(evt.data);
|
|
232
232
|
const parsed = normalizeBackendEvent(raw, evt.event);
|
|
233
233
|
onCursor(parsed.tandem_id, parsed.cursor);
|
|
234
|
+
if (parsed.wake === false) continue;
|
|
234
235
|
opts.queue?.push(parsed);
|
|
235
236
|
if (opts.eventLog) {
|
|
236
237
|
try {
|
|
@@ -340,8 +341,13 @@ function resolveDisplayname(rawDisplay, principal) {
|
|
|
340
341
|
}
|
|
341
342
|
function normalizeBackendEvent(raw, sseEventType) {
|
|
342
343
|
const obj = raw ?? {};
|
|
344
|
+
const rawWake = obj["wake"];
|
|
345
|
+
const wake = typeof rawWake === "boolean" ? rawWake : void 0;
|
|
346
|
+
const rawWakeReason = obj["wake_reason"];
|
|
347
|
+
const wakeReason = typeof rawWakeReason === "string" && rawWakeReason.trim() ? sanitizeDisplayname(rawWakeReason) : void 0;
|
|
348
|
+
const senderPresent = typeof obj === "object" && obj !== null && "sender" in obj;
|
|
343
349
|
const maybeFrom = obj["from"];
|
|
344
|
-
if (maybeFrom && typeof maybeFrom["principal"] === "string") {
|
|
350
|
+
if (!senderPresent && maybeFrom && typeof maybeFrom["principal"] === "string") {
|
|
345
351
|
const canonical = raw;
|
|
346
352
|
const canonicalPrincipal = sanitizeDisplayname(maybeFrom["principal"]);
|
|
347
353
|
return {
|
|
@@ -350,7 +356,9 @@ function normalizeBackendEvent(raw, sseEventType) {
|
|
|
350
356
|
...canonical.from,
|
|
351
357
|
principal: canonicalPrincipal,
|
|
352
358
|
displayname: resolveDisplayname(maybeFrom["displayname"], canonicalPrincipal)
|
|
353
|
-
}
|
|
359
|
+
},
|
|
360
|
+
...wake !== void 0 ? { wake } : {},
|
|
361
|
+
...wakeReason !== void 0 ? { wake_reason: wakeReason } : {}
|
|
354
362
|
};
|
|
355
363
|
}
|
|
356
364
|
const sender = obj["sender"] ?? {};
|
|
@@ -383,7 +391,9 @@ function normalizeBackendEvent(raw, sseEventType) {
|
|
|
383
391
|
},
|
|
384
392
|
...Array.isArray(obj["mentions"]) ? { mentions: obj["mentions"] } : {},
|
|
385
393
|
...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {},
|
|
386
|
-
...severity ? { severity } : {}
|
|
394
|
+
...severity ? { severity } : {},
|
|
395
|
+
...wake !== void 0 ? { wake } : {},
|
|
396
|
+
...wakeReason !== void 0 ? { wake_reason: wakeReason } : {}
|
|
387
397
|
};
|
|
388
398
|
}
|
|
389
399
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/hooks/format-channel.ts
|
|
2
|
+
function sanitizeChannelAttr(value) {
|
|
3
|
+
return String(value ?? "").replace(/["<>\u0000-\u001f\u007f]/g, "");
|
|
4
|
+
}
|
|
5
|
+
function formatChannelEvents(events) {
|
|
6
|
+
const header = `[${events.length} unread Tandem ${events.length === 1 ? "event" : "events"}]
|
|
7
|
+
|
|
8
|
+
`;
|
|
9
|
+
const bodies = events.map((evt) => {
|
|
10
|
+
const attrs = Object.entries(evt.meta).map(([k, v]) => `${k}="${sanitizeChannelAttr(v)}"`).join(" ");
|
|
11
|
+
return `<channel source="kojee-mcp" ${attrs}>
|
|
12
|
+
${evt.content}
|
|
13
|
+
</channel>`;
|
|
14
|
+
});
|
|
15
|
+
return header + bodies.join("\n\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
formatChannelEvents
|
|
20
|
+
};
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./chunk-HSR3GXCL.js";
|
|
16
16
|
import {
|
|
17
17
|
startEventStream
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MKDMAAMN.js";
|
|
19
19
|
import {
|
|
20
20
|
translateToolCallResult
|
|
21
21
|
} from "./chunk-LDZXU3DW.js";
|
|
@@ -245,6 +245,7 @@ var claudeCodeAdapter = {
|
|
|
245
245
|
from_display: event.from.displayname,
|
|
246
246
|
severity: computeSeverity(event)
|
|
247
247
|
};
|
|
248
|
+
if (event.wake_reason) meta.wake_reason = event.wake_reason;
|
|
248
249
|
return { content: formatBody(event), meta };
|
|
249
250
|
}
|
|
250
251
|
};
|
|
@@ -377,7 +378,7 @@ async function startProxy(config) {
|
|
|
377
378
|
cleanupDiscoveryByKey,
|
|
378
379
|
sweepStaleDiscovery
|
|
379
380
|
} = await import("./session-discovery-FNMJGFPM.js");
|
|
380
|
-
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-
|
|
381
|
+
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
|
|
381
382
|
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
382
383
|
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
383
384
|
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
@@ -514,7 +515,7 @@ async function startProxy(config) {
|
|
|
514
515
|
activeStreamHandle = streamHandle;
|
|
515
516
|
joinReconnect.notifyReady();
|
|
516
517
|
} else if (needsWebhookEventStream()) {
|
|
517
|
-
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-
|
|
518
|
+
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-B27VVEMK.js");
|
|
518
519
|
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
519
520
|
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
520
521
|
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
VERSION,
|
|
7
7
|
startProxy
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SCDWPGH3.js";
|
|
9
9
|
import "./chunk-X672ZN7V.js";
|
|
10
10
|
import "./chunk-BJMASMKX.js";
|
|
11
11
|
import {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./chunk-YH27B6SW.js";
|
|
14
14
|
import "./chunk-JXMVZEQ7.js";
|
|
15
15
|
import "./chunk-HSR3GXCL.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-MKDMAAMN.js";
|
|
17
17
|
import "./chunk-2MIISF2W.js";
|
|
18
18
|
import {
|
|
19
19
|
defaultPairedKeystorePath,
|
|
@@ -42,11 +42,11 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
|
|
|
42
42
|
});
|
|
43
43
|
program.command("hook").description("Run a kojee MCP hook script (called by Claude Code via ~/.claude/settings.json)").requiredOption("--type <type>", "Hook type: stop, user-prompt-submit, or codex-stop").action(async (opts) => {
|
|
44
44
|
if (opts.type === "stop") {
|
|
45
|
-
const { runStopHook } = await import("./stop-hook-
|
|
45
|
+
const { runStopHook } = await import("./stop-hook-GEJF47SN.js");
|
|
46
46
|
await runStopHook();
|
|
47
47
|
process.exit(0);
|
|
48
48
|
} else if (opts.type === "user-prompt-submit") {
|
|
49
|
-
const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-
|
|
49
|
+
const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-DGRRFHOB.js");
|
|
50
50
|
await runUserPromptSubmitHook();
|
|
51
51
|
process.exit(0);
|
|
52
52
|
} else if (opts.type === "codex-stop") {
|
|
@@ -87,7 +87,7 @@ program.command("send <tandem_id>").description(
|
|
|
87
87
|
process.exit(exitCode);
|
|
88
88
|
});
|
|
89
89
|
program.command("tail <path>").description("Stream a file's contents and follow appends (portable replacement for `tail -F`)").action(async (filePath) => {
|
|
90
|
-
const { runTail } = await import("./tail-stream-
|
|
90
|
+
const { runTail } = await import("./tail-stream-JNR4WFW3.js");
|
|
91
91
|
try {
|
|
92
92
|
await runTail(filePath);
|
|
93
93
|
} catch (err) {
|
|
@@ -96,7 +96,7 @@ program.command("tail <path>").description("Stream a file's contents and follow
|
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
program.command("doctor").description("Diagnose the kojee wake path (proxy, hook-server, SSE stream, event log, Monitor) and print the exact wake recipe").action(async () => {
|
|
99
|
-
const { runDoctor } = await import("./doctor-
|
|
99
|
+
const { runDoctor } = await import("./doctor-XK335W7B.js");
|
|
100
100
|
const code = await runDoctor();
|
|
101
101
|
process.exit(code);
|
|
102
102
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listTandemIds,
|
|
3
3
|
startProxy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SCDWPGH3.js";
|
|
5
5
|
import "./chunk-X672ZN7V.js";
|
|
6
6
|
import "./chunk-BJMASMKX.js";
|
|
7
7
|
import "./chunk-JXMVZEQ7.js";
|
|
8
8
|
import "./chunk-HSR3GXCL.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-MKDMAAMN.js";
|
|
10
10
|
import "./chunk-2MIISF2W.js";
|
|
11
11
|
import "./chunk-CH32ELFX.js";
|
|
12
12
|
import "./chunk-BLEGIR35.js";
|
package/dist/lib.d.ts
CHANGED
package/dist/lib.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatChannelEvents
|
|
3
|
+
} from "./chunk-PHXO5P25.js";
|
|
1
4
|
import {
|
|
2
5
|
readHookStdin
|
|
3
6
|
} from "./chunk-LSUB6QMP.js";
|
|
4
7
|
import {
|
|
5
8
|
monitorHeartbeatPath,
|
|
6
9
|
nudgeSentinelPath
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-DS26OORG.js";
|
|
8
11
|
import {
|
|
9
12
|
readSessionDiscoveryByKey
|
|
10
13
|
} from "./chunk-DO42NPNR.js";
|
|
@@ -37,7 +40,7 @@ async function decideStopHook(deps) {
|
|
|
37
40
|
if (!deps.discovery) return "{}";
|
|
38
41
|
const body = await deps.pollEvents().catch(() => null);
|
|
39
42
|
if (body && body.count > 0) {
|
|
40
|
-
return JSON.stringify({ decision: "block", reason:
|
|
43
|
+
return JSON.stringify({ decision: "block", reason: formatChannelEvents(body.events) });
|
|
41
44
|
}
|
|
42
45
|
const logPath = deps.discovery.eventLogPath;
|
|
43
46
|
if (logPath && deps.logHasContent(logPath) && !deps.monitorIsLive(logPath)) {
|
|
@@ -109,18 +112,6 @@ function monitorIsLive(logPath) {
|
|
|
109
112
|
return false;
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
|
-
function formatEvents(events) {
|
|
113
|
-
const header = `[${events.length} unread Tandem ${events.length === 1 ? "event" : "events"}]
|
|
114
|
-
|
|
115
|
-
`;
|
|
116
|
-
const bodies = events.map((evt) => {
|
|
117
|
-
const attrs = Object.entries(evt.meta).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
118
|
-
return `<channel source="kojee-mcp" ${attrs}>
|
|
119
|
-
${evt.content}
|
|
120
|
-
</channel>`;
|
|
121
|
-
});
|
|
122
|
-
return header + bodies.join("\n\n");
|
|
123
|
-
}
|
|
124
115
|
export {
|
|
125
116
|
decideStopHook,
|
|
126
117
|
runStopHook
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
STATUS_LINE_PREFIX,
|
|
3
3
|
monitorHeartbeatPath,
|
|
4
4
|
statusLogPath
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-DS26OORG.js";
|
|
6
6
|
import "./chunk-DO42NPNR.js";
|
|
7
7
|
import {
|
|
8
8
|
createAdaptiveWatchdog
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MKDMAAMN.js";
|
|
10
10
|
import "./chunk-2MIISF2W.js";
|
|
11
11
|
import "./chunk-BLEGIR35.js";
|
|
12
12
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatChannelEvents
|
|
3
|
+
} from "./chunk-PHXO5P25.js";
|
|
1
4
|
import {
|
|
2
5
|
readHookStdin
|
|
3
6
|
} from "./chunk-LSUB6QMP.js";
|
|
@@ -42,19 +45,7 @@ async function runUserPromptSubmitHook() {
|
|
|
42
45
|
process.stdout.write("{}");
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
|
-
process.stdout.write(JSON.stringify({ additionalContext:
|
|
46
|
-
}
|
|
47
|
-
function formatEvents(events) {
|
|
48
|
-
const header = `[${events.length} unread Tandem ${events.length === 1 ? "event" : "events"}]
|
|
49
|
-
|
|
50
|
-
`;
|
|
51
|
-
const bodies = events.map((evt) => {
|
|
52
|
-
const attrs = Object.entries(evt.meta).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
53
|
-
return `<channel source="kojee-mcp" ${attrs}>
|
|
54
|
-
${evt.content}
|
|
55
|
-
</channel>`;
|
|
56
|
-
});
|
|
57
|
-
return header + bodies.join("\n\n");
|
|
48
|
+
process.stdout.write(JSON.stringify({ additionalContext: formatChannelEvents(body.events) }));
|
|
58
49
|
}
|
|
59
50
|
export {
|
|
60
51
|
runUserPromptSubmitHook
|