@remnic/connector-weclone 1.0.1 → 9.3.517
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 +5 -0
- package/dist/{chunk-7V67D4WU.js → chunk-3RVYVFUV.js} +343 -57
- package/dist/chunk-3RVYVFUV.js.map +1 -0
- package/dist/cli.js +90 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-7V67D4WU.js.map +0 -1
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
|
|
49
|
-
if (
|
|
50
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
req.on("
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
req.on("
|
|
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
|
|
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 (
|
|
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
|
|
399
|
+
let parsedJson;
|
|
278
400
|
try {
|
|
279
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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.
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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 (
|
|
608
|
+
if (!observationDisabled && assistantContent.length > 0 && query.length > 0) {
|
|
414
609
|
observeTurn(
|
|
415
610
|
remnicDaemonUrl,
|
|
416
611
|
sessionKey,
|
|
417
612
|
query,
|
|
418
|
-
|
|
613
|
+
assistantContent,
|
|
419
614
|
config.remnicAuthToken
|
|
420
615
|
);
|
|
421
616
|
}
|
|
@@ -423,8 +618,7 @@ ${memoryBlock}`
|
|
|
423
618
|
}
|
|
424
619
|
return;
|
|
425
620
|
}
|
|
426
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/connector-weclone",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.3.517",
|
|
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
|
-
"
|
|
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":";;;AAaA,IAAM,kBAAkB;AAUjB,SAAS,kBACd,UACA,UACA,WACQ;AACR,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM;AAC1C,UAAM,QAAQ,EAAE,cAAc;AAC9B,UAAM,QAAQ,EAAE,cAAc;AAC9B,WAAO,QAAQ;AAAA,EACjB,CAAC;AAED,QAAM,WAAW,YAAY;AAC7B,MAAI,aAAa;AACjB,QAAM,WAAqB,CAAC;AAE5B,aAAW,UAAU,QAAQ;AAC3B,UAAM,OAAO,OAAO;AACpB,QAAI,aAAa,KAAK,SAAS,YAAY,SAAS,SAAS,GAAG;AAC9D;AAAA,IACF;AACA,aAAS,KAAK,IAAI;AAClB,kBAAc,KAAK;AAAA,EACrB;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,SAAS,KAAK,IAAI;AACvC,SAAO,SAAS,QAAQ,cAAc,MAAM,YAAY;AAC1D;;;AClCO,IAAM,sBAAN,MAAmD;AAAA,EACvC;AAAA,EAEjB,YAAY,MAAM,mBAAmB;AACnC,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,QACE,UACA,OACQ;AACR,WAAO,KAAK;AAAA,EACd;AACF;AAUO,IAAM,wBAAN,MAAqD;AAAA,EACzC;AAAA,EAEjB,YAAY,WAAW,WAAW;AAChC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,QACE,SACA,MACQ;AACR,UAAM,cAAc,QAAQ,aAAa;AACzC,QAAI,OAAO,gBAAgB,YAAY,YAAY,SAAS,GAAG;AAC7D,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,GAAG;AACzD,aAAO,KAAK;AAAA,IACd;AAEA,WAAO,KAAK;AAAA,EACd;AACF;;;AC9DA,YAAY,UAAU;AAoBtB,SAAS,SAAS,KAA4C;AAC5D,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAmB,CAAC;AAC1B,QAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,QAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO,CAAC,CAAC;AACpE,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAMA,SAAS,YAAY,KAA4C;AAC/D,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAmB,CAAC;AAC1B,QAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,QAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,OAAO,MAAM,CAAC,CAAC;AAClD,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAMA,SAAS,eACP,KACwB;AACxB,QAAM,SAAiC,CAAC;AACxC,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC5C,QAAI,QAAQ,OAAW;AACvB,WAAO,GAAG,IAAI,MAAM,QAAQ,GAAG,IAAI,IAAI,KAAK,IAAI,IAAI;AAAA,EACtD;AACA,SAAO;AACT;AAMA,SAAS,cAAc,WAA4C;AACjE,QAAM,UAAkC,EAAE,gBAAgB,mBAAmB;AAC7E,MAAI,WAAW;AACb,YAAQ,eAAe,IAAI,UAAU,SAAS;AAAA,EAChD;AACA,SAAO;AACT;AAKA,eAAe,eACb,WACA,YACA,OACA,WACyB;AACzB,QAAM,MAAM,GAAG,SAAS;AACxB,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,QAAQ;AAAA,IACR,SAAS,cAAc,SAAS;AAAA,IAChC,MAAM,KAAK,UAAU,EAAE,YAAY,MAAM,CAAC;AAAA,EAC5C,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,KAAK,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,EAC7E;AAEA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAM,YAA4B,KAAK,WAAW,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,IAChE,SAAS,EAAE,WAAW,EAAE,WAAW;AAAA,IACnC,YAAY,EAAE;AAAA,IACd,UAAU,EAAE;AAAA,EACd,EAAE;AACF,SAAO;AACT;AAMA,SAAS,YACP,WACA,YACA,aACA,kBACA,WACM;AACN,QAAM,MAAM,GAAG,SAAS;AACxB,QAAM,KAAK;AAAA,IACT,QAAQ;AAAA,IACR,SAAS,cAAc,SAAS;AAAA,IAChC,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,UAAU;AAAA,QACR,EAAE,MAAM,QAAQ,SAAS,YAAY;AAAA,QACrC,EAAE,MAAM,aAAa,SAAS,iBAAiB;AAAA,MACjD;AAAA,IACF,CAAC;AAAA,EACH,CAAC,EAAE,MAAM,MAAM;AAAA,EAEf,CAAC;AACH;AAYA,SAAS,mBAAmB,SAA0B;AACpD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,SAAS;AAC1B,QACE,QACA,OAAO,SAAS,YACf,KAA4B,SAAS,QACtC;AACA,YAAM,OAAQ,KAA4B;AAC1C,UAAI,OAAO,SAAS,SAAU,OAAM,KAAK,IAAI;AAAA,IAC/C;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAMA,SAAS,gBAAgB,UAA6D;AACpF,WAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC7C,QAAI,SAAS,CAAC,EAAE,SAAS,QAAQ;AAC/B,aAAO,mBAAmB,SAAS,CAAC,EAAE,OAAO;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,sBAAsB,cAA+C;AAC5E,QAAM,UAAU,aAAa;AAG7B,MAAI,WAAW,QAAQ,SAAS,GAAG;AACjC,WAAO,QAAQ,CAAC,GAAG,SAAS,WAAW;AAAA,EACzC;AACA,SAAO;AACT;AAOA,SAAS,qBAAqB,GAAmB;AAC/C,MAAI,MAAM,EAAE;AACZ,SAAO,MAAM,KAAK,EAAE,WAAW,MAAM,CAAC,MAAM,IAAc;AACxD;AAAA,EACF;AACA,SAAO,QAAQ,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG;AAC9C;AAOA,SAAS,aAAa,QAAsD;AAC1E,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,MAAM;AAC7B,UAAM,WAAW,qBAAqB,OAAO,QAAQ;AACrD,WAAO,EAAE,QAAQ,OAAO,QAAQ,SAAS;AAAA,EAC3C,QAAQ;AAGN,UAAM,YAAY,OAAO,QAAQ,KAAK;AACtC,QAAI,cAAc,IAAI;AACpB,aAAO,EAAE,QAAQ,qBAAqB,MAAM,GAAG,UAAU,GAAG;AAAA,IAC9D;AACA,UAAM,cAAc,OAAO,MAAM,YAAY,CAAC;AAC9C,UAAM,YAAY,YAAY,QAAQ,GAAG;AACzC,QAAI,cAAc,IAAI;AACpB,aAAO,EAAE,QAAQ,QAAQ,UAAU,GAAG;AAAA,IACxC;AACA,UAAM,SAAS,OAAO,MAAM,GAAG,YAAY,IAAI,SAAS;AACxD,UAAM,WAAW,qBAAqB,YAAY,MAAM,SAAS,CAAC;AAClE,WAAO,EAAE,QAAQ,SAAS;AAAA,EAC5B;AACF;AAYA,IAAM,6BAA6B,oBAAI,IAAI;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAWD,IAAM,8BAA8B,oBAAI,IAAI;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAgBD,eAAe,iBACb,SACA,QACA,MACA,SACA,MACA,KACe;AAaf,QAAM,OAAO,KAAK,QAAQ,GAAG;AAC7B,QAAM,UAAU,SAAS,KAAK,OAAO,KAAK,MAAM,GAAG,IAAI;AACvD,QAAM,cAAc,SAAS,KAAK,KAAK,KAAK,MAAM,IAAI;AACtD,MAAI,mBAAmB;AACvB,MAAI,QAAQ,SAAS,SAAS,GAAG;AAC/B,QAAI,YAAY,SAAS,QAAQ,WAAW,MAAM,GAAG;AACnD,yBAAmB,GAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3D,WAAW,CAAC,QAAQ,WAAW,QAAQ,QAAQ,GAAG;AAChD,yBAAmB,GAAG,QAAQ,QAAQ,GAAG,OAAO;AAAA,IAClD;AAAA,EACF;AACA,QAAM,YAAY,GAAG,QAAQ,MAAM,GAAG,gBAAgB,GAAG,WAAW;AAGpE,QAAM,iBAAyC,CAAC;AAChD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,QAAI,QAAQ,UAAU,2BAA2B,IAAI,GAAG,EAAG;AAE3D,QAAI,QAAQ,iBAAkB;AAC9B,mBAAe,GAAG,IAAI;AAAA,EACxB;AAEA,QAAM,YAAyB;AAAA,IAC7B;AAAA,IACA,SAAS;AAAA,EACX;AACA,MAAI,QAAQ,WAAW,SAAS,WAAW,QAAQ;AAIjD,UAAM,UAAU,IAAI,YAAY,KAAK,UAAU;AAC/C,QAAI,WAAW,OAAO,EAAE,IAAI,IAAI;AAChC,cAAU,OAAO;AAAA,EACnB;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,WAAW,SAAS;AAGjD,UAAM,eAAe,MAAM,SAAS,YAAY;AAChD,UAAM,iBAAiB,OAAO,KAAK,YAAY;AAG/C,UAAM,kBAA0C,CAAC;AACjD,eAAW,CAAC,KAAK,KAAK,KAAK,SAAS,QAAQ,QAAQ,GAAG;AACrD,UAAI,CAAC,4BAA4B,IAAI,IAAI,YAAY,CAAC,GAAG;AACvD,wBAAgB,GAAG,IAAI;AAAA,MACzB;AAAA,IACF;AACA,oBAAgB,gBAAgB,IAAI,OAAO,eAAe,MAAM;AAEhE,QAAI,UAAU,SAAS,QAAQ,eAAe;AAC9C,QAAI,IAAI,cAAc;AAAA,EACxB,SAAS,MAAM;AACb,QAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,uBAAuB,CAAC,CAAC;AAAA,EAC3D;AACF;AAKO,SAAS,mBAAmB,QAA8C;AAI/E,QAAM,gBAAgB,qBAAqB,OAAO,aAAa;AAC/D,QAAM,kBAAkB,qBAAqB,OAAO,eAAe;AAGnE,QAAM,eAAe,aAAa,aAAa;AAE/C,QAAM,gBACJ,OAAO,oBAAoB,cACvB,IAAI,sBAAsB,IAC1B,IAAI,oBAAoB;AAE9B,MAAI,SAA6B;AACjC,MAAI,eAAe,OAAO;AAE1B,QAAM,iBAAiB,OACrB,KACA,QACkB;AAClB,UAAM,MAAM,IAAI,OAAO;AACvB,UAAM,UAAU,IAAI,UAAU,OAAO,YAAY;AAMjD,QAAI,WAAW;AACf,UAAM,aAAa,IAAI,QAAQ,GAAG;AAClC,QAAI,eAAe,GAAI,YAAW,IAAI,MAAM,GAAG,UAAU;AAEzD,UAAM,qBACJ,SAAS,SAAS,KAAK,SAAS,SAAS,GAAG,IACxC,SAAS,MAAM,GAAG,EAAE,IACpB;AAGN,QAAI,uBAAuB,aAAa,WAAW,OAAO;AACxD,UAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,UAAI,IAAI,KAAK,UAAU;AAAA,QACrB,QAAQ;AAAA,QACR,YAAY,OAAO;AAAA,MACrB,CAAC,CAAC;AACF;AAAA,IACF;AAGA,QAAI,uBAAuB,0BAA0B,WAAW,QAAQ;AACtE,UAAI;AACJ,UAAI;AACF,kBAAU,MAAM,SAAS,GAAG;AAAA,MAC9B,QAAQ;AACN,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,OAAO,eAAe,QAAQ,8BAA8B,CAAC,CAAC;AACvF;AAAA,MACF;AAEA,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,OAAO;AAAA,MAC7B,QAAQ;AACN,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,OAAO,eAAe,QAAQ,oBAAoB,CAAC,CAAC;AAC7E;AAAA,MACF;AAEA,YAAM,UAAU,IAAI;AACpB,YAAM,aAAa,cAAc,QAAQ,SAAS,MAAM;AAIxD,UAAI,OAAO,aAAa,UAAa,CAAC,MAAM,QAAQ,OAAO,QAAQ,GAAG;AACpE,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI;AAAA,UACF,KAAK,UAAU;AAAA,YACb,OAAO;AAAA,YACP,QAAQ;AAAA,UACV,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAIA,YAAM,cAAyD,CAAC;AAChE,iBAAW,OAAO,OAAO,YAAY,CAAC,GAAG;AACvC,YAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU;AAC7C,cAAM,QAAQ;AACd,oBAAY,KAAK;AAAA,UACf,MAAM,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAAA,UACpD,SAAS,MAAM;AAAA,QACjB,CAAC;AAAA,MACH;AACA,YAAM,QAAQ,gBAAgB,WAAW;AAGzC,UAAI,cAAc;AAClB,UAAI,MAAM,SAAS,GAAG;AACpB,YAAI;AACF,gBAAM,WAAW,MAAM;AAAA,YACrB;AAAA,YACA;AAAA,YACA;AAAA,YACA,OAAO;AAAA,UACT;AACA,wBAAc;AAAA,YACZ;AAAA,YACA,OAAO,gBAAgB;AAAA,YACvB,OAAO,gBAAgB;AAAA,UACzB;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAOA,YAAM,cAAyD,CAAC;AAChE,YAAM,iBAAiB,YAAY,UAAU,CAAC,MAAM,EAAE,SAAS,QAAQ;AACvE,YAAM,WAAW,OAAO,gBAAgB;AAExC,UAAI,YAAY,WAAW,GAAG;AAE5B,mBAAW,KAAK,YAAa,aAAY,KAAK,CAAC;AAAA,MACjD,WAAW,mBAAmB,IAAI;AAEhC,oBAAY,KAAK,EAAE,MAAM,UAAU,SAAS,YAAY,CAAC;AACzD,mBAAW,KAAK,YAAa,aAAY,KAAK,CAAC;AAAA,MACjD,OAAO;AACL,iBAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,gBAAM,IAAI,YAAY,CAAC;AACvB,cAAI,MAAM,gBAAgB;AACxB,kBAAM,WAAW,mBAAmB,EAAE,OAAO;AAC7C,wBAAY,KAAK;AAAA,cACf,MAAM;AAAA,cACN,SACE,aAAa,mBACT,GAAG,WAAW;AAAA;AAAA,EAAO,QAAQ,KAC7B,GAAG,QAAQ;AAAA;AAAA,EAAO,WAAW;AAAA,YACrC,CAAC;AAAA,UACH,OAAO;AACL,wBAAY,KAAK,CAAC;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAEA,YAAM,eAAe;AAAA,QACnB,GAAG;AAAA,QACH,UAAU;AAAA,MACZ;AASA,YAAM,WAAW,aAAa,SAAS,SAAS,IAC5C,aAAa,WACb;AACJ,YAAM,OAAO,IAAI,QAAQ,GAAG;AAC5B,YAAM,cAAc,SAAS,KAAK,KAAK,IAAI,MAAM,IAAI;AACrD,YAAM,YACJ,GAAG,aAAa,MAAM,GAAG,QAAQ,oBAAoB,WAAW;AAClE,YAAM,iBAAyC;AAAA,QAC7C,gBAAgB;AAAA,MAClB;AAGA,YAAM,aAAa,IAAI,QAAQ,eAAe;AAC9C,UAAI,OAAO,eAAe,UAAU;AAClC,uBAAe,eAAe,IAAI;AAAA,MACpC;AAEA,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,WAAW;AAAA,UACtC,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,MAAM,KAAK,UAAU,YAAY;AAAA,QACnC,CAAC;AAGD,YAAI,OAAO,WAAW,MAAM;AAE1B,cAAI,CAAC,SAAS,IAAI;AAChB,kBAAM,UAAU,MAAM,SAAS,YAAY;AAC3C,gBAAI,UAAU,SAAS,QAAQ;AAAA,cAC7B,gBAAgB,SAAS,QAAQ,IAAI,cAAc,KAAK;AAAA,YAC1D,CAAC;AACD,gBAAI,IAAI,OAAO,KAAK,OAAO,CAAC;AAC5B;AAAA,UACF;AAEA,cAAI,UAAU,SAAS,QAAQ;AAAA,YAC7B,gBAAgB;AAAA,YAChB,iBAAiB;AAAA,YACjB,cAAc;AAAA,UAChB,CAAC;AAED,gBAAM,SAAS,SAAS,MAAM,UAAU;AACxC,cAAI,CAAC,QAAQ;AACX,gBAAI,IAAI;AACR;AAAA,UACF;AAEA,gBAAM,SAAuB,CAAC;AAC9B,cAAI;AACF,mBAAO,MAAM;AACX,oBAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,kBAAI,KAAM;AACV,qBAAO,KAAK,KAAK;AACjB,kBAAI,MAAM,KAAK;AAAA,YACjB;AAAA,UACF,UAAE;AACA,gBAAI,IAAI;AAAA,UACV;AAGA,cAAI;AACF,kBAAM,WAAW,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AACvD,kBAAM,eAAyB,CAAC;AAChC,uBAAW,QAAQ,SAAS,MAAM,IAAI,GAAG;AACvC,kBAAI,CAAC,KAAK,WAAW,QAAQ,KAAK,SAAS,eAAgB;AAC3D,kBAAI;AACF,sBAAM,QAAQ,KAAK,MAAM,KAAK,MAAM,CAAC,CAAC;AAGtC,sBAAM,QAAQ,MAAM,UAAU,CAAC,GAAG,OAAO;AACzC,oBAAI,MAAO,cAAa,KAAK,KAAK;AAAA,cACpC,QAAQ;AAAA,cAER;AAAA,YACF;AACA,gBAAI,aAAa,SAAS,KAAK,MAAM,SAAS,GAAG;AAC/C;AAAA,gBACE;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,aAAa,KAAK,EAAE;AAAA,gBACpB,OAAO;AAAA,cACT;AAAA,YACF;AAAA,UACF,QAAQ;AAAA,UAER;AACA;AAAA,QACF;AAGA,cAAM,iBAAiB,MAAM,SAAS,YAAY;AAClD,cAAM,gBAAgB,OAAO,KAAK,cAAc;AAGhD,YAAI,iBAAiB;AACrB,YAAI;AACF,gBAAM,eAAe,KAAK;AAAA,YACxB,cAAc,SAAS,OAAO;AAAA,UAChC;AACA,2BAAiB,sBAAsB,YAAY;AAAA,QACrD,QAAQ;AAAA,QAER;AAGA,YAAI,MAAM,SAAS,KAAK,eAAe,SAAS,GAAG;AACjD,sBAAY,iBAAiB,YAAY,OAAO,gBAAgB,OAAO,eAAe;AAAA,QACxF;AAGA,cAAM,sBAA8C,CAAC;AACrD,mBAAW,CAAC,KAAK,KAAK,KAAK,SAAS,QAAQ,QAAQ,GAAG;AACrD,cAAI,CAAC,4BAA4B,IAAI,IAAI,YAAY,CAAC,GAAG;AACvD,gCAAoB,GAAG,IAAI;AAAA,UAC7B;AAAA,QACF;AACA,4BAAoB,gBAAgB,IAAI,OAAO,cAAc,MAAM;AACnE,YAAI,UAAU,SAAS,QAAQ,mBAAmB;AAClD,YAAI,IAAI,aAAa;AAAA,MACvB,SAAS,MAAM;AACb,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU;AAAA,UACrB,OAAO;AAAA,QACT,CAAC,CAAC;AAAA,MACJ;AACA;AAAA,IACF;AAIA,UAAM,OAAO,WAAW,SAAS,WAAW,SAAS,MAAM,YAAY,GAAG,IAAI;AAC9E,UAAM,OAAO,eAAe,IAAI,OAAO;AACvC,UAAM,iBAAiB,cAAc,QAAQ,KAAK,MAAM,MAAM,GAAG;AAAA,EACnE;AAEA,SAAO;AAAA,IACL,IAAI,OAAO;AACT,aAAO;AAAA,IACT;AAAA,IAEA,QAAuB;AACrB,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,iBAAc,kBAAa,CAAC,KAAK,QAAQ;AACvC,yBAAe,KAAK,GAAG,EAAE,MAAM,CAAC,SAAS;AACvC,gBAAI,CAAC,IAAI,aAAa;AACpB,kBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,kBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,uBAAuB,CAAC,CAAC;AAAA,YAC3D;AAAA,UACF,CAAC;AAAA,QACH,CAAC;AAED,eAAO,GAAG,SAAS,MAAM;AAEzB,eAAO,OAAO,OAAO,WAAW,MAAM;AACpC,gBAAM,OAAO,OAAQ,QAAQ;AAC7B,cAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,2BAAe,KAAK;AAAA,UACtB;AACA,kBAAQ;AAAA,QACV,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IAEA,OAAsB;AACpB,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAI,CAAC,QAAQ;AACX,kBAAQ;AACR;AAAA,QACF;AACA,eAAO,MAAM,CAAC,QAAQ;AACpB,mBAAS;AACT,cAAI,IAAK,QAAO,GAAG;AAAA,cACd,SAAQ;AAAA,QACf,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;AC5qBO,IAAM,iBAAyC;AAAA,EACpD,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,IACf,WAAW;AAAA,IACX,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AACF;AAEA,IAAM,2BAA2B,CAAC,aAAa,QAAQ;AACvD,IAAM,kBAAkB,CAAC,iBAAiB,gBAAgB;AAQnD,SAAS,YAAY,KAAsC;AAChE,MAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAEA,QAAM,MAAM;AAGZ,MAAI,OAAO,IAAI,kBAAkB,YAAY,IAAI,cAAc,WAAW,GAAG;AAC3E,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MACE,OAAO,IAAI,cAAc,YACzB,CAAC,OAAO,UAAU,IAAI,SAAS,KAC/B,IAAI,aAAa,KACjB,IAAI,YAAY,OAChB;AACA,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,IAAI,oBAAoB,YAAY,IAAI,gBAAgB,WAAW,GAAG;AAC/E,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI,IAAI,oBAAoB,QAAW;AACrC,QAAI,OAAO,IAAI,oBAAoB,YAAY,IAAI,gBAAgB,WAAW,GAAG;AAC/E,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,sBAAkB,IAAI;AAAA,EACxB;AAEA,QAAM,mBACJ,IAAI,qBAAqB,SACrB,OAAO,IAAI,gBAAgB,IAC3B,eAAe;AAErB,MAAI,kBAAkB,eAAe;AACrC,MAAI,IAAI,oBAAoB,QAAW;AACrC,QAAI,CAAC,yBAAyB,SAAS,IAAI,eAA0D,GAAG;AACtG,YAAM,IAAI;AAAA,QACR,4CAA4C,yBAAyB,KAAK,IAAI,CAAC,UACrE,KAAK,UAAU,IAAI,eAAe,CAAC;AAAA,MAC/C;AAAA,IACF;AACA,sBAAkB,IAAI;AAAA,EACxB;AAGA,MAAI,kBAAkB,EAAE,GAAG,eAAe,gBAAgB;AAC1D,MAAI,IAAI,oBAAoB,QAAW;AACrC,QAAI,OAAO,IAAI,oBAAoB,YAAY,IAAI,oBAAoB,MAAM;AAC3E,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AACA,UAAM,KAAK,IAAI;AAEf,QAAI,GAAG,cAAc,QAAW;AAC9B,UAAI,OAAO,GAAG,cAAc,YAAY,CAAC,OAAO,UAAU,GAAG,SAAS,KAAK,GAAG,aAAa,GAAG;AAC5F,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,sBAAgB,YAAY,GAAG;AAAA,IACjC;AAEA,QAAI,GAAG,aAAa,QAAW;AAC7B,UAAI,CAAC,gBAAgB,SAAS,GAAG,QAA0C,GAAG;AAC5E,cAAM,IAAI;AAAA,UACR,qDACK,gBAAgB,KAAK,IAAI,CAAC,UAAU,KAAK,UAAU,GAAG,QAAQ,CAAC;AAAA,QACtE;AAAA,MACF;AACA,sBAAgB,WAAW,GAAG;AAAA,IAChC;AAEA,QAAI,GAAG,aAAa,QAAW;AAC7B,UAAI,OAAO,GAAG,aAAa,YAAY,GAAG,SAAS,WAAW,GAAG;AAC/D,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,sBAAgB,WAAW,GAAG;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,eAAe,IAAI;AAAA,IACnB;AAAA,IACA,WAAW,IAAI;AAAA,IACf,iBAAiB,IAAI;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|