kojee-mcp 0.5.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -4
- package/dist/{chunk-VLZADEFC.js → chunk-2TUAFAIW.js} +6 -9
- package/dist/chunk-BLEGIR35.js +43 -0
- package/dist/chunk-C6GZ2L2W.js +38 -0
- package/dist/{chunk-W6YRLSD4.js → chunk-DO42NPNR.js} +9 -16
- package/dist/chunk-EW72ZNQL.js +39 -0
- package/dist/chunk-F7L25L2J.js +60 -0
- package/dist/chunk-LVL25VLO.js +22 -0
- package/dist/chunk-SQL56SEB.js +14 -0
- package/dist/{chunk-QB22PD6T.js → chunk-WBMX4CHB.js} +29 -9
- package/dist/{chunk-LCFCCWMM.js → chunk-YEC7IHIG.js} +136 -78
- package/dist/{chunk-GBOTBYEP.js → chunk-YH27B6SW.js} +7 -8
- package/dist/chunk-ZW4SW7LJ.js +225 -0
- package/dist/cli.js +54 -80
- package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
- package/dist/{doctor-GILTOH2R.js → doctor-TSHOMT5X.js} +29 -14
- package/dist/doctor-codex-BMI5JOO6.js +130 -0
- package/dist/{event-log-R6VW6GAF.js → event-log-RSTM4PLL.js} +3 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -3
- package/dist/{install-D2HIPOMT.js → install-WBIUVBZW.js} +5 -4
- package/dist/{paired-config-RB4SABOS.js → paired-config-JTFLHMZ2.js} +2 -1
- package/dist/runtime-record-WO4IECM6.js +14 -0
- package/dist/runtimes-CO43XUUK.js +12 -0
- package/dist/{session-discovery-QE5TTAPS.js → session-discovery-FNMJGFPM.js} +2 -1
- package/dist/{stop-hook-VLQS6QPR.js → stop-hook-SEPWWETV.js} +6 -5
- package/dist/{tail-stream-UZ42UIWO.js → tail-stream-BYKO4DW6.js} +4 -3
- package/dist/{user-prompt-submit-hook-C42DPDBO.js → user-prompt-submit-hook-ARPEO6FF.js} +2 -1
- package/dist/webhook-config-5TLLX7RA.js +10 -0
- package/dist/webhook-sink-7OYZBWXA.js +163 -0
- package/dist/wizard-7KHD5JT4.js +265 -0
- package/package.json +1 -1
- package/dist/chunk-E26AHU6J.js +0 -27
package/README.md
CHANGED
|
@@ -91,14 +91,47 @@ To remove kojee from Claude:
|
|
|
91
91
|
npx kojee-mcp init --uninstall
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## Runtime-aware setup wizard (`init` selects the runtime)
|
|
95
|
+
|
|
96
|
+
`kojee-mcp init` is a runtime-aware wizard. The runtime selector is its core — one
|
|
97
|
+
proxy adapts to four harnesses. Bare `init` with no flag and no TTY still defaults
|
|
98
|
+
to `claude-code` (unchanged), so existing setups have zero regression.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx kojee-mcp init # interactive (prompts when stdin is a TTY)
|
|
102
|
+
npx kojee-mcp init --runtime claude-code # Claude Code (default): MCP entry + Stop/UserPromptSubmit hooks
|
|
103
|
+
npx kojee-mcp init --runtime codex --webhook-url https://your-receiver/kojee
|
|
104
|
+
# Codex: writes ~/.codex/config.toml + a codex-stop hook
|
|
105
|
+
npx kojee-mcp init --runtime hermes --webhook-url https://your-receiver/kojee
|
|
106
|
+
npx kojee-mcp init --runtime openclaw --webhook-url https://your-receiver/kojee
|
|
107
|
+
# daemon runtimes: print/record the webhook-sink env to export
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **codex** — writes `[mcp_servers.kojee]` (with `env.KOJEE_RUNTIME="codex"` + the
|
|
111
|
+
webhook env) into `~/.codex/config.toml`, and a `codex-stop` Stop hook into
|
|
112
|
+
`~/.codex/hooks.json`. A `KOJEE_WEBHOOK_SECRET` is generated if you don't supply
|
|
113
|
+
one. Codex has no Claude-style channel injection, so its wake is the **webhook
|
|
114
|
+
sink + a stop-hook fast PEEK + a model-chosen bounded `tandem_listen` (cap 8s,
|
|
115
|
+
never a blanket long-poll)**. See `docs/RUNTIMES.md`.
|
|
116
|
+
- **hermes / openclaw** — run the proxy as a daemon and consume the webhook sink;
|
|
117
|
+
the wizard writes no config/hooks of ours, validates the webhook env, and prints
|
|
118
|
+
+ records the env to export (secret redacted).
|
|
119
|
+
|
|
120
|
+
`kojee-mcp init --uninstall` is runtime-aware (uses the recorded runtime when
|
|
121
|
+
`--runtime` is omitted). `kojee-mcp doctor` is runtime-aware too.
|
|
122
|
+
|
|
123
|
+
> **Codex is build+PARK only:** there is no real Codex CLI on the build box, so the
|
|
124
|
+
> adapter + wizard are unit/integration-tested only. Live-Codex end-to-end is
|
|
125
|
+
> unverified — the wizard prints this note after a `--runtime codex` install.
|
|
126
|
+
|
|
94
127
|
## Claude Code Channels Support
|
|
95
128
|
|
|
96
129
|
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.
|
|
97
130
|
|
|
98
131
|
**Runtime detection (3 tiers, first match wins):**
|
|
99
|
-
1. `KOJEE_RUNTIME
|
|
100
|
-
2. `CLAUDE_CODE_SESSION_ID` env var (set by the terminal `claude` CLI; **not** forwarded by Claude.app to MCP stdio servers)
|
|
101
|
-
3. Process-ancestry walk for a parent process whose command line matches `\bclaude\b` (
|
|
132
|
+
1. `KOJEE_RUNTIME` env var — `claude-code` → CC, `codex` → Codex (set by `kojee-mcp init` in the MCP entry's `env` block — survives Claude.app's env strip). This is the **primary** path for Codex.
|
|
133
|
+
2. `CLAUDE_CODE_SESSION_ID` env var (set by the terminal `claude` CLI; **not** forwarded by Claude.app to MCP stdio servers). CC only — Codex has no documented stable equivalent, so there is no Tier-2 Codex path.
|
|
134
|
+
3. Process-ancestry walk for a parent process whose command line matches `\bclaude\b` (→ CC) or `\bcodex\b` (→ Codex) — low-confidence fallback for fresh installs where no env var is present.
|
|
102
135
|
|
|
103
136
|
CC Channels are in research preview — you must launch CC with `claude --dangerously-load-development-channels server:kojee` until kojee is on the official allowlist.
|
|
104
137
|
|
|
@@ -174,6 +207,79 @@ The proxy normalizes incoming SSE events from `/api/v2/tandems/stream` so that t
|
|
|
174
207
|
|
|
175
208
|
The normalizer also accepts canonical (spec-aligned) payloads unchanged, so the proxy works against either shape during any future backend migration.
|
|
176
209
|
|
|
210
|
+
## Webhook Sink (daemonized wake)
|
|
211
|
+
|
|
212
|
+
For runtimes that run the proxy as a daemon and inject Tandem events into a
|
|
213
|
+
session via a **local HTTP receiver** (e.g. Hermes), the proxy can POST every
|
|
214
|
+
Tandem event to a configured endpoint. It is a generic delivery sink — nothing
|
|
215
|
+
in it is Hermes-specific — and it is **OFF by default**.
|
|
216
|
+
|
|
217
|
+
| Env var | Meaning |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `KOJEE_WEBHOOK_URL` | Receiver endpoint (http/https). **Unset ⇒ sink OFF** (zero behavior change). |
|
|
220
|
+
| `KOJEE_WEBHOOK_SECRET` | HMAC-SHA256 key for the signature header. URL set but secret unset ⇒ sink **DISABLED with an error** (the proxy NEVER sends unsigned webhooks). |
|
|
221
|
+
| `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `5000`). |
|
|
222
|
+
| `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a retryable failure — network / 5xx / 408 / 429 (default `4`). |
|
|
223
|
+
| `KOJEE_WEBHOOK_SIGNATURE_HEADER` | Header name carrying the signature (default `X-Kojee-Signature`). |
|
|
224
|
+
| `KOJEE_WEBHOOK_SIGNATURE_PREFIX` | Literal string prepended to the hex digest (default empty — bare hex). |
|
|
225
|
+
| `KOJEE_WEBHOOK_SIGNATURE_FORMAT` | Optional preset. `github` ⇒ header `X-Hub-Signature-256`, prefix `sha256=` (the GitHub-webhook convention). Explicit `_HEADER`/`_PREFIX` vars override the preset's corresponding value. Unknown values are **warned about once and ignored** — never fatal. |
|
|
226
|
+
|
|
227
|
+
**Signature emission is configurable (0.5.3)** — the HMAC *computation* never
|
|
228
|
+
changes (hex SHA-256 HMAC of the raw body bytes, keyed by
|
|
229
|
+
`KOJEE_WEBHOOK_SECRET`); only the header name and an optional digest prefix do.
|
|
230
|
+
Defaults are byte-identical to 0.5.2. For a GitHub-convention receiver (Hermes
|
|
231
|
+
and most off-the-shelf webhook verifiers):
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
export KOJEE_WEBHOOK_SIGNATURE_FORMAT=github
|
|
235
|
+
# Each POST then carries:
|
|
236
|
+
# X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body, keyed by KOJEE_WEBHOOK_SECRET>
|
|
237
|
+
# which verifies with any GitHub-style check, e.g.:
|
|
238
|
+
# expected = "sha256=" + HMAC_SHA256_hex(secret, raw_body)
|
|
239
|
+
# timing_safe_equal(header, expected)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Or set the pieces individually (these override the preset):
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export KOJEE_WEBHOOK_SIGNATURE_HEADER="X-Hub-Signature-256"
|
|
246
|
+
export KOJEE_WEBHOOK_SIGNATURE_PREFIX="sha256="
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The wizard can persist this non-interactively, e.g.
|
|
250
|
+
`kojee-mcp init --runtime hermes --webhook-url https://… --webhook-signature-format github`
|
|
251
|
+
(also `--webhook-signature-header` / `--webhook-signature-prefix`).
|
|
252
|
+
|
|
253
|
+
**Receiver contract** (the single source of truth is `buildWebhookReceiverNote()`
|
|
254
|
+
in `src/tandem/recipe.ts`, and the exact body shape is the `WEBHOOK_BODY_SHAPE`
|
|
255
|
+
constant beside it):
|
|
256
|
+
|
|
257
|
+
- **Body** — each POST carries the **canonical normalized `TandemEvent`** as JSON
|
|
258
|
+
(the real field names a receiver sees, *not* the backend wire shape):
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
{ type, id, tandem_id, cursor, time,
|
|
262
|
+
from{ member_id, principal, agent_id?, session_id?, displayname },
|
|
263
|
+
kind, content{ body, format? }, mentions?, reply_to?, severity? }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`from.session_id` and `severity` are present only when the wire carried them.
|
|
267
|
+
There is no `sender` object — the body is fully normalized and carries
|
|
268
|
+
`from.principal`.
|
|
269
|
+
- **Verify** the `X-Kojee-Signature` header: it is the hex-encoded SHA-256 HMAC
|
|
270
|
+
of the **raw request-body bytes**, keyed by `KOJEE_WEBHOOK_SECRET`. Recompute
|
|
271
|
+
over the received bytes (before any re-serialization) and timing-safe compare;
|
|
272
|
+
reject mismatches.
|
|
273
|
+
- **Dedupe** by `message_id` (the body's `id`, also in the `X-Kojee-Delivery`
|
|
274
|
+
header). Delivery is **at-least-once** — the proxy replays backlog from the
|
|
275
|
+
cursor on restart, so the same event may arrive more than once. There is **no
|
|
276
|
+
exactly-once** promise; the receiver's dedupe is what makes redelivery safe.
|
|
277
|
+
|
|
278
|
+
The sink is isolated and fire-and-forget: a slow, hanging, or failing webhook can
|
|
279
|
+
never delay or break the Monitor (event-log) or Channel wake paths. The status
|
|
280
|
+
log redacts the secret and strips any basic-auth credentials embedded in
|
|
281
|
+
`KOJEE_WEBHOOK_URL`.
|
|
282
|
+
|
|
177
283
|
## Development
|
|
178
284
|
|
|
179
285
|
Run tests:
|
|
@@ -199,7 +305,7 @@ Some tools are governed by approval policies configured in the Kojee dashboard.
|
|
|
199
305
|
3. The proxy translates this into a clear MCP response telling the agent that the action is awaiting human approval.
|
|
200
306
|
4. Once approved (via dashboard, Slack, or other configured channel), the agent can retry the call and it will succeed.
|
|
201
307
|
|
|
202
|
-
|
|
308
|
+
Nonce rotation is handled transparently -- the agent never needs to deal with auth mechanics. Step-up re-authentication is a **deprecated** feature (removed 2026-06-10): the proxy no longer polls for approval. Should a `step_up_required` response ever still occur, it surfaces immediately as a structured tool error carrying the reason, rather than a silent multi-minute wait.
|
|
203
309
|
|
|
204
310
|
## Troubleshooting
|
|
205
311
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sessionDiscoveryDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DO42NPNR.js";
|
|
4
|
+
import {
|
|
5
|
+
secureFile
|
|
6
|
+
} from "./chunk-BLEGIR35.js";
|
|
4
7
|
|
|
5
8
|
// src/tandem/event-log.ts
|
|
6
9
|
import fs from "fs";
|
|
@@ -26,15 +29,9 @@ function startEventLog(opts) {
|
|
|
26
29
|
const statusPath = statusLogPath(filePath);
|
|
27
30
|
fs.mkdirSync(dir, { recursive: true });
|
|
28
31
|
fs.writeFileSync(filePath, "", { mode: 384 });
|
|
29
|
-
|
|
30
|
-
fs.chmodSync(filePath, 384);
|
|
31
|
-
} catch {
|
|
32
|
-
}
|
|
32
|
+
secureFile(filePath);
|
|
33
33
|
fs.writeFileSync(statusPath, "", { mode: 384 });
|
|
34
|
-
|
|
35
|
-
fs.chmodSync(statusPath, 384);
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
34
|
+
secureFile(statusPath);
|
|
38
35
|
let writtenIds = /* @__PURE__ */ new Set();
|
|
39
36
|
let bytesWritten = 0;
|
|
40
37
|
let linesWritten = 0;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/secure-file.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
function isWindows() {
|
|
6
|
+
return process.platform === "win32";
|
|
7
|
+
}
|
|
8
|
+
function secureFile(filePath) {
|
|
9
|
+
if (isWindows()) {
|
|
10
|
+
restrictAclToCurrentUser(filePath, "(F)");
|
|
11
|
+
} else {
|
|
12
|
+
try {
|
|
13
|
+
fs.chmodSync(filePath, 384);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function secureDir(dirPath) {
|
|
19
|
+
if (isWindows()) {
|
|
20
|
+
restrictAclToCurrentUser(dirPath, "(OI)(CI)(F)");
|
|
21
|
+
} else {
|
|
22
|
+
try {
|
|
23
|
+
fs.chmodSync(dirPath, 448);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function restrictAclToCurrentUser(target, permission) {
|
|
29
|
+
try {
|
|
30
|
+
const user = os.userInfo().username;
|
|
31
|
+
execFileSync(
|
|
32
|
+
"icacls",
|
|
33
|
+
[target, "/inheritance:r", "/grant:r", `${user}:${permission}`],
|
|
34
|
+
{ stdio: "ignore" }
|
|
35
|
+
);
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
secureFile,
|
|
42
|
+
secureDir
|
|
43
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/tandem/recipe.ts
|
|
2
|
+
var SEND_BODY_PARAM = "body";
|
|
3
|
+
function buildMonitorCommand(logPath) {
|
|
4
|
+
return `npx kojee-mcp tail "${logPath}"`;
|
|
5
|
+
}
|
|
6
|
+
function buildMonitorSpawn(logPath) {
|
|
7
|
+
return `Monitor(command=\`${buildMonitorCommand(logPath)}\`, persistent=true, description="kojee Tandem events")`;
|
|
8
|
+
}
|
|
9
|
+
function buildReplyRecipe(event) {
|
|
10
|
+
if (event) {
|
|
11
|
+
return `reply directly with tandem_send(tandem_id="${event.tandem_id}", ${SEND_BODY_PARAM}="...", reply_to="${event.message_id}"); fetch context only if needed via tandem_messages(tandem_id, since=cursor-1)`;
|
|
12
|
+
}
|
|
13
|
+
return `reply directly with tandem_send(tandem_id, ${SEND_BODY_PARAM}, reply_to=<msg>); fetch context only if needed via tandem_messages(tandem_id, since=cursor-1)`;
|
|
14
|
+
}
|
|
15
|
+
function buildCatchUpNote() {
|
|
16
|
+
return "If the event line `cursor=<n>` ever jumps (a gap) or the log resets (the proxy caps and truncates the messages log in place on overflow \u2014 a `status=rotated` line is recorded in the status sibling), you may have missed messages: catch up with tandem_messages(tandem_id, since=<last cursor you saw>).";
|
|
17
|
+
}
|
|
18
|
+
function buildMonitorNudge(logPath) {
|
|
19
|
+
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()}.`;
|
|
20
|
+
}
|
|
21
|
+
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? }";
|
|
22
|
+
function buildWebhookReceiverNote() {
|
|
23
|
+
return "Webhook sink (optional, OFF unless KOJEE_WEBHOOK_URL + KOJEE_WEBHOOK_SECRET are set): the proxy POSTs every Tandem event as JSON to your endpoint. The body is the canonical normalized TandemEvent \u2014 " + WEBHOOK_BODY_SHAPE + " \u2014 where from.session_id and severity are present only when the wire carried them (the body is fully normalized: it carries from.principal, never the raw backend sender envelope). To build a receiver: (1) verify the X-Kojee-Signature header \u2014 it is the hex-encoded SHA-256 HMAC of the RAW request body bytes keyed by your KOJEE_WEBHOOK_SECRET; recompute over the received bytes and timing-safe compare, reject mismatches. (2) Dedupe by message_id \u2014 the body's `id`, also in the X-Kojee-Delivery header: delivery is AT-LEAST-ONCE (the proxy replays backlog from the cursor on restart), so the same event may arrive more than once \u2014 there is no exactly-once promise.";
|
|
24
|
+
}
|
|
25
|
+
var CODEX_LISTEN_CAP_MS = 8e3;
|
|
26
|
+
function buildCodexWakeReason(cursor) {
|
|
27
|
+
return `[kojee] new Tandem event(s) pending (cursor=${cursor}). If relevant to your task, drain them now: call tandem_messages(tandem_id, since=${cursor - 1}) to read, then ` + buildReplyRecipe() + `. If you expect an imminent reply and want to wait for exactly one, call tandem_listen(tandem_id, since=${cursor}, timeout_ms<=${CODEX_LISTEN_CAP_MS}) \u2014 a BOUNDED wait, cap 8s, NEVER an unconditional long-poll. If not relevant, ignore.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
buildMonitorSpawn,
|
|
32
|
+
buildReplyRecipe,
|
|
33
|
+
buildCatchUpNote,
|
|
34
|
+
buildMonitorNudge,
|
|
35
|
+
buildWebhookReceiverNote,
|
|
36
|
+
CODEX_LISTEN_CAP_MS,
|
|
37
|
+
buildCodexWakeReason
|
|
38
|
+
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
secureDir,
|
|
3
|
+
secureFile
|
|
4
|
+
} from "./chunk-BLEGIR35.js";
|
|
5
|
+
|
|
1
6
|
// src/tandem/session-discovery.ts
|
|
2
7
|
import fs from "fs";
|
|
3
8
|
import os from "os";
|
|
@@ -14,16 +19,10 @@ function discoveryFileName(key) {
|
|
|
14
19
|
function writeSessionDiscovery(sessionId, entry) {
|
|
15
20
|
const dir = sessionDiscoveryDir();
|
|
16
21
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
17
|
-
|
|
18
|
-
fs.chmodSync(dir, 448);
|
|
19
|
-
} catch {
|
|
20
|
-
}
|
|
22
|
+
secureDir(dir);
|
|
21
23
|
const filePath = sessionDiscoveryPath(sessionId);
|
|
22
24
|
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
23
|
-
|
|
24
|
-
fs.chmodSync(filePath, 384);
|
|
25
|
-
} catch {
|
|
26
|
-
}
|
|
25
|
+
secureFile(filePath);
|
|
27
26
|
}
|
|
28
27
|
function readSessionDiscovery(sessionId) {
|
|
29
28
|
try {
|
|
@@ -45,16 +44,10 @@ function discoveryPathForKey(key) {
|
|
|
45
44
|
function writeDiscoveryByKey(key, entry) {
|
|
46
45
|
const dir = sessionDiscoveryDir();
|
|
47
46
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
48
|
-
|
|
49
|
-
fs.chmodSync(dir, 448);
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
47
|
+
secureDir(dir);
|
|
52
48
|
const filePath = discoveryPathForKey(key);
|
|
53
49
|
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
54
|
-
|
|
55
|
-
fs.chmodSync(filePath, 384);
|
|
56
|
-
} catch {
|
|
57
|
-
}
|
|
50
|
+
secureFile(filePath);
|
|
58
51
|
}
|
|
59
52
|
function cleanupDiscoveryByKey(key) {
|
|
60
53
|
try {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kojeeHomeDir
|
|
3
|
+
} from "./chunk-SQL56SEB.js";
|
|
4
|
+
import {
|
|
5
|
+
secureFile
|
|
6
|
+
} from "./chunk-BLEGIR35.js";
|
|
7
|
+
|
|
8
|
+
// src/wizard/runtime-record.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
function runtimeRecordPath() {
|
|
12
|
+
return path.join(kojeeHomeDir(), ".kojee", "runtime");
|
|
13
|
+
}
|
|
14
|
+
function recordRuntime(runtime, recordPath = runtimeRecordPath()) {
|
|
15
|
+
fs.mkdirSync(path.dirname(recordPath), { recursive: true, mode: 448 });
|
|
16
|
+
fs.writeFileSync(recordPath, runtime + "\n", { mode: 384 });
|
|
17
|
+
secureFile(recordPath);
|
|
18
|
+
}
|
|
19
|
+
function readRecordedRuntime(recordPath = runtimeRecordPath()) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(recordPath, "utf8").trim();
|
|
22
|
+
return raw.length > 0 ? raw : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function clearRuntimeRecord(recordPath = runtimeRecordPath()) {
|
|
28
|
+
try {
|
|
29
|
+
fs.unlinkSync(recordPath);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
runtimeRecordPath,
|
|
36
|
+
recordRuntime,
|
|
37
|
+
readRecordedRuntime,
|
|
38
|
+
clearRuntimeRecord
|
|
39
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/tandem/webhook-config.ts
|
|
2
|
+
var WEBHOOK_DEFAULT_TIMEOUT_MS = 5e3;
|
|
3
|
+
var WEBHOOK_DEFAULT_MAX_RETRIES = 4;
|
|
4
|
+
function redactUrlUserinfo(rawUrl, parsed) {
|
|
5
|
+
if (!parsed.username && !parsed.password) return rawUrl;
|
|
6
|
+
parsed.username = "";
|
|
7
|
+
parsed.password = "";
|
|
8
|
+
return parsed.toString();
|
|
9
|
+
}
|
|
10
|
+
function parsePositiveInt(raw, fallback) {
|
|
11
|
+
if (raw === void 0) return fallback;
|
|
12
|
+
const n = Number(raw);
|
|
13
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) return fallback;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
function resolveWebhookConfig(env = process.env) {
|
|
17
|
+
const url = (env["KOJEE_WEBHOOK_URL"] ?? "").trim();
|
|
18
|
+
if (!url) {
|
|
19
|
+
return { enabled: false, config: null };
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(url);
|
|
24
|
+
} catch {
|
|
25
|
+
return {
|
|
26
|
+
enabled: false,
|
|
27
|
+
config: null,
|
|
28
|
+
error: `KOJEE_WEBHOOK_URL is not a valid URL \u2014 webhook sink DISABLED`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
32
|
+
return {
|
|
33
|
+
enabled: false,
|
|
34
|
+
config: null,
|
|
35
|
+
error: `KOJEE_WEBHOOK_URL must be http(s) (got ${parsed.protocol}) \u2014 webhook sink DISABLED`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const secret = (env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
|
|
39
|
+
if (!secret) {
|
|
40
|
+
return {
|
|
41
|
+
enabled: false,
|
|
42
|
+
config: null,
|
|
43
|
+
error: "KOJEE_WEBHOOK_URL is set but KOJEE_WEBHOOK_SECRET is missing \u2014 webhook sink DISABLED (the proxy NEVER sends unsigned webhooks)"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const timeoutMs = parsePositiveInt(env["KOJEE_WEBHOOK_TIMEOUT_MS"], WEBHOOK_DEFAULT_TIMEOUT_MS);
|
|
47
|
+
const maxRetries = parsePositiveInt(env["KOJEE_WEBHOOK_MAX_RETRIES"], WEBHOOK_DEFAULT_MAX_RETRIES);
|
|
48
|
+
const safeUrl = redactUrlUserinfo(url, parsed);
|
|
49
|
+
const redactedSummary = `url=${safeUrl} secret=<redacted> timeoutMs=${timeoutMs} maxRetries=${maxRetries}`;
|
|
50
|
+
return {
|
|
51
|
+
enabled: true,
|
|
52
|
+
config: { url, secret, timeoutMs, maxRetries, redactedSummary }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
WEBHOOK_DEFAULT_TIMEOUT_MS,
|
|
58
|
+
WEBHOOK_DEFAULT_MAX_RETRIES,
|
|
59
|
+
resolveWebhookConfig
|
|
60
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/wizard/runtimes.ts
|
|
2
|
+
var WIZARD_RUNTIMES = ["claude-code", "hermes", "openclaw", "codex"];
|
|
3
|
+
var WEBHOOK_RUNTIMES = /* @__PURE__ */ new Set([
|
|
4
|
+
"hermes",
|
|
5
|
+
"openclaw"
|
|
6
|
+
]);
|
|
7
|
+
function isWizardRuntime(value) {
|
|
8
|
+
return WIZARD_RUNTIMES.includes(value);
|
|
9
|
+
}
|
|
10
|
+
var RUNTIME_MENU = [
|
|
11
|
+
{ index: 1, runtime: "claude-code" },
|
|
12
|
+
{ index: 2, runtime: "hermes" },
|
|
13
|
+
{ index: 3, runtime: "openclaw" },
|
|
14
|
+
{ index: 4, runtime: "codex" }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
WIZARD_RUNTIMES,
|
|
19
|
+
WEBHOOK_RUNTIMES,
|
|
20
|
+
isWizardRuntime,
|
|
21
|
+
RUNTIME_MENU
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/wizard/home.ts
|
|
2
|
+
import os from "os";
|
|
3
|
+
function kojeeHomeDir() {
|
|
4
|
+
const env = process.env;
|
|
5
|
+
const homeKey = "HOME";
|
|
6
|
+
const profileKey = "USERPROFILE";
|
|
7
|
+
const fromEnv = env[homeKey] ?? env[profileKey];
|
|
8
|
+
if (fromEnv && fromEnv.length > 0) return fromEnv;
|
|
9
|
+
return os.homedir();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
kojeeHomeDir
|
|
14
|
+
};
|
|
@@ -259,15 +259,22 @@ async function consumeSse(body, opts, controller, state, onCursor) {
|
|
|
259
259
|
console.error("[event-stream] event-log append failed:", err);
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
+
if (opts.adapter.supportsChannels) {
|
|
263
|
+
try {
|
|
264
|
+
const channel = opts.adapter.formatTandemEvent(parsed);
|
|
265
|
+
await opts.server.notification({
|
|
266
|
+
method: "notifications/claude/channel",
|
|
267
|
+
params: channel
|
|
268
|
+
});
|
|
269
|
+
opts.queue?.markChannelDelivered(parsed.id);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error("[event-stream] channel notification failed:", err);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
262
274
|
try {
|
|
263
|
-
|
|
264
|
-
await opts.server.notification({
|
|
265
|
-
method: "notifications/claude/channel",
|
|
266
|
-
params: channel
|
|
267
|
-
});
|
|
268
|
-
opts.queue?.markChannelDelivered(parsed.id);
|
|
275
|
+
opts.webhookSink?.enqueue(parsed);
|
|
269
276
|
} catch (err) {
|
|
270
|
-
console.error("[event-stream]
|
|
277
|
+
console.error("[event-stream] webhook enqueue failed:", err);
|
|
271
278
|
}
|
|
272
279
|
} catch (err) {
|
|
273
280
|
console.error("[event-stream] failed to handle event:", err);
|
|
@@ -316,6 +323,10 @@ function drainSseEvents(input) {
|
|
|
316
323
|
function sleep(ms) {
|
|
317
324
|
return new Promise((r) => setTimeout(r, ms));
|
|
318
325
|
}
|
|
326
|
+
var MAX_DISPLAYNAME_CHARS = 64;
|
|
327
|
+
function sanitizeDisplayname(name) {
|
|
328
|
+
return name.replace(/[\x00-\x1f\x7f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_DISPLAYNAME_CHARS);
|
|
329
|
+
}
|
|
319
330
|
function normalizeBackendEvent(raw, sseEventType) {
|
|
320
331
|
const obj = raw ?? {};
|
|
321
332
|
const maybeFrom = obj["from"];
|
|
@@ -325,7 +336,14 @@ function normalizeBackendEvent(raw, sseEventType) {
|
|
|
325
336
|
const sender = obj["sender"] ?? {};
|
|
326
337
|
const principal = sender["principal_id"] ?? "";
|
|
327
338
|
const agentId = sender["agent_id"];
|
|
328
|
-
const
|
|
339
|
+
const rawSessionId = sender["session_id"];
|
|
340
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.trim() ? rawSessionId : void 0;
|
|
341
|
+
const rawSeverity = obj["severity"];
|
|
342
|
+
const severity = typeof rawSeverity === "string" && rawSeverity.trim() ? rawSeverity : void 0;
|
|
343
|
+
const rawDisplay = sender["display"];
|
|
344
|
+
const trimmedDisplay = typeof rawDisplay === "string" ? rawDisplay.trim() : "";
|
|
345
|
+
const safeDisplay = trimmedDisplay ? sanitizeDisplayname(trimmedDisplay) : "";
|
|
346
|
+
const displayname = safeDisplay ? safeDisplay : principal ? `principal:${principal.slice(0, 8)}` : "unknown";
|
|
329
347
|
const type = sseEventType === "state_change" ? "state_change" : "message";
|
|
330
348
|
const kind = obj["kind"] ?? "message";
|
|
331
349
|
return {
|
|
@@ -338,6 +356,7 @@ function normalizeBackendEvent(raw, sseEventType) {
|
|
|
338
356
|
member_id: "",
|
|
339
357
|
principal,
|
|
340
358
|
...agentId ? { agent_id: agentId } : {},
|
|
359
|
+
...sessionId ? { session_id: sessionId } : {},
|
|
341
360
|
displayname
|
|
342
361
|
},
|
|
343
362
|
kind,
|
|
@@ -346,7 +365,8 @@ function normalizeBackendEvent(raw, sseEventType) {
|
|
|
346
365
|
...typeof obj["format"] === "string" ? { format: obj["format"] } : {}
|
|
347
366
|
},
|
|
348
367
|
...Array.isArray(obj["mentions"]) ? { mentions: obj["mentions"] } : {},
|
|
349
|
-
...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {}
|
|
368
|
+
...obj["reply_to"] !== void 0 ? { reply_to: obj["reply_to"] } : {},
|
|
369
|
+
...severity ? { severity } : {}
|
|
350
370
|
};
|
|
351
371
|
}
|
|
352
372
|
|