kojee-mcp 0.5.3 → 0.5.6

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 (37) hide show
  1. package/README.md +112 -5
  2. package/dist/{chunk-YEC7IHIG.js → chunk-2BDAM3TH.js} +92 -523
  3. package/dist/chunk-2MIISF2W.js +35 -0
  4. package/dist/chunk-3XDJOHMZ.js +223 -0
  5. package/dist/{chunk-ZW4SW7LJ.js → chunk-64EOLZNI.js} +14 -5
  6. package/dist/chunk-6SK6ITFE.js +142 -0
  7. package/dist/chunk-GI2CKKBL.js +46 -0
  8. package/dist/chunk-HIZ4NDWN.js +141 -0
  9. package/dist/chunk-LDZXU3DW.js +170 -0
  10. package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
  11. package/dist/{chunk-WBMX4CHB.js → chunk-UEGQGXPY.js} +57 -40
  12. package/dist/chunk-V5VZPYMZ.js +185 -0
  13. package/dist/{chunk-C6GZ2L2W.js → chunk-X672ZN7V.js} +5 -2
  14. package/dist/cli.js +47 -24
  15. package/dist/{codex-stop-hook-JOTBCS5K.js → codex-stop-hook-SWA53ECG.js} +1 -1
  16. package/dist/control-token-4BUCTYQB.js +13 -0
  17. package/dist/{doctor-TSHOMT5X.js → doctor-QCQDFLEH.js} +30 -17
  18. package/dist/{doctor-codex-BMI5JOO6.js → doctor-codex-NZ53ROQA.js} +12 -5
  19. package/dist/ensure-join-7AEDJMPE.js +96 -0
  20. package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
  21. package/dist/{hook-server-QF5JVUHV.js → hook-server-37E2LUKJ.js} +91 -0
  22. package/dist/index.d.ts +18 -15
  23. package/dist/index.js +9 -3
  24. package/dist/lib.d.ts +427 -0
  25. package/dist/lib.js +44 -0
  26. package/dist/reconnect-scheduler-JSXCJKQP.js +26 -0
  27. package/dist/resubscribe-G5OGDZJD.js +6 -0
  28. package/dist/send-cli-C2F4WTBN.js +72 -0
  29. package/dist/{stop-hook-SEPWWETV.js → stop-hook-TRAMQYNE.js} +16 -8
  30. package/dist/{tail-stream-BYKO4DW6.js → tail-stream-VUZBYKXS.js} +4 -3
  31. package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
  32. package/dist/webhook-config-O4WMQ532.js +20 -0
  33. package/dist/{webhook-sink-7OYZBWXA.js → webhook-sink-NWGCUDGY.js} +28 -5
  34. package/dist/{wizard-7KHD5JT4.js → wizard-OSOAY4GO.js} +64 -27
  35. package/package.json +11 -2
  36. package/dist/chunk-F7L25L2J.js +0 -60
  37. package/dist/webhook-config-5TLLX7RA.js +0 -10
@@ -0,0 +1,170 @@
1
+ // src/error-translator.ts
2
+ function translateGovernanceResult(result) {
3
+ const governance = result._meta?.governance;
4
+ if (!governance) return result;
5
+ if (governance.decision === "deny") {
6
+ return formatDenied(governance);
7
+ }
8
+ if (governance.decision === "require_approval") {
9
+ return formatApprovalRequired(governance);
10
+ }
11
+ return result;
12
+ }
13
+ function formatApprovalRequired(governance) {
14
+ const rules = formatRules(governance.triggered_guardrails);
15
+ const text = `APPROVAL REQUIRED: This action needs user approval before executing. Approval ID: ${governance.approval_id}. Expires: ${governance.expires_at ?? "unknown"}. Triggered rules: ${rules}. The user has been notified. Call kojee_check_approval with this approval_id to check the status.`;
16
+ return {
17
+ content: [{ type: "text", text }],
18
+ isError: true
19
+ };
20
+ }
21
+ function formatDenied(governance) {
22
+ const rules = formatRules(governance.triggered_guardrails);
23
+ const text = `DENIED: This action was blocked by governance policy. Triggered rules: ${rules}. This cannot proceed \u2014 modify your request or ask the user to adjust governance rules.`;
24
+ return {
25
+ content: [{ type: "text", text }],
26
+ isError: true
27
+ };
28
+ }
29
+ function translateHttpError(status, errorCode, trigger) {
30
+ if (status === 401) {
31
+ if (errorCode === "use_dpop_nonce") {
32
+ return null;
33
+ }
34
+ if (errorCode === "invalid_dpop_proof") {
35
+ return makeError(
36
+ "Authentication failed. The proxy will attempt to re-enroll. If this persists, regenerate your gateway token."
37
+ );
38
+ }
39
+ if (errorCode === "key_enrollment_required") {
40
+ return null;
41
+ }
42
+ return makeError(
43
+ "Gateway token is invalid or expired. Generate a new one."
44
+ );
45
+ }
46
+ if (status === 403 && errorCode === "step_up_required") {
47
+ const reason = trigger ? ` (reason: ${trigger})` : "";
48
+ return makeError(
49
+ `Device re-authorization required${reason}. This action can't proceed until the user re-authorizes this device in the Kojee dashboard.`
50
+ );
51
+ }
52
+ if (status === 429) {
53
+ return makeError(
54
+ "Rate limit exceeded. Wait before making more requests."
55
+ );
56
+ }
57
+ if (status >= 500) {
58
+ return makeError(
59
+ "Kojee gateway encountered an error. Try again."
60
+ );
61
+ }
62
+ return null;
63
+ }
64
+ var TANDEM_ERROR_MESSAGES = {
65
+ [-32003]: () => "This Tandem is hardened to owner-only membership; you can't join.",
66
+ [-32004]: () => "You aren't a member of that Tandem. Use tandem_join(join_link) first.",
67
+ [-32006]: (data) => {
68
+ const retry = data?.["retry_after_seconds"] ?? "a moment";
69
+ return `Rate limit hit on this Tandem. Retry after ${retry} seconds.`;
70
+ },
71
+ [-32007]: (data) => {
72
+ const rule = data?.["rule"] ?? "policy";
73
+ return `Message rejected by Tandem policy (${rule}).`;
74
+ },
75
+ [-32011]: (data) => {
76
+ const id = data?.["tandem_id"] ?? "unknown";
77
+ return `Tandem ${id} doesn't exist or isn't visible to you.`;
78
+ },
79
+ [-32015]: (data) => {
80
+ const candidates = data?.["candidates"] ?? [];
81
+ return `An @-mention matched multiple members and is ambiguous. Retry with explicit mentions[]. Candidates: ${candidates.join(", ")}.`;
82
+ }
83
+ };
84
+ function translateJsonRpcError(error) {
85
+ const msg = error.message ?? "";
86
+ const msgLower = msg.toLowerCase();
87
+ const tandemMessage = TANDEM_ERROR_MESSAGES[error.code];
88
+ if (tandemMessage) {
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: tandemMessage(error.data)
94
+ }
95
+ ],
96
+ isError: true
97
+ };
98
+ }
99
+ switch (error.code) {
100
+ case -32601:
101
+ return makeError(
102
+ `Tool not available. It may have been removed or is not connected. Check your connected services in the Kojee dashboard.`
103
+ );
104
+ case -32602:
105
+ return makeError(msg || "Invalid parameters for this tool call.");
106
+ case -32603: {
107
+ if (msgLower.includes("multiple accounts connected")) {
108
+ return makeError(msg);
109
+ }
110
+ if (msgLower.includes("not connected")) {
111
+ return makeError(
112
+ "The service is not connected. Connect it in the Kojee dashboard."
113
+ );
114
+ }
115
+ if (msgLower.includes("scope") && msgLower.includes("access")) {
116
+ return makeError(
117
+ "Token doesn't have access to this tool. Update scopes in the Kojee dashboard."
118
+ );
119
+ }
120
+ if (msgLower.includes("invalid_grant") || msgLower.includes("token refresh failed") || msgLower.includes("re-authorization") || msgLower.includes("reauthorization")) {
121
+ const serviceMatch = msg.match(
122
+ /(?:for|connected for)\s+(\w[\w-]*)/i
123
+ );
124
+ const service = serviceMatch ? serviceMatch[1] : "service";
125
+ return makeError(
126
+ `The ${service} connection needs re-authorization. Ask the user to reconnect it in the Kojee dashboard.`
127
+ );
128
+ }
129
+ return makeError(msg || "An internal error occurred on the gateway.");
130
+ }
131
+ case -32600:
132
+ return makeError(
133
+ "Unexpected response from gateway. This may be a temporary issue."
134
+ );
135
+ case -32e3:
136
+ return makeError(
137
+ "Rate limit exceeded. Wait before making more requests."
138
+ );
139
+ default:
140
+ return makeError(msg || "An unknown error occurred.");
141
+ }
142
+ }
143
+ function translateNetworkError(_error) {
144
+ return makeError(
145
+ "Cannot reach Kojee gateway. Check your connection."
146
+ );
147
+ }
148
+ function translateToolCallResult(result) {
149
+ if (result._meta?.governance) {
150
+ return translateGovernanceResult(result);
151
+ }
152
+ return result;
153
+ }
154
+ function formatRules(guardrails) {
155
+ if (!guardrails || guardrails.length === 0) return "unknown";
156
+ return guardrails.join(", ");
157
+ }
158
+ function makeError(text) {
159
+ return {
160
+ content: [{ type: "text", text }],
161
+ isError: true
162
+ };
163
+ }
164
+
165
+ export {
166
+ translateHttpError,
167
+ translateJsonRpcError,
168
+ translateNetworkError,
169
+ translateToolCallResult
170
+ };
@@ -54,6 +54,7 @@ async function resubscribeMemberships(opts) {
54
54
  });
55
55
  return touched;
56
56
  }
57
+
57
58
  export {
58
59
  resubscribeMemberships
59
60
  };
@@ -1,33 +1,7 @@
1
- // src/auth/dpop.ts
2
- import { SignJWT, base64url } from "jose";
3
- import crypto from "crypto";
4
- async function createDPoPProof(privateKey, kid, method, url, nonce, accessToken) {
5
- const payload = {
6
- htm: method,
7
- htu: url,
8
- jti: crypto.randomUUID()
9
- };
10
- if (nonce) {
11
- payload.nonce = nonce;
12
- }
13
- if (accessToken) {
14
- payload.ath = computeAth(accessToken);
15
- }
16
- const header = {
17
- typ: "dpop+jwt",
18
- alg: "ES256",
19
- jwk: { kid }
20
- };
21
- return new SignJWT(payload).setProtectedHeader(header).setIssuedAt().sign(privateKey);
22
- }
23
- function computeAth(accessToken) {
24
- const hash = crypto.createHash("sha256").update(accessToken).digest();
25
- return base64url.encode(hash);
26
- }
27
-
28
- // src/tandem/session-id.ts
29
- import { ulid } from "ulidx";
30
- var MCP_SESSION_ID = ulid();
1
+ import {
2
+ MCP_SESSION_ID,
3
+ createDPoPProof
4
+ } from "./chunk-2MIISF2W.js";
31
5
 
32
6
  // src/tandem/event-stream.ts
33
7
  var STALE_FLOOR_MS = 9e4;
@@ -151,6 +125,13 @@ async function startEventStream(opts) {
151
125
  state.connected = false;
152
126
  currentController.abort();
153
127
  });
128
+ handle.reconnect = () => {
129
+ if (stopped) return;
130
+ console.error(
131
+ "[event-stream] reconnect requested (membership change) \u2014 closing current connection"
132
+ );
133
+ currentController.abort();
134
+ };
154
135
  handle.getState = () => ({
155
136
  connected: state.connected,
156
137
  connectedSince: state.connectedSince,
@@ -324,26 +305,62 @@ function sleep(ms) {
324
305
  return new Promise((r) => setTimeout(r, ms));
325
306
  }
326
307
  var MAX_DISPLAYNAME_CHARS = 64;
308
+ var MAX_COMBINING_RUN = 4;
309
+ var STRIP_CATEGORIES_RE = /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}]/u;
310
+ var INVISIBLE_MARKS_RE = /[\u034f\ufe00-\ufe0f\u{e0100}-\u{e01ef}]/u;
311
+ var COMBINING_MARK_RE = /\p{Mn}/u;
327
312
  function sanitizeDisplayname(name) {
328
- return name.replace(/[\x00-\x1f\x7f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_DISPLAYNAME_CHARS);
313
+ const normalized = name.normalize("NFC");
314
+ let kept = "";
315
+ let combiningRun = 0;
316
+ for (const ch of normalized) {
317
+ if (STRIP_CATEGORIES_RE.test(ch) || INVISIBLE_MARKS_RE.test(ch)) continue;
318
+ if (COMBINING_MARK_RE.test(ch)) {
319
+ combiningRun += 1;
320
+ if (combiningRun > MAX_COMBINING_RUN) continue;
321
+ } else {
322
+ combiningRun = 0;
323
+ }
324
+ kept += ch;
325
+ }
326
+ const collapsed = kept.split(/\s+/).filter(Boolean).join(" ");
327
+ let capped = "";
328
+ for (const ch of collapsed) {
329
+ if (capped.length + ch.length > MAX_DISPLAYNAME_CHARS) break;
330
+ capped += ch;
331
+ }
332
+ return capped.trimEnd();
333
+ }
334
+ function resolveDisplayname(rawDisplay, principal) {
335
+ const trimmedDisplay = typeof rawDisplay === "string" ? rawDisplay.trim() : "";
336
+ const safeDisplay = trimmedDisplay ? sanitizeDisplayname(trimmedDisplay) : "";
337
+ if (safeDisplay) return safeDisplay;
338
+ if (!principal) return "unknown";
339
+ return `principal:${Array.from(principal).slice(0, 8).join("")}`;
329
340
  }
330
341
  function normalizeBackendEvent(raw, sseEventType) {
331
342
  const obj = raw ?? {};
332
343
  const maybeFrom = obj["from"];
333
344
  if (maybeFrom && typeof maybeFrom["principal"] === "string") {
334
- return raw;
345
+ const canonical = raw;
346
+ const canonicalPrincipal = sanitizeDisplayname(maybeFrom["principal"]);
347
+ return {
348
+ ...canonical,
349
+ from: {
350
+ ...canonical.from,
351
+ principal: canonicalPrincipal,
352
+ displayname: resolveDisplayname(maybeFrom["displayname"], canonicalPrincipal)
353
+ }
354
+ };
335
355
  }
336
356
  const sender = obj["sender"] ?? {};
337
- const principal = sender["principal_id"] ?? "";
357
+ const principal = sanitizeDisplayname(sender["principal_id"] ?? "");
338
358
  const agentId = sender["agent_id"];
339
359
  const rawSessionId = sender["session_id"];
340
360
  const sessionId = typeof rawSessionId === "string" && rawSessionId.trim() ? rawSessionId : void 0;
341
361
  const rawSeverity = obj["severity"];
342
362
  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";
363
+ const displayname = resolveDisplayname(sender["display"], principal);
347
364
  const type = sseEventType === "state_change" ? "state_change" : "message";
348
365
  const kind = obj["kind"] ?? "message";
349
366
  return {
@@ -371,8 +388,8 @@ function normalizeBackendEvent(raw, sseEventType) {
371
388
  }
372
389
 
373
390
  export {
374
- createDPoPProof,
375
- MCP_SESSION_ID,
376
391
  createAdaptiveWatchdog,
377
- startEventStream
392
+ startEventStream,
393
+ sanitizeDisplayname,
394
+ normalizeBackendEvent
378
395
  };
@@ -0,0 +1,185 @@
1
+ // src/tandem/webhook-config.ts
2
+ var WEBHOOK_DEFAULT_TIMEOUT_MS = 3e4;
3
+ var WEBHOOK_DEFAULT_MAX_RETRIES = 2;
4
+ var WEBHOOK_DEFAULT_SIGNATURE_HEADER = "X-Kojee-Signature";
5
+ var WEBHOOK_DEFAULT_SIGNATURE_PREFIX = "";
6
+ var WEBHOOK_SIGNATURE_FORMATS = {
7
+ github: { header: "X-Hub-Signature-256", prefix: "sha256=" }
8
+ };
9
+ function redactUrlUserinfo(rawUrl, parsed) {
10
+ if (!parsed.username && !parsed.password) return rawUrl;
11
+ parsed.username = "";
12
+ parsed.password = "";
13
+ return parsed.toString();
14
+ }
15
+ function parsePositiveInt(raw, fallback) {
16
+ if (raw === void 0) return fallback;
17
+ const n = Number(raw);
18
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) return fallback;
19
+ return n;
20
+ }
21
+ var HTTP_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
22
+ var INVALID_HEADER_VALUE_CHAR_RE = /[^\t\x20-\x7e\x80-\xff]/;
23
+ var RESERVED_SINK_HEADERS = [
24
+ // (1) sink-set on every POST:
25
+ "content-type",
26
+ "x-kojee-delivery",
27
+ // (2) fetch-forbidden / dispatch-validated (Headers construction accepts them):
28
+ "content-length",
29
+ "transfer-encoding",
30
+ "connection",
31
+ "host",
32
+ "expect",
33
+ "keep-alive",
34
+ "upgrade",
35
+ "te"
36
+ ];
37
+ var DRY_RUN_DIGEST = "0".repeat(64);
38
+ function headerNameRejectionReason(name) {
39
+ if (!HTTP_TOKEN_RE.test(name)) {
40
+ return "is not a valid HTTP header name (letters, digits, or !#$%&'*+-.^_`|~ only \u2014 no spaces or colons)";
41
+ }
42
+ if (RESERVED_SINK_HEADERS.includes(name.toLowerCase())) {
43
+ return "collides with a header the sink always sets (Content-Type, X-Kojee-Delivery) or is a connection/framing header the HTTP client validates or controls at send time (Content-Length, Transfer-Encoding, Connection, Host, Expect, Keep-Alive, Upgrade, TE)";
44
+ }
45
+ try {
46
+ void new Headers({ [name]: DRY_RUN_DIGEST });
47
+ } catch {
48
+ return "is rejected by the HTTP client when constructing request headers";
49
+ }
50
+ return null;
51
+ }
52
+ function prefixRejectionReason(prefix) {
53
+ const value = prefix + DRY_RUN_DIGEST;
54
+ if (INVALID_HEADER_VALUE_CHAR_RE.test(value)) {
55
+ return "contains a character that is illegal in an HTTP header value (legal: tab, printable ASCII, and Latin-1 0x80-0xFF \u2014 no control chars, no DEL, no characters above U+00FF)";
56
+ }
57
+ try {
58
+ void new Headers({ [WEBHOOK_DEFAULT_SIGNATURE_HEADER]: value });
59
+ } catch {
60
+ return "is rejected by the HTTP client when constructing request headers";
61
+ }
62
+ return null;
63
+ }
64
+ function emissionRejectionReason(header, prefix) {
65
+ const nameReason = headerNameRejectionReason(header);
66
+ if (nameReason) return `header "${header}" ${nameReason}`;
67
+ const prefixReason = prefixRejectionReason(prefix);
68
+ if (prefixReason) return `prefix ${prefixReason}`;
69
+ try {
70
+ void new Headers({
71
+ "Content-Type": "application/json",
72
+ [header]: prefix + DRY_RUN_DIGEST,
73
+ "X-Kojee-Delivery": DRY_RUN_DIGEST
74
+ });
75
+ } catch {
76
+ return "is rejected by the HTTP client when constructing request headers";
77
+ }
78
+ return null;
79
+ }
80
+ function resolveSignatureEmission(env) {
81
+ const warnings = [];
82
+ let presetHeader;
83
+ let presetPrefix;
84
+ const formatRaw = (env["KOJEE_WEBHOOK_SIGNATURE_FORMAT"] ?? "").trim();
85
+ if (formatRaw) {
86
+ if (Object.hasOwn(WEBHOOK_SIGNATURE_FORMATS, formatRaw)) {
87
+ const preset = WEBHOOK_SIGNATURE_FORMATS[formatRaw];
88
+ presetHeader = preset.header;
89
+ presetPrefix = preset.prefix;
90
+ } else {
91
+ warnings.push(
92
+ `KOJEE_WEBHOOK_SIGNATURE_FORMAT="${formatRaw}" is not a recognized preset (known: ${Object.keys(WEBHOOK_SIGNATURE_FORMATS).join(", ")}) \u2014 ignored; using defaults / explicit KOJEE_WEBHOOK_SIGNATURE_HEADER/_PREFIX`
93
+ );
94
+ }
95
+ }
96
+ let headerOverride = (env["KOJEE_WEBHOOK_SIGNATURE_HEADER"] ?? "").trim();
97
+ if (headerOverride) {
98
+ const reason = headerNameRejectionReason(headerOverride);
99
+ if (reason) {
100
+ const fallback = presetHeader ?? WEBHOOK_DEFAULT_SIGNATURE_HEADER;
101
+ warnings.push(
102
+ `KOJEE_WEBHOOK_SIGNATURE_HEADER="${headerOverride}" ${reason} \u2014 ignored; using ${fallback}. An unsendable name would make EVERY delivery attempt fail at send time (or send the webhook unsigned)`
103
+ );
104
+ headerOverride = "";
105
+ }
106
+ }
107
+ let prefixOverride = env["KOJEE_WEBHOOK_SIGNATURE_PREFIX"];
108
+ if (prefixOverride !== void 0) {
109
+ const reason = prefixRejectionReason(prefixOverride);
110
+ if (reason) {
111
+ const fallback = presetPrefix ?? WEBHOOK_DEFAULT_SIGNATURE_PREFIX;
112
+ warnings.push(
113
+ `KOJEE_WEBHOOK_SIGNATURE_PREFIX ${reason} \u2014 ignored; using ${fallback || "(none)"}. An unsendable value would make EVERY delivery attempt fail at send time`
114
+ );
115
+ prefixOverride = void 0;
116
+ }
117
+ }
118
+ let header = headerOverride || presetHeader || WEBHOOK_DEFAULT_SIGNATURE_HEADER;
119
+ let prefix = prefixOverride !== void 0 ? prefixOverride : presetPrefix ?? WEBHOOK_DEFAULT_SIGNATURE_PREFIX;
120
+ const finalReason = emissionRejectionReason(header, prefix);
121
+ if (finalReason) {
122
+ warnings.push(
123
+ `resolved signature emission ${finalReason} \u2014 using the 0.5.2 defaults (${WEBHOOK_DEFAULT_SIGNATURE_HEADER}, bare hex digest). Check KOJEE_WEBHOOK_SIGNATURE_FORMAT/_HEADER/_PREFIX`
124
+ );
125
+ header = WEBHOOK_DEFAULT_SIGNATURE_HEADER;
126
+ prefix = WEBHOOK_DEFAULT_SIGNATURE_PREFIX;
127
+ }
128
+ return { header, prefix, warnings };
129
+ }
130
+ function resolveWebhookConfig(env = process.env) {
131
+ const url = (env["KOJEE_WEBHOOK_URL"] ?? "").trim();
132
+ if (!url) {
133
+ return { enabled: false, config: null };
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = new URL(url);
138
+ } catch {
139
+ return {
140
+ enabled: false,
141
+ config: null,
142
+ error: `KOJEE_WEBHOOK_URL is not a valid URL \u2014 webhook sink DISABLED`
143
+ };
144
+ }
145
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
146
+ return {
147
+ enabled: false,
148
+ config: null,
149
+ error: `KOJEE_WEBHOOK_URL must be http(s) (got ${parsed.protocol}) \u2014 webhook sink DISABLED`
150
+ };
151
+ }
152
+ const secret = (env["KOJEE_WEBHOOK_SECRET"] ?? "").trim();
153
+ if (!secret) {
154
+ return {
155
+ enabled: false,
156
+ config: null,
157
+ error: "KOJEE_WEBHOOK_URL is set but KOJEE_WEBHOOK_SECRET is missing \u2014 webhook sink DISABLED (the proxy NEVER sends unsigned webhooks)"
158
+ };
159
+ }
160
+ const timeoutMs = parsePositiveInt(env["KOJEE_WEBHOOK_TIMEOUT_MS"], WEBHOOK_DEFAULT_TIMEOUT_MS);
161
+ const maxRetries = parsePositiveInt(env["KOJEE_WEBHOOK_MAX_RETRIES"], WEBHOOK_DEFAULT_MAX_RETRIES);
162
+ const emission = resolveSignatureEmission(env);
163
+ const warning = emission.warnings.length > 0 ? emission.warnings.join("; ") : void 0;
164
+ const signatureHeader = emission.header;
165
+ const signaturePrefix = emission.prefix;
166
+ const safeUrl = redactUrlUserinfo(url, parsed);
167
+ const sigSummary = signatureHeader !== WEBHOOK_DEFAULT_SIGNATURE_HEADER || signaturePrefix !== WEBHOOK_DEFAULT_SIGNATURE_PREFIX ? ` signatureHeader=${signatureHeader} signaturePrefix=${signaturePrefix || "(none)"}` : "";
168
+ const redactedSummary = `url=${safeUrl} secret=<redacted> timeoutMs=${timeoutMs} maxRetries=${maxRetries}${sigSummary}`;
169
+ return {
170
+ enabled: true,
171
+ config: { url, secret, timeoutMs, maxRetries, signatureHeader, signaturePrefix, redactedSummary },
172
+ ...warning !== void 0 ? { warning } : {}
173
+ };
174
+ }
175
+
176
+ export {
177
+ WEBHOOK_DEFAULT_TIMEOUT_MS,
178
+ WEBHOOK_DEFAULT_MAX_RETRIES,
179
+ WEBHOOK_DEFAULT_SIGNATURE_HEADER,
180
+ WEBHOOK_DEFAULT_SIGNATURE_PREFIX,
181
+ WEBHOOK_SIGNATURE_FORMATS,
182
+ emissionRejectionReason,
183
+ resolveSignatureEmission,
184
+ resolveWebhookConfig
185
+ };
@@ -19,8 +19,11 @@ function buildMonitorNudge(logPath) {
19
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
20
  }
21
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.";
22
+ function buildWebhookReceiverNote(sig) {
23
+ const header = sig?.header ?? "X-Kojee-Signature";
24
+ const prefix = sig?.prefix ?? "";
25
+ const digestDesc = prefix ? `the literal prefix \`${prefix}\` followed by the hex-encoded SHA-256 HMAC` : "the hex-encoded SHA-256 HMAC";
26
+ 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 ${header} header \u2014 it is ${digestDesc} 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
27
  }
25
28
  var CODEX_LISTEN_CAP_MS = 8e3;
26
29
  function buildCodexWakeReason(cursor) {
package/dist/cli.js CHANGED
@@ -1,23 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ VERSION,
4
+ startProxy
5
+ } from "./chunk-2BDAM3TH.js";
6
+ import "./chunk-X672ZN7V.js";
7
+ import "./chunk-BJMASMKX.js";
2
8
  import {
3
9
  loadPairedConfig,
4
10
  pairedConfigPath,
5
11
  savePairedConfig
6
12
  } from "./chunk-YH27B6SW.js";
7
13
  import {
8
- AuthModule,
9
- VERSION,
10
- startProxy
11
- } from "./chunk-YEC7IHIG.js";
12
- import "./chunk-BJMASMKX.js";
13
- import "./chunk-C6GZ2L2W.js";
14
- import "./chunk-WBMX4CHB.js";
14
+ AuthModule
15
+ } from "./chunk-6SK6ITFE.js";
16
+ import {
17
+ defaultPairedKeystorePath,
18
+ deriveKeystorePath
19
+ } from "./chunk-3XDJOHMZ.js";
20
+ import "./chunk-UEGQGXPY.js";
21
+ import "./chunk-2MIISF2W.js";
22
+ import "./chunk-LDZXU3DW.js";
15
23
  import "./chunk-BLEGIR35.js";
16
24
 
17
25
  // src/cli.ts
18
26
  import { Command } from "commander";
19
- import crypto from "crypto";
20
- import os from "os";
21
27
  import path from "path";
22
28
 
23
29
  // src/tandem/pair.ts
@@ -60,14 +66,6 @@ async function runPair(opts) {
60
66
  }
61
67
 
62
68
  // src/cli.ts
63
- var KOJEE_DIR = path.join(os.homedir(), ".kojee");
64
- function deriveKeystorePath(token) {
65
- const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
66
- return path.join(KOJEE_DIR, `keypair-${hash}.json`);
67
- }
68
- function defaultPairedKeystorePath() {
69
- return path.join(KOJEE_DIR, "keypair.json");
70
- }
71
69
  var program = new Command().name("kojee-mcp").description(
72
70
  "Local MCP proxy for Kojee \u2014 handles DPoP auth, tool discovery, and governance transparently"
73
71
  ).version(VERSION).enablePositionalOptions();
@@ -85,15 +83,15 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
85
83
  });
86
84
  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) => {
87
85
  if (opts.type === "stop") {
88
- const { runStopHook } = await import("./stop-hook-SEPWWETV.js");
86
+ const { runStopHook } = await import("./stop-hook-TRAMQYNE.js");
89
87
  await runStopHook();
90
88
  process.exit(0);
91
89
  } else if (opts.type === "user-prompt-submit") {
92
- const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-ARPEO6FF.js");
90
+ const { runUserPromptSubmitHook } = await import("./user-prompt-submit-hook-ZD2XKN7U.js");
93
91
  await runUserPromptSubmitHook();
94
92
  process.exit(0);
95
93
  } else if (opts.type === "codex-stop") {
96
- const { runCodexStopHook } = await import("./codex-stop-hook-JOTBCS5K.js");
94
+ const { runCodexStopHook } = await import("./codex-stop-hook-SWA53ECG.js");
97
95
  await runCodexStopHook();
98
96
  process.exit(0);
99
97
  } else {
@@ -116,8 +114,21 @@ Restart Claude Code for hooks to take effect.`
116
114
  );
117
115
  }
118
116
  });
117
+ program.command("send <tandem_id>").description(
118
+ "Send a Tandem message using this machine's paired credentials (~/.kojee). Prints one JSON envelope to stdout: {ok, message_id, cursor, text} on success, {ok:false, error:<typed code>, message} on failure (exit 1)."
119
+ ).requiredOption("--body <text>", "Message body (required)").option("--reply-to <message_id>", "Message id this send replies to").option("--kind <kind>", "Message kind: message | status (default: backend default)").action(async (tandemId, opts) => {
120
+ const { runSendCli } = await import("./send-cli-C2F4WTBN.js");
121
+ const { exitCode, envelope } = await runSendCli({
122
+ tandemId,
123
+ body: opts.body,
124
+ ...opts.replyTo !== void 0 ? { replyTo: opts.replyTo } : {},
125
+ ...opts.kind !== void 0 ? { kind: opts.kind } : {}
126
+ });
127
+ process.stdout.write(JSON.stringify(envelope) + "\n");
128
+ process.exit(exitCode);
129
+ });
119
130
  program.command("tail <path>").description("Stream a file's contents and follow appends (portable replacement for `tail -F`)").action(async (filePath) => {
120
- const { runTail } = await import("./tail-stream-BYKO4DW6.js");
131
+ const { runTail } = await import("./tail-stream-VUZBYKXS.js");
121
132
  try {
122
133
  await runTail(filePath);
123
134
  } catch (err) {
@@ -126,19 +137,28 @@ program.command("tail <path>").description("Stream a file's contents and follow
126
137
  }
127
138
  });
128
139
  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 () => {
129
- const { runDoctor } = await import("./doctor-TSHOMT5X.js");
140
+ const { runDoctor } = await import("./doctor-QCQDFLEH.js");
130
141
  const code = await runDoctor();
131
142
  process.exit(code);
132
143
  });
133
144
  program.command("init").description(
134
145
  "Set up kojee for a runtime (claude-code | hermes | openclaw | codex). Interactive when stdin is a TTY; `--runtime <id>` for non-interactive/CI. Run after `kojee-mcp pair`."
135
- ).option("--runtime <id>", "Target runtime: claude-code | hermes | openclaw | codex").option("--config-path <path>", "Override the runtime's MCP-config path").option("--hooks-path <path>", "Override the runtime's hooks-file path").option("--webhook-url <url>", "Webhook receiver URL (codex/hermes/openclaw)").option("--webhook-secret <secret>", "Webhook HMAC secret (generated if omitted)").option("--uninstall", "Remove the kojee config for the chosen (or recorded) runtime").action(async (opts) => {
146
+ ).option("--runtime <id>", "Target runtime: claude-code | hermes | openclaw | codex").option("--config-path <path>", "Override the runtime's MCP-config path").option("--hooks-path <path>", "Override the runtime's hooks-file path").option("--webhook-url <url>", "Webhook receiver URL (codex/hermes/openclaw)").option("--webhook-secret <secret>", "Webhook HMAC secret (generated if omitted)").option(
147
+ "--webhook-signature-format <preset>",
148
+ 'Signature preset: "github" = header X-Hub-Signature-256, value sha256=<hex> (default: bare hex in X-Kojee-Signature)'
149
+ ).option(
150
+ "--webhook-signature-header <name>",
151
+ "Signature header name (default X-Kojee-Signature; overrides the preset's header)"
152
+ ).option(
153
+ "--webhook-signature-prefix <prefix>",
154
+ "Literal string prepended to the hex digest (default empty; overrides the preset's prefix)"
155
+ ).option("--uninstall", "Remove the kojee config for the chosen (or recorded) runtime").action(async (opts) => {
136
156
  const { loadPairedConfig: loadPairedConfig2 } = await import("./paired-config-JTFLHMZ2.js");
137
157
  if (loadPairedConfig2() === null && !opts.uninstall) {
138
158
  console.error("Not paired. Run `kojee-mcp pair <code> --url <broker>` first, then re-run `init`.");
139
159
  process.exit(1);
140
160
  }
141
- const { runWizard } = await import("./wizard-7KHD5JT4.js");
161
+ const { runWizard } = await import("./wizard-OSOAY4GO.js");
142
162
  const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
143
163
  const result = await runWizard({
144
164
  ...opts.runtime !== void 0 ? { runtime: opts.runtime } : {},
@@ -147,6 +167,9 @@ program.command("init").description(
147
167
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
148
168
  ...opts.webhookUrl ? { webhookUrl: opts.webhookUrl } : {},
149
169
  ...opts.webhookSecret ? { webhookSecret: opts.webhookSecret } : {},
170
+ ...opts.webhookSignatureFormat ? { webhookSignatureFormat: opts.webhookSignatureFormat } : {},
171
+ ...opts.webhookSignatureHeader ? { webhookSignatureHeader: opts.webhookSignatureHeader } : {},
172
+ ...opts.webhookSignaturePrefix !== void 0 ? { webhookSignaturePrefix: opts.webhookSignaturePrefix } : {},
150
173
  interactive,
151
174
  ...interactive ? { promptRuntime: promptRuntimeFromTty } : {}
152
175
  });
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-LSUB6QMP.js";
4
4
  import {
5
5
  buildCodexWakeReason
6
- } from "./chunk-C6GZ2L2W.js";
6
+ } from "./chunk-X672ZN7V.js";
7
7
 
8
8
  // src/hooks/codex-stop-hook.ts
9
9
  import fs from "fs";
@@ -0,0 +1,13 @@
1
+ import {
2
+ controlTokenAuthHeaders,
3
+ controlTokenPath,
4
+ issueControlToken,
5
+ loadControlToken
6
+ } from "./chunk-GI2CKKBL.js";
7
+ import "./chunk-BLEGIR35.js";
8
+ export {
9
+ controlTokenAuthHeaders,
10
+ controlTokenPath,
11
+ issueControlToken,
12
+ loadControlToken
13
+ };