@cognisos/liminal 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1459 -234
- 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.1" : "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
127
|
latencyBudgetMs: 5e3,
|
|
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;
|
|
@@ -871,19 +964,24 @@ 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
970
|
// No env vars — GUI only
|
|
879
971
|
postSetupInstructions: [
|
|
880
|
-
"Cursor
|
|
972
|
+
"Cursor routes API calls through its cloud servers, so localhost",
|
|
973
|
+
"URLs are blocked. You need a tunnel to expose the proxy:",
|
|
974
|
+
"",
|
|
975
|
+
` npx cloudflared tunnel --url http://localhost:${port}`,
|
|
976
|
+
"",
|
|
977
|
+
"Then configure Cursor with the tunnel URL:",
|
|
881
978
|
"",
|
|
882
979
|
" 1. Open Cursor Settings (not VS Code settings)",
|
|
883
980
|
" 2. Go to Models",
|
|
884
981
|
' 3. Enable "Override OpenAI Base URL (when using key)"',
|
|
885
|
-
|
|
886
|
-
|
|
982
|
+
" 4. Set the base URL to your tunnel URL + /v1",
|
|
983
|
+
" (e.g., https://abc123.trycloudflare.com/v1)",
|
|
984
|
+
" 5. Enter your real OpenAI/Anthropic API key",
|
|
887
985
|
" 6. Restart Cursor",
|
|
888
986
|
"",
|
|
889
987
|
"Cursor uses OpenAI format for all models, including Claude.",
|
|
@@ -1140,64 +1238,6 @@ async function logoutCommand() {
|
|
|
1140
1238
|
console.log(" Run \x1B[1mliminal login\x1B[0m to reconnect.");
|
|
1141
1239
|
}
|
|
1142
1240
|
|
|
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
1241
|
// src/proxy/completions.ts
|
|
1202
1242
|
import { RSCCircuitOpenError as RSCCircuitOpenError2 } from "@cognisos/rsc-sdk";
|
|
1203
1243
|
|
|
@@ -1303,66 +1343,115 @@ function isIndentedCodeLine(line) {
|
|
|
1303
1343
|
}
|
|
1304
1344
|
|
|
1305
1345
|
// src/rsc/message-compressor.ts
|
|
1346
|
+
var PASSTHROUGH_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
|
|
1347
|
+
function sanitizeCompressedText(text) {
|
|
1348
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
1349
|
+
}
|
|
1306
1350
|
async function compressMessages(messages, pipeline, session, compressRoles) {
|
|
1307
1351
|
let anyCompressed = false;
|
|
1308
1352
|
let totalTokensSaved = 0;
|
|
1309
1353
|
const compressed = await Promise.all(
|
|
1310
1354
|
messages.map(async (msg) => {
|
|
1311
1355
|
if (!compressRoles.has(msg.role)) return msg;
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1356
|
+
return compressMessage(msg, pipeline, session, (c, saved) => {
|
|
1357
|
+
anyCompressed = anyCompressed || c;
|
|
1358
|
+
totalTokensSaved += saved;
|
|
1359
|
+
});
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
return { messages: compressed, anyCompressed, totalTokensSaved };
|
|
1363
|
+
}
|
|
1364
|
+
async function compressConversation(pipeline, session, plan, options = { compressToolResults: true }) {
|
|
1365
|
+
if (!plan.shouldCompress) {
|
|
1366
|
+
return {
|
|
1367
|
+
messages: plan.messages.map((tm) => tm.message),
|
|
1368
|
+
anyCompressed: false,
|
|
1369
|
+
totalTokensSaved: 0
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
let anyCompressed = false;
|
|
1373
|
+
let totalTokensSaved = 0;
|
|
1374
|
+
const record = (c, saved) => {
|
|
1375
|
+
anyCompressed = anyCompressed || c;
|
|
1376
|
+
totalTokensSaved += saved;
|
|
1377
|
+
};
|
|
1378
|
+
const log = options.logFn;
|
|
1379
|
+
const compressed = await Promise.all(
|
|
1380
|
+
plan.messages.map(async (tm) => {
|
|
1381
|
+
const role = tm.message.role;
|
|
1382
|
+
const blockTypes = Array.isArray(tm.message.content) ? tm.message.content.map((b) => b.type).join(",") : "string";
|
|
1383
|
+
if (tm.tier === "hot") {
|
|
1384
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 HOT (verbatim)`);
|
|
1385
|
+
return tm.message;
|
|
1317
1386
|
}
|
|
1318
|
-
if (
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
totalTokensSaved += saved;
|
|
1322
|
-
});
|
|
1387
|
+
if (tm.eligibleTokens === 0) {
|
|
1388
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (0 eligible tok, skip)`);
|
|
1389
|
+
return tm.message;
|
|
1323
1390
|
}
|
|
1324
|
-
|
|
1391
|
+
log?.(`[BLOCK] #${tm.index} ${role} [${blockTypes}] \u2192 ${tm.tier.toUpperCase()} (${tm.eligibleTokens} eligible tok, batch compressing)`);
|
|
1392
|
+
return batchCompressMessage(tm.message, pipeline, session, record, options);
|
|
1325
1393
|
})
|
|
1326
1394
|
);
|
|
1327
1395
|
return { messages: compressed, anyCompressed, totalTokensSaved };
|
|
1328
1396
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
if (
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1397
|
+
function extractToolResultText(part) {
|
|
1398
|
+
if (typeof part.content === "string" && part.content.trim()) {
|
|
1399
|
+
return part.content;
|
|
1400
|
+
}
|
|
1401
|
+
if (Array.isArray(part.content)) {
|
|
1402
|
+
const texts = part.content.filter((inner) => inner.type === "text" && typeof inner.text === "string" && inner.text.trim()).map((inner) => inner.text);
|
|
1403
|
+
return texts.length > 0 ? texts.join("\n") : null;
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
async function batchCompressMessage(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1408
|
+
if (typeof msg.content === "string") {
|
|
1409
|
+
return compressStringContent(msg, pipeline, session, record, options.semaphore, options.semaphoreTimeoutMs);
|
|
1410
|
+
}
|
|
1411
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
1412
|
+
const parts = msg.content;
|
|
1413
|
+
const textSegments = [];
|
|
1414
|
+
const batchedIndices = /* @__PURE__ */ new Set();
|
|
1415
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1416
|
+
const part = parts[i];
|
|
1417
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) continue;
|
|
1418
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1419
|
+
textSegments.push(part.text);
|
|
1420
|
+
batchedIndices.add(i);
|
|
1421
|
+
}
|
|
1422
|
+
if (part.type === "tool_result" && options.compressToolResults) {
|
|
1423
|
+
const extracted = extractToolResultText(part);
|
|
1424
|
+
if (extracted) {
|
|
1425
|
+
textSegments.push(extracted);
|
|
1426
|
+
batchedIndices.add(i);
|
|
1343
1427
|
}
|
|
1344
|
-
session.recordFailure();
|
|
1345
|
-
return msg;
|
|
1346
1428
|
}
|
|
1347
1429
|
}
|
|
1430
|
+
if (textSegments.length === 0) return msg;
|
|
1431
|
+
const batchText = textSegments.join("\n\n");
|
|
1348
1432
|
try {
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1433
|
+
const compressed = await compressTextWithSegmentation(batchText, pipeline, session, record, options.semaphore, options.semaphoreTimeoutMs);
|
|
1434
|
+
const newParts = [];
|
|
1435
|
+
let isFirstEligible = true;
|
|
1436
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1437
|
+
if (!batchedIndices.has(i)) {
|
|
1438
|
+
newParts.push(parts[i]);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (isFirstEligible) {
|
|
1442
|
+
if (parts[i].type === "text") {
|
|
1443
|
+
newParts.push({ ...parts[i], text: compressed });
|
|
1444
|
+
} else if (parts[i].type === "tool_result") {
|
|
1445
|
+
newParts.push({ ...parts[i], content: compressed });
|
|
1362
1446
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1447
|
+
isFirstEligible = false;
|
|
1448
|
+
} else {
|
|
1449
|
+
if (parts[i].type === "tool_result") {
|
|
1450
|
+
newParts.push({ ...parts[i], content: "" });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return { ...msg, content: newParts };
|
|
1366
1455
|
} catch (err) {
|
|
1367
1456
|
if (err instanceof RSCCircuitOpenError) {
|
|
1368
1457
|
session.recordFailure();
|
|
@@ -1372,37 +1461,34 @@ async function compressStringContent(msg, pipeline, session, record) {
|
|
|
1372
1461
|
return msg;
|
|
1373
1462
|
}
|
|
1374
1463
|
}
|
|
1375
|
-
async function
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (!hasCode) {
|
|
1379
|
-
const result = await pipeline.compressForLLM(text);
|
|
1380
|
-
session.recordCompression(result.metrics);
|
|
1381
|
-
record(!result.metrics.skipped, result.metrics.tokensSaved);
|
|
1382
|
-
return result.text;
|
|
1464
|
+
async function compressMessage(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1465
|
+
if (typeof msg.content === "string") {
|
|
1466
|
+
return compressStringContent(msg, pipeline, session, record);
|
|
1383
1467
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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("");
|
|
1468
|
+
if (Array.isArray(msg.content)) {
|
|
1469
|
+
return compressArrayContent(msg, pipeline, session, record, options);
|
|
1470
|
+
}
|
|
1471
|
+
return msg;
|
|
1401
1472
|
}
|
|
1402
|
-
async function
|
|
1473
|
+
async function compressStringContent(msg, pipeline, session, record, semaphore, semaphoreTimeoutMs) {
|
|
1474
|
+
const text = msg.content;
|
|
1475
|
+
try {
|
|
1476
|
+
const compressed = await compressTextWithSegmentation(text, pipeline, session, record, semaphore, semaphoreTimeoutMs);
|
|
1477
|
+
return { ...msg, content: compressed };
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1480
|
+
session.recordFailure();
|
|
1481
|
+
throw err;
|
|
1482
|
+
}
|
|
1483
|
+
session.recordFailure();
|
|
1484
|
+
return msg;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async function compressArrayContent(msg, pipeline, session, record, options = { compressToolResults: true }) {
|
|
1403
1488
|
const parts = msg.content;
|
|
1404
1489
|
const compressedParts = await Promise.all(
|
|
1405
1490
|
parts.map(async (part) => {
|
|
1491
|
+
if (PASSTHROUGH_BLOCK_TYPES.has(part.type)) return part;
|
|
1406
1492
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1407
1493
|
try {
|
|
1408
1494
|
const compressed = await compressTextWithSegmentation(part.text, pipeline, session, record);
|
|
@@ -1416,11 +1502,183 @@ async function compressArrayContent(msg, pipeline, session, record) {
|
|
|
1416
1502
|
return part;
|
|
1417
1503
|
}
|
|
1418
1504
|
}
|
|
1505
|
+
if (part.type === "tool_result" && options.compressToolResults) {
|
|
1506
|
+
return compressToolResult(part, pipeline, session, record);
|
|
1507
|
+
}
|
|
1419
1508
|
return part;
|
|
1420
1509
|
})
|
|
1421
1510
|
);
|
|
1422
1511
|
return { ...msg, content: compressedParts };
|
|
1423
1512
|
}
|
|
1513
|
+
async function compressToolResult(part, pipeline, session, record) {
|
|
1514
|
+
const content = part.content;
|
|
1515
|
+
if (typeof content === "string") {
|
|
1516
|
+
try {
|
|
1517
|
+
const compressed = await compressTextWithSegmentation(content, pipeline, session, record);
|
|
1518
|
+
return { ...part, content: compressed };
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1521
|
+
session.recordFailure();
|
|
1522
|
+
throw err;
|
|
1523
|
+
}
|
|
1524
|
+
session.recordFailure();
|
|
1525
|
+
return part;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (Array.isArray(content)) {
|
|
1529
|
+
try {
|
|
1530
|
+
const compressedInner = await Promise.all(
|
|
1531
|
+
content.map(async (inner) => {
|
|
1532
|
+
if (inner.type === "text" && typeof inner.text === "string") {
|
|
1533
|
+
try {
|
|
1534
|
+
const compressed = await compressTextWithSegmentation(inner.text, pipeline, session, record);
|
|
1535
|
+
return { ...inner, text: compressed };
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1538
|
+
session.recordFailure();
|
|
1539
|
+
return inner;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return inner;
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
return { ...part, content: compressedInner };
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
if (err instanceof RSCCircuitOpenError) {
|
|
1548
|
+
session.recordFailure();
|
|
1549
|
+
throw err;
|
|
1550
|
+
}
|
|
1551
|
+
session.recordFailure();
|
|
1552
|
+
return part;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return part;
|
|
1556
|
+
}
|
|
1557
|
+
async function compressTextWithSegmentation(text, pipeline, session, record, semaphore, semaphoreTimeoutMs) {
|
|
1558
|
+
const segments = segmentContent(text);
|
|
1559
|
+
const hasCode = segments.some((s) => s.type === "code");
|
|
1560
|
+
if (!hasCode) {
|
|
1561
|
+
if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
|
|
1562
|
+
try {
|
|
1563
|
+
const result = await pipeline.compressForLLM(text);
|
|
1564
|
+
session.recordCompression(result.metrics);
|
|
1565
|
+
const saved = Math.max(0, result.metrics.tokensSaved);
|
|
1566
|
+
record(!result.metrics.skipped, saved);
|
|
1567
|
+
return sanitizeCompressedText(result.text);
|
|
1568
|
+
} finally {
|
|
1569
|
+
if (semaphore) semaphore.release();
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const parts = await Promise.all(
|
|
1573
|
+
segments.map(async (seg) => {
|
|
1574
|
+
if (seg.type === "code") return seg.text;
|
|
1575
|
+
if (seg.text.trim().length === 0) return seg.text;
|
|
1576
|
+
if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await pipeline.compressForLLM(seg.text);
|
|
1579
|
+
session.recordCompression(result.metrics);
|
|
1580
|
+
const saved = Math.max(0, result.metrics.tokensSaved);
|
|
1581
|
+
record(!result.metrics.skipped, saved);
|
|
1582
|
+
return sanitizeCompressedText(result.text);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (err instanceof RSCCircuitOpenError) throw err;
|
|
1585
|
+
session.recordFailure();
|
|
1586
|
+
return seg.text;
|
|
1587
|
+
} finally {
|
|
1588
|
+
if (semaphore) semaphore.release();
|
|
1589
|
+
}
|
|
1590
|
+
})
|
|
1591
|
+
);
|
|
1592
|
+
return parts.join("");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/rsc/conversation-analyzer.ts
|
|
1596
|
+
var SKIP_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
|
|
1597
|
+
function estimateTokens(text) {
|
|
1598
|
+
return Math.ceil(text.length / 4);
|
|
1599
|
+
}
|
|
1600
|
+
function estimateBlockTokens(block, compressToolResults) {
|
|
1601
|
+
if (SKIP_BLOCK_TYPES.has(block.type)) return 0;
|
|
1602
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1603
|
+
return estimateTokens(block.text);
|
|
1604
|
+
}
|
|
1605
|
+
if (block.type === "tool_result" && compressToolResults) {
|
|
1606
|
+
if (typeof block.content === "string") {
|
|
1607
|
+
return estimateTokens(block.content);
|
|
1608
|
+
}
|
|
1609
|
+
if (Array.isArray(block.content)) {
|
|
1610
|
+
return block.content.reduce(
|
|
1611
|
+
(sum, inner) => sum + estimateBlockTokens(inner, compressToolResults),
|
|
1612
|
+
0
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
function estimateMessageTokens(msg, config) {
|
|
1619
|
+
if (!config.compressRoles.has(msg.role)) return 0;
|
|
1620
|
+
if (typeof msg.content === "string") {
|
|
1621
|
+
return estimateTokens(msg.content);
|
|
1622
|
+
}
|
|
1623
|
+
if (Array.isArray(msg.content)) {
|
|
1624
|
+
return msg.content.reduce(
|
|
1625
|
+
(sum, part) => sum + estimateBlockTokens(part, config.compressToolResults),
|
|
1626
|
+
0
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
return 0;
|
|
1630
|
+
}
|
|
1631
|
+
function analyzeConversation(messages, config) {
|
|
1632
|
+
const n = messages.length;
|
|
1633
|
+
if (n < 5) {
|
|
1634
|
+
const tiered2 = messages.map((msg, i) => ({
|
|
1635
|
+
index: i,
|
|
1636
|
+
message: msg,
|
|
1637
|
+
tier: "hot",
|
|
1638
|
+
eligibleTokens: estimateMessageTokens(msg, config)
|
|
1639
|
+
}));
|
|
1640
|
+
return {
|
|
1641
|
+
messages: tiered2,
|
|
1642
|
+
totalEligibleTokens: 0,
|
|
1643
|
+
shouldCompress: false,
|
|
1644
|
+
hotCount: n,
|
|
1645
|
+
warmCount: 0,
|
|
1646
|
+
coldCount: 0
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
const coldEnd = Math.floor(n * config.coldFraction);
|
|
1650
|
+
const hotStart = n - Math.floor(n * config.hotFraction);
|
|
1651
|
+
let totalEligibleTokens = 0;
|
|
1652
|
+
let hotCount = 0;
|
|
1653
|
+
let warmCount = 0;
|
|
1654
|
+
let coldCount = 0;
|
|
1655
|
+
const tiered = messages.map((msg, i) => {
|
|
1656
|
+
let tier;
|
|
1657
|
+
if (i >= hotStart) {
|
|
1658
|
+
tier = "hot";
|
|
1659
|
+
hotCount++;
|
|
1660
|
+
} else if (i < coldEnd) {
|
|
1661
|
+
tier = "cold";
|
|
1662
|
+
coldCount++;
|
|
1663
|
+
} else {
|
|
1664
|
+
tier = "warm";
|
|
1665
|
+
warmCount++;
|
|
1666
|
+
}
|
|
1667
|
+
const eligibleTokens = estimateMessageTokens(msg, config);
|
|
1668
|
+
if (tier !== "hot") {
|
|
1669
|
+
totalEligibleTokens += eligibleTokens;
|
|
1670
|
+
}
|
|
1671
|
+
return { index: i, message: msg, tier, eligibleTokens };
|
|
1672
|
+
});
|
|
1673
|
+
return {
|
|
1674
|
+
messages: tiered,
|
|
1675
|
+
totalEligibleTokens,
|
|
1676
|
+
shouldCompress: totalEligibleTokens >= config.aggregateThreshold,
|
|
1677
|
+
hotCount,
|
|
1678
|
+
warmCount,
|
|
1679
|
+
coldCount
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1424
1682
|
|
|
1425
1683
|
// src/rsc/learning.ts
|
|
1426
1684
|
function createStreamLearningBuffer(pipeline) {
|
|
@@ -1523,6 +1781,25 @@ async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onCo
|
|
|
1523
1781
|
}
|
|
1524
1782
|
}
|
|
1525
1783
|
|
|
1784
|
+
// src/terminology.ts
|
|
1785
|
+
var TIER_LABELS = {
|
|
1786
|
+
HOT: "Active",
|
|
1787
|
+
WARM: "Recent",
|
|
1788
|
+
COLD: "Archived"
|
|
1789
|
+
};
|
|
1790
|
+
function formatTiersLog(hot, warm, cold, eligibleTokens) {
|
|
1791
|
+
return `[MEMORY] ${TIER_LABELS.HOT}:${hot} ${TIER_LABELS.WARM}:${warm} ${TIER_LABELS.COLD}:${cold} \xB7 ${eligibleTokens} tokens eligible`;
|
|
1792
|
+
}
|
|
1793
|
+
function formatSavedLog(tokensSaved, latencyMs) {
|
|
1794
|
+
return `[SAVED] ${tokensSaved} tokens (${latencyMs}ms)`;
|
|
1795
|
+
}
|
|
1796
|
+
function formatDegradeLog() {
|
|
1797
|
+
return "[STATUS] Connection degraded \u2014 passing through directly";
|
|
1798
|
+
}
|
|
1799
|
+
function formatResponseLog(model, tokensSaved, streaming = false) {
|
|
1800
|
+
return `[RESPONSE] ${streaming ? "Streaming " : ""}${model} response \u2192 client (saved:${tokensSaved}tok)`;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1526
1803
|
// src/proxy/completions.ts
|
|
1527
1804
|
function setCORSHeaders(res) {
|
|
1528
1805
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -1539,7 +1816,7 @@ function extractBearerToken(req) {
|
|
|
1539
1816
|
if (!auth || !auth.startsWith("Bearer ")) return null;
|
|
1540
1817
|
return auth.slice(7);
|
|
1541
1818
|
}
|
|
1542
|
-
async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
1819
|
+
async function handleChatCompletions(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
1543
1820
|
const request = body;
|
|
1544
1821
|
if (!request.messages || !Array.isArray(request.messages)) {
|
|
1545
1822
|
sendJSON(res, 400, {
|
|
@@ -1558,23 +1835,44 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1558
1835
|
let anyCompressed = false;
|
|
1559
1836
|
let totalTokensSaved = 0;
|
|
1560
1837
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
1838
|
+
const compressStart = Date.now();
|
|
1561
1839
|
try {
|
|
1562
1840
|
const compressRoles = new Set(config.compressRoles);
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1841
|
+
const plan = analyzeConversation(request.messages, {
|
|
1842
|
+
hotFraction: config.hotFraction,
|
|
1843
|
+
coldFraction: config.coldFraction,
|
|
1844
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
1845
|
+
compressRoles,
|
|
1846
|
+
compressToolResults: config.compressToolResults
|
|
1847
|
+
});
|
|
1848
|
+
if (plan.shouldCompress) {
|
|
1849
|
+
logger.log(formatTiersLog(plan.hotCount, plan.warmCount, plan.coldCount, plan.totalEligibleTokens));
|
|
1850
|
+
}
|
|
1851
|
+
const result = await compressConversation(
|
|
1565
1852
|
pipeline.pipeline,
|
|
1566
1853
|
pipeline.session,
|
|
1567
|
-
|
|
1854
|
+
plan,
|
|
1855
|
+
{
|
|
1856
|
+
compressToolResults: config.compressToolResults,
|
|
1857
|
+
logFn: (msg) => logger.log(msg),
|
|
1858
|
+
semaphore,
|
|
1859
|
+
semaphoreTimeoutMs: config.concurrencyTimeoutMs
|
|
1860
|
+
}
|
|
1568
1861
|
);
|
|
1569
1862
|
messages = result.messages;
|
|
1570
1863
|
anyCompressed = result.anyCompressed;
|
|
1571
1864
|
totalTokensSaved = result.totalTokensSaved;
|
|
1865
|
+
const latencyMs = Date.now() - compressStart;
|
|
1866
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
1867
|
+
if (alert) {
|
|
1868
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
1869
|
+
}
|
|
1572
1870
|
if (result.totalTokensSaved > 0) {
|
|
1573
|
-
logger.log(
|
|
1871
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
1574
1872
|
}
|
|
1575
1873
|
} catch (err) {
|
|
1576
1874
|
if (err instanceof RSCCircuitOpenError2) {
|
|
1577
|
-
logger.log(
|
|
1875
|
+
logger.log(formatDegradeLog());
|
|
1578
1876
|
} else {
|
|
1579
1877
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1580
1878
|
}
|
|
@@ -1607,6 +1905,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1607
1905
|
}
|
|
1608
1906
|
if (request.stream && upstreamResponse.body) {
|
|
1609
1907
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
1908
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
1610
1909
|
await pipeSSEResponse(
|
|
1611
1910
|
upstreamResponse,
|
|
1612
1911
|
res,
|
|
@@ -1617,6 +1916,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
|
|
|
1617
1916
|
return;
|
|
1618
1917
|
}
|
|
1619
1918
|
const responseBody = await upstreamResponse.text();
|
|
1919
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
1620
1920
|
let finalBody = responseBody;
|
|
1621
1921
|
if (totalTokensSaved > 0) {
|
|
1622
1922
|
try {
|
|
@@ -1757,7 +2057,7 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
|
|
|
1757
2057
|
function setCORSHeaders2(res) {
|
|
1758
2058
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1759
2059
|
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");
|
|
2060
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, x-liminal-session");
|
|
1761
2061
|
}
|
|
1762
2062
|
function sendAnthropicError(res, status, type, message) {
|
|
1763
2063
|
setCORSHeaders2(res);
|
|
@@ -1788,7 +2088,7 @@ function convertCompressedToAnthropic(messages) {
|
|
|
1788
2088
|
content: msg.content
|
|
1789
2089
|
}));
|
|
1790
2090
|
}
|
|
1791
|
-
async function handleAnthropicMessages(req, res, body, pipeline, config, logger) {
|
|
2091
|
+
async function handleAnthropicMessages(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
1792
2092
|
const request = body;
|
|
1793
2093
|
if (!request.messages || !Array.isArray(request.messages)) {
|
|
1794
2094
|
sendAnthropicError(res, 400, "invalid_request_error", "messages is required and must be an array");
|
|
@@ -1807,24 +2107,45 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1807
2107
|
let anyCompressed = false;
|
|
1808
2108
|
let totalTokensSaved = 0;
|
|
1809
2109
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
2110
|
+
const compressStart = Date.now();
|
|
1810
2111
|
try {
|
|
1811
2112
|
const compressRoles = new Set(config.compressRoles);
|
|
1812
2113
|
const compressible = convertAnthropicToCompressible(request.messages);
|
|
1813
|
-
const
|
|
1814
|
-
|
|
2114
|
+
const plan = analyzeConversation(compressible, {
|
|
2115
|
+
hotFraction: config.hotFraction,
|
|
2116
|
+
coldFraction: config.coldFraction,
|
|
2117
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
2118
|
+
compressRoles,
|
|
2119
|
+
compressToolResults: config.compressToolResults
|
|
2120
|
+
});
|
|
2121
|
+
if (plan.shouldCompress) {
|
|
2122
|
+
logger.log(formatTiersLog(plan.hotCount, plan.warmCount, plan.coldCount, plan.totalEligibleTokens));
|
|
2123
|
+
}
|
|
2124
|
+
const result = await compressConversation(
|
|
1815
2125
|
pipeline.pipeline,
|
|
1816
2126
|
pipeline.session,
|
|
1817
|
-
|
|
2127
|
+
plan,
|
|
2128
|
+
{
|
|
2129
|
+
compressToolResults: config.compressToolResults,
|
|
2130
|
+
logFn: (msg) => logger.log(msg),
|
|
2131
|
+
semaphore,
|
|
2132
|
+
semaphoreTimeoutMs: config.concurrencyTimeoutMs
|
|
2133
|
+
}
|
|
1818
2134
|
);
|
|
1819
2135
|
messages = convertCompressedToAnthropic(result.messages);
|
|
1820
2136
|
anyCompressed = result.anyCompressed;
|
|
1821
2137
|
totalTokensSaved = result.totalTokensSaved;
|
|
2138
|
+
const latencyMs = Date.now() - compressStart;
|
|
2139
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
2140
|
+
if (alert) {
|
|
2141
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
2142
|
+
}
|
|
1822
2143
|
if (result.totalTokensSaved > 0) {
|
|
1823
|
-
logger.log(
|
|
2144
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
1824
2145
|
}
|
|
1825
2146
|
} catch (err) {
|
|
1826
2147
|
if (err instanceof RSCCircuitOpenError3) {
|
|
1827
|
-
logger.log(
|
|
2148
|
+
logger.log(formatDegradeLog());
|
|
1828
2149
|
} else {
|
|
1829
2150
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1830
2151
|
}
|
|
@@ -1862,6 +2183,7 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1862
2183
|
}
|
|
1863
2184
|
if (request.stream && upstreamResponse.body) {
|
|
1864
2185
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
2186
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
1865
2187
|
await pipeAnthropicSSEResponse(
|
|
1866
2188
|
upstreamResponse,
|
|
1867
2189
|
res,
|
|
@@ -1872,6 +2194,7 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
|
|
|
1872
2194
|
return;
|
|
1873
2195
|
}
|
|
1874
2196
|
const responseBody = await upstreamResponse.text();
|
|
2197
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
1875
2198
|
let finalBody = responseBody;
|
|
1876
2199
|
if (totalTokensSaved > 0) {
|
|
1877
2200
|
try {
|
|
@@ -2102,7 +2425,7 @@ function extractOutputText(output) {
|
|
|
2102
2425
|
}
|
|
2103
2426
|
return texts.join("");
|
|
2104
2427
|
}
|
|
2105
|
-
async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
2428
|
+
async function handleResponses(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
|
|
2106
2429
|
const request = body;
|
|
2107
2430
|
if (request.input === void 0 || request.input === null) {
|
|
2108
2431
|
sendJSON2(res, 400, {
|
|
@@ -2121,6 +2444,7 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2121
2444
|
let anyCompressed = false;
|
|
2122
2445
|
let totalTokensSaved = 0;
|
|
2123
2446
|
if (config.enabled && !pipeline.isCircuitOpen()) {
|
|
2447
|
+
const compressStart = Date.now();
|
|
2124
2448
|
try {
|
|
2125
2449
|
const compressRoles = new Set(config.compressRoles);
|
|
2126
2450
|
const compressible = inputToCompressibleMessages(request.input);
|
|
@@ -2134,13 +2458,18 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2134
2458
|
compressedInput = applyCompressedToInput(request.input, result.messages);
|
|
2135
2459
|
anyCompressed = result.anyCompressed;
|
|
2136
2460
|
totalTokensSaved = result.totalTokensSaved;
|
|
2461
|
+
const latencyMs = Date.now() - compressStart;
|
|
2462
|
+
const alert = latencyMonitor.record(sessionKey, latencyMs);
|
|
2463
|
+
if (alert) {
|
|
2464
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message}`);
|
|
2465
|
+
}
|
|
2137
2466
|
if (result.totalTokensSaved > 0) {
|
|
2138
|
-
logger.log(
|
|
2467
|
+
logger.log(formatSavedLog(result.totalTokensSaved, latencyMs));
|
|
2139
2468
|
}
|
|
2140
2469
|
}
|
|
2141
2470
|
} catch (err) {
|
|
2142
2471
|
if (err instanceof RSCCircuitOpenError4) {
|
|
2143
|
-
logger.log(
|
|
2472
|
+
logger.log(formatDegradeLog());
|
|
2144
2473
|
} else {
|
|
2145
2474
|
logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2146
2475
|
}
|
|
@@ -2175,6 +2504,7 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2175
2504
|
}
|
|
2176
2505
|
if (request.stream && upstreamResponse.body) {
|
|
2177
2506
|
const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
|
|
2507
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved, true));
|
|
2178
2508
|
await pipeResponsesSSE(
|
|
2179
2509
|
upstreamResponse,
|
|
2180
2510
|
res,
|
|
@@ -2185,6 +2515,7 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2185
2515
|
return;
|
|
2186
2516
|
}
|
|
2187
2517
|
const responseBody = await upstreamResponse.text();
|
|
2518
|
+
logger.log(formatResponseLog(request.model, totalTokensSaved));
|
|
2188
2519
|
let finalBody = responseBody;
|
|
2189
2520
|
if (totalTokensSaved > 0) {
|
|
2190
2521
|
try {
|
|
@@ -2226,11 +2557,51 @@ async function handleResponses(req, res, body, pipeline, config, logger) {
|
|
|
2226
2557
|
}
|
|
2227
2558
|
}
|
|
2228
2559
|
|
|
2560
|
+
// src/proxy/session-identity.ts
|
|
2561
|
+
import * as crypto from "crypto";
|
|
2562
|
+
function identifySession(req, pathname) {
|
|
2563
|
+
const connector = detectConnector(req, pathname);
|
|
2564
|
+
const windowHash = deriveWindowHash(req);
|
|
2565
|
+
return { connector, windowHash, raw: `${connector}:${windowHash}` };
|
|
2566
|
+
}
|
|
2567
|
+
function detectConnector(req, pathname) {
|
|
2568
|
+
if (req.headers["x-api-key"] || req.headers["anthropic-version"]) {
|
|
2569
|
+
return "claude-code";
|
|
2570
|
+
}
|
|
2571
|
+
if (pathname.startsWith("/v1/responses") || pathname.startsWith("/responses")) {
|
|
2572
|
+
return "codex";
|
|
2573
|
+
}
|
|
2574
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
2575
|
+
if (/cursor/i.test(ua)) {
|
|
2576
|
+
return "cursor";
|
|
2577
|
+
}
|
|
2578
|
+
return "openai-compatible";
|
|
2579
|
+
}
|
|
2580
|
+
function deriveWindowHash(req) {
|
|
2581
|
+
const liminalSession = req.headers["x-liminal-session"];
|
|
2582
|
+
if (typeof liminalSession === "string" && liminalSession.length > 0) {
|
|
2583
|
+
return liminalSession;
|
|
2584
|
+
}
|
|
2585
|
+
const credential = extractCredential(req);
|
|
2586
|
+
if (!credential) return "anonymous";
|
|
2587
|
+
return crypto.createHash("sha256").update(credential).digest("hex").slice(0, 8);
|
|
2588
|
+
}
|
|
2589
|
+
function extractCredential(req) {
|
|
2590
|
+
const apiKey = req.headers["x-api-key"];
|
|
2591
|
+
if (typeof apiKey === "string" && apiKey.length > 0) return apiKey;
|
|
2592
|
+
const auth = req.headers["authorization"];
|
|
2593
|
+
if (typeof auth === "string") {
|
|
2594
|
+
const match = auth.match(/^Bearer\s+(.+)$/i);
|
|
2595
|
+
if (match) return match[1];
|
|
2596
|
+
}
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2229
2600
|
// src/proxy/handler.ts
|
|
2230
2601
|
function setCORSHeaders4(res) {
|
|
2231
2602
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2232
2603
|
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");
|
|
2604
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, anthropic-dangerous-direct-browser-access, x-liminal-session");
|
|
2234
2605
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
2235
2606
|
}
|
|
2236
2607
|
function sendJSON3(res, status, body) {
|
|
@@ -2331,7 +2702,8 @@ async function passthroughToUpstream(req, res, fullUrl, config, logger) {
|
|
|
2331
2702
|
}
|
|
2332
2703
|
}
|
|
2333
2704
|
}
|
|
2334
|
-
function createRequestHandler(
|
|
2705
|
+
function createRequestHandler(deps) {
|
|
2706
|
+
const { sessions, semaphore, latencyMonitor, config, logger } = deps;
|
|
2335
2707
|
const startTime = Date.now();
|
|
2336
2708
|
return async (req, res) => {
|
|
2337
2709
|
try {
|
|
@@ -2347,27 +2719,37 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2347
2719
|
return;
|
|
2348
2720
|
}
|
|
2349
2721
|
if (method === "GET" && (url === "/health" || url === "/")) {
|
|
2350
|
-
const
|
|
2722
|
+
const sessionSummaries = sessions.getAllSummaries();
|
|
2351
2723
|
sendJSON3(res, 200, {
|
|
2352
2724
|
status: "ok",
|
|
2353
2725
|
version: config.rscApiKey ? "connected" : "no-api-key",
|
|
2354
|
-
rsc_connected: !pipeline.isCircuitOpen(),
|
|
2355
|
-
circuit_state: pipeline.getCircuitState(),
|
|
2356
|
-
session_id: summary.sessionId,
|
|
2357
2726
|
uptime_ms: Date.now() - startTime,
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2727
|
+
concurrency: {
|
|
2728
|
+
active_sessions: sessions.activeCount,
|
|
2729
|
+
semaphore_available: semaphore.available,
|
|
2730
|
+
semaphore_waiting: semaphore.waiting,
|
|
2731
|
+
max_concurrent_rsc_calls: config.concurrencyLimit
|
|
2732
|
+
},
|
|
2733
|
+
latency: {
|
|
2734
|
+
global_p95_ms: latencyMonitor.getGlobalP95()
|
|
2735
|
+
},
|
|
2736
|
+
sessions: sessionSummaries.map((s) => ({
|
|
2737
|
+
session_key: s.key,
|
|
2738
|
+
connector: s.connector,
|
|
2739
|
+
circuit_state: s.circuitState,
|
|
2740
|
+
tokens_processed: s.tokensProcessed,
|
|
2741
|
+
tokens_saved: s.tokensSaved,
|
|
2742
|
+
calls_total: s.totalCalls,
|
|
2743
|
+
calls_compressed: s.compressedCalls,
|
|
2744
|
+
calls_failed: s.failedCalls,
|
|
2745
|
+
p95_latency_ms: latencyMonitor.getSessionP95(s.key),
|
|
2746
|
+
last_active_ago_ms: Date.now() - s.lastAccessedAt
|
|
2747
|
+
}))
|
|
2368
2748
|
});
|
|
2369
2749
|
return;
|
|
2370
2750
|
}
|
|
2751
|
+
const sessionKey = identifySession(req, url);
|
|
2752
|
+
const pipeline = sessions.getOrCreate(sessionKey);
|
|
2371
2753
|
if (method === "POST" && (url === "/v1/chat/completions" || url === "/chat/completions")) {
|
|
2372
2754
|
const body = await readBody(req);
|
|
2373
2755
|
let parsed;
|
|
@@ -2379,7 +2761,7 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2379
2761
|
});
|
|
2380
2762
|
return;
|
|
2381
2763
|
}
|
|
2382
|
-
await handleChatCompletions(req, res, parsed, pipeline, config, logger);
|
|
2764
|
+
await handleChatCompletions(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
2383
2765
|
return;
|
|
2384
2766
|
}
|
|
2385
2767
|
if (method === "POST" && (url === "/v1/responses" || url === "/responses")) {
|
|
@@ -2393,7 +2775,7 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2393
2775
|
});
|
|
2394
2776
|
return;
|
|
2395
2777
|
}
|
|
2396
|
-
await handleResponses(req, res, parsed, pipeline, config, logger);
|
|
2778
|
+
await handleResponses(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
2397
2779
|
return;
|
|
2398
2780
|
}
|
|
2399
2781
|
if (method === "POST" && (url === "/v1/messages" || url === "/messages")) {
|
|
@@ -2408,7 +2790,7 @@ function createRequestHandler(pipeline, config, logger) {
|
|
|
2408
2790
|
});
|
|
2409
2791
|
return;
|
|
2410
2792
|
}
|
|
2411
|
-
await handleAnthropicMessages(req, res, parsed, pipeline, config, logger);
|
|
2793
|
+
await handleAnthropicMessages(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
|
|
2412
2794
|
return;
|
|
2413
2795
|
}
|
|
2414
2796
|
await passthroughToUpstream(req, res, fullUrl, config, logger);
|
|
@@ -2432,9 +2814,11 @@ var ProxyServer = class {
|
|
|
2432
2814
|
activePort = null;
|
|
2433
2815
|
requestedPort;
|
|
2434
2816
|
handler;
|
|
2435
|
-
|
|
2817
|
+
connectHandler;
|
|
2818
|
+
constructor(port, handler, connectHandler) {
|
|
2436
2819
|
this.requestedPort = port;
|
|
2437
2820
|
this.handler = handler;
|
|
2821
|
+
this.connectHandler = connectHandler ?? null;
|
|
2438
2822
|
}
|
|
2439
2823
|
async start() {
|
|
2440
2824
|
let lastError = null;
|
|
@@ -2456,6 +2840,9 @@ var ProxyServer = class {
|
|
|
2456
2840
|
listen(port) {
|
|
2457
2841
|
return new Promise((resolve, reject) => {
|
|
2458
2842
|
const server = http.createServer(this.handler);
|
|
2843
|
+
if (this.connectHandler) {
|
|
2844
|
+
server.on("connect", this.connectHandler);
|
|
2845
|
+
}
|
|
2459
2846
|
server.on("error", reject);
|
|
2460
2847
|
server.listen(port, "127.0.0.1", () => {
|
|
2461
2848
|
server.removeListener("error", reject);
|
|
@@ -2480,6 +2867,10 @@ var ProxyServer = class {
|
|
|
2480
2867
|
getPort() {
|
|
2481
2868
|
return this.activePort;
|
|
2482
2869
|
}
|
|
2870
|
+
/** Expose internal HTTP server for MITM bridge socket injection */
|
|
2871
|
+
getHttpServer() {
|
|
2872
|
+
return this.server;
|
|
2873
|
+
}
|
|
2483
2874
|
};
|
|
2484
2875
|
|
|
2485
2876
|
// src/daemon/logger.ts
|
|
@@ -2530,17 +2921,668 @@ var FileLogger = class {
|
|
|
2530
2921
|
}
|
|
2531
2922
|
};
|
|
2532
2923
|
|
|
2924
|
+
// src/rsc/session-manager.ts
|
|
2925
|
+
init_pipeline();
|
|
2926
|
+
var DEFAULT_MAX_SESSIONS = 10;
|
|
2927
|
+
var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
2928
|
+
var EVICTION_INTERVAL_MS = 6e4;
|
|
2929
|
+
var SessionManager = class {
|
|
2930
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2931
|
+
config;
|
|
2932
|
+
evictionTimer = null;
|
|
2933
|
+
constructor(config) {
|
|
2934
|
+
this.config = {
|
|
2935
|
+
...config,
|
|
2936
|
+
maxSessions: config.maxSessions ?? DEFAULT_MAX_SESSIONS,
|
|
2937
|
+
sessionTtlMs: config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS
|
|
2938
|
+
};
|
|
2939
|
+
this.evictionTimer = setInterval(() => this.evictStale(), EVICTION_INTERVAL_MS);
|
|
2940
|
+
if (this.evictionTimer.unref) {
|
|
2941
|
+
this.evictionTimer.unref();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
2945
|
+
getOrCreate(key) {
|
|
2946
|
+
const existing = this.sessions.get(key.raw);
|
|
2947
|
+
if (existing) {
|
|
2948
|
+
existing.lastAccessedAt = Date.now();
|
|
2949
|
+
existing.requestCount++;
|
|
2950
|
+
return existing.pipeline;
|
|
2951
|
+
}
|
|
2952
|
+
if (this.sessions.size >= this.config.maxSessions) {
|
|
2953
|
+
this.evictLRU();
|
|
2954
|
+
}
|
|
2955
|
+
const pipeline = new RSCPipelineWrapper({
|
|
2956
|
+
...this.config.pipelineConfig,
|
|
2957
|
+
sessionId: key.raw
|
|
2958
|
+
});
|
|
2959
|
+
this.sessions.set(key.raw, {
|
|
2960
|
+
pipeline,
|
|
2961
|
+
lastAccessedAt: Date.now(),
|
|
2962
|
+
requestCount: 1,
|
|
2963
|
+
connector: key.connector
|
|
2964
|
+
});
|
|
2965
|
+
this.config.onSessionCreated?.(key.raw, pipeline);
|
|
2966
|
+
return pipeline;
|
|
2967
|
+
}
|
|
2968
|
+
getAllSummaries() {
|
|
2969
|
+
const entries = [];
|
|
2970
|
+
for (const [key, managed] of this.sessions) {
|
|
2971
|
+
entries.push(this.buildHealthEntry(key, managed));
|
|
2972
|
+
}
|
|
2973
|
+
return entries;
|
|
2974
|
+
}
|
|
2975
|
+
getSessionSummary(key) {
|
|
2976
|
+
const managed = this.sessions.get(key);
|
|
2977
|
+
if (!managed) return null;
|
|
2978
|
+
return this.buildHealthEntry(key, managed);
|
|
2979
|
+
}
|
|
2980
|
+
get activeCount() {
|
|
2981
|
+
return this.sessions.size;
|
|
2982
|
+
}
|
|
2983
|
+
shutdown() {
|
|
2984
|
+
if (this.evictionTimer !== null) {
|
|
2985
|
+
clearInterval(this.evictionTimer);
|
|
2986
|
+
this.evictionTimer = null;
|
|
2987
|
+
}
|
|
2988
|
+
this.sessions.clear();
|
|
2989
|
+
}
|
|
2990
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
2991
|
+
buildHealthEntry(key, managed) {
|
|
2992
|
+
const summary = managed.pipeline.getSessionSummary();
|
|
2993
|
+
return {
|
|
2994
|
+
key,
|
|
2995
|
+
connector: managed.connector,
|
|
2996
|
+
circuitState: managed.pipeline.getCircuitState(),
|
|
2997
|
+
tokensProcessed: summary.tokensProcessed,
|
|
2998
|
+
tokensSaved: summary.tokensSaved,
|
|
2999
|
+
totalCalls: summary.totalCalls,
|
|
3000
|
+
compressedCalls: summary.compressedCalls,
|
|
3001
|
+
failedCalls: summary.failedCalls,
|
|
3002
|
+
lastAccessedAt: managed.lastAccessedAt
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
evictStale() {
|
|
3006
|
+
const now = Date.now();
|
|
3007
|
+
const cutoff = now - this.config.sessionTtlMs;
|
|
3008
|
+
for (const [key, managed] of this.sessions) {
|
|
3009
|
+
if (managed.lastAccessedAt < cutoff) {
|
|
3010
|
+
this.sessions.delete(key);
|
|
3011
|
+
this.config.onSessionEvicted?.(key);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
evictLRU() {
|
|
3016
|
+
let oldestKey = null;
|
|
3017
|
+
let oldestTime = Infinity;
|
|
3018
|
+
for (const [key, managed] of this.sessions) {
|
|
3019
|
+
if (managed.lastAccessedAt < oldestTime) {
|
|
3020
|
+
oldestTime = managed.lastAccessedAt;
|
|
3021
|
+
oldestKey = key;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
if (oldestKey !== null) {
|
|
3025
|
+
this.sessions.delete(oldestKey);
|
|
3026
|
+
this.config.onSessionEvicted?.(oldestKey);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
|
|
3031
|
+
// src/rsc/semaphore.ts
|
|
3032
|
+
var SemaphoreTimeoutError = class extends Error {
|
|
3033
|
+
constructor(timeoutMs) {
|
|
3034
|
+
super(`Semaphore acquire timed out after ${timeoutMs}ms`);
|
|
3035
|
+
this.name = "SemaphoreTimeoutError";
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
var Semaphore = class {
|
|
3039
|
+
permits;
|
|
3040
|
+
queue = [];
|
|
3041
|
+
constructor(maxPermits) {
|
|
3042
|
+
if (maxPermits < 1) throw new RangeError("maxPermits must be >= 1");
|
|
3043
|
+
this.permits = maxPermits;
|
|
3044
|
+
}
|
|
3045
|
+
get available() {
|
|
3046
|
+
return this.permits;
|
|
3047
|
+
}
|
|
3048
|
+
get waiting() {
|
|
3049
|
+
return this.queue.length;
|
|
3050
|
+
}
|
|
3051
|
+
acquire(timeoutMs) {
|
|
3052
|
+
if (this.permits > 0) {
|
|
3053
|
+
this.permits--;
|
|
3054
|
+
return Promise.resolve();
|
|
3055
|
+
}
|
|
3056
|
+
return new Promise((resolve, reject) => {
|
|
3057
|
+
const waiter = { resolve, reject };
|
|
3058
|
+
this.queue.push(waiter);
|
|
3059
|
+
if (timeoutMs !== void 0 && timeoutMs >= 0) {
|
|
3060
|
+
const timer = setTimeout(() => {
|
|
3061
|
+
const idx = this.queue.indexOf(waiter);
|
|
3062
|
+
if (idx !== -1) {
|
|
3063
|
+
this.queue.splice(idx, 1);
|
|
3064
|
+
reject(new SemaphoreTimeoutError(timeoutMs));
|
|
3065
|
+
}
|
|
3066
|
+
}, timeoutMs);
|
|
3067
|
+
const originalResolve = waiter.resolve;
|
|
3068
|
+
waiter.resolve = () => {
|
|
3069
|
+
clearTimeout(timer);
|
|
3070
|
+
originalResolve();
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
release() {
|
|
3076
|
+
const next = this.queue.shift();
|
|
3077
|
+
if (next) {
|
|
3078
|
+
next.resolve();
|
|
3079
|
+
} else {
|
|
3080
|
+
this.permits++;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
|
|
3085
|
+
// src/rsc/latency-monitor.ts
|
|
3086
|
+
var DEFAULT_CONFIG = {
|
|
3087
|
+
warningThresholdMs: 4e3,
|
|
3088
|
+
criticalThresholdMs: 8e3,
|
|
3089
|
+
windowSize: 50
|
|
3090
|
+
};
|
|
3091
|
+
var CircularBuffer = class {
|
|
3092
|
+
buffer;
|
|
3093
|
+
index = 0;
|
|
3094
|
+
count = 0;
|
|
3095
|
+
capacity;
|
|
3096
|
+
constructor(capacity) {
|
|
3097
|
+
this.capacity = capacity;
|
|
3098
|
+
this.buffer = new Array(capacity);
|
|
3099
|
+
}
|
|
3100
|
+
push(value) {
|
|
3101
|
+
this.buffer[this.index] = value;
|
|
3102
|
+
this.index = (this.index + 1) % this.capacity;
|
|
3103
|
+
if (this.count < this.capacity) {
|
|
3104
|
+
this.count++;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
getValues() {
|
|
3108
|
+
if (this.count < this.capacity) {
|
|
3109
|
+
return this.buffer.slice(0, this.count);
|
|
3110
|
+
}
|
|
3111
|
+
return [...this.buffer.slice(this.index), ...this.buffer.slice(0, this.index)];
|
|
3112
|
+
}
|
|
3113
|
+
get size() {
|
|
3114
|
+
return this.count;
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
function calculateP95(values) {
|
|
3118
|
+
if (values.length === 0) return null;
|
|
3119
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3120
|
+
const idx = Math.floor(sorted.length * 0.95);
|
|
3121
|
+
return sorted[Math.min(idx, sorted.length - 1)];
|
|
3122
|
+
}
|
|
3123
|
+
var LatencyMonitor = class {
|
|
3124
|
+
config;
|
|
3125
|
+
sessionWindows = /* @__PURE__ */ new Map();
|
|
3126
|
+
globalWindow;
|
|
3127
|
+
callbacks = [];
|
|
3128
|
+
constructor(config) {
|
|
3129
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3130
|
+
this.globalWindow = new CircularBuffer(this.config.windowSize * 4);
|
|
3131
|
+
}
|
|
3132
|
+
record(sessionKey, latencyMs) {
|
|
3133
|
+
let sessionBuf = this.sessionWindows.get(sessionKey);
|
|
3134
|
+
if (!sessionBuf) {
|
|
3135
|
+
sessionBuf = new CircularBuffer(this.config.windowSize);
|
|
3136
|
+
this.sessionWindows.set(sessionKey, sessionBuf);
|
|
3137
|
+
}
|
|
3138
|
+
sessionBuf.push(latencyMs);
|
|
3139
|
+
this.globalWindow.push(latencyMs);
|
|
3140
|
+
const globalP95 = calculateP95(this.globalWindow.getValues());
|
|
3141
|
+
if (globalP95 === null) return null;
|
|
3142
|
+
let alert = null;
|
|
3143
|
+
if (globalP95 >= this.config.criticalThresholdMs) {
|
|
3144
|
+
alert = {
|
|
3145
|
+
type: "critical",
|
|
3146
|
+
message: `Global p95 latency ${globalP95.toFixed(0)}ms exceeds critical threshold ${this.config.criticalThresholdMs}ms`,
|
|
3147
|
+
sessionKey,
|
|
3148
|
+
p95Ms: globalP95,
|
|
3149
|
+
thresholdMs: this.config.criticalThresholdMs,
|
|
3150
|
+
activeSessions: this.sessionWindows.size,
|
|
3151
|
+
suggestion: "Reduce active sessions or increase latency budget"
|
|
3152
|
+
};
|
|
3153
|
+
} else if (globalP95 >= this.config.warningThresholdMs) {
|
|
3154
|
+
alert = {
|
|
3155
|
+
type: "warning",
|
|
3156
|
+
message: `Global p95 latency ${globalP95.toFixed(0)}ms exceeds warning threshold ${this.config.warningThresholdMs}ms`,
|
|
3157
|
+
sessionKey,
|
|
3158
|
+
p95Ms: globalP95,
|
|
3159
|
+
thresholdMs: this.config.warningThresholdMs,
|
|
3160
|
+
activeSessions: this.sessionWindows.size,
|
|
3161
|
+
suggestion: "Consider reducing active sessions"
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
if (alert) {
|
|
3165
|
+
for (const cb of this.callbacks) {
|
|
3166
|
+
cb(alert);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
return alert;
|
|
3170
|
+
}
|
|
3171
|
+
getSessionP95(sessionKey) {
|
|
3172
|
+
const buf = this.sessionWindows.get(sessionKey);
|
|
3173
|
+
if (!buf) return null;
|
|
3174
|
+
return calculateP95(buf.getValues());
|
|
3175
|
+
}
|
|
3176
|
+
getGlobalP95() {
|
|
3177
|
+
return calculateP95(this.globalWindow.getValues());
|
|
3178
|
+
}
|
|
3179
|
+
onAlert(cb) {
|
|
3180
|
+
this.callbacks.push(cb);
|
|
3181
|
+
}
|
|
3182
|
+
get sessionCount() {
|
|
3183
|
+
return this.sessionWindows.size;
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// src/tls/connect-handler.ts
|
|
3188
|
+
import * as net from "net";
|
|
3189
|
+
|
|
3190
|
+
// src/tls/allowlist.ts
|
|
3191
|
+
var MITM_HOSTS = /* @__PURE__ */ new Set([
|
|
3192
|
+
"api.openai.com",
|
|
3193
|
+
"api.anthropic.com",
|
|
3194
|
+
"generativelanguage.googleapis.com"
|
|
3195
|
+
]);
|
|
3196
|
+
function shouldIntercept(hostname) {
|
|
3197
|
+
return MITM_HOSTS.has(hostname);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/tls/connect-handler.ts
|
|
3201
|
+
function createConnectHandler(options) {
|
|
3202
|
+
const { logger, onIntercept } = options;
|
|
3203
|
+
return (req, clientSocket, head) => {
|
|
3204
|
+
const target = req.url ?? "";
|
|
3205
|
+
const [hostname, portStr] = parseConnectTarget(target);
|
|
3206
|
+
const port = parseInt(portStr, 10) || 443;
|
|
3207
|
+
if (!hostname) {
|
|
3208
|
+
logger.log(`[CONNECT] Invalid target: ${target}`);
|
|
3209
|
+
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
3210
|
+
clientSocket.destroy();
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if (shouldIntercept(hostname) && onIntercept) {
|
|
3214
|
+
logger.log(`[CONNECT] ${hostname}:${port} \u2192 intercept`);
|
|
3215
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
3216
|
+
if (head.length > 0) {
|
|
3217
|
+
clientSocket.unshift(head);
|
|
3218
|
+
}
|
|
3219
|
+
onIntercept(clientSocket, hostname, port);
|
|
3220
|
+
} else {
|
|
3221
|
+
logger.log(`[TUNNEL] ${hostname}:${port} \u2192 passthrough`);
|
|
3222
|
+
const upstreamSocket = net.connect(port, hostname, () => {
|
|
3223
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
3224
|
+
if (head.length > 0) {
|
|
3225
|
+
upstreamSocket.write(head);
|
|
3226
|
+
}
|
|
3227
|
+
clientSocket.pipe(upstreamSocket);
|
|
3228
|
+
upstreamSocket.pipe(clientSocket);
|
|
3229
|
+
});
|
|
3230
|
+
upstreamSocket.on("error", (err) => {
|
|
3231
|
+
logger.log(`[TUNNEL] ${hostname}:${port} upstream error: ${err.message}`);
|
|
3232
|
+
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
3233
|
+
clientSocket.destroy();
|
|
3234
|
+
});
|
|
3235
|
+
clientSocket.on("error", (err) => {
|
|
3236
|
+
logger.log(`[TUNNEL] ${hostname}:${port} client error: ${err.message}`);
|
|
3237
|
+
upstreamSocket.destroy();
|
|
3238
|
+
});
|
|
3239
|
+
clientSocket.on("close", () => upstreamSocket.destroy());
|
|
3240
|
+
upstreamSocket.on("close", () => clientSocket.destroy());
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
function parseConnectTarget(target) {
|
|
3245
|
+
const colonIdx = target.lastIndexOf(":");
|
|
3246
|
+
if (colonIdx === -1) return [target, "443"];
|
|
3247
|
+
return [target.slice(0, colonIdx), target.slice(colonIdx + 1)];
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// src/tls/ca.ts
|
|
3251
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
|
|
3252
|
+
import { join as join5 } from "path";
|
|
3253
|
+
import forge from "node-forge";
|
|
3254
|
+
var CA_CERT_PATH = join5(LIMINAL_DIR, "ca.pem");
|
|
3255
|
+
var CA_KEY_PATH = join5(LIMINAL_DIR, "ca-key.pem");
|
|
3256
|
+
function generateCA() {
|
|
3257
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
3258
|
+
const cert = forge.pki.createCertificate();
|
|
3259
|
+
cert.publicKey = keys.publicKey;
|
|
3260
|
+
cert.serialNumber = generateSerialNumber();
|
|
3261
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
3262
|
+
cert.validity.notAfter = /* @__PURE__ */ new Date();
|
|
3263
|
+
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 5);
|
|
3264
|
+
const attrs = [
|
|
3265
|
+
{ name: "commonName", value: "Liminal Proxy CA" },
|
|
3266
|
+
{ name: "organizationName", value: "Liminal (Cognisos)" },
|
|
3267
|
+
{ shortName: "OU", value: "Local Development" }
|
|
3268
|
+
];
|
|
3269
|
+
cert.setSubject(attrs);
|
|
3270
|
+
cert.setIssuer(attrs);
|
|
3271
|
+
cert.setExtensions([
|
|
3272
|
+
{ name: "basicConstraints", cA: true, critical: true },
|
|
3273
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
|
|
3274
|
+
{
|
|
3275
|
+
name: "subjectKeyIdentifier"
|
|
3276
|
+
}
|
|
3277
|
+
]);
|
|
3278
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
3279
|
+
return {
|
|
3280
|
+
certPem: forge.pki.certificateToPem(cert),
|
|
3281
|
+
keyPem: forge.pki.privateKeyToPem(keys.privateKey)
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function generateAndSaveCA() {
|
|
3285
|
+
if (!existsSync6(LIMINAL_DIR)) {
|
|
3286
|
+
mkdirSync3(LIMINAL_DIR, { recursive: true, mode: 448 });
|
|
3287
|
+
}
|
|
3288
|
+
const { certPem, keyPem } = generateCA();
|
|
3289
|
+
writeFileSync3(CA_CERT_PATH, certPem, { encoding: "utf-8", mode: 420 });
|
|
3290
|
+
writeFileSync3(CA_KEY_PATH, keyPem, { encoding: "utf-8", mode: 384 });
|
|
3291
|
+
return { certPem, keyPem };
|
|
3292
|
+
}
|
|
3293
|
+
function loadCA() {
|
|
3294
|
+
if (!existsSync6(CA_CERT_PATH) || !existsSync6(CA_KEY_PATH)) {
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
return {
|
|
3298
|
+
certPem: readFileSync4(CA_CERT_PATH, "utf-8"),
|
|
3299
|
+
keyPem: readFileSync4(CA_KEY_PATH, "utf-8")
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
function ensureCA() {
|
|
3303
|
+
const existing = loadCA();
|
|
3304
|
+
if (existing) return existing;
|
|
3305
|
+
return generateAndSaveCA();
|
|
3306
|
+
}
|
|
3307
|
+
function hasCA() {
|
|
3308
|
+
return existsSync6(CA_CERT_PATH) && existsSync6(CA_KEY_PATH);
|
|
3309
|
+
}
|
|
3310
|
+
function removeCA() {
|
|
3311
|
+
if (existsSync6(CA_CERT_PATH)) unlinkSync(CA_CERT_PATH);
|
|
3312
|
+
if (existsSync6(CA_KEY_PATH)) unlinkSync(CA_KEY_PATH);
|
|
3313
|
+
}
|
|
3314
|
+
function getCAInfo() {
|
|
3315
|
+
const ca = loadCA();
|
|
3316
|
+
if (!ca) return null;
|
|
3317
|
+
const cert = forge.pki.certificateFromPem(ca.certPem);
|
|
3318
|
+
const cn = cert.subject.getField("CN");
|
|
3319
|
+
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
|
|
3320
|
+
const md = forge.md.sha256.create();
|
|
3321
|
+
md.update(der);
|
|
3322
|
+
const fingerprint = md.digest().toHex().match(/.{2}/g).join(":").toUpperCase();
|
|
3323
|
+
return {
|
|
3324
|
+
commonName: cn ? cn.value : "Unknown",
|
|
3325
|
+
validFrom: cert.validity.notBefore,
|
|
3326
|
+
validTo: cert.validity.notAfter,
|
|
3327
|
+
fingerprint
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
function generateSerialNumber() {
|
|
3331
|
+
const bytes = forge.random.getBytesSync(16);
|
|
3332
|
+
return forge.util.bytesToHex(bytes);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// src/tls/trust.ts
|
|
3336
|
+
import { execSync as execSync3 } from "child_process";
|
|
3337
|
+
import { existsSync as existsSync7, copyFileSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3338
|
+
function installCA() {
|
|
3339
|
+
if (!existsSync7(CA_CERT_PATH)) {
|
|
3340
|
+
return { success: false, message: 'CA certificate not found. Run "liminal init" first.', requiresSudo: false };
|
|
3341
|
+
}
|
|
3342
|
+
const platform = process.platform;
|
|
3343
|
+
if (platform === "darwin") return installMacOS();
|
|
3344
|
+
if (platform === "linux") return installLinux();
|
|
3345
|
+
if (platform === "win32") return installWindows();
|
|
3346
|
+
return { success: false, message: `Unsupported platform: ${platform}`, requiresSudo: false };
|
|
3347
|
+
}
|
|
3348
|
+
function removeCA2() {
|
|
3349
|
+
const platform = process.platform;
|
|
3350
|
+
if (platform === "darwin") return removeMacOS();
|
|
3351
|
+
if (platform === "linux") return removeLinux();
|
|
3352
|
+
if (platform === "win32") return removeWindows();
|
|
3353
|
+
return { success: false, message: `Unsupported platform: ${platform}`, requiresSudo: false };
|
|
3354
|
+
}
|
|
3355
|
+
function isCATrusted() {
|
|
3356
|
+
const platform = process.platform;
|
|
3357
|
+
if (platform === "darwin") return isTrustedMacOS();
|
|
3358
|
+
if (platform === "linux") return isTrustedLinux();
|
|
3359
|
+
if (platform === "win32") return isTrustedWindows();
|
|
3360
|
+
return false;
|
|
3361
|
+
}
|
|
3362
|
+
var MACOS_LABEL = "Liminal Proxy CA";
|
|
3363
|
+
function installMacOS() {
|
|
3364
|
+
try {
|
|
3365
|
+
execSync3(
|
|
3366
|
+
`security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db "${CA_CERT_PATH}"`,
|
|
3367
|
+
{ stdio: "pipe" }
|
|
3368
|
+
);
|
|
3369
|
+
return { success: true, message: "CA installed in login keychain (trusted for SSL)", requiresSudo: false };
|
|
3370
|
+
} catch (err) {
|
|
3371
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3372
|
+
if (msg.includes("authorization") || msg.includes("permission")) {
|
|
3373
|
+
return {
|
|
3374
|
+
success: false,
|
|
3375
|
+
message: "Keychain access denied. You may need to unlock your keychain or run with sudo.",
|
|
3376
|
+
requiresSudo: true
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: false };
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
function removeMacOS() {
|
|
3383
|
+
try {
|
|
3384
|
+
execSync3(
|
|
3385
|
+
`security delete-certificate -c "${MACOS_LABEL}" ~/Library/Keychains/login.keychain-db`,
|
|
3386
|
+
{ stdio: "pipe" }
|
|
3387
|
+
);
|
|
3388
|
+
return { success: true, message: "CA removed from login keychain", requiresSudo: false };
|
|
3389
|
+
} catch (err) {
|
|
3390
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3391
|
+
if (msg.includes("could not be found")) {
|
|
3392
|
+
return { success: true, message: "CA was not in keychain (already removed)", requiresSudo: false };
|
|
3393
|
+
}
|
|
3394
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: false };
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
function isTrustedMacOS() {
|
|
3398
|
+
try {
|
|
3399
|
+
const out = execSync3(
|
|
3400
|
+
`security find-certificate -c "${MACOS_LABEL}" ~/Library/Keychains/login.keychain-db`,
|
|
3401
|
+
{ stdio: "pipe", encoding: "utf-8" }
|
|
3402
|
+
);
|
|
3403
|
+
return out.includes(MACOS_LABEL);
|
|
3404
|
+
} catch {
|
|
3405
|
+
return false;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
var LINUX_CERT_PATH = "/usr/local/share/ca-certificates/liminal-proxy-ca.crt";
|
|
3409
|
+
function installLinux() {
|
|
3410
|
+
try {
|
|
3411
|
+
copyFileSync(CA_CERT_PATH, LINUX_CERT_PATH);
|
|
3412
|
+
execSync3("update-ca-certificates", { stdio: "pipe" });
|
|
3413
|
+
return { success: true, message: "CA installed in system trust store", requiresSudo: true };
|
|
3414
|
+
} catch (err) {
|
|
3415
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3416
|
+
if (msg.includes("EACCES") || msg.includes("permission")) {
|
|
3417
|
+
return {
|
|
3418
|
+
success: false,
|
|
3419
|
+
message: `Permission denied. Run with sudo:
|
|
3420
|
+
sudo liminal trust-ca`,
|
|
3421
|
+
requiresSudo: true
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: true };
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function removeLinux() {
|
|
3428
|
+
try {
|
|
3429
|
+
if (existsSync7(LINUX_CERT_PATH)) {
|
|
3430
|
+
unlinkSync2(LINUX_CERT_PATH);
|
|
3431
|
+
execSync3("update-ca-certificates --fresh", { stdio: "pipe" });
|
|
3432
|
+
}
|
|
3433
|
+
return { success: true, message: "CA removed from system trust store", requiresSudo: true };
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3436
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: true };
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
function isTrustedLinux() {
|
|
3440
|
+
return existsSync7(LINUX_CERT_PATH);
|
|
3441
|
+
}
|
|
3442
|
+
function installWindows() {
|
|
3443
|
+
try {
|
|
3444
|
+
execSync3(`certutil -addstore -user -f "ROOT" "${CA_CERT_PATH}"`, { stdio: "pipe" });
|
|
3445
|
+
return { success: true, message: "CA installed in user certificate store", requiresSudo: false };
|
|
3446
|
+
} catch (err) {
|
|
3447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3448
|
+
return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: false };
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
function removeWindows() {
|
|
3452
|
+
try {
|
|
3453
|
+
execSync3(`certutil -delstore -user "ROOT" "${MACOS_LABEL}"`, { stdio: "pipe" });
|
|
3454
|
+
return { success: true, message: "CA removed from user certificate store", requiresSudo: false };
|
|
3455
|
+
} catch (err) {
|
|
3456
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3457
|
+
return { success: false, message: `Failed to remove CA: ${msg}`, requiresSudo: false };
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function isTrustedWindows() {
|
|
3461
|
+
try {
|
|
3462
|
+
const out = execSync3(`certutil -verifystore -user "ROOT" "${MACOS_LABEL}"`, {
|
|
3463
|
+
stdio: "pipe",
|
|
3464
|
+
encoding: "utf-8"
|
|
3465
|
+
});
|
|
3466
|
+
return out.includes("Liminal");
|
|
3467
|
+
} catch {
|
|
3468
|
+
return false;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// src/tls/mitm-bridge.ts
|
|
3473
|
+
import * as tls from "tls";
|
|
3474
|
+
|
|
3475
|
+
// src/tls/cert-generator.ts
|
|
3476
|
+
import forge2 from "node-forge";
|
|
3477
|
+
var CERT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3478
|
+
var MAX_CACHE_SIZE = 50;
|
|
3479
|
+
var CertGenerator = class {
|
|
3480
|
+
caCert;
|
|
3481
|
+
caKey;
|
|
3482
|
+
cache = /* @__PURE__ */ new Map();
|
|
3483
|
+
constructor(caCertPem, caKeyPem) {
|
|
3484
|
+
this.caCert = forge2.pki.certificateFromPem(caCertPem);
|
|
3485
|
+
this.caKey = forge2.pki.privateKeyFromPem(caKeyPem);
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Get or generate a TLS certificate for the given hostname.
|
|
3489
|
+
* Certificates are cached for 24 hours.
|
|
3490
|
+
*/
|
|
3491
|
+
getCert(hostname) {
|
|
3492
|
+
const now = Date.now();
|
|
3493
|
+
const cached = this.cache.get(hostname);
|
|
3494
|
+
if (cached && cached.expiresAt > now) {
|
|
3495
|
+
return cached.cert;
|
|
3496
|
+
}
|
|
3497
|
+
const cert = this.generate(hostname);
|
|
3498
|
+
if (this.cache.size >= MAX_CACHE_SIZE) {
|
|
3499
|
+
const oldest = this.cache.keys().next().value;
|
|
3500
|
+
if (oldest) this.cache.delete(oldest);
|
|
3501
|
+
}
|
|
3502
|
+
this.cache.set(hostname, { cert, expiresAt: now + CERT_TTL_MS });
|
|
3503
|
+
return cert;
|
|
3504
|
+
}
|
|
3505
|
+
get cacheSize() {
|
|
3506
|
+
return this.cache.size;
|
|
3507
|
+
}
|
|
3508
|
+
generate(hostname) {
|
|
3509
|
+
const keys = forge2.pki.rsa.generateKeyPair(2048);
|
|
3510
|
+
const cert = forge2.pki.createCertificate();
|
|
3511
|
+
cert.publicKey = keys.publicKey;
|
|
3512
|
+
cert.serialNumber = randomSerial();
|
|
3513
|
+
cert.validity.notBefore = new Date(Date.now() - 24 * 60 * 60 * 1e3);
|
|
3514
|
+
cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1e3);
|
|
3515
|
+
cert.setSubject([
|
|
3516
|
+
{ name: "commonName", value: hostname },
|
|
3517
|
+
{ name: "organizationName", value: "Liminal Proxy (local)" }
|
|
3518
|
+
]);
|
|
3519
|
+
cert.setIssuer(this.caCert.subject.attributes);
|
|
3520
|
+
cert.setExtensions([
|
|
3521
|
+
{ name: "basicConstraints", cA: false },
|
|
3522
|
+
{
|
|
3523
|
+
name: "keyUsage",
|
|
3524
|
+
digitalSignature: true,
|
|
3525
|
+
keyEncipherment: true,
|
|
3526
|
+
critical: true
|
|
3527
|
+
},
|
|
3528
|
+
{
|
|
3529
|
+
name: "extKeyUsage",
|
|
3530
|
+
serverAuth: true
|
|
3531
|
+
},
|
|
3532
|
+
{
|
|
3533
|
+
name: "subjectAltName",
|
|
3534
|
+
altNames: [{ type: 2, value: hostname }]
|
|
3535
|
+
// DNS name
|
|
3536
|
+
}
|
|
3537
|
+
]);
|
|
3538
|
+
cert.sign(this.caKey, forge2.md.sha256.create());
|
|
3539
|
+
return {
|
|
3540
|
+
certPem: forge2.pki.certificateToPem(cert),
|
|
3541
|
+
keyPem: forge2.pki.privateKeyToPem(keys.privateKey)
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
};
|
|
3545
|
+
function randomSerial() {
|
|
3546
|
+
return forge2.util.bytesToHex(forge2.random.getBytesSync(16));
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// src/tls/mitm-bridge.ts
|
|
3550
|
+
function createMitmBridge(options) {
|
|
3551
|
+
const { httpServer, caCertPem, caKeyPem, logger } = options;
|
|
3552
|
+
const certGen = new CertGenerator(caCertPem, caKeyPem);
|
|
3553
|
+
return (clientSocket, hostname, _port) => {
|
|
3554
|
+
try {
|
|
3555
|
+
const { certPem, keyPem } = certGen.getCert(hostname);
|
|
3556
|
+
const tlsSocket = new tls.TLSSocket(clientSocket, {
|
|
3557
|
+
isServer: true,
|
|
3558
|
+
key: keyPem,
|
|
3559
|
+
cert: certPem
|
|
3560
|
+
});
|
|
3561
|
+
tlsSocket.on("error", (err) => {
|
|
3562
|
+
logger.log(`[MITM] TLS error for ${hostname}: ${err.message}`);
|
|
3563
|
+
tlsSocket.destroy();
|
|
3564
|
+
});
|
|
3565
|
+
httpServer.emit("connection", tlsSocket);
|
|
3566
|
+
logger.log(`[MITM] TLS bridge established for ${hostname} (cert cache: ${certGen.cacheSize})`);
|
|
3567
|
+
} catch (err) {
|
|
3568
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3569
|
+
logger.log(`[MITM] Failed to establish bridge for ${hostname}: ${message}`);
|
|
3570
|
+
clientSocket.destroy();
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
|
|
2533
3575
|
// src/daemon/lifecycle.ts
|
|
2534
|
-
import { readFileSync as
|
|
3576
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
2535
3577
|
import { fork } from "child_process";
|
|
2536
3578
|
import { fileURLToPath } from "url";
|
|
2537
3579
|
function writePidFile(pid) {
|
|
2538
|
-
|
|
3580
|
+
writeFileSync4(PID_FILE, String(pid), "utf-8");
|
|
2539
3581
|
}
|
|
2540
3582
|
function readPidFile() {
|
|
2541
|
-
if (!
|
|
3583
|
+
if (!existsSync8(PID_FILE)) return null;
|
|
2542
3584
|
try {
|
|
2543
|
-
const content =
|
|
3585
|
+
const content = readFileSync5(PID_FILE, "utf-8").trim();
|
|
2544
3586
|
const pid = parseInt(content, 10);
|
|
2545
3587
|
return isNaN(pid) ? null : pid;
|
|
2546
3588
|
} catch {
|
|
@@ -2549,7 +3591,7 @@ function readPidFile() {
|
|
|
2549
3591
|
}
|
|
2550
3592
|
function removePidFile() {
|
|
2551
3593
|
try {
|
|
2552
|
-
if (
|
|
3594
|
+
if (existsSync8(PID_FILE)) unlinkSync3(PID_FILE);
|
|
2553
3595
|
} catch {
|
|
2554
3596
|
}
|
|
2555
3597
|
}
|
|
@@ -2651,37 +3693,81 @@ async function startCommand(flags) {
|
|
|
2651
3693
|
rscBaseUrl: config.apiBaseUrl,
|
|
2652
3694
|
proxyPort: config.port,
|
|
2653
3695
|
compressionThreshold: config.compressionThreshold,
|
|
3696
|
+
aggregateThreshold: config.aggregateThreshold,
|
|
3697
|
+
hotFraction: config.hotFraction,
|
|
3698
|
+
coldFraction: config.coldFraction,
|
|
2654
3699
|
compressRoles: config.compressRoles,
|
|
3700
|
+
compressToolResults: config.compressToolResults,
|
|
2655
3701
|
learnFromResponses: config.learnFromResponses,
|
|
2656
3702
|
latencyBudgetMs: config.latencyBudgetMs || void 0,
|
|
2657
3703
|
upstreamBaseUrl: config.upstreamBaseUrl,
|
|
2658
3704
|
anthropicUpstreamUrl: config.anthropicUpstreamUrl,
|
|
2659
3705
|
enabled: config.enabled,
|
|
2660
|
-
tools: config.tools
|
|
3706
|
+
tools: config.tools,
|
|
3707
|
+
concurrencyLimit: config.concurrencyLimit,
|
|
3708
|
+
concurrencyTimeoutMs: config.concurrencyTimeoutMs,
|
|
3709
|
+
maxSessions: config.maxSessions,
|
|
3710
|
+
sessionTtlMs: config.sessionTtlMs,
|
|
3711
|
+
latencyWarningMs: config.latencyWarningMs,
|
|
3712
|
+
latencyCriticalMs: config.latencyCriticalMs
|
|
2661
3713
|
};
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
learnFromResponses: config.learnFromResponses,
|
|
2667
|
-
latencyBudgetMs: config.latencyBudgetMs || void 0
|
|
3714
|
+
const semaphore = new Semaphore(resolvedConfig.concurrencyLimit);
|
|
3715
|
+
const latencyMonitor = new LatencyMonitor({
|
|
3716
|
+
warningThresholdMs: resolvedConfig.latencyWarningMs,
|
|
3717
|
+
criticalThresholdMs: resolvedConfig.latencyCriticalMs
|
|
2668
3718
|
});
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
3719
|
+
function wireSessionEvents(key, pipeline) {
|
|
3720
|
+
pipeline.events.on("compression", (event) => {
|
|
3721
|
+
if (event.tokensSaved > 0) {
|
|
3722
|
+
logger.log(`[LIMINAL] [${key}] Compressed: ${event.tokensSaved} tokens saved (${event.ratio.toFixed(3)} ratio)`);
|
|
3723
|
+
}
|
|
3724
|
+
});
|
|
3725
|
+
pipeline.events.on("compression_skipped", (event) => {
|
|
3726
|
+
const detail = event.reason === "latency_budget" ? `${event.reason} (${event.estimatedTokens}tok, budget:${event.budgetMs}ms, elapsed:${event.elapsedMs}ms)` : event.reason === "below_threshold" ? `${event.reason} (${event.estimatedTokens}tok < ${config.compressionThreshold}tok threshold)` : `${event.reason} (${event.estimatedTokens}tok)`;
|
|
3727
|
+
logger.log(`[LIMINAL] [${key}] Skipped: ${detail}`);
|
|
3728
|
+
});
|
|
3729
|
+
pipeline.events.on("error", (event) => {
|
|
3730
|
+
logger.log(`[LIMINAL] [${key}] Error: ${event.error.message}`);
|
|
3731
|
+
});
|
|
3732
|
+
pipeline.events.on("degradation", (event) => {
|
|
3733
|
+
logger.log(`[LIMINAL] [${key}] Circuit ${event.circuitState}: ${event.reason}`);
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
const sessions = new SessionManager({
|
|
3737
|
+
pipelineConfig: {
|
|
3738
|
+
rscApiKey: config.apiKey,
|
|
3739
|
+
rscBaseUrl: config.apiBaseUrl,
|
|
3740
|
+
compressionThreshold: config.compressionThreshold,
|
|
3741
|
+
learnFromResponses: config.learnFromResponses,
|
|
3742
|
+
latencyBudgetMs: config.latencyBudgetMs || void 0
|
|
3743
|
+
},
|
|
3744
|
+
maxSessions: resolvedConfig.maxSessions,
|
|
3745
|
+
sessionTtlMs: resolvedConfig.sessionTtlMs,
|
|
3746
|
+
onSessionCreated: (key, pipeline) => {
|
|
3747
|
+
logger.log(`[SESSION] Created: ${key}`);
|
|
3748
|
+
wireSessionEvents(key, pipeline);
|
|
3749
|
+
},
|
|
3750
|
+
onSessionEvicted: (key) => {
|
|
3751
|
+
logger.log(`[SESSION] Evicted: ${key} (idle)`);
|
|
2672
3752
|
}
|
|
2673
3753
|
});
|
|
2674
|
-
|
|
2675
|
-
logger.log(`[
|
|
2676
|
-
});
|
|
2677
|
-
pipeline.events.on("error", (event) => {
|
|
2678
|
-
logger.log(`[LIMINAL] Error: ${event.error.message}`);
|
|
3754
|
+
latencyMonitor.onAlert((alert) => {
|
|
3755
|
+
logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message} (${alert.activeSessions} sessions) \u2014 ${alert.suggestion}`);
|
|
2679
3756
|
});
|
|
2680
|
-
|
|
2681
|
-
|
|
3757
|
+
const deps = { sessions, semaphore, latencyMonitor, config: resolvedConfig, logger };
|
|
3758
|
+
const handler = createRequestHandler(deps);
|
|
3759
|
+
let mitmHandler;
|
|
3760
|
+
const connectHandler = createConnectHandler({
|
|
3761
|
+
logger,
|
|
3762
|
+
onIntercept: (socket, hostname, port) => {
|
|
3763
|
+
if (mitmHandler) {
|
|
3764
|
+
mitmHandler(socket, hostname, port);
|
|
3765
|
+
} else {
|
|
3766
|
+
logger.log(`[MITM] No bridge available for ${hostname} \u2014 falling back to passthrough`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
2682
3769
|
});
|
|
2683
|
-
const
|
|
2684
|
-
const server = new ProxyServer(config.port, handler);
|
|
3770
|
+
const server = new ProxyServer(config.port, handler, connectHandler);
|
|
2685
3771
|
setupSignalHandlers(server, logger);
|
|
2686
3772
|
try {
|
|
2687
3773
|
const actualPort = await server.start();
|
|
@@ -2691,15 +3777,42 @@ async function startCommand(flags) {
|
|
|
2691
3777
|
logger.log(`[DAEMON] Upstream (Anthropic): ${config.anthropicUpstreamUrl}`);
|
|
2692
3778
|
logger.log(`[DAEMON] Liminal API: ${config.apiBaseUrl}`);
|
|
2693
3779
|
logger.log(`[DAEMON] PID: ${process.pid}`);
|
|
3780
|
+
logger.log(`[DAEMON] Max sessions: ${resolvedConfig.maxSessions}, Concurrency limit: ${resolvedConfig.concurrencyLimit}`);
|
|
3781
|
+
const caReady = hasCA() && isCATrusted();
|
|
3782
|
+
if (caReady) {
|
|
3783
|
+
const httpServer = server.getHttpServer();
|
|
3784
|
+
const ca = loadCA();
|
|
3785
|
+
if (httpServer && ca) {
|
|
3786
|
+
mitmHandler = createMitmBridge({
|
|
3787
|
+
httpServer,
|
|
3788
|
+
caCertPem: ca.certPem,
|
|
3789
|
+
caKeyPem: ca.keyPem,
|
|
3790
|
+
logger
|
|
3791
|
+
});
|
|
3792
|
+
logger.log("[MITM] TLS bridge active \u2014 intercepting LLM API calls");
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
logger.log(`[DAEMON] CONNECT handler: active | MITM: ${caReady ? "ready (CA trusted)" : "passthrough only (run liminal trust-ca)"}`);
|
|
2694
3796
|
if (isForeground && !isForked) {
|
|
2695
3797
|
printBanner();
|
|
2696
3798
|
console.log(` Liminal proxy running on http://127.0.0.1:${actualPort}/v1`);
|
|
2697
3799
|
console.log(` Upstream: ${config.upstreamBaseUrl}`);
|
|
3800
|
+
console.log(` Max sessions: ${resolvedConfig.maxSessions} | Concurrency: ${resolvedConfig.concurrencyLimit}`);
|
|
3801
|
+
if (caReady) {
|
|
3802
|
+
console.log(" MITM: active (Cursor interception ready)");
|
|
3803
|
+
}
|
|
2698
3804
|
console.log();
|
|
2699
3805
|
console.log(" Point your AI tool's base URL here. Press Ctrl+C to stop.");
|
|
2700
3806
|
console.log();
|
|
2701
3807
|
}
|
|
2702
|
-
const
|
|
3808
|
+
const { RSCPipelineWrapper: RSCPipelineWrapper2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
3809
|
+
const probe = new RSCPipelineWrapper2({
|
|
3810
|
+
rscApiKey: config.apiKey,
|
|
3811
|
+
rscBaseUrl: config.apiBaseUrl,
|
|
3812
|
+
compressionThreshold: config.compressionThreshold,
|
|
3813
|
+
learnFromResponses: false
|
|
3814
|
+
});
|
|
3815
|
+
const healthy = await probe.healthCheck();
|
|
2703
3816
|
if (healthy) {
|
|
2704
3817
|
logger.log("[DAEMON] Liminal API health check: OK");
|
|
2705
3818
|
} else {
|
|
@@ -2766,19 +3879,39 @@ async function statusCommand() {
|
|
|
2766
3879
|
const data = await res.json();
|
|
2767
3880
|
const uptime = formatUptime(data.uptime_ms);
|
|
2768
3881
|
console.log(`Liminal Daemon: running (PID ${state.pid}, port ${port})`);
|
|
2769
|
-
console.log(`
|
|
2770
|
-
console.log(`Session: ${data.session_id}`);
|
|
3882
|
+
console.log(`Status: ${data.status} (${data.version})`);
|
|
2771
3883
|
console.log(`Uptime: ${uptime}`);
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3884
|
+
console.log();
|
|
3885
|
+
const c = data.concurrency;
|
|
3886
|
+
console.log(`Sessions: ${c.active_sessions} active (max ${config.maxSessions})`);
|
|
3887
|
+
console.log(`Semaphore: ${c.semaphore_available}/${c.max_concurrent_rsc_calls} available` + (c.semaphore_waiting > 0 ? ` (${c.semaphore_waiting} waiting)` : ""));
|
|
3888
|
+
const globalP95 = data.latency.global_p95_ms;
|
|
3889
|
+
if (globalP95 !== null) {
|
|
3890
|
+
const latencyFlag = globalP95 >= config.latencyCriticalMs ? " CRITICAL" : globalP95 >= config.latencyWarningMs ? " WARNING" : "";
|
|
3891
|
+
console.log(`Latency: p95 ${globalP95.toFixed(0)}ms${latencyFlag}`);
|
|
3892
|
+
}
|
|
3893
|
+
if (data.sessions.length > 0) {
|
|
3894
|
+
console.log();
|
|
3895
|
+
console.log("\u2500\u2500\u2500 Sessions \u2500\u2500\u2500");
|
|
3896
|
+
for (const s of data.sessions) {
|
|
3897
|
+
const savingsPercent = s.tokens_processed > 0 ? (s.tokens_saved / s.tokens_processed * 100).toFixed(1) : "0.0";
|
|
3898
|
+
const activeAgo = formatUptime(s.last_active_ago_ms);
|
|
3899
|
+
const p95 = s.p95_latency_ms !== null ? `${s.p95_latency_ms.toFixed(0)}ms` : "-";
|
|
3900
|
+
console.log();
|
|
3901
|
+
console.log(` ${s.connector} [${s.session_key}]`);
|
|
3902
|
+
console.log(` Circuit: ${s.circuit_state}`);
|
|
3903
|
+
console.log(` Tokens: ${s.tokens_processed.toLocaleString()} processed, ${s.tokens_saved.toLocaleString()} saved (${savingsPercent}%)`);
|
|
3904
|
+
console.log(` Calls: ${s.calls_total} total (${s.calls_compressed} compressed, ${s.calls_failed} failed)`);
|
|
3905
|
+
console.log(` Latency: p95 ${p95}`);
|
|
3906
|
+
console.log(` Active: ${activeAgo} ago`);
|
|
3907
|
+
}
|
|
3908
|
+
} else {
|
|
2775
3909
|
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)`);
|
|
3910
|
+
console.log("No active sessions.");
|
|
2778
3911
|
}
|
|
2779
3912
|
} catch {
|
|
2780
3913
|
console.log(`Liminal Daemon: running (PID ${state.pid}, port ${port})`);
|
|
2781
|
-
console.log("
|
|
3914
|
+
console.log("Status: unknown (could not reach /health)");
|
|
2782
3915
|
}
|
|
2783
3916
|
}
|
|
2784
3917
|
function formatUptime(ms) {
|
|
@@ -2930,17 +4063,17 @@ async function configCommand(flags) {
|
|
|
2930
4063
|
}
|
|
2931
4064
|
|
|
2932
4065
|
// src/commands/logs.ts
|
|
2933
|
-
import { readFileSync as
|
|
4066
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync2, createReadStream } from "fs";
|
|
2934
4067
|
import { watchFile, unwatchFile } from "fs";
|
|
2935
4068
|
async function logsCommand(flags) {
|
|
2936
4069
|
const follow = flags.has("follow") || flags.has("f");
|
|
2937
4070
|
const linesFlag = flags.get("lines") ?? flags.get("n");
|
|
2938
4071
|
const lines = typeof linesFlag === "string" ? parseInt(linesFlag, 10) : 50;
|
|
2939
|
-
if (!
|
|
4072
|
+
if (!existsSync9(LOG_FILE)) {
|
|
2940
4073
|
console.log('No log file found. Start the daemon with "liminal start" to generate logs.');
|
|
2941
4074
|
return;
|
|
2942
4075
|
}
|
|
2943
|
-
const content =
|
|
4076
|
+
const content = readFileSync6(LOG_FILE, "utf-8");
|
|
2944
4077
|
const allLines = content.split("\n");
|
|
2945
4078
|
const tail = allLines.slice(-lines - 1);
|
|
2946
4079
|
process.stdout.write(tail.join("\n"));
|
|
@@ -2966,16 +4099,16 @@ async function logsCommand(flags) {
|
|
|
2966
4099
|
}
|
|
2967
4100
|
|
|
2968
4101
|
// src/commands/uninstall.ts
|
|
2969
|
-
import { existsSync as
|
|
4102
|
+
import { existsSync as existsSync10, rmSync, readFileSync as readFileSync7 } from "fs";
|
|
2970
4103
|
var BOLD2 = "\x1B[1m";
|
|
2971
4104
|
var DIM2 = "\x1B[2m";
|
|
2972
4105
|
var GREEN2 = "\x1B[32m";
|
|
2973
4106
|
var YELLOW2 = "\x1B[33m";
|
|
2974
4107
|
var RESET2 = "\x1B[0m";
|
|
2975
4108
|
function loadConfiguredTools() {
|
|
2976
|
-
if (!
|
|
4109
|
+
if (!existsSync10(CONFIG_FILE)) return [];
|
|
2977
4110
|
try {
|
|
2978
|
-
const raw =
|
|
4111
|
+
const raw = readFileSync7(CONFIG_FILE, "utf-8");
|
|
2979
4112
|
const config = JSON.parse(raw);
|
|
2980
4113
|
if (Array.isArray(config.tools)) return config.tools;
|
|
2981
4114
|
} catch {
|
|
@@ -3063,7 +4196,7 @@ async function uninstallCommand() {
|
|
|
3063
4196
|
}
|
|
3064
4197
|
}
|
|
3065
4198
|
}
|
|
3066
|
-
if (
|
|
4199
|
+
if (existsSync10(LIMINAL_DIR)) {
|
|
3067
4200
|
console.log();
|
|
3068
4201
|
const removeData = await selectPrompt({
|
|
3069
4202
|
message: "Remove ~/.liminal/ directory? (config, logs, PID file)",
|
|
@@ -3094,6 +4227,90 @@ async function uninstallCommand() {
|
|
|
3094
4227
|
console.log();
|
|
3095
4228
|
}
|
|
3096
4229
|
|
|
4230
|
+
// src/commands/trust-ca.ts
|
|
4231
|
+
async function trustCACommand() {
|
|
4232
|
+
printBanner();
|
|
4233
|
+
if (isCATrusted()) {
|
|
4234
|
+
console.log(" Liminal CA is already trusted.");
|
|
4235
|
+
const info = getCAInfo();
|
|
4236
|
+
if (info) {
|
|
4237
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4238
|
+
console.log(` Valid until: ${info.validTo.toLocaleDateString()}`);
|
|
4239
|
+
}
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
if (!hasCA()) {
|
|
4243
|
+
console.log(" Generating CA certificate...");
|
|
4244
|
+
ensureCA();
|
|
4245
|
+
console.log(" Created ~/.liminal/ca.pem");
|
|
4246
|
+
console.log();
|
|
4247
|
+
}
|
|
4248
|
+
console.log(" Installing Liminal CA certificate");
|
|
4249
|
+
console.log();
|
|
4250
|
+
console.log(" This allows Liminal to transparently compress LLM API");
|
|
4251
|
+
console.log(" traffic from Cursor and other Electron-based editors.");
|
|
4252
|
+
console.log();
|
|
4253
|
+
console.log(" The certificate is scoped to your user account and only");
|
|
4254
|
+
console.log(" used by the local Liminal proxy on 127.0.0.1.");
|
|
4255
|
+
console.log();
|
|
4256
|
+
console.log(` Certificate: ${CA_CERT_PATH}`);
|
|
4257
|
+
console.log();
|
|
4258
|
+
const result = installCA();
|
|
4259
|
+
if (result.success) {
|
|
4260
|
+
console.log(` ${result.message}`);
|
|
4261
|
+
console.log();
|
|
4262
|
+
const info = getCAInfo();
|
|
4263
|
+
if (info) {
|
|
4264
|
+
console.log(` Fingerprint: ${info.fingerprint}`);
|
|
4265
|
+
console.log(` Valid until: ${info.validTo.toLocaleDateString()}`);
|
|
4266
|
+
}
|
|
4267
|
+
console.log();
|
|
4268
|
+
console.log(" You can now use Liminal with Cursor:");
|
|
4269
|
+
console.log(" liminal start");
|
|
4270
|
+
console.log(" cursor --proxy-server=http://127.0.0.1:3141 --disable-http2");
|
|
4271
|
+
} else {
|
|
4272
|
+
console.error(` Failed: ${result.message}`);
|
|
4273
|
+
if (result.requiresSudo) {
|
|
4274
|
+
console.log();
|
|
4275
|
+
console.log(" Try running with elevated permissions:");
|
|
4276
|
+
console.log(" sudo liminal trust-ca");
|
|
4277
|
+
}
|
|
4278
|
+
process.exit(1);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
// src/commands/untrust-ca.ts
|
|
4283
|
+
async function untrustCACommand() {
|
|
4284
|
+
printBanner();
|
|
4285
|
+
if (!hasCA() && !isCATrusted()) {
|
|
4286
|
+
console.log(" No Liminal CA found (nothing to remove).");
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
if (isCATrusted()) {
|
|
4290
|
+
console.log(" Removing CA from system trust store...");
|
|
4291
|
+
const result = removeCA2();
|
|
4292
|
+
if (result.success) {
|
|
4293
|
+
console.log(` ${result.message}`);
|
|
4294
|
+
} else {
|
|
4295
|
+
console.error(` ${result.message}`);
|
|
4296
|
+
if (result.requiresSudo) {
|
|
4297
|
+
console.log();
|
|
4298
|
+
console.log(" Try running with elevated permissions:");
|
|
4299
|
+
console.log(" sudo liminal untrust-ca");
|
|
4300
|
+
}
|
|
4301
|
+
process.exit(1);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
if (hasCA()) {
|
|
4305
|
+
console.log(" Removing CA certificate files...");
|
|
4306
|
+
removeCA();
|
|
4307
|
+
console.log(" Removed ~/.liminal/ca.pem and ca-key.pem");
|
|
4308
|
+
}
|
|
4309
|
+
console.log();
|
|
4310
|
+
console.log(" Liminal CA fully removed.");
|
|
4311
|
+
console.log(" Cursor MITM interception is no longer available.");
|
|
4312
|
+
}
|
|
4313
|
+
|
|
3097
4314
|
// src/bin.ts
|
|
3098
4315
|
var USAGE = `
|
|
3099
4316
|
liminal v${VERSION} \u2014 Transparent LLM context compression proxy
|
|
@@ -3108,6 +4325,8 @@ var USAGE = `
|
|
|
3108
4325
|
liminal summary Detailed session metrics
|
|
3109
4326
|
liminal config [--set k=v] [--get k] View or edit configuration
|
|
3110
4327
|
liminal logs [--follow] [--lines N] View proxy logs
|
|
4328
|
+
liminal trust-ca Install CA cert (for Cursor MITM)
|
|
4329
|
+
liminal untrust-ca Remove CA cert
|
|
3111
4330
|
liminal uninstall Remove Liminal configuration
|
|
3112
4331
|
|
|
3113
4332
|
Options:
|
|
@@ -3120,7 +4339,7 @@ var USAGE = `
|
|
|
3120
4339
|
3. Connect your AI tools:
|
|
3121
4340
|
Claude Code: export ANTHROPIC_BASE_URL=http://localhost:3141
|
|
3122
4341
|
Codex: export OPENAI_BASE_URL=http://localhost:3141/v1
|
|
3123
|
-
Cursor:
|
|
4342
|
+
Cursor: liminal trust-ca && cursor --proxy-server=http://localhost:3141
|
|
3124
4343
|
`;
|
|
3125
4344
|
function parseArgs(argv) {
|
|
3126
4345
|
const command = argv[2] ?? "";
|
|
@@ -3186,6 +4405,12 @@ async function main() {
|
|
|
3186
4405
|
case "logs":
|
|
3187
4406
|
await logsCommand(flags);
|
|
3188
4407
|
break;
|
|
4408
|
+
case "trust-ca":
|
|
4409
|
+
await trustCACommand();
|
|
4410
|
+
break;
|
|
4411
|
+
case "untrust-ca":
|
|
4412
|
+
await untrustCACommand();
|
|
4413
|
+
break;
|
|
3189
4414
|
case "uninstall":
|
|
3190
4415
|
await uninstallCommand();
|
|
3191
4416
|
break;
|