@cognisos/liminal 2.3.0 → 2.4.1

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