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.
Files changed (33) hide show
  1. package/README.md +110 -4
  2. package/dist/{chunk-VLZADEFC.js → chunk-2TUAFAIW.js} +6 -9
  3. package/dist/chunk-BLEGIR35.js +43 -0
  4. package/dist/chunk-C6GZ2L2W.js +38 -0
  5. package/dist/{chunk-W6YRLSD4.js → chunk-DO42NPNR.js} +9 -16
  6. package/dist/chunk-EW72ZNQL.js +39 -0
  7. package/dist/chunk-F7L25L2J.js +60 -0
  8. package/dist/chunk-LVL25VLO.js +22 -0
  9. package/dist/chunk-SQL56SEB.js +14 -0
  10. package/dist/{chunk-QB22PD6T.js → chunk-WBMX4CHB.js} +29 -9
  11. package/dist/{chunk-LCFCCWMM.js → chunk-YEC7IHIG.js} +136 -78
  12. package/dist/{chunk-GBOTBYEP.js → chunk-YH27B6SW.js} +7 -8
  13. package/dist/chunk-ZW4SW7LJ.js +225 -0
  14. package/dist/cli.js +54 -80
  15. package/dist/codex-stop-hook-JOTBCS5K.js +72 -0
  16. package/dist/{doctor-GILTOH2R.js → doctor-TSHOMT5X.js} +29 -14
  17. package/dist/doctor-codex-BMI5JOO6.js +130 -0
  18. package/dist/{event-log-R6VW6GAF.js → event-log-RSTM4PLL.js} +3 -2
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +4 -3
  21. package/dist/{install-D2HIPOMT.js → install-WBIUVBZW.js} +5 -4
  22. package/dist/{paired-config-RB4SABOS.js → paired-config-JTFLHMZ2.js} +2 -1
  23. package/dist/runtime-record-WO4IECM6.js +14 -0
  24. package/dist/runtimes-CO43XUUK.js +12 -0
  25. package/dist/{session-discovery-QE5TTAPS.js → session-discovery-FNMJGFPM.js} +2 -1
  26. package/dist/{stop-hook-VLQS6QPR.js → stop-hook-SEPWWETV.js} +6 -5
  27. package/dist/{tail-stream-UZ42UIWO.js → tail-stream-BYKO4DW6.js} +4 -3
  28. package/dist/{user-prompt-submit-hook-C42DPDBO.js → user-prompt-submit-hook-ARPEO6FF.js} +2 -1
  29. package/dist/webhook-config-5TLLX7RA.js +10 -0
  30. package/dist/webhook-sink-7OYZBWXA.js +163 -0
  31. package/dist/wizard-7KHD5JT4.js +265 -0
  32. package/package.json +1 -1
  33. 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=claude-code` env var (set by `kojee-mcp init` in the MCP entry's `env` block — survives Claude.app's env strip)
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` (handles fresh installs where neither env var is present)
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
- Step-up re-authentication and nonce rotation are handled transparently -- the agent never needs to deal with auth mechanics.
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-W6YRLSD4.js";
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
- try {
30
- fs.chmodSync(filePath, 384);
31
- } catch {
32
- }
32
+ secureFile(filePath);
33
33
  fs.writeFileSync(statusPath, "", { mode: 384 });
34
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- const channel = opts.adapter.formatTandemEvent(parsed);
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] channel notification failed:", err);
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 displayname = principal ? `principal:${principal.slice(0, 8)}` : "unknown";
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