@mnemom/mnemom 0.17.0-next.6 → 0.17.0-next.8

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 (34) hide show
  1. package/README.md +26 -0
  2. package/dist/commands/agent-config.d.ts +29 -0
  3. package/dist/commands/agent-config.js +193 -0
  4. package/dist/commands/{code-doctor.d.ts → agent-doctor.d.ts} +2 -2
  5. package/dist/commands/{code-doctor.js → agent-doctor.js} +27 -22
  6. package/dist/commands/{code-invite.d.ts → agent-invite.d.ts} +18 -18
  7. package/dist/commands/{code-invite.js → agent-invite.js} +30 -30
  8. package/dist/commands/agent-session.d.ts +68 -0
  9. package/dist/commands/agent-session.js +114 -0
  10. package/dist/commands/{code-setup.d.ts → agent-setup.d.ts} +13 -13
  11. package/dist/commands/{code-setup.js → agent-setup.js} +46 -42
  12. package/dist/commands/{code.d.ts → agent.d.ts} +50 -14
  13. package/dist/commands/{code.js → agent.js} +144 -86
  14. package/dist/commands/agents.d.ts +2 -2
  15. package/dist/commands/agents.js +5 -5
  16. package/dist/commands/onboard.js +1 -1
  17. package/dist/commands/wrap.js +1 -1
  18. package/dist/index.js +59 -51
  19. package/dist/lib/{code.d.ts → agent.d.ts} +52 -10
  20. package/dist/lib/{code.js → agent.js} +94 -15
  21. package/dist/lib/api.d.ts +4 -4
  22. package/dist/lib/api.js +9 -9
  23. package/dist/lib/auth.js +21 -1
  24. package/dist/lib/{code-config.d.ts → launcher-config.d.ts} +25 -23
  25. package/dist/lib/{code-config.js → launcher-config.js} +50 -31
  26. package/dist/lib/oauth.d.ts +26 -4
  27. package/dist/lib/oauth.js +98 -29
  28. package/dist/lib/version-gate.js +1 -1
  29. package/dist/rc-proxy.mjs +309 -136
  30. package/package.json +7 -3
  31. package/dist/commands/code-config.d.ts +0 -17
  32. package/dist/commands/code-config.js +0 -147
  33. package/dist/commands/code-session.d.ts +0 -52
  34. package/dist/commands/code-session.js +0 -78
package/dist/rc-proxy.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // mnemom-code-rc-proxy — the local proxy behind ``mnemom code … --remote-control``.
2
+ // mnemom-code-rc-proxy — the local proxy behind ``mnemom agent … --remote-control``.
3
3
  //
4
4
  // WHY THIS EXISTS
5
5
  // Claude Code's `remote-control` subcommand (drive a local session from claude.ai / the
6
6
  // mobile app) is gated: it refuses to start unless the CLI believes it talks to
7
7
  // api.anthropic.com with a claude.ai (OAuth) login — no ANTHROPIC_BASE_URL, no
8
8
  // ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN, no ANTHROPIC_UNIX_SOCKET. Every knob the
9
- // normal `mnemom code` launch uses to reach the Mnemom gateway trips that gate.
9
+ // normal `mnemom agent` launch uses to reach the Mnemom gateway trips that gate.
10
10
  // HTTPS_PROXY + NODE_EXTRA_CA_CERTS are honoured and NOT gated (measured 2026-09-13,
11
11
  // Claude Code 2.1.269). So `--remote-control` runs Claude Code untouched — OAuth login,
12
12
  // first-party base URL — and puts this proxy in front of it.
@@ -19,7 +19,7 @@
19
19
  // * Inside api.anthropic.com, only `POST /v1/messages[?…]` is rewritten to the Mnemom
20
20
  // gateway door: the real Anthropic key rides as `x-api-key` (API-key billing), plus
21
21
  // `x-mnemom-agent`, `x-mnemom-conversation-id`, and optionally `x-mnemom-api-key`,
22
- // `x-mnemom-provider`, `x-mnemom-egress-key`, `x-mnemom-contract` — the exact header set
22
+ // `x-mnemom-provider`, `x-mnemom-egress-key`, `x-mnemom-contract` / `x-mnemom-goal-mode` — the exact header set
23
23
  // the non-RC launch sends via ANTHROPIC_CUSTOM_HEADERS. The OAuth `authorization` bearer
24
24
  // is STRIPPED on that leg (it never leaves the machine) and `oauth-*` tokens are removed
25
25
  // from `anthropic-beta`. Everything else under api.anthropic.com (OAuth refresh, the
@@ -28,7 +28,7 @@
28
28
  // * Serves BOTH request forms: CONNECT tunnels and absolute-form plaintext requests to the
29
29
  // proxy port. Remote Control registration and the environment work-poll loop use the
30
30
  // latter for https URLs; a CONNECT-only proxy answers 405 and registration fails.
31
- // * Body handling on the gateway leg (see the mnemom code docs (Remote Control)):
31
+ // * Body handling on the gateway leg (see the mnemom agent docs (Remote Control)):
32
32
  // - deletes every `cache_control.ttl` — Claude Code on claude.ai auth marks blocks
33
33
  // `ttl:"1h"`; the gateway prepends its own 5m-cached system block and Anthropic
34
34
  // rejects a 1h block that follows a 5m one (400). Forwarded bodies only.
@@ -46,105 +46,193 @@
46
46
  // one more local 400: typically one or two `-> local 400` lines early in the
47
47
  // session, each an instant local round trip, and zero gateway 400s. A `thread: null`
48
48
  // is not a thread — the key is deleted and the body forwarded.
49
- // MNEMOM_CODE_RC_THREAD_MODE=strip restores the legacy delete-and-forward behaviour
49
+ // MNEMOM_AGENT_RC_THREAD_MODE=strip restores the legacy delete-and-forward behaviour
50
50
  // (diagnostic only: it forwards continue-deltas the API rejects).
51
51
  //
52
52
  // SECRETS
53
53
  // Arrive on stdin as three newline-terminated lines: Anthropic key, Mnemom key (may be
54
54
  // empty), OpenAI egress key (may be empty). Never argv, never env, never disk, never logged.
55
- // The log (MNEMOM_CODE_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
55
+ // The log (MNEMOM_AGENT_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
56
56
  // gateway's `x-mnemom-agent` / `x-mnemom-goal-verdict` response headers only. Gateway error
57
57
  // bodies (status ≥ 400) are logged truncated so a 4xx is diagnosable (compressed ones as
58
58
  // `body=<encoding> <N> bytes`). Locally answered thread 400s log the thread type only.
59
59
  //
60
60
  // CONFIG (env, none secret)
61
- // MNEMOM_CODE_RC_DOOR gateway door URL (required), e.g. https://gateway.mnemom.ai/anthropic
62
- // MNEMOM_CODE_RC_AGENT x-mnemom-agent value (required)
63
- // MNEMOM_CODE_RC_CONVERSATION_ID x-mnemom-conversation-id value (required)
64
- // MNEMOM_CODE_RC_CONTRACT_B64 base64 contract for x-mnemom-contract (optional)
65
- // MNEMOM_CODE_RC_PROVIDER x-mnemom-provider directive JSON (optional; router door)
66
- // MNEMOM_CODE_RC_THREAD_MODE `reject` (default: answer thread-bearing requests locally,
61
+ // MNEMOM_AGENT_RC_DOOR gateway door URL (required), e.g. https://gateway.mnemom.ai/anthropic
62
+ // MNEMOM_AGENT_RC_AGENT x-mnemom-agent value (required)
63
+ // MNEMOM_AGENT_RC_CONVERSATION_ID x-mnemom-conversation-id value (required)
64
+ // MNEMOM_AGENT_RC_CONTRACT_B64 base64 contract for x-mnemom-contract (optional)
65
+ // MNEMOM_AGENT_RC_GOAL_MODE x-mnemom-goal-mode value (optional; `--implicit` — no contract,
66
+ // the gateway infers the goal from the user's messages)
67
+ // MNEMOM_AGENT_RC_PROVIDER x-mnemom-provider directive JSON (optional; router door)
68
+ // MNEMOM_AGENT_RC_THREAD_MODE `reject` (default: answer thread-bearing requests locally,
67
69
  // see above) | `strip` (legacy: delete `thread`, forward)
68
- // MNEMOM_CODE_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
69
- // MNEMOM_CODE_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
70
- // MNEMOM_CODE_RC_PORT listen port (default 0 = ephemeral)
70
+ // MNEMOM_AGENT_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
71
+ // MNEMOM_AGENT_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
72
+ // MNEMOM_AGENT_RC_PORT listen port (default 0 = ephemeral)
71
73
  //
72
74
  // stdout: exactly one line, `READY <port> <ca.pem path>`, once listening. Node ≥ 18, no deps.
73
- import http from 'node:http';
74
- import https from 'node:https';
75
- import net from 'node:net';
76
- import fs from 'node:fs';
77
- import os from 'node:os';
78
- import path from 'node:path';
79
- import { execFileSync } from 'node:child_process';
75
+ import http from "node:http";
76
+ import https from "node:https";
77
+ import net from "node:net";
78
+ import fs from "node:fs";
79
+ import os from "node:os";
80
+ import path from "node:path";
81
+ import { execFileSync } from "node:child_process";
80
82
 
81
- const MITM_HOST = 'api.anthropic.com';
83
+ const MITM_HOST = "api.anthropic.com";
82
84
  const env = process.env;
83
85
 
84
- function die(msg) { process.stderr.write(`mnemom-code-rc-proxy: ${msg}\n`); process.exit(2); }
86
+ function die(msg) {
87
+ process.stderr.write(`mnemom-code-rc-proxy: ${msg}\n`);
88
+ process.exit(2);
89
+ }
85
90
 
86
- if (!env.MNEMOM_CODE_RC_DOOR) die('MNEMOM_CODE_RC_DOOR is required');
91
+ if (!env.MNEMOM_AGENT_RC_DOOR) die("MNEMOM_AGENT_RC_DOOR is required");
87
92
  let GATEWAY;
88
- try { GATEWAY = new URL(env.MNEMOM_CODE_RC_DOOR); } catch { die(`MNEMOM_CODE_RC_DOOR is not a URL: ${env.MNEMOM_CODE_RC_DOOR}`); }
89
- if (GATEWAY.protocol !== 'https:') die('MNEMOM_CODE_RC_DOOR must be https');
90
- const AGENT = env.MNEMOM_CODE_RC_AGENT || die('MNEMOM_CODE_RC_AGENT is required');
91
- const CONV = env.MNEMOM_CODE_RC_CONVERSATION_ID || die('MNEMOM_CODE_RC_CONVERSATION_ID is required');
92
- const CONTRACT_B64 = env.MNEMOM_CODE_RC_CONTRACT_B64 || '';
93
- const PROVIDER = env.MNEMOM_CODE_RC_PROVIDER || '';
94
- const THREAD_MODE = env.MNEMOM_CODE_RC_THREAD_MODE || 'reject';
95
- if (THREAD_MODE !== 'reject' && THREAD_MODE !== 'strip') die(`MNEMOM_CODE_RC_THREAD_MODE must be 'reject' or 'strip', got '${THREAD_MODE}'`);
96
- const LOGFILE = env.MNEMOM_CODE_RC_PROXY_LOG || '';
97
- const PARENT = Number(env.MNEMOM_CODE_RC_PARENT_PID || 0);
98
- const PORT = Number(env.MNEMOM_CODE_RC_PORT || 0);
93
+ try {
94
+ GATEWAY = new URL(env.MNEMOM_AGENT_RC_DOOR);
95
+ } catch {
96
+ die(`MNEMOM_AGENT_RC_DOOR is not a URL: ${env.MNEMOM_AGENT_RC_DOOR}`);
97
+ }
98
+ if (GATEWAY.protocol !== "https:") die("MNEMOM_AGENT_RC_DOOR must be https");
99
+ const AGENT = env.MNEMOM_AGENT_RC_AGENT || die("MNEMOM_AGENT_RC_AGENT is required");
100
+ const CONV =
101
+ env.MNEMOM_AGENT_RC_CONVERSATION_ID || die("MNEMOM_AGENT_RC_CONVERSATION_ID is required");
102
+ const CONTRACT_B64 = env.MNEMOM_AGENT_RC_CONTRACT_B64 || "";
103
+ const GOAL_MODE = env.MNEMOM_AGENT_RC_GOAL_MODE || "";
104
+ const PROVIDER = env.MNEMOM_AGENT_RC_PROVIDER || "";
105
+ const THREAD_MODE = env.MNEMOM_AGENT_RC_THREAD_MODE || "reject";
106
+ if (THREAD_MODE !== "reject" && THREAD_MODE !== "strip")
107
+ die(`MNEMOM_AGENT_RC_THREAD_MODE must be 'reject' or 'strip', got '${THREAD_MODE}'`);
108
+ const LOGFILE = env.MNEMOM_AGENT_RC_PROXY_LOG || "";
109
+ const PARENT = Number(env.MNEMOM_AGENT_RC_PARENT_PID || 0);
110
+ const PORT = Number(env.MNEMOM_AGENT_RC_PORT || 0);
99
111
 
100
112
  const log = (line) => {
101
- const s = `${new Date().toISOString()} ${line}\n`;
102
- if (LOGFILE) { try { fs.appendFileSync(LOGFILE, s); return; } catch { /* fall through */ } }
113
+ // One record per line: gateway header values and error bodies are spliced into log lines, so a
114
+ // CR/LF inside them must not fabricate extra records (log injection).
115
+ const s = `${new Date().toISOString()} ${String(line).replace(/[\r\n]+/g, " ")}\n`;
116
+ if (LOGFILE) {
117
+ try {
118
+ fs.appendFileSync(LOGFILE, s);
119
+ return;
120
+ } catch {
121
+ /* fall through */
122
+ }
123
+ }
103
124
  process.stderr.write(s);
104
125
  };
105
126
 
106
127
  // --- secrets: three lines on stdin ------------------------------------------------------
107
128
  let secrets;
108
129
  try {
109
- const lines = fs.readFileSync(0, 'utf8').split('\n');
110
- secrets = { anthropic: (lines[0] || '').trim(), mnemom: (lines[1] || '').trim(), openai: (lines[2] || '').trim() };
130
+ const lines = fs.readFileSync(0, "utf8").split("\n");
131
+ secrets = {
132
+ anthropic: (lines[0] || "").trim(),
133
+ mnemom: (lines[1] || "").trim(),
134
+ openai: (lines[2] || "").trim(),
135
+ };
111
136
  } catch {
112
- die('expected three lines of secrets on stdin (anthropic, mnemom, openai)');
137
+ die("expected three lines of secrets on stdin (anthropic, mnemom, openai)");
113
138
  }
114
- if (!secrets.anthropic) die('the Anthropic key (stdin line 1) is empty');
139
+ if (!secrets.anthropic) die("the Anthropic key (stdin line 1) is empty");
115
140
 
116
141
  // --- ephemeral CA + leaf for api.anthropic.com ------------------------------------------
117
142
  // Private 0700 dir under the OS tmpdir; the CA key is deleted as soon as the leaf is signed
118
143
  // (only the leaf key is needed to serve sessions) and the whole dir goes on exit. Works with
119
144
  // macOS's system LibreSSL (3.3+) and OpenSSL 1.1.1+ (`-addext`).
120
- const caDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mnemom-code-rc-ca-'));
145
+ const caDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-code-rc-ca-"));
121
146
  fs.chmodSync(caDir, 0o700);
122
147
  const p = (f) => path.join(caDir, f);
123
- const cleanup = () => { try { fs.rmSync(caDir, { recursive: true, force: true }); } catch { /* best effort */ } };
124
- process.on('exit', cleanup);
125
- for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => process.exit(0));
148
+ const cleanup = () => {
149
+ try {
150
+ fs.rmSync(caDir, { recursive: true, force: true });
151
+ } catch {
152
+ /* best effort */
153
+ }
154
+ };
155
+ process.on("exit", cleanup);
156
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
126
157
  try {
127
- const ossl = (...a) => execFileSync('openssl', a, { stdio: ['ignore', 'ignore', 'pipe'] });
128
- ossl('req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes',
129
- '-keyout', p('ca.key'), '-out', p('ca.pem'), '-days', '2', '-subj', '/CN=mnemom code local CA (ephemeral)',
130
- '-addext', 'basicConstraints=critical,CA:TRUE', '-addext', 'keyUsage=critical,keyCertSign,cRLSign');
131
- ossl('req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes',
132
- '-keyout', p('leaf.key'), '-out', p('leaf.csr'), '-subj', `/CN=${MITM_HOST}`);
133
- fs.writeFileSync(p('leaf.ext'),
134
- `subjectAltName=DNS:${MITM_HOST}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`);
135
- ossl('x509', '-req', '-in', p('leaf.csr'), '-CA', p('ca.pem'), '-CAkey', p('ca.key'), '-CAcreateserial',
136
- '-out', p('leaf.pem'), '-days', '2', '-extfile', p('leaf.ext'));
137
- fs.rmSync(p('ca.key'));
158
+ const ossl = (...a) => execFileSync("openssl", a, { stdio: ["ignore", "ignore", "pipe"] });
159
+ ossl(
160
+ "req",
161
+ "-x509",
162
+ "-newkey",
163
+ "ec",
164
+ "-pkeyopt",
165
+ "ec_paramgen_curve:prime256v1",
166
+ "-nodes",
167
+ "-keyout",
168
+ p("ca.key"),
169
+ "-out",
170
+ p("ca.pem"),
171
+ "-days",
172
+ "2",
173
+ "-subj",
174
+ "/CN=mnemom agent local CA (ephemeral)",
175
+ "-addext",
176
+ "basicConstraints=critical,CA:TRUE",
177
+ "-addext",
178
+ "keyUsage=critical,keyCertSign,cRLSign",
179
+ );
180
+ ossl(
181
+ "req",
182
+ "-newkey",
183
+ "ec",
184
+ "-pkeyopt",
185
+ "ec_paramgen_curve:prime256v1",
186
+ "-nodes",
187
+ "-keyout",
188
+ p("leaf.key"),
189
+ "-out",
190
+ p("leaf.csr"),
191
+ "-subj",
192
+ `/CN=${MITM_HOST}`,
193
+ );
194
+ fs.writeFileSync(
195
+ p("leaf.ext"),
196
+ `subjectAltName=DNS:${MITM_HOST}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`,
197
+ );
198
+ ossl(
199
+ "x509",
200
+ "-req",
201
+ "-in",
202
+ p("leaf.csr"),
203
+ "-CA",
204
+ p("ca.pem"),
205
+ "-CAkey",
206
+ p("ca.key"),
207
+ "-CAcreateserial",
208
+ "-out",
209
+ p("leaf.pem"),
210
+ "-days",
211
+ "2",
212
+ "-extfile",
213
+ p("leaf.ext"),
214
+ );
215
+ fs.rmSync(p("ca.key"));
138
216
  } catch (e) {
139
- die(`could not mint the ephemeral CA with openssl: ${e.stderr ? e.stderr.toString() : e.message}`);
217
+ die(
218
+ `could not mint the ephemeral CA with openssl: ${e.stderr ? e.stderr.toString() : e.message}`,
219
+ );
140
220
  }
141
221
 
142
222
  // --- request handling ---------------------------------------------------------------------
143
223
  const gatewayAgent = new https.Agent({ keepAlive: false }); // a reused socket to the gateway gave ECONNRESET on retry
144
224
  const upstreamAgent = new https.Agent({ keepAlive: true, maxSockets: 32 });
145
- const HOP = new Set(['connection', 'proxy-connection', 'keep-alive', 'upgrade', 'host', 'te', 'trailer']);
225
+ const HOP = new Set([
226
+ "connection",
227
+ "proxy-connection",
228
+ "keep-alive",
229
+ "upgrade",
230
+ "host",
231
+ "te",
232
+ "trailer",
233
+ ]);
146
234
 
147
- const isMessagesPath = (u) => u === '/v1/messages' || u.startsWith('/v1/messages?');
235
+ const isMessagesPath = (u) => u === "/v1/messages" || u.startsWith("/v1/messages?");
148
236
 
149
237
  // Inspect + normalise a /v1/messages body for the gateway leg. Returns
150
238
  // { body: <possibly rewritten Buffer>, thread: null | 'create' | 'continue' | 'other' }
@@ -157,24 +245,34 @@ const isMessagesPath = (u) => u === '/v1/messages' || u.startsWith('/v1/messages
157
245
  // unchanged apart from `cache_control.ttl`, because the caller will not forward it.
158
246
  function normalizeBody(body) {
159
247
  try {
160
- const j = JSON.parse(body.toString('utf8'));
248
+ const j = JSON.parse(body.toString("utf8"));
161
249
  let n = 0;
162
250
  const walk = (o) => {
163
- if (Array.isArray(o)) { for (const x of o) walk(x); return; }
164
- if (o && typeof o === 'object') {
165
- if (o.cache_control && typeof o.cache_control === 'object' && 'ttl' in o.cache_control) { delete o.cache_control.ttl; n++; }
251
+ if (Array.isArray(o)) {
252
+ for (const x of o) walk(x);
253
+ return;
254
+ }
255
+ if (o && typeof o === "object") {
256
+ if (o.cache_control && typeof o.cache_control === "object" && "ttl" in o.cache_control) {
257
+ delete o.cache_control.ttl;
258
+ n++;
259
+ }
166
260
  for (const v of Object.values(o)) walk(v);
167
261
  }
168
262
  };
169
263
  walk(j);
170
264
  let thread = null;
171
- if (j && typeof j === 'object' && !Array.isArray(j) && 'thread' in j) {
265
+ if (j && typeof j === "object" && !Array.isArray(j) && "thread" in j) {
172
266
  if (j.thread === null || j.thread === undefined) {
173
- delete j.thread; n++; // not a thread: forward the rest
267
+ delete j.thread;
268
+ n++; // not a thread: forward the rest
174
269
  } else {
175
- const t = typeof j.thread === 'object' ? j.thread.type : undefined;
176
- thread = t === 'create' || t === 'continue' ? t : 'other';
177
- if (THREAD_MODE === 'strip') { delete j.thread; n++; }
270
+ const t = typeof j.thread === "object" ? j.thread.type : undefined;
271
+ thread = t === "create" || t === "continue" ? t : "other";
272
+ if (THREAD_MODE === "strip") {
273
+ delete j.thread;
274
+ n++;
275
+ }
178
276
  }
179
277
  }
180
278
  return { body: n ? Buffer.from(JSON.stringify(j)) : body, thread };
@@ -187,36 +285,50 @@ function normalizeBody(body) {
187
285
  // classifier must land on "unsupported_request": `details.error_code` is the primary
188
286
  // signal, and the message names the beta, the `thread` param and `previous_message_id`
189
287
  // so the fallback regexes agree even if the details path were ever ignored.
190
- const THREAD_400 = Buffer.from(JSON.stringify({
191
- type: 'error',
192
- error: {
193
- type: 'invalid_request_error',
194
- message: 'message threads (beta message-threads-2026-08-12: top-level thread param / previous_message_id) are not supported on the mnemom gateway leg; resend the full transcript without a thread',
195
- details: { error_code: 'thread_unsupported_request' },
196
- },
197
- }));
288
+ const THREAD_400 = Buffer.from(
289
+ JSON.stringify({
290
+ type: "error",
291
+ error: {
292
+ type: "invalid_request_error",
293
+ message:
294
+ "message threads (beta message-threads-2026-08-12: top-level thread param / previous_message_id) are not supported on the mnemom gateway leg; resend the full transcript without a thread",
295
+ details: { error_code: "thread_unsupported_request" },
296
+ },
297
+ }),
298
+ );
198
299
  let localSeq = 0;
199
300
  function rejectThread(res, tag, thread) {
200
301
  res.writeHead(400, {
201
- 'content-type': 'application/json',
202
- 'content-length': String(THREAD_400.length),
203
- 'request-id': `mnemom-code-rc-proxy-${++localSeq}`,
302
+ "content-type": "application/json",
303
+ "content-length": String(THREAD_400.length),
304
+ "request-id": `mnemom-code-rc-proxy-${++localSeq}`,
204
305
  });
205
306
  res.end(THREAD_400);
206
- log(`${tag} -> local 400 thread=${thread} (thread_unsupported_request; Claude Code resends this turn stateless and stops threading for this agent+model for the rest of the session — a subagent or model switch takes one more local 400)`);
307
+ log(
308
+ `${tag} -> local 400 thread=${thread} (thread_unsupported_request; Claude Code resends this turn stateless and stops threading for this agent+model for the rest of the session — a subagent or model switch takes one more local 400)`,
309
+ );
207
310
  }
208
311
 
209
312
  // One handler serves both the MITM'd TLS stream (origin-form URLs, host = api.anthropic.com)
210
313
  // and absolute-form plaintext requests sent straight to the proxy port.
211
314
  function handle(req, res, viaPlain) {
212
- let host = MITM_HOST, port = 443, urlPath = req.url, proto = https;
315
+ let host = MITM_HOST,
316
+ port = 443,
317
+ urlPath = req.url,
318
+ proto = https;
213
319
  if (viaPlain) {
214
320
  let u;
215
- try { u = new URL(req.url); } catch { res.writeHead(400); res.end('mnemom-code-rc-proxy: absolute-form URL required\n'); return; }
321
+ try {
322
+ u = new URL(req.url);
323
+ } catch {
324
+ res.writeHead(400);
325
+ res.end("mnemom-code-rc-proxy: absolute-form URL required\n");
326
+ return;
327
+ }
216
328
  host = u.hostname;
217
- port = Number(u.port || (u.protocol === 'http:' ? 80 : 443));
329
+ port = Number(u.port || (u.protocol === "http:" ? 80 : 443));
218
330
  urlPath = u.pathname + u.search;
219
- proto = u.protocol === 'http:' ? http : https;
331
+ proto = u.protocol === "http:" ? http : https;
220
332
  }
221
333
  const headers = {};
222
334
  for (const [k, v] of Object.entries(req.headers)) if (!HOP.has(k)) headers[k] = v;
@@ -224,27 +336,42 @@ function handle(req, res, viaPlain) {
224
336
  const toGateway = host === MITM_HOST && isMessagesPath(urlPath);
225
337
  let target;
226
338
  if (toGateway) {
227
- target = { hostname: GATEWAY.hostname, port: GATEWAY.port || 443,
228
- path: GATEWAY.pathname.replace(/\/$/, '') + urlPath, agent: gatewayAgent };
229
- delete headers.authorization; // the OAuth bearer stays local
230
- headers['x-api-key'] = secrets.anthropic;
231
- headers['x-mnemom-agent'] = AGENT;
232
- headers['x-mnemom-conversation-id'] = CONV;
233
- if (secrets.mnemom) headers['x-mnemom-api-key'] = secrets.mnemom;
234
- if (PROVIDER) headers['x-mnemom-provider'] = PROVIDER;
235
- if (secrets.openai) headers['x-mnemom-egress-key'] = secrets.openai;
236
- if (CONTRACT_B64) headers['x-mnemom-contract'] = CONTRACT_B64;
237
- if (headers['anthropic-beta']) { // drop the OAuth-only beta tokens
238
- const b = String(headers['anthropic-beta']).split(',').map((s) => s.trim()).filter((s) => s && !s.startsWith('oauth-'));
239
- if (b.length) headers['anthropic-beta'] = b.join(','); else delete headers['anthropic-beta'];
339
+ target = {
340
+ hostname: GATEWAY.hostname,
341
+ port: GATEWAY.port || 443,
342
+ path: GATEWAY.pathname.replace(/\/$/, "") + urlPath,
343
+ agent: gatewayAgent,
344
+ };
345
+ delete headers.authorization; // the OAuth bearer stays local
346
+ headers["x-api-key"] = secrets.anthropic;
347
+ headers["x-mnemom-agent"] = AGENT;
348
+ headers["x-mnemom-conversation-id"] = CONV;
349
+ if (secrets.mnemom) headers["x-mnemom-api-key"] = secrets.mnemom;
350
+ if (PROVIDER) headers["x-mnemom-provider"] = PROVIDER;
351
+ if (secrets.openai) headers["x-mnemom-egress-key"] = secrets.openai;
352
+ if (CONTRACT_B64) headers["x-mnemom-contract"] = CONTRACT_B64;
353
+ if (GOAL_MODE) headers["x-mnemom-goal-mode"] = GOAL_MODE;
354
+ if (headers["anthropic-beta"]) {
355
+ // drop the OAuth-only beta tokens
356
+ const b = String(headers["anthropic-beta"])
357
+ .split(",")
358
+ .map((s) => s.trim())
359
+ .filter((s) => s && !s.startsWith("oauth-"));
360
+ if (b.length) headers["anthropic-beta"] = b.join(",");
361
+ else delete headers["anthropic-beta"];
240
362
  }
241
363
  } else {
242
- target = { hostname: host, port, path: urlPath, agent: proto === https ? upstreamAgent : undefined };
364
+ target = {
365
+ hostname: host,
366
+ port,
367
+ path: urlPath,
368
+ agent: proto === https ? upstreamAgent : undefined,
369
+ };
243
370
  }
244
371
 
245
372
  const started = Date.now();
246
- const tag = `${req.method} ${viaPlain ? host : ''}${urlPath}`;
247
- const leg = toGateway ? 'gateway' : 'upstream';
373
+ const tag = `${req.method} ${viaPlain ? host : ""}${urlPath}`;
374
+ const leg = toGateway ? "gateway" : "upstream";
248
375
 
249
376
  // Open the upstream request only once we know we are forwarding. `body` is the fully
250
377
  // buffered (normalised) gateway body; undefined means stream the client request through.
@@ -256,45 +383,72 @@ function handle(req, res, viaPlain) {
256
383
  // Gateway error bodies are logged (truncated) so a 4xx is diagnosable — unless the
257
384
  // body is compressed, in which case only its encoding + size is useful
258
385
  // (`identity` is not a compression).
259
- let errBody = '', errBytes = 0;
260
- const enc = ur.headers['content-encoding'];
261
- const errEnc = enc && enc !== 'identity' ? enc : '';
386
+ let errBody = "",
387
+ errBytes = 0;
388
+ const enc = ur.headers["content-encoding"];
389
+ const errEnc = enc && enc !== "identity" ? enc : "";
262
390
  if (toGateway && ur.statusCode >= 400) {
263
- ur.on('data', (c) => { errBytes += c.length; if (!errEnc && errBody.length < 1500) errBody += c.toString('utf8'); });
391
+ ur.on("data", (c) => {
392
+ errBytes += c.length;
393
+ if (!errEnc && errBody.length < 1500) errBody += c.toString("utf8");
394
+ });
264
395
  }
265
396
  ur.pipe(res);
266
- const ident = toGateway ? ` agent=${ur.headers['x-mnemom-agent'] || '-'} verdict=${ur.headers['x-mnemom-goal-verdict'] || '-'}` : '';
267
- ur.on('end', () => {
268
- let bodyNote = '';
397
+ const goalRev = ur.headers["x-mnemom-goal-revision"];
398
+ const steer = ur.headers["x-mnemom-goal-steer"];
399
+ const ident = toGateway
400
+ ? ` agent=${ur.headers["x-mnemom-agent"] || "-"} verdict=${ur.headers["x-mnemom-goal-verdict"] || "-"}` +
401
+ (goalRev ? ` goal-rev=${goalRev}` : "") +
402
+ (steer ? ` steer=${steer}` : "")
403
+ : "";
404
+ ur.on("end", () => {
405
+ let bodyNote = "";
269
406
  if (toGateway && ur.statusCode >= 400) {
270
- bodyNote = errEnc ? ` body=${errEnc} ${errBytes} bytes` : (errBody ? ' body=' + errBody.slice(0, 1500).replace(/\s+/g, ' ') : '');
407
+ bodyNote = errEnc
408
+ ? ` body=${errEnc} ${errBytes} bytes`
409
+ : errBody
410
+ ? " body=" + errBody.slice(0, 1500).replace(/\s+/g, " ")
411
+ : "";
271
412
  }
272
413
  log(`${tag} -> ${leg} ${ur.statusCode} ${Date.now() - started}ms${ident}${bodyNote}`);
273
414
  });
274
415
  });
275
- up.on('error', (e) => {
416
+ up.on("error", (e) => {
276
417
  log(`${tag} -> ${leg} ERROR ${e.code || e.message}`);
277
- if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });
278
- res.end(JSON.stringify({ type: 'error', error: { type: 'proxy_error', message: `mnemom-code-rc-proxy upstream: ${e.code || e.message}` } }));
418
+ if (!res.headersSent) res.writeHead(502, { "content-type": "application/json" });
419
+ res.end(
420
+ JSON.stringify({
421
+ type: "error",
422
+ error: {
423
+ type: "proxy_error",
424
+ message: `mnemom-code-rc-proxy upstream: ${e.code || e.message}`,
425
+ },
426
+ }),
427
+ );
428
+ });
429
+ res.on("close", () => {
430
+ if (!up.destroyed) up.destroy();
279
431
  });
280
- res.on('close', () => { if (!up.destroyed) up.destroy(); });
281
432
  if (body !== undefined) {
282
- up.removeHeader('transfer-encoding');
283
- up.setHeader('content-length', String(body.length));
433
+ up.removeHeader("transfer-encoding");
434
+ up.setHeader("content-length", String(body.length));
284
435
  up.end(body);
285
436
  } else {
286
437
  req.pipe(up);
287
438
  }
288
439
  };
289
440
 
290
- if (toGateway && req.method === 'POST') {
441
+ if (toGateway && req.method === "POST") {
291
442
  // Buffer the body first: a thread-bearing request is answered here and nothing may be
292
443
  // sent to (or left half-open toward) the gateway for it.
293
444
  const chunks = [];
294
- req.on('data', (c) => chunks.push(c));
295
- req.on('end', () => {
445
+ req.on("data", (c) => chunks.push(c));
446
+ req.on("end", () => {
296
447
  const { body, thread } = normalizeBody(Buffer.concat(chunks));
297
- if (thread !== null && THREAD_MODE === 'reject') { rejectThread(res, tag, thread); return; }
448
+ if (thread !== null && THREAD_MODE === "reject") {
449
+ rejectThread(res, tag, thread);
450
+ return;
451
+ }
298
452
  forward(body);
299
453
  });
300
454
  } else {
@@ -303,39 +457,58 @@ function handle(req, res, viaPlain) {
303
457
  }
304
458
 
305
459
  const tlsServer = https.createServer(
306
- { key: fs.readFileSync(p('leaf.key')), cert: fs.readFileSync(p('leaf.pem')) },
460
+ { key: fs.readFileSync(p("leaf.key")), cert: fs.readFileSync(p("leaf.pem")) },
307
461
  (req, res) => handle(req, res, false),
308
462
  );
309
- tlsServer.on('tlsClientError', (e) => log(`tls client error: ${e.code || e.message}`));
463
+ tlsServer.on("tlsClientError", (e) => log(`tls client error: ${e.code || e.message}`));
310
464
 
311
465
  const proxy = http.createServer((req, res) => handle(req, res, true));
312
- proxy.on('connect', (req, sock, head) => {
313
- const [host, portStr] = req.url.split(':');
466
+ proxy.on("connect", (req, sock, head) => {
467
+ const [host, portStr] = req.url.split(":");
314
468
  const port = Number(portStr || 443);
315
- sock.on('error', () => {});
469
+ sock.on("error", () => {});
316
470
  if (host === MITM_HOST && port === 443) {
317
- sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
471
+ sock.write("HTTP/1.1 200 Connection Established\r\n\r\n");
318
472
  if (head && head.length) sock.unshift(head);
319
- tlsServer.emit('connection', sock);
473
+ tlsServer.emit("connection", sock);
320
474
  return;
321
475
  }
322
476
  const up = net.connect(port, host, () => {
323
- sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
477
+ sock.write("HTTP/1.1 200 Connection Established\r\n\r\n");
324
478
  if (head && head.length) up.write(head);
325
- up.pipe(sock); sock.pipe(up);
479
+ up.pipe(sock);
480
+ sock.pipe(up);
326
481
  });
327
- up.on('error', (e) => { log(`CONNECT ${req.url} tunnel ERROR ${e.code || e.message}`); sock.destroy(); });
328
- sock.on('close', () => up.destroy());
329
- up.on('close', () => sock.destroy());
482
+ up.on("error", (e) => {
483
+ log(`CONNECT ${req.url} tunnel ERROR ${e.code || e.message}`);
484
+ sock.destroy();
485
+ });
486
+ sock.on("close", () => up.destroy());
487
+ up.on("close", () => sock.destroy());
330
488
  log(`CONNECT ${req.url} tunnel`);
331
489
  });
332
- proxy.on('clientError', (e, sock) => { try { sock.destroy(); } catch { /* already gone */ } });
490
+ proxy.on("clientError", (e, sock) => {
491
+ try {
492
+ sock.destroy();
493
+ } catch {
494
+ /* already gone */
495
+ }
496
+ });
333
497
 
334
- proxy.listen(PORT, '127.0.0.1', () => {
498
+ proxy.listen(PORT, "127.0.0.1", () => {
335
499
  const { port } = proxy.address();
336
- log(`listening 127.0.0.1:${port} agent=${AGENT} conv=${CONV} door=${GATEWAY.href} contract=${CONTRACT_B64 ? 'yes' : 'no'} provider=${PROVIDER || '-'} thread=${THREAD_MODE}`);
337
- process.stdout.write(`READY ${port} ${p('ca.pem')}\n`);
500
+ log(
501
+ `listening 127.0.0.1:${port} agent=${AGENT} conv=${CONV} door=${GATEWAY.href} contract=${CONTRACT_B64 ? "yes" : "no"} goal_mode=${GOAL_MODE || "-"} provider=${PROVIDER || "-"} thread=${THREAD_MODE}`,
502
+ );
503
+ process.stdout.write(`READY ${port} ${p("ca.pem")}\n`);
338
504
  });
339
505
 
340
506
  // Die with the launcher so no orphan keeps the secrets in memory.
341
- if (PARENT) setInterval(() => { try { process.kill(PARENT, 0); } catch { process.exit(0); } }, 2000).unref();
507
+ if (PARENT)
508
+ setInterval(() => {
509
+ try {
510
+ process.kill(PARENT, 0);
511
+ } catch {
512
+ process.exit(0);
513
+ }
514
+ }, 2000).unref();