@cognisos/liminal 2.3.0 → 2.4.1
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/dist/bin.js +2751 -533
- package/dist/bin.js.map +1 -1
- package/package.json +4 -2
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/rsc/pipeline.ts
|
|
13
|
+
var pipeline_exports = {};
|
|
14
|
+
__export(pipeline_exports, {
|
|
15
|
+
RSCPipelineWrapper: () => RSCPipelineWrapper
|
|
16
|
+
});
|
|
17
|
+
import {
|
|
18
|
+
CompressionPipeline,
|
|
19
|
+
RSCTransport,
|
|
20
|
+
RSCEventEmitter,
|
|
21
|
+
Session,
|
|
22
|
+
CircuitBreaker
|
|
23
|
+
} from "@cognisos/rsc-sdk";
|
|
24
|
+
var RSCPipelineWrapper;
|
|
25
|
+
var init_pipeline = __esm({
|
|
26
|
+
"src/rsc/pipeline.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
RSCPipelineWrapper = class {
|
|
29
|
+
pipeline;
|
|
30
|
+
session;
|
|
31
|
+
events;
|
|
32
|
+
transport;
|
|
33
|
+
circuitBreaker;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.circuitBreaker = new CircuitBreaker(5, 5 * 60 * 1e3);
|
|
36
|
+
this.transport = new RSCTransport({
|
|
37
|
+
baseUrl: config.rscBaseUrl,
|
|
38
|
+
apiKey: config.rscApiKey,
|
|
39
|
+
timeout: 3e4,
|
|
40
|
+
maxRetries: 3,
|
|
41
|
+
circuitBreaker: this.circuitBreaker
|
|
42
|
+
});
|
|
43
|
+
this.events = new RSCEventEmitter();
|
|
44
|
+
this.session = new Session(config.sessionId);
|
|
45
|
+
this.pipeline = new CompressionPipeline(
|
|
46
|
+
this.transport,
|
|
47
|
+
{
|
|
48
|
+
threshold: config.compressionThreshold,
|
|
49
|
+
learnFromResponses: config.learnFromResponses,
|
|
50
|
+
latencyBudgetMs: config.latencyBudgetMs,
|
|
51
|
+
sessionId: this.session.sessionId
|
|
52
|
+
},
|
|
53
|
+
this.events
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
async healthCheck() {
|
|
57
|
+
try {
|
|
58
|
+
await this.transport.get("/health");
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
getSessionSummary() {
|
|
65
|
+
return this.session.getSummary();
|
|
66
|
+
}
|
|
67
|
+
getCircuitState() {
|
|
68
|
+
return this.circuitBreaker.getState();
|
|
69
|
+
}
|
|
70
|
+
isCircuitOpen() {
|
|
71
|
+
return this.circuitBreaker.getState() === "open";
|
|
72
|
+
}
|
|
73
|
+
resetCircuitBreaker() {
|
|
74
|
+
this.circuitBreaker.reset();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
2
79
|
|
|
3
80
|
// src/version.ts
|
|
4
|
-
var VERSION = true ? "2.
|
|
81
|
+
var VERSION = true ? "2.4.1" : "0.2.1";
|
|
5
82
|
var BANNER_LINES = [
|
|
6
83
|
" ___ ___ _____ ______ ___ ________ ________ ___",
|
|
7
84
|
"|\\ \\ |\\ \\|\\ _ \\ _ \\|\\ \\|\\ ___ \\|\\ __ \\|\\ \\",
|
|
@@ -21,15 +98,8 @@ function printBanner() {
|
|
|
21
98
|
console.log();
|
|
22
99
|
}
|
|
23
100
|
|
|
24
|
-
// src/commands/init.ts
|
|
25
|
-
import { createInterface as createInterface2 } from "readline/promises";
|
|
26
|
-
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
27
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
28
|
-
import { join as join2 } from "path";
|
|
29
|
-
import { homedir as homedir2 } from "os";
|
|
30
|
-
|
|
31
101
|
// src/config/loader.ts
|
|
32
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
102
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
33
103
|
import { dirname } from "path";
|
|
34
104
|
|
|
35
105
|
// src/config/paths.ts
|
|
@@ -43,16 +113,26 @@ var LOG_FILE = join(LOG_DIR, "liminal.log");
|
|
|
43
113
|
|
|
44
114
|
// src/config/schema.ts
|
|
45
115
|
var DEFAULTS = {
|
|
46
|
-
apiBaseUrl: "https://
|
|
116
|
+
apiBaseUrl: "https://api.cognisos.ai",
|
|
47
117
|
upstreamBaseUrl: "https://api.openai.com",
|
|
48
118
|
anthropicUpstreamUrl: "https://api.anthropic.com",
|
|
49
119
|
port: 3141,
|
|
50
120
|
compressionThreshold: 100,
|
|
51
|
-
|
|
121
|
+
aggregateThreshold: 500,
|
|
122
|
+
hotFraction: 0.3,
|
|
123
|
+
coldFraction: 0.3,
|
|
124
|
+
compressRoles: ["user", "assistant"],
|
|
125
|
+
compressToolResults: true,
|
|
52
126
|
learnFromResponses: true,
|
|
53
127
|
latencyBudgetMs: 5e3,
|
|
54
128
|
enabled: true,
|
|
55
|
-
tools: []
|
|
129
|
+
tools: [],
|
|
130
|
+
concurrencyLimit: 6,
|
|
131
|
+
concurrencyTimeoutMs: 15e3,
|
|
132
|
+
maxSessions: 10,
|
|
133
|
+
sessionTtlMs: 18e5,
|
|
134
|
+
latencyWarningMs: 4e3,
|
|
135
|
+
latencyCriticalMs: 8e3
|
|
56
136
|
};
|
|
57
137
|
var CONFIGURABLE_KEYS = /* @__PURE__ */ new Set([
|
|
58
138
|
"apiBaseUrl",
|
|
@@ -60,11 +140,21 @@ var CONFIGURABLE_KEYS = /* @__PURE__ */ new Set([
|
|
|
60
140
|
"anthropicUpstreamUrl",
|
|
61
141
|
"port",
|
|
62
142
|
"compressionThreshold",
|
|
143
|
+
"aggregateThreshold",
|
|
144
|
+
"hotFraction",
|
|
145
|
+
"coldFraction",
|
|
63
146
|
"compressRoles",
|
|
147
|
+
"compressToolResults",
|
|
64
148
|
"learnFromResponses",
|
|
65
149
|
"latencyBudgetMs",
|
|
66
150
|
"enabled",
|
|
67
|
-
"tools"
|
|
151
|
+
"tools",
|
|
152
|
+
"concurrencyLimit",
|
|
153
|
+
"concurrencyTimeoutMs",
|
|
154
|
+
"maxSessions",
|
|
155
|
+
"sessionTtlMs",
|
|
156
|
+
"latencyWarningMs",
|
|
157
|
+
"latencyCriticalMs"
|
|
68
158
|
]);
|
|
69
159
|
|
|
70
160
|
// src/config/loader.ts
|
|
@@ -84,11 +174,21 @@ function loadConfig() {
|
|
|
84
174
|
anthropicUpstreamUrl: DEFAULTS.anthropicUpstreamUrl,
|
|
85
175
|
port: DEFAULTS.port,
|
|
86
176
|
compressionThreshold: DEFAULTS.compressionThreshold,
|
|
177
|
+
aggregateThreshold: DEFAULTS.aggregateThreshold,
|
|
178
|
+
hotFraction: DEFAULTS.hotFraction,
|
|
179
|
+
coldFraction: DEFAULTS.coldFraction,
|
|
87
180
|
compressRoles: DEFAULTS.compressRoles,
|
|
181
|
+
compressToolResults: DEFAULTS.compressToolResults,
|
|
88
182
|
learnFromResponses: DEFAULTS.learnFromResponses,
|
|
89
183
|
latencyBudgetMs: DEFAULTS.latencyBudgetMs,
|
|
90
184
|
enabled: DEFAULTS.enabled,
|
|
91
185
|
tools: DEFAULTS.tools,
|
|
186
|
+
concurrencyLimit: DEFAULTS.concurrencyLimit,
|
|
187
|
+
concurrencyTimeoutMs: DEFAULTS.concurrencyTimeoutMs,
|
|
188
|
+
maxSessions: DEFAULTS.maxSessions,
|
|
189
|
+
sessionTtlMs: DEFAULTS.sessionTtlMs,
|
|
190
|
+
latencyWarningMs: DEFAULTS.latencyWarningMs,
|
|
191
|
+
latencyCriticalMs: DEFAULTS.latencyCriticalMs,
|
|
92
192
|
...fileConfig
|
|
93
193
|
};
|
|
94
194
|
if (process.env.LIMINAL_API_KEY) merged.apiKey = process.env.LIMINAL_API_KEY;
|
|
@@ -111,11 +211,15 @@ function saveConfig(config) {
|
|
|
111
211
|
}
|
|
112
212
|
}
|
|
113
213
|
const merged = { ...existing, ...config };
|
|
114
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
214
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
215
|
+
try {
|
|
216
|
+
chmodSync(CONFIG_FILE, 384);
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
115
219
|
}
|
|
116
220
|
function ensureDirectories() {
|
|
117
|
-
if (!existsSync(LIMINAL_DIR)) mkdirSync(LIMINAL_DIR, { recursive: true });
|
|
118
|
-
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
|
221
|
+
if (!existsSync(LIMINAL_DIR)) mkdirSync(LIMINAL_DIR, { recursive: true, mode: 448 });
|
|
222
|
+
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true, mode: 448 });
|
|
119
223
|
const configDir = dirname(CONFIG_FILE);
|
|
120
224
|
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
|
|
121
225
|
}
|
|
@@ -132,6 +236,99 @@ function maskApiKey(key) {
|
|
|
132
236
|
return key.slice(0, 8) + "..." + key.slice(-4);
|
|
133
237
|
}
|
|
134
238
|
|
|
239
|
+
// src/config/shell.ts
|
|
240
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync } from "fs";
|
|
241
|
+
import { join as join2 } from "path";
|
|
242
|
+
import { homedir as homedir2 } from "os";
|
|
243
|
+
var LIMINAL_BLOCK_HEADER = "# Liminal \u2014 route AI tools through compression proxy";
|
|
244
|
+
function detectShellProfile() {
|
|
245
|
+
const shell = process.env.SHELL || "";
|
|
246
|
+
const home = homedir2();
|
|
247
|
+
if (shell.endsWith("/zsh")) {
|
|
248
|
+
return { name: "~/.zshrc", path: join2(home, ".zshrc") };
|
|
249
|
+
}
|
|
250
|
+
if (shell.endsWith("/bash")) {
|
|
251
|
+
const bashProfile = join2(home, ".bash_profile");
|
|
252
|
+
if (existsSync2(bashProfile)) {
|
|
253
|
+
return { name: "~/.bash_profile", path: bashProfile };
|
|
254
|
+
}
|
|
255
|
+
return { name: "~/.bashrc", path: join2(home, ".bashrc") };
|
|
256
|
+
}
|
|
257
|
+
const candidates = [
|
|
258
|
+
{ name: "~/.zshrc", path: join2(home, ".zshrc") },
|
|
259
|
+
{ name: "~/.bashrc", path: join2(home, ".bashrc") },
|
|
260
|
+
{ name: "~/.profile", path: join2(home, ".profile") }
|
|
261
|
+
];
|
|
262
|
+
for (const c of candidates) {
|
|
263
|
+
if (existsSync2(c.path)) return c;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function lineExistsInFile(filePath, line) {
|
|
268
|
+
if (!existsSync2(filePath)) return false;
|
|
269
|
+
try {
|
|
270
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
271
|
+
return content.includes(line);
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function appendToShellProfile(profile, lines) {
|
|
277
|
+
const newLines = lines.filter((line) => !lineExistsInFile(profile.path, line));
|
|
278
|
+
if (newLines.length === 0) return [];
|
|
279
|
+
const block = [
|
|
280
|
+
"",
|
|
281
|
+
LIMINAL_BLOCK_HEADER,
|
|
282
|
+
...newLines
|
|
283
|
+
].join("\n") + "\n";
|
|
284
|
+
appendFileSync(profile.path, block, "utf-8");
|
|
285
|
+
return newLines;
|
|
286
|
+
}
|
|
287
|
+
function removeLiminalFromShellProfile(profile) {
|
|
288
|
+
if (!existsSync2(profile.path)) return [];
|
|
289
|
+
let content;
|
|
290
|
+
try {
|
|
291
|
+
content = readFileSync2(profile.path, "utf-8");
|
|
292
|
+
} catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
const lines = content.split("\n");
|
|
296
|
+
const removed = [];
|
|
297
|
+
const kept = [];
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
if (line.trim() === LIMINAL_BLOCK_HEADER) {
|
|
300
|
+
removed.push(line);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (isLiminalExportLine(line)) {
|
|
304
|
+
removed.push(line);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
kept.push(line);
|
|
308
|
+
}
|
|
309
|
+
if (removed.length > 0) {
|
|
310
|
+
const cleaned = kept.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
311
|
+
writeFileSync2(profile.path, cleaned, "utf-8");
|
|
312
|
+
}
|
|
313
|
+
return removed;
|
|
314
|
+
}
|
|
315
|
+
function isLiminalExportLine(line) {
|
|
316
|
+
const trimmed = line.trim();
|
|
317
|
+
if (!trimmed.startsWith("export ")) return false;
|
|
318
|
+
if (trimmed.includes("ANTHROPIC_BASE_URL=http://127.0.0.1:")) return true;
|
|
319
|
+
if (trimmed.includes("OPENAI_BASE_URL=http://127.0.0.1:")) return true;
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function findLiminalExportsInProfile(profile) {
|
|
323
|
+
if (!existsSync2(profile.path)) return [];
|
|
324
|
+
try {
|
|
325
|
+
const content = readFileSync2(profile.path, "utf-8");
|
|
326
|
+
return content.split("\n").filter(isLiminalExportLine);
|
|
327
|
+
} catch {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
135
332
|
// src/ui/prompts.ts
|
|
136
333
|
var ANSI = {
|
|
137
334
|
HIDE_CURSOR: "\x1B[?25l",
|
|
@@ -190,23 +387,23 @@ function renderMultiSelect(options, cursorIndex, selected, message) {
|
|
|
190
387
|
lines.push(` ${pointer} ${box} ${label}${desc}`);
|
|
191
388
|
}
|
|
192
389
|
lines.push("");
|
|
193
|
-
lines.push(` ${ANSI.DIM}Space
|
|
390
|
+
lines.push(` ${ANSI.DIM}\u2191/\u2193 Navigate ${ANSI.RESET}${ANSI.CYAN}Space${ANSI.RESET}${ANSI.DIM} Select ${ANSI.RESET}${ANSI.CYAN}Enter${ANSI.RESET}${ANSI.DIM} Confirm${ANSI.RESET}`);
|
|
194
391
|
lines.push("");
|
|
195
392
|
return { text: lines.join("\n"), lineCount: lines.length };
|
|
196
393
|
}
|
|
197
394
|
function withRawMode(streams, handler) {
|
|
198
|
-
const { stdin:
|
|
395
|
+
const { stdin: stdin2, stdout: stdout2 } = streams;
|
|
199
396
|
return new Promise((resolve, reject) => {
|
|
200
397
|
let cleaned = false;
|
|
201
398
|
function cleanup() {
|
|
202
399
|
if (cleaned) return;
|
|
203
400
|
cleaned = true;
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
if ("pause" in
|
|
207
|
-
|
|
401
|
+
stdin2.removeListener("data", onData);
|
|
402
|
+
if (stdin2.setRawMode) stdin2.setRawMode(false);
|
|
403
|
+
if ("pause" in stdin2 && typeof stdin2.pause === "function") {
|
|
404
|
+
stdin2.pause();
|
|
208
405
|
}
|
|
209
|
-
|
|
406
|
+
stdout2.write(ANSI.SHOW_CURSOR);
|
|
210
407
|
process.removeListener("exit", cleanup);
|
|
211
408
|
}
|
|
212
409
|
function onData(data) {
|
|
@@ -223,11 +420,11 @@ function withRawMode(streams, handler) {
|
|
|
223
420
|
});
|
|
224
421
|
process.on("exit", cleanup);
|
|
225
422
|
try {
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if ("resume" in
|
|
230
|
-
|
|
423
|
+
if (stdin2.setRawMode) stdin2.setRawMode(true);
|
|
424
|
+
stdout2.write(ANSI.HIDE_CURSOR);
|
|
425
|
+
stdin2.on("data", onData);
|
|
426
|
+
if ("resume" in stdin2 && typeof stdin2.resume === "function") {
|
|
427
|
+
stdin2.resume();
|
|
231
428
|
}
|
|
232
429
|
} catch (err) {
|
|
233
430
|
cleanup();
|
|
@@ -395,7 +592,7 @@ import { createInterface } from "readline/promises";
|
|
|
395
592
|
import { stdin, stdout } from "process";
|
|
396
593
|
|
|
397
594
|
// src/auth/supabase.ts
|
|
398
|
-
import { randomBytes } from "crypto";
|
|
595
|
+
import { randomBytes, createHash } from "crypto";
|
|
399
596
|
var SUPABASE_URL = "https://nzcneiyymvgxvttbenhp.supabase.co";
|
|
400
597
|
var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im56Y25laXl5bXZneHZ0dGJlbmhwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQwNjQ0MjcsImV4cCI6MjA2OTY0MDQyN30.x3E-zGRadbPMmxRqT_PB_KOi00htKpgeb8GiQa4g2z0";
|
|
401
598
|
function supabaseHeaders(accessToken) {
|
|
@@ -452,25 +649,13 @@ async function signUp(email, password, name) {
|
|
|
452
649
|
email: body.user?.email ?? email
|
|
453
650
|
};
|
|
454
651
|
}
|
|
455
|
-
async function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
is_active: "eq.true",
|
|
459
|
-
select: "api_key",
|
|
460
|
-
limit: "1"
|
|
461
|
-
});
|
|
462
|
-
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_api_keys?${params}`, {
|
|
652
|
+
async function createApiKey(accessToken, userId) {
|
|
653
|
+
await fetch(`${SUPABASE_URL}/rest/v1/user_api_keys?user_id=eq.${userId}`, {
|
|
654
|
+
method: "DELETE",
|
|
463
655
|
headers: supabaseHeaders(accessToken)
|
|
464
656
|
});
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
if (Array.isArray(rows) && rows.length > 0 && rows[0].api_key) {
|
|
468
|
-
return rows[0].api_key;
|
|
469
|
-
}
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
async function createApiKey(accessToken, userId) {
|
|
473
|
-
const apiKey = `fmcp_${randomBytes(32).toString("hex")}`;
|
|
657
|
+
const apiKey = `lim_${randomBytes(32).toString("hex")}`;
|
|
658
|
+
const keyHash = createHash("sha256").update(apiKey).digest("hex");
|
|
474
659
|
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_api_keys`, {
|
|
475
660
|
method: "POST",
|
|
476
661
|
headers: {
|
|
@@ -480,7 +665,7 @@ async function createApiKey(accessToken, userId) {
|
|
|
480
665
|
body: JSON.stringify({
|
|
481
666
|
user_id: userId,
|
|
482
667
|
key_name: "Liminal CLI",
|
|
483
|
-
|
|
668
|
+
key_hash: keyHash,
|
|
484
669
|
is_active: true
|
|
485
670
|
})
|
|
486
671
|
});
|
|
@@ -492,8 +677,6 @@ async function createApiKey(accessToken, userId) {
|
|
|
492
677
|
return apiKey;
|
|
493
678
|
}
|
|
494
679
|
async function authenticateAndGetKey(auth) {
|
|
495
|
-
const existingKey = await fetchApiKey(auth.accessToken, auth.userId);
|
|
496
|
-
if (existingKey) return existingKey;
|
|
497
680
|
return createApiKey(auth.accessToken, auth.userId);
|
|
498
681
|
}
|
|
499
682
|
|
|
@@ -502,7 +685,7 @@ async function loginCommand() {
|
|
|
502
685
|
printBanner();
|
|
503
686
|
try {
|
|
504
687
|
const config = loadConfig();
|
|
505
|
-
if (config.apiKey && config.apiKey.startsWith("fmcp_")) {
|
|
688
|
+
if (config.apiKey && (config.apiKey.startsWith("lim_") || config.apiKey.startsWith("fmcp_"))) {
|
|
506
689
|
console.log(" Already logged in.");
|
|
507
690
|
console.log(" Run \x1B[1mliminal logout\x1B[0m first to switch accounts.");
|
|
508
691
|
return;
|
|
@@ -575,59 +758,327 @@ async function runAuthFlow() {
|
|
|
575
758
|
}
|
|
576
759
|
}
|
|
577
760
|
|
|
578
|
-
// src/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
761
|
+
// src/connectors/claude-code.ts
|
|
762
|
+
import { execSync } from "child_process";
|
|
763
|
+
var ENV_VAR = "ANTHROPIC_BASE_URL";
|
|
764
|
+
var INFO = {
|
|
765
|
+
id: "claude-code",
|
|
766
|
+
label: "Claude Code",
|
|
767
|
+
description: "Anthropic CLI for coding with Claude",
|
|
768
|
+
protocol: "anthropic-messages",
|
|
769
|
+
automatable: true
|
|
770
|
+
};
|
|
771
|
+
function isClaudeInstalled() {
|
|
772
|
+
try {
|
|
773
|
+
execSync("which claude", { stdio: "ignore" });
|
|
774
|
+
return true;
|
|
775
|
+
} catch {
|
|
776
|
+
return false;
|
|
585
777
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
778
|
+
}
|
|
779
|
+
function getCurrentBaseUrl() {
|
|
780
|
+
return process.env[ENV_VAR] || void 0;
|
|
781
|
+
}
|
|
782
|
+
var claudeCodeConnector = {
|
|
783
|
+
info: INFO,
|
|
784
|
+
async detect() {
|
|
785
|
+
const installed = isClaudeInstalled();
|
|
786
|
+
const currentUrl = getCurrentBaseUrl();
|
|
787
|
+
const configured = currentUrl?.includes("127.0.0.1") ?? false;
|
|
788
|
+
if (!installed) {
|
|
789
|
+
return { installed, configured: false, detail: "Claude Code not found in PATH" };
|
|
590
790
|
}
|
|
591
|
-
|
|
791
|
+
if (configured) {
|
|
792
|
+
return { installed, configured, detail: `Routing through ${currentUrl}` };
|
|
793
|
+
}
|
|
794
|
+
return { installed, configured, detail: "Installed but not routing through Liminal" };
|
|
795
|
+
},
|
|
796
|
+
getShellExports(port) {
|
|
797
|
+
return [`export ${ENV_VAR}=http://127.0.0.1:${port}`];
|
|
798
|
+
},
|
|
799
|
+
async setup(port) {
|
|
800
|
+
const exports = this.getShellExports(port);
|
|
801
|
+
return {
|
|
802
|
+
success: true,
|
|
803
|
+
shellExports: exports,
|
|
804
|
+
postSetupInstructions: [
|
|
805
|
+
"Claude Code will automatically route through Liminal.",
|
|
806
|
+
"Make sure to source your shell profile or restart your terminal."
|
|
807
|
+
]
|
|
808
|
+
};
|
|
809
|
+
},
|
|
810
|
+
async teardown() {
|
|
811
|
+
return {
|
|
812
|
+
success: true,
|
|
813
|
+
manualSteps: [
|
|
814
|
+
`Remove the line \`export ${ENV_VAR}=...\` from your shell profile (~/.zshrc or ~/.bashrc).`,
|
|
815
|
+
"Restart your terminal or run: unset ANTHROPIC_BASE_URL"
|
|
816
|
+
]
|
|
817
|
+
};
|
|
592
818
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/connectors/codex.ts
|
|
822
|
+
import { execSync as execSync2 } from "child_process";
|
|
823
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
824
|
+
import { join as join3 } from "path";
|
|
825
|
+
import { homedir as homedir3 } from "os";
|
|
826
|
+
var ENV_VAR2 = "OPENAI_BASE_URL";
|
|
827
|
+
var CODEX_CONFIG_DIR = join3(homedir3(), ".codex");
|
|
828
|
+
var CODEX_CONFIG_FILE = join3(CODEX_CONFIG_DIR, "config.toml");
|
|
829
|
+
var INFO2 = {
|
|
830
|
+
id: "codex",
|
|
831
|
+
label: "Codex CLI",
|
|
832
|
+
description: "OpenAI CLI agent for coding (Responses API)",
|
|
833
|
+
protocol: "openai-responses",
|
|
834
|
+
automatable: true
|
|
835
|
+
};
|
|
836
|
+
function isCodexInstalled() {
|
|
837
|
+
try {
|
|
838
|
+
execSync2("which codex", { stdio: "ignore" });
|
|
839
|
+
return true;
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
600
842
|
}
|
|
601
|
-
return null;
|
|
602
843
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
const lines = [];
|
|
606
|
-
if (tools.includes("claude-code")) {
|
|
607
|
-
lines.push(`export ANTHROPIC_BASE_URL=${base}`);
|
|
608
|
-
}
|
|
609
|
-
if (tools.includes("codex") || tools.includes("openai-compatible")) {
|
|
610
|
-
lines.push(`export OPENAI_BASE_URL=${base}/v1`);
|
|
611
|
-
}
|
|
612
|
-
return lines;
|
|
844
|
+
function getCurrentBaseUrl2() {
|
|
845
|
+
return process.env[ENV_VAR2] || void 0;
|
|
613
846
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
847
|
+
function hasCodexConfig() {
|
|
848
|
+
return existsSync3(CODEX_CONFIG_FILE);
|
|
849
|
+
}
|
|
850
|
+
function codexConfigMentionsLiminal() {
|
|
851
|
+
if (!hasCodexConfig()) return false;
|
|
616
852
|
try {
|
|
617
|
-
const content =
|
|
618
|
-
return content.includes(
|
|
853
|
+
const content = readFileSync3(CODEX_CONFIG_FILE, "utf-8");
|
|
854
|
+
return content.includes("127.0.0.1") || content.includes("liminal");
|
|
619
855
|
} catch {
|
|
620
856
|
return false;
|
|
621
857
|
}
|
|
622
858
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
859
|
+
var codexConnector = {
|
|
860
|
+
info: INFO2,
|
|
861
|
+
async detect() {
|
|
862
|
+
const installed = isCodexInstalled();
|
|
863
|
+
const currentUrl = getCurrentBaseUrl2();
|
|
864
|
+
const envConfigured = currentUrl?.includes("127.0.0.1") ?? false;
|
|
865
|
+
const tomlConfigured = codexConfigMentionsLiminal();
|
|
866
|
+
const configured = envConfigured || tomlConfigured;
|
|
867
|
+
if (!installed) {
|
|
868
|
+
return { installed, configured: false, detail: "Codex CLI not found in PATH" };
|
|
869
|
+
}
|
|
870
|
+
if (configured) {
|
|
871
|
+
const via = envConfigured ? ENV_VAR2 : "config.toml";
|
|
872
|
+
return { installed, configured, detail: `Routing through Liminal (via ${via})` };
|
|
873
|
+
}
|
|
874
|
+
return { installed, configured, detail: "Installed but not routing through Liminal" };
|
|
875
|
+
},
|
|
876
|
+
getShellExports(port) {
|
|
877
|
+
return [`export ${ENV_VAR2}=http://127.0.0.1:${port}/v1`];
|
|
878
|
+
},
|
|
879
|
+
async setup(port) {
|
|
880
|
+
const exports = this.getShellExports(port);
|
|
881
|
+
const instructions = [
|
|
882
|
+
"Codex CLI will automatically route through Liminal.",
|
|
883
|
+
"Make sure to source your shell profile or restart your terminal."
|
|
884
|
+
];
|
|
885
|
+
instructions.push(
|
|
886
|
+
"Codex uses the OpenAI Responses API (/v1/responses) by default."
|
|
887
|
+
);
|
|
888
|
+
return {
|
|
889
|
+
success: true,
|
|
890
|
+
shellExports: exports,
|
|
891
|
+
postSetupInstructions: instructions
|
|
892
|
+
};
|
|
893
|
+
},
|
|
894
|
+
async teardown() {
|
|
895
|
+
const steps = [
|
|
896
|
+
`Remove the line \`export ${ENV_VAR2}=...\` from your shell profile (~/.zshrc or ~/.bashrc).`,
|
|
897
|
+
"Restart your terminal or run: unset OPENAI_BASE_URL"
|
|
898
|
+
];
|
|
899
|
+
if (codexConfigMentionsLiminal()) {
|
|
900
|
+
steps.push(
|
|
901
|
+
`Remove the Liminal provider block from ${CODEX_CONFIG_FILE}.`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return { success: true, manualSteps: steps };
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/connectors/cursor.ts
|
|
909
|
+
import { existsSync as existsSync4 } from "fs";
|
|
910
|
+
import { join as join4 } from "path";
|
|
911
|
+
import { homedir as homedir4 } from "os";
|
|
912
|
+
var INFO3 = {
|
|
913
|
+
id: "cursor",
|
|
914
|
+
label: "Cursor",
|
|
915
|
+
description: "AI-first code editor (GUI config required)",
|
|
916
|
+
protocol: "openai-chat",
|
|
917
|
+
automatable: false
|
|
918
|
+
};
|
|
919
|
+
function getCursorPaths() {
|
|
920
|
+
const platform = process.platform;
|
|
921
|
+
const home = homedir4();
|
|
922
|
+
if (platform === "darwin") {
|
|
923
|
+
return {
|
|
924
|
+
app: "/Applications/Cursor.app",
|
|
925
|
+
data: join4(home, "Library", "Application Support", "Cursor")
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (platform === "win32") {
|
|
929
|
+
const appData = process.env.APPDATA || join4(home, "AppData", "Roaming");
|
|
930
|
+
const localAppData = process.env.LOCALAPPDATA || join4(home, "AppData", "Local");
|
|
931
|
+
return {
|
|
932
|
+
app: join4(localAppData, "Programs", "Cursor", "Cursor.exe"),
|
|
933
|
+
data: join4(appData, "Cursor")
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
app: "/usr/bin/cursor",
|
|
938
|
+
data: join4(home, ".config", "Cursor")
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function isCursorInstalled() {
|
|
942
|
+
const { app, data } = getCursorPaths();
|
|
943
|
+
return existsSync4(app) || existsSync4(data);
|
|
944
|
+
}
|
|
945
|
+
function getSettingsDbPath() {
|
|
946
|
+
const { data } = getCursorPaths();
|
|
947
|
+
return join4(data, "User", "globalStorage", "state.vscdb");
|
|
948
|
+
}
|
|
949
|
+
var cursorConnector = {
|
|
950
|
+
info: INFO3,
|
|
951
|
+
async detect() {
|
|
952
|
+
const installed = isCursorInstalled();
|
|
953
|
+
const dbExists = existsSync4(getSettingsDbPath());
|
|
954
|
+
if (!installed) {
|
|
955
|
+
return { installed, configured: false, detail: "Cursor not found on this system" };
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
installed,
|
|
959
|
+
configured: false,
|
|
960
|
+
detail: dbExists ? "Installed \u2014 configuration requires Cursor Settings GUI" : "Installed but settings database not found"
|
|
961
|
+
};
|
|
962
|
+
},
|
|
963
|
+
getShellExports(_port) {
|
|
964
|
+
return [];
|
|
965
|
+
},
|
|
966
|
+
async setup(port) {
|
|
967
|
+
return {
|
|
968
|
+
success: true,
|
|
969
|
+
shellExports: [],
|
|
970
|
+
// No env vars — GUI only
|
|
971
|
+
postSetupInstructions: [
|
|
972
|
+
"Cursor routes API calls through its cloud servers, so localhost",
|
|
973
|
+
"URLs are blocked. You need a tunnel to expose the proxy:",
|
|
974
|
+
"",
|
|
975
|
+
` npx cloudflared tunnel --url http://localhost:${port}`,
|
|
976
|
+
"",
|
|
977
|
+
"Then configure Cursor with the tunnel URL:",
|
|
978
|
+
"",
|
|
979
|
+
" 1. Open Cursor Settings (not VS Code settings)",
|
|
980
|
+
" 2. Go to Models",
|
|
981
|
+
' 3. Enable "Override OpenAI Base URL (when using key)"',
|
|
982
|
+
" 4. Set the base URL to your tunnel URL + /v1",
|
|
983
|
+
" (e.g., https://abc123.trycloudflare.com/v1)",
|
|
984
|
+
" 5. Enter your real OpenAI/Anthropic API key",
|
|
985
|
+
" 6. Restart Cursor",
|
|
986
|
+
"",
|
|
987
|
+
"Cursor uses OpenAI format for all models, including Claude.",
|
|
988
|
+
"Both Chat Completions and Agent mode (Responses API) are supported."
|
|
989
|
+
]
|
|
990
|
+
};
|
|
991
|
+
},
|
|
992
|
+
async teardown() {
|
|
993
|
+
return {
|
|
994
|
+
success: true,
|
|
995
|
+
manualSteps: [
|
|
996
|
+
"In Cursor Settings > Models:",
|
|
997
|
+
' 1. Disable "Override OpenAI Base URL (when using key)"',
|
|
998
|
+
" 2. Clear the base URL field",
|
|
999
|
+
" 3. Restart Cursor"
|
|
1000
|
+
]
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// src/connectors/openai-compatible.ts
|
|
1006
|
+
var ENV_VAR3 = "OPENAI_BASE_URL";
|
|
1007
|
+
var INFO4 = {
|
|
1008
|
+
id: "openai-compatible",
|
|
1009
|
+
label: "Other / OpenAI-compatible",
|
|
1010
|
+
description: "Any tool that reads OPENAI_BASE_URL",
|
|
1011
|
+
protocol: "openai-chat",
|
|
1012
|
+
automatable: true
|
|
1013
|
+
};
|
|
1014
|
+
function getCurrentBaseUrl3() {
|
|
1015
|
+
return process.env[ENV_VAR3] || void 0;
|
|
1016
|
+
}
|
|
1017
|
+
var openaiCompatibleConnector = {
|
|
1018
|
+
info: INFO4,
|
|
1019
|
+
async detect() {
|
|
1020
|
+
const currentUrl = getCurrentBaseUrl3();
|
|
1021
|
+
const configured = currentUrl?.includes("127.0.0.1") ?? false;
|
|
1022
|
+
return {
|
|
1023
|
+
installed: true,
|
|
1024
|
+
// Generic — always "available"
|
|
1025
|
+
configured,
|
|
1026
|
+
detail: configured ? `OPENAI_BASE_URL \u2192 ${currentUrl}` : "OPENAI_BASE_URL not set to Liminal"
|
|
1027
|
+
};
|
|
1028
|
+
},
|
|
1029
|
+
getShellExports(port) {
|
|
1030
|
+
return [`export ${ENV_VAR3}=http://127.0.0.1:${port}/v1`];
|
|
1031
|
+
},
|
|
1032
|
+
async setup(port) {
|
|
1033
|
+
const exports = this.getShellExports(port);
|
|
1034
|
+
return {
|
|
1035
|
+
success: true,
|
|
1036
|
+
shellExports: exports,
|
|
1037
|
+
postSetupInstructions: [
|
|
1038
|
+
"Any tool that reads OPENAI_BASE_URL will route through Liminal.",
|
|
1039
|
+
"Make sure to source your shell profile or restart your terminal.",
|
|
1040
|
+
"",
|
|
1041
|
+
"If your tool uses a different env var (e.g., OPENAI_API_BASE),",
|
|
1042
|
+
`set it to: http://127.0.0.1:${port}/v1`
|
|
1043
|
+
]
|
|
1044
|
+
};
|
|
1045
|
+
},
|
|
1046
|
+
async teardown() {
|
|
1047
|
+
return {
|
|
1048
|
+
success: true,
|
|
1049
|
+
manualSteps: [
|
|
1050
|
+
`Remove the line \`export ${ENV_VAR3}=...\` from your shell profile (~/.zshrc or ~/.bashrc).`,
|
|
1051
|
+
"Restart your terminal or run: unset OPENAI_BASE_URL"
|
|
1052
|
+
]
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// src/connectors/index.ts
|
|
1058
|
+
var CONNECTORS = [
|
|
1059
|
+
claudeCodeConnector,
|
|
1060
|
+
codexConnector,
|
|
1061
|
+
cursorConnector,
|
|
1062
|
+
openaiCompatibleConnector
|
|
1063
|
+
];
|
|
1064
|
+
function getConnector(id) {
|
|
1065
|
+
const connector = CONNECTORS.find((c) => c.info.id === id);
|
|
1066
|
+
if (!connector) {
|
|
1067
|
+
throw new Error(`Unknown connector: ${id}`);
|
|
1068
|
+
}
|
|
1069
|
+
return connector;
|
|
630
1070
|
}
|
|
1071
|
+
function getConnectors(ids) {
|
|
1072
|
+
return ids.map(getConnector);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/commands/init.ts
|
|
1076
|
+
var BOLD = "\x1B[1m";
|
|
1077
|
+
var DIM = "\x1B[2m";
|
|
1078
|
+
var CYAN = "\x1B[36m";
|
|
1079
|
+
var GREEN = "\x1B[32m";
|
|
1080
|
+
var YELLOW = "\x1B[33m";
|
|
1081
|
+
var RESET = "\x1B[0m";
|
|
631
1082
|
async function initCommand() {
|
|
632
1083
|
printBanner();
|
|
633
1084
|
console.log(" Welcome to Liminal -- Your Transparency & Context Partner");
|
|
@@ -636,29 +1087,38 @@ async function initCommand() {
|
|
|
636
1087
|
console.log();
|
|
637
1088
|
const apiKey = await runAuthFlow();
|
|
638
1089
|
console.log();
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
1090
|
+
const port = DEFAULTS.port;
|
|
1091
|
+
console.log(` ${BOLD}Detecting installed tools...${RESET}`);
|
|
1092
|
+
console.log();
|
|
1093
|
+
const detectionResults = await Promise.all(
|
|
1094
|
+
CONNECTORS.map(async (c) => {
|
|
1095
|
+
const status = await c.detect();
|
|
1096
|
+
return { connector: c, status };
|
|
1097
|
+
})
|
|
1098
|
+
);
|
|
1099
|
+
for (const { connector, status } of detectionResults) {
|
|
1100
|
+
const icon = status.installed ? `${GREEN}\u2713${RESET}` : `${DIM}\xB7${RESET}`;
|
|
1101
|
+
console.log(` ${icon} ${connector.info.label} ${DIM}${status.detail}${RESET}`);
|
|
650
1102
|
}
|
|
651
1103
|
console.log();
|
|
1104
|
+
const toolOptions = CONNECTORS.map((c) => {
|
|
1105
|
+
const detected = detectionResults.find((r) => r.connector.info.id === c.info.id);
|
|
1106
|
+
const installed = detected?.status.installed ?? false;
|
|
1107
|
+
let description = c.info.description;
|
|
1108
|
+
if (!installed) description += ` ${DIM}(not detected)${RESET}`;
|
|
1109
|
+
if (!c.info.automatable) description += ` ${DIM}(manual setup)${RESET}`;
|
|
1110
|
+
return {
|
|
1111
|
+
label: c.info.label,
|
|
1112
|
+
value: c.info.id,
|
|
1113
|
+
description,
|
|
1114
|
+
default: c.info.id === "claude-code" && installed
|
|
1115
|
+
};
|
|
1116
|
+
});
|
|
652
1117
|
const toolsResult = await multiSelectPrompt({
|
|
653
1118
|
message: "Which AI tools will you use with Liminal?",
|
|
654
|
-
options:
|
|
655
|
-
{ label: "Claude Code", value: "claude-code", default: true },
|
|
656
|
-
{ label: "Codex", value: "codex" },
|
|
657
|
-
{ label: "Cursor", value: "cursor" },
|
|
658
|
-
{ label: "Other / OpenAI", value: "openai-compatible" }
|
|
659
|
-
]
|
|
1119
|
+
options: toolOptions
|
|
660
1120
|
});
|
|
661
|
-
const
|
|
1121
|
+
const selectedIds = toolsResult ?? ["claude-code"];
|
|
662
1122
|
console.log();
|
|
663
1123
|
const learnResult = await selectPrompt({
|
|
664
1124
|
message: "Learn from LLM responses?",
|
|
@@ -670,78 +1130,100 @@ async function initCommand() {
|
|
|
670
1130
|
});
|
|
671
1131
|
const learnFromResponses = learnResult ?? true;
|
|
672
1132
|
console.log();
|
|
673
|
-
const apiBaseUrl = DEFAULTS.apiBaseUrl;
|
|
674
1133
|
ensureDirectories();
|
|
675
1134
|
saveConfig({
|
|
676
1135
|
apiKey,
|
|
677
|
-
apiBaseUrl,
|
|
1136
|
+
apiBaseUrl: DEFAULTS.apiBaseUrl,
|
|
678
1137
|
upstreamBaseUrl: DEFAULTS.upstreamBaseUrl,
|
|
679
1138
|
anthropicUpstreamUrl: DEFAULTS.anthropicUpstreamUrl,
|
|
680
1139
|
port,
|
|
681
1140
|
learnFromResponses,
|
|
682
|
-
tools,
|
|
1141
|
+
tools: selectedIds,
|
|
683
1142
|
compressionThreshold: DEFAULTS.compressionThreshold,
|
|
684
1143
|
compressRoles: DEFAULTS.compressRoles,
|
|
685
1144
|
latencyBudgetMs: DEFAULTS.latencyBudgetMs,
|
|
686
1145
|
enabled: DEFAULTS.enabled
|
|
687
1146
|
});
|
|
1147
|
+
console.log(` ${GREEN}\u2713${RESET} Configuration saved to ${DIM}${CONFIG_FILE}${RESET}`);
|
|
688
1148
|
console.log();
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1149
|
+
const connectors = getConnectors(selectedIds);
|
|
1150
|
+
const allShellExports = [];
|
|
1151
|
+
const profile = detectShellProfile();
|
|
1152
|
+
console.log(` ${BOLD}Configuring ${connectors.length} tool${connectors.length > 1 ? "s" : ""}...${RESET}`);
|
|
1153
|
+
for (const connector of connectors) {
|
|
1154
|
+
const result = await connector.setup(port);
|
|
1155
|
+
const protocol = connector.info.protocol === "anthropic-messages" ? "Anthropic Messages API" : connector.info.protocol === "openai-responses" ? "Responses API" : "Chat Completions API";
|
|
1156
|
+
console.log();
|
|
1157
|
+
console.log(` ${CYAN}\u2500\u2500 ${connector.info.label} ${RESET}${DIM}(${protocol})${RESET}`);
|
|
1158
|
+
if (connector.info.automatable && result.shellExports.length > 0) {
|
|
1159
|
+
for (const line of result.shellExports) {
|
|
1160
|
+
console.log(` ${GREEN}\u2713${RESET} ${line}`);
|
|
1161
|
+
}
|
|
1162
|
+
allShellExports.push(...result.shellExports);
|
|
1163
|
+
}
|
|
1164
|
+
for (const line of result.postSetupInstructions) {
|
|
1165
|
+
if (line === "") {
|
|
1166
|
+
console.log();
|
|
699
1167
|
} else {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
],
|
|
706
|
-
defaultIndex: 0
|
|
707
|
-
});
|
|
708
|
-
if (autoResult === true) {
|
|
709
|
-
const newLines = exportLines.filter((line) => !lineExistsInFile(profile.path, line));
|
|
710
|
-
if (newLines.length > 0) {
|
|
711
|
-
appendToShellProfile(profile, newLines);
|
|
712
|
-
}
|
|
713
|
-
console.log();
|
|
714
|
-
console.log(` Added to ${profile.name}:`);
|
|
715
|
-
for (const line of exportLines) {
|
|
716
|
-
console.log(` ${line}`);
|
|
717
|
-
}
|
|
718
|
-
console.log();
|
|
719
|
-
console.log(` Run \x1B[1msource ${profile.name}\x1B[0m or restart your terminal to apply.`);
|
|
1168
|
+
if (line.includes("source your shell profile") || line.includes("restart your terminal")) continue;
|
|
1169
|
+
if (line.includes("will automatically route through Liminal")) {
|
|
1170
|
+
console.log(` ${DIM}${line}${RESET}`);
|
|
1171
|
+
} else if (line.startsWith(" ")) {
|
|
1172
|
+
console.log(` ${line}`);
|
|
720
1173
|
} else {
|
|
721
|
-
console.log();
|
|
722
|
-
console.log(" Add these to your shell profile:");
|
|
723
|
-
console.log();
|
|
724
|
-
for (const line of exportLines) {
|
|
725
|
-
console.log(` ${line}`);
|
|
726
|
-
}
|
|
1174
|
+
console.log(` ${line}`);
|
|
727
1175
|
}
|
|
728
1176
|
}
|
|
1177
|
+
}
|
|
1178
|
+
if (!connector.info.automatable) {
|
|
1179
|
+
console.log(` ${YELLOW}\u26A0 Requires manual configuration (see steps above)${RESET}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const uniqueExports = [...new Set(allShellExports)];
|
|
1183
|
+
if (uniqueExports.length > 0 && profile) {
|
|
1184
|
+
const allExist = uniqueExports.every((line) => lineExistsInFile(profile.path, line));
|
|
1185
|
+
console.log();
|
|
1186
|
+
if (allExist) {
|
|
1187
|
+
console.log(` ${GREEN}\u2713${RESET} Shell already configured in ${profile.name}`);
|
|
729
1188
|
} else {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1189
|
+
const autoResult = await selectPrompt({
|
|
1190
|
+
message: `Add proxy exports to ${profile.name}?`,
|
|
1191
|
+
options: [
|
|
1192
|
+
{ label: "Yes", value: true, description: "Automatic shell configuration" },
|
|
1193
|
+
{ label: "No", value: false, description: "I'll set it up manually" }
|
|
1194
|
+
],
|
|
1195
|
+
defaultIndex: 0
|
|
1196
|
+
});
|
|
1197
|
+
if (autoResult === true) {
|
|
1198
|
+
const added = appendToShellProfile(profile, uniqueExports);
|
|
1199
|
+
if (added.length > 0) {
|
|
1200
|
+
console.log();
|
|
1201
|
+
console.log(` ${GREEN}\u2713${RESET} Added to ${profile.name}`);
|
|
1202
|
+
console.log();
|
|
1203
|
+
console.log(` Run ${BOLD}source ${profile.name}${RESET} or restart your terminal.`);
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
console.log();
|
|
1207
|
+
console.log(" Add these to your shell profile:");
|
|
1208
|
+
console.log();
|
|
1209
|
+
for (const line of uniqueExports) {
|
|
1210
|
+
console.log(` ${CYAN}${line}${RESET}`);
|
|
1211
|
+
}
|
|
734
1212
|
}
|
|
735
1213
|
}
|
|
736
|
-
}
|
|
737
|
-
|
|
1214
|
+
} else if (uniqueExports.length > 0) {
|
|
1215
|
+
console.log();
|
|
1216
|
+
console.log(" Add these to your shell profile:");
|
|
738
1217
|
console.log();
|
|
739
|
-
|
|
740
|
-
|
|
1218
|
+
for (const line of uniqueExports) {
|
|
1219
|
+
console.log(` ${CYAN}${line}${RESET}`);
|
|
1220
|
+
}
|
|
741
1221
|
}
|
|
742
1222
|
console.log();
|
|
1223
|
+
console.log(` ${BOLD}Setup complete!${RESET}`);
|
|
1224
|
+
console.log();
|
|
743
1225
|
console.log(" Next step:");
|
|
744
|
-
console.log(
|
|
1226
|
+
console.log(` ${BOLD}liminal start${RESET}`);
|
|
745
1227
|
console.log();
|
|
746
1228
|
}
|
|
747
1229
|
|
|
@@ -756,64 +1238,6 @@ async function logoutCommand() {
|
|
|
756
1238
|
console.log(" Run \x1B[1mliminal login\x1B[0m to reconnect.");
|
|
757
1239
|
}
|
|
758
1240
|
|
|
759
|
-
// src/rsc/pipeline.ts
|
|
760
|
-
import {
|
|
761
|
-
CompressionPipeline,
|
|
762
|
-
RSCTransport,
|
|
763
|
-
RSCEventEmitter,
|
|
764
|
-
Session,
|
|
765
|
-
CircuitBreaker
|
|
766
|
-
} from "@cognisos/rsc-sdk";
|
|
767
|
-
var RSCPipelineWrapper = class {
|
|
768
|
-
pipeline;
|
|
769
|
-
session;
|
|
770
|
-
events;
|
|
771
|
-
transport;
|
|
772
|
-
circuitBreaker;
|
|
773
|
-
constructor(config) {
|
|
774
|
-
this.circuitBreaker = new CircuitBreaker(5, 5 * 60 * 1e3);
|
|
775
|
-
this.transport = new RSCTransport({
|
|
776
|
-
baseUrl: config.rscBaseUrl,
|
|
777
|
-
apiKey: config.rscApiKey,
|
|
778
|
-
timeout: 3e4,
|
|
779
|
-
maxRetries: 3,
|
|
780
|
-
circuitBreaker: this.circuitBreaker
|
|
781
|
-
});
|
|
782
|
-
this.events = new RSCEventEmitter();
|
|
783
|
-
this.session = new Session(config.sessionId);
|
|
784
|
-
this.pipeline = new CompressionPipeline(
|
|
785
|
-
this.transport,
|
|
786
|
-
{
|
|
787
|
-
threshold: config.compressionThreshold,
|
|
788
|
-
learnFromResponses: config.learnFromResponses,
|
|
789
|
-
latencyBudgetMs: config.latencyBudgetMs,
|
|
790
|
-
sessionId: this.session.sessionId
|
|
791
|
-
},
|
|
792
|
-
this.events
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
async healthCheck() {
|
|
796
|
-
try {
|
|
797
|
-
await this.transport.get("/health");
|
|
798
|
-
return true;
|
|
799
|
-
} catch {
|
|
800
|
-
return false;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
getSessionSummary() {
|
|
804
|
-
return this.session.getSummary();
|
|
805
|
-
}
|
|
806
|
-
getCircuitState() {
|
|
807
|
-
return this.circuitBreaker.getState();
|
|
808
|
-
}
|
|
809
|
-
isCircuitOpen() {
|
|
810
|
-
return this.circuitBreaker.getState() === "open";
|
|
811
|
-
}
|
|
812
|
-
resetCircuitBreaker() {
|
|
813
|
-
this.circuitBreaker.reset();
|
|
814
|
-
}
|
|
815
|
-
};
|
|
816
|
-
|
|
817
1241
|
// src/proxy/completions.ts
|
|
818
1242
|
import { RSCCircuitOpenError as RSCCircuitOpenError2 } from "@cognisos/rsc-sdk";
|
|
819
1243
|
|
|
@@ -919,66 +1343,115 @@ function isIndentedCodeLine(line) {
|
|
|
919
1343
|
}
|
|
920
1344
|
|
|
921
1345
|
// src/rsc/message-compressor.ts
|
|
1346
|
+
var PASSTHROUGH_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
|
|
1347
|
+
function sanitizeCompressedText(text) {
|
|
1348
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
1349
|
+
}
|
|
922
1350
|
async function compressMessages(messages, pipeline, session, compressRoles) {
|
|
923
1351
|
let anyCompressed = false;
|
|
924
1352
|
let totalTokensSaved = 0;
|
|
925
1353
|
const compressed = await Promise.all(
|
|
926
1354
|
messages.map(async (msg) => {
|
|
927
1355
|
if (!compressRoles.has(msg.role)) return msg;
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1356
|
+
return compressMessage(msg, pipeline, session, (c, saved) => {
|
|
1357
|
+
anyCompressed = anyCompressed || c;
|
|
1358
|
+
totalTokensSaved += saved;
|
|
1359
|
+
});
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
return { messages: compressed, anyCompressed, totalTokensSaved };
|
|
1363
|
+
}
|
|
1364
|
+
async function compressConversation(pipeline, session, plan, options = { compressToolResults: true }) {
|
|
1365
|
+
if (!plan.shouldCompress) {
|
|
1366
|
+
return {
|
|
1367
|
+
messages: plan.messages.map((tm) => tm.message),
|
|
1368
|
+
anyCompressed: false,
|
|
1369
|
+
totalTokensSaved: 0
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
let anyCompressed = false;
|
|
1373
|
+
let totalTokensSaved = 0;
|
|
1374
|
+
const record = (c, saved) => {
|
|
1375
|
+
anyCompressed = anyCompressed || c;
|
|
1376
|
+
totalTokensSaved += saved;
|
|
1377
|
+
};
|
|
1378
|
+
const log = options.logFn;
|
|
1379
|
+
const compressed = await Promise.all(
|
|
1380
|
+
plan.messages.map(async (tm) => {
|
|
1381
|
+
const role = tm.message.role;
|
|
1382
|
+
const blockTypes = Array.isArray(tm.message.content) ? tm.message.content.map((b) => b.type).join(",") : "string";
|
|
1383
|
+
if (tm.tier === "hot") {
|
|
1384
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 HOT (verbatim)`);
|
|
1385
|
+
return tm.message;
|
|
933
1386
|
}
|
|
934
|
-
if (
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
totalTokensSaved += saved;
|
|
938
|
-
});
|
|
1387
|
+
if (tm.eligibleTokens === 0) {
|
|
1388
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (0 eligible tok, skip)`);
|
|
1389
|
+
return tm.message;
|
|
939
1390
|
}
|
|
940
|
-
|
|
1391
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (${tm.eligibleTokens} eligible tok, batch compressing)`);
|
|
1392
|
+
return batchCompressMessage(tm.message, pipeline, session, record, options);
|
|
941
1393
|
})
|
|
942
1394
|
);
|
|
943
1395
|
return { messages: compressed, anyCompressed, totalTokensSaved };
|
|
944
1396
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1397
|
+
function extractToolResultText(part) {
|
|
1398
|
+
if (typeof part.content === "string" && part.content.trim()) {
|
|
1399
|
+
return part.content;
|
|
1400
|
+
}
|
|
1401
|
+
if (Array.isArray(part.content)) {
|
|
1402
|
+
const texts = part.content.filter((inner) => inner.type === "text" && typeof inner.text === "string" && inner.text.trim()).map((inner) => inner.text);
|
|
1403
|
+
return texts.length > 0 ? texts.join("\n") : null;
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
async function batchCompressMessage(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1408
|
+
if (typeof msg.content === "string") {
|
|
1409
|
+
return compressStringContent(msg, pipeline, session, record, options.semaphore, options.semaphoreTimeoutMs);
|
|
1410
|
+
}
|
|
1411
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
1412
|
+
const parts = msg.content;
|
|
1413
|
+
const textSegments = [];
|
|
1414
|
+
const batchedIndices = /* @__PURE__ */ new Set();
|
|
1415
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1416
|
+
const part = parts[i];
|
|
1417
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) continue;
|
|
1418
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1419
|
+
textSegments.push(part.text);
|
|
1420
|
+
batchedIndices.add(i);
|
|
1421
|
+
}
|
|
1422
|
+
if (part.type === "tool_result" && options.compressToolResults) {
|
|
1423
|
+
const extracted = extractToolResultText(part);
|
|
1424
|
+
if (extracted) {
|
|
1425
|
+
textSegments.push(extracted);
|
|
1426
|
+
batchedIndices.add(i);
|
|
959
1427
|
}
|
|
960
|
-
session.recordFailure();
|
|
961
|
-
return msg;
|
|
962
1428
|
}
|
|
963
1429
|
}
|
|
1430
|
+
if (textSegments.length === 0) return msg;
|
|
1431
|
+
const batchText = textSegments.join("\n\n");
|
|
964
1432
|
try {
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1433
|
+
const compressed = await compressTextWithSegmentation(batchText, pipeline, session, record, options.semaphore, options.semaphoreTimeoutMs);
|
|
1434
|
+
const newParts = [];
|
|
1435
|
+
let isFirstEligible = true;
|
|
1436
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1437
|
+
if (!batchedIndices.has(i)) {
|
|
1438
|
+
newParts.push(parts[i]);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (isFirstEligible) {
|
|
1442
|
+
if (parts[i].type === "text") {
|
|
1443
|
+
newParts.push({ ...parts[i], text: compressed });
|
|
1444
|
+
} else if (parts[i].type === "tool_result") {
|
|
1445
|
+
newParts.push({ ...parts[i], content: compressed });
|
|
978
1446
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1447
|
+
isFirstEligible = false;
|
|
1448
|
+
} else {
|
|
1449
|
+
if (parts[i].type === "tool_result") {
|
|
1450
|
+
newParts.push({ ...parts[i], content: "" });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return { ...msg, content: newParts };
|
|
982
1455
|
} catch (err) {
|
|
983
1456
|
if (err instanceof RSCCircuitOpenError) {
|
|
984
1457
|
session.recordFailure();
|
|
@@ -988,37 +1461,34 @@ async function compressStringContent(msg, pipeline, session, record) {
|
|
|
988
1461
|
return msg;
|
|
989
1462
|
}
|
|
990
1463
|
}
|
|
991
|
-
async function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
session
|
|
997
|
-
|
|
998
|
-
|
|
1464
|
+
async function compressMessage(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1465
|
+
if (typeof msg.content === "string") {
|
|
1466
|
+
return compressStringContent(msg, pipeline, session, record);
|
|
1467
|
+
}
|
|
1468
|
+
if (Array.isArray(msg.content)) {
|
|
1469
|
+
return compressArrayContent(msg, pipeline, session, record, options);
|
|
1470
|
+
}
|
|
1471
|
+
return msg;
|
|
1472
|
+
}
|
|
1473
|
+
async function compressStringContent(msg, pipeline, session, record, semaphore, semaphoreTimeoutMs) {
|
|
1474
|
+
const text = msg.content;
|
|
1475
|
+
try {
|
|
1476
|
+
const compressed = await compressTextWithSegmentation(text, pipeline, session, record, semaphore, semaphoreTimeoutMs);
|
|
1477
|
+
return { ...msg, content: compressed };
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1480
|
+
session.recordFailure();
|
|
1481
|
+
throw err;
|
|
1482
|
+
}
|
|
1483
|
+
session.recordFailure();
|
|
1484
|
+
return msg;
|
|
999
1485
|
}
|
|
1000
|
-
const parts = await Promise.all(
|
|
1001
|
-
segments.map(async (seg) => {
|
|
1002
|
-
if (seg.type === "code") return seg.text;
|
|
1003
|
-
if (seg.text.trim().length === 0) return seg.text;
|
|
1004
|
-
try {
|
|
1005
|
-
const result = await pipeline.compressForLLM(seg.text);
|
|
1006
|
-
session.recordCompression(result.metrics);
|
|
1007
|
-
record(!result.metrics.skipped, result.metrics.tokensSaved);
|
|
1008
|
-
return result.text;
|
|
1009
|
-
} catch (err) {
|
|
1010
|
-
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1011
|
-
session.recordFailure();
|
|
1012
|
-
return seg.text;
|
|
1013
|
-
}
|
|
1014
|
-
})
|
|
1015
|
-
);
|
|
1016
|
-
return parts.join("");
|
|
1017
1486
|
}
|
|
1018
|
-
async function compressArrayContent(msg, pipeline, session, record) {
|
|
1487
|
+
async function compressArrayContent(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1019
1488
|
const parts = msg.content;
|
|
1020
1489
|
const compressedParts = await Promise.all(
|
|
1021
1490
|
parts.map(async (part) => {
|
|
1491
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) return part;
|
|
1022
1492
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1023
1493
|
try {
|
|
1024
1494
|
const compressed = await compressTextWithSegmentation(part.text, pipeline, session, record);
|
|
@@ -1032,11 +1502,183 @@ async function compressArrayContent(msg, pipeline, session, record) {
|
|
|
1032
1502
|
return part;
|
|
1033
1503
|
}
|
|
1034
1504
|
}
|
|
1505
|
+
if (part.type === "tool_result" && options.compressToolResults) {
|
|
1506
|
+
return compressToolResult(part, pipeline, session, record);
|
|
1507
|
+
}
|
|
1035
1508
|
return part;
|
|
1036
1509
|
})
|
|
1037
1510
|
);
|
|
1038
1511
|
return { ...msg, content: compressedParts };
|
|
1039
1512
|
}
|
|
1513
|
+
async function compressToolResult(part, pipeline, session, record) {
|
|
1514
|
+
const content = part.content;
|
|
1515
|
+
if (typeof content === "string") {
|
|
1516
|
+
try {
|
|
1517
|
+
const compressed = await compressTextWithSegmentation(content, pipeline, session, record);
|
|
1518
|
+
return { ...part, content: compressed };
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1521
|
+
session.recordFailure();
|
|
1522
|
+
throw err;
|
|
1523
|
+
}
|
|
1524
|
+
session.recordFailure();
|
|
1525
|
+
return part;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (Array.isArray(content)) {
|
|
1529
|
+
try {
|
|
1530
|
+
const compressedInner = await Promise.all(
|
|
1531
|
+
content.map(async (inner) => {
|
|
1532
|
+
if (inner.type === "text" && typeof inner.text === "string") {
|
|
1533
|
+
try {
|
|
1534
|
+
const compressed = await compressTextWithSegmentation(inner.text, pipeline, session, record);
|
|
1535
|
+
return { ...inner, text: compressed };
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1538
|
+
session.recordFailure();
|
|
1539
|
+
return inner;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return inner;
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
return { ...part, content: compressedInner };
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1548
|
+
session.recordFailure();
|
|
1549
|
+
throw err;
|
|
1550
|
+
}
|
|
1551
|
+
session.recordFailure();
|
|
1552
|
+
return part;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return part;
|
|
1556
|
+
}
|
|
1557
|
+
async function compressTextWithSegmentation(text, pipeline, session, record, semaphore, semaphoreTimeoutMs) {
|
|
1558
|
+
const segments = segmentContent(text);
|
|
1559
|
+
const hasCode = segments.some((s) => s.type === "code");
|
|
1560
|
+
if (!hasCode) {
|
|
1561
|
+
if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
|
|
1562
|
+
try {
|
|
1563
|
+
const result = await pipeline.compressForLLM(text);
|
|
1564
|
+
session.recordCompression(result.metrics);
|
|
1565
|
+
const saved = Math.max(0, result.metrics.tokensSaved);
|
|
1566
|
+
record(!result.metrics.skipped, saved);
|
|
1567
|
+
return sanitizeCompressedText(result.text);
|
|
1568
|
+
} finally {
|
|
1569
|
+
if (semaphore) semaphore.release();
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const parts = await Promise.all(
|
|
1573
|
+
segments.map(async (seg) => {
|
|
1574
|
+
if (seg.type === "code") return seg.text;
|
|
1575
|
+
if (seg.text.trim().length === 0) return seg.text;
|
|
1576
|
+
if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await pipeline.compressForLLM(seg.text);
|
|
1579
|
+
session.recordCompression(result.metrics);
|
|
1580
|
+
const saved = Math.max(0, result.metrics.tokensSaved);
|
|
1581
|
+
record(!result.metrics.skipped, saved);
|
|
1582
|
+
return sanitizeCompressedText(result.text);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1585
|
+
session.recordFailure();
|
|
1586
|
+
return seg.text;
|
|
1587
|
+
} finally {
|
|
1588
|
+
if (semaphore) semaphore.release();
|
|
1589
|
+
}
|
|
1590
|
+
})
|
|
1591
|
+
);
|
|
1592
|
+
return parts.join("");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/rsc/conversation-analyzer.ts
|
|
1596
|
+
var SKIP_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
|
|
1597
|
+
function estimateTokens(text) {
|
|
1598
|
+
return Math.ceil(text.length / 4);
|
|
1599
|
+
}
|
|
1600
|
+
function estimateBlockTokens(block, compressToolResults) {
|
|
1601
|
+
if (SKIP_BLOCK_TYPES.has(block.type)) return 0;
|
|
1602
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1603
|
+
return estimateTokens(block.text);
|
|
1604
|
+
}
|
|
1605
|
+
if (block.type === "tool_result" && compressToolResults) {
|
|
1606
|
+
if (typeof block.content === "string") {
|
|
1607
|
+
return estimateTokens(block.content);
|
|
1608
|
+
}
|
|
1609
|
+
if (Array.isArray(block.content)) {
|
|
1610
|
+
return block.content.reduce(
|
|
1611
|
+
(sum, inner) => sum + estimateBlockTokens(inner, compressToolResults),
|
|
1612
|
+
0
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
function estimateMessageTokens(msg, config) {
|
|
1619
|
+
if (!config.compressRoles.has(msg.role)) return 0;
|
|
1620
|
+
if (typeof msg.content === "string") {
|
|
1621
|
+
return estimateTokens(msg.content);
|
|
1622
|
+
}
|
|
1623
|
+
if (Array.isArray(msg.content)) {
|
|
1624
|
+
return msg.content.reduce(
|
|
1625
|
+
(sum, part) => sum + estimateBlockTokens(part, config.compressToolResults),
|
|
1626
|
+
0
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
return 0;
|
|
1630
|
+
}
|
|
1631
|
+
function analyzeConversation(messages, config) {
|
|
1632
|
+
const n = messages.length;
|
|
1633
|
+
if (n < 5) {
|
|
1634
|
+
const tiered2 = messages.map((msg, i) => ({
|
|
1635
|
+
index: i,
|
|
1636
|
+
message: msg,
|
|
1637
|
+
tier: "hot",
|
|
1638
|
+
eligibleTokens: estimateMessageTokens(msg, config)
|
|
1639
|
+
}));
|
|
1640
|
+
return {
|
|
1641
|
+
messages: tiered2,
|
|
1642
|
+
totalEligibleTokens: 0,
|
|
1643
|
+
shouldCompress: false,
|
|
1644
|
+
hotCount: n,
|
|
1645
|
+
warmCount: 0,
|
|
1646
|
+
coldCount: 0
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
const coldEnd = Math.floor(n * config.coldFraction);
|
|
1650
|
+
const hotStart = n - Math.floor(n * config.hotFraction);
|
|
1651
|
+
let totalEligibleTokens = 0;
|
|
1652
|
+
let hotCount = 0;
|
|
1653
|
+
let warmCount = 0;
|
|
1654
|
+
let coldCount = 0;
|
|
1655
|
+
const tiered = messages.map((msg, i) => {
|
|
1656
|
+
let tier;
|
|
1657
|
+
if (i >= hotStart) {
|
|
1658
|
+
tier = "hot";
|
|
1659
|
+
hotCount++;
|
|
1660
|
+
} else if (i < coldEnd) {
|
|
1661
|
+
tier = "cold";
|
|
1662
|
+
coldCount++;
|
|
1663
|
+
} else {
|
|
1664
|
+
tier = "warm";
|
|
1665
|
+
warmCount++;
|
|
1666
|
+
}
|
|
1667
|
+
const eligibleTokens = estimateMessageTokens(msg, config);
|
|
1668
|
+
if (tier !== "hot") {
|
|
1669
|
+
totalEligibleTokens += eligibleTokens;
|
|
1670
|
+
}
|
|
1671
|
+
return { index: i, message: msg, tier, eligibleTokens };
|
|
1672
|
+
});
|
|
1673
|
+
return {
|
|
1674
|
+
messages: tiered,
|
|
1675
|
+
totalEligibleTokens,
|
|
1676
|
+
shouldCompress: totalEligibleTokens >= config.aggregateThreshold,
|
|
1677
|
+
hotCount,
|
|
1678
|
+
warmCount,
|
|
1679
|
+
coldCount
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1040
1682
|
|
|
1041
1683
|
// src/rsc/learning.ts
|
|
1042
1684
|
function createStreamLearningBuffer(pipeline) {
|
|
@@ -1061,7 +1703,7 @@ function createStreamLearningBuffer(pipeline) {
|
|
|
1061
1703
|
}
|
|
1062
1704
|
|
|
1063
1705
|
// src/proxy/streaming.ts
|
|
1064
|
-
async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete) {
|
|
1706
|
+
async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
|
|
1065
1707
|
clientRes.writeHead(200, {
|
|
1066
1708
|
"Content-Type": "text/event-stream",
|
|
1067
1709
|
"Cache-Control": "no-cache",
|
|
@@ -1071,27 +1713,67 @@ async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onCo
|
|
|
1071
1713
|
const reader = upstreamResponse.body.getReader();
|
|
1072
1714
|
const decoder = new TextDecoder();
|
|
1073
1715
|
let lineBuf = "";
|
|
1716
|
+
const needsAdjustment = totalTokensSaved > 0;
|
|
1074
1717
|
try {
|
|
1075
1718
|
while (true) {
|
|
1076
1719
|
const { done, value } = await reader.read();
|
|
1077
1720
|
if (done) break;
|
|
1078
1721
|
const chunk = decoder.decode(value, { stream: true });
|
|
1079
|
-
|
|
1722
|
+
if (!needsAdjustment) {
|
|
1723
|
+
clientRes.write(chunk);
|
|
1724
|
+
lineBuf += chunk;
|
|
1725
|
+
const lines2 = lineBuf.split("\n");
|
|
1726
|
+
lineBuf = lines2.pop() || "";
|
|
1727
|
+
for (const line of lines2) {
|
|
1728
|
+
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
1729
|
+
try {
|
|
1730
|
+
const json = JSON.parse(line.slice(6));
|
|
1731
|
+
const content = json?.choices?.[0]?.delta?.content;
|
|
1732
|
+
if (typeof content === "string") {
|
|
1733
|
+
onContentDelta(content);
|
|
1734
|
+
}
|
|
1735
|
+
} catch {
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1080
1741
|
lineBuf += chunk;
|
|
1081
1742
|
const lines = lineBuf.split("\n");
|
|
1082
1743
|
lineBuf = lines.pop() || "";
|
|
1744
|
+
let adjusted = false;
|
|
1745
|
+
const outputLines = [];
|
|
1083
1746
|
for (const line of lines) {
|
|
1084
1747
|
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
1085
1748
|
try {
|
|
1086
1749
|
const json = JSON.parse(line.slice(6));
|
|
1750
|
+
if (json?.usage?.prompt_tokens != null) {
|
|
1751
|
+
json.usage.prompt_tokens += totalTokensSaved;
|
|
1752
|
+
if (json.usage.total_tokens != null) {
|
|
1753
|
+
json.usage.total_tokens += totalTokensSaved;
|
|
1754
|
+
}
|
|
1755
|
+
outputLines.push(`data: ${JSON.stringify(json)}`);
|
|
1756
|
+
adjusted = true;
|
|
1757
|
+
} else {
|
|
1758
|
+
outputLines.push(line);
|
|
1759
|
+
}
|
|
1087
1760
|
const content = json?.choices?.[0]?.delta?.content;
|
|
1088
1761
|
if (typeof content === "string") {
|
|
1089
1762
|
onContentDelta(content);
|
|
1090
1763
|
}
|
|
1091
1764
|
} catch {
|
|
1765
|
+
outputLines.push(line);
|
|
1092
1766
|
}
|
|
1767
|
+
} else {
|
|
1768
|
+
outputLines.push(line);
|
|
1093
1769
|
}
|
|
1094
1770
|
}
|
|
1771
|
+
if (adjusted) {
|
|
1772
|
+
const reconstructed = outputLines.join("\n") + "\n";
|
|
1773
|
+
clientRes.write(reconstructed);
|
|
1774
|
+
} else {
|
|
1775
|
+
clientRes.write(chunk);
|
|
1776
|
+
}
|
|
1095
1777
|
}
|
|
1096
1778
|
} finally {
|
|
1097
1779
|
clientRes.end();
|
|
@@ -1099,6 +1781,25 @@ async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onCo
|
|
|
1099
1781
|
}
|
|
1100
1782
|
}
|
|
1101
1783
|
|
|
1784
|
+
// src/terminology.ts
|
|
1785
|
+
var TIER_LABELS = {
|
|
1786
|
+
HOT: "Active",
|
|
1787
|
+
WARM: "Recent",
|
|
1788
|
+
COLD: "Archived"
|
|
1789
|
+
};
|
|
1790
|
+
function formatTiersLog(hot, warm, cold, eligibleTokens) {
|
|
1791
|
+
return `[MEMORY] ${TIER_LABELS.HOT}:${hot} ${TIER_LABELS.WARM}:${warm} ${TIER_LABELS.COLD}:${cold} \xB7 ${eligibleTokens} tokens eligible`;
|
|
1792
|
+
}
|
|
1793
|
+
function formatSavedLog(tokensSaved, latencyMs) {
|
|
1794
|
+
return `[SAVED] ${tokensSaved} tokens (${latencyMs}ms)`;
|
|
1795
|
+
}
|
|
1796
|
+
function formatDegradeLog() {
|
|
1797
|
+
return "[STATUS] Connection degraded \u2014 passing through directly";
|
|
1798
|
+
}
|
|
1799
|
+
function formatResponseLog(model, tokensSaved, streaming = false) {
|
|
1800
|
+
return `[RESPONSE] ${streaming ? "Streaming " : ""}${model} response \u2192 client (saved:${tokensSaved}tok)`;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1102
1803
|
// src/proxy/completions.ts
|
|
1103
1804
|
function setCORSHeaders(res) {
|
|
1104
1805
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -1115,7 +1816,7 @@ function extractBearerToken(req) {
|
|
|
1115
1816
|
if (!auth || !auth.startsWith("Bearer ")) return null;
|
|
1116
1817
|
return auth.slice(7);
|
|
1117
1818
|
}
|
|
1118
|
-
async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
1819
|
+
async function handleChatCompletions(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
1119
1820
|
const request = body;
|
|
1120
1821
|
if (!request.messages || !Array.isArray(request.messages)) {
|
|
1121
1822
|
sendJSON(res, 400, {
|
|
@@ -1132,23 +1833,46 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1132
1833
|
}
|
|
1133
1834
|
let messages = request.messages;
|
|
1134
1835
|
let anyCompressed = false;
|
|
1836
|
+
let totalTokensSaved = 0;
|
|
1135
1837
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
1838
|
+
const compressStart = Date.now();
|
|
1136
1839
|
try {
|
|
1137
1840
|
const compressRoles = new Set(config.compressRoles);
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1841
|
+
const plan = analyzeConversation(request.messages, {
|
|
1842
|
+
hotFraction: config.hotFraction,
|
|
1843
|
+
coldFraction: config.coldFraction,
|
|
1844
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
1845
|
+
compressRoles,
|
|
1846
|
+
compressToolResults: config.compressToolResults
|
|
1847
|
+
});
|
|
1848
|
+
if (plan.shouldCompress) {
|
|
1849
|
+
logger.log(formatTiersLog(plan.hotCount, plan.warmCount, plan.coldCount, plan.totalEligibleTokens));
|
|
1850
|
+
}
|
|
1851
|
+
const result = await compressConversation(
|
|
1140
1852
|
pipeline.pipeline,
|
|
1141
1853
|
pipeline.session,
|
|
1142
|
-
|
|
1854
|
+
plan,
|
|
1855
|
+
{
|
|
1856
|
+
compressToolResults: config.compressToolResults,
|
|
1857
|
+
logFn: (msg) => logger.log(msg),
|
|
1858
|
+
semaphore,
|
|
1859
|
+
semaphoreTimeoutMs: config.concurrencyTimeoutMs
|
|
1860
|
+
}
|
|
1143
1861
|
);
|
|
1144
1862
|
messages = result.messages;
|
|
1145
1863
|
anyCompressed = result.anyCompressed;
|
|
1864
|
+
totalTokensSaved = result.totalTokensSaved;
|
|
1865
|
+
const latencyMs = Date.now() - compressStart;
|
|
1866
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
1867
|
+
if (alert) {
|
|
1868
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
1869
|
+
}
|
|
1146
1870
|
if (result.totalTokensSaved > 0) {
|
|
1147
|
-
logger.log(
|
|
1871
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
1148
1872
|
}
|
|
1149
1873
|
} catch (err) {
|
|
1150
1874
|
if (err instanceof RSCCircuitOpenError2) {
|
|
1151
|
-
logger.log(
|
|
1875
|
+
logger.log(formatDegradeLog());
|
|
1152
1876
|
} else {
|
|
1153
1877
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1154
1878
|
}
|
|
@@ -1181,18 +1905,36 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1181
1905
|
}
|
|
1182
1906
|
if (request.stream && upstreamResponse.body) {
|
|
1183
1907
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
1908
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
1184
1909
|
await pipeSSEResponse(
|
|
1185
1910
|
upstreamResponse,
|
|
1186
1911
|
res,
|
|
1187
1912
|
(text) => learningBuffer?.append(text),
|
|
1188
|
-
() => learningBuffer?.flush()
|
|
1913
|
+
() => learningBuffer?.flush(),
|
|
1914
|
+
totalTokensSaved
|
|
1189
1915
|
);
|
|
1190
1916
|
return;
|
|
1191
1917
|
}
|
|
1192
1918
|
const responseBody = await upstreamResponse.text();
|
|
1919
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
1920
|
+
let finalBody = responseBody;
|
|
1921
|
+
if (totalTokensSaved > 0) {
|
|
1922
|
+
try {
|
|
1923
|
+
const parsed = JSON.parse(responseBody);
|
|
1924
|
+
if (parsed?.usage?.prompt_tokens != null) {
|
|
1925
|
+
parsed.usage.prompt_tokens += totalTokensSaved;
|
|
1926
|
+
if (parsed.usage.total_tokens != null) {
|
|
1927
|
+
parsed.usage.total_tokens += totalTokensSaved;
|
|
1928
|
+
}
|
|
1929
|
+
finalBody = JSON.stringify(parsed);
|
|
1930
|
+
logger.log(`[TOKENS] Adjusted prompt_tokens by +${totalTokensSaved}`);
|
|
1931
|
+
}
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1193
1935
|
setCORSHeaders(res);
|
|
1194
1936
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1195
|
-
res.end(
|
|
1937
|
+
res.end(finalBody);
|
|
1196
1938
|
if (anyCompressed) {
|
|
1197
1939
|
try {
|
|
1198
1940
|
const parsed = JSON.parse(responseBody);
|
|
@@ -1218,7 +1960,18 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1218
1960
|
import { RSCCircuitOpenError as RSCCircuitOpenError3 } from "@cognisos/rsc-sdk";
|
|
1219
1961
|
|
|
1220
1962
|
// src/proxy/anthropic-streaming.ts
|
|
1221
|
-
|
|
1963
|
+
function adjustMessageStartLine(dataLine, tokensSaved) {
|
|
1964
|
+
try {
|
|
1965
|
+
const json = JSON.parse(dataLine.slice(6));
|
|
1966
|
+
if (json?.message?.usage?.input_tokens != null) {
|
|
1967
|
+
json.message.usage.input_tokens += tokensSaved;
|
|
1968
|
+
return `data: ${JSON.stringify(json)}`;
|
|
1969
|
+
}
|
|
1970
|
+
} catch {
|
|
1971
|
+
}
|
|
1972
|
+
return null;
|
|
1973
|
+
}
|
|
1974
|
+
async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
|
|
1222
1975
|
clientRes.writeHead(200, {
|
|
1223
1976
|
"Content-Type": "text/event-stream",
|
|
1224
1977
|
"Cache-Control": "no-cache",
|
|
@@ -1229,28 +1982,70 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
|
|
|
1229
1982
|
const decoder = new TextDecoder();
|
|
1230
1983
|
let lineBuf = "";
|
|
1231
1984
|
let currentEvent = "";
|
|
1985
|
+
let usageAdjusted = false;
|
|
1986
|
+
const needsAdjustment = totalTokensSaved > 0;
|
|
1232
1987
|
try {
|
|
1233
1988
|
while (true) {
|
|
1234
1989
|
const { done, value } = await reader.read();
|
|
1235
1990
|
if (done) break;
|
|
1236
1991
|
const chunk = decoder.decode(value, { stream: true });
|
|
1237
|
-
|
|
1992
|
+
if (!needsAdjustment || usageAdjusted) {
|
|
1993
|
+
clientRes.write(chunk);
|
|
1994
|
+
lineBuf += chunk;
|
|
1995
|
+
const lines2 = lineBuf.split("\n");
|
|
1996
|
+
lineBuf = lines2.pop() || "";
|
|
1997
|
+
for (const line of lines2) {
|
|
1998
|
+
if (line.startsWith("event: ")) {
|
|
1999
|
+
currentEvent = line.slice(7).trim();
|
|
2000
|
+
} else if (line.startsWith("data: ") && currentEvent === "content_block_delta") {
|
|
2001
|
+
try {
|
|
2002
|
+
const json = JSON.parse(line.slice(6));
|
|
2003
|
+
if (json?.delta?.type === "text_delta" && typeof json.delta.text === "string") {
|
|
2004
|
+
onContentDelta(json.delta.text);
|
|
2005
|
+
}
|
|
2006
|
+
} catch {
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
1238
2012
|
lineBuf += chunk;
|
|
1239
2013
|
const lines = lineBuf.split("\n");
|
|
1240
2014
|
lineBuf = lines.pop() || "";
|
|
2015
|
+
let adjusted = false;
|
|
2016
|
+
const outputLines = [];
|
|
1241
2017
|
for (const line of lines) {
|
|
1242
2018
|
if (line.startsWith("event: ")) {
|
|
1243
2019
|
currentEvent = line.slice(7).trim();
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
2020
|
+
outputLines.push(line);
|
|
2021
|
+
} else if (line.startsWith("data: ") && currentEvent === "message_start" && !usageAdjusted) {
|
|
2022
|
+
const adjustedLine = adjustMessageStartLine(line, totalTokensSaved);
|
|
2023
|
+
if (adjustedLine) {
|
|
2024
|
+
outputLines.push(adjustedLine);
|
|
2025
|
+
usageAdjusted = true;
|
|
2026
|
+
adjusted = true;
|
|
2027
|
+
} else {
|
|
2028
|
+
outputLines.push(line);
|
|
2029
|
+
}
|
|
2030
|
+
} else {
|
|
2031
|
+
outputLines.push(line);
|
|
2032
|
+
if (line.startsWith("data: ") && currentEvent === "content_block_delta") {
|
|
2033
|
+
try {
|
|
2034
|
+
const json = JSON.parse(line.slice(6));
|
|
2035
|
+
if (json?.delta?.type === "text_delta" && typeof json.delta.text === "string") {
|
|
2036
|
+
onContentDelta(json.delta.text);
|
|
2037
|
+
}
|
|
2038
|
+
} catch {
|
|
1249
2039
|
}
|
|
1250
|
-
} catch {
|
|
1251
2040
|
}
|
|
1252
2041
|
}
|
|
1253
2042
|
}
|
|
2043
|
+
if (adjusted) {
|
|
2044
|
+
const reconstructed = outputLines.join("\n") + "\n" + (lineBuf ? "" : "");
|
|
2045
|
+
clientRes.write(reconstructed);
|
|
2046
|
+
} else {
|
|
2047
|
+
clientRes.write(chunk);
|
|
2048
|
+
}
|
|
1254
2049
|
}
|
|
1255
2050
|
} finally {
|
|
1256
2051
|
clientRes.end();
|
|
@@ -1262,7 +2057,7 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
|
|
|
1262
2057
|
function setCORSHeaders2(res) {
|
|
1263
2058
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1264
2059
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
1265
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta");
|
|
2060
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, x-liminal-session");
|
|
1266
2061
|
}
|
|
1267
2062
|
function sendAnthropicError(res, status, type, message) {
|
|
1268
2063
|
setCORSHeaders2(res);
|
|
@@ -1293,7 +2088,7 @@ function convertCompressedToAnthropic(messages) {
|
|
|
1293
2088
|
content: msg.content
|
|
1294
2089
|
}));
|
|
1295
2090
|
}
|
|
1296
|
-
async function handleAnthropicMessages(req, res, body, pipeline, config, logger) {
|
|
2091
|
+
async function handleAnthropicMessages(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
1297
2092
|
const request = body;
|
|
1298
2093
|
if (!request.messages || !Array.isArray(request.messages)) {
|
|
1299
2094
|
sendAnthropicError(res, 400, "invalid_request_error", "messages is required and must be an array");
|
|
@@ -1310,24 +2105,47 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1310
2105
|
}
|
|
1311
2106
|
let messages = request.messages;
|
|
1312
2107
|
let anyCompressed = false;
|
|
2108
|
+
let totalTokensSaved = 0;
|
|
1313
2109
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
2110
|
+
const compressStart = Date.now();
|
|
1314
2111
|
try {
|
|
1315
2112
|
const compressRoles = new Set(config.compressRoles);
|
|
1316
2113
|
const compressible = convertAnthropicToCompressible(request.messages);
|
|
1317
|
-
const
|
|
1318
|
-
|
|
2114
|
+
const plan = analyzeConversation(compressible, {
|
|
2115
|
+
hotFraction: config.hotFraction,
|
|
2116
|
+
coldFraction: config.coldFraction,
|
|
2117
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
2118
|
+
compressRoles,
|
|
2119
|
+
compressToolResults: config.compressToolResults
|
|
2120
|
+
});
|
|
2121
|
+
if (plan.shouldCompress) {
|
|
2122
|
+
logger.log(formatTiersLog(plan.hotCount, plan.warmCount, plan.coldCount, plan.totalEligibleTokens));
|
|
2123
|
+
}
|
|
2124
|
+
const result = await compressConversation(
|
|
1319
2125
|
pipeline.pipeline,
|
|
1320
2126
|
pipeline.session,
|
|
1321
|
-
|
|
2127
|
+
plan,
|
|
2128
|
+
{
|
|
2129
|
+
compressToolResults: config.compressToolResults,
|
|
2130
|
+
logFn: (msg) => logger.log(msg),
|
|
2131
|
+
semaphore,
|
|
2132
|
+
semaphoreTimeoutMs: config.concurrencyTimeoutMs
|
|
2133
|
+
}
|
|
1322
2134
|
);
|
|
1323
2135
|
messages = convertCompressedToAnthropic(result.messages);
|
|
1324
2136
|
anyCompressed = result.anyCompressed;
|
|
2137
|
+
totalTokensSaved = result.totalTokensSaved;
|
|
2138
|
+
const latencyMs = Date.now() - compressStart;
|
|
2139
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
2140
|
+
if (alert) {
|
|
2141
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
2142
|
+
}
|
|
1325
2143
|
if (result.totalTokensSaved > 0) {
|
|
1326
|
-
logger.log(
|
|
2144
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
1327
2145
|
}
|
|
1328
2146
|
} catch (err) {
|
|
1329
2147
|
if (err instanceof RSCCircuitOpenError3) {
|
|
1330
|
-
logger.log(
|
|
2148
|
+
logger.log(formatDegradeLog());
|
|
1331
2149
|
} else {
|
|
1332
2150
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1333
2151
|
}
|
|
@@ -1365,18 +2183,33 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1365
2183
|
}
|
|
1366
2184
|
if (request.stream && upstreamResponse.body) {
|
|
1367
2185
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
2186
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
1368
2187
|
await pipeAnthropicSSEResponse(
|
|
1369
2188
|
upstreamResponse,
|
|
1370
2189
|
res,
|
|
1371
2190
|
(text) => learningBuffer?.append(text),
|
|
1372
|
-
() => learningBuffer?.flush()
|
|
2191
|
+
() => learningBuffer?.flush(),
|
|
2192
|
+
totalTokensSaved
|
|
1373
2193
|
);
|
|
1374
2194
|
return;
|
|
1375
2195
|
}
|
|
1376
2196
|
const responseBody = await upstreamResponse.text();
|
|
2197
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
2198
|
+
let finalBody = responseBody;
|
|
2199
|
+
if (totalTokensSaved > 0) {
|
|
2200
|
+
try {
|
|
2201
|
+
const parsed = JSON.parse(responseBody);
|
|
2202
|
+
if (parsed?.usage?.input_tokens != null) {
|
|
2203
|
+
parsed.usage.input_tokens += totalTokensSaved;
|
|
2204
|
+
finalBody = JSON.stringify(parsed);
|
|
2205
|
+
logger.log(`[TOKENS] Adjusted input_tokens by +${totalTokensSaved}`);
|
|
2206
|
+
}
|
|
2207
|
+
} catch {
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
1377
2210
|
setCORSHeaders2(res);
|
|
1378
2211
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1379
|
-
res.end(
|
|
2212
|
+
res.end(finalBody);
|
|
1380
2213
|
if (anyCompressed) {
|
|
1381
2214
|
try {
|
|
1382
2215
|
const parsed = JSON.parse(responseBody);
|
|
@@ -1399,61 +2232,478 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1399
2232
|
}
|
|
1400
2233
|
}
|
|
1401
2234
|
|
|
1402
|
-
// src/proxy/
|
|
2235
|
+
// src/proxy/responses.ts
|
|
2236
|
+
import { RSCCircuitOpenError as RSCCircuitOpenError4 } from "@cognisos/rsc-sdk";
|
|
2237
|
+
|
|
2238
|
+
// src/proxy/responses-streaming.ts
|
|
2239
|
+
async function pipeResponsesSSE(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
|
|
2240
|
+
clientRes.writeHead(200, {
|
|
2241
|
+
"Content-Type": "text/event-stream",
|
|
2242
|
+
"Cache-Control": "no-cache",
|
|
2243
|
+
"Connection": "keep-alive",
|
|
2244
|
+
"Access-Control-Allow-Origin": "*"
|
|
2245
|
+
});
|
|
2246
|
+
const reader = upstreamResponse.body.getReader();
|
|
2247
|
+
const decoder = new TextDecoder();
|
|
2248
|
+
let lineBuf = "";
|
|
2249
|
+
let currentEvent = "";
|
|
2250
|
+
let usageAdjusted = false;
|
|
2251
|
+
const needsAdjustment = totalTokensSaved > 0;
|
|
2252
|
+
try {
|
|
2253
|
+
while (true) {
|
|
2254
|
+
const { done, value } = await reader.read();
|
|
2255
|
+
if (done) break;
|
|
2256
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2257
|
+
if (!needsAdjustment || usageAdjusted) {
|
|
2258
|
+
clientRes.write(chunk);
|
|
2259
|
+
lineBuf += chunk;
|
|
2260
|
+
const lines2 = lineBuf.split("\n");
|
|
2261
|
+
lineBuf = lines2.pop() || "";
|
|
2262
|
+
for (const line of lines2) {
|
|
2263
|
+
if (line.startsWith("event: ")) {
|
|
2264
|
+
currentEvent = line.slice(7).trim();
|
|
2265
|
+
} else if (line.startsWith("data: ") && currentEvent === "response.output_text.delta") {
|
|
2266
|
+
try {
|
|
2267
|
+
const json = JSON.parse(line.slice(6));
|
|
2268
|
+
if (typeof json?.delta === "string") {
|
|
2269
|
+
onContentDelta(json.delta);
|
|
2270
|
+
}
|
|
2271
|
+
} catch {
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
lineBuf += chunk;
|
|
2278
|
+
const lines = lineBuf.split("\n");
|
|
2279
|
+
lineBuf = lines.pop() || "";
|
|
2280
|
+
let adjusted = false;
|
|
2281
|
+
const outputLines = [];
|
|
2282
|
+
for (const line of lines) {
|
|
2283
|
+
if (line.startsWith("event: ")) {
|
|
2284
|
+
currentEvent = line.slice(7).trim();
|
|
2285
|
+
outputLines.push(line);
|
|
2286
|
+
} else if (line.startsWith("data: ") && currentEvent === "response.completed" && !usageAdjusted) {
|
|
2287
|
+
try {
|
|
2288
|
+
const json = JSON.parse(line.slice(6));
|
|
2289
|
+
if (json?.response?.usage?.input_tokens != null) {
|
|
2290
|
+
json.response.usage.input_tokens += totalTokensSaved;
|
|
2291
|
+
if (json.response.usage.total_tokens != null) {
|
|
2292
|
+
json.response.usage.total_tokens += totalTokensSaved;
|
|
2293
|
+
}
|
|
2294
|
+
outputLines.push(`data: ${JSON.stringify(json)}`);
|
|
2295
|
+
usageAdjusted = true;
|
|
2296
|
+
adjusted = true;
|
|
2297
|
+
} else {
|
|
2298
|
+
outputLines.push(line);
|
|
2299
|
+
}
|
|
2300
|
+
} catch {
|
|
2301
|
+
outputLines.push(line);
|
|
2302
|
+
}
|
|
2303
|
+
} else {
|
|
2304
|
+
outputLines.push(line);
|
|
2305
|
+
if (line.startsWith("data: ") && currentEvent === "response.output_text.delta") {
|
|
2306
|
+
try {
|
|
2307
|
+
const json = JSON.parse(line.slice(6));
|
|
2308
|
+
if (typeof json?.delta === "string") {
|
|
2309
|
+
onContentDelta(json.delta);
|
|
2310
|
+
}
|
|
2311
|
+
} catch {
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
if (adjusted) {
|
|
2317
|
+
const reconstructed = outputLines.join("\n") + "\n";
|
|
2318
|
+
clientRes.write(reconstructed);
|
|
2319
|
+
} else {
|
|
2320
|
+
clientRes.write(chunk);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
} finally {
|
|
2324
|
+
clientRes.end();
|
|
2325
|
+
onComplete();
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// src/proxy/responses.ts
|
|
1403
2330
|
function setCORSHeaders3(res) {
|
|
1404
2331
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1405
2332
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
1406
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization
|
|
1407
|
-
res.setHeader("Access-Control-Max-Age", "86400");
|
|
2333
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1408
2334
|
}
|
|
1409
2335
|
function sendJSON2(res, status, body) {
|
|
1410
2336
|
setCORSHeaders3(res);
|
|
1411
2337
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1412
2338
|
res.end(JSON.stringify(body));
|
|
1413
2339
|
}
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
1419
|
-
req.on("error", reject);
|
|
1420
|
-
});
|
|
2340
|
+
function extractBearerToken2(req) {
|
|
2341
|
+
const auth = req.headers.authorization;
|
|
2342
|
+
if (!auth || !auth.startsWith("Bearer ")) return null;
|
|
2343
|
+
return auth.slice(7);
|
|
1421
2344
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
2345
|
+
function isMessageItem(item) {
|
|
2346
|
+
return item.type === "message";
|
|
2347
|
+
}
|
|
2348
|
+
function inputToCompressibleMessages(input) {
|
|
2349
|
+
if (typeof input === "string") {
|
|
2350
|
+
return [{ role: "user", content: input }];
|
|
2351
|
+
}
|
|
2352
|
+
const messages = [];
|
|
2353
|
+
for (const item of input) {
|
|
2354
|
+
if (!isMessageItem(item)) continue;
|
|
2355
|
+
if (typeof item.content === "string") {
|
|
2356
|
+
const role = item.role === "developer" ? "system" : item.role;
|
|
2357
|
+
messages.push({ role, content: item.content });
|
|
2358
|
+
} else if (Array.isArray(item.content)) {
|
|
2359
|
+
const role = item.role === "developer" ? "system" : item.role;
|
|
2360
|
+
const parts = item.content.map((c) => {
|
|
2361
|
+
if (c.type === "input_text") {
|
|
2362
|
+
return { type: "text", text: c.text };
|
|
2363
|
+
}
|
|
2364
|
+
return c;
|
|
2365
|
+
});
|
|
2366
|
+
messages.push({ role, content: parts });
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return messages;
|
|
2370
|
+
}
|
|
2371
|
+
function applyCompressedToInput(originalInput, compressedMessages) {
|
|
2372
|
+
if (typeof originalInput === "string") {
|
|
2373
|
+
const first = compressedMessages[0];
|
|
2374
|
+
if (first && typeof first.content === "string") {
|
|
2375
|
+
return first.content;
|
|
2376
|
+
}
|
|
2377
|
+
return originalInput;
|
|
2378
|
+
}
|
|
2379
|
+
let msgIdx = 0;
|
|
2380
|
+
const result = [];
|
|
2381
|
+
for (const item of originalInput) {
|
|
2382
|
+
if (!isMessageItem(item)) {
|
|
2383
|
+
result.push(item);
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const compressed = compressedMessages[msgIdx];
|
|
2387
|
+
msgIdx++;
|
|
2388
|
+
if (!compressed) {
|
|
2389
|
+
result.push(item);
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
if (typeof compressed.content === "string") {
|
|
2393
|
+
result.push({
|
|
2394
|
+
...item,
|
|
2395
|
+
content: compressed.content
|
|
2396
|
+
});
|
|
2397
|
+
} else if (Array.isArray(compressed.content)) {
|
|
2398
|
+
const content = compressed.content.map((part) => {
|
|
2399
|
+
if (part.type === "text" && "text" in part) {
|
|
2400
|
+
return { type: "input_text", text: part.text };
|
|
2401
|
+
}
|
|
2402
|
+
return part;
|
|
2403
|
+
});
|
|
2404
|
+
result.push({
|
|
2405
|
+
...item,
|
|
2406
|
+
content
|
|
2407
|
+
});
|
|
2408
|
+
} else {
|
|
2409
|
+
result.push(item);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return result;
|
|
2413
|
+
}
|
|
2414
|
+
function extractOutputText(output) {
|
|
2415
|
+
const texts = [];
|
|
2416
|
+
for (const item of output) {
|
|
2417
|
+
if (item.type === "message") {
|
|
2418
|
+
const msg = item;
|
|
2419
|
+
for (const block of msg.content) {
|
|
2420
|
+
if (block.type === "output_text" && typeof block.text === "string") {
|
|
2421
|
+
texts.push(block.text);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
1454
2425
|
}
|
|
2426
|
+
return texts.join("");
|
|
1455
2427
|
}
|
|
1456
|
-
function
|
|
2428
|
+
async function handleResponses(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
2429
|
+
const request = body;
|
|
2430
|
+
if (request.input === void 0 || request.input === null) {
|
|
2431
|
+
sendJSON2(res, 400, {
|
|
2432
|
+
error: { message: "input is required", type: "invalid_request_error" }
|
|
2433
|
+
});
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const llmApiKey = extractBearerToken2(req);
|
|
2437
|
+
if (!llmApiKey) {
|
|
2438
|
+
sendJSON2(res, 401, {
|
|
2439
|
+
error: { message: "Authorization header with Bearer token is required", type: "authentication_error" }
|
|
2440
|
+
});
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
let compressedInput = request.input;
|
|
2444
|
+
let anyCompressed = false;
|
|
2445
|
+
let totalTokensSaved = 0;
|
|
2446
|
+
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
2447
|
+
const compressStart = Date.now();
|
|
2448
|
+
try {
|
|
2449
|
+
const compressRoles = new Set(config.compressRoles);
|
|
2450
|
+
const compressible = inputToCompressibleMessages(request.input);
|
|
2451
|
+
if (compressible.length > 0) {
|
|
2452
|
+
const result = await compressMessages(
|
|
2453
|
+
compressible,
|
|
2454
|
+
pipeline.pipeline,
|
|
2455
|
+
pipeline.session,
|
|
2456
|
+
compressRoles
|
|
2457
|
+
);
|
|
2458
|
+
compressedInput = applyCompressedToInput(request.input, result.messages);
|
|
2459
|
+
anyCompressed = result.anyCompressed;
|
|
2460
|
+
totalTokensSaved = result.totalTokensSaved;
|
|
2461
|
+
const latencyMs = Date.now() - compressStart;
|
|
2462
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
2463
|
+
if (alert) {
|
|
2464
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
2465
|
+
}
|
|
2466
|
+
if (result.totalTokensSaved > 0) {
|
|
2467
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
if (err instanceof RSCCircuitOpenError4) {
|
|
2472
|
+
logger.log(formatDegradeLog());
|
|
2473
|
+
} else {
|
|
2474
|
+
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2475
|
+
}
|
|
2476
|
+
compressedInput = request.input;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
const upstreamUrl = `${config.upstreamBaseUrl}/v1/responses`;
|
|
2480
|
+
const upstreamBody = { ...request, input: compressedInput };
|
|
2481
|
+
const upstreamHeaders = {
|
|
2482
|
+
"Authorization": `Bearer ${llmApiKey}`,
|
|
2483
|
+
"Content-Type": "application/json"
|
|
2484
|
+
};
|
|
2485
|
+
if (request.stream) {
|
|
2486
|
+
upstreamHeaders["Accept"] = "text/event-stream";
|
|
2487
|
+
}
|
|
2488
|
+
logger.log(`[RESPONSES] ${request.model} \u2192 ${upstreamUrl}`);
|
|
2489
|
+
try {
|
|
2490
|
+
const upstreamResponse = await fetch(upstreamUrl, {
|
|
2491
|
+
method: "POST",
|
|
2492
|
+
headers: upstreamHeaders,
|
|
2493
|
+
body: JSON.stringify(upstreamBody)
|
|
2494
|
+
});
|
|
2495
|
+
if (!upstreamResponse.ok) {
|
|
2496
|
+
const errorBody = await upstreamResponse.text();
|
|
2497
|
+
logger.log(`[RESPONSES] Upstream error ${upstreamResponse.status}: ${errorBody.slice(0, 500)}`);
|
|
2498
|
+
setCORSHeaders3(res);
|
|
2499
|
+
res.writeHead(upstreamResponse.status, {
|
|
2500
|
+
"Content-Type": upstreamResponse.headers.get("Content-Type") || "application/json"
|
|
2501
|
+
});
|
|
2502
|
+
res.end(errorBody);
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
if (request.stream && upstreamResponse.body) {
|
|
2506
|
+
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
2507
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
2508
|
+
await pipeResponsesSSE(
|
|
2509
|
+
upstreamResponse,
|
|
2510
|
+
res,
|
|
2511
|
+
(text) => learningBuffer?.append(text),
|
|
2512
|
+
() => learningBuffer?.flush(),
|
|
2513
|
+
totalTokensSaved
|
|
2514
|
+
);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
const responseBody = await upstreamResponse.text();
|
|
2518
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
2519
|
+
let finalBody = responseBody;
|
|
2520
|
+
if (totalTokensSaved > 0) {
|
|
2521
|
+
try {
|
|
2522
|
+
const parsed = JSON.parse(responseBody);
|
|
2523
|
+
if (parsed?.usage?.input_tokens != null) {
|
|
2524
|
+
parsed.usage.input_tokens += totalTokensSaved;
|
|
2525
|
+
if (parsed.usage.total_tokens != null) {
|
|
2526
|
+
parsed.usage.total_tokens += totalTokensSaved;
|
|
2527
|
+
}
|
|
2528
|
+
finalBody = JSON.stringify(parsed);
|
|
2529
|
+
logger.log(`[TOKENS] Adjusted input_tokens by +${totalTokensSaved}`);
|
|
2530
|
+
}
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
setCORSHeaders3(res);
|
|
2535
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2536
|
+
res.end(finalBody);
|
|
2537
|
+
if (anyCompressed) {
|
|
2538
|
+
try {
|
|
2539
|
+
const parsed = JSON.parse(responseBody);
|
|
2540
|
+
if (parsed?.output) {
|
|
2541
|
+
const text = extractOutputText(parsed.output);
|
|
2542
|
+
if (text.length > 0) {
|
|
2543
|
+
pipeline.pipeline.triggerLearning(text);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
} catch {
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2551
|
+
logger.log(`[ERROR] Upstream request failed: ${message}`);
|
|
2552
|
+
if (!res.headersSent) {
|
|
2553
|
+
sendJSON2(res, 502, {
|
|
2554
|
+
error: { message: `Failed to reach upstream LLM: ${message}`, type: "server_error" }
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/proxy/session-identity.ts
|
|
2561
|
+
import * as crypto from "crypto";
|
|
2562
|
+
function identifySession(req, pathname) {
|
|
2563
|
+
const connector = detectConnector(req, pathname);
|
|
2564
|
+
const windowHash = deriveWindowHash(req);
|
|
2565
|
+
return { connector, windowHash, raw: `${connector}:${windowHash}` };
|
|
2566
|
+
}
|
|
2567
|
+
function detectConnector(req, pathname) {
|
|
2568
|
+
if (req.headers["x-api-key"] || req.headers["anthropic-version"]) {
|
|
2569
|
+
return "claude-code";
|
|
2570
|
+
}
|
|
2571
|
+
if (pathname.startsWith("/v1/responses") || pathname.startsWith("/responses")) {
|
|
2572
|
+
return "codex";
|
|
2573
|
+
}
|
|
2574
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
2575
|
+
if (/cursor/i.test(ua)) {
|
|
2576
|
+
return "cursor";
|
|
2577
|
+
}
|
|
2578
|
+
return "openai-compatible";
|
|
2579
|
+
}
|
|
2580
|
+
function deriveWindowHash(req) {
|
|
2581
|
+
const liminalSession = req.headers["x-liminal-session"];
|
|
2582
|
+
if (typeof liminalSession === "string" && liminalSession.length > 0) {
|
|
2583
|
+
return liminalSession;
|
|
2584
|
+
}
|
|
2585
|
+
const credential = extractCredential(req);
|
|
2586
|
+
if (!credential) return "anonymous";
|
|
2587
|
+
return crypto.createHash("sha256").update(credential).digest("hex").slice(0, 8);
|
|
2588
|
+
}
|
|
2589
|
+
function extractCredential(req) {
|
|
2590
|
+
const apiKey = req.headers["x-api-key"];
|
|
2591
|
+
if (typeof apiKey === "string" && apiKey.length > 0) return apiKey;
|
|
2592
|
+
const auth = req.headers["authorization"];
|
|
2593
|
+
if (typeof auth === "string") {
|
|
2594
|
+
const match = auth.match(/^Bearer\s+(.+)$/i);
|
|
2595
|
+
if (match) return match[1];
|
|
2596
|
+
}
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// src/proxy/handler.ts
|
|
2601
|
+
function setCORSHeaders4(res) {
|
|
2602
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2603
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH");
|
|
2604
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, anthropic-dangerous-direct-browser-access, x-liminal-session");
|
|
2605
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
2606
|
+
}
|
|
2607
|
+
function sendJSON3(res, status, body) {
|
|
2608
|
+
setCORSHeaders4(res);
|
|
2609
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2610
|
+
res.end(JSON.stringify(body));
|
|
2611
|
+
}
|
|
2612
|
+
function readBody(req) {
|
|
2613
|
+
return new Promise((resolve, reject) => {
|
|
2614
|
+
const chunks = [];
|
|
2615
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2616
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
2617
|
+
req.on("error", reject);
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
function detectUpstream(req, url) {
|
|
2621
|
+
if (req.headers["x-api-key"]) return "anthropic";
|
|
2622
|
+
if (req.headers["anthropic-version"]) return "anthropic";
|
|
2623
|
+
if (url.startsWith("/v1/messages") || url.startsWith("/messages")) return "anthropic";
|
|
2624
|
+
return "anthropic";
|
|
2625
|
+
}
|
|
2626
|
+
function getUpstreamBaseUrl(target, config) {
|
|
2627
|
+
return target === "anthropic" ? config.anthropicUpstreamUrl : config.upstreamBaseUrl;
|
|
2628
|
+
}
|
|
2629
|
+
var HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
2630
|
+
"host",
|
|
2631
|
+
"connection",
|
|
2632
|
+
"keep-alive",
|
|
2633
|
+
"transfer-encoding",
|
|
2634
|
+
"te",
|
|
2635
|
+
"trailer",
|
|
2636
|
+
"upgrade",
|
|
2637
|
+
"proxy-authorization",
|
|
2638
|
+
"proxy-authenticate"
|
|
2639
|
+
]);
|
|
2640
|
+
function buildUpstreamHeaders(req) {
|
|
2641
|
+
const headers = {};
|
|
2642
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
2643
|
+
if (HOP_BY_HOP.has(key)) continue;
|
|
2644
|
+
if (value === void 0) continue;
|
|
2645
|
+
headers[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
2646
|
+
}
|
|
2647
|
+
return headers;
|
|
2648
|
+
}
|
|
2649
|
+
async function passthroughToUpstream(req, res, fullUrl, config, logger) {
|
|
2650
|
+
const target = detectUpstream(req, fullUrl);
|
|
2651
|
+
const upstreamBase = getUpstreamBaseUrl(target, config);
|
|
2652
|
+
const upstreamUrl = `${upstreamBase}${fullUrl}`;
|
|
2653
|
+
const method = req.method?.toUpperCase() ?? "GET";
|
|
2654
|
+
logger.log(`[PASSTHROUGH] ${method} ${fullUrl} \u2192 ${target} (${upstreamUrl})`);
|
|
2655
|
+
const headers = buildUpstreamHeaders(req);
|
|
2656
|
+
try {
|
|
2657
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
2658
|
+
const body = hasBody ? await readBody(req) : void 0;
|
|
2659
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
2660
|
+
method,
|
|
2661
|
+
headers,
|
|
2662
|
+
body
|
|
2663
|
+
});
|
|
2664
|
+
const contentType = upstreamRes.headers.get("Content-Type") || "application/json";
|
|
2665
|
+
const isStreaming = contentType.includes("text/event-stream");
|
|
2666
|
+
if (isStreaming && upstreamRes.body) {
|
|
2667
|
+
setCORSHeaders4(res);
|
|
2668
|
+
res.writeHead(upstreamRes.status, {
|
|
2669
|
+
"Content-Type": contentType,
|
|
2670
|
+
"Cache-Control": "no-cache",
|
|
2671
|
+
"Connection": "keep-alive"
|
|
2672
|
+
});
|
|
2673
|
+
const reader = upstreamRes.body.getReader();
|
|
2674
|
+
try {
|
|
2675
|
+
while (true) {
|
|
2676
|
+
const { done, value } = await reader.read();
|
|
2677
|
+
if (done) break;
|
|
2678
|
+
res.write(value);
|
|
2679
|
+
}
|
|
2680
|
+
} finally {
|
|
2681
|
+
res.end();
|
|
2682
|
+
}
|
|
2683
|
+
} else {
|
|
2684
|
+
const responseBody = await upstreamRes.arrayBuffer();
|
|
2685
|
+
setCORSHeaders4(res);
|
|
2686
|
+
const responseHeaders = { "Content-Type": contentType };
|
|
2687
|
+
const reqId = upstreamRes.headers.get("request-id");
|
|
2688
|
+
if (reqId) responseHeaders["request-id"] = reqId;
|
|
2689
|
+
res.writeHead(upstreamRes.status, responseHeaders);
|
|
2690
|
+
res.end(Buffer.from(responseBody));
|
|
2691
|
+
}
|
|
2692
|
+
} catch (err) {
|
|
2693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2694
|
+
logger.log(`[ERROR] Passthrough to ${target} failed: ${message}`);
|
|
2695
|
+
if (!res.headersSent) {
|
|
2696
|
+
setCORSHeaders4(res);
|
|
2697
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
2698
|
+
res.end(JSON.stringify({
|
|
2699
|
+
type: "error",
|
|
2700
|
+
error: { type: "api_error", message: `Liminal proxy: failed to reach ${target} upstream: ${message}` }
|
|
2701
|
+
}));
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
function createRequestHandler(deps) {
|
|
2706
|
+
const { sessions, semaphore, latencyMonitor, config, logger } = deps;
|
|
1457
2707
|
const startTime = Date.now();
|
|
1458
2708
|
return async (req, res) => {
|
|
1459
2709
|
try {
|
|
@@ -1463,100 +2713,92 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
1463
2713
|
const authType = req.headers["x-api-key"] ? "x-api-key" : req.headers["authorization"] ? "bearer" : "none";
|
|
1464
2714
|
logger.log(`[REQUEST] ${method} ${fullUrl} (auth: ${authType})`);
|
|
1465
2715
|
if (method === "OPTIONS") {
|
|
1466
|
-
|
|
2716
|
+
setCORSHeaders4(res);
|
|
1467
2717
|
res.writeHead(204);
|
|
1468
2718
|
res.end();
|
|
1469
2719
|
return;
|
|
1470
2720
|
}
|
|
1471
2721
|
if (method === "GET" && (url === "/health" || url === "/")) {
|
|
1472
|
-
const
|
|
1473
|
-
|
|
2722
|
+
const sessionSummaries = sessions.getAllSummaries();
|
|
2723
|
+
sendJSON3(res, 200, {
|
|
1474
2724
|
status: "ok",
|
|
1475
2725
|
version: config.rscApiKey ? "connected" : "no-api-key",
|
|
1476
|
-
rsc_connected: !pipeline.isCircuitOpen(),
|
|
1477
|
-
circuit_state: pipeline.getCircuitState(),
|
|
1478
|
-
session_id: summary.sessionId,
|
|
1479
2726
|
uptime_ms: Date.now() - startTime,
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
2727
|
+
concurrency: {
|
|
2728
|
+
active_sessions: sessions.activeCount,
|
|
2729
|
+
semaphore_available: semaphore.available,
|
|
2730
|
+
semaphore_waiting: semaphore.waiting,
|
|
2731
|
+
max_concurrent_rsc_calls: config.concurrencyLimit
|
|
2732
|
+
},
|
|
2733
|
+
latency: {
|
|
2734
|
+
global_p95_ms: latencyMonitor.getGlobalP95()
|
|
2735
|
+
},
|
|
2736
|
+
sessions: sessionSummaries.map((s) => ({
|
|
2737
|
+
session_key: s.key,
|
|
2738
|
+
connector: s.connector,
|
|
2739
|
+
circuit_state: s.circuitState,
|
|
2740
|
+
tokens_processed: s.tokensProcessed,
|
|
2741
|
+
tokens_saved: s.tokensSaved,
|
|
2742
|
+
calls_total: s.totalCalls,
|
|
2743
|
+
calls_compressed: s.compressedCalls,
|
|
2744
|
+
calls_failed: s.failedCalls,
|
|
2745
|
+
p95_latency_ms: latencyMonitor.getSessionP95(s.key),
|
|
2746
|
+
last_active_ago_ms: Date.now() - s.lastAccessedAt
|
|
2747
|
+
}))
|
|
1490
2748
|
});
|
|
1491
2749
|
return;
|
|
1492
2750
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
});
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
2751
|
+
const sessionKey = identifySession(req, url);
|
|
2752
|
+
const pipeline = sessions.getOrCreate(sessionKey);
|
|
2753
|
+
if (method === "POST" && (url === "/v1/chat/completions" || url === "/chat/completions")) {
|
|
2754
|
+
const body = await readBody(req);
|
|
2755
|
+
let parsed;
|
|
1501
2756
|
try {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
setCORSHeaders3(res);
|
|
1507
|
-
res.writeHead(upstreamRes.status, {
|
|
1508
|
-
"Content-Type": upstreamRes.headers.get("Content-Type") || "application/json"
|
|
1509
|
-
});
|
|
1510
|
-
res.end(body);
|
|
1511
|
-
} catch (err) {
|
|
1512
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1513
|
-
sendJSON2(res, 502, {
|
|
1514
|
-
error: { message: `Failed to reach upstream: ${message}`, type: "server_error" }
|
|
2757
|
+
parsed = JSON.parse(body.toString("utf-8"));
|
|
2758
|
+
} catch {
|
|
2759
|
+
sendJSON3(res, 400, {
|
|
2760
|
+
error: { message: "Invalid JSON body", type: "invalid_request_error" }
|
|
1515
2761
|
});
|
|
2762
|
+
return;
|
|
1516
2763
|
}
|
|
2764
|
+
await handleChatCompletions(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
1517
2765
|
return;
|
|
1518
2766
|
}
|
|
1519
|
-
if (method === "POST" && (url === "/v1/
|
|
2767
|
+
if (method === "POST" && (url === "/v1/responses" || url === "/responses")) {
|
|
1520
2768
|
const body = await readBody(req);
|
|
1521
2769
|
let parsed;
|
|
1522
2770
|
try {
|
|
1523
|
-
parsed = JSON.parse(body);
|
|
2771
|
+
parsed = JSON.parse(body.toString("utf-8"));
|
|
1524
2772
|
} catch {
|
|
1525
|
-
|
|
2773
|
+
sendJSON3(res, 400, {
|
|
1526
2774
|
error: { message: "Invalid JSON body", type: "invalid_request_error" }
|
|
1527
2775
|
});
|
|
1528
2776
|
return;
|
|
1529
2777
|
}
|
|
1530
|
-
await
|
|
2778
|
+
await handleResponses(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
1531
2779
|
return;
|
|
1532
2780
|
}
|
|
1533
2781
|
if (method === "POST" && (url === "/v1/messages" || url === "/messages")) {
|
|
1534
2782
|
const body = await readBody(req);
|
|
1535
2783
|
let parsed;
|
|
1536
2784
|
try {
|
|
1537
|
-
parsed = JSON.parse(body);
|
|
2785
|
+
parsed = JSON.parse(body.toString("utf-8"));
|
|
1538
2786
|
} catch {
|
|
1539
|
-
|
|
2787
|
+
sendJSON3(res, 400, {
|
|
1540
2788
|
type: "error",
|
|
1541
2789
|
error: { type: "invalid_request_error", message: "Invalid JSON body" }
|
|
1542
2790
|
});
|
|
1543
2791
|
return;
|
|
1544
2792
|
}
|
|
1545
|
-
await handleAnthropicMessages(req, res, parsed, pipeline, config, logger);
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
if (method === "POST" && url.startsWith("/v1/messages/")) {
|
|
1549
|
-
await passthroughAnthropic(req, res, fullUrl, config, logger);
|
|
2793
|
+
await handleAnthropicMessages(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
1550
2794
|
return;
|
|
1551
2795
|
}
|
|
1552
|
-
|
|
1553
|
-
error: { message: `Not found: ${method} ${url}`, type: "invalid_request_error" }
|
|
1554
|
-
});
|
|
2796
|
+
await passthroughToUpstream(req, res, fullUrl, config, logger);
|
|
1555
2797
|
} catch (err) {
|
|
1556
2798
|
const message = err instanceof Error ? err.message : String(err);
|
|
1557
2799
|
logger.log(`[ERROR] Proxy handler error: ${message}`);
|
|
1558
2800
|
if (!res.headersSent) {
|
|
1559
|
-
|
|
2801
|
+
sendJSON3(res, 500, {
|
|
1560
2802
|
error: { message: "Internal proxy error", type: "server_error" }
|
|
1561
2803
|
});
|
|
1562
2804
|
}
|
|
@@ -1564,123 +2806,783 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
1564
2806
|
};
|
|
1565
2807
|
}
|
|
1566
2808
|
|
|
1567
|
-
// src/proxy/server.ts
|
|
1568
|
-
import * as http from "http";
|
|
1569
|
-
var MAX_PORT_RETRIES = 5;
|
|
1570
|
-
var ProxyServer = class {
|
|
1571
|
-
server = null;
|
|
1572
|
-
activePort = null;
|
|
1573
|
-
requestedPort;
|
|
1574
|
-
handler;
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
this.
|
|
2809
|
+
// src/proxy/server.ts
|
|
2810
|
+
import * as http from "http";
|
|
2811
|
+
var MAX_PORT_RETRIES = 5;
|
|
2812
|
+
var ProxyServer = class {
|
|
2813
|
+
server = null;
|
|
2814
|
+
activePort = null;
|
|
2815
|
+
requestedPort;
|
|
2816
|
+
handler;
|
|
2817
|
+
connectHandler;
|
|
2818
|
+
constructor(port, handler, connectHandler) {
|
|
2819
|
+
this.requestedPort = port;
|
|
2820
|
+
this.handler = handler;
|
|
2821
|
+
this.connectHandler = connectHandler ?? null;
|
|
2822
|
+
}
|
|
2823
|
+
async start() {
|
|
2824
|
+
let lastError = null;
|
|
2825
|
+
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
2826
|
+
const port = this.requestedPort + attempt;
|
|
2827
|
+
try {
|
|
2828
|
+
await this.listen(port);
|
|
2829
|
+
this.activePort = port;
|
|
2830
|
+
return port;
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2833
|
+
if (err.code !== "EADDRINUSE") {
|
|
2834
|
+
throw lastError;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
throw lastError ?? new Error(`All ports ${this.requestedPort}-${this.requestedPort + MAX_PORT_RETRIES - 1} in use`);
|
|
2839
|
+
}
|
|
2840
|
+
listen(port) {
|
|
2841
|
+
return new Promise((resolve, reject) => {
|
|
2842
|
+
const server = http.createServer(this.handler);
|
|
2843
|
+
if (this.connectHandler) {
|
|
2844
|
+
server.on("connect", this.connectHandler);
|
|
2845
|
+
}
|
|
2846
|
+
server.on("error", reject);
|
|
2847
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2848
|
+
server.removeListener("error", reject);
|
|
2849
|
+
this.server = server;
|
|
2850
|
+
resolve();
|
|
2851
|
+
});
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
async stop() {
|
|
2855
|
+
if (!this.server) return;
|
|
2856
|
+
return new Promise((resolve) => {
|
|
2857
|
+
this.server.close(() => {
|
|
2858
|
+
this.server = null;
|
|
2859
|
+
this.activePort = null;
|
|
2860
|
+
resolve();
|
|
2861
|
+
});
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
isRunning() {
|
|
2865
|
+
return this.server !== null && this.server.listening;
|
|
2866
|
+
}
|
|
2867
|
+
getPort() {
|
|
2868
|
+
return this.activePort;
|
|
2869
|
+
}
|
|
2870
|
+
/** Expose internal HTTP server for MITM bridge socket injection */
|
|
2871
|
+
getHttpServer() {
|
|
2872
|
+
return this.server;
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
|
|
2876
|
+
// src/daemon/logger.ts
|
|
2877
|
+
import { appendFileSync as appendFileSync2, statSync, renameSync, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
|
|
2878
|
+
import { dirname as dirname2 } from "path";
|
|
2879
|
+
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
2880
|
+
var MAX_BACKUPS = 2;
|
|
2881
|
+
var FileLogger = class {
|
|
2882
|
+
logFile;
|
|
2883
|
+
mirrorStdout;
|
|
2884
|
+
constructor(options) {
|
|
2885
|
+
this.logFile = options?.logFile ?? LOG_FILE;
|
|
2886
|
+
this.mirrorStdout = options?.mirrorStdout ?? false;
|
|
2887
|
+
const logDir = dirname2(this.logFile);
|
|
2888
|
+
if (!existsSync5(logDir)) {
|
|
2889
|
+
mkdirSync2(logDir, { recursive: true });
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
log(message) {
|
|
2893
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
2894
|
+
const line = `[${timestamp}] ${message}
|
|
2895
|
+
`;
|
|
2896
|
+
try {
|
|
2897
|
+
appendFileSync2(this.logFile, line);
|
|
2898
|
+
} catch {
|
|
2899
|
+
process.stderr.write(`[LOG-WRITE-FAILED] ${line}`);
|
|
2900
|
+
}
|
|
2901
|
+
if (this.mirrorStdout) {
|
|
2902
|
+
process.stdout.write(line);
|
|
2903
|
+
}
|
|
2904
|
+
this.rotateIfNeeded();
|
|
2905
|
+
}
|
|
2906
|
+
rotateIfNeeded() {
|
|
2907
|
+
try {
|
|
2908
|
+
const stats = statSync(this.logFile);
|
|
2909
|
+
if (stats.size <= MAX_LOG_SIZE) return;
|
|
2910
|
+
for (let i = MAX_BACKUPS - 1; i >= 1; i--) {
|
|
2911
|
+
const from = `${this.logFile}.${i}`;
|
|
2912
|
+
const to = `${this.logFile}.${i + 1}`;
|
|
2913
|
+
if (existsSync5(from)) renameSync(from, to);
|
|
2914
|
+
}
|
|
2915
|
+
renameSync(this.logFile, `${this.logFile}.1`);
|
|
2916
|
+
} catch {
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
getLogFile() {
|
|
2920
|
+
return this.logFile;
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
|
|
2924
|
+
// src/rsc/session-manager.ts
|
|
2925
|
+
init_pipeline();
|
|
2926
|
+
var DEFAULT_MAX_SESSIONS = 10;
|
|
2927
|
+
var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
2928
|
+
var EVICTION_INTERVAL_MS = 6e4;
|
|
2929
|
+
var SessionManager = class {
|
|
2930
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2931
|
+
config;
|
|
2932
|
+
evictionTimer = null;
|
|
2933
|
+
constructor(config) {
|
|
2934
|
+
this.config = {
|
|
2935
|
+
...config,
|
|
2936
|
+
maxSessions: config.maxSessions ?? DEFAULT_MAX_SESSIONS,
|
|
2937
|
+
sessionTtlMs: config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS
|
|
2938
|
+
};
|
|
2939
|
+
this.evictionTimer = setInterval(() => this.evictStale(), EVICTION_INTERVAL_MS);
|
|
2940
|
+
if (this.evictionTimer.unref) {
|
|
2941
|
+
this.evictionTimer.unref();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
2945
|
+
getOrCreate(key) {
|
|
2946
|
+
const existing = this.sessions.get(key.raw);
|
|
2947
|
+
if (existing) {
|
|
2948
|
+
existing.lastAccessedAt = Date.now();
|
|
2949
|
+
existing.requestCount++;
|
|
2950
|
+
return existing.pipeline;
|
|
2951
|
+
}
|
|
2952
|
+
if (this.sessions.size >= this.config.maxSessions) {
|
|
2953
|
+
this.evictLRU();
|
|
2954
|
+
}
|
|
2955
|
+
const pipeline = new RSCPipelineWrapper({
|
|
2956
|
+
...this.config.pipelineConfig,
|
|
2957
|
+
sessionId: key.raw
|
|
2958
|
+
});
|
|
2959
|
+
this.sessions.set(key.raw, {
|
|
2960
|
+
pipeline,
|
|
2961
|
+
lastAccessedAt: Date.now(),
|
|
2962
|
+
requestCount: 1,
|
|
2963
|
+
connector: key.connector
|
|
2964
|
+
});
|
|
2965
|
+
this.config.onSessionCreated?.(key.raw, pipeline);
|
|
2966
|
+
return pipeline;
|
|
2967
|
+
}
|
|
2968
|
+
getAllSummaries() {
|
|
2969
|
+
const entries = [];
|
|
2970
|
+
for (const [key, managed] of this.sessions) {
|
|
2971
|
+
entries.push(this.buildHealthEntry(key, managed));
|
|
2972
|
+
}
|
|
2973
|
+
return entries;
|
|
2974
|
+
}
|
|
2975
|
+
getSessionSummary(key) {
|
|
2976
|
+
const managed = this.sessions.get(key);
|
|
2977
|
+
if (!managed) return null;
|
|
2978
|
+
return this.buildHealthEntry(key, managed);
|
|
2979
|
+
}
|
|
2980
|
+
get activeCount() {
|
|
2981
|
+
return this.sessions.size;
|
|
2982
|
+
}
|
|
2983
|
+
shutdown() {
|
|
2984
|
+
if (this.evictionTimer !== null) {
|
|
2985
|
+
clearInterval(this.evictionTimer);
|
|
2986
|
+
this.evictionTimer = null;
|
|
2987
|
+
}
|
|
2988
|
+
this.sessions.clear();
|
|
2989
|
+
}
|
|
2990
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
2991
|
+
buildHealthEntry(key, managed) {
|
|
2992
|
+
const summary = managed.pipeline.getSessionSummary();
|
|
2993
|
+
return {
|
|
2994
|
+
key,
|
|
2995
|
+
connector: managed.connector,
|
|
2996
|
+
circuitState: managed.pipeline.getCircuitState(),
|
|
2997
|
+
tokensProcessed: summary.tokensProcessed,
|
|
2998
|
+
tokensSaved: summary.tokensSaved,
|
|
2999
|
+
totalCalls: summary.totalCalls,
|
|
3000
|
+
compressedCalls: summary.compressedCalls,
|
|
3001
|
+
failedCalls: summary.failedCalls,
|
|
3002
|
+
lastAccessedAt: managed.lastAccessedAt
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
evictStale() {
|
|
3006
|
+
const now = Date.now();
|
|
3007
|
+
const cutoff = now - this.config.sessionTtlMs;
|
|
3008
|
+
for (const [key, managed] of this.sessions) {
|
|
3009
|
+
if (managed.lastAccessedAt < cutoff) {
|
|
3010
|
+
this.sessions.delete(key);
|
|
3011
|
+
this.config.onSessionEvicted?.(key);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
evictLRU() {
|
|
3016
|
+
let oldestKey = null;
|
|
3017
|
+
let oldestTime = Infinity;
|
|
3018
|
+
for (const [key, managed] of this.sessions) {
|
|
3019
|
+
if (managed.lastAccessedAt < oldestTime) {
|
|
3020
|
+
oldestTime = managed.lastAccessedAt;
|
|
3021
|
+
oldestKey = key;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
if (oldestKey !== null) {
|
|
3025
|
+
this.sessions.delete(oldestKey);
|
|
3026
|
+
this.config.onSessionEvicted?.(oldestKey);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
|
|
3031
|
+
// src/rsc/semaphore.ts
|
|
3032
|
+
var SemaphoreTimeoutError = class extends Error {
|
|
3033
|
+
constructor(timeoutMs) {
|
|
3034
|
+
super(`Semaphore acquire timed out after ${timeoutMs}ms`);
|
|
3035
|
+
this.name = "SemaphoreTimeoutError";
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
var Semaphore = class {
|
|
3039
|
+
permits;
|
|
3040
|
+
queue = [];
|
|
3041
|
+
constructor(maxPermits) {
|
|
3042
|
+
if (maxPermits < 1) throw new RangeError("maxPermits must be >= 1");
|
|
3043
|
+
this.permits = maxPermits;
|
|
3044
|
+
}
|
|
3045
|
+
get available() {
|
|
3046
|
+
return this.permits;
|
|
3047
|
+
}
|
|
3048
|
+
get waiting() {
|
|
3049
|
+
return this.queue.length;
|
|
3050
|
+
}
|
|
3051
|
+
acquire(timeoutMs) {
|
|
3052
|
+
if (this.permits > 0) {
|
|
3053
|
+
this.permits--;
|
|
3054
|
+
return Promise.resolve();
|
|
3055
|
+
}
|
|
3056
|
+
return new Promise((resolve, reject) => {
|
|
3057
|
+
const waiter = { resolve, reject };
|
|
3058
|
+
this.queue.push(waiter);
|
|
3059
|
+
if (timeoutMs !== void 0 && timeoutMs >= 0) {
|
|
3060
|
+
const timer = setTimeout(() => {
|
|
3061
|
+
const idx = this.queue.indexOf(waiter);
|
|
3062
|
+
if (idx !== -1) {
|
|
3063
|
+
this.queue.splice(idx, 1);
|
|
3064
|
+
reject(new SemaphoreTimeoutError(timeoutMs));
|
|
3065
|
+
}
|
|
3066
|
+
}, timeoutMs);
|
|
3067
|
+
const originalResolve = waiter.resolve;
|
|
3068
|
+
waiter.resolve = () => {
|
|
3069
|
+
clearTimeout(timer);
|
|
3070
|
+
originalResolve();
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
release() {
|
|
3076
|
+
const next = this.queue.shift();
|
|
3077
|
+
if (next) {
|
|
3078
|
+
next.resolve();
|
|
3079
|
+
} else {
|
|
3080
|
+
this.permits++;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
|
|
3085
|
+
// src/rsc/latency-monitor.ts
|
|
3086
|
+
var DEFAULT_CONFIG = {
|
|
3087
|
+
warningThresholdMs: 4e3,
|
|
3088
|
+
criticalThresholdMs: 8e3,
|
|
3089
|
+
windowSize: 50
|
|
3090
|
+
};
|
|
3091
|
+
var CircularBuffer = class {
|
|
3092
|
+
buffer;
|
|
3093
|
+
index = 0;
|
|
3094
|
+
count = 0;
|
|
3095
|
+
capacity;
|
|
3096
|
+
constructor(capacity) {
|
|
3097
|
+
this.capacity = capacity;
|
|
3098
|
+
this.buffer = new Array(capacity);
|
|
3099
|
+
}
|
|
3100
|
+
push(value) {
|
|
3101
|
+
this.buffer[this.index] = value;
|
|
3102
|
+
this.index = (this.index + 1) % this.capacity;
|
|
3103
|
+
if (this.count < this.capacity) {
|
|
3104
|
+
this.count++;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
getValues() {
|
|
3108
|
+
if (this.count < this.capacity) {
|
|
3109
|
+
return this.buffer.slice(0, this.count);
|
|
3110
|
+
}
|
|
3111
|
+
return [...this.buffer.slice(this.index), ...this.buffer.slice(0, this.index)];
|
|
3112
|
+
}
|
|
3113
|
+
get size() {
|
|
3114
|
+
return this.count;
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
function calculateP95(values) {
|
|
3118
|
+
if (values.length === 0) return null;
|
|
3119
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3120
|
+
const idx = Math.floor(sorted.length * 0.95);
|
|
3121
|
+
return sorted[Math.min(idx, sorted.length - 1)];
|
|
3122
|
+
}
|
|
3123
|
+
var LatencyMonitor = class {
|
|
3124
|
+
config;
|
|
3125
|
+
sessionWindows = /* @__PURE__ */ new Map();
|
|
3126
|
+
globalWindow;
|
|
3127
|
+
callbacks = [];
|
|
3128
|
+
constructor(config) {
|
|
3129
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3130
|
+
this.globalWindow = new CircularBuffer(this.config.windowSize * 4);
|
|
3131
|
+
}
|
|
3132
|
+
record(sessionKey, latencyMs) {
|
|
3133
|
+
let sessionBuf = this.sessionWindows.get(sessionKey);
|
|
3134
|
+
if (!sessionBuf) {
|
|
3135
|
+
sessionBuf = new CircularBuffer(this.config.windowSize);
|
|
3136
|
+
this.sessionWindows.set(sessionKey, sessionBuf);
|
|
3137
|
+
}
|
|
3138
|
+
sessionBuf.push(latencyMs);
|
|
3139
|
+
this.globalWindow.push(latencyMs);
|
|
3140
|
+
const globalP95 = calculateP95(this.globalWindow.getValues());
|
|
3141
|
+
if (globalP95 === null) return null;
|
|
3142
|
+
let alert = null;
|
|
3143
|
+
if (globalP95 >= this.config.criticalThresholdMs) {
|
|
3144
|
+
alert = {
|
|
3145
|
+
type: "critical",
|
|
3146
|
+
message: `Global p95 latency ${globalP95.toFixed(0)}ms exceeds critical threshold ${this.config.criticalThresholdMs}ms`,
|
|
3147
|
+
sessionKey,
|
|
3148
|
+
p95Ms: globalP95,
|
|
3149
|
+
thresholdMs: this.config.criticalThresholdMs,
|
|
3150
|
+
activeSessions: this.sessionWindows.size,
|
|
3151
|
+
suggestion: "Reduce active sessions or increase latency budget"
|
|
3152
|
+
};
|
|
3153
|
+
} else if (globalP95 >= this.config.warningThresholdMs) {
|
|
3154
|
+
alert = {
|
|
3155
|
+
type: "warning",
|
|
3156
|
+
message: `Global p95 latency ${globalP95.toFixed(0)}ms exceeds warning threshold ${this.config.warningThresholdMs}ms`,
|
|
3157
|
+
sessionKey,
|
|
3158
|
+
p95Ms: globalP95,
|
|
3159
|
+
thresholdMs: this.config.warningThresholdMs,
|
|
3160
|
+
activeSessions: this.sessionWindows.size,
|
|
3161
|
+
suggestion: "Consider reducing active sessions"
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
if (alert) {
|
|
3165
|
+
for (const cb of this.callbacks) {
|
|
3166
|
+
cb(alert);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
return alert;
|
|
3170
|
+
}
|
|
3171
|
+
getSessionP95(sessionKey) {
|
|
3172
|
+
const buf = this.sessionWindows.get(sessionKey);
|
|
3173
|
+
if (!buf) return null;
|
|
3174
|
+
return calculateP95(buf.getValues());
|
|
3175
|
+
}
|
|
3176
|
+
getGlobalP95() {
|
|
3177
|
+
return calculateP95(this.globalWindow.getValues());
|
|
3178
|
+
}
|
|
3179
|
+
onAlert(cb) {
|
|
3180
|
+
this.callbacks.push(cb);
|
|
3181
|
+
}
|
|
3182
|
+
get sessionCount() {
|
|
3183
|
+
return this.sessionWindows.size;
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// src/tls/connect-handler.ts
|
|
3188
|
+
import * as net from "net";
|
|
3189
|
+
|
|
3190
|
+
// src/tls/allowlist.ts
|
|
3191
|
+
var MITM_HOSTS = /* @__PURE__ */ new Set([
|
|
3192
|
+
"api.openai.com",
|
|
3193
|
+
"api.anthropic.com",
|
|
3194
|
+
"generativelanguage.googleapis.com"
|
|
3195
|
+
]);
|
|
3196
|
+
function shouldIntercept(hostname) {
|
|
3197
|
+
return MITM_HOSTS.has(hostname);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/tls/connect-handler.ts
|
|
3201
|
+
function createConnectHandler(options) {
|
|
3202
|
+
const { logger, onIntercept } = options;
|
|
3203
|
+
return (req, clientSocket, head) => {
|
|
3204
|
+
const target = req.url ?? "";
|
|
3205
|
+
const [hostname, portStr] = parseConnectTarget(target);
|
|
3206
|
+
const port = parseInt(portStr, 10) || 443;
|
|
3207
|
+
if (!hostname) {
|
|
3208
|
+
logger.log(`[CONNECT] Invalid target: ${target}`);
|
|
3209
|
+
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
3210
|
+
clientSocket.destroy();
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if (shouldIntercept(hostname) && onIntercept) {
|
|
3214
|
+
logger.log(`[CONNECT] ${hostname}:${port} \u2192 intercept`);
|
|
3215
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
3216
|
+
if (head.length > 0) {
|
|
3217
|
+
clientSocket.unshift(head);
|
|
3218
|
+
}
|
|
3219
|
+
onIntercept(clientSocket, hostname, port);
|
|
3220
|
+
} else {
|
|
3221
|
+
logger.log(`[TUNNEL] ${hostname}:${port} \u2192 passthrough`);
|
|
3222
|
+
const upstreamSocket = net.connect(port, hostname, () => {
|
|
3223
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
3224
|
+
if (head.length > 0) {
|
|
3225
|
+
upstreamSocket.write(head);
|
|
3226
|
+
}
|
|
3227
|
+
clientSocket.pipe(upstreamSocket);
|
|
3228
|
+
upstreamSocket.pipe(clientSocket);
|
|
3229
|
+
});
|
|
3230
|
+
upstreamSocket.on("error", (err) => {
|
|
3231
|
+
logger.log(`[TUNNEL] ${hostname}:${port} upstream error: ${err.message}`);
|
|
3232
|
+
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
3233
|
+
clientSocket.destroy();
|
|
3234
|
+
});
|
|
3235
|
+
clientSocket.on("error", (err) => {
|
|
3236
|
+
logger.log(`[TUNNEL] ${hostname}:${port} client error: ${err.message}`);
|
|
3237
|
+
upstreamSocket.destroy();
|
|
3238
|
+
});
|
|
3239
|
+
clientSocket.on("close", () => upstreamSocket.destroy());
|
|
3240
|
+
upstreamSocket.on("close", () => clientSocket.destroy());
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
function parseConnectTarget(target) {
|
|
3245
|
+
const colonIdx = target.lastIndexOf(":");
|
|
3246
|
+
if (colonIdx === -1) return [target, "443"];
|
|
3247
|
+
return [target.slice(0, colonIdx), target.slice(colonIdx + 1)];
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// src/tls/ca.ts
|
|
3251
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
|
|
3252
|
+
import { join as join5 } from "path";
|
|
3253
|
+
import forge from "node-forge";
|
|
3254
|
+
var CA_CERT_PATH = join5(LIMINAL_DIR, "ca.pem");
|
|
3255
|
+
var CA_KEY_PATH = join5(LIMINAL_DIR, "ca-key.pem");
|
|
3256
|
+
function generateCA() {
|
|
3257
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
3258
|
+
const cert = forge.pki.createCertificate();
|
|
3259
|
+
cert.publicKey = keys.publicKey;
|
|
3260
|
+
cert.serialNumber = generateSerialNumber();
|
|
3261
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
3262
|
+
cert.validity.notAfter = /* @__PURE__ */ new Date();
|
|
3263
|
+
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 5);
|
|
3264
|
+
const attrs = [
|
|
3265
|
+
{ name: "commonName", value: "Liminal Proxy CA" },
|
|
3266
|
+
{ name: "organizationName", value: "Liminal (Cognisos)" },
|
|
3267
|
+
{ shortName: "OU", value: "Local Development" }
|
|
3268
|
+
];
|
|
3269
|
+
cert.setSubject(attrs);
|
|
3270
|
+
cert.setIssuer(attrs);
|
|
3271
|
+
cert.setExtensions([
|
|
3272
|
+
{ name: "basicConstraints", cA: true, critical: true },
|
|
3273
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
|
|
3274
|
+
{
|
|
3275
|
+
name: "subjectKeyIdentifier"
|
|
3276
|
+
}
|
|
3277
|
+
]);
|
|
3278
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
3279
|
+
return {
|
|
3280
|
+
certPem: forge.pki.certificateToPem(cert),
|
|
3281
|
+
keyPem: forge.pki.privateKeyToPem(keys.privateKey)
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function generateAndSaveCA() {
|
|
3285
|
+
if (!existsSync6(LIMINAL_DIR)) {
|
|
3286
|
+
mkdirSync3(LIMINAL_DIR, { recursive: true, mode: 448 });
|
|
3287
|
+
}
|
|
3288
|
+
const { certPem, keyPem } = generateCA();
|
|
3289
|
+
writeFileSync3(CA_CERT_PATH, certPem, { encoding: "utf-8", mode: 420 });
|
|
3290
|
+
writeFileSync3(CA_KEY_PATH, keyPem, { encoding: "utf-8", mode: 384 });
|
|
3291
|
+
return { certPem, keyPem };
|
|
3292
|
+
}
|
|
3293
|
+
function loadCA() {
|
|
3294
|
+
if (!existsSync6(CA_CERT_PATH) || !existsSync6(CA_KEY_PATH)) {
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
return {
|
|
3298
|
+
certPem: readFileSync4(CA_CERT_PATH, "utf-8"),
|
|
3299
|
+
keyPem: readFileSync4(CA_KEY_PATH, "utf-8")
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
function ensureCA() {
|
|
3303
|
+
const existing = loadCA();
|
|
3304
|
+
if (existing) return existing;
|
|
3305
|
+
return generateAndSaveCA();
|
|
3306
|
+
}
|
|
3307
|
+
function hasCA() {
|
|
3308
|
+
return existsSync6(CA_CERT_PATH) && existsSync6(CA_KEY_PATH);
|
|
3309
|
+
}
|
|
3310
|
+
function removeCA() {
|
|
3311
|
+
if (existsSync6(CA_CERT_PATH)) unlinkSync(CA_CERT_PATH);
|
|
3312
|
+
if (existsSync6(CA_KEY_PATH)) unlinkSync(CA_KEY_PATH);
|
|
3313
|
+
}
|
|
3314
|
+
function getCAInfo() {
|
|
3315
|
+
const ca = loadCA();
|
|
3316
|
+
if (!ca) return null;
|
|
3317
|
+
const cert = forge.pki.certificateFromPem(ca.certPem);
|
|
3318
|
+
const cn = cert.subject.getField("CN");
|
|
3319
|
+
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
|
|
3320
|
+
const md = forge.md.sha256.create();
|
|
3321
|
+
md.update(der);
|
|
3322
|
+
const fingerprint = md.digest().toHex().match(/.{2}/g).join(":").toUpperCase();
|
|
3323
|
+
return {
|
|
3324
|
+
commonName: cn ? cn.value : "Unknown",
|
|
3325
|
+
validFrom: cert.validity.notBefore,
|
|
3326
|
+
validTo: cert.validity.notAfter,
|
|
3327
|
+
fingerprint
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
function generateSerialNumber() {
|
|
3331
|
+
const bytes = forge.random.getBytesSync(16);
|
|
3332
|
+
return forge.util.bytesToHex(bytes);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// src/tls/trust.ts
|
|
3336
|
+
import { execSync as execSync3 } from "child_process";
|
|
3337
|
+
import { existsSync as existsSync7, copyFileSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3338
|
+
function installCA() {
|
|
3339
|
+
if (!existsSync7(CA_CERT_PATH)) {
|
|
3340
|
+
return { success: false, message: 'CA certificate not found. Run "liminal init" first.', requiresSudo: false };
|
|
1578
3341
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
3342
|
+
const platform = process.platform;
|
|
3343
|
+
if (platform === "darwin") return installMacOS();
|
|
3344
|
+
if (platform === "linux") return installLinux();
|
|
3345
|
+
if (platform === "win32") return installWindows();
|
|
3346
|
+
return { success: false, message: `Unsupported platform: ${platform}`, requiresSudo: false };
|
|
3347
|
+
}
|
|
3348
|
+
function removeCA2() {
|
|
3349
|
+
const platform = process.platform;
|
|
3350
|
+
if (platform === "darwin") return removeMacOS();
|
|
3351
|
+
if (platform === "linux") return removeLinux();
|
|
3352
|
+
if (platform === "win32") return removeWindows();
|
|
3353
|
+
return { success: false, message: `Unsupported platform: ${platform}`, requiresSudo: false };
|
|
3354
|
+
}
|
|
3355
|
+
function isCATrusted() {
|
|
3356
|
+
const platform = process.platform;
|
|
3357
|
+
if (platform === "darwin") return isTrustedMacOS();
|
|
3358
|
+
if (platform === "linux") return isTrustedLinux();
|
|
3359
|
+
if (platform === "win32") return isTrustedWindows();
|
|
3360
|
+
return false;
|
|
3361
|
+
}
|
|
3362
|
+
var MACOS_LABEL = "Liminal Proxy CA";
|
|
3363
|
+
function installMacOS() {
|
|
3364
|
+
try {
|
|
3365
|
+
execSync3(
|
|
3366
|
+
`security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db "${CA_CERT_PATH}"`,
|
|
3367
|
+
{ stdio: "pipe" }
|
|
3368
|
+
);
|
|
3369
|
+
return { success: true, message: "CA installed in login keychain (trusted for SSL)", requiresSudo: false };
|
|
3370
|
+
} catch (err) {
|
|
3371
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3372
|
+
if (msg.includes("authorization") || msg.includes("permission")) {
|
|
3373
|
+
return {
|
|
3374
|
+
success: false,
|
|
3375
|
+
message: "Keychain access denied. You may need to unlock your keychain or run with sudo.",
|
|
3376
|
+
requiresSudo: true
|
|
3377
|
+
};
|
|
1593
3378
|
}
|
|
1594
|
-
|
|
3379
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: false };
|
|
1595
3380
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
3381
|
+
}
|
|
3382
|
+
function removeMacOS() {
|
|
3383
|
+
try {
|
|
3384
|
+
execSync3(
|
|
3385
|
+
`security delete-certificate -c "${MACOS_LABEL}" ~/Library/Keychains/login.keychain-db`,
|
|
3386
|
+
{ stdio: "pipe" }
|
|
3387
|
+
);
|
|
3388
|
+
return { success: true, message: "CA removed from login keychain", requiresSudo: false };
|
|
3389
|
+
} catch (err) {
|
|
3390
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3391
|
+
if (msg.includes("could not be found")) {
|
|
3392
|
+
return { success: true, message: "CA was not in keychain (already removed)", requiresSudo: false };
|
|
3393
|
+
}
|
|
3394
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: false };
|
|
1606
3395
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
3396
|
+
}
|
|
3397
|
+
function isTrustedMacOS() {
|
|
3398
|
+
try {
|
|
3399
|
+
const out = execSync3(
|
|
3400
|
+
`security find-certificate -c "${MACOS_LABEL}" ~/Library/Keychains/login.keychain-db`,
|
|
3401
|
+
{ stdio: "pipe", encoding: "utf-8" }
|
|
3402
|
+
);
|
|
3403
|
+
return out.includes(MACOS_LABEL);
|
|
3404
|
+
} catch {
|
|
3405
|
+
return false;
|
|
1616
3406
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
3407
|
+
}
|
|
3408
|
+
var LINUX_CERT_PATH = "/usr/local/share/ca-certificates/liminal-proxy-ca.crt";
|
|
3409
|
+
function installLinux() {
|
|
3410
|
+
try {
|
|
3411
|
+
copyFileSync(CA_CERT_PATH, LINUX_CERT_PATH);
|
|
3412
|
+
execSync3("update-ca-certificates", { stdio: "pipe" });
|
|
3413
|
+
return { success: true, message: "CA installed in system trust store", requiresSudo: true };
|
|
3414
|
+
} catch (err) {
|
|
3415
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3416
|
+
if (msg.includes("EACCES") || msg.includes("permission")) {
|
|
3417
|
+
return {
|
|
3418
|
+
success: false,
|
|
3419
|
+
message: `Permission denied. Run with sudo:
|
|
3420
|
+
sudo liminal trust-ca`,
|
|
3421
|
+
requiresSudo: true
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: true };
|
|
1619
3425
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
3426
|
+
}
|
|
3427
|
+
function removeLinux() {
|
|
3428
|
+
try {
|
|
3429
|
+
if (existsSync7(LINUX_CERT_PATH)) {
|
|
3430
|
+
unlinkSync2(LINUX_CERT_PATH);
|
|
3431
|
+
execSync3("update-ca-certificates --fresh", { stdio: "pipe" });
|
|
3432
|
+
}
|
|
3433
|
+
return { success: true, message: "CA removed from system trust store", requiresSudo: true };
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3436
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: true };
|
|
1622
3437
|
}
|
|
1623
|
-
}
|
|
3438
|
+
}
|
|
3439
|
+
function isTrustedLinux() {
|
|
3440
|
+
return existsSync7(LINUX_CERT_PATH);
|
|
3441
|
+
}
|
|
3442
|
+
function installWindows() {
|
|
3443
|
+
try {
|
|
3444
|
+
execSync3(`certutil -addstore -user -f "ROOT" "${CA_CERT_PATH}"`, { stdio: "pipe" });
|
|
3445
|
+
return { success: true, message: "CA installed in user certificate store", requiresSudo: false };
|
|
3446
|
+
} catch (err) {
|
|
3447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3448
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: false };
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
function removeWindows() {
|
|
3452
|
+
try {
|
|
3453
|
+
execSync3(`certutil -delstore -user "ROOT" "${MACOS_LABEL}"`, { stdio: "pipe" });
|
|
3454
|
+
return { success: true, message: "CA removed from user certificate store", requiresSudo: false };
|
|
3455
|
+
} catch (err) {
|
|
3456
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3457
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: false };
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function isTrustedWindows() {
|
|
3461
|
+
try {
|
|
3462
|
+
const out = execSync3(`certutil -verifystore -user "ROOT" "${MACOS_LABEL}"`, {
|
|
3463
|
+
stdio: "pipe",
|
|
3464
|
+
encoding: "utf-8"
|
|
3465
|
+
});
|
|
3466
|
+
return out.includes("Liminal");
|
|
3467
|
+
} catch {
|
|
3468
|
+
return false;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
1624
3471
|
|
|
1625
|
-
// src/
|
|
1626
|
-
import
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
var
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
}
|
|
3472
|
+
// src/tls/mitm-bridge.ts
|
|
3473
|
+
import * as tls from "tls";
|
|
3474
|
+
|
|
3475
|
+
// src/tls/cert-generator.ts
|
|
3476
|
+
import forge2 from "node-forge";
|
|
3477
|
+
var CERT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3478
|
+
var MAX_CACHE_SIZE = 50;
|
|
3479
|
+
var CertGenerator = class {
|
|
3480
|
+
caCert;
|
|
3481
|
+
caKey;
|
|
3482
|
+
cache = /* @__PURE__ */ new Map();
|
|
3483
|
+
constructor(caCertPem, caKeyPem) {
|
|
3484
|
+
this.caCert = forge2.pki.certificateFromPem(caCertPem);
|
|
3485
|
+
this.caKey = forge2.pki.privateKeyFromPem(caKeyPem);
|
|
1640
3486
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
3487
|
+
/**
|
|
3488
|
+
* Get or generate a TLS certificate for the given hostname.
|
|
3489
|
+
* Certificates are cached for 24 hours.
|
|
3490
|
+
*/
|
|
3491
|
+
getCert(hostname) {
|
|
3492
|
+
const now = Date.now();
|
|
3493
|
+
const cached = this.cache.get(hostname);
|
|
3494
|
+
if (cached && cached.expiresAt > now) {
|
|
3495
|
+
return cached.cert;
|
|
1649
3496
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
3497
|
+
const cert = this.generate(hostname);
|
|
3498
|
+
if (this.cache.size >= MAX_CACHE_SIZE) {
|
|
3499
|
+
const oldest = this.cache.keys().next().value;
|
|
3500
|
+
if (oldest) this.cache.delete(oldest);
|
|
1652
3501
|
}
|
|
1653
|
-
this.
|
|
3502
|
+
this.cache.set(hostname, { cert, expiresAt: now + CERT_TTL_MS });
|
|
3503
|
+
return cert;
|
|
1654
3504
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
const stats = statSync(this.logFile);
|
|
1658
|
-
if (stats.size <= MAX_LOG_SIZE) return;
|
|
1659
|
-
for (let i = MAX_BACKUPS - 1; i >= 1; i--) {
|
|
1660
|
-
const from = `${this.logFile}.${i}`;
|
|
1661
|
-
const to = `${this.logFile}.${i + 1}`;
|
|
1662
|
-
if (existsSync3(from)) renameSync(from, to);
|
|
1663
|
-
}
|
|
1664
|
-
renameSync(this.logFile, `${this.logFile}.1`);
|
|
1665
|
-
} catch {
|
|
1666
|
-
}
|
|
3505
|
+
get cacheSize() {
|
|
3506
|
+
return this.cache.size;
|
|
1667
3507
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
3508
|
+
generate(hostname) {
|
|
3509
|
+
const keys = forge2.pki.rsa.generateKeyPair(2048);
|
|
3510
|
+
const cert = forge2.pki.createCertificate();
|
|
3511
|
+
cert.publicKey = keys.publicKey;
|
|
3512
|
+
cert.serialNumber = randomSerial();
|
|
3513
|
+
cert.validity.notBefore = new Date(Date.now() - 24 * 60 * 60 * 1e3);
|
|
3514
|
+
cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1e3);
|
|
3515
|
+
cert.setSubject([
|
|
3516
|
+
{ name: "commonName", value: hostname },
|
|
3517
|
+
{ name: "organizationName", value: "Liminal Proxy (local)" }
|
|
3518
|
+
]);
|
|
3519
|
+
cert.setIssuer(this.caCert.subject.attributes);
|
|
3520
|
+
cert.setExtensions([
|
|
3521
|
+
{ name: "basicConstraints", cA: false },
|
|
3522
|
+
{
|
|
3523
|
+
name: "keyUsage",
|
|
3524
|
+
digitalSignature: true,
|
|
3525
|
+
keyEncipherment: true,
|
|
3526
|
+
critical: true
|
|
3527
|
+
},
|
|
3528
|
+
{
|
|
3529
|
+
name: "extKeyUsage",
|
|
3530
|
+
serverAuth: true
|
|
3531
|
+
},
|
|
3532
|
+
{
|
|
3533
|
+
name: "subjectAltName",
|
|
3534
|
+
altNames: [{ type: 2, value: hostname }]
|
|
3535
|
+
// DNS name
|
|
3536
|
+
}
|
|
3537
|
+
]);
|
|
3538
|
+
cert.sign(this.caKey, forge2.md.sha256.create());
|
|
3539
|
+
return {
|
|
3540
|
+
certPem: forge2.pki.certificateToPem(cert),
|
|
3541
|
+
keyPem: forge2.pki.privateKeyToPem(keys.privateKey)
|
|
3542
|
+
};
|
|
1670
3543
|
}
|
|
1671
3544
|
};
|
|
3545
|
+
function randomSerial() {
|
|
3546
|
+
return forge2.util.bytesToHex(forge2.random.getBytesSync(16));
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// src/tls/mitm-bridge.ts
|
|
3550
|
+
function createMitmBridge(options) {
|
|
3551
|
+
const { httpServer, caCertPem, caKeyPem, logger } = options;
|
|
3552
|
+
const certGen = new CertGenerator(caCertPem, caKeyPem);
|
|
3553
|
+
return (clientSocket, hostname, _port) => {
|
|
3554
|
+
try {
|
|
3555
|
+
const { certPem, keyPem } = certGen.getCert(hostname);
|
|
3556
|
+
const tlsSocket = new tls.TLSSocket(clientSocket, {
|
|
3557
|
+
isServer: true,
|
|
3558
|
+
key: keyPem,
|
|
3559
|
+
cert: certPem
|
|
3560
|
+
});
|
|
3561
|
+
tlsSocket.on("error", (err) => {
|
|
3562
|
+
logger.log(`[MITM] TLS error for ${hostname}: ${err.message}`);
|
|
3563
|
+
tlsSocket.destroy();
|
|
3564
|
+
});
|
|
3565
|
+
httpServer.emit("connection", tlsSocket);
|
|
3566
|
+
logger.log(`[MITM] TLS bridge established for ${hostname} (cert cache: ${certGen.cacheSize})`);
|
|
3567
|
+
} catch (err) {
|
|
3568
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3569
|
+
logger.log(`[MITM] Failed to establish bridge for ${hostname}: ${message}`);
|
|
3570
|
+
clientSocket.destroy();
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
1672
3574
|
|
|
1673
3575
|
// src/daemon/lifecycle.ts
|
|
1674
|
-
import { readFileSync as
|
|
3576
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
1675
3577
|
import { fork } from "child_process";
|
|
1676
3578
|
import { fileURLToPath } from "url";
|
|
1677
3579
|
function writePidFile(pid) {
|
|
1678
|
-
|
|
3580
|
+
writeFileSync4(PID_FILE, String(pid), "utf-8");
|
|
1679
3581
|
}
|
|
1680
3582
|
function readPidFile() {
|
|
1681
|
-
if (!
|
|
3583
|
+
if (!existsSync8(PID_FILE)) return null;
|
|
1682
3584
|
try {
|
|
1683
|
-
const content =
|
|
3585
|
+
const content = readFileSync5(PID_FILE, "utf-8").trim();
|
|
1684
3586
|
const pid = parseInt(content, 10);
|
|
1685
3587
|
return isNaN(pid) ? null : pid;
|
|
1686
3588
|
} catch {
|
|
@@ -1689,7 +3591,7 @@ function readPidFile() {
|
|
|
1689
3591
|
}
|
|
1690
3592
|
function removePidFile() {
|
|
1691
3593
|
try {
|
|
1692
|
-
if (
|
|
3594
|
+
if (existsSync8(PID_FILE)) unlinkSync3(PID_FILE);
|
|
1693
3595
|
} catch {
|
|
1694
3596
|
}
|
|
1695
3597
|
}
|
|
@@ -1791,37 +3693,81 @@ async function startCommand(flags) {
|
|
|
1791
3693
|
rscBaseUrl: config.apiBaseUrl,
|
|
1792
3694
|
proxyPort: config.port,
|
|
1793
3695
|
compressionThreshold: config.compressionThreshold,
|
|
3696
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
3697
|
+
hotFraction: config.hotFraction,
|
|
3698
|
+
coldFraction: config.coldFraction,
|
|
1794
3699
|
compressRoles: config.compressRoles,
|
|
3700
|
+
compressToolResults: config.compressToolResults,
|
|
1795
3701
|
learnFromResponses: config.learnFromResponses,
|
|
1796
3702
|
latencyBudgetMs: config.latencyBudgetMs || void 0,
|
|
1797
3703
|
upstreamBaseUrl: config.upstreamBaseUrl,
|
|
1798
3704
|
anthropicUpstreamUrl: config.anthropicUpstreamUrl,
|
|
1799
3705
|
enabled: config.enabled,
|
|
1800
|
-
tools: config.tools
|
|
3706
|
+
tools: config.tools,
|
|
3707
|
+
concurrencyLimit: config.concurrencyLimit,
|
|
3708
|
+
concurrencyTimeoutMs: config.concurrencyTimeoutMs,
|
|
3709
|
+
maxSessions: config.maxSessions,
|
|
3710
|
+
sessionTtlMs: config.sessionTtlMs,
|
|
3711
|
+
latencyWarningMs: config.latencyWarningMs,
|
|
3712
|
+
latencyCriticalMs: config.latencyCriticalMs
|
|
1801
3713
|
};
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
learnFromResponses: config.learnFromResponses,
|
|
1807
|
-
latencyBudgetMs: config.latencyBudgetMs || void 0
|
|
3714
|
+
const semaphore = new Semaphore(resolvedConfig.concurrencyLimit);
|
|
3715
|
+
const latencyMonitor = new LatencyMonitor({
|
|
3716
|
+
warningThresholdMs: resolvedConfig.latencyWarningMs,
|
|
3717
|
+
criticalThresholdMs: resolvedConfig.latencyCriticalMs
|
|
1808
3718
|
});
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
3719
|
+
function wireSessionEvents(key, pipeline) {
|
|
3720
|
+
pipeline.events.on("compression", (event) => {
|
|
3721
|
+
if (event.tokensSaved > 0) {
|
|
3722
|
+
logger.log(`[LIMINAL] [${key}] Compressed: ${event.tokensSaved} tokens saved (${event.ratio.toFixed(3)} ratio)`);
|
|
3723
|
+
}
|
|
3724
|
+
});
|
|
3725
|
+
pipeline.events.on("compression_skipped", (event) => {
|
|
3726
|
+
const detail = event.reason === "latency_budget" ? `${event.reason} (${event.estimatedTokens}tok, budget:${event.budgetMs}ms, elapsed:${event.elapsedMs}ms)` : event.reason === "below_threshold" ? `${event.reason} (${event.estimatedTokens}tok < ${config.compressionThreshold}tok threshold)` : `${event.reason} (${event.estimatedTokens}tok)`;
|
|
3727
|
+
logger.log(`[LIMINAL] [${key}] Skipped: ${detail}`);
|
|
3728
|
+
});
|
|
3729
|
+
pipeline.events.on("error", (event) => {
|
|
3730
|
+
logger.log(`[LIMINAL] [${key}] Error: ${event.error.message}`);
|
|
3731
|
+
});
|
|
3732
|
+
pipeline.events.on("degradation", (event) => {
|
|
3733
|
+
logger.log(`[LIMINAL] [${key}] Circuit ${event.circuitState}: ${event.reason}`);
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
const sessions = new SessionManager({
|
|
3737
|
+
pipelineConfig: {
|
|
3738
|
+
rscApiKey: config.apiKey,
|
|
3739
|
+
rscBaseUrl: config.apiBaseUrl,
|
|
3740
|
+
compressionThreshold: config.compressionThreshold,
|
|
3741
|
+
learnFromResponses: config.learnFromResponses,
|
|
3742
|
+
latencyBudgetMs: config.latencyBudgetMs || void 0
|
|
3743
|
+
},
|
|
3744
|
+
maxSessions: resolvedConfig.maxSessions,
|
|
3745
|
+
sessionTtlMs: resolvedConfig.sessionTtlMs,
|
|
3746
|
+
onSessionCreated: (key, pipeline) => {
|
|
3747
|
+
logger.log(`[SESSION] Created: ${key}`);
|
|
3748
|
+
wireSessionEvents(key, pipeline);
|
|
3749
|
+
},
|
|
3750
|
+
onSessionEvicted: (key) => {
|
|
3751
|
+
logger.log(`[SESSION] Evicted: ${key} (idle)`);
|
|
1812
3752
|
}
|
|
1813
3753
|
});
|
|
1814
|
-
|
|
1815
|
-
logger.log(`[
|
|
1816
|
-
});
|
|
1817
|
-
pipeline.events.on("error", (event) => {
|
|
1818
|
-
logger.log(`[LIMINAL] Error: ${event.error.message}`);
|
|
3754
|
+
latencyMonitor.onAlert((alert) => {
|
|
3755
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message} (${alert.activeSessions} sessions) \u2014 ${alert.suggestion}`);
|
|
1819
3756
|
});
|
|
1820
|
-
|
|
1821
|
-
|
|
3757
|
+
const deps = { sessions, semaphore, latencyMonitor, config: resolvedConfig, logger };
|
|
3758
|
+
const handler = createRequestHandler(deps);
|
|
3759
|
+
let mitmHandler;
|
|
3760
|
+
const connectHandler = createConnectHandler({
|
|
3761
|
+
logger,
|
|
3762
|
+
onIntercept: (socket, hostname, port) => {
|
|
3763
|
+
if (mitmHandler) {
|
|
3764
|
+
mitmHandler(socket, hostname, port);
|
|
3765
|
+
} else {
|
|
3766
|
+
logger.log(`[MITM] No bridge available for ${hostname} \u2014 falling back to passthrough`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
1822
3769
|
});
|
|
1823
|
-
const
|
|
1824
|
-
const server = new ProxyServer(config.port, handler);
|
|
3770
|
+
const server = new ProxyServer(config.port, handler, connectHandler);
|
|
1825
3771
|
setupSignalHandlers(server, logger);
|
|
1826
3772
|
try {
|
|
1827
3773
|
const actualPort = await server.start();
|
|
@@ -1831,15 +3777,42 @@ async function startCommand(flags) {
|
|
|
1831
3777
|
logger.log(`[DAEMON] Upstream (Anthropic): ${config.anthropicUpstreamUrl}`);
|
|
1832
3778
|
logger.log(`[DAEMON] Liminal API: ${config.apiBaseUrl}`);
|
|
1833
3779
|
logger.log(`[DAEMON] PID: ${process.pid}`);
|
|
3780
|
+
logger.log(`[DAEMON] Max sessions: ${resolvedConfig.maxSessions}, Concurrency limit: ${resolvedConfig.concurrencyLimit}`);
|
|
3781
|
+
const caReady = hasCA() && isCATrusted();
|
|
3782
|
+
if (caReady) {
|
|
3783
|
+
const httpServer = server.getHttpServer();
|
|
3784
|
+
const ca = loadCA();
|
|
3785
|
+
if (httpServer && ca) {
|
|
3786
|
+
mitmHandler = createMitmBridge({
|
|
3787
|
+
httpServer,
|
|
3788
|
+
caCertPem: ca.certPem,
|
|
3789
|
+
caKeyPem: ca.keyPem,
|
|
3790
|
+
logger
|
|
3791
|
+
});
|
|
3792
|
+
logger.log("[MITM] TLS bridge active \u2014 intercepting LLM API calls");
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
logger.log(`[DAEMON] CONNECT handler: active | MITM: ${caReady ? "ready (CA trusted)" : "passthrough only (run liminal trust-ca)"}`);
|
|
1834
3796
|
if (isForeground && !isForked) {
|
|
1835
3797
|
printBanner();
|
|
1836
3798
|
console.log(` Liminal proxy running on http://127.0.0.1:${actualPort}/v1`);
|
|
1837
3799
|
console.log(` Upstream: ${config.upstreamBaseUrl}`);
|
|
3800
|
+
console.log(` Max sessions: ${resolvedConfig.maxSessions} | Concurrency: ${resolvedConfig.concurrencyLimit}`);
|
|
3801
|
+
if (caReady) {
|
|
3802
|
+
console.log(" MITM: active (Cursor interception ready)");
|
|
3803
|
+
}
|
|
1838
3804
|
console.log();
|
|
1839
3805
|
console.log(" Point your AI tool's base URL here. Press Ctrl+C to stop.");
|
|
1840
3806
|
console.log();
|
|
1841
3807
|
}
|
|
1842
|
-
const
|
|
3808
|
+
const { RSCPipelineWrapper: RSCPipelineWrapper2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
3809
|
+
const probe = new RSCPipelineWrapper2({
|
|
3810
|
+
rscApiKey: config.apiKey,
|
|
3811
|
+
rscBaseUrl: config.apiBaseUrl,
|
|
3812
|
+
compressionThreshold: config.compressionThreshold,
|
|
3813
|
+
learnFromResponses: false
|
|
3814
|
+
});
|
|
3815
|
+
const healthy = await probe.healthCheck();
|
|
1843
3816
|
if (healthy) {
|
|
1844
3817
|
logger.log("[DAEMON] Liminal API health check: OK");
|
|
1845
3818
|
} else {
|
|
@@ -1906,19 +3879,39 @@ async function statusCommand() {
|
|
|
1906
3879
|
const data = await res.json();
|
|
1907
3880
|
const uptime = formatUptime(data.uptime_ms);
|
|
1908
3881
|
console.log(`Liminal Daemon: running (PID ${state.pid}, port ${port})`);
|
|
1909
|
-
console.log(`
|
|
1910
|
-
console.log(`Session: ${data.session_id}`);
|
|
3882
|
+
console.log(`Status: ${data.status} (${data.version})`);
|
|
1911
3883
|
console.log(`Uptime: ${uptime}`);
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
3884
|
+
console.log();
|
|
3885
|
+
const c = data.concurrency;
|
|
3886
|
+
console.log(`Sessions: ${c.active_sessions} active (max ${config.maxSessions})`);
|
|
3887
|
+
console.log(`Semaphore: ${c.semaphore_available}/${c.max_concurrent_rsc_calls} available` + (c.semaphore_waiting > 0 ? ` (${c.semaphore_waiting} waiting)` : ""));
|
|
3888
|
+
const globalP95 = data.latency.global_p95_ms;
|
|
3889
|
+
if (globalP95 !== null) {
|
|
3890
|
+
const latencyFlag = globalP95 >= config.latencyCriticalMs ? " CRITICAL" : globalP95 >= config.latencyWarningMs ? " WARNING" : "";
|
|
3891
|
+
console.log(`Latency: p95 ${globalP95.toFixed(0)}ms${latencyFlag}`);
|
|
3892
|
+
}
|
|
3893
|
+
if (data.sessions.length > 0) {
|
|
3894
|
+
console.log();
|
|
3895
|
+
console.log("\u2500\u2500\u2500 Sessions \u2500\u2500\u2500");
|
|
3896
|
+
for (const s of data.sessions) {
|
|
3897
|
+
const savingsPercent = s.tokens_processed > 0 ? (s.tokens_saved / s.tokens_processed * 100).toFixed(1) : "0.0";
|
|
3898
|
+
const activeAgo = formatUptime(s.last_active_ago_ms);
|
|
3899
|
+
const p95 = s.p95_latency_ms !== null ? `${s.p95_latency_ms.toFixed(0)}ms` : "-";
|
|
3900
|
+
console.log();
|
|
3901
|
+
console.log(` ${s.connector} [${s.session_key}]`);
|
|
3902
|
+
console.log(` Circuit: ${s.circuit_state}`);
|
|
3903
|
+
console.log(` Tokens: ${s.tokens_processed.toLocaleString()} processed, ${s.tokens_saved.toLocaleString()} saved (${savingsPercent}%)`);
|
|
3904
|
+
console.log(` Calls: ${s.calls_total} total (${s.calls_compressed} compressed, ${s.calls_failed} failed)`);
|
|
3905
|
+
console.log(` Latency: p95 ${p95}`);
|
|
3906
|
+
console.log(` Active: ${activeAgo} ago`);
|
|
3907
|
+
}
|
|
3908
|
+
} else {
|
|
1915
3909
|
console.log();
|
|
1916
|
-
console.log(
|
|
1917
|
-
console.log(`Calls: ${s.calls_total} total (${s.calls_compressed} compressed, ${s.calls_skipped} skipped, ${s.calls_failed} failed)`);
|
|
3910
|
+
console.log("No active sessions.");
|
|
1918
3911
|
}
|
|
1919
3912
|
} catch {
|
|
1920
3913
|
console.log(`Liminal Daemon: running (PID ${state.pid}, port ${port})`);
|
|
1921
|
-
console.log("
|
|
3914
|
+
console.log("Status: unknown (could not reach /health)");
|
|
1922
3915
|
}
|
|
1923
3916
|
}
|
|
1924
3917
|
function formatUptime(ms) {
|
|
@@ -2070,17 +4063,17 @@ async function configCommand(flags) {
|
|
|
2070
4063
|
}
|
|
2071
4064
|
|
|
2072
4065
|
// src/commands/logs.ts
|
|
2073
|
-
import { readFileSync as
|
|
4066
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync2, createReadStream } from "fs";
|
|
2074
4067
|
import { watchFile, unwatchFile } from "fs";
|
|
2075
4068
|
async function logsCommand(flags) {
|
|
2076
4069
|
const follow = flags.has("follow") || flags.has("f");
|
|
2077
4070
|
const linesFlag = flags.get("lines") ?? flags.get("n");
|
|
2078
4071
|
const lines = typeof linesFlag === "string" ? parseInt(linesFlag, 10) : 50;
|
|
2079
|
-
if (!
|
|
4072
|
+
if (!existsSync9(LOG_FILE)) {
|
|
2080
4073
|
console.log('No log file found. Start the daemon with "liminal start" to generate logs.');
|
|
2081
4074
|
return;
|
|
2082
4075
|
}
|
|
2083
|
-
const content =
|
|
4076
|
+
const content = readFileSync6(LOG_FILE, "utf-8");
|
|
2084
4077
|
const allLines = content.split("\n");
|
|
2085
4078
|
const tail = allLines.slice(-lines - 1);
|
|
2086
4079
|
process.stdout.write(tail.join("\n"));
|
|
@@ -2105,6 +4098,219 @@ async function logsCommand(flags) {
|
|
|
2105
4098
|
});
|
|
2106
4099
|
}
|
|
2107
4100
|
|
|
4101
|
+
// src/commands/uninstall.ts
|
|
4102
|
+
import { existsSync as existsSync10, rmSync, readFileSync as readFileSync7 } from "fs";
|
|
4103
|
+
var BOLD2 = "\x1B[1m";
|
|
4104
|
+
var DIM2 = "\x1B[2m";
|
|
4105
|
+
var GREEN2 = "\x1B[32m";
|
|
4106
|
+
var YELLOW2 = "\x1B[33m";
|
|
4107
|
+
var RESET2 = "\x1B[0m";
|
|
4108
|
+
function loadConfiguredTools() {
|
|
4109
|
+
if (!existsSync10(CONFIG_FILE)) return [];
|
|
4110
|
+
try {
|
|
4111
|
+
const raw = readFileSync7(CONFIG_FILE, "utf-8");
|
|
4112
|
+
const config = JSON.parse(raw);
|
|
4113
|
+
if (Array.isArray(config.tools)) return config.tools;
|
|
4114
|
+
} catch {
|
|
4115
|
+
}
|
|
4116
|
+
return [];
|
|
4117
|
+
}
|
|
4118
|
+
async function uninstallCommand() {
|
|
4119
|
+
console.log();
|
|
4120
|
+
console.log(` ${BOLD2}Liminal Uninstall${RESET2}`);
|
|
4121
|
+
console.log();
|
|
4122
|
+
const confirm = await selectPrompt({
|
|
4123
|
+
message: "Remove Liminal configuration and restore tool settings?",
|
|
4124
|
+
options: [
|
|
4125
|
+
{ label: "Yes", value: true, description: "Undo all Liminal setup" },
|
|
4126
|
+
{ label: "No", value: false, description: "Cancel" }
|
|
4127
|
+
],
|
|
4128
|
+
defaultIndex: 1
|
|
4129
|
+
// Default to No for safety
|
|
4130
|
+
});
|
|
4131
|
+
if (confirm !== true) {
|
|
4132
|
+
console.log();
|
|
4133
|
+
console.log(" Cancelled.");
|
|
4134
|
+
console.log();
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
console.log();
|
|
4138
|
+
const state = isDaemonRunning();
|
|
4139
|
+
if (state.running && state.pid) {
|
|
4140
|
+
console.log(` Stopping Liminal daemon (PID ${state.pid})...`);
|
|
4141
|
+
try {
|
|
4142
|
+
process.kill(state.pid, "SIGTERM");
|
|
4143
|
+
for (let i = 0; i < 15; i++) {
|
|
4144
|
+
await sleep(200);
|
|
4145
|
+
if (!isProcessAlive(state.pid)) break;
|
|
4146
|
+
}
|
|
4147
|
+
if (isProcessAlive(state.pid)) {
|
|
4148
|
+
process.kill(state.pid, "SIGKILL");
|
|
4149
|
+
}
|
|
4150
|
+
} catch {
|
|
4151
|
+
}
|
|
4152
|
+
removePidFile();
|
|
4153
|
+
console.log(` ${GREEN2}\u2713${RESET2} Daemon stopped`);
|
|
4154
|
+
} else {
|
|
4155
|
+
console.log(` ${DIM2}\xB7${RESET2} Daemon not running`);
|
|
4156
|
+
}
|
|
4157
|
+
const profile = detectShellProfile();
|
|
4158
|
+
if (profile) {
|
|
4159
|
+
const existing = findLiminalExportsInProfile(profile);
|
|
4160
|
+
if (existing.length > 0) {
|
|
4161
|
+
const removed = removeLiminalFromShellProfile(profile);
|
|
4162
|
+
if (removed.length > 0) {
|
|
4163
|
+
console.log(` ${GREEN2}\u2713${RESET2} Removed ${removed.length} line${removed.length > 1 ? "s" : ""} from ${profile.name}:`);
|
|
4164
|
+
for (const line of removed) {
|
|
4165
|
+
const trimmed = line.trim();
|
|
4166
|
+
if (trimmed && trimmed !== "# Liminal \u2014 route AI tools through compression proxy") {
|
|
4167
|
+
console.log(` ${DIM2}${trimmed}${RESET2}`);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
} else {
|
|
4172
|
+
console.log(` ${DIM2}\xB7${RESET2} No Liminal exports found in ${profile.name}`);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
const configuredTools = loadConfiguredTools();
|
|
4176
|
+
const allTools = configuredTools.length > 0 ? configuredTools : CONNECTORS.map((c) => c.info.id);
|
|
4177
|
+
const connectors = getConnectors(allTools);
|
|
4178
|
+
const manualSteps = [];
|
|
4179
|
+
for (const connector of connectors) {
|
|
4180
|
+
const result = await connector.teardown();
|
|
4181
|
+
if (result.manualSteps.length > 0 && !connector.info.automatable) {
|
|
4182
|
+
manualSteps.push({
|
|
4183
|
+
label: connector.info.label,
|
|
4184
|
+
steps: result.manualSteps
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
if (manualSteps.length > 0) {
|
|
4189
|
+
console.log();
|
|
4190
|
+
console.log(` ${YELLOW2}Manual steps needed:${RESET2}`);
|
|
4191
|
+
for (const { label, steps } of manualSteps) {
|
|
4192
|
+
console.log();
|
|
4193
|
+
console.log(` ${BOLD2}${label}:${RESET2}`);
|
|
4194
|
+
for (const step of steps) {
|
|
4195
|
+
console.log(` ${step}`);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
if (existsSync10(LIMINAL_DIR)) {
|
|
4200
|
+
console.log();
|
|
4201
|
+
const removeData = await selectPrompt({
|
|
4202
|
+
message: "Remove ~/.liminal/ directory? (config, logs, PID file)",
|
|
4203
|
+
options: [
|
|
4204
|
+
{ label: "Yes", value: true, description: "Delete all Liminal data" },
|
|
4205
|
+
{ label: "No", value: false, description: "Keep config and logs" }
|
|
4206
|
+
],
|
|
4207
|
+
defaultIndex: 1
|
|
4208
|
+
// Default to keep
|
|
4209
|
+
});
|
|
4210
|
+
if (removeData === true) {
|
|
4211
|
+
rmSync(LIMINAL_DIR, { recursive: true, force: true });
|
|
4212
|
+
console.log(` ${GREEN2}\u2713${RESET2} Removed ~/.liminal/`);
|
|
4213
|
+
} else {
|
|
4214
|
+
console.log(` ${DIM2}\xB7${RESET2} Kept ~/.liminal/`);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
console.log();
|
|
4218
|
+
console.log(` ${GREEN2}Liminal has been uninstalled.${RESET2}`);
|
|
4219
|
+
console.log();
|
|
4220
|
+
console.log(` ${DIM2}Your AI tools will connect directly to their APIs.${RESET2}`);
|
|
4221
|
+
console.log(` ${DIM2}Restart your terminal for shell changes to take effect.${RESET2}`);
|
|
4222
|
+
if (manualSteps.length > 0) {
|
|
4223
|
+
console.log(` ${YELLOW2}Don't forget the manual steps above for ${manualSteps.map((s) => s.label).join(", ")}.${RESET2}`);
|
|
4224
|
+
}
|
|
4225
|
+
console.log();
|
|
4226
|
+
console.log(` ${DIM2}To reinstall: npx @cognisos/liminal init${RESET2}`);
|
|
4227
|
+
console.log();
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
// src/commands/trust-ca.ts
|
|
4231
|
+
async function trustCACommand() {
|
|
4232
|
+
printBanner();
|
|
4233
|
+
if (isCATrusted()) {
|
|
4234
|
+
console.log(" Liminal CA is already trusted.");
|
|
4235
|
+
const info = getCAInfo();
|
|
4236
|
+
if (info) {
|
|
4237
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4238
|
+
console.log(` Valid until: ${info.validTo.toLocaleDateString()}`);
|
|
4239
|
+
}
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
if (!hasCA()) {
|
|
4243
|
+
console.log(" Generating CA certificate...");
|
|
4244
|
+
ensureCA();
|
|
4245
|
+
console.log(" Created ~/.liminal/ca.pem");
|
|
4246
|
+
console.log();
|
|
4247
|
+
}
|
|
4248
|
+
console.log(" Installing Liminal CA certificate");
|
|
4249
|
+
console.log();
|
|
4250
|
+
console.log(" This allows Liminal to transparently compress LLM API");
|
|
4251
|
+
console.log(" traffic from Cursor and other Electron-based editors.");
|
|
4252
|
+
console.log();
|
|
4253
|
+
console.log(" The certificate is scoped to your user account and only");
|
|
4254
|
+
console.log(" used by the local Liminal proxy on 127.0.0.1.");
|
|
4255
|
+
console.log();
|
|
4256
|
+
console.log(` Certificate: ${CA_CERT_PATH}`);
|
|
4257
|
+
console.log();
|
|
4258
|
+
const result = installCA();
|
|
4259
|
+
if (result.success) {
|
|
4260
|
+
console.log(` ${result.message}`);
|
|
4261
|
+
console.log();
|
|
4262
|
+
const info = getCAInfo();
|
|
4263
|
+
if (info) {
|
|
4264
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4265
|
+
console.log(` Valid until: ${info.validTo.toLocaleDateString()}`);
|
|
4266
|
+
}
|
|
4267
|
+
console.log();
|
|
4268
|
+
console.log(" You can now use Liminal with Cursor:");
|
|
4269
|
+
console.log(" liminal start");
|
|
4270
|
+
console.log(" cursor --proxy-server=http://127.0.0.1:3141 --disable-http2");
|
|
4271
|
+
} else {
|
|
4272
|
+
console.error(` Failed: ${result.message}`);
|
|
4273
|
+
if (result.requiresSudo) {
|
|
4274
|
+
console.log();
|
|
4275
|
+
console.log(" Try running with elevated permissions:");
|
|
4276
|
+
console.log(" sudo liminal trust-ca");
|
|
4277
|
+
}
|
|
4278
|
+
process.exit(1);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
// src/commands/untrust-ca.ts
|
|
4283
|
+
async function untrustCACommand() {
|
|
4284
|
+
printBanner();
|
|
4285
|
+
if (!hasCA() && !isCATrusted()) {
|
|
4286
|
+
console.log(" No Liminal CA found (nothing to remove).");
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
if (isCATrusted()) {
|
|
4290
|
+
console.log(" Removing CA from system trust store...");
|
|
4291
|
+
const result = removeCA2();
|
|
4292
|
+
if (result.success) {
|
|
4293
|
+
console.log(` ${result.message}`);
|
|
4294
|
+
} else {
|
|
4295
|
+
console.error(` ${result.message}`);
|
|
4296
|
+
if (result.requiresSudo) {
|
|
4297
|
+
console.log();
|
|
4298
|
+
console.log(" Try running with elevated permissions:");
|
|
4299
|
+
console.log(" sudo liminal untrust-ca");
|
|
4300
|
+
}
|
|
4301
|
+
process.exit(1);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
if (hasCA()) {
|
|
4305
|
+
console.log(" Removing CA certificate files...");
|
|
4306
|
+
removeCA();
|
|
4307
|
+
console.log(" Removed ~/.liminal/ca.pem and ca-key.pem");
|
|
4308
|
+
}
|
|
4309
|
+
console.log();
|
|
4310
|
+
console.log(" Liminal CA fully removed.");
|
|
4311
|
+
console.log(" Cursor MITM interception is no longer available.");
|
|
4312
|
+
}
|
|
4313
|
+
|
|
2108
4314
|
// src/bin.ts
|
|
2109
4315
|
var USAGE = `
|
|
2110
4316
|
liminal v${VERSION} \u2014 Transparent LLM context compression proxy
|
|
@@ -2119,6 +4325,9 @@ var USAGE = `
|
|
|
2119
4325
|
liminal summary Detailed session metrics
|
|
2120
4326
|
liminal config [--set k=v] [--get k] View or edit configuration
|
|
2121
4327
|
liminal logs [--follow] [--lines N] View proxy logs
|
|
4328
|
+
liminal trust-ca Install CA cert (for Cursor MITM)
|
|
4329
|
+
liminal untrust-ca Remove CA cert
|
|
4330
|
+
liminal uninstall Remove Liminal configuration
|
|
2122
4331
|
|
|
2123
4332
|
Options:
|
|
2124
4333
|
-h, --help Show this help message
|
|
@@ -2130,7 +4339,7 @@ var USAGE = `
|
|
|
2130
4339
|
3. Connect your AI tools:
|
|
2131
4340
|
Claude Code: export ANTHROPIC_BASE_URL=http://localhost:3141
|
|
2132
4341
|
Codex: export OPENAI_BASE_URL=http://localhost:3141/v1
|
|
2133
|
-
Cursor:
|
|
4342
|
+
Cursor: liminal trust-ca && cursor --proxy-server=http://localhost:3141
|
|
2134
4343
|
`;
|
|
2135
4344
|
function parseArgs(argv) {
|
|
2136
4345
|
const command = argv[2] ?? "";
|
|
@@ -2196,6 +4405,15 @@ async function main() {
|
|
|
2196
4405
|
case "logs":
|
|
2197
4406
|
await logsCommand(flags);
|
|
2198
4407
|
break;
|
|
4408
|
+
case "trust-ca":
|
|
4409
|
+
await trustCACommand();
|
|
4410
|
+
break;
|
|
4411
|
+
case "untrust-ca":
|
|
4412
|
+
await untrustCACommand();
|
|
4413
|
+
break;
|
|
4414
|
+
case "uninstall":
|
|
4415
|
+
await uninstallCommand();
|
|
4416
|
+
break;
|
|
2199
4417
|
case "":
|
|
2200
4418
|
console.log(USAGE);
|
|
2201
4419
|
process.exit(0);
|