@cognisos/liminal 2.4.0 → 2.4.2
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 +1710 -258
- 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.4.
|
|
81
|
+
var VERSION = true ? "2.4.2" : "0.2.1";
|
|
5
82
|
var BANNER_LINES = [
|
|
6
83
|
" ___ ___ _____ ______ ___ ________ ________ ___",
|
|
7
84
|
"|\\ \\ |\\ \\|\\ _ \\ _ \\|\\ \\|\\ ___ \\|\\ __ \\|\\ \\",
|
|
@@ -41,11 +118,21 @@ var DEFAULTS = {
|
|
|
41
118
|
anthropicUpstreamUrl: "https://api.anthropic.com",
|
|
42
119
|
port: 3141,
|
|
43
120
|
compressionThreshold: 100,
|
|
44
|
-
|
|
121
|
+
aggregateThreshold: 500,
|
|
122
|
+
hotFraction: 0.3,
|
|
123
|
+
coldFraction: 0.3,
|
|
124
|
+
compressRoles: ["user", "assistant"],
|
|
125
|
+
compressToolResults: true,
|
|
45
126
|
learnFromResponses: true,
|
|
46
|
-
latencyBudgetMs:
|
|
127
|
+
latencyBudgetMs: 1e4,
|
|
47
128
|
enabled: true,
|
|
48
|
-
tools: []
|
|
129
|
+
tools: [],
|
|
130
|
+
concurrencyLimit: 6,
|
|
131
|
+
concurrencyTimeoutMs: 15e3,
|
|
132
|
+
maxSessions: 10,
|
|
133
|
+
sessionTtlMs: 18e5,
|
|
134
|
+
latencyWarningMs: 4e3,
|
|
135
|
+
latencyCriticalMs: 8e3
|
|
49
136
|
};
|
|
50
137
|
var CONFIGURABLE_KEYS = /* @__PURE__ */ new Set([
|
|
51
138
|
"apiBaseUrl",
|
|
@@ -53,11 +140,21 @@ var CONFIGURABLE_KEYS = /* @__PURE__ */ new Set([
|
|
|
53
140
|
"anthropicUpstreamUrl",
|
|
54
141
|
"port",
|
|
55
142
|
"compressionThreshold",
|
|
143
|
+
"aggregateThreshold",
|
|
144
|
+
"hotFraction",
|
|
145
|
+
"coldFraction",
|
|
56
146
|
"compressRoles",
|
|
147
|
+
"compressToolResults",
|
|
57
148
|
"learnFromResponses",
|
|
58
149
|
"latencyBudgetMs",
|
|
59
150
|
"enabled",
|
|
60
|
-
"tools"
|
|
151
|
+
"tools",
|
|
152
|
+
"concurrencyLimit",
|
|
153
|
+
"concurrencyTimeoutMs",
|
|
154
|
+
"maxSessions",
|
|
155
|
+
"sessionTtlMs",
|
|
156
|
+
"latencyWarningMs",
|
|
157
|
+
"latencyCriticalMs"
|
|
61
158
|
]);
|
|
62
159
|
|
|
63
160
|
// src/config/loader.ts
|
|
@@ -77,11 +174,21 @@ function loadConfig() {
|
|
|
77
174
|
anthropicUpstreamUrl: DEFAULTS.anthropicUpstreamUrl,
|
|
78
175
|
port: DEFAULTS.port,
|
|
79
176
|
compressionThreshold: DEFAULTS.compressionThreshold,
|
|
177
|
+
aggregateThreshold: DEFAULTS.aggregateThreshold,
|
|
178
|
+
hotFraction: DEFAULTS.hotFraction,
|
|
179
|
+
coldFraction: DEFAULTS.coldFraction,
|
|
80
180
|
compressRoles: DEFAULTS.compressRoles,
|
|
181
|
+
compressToolResults: DEFAULTS.compressToolResults,
|
|
81
182
|
learnFromResponses: DEFAULTS.learnFromResponses,
|
|
82
183
|
latencyBudgetMs: DEFAULTS.latencyBudgetMs,
|
|
83
184
|
enabled: DEFAULTS.enabled,
|
|
84
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,
|
|
85
192
|
...fileConfig
|
|
86
193
|
};
|
|
87
194
|
if (process.env.LIMINAL_API_KEY) merged.apiKey = process.env.LIMINAL_API_KEY;
|
|
@@ -485,7 +592,7 @@ import { createInterface } from "readline/promises";
|
|
|
485
592
|
import { stdin, stdout } from "process";
|
|
486
593
|
|
|
487
594
|
// src/auth/supabase.ts
|
|
488
|
-
import { randomBytes } from "crypto";
|
|
595
|
+
import { randomBytes, createHash } from "crypto";
|
|
489
596
|
var SUPABASE_URL = "https://nzcneiyymvgxvttbenhp.supabase.co";
|
|
490
597
|
var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im56Y25laXl5bXZneHZ0dGJlbmhwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQwNjQ0MjcsImV4cCI6MjA2OTY0MDQyN30.x3E-zGRadbPMmxRqT_PB_KOi00htKpgeb8GiQa4g2z0";
|
|
491
598
|
function supabaseHeaders(accessToken) {
|
|
@@ -542,25 +649,13 @@ async function signUp(email, password, name) {
|
|
|
542
649
|
email: body.user?.email ?? email
|
|
543
650
|
};
|
|
544
651
|
}
|
|
545
|
-
async function
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
is_active: "eq.true",
|
|
549
|
-
select: "api_key",
|
|
550
|
-
limit: "1"
|
|
551
|
-
});
|
|
552
|
-
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",
|
|
553
655
|
headers: supabaseHeaders(accessToken)
|
|
554
656
|
});
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
if (Array.isArray(rows) && rows.length > 0 && rows[0].api_key) {
|
|
558
|
-
return rows[0].api_key;
|
|
559
|
-
}
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
async function createApiKey(accessToken, userId) {
|
|
563
|
-
const apiKey = `fmcp_${randomBytes(32).toString("hex")}`;
|
|
657
|
+
const apiKey = `lim_${randomBytes(32).toString("hex")}`;
|
|
658
|
+
const keyHash = createHash("sha256").update(apiKey).digest("hex");
|
|
564
659
|
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_api_keys`, {
|
|
565
660
|
method: "POST",
|
|
566
661
|
headers: {
|
|
@@ -570,7 +665,7 @@ async function createApiKey(accessToken, userId) {
|
|
|
570
665
|
body: JSON.stringify({
|
|
571
666
|
user_id: userId,
|
|
572
667
|
key_name: "Liminal CLI",
|
|
573
|
-
|
|
668
|
+
key_hash: keyHash,
|
|
574
669
|
is_active: true
|
|
575
670
|
})
|
|
576
671
|
});
|
|
@@ -582,8 +677,6 @@ async function createApiKey(accessToken, userId) {
|
|
|
582
677
|
return apiKey;
|
|
583
678
|
}
|
|
584
679
|
async function authenticateAndGetKey(auth) {
|
|
585
|
-
const existingKey = await fetchApiKey(auth.accessToken, auth.userId);
|
|
586
|
-
if (existingKey) return existingKey;
|
|
587
680
|
return createApiKey(auth.accessToken, auth.userId);
|
|
588
681
|
}
|
|
589
682
|
|
|
@@ -592,7 +685,7 @@ async function loginCommand() {
|
|
|
592
685
|
printBanner();
|
|
593
686
|
try {
|
|
594
687
|
const config = loadConfig();
|
|
595
|
-
if (config.apiKey && config.apiKey.startsWith("fmcp_")) {
|
|
688
|
+
if (config.apiKey && (config.apiKey.startsWith("lim_") || config.apiKey.startsWith("fmcp_"))) {
|
|
596
689
|
console.log(" Already logged in.");
|
|
597
690
|
console.log(" Run \x1B[1mliminal logout\x1B[0m first to switch accounts.");
|
|
598
691
|
return;
|
|
@@ -819,9 +912,9 @@ import { homedir as homedir4 } from "os";
|
|
|
819
912
|
var INFO3 = {
|
|
820
913
|
id: "cursor",
|
|
821
914
|
label: "Cursor",
|
|
822
|
-
description: "AI-first code editor (
|
|
915
|
+
description: "AI-first code editor (proxy-based, automated)",
|
|
823
916
|
protocol: "openai-chat",
|
|
824
|
-
automatable:
|
|
917
|
+
automatable: true
|
|
825
918
|
};
|
|
826
919
|
function getCursorPaths() {
|
|
827
920
|
const platform = process.platform;
|
|
@@ -871,23 +964,23 @@ var cursorConnector = {
|
|
|
871
964
|
return [];
|
|
872
965
|
},
|
|
873
966
|
async setup(port) {
|
|
874
|
-
const baseUrl = `http://127.0.0.1:${port}/v1`;
|
|
875
967
|
return {
|
|
876
968
|
success: true,
|
|
877
969
|
shellExports: [],
|
|
878
|
-
// No env vars — GUI only
|
|
879
970
|
postSetupInstructions: [
|
|
880
|
-
"
|
|
971
|
+
"Run the automated setup:",
|
|
972
|
+
"",
|
|
973
|
+
" liminal setup cursor",
|
|
881
974
|
"",
|
|
882
|
-
"
|
|
883
|
-
"
|
|
884
|
-
|
|
885
|
-
` 4. Set the base URL to: ${baseUrl}`,
|
|
886
|
-
' 5. Enter any string as the API key (e.g., "liminal")',
|
|
887
|
-
" 6. Restart Cursor",
|
|
975
|
+
"This will:",
|
|
976
|
+
" 1. Generate and install a local CA certificate",
|
|
977
|
+
" 2. Launch Cursor with --proxy-server=http://127.0.0.1:" + port,
|
|
888
978
|
"",
|
|
889
|
-
"
|
|
890
|
-
"
|
|
979
|
+
"All LLM API calls are transparently intercepted and compressed.",
|
|
980
|
+
"No Cursor settings changes needed \u2014 works with all models.",
|
|
981
|
+
"",
|
|
982
|
+
"Or launch Cursor manually:",
|
|
983
|
+
` cursor --proxy-server=http://127.0.0.1:${port}`
|
|
891
984
|
]
|
|
892
985
|
};
|
|
893
986
|
},
|
|
@@ -1140,64 +1233,6 @@ async function logoutCommand() {
|
|
|
1140
1233
|
console.log(" Run \x1B[1mliminal login\x1B[0m to reconnect.");
|
|
1141
1234
|
}
|
|
1142
1235
|
|
|
1143
|
-
// src/rsc/pipeline.ts
|
|
1144
|
-
import {
|
|
1145
|
-
CompressionPipeline,
|
|
1146
|
-
RSCTransport,
|
|
1147
|
-
RSCEventEmitter,
|
|
1148
|
-
Session,
|
|
1149
|
-
CircuitBreaker
|
|
1150
|
-
} from "@cognisos/rsc-sdk";
|
|
1151
|
-
var RSCPipelineWrapper = class {
|
|
1152
|
-
pipeline;
|
|
1153
|
-
session;
|
|
1154
|
-
events;
|
|
1155
|
-
transport;
|
|
1156
|
-
circuitBreaker;
|
|
1157
|
-
constructor(config) {
|
|
1158
|
-
this.circuitBreaker = new CircuitBreaker(5, 5 * 60 * 1e3);
|
|
1159
|
-
this.transport = new RSCTransport({
|
|
1160
|
-
baseUrl: config.rscBaseUrl,
|
|
1161
|
-
apiKey: config.rscApiKey,
|
|
1162
|
-
timeout: 3e4,
|
|
1163
|
-
maxRetries: 3,
|
|
1164
|
-
circuitBreaker: this.circuitBreaker
|
|
1165
|
-
});
|
|
1166
|
-
this.events = new RSCEventEmitter();
|
|
1167
|
-
this.session = new Session(config.sessionId);
|
|
1168
|
-
this.pipeline = new CompressionPipeline(
|
|
1169
|
-
this.transport,
|
|
1170
|
-
{
|
|
1171
|
-
threshold: config.compressionThreshold,
|
|
1172
|
-
learnFromResponses: config.learnFromResponses,
|
|
1173
|
-
latencyBudgetMs: config.latencyBudgetMs,
|
|
1174
|
-
sessionId: this.session.sessionId
|
|
1175
|
-
},
|
|
1176
|
-
this.events
|
|
1177
|
-
);
|
|
1178
|
-
}
|
|
1179
|
-
async healthCheck() {
|
|
1180
|
-
try {
|
|
1181
|
-
await this.transport.get("/health");
|
|
1182
|
-
return true;
|
|
1183
|
-
} catch {
|
|
1184
|
-
return false;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
getSessionSummary() {
|
|
1188
|
-
return this.session.getSummary();
|
|
1189
|
-
}
|
|
1190
|
-
getCircuitState() {
|
|
1191
|
-
return this.circuitBreaker.getState();
|
|
1192
|
-
}
|
|
1193
|
-
isCircuitOpen() {
|
|
1194
|
-
return this.circuitBreaker.getState() === "open";
|
|
1195
|
-
}
|
|
1196
|
-
resetCircuitBreaker() {
|
|
1197
|
-
this.circuitBreaker.reset();
|
|
1198
|
-
}
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
1236
|
// src/proxy/completions.ts
|
|
1202
1237
|
import { RSCCircuitOpenError as RSCCircuitOpenError2 } from "@cognisos/rsc-sdk";
|
|
1203
1238
|
|
|
@@ -1303,66 +1338,154 @@ function isIndentedCodeLine(line) {
|
|
|
1303
1338
|
}
|
|
1304
1339
|
|
|
1305
1340
|
// src/rsc/message-compressor.ts
|
|
1341
|
+
var PASSTHROUGH_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
|
|
1342
|
+
function sanitizeCompressedText(text) {
|
|
1343
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
1344
|
+
}
|
|
1306
1345
|
async function compressMessages(messages, pipeline, session, compressRoles) {
|
|
1307
1346
|
let anyCompressed = false;
|
|
1308
1347
|
let totalTokensSaved = 0;
|
|
1309
1348
|
const compressed = await Promise.all(
|
|
1310
1349
|
messages.map(async (msg) => {
|
|
1311
1350
|
if (!compressRoles.has(msg.role)) return msg;
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
if (Array.isArray(msg.content)) {
|
|
1319
|
-
return compressArrayContent(msg, pipeline, session, (c, saved) => {
|
|
1320
|
-
anyCompressed = anyCompressed || c;
|
|
1321
|
-
totalTokensSaved += saved;
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
return msg;
|
|
1351
|
+
return compressMessage(msg, pipeline, session, (c, saved) => {
|
|
1352
|
+
anyCompressed = anyCompressed || c;
|
|
1353
|
+
totalTokensSaved += saved;
|
|
1354
|
+
});
|
|
1325
1355
|
})
|
|
1326
1356
|
);
|
|
1327
1357
|
return { messages: compressed, anyCompressed, totalTokensSaved };
|
|
1328
1358
|
}
|
|
1329
|
-
async function
|
|
1330
|
-
|
|
1359
|
+
async function compressConversation(pipeline, session, plan, options = { compressToolResults: true }) {
|
|
1360
|
+
if (!plan.shouldCompress) {
|
|
1361
|
+
return {
|
|
1362
|
+
messages: plan.messages.map((tm) => tm.message),
|
|
1363
|
+
anyCompressed: false,
|
|
1364
|
+
totalTokensSaved: 0
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
let anyCompressed = false;
|
|
1368
|
+
let totalTokensSaved = 0;
|
|
1369
|
+
const record = (c, saved) => {
|
|
1370
|
+
anyCompressed = anyCompressed || c;
|
|
1371
|
+
totalTokensSaved += saved;
|
|
1372
|
+
};
|
|
1373
|
+
const log = options.logFn;
|
|
1374
|
+
const threshold = options.compressionThreshold ?? 100;
|
|
1375
|
+
const results = new Array(plan.messages.length);
|
|
1376
|
+
const compressible = [];
|
|
1377
|
+
for (let i = 0; i < plan.messages.length; i++) {
|
|
1378
|
+
const tm = plan.messages[i];
|
|
1379
|
+
const role = tm.message.role;
|
|
1380
|
+
const blockTypes = Array.isArray(tm.message.content) ? tm.message.content.map((b) => b.type).join(",") : "string";
|
|
1381
|
+
if (tm.tier === "hot") {
|
|
1382
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 HOT (verbatim)`);
|
|
1383
|
+
results[i] = tm.message;
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (tm.eligibleTokens === 0) {
|
|
1387
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (0 eligible tok, skip)`);
|
|
1388
|
+
results[i] = tm.message;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const proseEstimate = estimateProseTokens(tm.message, options.compressToolResults);
|
|
1392
|
+
if (proseEstimate < threshold) {
|
|
1393
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (${proseEstimate} prose tok after segmentation < ${threshold} threshold, skip)`);
|
|
1394
|
+
results[i] = tm.message;
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (${tm.eligibleTokens} eligible tok, ~${proseEstimate} prose tok, compressing)`);
|
|
1398
|
+
compressible.push({ planIdx: i, tm });
|
|
1399
|
+
}
|
|
1400
|
+
compressible.sort((a, b) => b.tm.eligibleTokens - a.tm.eligibleTokens);
|
|
1401
|
+
for (const { planIdx, tm } of compressible) {
|
|
1402
|
+
results[planIdx] = await batchCompressMessage(tm.message, pipeline, session, record, options);
|
|
1403
|
+
}
|
|
1404
|
+
return { messages: results, anyCompressed, totalTokensSaved };
|
|
1405
|
+
}
|
|
1406
|
+
function estimateProseTokens(msg, compressToolResults) {
|
|
1407
|
+
const text = extractCompressibleText(msg, compressToolResults);
|
|
1408
|
+
if (!text) return 0;
|
|
1331
1409
|
const segments = segmentContent(text);
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1410
|
+
return segments.filter((s) => s.type === "prose").reduce((sum, s) => sum + Math.ceil(s.text.length / 4), 0);
|
|
1411
|
+
}
|
|
1412
|
+
function extractCompressibleText(msg, compressToolResults) {
|
|
1413
|
+
if (typeof msg.content === "string") {
|
|
1414
|
+
return msg.content.trim() ? msg.content : null;
|
|
1415
|
+
}
|
|
1416
|
+
if (!Array.isArray(msg.content)) return null;
|
|
1417
|
+
const parts = msg.content;
|
|
1418
|
+
const textSegments = [];
|
|
1419
|
+
for (const part of parts) {
|
|
1420
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) continue;
|
|
1421
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1422
|
+
textSegments.push(part.text);
|
|
1423
|
+
}
|
|
1424
|
+
if (part.type === "tool_result" && compressToolResults) {
|
|
1425
|
+
const extracted = extractToolResultText(part);
|
|
1426
|
+
if (extracted) textSegments.push(extracted);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return textSegments.length > 0 ? textSegments.join("\n\n") : null;
|
|
1430
|
+
}
|
|
1431
|
+
function extractToolResultText(part) {
|
|
1432
|
+
if (typeof part.content === "string" && part.content.trim()) {
|
|
1433
|
+
return part.content;
|
|
1434
|
+
}
|
|
1435
|
+
if (Array.isArray(part.content)) {
|
|
1436
|
+
const texts = part.content.filter((inner) => inner.type === "text" && typeof inner.text === "string" && inner.text.trim()).map((inner) => inner.text);
|
|
1437
|
+
return texts.length > 0 ? texts.join("\n") : null;
|
|
1438
|
+
}
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
async function batchCompressMessage(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1442
|
+
if (typeof msg.content === "string") {
|
|
1443
|
+
return compressStringContent(msg, pipeline, session, record, options.semaphore, options.semaphoreTimeoutMs);
|
|
1444
|
+
}
|
|
1445
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
1446
|
+
const parts = msg.content;
|
|
1447
|
+
const textSegments = [];
|
|
1448
|
+
const batchedIndices = /* @__PURE__ */ new Set();
|
|
1449
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1450
|
+
const part = parts[i];
|
|
1451
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) continue;
|
|
1452
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1453
|
+
textSegments.push(part.text);
|
|
1454
|
+
batchedIndices.add(i);
|
|
1455
|
+
}
|
|
1456
|
+
if (part.type === "tool_result" && options.compressToolResults) {
|
|
1457
|
+
const extracted = extractToolResultText(part);
|
|
1458
|
+
if (extracted) {
|
|
1459
|
+
textSegments.push(extracted);
|
|
1460
|
+
batchedIndices.add(i);
|
|
1343
1461
|
}
|
|
1344
|
-
session.recordFailure();
|
|
1345
|
-
return msg;
|
|
1346
1462
|
}
|
|
1347
1463
|
}
|
|
1464
|
+
if (textSegments.length === 0) return msg;
|
|
1465
|
+
const batchText = textSegments.join("\n\n");
|
|
1348
1466
|
try {
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1467
|
+
const compressed = await compressTextWithSegmentation(batchText, pipeline, session, record, options.semaphore, options.semaphoreTimeoutMs);
|
|
1468
|
+
const newParts = [];
|
|
1469
|
+
let isFirstEligible = true;
|
|
1470
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1471
|
+
if (!batchedIndices.has(i)) {
|
|
1472
|
+
newParts.push(parts[i]);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (isFirstEligible) {
|
|
1476
|
+
if (parts[i].type === "text") {
|
|
1477
|
+
newParts.push({ ...parts[i], text: compressed });
|
|
1478
|
+
} else if (parts[i].type === "tool_result") {
|
|
1479
|
+
newParts.push({ ...parts[i], content: compressed });
|
|
1362
1480
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1481
|
+
isFirstEligible = false;
|
|
1482
|
+
} else {
|
|
1483
|
+
if (parts[i].type === "tool_result") {
|
|
1484
|
+
newParts.push({ ...parts[i], content: "" });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return { ...msg, content: newParts };
|
|
1366
1489
|
} catch (err) {
|
|
1367
1490
|
if (err instanceof RSCCircuitOpenError) {
|
|
1368
1491
|
session.recordFailure();
|
|
@@ -1372,37 +1495,34 @@ async function compressStringContent(msg, pipeline, session, record) {
|
|
|
1372
1495
|
return msg;
|
|
1373
1496
|
}
|
|
1374
1497
|
}
|
|
1375
|
-
async function
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
session
|
|
1381
|
-
|
|
1382
|
-
|
|
1498
|
+
async function compressMessage(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1499
|
+
if (typeof msg.content === "string") {
|
|
1500
|
+
return compressStringContent(msg, pipeline, session, record);
|
|
1501
|
+
}
|
|
1502
|
+
if (Array.isArray(msg.content)) {
|
|
1503
|
+
return compressArrayContent(msg, pipeline, session, record, options);
|
|
1504
|
+
}
|
|
1505
|
+
return msg;
|
|
1506
|
+
}
|
|
1507
|
+
async function compressStringContent(msg, pipeline, session, record, semaphore, semaphoreTimeoutMs) {
|
|
1508
|
+
const text = msg.content;
|
|
1509
|
+
try {
|
|
1510
|
+
const compressed = await compressTextWithSegmentation(text, pipeline, session, record, semaphore, semaphoreTimeoutMs);
|
|
1511
|
+
return { ...msg, content: compressed };
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1514
|
+
session.recordFailure();
|
|
1515
|
+
throw err;
|
|
1516
|
+
}
|
|
1517
|
+
session.recordFailure();
|
|
1518
|
+
return msg;
|
|
1383
1519
|
}
|
|
1384
|
-
const parts = await Promise.all(
|
|
1385
|
-
segments.map(async (seg) => {
|
|
1386
|
-
if (seg.type === "code") return seg.text;
|
|
1387
|
-
if (seg.text.trim().length === 0) return seg.text;
|
|
1388
|
-
try {
|
|
1389
|
-
const result = await pipeline.compressForLLM(seg.text);
|
|
1390
|
-
session.recordCompression(result.metrics);
|
|
1391
|
-
record(!result.metrics.skipped, result.metrics.tokensSaved);
|
|
1392
|
-
return result.text;
|
|
1393
|
-
} catch (err) {
|
|
1394
|
-
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1395
|
-
session.recordFailure();
|
|
1396
|
-
return seg.text;
|
|
1397
|
-
}
|
|
1398
|
-
})
|
|
1399
|
-
);
|
|
1400
|
-
return parts.join("");
|
|
1401
1520
|
}
|
|
1402
|
-
async function compressArrayContent(msg, pipeline, session, record) {
|
|
1521
|
+
async function compressArrayContent(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1403
1522
|
const parts = msg.content;
|
|
1404
1523
|
const compressedParts = await Promise.all(
|
|
1405
1524
|
parts.map(async (part) => {
|
|
1525
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) return part;
|
|
1406
1526
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1407
1527
|
try {
|
|
1408
1528
|
const compressed = await compressTextWithSegmentation(part.text, pipeline, session, record);
|
|
@@ -1416,11 +1536,183 @@ async function compressArrayContent(msg, pipeline, session, record) {
|
|
|
1416
1536
|
return part;
|
|
1417
1537
|
}
|
|
1418
1538
|
}
|
|
1539
|
+
if (part.type === "tool_result" && options.compressToolResults) {
|
|
1540
|
+
return compressToolResult(part, pipeline, session, record);
|
|
1541
|
+
}
|
|
1419
1542
|
return part;
|
|
1420
1543
|
})
|
|
1421
1544
|
);
|
|
1422
1545
|
return { ...msg, content: compressedParts };
|
|
1423
1546
|
}
|
|
1547
|
+
async function compressToolResult(part, pipeline, session, record) {
|
|
1548
|
+
const content = part.content;
|
|
1549
|
+
if (typeof content === "string") {
|
|
1550
|
+
try {
|
|
1551
|
+
const compressed = await compressTextWithSegmentation(content, pipeline, session, record);
|
|
1552
|
+
return { ...part, content: compressed };
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1555
|
+
session.recordFailure();
|
|
1556
|
+
throw err;
|
|
1557
|
+
}
|
|
1558
|
+
session.recordFailure();
|
|
1559
|
+
return part;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (Array.isArray(content)) {
|
|
1563
|
+
try {
|
|
1564
|
+
const compressedInner = await Promise.all(
|
|
1565
|
+
content.map(async (inner) => {
|
|
1566
|
+
if (inner.type === "text" && typeof inner.text === "string") {
|
|
1567
|
+
try {
|
|
1568
|
+
const compressed = await compressTextWithSegmentation(inner.text, pipeline, session, record);
|
|
1569
|
+
return { ...inner, text: compressed };
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1572
|
+
session.recordFailure();
|
|
1573
|
+
return inner;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return inner;
|
|
1577
|
+
})
|
|
1578
|
+
);
|
|
1579
|
+
return { ...part, content: compressedInner };
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1582
|
+
session.recordFailure();
|
|
1583
|
+
throw err;
|
|
1584
|
+
}
|
|
1585
|
+
session.recordFailure();
|
|
1586
|
+
return part;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return part;
|
|
1590
|
+
}
|
|
1591
|
+
async function compressTextWithSegmentation(text, pipeline, session, record, semaphore, semaphoreTimeoutMs) {
|
|
1592
|
+
const segments = segmentContent(text);
|
|
1593
|
+
const hasCode = segments.some((s) => s.type === "code");
|
|
1594
|
+
if (!hasCode) {
|
|
1595
|
+
if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
|
|
1596
|
+
try {
|
|
1597
|
+
const result = await pipeline.compressForLLM(text);
|
|
1598
|
+
session.recordCompression(result.metrics);
|
|
1599
|
+
const saved = Math.max(0, result.metrics.tokensSaved);
|
|
1600
|
+
record(!result.metrics.skipped, saved);
|
|
1601
|
+
return sanitizeCompressedText(result.text);
|
|
1602
|
+
} finally {
|
|
1603
|
+
if (semaphore) semaphore.release();
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const parts = await Promise.all(
|
|
1607
|
+
segments.map(async (seg) => {
|
|
1608
|
+
if (seg.type === "code") return seg.text;
|
|
1609
|
+
if (seg.text.trim().length === 0) return seg.text;
|
|
1610
|
+
if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
|
|
1611
|
+
try {
|
|
1612
|
+
const result = await pipeline.compressForLLM(seg.text);
|
|
1613
|
+
session.recordCompression(result.metrics);
|
|
1614
|
+
const saved = Math.max(0, result.metrics.tokensSaved);
|
|
1615
|
+
record(!result.metrics.skipped, saved);
|
|
1616
|
+
return sanitizeCompressedText(result.text);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1619
|
+
session.recordFailure();
|
|
1620
|
+
return seg.text;
|
|
1621
|
+
} finally {
|
|
1622
|
+
if (semaphore) semaphore.release();
|
|
1623
|
+
}
|
|
1624
|
+
})
|
|
1625
|
+
);
|
|
1626
|
+
return parts.join("");
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/rsc/conversation-analyzer.ts
|
|
1630
|
+
var SKIP_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
|
|
1631
|
+
function estimateTokens(text) {
|
|
1632
|
+
return Math.ceil(text.length / 4);
|
|
1633
|
+
}
|
|
1634
|
+
function estimateBlockTokens(block, compressToolResults) {
|
|
1635
|
+
if (SKIP_BLOCK_TYPES.has(block.type)) return 0;
|
|
1636
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1637
|
+
return estimateTokens(block.text);
|
|
1638
|
+
}
|
|
1639
|
+
if (block.type === "tool_result" && compressToolResults) {
|
|
1640
|
+
if (typeof block.content === "string") {
|
|
1641
|
+
return estimateTokens(block.content);
|
|
1642
|
+
}
|
|
1643
|
+
if (Array.isArray(block.content)) {
|
|
1644
|
+
return block.content.reduce(
|
|
1645
|
+
(sum, inner) => sum + estimateBlockTokens(inner, compressToolResults),
|
|
1646
|
+
0
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return 0;
|
|
1651
|
+
}
|
|
1652
|
+
function estimateMessageTokens(msg, config) {
|
|
1653
|
+
if (!config.compressRoles.has(msg.role)) return 0;
|
|
1654
|
+
if (typeof msg.content === "string") {
|
|
1655
|
+
return estimateTokens(msg.content);
|
|
1656
|
+
}
|
|
1657
|
+
if (Array.isArray(msg.content)) {
|
|
1658
|
+
return msg.content.reduce(
|
|
1659
|
+
(sum, part) => sum + estimateBlockTokens(part, config.compressToolResults),
|
|
1660
|
+
0
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
return 0;
|
|
1664
|
+
}
|
|
1665
|
+
function analyzeConversation(messages, config) {
|
|
1666
|
+
const n = messages.length;
|
|
1667
|
+
if (n < 5) {
|
|
1668
|
+
const tiered2 = messages.map((msg, i) => ({
|
|
1669
|
+
index: i,
|
|
1670
|
+
message: msg,
|
|
1671
|
+
tier: "hot",
|
|
1672
|
+
eligibleTokens: estimateMessageTokens(msg, config)
|
|
1673
|
+
}));
|
|
1674
|
+
return {
|
|
1675
|
+
messages: tiered2,
|
|
1676
|
+
totalEligibleTokens: 0,
|
|
1677
|
+
shouldCompress: false,
|
|
1678
|
+
hotCount: n,
|
|
1679
|
+
warmCount: 0,
|
|
1680
|
+
coldCount: 0
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const coldEnd = Math.floor(n * config.coldFraction);
|
|
1684
|
+
const hotStart = n - Math.floor(n * config.hotFraction);
|
|
1685
|
+
let totalEligibleTokens = 0;
|
|
1686
|
+
let hotCount = 0;
|
|
1687
|
+
let warmCount = 0;
|
|
1688
|
+
let coldCount = 0;
|
|
1689
|
+
const tiered = messages.map((msg, i) => {
|
|
1690
|
+
let tier;
|
|
1691
|
+
if (i >= hotStart) {
|
|
1692
|
+
tier = "hot";
|
|
1693
|
+
hotCount++;
|
|
1694
|
+
} else if (i < coldEnd) {
|
|
1695
|
+
tier = "cold";
|
|
1696
|
+
coldCount++;
|
|
1697
|
+
} else {
|
|
1698
|
+
tier = "warm";
|
|
1699
|
+
warmCount++;
|
|
1700
|
+
}
|
|
1701
|
+
const eligibleTokens = estimateMessageTokens(msg, config);
|
|
1702
|
+
if (tier !== "hot") {
|
|
1703
|
+
totalEligibleTokens += eligibleTokens;
|
|
1704
|
+
}
|
|
1705
|
+
return { index: i, message: msg, tier, eligibleTokens };
|
|
1706
|
+
});
|
|
1707
|
+
return {
|
|
1708
|
+
messages: tiered,
|
|
1709
|
+
totalEligibleTokens,
|
|
1710
|
+
shouldCompress: totalEligibleTokens >= config.aggregateThreshold,
|
|
1711
|
+
hotCount,
|
|
1712
|
+
warmCount,
|
|
1713
|
+
coldCount
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1424
1716
|
|
|
1425
1717
|
// src/rsc/learning.ts
|
|
1426
1718
|
function createStreamLearningBuffer(pipeline) {
|
|
@@ -1523,6 +1815,25 @@ async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onCo
|
|
|
1523
1815
|
}
|
|
1524
1816
|
}
|
|
1525
1817
|
|
|
1818
|
+
// src/terminology.ts
|
|
1819
|
+
var TIER_LABELS = {
|
|
1820
|
+
HOT: "Active",
|
|
1821
|
+
WARM: "Recent",
|
|
1822
|
+
COLD: "Archived"
|
|
1823
|
+
};
|
|
1824
|
+
function formatTiersLog(hot, warm, cold, eligibleTokens) {
|
|
1825
|
+
return `[MEMORY] ${TIER_LABELS.HOT}:${hot} ${TIER_LABELS.WARM}:${warm} ${TIER_LABELS.COLD}:${cold} \xB7 ${eligibleTokens} tokens eligible`;
|
|
1826
|
+
}
|
|
1827
|
+
function formatSavedLog(tokensSaved, latencyMs) {
|
|
1828
|
+
return `[SAVED] ${tokensSaved} tokens (${latencyMs}ms)`;
|
|
1829
|
+
}
|
|
1830
|
+
function formatDegradeLog() {
|
|
1831
|
+
return "[STATUS] Connection degraded \u2014 passing through directly";
|
|
1832
|
+
}
|
|
1833
|
+
function formatResponseLog(model, tokensSaved, streaming = false) {
|
|
1834
|
+
return `[RESPONSE] ${streaming ? "Streaming " : ""}${model} response \u2192 client (saved:${tokensSaved}tok)`;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1526
1837
|
// src/proxy/completions.ts
|
|
1527
1838
|
function setCORSHeaders(res) {
|
|
1528
1839
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -1539,7 +1850,7 @@ function extractBearerToken(req) {
|
|
|
1539
1850
|
if (!auth || !auth.startsWith("Bearer ")) return null;
|
|
1540
1851
|
return auth.slice(7);
|
|
1541
1852
|
}
|
|
1542
|
-
async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
1853
|
+
async function handleChatCompletions(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
1543
1854
|
const request = body;
|
|
1544
1855
|
if (!request.messages || !Array.isArray(request.messages)) {
|
|
1545
1856
|
sendJSON(res, 400, {
|
|
@@ -1558,23 +1869,45 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1558
1869
|
let anyCompressed = false;
|
|
1559
1870
|
let totalTokensSaved = 0;
|
|
1560
1871
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
1872
|
+
const compressStart = Date.now();
|
|
1561
1873
|
try {
|
|
1562
1874
|
const compressRoles = new Set(config.compressRoles);
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1875
|
+
const plan = analyzeConversation(request.messages, {
|
|
1876
|
+
hotFraction: config.hotFraction,
|
|
1877
|
+
coldFraction: config.coldFraction,
|
|
1878
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
1879
|
+
compressRoles,
|
|
1880
|
+
compressToolResults: config.compressToolResults
|
|
1881
|
+
});
|
|
1882
|
+
if (plan.shouldCompress) {
|
|
1883
|
+
logger.log(formatTiersLog(plan.hotCount, plan.warmCount, plan.coldCount, plan.totalEligibleTokens));
|
|
1884
|
+
}
|
|
1885
|
+
const result = await compressConversation(
|
|
1565
1886
|
pipeline.pipeline,
|
|
1566
1887
|
pipeline.session,
|
|
1567
|
-
|
|
1888
|
+
plan,
|
|
1889
|
+
{
|
|
1890
|
+
compressToolResults: config.compressToolResults,
|
|
1891
|
+
compressionThreshold: config.compressionThreshold,
|
|
1892
|
+
logFn: (msg) => logger.log(msg),
|
|
1893
|
+
semaphore,
|
|
1894
|
+
semaphoreTimeoutMs: config.concurrencyTimeoutMs
|
|
1895
|
+
}
|
|
1568
1896
|
);
|
|
1569
1897
|
messages = result.messages;
|
|
1570
1898
|
anyCompressed = result.anyCompressed;
|
|
1571
1899
|
totalTokensSaved = result.totalTokensSaved;
|
|
1900
|
+
const latencyMs = Date.now() - compressStart;
|
|
1901
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
1902
|
+
if (alert) {
|
|
1903
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
1904
|
+
}
|
|
1572
1905
|
if (result.totalTokensSaved > 0) {
|
|
1573
|
-
logger.log(
|
|
1906
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
1574
1907
|
}
|
|
1575
1908
|
} catch (err) {
|
|
1576
1909
|
if (err instanceof RSCCircuitOpenError2) {
|
|
1577
|
-
logger.log(
|
|
1910
|
+
logger.log(formatDegradeLog());
|
|
1578
1911
|
} else {
|
|
1579
1912
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1580
1913
|
}
|
|
@@ -1607,6 +1940,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1607
1940
|
}
|
|
1608
1941
|
if (request.stream && upstreamResponse.body) {
|
|
1609
1942
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
1943
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
1610
1944
|
await pipeSSEResponse(
|
|
1611
1945
|
upstreamResponse,
|
|
1612
1946
|
res,
|
|
@@ -1617,6 +1951,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1617
1951
|
return;
|
|
1618
1952
|
}
|
|
1619
1953
|
const responseBody = await upstreamResponse.text();
|
|
1954
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
1620
1955
|
let finalBody = responseBody;
|
|
1621
1956
|
if (totalTokensSaved > 0) {
|
|
1622
1957
|
try {
|
|
@@ -1757,7 +2092,7 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
|
|
|
1757
2092
|
function setCORSHeaders2(res) {
|
|
1758
2093
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1759
2094
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
1760
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta");
|
|
2095
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, x-liminal-session");
|
|
1761
2096
|
}
|
|
1762
2097
|
function sendAnthropicError(res, status, type, message) {
|
|
1763
2098
|
setCORSHeaders2(res);
|
|
@@ -1788,7 +2123,7 @@ function convertCompressedToAnthropic(messages) {
|
|
|
1788
2123
|
content: msg.content
|
|
1789
2124
|
}));
|
|
1790
2125
|
}
|
|
1791
|
-
async function handleAnthropicMessages(req, res, body, pipeline, config, logger) {
|
|
2126
|
+
async function handleAnthropicMessages(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
1792
2127
|
const request = body;
|
|
1793
2128
|
if (!request.messages || !Array.isArray(request.messages)) {
|
|
1794
2129
|
sendAnthropicError(res, 400, "invalid_request_error", "messages is required and must be an array");
|
|
@@ -1807,24 +2142,46 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1807
2142
|
let anyCompressed = false;
|
|
1808
2143
|
let totalTokensSaved = 0;
|
|
1809
2144
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
2145
|
+
const compressStart = Date.now();
|
|
1810
2146
|
try {
|
|
1811
2147
|
const compressRoles = new Set(config.compressRoles);
|
|
1812
2148
|
const compressible = convertAnthropicToCompressible(request.messages);
|
|
1813
|
-
const
|
|
1814
|
-
|
|
2149
|
+
const plan = analyzeConversation(compressible, {
|
|
2150
|
+
hotFraction: config.hotFraction,
|
|
2151
|
+
coldFraction: config.coldFraction,
|
|
2152
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
2153
|
+
compressRoles,
|
|
2154
|
+
compressToolResults: config.compressToolResults
|
|
2155
|
+
});
|
|
2156
|
+
if (plan.shouldCompress) {
|
|
2157
|
+
logger.log(formatTiersLog(plan.hotCount, plan.warmCount, plan.coldCount, plan.totalEligibleTokens));
|
|
2158
|
+
}
|
|
2159
|
+
const result = await compressConversation(
|
|
1815
2160
|
pipeline.pipeline,
|
|
1816
2161
|
pipeline.session,
|
|
1817
|
-
|
|
2162
|
+
plan,
|
|
2163
|
+
{
|
|
2164
|
+
compressToolResults: config.compressToolResults,
|
|
2165
|
+
compressionThreshold: config.compressionThreshold,
|
|
2166
|
+
logFn: (msg) => logger.log(msg),
|
|
2167
|
+
semaphore,
|
|
2168
|
+
semaphoreTimeoutMs: config.concurrencyTimeoutMs
|
|
2169
|
+
}
|
|
1818
2170
|
);
|
|
1819
2171
|
messages = convertCompressedToAnthropic(result.messages);
|
|
1820
2172
|
anyCompressed = result.anyCompressed;
|
|
1821
2173
|
totalTokensSaved = result.totalTokensSaved;
|
|
2174
|
+
const latencyMs = Date.now() - compressStart;
|
|
2175
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
2176
|
+
if (alert) {
|
|
2177
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
2178
|
+
}
|
|
1822
2179
|
if (result.totalTokensSaved > 0) {
|
|
1823
|
-
logger.log(
|
|
2180
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
1824
2181
|
}
|
|
1825
2182
|
} catch (err) {
|
|
1826
2183
|
if (err instanceof RSCCircuitOpenError3) {
|
|
1827
|
-
logger.log(
|
|
2184
|
+
logger.log(formatDegradeLog());
|
|
1828
2185
|
} else {
|
|
1829
2186
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1830
2187
|
}
|
|
@@ -1862,6 +2219,7 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1862
2219
|
}
|
|
1863
2220
|
if (request.stream && upstreamResponse.body) {
|
|
1864
2221
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
2222
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
1865
2223
|
await pipeAnthropicSSEResponse(
|
|
1866
2224
|
upstreamResponse,
|
|
1867
2225
|
res,
|
|
@@ -1872,6 +2230,7 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1872
2230
|
return;
|
|
1873
2231
|
}
|
|
1874
2232
|
const responseBody = await upstreamResponse.text();
|
|
2233
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
1875
2234
|
let finalBody = responseBody;
|
|
1876
2235
|
if (totalTokensSaved > 0) {
|
|
1877
2236
|
try {
|
|
@@ -2102,7 +2461,7 @@ function extractOutputText(output) {
|
|
|
2102
2461
|
}
|
|
2103
2462
|
return texts.join("");
|
|
2104
2463
|
}
|
|
2105
|
-
async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
2464
|
+
async function handleResponses(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
2106
2465
|
const request = body;
|
|
2107
2466
|
if (request.input === void 0 || request.input === null) {
|
|
2108
2467
|
sendJSON2(res, 400, {
|
|
@@ -2121,6 +2480,7 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2121
2480
|
let anyCompressed = false;
|
|
2122
2481
|
let totalTokensSaved = 0;
|
|
2123
2482
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
2483
|
+
const compressStart = Date.now();
|
|
2124
2484
|
try {
|
|
2125
2485
|
const compressRoles = new Set(config.compressRoles);
|
|
2126
2486
|
const compressible = inputToCompressibleMessages(request.input);
|
|
@@ -2134,13 +2494,18 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2134
2494
|
compressedInput = applyCompressedToInput(request.input, result.messages);
|
|
2135
2495
|
anyCompressed = result.anyCompressed;
|
|
2136
2496
|
totalTokensSaved = result.totalTokensSaved;
|
|
2497
|
+
const latencyMs = Date.now() - compressStart;
|
|
2498
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
2499
|
+
if (alert) {
|
|
2500
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
2501
|
+
}
|
|
2137
2502
|
if (result.totalTokensSaved > 0) {
|
|
2138
|
-
logger.log(
|
|
2503
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
2139
2504
|
}
|
|
2140
2505
|
}
|
|
2141
2506
|
} catch (err) {
|
|
2142
2507
|
if (err instanceof RSCCircuitOpenError4) {
|
|
2143
|
-
logger.log(
|
|
2508
|
+
logger.log(formatDegradeLog());
|
|
2144
2509
|
} else {
|
|
2145
2510
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2146
2511
|
}
|
|
@@ -2175,6 +2540,7 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2175
2540
|
}
|
|
2176
2541
|
if (request.stream && upstreamResponse.body) {
|
|
2177
2542
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
2543
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
2178
2544
|
await pipeResponsesSSE(
|
|
2179
2545
|
upstreamResponse,
|
|
2180
2546
|
res,
|
|
@@ -2185,6 +2551,7 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2185
2551
|
return;
|
|
2186
2552
|
}
|
|
2187
2553
|
const responseBody = await upstreamResponse.text();
|
|
2554
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
2188
2555
|
let finalBody = responseBody;
|
|
2189
2556
|
if (totalTokensSaved > 0) {
|
|
2190
2557
|
try {
|
|
@@ -2226,11 +2593,51 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2226
2593
|
}
|
|
2227
2594
|
}
|
|
2228
2595
|
|
|
2596
|
+
// src/proxy/session-identity.ts
|
|
2597
|
+
import * as crypto from "crypto";
|
|
2598
|
+
function identifySession(req, pathname) {
|
|
2599
|
+
const connector = detectConnector(req, pathname);
|
|
2600
|
+
const windowHash = deriveWindowHash(req);
|
|
2601
|
+
return { connector, windowHash, raw: `${connector}:${windowHash}` };
|
|
2602
|
+
}
|
|
2603
|
+
function detectConnector(req, pathname) {
|
|
2604
|
+
if (req.headers["x-api-key"] || req.headers["anthropic-version"]) {
|
|
2605
|
+
return "claude-code";
|
|
2606
|
+
}
|
|
2607
|
+
if (pathname.startsWith("/v1/responses") || pathname.startsWith("/responses")) {
|
|
2608
|
+
return "codex";
|
|
2609
|
+
}
|
|
2610
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
2611
|
+
if (/cursor/i.test(ua)) {
|
|
2612
|
+
return "cursor";
|
|
2613
|
+
}
|
|
2614
|
+
return "openai-compatible";
|
|
2615
|
+
}
|
|
2616
|
+
function deriveWindowHash(req) {
|
|
2617
|
+
const liminalSession = req.headers["x-liminal-session"];
|
|
2618
|
+
if (typeof liminalSession === "string" && liminalSession.length > 0) {
|
|
2619
|
+
return liminalSession;
|
|
2620
|
+
}
|
|
2621
|
+
const credential = extractCredential(req);
|
|
2622
|
+
if (!credential) return "anonymous";
|
|
2623
|
+
return crypto.createHash("sha256").update(credential).digest("hex").slice(0, 8);
|
|
2624
|
+
}
|
|
2625
|
+
function extractCredential(req) {
|
|
2626
|
+
const apiKey = req.headers["x-api-key"];
|
|
2627
|
+
if (typeof apiKey === "string" && apiKey.length > 0) return apiKey;
|
|
2628
|
+
const auth = req.headers["authorization"];
|
|
2629
|
+
if (typeof auth === "string") {
|
|
2630
|
+
const match = auth.match(/^Bearer\s+(.+)$/i);
|
|
2631
|
+
if (match) return match[1];
|
|
2632
|
+
}
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2229
2636
|
// src/proxy/handler.ts
|
|
2230
2637
|
function setCORSHeaders4(res) {
|
|
2231
2638
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2232
2639
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH");
|
|
2233
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, anthropic-dangerous-direct-browser-access");
|
|
2640
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, anthropic-dangerous-direct-browser-access, x-liminal-session");
|
|
2234
2641
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
2235
2642
|
}
|
|
2236
2643
|
function sendJSON3(res, status, body) {
|
|
@@ -2331,7 +2738,8 @@ async function passthroughToUpstream(req, res, fullUrl, config, logger) {
|
|
|
2331
2738
|
}
|
|
2332
2739
|
}
|
|
2333
2740
|
}
|
|
2334
|
-
function createRequestHandler(
|
|
2741
|
+
function createRequestHandler(deps) {
|
|
2742
|
+
const { sessions, semaphore, latencyMonitor, mitmStats, config, logger } = deps;
|
|
2335
2743
|
const startTime = Date.now();
|
|
2336
2744
|
return async (req, res) => {
|
|
2337
2745
|
try {
|
|
@@ -2347,27 +2755,38 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2347
2755
|
return;
|
|
2348
2756
|
}
|
|
2349
2757
|
if (method === "GET" && (url === "/health" || url === "/")) {
|
|
2350
|
-
const
|
|
2758
|
+
const sessionSummaries = sessions.getAllSummaries();
|
|
2351
2759
|
sendJSON3(res, 200, {
|
|
2352
2760
|
status: "ok",
|
|
2353
2761
|
version: config.rscApiKey ? "connected" : "no-api-key",
|
|
2354
|
-
rsc_connected: !pipeline.isCircuitOpen(),
|
|
2355
|
-
circuit_state: pipeline.getCircuitState(),
|
|
2356
|
-
session_id: summary.sessionId,
|
|
2357
2762
|
uptime_ms: Date.now() - startTime,
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2763
|
+
concurrency: {
|
|
2764
|
+
active_sessions: sessions.activeCount,
|
|
2765
|
+
semaphore_available: semaphore.available,
|
|
2766
|
+
semaphore_waiting: semaphore.waiting,
|
|
2767
|
+
max_concurrent_rsc_calls: config.concurrencyLimit
|
|
2768
|
+
},
|
|
2769
|
+
latency: {
|
|
2770
|
+
global_p95_ms: latencyMonitor.getGlobalP95()
|
|
2771
|
+
},
|
|
2772
|
+
mitm: mitmStats.snapshot(),
|
|
2773
|
+
sessions: sessionSummaries.map((s) => ({
|
|
2774
|
+
session_key: s.key,
|
|
2775
|
+
connector: s.connector,
|
|
2776
|
+
circuit_state: s.circuitState,
|
|
2777
|
+
tokens_processed: s.tokensProcessed,
|
|
2778
|
+
tokens_saved: s.tokensSaved,
|
|
2779
|
+
calls_total: s.totalCalls,
|
|
2780
|
+
calls_compressed: s.compressedCalls,
|
|
2781
|
+
calls_failed: s.failedCalls,
|
|
2782
|
+
p95_latency_ms: latencyMonitor.getSessionP95(s.key),
|
|
2783
|
+
last_active_ago_ms: Date.now() - s.lastAccessedAt
|
|
2784
|
+
}))
|
|
2368
2785
|
});
|
|
2369
2786
|
return;
|
|
2370
2787
|
}
|
|
2788
|
+
const sessionKey = identifySession(req, url);
|
|
2789
|
+
const pipeline = sessions.getOrCreate(sessionKey);
|
|
2371
2790
|
if (method === "POST" && (url === "/v1/chat/completions" || url === "/chat/completions")) {
|
|
2372
2791
|
const body = await readBody(req);
|
|
2373
2792
|
let parsed;
|
|
@@ -2379,7 +2798,7 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2379
2798
|
});
|
|
2380
2799
|
return;
|
|
2381
2800
|
}
|
|
2382
|
-
await handleChatCompletions(req, res, parsed, pipeline, config, logger);
|
|
2801
|
+
await handleChatCompletions(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
2383
2802
|
return;
|
|
2384
2803
|
}
|
|
2385
2804
|
if (method === "POST" && (url === "/v1/responses" || url === "/responses")) {
|
|
@@ -2393,7 +2812,7 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2393
2812
|
});
|
|
2394
2813
|
return;
|
|
2395
2814
|
}
|
|
2396
|
-
await handleResponses(req, res, parsed, pipeline, config, logger);
|
|
2815
|
+
await handleResponses(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
2397
2816
|
return;
|
|
2398
2817
|
}
|
|
2399
2818
|
if (method === "POST" && (url === "/v1/messages" || url === "/messages")) {
|
|
@@ -2408,7 +2827,7 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2408
2827
|
});
|
|
2409
2828
|
return;
|
|
2410
2829
|
}
|
|
2411
|
-
await handleAnthropicMessages(req, res, parsed, pipeline, config, logger);
|
|
2830
|
+
await handleAnthropicMessages(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
2412
2831
|
return;
|
|
2413
2832
|
}
|
|
2414
2833
|
await passthroughToUpstream(req, res, fullUrl, config, logger);
|
|
@@ -2432,9 +2851,11 @@ var ProxyServer = class {
|
|
|
2432
2851
|
activePort = null;
|
|
2433
2852
|
requestedPort;
|
|
2434
2853
|
handler;
|
|
2435
|
-
|
|
2854
|
+
connectHandler;
|
|
2855
|
+
constructor(port, handler, connectHandler) {
|
|
2436
2856
|
this.requestedPort = port;
|
|
2437
2857
|
this.handler = handler;
|
|
2858
|
+
this.connectHandler = connectHandler ?? null;
|
|
2438
2859
|
}
|
|
2439
2860
|
async start() {
|
|
2440
2861
|
let lastError = null;
|
|
@@ -2456,6 +2877,9 @@ var ProxyServer = class {
|
|
|
2456
2877
|
listen(port) {
|
|
2457
2878
|
return new Promise((resolve, reject) => {
|
|
2458
2879
|
const server = http.createServer(this.handler);
|
|
2880
|
+
if (this.connectHandler) {
|
|
2881
|
+
server.on("connect", this.connectHandler);
|
|
2882
|
+
}
|
|
2459
2883
|
server.on("error", reject);
|
|
2460
2884
|
server.listen(port, "127.0.0.1", () => {
|
|
2461
2885
|
server.removeListener("error", reject);
|
|
@@ -2480,6 +2904,10 @@ var ProxyServer = class {
|
|
|
2480
2904
|
getPort() {
|
|
2481
2905
|
return this.activePort;
|
|
2482
2906
|
}
|
|
2907
|
+
/** Expose internal HTTP server for MITM bridge socket injection */
|
|
2908
|
+
getHttpServer() {
|
|
2909
|
+
return this.server;
|
|
2910
|
+
}
|
|
2483
2911
|
};
|
|
2484
2912
|
|
|
2485
2913
|
// src/daemon/logger.ts
|
|
@@ -2530,26 +2958,710 @@ var FileLogger = class {
|
|
|
2530
2958
|
}
|
|
2531
2959
|
};
|
|
2532
2960
|
|
|
2533
|
-
// src/
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2961
|
+
// src/rsc/session-manager.ts
|
|
2962
|
+
init_pipeline();
|
|
2963
|
+
var DEFAULT_MAX_SESSIONS = 10;
|
|
2964
|
+
var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
2965
|
+
var EVICTION_INTERVAL_MS = 6e4;
|
|
2966
|
+
var SessionManager = class {
|
|
2967
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2968
|
+
config;
|
|
2969
|
+
evictionTimer = null;
|
|
2970
|
+
constructor(config) {
|
|
2971
|
+
this.config = {
|
|
2972
|
+
...config,
|
|
2973
|
+
maxSessions: config.maxSessions ?? DEFAULT_MAX_SESSIONS,
|
|
2974
|
+
sessionTtlMs: config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS
|
|
2975
|
+
};
|
|
2976
|
+
this.evictionTimer = setInterval(() => this.evictStale(), EVICTION_INTERVAL_MS);
|
|
2977
|
+
if (this.evictionTimer.unref) {
|
|
2978
|
+
this.evictionTimer.unref();
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
2982
|
+
getOrCreate(key) {
|
|
2983
|
+
const existing = this.sessions.get(key.raw);
|
|
2984
|
+
if (existing) {
|
|
2985
|
+
existing.lastAccessedAt = Date.now();
|
|
2986
|
+
existing.requestCount++;
|
|
2987
|
+
return existing.pipeline;
|
|
2988
|
+
}
|
|
2989
|
+
if (this.sessions.size >= this.config.maxSessions) {
|
|
2990
|
+
this.evictLRU();
|
|
2991
|
+
}
|
|
2992
|
+
const pipeline = new RSCPipelineWrapper({
|
|
2993
|
+
...this.config.pipelineConfig,
|
|
2994
|
+
sessionId: key.raw
|
|
2995
|
+
});
|
|
2996
|
+
this.sessions.set(key.raw, {
|
|
2997
|
+
pipeline,
|
|
2998
|
+
lastAccessedAt: Date.now(),
|
|
2999
|
+
requestCount: 1,
|
|
3000
|
+
connector: key.connector
|
|
3001
|
+
});
|
|
3002
|
+
this.config.onSessionCreated?.(key.raw, pipeline);
|
|
3003
|
+
return pipeline;
|
|
3004
|
+
}
|
|
3005
|
+
getAllSummaries() {
|
|
3006
|
+
const entries = [];
|
|
3007
|
+
for (const [key, managed] of this.sessions) {
|
|
3008
|
+
entries.push(this.buildHealthEntry(key, managed));
|
|
3009
|
+
}
|
|
3010
|
+
return entries;
|
|
3011
|
+
}
|
|
3012
|
+
getSessionSummary(key) {
|
|
3013
|
+
const managed = this.sessions.get(key);
|
|
3014
|
+
if (!managed) return null;
|
|
3015
|
+
return this.buildHealthEntry(key, managed);
|
|
3016
|
+
}
|
|
3017
|
+
get activeCount() {
|
|
3018
|
+
return this.sessions.size;
|
|
3019
|
+
}
|
|
3020
|
+
shutdown() {
|
|
3021
|
+
if (this.evictionTimer !== null) {
|
|
3022
|
+
clearInterval(this.evictionTimer);
|
|
3023
|
+
this.evictionTimer = null;
|
|
3024
|
+
}
|
|
3025
|
+
this.sessions.clear();
|
|
3026
|
+
}
|
|
3027
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
3028
|
+
buildHealthEntry(key, managed) {
|
|
3029
|
+
const summary = managed.pipeline.getSessionSummary();
|
|
3030
|
+
return {
|
|
3031
|
+
key,
|
|
3032
|
+
connector: managed.connector,
|
|
3033
|
+
circuitState: managed.pipeline.getCircuitState(),
|
|
3034
|
+
tokensProcessed: summary.tokensProcessed,
|
|
3035
|
+
tokensSaved: summary.tokensSaved,
|
|
3036
|
+
totalCalls: summary.totalCalls,
|
|
3037
|
+
compressedCalls: summary.compressedCalls,
|
|
3038
|
+
failedCalls: summary.failedCalls,
|
|
3039
|
+
lastAccessedAt: managed.lastAccessedAt
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
evictStale() {
|
|
3043
|
+
const now = Date.now();
|
|
3044
|
+
const cutoff = now - this.config.sessionTtlMs;
|
|
3045
|
+
for (const [key, managed] of this.sessions) {
|
|
3046
|
+
if (managed.lastAccessedAt < cutoff) {
|
|
3047
|
+
this.sessions.delete(key);
|
|
3048
|
+
this.config.onSessionEvicted?.(key);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
evictLRU() {
|
|
3053
|
+
let oldestKey = null;
|
|
3054
|
+
let oldestTime = Infinity;
|
|
3055
|
+
for (const [key, managed] of this.sessions) {
|
|
3056
|
+
if (managed.lastAccessedAt < oldestTime) {
|
|
3057
|
+
oldestTime = managed.lastAccessedAt;
|
|
3058
|
+
oldestKey = key;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (oldestKey !== null) {
|
|
3062
|
+
this.sessions.delete(oldestKey);
|
|
3063
|
+
this.config.onSessionEvicted?.(oldestKey);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
|
|
3068
|
+
// src/rsc/semaphore.ts
|
|
3069
|
+
var SemaphoreTimeoutError = class extends Error {
|
|
3070
|
+
constructor(timeoutMs) {
|
|
3071
|
+
super(`Semaphore acquire timed out after ${timeoutMs}ms`);
|
|
3072
|
+
this.name = "SemaphoreTimeoutError";
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
var Semaphore = class {
|
|
3076
|
+
permits;
|
|
3077
|
+
queue = [];
|
|
3078
|
+
constructor(maxPermits) {
|
|
3079
|
+
if (maxPermits < 1) throw new RangeError("maxPermits must be >= 1");
|
|
3080
|
+
this.permits = maxPermits;
|
|
3081
|
+
}
|
|
3082
|
+
get available() {
|
|
3083
|
+
return this.permits;
|
|
3084
|
+
}
|
|
3085
|
+
get waiting() {
|
|
3086
|
+
return this.queue.length;
|
|
3087
|
+
}
|
|
3088
|
+
acquire(timeoutMs) {
|
|
3089
|
+
if (this.permits > 0) {
|
|
3090
|
+
this.permits--;
|
|
3091
|
+
return Promise.resolve();
|
|
3092
|
+
}
|
|
3093
|
+
return new Promise((resolve, reject) => {
|
|
3094
|
+
const waiter = { resolve, reject };
|
|
3095
|
+
this.queue.push(waiter);
|
|
3096
|
+
if (timeoutMs !== void 0 && timeoutMs >= 0) {
|
|
3097
|
+
const timer = setTimeout(() => {
|
|
3098
|
+
const idx = this.queue.indexOf(waiter);
|
|
3099
|
+
if (idx !== -1) {
|
|
3100
|
+
this.queue.splice(idx, 1);
|
|
3101
|
+
reject(new SemaphoreTimeoutError(timeoutMs));
|
|
3102
|
+
}
|
|
3103
|
+
}, timeoutMs);
|
|
3104
|
+
const originalResolve = waiter.resolve;
|
|
3105
|
+
waiter.resolve = () => {
|
|
3106
|
+
clearTimeout(timer);
|
|
3107
|
+
originalResolve();
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
release() {
|
|
3113
|
+
const next = this.queue.shift();
|
|
3114
|
+
if (next) {
|
|
3115
|
+
next.resolve();
|
|
3116
|
+
} else {
|
|
3117
|
+
this.permits++;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
|
|
3122
|
+
// src/rsc/latency-monitor.ts
|
|
3123
|
+
var DEFAULT_CONFIG = {
|
|
3124
|
+
warningThresholdMs: 4e3,
|
|
3125
|
+
criticalThresholdMs: 8e3,
|
|
3126
|
+
windowSize: 50
|
|
3127
|
+
};
|
|
3128
|
+
var CircularBuffer = class {
|
|
3129
|
+
buffer;
|
|
3130
|
+
index = 0;
|
|
3131
|
+
count = 0;
|
|
3132
|
+
capacity;
|
|
3133
|
+
constructor(capacity) {
|
|
3134
|
+
this.capacity = capacity;
|
|
3135
|
+
this.buffer = new Array(capacity);
|
|
3136
|
+
}
|
|
3137
|
+
push(value) {
|
|
3138
|
+
this.buffer[this.index] = value;
|
|
3139
|
+
this.index = (this.index + 1) % this.capacity;
|
|
3140
|
+
if (this.count < this.capacity) {
|
|
3141
|
+
this.count++;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
getValues() {
|
|
3145
|
+
if (this.count < this.capacity) {
|
|
3146
|
+
return this.buffer.slice(0, this.count);
|
|
3147
|
+
}
|
|
3148
|
+
return [...this.buffer.slice(this.index), ...this.buffer.slice(0, this.index)];
|
|
3149
|
+
}
|
|
3150
|
+
get size() {
|
|
3151
|
+
return this.count;
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
function calculateP95(values) {
|
|
3155
|
+
if (values.length === 0) return null;
|
|
3156
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3157
|
+
const idx = Math.floor(sorted.length * 0.95);
|
|
3158
|
+
return sorted[Math.min(idx, sorted.length - 1)];
|
|
3159
|
+
}
|
|
3160
|
+
var LatencyMonitor = class {
|
|
3161
|
+
config;
|
|
3162
|
+
sessionWindows = /* @__PURE__ */ new Map();
|
|
3163
|
+
globalWindow;
|
|
3164
|
+
callbacks = [];
|
|
3165
|
+
constructor(config) {
|
|
3166
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3167
|
+
this.globalWindow = new CircularBuffer(this.config.windowSize * 4);
|
|
3168
|
+
}
|
|
3169
|
+
record(sessionKey, latencyMs) {
|
|
3170
|
+
let sessionBuf = this.sessionWindows.get(sessionKey);
|
|
3171
|
+
if (!sessionBuf) {
|
|
3172
|
+
sessionBuf = new CircularBuffer(this.config.windowSize);
|
|
3173
|
+
this.sessionWindows.set(sessionKey, sessionBuf);
|
|
3174
|
+
}
|
|
3175
|
+
sessionBuf.push(latencyMs);
|
|
3176
|
+
this.globalWindow.push(latencyMs);
|
|
3177
|
+
const globalP95 = calculateP95(this.globalWindow.getValues());
|
|
3178
|
+
if (globalP95 === null) return null;
|
|
3179
|
+
let alert = null;
|
|
3180
|
+
if (globalP95 >= this.config.criticalThresholdMs) {
|
|
3181
|
+
alert = {
|
|
3182
|
+
type: "critical",
|
|
3183
|
+
message: `Global p95 latency ${globalP95.toFixed(0)}ms exceeds critical threshold ${this.config.criticalThresholdMs}ms`,
|
|
3184
|
+
sessionKey,
|
|
3185
|
+
p95Ms: globalP95,
|
|
3186
|
+
thresholdMs: this.config.criticalThresholdMs,
|
|
3187
|
+
activeSessions: this.sessionWindows.size,
|
|
3188
|
+
suggestion: "Reduce active sessions or increase latency budget"
|
|
3189
|
+
};
|
|
3190
|
+
} else if (globalP95 >= this.config.warningThresholdMs) {
|
|
3191
|
+
alert = {
|
|
3192
|
+
type: "warning",
|
|
3193
|
+
message: `Global p95 latency ${globalP95.toFixed(0)}ms exceeds warning threshold ${this.config.warningThresholdMs}ms`,
|
|
3194
|
+
sessionKey,
|
|
3195
|
+
p95Ms: globalP95,
|
|
3196
|
+
thresholdMs: this.config.warningThresholdMs,
|
|
3197
|
+
activeSessions: this.sessionWindows.size,
|
|
3198
|
+
suggestion: "Consider reducing active sessions"
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
if (alert) {
|
|
3202
|
+
for (const cb of this.callbacks) {
|
|
3203
|
+
cb(alert);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
return alert;
|
|
3207
|
+
}
|
|
3208
|
+
getSessionP95(sessionKey) {
|
|
3209
|
+
const buf = this.sessionWindows.get(sessionKey);
|
|
3210
|
+
if (!buf) return null;
|
|
3211
|
+
return calculateP95(buf.getValues());
|
|
3212
|
+
}
|
|
3213
|
+
getGlobalP95() {
|
|
3214
|
+
return calculateP95(this.globalWindow.getValues());
|
|
3215
|
+
}
|
|
3216
|
+
onAlert(cb) {
|
|
3217
|
+
this.callbacks.push(cb);
|
|
3218
|
+
}
|
|
3219
|
+
get sessionCount() {
|
|
3220
|
+
return this.sessionWindows.size;
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
|
|
3224
|
+
// src/tls/connect-handler.ts
|
|
3225
|
+
import * as net from "net";
|
|
3226
|
+
|
|
3227
|
+
// src/tls/allowlist.ts
|
|
3228
|
+
var MITM_HOSTS = /* @__PURE__ */ new Set([
|
|
3229
|
+
"api.openai.com",
|
|
3230
|
+
"api.anthropic.com",
|
|
3231
|
+
"generativelanguage.googleapis.com"
|
|
3232
|
+
]);
|
|
3233
|
+
function shouldIntercept(hostname) {
|
|
3234
|
+
return MITM_HOSTS.has(hostname);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// src/tls/connect-handler.ts
|
|
3238
|
+
function createConnectHandler(options) {
|
|
3239
|
+
const { logger, onIntercept, onPassthrough } = options;
|
|
3240
|
+
return (req, clientSocket, head) => {
|
|
3241
|
+
const target = req.url ?? "";
|
|
3242
|
+
const [hostname, portStr] = parseConnectTarget(target);
|
|
3243
|
+
const port = parseInt(portStr, 10) || 443;
|
|
3244
|
+
if (!hostname) {
|
|
3245
|
+
logger.log(`[CONNECT] Invalid target: ${target}`);
|
|
3246
|
+
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
3247
|
+
clientSocket.destroy();
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
if (shouldIntercept(hostname) && onIntercept) {
|
|
3251
|
+
logger.log(`[CONNECT] ${hostname}:${port} \u2192 intercept`);
|
|
3252
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
3253
|
+
if (head.length > 0) {
|
|
3254
|
+
clientSocket.unshift(head);
|
|
3255
|
+
}
|
|
3256
|
+
onIntercept(clientSocket, hostname, port);
|
|
3257
|
+
} else {
|
|
3258
|
+
logger.log(`[TUNNEL] ${hostname}:${port} \u2192 passthrough`);
|
|
3259
|
+
onPassthrough?.();
|
|
3260
|
+
const upstreamSocket = net.connect(port, hostname, () => {
|
|
3261
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
3262
|
+
if (head.length > 0) {
|
|
3263
|
+
upstreamSocket.write(head);
|
|
3264
|
+
}
|
|
3265
|
+
clientSocket.pipe(upstreamSocket);
|
|
3266
|
+
upstreamSocket.pipe(clientSocket);
|
|
3267
|
+
});
|
|
3268
|
+
upstreamSocket.on("error", (err) => {
|
|
3269
|
+
logger.log(`[TUNNEL] ${hostname}:${port} upstream error: ${err.message}`);
|
|
3270
|
+
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
3271
|
+
clientSocket.destroy();
|
|
3272
|
+
});
|
|
3273
|
+
clientSocket.on("error", (err) => {
|
|
3274
|
+
logger.log(`[TUNNEL] ${hostname}:${port} client error: ${err.message}`);
|
|
3275
|
+
upstreamSocket.destroy();
|
|
3276
|
+
});
|
|
3277
|
+
clientSocket.on("close", () => upstreamSocket.destroy());
|
|
3278
|
+
upstreamSocket.on("close", () => clientSocket.destroy());
|
|
3279
|
+
}
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
function parseConnectTarget(target) {
|
|
3283
|
+
const colonIdx = target.lastIndexOf(":");
|
|
3284
|
+
if (colonIdx === -1) return [target, "443"];
|
|
3285
|
+
return [target.slice(0, colonIdx), target.slice(colonIdx + 1)];
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
// src/tls/ca.ts
|
|
3289
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
|
|
3290
|
+
import { join as join5 } from "path";
|
|
3291
|
+
import forge from "node-forge";
|
|
3292
|
+
var CA_CERT_PATH = join5(LIMINAL_DIR, "ca.pem");
|
|
3293
|
+
var CA_KEY_PATH = join5(LIMINAL_DIR, "ca-key.pem");
|
|
3294
|
+
function generateCA() {
|
|
3295
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
3296
|
+
const cert = forge.pki.createCertificate();
|
|
3297
|
+
cert.publicKey = keys.publicKey;
|
|
3298
|
+
cert.serialNumber = generateSerialNumber();
|
|
3299
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
3300
|
+
cert.validity.notAfter = /* @__PURE__ */ new Date();
|
|
3301
|
+
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 5);
|
|
3302
|
+
const attrs = [
|
|
3303
|
+
{ name: "commonName", value: "Liminal Proxy CA" },
|
|
3304
|
+
{ name: "organizationName", value: "Liminal (Cognisos)" },
|
|
3305
|
+
{ shortName: "OU", value: "Local Development" }
|
|
3306
|
+
];
|
|
3307
|
+
cert.setSubject(attrs);
|
|
3308
|
+
cert.setIssuer(attrs);
|
|
3309
|
+
cert.setExtensions([
|
|
3310
|
+
{ name: "basicConstraints", cA: true, critical: true },
|
|
3311
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
|
|
3312
|
+
{
|
|
3313
|
+
name: "subjectKeyIdentifier"
|
|
3314
|
+
}
|
|
3315
|
+
]);
|
|
3316
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
3317
|
+
return {
|
|
3318
|
+
certPem: forge.pki.certificateToPem(cert),
|
|
3319
|
+
keyPem: forge.pki.privateKeyToPem(keys.privateKey)
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
function generateAndSaveCA() {
|
|
3323
|
+
if (!existsSync6(LIMINAL_DIR)) {
|
|
3324
|
+
mkdirSync3(LIMINAL_DIR, { recursive: true, mode: 448 });
|
|
3325
|
+
}
|
|
3326
|
+
const { certPem, keyPem } = generateCA();
|
|
3327
|
+
writeFileSync3(CA_CERT_PATH, certPem, { encoding: "utf-8", mode: 420 });
|
|
3328
|
+
writeFileSync3(CA_KEY_PATH, keyPem, { encoding: "utf-8", mode: 384 });
|
|
3329
|
+
return { certPem, keyPem };
|
|
3330
|
+
}
|
|
3331
|
+
function loadCA() {
|
|
3332
|
+
if (!existsSync6(CA_CERT_PATH) || !existsSync6(CA_KEY_PATH)) {
|
|
3333
|
+
return null;
|
|
3334
|
+
}
|
|
3335
|
+
return {
|
|
3336
|
+
certPem: readFileSync4(CA_CERT_PATH, "utf-8"),
|
|
3337
|
+
keyPem: readFileSync4(CA_KEY_PATH, "utf-8")
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
function ensureCA() {
|
|
3341
|
+
const existing = loadCA();
|
|
3342
|
+
if (existing) return existing;
|
|
3343
|
+
return generateAndSaveCA();
|
|
3344
|
+
}
|
|
3345
|
+
function hasCA() {
|
|
3346
|
+
return existsSync6(CA_CERT_PATH) && existsSync6(CA_KEY_PATH);
|
|
3347
|
+
}
|
|
3348
|
+
function removeCA() {
|
|
3349
|
+
if (existsSync6(CA_CERT_PATH)) unlinkSync(CA_CERT_PATH);
|
|
3350
|
+
if (existsSync6(CA_KEY_PATH)) unlinkSync(CA_KEY_PATH);
|
|
3351
|
+
}
|
|
3352
|
+
function getCAInfo() {
|
|
3353
|
+
const ca = loadCA();
|
|
3354
|
+
if (!ca) return null;
|
|
3355
|
+
const cert = forge.pki.certificateFromPem(ca.certPem);
|
|
3356
|
+
const cn = cert.subject.getField("CN");
|
|
3357
|
+
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
|
|
3358
|
+
const md = forge.md.sha256.create();
|
|
3359
|
+
md.update(der);
|
|
3360
|
+
const fingerprint = md.digest().toHex().match(/.{2}/g).join(":").toUpperCase();
|
|
3361
|
+
return {
|
|
3362
|
+
commonName: cn ? cn.value : "Unknown",
|
|
3363
|
+
validFrom: cert.validity.notBefore,
|
|
3364
|
+
validTo: cert.validity.notAfter,
|
|
3365
|
+
fingerprint
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
function generateSerialNumber() {
|
|
3369
|
+
const bytes = forge.random.getBytesSync(16);
|
|
3370
|
+
return forge.util.bytesToHex(bytes);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/tls/trust.ts
|
|
3374
|
+
import { execSync as execSync3 } from "child_process";
|
|
3375
|
+
import { existsSync as existsSync7, copyFileSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3376
|
+
function installCA() {
|
|
3377
|
+
if (!existsSync7(CA_CERT_PATH)) {
|
|
3378
|
+
return { success: false, message: 'CA certificate not found. Run "liminal init" first.', requiresSudo: false };
|
|
3379
|
+
}
|
|
3380
|
+
const platform = process.platform;
|
|
3381
|
+
if (platform === "darwin") return installMacOS();
|
|
3382
|
+
if (platform === "linux") return installLinux();
|
|
3383
|
+
if (platform === "win32") return installWindows();
|
|
3384
|
+
return { success: false, message: `Unsupported platform: ${platform}`, requiresSudo: false };
|
|
3385
|
+
}
|
|
3386
|
+
function removeCA2() {
|
|
3387
|
+
const platform = process.platform;
|
|
3388
|
+
if (platform === "darwin") return removeMacOS();
|
|
3389
|
+
if (platform === "linux") return removeLinux();
|
|
3390
|
+
if (platform === "win32") return removeWindows();
|
|
3391
|
+
return { success: false, message: `Unsupported platform: ${platform}`, requiresSudo: false };
|
|
3392
|
+
}
|
|
3393
|
+
function isCATrusted() {
|
|
3394
|
+
const platform = process.platform;
|
|
3395
|
+
if (platform === "darwin") return isTrustedMacOS();
|
|
3396
|
+
if (platform === "linux") return isTrustedLinux();
|
|
3397
|
+
if (platform === "win32") return isTrustedWindows();
|
|
3398
|
+
return false;
|
|
3399
|
+
}
|
|
3400
|
+
var MACOS_LABEL = "Liminal Proxy CA";
|
|
3401
|
+
function installMacOS() {
|
|
3402
|
+
try {
|
|
3403
|
+
execSync3(
|
|
3404
|
+
`security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db "${CA_CERT_PATH}"`,
|
|
3405
|
+
{ stdio: "pipe" }
|
|
3406
|
+
);
|
|
3407
|
+
return { success: true, message: "CA installed in login keychain (trusted for SSL)", requiresSudo: false };
|
|
3408
|
+
} catch (err) {
|
|
3409
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3410
|
+
if (msg.includes("authorization") || msg.includes("permission")) {
|
|
3411
|
+
return {
|
|
3412
|
+
success: false,
|
|
3413
|
+
message: "Keychain access denied. You may need to unlock your keychain or run with sudo.",
|
|
3414
|
+
requiresSudo: true
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: false };
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
function removeMacOS() {
|
|
3421
|
+
try {
|
|
3422
|
+
execSync3(
|
|
3423
|
+
`security delete-certificate -c "${MACOS_LABEL}" ~/Library/Keychains/login.keychain-db`,
|
|
3424
|
+
{ stdio: "pipe" }
|
|
3425
|
+
);
|
|
3426
|
+
return { success: true, message: "CA removed from login keychain", requiresSudo: false };
|
|
3427
|
+
} catch (err) {
|
|
3428
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3429
|
+
if (msg.includes("could not be found")) {
|
|
3430
|
+
return { success: true, message: "CA was not in keychain (already removed)", requiresSudo: false };
|
|
3431
|
+
}
|
|
3432
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: false };
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
function isTrustedMacOS() {
|
|
3436
|
+
try {
|
|
3437
|
+
const out = execSync3(
|
|
3438
|
+
`security find-certificate -c "${MACOS_LABEL}" ~/Library/Keychains/login.keychain-db`,
|
|
3439
|
+
{ stdio: "pipe", encoding: "utf-8" }
|
|
3440
|
+
);
|
|
3441
|
+
return out.includes(MACOS_LABEL);
|
|
3442
|
+
} catch {
|
|
3443
|
+
return false;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
var LINUX_CERT_PATH = "/usr/local/share/ca-certificates/liminal-proxy-ca.crt";
|
|
3447
|
+
function installLinux() {
|
|
3448
|
+
try {
|
|
3449
|
+
copyFileSync(CA_CERT_PATH, LINUX_CERT_PATH);
|
|
3450
|
+
execSync3("update-ca-certificates", { stdio: "pipe" });
|
|
3451
|
+
return { success: true, message: "CA installed in system trust store", requiresSudo: true };
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3454
|
+
if (msg.includes("EACCES") || msg.includes("permission")) {
|
|
3455
|
+
return {
|
|
3456
|
+
success: false,
|
|
3457
|
+
message: `Permission denied. Run with sudo:
|
|
3458
|
+
sudo liminal trust-ca`,
|
|
3459
|
+
requiresSudo: true
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: true };
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
function removeLinux() {
|
|
3466
|
+
try {
|
|
3467
|
+
if (existsSync7(LINUX_CERT_PATH)) {
|
|
3468
|
+
unlinkSync2(LINUX_CERT_PATH);
|
|
3469
|
+
execSync3("update-ca-certificates --fresh", { stdio: "pipe" });
|
|
3470
|
+
}
|
|
3471
|
+
return { success: true, message: "CA removed from system trust store", requiresSudo: true };
|
|
3472
|
+
} catch (err) {
|
|
3473
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3474
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: true };
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
function isTrustedLinux() {
|
|
3478
|
+
return existsSync7(LINUX_CERT_PATH);
|
|
3479
|
+
}
|
|
3480
|
+
function installWindows() {
|
|
3481
|
+
try {
|
|
3482
|
+
execSync3(`certutil -addstore -user -f "ROOT" "${CA_CERT_PATH}"`, { stdio: "pipe" });
|
|
3483
|
+
return { success: true, message: "CA installed in user certificate store", requiresSudo: false };
|
|
3484
|
+
} catch (err) {
|
|
3485
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3486
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: false };
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
function removeWindows() {
|
|
3490
|
+
try {
|
|
3491
|
+
execSync3(`certutil -delstore -user "ROOT" "${MACOS_LABEL}"`, { stdio: "pipe" });
|
|
3492
|
+
return { success: true, message: "CA removed from user certificate store", requiresSudo: false };
|
|
3493
|
+
} catch (err) {
|
|
3494
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3495
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: false };
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
function isTrustedWindows() {
|
|
3499
|
+
try {
|
|
3500
|
+
const out = execSync3(`certutil -verifystore -user "ROOT" "${MACOS_LABEL}"`, {
|
|
3501
|
+
stdio: "pipe",
|
|
3502
|
+
encoding: "utf-8"
|
|
3503
|
+
});
|
|
3504
|
+
return out.includes("Liminal");
|
|
3505
|
+
} catch {
|
|
3506
|
+
return false;
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// src/tls/mitm-bridge.ts
|
|
3511
|
+
import * as tls from "tls";
|
|
3512
|
+
|
|
3513
|
+
// src/tls/cert-generator.ts
|
|
3514
|
+
import forge2 from "node-forge";
|
|
3515
|
+
var CERT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3516
|
+
var MAX_CACHE_SIZE = 50;
|
|
3517
|
+
var CertGenerator = class {
|
|
3518
|
+
caCert;
|
|
3519
|
+
caKey;
|
|
3520
|
+
cache = /* @__PURE__ */ new Map();
|
|
3521
|
+
constructor(caCertPem, caKeyPem) {
|
|
3522
|
+
this.caCert = forge2.pki.certificateFromPem(caCertPem);
|
|
3523
|
+
this.caKey = forge2.pki.privateKeyFromPem(caKeyPem);
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Get or generate a TLS certificate for the given hostname.
|
|
3527
|
+
* Certificates are cached for 24 hours.
|
|
3528
|
+
*/
|
|
3529
|
+
getCert(hostname) {
|
|
3530
|
+
const now = Date.now();
|
|
3531
|
+
const cached = this.cache.get(hostname);
|
|
3532
|
+
if (cached && cached.expiresAt > now) {
|
|
3533
|
+
return cached.cert;
|
|
3534
|
+
}
|
|
3535
|
+
const cert = this.generate(hostname);
|
|
3536
|
+
if (this.cache.size >= MAX_CACHE_SIZE) {
|
|
3537
|
+
const oldest = this.cache.keys().next().value;
|
|
3538
|
+
if (oldest) this.cache.delete(oldest);
|
|
3539
|
+
}
|
|
3540
|
+
this.cache.set(hostname, { cert, expiresAt: now + CERT_TTL_MS });
|
|
3541
|
+
return cert;
|
|
3542
|
+
}
|
|
3543
|
+
get cacheSize() {
|
|
3544
|
+
return this.cache.size;
|
|
3545
|
+
}
|
|
3546
|
+
generate(hostname) {
|
|
3547
|
+
const keys = forge2.pki.rsa.generateKeyPair(2048);
|
|
3548
|
+
const cert = forge2.pki.createCertificate();
|
|
3549
|
+
cert.publicKey = keys.publicKey;
|
|
3550
|
+
cert.serialNumber = randomSerial();
|
|
3551
|
+
cert.validity.notBefore = new Date(Date.now() - 24 * 60 * 60 * 1e3);
|
|
3552
|
+
cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1e3);
|
|
3553
|
+
cert.setSubject([
|
|
3554
|
+
{ name: "commonName", value: hostname },
|
|
3555
|
+
{ name: "organizationName", value: "Liminal Proxy (local)" }
|
|
3556
|
+
]);
|
|
3557
|
+
cert.setIssuer(this.caCert.subject.attributes);
|
|
3558
|
+
cert.setExtensions([
|
|
3559
|
+
{ name: "basicConstraints", cA: false },
|
|
3560
|
+
{
|
|
3561
|
+
name: "keyUsage",
|
|
3562
|
+
digitalSignature: true,
|
|
3563
|
+
keyEncipherment: true,
|
|
3564
|
+
critical: true
|
|
3565
|
+
},
|
|
3566
|
+
{
|
|
3567
|
+
name: "extKeyUsage",
|
|
3568
|
+
serverAuth: true
|
|
3569
|
+
},
|
|
3570
|
+
{
|
|
3571
|
+
name: "subjectAltName",
|
|
3572
|
+
altNames: [{ type: 2, value: hostname }]
|
|
3573
|
+
// DNS name
|
|
3574
|
+
}
|
|
3575
|
+
]);
|
|
3576
|
+
cert.sign(this.caKey, forge2.md.sha256.create());
|
|
3577
|
+
return {
|
|
3578
|
+
certPem: forge2.pki.certificateToPem(cert),
|
|
3579
|
+
keyPem: forge2.pki.privateKeyToPem(keys.privateKey)
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
function randomSerial() {
|
|
3584
|
+
return forge2.util.bytesToHex(forge2.random.getBytesSync(16));
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/tls/mitm-bridge.ts
|
|
3588
|
+
function createMitmBridge(options) {
|
|
3589
|
+
const { httpServer, caCertPem, caKeyPem, logger } = options;
|
|
3590
|
+
const certGen = new CertGenerator(caCertPem, caKeyPem);
|
|
3591
|
+
return (clientSocket, hostname, _port) => {
|
|
3592
|
+
try {
|
|
3593
|
+
const { certPem, keyPem } = certGen.getCert(hostname);
|
|
3594
|
+
const tlsSocket = new tls.TLSSocket(clientSocket, {
|
|
3595
|
+
isServer: true,
|
|
3596
|
+
key: keyPem,
|
|
3597
|
+
cert: certPem
|
|
3598
|
+
});
|
|
3599
|
+
tlsSocket.on("error", (err) => {
|
|
3600
|
+
logger.log(`[MITM] TLS error for ${hostname}: ${err.message}`);
|
|
3601
|
+
tlsSocket.destroy();
|
|
3602
|
+
});
|
|
3603
|
+
httpServer.emit("connection", tlsSocket);
|
|
3604
|
+
logger.log(`[MITM] TLS bridge established for ${hostname} (cert cache: ${certGen.cacheSize})`);
|
|
3605
|
+
} catch (err) {
|
|
3606
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3607
|
+
logger.log(`[MITM] Failed to establish bridge for ${hostname}: ${message}`);
|
|
3608
|
+
clientSocket.destroy();
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
// src/tls/mitm-stats.ts
|
|
3614
|
+
var MitmStats = class {
|
|
3615
|
+
_intercepted = 0;
|
|
3616
|
+
_passthrough = 0;
|
|
3617
|
+
_activeBridges = 0;
|
|
3618
|
+
_hosts = /* @__PURE__ */ new Set();
|
|
3619
|
+
_enabled = false;
|
|
3620
|
+
enable() {
|
|
3621
|
+
this._enabled = true;
|
|
3622
|
+
}
|
|
3623
|
+
recordIntercept(hostname) {
|
|
3624
|
+
this._intercepted++;
|
|
3625
|
+
this._activeBridges++;
|
|
3626
|
+
this._hosts.add(hostname);
|
|
3627
|
+
}
|
|
3628
|
+
recordBridgeClosed() {
|
|
3629
|
+
if (this._activeBridges > 0) this._activeBridges--;
|
|
3630
|
+
}
|
|
3631
|
+
recordPassthrough() {
|
|
3632
|
+
this._passthrough++;
|
|
3633
|
+
}
|
|
3634
|
+
snapshot() {
|
|
3635
|
+
return {
|
|
3636
|
+
intercepted: this._intercepted,
|
|
3637
|
+
passthrough: this._passthrough,
|
|
3638
|
+
activeBridges: this._activeBridges,
|
|
3639
|
+
hostsIntercepted: [...this._hosts],
|
|
3640
|
+
enabled: this._enabled
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
|
|
3645
|
+
// src/daemon/lifecycle.ts
|
|
3646
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
3647
|
+
import { fork } from "child_process";
|
|
3648
|
+
import { fileURLToPath } from "url";
|
|
3649
|
+
function writePidFile(pid) {
|
|
3650
|
+
writeFileSync4(PID_FILE, String(pid), "utf-8");
|
|
3651
|
+
}
|
|
3652
|
+
function readPidFile() {
|
|
3653
|
+
if (!existsSync8(PID_FILE)) return null;
|
|
3654
|
+
try {
|
|
3655
|
+
const content = readFileSync5(PID_FILE, "utf-8").trim();
|
|
3656
|
+
const pid = parseInt(content, 10);
|
|
3657
|
+
return isNaN(pid) ? null : pid;
|
|
3658
|
+
} catch {
|
|
3659
|
+
return null;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
2550
3662
|
function removePidFile() {
|
|
2551
3663
|
try {
|
|
2552
|
-
if (
|
|
3664
|
+
if (existsSync8(PID_FILE)) unlinkSync3(PID_FILE);
|
|
2553
3665
|
} catch {
|
|
2554
3666
|
}
|
|
2555
3667
|
}
|
|
@@ -2651,37 +3763,87 @@ async function startCommand(flags) {
|
|
|
2651
3763
|
rscBaseUrl: config.apiBaseUrl,
|
|
2652
3764
|
proxyPort: config.port,
|
|
2653
3765
|
compressionThreshold: config.compressionThreshold,
|
|
3766
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
3767
|
+
hotFraction: config.hotFraction,
|
|
3768
|
+
coldFraction: config.coldFraction,
|
|
2654
3769
|
compressRoles: config.compressRoles,
|
|
3770
|
+
compressToolResults: config.compressToolResults,
|
|
2655
3771
|
learnFromResponses: config.learnFromResponses,
|
|
2656
3772
|
latencyBudgetMs: config.latencyBudgetMs || void 0,
|
|
2657
3773
|
upstreamBaseUrl: config.upstreamBaseUrl,
|
|
2658
3774
|
anthropicUpstreamUrl: config.anthropicUpstreamUrl,
|
|
2659
3775
|
enabled: config.enabled,
|
|
2660
|
-
tools: config.tools
|
|
3776
|
+
tools: config.tools,
|
|
3777
|
+
concurrencyLimit: config.concurrencyLimit,
|
|
3778
|
+
concurrencyTimeoutMs: config.concurrencyTimeoutMs,
|
|
3779
|
+
maxSessions: config.maxSessions,
|
|
3780
|
+
sessionTtlMs: config.sessionTtlMs,
|
|
3781
|
+
latencyWarningMs: config.latencyWarningMs,
|
|
3782
|
+
latencyCriticalMs: config.latencyCriticalMs
|
|
2661
3783
|
};
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
learnFromResponses: config.learnFromResponses,
|
|
2667
|
-
latencyBudgetMs: config.latencyBudgetMs || void 0
|
|
3784
|
+
const semaphore = new Semaphore(resolvedConfig.concurrencyLimit);
|
|
3785
|
+
const latencyMonitor = new LatencyMonitor({
|
|
3786
|
+
warningThresholdMs: resolvedConfig.latencyWarningMs,
|
|
3787
|
+
criticalThresholdMs: resolvedConfig.latencyCriticalMs
|
|
2668
3788
|
});
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
3789
|
+
function wireSessionEvents(key, pipeline) {
|
|
3790
|
+
pipeline.events.on("compression", (event) => {
|
|
3791
|
+
if (event.tokensSaved > 0) {
|
|
3792
|
+
logger.log(`[LIMINAL] [${key}] Compressed: ${event.tokensSaved} tokens saved (${event.ratio.toFixed(3)} ratio)`);
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3795
|
+
pipeline.events.on("compression_skipped", (event) => {
|
|
3796
|
+
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)`;
|
|
3797
|
+
logger.log(`[LIMINAL] [${key}] Skipped: ${detail}`);
|
|
3798
|
+
});
|
|
3799
|
+
pipeline.events.on("error", (event) => {
|
|
3800
|
+
logger.log(`[LIMINAL] [${key}] Error: ${event.error.message}`);
|
|
3801
|
+
});
|
|
3802
|
+
pipeline.events.on("degradation", (event) => {
|
|
3803
|
+
logger.log(`[LIMINAL] [${key}] Circuit ${event.circuitState}: ${event.reason}`);
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
const sessions = new SessionManager({
|
|
3807
|
+
pipelineConfig: {
|
|
3808
|
+
rscApiKey: config.apiKey,
|
|
3809
|
+
rscBaseUrl: config.apiBaseUrl,
|
|
3810
|
+
compressionThreshold: config.compressionThreshold,
|
|
3811
|
+
learnFromResponses: config.learnFromResponses,
|
|
3812
|
+
latencyBudgetMs: config.latencyBudgetMs || void 0
|
|
3813
|
+
},
|
|
3814
|
+
maxSessions: resolvedConfig.maxSessions,
|
|
3815
|
+
sessionTtlMs: resolvedConfig.sessionTtlMs,
|
|
3816
|
+
onSessionCreated: (key, pipeline) => {
|
|
3817
|
+
logger.log(`[SESSION] Created: ${key}`);
|
|
3818
|
+
wireSessionEvents(key, pipeline);
|
|
3819
|
+
},
|
|
3820
|
+
onSessionEvicted: (key) => {
|
|
3821
|
+
logger.log(`[SESSION] Evicted: ${key} (idle)`);
|
|
2672
3822
|
}
|
|
2673
3823
|
});
|
|
2674
|
-
|
|
2675
|
-
logger.log(`[
|
|
2676
|
-
});
|
|
2677
|
-
pipeline.events.on("error", (event) => {
|
|
2678
|
-
logger.log(`[LIMINAL] Error: ${event.error.message}`);
|
|
3824
|
+
latencyMonitor.onAlert((alert) => {
|
|
3825
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message} (${alert.activeSessions} sessions) \u2014 ${alert.suggestion}`);
|
|
2679
3826
|
});
|
|
2680
|
-
|
|
2681
|
-
|
|
3827
|
+
const mitmStats = new MitmStats();
|
|
3828
|
+
const deps = { sessions, semaphore, latencyMonitor, mitmStats, config: resolvedConfig, logger };
|
|
3829
|
+
const handler = createRequestHandler(deps);
|
|
3830
|
+
let mitmHandler;
|
|
3831
|
+
const connectHandler = createConnectHandler({
|
|
3832
|
+
logger,
|
|
3833
|
+
onIntercept: (socket, hostname, port) => {
|
|
3834
|
+
if (mitmHandler) {
|
|
3835
|
+
mitmStats.recordIntercept(hostname);
|
|
3836
|
+
mitmHandler(socket, hostname, port);
|
|
3837
|
+
} else {
|
|
3838
|
+
logger.log(`[MITM] No bridge available for ${hostname} \u2014 falling back to passthrough`);
|
|
3839
|
+
mitmStats.recordPassthrough();
|
|
3840
|
+
}
|
|
3841
|
+
},
|
|
3842
|
+
onPassthrough: () => {
|
|
3843
|
+
mitmStats.recordPassthrough();
|
|
3844
|
+
}
|
|
2682
3845
|
});
|
|
2683
|
-
const
|
|
2684
|
-
const server = new ProxyServer(config.port, handler);
|
|
3846
|
+
const server = new ProxyServer(config.port, handler, connectHandler);
|
|
2685
3847
|
setupSignalHandlers(server, logger);
|
|
2686
3848
|
try {
|
|
2687
3849
|
const actualPort = await server.start();
|
|
@@ -2691,15 +3853,43 @@ async function startCommand(flags) {
|
|
|
2691
3853
|
logger.log(`[DAEMON] Upstream (Anthropic): ${config.anthropicUpstreamUrl}`);
|
|
2692
3854
|
logger.log(`[DAEMON] Liminal API: ${config.apiBaseUrl}`);
|
|
2693
3855
|
logger.log(`[DAEMON] PID: ${process.pid}`);
|
|
3856
|
+
logger.log(`[DAEMON] Max sessions: ${resolvedConfig.maxSessions}, Concurrency limit: ${resolvedConfig.concurrencyLimit}`);
|
|
3857
|
+
const caReady = hasCA() && isCATrusted();
|
|
3858
|
+
if (caReady) {
|
|
3859
|
+
const httpServer = server.getHttpServer();
|
|
3860
|
+
const ca = loadCA();
|
|
3861
|
+
if (httpServer && ca) {
|
|
3862
|
+
mitmHandler = createMitmBridge({
|
|
3863
|
+
httpServer,
|
|
3864
|
+
caCertPem: ca.certPem,
|
|
3865
|
+
caKeyPem: ca.keyPem,
|
|
3866
|
+
logger
|
|
3867
|
+
});
|
|
3868
|
+
mitmStats.enable();
|
|
3869
|
+
logger.log("[MITM] TLS bridge active \u2014 intercepting LLM API calls");
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
logger.log(`[DAEMON] CONNECT handler: active | MITM: ${caReady ? "ready (CA trusted)" : "passthrough only (run liminal trust-ca)"}`);
|
|
2694
3873
|
if (isForeground && !isForked) {
|
|
2695
3874
|
printBanner();
|
|
2696
3875
|
console.log(` Liminal proxy running on http://127.0.0.1:${actualPort}/v1`);
|
|
2697
3876
|
console.log(` Upstream: ${config.upstreamBaseUrl}`);
|
|
3877
|
+
console.log(` Max sessions: ${resolvedConfig.maxSessions} | Concurrency: ${resolvedConfig.concurrencyLimit}`);
|
|
3878
|
+
if (caReady) {
|
|
3879
|
+
console.log(" MITM: active (Cursor interception ready)");
|
|
3880
|
+
}
|
|
2698
3881
|
console.log();
|
|
2699
3882
|
console.log(" Point your AI tool's base URL here. Press Ctrl+C to stop.");
|
|
2700
3883
|
console.log();
|
|
2701
3884
|
}
|
|
2702
|
-
const
|
|
3885
|
+
const { RSCPipelineWrapper: RSCPipelineWrapper2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
3886
|
+
const probe = new RSCPipelineWrapper2({
|
|
3887
|
+
rscApiKey: config.apiKey,
|
|
3888
|
+
rscBaseUrl: config.apiBaseUrl,
|
|
3889
|
+
compressionThreshold: config.compressionThreshold,
|
|
3890
|
+
learnFromResponses: false
|
|
3891
|
+
});
|
|
3892
|
+
const healthy = await probe.healthCheck();
|
|
2703
3893
|
if (healthy) {
|
|
2704
3894
|
logger.log("[DAEMON] Liminal API health check: OK");
|
|
2705
3895
|
} else {
|
|
@@ -2766,19 +3956,50 @@ async function statusCommand() {
|
|
|
2766
3956
|
const data = await res.json();
|
|
2767
3957
|
const uptime = formatUptime(data.uptime_ms);
|
|
2768
3958
|
console.log(`Liminal Daemon: running (PID ${state.pid}, port ${port})`);
|
|
2769
|
-
console.log(`
|
|
2770
|
-
console.log(`Session: ${data.session_id}`);
|
|
3959
|
+
console.log(`Status: ${data.status} (${data.version})`);
|
|
2771
3960
|
console.log(`Uptime: ${uptime}`);
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3961
|
+
console.log();
|
|
3962
|
+
const c = data.concurrency;
|
|
3963
|
+
console.log(`Sessions: ${c.active_sessions} active (max ${config.maxSessions})`);
|
|
3964
|
+
console.log(`Semaphore: ${c.semaphore_available}/${c.max_concurrent_rsc_calls} available` + (c.semaphore_waiting > 0 ? ` (${c.semaphore_waiting} waiting)` : ""));
|
|
3965
|
+
const globalP95 = data.latency.global_p95_ms;
|
|
3966
|
+
if (globalP95 !== null) {
|
|
3967
|
+
const latencyFlag = globalP95 >= config.latencyCriticalMs ? " CRITICAL" : globalP95 >= config.latencyWarningMs ? " WARNING" : "";
|
|
3968
|
+
console.log(`Latency: p95 ${globalP95.toFixed(0)}ms${latencyFlag}`);
|
|
3969
|
+
}
|
|
3970
|
+
if (data.mitm) {
|
|
3971
|
+
const m = data.mitm;
|
|
3972
|
+
if (m.enabled) {
|
|
3973
|
+
console.log(`MITM: active (${m.intercepted} intercepted, ${m.passthrough} passthrough, ${m.activeBridges} active)`);
|
|
3974
|
+
if (m.hostsIntercepted.length > 0) {
|
|
3975
|
+
console.log(` Hosts: ${m.hostsIntercepted.join(", ")}`);
|
|
3976
|
+
}
|
|
3977
|
+
} else {
|
|
3978
|
+
console.log('MITM: disabled (run "liminal trust-ca" to enable)');
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
if (data.sessions.length > 0) {
|
|
3982
|
+
console.log();
|
|
3983
|
+
console.log("\u2500\u2500\u2500 Sessions \u2500\u2500\u2500");
|
|
3984
|
+
for (const s of data.sessions) {
|
|
3985
|
+
const savingsPercent = s.tokens_processed > 0 ? (s.tokens_saved / s.tokens_processed * 100).toFixed(1) : "0.0";
|
|
3986
|
+
const activeAgo = formatUptime(s.last_active_ago_ms);
|
|
3987
|
+
const p95 = s.p95_latency_ms !== null ? `${s.p95_latency_ms.toFixed(0)}ms` : "-";
|
|
3988
|
+
console.log();
|
|
3989
|
+
console.log(` ${s.connector} [${s.session_key}]`);
|
|
3990
|
+
console.log(` Circuit: ${s.circuit_state}`);
|
|
3991
|
+
console.log(` Tokens: ${s.tokens_processed.toLocaleString()} processed, ${s.tokens_saved.toLocaleString()} saved (${savingsPercent}%)`);
|
|
3992
|
+
console.log(` Calls: ${s.calls_total} total (${s.calls_compressed} compressed, ${s.calls_failed} failed)`);
|
|
3993
|
+
console.log(` Latency: p95 ${p95}`);
|
|
3994
|
+
console.log(` Active: ${activeAgo} ago`);
|
|
3995
|
+
}
|
|
3996
|
+
} else {
|
|
2775
3997
|
console.log();
|
|
2776
|
-
console.log(
|
|
2777
|
-
console.log(`Calls: ${s.calls_total} total (${s.calls_compressed} compressed, ${s.calls_skipped} skipped, ${s.calls_failed} failed)`);
|
|
3998
|
+
console.log("No active sessions.");
|
|
2778
3999
|
}
|
|
2779
4000
|
} catch {
|
|
2780
4001
|
console.log(`Liminal Daemon: running (PID ${state.pid}, port ${port})`);
|
|
2781
|
-
console.log("
|
|
4002
|
+
console.log("Status: unknown (could not reach /health)");
|
|
2782
4003
|
}
|
|
2783
4004
|
}
|
|
2784
4005
|
function formatUptime(ms) {
|
|
@@ -2930,17 +4151,17 @@ async function configCommand(flags) {
|
|
|
2930
4151
|
}
|
|
2931
4152
|
|
|
2932
4153
|
// src/commands/logs.ts
|
|
2933
|
-
import { readFileSync as
|
|
4154
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync2, createReadStream } from "fs";
|
|
2934
4155
|
import { watchFile, unwatchFile } from "fs";
|
|
2935
4156
|
async function logsCommand(flags) {
|
|
2936
4157
|
const follow = flags.has("follow") || flags.has("f");
|
|
2937
4158
|
const linesFlag = flags.get("lines") ?? flags.get("n");
|
|
2938
4159
|
const lines = typeof linesFlag === "string" ? parseInt(linesFlag, 10) : 50;
|
|
2939
|
-
if (!
|
|
4160
|
+
if (!existsSync9(LOG_FILE)) {
|
|
2940
4161
|
console.log('No log file found. Start the daemon with "liminal start" to generate logs.');
|
|
2941
4162
|
return;
|
|
2942
4163
|
}
|
|
2943
|
-
const content =
|
|
4164
|
+
const content = readFileSync6(LOG_FILE, "utf-8");
|
|
2944
4165
|
const allLines = content.split("\n");
|
|
2945
4166
|
const tail = allLines.slice(-lines - 1);
|
|
2946
4167
|
process.stdout.write(tail.join("\n"));
|
|
@@ -2966,16 +4187,16 @@ async function logsCommand(flags) {
|
|
|
2966
4187
|
}
|
|
2967
4188
|
|
|
2968
4189
|
// src/commands/uninstall.ts
|
|
2969
|
-
import { existsSync as
|
|
4190
|
+
import { existsSync as existsSync10, rmSync, readFileSync as readFileSync7 } from "fs";
|
|
2970
4191
|
var BOLD2 = "\x1B[1m";
|
|
2971
4192
|
var DIM2 = "\x1B[2m";
|
|
2972
4193
|
var GREEN2 = "\x1B[32m";
|
|
2973
4194
|
var YELLOW2 = "\x1B[33m";
|
|
2974
4195
|
var RESET2 = "\x1B[0m";
|
|
2975
4196
|
function loadConfiguredTools() {
|
|
2976
|
-
if (!
|
|
4197
|
+
if (!existsSync10(CONFIG_FILE)) return [];
|
|
2977
4198
|
try {
|
|
2978
|
-
const raw =
|
|
4199
|
+
const raw = readFileSync7(CONFIG_FILE, "utf-8");
|
|
2979
4200
|
const config = JSON.parse(raw);
|
|
2980
4201
|
if (Array.isArray(config.tools)) return config.tools;
|
|
2981
4202
|
} catch {
|
|
@@ -3063,7 +4284,7 @@ async function uninstallCommand() {
|
|
|
3063
4284
|
}
|
|
3064
4285
|
}
|
|
3065
4286
|
}
|
|
3066
|
-
if (
|
|
4287
|
+
if (existsSync10(LIMINAL_DIR)) {
|
|
3067
4288
|
console.log();
|
|
3068
4289
|
const removeData = await selectPrompt({
|
|
3069
4290
|
message: "Remove ~/.liminal/ directory? (config, logs, PID file)",
|
|
@@ -3094,6 +4315,217 @@ async function uninstallCommand() {
|
|
|
3094
4315
|
console.log();
|
|
3095
4316
|
}
|
|
3096
4317
|
|
|
4318
|
+
// src/commands/trust-ca.ts
|
|
4319
|
+
async function trustCACommand() {
|
|
4320
|
+
printBanner();
|
|
4321
|
+
if (isCATrusted()) {
|
|
4322
|
+
console.log(" Liminal CA is already trusted.");
|
|
4323
|
+
const info = getCAInfo();
|
|
4324
|
+
if (info) {
|
|
4325
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4326
|
+
console.log(` Valid until: ${info.validTo.toLocaleDateString()}`);
|
|
4327
|
+
}
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
if (!hasCA()) {
|
|
4331
|
+
console.log(" Generating CA certificate...");
|
|
4332
|
+
ensureCA();
|
|
4333
|
+
console.log(" Created ~/.liminal/ca.pem");
|
|
4334
|
+
console.log();
|
|
4335
|
+
}
|
|
4336
|
+
console.log(" Installing Liminal CA certificate");
|
|
4337
|
+
console.log();
|
|
4338
|
+
console.log(" This allows Liminal to transparently compress LLM API");
|
|
4339
|
+
console.log(" traffic from Cursor and other Electron-based editors.");
|
|
4340
|
+
console.log();
|
|
4341
|
+
console.log(" The certificate is scoped to your user account and only");
|
|
4342
|
+
console.log(" used by the local Liminal proxy on 127.0.0.1.");
|
|
4343
|
+
console.log();
|
|
4344
|
+
console.log(` Certificate: ${CA_CERT_PATH}`);
|
|
4345
|
+
console.log();
|
|
4346
|
+
const result = installCA();
|
|
4347
|
+
if (result.success) {
|
|
4348
|
+
console.log(` ${result.message}`);
|
|
4349
|
+
console.log();
|
|
4350
|
+
const info = getCAInfo();
|
|
4351
|
+
if (info) {
|
|
4352
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4353
|
+
console.log(` Valid until: ${info.validTo.toLocaleDateString()}`);
|
|
4354
|
+
}
|
|
4355
|
+
console.log();
|
|
4356
|
+
console.log(" You can now use Liminal with Cursor:");
|
|
4357
|
+
console.log(" liminal start");
|
|
4358
|
+
console.log(" cursor --proxy-server=http://127.0.0.1:3141 --disable-http2");
|
|
4359
|
+
} else {
|
|
4360
|
+
console.error(` Failed: ${result.message}`);
|
|
4361
|
+
if (result.requiresSudo) {
|
|
4362
|
+
console.log();
|
|
4363
|
+
console.log(" Try running with elevated permissions:");
|
|
4364
|
+
console.log(" sudo liminal trust-ca");
|
|
4365
|
+
}
|
|
4366
|
+
process.exit(1);
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/commands/untrust-ca.ts
|
|
4371
|
+
async function untrustCACommand() {
|
|
4372
|
+
printBanner();
|
|
4373
|
+
if (!hasCA() && !isCATrusted()) {
|
|
4374
|
+
console.log(" No Liminal CA found (nothing to remove).");
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
if (isCATrusted()) {
|
|
4378
|
+
console.log(" Removing CA from system trust store...");
|
|
4379
|
+
const result = removeCA2();
|
|
4380
|
+
if (result.success) {
|
|
4381
|
+
console.log(` ${result.message}`);
|
|
4382
|
+
} else {
|
|
4383
|
+
console.error(` ${result.message}`);
|
|
4384
|
+
if (result.requiresSudo) {
|
|
4385
|
+
console.log();
|
|
4386
|
+
console.log(" Try running with elevated permissions:");
|
|
4387
|
+
console.log(" sudo liminal untrust-ca");
|
|
4388
|
+
}
|
|
4389
|
+
process.exit(1);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
if (hasCA()) {
|
|
4393
|
+
console.log(" Removing CA certificate files...");
|
|
4394
|
+
removeCA();
|
|
4395
|
+
console.log(" Removed ~/.liminal/ca.pem and ca-key.pem");
|
|
4396
|
+
}
|
|
4397
|
+
console.log();
|
|
4398
|
+
console.log(" Liminal CA fully removed.");
|
|
4399
|
+
console.log(" Cursor MITM interception is no longer available.");
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
// src/commands/setup-cursor.ts
|
|
4403
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4404
|
+
import { homedir as homedir5 } from "os";
|
|
4405
|
+
import { join as join6 } from "path";
|
|
4406
|
+
import { execSync as execSync4, spawn } from "child_process";
|
|
4407
|
+
function findCursorBinary() {
|
|
4408
|
+
const platform = process.platform;
|
|
4409
|
+
if (platform === "darwin") {
|
|
4410
|
+
const cliPath = "/usr/local/bin/cursor";
|
|
4411
|
+
if (existsSync11(cliPath)) return cliPath;
|
|
4412
|
+
const appPath = "/Applications/Cursor.app";
|
|
4413
|
+
if (existsSync11(appPath)) {
|
|
4414
|
+
return "open";
|
|
4415
|
+
}
|
|
4416
|
+
return null;
|
|
4417
|
+
}
|
|
4418
|
+
if (platform === "win32") {
|
|
4419
|
+
const localAppData = process.env.LOCALAPPDATA || join6(homedir5(), "AppData", "Local");
|
|
4420
|
+
const exePath = join6(localAppData, "Programs", "Cursor", "Cursor.exe");
|
|
4421
|
+
if (existsSync11(exePath)) return exePath;
|
|
4422
|
+
return null;
|
|
4423
|
+
}
|
|
4424
|
+
const linuxPath = "/usr/bin/cursor";
|
|
4425
|
+
if (existsSync11(linuxPath)) return linuxPath;
|
|
4426
|
+
try {
|
|
4427
|
+
return execSync4("which cursor", { encoding: "utf-8" }).trim();
|
|
4428
|
+
} catch {
|
|
4429
|
+
return null;
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
async function setupCursorCommand(flags) {
|
|
4433
|
+
printBanner();
|
|
4434
|
+
const launch = !flags.has("no-launch");
|
|
4435
|
+
console.log(" [1/4] Checking Cursor installation...");
|
|
4436
|
+
const cursorBin = findCursorBinary();
|
|
4437
|
+
if (!cursorBin) {
|
|
4438
|
+
console.error(" Cursor not found. Install Cursor from https://cursor.com");
|
|
4439
|
+
process.exit(1);
|
|
4440
|
+
}
|
|
4441
|
+
console.log(` Found: ${cursorBin}`);
|
|
4442
|
+
console.log();
|
|
4443
|
+
console.log(" [2/4] Checking CA certificate...");
|
|
4444
|
+
if (!hasCA()) {
|
|
4445
|
+
console.log(" Generating CA certificate...");
|
|
4446
|
+
ensureCA();
|
|
4447
|
+
console.log(` Created ${CA_CERT_PATH}`);
|
|
4448
|
+
}
|
|
4449
|
+
if (!isCATrusted()) {
|
|
4450
|
+
console.log(" Installing CA into system trust store...");
|
|
4451
|
+
console.log();
|
|
4452
|
+
console.log(" This allows Liminal to transparently compress LLM API");
|
|
4453
|
+
console.log(" traffic from Cursor. The certificate is scoped to your");
|
|
4454
|
+
console.log(" user account and only used locally.");
|
|
4455
|
+
console.log();
|
|
4456
|
+
const result = installCA();
|
|
4457
|
+
if (!result.success) {
|
|
4458
|
+
console.error(` CA install failed: ${result.message}`);
|
|
4459
|
+
if (result.requiresSudo) {
|
|
4460
|
+
console.log(" Try: sudo liminal setup cursor");
|
|
4461
|
+
}
|
|
4462
|
+
process.exit(1);
|
|
4463
|
+
}
|
|
4464
|
+
console.log(` ${result.message}`);
|
|
4465
|
+
} else {
|
|
4466
|
+
console.log(" CA already trusted");
|
|
4467
|
+
}
|
|
4468
|
+
const info = getCAInfo();
|
|
4469
|
+
if (info) {
|
|
4470
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4471
|
+
}
|
|
4472
|
+
console.log();
|
|
4473
|
+
console.log(" [3/4] Checking Liminal proxy...");
|
|
4474
|
+
if (!isConfigured()) {
|
|
4475
|
+
console.error(' Liminal is not configured. Run "liminal init" first.');
|
|
4476
|
+
process.exit(1);
|
|
4477
|
+
}
|
|
4478
|
+
const state = isDaemonRunning();
|
|
4479
|
+
const config = loadConfig();
|
|
4480
|
+
const port = config.port;
|
|
4481
|
+
if (!state.running) {
|
|
4482
|
+
console.log(" Proxy is not running.");
|
|
4483
|
+
console.log();
|
|
4484
|
+
console.log(" Start the proxy first, then re-run this command:");
|
|
4485
|
+
console.log(" liminal start -d");
|
|
4486
|
+
console.log(" liminal setup cursor");
|
|
4487
|
+
console.log();
|
|
4488
|
+
console.log(" Or start in foreground + launch Cursor manually:");
|
|
4489
|
+
console.log(` liminal start & cursor --proxy-server=http://127.0.0.1:${port}`);
|
|
4490
|
+
process.exit(1);
|
|
4491
|
+
}
|
|
4492
|
+
console.log(` Proxy running on port ${port} (PID ${state.pid})`);
|
|
4493
|
+
console.log();
|
|
4494
|
+
const proxyUrl = `http://127.0.0.1:${port}`;
|
|
4495
|
+
if (launch) {
|
|
4496
|
+
console.log(" [4/4] Launching Cursor with proxy...");
|
|
4497
|
+
console.log(` --proxy-server=${proxyUrl}`);
|
|
4498
|
+
console.log();
|
|
4499
|
+
launchCursor(cursorBin, proxyUrl);
|
|
4500
|
+
console.log(" Cursor launched with Liminal compression active.");
|
|
4501
|
+
console.log();
|
|
4502
|
+
console.log(" All LLM API calls will be transparently compressed.");
|
|
4503
|
+
console.log(' Use "liminal status" to monitor compression stats.');
|
|
4504
|
+
console.log(' Use "liminal logs -f" to watch live traffic.');
|
|
4505
|
+
} else {
|
|
4506
|
+
console.log(" [4/4] Ready! Launch Cursor with:");
|
|
4507
|
+
console.log();
|
|
4508
|
+
console.log(` cursor --proxy-server=${proxyUrl}`);
|
|
4509
|
+
console.log();
|
|
4510
|
+
console.log(" Or create a shell alias:");
|
|
4511
|
+
console.log(` alias cursor-liminal='cursor --proxy-server=${proxyUrl}'`);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
function launchCursor(bin, proxyUrl) {
|
|
4515
|
+
const args = [`--proxy-server=${proxyUrl}`];
|
|
4516
|
+
if (bin === "open" && process.platform === "darwin") {
|
|
4517
|
+
spawn("open", ["-a", "Cursor", "--args", ...args], {
|
|
4518
|
+
detached: true,
|
|
4519
|
+
stdio: "ignore"
|
|
4520
|
+
}).unref();
|
|
4521
|
+
} else {
|
|
4522
|
+
spawn(bin, args, {
|
|
4523
|
+
detached: true,
|
|
4524
|
+
stdio: "ignore"
|
|
4525
|
+
}).unref();
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
|
|
3097
4529
|
// src/bin.ts
|
|
3098
4530
|
var USAGE = `
|
|
3099
4531
|
liminal v${VERSION} \u2014 Transparent LLM context compression proxy
|
|
@@ -3108,6 +4540,9 @@ var USAGE = `
|
|
|
3108
4540
|
liminal summary Detailed session metrics
|
|
3109
4541
|
liminal config [--set k=v] [--get k] View or edit configuration
|
|
3110
4542
|
liminal logs [--follow] [--lines N] View proxy logs
|
|
4543
|
+
liminal setup cursor [--no-launch] Set up and launch Cursor with Liminal
|
|
4544
|
+
liminal trust-ca Install CA cert (for Cursor MITM)
|
|
4545
|
+
liminal untrust-ca Remove CA cert
|
|
3111
4546
|
liminal uninstall Remove Liminal configuration
|
|
3112
4547
|
|
|
3113
4548
|
Options:
|
|
@@ -3120,7 +4555,7 @@ var USAGE = `
|
|
|
3120
4555
|
3. Connect your AI tools:
|
|
3121
4556
|
Claude Code: export ANTHROPIC_BASE_URL=http://localhost:3141
|
|
3122
4557
|
Codex: export OPENAI_BASE_URL=http://localhost:3141/v1
|
|
3123
|
-
Cursor:
|
|
4558
|
+
Cursor: liminal trust-ca && cursor --proxy-server=http://localhost:3141
|
|
3124
4559
|
`;
|
|
3125
4560
|
function parseArgs(argv) {
|
|
3126
4561
|
const command = argv[2] ?? "";
|
|
@@ -3186,6 +4621,23 @@ async function main() {
|
|
|
3186
4621
|
case "logs":
|
|
3187
4622
|
await logsCommand(flags);
|
|
3188
4623
|
break;
|
|
4624
|
+
case "setup": {
|
|
4625
|
+
const subcommand = process.argv[3];
|
|
4626
|
+
if (subcommand === "cursor") {
|
|
4627
|
+
await setupCursorCommand(flags);
|
|
4628
|
+
} else {
|
|
4629
|
+
console.error(`Unknown setup target: ${subcommand ?? "(none)"}`);
|
|
4630
|
+
console.error("Available: liminal setup cursor");
|
|
4631
|
+
process.exit(1);
|
|
4632
|
+
}
|
|
4633
|
+
break;
|
|
4634
|
+
}
|
|
4635
|
+
case "trust-ca":
|
|
4636
|
+
await trustCACommand();
|
|
4637
|
+
break;
|
|
4638
|
+
case "untrust-ca":
|
|
4639
|
+
await untrustCACommand();
|
|
4640
|
+
break;
|
|
3189
4641
|
case "uninstall":
|
|
3190
4642
|
await uninstallCommand();
|
|
3191
4643
|
break;
|