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,35 @@
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();
31
+
32
+ export {
33
+ createDPoPProof,
34
+ MCP_SESSION_ID
35
+ };
@@ -0,0 +1,223 @@
1
+ import {
2
+ MCP_SESSION_ID,
3
+ createDPoPProof
4
+ } from "./chunk-2MIISF2W.js";
5
+ import {
6
+ translateHttpError,
7
+ translateJsonRpcError,
8
+ translateNetworkError
9
+ } from "./chunk-LDZXU3DW.js";
10
+ import {
11
+ secureDir,
12
+ secureFile
13
+ } from "./chunk-BLEGIR35.js";
14
+
15
+ // src/auth/keystore.ts
16
+ import { importJWK, exportJWK, generateKeyPair } from "jose";
17
+ import crypto from "crypto";
18
+ import fs from "fs";
19
+ import os from "os";
20
+ import path from "path";
21
+ var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
22
+ function defaultPairedKeystorePath() {
23
+ return path.join(os.homedir(), ".kojee", "keypair.json");
24
+ }
25
+ function deriveKeystorePath(token) {
26
+ const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
27
+ return path.join(os.homedir(), ".kojee", `keypair-${hash}.json`);
28
+ }
29
+ async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
30
+ if (!fs.existsSync(keystorePath)) {
31
+ return null;
32
+ }
33
+ const raw = fs.readFileSync(keystorePath, "utf-8");
34
+ const data = JSON.parse(raw);
35
+ if (expectedBrokerUrl && data.broker_url !== expectedBrokerUrl) {
36
+ return null;
37
+ }
38
+ const privateKey = await importJWK(data.private_key_jwk, "ES256");
39
+ return {
40
+ privateKey,
41
+ publicJwk: data.public_jwk,
42
+ kid: data.kid,
43
+ data
44
+ };
45
+ }
46
+ async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath = DEFAULT_PATH) {
47
+ const dir = path.dirname(keystorePath);
48
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
49
+ secureDir(dir);
50
+ const privateJwk = await exportJWK(privateKey);
51
+ const data = {
52
+ private_key_jwk: privateJwk,
53
+ kid,
54
+ broker_url: brokerUrl,
55
+ public_jwk: publicJwk,
56
+ enrolled_at: (/* @__PURE__ */ new Date()).toISOString()
57
+ };
58
+ fs.writeFileSync(keystorePath, JSON.stringify(data, null, 2), {
59
+ mode: 384
60
+ });
61
+ secureFile(keystorePath);
62
+ }
63
+ async function generateES256KeyPair() {
64
+ const { privateKey, publicKey } = await generateKeyPair("ES256");
65
+ const publicJwk = await exportJWK(publicKey);
66
+ publicJwk.kty = "EC";
67
+ publicJwk.crv = "P-256";
68
+ return { privateKey, publicJwk };
69
+ }
70
+
71
+ // src/gateway-client.ts
72
+ import crypto2 from "crypto";
73
+ var GatewayClient = class {
74
+ constructor(brokerUrl, token, privateKey, kid, sessionId) {
75
+ this.brokerUrl = brokerUrl;
76
+ this.token = token;
77
+ this.privateKey = privateKey;
78
+ this.kid = kid;
79
+ this.endpoint = `${brokerUrl}/mcp/messages/${sessionId}/`;
80
+ }
81
+ brokerUrl;
82
+ token;
83
+ privateKey;
84
+ kid;
85
+ currentNonce;
86
+ requestCounter = 0;
87
+ endpoint;
88
+ /**
89
+ * Expose the DPoP signing key so peer modules sharing auth state
90
+ * (e.g. tandem/event-stream.ts) can sign their own requests.
91
+ */
92
+ getPrivateKey() {
93
+ return this.privateKey;
94
+ }
95
+ /**
96
+ * Expose the bot_key_id (kid) for DPoP proof headers. Paired with
97
+ * getPrivateKey() so peer modules can construct proofs without
98
+ * threading the key material through their own constructors.
99
+ */
100
+ getKid() {
101
+ return this.kid;
102
+ }
103
+ /**
104
+ * Derive a deterministic session ID from the gateway token.
105
+ * session_id = sha256(token + "proxy").slice(0, 16)
106
+ */
107
+ static deriveSessionId(token) {
108
+ const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
109
+ return hash.slice(0, 16);
110
+ }
111
+ /**
112
+ * Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth and
113
+ * nonce retry transparently. A 403 `step_up_required` (deprecated feature,
114
+ * owner ruling 2026-06-10) is no longer polled — it surfaces immediately as
115
+ * a structured tool error via translateHttpError.
116
+ *
117
+ * `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
118
+ * underlying `fetch` option — NOT placed inside `params`/`arguments`. A
119
+ * caller with a per-call timeout budget (e.g. resubscribeMemberships) passes
120
+ * its controller's signal here so a hung backend aborts at the budget instead
121
+ * of hanging forever. Putting the signal in `arguments` (the round-2 bug) both
122
+ * left fetch un-aborted AND serialized a junk `{}` onto the wire body.
123
+ */
124
+ async sendRpc(method, params = {}, signal) {
125
+ const rpcRequest = {
126
+ jsonrpc: "2.0",
127
+ id: ++this.requestCounter,
128
+ method,
129
+ params
130
+ };
131
+ return this.executeWithRetries(rpcRequest, signal);
132
+ }
133
+ async executeWithRetries(rpcRequest, signal) {
134
+ let response;
135
+ try {
136
+ response = await this.sendHttpRequest(rpcRequest, signal);
137
+ } catch (err) {
138
+ return translateNetworkError(err);
139
+ }
140
+ this.trackNonce(response);
141
+ if (response.status === 401) {
142
+ const body = await this.tryParseErrorBody(response);
143
+ if (body?.error === "use_dpop_nonce") {
144
+ console.error("[gateway] Nonce expired, retrying with fresh nonce...");
145
+ try {
146
+ response = await this.sendHttpRequest(rpcRequest, signal);
147
+ } catch (err) {
148
+ return translateNetworkError(err);
149
+ }
150
+ this.trackNonce(response);
151
+ } else {
152
+ const translated = translateHttpError(401, body?.error);
153
+ if (translated) return translated;
154
+ }
155
+ }
156
+ if (response.status === 403) {
157
+ const body = await this.tryParseErrorBody(response);
158
+ const translated = translateHttpError(403, body?.error, body?.trigger);
159
+ if (translated) return translated;
160
+ }
161
+ if (!response.ok) {
162
+ const body = await this.tryParseErrorBody(response);
163
+ const translated = translateHttpError(response.status, body?.error);
164
+ if (translated) return translated;
165
+ return {
166
+ content: [{ type: "text", text: `Gateway error: ${response.status}` }],
167
+ isError: true
168
+ };
169
+ }
170
+ const rpcResponse = await response.json();
171
+ if (rpcResponse.error) {
172
+ return translateJsonRpcError(rpcResponse.error);
173
+ }
174
+ const result = rpcResponse.result;
175
+ return result ?? { content: [{ type: "text", text: "No result" }] };
176
+ }
177
+ async sendHttpRequest(rpcRequest, signal) {
178
+ const proof = await createDPoPProof(
179
+ this.privateKey,
180
+ this.kid,
181
+ "POST",
182
+ this.endpoint,
183
+ this.currentNonce,
184
+ this.token
185
+ );
186
+ return fetch(this.endpoint, {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ Authorization: `DPoP ${this.token}`,
191
+ DPoP: proof,
192
+ "Mcp-Session-Id": MCP_SESSION_ID
193
+ },
194
+ body: JSON.stringify(rpcRequest),
195
+ // ROUND-3 MAJOR A: the caller's AbortSignal rides HERE (a real fetch
196
+ // option), never inside the JSON-RPC body. `undefined` is a valid value
197
+ // for the fetch `signal` option (no abort wired).
198
+ ...signal ? { signal } : {}
199
+ });
200
+ }
201
+ trackNonce(response) {
202
+ const nonce = response.headers.get("DPoP-Nonce");
203
+ if (nonce) {
204
+ this.currentNonce = nonce;
205
+ }
206
+ }
207
+ async tryParseErrorBody(response) {
208
+ try {
209
+ return await response.json();
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+ };
215
+
216
+ export {
217
+ defaultPairedKeystorePath,
218
+ deriveKeystorePath,
219
+ loadKeystore,
220
+ saveKeystore,
221
+ generateES256KeyPair,
222
+ GatewayClient
223
+ };
@@ -24,7 +24,9 @@ function buildCodexMcpServerTable(opts) {
24
24
  "[mcp_servers.kojee.env]",
25
25
  'KOJEE_RUNTIME = "codex"',
26
26
  `KOJEE_WEBHOOK_URL = "${escapeTomlString(opts.webhookUrl)}"`,
27
- `KOJEE_WEBHOOK_SECRET = "${escapeTomlString(opts.webhookSecret)}"`
27
+ `KOJEE_WEBHOOK_SECRET = "${escapeTomlString(opts.webhookSecret)}"`,
28
+ // Signature emission overrides (0.5.3) — emitted only when configured.
29
+ ...(opts.signatureEnv ?? []).map(([k, v]) => `${k} = "${escapeTomlString(v)}"`)
28
30
  ].join("\n");
29
31
  }
30
32
  function buildCodexStopHookBlock() {
@@ -48,7 +50,7 @@ function writeCodexConfig(inputs) {
48
50
  toml = fs.readFileSync(configPath, "utf8");
49
51
  } catch {
50
52
  }
51
- toml = upsertKojeeTomlTables(toml, inputs.webhookUrl, inputs.webhookSecret);
53
+ toml = upsertKojeeTomlTables(toml, inputs.webhookUrl, inputs.webhookSecret, inputs.signatureEnv ?? []);
52
54
  writeFile600(configPath, toml);
53
55
  const hooks = readJson(hooksPath);
54
56
  hooks.hooks ??= {};
@@ -93,10 +95,14 @@ function removeCodexConfig(opts = {}) {
93
95
  }
94
96
  return result;
95
97
  }
96
- function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret) {
98
+ function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret, signatureEnv = []) {
97
99
  const parsed = extractKojeeBlock(existing);
98
100
  if (!parsed) {
99
- const block = buildCodexMcpServerTable({ webhookUrl, webhookSecret });
101
+ const block = buildCodexMcpServerTable({
102
+ webhookUrl,
103
+ webhookSecret,
104
+ ...signatureEnv.length > 0 ? { signatureEnv } : {}
105
+ });
100
106
  const base2 = existing.replace(/\s*$/, "");
101
107
  return base2.length === 0 ? block + "\n" : base2 + "\n\n" + block + "\n";
102
108
  }
@@ -107,7 +113,10 @@ function upsertKojeeTomlTables(existing, webhookUrl, webhookSecret) {
107
113
  const envKeys = upsertKeyLines(parsed.envKeys, [
108
114
  ["KOJEE_RUNTIME", '"codex"'],
109
115
  ["KOJEE_WEBHOOK_URL", `"${escapeTomlString(webhookUrl)}"`],
110
- ["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`]
116
+ ["KOJEE_WEBHOOK_SECRET", `"${escapeTomlString(webhookSecret)}"`],
117
+ // Signature emission overrides (0.5.3) — owned only when configured this
118
+ // run; an operator's existing signature lines are otherwise preserved.
119
+ ...signatureEnv.map(([k, v]) => [k, `"${escapeTomlString(v)}"`])
111
120
  ]);
112
121
  const merged = [
113
122
  KOJEE_TABLE_HEADER,
@@ -0,0 +1,142 @@
1
+ import {
2
+ generateES256KeyPair,
3
+ loadKeystore,
4
+ saveKeystore
5
+ } from "./chunk-3XDJOHMZ.js";
6
+
7
+ // src/auth/auth-module.ts
8
+ import { calculateJwkThumbprint } from "jose";
9
+ import crypto from "crypto";
10
+
11
+ // src/auth/registration.ts
12
+ async function registerKey(brokerUrl, token, publicJwk) {
13
+ const url = `${brokerUrl}/api/v1/bots/keys/register/`;
14
+ const response = await fetch(url, {
15
+ method: "POST",
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ Authorization: `Bearer ${token}`
19
+ },
20
+ body: JSON.stringify({ public_jwk: publicJwk })
21
+ });
22
+ if (!response.ok) {
23
+ const body = await response.text();
24
+ throw new Error(
25
+ `Key registration failed (${response.status}): ${body}`
26
+ );
27
+ }
28
+ return await response.json();
29
+ }
30
+ async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
31
+ const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
32
+ const response = await fetch(url, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ Authorization: `Bearer ${token}`
37
+ },
38
+ body: JSON.stringify({
39
+ bot_key_id: botKeyId,
40
+ challenge,
41
+ signature
42
+ })
43
+ });
44
+ if (!response.ok) {
45
+ const body = await response.text();
46
+ throw new Error(
47
+ `Key confirmation failed (${response.status}): ${body}`
48
+ );
49
+ }
50
+ return await response.json();
51
+ }
52
+
53
+ // src/auth/auth-module.ts
54
+ async function signChallengeRaw(privateKey, data) {
55
+ const signer = crypto.createSign("SHA256");
56
+ signer.update(data);
57
+ signer.end();
58
+ const derSignature = signer.sign(
59
+ privateKey
60
+ );
61
+ return derSignature.toString("base64url");
62
+ }
63
+ var AuthModule = class {
64
+ constructor(token, brokerUrl, keystorePath) {
65
+ this.token = token;
66
+ this.brokerUrl = brokerUrl;
67
+ this.keystorePath = keystorePath;
68
+ }
69
+ token;
70
+ brokerUrl;
71
+ keystorePath;
72
+ privateKey = null;
73
+ publicJwk = null;
74
+ kid = null;
75
+ /**
76
+ * Ensure we have an enrolled keypair. Either loads from disk or
77
+ * performs the full enrollment flow.
78
+ */
79
+ async ensureEnrolled() {
80
+ const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
81
+ if (existing) {
82
+ this.privateKey = existing.privateKey;
83
+ this.publicJwk = existing.publicJwk;
84
+ this.kid = existing.kid;
85
+ console.error("[auth] Loaded existing keypair from keystore");
86
+ return {
87
+ privateKey: existing.privateKey,
88
+ publicJwk: existing.publicJwk,
89
+ kid: existing.kid
90
+ };
91
+ }
92
+ console.error("[auth] No valid keystore found, enrolling new keypair...");
93
+ const { privateKey, publicJwk } = await generateES256KeyPair();
94
+ const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
95
+ console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
96
+ const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
97
+ const challengeData = `${regResult.challenge}.${thumbprint}`;
98
+ const signature = await signChallengeRaw(privateKey, challengeData);
99
+ const confirmResult = await confirmKey(
100
+ this.brokerUrl,
101
+ this.token,
102
+ regResult.bot_key_id,
103
+ regResult.challenge,
104
+ signature
105
+ );
106
+ if (!confirmResult.key_confirmed) {
107
+ throw new Error("Key enrollment failed: confirmation was rejected");
108
+ }
109
+ console.error("[auth] Key enrollment confirmed");
110
+ await saveKeystore(
111
+ privateKey,
112
+ publicJwk,
113
+ regResult.bot_key_id,
114
+ this.brokerUrl,
115
+ this.keystorePath
116
+ );
117
+ this.privateKey = privateKey;
118
+ this.publicJwk = publicJwk;
119
+ this.kid = regResult.bot_key_id;
120
+ return {
121
+ privateKey,
122
+ publicJwk,
123
+ kid: regResult.bot_key_id
124
+ };
125
+ }
126
+ getPrivateKey() {
127
+ if (!this.privateKey) throw new Error("Not enrolled yet");
128
+ return this.privateKey;
129
+ }
130
+ getPublicJwk() {
131
+ if (!this.publicJwk) throw new Error("Not enrolled yet");
132
+ return this.publicJwk;
133
+ }
134
+ getKid() {
135
+ if (!this.kid) throw new Error("Not enrolled yet");
136
+ return this.kid;
137
+ }
138
+ };
139
+
140
+ export {
141
+ AuthModule
142
+ };
@@ -0,0 +1,46 @@
1
+ import {
2
+ secureDir,
3
+ secureFile
4
+ } from "./chunk-BLEGIR35.js";
5
+
6
+ // src/tandem/control-token.ts
7
+ import crypto from "crypto";
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import path from "path";
11
+ function controlTokenPath(kojeeDir = path.join(os.homedir(), ".kojee")) {
12
+ return path.join(kojeeDir, "control-token");
13
+ }
14
+ function issueControlToken(filePath = controlTokenPath()) {
15
+ const token = crypto.randomBytes(32).toString("hex");
16
+ const dir = path.dirname(filePath);
17
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
18
+ secureDir(dir);
19
+ try {
20
+ fs.unlinkSync(filePath);
21
+ } catch (err) {
22
+ if (err.code !== "ENOENT") throw err;
23
+ }
24
+ fs.writeFileSync(filePath, token + "\n", { mode: 384, flag: "wx" });
25
+ secureFile(filePath);
26
+ return token;
27
+ }
28
+ function loadControlToken(filePath = controlTokenPath()) {
29
+ try {
30
+ const raw = fs.readFileSync(filePath, "utf8").trim();
31
+ return raw.length > 0 ? raw : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function controlTokenAuthHeaders(filePath) {
37
+ const token = loadControlToken(filePath);
38
+ return token ? { Authorization: `Bearer ${token}` } : {};
39
+ }
40
+
41
+ export {
42
+ controlTokenPath,
43
+ issueControlToken,
44
+ loadControlToken,
45
+ controlTokenAuthHeaders
46
+ };
@@ -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
+ };