kojee-mcp 0.5.3 → 0.5.4

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.
@@ -0,0 +1,141 @@
1
+ import {
2
+ translateToolCallResult
3
+ } from "./chunk-LDZXU3DW.js";
4
+
5
+ // src/tandem/send.ts
6
+ var SEND_KINDS = ["message", "status"];
7
+ function sendFailure(error, message) {
8
+ return { ok: false, error, message };
9
+ }
10
+ function parseSendRequest(input) {
11
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
12
+ return sendFailure("bad_request", "request must be a JSON object");
13
+ }
14
+ const obj = input;
15
+ const tandemId = obj["tandem_id"];
16
+ if (typeof tandemId !== "string" || tandemId.length === 0) {
17
+ return sendFailure("bad_request", "tandem_id is required and must be a non-empty string");
18
+ }
19
+ const body = obj["body"];
20
+ if (typeof body !== "string" || body.length === 0) {
21
+ return sendFailure("bad_request", "body is required and must be a non-empty string");
22
+ }
23
+ const request = { tandem_id: tandemId, body };
24
+ if (obj["reply_to"] !== void 0) {
25
+ if (typeof obj["reply_to"] !== "string" || obj["reply_to"].length === 0) {
26
+ return sendFailure("bad_request", "reply_to must be a non-empty string when present");
27
+ }
28
+ request.reply_to = obj["reply_to"];
29
+ }
30
+ if (obj["kind"] !== void 0) {
31
+ const kind = obj["kind"];
32
+ if (typeof kind !== "string" || !SEND_KINDS.includes(kind)) {
33
+ return sendFailure(
34
+ "bad_request",
35
+ `kind must be one of: ${SEND_KINDS.join(", ")}`
36
+ );
37
+ }
38
+ request.kind = kind;
39
+ }
40
+ return { ok: true, request };
41
+ }
42
+ function classifySendFailure(text) {
43
+ if (/member[\s_-]?cap/i.test(text)) {
44
+ return sendFailure("member_cap", text);
45
+ }
46
+ if (/gateway error:\s*403/i.test(text) || /mcp_request_blocked/i.test(text) || /blocked by a firewall/i.test(text) || /security service/i.test(text)) {
47
+ return sendFailure(
48
+ "content_blocked",
49
+ `content blocked by gateway \u2014 the gateway's firewall rejected the message content (commonly a literal URL in the body). De-fang or split the content and retry. Gateway said: ${text}`
50
+ );
51
+ }
52
+ if (/token is invalid or expired/i.test(text) || /authentication failed/i.test(text) || /re-?authoriz/i.test(text)) {
53
+ return sendFailure("gateway_auth", text);
54
+ }
55
+ if (/aren'?t a member|not a member/i.test(text)) {
56
+ return sendFailure("not_member", text);
57
+ }
58
+ if (/rate limit/i.test(text)) {
59
+ return sendFailure("rate_limited", text);
60
+ }
61
+ if (/cannot reach kojee gateway/i.test(text)) {
62
+ return sendFailure("network", text);
63
+ }
64
+ if (/^DENIED:/.test(text)) {
65
+ return sendFailure("governance_denied", text);
66
+ }
67
+ if (/^APPROVAL REQUIRED:/.test(text)) {
68
+ return sendFailure("approval_required", text);
69
+ }
70
+ return sendFailure("send_failed", text);
71
+ }
72
+ async function executeSend(gateway, request) {
73
+ const args = {
74
+ tandem_id: request.tandem_id,
75
+ body: request.body,
76
+ ...request.reply_to !== void 0 ? { reply_to: request.reply_to } : {},
77
+ ...request.kind !== void 0 ? { kind: request.kind } : {}
78
+ };
79
+ let result;
80
+ try {
81
+ result = await gateway.sendRpc("tools/call", {
82
+ name: "tandem_send",
83
+ arguments: args
84
+ });
85
+ } catch (err) {
86
+ return classifySendFailure(
87
+ `tandem_send failed: ${err?.message ?? String(err)}`
88
+ );
89
+ }
90
+ const translated = translateToolCallResult(result);
91
+ const text = (translated.content ?? []).map((c) => typeof c?.text === "string" ? c.text : "").filter(Boolean).join("\n");
92
+ if (translated.isError) {
93
+ return classifySendFailure(text || "tandem_send returned an error with no text");
94
+ }
95
+ let messageId = null;
96
+ let cursor = null;
97
+ try {
98
+ const parsed = JSON.parse(text);
99
+ const rawId = parsed["message_id"] ?? parsed["id"];
100
+ if (rawId !== void 0 && rawId !== null) messageId = String(rawId);
101
+ const rawCursor = parsed["cursor"];
102
+ if (typeof rawCursor === "number") cursor = rawCursor;
103
+ } catch {
104
+ }
105
+ return {
106
+ ok: true,
107
+ tandem_id: request.tandem_id,
108
+ message_id: messageId,
109
+ cursor,
110
+ text
111
+ };
112
+ }
113
+ var HTTP_STATUS_BY_CODE = {
114
+ bad_request: 400,
115
+ unauthorized: 401,
116
+ content_blocked: 403,
117
+ member_cap: 403,
118
+ not_member: 403,
119
+ governance_denied: 403,
120
+ approval_required: 403,
121
+ rate_limited: 429,
122
+ // Upstream/credential problems — the LOCAL surface worked, the gateway leg
123
+ // failed: a gateway (502) class from the caller's point of view.
124
+ not_paired: 502,
125
+ not_enrolled: 502,
126
+ gateway_auth: 502,
127
+ network: 502,
128
+ send_failed: 502,
129
+ send_unavailable: 503
130
+ };
131
+ function httpStatusForEnvelope(envelope) {
132
+ if (envelope.ok) return 200;
133
+ return HTTP_STATUS_BY_CODE[envelope.error] ?? 502;
134
+ }
135
+
136
+ export {
137
+ sendFailure,
138
+ parseSendRequest,
139
+ executeSend,
140
+ httpStatusForEnvelope
141
+ };
@@ -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
+ };
@@ -0,0 +1,185 @@
1
+ // src/tandem/webhook-config.ts
2
+ var WEBHOOK_DEFAULT_TIMEOUT_MS = 5e3;
3
+ var WEBHOOK_DEFAULT_MAX_RETRIES = 4;
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) {
@@ -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;
@@ -371,8 +345,6 @@ function normalizeBackendEvent(raw, sseEventType) {
371
345
  }
372
346
 
373
347
  export {
374
- createDPoPProof,
375
- MCP_SESSION_ID,
376
348
  createAdaptiveWatchdog,
377
349
  startEventStream
378
350
  };
package/dist/cli.js CHANGED
@@ -8,10 +8,13 @@ import {
8
8
  AuthModule,
9
9
  VERSION,
10
10
  startProxy
11
- } from "./chunk-YEC7IHIG.js";
11
+ } from "./chunk-62KH6VNQ.js";
12
+ import "./chunk-YVUXQ4Z2.js";
12
13
  import "./chunk-BJMASMKX.js";
13
- import "./chunk-C6GZ2L2W.js";
14
- import "./chunk-WBMX4CHB.js";
14
+ import "./chunk-X672ZN7V.js";
15
+ import "./chunk-36L3GCU3.js";
16
+ import "./chunk-2MIISF2W.js";
17
+ import "./chunk-LDZXU3DW.js";
15
18
  import "./chunk-BLEGIR35.js";
16
19
 
17
20
  // src/cli.ts
@@ -85,7 +88,7 @@ program.command("pair <code>").description("Pair this machine against Kojee usin
85
88
  });
86
89
  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
90
  if (opts.type === "stop") {
88
- const { runStopHook } = await import("./stop-hook-SEPWWETV.js");
91
+ const { runStopHook } = await import("./stop-hook-GO363SMD.js");
89
92
  await runStopHook();
90
93
  process.exit(0);
91
94
  } else if (opts.type === "user-prompt-submit") {
@@ -93,7 +96,7 @@ program.command("hook").description("Run a kojee MCP hook script (called by Clau
93
96
  await runUserPromptSubmitHook();
94
97
  process.exit(0);
95
98
  } else if (opts.type === "codex-stop") {
96
- const { runCodexStopHook } = await import("./codex-stop-hook-JOTBCS5K.js");
99
+ const { runCodexStopHook } = await import("./codex-stop-hook-SWA53ECG.js");
97
100
  await runCodexStopHook();
98
101
  process.exit(0);
99
102
  } else {
@@ -116,8 +119,21 @@ Restart Claude Code for hooks to take effect.`
116
119
  );
117
120
  }
118
121
  });
122
+ program.command("send <tandem_id>").description(
123
+ "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)."
124
+ ).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) => {
125
+ const { runSendCli } = await import("./send-cli-7QJ36YY7.js");
126
+ const { exitCode, envelope } = await runSendCli({
127
+ tandemId,
128
+ body: opts.body,
129
+ ...opts.replyTo !== void 0 ? { replyTo: opts.replyTo } : {},
130
+ ...opts.kind !== void 0 ? { kind: opts.kind } : {}
131
+ });
132
+ process.stdout.write(JSON.stringify(envelope) + "\n");
133
+ process.exit(exitCode);
134
+ });
119
135
  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");
136
+ const { runTail } = await import("./tail-stream-U436QL2X.js");
121
137
  try {
122
138
  await runTail(filePath);
123
139
  } catch (err) {
@@ -126,19 +142,28 @@ program.command("tail <path>").description("Stream a file's contents and follow
126
142
  }
127
143
  });
128
144
  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");
145
+ const { runDoctor } = await import("./doctor-TXWMMSRC.js");
130
146
  const code = await runDoctor();
131
147
  process.exit(code);
132
148
  });
133
149
  program.command("init").description(
134
150
  "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) => {
151
+ ).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(
152
+ "--webhook-signature-format <preset>",
153
+ 'Signature preset: "github" = header X-Hub-Signature-256, value sha256=<hex> (default: bare hex in X-Kojee-Signature)'
154
+ ).option(
155
+ "--webhook-signature-header <name>",
156
+ "Signature header name (default X-Kojee-Signature; overrides the preset's header)"
157
+ ).option(
158
+ "--webhook-signature-prefix <prefix>",
159
+ "Literal string prepended to the hex digest (default empty; overrides the preset's prefix)"
160
+ ).option("--uninstall", "Remove the kojee config for the chosen (or recorded) runtime").action(async (opts) => {
136
161
  const { loadPairedConfig: loadPairedConfig2 } = await import("./paired-config-JTFLHMZ2.js");
137
162
  if (loadPairedConfig2() === null && !opts.uninstall) {
138
163
  console.error("Not paired. Run `kojee-mcp pair <code> --url <broker>` first, then re-run `init`.");
139
164
  process.exit(1);
140
165
  }
141
- const { runWizard } = await import("./wizard-7KHD5JT4.js");
166
+ const { runWizard } = await import("./wizard-Z5JA3YPV.js");
142
167
  const interactive = process.stdin.isTTY === true && opts.runtime === void 0;
143
168
  const result = await runWizard({
144
169
  ...opts.runtime !== void 0 ? { runtime: opts.runtime } : {},
@@ -147,6 +172,9 @@ program.command("init").description(
147
172
  ...opts.hooksPath ? { hooksPath: opts.hooksPath } : {},
148
173
  ...opts.webhookUrl ? { webhookUrl: opts.webhookUrl } : {},
149
174
  ...opts.webhookSecret ? { webhookSecret: opts.webhookSecret } : {},
175
+ ...opts.webhookSignatureFormat ? { webhookSignatureFormat: opts.webhookSignatureFormat } : {},
176
+ ...opts.webhookSignatureHeader ? { webhookSignatureHeader: opts.webhookSignatureHeader } : {},
177
+ ...opts.webhookSignaturePrefix !== void 0 ? { webhookSignaturePrefix: opts.webhookSignaturePrefix } : {},
150
178
  interactive,
151
179
  ...interactive ? { promptRuntime: promptRuntimeFromTty } : {}
152
180
  });
@@ -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";