@remnic/connector-weclone 1.0.1 → 9.3.515

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.
package/README.md CHANGED
@@ -11,6 +11,8 @@ avatar remembers what happened yesterday and sounds like you while doing it.
11
11
  - Runs as a local OpenAI-compatible HTTP proxy in front of a WeClone API server.
12
12
  - On every `POST /v1/chat/completions`, calls Remnic `/engram/v1/recall` and injects
13
13
  retrieved memory into the system prompt before forwarding to WeClone.
14
+ - Preserves OpenAI-compatible message metadata and end-to-end request headers
15
+ while injecting memory; only the injected system-message content is rewritten.
14
16
  - After WeClone responds, calls `/engram/v1/observe` fire-and-forget so the turn is
15
17
  buffered for extraction.
16
18
  - Forwards all other OpenAI-compatible endpoints (`/v1/models`, uploads, etc.)
@@ -67,6 +69,8 @@ The proxy config file accepts the following fields:
67
69
  | `wecloneApiUrl` | `http://localhost:8000/v1` | Base URL of the WeClone API. Both path-prefixed (`/v1`, `/weclone/v1`) and bare origins are supported. |
68
70
  | `wecloneModelName` | `weclone-avatar` | Optional fine-tuned model name passed through to WeClone. |
69
71
  | `proxyPort` | `8100` | Local port the proxy listens on. |
72
+ | `proxyBindHost` | `127.0.0.1` | Host/interface the proxy binds to. Defaults to loopback only. |
73
+ | `allowPublicBind` | `false` | Must be `true` to bind `proxyBindHost` to `0.0.0.0` or `::`. |
70
74
  | `remnicDaemonUrl` | `http://localhost:4318` | URL of the Remnic daemon exposing `/engram/v1/recall` and `/engram/v1/observe`. |
71
75
  | `remnicAuthToken` | — | Bearer token for the Remnic daemon. Populated by `remnic connectors install weclone`. |
72
76
  | `sessionStrategy` | `single` | `single` uses one shared memory session; `caller-id` maps each caller (via `X-Caller-Id` header or `user` field) to its own namespace. |
@@ -80,6 +84,7 @@ The proxy config file accepts the following fields:
80
84
  {
81
85
  "wecloneApiUrl": "http://localhost:8000/v1",
82
86
  "proxyPort": 8100,
87
+ "proxyBindHost": "127.0.0.1",
83
88
  "remnicDaemonUrl": "http://localhost:4318",
84
89
  "remnicAuthToken": "${REMNIC_TOKEN}",
85
90
  "sessionStrategy": "caller-id",
@@ -30,6 +30,15 @@ function formatMemoryBlock(memories, template, maxTokens) {
30
30
  }
31
31
 
32
32
  // src/session.ts
33
+ function headerValue(headers, key) {
34
+ const normalizedKey = key.toLowerCase();
35
+ for (const [headerKey, raw] of Object.entries(headers)) {
36
+ if (headerKey.toLowerCase() !== normalizedKey) continue;
37
+ const value = Array.isArray(raw) ? raw[0] : raw;
38
+ return typeof value === "string" ? value : void 0;
39
+ }
40
+ return void 0;
41
+ }
33
42
  var SingleSessionMapper = class {
34
43
  key;
35
44
  constructor(key = "weclone-default") {
@@ -45,9 +54,9 @@ var CallerIdSessionMapper = class {
45
54
  this.fallback = fallback;
46
55
  }
47
56
  resolve(headers, body) {
48
- const headerValue = headers["x-caller-id"];
49
- if (typeof headerValue === "string" && headerValue.length > 0) {
50
- return headerValue;
57
+ const callerId = headerValue(headers, "x-caller-id");
58
+ if (callerId && callerId.length > 0) {
59
+ return callerId;
51
60
  }
52
61
  if (typeof body.user === "string" && body.user.length > 0) {
53
62
  return body.user;
@@ -58,22 +67,109 @@ var CallerIdSessionMapper = class {
58
67
 
59
68
  // src/proxy.ts
60
69
  import * as http from "http";
61
- function readBody(req) {
70
+ var DEFAULT_MAX_REQUEST_BYTES = 10 * 1024 * 1024;
71
+ var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
72
+ var DEFAULT_STREAM_OBSERVATION_MAX_BYTES = 1024 * 1024;
73
+ var BodyLimitExceededError = class extends Error {
74
+ constructor(limitBytes) {
75
+ super(`body exceeds ${limitBytes} byte limit`);
76
+ this.limitBytes = limitBytes;
77
+ this.name = "BodyLimitExceededError";
78
+ }
79
+ limitBytes;
80
+ };
81
+ function readBody(req, maxBytes) {
62
82
  return new Promise((resolve, reject) => {
63
83
  const chunks = [];
64
- req.on("data", (chunk) => chunks.push(chunk));
65
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
66
- req.on("error", reject);
84
+ let totalBytes = 0;
85
+ let exceeded = false;
86
+ req.on("data", (chunk) => {
87
+ if (exceeded) return;
88
+ totalBytes += chunk.byteLength;
89
+ if (totalBytes > maxBytes) {
90
+ exceeded = true;
91
+ reject(new BodyLimitExceededError(maxBytes));
92
+ req.resume();
93
+ return;
94
+ }
95
+ chunks.push(chunk);
96
+ });
97
+ req.on("end", () => {
98
+ if (!exceeded) resolve(Buffer.concat(chunks).toString("utf-8"));
99
+ });
100
+ req.on("error", (err) => {
101
+ if (!exceeded) reject(err);
102
+ });
67
103
  });
68
104
  }
69
- function readRawBody(req) {
105
+ function readRawBody(req, maxBytes) {
70
106
  return new Promise((resolve, reject) => {
71
107
  const chunks = [];
72
- req.on("data", (chunk) => chunks.push(chunk));
73
- req.on("end", () => resolve(Buffer.concat(chunks)));
74
- req.on("error", reject);
108
+ let totalBytes = 0;
109
+ let exceeded = false;
110
+ req.on("data", (chunk) => {
111
+ if (exceeded) return;
112
+ totalBytes += chunk.byteLength;
113
+ if (totalBytes > maxBytes) {
114
+ exceeded = true;
115
+ reject(new BodyLimitExceededError(maxBytes));
116
+ req.resume();
117
+ return;
118
+ }
119
+ chunks.push(chunk);
120
+ });
121
+ req.on("end", () => {
122
+ if (!exceeded) resolve(Buffer.concat(chunks));
123
+ });
124
+ req.on("error", (err) => {
125
+ if (!exceeded) reject(err);
126
+ });
127
+ });
128
+ }
129
+ async function readResponseBuffer(response, maxBytes) {
130
+ const reader = response.body?.getReader();
131
+ if (!reader) return Buffer.alloc(0);
132
+ const chunks = [];
133
+ let totalBytes = 0;
134
+ while (true) {
135
+ const { done, value } = await reader.read();
136
+ if (done) break;
137
+ totalBytes += value.byteLength;
138
+ if (totalBytes > maxBytes) {
139
+ await reader.cancel().catch(() => {
140
+ });
141
+ throw new BodyLimitExceededError(maxBytes);
142
+ }
143
+ chunks.push(Buffer.from(value));
144
+ }
145
+ return Buffer.concat(chunks);
146
+ }
147
+ function waitForResponseDrain(res) {
148
+ if (res.destroyed || res.writableEnded) return Promise.resolve("closed");
149
+ return new Promise((resolve) => {
150
+ const cleanup = () => {
151
+ res.off("drain", onDrain);
152
+ res.off("close", onClose);
153
+ res.off("error", onClose);
154
+ };
155
+ const onDrain = () => {
156
+ cleanup();
157
+ resolve("drain");
158
+ };
159
+ const onClose = () => {
160
+ cleanup();
161
+ resolve("closed");
162
+ };
163
+ res.once("drain", onDrain);
164
+ res.once("close", onClose);
165
+ res.once("error", onClose);
75
166
  });
76
167
  }
168
+ async function writeResponseChunkRespectingBackpressure(res, chunk) {
169
+ if (res.destroyed || res.writableEnded) return false;
170
+ if (res.write(chunk)) return true;
171
+ return await waitForResponseDrain(res) === "drain";
172
+ }
77
173
  function flattenHeaders(raw) {
78
174
  const result = {};
79
175
  for (const [key, val] of Object.entries(raw)) {
@@ -82,6 +178,20 @@ function flattenHeaders(raw) {
82
178
  }
83
179
  return result;
84
180
  }
181
+ function forwardRequestHeaders(headers, options = {}) {
182
+ const forwardHeaders = {};
183
+ for (const [key, value] of Object.entries(headers)) {
184
+ const lowerKey = key.toLowerCase();
185
+ if (lowerKey === "host" || HOP_BY_HOP_REQUEST_HEADERS.has(lowerKey)) continue;
186
+ if (lowerKey === "content-length") continue;
187
+ if (options.reserializedJson && lowerKey === "content-type") continue;
188
+ forwardHeaders[key] = value;
189
+ }
190
+ if (options.reserializedJson) {
191
+ forwardHeaders["Content-Type"] = "application/json";
192
+ }
193
+ return forwardHeaders;
194
+ }
85
195
  function remnicHeaders(authToken) {
86
196
  const headers = { "Content-Type": "application/json" };
87
197
  if (authToken) {
@@ -142,6 +252,9 @@ function lastUserMessage(messages) {
142
252
  }
143
253
  return "";
144
254
  }
255
+ function isPlainRecord(value) {
256
+ return value !== null && typeof value === "object" && !Array.isArray(value);
257
+ }
145
258
  function extractAssistantReply(responseBody) {
146
259
  const choices = responseBody.choices;
147
260
  if (choices && choices.length > 0) {
@@ -197,7 +310,7 @@ var HOP_BY_HOP_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
197
310
  "te",
198
311
  "trailer"
199
312
  ]);
200
- async function transparentProxy(weclone, method, path, headers, body, res) {
313
+ async function transparentProxy(weclone, method, path, headers, body, res, maxResponseBytes) {
201
314
  const qIdx = path.indexOf("?");
202
315
  const rawPath = qIdx === -1 ? path : path.slice(0, qIdx);
203
316
  const querySuffix = qIdx === -1 ? "" : path.slice(qIdx);
@@ -210,12 +323,7 @@ async function transparentProxy(weclone, method, path, headers, body, res) {
210
323
  }
211
324
  }
212
325
  const targetUrl = `${weclone.origin}${upstreamPathname}${querySuffix}`;
213
- const forwardHeaders = {};
214
- for (const [key, value] of Object.entries(headers)) {
215
- if (key === "host" || HOP_BY_HOP_REQUEST_HEADERS.has(key)) continue;
216
- if (key === "content-length") continue;
217
- forwardHeaders[key] = value;
218
- }
326
+ const forwardHeaders = forwardRequestHeaders(headers);
219
327
  const fetchInit = {
220
328
  method,
221
329
  headers: forwardHeaders
@@ -227,8 +335,7 @@ async function transparentProxy(weclone, method, path, headers, body, res) {
227
335
  }
228
336
  try {
229
337
  const upstream = await fetch(targetUrl, fetchInit);
230
- const responseBody = await upstream.arrayBuffer();
231
- const responseBuffer = Buffer.from(responseBody);
338
+ const responseBuffer = await readResponseBuffer(upstream, maxResponseBytes);
232
339
  const responseHeaders = {};
233
340
  for (const [key, value] of upstream.headers.entries()) {
234
341
  if (!HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase())) {
@@ -238,7 +345,12 @@ async function transparentProxy(weclone, method, path, headers, body, res) {
238
345
  responseHeaders["content-length"] = String(responseBuffer.length);
239
346
  res.writeHead(upstream.status, responseHeaders);
240
347
  res.end(responseBuffer);
241
- } catch (_err) {
348
+ } catch (err) {
349
+ if (err instanceof BodyLimitExceededError) {
350
+ res.writeHead(502, { "Content-Type": "application/json" });
351
+ res.end(JSON.stringify({ error: "upstream_response_too_large" }));
352
+ return;
353
+ }
242
354
  res.writeHead(502, { "Content-Type": "application/json" });
243
355
  res.end(JSON.stringify({ error: "upstream_unreachable" }));
244
356
  }
@@ -247,9 +359,14 @@ function createWeCloneProxy(config) {
247
359
  const wecloneApiUrl = stripTrailingSlashes(config.wecloneApiUrl);
248
360
  const remnicDaemonUrl = stripTrailingSlashes(config.remnicDaemonUrl);
249
361
  const wecloneParts = splitBaseUrl(wecloneApiUrl);
362
+ const maxRequestBytes = config.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES;
363
+ const maxResponseBytes = config.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
364
+ const streamObservationMaxBytes = config.streamObservationMaxBytes ?? DEFAULT_STREAM_OBSERVATION_MAX_BYTES;
365
+ const proxyBindHost = config.proxyBindHost ?? "127.0.0.1";
250
366
  const sessionMapper = config.sessionStrategy === "caller-id" ? new CallerIdSessionMapper() : new SingleSessionMapper();
251
367
  let server = null;
252
368
  let resolvedPort = config.proxyPort;
369
+ let resolvedHost = proxyBindHost;
253
370
  const requestHandler = async (req, res) => {
254
371
  const url = req.url ?? "/";
255
372
  const method = (req.method ?? "GET").toUpperCase();
@@ -268,20 +385,36 @@ function createWeCloneProxy(config) {
268
385
  if (normalizedPathname === "/v1/chat/completions" && method === "POST") {
269
386
  let bodyStr;
270
387
  try {
271
- bodyStr = await readBody(req);
272
- } catch {
388
+ bodyStr = await readBody(req, maxRequestBytes);
389
+ } catch (err) {
390
+ if (err instanceof BodyLimitExceededError) {
391
+ res.writeHead(413, { "Content-Type": "application/json" });
392
+ res.end(JSON.stringify({ error: "request_body_too_large" }));
393
+ return;
394
+ }
273
395
  res.writeHead(400, { "Content-Type": "application/json" });
274
396
  res.end(JSON.stringify({ error: "bad_request", detail: "Could not read request body" }));
275
397
  return;
276
398
  }
277
- let parsed;
399
+ let parsedJson;
278
400
  try {
279
- parsed = JSON.parse(bodyStr);
401
+ parsedJson = JSON.parse(bodyStr);
280
402
  } catch {
281
403
  res.writeHead(400, { "Content-Type": "application/json" });
282
404
  res.end(JSON.stringify({ error: "bad_request", detail: "Invalid JSON body" }));
283
405
  return;
284
406
  }
407
+ if (!isPlainRecord(parsedJson)) {
408
+ res.writeHead(400, { "Content-Type": "application/json" });
409
+ res.end(
410
+ JSON.stringify({
411
+ error: "bad_request",
412
+ detail: "JSON body must be an object"
413
+ })
414
+ );
415
+ return;
416
+ }
417
+ const parsed = parsedJson;
285
418
  const headers = req.headers;
286
419
  const sessionKey = sessionMapper.resolve(headers, parsed);
287
420
  if (parsed.messages !== void 0 && !Array.isArray(parsed.messages)) {
@@ -299,6 +432,7 @@ function createWeCloneProxy(config) {
299
432
  if (raw === null || typeof raw !== "object") continue;
300
433
  const entry = raw;
301
434
  rawMessages.push({
435
+ ...entry,
302
436
  role: typeof entry.role === "string" ? entry.role : "",
303
437
  content: entry.content
304
438
  });
@@ -335,6 +469,7 @@ function createWeCloneProxy(config) {
335
469
  if (i === firstSystemIdx) {
336
470
  const existing = extractTextContent(m.content);
337
471
  outMessages.push({
472
+ ...m,
338
473
  role: "system",
339
474
  content: position === "system-prepend" ? `${memoryBlock}
340
475
 
@@ -349,19 +484,16 @@ ${memoryBlock}`
349
484
  }
350
485
  const modifiedBody = {
351
486
  ...parsed,
487
+ ...config.wecloneModelName ? { model: config.wecloneModelName } : {},
352
488
  messages: outMessages
353
489
  };
354
490
  const chatBase = wecloneParts.basePath.length > 0 ? wecloneParts.basePath : "/v1";
355
491
  const qIdx = url.indexOf("?");
356
492
  const querySuffix = qIdx === -1 ? "" : url.slice(qIdx);
357
493
  const targetUrl = `${wecloneParts.origin}${chatBase}/chat/completions${querySuffix}`;
358
- const forwardHeaders = {
359
- "Content-Type": "application/json"
360
- };
361
- const authHeader = req.headers["authorization"];
362
- if (typeof authHeader === "string") {
363
- forwardHeaders["Authorization"] = authHeader;
364
- }
494
+ const forwardHeaders = forwardRequestHeaders(flattenHeaders(req.headers), {
495
+ reserializedJson: true
496
+ });
365
497
  try {
366
498
  const upstream = await fetch(targetUrl, {
367
499
  method: "POST",
@@ -370,11 +502,11 @@ ${memoryBlock}`
370
502
  });
371
503
  if (parsed.stream === true) {
372
504
  if (!upstream.ok) {
373
- const errBody = await upstream.arrayBuffer();
505
+ const errBody = await readResponseBuffer(upstream, maxResponseBytes);
374
506
  res.writeHead(upstream.status, {
375
507
  "content-type": upstream.headers.get("content-type") || "application/json"
376
508
  });
377
- res.end(Buffer.from(errBody));
509
+ res.end(errBody);
378
510
  return;
379
511
  }
380
512
  res.writeHead(upstream.status, {
@@ -387,35 +519,98 @@ ${memoryBlock}`
387
519
  res.end();
388
520
  return;
389
521
  }
390
- const chunks = [];
522
+ let clientClosed = false;
523
+ const onClientClose = () => {
524
+ clientClosed = true;
525
+ void reader.cancel().catch(() => {
526
+ });
527
+ };
528
+ res.once("close", onClientClose);
529
+ const decoder = new TextDecoder();
530
+ let streamBuffer = "";
531
+ let assistantContent = "";
532
+ let streamedResponseBytes = 0;
533
+ let streamLimitExceeded = false;
534
+ let observationTextBytes = 0;
535
+ let observationDisabled = false;
536
+ const disableObservationBuffer = () => {
537
+ observationDisabled = true;
538
+ streamBuffer = "";
539
+ assistantContent = "";
540
+ observationTextBytes = 0;
541
+ };
542
+ const appendObservationText = (text) => {
543
+ const nextBytes = observationTextBytes + Buffer.byteLength(text, "utf8");
544
+ if (nextBytes > streamObservationMaxBytes) {
545
+ disableObservationBuffer();
546
+ return;
547
+ }
548
+ observationTextBytes = nextBytes;
549
+ assistantContent += text;
550
+ };
551
+ const consumeSseLine = (line) => {
552
+ if (!line.startsWith("data: ") || line === "data: [DONE]") return;
553
+ try {
554
+ const event = JSON.parse(line.slice(6));
555
+ const delta = event.choices?.[0]?.delta?.content;
556
+ if (delta) appendObservationText(delta);
557
+ } catch {
558
+ }
559
+ };
391
560
  try {
392
561
  while (true) {
562
+ if (clientClosed) break;
393
563
  const { done, value } = await reader.read();
394
564
  if (done) break;
395
- chunks.push(value);
396
- res.write(value);
565
+ streamedResponseBytes += value.byteLength;
566
+ if (streamedResponseBytes > maxResponseBytes) {
567
+ streamLimitExceeded = true;
568
+ await reader.cancel().catch(() => {
569
+ });
570
+ break;
571
+ }
572
+ const wrote = await writeResponseChunkRespectingBackpressure(res, value);
573
+ if (!wrote) {
574
+ clientClosed = true;
575
+ await reader.cancel().catch(() => {
576
+ });
577
+ break;
578
+ }
579
+ if (observationDisabled) continue;
580
+ if (Buffer.byteLength(streamBuffer, "utf8") + value.byteLength > streamObservationMaxBytes) {
581
+ disableObservationBuffer();
582
+ continue;
583
+ }
584
+ streamBuffer += decoder.decode(value, { stream: true });
585
+ const lines = streamBuffer.split("\n");
586
+ streamBuffer = lines.pop() ?? "";
587
+ for (const line of lines) {
588
+ consumeSseLine(line);
589
+ }
397
590
  }
398
591
  } finally {
399
- res.end();
592
+ res.off("close", onClientClose);
593
+ if (!res.destroyed && !res.writableEnded) {
594
+ res.end();
595
+ }
400
596
  }
597
+ if (clientClosed || streamLimitExceeded) return;
401
598
  try {
402
- const fullText = Buffer.concat(chunks).toString("utf-8");
403
- const contentParts = [];
404
- for (const line of fullText.split("\n")) {
405
- if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
406
- try {
407
- const event = JSON.parse(line.slice(6));
408
- const delta = event.choices?.[0]?.delta?.content;
409
- if (delta) contentParts.push(delta);
410
- } catch {
599
+ if (!observationDisabled) {
600
+ const tail = decoder.decode();
601
+ if (tail) streamBuffer += tail;
602
+ if (streamBuffer.length > 0) {
603
+ for (const line of streamBuffer.split("\n")) {
604
+ consumeSseLine(line);
605
+ }
411
606
  }
412
607
  }
413
- if (contentParts.length > 0 && query.length > 0) {
608
+ if (!observationDisabled && assistantContent.length > 0 && query.length > 0) {
414
609
  observeTurn(
415
610
  remnicDaemonUrl,
416
611
  sessionKey,
417
612
  query,
418
- contentParts.join(""),
613
+ assistantContent,
419
614
  config.remnicAuthToken
420
615
  );
421
616
  }
@@ -423,8 +618,7 @@ ${memoryBlock}`
423
618
  }
424
619
  return;
425
620
  }
426
- const responseBuffer = await upstream.arrayBuffer();
427
- const responseBytes = Buffer.from(responseBuffer);
621
+ const responseBytes = await readResponseBuffer(upstream, maxResponseBytes);
428
622
  let assistantReply = "";
429
623
  try {
430
624
  const responseJson = JSON.parse(
@@ -445,7 +639,12 @@ ${memoryBlock}`
445
639
  chatResponseHeaders["content-length"] = String(responseBytes.length);
446
640
  res.writeHead(upstream.status, chatResponseHeaders);
447
641
  res.end(responseBytes);
448
- } catch (_err) {
642
+ } catch (err) {
643
+ if (err instanceof BodyLimitExceededError) {
644
+ res.writeHead(502, { "Content-Type": "application/json" });
645
+ res.end(JSON.stringify({ error: "upstream_response_too_large" }));
646
+ return;
647
+ }
449
648
  res.writeHead(502, { "Content-Type": "application/json" });
450
649
  res.end(JSON.stringify({
451
650
  error: "upstream_unreachable"
@@ -453,14 +652,27 @@ ${memoryBlock}`
453
652
  }
454
653
  return;
455
654
  }
456
- const body = method !== "GET" && method !== "HEAD" ? await readRawBody(req) : null;
655
+ let body = null;
656
+ try {
657
+ body = method !== "GET" && method !== "HEAD" ? await readRawBody(req, maxRequestBytes) : null;
658
+ } catch (err) {
659
+ if (err instanceof BodyLimitExceededError) {
660
+ res.writeHead(413, { "Content-Type": "application/json" });
661
+ res.end(JSON.stringify({ error: "request_body_too_large" }));
662
+ return;
663
+ }
664
+ throw err;
665
+ }
457
666
  const flat = flattenHeaders(req.headers);
458
- await transparentProxy(wecloneParts, method, url, flat, body, res);
667
+ await transparentProxy(wecloneParts, method, url, flat, body, res, maxResponseBytes);
459
668
  };
460
669
  return {
461
670
  get port() {
462
671
  return resolvedPort;
463
672
  },
673
+ get host() {
674
+ return resolvedHost;
675
+ },
464
676
  start() {
465
677
  return new Promise((resolve, reject) => {
466
678
  server = http.createServer((req, res) => {
@@ -472,10 +684,11 @@ ${memoryBlock}`
472
684
  });
473
685
  });
474
686
  server.on("error", reject);
475
- server.listen(config.proxyPort, () => {
687
+ server.listen(config.proxyPort, proxyBindHost, () => {
476
688
  const addr = server.address();
477
689
  if (typeof addr === "object" && addr !== null) {
478
690
  resolvedPort = addr.port;
691
+ resolvedHost = addr.address;
479
692
  }
480
693
  resolve();
481
694
  });
@@ -498,10 +711,13 @@ ${memoryBlock}`
498
711
  }
499
712
 
500
713
  // src/config.ts
714
+ import * as net from "net";
501
715
  var DEFAULT_CONFIG = {
502
716
  wecloneApiUrl: "http://localhost:8000/v1",
503
717
  wecloneModelName: "weclone-avatar",
504
718
  proxyPort: 8100,
719
+ proxyBindHost: "127.0.0.1",
720
+ allowPublicBind: false,
505
721
  remnicDaemonUrl: "http://localhost:4318",
506
722
  sessionStrategy: "single",
507
723
  memoryInjection: {
@@ -512,6 +728,61 @@ var DEFAULT_CONFIG = {
512
728
  };
513
729
  var VALID_SESSION_STRATEGIES = ["caller-id", "single"];
514
730
  var VALID_POSITIONS = ["system-append", "system-prepend"];
731
+ function normalizeBindHostForValidation(host) {
732
+ const trimmed = host.trim().toLowerCase();
733
+ return trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
734
+ }
735
+ function expandIpv6Groups(host) {
736
+ if (net.isIP(host) !== 6) return null;
737
+ const parts = host.split("::");
738
+ if (parts.length > 2) return null;
739
+ const parseGroups = (segment) => {
740
+ if (!segment) return [];
741
+ const groups2 = [];
742
+ const rawGroups = segment.split(":");
743
+ for (let i = 0; i < rawGroups.length; i += 1) {
744
+ const group = rawGroups[i];
745
+ if (group.includes(".")) {
746
+ if (i !== rawGroups.length - 1 || net.isIP(group) !== 4) return null;
747
+ const octets = group.split(".").map((octet) => Number.parseInt(octet, 10));
748
+ groups2.push(octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]);
749
+ continue;
750
+ }
751
+ if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
752
+ groups2.push(Number.parseInt(group, 16));
753
+ }
754
+ return groups2;
755
+ };
756
+ const head = parseGroups(parts[0] ?? "");
757
+ const tail = parts.length === 2 ? parseGroups(parts[1] ?? "") : [];
758
+ if (!head || !tail) return null;
759
+ const explicitGroupCount = head.length + tail.length;
760
+ const zeroFillCount = parts.length === 2 ? 8 - explicitGroupCount : 0;
761
+ if (parts.length === 1 && explicitGroupCount !== 8) return null;
762
+ if (parts.length === 2 && zeroFillCount < 1) return null;
763
+ const groups = [...head, ...Array.from({ length: zeroFillCount }, () => 0), ...tail];
764
+ return groups.length === 8 ? groups : null;
765
+ }
766
+ function isAllZeroIpv6Address(host) {
767
+ const groups = expandIpv6Groups(host);
768
+ return groups !== null && groups.every((group) => group === 0);
769
+ }
770
+ function isIpv4MappedWildcardAddress(host) {
771
+ const groups = expandIpv6Groups(host);
772
+ return groups !== null && groups.slice(0, 5).every((group) => group === 0) && groups[5] === 65535 && groups[6] === 0 && groups[7] === 0;
773
+ }
774
+ function isPublicBindHost(host) {
775
+ const normalized = normalizeBindHostForValidation(host);
776
+ return normalized === "0.0.0.0" || isAllZeroIpv6Address(normalized) || isIpv4MappedWildcardAddress(normalized);
777
+ }
778
+ function parseOptionalPositiveInteger(obj, key) {
779
+ const value = obj[key];
780
+ if (value === void 0) return void 0;
781
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
782
+ throw new Error(`Config '${key}' must be a positive integer when provided`);
783
+ }
784
+ return value;
785
+ }
515
786
  function parseConfig(raw) {
516
787
  if (typeof raw !== "object" || raw === null) {
517
788
  throw new Error("Config must be a non-null object");
@@ -542,6 +813,16 @@ function parseConfig(raw) {
542
813
  remnicAuthToken = obj.remnicAuthToken;
543
814
  }
544
815
  const wecloneModelName = obj.wecloneModelName !== void 0 ? String(obj.wecloneModelName) : DEFAULT_CONFIG.wecloneModelName;
816
+ const proxyBindHost = obj.proxyBindHost !== void 0 ? String(obj.proxyBindHost).trim() : DEFAULT_CONFIG.proxyBindHost;
817
+ if (!proxyBindHost) {
818
+ throw new Error("Config 'proxyBindHost' must be a non-empty string when provided");
819
+ }
820
+ const allowPublicBind = obj.allowPublicBind === true;
821
+ if (isPublicBindHost(proxyBindHost) && !allowPublicBind) {
822
+ throw new Error(
823
+ "Config 'proxyBindHost' cannot bind to all interfaces unless allowPublicBind is true"
824
+ );
825
+ }
545
826
  let sessionStrategy = DEFAULT_CONFIG.sessionStrategy;
546
827
  if (obj.sessionStrategy !== void 0) {
547
828
  if (!VALID_SESSION_STRATEGIES.includes(obj.sessionStrategy)) {
@@ -586,10 +867,15 @@ function parseConfig(raw) {
586
867
  wecloneApiUrl: obj.wecloneApiUrl,
587
868
  wecloneModelName,
588
869
  proxyPort: obj.proxyPort,
870
+ proxyBindHost,
871
+ allowPublicBind,
589
872
  remnicDaemonUrl: obj.remnicDaemonUrl,
590
873
  remnicAuthToken,
591
874
  sessionStrategy,
592
- memoryInjection
875
+ memoryInjection,
876
+ maxRequestBytes: parseOptionalPositiveInteger(obj, "maxRequestBytes"),
877
+ maxResponseBytes: parseOptionalPositiveInteger(obj, "maxResponseBytes"),
878
+ streamObservationMaxBytes: parseOptionalPositiveInteger(obj, "streamObservationMaxBytes")
593
879
  };
594
880
  }
595
881
 
@@ -601,4 +887,4 @@ export {
601
887
  DEFAULT_CONFIG,
602
888
  parseConfig
603
889
  };
604
- //# sourceMappingURL=chunk-7V67D4WU.js.map
890
+ //# sourceMappingURL=chunk-3RVYVFUV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/format.ts","../src/session.ts","../src/proxy.ts","../src/config.ts"],"sourcesContent":["/**\n * Memory format adapter.\n *\n * Converts Remnic recall results into system prompt sections that\n * can be injected into OpenAI-compatible chat completion requests.\n */\n\nexport interface RecallResult {\n content: string;\n confidence?: number;\n category?: string;\n}\n\nconst CHARS_PER_TOKEN = 4;\n\n/**\n * Format recall results into a memory block suitable for prompt injection.\n *\n * - Sorts memories by confidence (highest first; missing confidence sorts last)\n * - Truncates combined content to fit within `maxTokens` (approx 4 chars/token)\n * - Fills in the template's `{memories}` placeholder\n * - Returns empty string if no memories are provided\n */\nexport function formatMemoryBlock(\n memories: RecallResult[],\n template: string,\n maxTokens: number\n): string {\n if (memories.length === 0) {\n return \"\";\n }\n\n // Sort by confidence descending; undefined confidence sorts last\n const sorted = [...memories].sort((a, b) => {\n const aConf = a.confidence ?? -1;\n const bConf = b.confidence ?? -1;\n return bConf - aConf;\n });\n\n const maxChars = maxTokens * CHARS_PER_TOKEN;\n let totalChars = 0;\n const included: string[] = [];\n\n for (const memory of sorted) {\n const line = memory.content;\n if (totalChars + line.length > maxChars && included.length > 0) {\n break;\n }\n included.push(line);\n totalChars += line.length;\n }\n\n if (included.length === 0) {\n return \"\";\n }\n\n const memoriesText = included.join(\"\\n\");\n return template.replace(\"{memories}\", () => memoriesText);\n}\n","/**\n * Session mapping strategies.\n *\n * Maps caller identity to Remnic session keys so memory is scoped\n * appropriately per user or shared across all callers.\n */\n\nexport interface ChatCompletionRequest {\n model?: string;\n messages?: Array<{ role: string; content: string }>;\n user?: string;\n [key: string]: unknown;\n}\n\nexport interface SessionMapper {\n resolve(\n headers: Record<string, string | string[] | undefined>,\n body: ChatCompletionRequest\n ): string;\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined>,\n key: string\n): string | undefined {\n const normalizedKey = key.toLowerCase();\n for (const [headerKey, raw] of Object.entries(headers)) {\n if (headerKey.toLowerCase() !== normalizedKey) continue;\n const value = Array.isArray(raw) ? raw[0] : raw;\n return typeof value === \"string\" ? value : undefined;\n }\n return undefined;\n}\n\n/**\n * Returns a fixed session key for single-user setups.\n */\nexport class SingleSessionMapper implements SessionMapper {\n private readonly key: string;\n\n constructor(key = \"weclone-default\") {\n this.key = key;\n }\n\n resolve(\n _headers: Record<string, string | string[] | undefined>,\n _body: ChatCompletionRequest\n ): string {\n return this.key;\n }\n}\n\n/**\n * Extracts caller identity from request metadata.\n *\n * Resolution order:\n * 1. `X-Caller-Id` header\n * 2. `user` field in the request body\n * 3. Falls back to \"default\"\n */\nexport class CallerIdSessionMapper implements SessionMapper {\n private readonly fallback: string;\n\n constructor(fallback = \"default\") {\n this.fallback = fallback;\n }\n\n resolve(\n headers: Record<string, string | string[] | undefined>,\n body: ChatCompletionRequest\n ): string {\n const callerId = headerValue(headers, \"x-caller-id\");\n if (callerId && callerId.length > 0) {\n return callerId;\n }\n\n if (typeof body.user === \"string\" && body.user.length > 0) {\n return body.user;\n }\n\n return this.fallback;\n }\n}\n","/**\n * OpenAI-compatible HTTP proxy for WeClone with Remnic memory injection.\n *\n * Intercepts POST /v1/chat/completions to inject recalled memories,\n * forwards all other requests transparently to the WeClone API.\n */\n\nimport * as http from \"node:http\";\nimport type { WeCloneConnectorConfig } from \"./config.js\";\nimport { formatMemoryBlock, type RecallResult } from \"./format.js\";\nimport {\n SingleSessionMapper,\n CallerIdSessionMapper,\n type SessionMapper,\n type ChatCompletionRequest,\n} from \"./session.js\";\n\nexport interface WeCloneProxy {\n start(): Promise<void>;\n stop(): Promise<void>;\n port: number;\n host: string;\n}\n\nconst DEFAULT_MAX_REQUEST_BYTES = 10 * 1024 * 1024;\nconst DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;\nconst DEFAULT_STREAM_OBSERVATION_MAX_BYTES = 1024 * 1024;\n\nclass BodyLimitExceededError extends Error {\n constructor(readonly limitBytes: number) {\n super(`body exceeds ${limitBytes} byte limit`);\n this.name = \"BodyLimitExceededError\";\n }\n}\n\n/**\n * Read the entire body of an IncomingMessage as a string (UTF-8).\n * Used for paths that need to parse JSON (e.g. chat completions).\n */\nfunction readBody(req: http.IncomingMessage, maxBytes: number): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n let exceeded = false;\n req.on(\"data\", (chunk: Buffer) => {\n if (exceeded) return;\n totalBytes += chunk.byteLength;\n if (totalBytes > maxBytes) {\n exceeded = true;\n reject(new BodyLimitExceededError(maxBytes));\n req.resume();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => {\n if (!exceeded) resolve(Buffer.concat(chunks).toString(\"utf-8\"));\n });\n req.on(\"error\", (err) => {\n if (!exceeded) reject(err);\n });\n });\n}\n\n/**\n * Read the entire body of an IncomingMessage as raw bytes.\n * Used for the transparent proxy path to avoid corrupting binary/multipart uploads.\n */\nfunction readRawBody(req: http.IncomingMessage, maxBytes: number): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n let exceeded = false;\n req.on(\"data\", (chunk: Buffer) => {\n if (exceeded) return;\n totalBytes += chunk.byteLength;\n if (totalBytes > maxBytes) {\n exceeded = true;\n reject(new BodyLimitExceededError(maxBytes));\n req.resume();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => {\n if (!exceeded) resolve(Buffer.concat(chunks));\n });\n req.on(\"error\", (err) => {\n if (!exceeded) reject(err);\n });\n });\n}\n\nasync function readResponseBuffer(response: Response, maxBytes: number): Promise<Buffer> {\n const reader = response.body?.getReader();\n if (!reader) return Buffer.alloc(0);\n\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel().catch(() => {});\n throw new BodyLimitExceededError(maxBytes);\n }\n chunks.push(Buffer.from(value));\n }\n return Buffer.concat(chunks);\n}\n\nfunction waitForResponseDrain(res: http.ServerResponse): Promise<\"drain\" | \"closed\"> {\n if (res.destroyed || res.writableEnded) return Promise.resolve(\"closed\");\n return new Promise((resolve) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve(\"drain\");\n };\n const onClose = () => {\n cleanup();\n resolve(\"closed\");\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\nexport async function writeResponseChunkRespectingBackpressure(\n res: http.ServerResponse,\n chunk: Uint8Array,\n): Promise<boolean> {\n if (res.destroyed || res.writableEnded) return false;\n if (res.write(chunk)) return true;\n return (await waitForResponseDrain(res)) === \"drain\";\n}\n\n/**\n * Build a flat headers record from IncomingHttpHeaders,\n * normalizing array values to comma-separated strings.\n */\nfunction flattenHeaders(\n raw: http.IncomingHttpHeaders\n): Record<string, string> {\n const result: Record<string, string> = {};\n for (const [key, val] of Object.entries(raw)) {\n if (val === undefined) continue;\n result[key] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return result;\n}\n\nfunction forwardRequestHeaders(\n headers: Record<string, string>,\n options: { reserializedJson?: boolean } = {}\n): Record<string, string> {\n const forwardHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n const lowerKey = key.toLowerCase();\n if (lowerKey === \"host\" || HOP_BY_HOP_REQUEST_HEADERS.has(lowerKey)) continue;\n if (lowerKey === \"content-length\") continue;\n if (options.reserializedJson && lowerKey === \"content-type\") continue;\n forwardHeaders[key] = value;\n }\n if (options.reserializedJson) {\n forwardHeaders[\"Content-Type\"] = \"application/json\";\n }\n return forwardHeaders;\n}\n\n/**\n * Build standard headers for Remnic daemon requests.\n * Includes Authorization if an auth token is configured.\n */\nfunction remnicHeaders(authToken?: string): Record<string, string> {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (authToken) {\n headers[\"Authorization\"] = `Bearer ${authToken}`;\n }\n return headers;\n}\n\n/**\n * Call Remnic daemon recall endpoint for the given session and query.\n */\nasync function recallMemories(\n daemonUrl: string,\n sessionKey: string,\n query: string,\n authToken?: string\n): Promise<RecallResult[]> {\n const url = `${daemonUrl}/engram/v1/recall`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: remnicHeaders(authToken),\n body: JSON.stringify({ sessionKey, query }),\n });\n\n if (!res.ok) {\n throw new Error(`Remnic recall returned ${res.status}: ${await res.text()}`);\n }\n\n const data = (await res.json()) as { results?: Array<{ preview?: string; content?: string; confidence?: number; category?: string }> };\n const memories: RecallResult[] = (data.results ?? []).map((r) => ({\n content: r.preview || r.content || \"\",\n confidence: r.confidence,\n category: r.category,\n }));\n return memories;\n}\n\n/**\n * Fire-and-forget observation to the Remnic daemon.\n * Errors are caught and silently discarded to avoid adding latency.\n */\nfunction observeTurn(\n daemonUrl: string,\n sessionKey: string,\n userMessage: string,\n assistantMessage: string,\n authToken?: string\n): void {\n const url = `${daemonUrl}/engram/v1/observe`;\n fetch(url, {\n method: \"POST\",\n headers: remnicHeaders(authToken),\n body: JSON.stringify({\n sessionKey,\n messages: [\n { role: \"user\", content: userMessage },\n { role: \"assistant\", content: assistantMessage },\n ],\n }),\n }).catch(() => {\n // Intentionally swallowed -- observation must not affect the response path\n });\n}\n\n/**\n * Coerce an OpenAI chat message `content` into a plain text string.\n *\n * OpenAI chat messages can be either a string or an array of content\n * parts (e.g. `[{type:\"text\",text:\"...\"},{type:\"image_url\",...}]`) for\n * multimodal inputs. Recall/observe only operate on text, so we extract\n * and concatenate the `text` parts. Returns an empty string if no text\n * is present (e.g. image-only turn) so we skip recall rather than sending\n * non-string payloads to the Remnic daemon.\n */\nfunction extractTextContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const part of content) {\n if (\n part &&\n typeof part === \"object\" &&\n (part as { type?: unknown }).type === \"text\"\n ) {\n const text = (part as { text?: unknown }).text;\n if (typeof text === \"string\") parts.push(text);\n }\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the last user message's text content from a chat completion\n * messages array. Handles both string and multimodal array content.\n */\nfunction lastUserMessage(messages: Array<{ role: string; content: unknown }>): string {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") {\n return extractTextContent(messages[i].content);\n }\n }\n return \"\";\n}\n\ntype ForwardedChatMessage = Record<string, unknown> & {\n role: string;\n content: unknown;\n};\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Extract the assistant reply from a WeClone chat completion response.\n */\nfunction extractAssistantReply(responseBody: Record<string, unknown>): string {\n const choices = responseBody.choices as\n | Array<{ message?: { content?: string } }>\n | undefined;\n if (choices && choices.length > 0) {\n return choices[0]?.message?.content ?? \"\";\n }\n return \"\";\n}\n\n/**\n * Strip trailing slashes from a URL without using a regex quantifier\n * on the same character, which CodeQL flags as polynomial ReDoS\n * (`js/polynomial-redos`). A simple loop is O(n) and cannot backtrack.\n */\nfunction stripTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */) {\n end--;\n }\n return end === s.length ? s : s.slice(0, end);\n}\n\n/**\n * Parse a URL string into { origin, basePath } where `basePath` is the\n * configured path prefix (e.g. \"/weclone/v1\") with any trailing slashes\n * stripped. Falls back safely for malformed inputs.\n */\nfunction splitBaseUrl(urlStr: string): { origin: string; basePath: string } {\n try {\n const parsed = new URL(urlStr);\n const basePath = stripTrailingSlashes(parsed.pathname);\n return { origin: parsed.origin, basePath };\n } catch {\n // Fallback: strip trailing path components without ReDoS-prone regex.\n // Split on the first \"/\" after the scheme.\n const schemeEnd = urlStr.indexOf(\"://\");\n if (schemeEnd === -1) {\n return { origin: stripTrailingSlashes(urlStr), basePath: \"\" };\n }\n const afterScheme = urlStr.slice(schemeEnd + 3);\n const pathStart = afterScheme.indexOf(\"/\");\n if (pathStart === -1) {\n return { origin: urlStr, basePath: \"\" };\n }\n const origin = urlStr.slice(0, schemeEnd + 3 + pathStart);\n const basePath = stripTrailingSlashes(afterScheme.slice(pathStart));\n return { origin, basePath };\n }\n}\n\n/**\n * Hop-by-hop request headers that must not be forwarded to upstream.\n * Per RFC 2616 §13.5.1 / RFC 7230 §6.1 these apply only to the\n * immediate transport connection. `proxy-authorization` is the most\n * critical — leaking it would send proxy credentials to the origin.\n *\n * `host` is deliberately excluded from this set because it is\n * always replaced (not just stripped) with the upstream origin\n * and is handled separately below.\n */\nconst HOP_BY_HOP_REQUEST_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\n/**\n * Headers that must not be forwarded from the upstream response.\n * These are hop-by-hop headers that apply to a single transport connection\n * and would conflict with our fully-buffered response write.\n *\n * `content-encoding` is included because fetch() auto-decompresses the body.\n * When we buffer with arrayBuffer() and relay, the bytes are already decoded;\n * forwarding `content-encoding: gzip` would label decompressed bytes as gzip.\n */\nconst HOP_BY_HOP_RESPONSE_HEADERS = new Set([\n \"transfer-encoding\",\n \"content-encoding\",\n \"connection\",\n \"keep-alive\",\n \"upgrade\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n]);\n\n/**\n * Forward a request transparently to the WeClone API.\n *\n * If the configured WeClone URL has a non-empty base path (e.g.\n * \"https://host/weclone/v1\"), the proxy forwards incoming request paths\n * such that \"/v1/models\" maps to \"https://host/weclone/v1/models\". For\n * URLs without a base path, paths map 1:1 to the upstream origin.\n *\n * The request body (if any) is forwarded as raw bytes via Uint8Array so\n * that multipart/binary uploads are not corrupted.\n *\n * Reads the full upstream response before writing to the client\n * to avoid partial-header or hanging-body issues.\n */\nasync function transparentProxy(\n weclone: { origin: string; basePath: string },\n method: string,\n path: string,\n headers: Record<string, string>,\n body: Buffer | null,\n res: http.ServerResponse,\n maxResponseBytes: number\n): Promise<void> {\n // Map the client-facing path into an upstream path.\n //\n // The proxy exposes an OpenAI-compatible `/v1/...` surface. When the\n // configured `wecloneApiUrl` itself already ends in `/v1` (or any\n // path prefix), treat the configured prefix as the upstream mount\n // point and rewrite `/v1/<rest>` to `<basePath>/<rest>`.\n //\n // - basePath \"\" (no prefix): forward path as-is.\n // - basePath \"/v1\": \"/v1/models\" -> \"/v1/models\" (no change).\n // - basePath \"/weclone/v1\": \"/v1/models\" -> \"/weclone/v1/models\".\n //\n // Split off any query string so rewriting operates on the pathname only.\n const qIdx = path.indexOf(\"?\");\n const rawPath = qIdx === -1 ? path : path.slice(0, qIdx);\n const querySuffix = qIdx === -1 ? \"\" : path.slice(qIdx);\n let upstreamPathname = rawPath;\n if (weclone.basePath.length > 0) {\n if (rawPath === \"/v1\" || rawPath.startsWith(\"/v1/\")) {\n upstreamPathname = `${weclone.basePath}${rawPath.slice(3)}`;\n } else if (!rawPath.startsWith(weclone.basePath)) {\n upstreamPathname = `${weclone.basePath}${rawPath}`;\n }\n }\n const targetUrl = `${weclone.origin}${upstreamPathname}${querySuffix}`;\n\n // Remove hop-by-hop request headers and replace host with upstream origin\n const forwardHeaders = forwardRequestHeaders(headers);\n\n const fetchInit: RequestInit = {\n method,\n headers: forwardHeaders,\n };\n if (body && method !== \"GET\" && method !== \"HEAD\") {\n // Copy into a plain ArrayBuffer so the forwarded request keeps the exact\n // byte payload while remaining compatible with this package's BodyInit\n // typing during declaration builds.\n const rawBody = new ArrayBuffer(body.byteLength);\n new Uint8Array(rawBody).set(body);\n fetchInit.body = rawBody;\n }\n\n try {\n const upstream = await fetch(targetUrl, fetchInit);\n\n // Read full body before sending any headers to the client\n const responseBuffer = await readResponseBuffer(upstream, maxResponseBytes);\n\n // Build response headers, filtering hop-by-hop and setting Content-Length\n const responseHeaders: Record<string, string> = {};\n for (const [key, value] of upstream.headers.entries()) {\n if (!HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase())) {\n responseHeaders[key] = value;\n }\n }\n responseHeaders[\"content-length\"] = String(responseBuffer.length);\n\n res.writeHead(upstream.status, responseHeaders);\n res.end(responseBuffer);\n } catch (err) {\n if (err instanceof BodyLimitExceededError) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"upstream_response_too_large\" }));\n return;\n }\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"upstream_unreachable\" }));\n }\n}\n\n/**\n * Create a WeClone proxy instance.\n */\nexport function createWeCloneProxy(config: WeCloneConnectorConfig): WeCloneProxy {\n // Normalize upstream URLs: strip trailing slashes to prevent double-slash\n // when appending path segments. Use a loop (not regex) to avoid the\n // polynomial-ReDoS class flagged by CodeQL for `/\\/+$/`.\n const wecloneApiUrl = stripTrailingSlashes(config.wecloneApiUrl);\n const remnicDaemonUrl = stripTrailingSlashes(config.remnicDaemonUrl);\n // Pre-split the WeClone URL so transparentProxy and the chat path can\n // honor a configured base path (e.g. \"/weclone/v1\").\n const wecloneParts = splitBaseUrl(wecloneApiUrl);\n const maxRequestBytes = config.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES;\n const maxResponseBytes = config.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const streamObservationMaxBytes =\n config.streamObservationMaxBytes ?? DEFAULT_STREAM_OBSERVATION_MAX_BYTES;\n const proxyBindHost = config.proxyBindHost ?? \"127.0.0.1\";\n\n const sessionMapper: SessionMapper =\n config.sessionStrategy === \"caller-id\"\n ? new CallerIdSessionMapper()\n : new SingleSessionMapper();\n\n let server: http.Server | null = null;\n let resolvedPort = config.proxyPort;\n let resolvedHost = proxyBindHost;\n\n const requestHandler = async (\n req: http.IncomingMessage,\n res: http.ServerResponse\n ): Promise<void> => {\n const url = req.url ?? \"/\";\n const method = (req.method ?? \"GET\").toUpperCase();\n\n // Parse the request URL into a pathname (stripping query string and\n // normalizing trailing slash). Using pathname for route matching avoids\n // silently falling through when clients append query params like\n // `?api-version=2023-05-15` (common with Azure OpenAI-compatible SDKs).\n let pathname = url;\n const queryStart = url.indexOf(\"?\");\n if (queryStart !== -1) pathname = url.slice(0, queryStart);\n // Normalize trailing slash for route matching only (not for forwarding).\n const normalizedPathname =\n pathname.length > 1 && pathname.endsWith(\"/\")\n ? pathname.slice(0, -1)\n : pathname;\n\n // --- Health check ---\n if (normalizedPathname === \"/health\" && method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({\n status: \"ok\",\n wecloneApi: config.wecloneApiUrl,\n }));\n return;\n }\n\n // --- Chat completions with memory injection ---\n if (normalizedPathname === \"/v1/chat/completions\" && method === \"POST\") {\n let bodyStr: string;\n try {\n bodyStr = await readBody(req, maxRequestBytes);\n } catch (err) {\n if (err instanceof BodyLimitExceededError) {\n res.writeHead(413, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"request_body_too_large\" }));\n return;\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"bad_request\", detail: \"Could not read request body\" }));\n return;\n }\n\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(bodyStr) as unknown;\n } catch {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"bad_request\", detail: \"Invalid JSON body\" }));\n return;\n }\n if (!isPlainRecord(parsedJson)) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"bad_request\",\n detail: \"JSON body must be an object\",\n })\n );\n return;\n }\n const parsed = parsedJson as ChatCompletionRequest;\n\n const headers = req.headers as Record<string, string | string[] | undefined>;\n const sessionKey = sessionMapper.resolve(headers, parsed);\n // Validate `messages` is an array with object entries before use so\n // malformed payloads (`messages: \"...\"`, `messages: {}`, etc.) return\n // a structured 400 instead of surfacing as a 500 internal error.\n if (parsed.messages !== undefined && !Array.isArray(parsed.messages)) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"bad_request\",\n detail: \"messages must be an array\",\n })\n );\n return;\n }\n // Messages may contain multimodal content-parts arrays; keep them\n // untyped and validate strings at each use site. Drop entries that\n // are not plain objects so downstream `.map()` cannot throw.\n const rawMessages: ForwardedChatMessage[] = [];\n for (const raw of parsed.messages ?? []) {\n if (raw === null || typeof raw !== \"object\") continue;\n const entry = raw as Record<string, unknown>;\n rawMessages.push({\n ...entry,\n role: typeof entry.role === \"string\" ? entry.role : \"\",\n content: entry.content,\n });\n }\n const query = lastUserMessage(rawMessages);\n\n // Recall memories (graceful degradation on failure)\n let memoryBlock = \"\";\n if (query.length > 0) {\n try {\n const memories = await recallMemories(\n remnicDaemonUrl,\n sessionKey,\n query,\n config.remnicAuthToken\n );\n memoryBlock = formatMemoryBlock(\n memories,\n config.memoryInjection.template,\n config.memoryInjection.maxTokens\n );\n } catch {\n // Remnic recall failed -- proceed without memory injection\n }\n }\n\n // Build the forwarded messages array. Only the *first* system message\n // is rewritten with injected memory (or, if no system exists, a\n // synthetic system message is prepended). Subsequent system messages\n // are forwarded verbatim so distinct system instructions are not\n // silently overwritten.\n const outMessages: ForwardedChatMessage[] = [];\n const firstSystemIdx = rawMessages.findIndex((m) => m.role === \"system\");\n const position = config.memoryInjection.position;\n\n if (memoryBlock.length === 0) {\n // No memory to inject — forward original messages unchanged.\n for (const m of rawMessages) outMessages.push(m);\n } else if (firstSystemIdx === -1) {\n // No existing system message: prepend a synthetic one.\n outMessages.push({ role: \"system\", content: memoryBlock });\n for (const m of rawMessages) outMessages.push(m);\n } else {\n for (let i = 0; i < rawMessages.length; i++) {\n const m = rawMessages[i];\n if (i === firstSystemIdx) {\n const existing = extractTextContent(m.content);\n outMessages.push({\n ...m,\n role: \"system\",\n content:\n position === \"system-prepend\"\n ? `${memoryBlock}\\n\\n${existing}`\n : `${existing}\\n\\n${memoryBlock}`,\n });\n } else {\n outMessages.push(m);\n }\n }\n }\n\n const modifiedBody = {\n ...parsed,\n ...(config.wecloneModelName ? { model: config.wecloneModelName } : {}),\n messages: outMessages,\n };\n\n // Forward to WeClone. If `wecloneApiUrl` has a path prefix (the\n // common `/v1` or custom mounts like `/weclone/v1`), forward to\n // `${basePath}/chat/completions`. If the configured URL has no\n // base path at all, default to the standard OpenAI `/v1/chat/completions`.\n // Preserve any query string on the incoming request (e.g. Azure's\n // `?api-version=...`) so version selectors and tenant hints reach\n // upstream unchanged.\n const chatBase = wecloneParts.basePath.length > 0\n ? wecloneParts.basePath\n : \"/v1\";\n const qIdx = url.indexOf(\"?\");\n const querySuffix = qIdx === -1 ? \"\" : url.slice(qIdx);\n const targetUrl =\n `${wecloneParts.origin}${chatBase}/chat/completions${querySuffix}`;\n const forwardHeaders = forwardRequestHeaders(flattenHeaders(req.headers), {\n reserializedJson: true,\n });\n\n try {\n const upstream = await fetch(targetUrl, {\n method: \"POST\",\n headers: forwardHeaders,\n body: JSON.stringify(modifiedBody),\n });\n\n // --- Streaming path ---\n if (parsed.stream === true) {\n // If upstream returned an error, pass through as-is (don't force SSE headers)\n if (!upstream.ok) {\n const errBody = await readResponseBuffer(upstream, maxResponseBytes);\n res.writeHead(upstream.status, {\n \"content-type\": upstream.headers.get(\"content-type\") || \"application/json\",\n });\n res.end(errBody);\n return;\n }\n\n res.writeHead(upstream.status, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n });\n\n const reader = upstream.body?.getReader();\n if (!reader) {\n res.end();\n return;\n }\n let clientClosed = false;\n const onClientClose = () => {\n clientClosed = true;\n void reader.cancel().catch(() => {});\n };\n res.once(\"close\", onClientClose);\n\n const decoder = new TextDecoder();\n let streamBuffer = \"\";\n let assistantContent = \"\";\n let streamedResponseBytes = 0;\n let streamLimitExceeded = false;\n let observationTextBytes = 0;\n let observationDisabled = false;\n const disableObservationBuffer = () => {\n observationDisabled = true;\n streamBuffer = \"\";\n assistantContent = \"\";\n observationTextBytes = 0;\n };\n const appendObservationText = (text: string) => {\n const nextBytes = observationTextBytes + Buffer.byteLength(text, \"utf8\");\n if (nextBytes > streamObservationMaxBytes) {\n disableObservationBuffer();\n return;\n }\n observationTextBytes = nextBytes;\n assistantContent += text;\n };\n const consumeSseLine = (line: string) => {\n if (!line.startsWith(\"data: \") || line === \"data: [DONE]\") return;\n try {\n const event = JSON.parse(line.slice(6)) as {\n choices?: Array<{ delta?: { content?: string } }>;\n };\n const delta = event.choices?.[0]?.delta?.content;\n if (delta) appendObservationText(delta);\n } catch {\n // Malformed SSE chunk -- skip\n }\n };\n try {\n while (true) {\n if (clientClosed) break;\n const { done, value } = await reader.read();\n if (done) break;\n streamedResponseBytes += value.byteLength;\n if (streamedResponseBytes > maxResponseBytes) {\n streamLimitExceeded = true;\n await reader.cancel().catch(() => {});\n break;\n }\n const wrote = await writeResponseChunkRespectingBackpressure(res, value);\n if (!wrote) {\n clientClosed = true;\n await reader.cancel().catch(() => {});\n break;\n }\n if (observationDisabled) continue;\n\n if (\n Buffer.byteLength(streamBuffer, \"utf8\") + value.byteLength >\n streamObservationMaxBytes\n ) {\n disableObservationBuffer();\n continue;\n }\n\n streamBuffer += decoder.decode(value, { stream: true });\n const lines = streamBuffer.split(\"\\n\");\n streamBuffer = lines.pop() ?? \"\";\n for (const line of lines) {\n consumeSseLine(line);\n }\n }\n } finally {\n res.off(\"close\", onClientClose);\n if (!res.destroyed && !res.writableEnded) {\n res.end();\n }\n }\n if (clientClosed || streamLimitExceeded) return;\n\n // Best-effort: reconstruct assistant content for observation\n try {\n if (!observationDisabled) {\n const tail = decoder.decode();\n if (tail) streamBuffer += tail;\n if (streamBuffer.length > 0) {\n for (const line of streamBuffer.split(\"\\n\")) {\n consumeSseLine(line);\n }\n }\n }\n if (!observationDisabled && assistantContent.length > 0 && query.length > 0) {\n observeTurn(\n remnicDaemonUrl,\n sessionKey,\n query,\n assistantContent,\n config.remnicAuthToken\n );\n }\n } catch {\n // Observation reconstruction failed -- non-critical\n }\n return;\n }\n\n // --- Non-streaming path ---\n const responseBytes = await readResponseBuffer(upstream, maxResponseBytes);\n\n // Parse response for observation (best-effort)\n let assistantReply = \"\";\n try {\n const responseJson = JSON.parse(\n responseBytes.toString(\"utf-8\")\n ) as Record<string, unknown>;\n assistantReply = extractAssistantReply(responseJson);\n } catch {\n // Non-JSON response -- skip observation\n }\n\n // Fire-and-forget observe\n if (query.length > 0 && assistantReply.length > 0) {\n observeTurn(remnicDaemonUrl, sessionKey, query, assistantReply, config.remnicAuthToken);\n }\n\n // Return upstream response to caller, stripping hop-by-hop headers\n const chatResponseHeaders: Record<string, string> = {};\n for (const [key, value] of upstream.headers.entries()) {\n if (!HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase())) {\n chatResponseHeaders[key] = value;\n }\n }\n chatResponseHeaders[\"content-length\"] = String(responseBytes.length);\n res.writeHead(upstream.status, chatResponseHeaders);\n res.end(responseBytes);\n } catch (err) {\n if (err instanceof BodyLimitExceededError) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"upstream_response_too_large\" }));\n return;\n }\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({\n error: \"upstream_unreachable\",\n }));\n }\n return;\n }\n\n // --- All other paths: transparent proxy ---\n // Use raw bytes to avoid corrupting binary/multipart uploads.\n let body: Buffer | null = null;\n try {\n body = method !== \"GET\" && method !== \"HEAD\"\n ? await readRawBody(req, maxRequestBytes)\n : null;\n } catch (err) {\n if (err instanceof BodyLimitExceededError) {\n res.writeHead(413, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"request_body_too_large\" }));\n return;\n }\n throw err;\n }\n const flat = flattenHeaders(req.headers);\n await transparentProxy(wecloneParts, method, url, flat, body, res, maxResponseBytes);\n };\n\n return {\n get port() {\n return resolvedPort;\n },\n get host() {\n return resolvedHost;\n },\n\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n server = http.createServer((req, res) => {\n requestHandler(req, res).catch((_err) => {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"internal_proxy_error\" }));\n }\n });\n });\n\n server.on(\"error\", reject);\n\n server.listen(config.proxyPort, proxyBindHost, () => {\n const addr = server!.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolvedPort = addr.port;\n resolvedHost = addr.address;\n }\n resolve();\n });\n });\n },\n\n stop(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (!server) {\n resolve();\n return;\n }\n server.close((err) => {\n server = null;\n if (err) reject(err);\n else resolve();\n });\n });\n },\n };\n}\n","/**\n * WeClone connector configuration.\n *\n * Validates user-provided config and applies defaults for optional fields.\n */\n\nimport * as net from \"node:net\";\n\nexport interface MemoryInjectionConfig {\n maxTokens: number;\n position: \"system-append\" | \"system-prepend\";\n template: string;\n}\n\nexport interface WeCloneConnectorConfig {\n wecloneApiUrl: string;\n wecloneModelName?: string;\n proxyPort: number;\n proxyBindHost?: string;\n allowPublicBind?: boolean;\n remnicDaemonUrl: string;\n remnicAuthToken?: string;\n sessionStrategy: \"caller-id\" | \"single\";\n memoryInjection: MemoryInjectionConfig;\n maxRequestBytes?: number;\n maxResponseBytes?: number;\n streamObservationMaxBytes?: number;\n}\n\nexport const DEFAULT_CONFIG: WeCloneConnectorConfig = {\n wecloneApiUrl: \"http://localhost:8000/v1\",\n wecloneModelName: \"weclone-avatar\",\n proxyPort: 8100,\n proxyBindHost: \"127.0.0.1\",\n allowPublicBind: false,\n remnicDaemonUrl: \"http://localhost:4318\",\n sessionStrategy: \"single\",\n memoryInjection: {\n maxTokens: 1500,\n position: \"system-append\",\n template: \"[Memory Context]\\n{memories}\\n[End Memory Context]\",\n },\n};\n\nconst VALID_SESSION_STRATEGIES = [\"caller-id\", \"single\"] as const;\nconst VALID_POSITIONS = [\"system-append\", \"system-prepend\"] as const;\n\nfunction normalizeBindHostForValidation(host: string): string {\n const trimmed = host.trim().toLowerCase();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\")\n ? trimmed.slice(1, -1)\n : trimmed;\n}\n\nfunction expandIpv6Groups(host: string): number[] | null {\n if (net.isIP(host) !== 6) return null;\n const parts = host.split(\"::\");\n if (parts.length > 2) return null;\n\n const parseGroups = (segment: string): number[] | null => {\n if (!segment) return [];\n const groups: number[] = [];\n const rawGroups = segment.split(\":\");\n for (let i = 0; i < rawGroups.length; i += 1) {\n const group = rawGroups[i];\n if (group.includes(\".\")) {\n if (i !== rawGroups.length - 1 || net.isIP(group) !== 4) return null;\n const octets = group.split(\".\").map((octet) => Number.parseInt(octet, 10));\n groups.push((octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]);\n continue;\n }\n if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;\n groups.push(Number.parseInt(group, 16));\n }\n return groups;\n };\n\n const head = parseGroups(parts[0] ?? \"\");\n const tail = parts.length === 2 ? parseGroups(parts[1] ?? \"\") : [];\n if (!head || !tail) return null;\n const explicitGroupCount = head.length + tail.length;\n const zeroFillCount = parts.length === 2 ? 8 - explicitGroupCount : 0;\n if (parts.length === 1 && explicitGroupCount !== 8) return null;\n if (parts.length === 2 && zeroFillCount < 1) return null;\n\n const groups = [...head, ...Array.from({ length: zeroFillCount }, () => 0), ...tail];\n return groups.length === 8 ? groups : null;\n}\n\nfunction isAllZeroIpv6Address(host: string): boolean {\n const groups = expandIpv6Groups(host);\n return groups !== null && groups.every((group) => group === 0);\n}\n\nfunction isIpv4MappedWildcardAddress(host: string): boolean {\n const groups = expandIpv6Groups(host);\n return (\n groups !== null &&\n groups.slice(0, 5).every((group) => group === 0) &&\n groups[5] === 0xffff &&\n groups[6] === 0 &&\n groups[7] === 0\n );\n}\n\nfunction isPublicBindHost(host: string): boolean {\n const normalized = normalizeBindHostForValidation(host);\n return (\n normalized === \"0.0.0.0\" ||\n isAllZeroIpv6Address(normalized) ||\n isIpv4MappedWildcardAddress(normalized)\n );\n}\n\nfunction parseOptionalPositiveInteger(\n obj: Record<string, unknown>,\n key: string,\n): number | undefined {\n const value = obj[key];\n if (value === undefined) return undefined;\n if (typeof value !== \"number\" || !Number.isInteger(value) || value <= 0) {\n throw new Error(`Config '${key}' must be a positive integer when provided`);\n }\n return value;\n}\n\n/**\n * Parse and validate a raw config object into a WeCloneConnectorConfig.\n *\n * Rejects missing required fields and invalid values with clear messages.\n * Applies defaults for all optional fields.\n */\nexport function parseConfig(raw: unknown): WeCloneConnectorConfig {\n if (typeof raw !== \"object\" || raw === null) {\n throw new Error(\"Config must be a non-null object\");\n }\n\n const obj = raw as Record<string, unknown>;\n\n // --- Required fields ---\n if (typeof obj.wecloneApiUrl !== \"string\" || obj.wecloneApiUrl.length === 0) {\n throw new Error(\n \"Config 'wecloneApiUrl' is required and must be a non-empty string\"\n );\n }\n\n if (\n typeof obj.proxyPort !== \"number\" ||\n !Number.isInteger(obj.proxyPort) ||\n obj.proxyPort <= 0 ||\n obj.proxyPort > 65535\n ) {\n throw new Error(\n \"Config 'proxyPort' is required and must be an integer between 1 and 65535\"\n );\n }\n\n if (typeof obj.remnicDaemonUrl !== \"string\" || obj.remnicDaemonUrl.length === 0) {\n throw new Error(\n \"Config 'remnicDaemonUrl' is required and must be a non-empty string\"\n );\n }\n\n // --- Optional fields with validation ---\n let remnicAuthToken: string | undefined;\n if (obj.remnicAuthToken !== undefined) {\n if (typeof obj.remnicAuthToken !== \"string\" || obj.remnicAuthToken.length === 0) {\n throw new Error(\n \"Config 'remnicAuthToken' must be a non-empty string when provided\"\n );\n }\n remnicAuthToken = obj.remnicAuthToken;\n }\n\n const wecloneModelName =\n obj.wecloneModelName !== undefined\n ? String(obj.wecloneModelName)\n : DEFAULT_CONFIG.wecloneModelName;\n\n const proxyBindHost =\n obj.proxyBindHost !== undefined\n ? String(obj.proxyBindHost).trim()\n : DEFAULT_CONFIG.proxyBindHost;\n if (!proxyBindHost) {\n throw new Error(\"Config 'proxyBindHost' must be a non-empty string when provided\");\n }\n const allowPublicBind = obj.allowPublicBind === true;\n if (isPublicBindHost(proxyBindHost) && !allowPublicBind) {\n throw new Error(\n \"Config 'proxyBindHost' cannot bind to all interfaces unless allowPublicBind is true\",\n );\n }\n\n let sessionStrategy = DEFAULT_CONFIG.sessionStrategy;\n if (obj.sessionStrategy !== undefined) {\n if (!VALID_SESSION_STRATEGIES.includes(obj.sessionStrategy as typeof VALID_SESSION_STRATEGIES[number])) {\n throw new Error(\n `Config 'sessionStrategy' must be one of: ${VALID_SESSION_STRATEGIES.join(\", \")}. ` +\n `Got: ${JSON.stringify(obj.sessionStrategy)}`\n );\n }\n sessionStrategy = obj.sessionStrategy as typeof sessionStrategy;\n }\n\n // --- Memory injection ---\n let memoryInjection = { ...DEFAULT_CONFIG.memoryInjection };\n if (obj.memoryInjection !== undefined) {\n if (typeof obj.memoryInjection !== \"object\" || obj.memoryInjection === null) {\n throw new Error(\"Config 'memoryInjection' must be an object\");\n }\n const mi = obj.memoryInjection as Record<string, unknown>;\n\n if (mi.maxTokens !== undefined) {\n if (typeof mi.maxTokens !== \"number\" || !Number.isInteger(mi.maxTokens) || mi.maxTokens <= 0) {\n throw new Error(\n \"Config 'memoryInjection.maxTokens' must be a positive integer\"\n );\n }\n memoryInjection.maxTokens = mi.maxTokens;\n }\n\n if (mi.position !== undefined) {\n if (!VALID_POSITIONS.includes(mi.position as typeof VALID_POSITIONS[number])) {\n throw new Error(\n `Config 'memoryInjection.position' must be one of: ` +\n `${VALID_POSITIONS.join(\", \")}. Got: ${JSON.stringify(mi.position)}`\n );\n }\n memoryInjection.position = mi.position as typeof memoryInjection.position;\n }\n\n if (mi.template !== undefined) {\n if (typeof mi.template !== \"string\" || mi.template.length === 0) {\n throw new Error(\n \"Config 'memoryInjection.template' must be a non-empty string\"\n );\n }\n memoryInjection.template = mi.template;\n }\n }\n\n return {\n wecloneApiUrl: obj.wecloneApiUrl,\n wecloneModelName,\n proxyPort: obj.proxyPort,\n proxyBindHost,\n allowPublicBind,\n remnicDaemonUrl: obj.remnicDaemonUrl,\n remnicAuthToken,\n sessionStrategy,\n memoryInjection,\n maxRequestBytes: parseOptionalPositiveInteger(obj, \"maxRequestBytes\"),\n maxResponseBytes: parseOptionalPositiveInteger(obj, \"maxResponseBytes\"),\n streamObservationMaxBytes: parseOptionalPositiveInteger(obj, \"streamObservationMaxBytes\"),\n };\n}\n"],"mappings":"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","names":["groups"]}
package/dist/cli.js CHANGED
@@ -3,65 +3,108 @@
3
3
  import {
4
4
  createWeCloneProxy,
5
5
  parseConfig
6
- } from "./chunk-7V67D4WU.js";
6
+ } from "./chunk-3RVYVFUV.js";
7
+
8
+ // src/shutdown.ts
9
+ function errorMessage(err) {
10
+ return err instanceof Error ? err.message : String(err);
11
+ }
12
+ function createGracefulShutdownHandler(proxy, options = {}) {
13
+ const exit = options.exit ?? ((code) => process.exit(code));
14
+ const logError = options.logError ?? ((message) => console.error(message));
15
+ let stopping = false;
16
+ return () => {
17
+ if (stopping) return;
18
+ stopping = true;
19
+ void (async () => {
20
+ try {
21
+ await proxy.stop();
22
+ exit(0);
23
+ } catch (err) {
24
+ logError(`Failed to stop WeClone proxy: ${errorMessage(err)}`);
25
+ exit(1);
26
+ }
27
+ })();
28
+ };
29
+ }
7
30
 
8
31
  // src/cli.ts
9
32
  import { readFileSync, existsSync } from "fs";
10
- import { resolve } from "path";
33
+ import { join, resolve } from "path";
11
34
  import { homedir } from "os";
35
+ function homeDir() {
36
+ const envHome = process.env.HOME;
37
+ return envHome && envHome.length > 0 ? envHome : homedir();
38
+ }
39
+ function expandTildePath(input) {
40
+ if (input === "~") return homeDir();
41
+ if (input.startsWith("~/") || input.startsWith("~\\")) {
42
+ return join(homeDir(), input.slice(2));
43
+ }
44
+ return input;
45
+ }
12
46
  function defaultConfigPath() {
13
- const override = process.env.REMNIC_HOME ?? process.env.ENGRAM_HOME;
47
+ const override = process.env.REMNIC_HOME && process.env.REMNIC_HOME.length > 0 ? process.env.REMNIC_HOME : process.env.ENGRAM_HOME;
14
48
  if (override && override.length > 0) {
15
- return resolve(override, "connectors", "weclone.json");
49
+ return resolve(expandTildePath(override), "connectors", "weclone.json");
16
50
  }
17
- const envHome = process.env.HOME;
18
- const home = envHome && envHome.length > 0 ? envHome : homedir();
19
- return resolve(home, ".remnic", "connectors", "weclone.json");
51
+ return resolve(homeDir(), ".remnic", "connectors", "weclone.json");
20
52
  }
21
- var args = process.argv.slice(2);
22
- var configPath = null;
23
- for (let i = 0; i < args.length; i++) {
24
- if (args[i] === "--config") {
25
- if (!args[i + 1]) {
26
- console.error("Error: --config requires a path argument");
27
- process.exit(1);
53
+ async function main() {
54
+ const args = process.argv.slice(2);
55
+ let configPath = null;
56
+ for (let i = 0; i < args.length; i++) {
57
+ if (args[i] === "--config") {
58
+ if (!args[i + 1] || args[i + 1].startsWith("-")) {
59
+ console.error("Error: --config requires a path argument");
60
+ process.exit(1);
61
+ }
62
+ configPath = resolve(expandTildePath(args[i + 1]));
63
+ i++;
28
64
  }
29
- configPath = resolve(args[i + 1]);
30
- i++;
31
65
  }
32
- }
33
- if (configPath === null) {
34
- configPath = defaultConfigPath();
35
- }
36
- if (!existsSync(configPath)) {
37
- console.error(`Config not found: ${configPath}`);
38
- console.error("Run: remnic connectors install weclone");
39
- process.exit(1);
40
- }
41
- var raw;
42
- try {
43
- raw = JSON.parse(readFileSync(configPath, "utf-8"));
44
- } catch (err) {
45
- console.error(`Failed to parse config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
46
- process.exit(1);
47
- }
48
- if (typeof raw !== "object" || raw === null) {
49
- console.error(`Config at ${configPath} must be a JSON object`);
50
- process.exit(1);
51
- }
52
- var config = parseConfig(raw);
53
- var proxy = createWeCloneProxy(config);
54
- proxy.start().then(() => {
66
+ if (configPath === null) {
67
+ configPath = defaultConfigPath();
68
+ }
69
+ if (!existsSync(configPath)) {
70
+ console.error(`Config not found: ${configPath}`);
71
+ console.error("Run: remnic connectors install weclone");
72
+ process.exit(1);
73
+ }
74
+ let raw;
75
+ try {
76
+ raw = JSON.parse(readFileSync(configPath, "utf-8"));
77
+ } catch (err) {
78
+ console.error(`Failed to parse config at ${configPath}: ${errorMessage(err)}`);
79
+ process.exit(1);
80
+ }
81
+ if (typeof raw !== "object" || raw === null) {
82
+ console.error(`Config at ${configPath} must be a JSON object`);
83
+ process.exit(1);
84
+ }
85
+ let config;
86
+ try {
87
+ config = parseConfig(raw);
88
+ } catch (err) {
89
+ console.error(`Invalid config at ${configPath}: ${errorMessage(err)}`);
90
+ process.exit(1);
91
+ }
92
+ const proxy = createWeCloneProxy(config);
93
+ try {
94
+ await proxy.start();
95
+ } catch (err) {
96
+ console.error(`Failed to start WeClone proxy: ${errorMessage(err)}`);
97
+ process.exit(1);
98
+ }
55
99
  console.log(`WeClone memory proxy listening on :${config.proxyPort}`);
56
100
  console.log(` WeClone API: ${config.wecloneApiUrl}`);
57
101
  console.log(` Remnic daemon: ${config.remnicDaemonUrl}`);
58
- });
59
- process.on("SIGINT", () => {
60
- proxy.stop();
61
- process.exit(0);
62
- });
63
- process.on("SIGTERM", () => {
64
- proxy.stop();
65
- process.exit(0);
102
+ const stopAndExit = createGracefulShutdownHandler(proxy);
103
+ process.on("SIGINT", stopAndExit);
104
+ process.on("SIGTERM", stopAndExit);
105
+ }
106
+ void main().catch((err) => {
107
+ console.error(`Failed to start WeClone proxy: ${errorMessage(err)}`);
108
+ process.exit(1);
66
109
  });
67
110
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * CLI entrypoint for @remnic/connector-weclone.\n *\n * Reads config from ~/.remnic/connectors/weclone.json (or --config path)\n * and starts the OpenAI-compatible memory proxy. `REMNIC_HOME` (or legacy\n * `ENGRAM_HOME`) can override the default home directory — this matches the\n * override honoured by `remnic connectors install weclone` in @remnic/core.\n */\n\nimport { createWeCloneProxy } from \"./proxy.js\";\nimport { parseConfig } from \"./config.js\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/**\n * Resolve the default proxy config path. Kept in lockstep with\n * @remnic/core's `resolveWeCloneProxyConfigPath()` so install/run pair up\n * without additional wiring from the caller.\n *\n * Both sides use `path.resolve()` (absolute) — NOT `path.join()` — so a\n * relative override like `REMNIC_HOME=tmp/remnic` is normalized against the\n * current working directory. If core and CLI disagreed on this, a relative\n * override could write the config in one location and read it from another,\n * producing spurious \"Config not found\" errors right after a successful\n * install.\n *\n * `HOME=\"\"` edge case: treat an empty-string HOME as absent and fall back\n * to `os.homedir()`. The core helper does the same; if they diverged here,\n * install and run would target different directories when `HOME` is\n * cleared (empty string is not nullish, so `?? os.homedir()` does NOT\n * substitute it).\n */\nfunction defaultConfigPath(): string {\n const override = process.env.REMNIC_HOME ?? process.env.ENGRAM_HOME;\n if (override && override.length > 0) {\n return resolve(override, \"connectors\", \"weclone.json\");\n }\n const envHome = process.env.HOME;\n const home = envHome && envHome.length > 0 ? envHome : homedir();\n return resolve(home, \".remnic\", \"connectors\", \"weclone.json\");\n}\n\n// Parse --config first so an explicit path takes precedence over env-var\n// resolution. Only fall back to defaultConfigPath() when the user has not\n// supplied an explicit --config flag. This lets `remnic-weclone-proxy\n// --config /abs/path` work even in environments where REMNIC_HOME is\n// misconfigured, without defaultConfigPath() (and any env-var validation\n// it contains) running unnecessarily.\nconst args = process.argv.slice(2);\nlet configPath: string | null = null;\n\nfor (let i = 0; i < args.length; i++) {\n if (args[i] === \"--config\") {\n if (!args[i + 1]) {\n console.error(\"Error: --config requires a path argument\");\n process.exit(1);\n }\n configPath = resolve(args[i + 1]);\n i++;\n }\n}\n\nif (configPath === null) {\n configPath = defaultConfigPath();\n}\n\nif (!existsSync(configPath)) {\n console.error(`Config not found: ${configPath}`);\n console.error(\"Run: remnic connectors install weclone\");\n process.exit(1);\n}\n\nlet raw: unknown;\ntry {\n raw = JSON.parse(readFileSync(configPath, \"utf-8\"));\n} catch (err) {\n console.error(`Failed to parse config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n}\n\nif (typeof raw !== \"object\" || raw === null) {\n console.error(`Config at ${configPath} must be a JSON object`);\n process.exit(1);\n}\n\nconst config = parseConfig(raw);\nconst proxy = createWeCloneProxy(config);\n\nproxy.start().then(() => {\n console.log(`WeClone memory proxy listening on :${config.proxyPort}`);\n console.log(` WeClone API: ${config.wecloneApiUrl}`);\n console.log(` Remnic daemon: ${config.remnicDaemonUrl}`);\n});\n\nprocess.on(\"SIGINT\", () => {\n proxy.stop();\n process.exit(0);\n});\nprocess.on(\"SIGTERM\", () => {\n proxy.stop();\n process.exit(0);\n});\n"],"mappings":";;;;;;;;AAYA,SAAS,cAAc,kBAAkB;AACzC,SAAS,eAAe;AACxB,SAAS,eAAe;AAoBxB,SAAS,oBAA4B;AACnC,QAAM,WAAW,QAAQ,IAAI,eAAe,QAAQ,IAAI;AACxD,MAAI,YAAY,SAAS,SAAS,GAAG;AACnC,WAAO,QAAQ,UAAU,cAAc,cAAc;AAAA,EACvD;AACA,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,OAAO,WAAW,QAAQ,SAAS,IAAI,UAAU,QAAQ;AAC/D,SAAO,QAAQ,MAAM,WAAW,cAAc,cAAc;AAC9D;AAQA,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,IAAI,aAA4B;AAEhC,SAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,MAAI,KAAK,CAAC,MAAM,YAAY;AAC1B,QAAI,CAAC,KAAK,IAAI,CAAC,GAAG;AAChB,cAAQ,MAAM,0CAA0C;AACxD,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,iBAAa,QAAQ,KAAK,IAAI,CAAC,CAAC;AAChC;AAAA,EACF;AACF;AAEA,IAAI,eAAe,MAAM;AACvB,eAAa,kBAAkB;AACjC;AAEA,IAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,UAAQ,MAAM,qBAAqB,UAAU,EAAE;AAC/C,UAAQ,MAAM,wCAAwC;AACtD,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI;AACJ,IAAI;AACF,QAAM,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AACpD,SAAS,KAAK;AACZ,UAAQ,MAAM,6BAA6B,UAAU,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC5G,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,UAAQ,MAAM,aAAa,UAAU,wBAAwB;AAC7D,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,YAAY,GAAG;AAC9B,IAAM,QAAQ,mBAAmB,MAAM;AAEvC,MAAM,MAAM,EAAE,KAAK,MAAM;AACvB,UAAQ,IAAI,sCAAsC,OAAO,SAAS,EAAE;AACpE,UAAQ,IAAI,kBAAkB,OAAO,aAAa,EAAE;AACpD,UAAQ,IAAI,oBAAoB,OAAO,eAAe,EAAE;AAC1D,CAAC;AAED,QAAQ,GAAG,UAAU,MAAM;AACzB,QAAM,KAAK;AACX,UAAQ,KAAK,CAAC;AAChB,CAAC;AACD,QAAQ,GAAG,WAAW,MAAM;AAC1B,QAAM,KAAK;AACX,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/shutdown.ts","../src/cli.ts"],"sourcesContent":["import type { WeCloneProxy } from \"./proxy.js\";\n\nexport function errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport function createGracefulShutdownHandler(\n proxy: Pick<WeCloneProxy, \"stop\">,\n options: {\n exit?: (code: number) => void;\n logError?: (message: string) => void;\n } = {},\n): () => void {\n const exit = options.exit ?? ((code: number) => process.exit(code));\n const logError = options.logError ?? ((message: string) => console.error(message));\n let stopping = false;\n\n return () => {\n if (stopping) return;\n stopping = true;\n\n void (async () => {\n try {\n await proxy.stop();\n exit(0);\n } catch (err) {\n logError(`Failed to stop WeClone proxy: ${errorMessage(err)}`);\n exit(1);\n }\n })();\n };\n}\n","#!/usr/bin/env node\n/**\n * CLI entrypoint for @remnic/connector-weclone.\n *\n * Reads config from ~/.remnic/connectors/weclone.json (or --config path)\n * and starts the OpenAI-compatible memory proxy. `REMNIC_HOME` (or legacy\n * `ENGRAM_HOME`) can override the default home directory — this matches the\n * override honoured by `remnic connectors install weclone` in @remnic/core.\n */\n\nimport { createWeCloneProxy } from \"./proxy.js\";\nimport { parseConfig, type WeCloneConnectorConfig } from \"./config.js\";\nimport { createGracefulShutdownHandler, errorMessage } from \"./shutdown.js\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/**\n * Resolve the default proxy config path. Kept in lockstep with\n * @remnic/core's `resolveWeCloneProxyConfigPath()` so install/run pair up\n * without additional wiring from the caller.\n *\n * Both sides use `path.resolve()` (absolute) — NOT `path.join()` — so a\n * relative override like `REMNIC_HOME=tmp/remnic` is normalized against the\n * current working directory. If core and CLI disagreed on this, a relative\n * override could write the config in one location and read it from another,\n * producing spurious \"Config not found\" errors right after a successful\n * install.\n *\n * `HOME=\"\"` edge case: treat an empty-string HOME as absent and fall back\n * to `os.homedir()`. The core helper does the same; if they diverged here,\n * install and run would target different directories when `HOME` is\n * cleared (empty string is not nullish, so `?? os.homedir()` does NOT\n * substitute it).\n */\nfunction homeDir(): string {\n const envHome = process.env.HOME;\n return envHome && envHome.length > 0 ? envHome : homedir();\n}\n\nfunction expandTildePath(input: string): string {\n if (input === \"~\") return homeDir();\n if (input.startsWith(\"~/\") || input.startsWith(\"~\\\\\")) {\n return join(homeDir(), input.slice(2));\n }\n return input;\n}\n\nfunction defaultConfigPath(): string {\n const override =\n process.env.REMNIC_HOME && process.env.REMNIC_HOME.length > 0\n ? process.env.REMNIC_HOME\n : process.env.ENGRAM_HOME;\n if (override && override.length > 0) {\n return resolve(expandTildePath(override), \"connectors\", \"weclone.json\");\n }\n return resolve(homeDir(), \".remnic\", \"connectors\", \"weclone.json\");\n}\n\nasync function main(): Promise<void> {\n // Parse --config first so an explicit path takes precedence over env-var\n // resolution. Only fall back to defaultConfigPath() when the user has not\n // supplied an explicit --config flag. This lets `remnic-weclone-proxy\n // --config /abs/path` work even in environments where REMNIC_HOME is\n // misconfigured, without defaultConfigPath() (and any env-var validation\n // it contains) running unnecessarily.\n const args = process.argv.slice(2);\n let configPath: string | null = null;\n\n for (let i = 0; i < args.length; i++) {\n if (args[i] === \"--config\") {\n if (!args[i + 1] || args[i + 1].startsWith(\"-\")) {\n console.error(\"Error: --config requires a path argument\");\n process.exit(1);\n }\n configPath = resolve(expandTildePath(args[i + 1]));\n i++;\n }\n }\n\n if (configPath === null) {\n configPath = defaultConfigPath();\n }\n\n if (!existsSync(configPath)) {\n console.error(`Config not found: ${configPath}`);\n console.error(\"Run: remnic connectors install weclone\");\n process.exit(1);\n }\n\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(configPath, \"utf-8\"));\n } catch (err) {\n console.error(`Failed to parse config at ${configPath}: ${errorMessage(err)}`);\n process.exit(1);\n }\n\n if (typeof raw !== \"object\" || raw === null) {\n console.error(`Config at ${configPath} must be a JSON object`);\n process.exit(1);\n }\n\n let config: WeCloneConnectorConfig;\n try {\n config = parseConfig(raw);\n } catch (err) {\n console.error(`Invalid config at ${configPath}: ${errorMessage(err)}`);\n process.exit(1);\n }\n\n const proxy = createWeCloneProxy(config);\n try {\n await proxy.start();\n } catch (err) {\n console.error(`Failed to start WeClone proxy: ${errorMessage(err)}`);\n process.exit(1);\n }\n\n console.log(`WeClone memory proxy listening on :${config.proxyPort}`);\n console.log(` WeClone API: ${config.wecloneApiUrl}`);\n console.log(` Remnic daemon: ${config.remnicDaemonUrl}`);\n\n const stopAndExit = createGracefulShutdownHandler(proxy);\n process.on(\"SIGINT\", stopAndExit);\n process.on(\"SIGTERM\", stopAndExit);\n}\n\nvoid main().catch((err) => {\n console.error(`Failed to start WeClone proxy: ${errorMessage(err)}`);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -12,10 +12,15 @@ interface WeCloneConnectorConfig {
12
12
  wecloneApiUrl: string;
13
13
  wecloneModelName?: string;
14
14
  proxyPort: number;
15
+ proxyBindHost?: string;
16
+ allowPublicBind?: boolean;
15
17
  remnicDaemonUrl: string;
16
18
  remnicAuthToken?: string;
17
19
  sessionStrategy: "caller-id" | "single";
18
20
  memoryInjection: MemoryInjectionConfig;
21
+ maxRequestBytes?: number;
22
+ maxResponseBytes?: number;
23
+ streamObservationMaxBytes?: number;
19
24
  }
20
25
  declare const DEFAULT_CONFIG: WeCloneConnectorConfig;
21
26
  /**
@@ -37,6 +42,7 @@ interface WeCloneProxy {
37
42
  start(): Promise<void>;
38
43
  stop(): Promise<void>;
39
44
  port: number;
45
+ host: string;
40
46
  }
41
47
  /**
42
48
  * Create a WeClone proxy instance.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  createWeCloneProxy,
7
7
  formatMemoryBlock,
8
8
  parseConfig
9
- } from "./chunk-7V67D4WU.js";
9
+ } from "./chunk-3RVYVFUV.js";
10
10
 
11
11
  // src/installer.ts
12
12
  function generateWeCloneInstructions(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/connector-weclone",
3
- "version": "1.0.1",
3
+ "version": "9.3.515",
4
4
  "description": "OpenAI-compatible proxy adding Remnic persistent memory to WeClone avatars",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,6 +41,7 @@
41
41
  ],
42
42
  "scripts": {
43
43
  "build": "tsup src/index.ts src/cli.ts --format esm --dts",
44
- "test": "tsx --test src/*.test.ts"
44
+ "check-types": "tsc --noEmit",
45
+ "test": "npm run build && tsx --test src/*.test.ts"
45
46
  }
46
47
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/format.ts","../src/session.ts","../src/proxy.ts","../src/config.ts"],"sourcesContent":["/**\n * Memory format adapter.\n *\n * Converts Remnic recall results into system prompt sections that\n * can be injected into OpenAI-compatible chat completion requests.\n */\n\nexport interface RecallResult {\n content: string;\n confidence?: number;\n category?: string;\n}\n\nconst CHARS_PER_TOKEN = 4;\n\n/**\n * Format recall results into a memory block suitable for prompt injection.\n *\n * - Sorts memories by confidence (highest first; missing confidence sorts last)\n * - Truncates combined content to fit within `maxTokens` (approx 4 chars/token)\n * - Fills in the template's `{memories}` placeholder\n * - Returns empty string if no memories are provided\n */\nexport function formatMemoryBlock(\n memories: RecallResult[],\n template: string,\n maxTokens: number\n): string {\n if (memories.length === 0) {\n return \"\";\n }\n\n // Sort by confidence descending; undefined confidence sorts last\n const sorted = [...memories].sort((a, b) => {\n const aConf = a.confidence ?? -1;\n const bConf = b.confidence ?? -1;\n return bConf - aConf;\n });\n\n const maxChars = maxTokens * CHARS_PER_TOKEN;\n let totalChars = 0;\n const included: string[] = [];\n\n for (const memory of sorted) {\n const line = memory.content;\n if (totalChars + line.length > maxChars && included.length > 0) {\n break;\n }\n included.push(line);\n totalChars += line.length;\n }\n\n if (included.length === 0) {\n return \"\";\n }\n\n const memoriesText = included.join(\"\\n\");\n return template.replace(\"{memories}\", () => memoriesText);\n}\n","/**\n * Session mapping strategies.\n *\n * Maps caller identity to Remnic session keys so memory is scoped\n * appropriately per user or shared across all callers.\n */\n\nexport interface ChatCompletionRequest {\n model?: string;\n messages?: Array<{ role: string; content: string }>;\n user?: string;\n [key: string]: unknown;\n}\n\nexport interface SessionMapper {\n resolve(\n headers: Record<string, string | string[] | undefined>,\n body: ChatCompletionRequest\n ): string;\n}\n\n/**\n * Returns a fixed session key for single-user setups.\n */\nexport class SingleSessionMapper implements SessionMapper {\n private readonly key: string;\n\n constructor(key = \"weclone-default\") {\n this.key = key;\n }\n\n resolve(\n _headers: Record<string, string | string[] | undefined>,\n _body: ChatCompletionRequest\n ): string {\n return this.key;\n }\n}\n\n/**\n * Extracts caller identity from request metadata.\n *\n * Resolution order:\n * 1. `X-Caller-Id` header\n * 2. `user` field in the request body\n * 3. Falls back to \"default\"\n */\nexport class CallerIdSessionMapper implements SessionMapper {\n private readonly fallback: string;\n\n constructor(fallback = \"default\") {\n this.fallback = fallback;\n }\n\n resolve(\n headers: Record<string, string | string[] | undefined>,\n body: ChatCompletionRequest\n ): string {\n const headerValue = headers[\"x-caller-id\"];\n if (typeof headerValue === \"string\" && headerValue.length > 0) {\n return headerValue;\n }\n\n if (typeof body.user === \"string\" && body.user.length > 0) {\n return body.user;\n }\n\n return this.fallback;\n }\n}\n","/**\n * OpenAI-compatible HTTP proxy for WeClone with Remnic memory injection.\n *\n * Intercepts POST /v1/chat/completions to inject recalled memories,\n * forwards all other requests transparently to the WeClone API.\n */\n\nimport * as http from \"node:http\";\nimport type { WeCloneConnectorConfig } from \"./config.js\";\nimport { formatMemoryBlock, type RecallResult } from \"./format.js\";\nimport {\n SingleSessionMapper,\n CallerIdSessionMapper,\n type SessionMapper,\n type ChatCompletionRequest,\n} from \"./session.js\";\n\nexport interface WeCloneProxy {\n start(): Promise<void>;\n stop(): Promise<void>;\n port: number;\n}\n\n/**\n * Read the entire body of an IncomingMessage as a string (UTF-8).\n * Used for paths that need to parse JSON (e.g. chat completions).\n */\nfunction readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf-8\")));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Read the entire body of an IncomingMessage as raw bytes.\n * Used for the transparent proxy path to avoid corrupting binary/multipart uploads.\n */\nfunction readRawBody(req: http.IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Build a flat headers record from IncomingHttpHeaders,\n * normalizing array values to comma-separated strings.\n */\nfunction flattenHeaders(\n raw: http.IncomingHttpHeaders\n): Record<string, string> {\n const result: Record<string, string> = {};\n for (const [key, val] of Object.entries(raw)) {\n if (val === undefined) continue;\n result[key] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return result;\n}\n\n/**\n * Build standard headers for Remnic daemon requests.\n * Includes Authorization if an auth token is configured.\n */\nfunction remnicHeaders(authToken?: string): Record<string, string> {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (authToken) {\n headers[\"Authorization\"] = `Bearer ${authToken}`;\n }\n return headers;\n}\n\n/**\n * Call Remnic daemon recall endpoint for the given session and query.\n */\nasync function recallMemories(\n daemonUrl: string,\n sessionKey: string,\n query: string,\n authToken?: string\n): Promise<RecallResult[]> {\n const url = `${daemonUrl}/engram/v1/recall`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: remnicHeaders(authToken),\n body: JSON.stringify({ sessionKey, query }),\n });\n\n if (!res.ok) {\n throw new Error(`Remnic recall returned ${res.status}: ${await res.text()}`);\n }\n\n const data = (await res.json()) as { results?: Array<{ preview?: string; content?: string; confidence?: number; category?: string }> };\n const memories: RecallResult[] = (data.results ?? []).map((r) => ({\n content: r.preview || r.content || \"\",\n confidence: r.confidence,\n category: r.category,\n }));\n return memories;\n}\n\n/**\n * Fire-and-forget observation to the Remnic daemon.\n * Errors are caught and silently discarded to avoid adding latency.\n */\nfunction observeTurn(\n daemonUrl: string,\n sessionKey: string,\n userMessage: string,\n assistantMessage: string,\n authToken?: string\n): void {\n const url = `${daemonUrl}/engram/v1/observe`;\n fetch(url, {\n method: \"POST\",\n headers: remnicHeaders(authToken),\n body: JSON.stringify({\n sessionKey,\n messages: [\n { role: \"user\", content: userMessage },\n { role: \"assistant\", content: assistantMessage },\n ],\n }),\n }).catch(() => {\n // Intentionally swallowed -- observation must not affect the response path\n });\n}\n\n/**\n * Coerce an OpenAI chat message `content` into a plain text string.\n *\n * OpenAI chat messages can be either a string or an array of content\n * parts (e.g. `[{type:\"text\",text:\"...\"},{type:\"image_url\",...}]`) for\n * multimodal inputs. Recall/observe only operate on text, so we extract\n * and concatenate the `text` parts. Returns an empty string if no text\n * is present (e.g. image-only turn) so we skip recall rather than sending\n * non-string payloads to the Remnic daemon.\n */\nfunction extractTextContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const part of content) {\n if (\n part &&\n typeof part === \"object\" &&\n (part as { type?: unknown }).type === \"text\"\n ) {\n const text = (part as { text?: unknown }).text;\n if (typeof text === \"string\") parts.push(text);\n }\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the last user message's text content from a chat completion\n * messages array. Handles both string and multimodal array content.\n */\nfunction lastUserMessage(messages: Array<{ role: string; content: unknown }>): string {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") {\n return extractTextContent(messages[i].content);\n }\n }\n return \"\";\n}\n\n/**\n * Extract the assistant reply from a WeClone chat completion response.\n */\nfunction extractAssistantReply(responseBody: Record<string, unknown>): string {\n const choices = responseBody.choices as\n | Array<{ message?: { content?: string } }>\n | undefined;\n if (choices && choices.length > 0) {\n return choices[0]?.message?.content ?? \"\";\n }\n return \"\";\n}\n\n/**\n * Strip trailing slashes from a URL without using a regex quantifier\n * on the same character, which CodeQL flags as polynomial ReDoS\n * (`js/polynomial-redos`). A simple loop is O(n) and cannot backtrack.\n */\nfunction stripTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */) {\n end--;\n }\n return end === s.length ? s : s.slice(0, end);\n}\n\n/**\n * Parse a URL string into { origin, basePath } where `basePath` is the\n * configured path prefix (e.g. \"/weclone/v1\") with any trailing slashes\n * stripped. Falls back safely for malformed inputs.\n */\nfunction splitBaseUrl(urlStr: string): { origin: string; basePath: string } {\n try {\n const parsed = new URL(urlStr);\n const basePath = stripTrailingSlashes(parsed.pathname);\n return { origin: parsed.origin, basePath };\n } catch {\n // Fallback: strip trailing path components without ReDoS-prone regex.\n // Split on the first \"/\" after the scheme.\n const schemeEnd = urlStr.indexOf(\"://\");\n if (schemeEnd === -1) {\n return { origin: stripTrailingSlashes(urlStr), basePath: \"\" };\n }\n const afterScheme = urlStr.slice(schemeEnd + 3);\n const pathStart = afterScheme.indexOf(\"/\");\n if (pathStart === -1) {\n return { origin: urlStr, basePath: \"\" };\n }\n const origin = urlStr.slice(0, schemeEnd + 3 + pathStart);\n const basePath = stripTrailingSlashes(afterScheme.slice(pathStart));\n return { origin, basePath };\n }\n}\n\n/**\n * Hop-by-hop request headers that must not be forwarded to upstream.\n * Per RFC 2616 §13.5.1 / RFC 7230 §6.1 these apply only to the\n * immediate transport connection. `proxy-authorization` is the most\n * critical — leaking it would send proxy credentials to the origin.\n *\n * `host` is deliberately excluded from this set because it is\n * always replaced (not just stripped) with the upstream origin\n * and is handled separately below.\n */\nconst HOP_BY_HOP_REQUEST_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\n/**\n * Headers that must not be forwarded from the upstream response.\n * These are hop-by-hop headers that apply to a single transport connection\n * and would conflict with our fully-buffered response write.\n *\n * `content-encoding` is included because fetch() auto-decompresses the body.\n * When we buffer with arrayBuffer() and relay, the bytes are already decoded;\n * forwarding `content-encoding: gzip` would label decompressed bytes as gzip.\n */\nconst HOP_BY_HOP_RESPONSE_HEADERS = new Set([\n \"transfer-encoding\",\n \"content-encoding\",\n \"connection\",\n \"keep-alive\",\n \"upgrade\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n]);\n\n/**\n * Forward a request transparently to the WeClone API.\n *\n * If the configured WeClone URL has a non-empty base path (e.g.\n * \"https://host/weclone/v1\"), the proxy forwards incoming request paths\n * such that \"/v1/models\" maps to \"https://host/weclone/v1/models\". For\n * URLs without a base path, paths map 1:1 to the upstream origin.\n *\n * The request body (if any) is forwarded as raw bytes via Uint8Array so\n * that multipart/binary uploads are not corrupted.\n *\n * Reads the full upstream response before writing to the client\n * to avoid partial-header or hanging-body issues.\n */\nasync function transparentProxy(\n weclone: { origin: string; basePath: string },\n method: string,\n path: string,\n headers: Record<string, string>,\n body: Buffer | null,\n res: http.ServerResponse\n): Promise<void> {\n // Map the client-facing path into an upstream path.\n //\n // The proxy exposes an OpenAI-compatible `/v1/...` surface. When the\n // configured `wecloneApiUrl` itself already ends in `/v1` (or any\n // path prefix), treat the configured prefix as the upstream mount\n // point and rewrite `/v1/<rest>` to `<basePath>/<rest>`.\n //\n // - basePath \"\" (no prefix): forward path as-is.\n // - basePath \"/v1\": \"/v1/models\" -> \"/v1/models\" (no change).\n // - basePath \"/weclone/v1\": \"/v1/models\" -> \"/weclone/v1/models\".\n //\n // Split off any query string so rewriting operates on the pathname only.\n const qIdx = path.indexOf(\"?\");\n const rawPath = qIdx === -1 ? path : path.slice(0, qIdx);\n const querySuffix = qIdx === -1 ? \"\" : path.slice(qIdx);\n let upstreamPathname = rawPath;\n if (weclone.basePath.length > 0) {\n if (rawPath === \"/v1\" || rawPath.startsWith(\"/v1/\")) {\n upstreamPathname = `${weclone.basePath}${rawPath.slice(3)}`;\n } else if (!rawPath.startsWith(weclone.basePath)) {\n upstreamPathname = `${weclone.basePath}${rawPath}`;\n }\n }\n const targetUrl = `${weclone.origin}${upstreamPathname}${querySuffix}`;\n\n // Remove hop-by-hop request headers and replace host with upstream origin\n const forwardHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (key === \"host\" || HOP_BY_HOP_REQUEST_HEADERS.has(key)) continue;\n // content-length is recomputed by fetch() for the forwarded body\n if (key === \"content-length\") continue;\n forwardHeaders[key] = value;\n }\n\n const fetchInit: RequestInit = {\n method,\n headers: forwardHeaders,\n };\n if (body && method !== \"GET\" && method !== \"HEAD\") {\n // Copy into a plain ArrayBuffer so the forwarded request keeps the exact\n // byte payload while remaining compatible with this package's BodyInit\n // typing during declaration builds.\n const rawBody = new ArrayBuffer(body.byteLength);\n new Uint8Array(rawBody).set(body);\n fetchInit.body = rawBody;\n }\n\n try {\n const upstream = await fetch(targetUrl, fetchInit);\n\n // Read full body before sending any headers to the client\n const responseBody = await upstream.arrayBuffer();\n const responseBuffer = Buffer.from(responseBody);\n\n // Build response headers, filtering hop-by-hop and setting Content-Length\n const responseHeaders: Record<string, string> = {};\n for (const [key, value] of upstream.headers.entries()) {\n if (!HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase())) {\n responseHeaders[key] = value;\n }\n }\n responseHeaders[\"content-length\"] = String(responseBuffer.length);\n\n res.writeHead(upstream.status, responseHeaders);\n res.end(responseBuffer);\n } catch (_err) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"upstream_unreachable\" }));\n }\n}\n\n/**\n * Create a WeClone proxy instance.\n */\nexport function createWeCloneProxy(config: WeCloneConnectorConfig): WeCloneProxy {\n // Normalize upstream URLs: strip trailing slashes to prevent double-slash\n // when appending path segments. Use a loop (not regex) to avoid the\n // polynomial-ReDoS class flagged by CodeQL for `/\\/+$/`.\n const wecloneApiUrl = stripTrailingSlashes(config.wecloneApiUrl);\n const remnicDaemonUrl = stripTrailingSlashes(config.remnicDaemonUrl);\n // Pre-split the WeClone URL so transparentProxy and the chat path can\n // honor a configured base path (e.g. \"/weclone/v1\").\n const wecloneParts = splitBaseUrl(wecloneApiUrl);\n\n const sessionMapper: SessionMapper =\n config.sessionStrategy === \"caller-id\"\n ? new CallerIdSessionMapper()\n : new SingleSessionMapper();\n\n let server: http.Server | null = null;\n let resolvedPort = config.proxyPort;\n\n const requestHandler = async (\n req: http.IncomingMessage,\n res: http.ServerResponse\n ): Promise<void> => {\n const url = req.url ?? \"/\";\n const method = (req.method ?? \"GET\").toUpperCase();\n\n // Parse the request URL into a pathname (stripping query string and\n // normalizing trailing slash). Using pathname for route matching avoids\n // silently falling through when clients append query params like\n // `?api-version=2023-05-15` (common with Azure OpenAI-compatible SDKs).\n let pathname = url;\n const queryStart = url.indexOf(\"?\");\n if (queryStart !== -1) pathname = url.slice(0, queryStart);\n // Normalize trailing slash for route matching only (not for forwarding).\n const normalizedPathname =\n pathname.length > 1 && pathname.endsWith(\"/\")\n ? pathname.slice(0, -1)\n : pathname;\n\n // --- Health check ---\n if (normalizedPathname === \"/health\" && method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({\n status: \"ok\",\n wecloneApi: config.wecloneApiUrl,\n }));\n return;\n }\n\n // --- Chat completions with memory injection ---\n if (normalizedPathname === \"/v1/chat/completions\" && method === \"POST\") {\n let bodyStr: string;\n try {\n bodyStr = await readBody(req);\n } catch {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"bad_request\", detail: \"Could not read request body\" }));\n return;\n }\n\n let parsed: ChatCompletionRequest;\n try {\n parsed = JSON.parse(bodyStr) as ChatCompletionRequest;\n } catch {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"bad_request\", detail: \"Invalid JSON body\" }));\n return;\n }\n\n const headers = req.headers as Record<string, string | string[] | undefined>;\n const sessionKey = sessionMapper.resolve(headers, parsed);\n // Validate `messages` is an array with object entries before use so\n // malformed payloads (`messages: \"...\"`, `messages: {}`, etc.) return\n // a structured 400 instead of surfacing as a 500 internal error.\n if (parsed.messages !== undefined && !Array.isArray(parsed.messages)) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"bad_request\",\n detail: \"messages must be an array\",\n })\n );\n return;\n }\n // Messages may contain multimodal content-parts arrays; keep them\n // untyped and validate strings at each use site. Drop entries that\n // are not plain objects so downstream `.map()` cannot throw.\n const rawMessages: Array<{ role: string; content: unknown }> = [];\n for (const raw of parsed.messages ?? []) {\n if (raw === null || typeof raw !== \"object\") continue;\n const entry = raw as { role?: unknown; content?: unknown };\n rawMessages.push({\n role: typeof entry.role === \"string\" ? entry.role : \"\",\n content: entry.content,\n });\n }\n const query = lastUserMessage(rawMessages);\n\n // Recall memories (graceful degradation on failure)\n let memoryBlock = \"\";\n if (query.length > 0) {\n try {\n const memories = await recallMemories(\n remnicDaemonUrl,\n sessionKey,\n query,\n config.remnicAuthToken\n );\n memoryBlock = formatMemoryBlock(\n memories,\n config.memoryInjection.template,\n config.memoryInjection.maxTokens\n );\n } catch {\n // Remnic recall failed -- proceed without memory injection\n }\n }\n\n // Build the forwarded messages array. Only the *first* system message\n // is rewritten with injected memory (or, if no system exists, a\n // synthetic system message is prepended). Subsequent system messages\n // are forwarded verbatim so distinct system instructions are not\n // silently overwritten.\n const outMessages: Array<{ role: string; content: unknown }> = [];\n const firstSystemIdx = rawMessages.findIndex((m) => m.role === \"system\");\n const position = config.memoryInjection.position;\n\n if (memoryBlock.length === 0) {\n // No memory to inject — forward original messages unchanged.\n for (const m of rawMessages) outMessages.push(m);\n } else if (firstSystemIdx === -1) {\n // No existing system message: prepend a synthetic one.\n outMessages.push({ role: \"system\", content: memoryBlock });\n for (const m of rawMessages) outMessages.push(m);\n } else {\n for (let i = 0; i < rawMessages.length; i++) {\n const m = rawMessages[i];\n if (i === firstSystemIdx) {\n const existing = extractTextContent(m.content);\n outMessages.push({\n role: \"system\",\n content:\n position === \"system-prepend\"\n ? `${memoryBlock}\\n\\n${existing}`\n : `${existing}\\n\\n${memoryBlock}`,\n });\n } else {\n outMessages.push(m);\n }\n }\n }\n\n const modifiedBody = {\n ...parsed,\n messages: outMessages,\n };\n\n // Forward to WeClone. If `wecloneApiUrl` has a path prefix (the\n // common `/v1` or custom mounts like `/weclone/v1`), forward to\n // `${basePath}/chat/completions`. If the configured URL has no\n // base path at all, default to the standard OpenAI `/v1/chat/completions`.\n // Preserve any query string on the incoming request (e.g. Azure's\n // `?api-version=...`) so version selectors and tenant hints reach\n // upstream unchanged.\n const chatBase = wecloneParts.basePath.length > 0\n ? wecloneParts.basePath\n : \"/v1\";\n const qIdx = url.indexOf(\"?\");\n const querySuffix = qIdx === -1 ? \"\" : url.slice(qIdx);\n const targetUrl =\n `${wecloneParts.origin}${chatBase}/chat/completions${querySuffix}`;\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n // Preserve authorization if present\n const authHeader = req.headers[\"authorization\"];\n if (typeof authHeader === \"string\") {\n forwardHeaders[\"Authorization\"] = authHeader;\n }\n\n try {\n const upstream = await fetch(targetUrl, {\n method: \"POST\",\n headers: forwardHeaders,\n body: JSON.stringify(modifiedBody),\n });\n\n // --- Streaming path ---\n if (parsed.stream === true) {\n // If upstream returned an error, pass through as-is (don't force SSE headers)\n if (!upstream.ok) {\n const errBody = await upstream.arrayBuffer();\n res.writeHead(upstream.status, {\n \"content-type\": upstream.headers.get(\"content-type\") || \"application/json\",\n });\n res.end(Buffer.from(errBody));\n return;\n }\n\n res.writeHead(upstream.status, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n });\n\n const reader = upstream.body?.getReader();\n if (!reader) {\n res.end();\n return;\n }\n\n const chunks: Uint8Array[] = [];\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n res.write(value);\n }\n } finally {\n res.end();\n }\n\n // Best-effort: reconstruct assistant content for observation\n try {\n const fullText = Buffer.concat(chunks).toString(\"utf-8\");\n const contentParts: string[] = [];\n for (const line of fullText.split(\"\\n\")) {\n if (!line.startsWith(\"data: \") || line === \"data: [DONE]\") continue;\n try {\n const event = JSON.parse(line.slice(6)) as {\n choices?: Array<{ delta?: { content?: string } }>;\n };\n const delta = event.choices?.[0]?.delta?.content;\n if (delta) contentParts.push(delta);\n } catch {\n // Malformed SSE chunk -- skip\n }\n }\n if (contentParts.length > 0 && query.length > 0) {\n observeTurn(\n remnicDaemonUrl,\n sessionKey,\n query,\n contentParts.join(\"\"),\n config.remnicAuthToken\n );\n }\n } catch {\n // Observation reconstruction failed -- non-critical\n }\n return;\n }\n\n // --- Non-streaming path ---\n const responseBuffer = await upstream.arrayBuffer();\n const responseBytes = Buffer.from(responseBuffer);\n\n // Parse response for observation (best-effort)\n let assistantReply = \"\";\n try {\n const responseJson = JSON.parse(\n responseBytes.toString(\"utf-8\")\n ) as Record<string, unknown>;\n assistantReply = extractAssistantReply(responseJson);\n } catch {\n // Non-JSON response -- skip observation\n }\n\n // Fire-and-forget observe\n if (query.length > 0 && assistantReply.length > 0) {\n observeTurn(remnicDaemonUrl, sessionKey, query, assistantReply, config.remnicAuthToken);\n }\n\n // Return upstream response to caller, stripping hop-by-hop headers\n const chatResponseHeaders: Record<string, string> = {};\n for (const [key, value] of upstream.headers.entries()) {\n if (!HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase())) {\n chatResponseHeaders[key] = value;\n }\n }\n chatResponseHeaders[\"content-length\"] = String(responseBytes.length);\n res.writeHead(upstream.status, chatResponseHeaders);\n res.end(responseBytes);\n } catch (_err) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({\n error: \"upstream_unreachable\",\n }));\n }\n return;\n }\n\n // --- All other paths: transparent proxy ---\n // Use raw bytes to avoid corrupting binary/multipart uploads.\n const body = method !== \"GET\" && method !== \"HEAD\" ? await readRawBody(req) : null;\n const flat = flattenHeaders(req.headers);\n await transparentProxy(wecloneParts, method, url, flat, body, res);\n };\n\n return {\n get port() {\n return resolvedPort;\n },\n\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n server = http.createServer((req, res) => {\n requestHandler(req, res).catch((_err) => {\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"internal_proxy_error\" }));\n }\n });\n });\n\n server.on(\"error\", reject);\n\n server.listen(config.proxyPort, () => {\n const addr = server!.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolvedPort = addr.port;\n }\n resolve();\n });\n });\n },\n\n stop(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (!server) {\n resolve();\n return;\n }\n server.close((err) => {\n server = null;\n if (err) reject(err);\n else resolve();\n });\n });\n },\n };\n}\n","/**\n * WeClone connector configuration.\n *\n * Validates user-provided config and applies defaults for optional fields.\n */\n\nexport interface MemoryInjectionConfig {\n maxTokens: number;\n position: \"system-append\" | \"system-prepend\";\n template: string;\n}\n\nexport interface WeCloneConnectorConfig {\n wecloneApiUrl: string;\n wecloneModelName?: string;\n proxyPort: number;\n remnicDaemonUrl: string;\n remnicAuthToken?: string;\n sessionStrategy: \"caller-id\" | \"single\";\n memoryInjection: MemoryInjectionConfig;\n}\n\nexport const DEFAULT_CONFIG: WeCloneConnectorConfig = {\n wecloneApiUrl: \"http://localhost:8000/v1\",\n wecloneModelName: \"weclone-avatar\",\n proxyPort: 8100,\n remnicDaemonUrl: \"http://localhost:4318\",\n sessionStrategy: \"single\",\n memoryInjection: {\n maxTokens: 1500,\n position: \"system-append\",\n template: \"[Memory Context]\\n{memories}\\n[End Memory Context]\",\n },\n};\n\nconst VALID_SESSION_STRATEGIES = [\"caller-id\", \"single\"] as const;\nconst VALID_POSITIONS = [\"system-append\", \"system-prepend\"] as const;\n\n/**\n * Parse and validate a raw config object into a WeCloneConnectorConfig.\n *\n * Rejects missing required fields and invalid values with clear messages.\n * Applies defaults for all optional fields.\n */\nexport function parseConfig(raw: unknown): WeCloneConnectorConfig {\n if (typeof raw !== \"object\" || raw === null) {\n throw new Error(\"Config must be a non-null object\");\n }\n\n const obj = raw as Record<string, unknown>;\n\n // --- Required fields ---\n if (typeof obj.wecloneApiUrl !== \"string\" || obj.wecloneApiUrl.length === 0) {\n throw new Error(\n \"Config 'wecloneApiUrl' is required and must be a non-empty string\"\n );\n }\n\n if (\n typeof obj.proxyPort !== \"number\" ||\n !Number.isInteger(obj.proxyPort) ||\n obj.proxyPort <= 0 ||\n obj.proxyPort > 65535\n ) {\n throw new Error(\n \"Config 'proxyPort' is required and must be an integer between 1 and 65535\"\n );\n }\n\n if (typeof obj.remnicDaemonUrl !== \"string\" || obj.remnicDaemonUrl.length === 0) {\n throw new Error(\n \"Config 'remnicDaemonUrl' is required and must be a non-empty string\"\n );\n }\n\n // --- Optional fields with validation ---\n let remnicAuthToken: string | undefined;\n if (obj.remnicAuthToken !== undefined) {\n if (typeof obj.remnicAuthToken !== \"string\" || obj.remnicAuthToken.length === 0) {\n throw new Error(\n \"Config 'remnicAuthToken' must be a non-empty string when provided\"\n );\n }\n remnicAuthToken = obj.remnicAuthToken;\n }\n\n const wecloneModelName =\n obj.wecloneModelName !== undefined\n ? String(obj.wecloneModelName)\n : DEFAULT_CONFIG.wecloneModelName;\n\n let sessionStrategy = DEFAULT_CONFIG.sessionStrategy;\n if (obj.sessionStrategy !== undefined) {\n if (!VALID_SESSION_STRATEGIES.includes(obj.sessionStrategy as typeof VALID_SESSION_STRATEGIES[number])) {\n throw new Error(\n `Config 'sessionStrategy' must be one of: ${VALID_SESSION_STRATEGIES.join(\", \")}. ` +\n `Got: ${JSON.stringify(obj.sessionStrategy)}`\n );\n }\n sessionStrategy = obj.sessionStrategy as typeof sessionStrategy;\n }\n\n // --- Memory injection ---\n let memoryInjection = { ...DEFAULT_CONFIG.memoryInjection };\n if (obj.memoryInjection !== undefined) {\n if (typeof obj.memoryInjection !== \"object\" || obj.memoryInjection === null) {\n throw new Error(\"Config 'memoryInjection' must be an object\");\n }\n const mi = obj.memoryInjection as Record<string, unknown>;\n\n if (mi.maxTokens !== undefined) {\n if (typeof mi.maxTokens !== \"number\" || !Number.isInteger(mi.maxTokens) || mi.maxTokens <= 0) {\n throw new Error(\n \"Config 'memoryInjection.maxTokens' must be a positive integer\"\n );\n }\n memoryInjection.maxTokens = mi.maxTokens;\n }\n\n if (mi.position !== undefined) {\n if (!VALID_POSITIONS.includes(mi.position as typeof VALID_POSITIONS[number])) {\n throw new Error(\n `Config 'memoryInjection.position' must be one of: ` +\n `${VALID_POSITIONS.join(\", \")}. Got: ${JSON.stringify(mi.position)}`\n );\n }\n memoryInjection.position = mi.position as typeof memoryInjection.position;\n }\n\n if (mi.template !== undefined) {\n if (typeof mi.template !== \"string\" || mi.template.length === 0) {\n throw new Error(\n \"Config 'memoryInjection.template' must be a non-empty string\"\n );\n }\n memoryInjection.template = mi.template;\n }\n }\n\n return {\n wecloneApiUrl: obj.wecloneApiUrl,\n wecloneModelName,\n proxyPort: obj.proxyPort,\n remnicDaemonUrl: obj.remnicDaemonUrl,\n remnicAuthToken,\n sessionStrategy,\n memoryInjection,\n };\n}\n"],"mappings":"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","names":[]}