@cognisos/liminal 2.2.2 → 2.4.0

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 +1384 -235
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var VERSION = true ? "2.2.2" : "0.2.1";
4
+ var VERSION = true ? "2.4.0" : "0.2.1";
5
5
  var BANNER_LINES = [
6
6
  " ___ ___ _____ ______ ___ ________ ________ ___",
7
7
  "|\\ \\ |\\ \\|\\ _ \\ _ \\|\\ \\|\\ ___ \\|\\ __ \\|\\ \\",
@@ -21,15 +21,8 @@ function printBanner() {
21
21
  console.log();
22
22
  }
23
23
 
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
24
  // src/config/loader.ts
32
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
25
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
33
26
  import { dirname } from "path";
34
27
 
35
28
  // src/config/paths.ts
@@ -43,14 +36,14 @@ var LOG_FILE = join(LOG_DIR, "liminal.log");
43
36
 
44
37
  // src/config/schema.ts
45
38
  var DEFAULTS = {
46
- apiBaseUrl: "https://rsc-platform-production.up.railway.app",
39
+ apiBaseUrl: "https://api.cognisos.ai",
47
40
  upstreamBaseUrl: "https://api.openai.com",
48
41
  anthropicUpstreamUrl: "https://api.anthropic.com",
49
42
  port: 3141,
50
43
  compressionThreshold: 100,
51
44
  compressRoles: ["user"],
52
45
  learnFromResponses: true,
53
- latencyBudgetMs: 0,
46
+ latencyBudgetMs: 5e3,
54
47
  enabled: true,
55
48
  tools: []
56
49
  };
@@ -111,11 +104,15 @@ function saveConfig(config) {
111
104
  }
112
105
  }
113
106
  const merged = { ...existing, ...config };
114
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf-8");
107
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
108
+ try {
109
+ chmodSync(CONFIG_FILE, 384);
110
+ } catch {
111
+ }
115
112
  }
116
113
  function ensureDirectories() {
117
- if (!existsSync(LIMINAL_DIR)) mkdirSync(LIMINAL_DIR, { recursive: true });
118
- if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
114
+ if (!existsSync(LIMINAL_DIR)) mkdirSync(LIMINAL_DIR, { recursive: true, mode: 448 });
115
+ if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true, mode: 448 });
119
116
  const configDir = dirname(CONFIG_FILE);
120
117
  if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
121
118
  }
@@ -132,6 +129,99 @@ function maskApiKey(key) {
132
129
  return key.slice(0, 8) + "..." + key.slice(-4);
133
130
  }
134
131
 
132
+ // src/config/shell.ts
133
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync } from "fs";
134
+ import { join as join2 } from "path";
135
+ import { homedir as homedir2 } from "os";
136
+ var LIMINAL_BLOCK_HEADER = "# Liminal \u2014 route AI tools through compression proxy";
137
+ function detectShellProfile() {
138
+ const shell = process.env.SHELL || "";
139
+ const home = homedir2();
140
+ if (shell.endsWith("/zsh")) {
141
+ return { name: "~/.zshrc", path: join2(home, ".zshrc") };
142
+ }
143
+ if (shell.endsWith("/bash")) {
144
+ const bashProfile = join2(home, ".bash_profile");
145
+ if (existsSync2(bashProfile)) {
146
+ return { name: "~/.bash_profile", path: bashProfile };
147
+ }
148
+ return { name: "~/.bashrc", path: join2(home, ".bashrc") };
149
+ }
150
+ const candidates = [
151
+ { name: "~/.zshrc", path: join2(home, ".zshrc") },
152
+ { name: "~/.bashrc", path: join2(home, ".bashrc") },
153
+ { name: "~/.profile", path: join2(home, ".profile") }
154
+ ];
155
+ for (const c of candidates) {
156
+ if (existsSync2(c.path)) return c;
157
+ }
158
+ return null;
159
+ }
160
+ function lineExistsInFile(filePath, line) {
161
+ if (!existsSync2(filePath)) return false;
162
+ try {
163
+ const content = readFileSync2(filePath, "utf-8");
164
+ return content.includes(line);
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+ function appendToShellProfile(profile, lines) {
170
+ const newLines = lines.filter((line) => !lineExistsInFile(profile.path, line));
171
+ if (newLines.length === 0) return [];
172
+ const block = [
173
+ "",
174
+ LIMINAL_BLOCK_HEADER,
175
+ ...newLines
176
+ ].join("\n") + "\n";
177
+ appendFileSync(profile.path, block, "utf-8");
178
+ return newLines;
179
+ }
180
+ function removeLiminalFromShellProfile(profile) {
181
+ if (!existsSync2(profile.path)) return [];
182
+ let content;
183
+ try {
184
+ content = readFileSync2(profile.path, "utf-8");
185
+ } catch {
186
+ return [];
187
+ }
188
+ const lines = content.split("\n");
189
+ const removed = [];
190
+ const kept = [];
191
+ for (const line of lines) {
192
+ if (line.trim() === LIMINAL_BLOCK_HEADER) {
193
+ removed.push(line);
194
+ continue;
195
+ }
196
+ if (isLiminalExportLine(line)) {
197
+ removed.push(line);
198
+ continue;
199
+ }
200
+ kept.push(line);
201
+ }
202
+ if (removed.length > 0) {
203
+ const cleaned = kept.join("\n").replace(/\n{3,}/g, "\n\n");
204
+ writeFileSync2(profile.path, cleaned, "utf-8");
205
+ }
206
+ return removed;
207
+ }
208
+ function isLiminalExportLine(line) {
209
+ const trimmed = line.trim();
210
+ if (!trimmed.startsWith("export ")) return false;
211
+ if (trimmed.includes("ANTHROPIC_BASE_URL=http://127.0.0.1:")) return true;
212
+ if (trimmed.includes("OPENAI_BASE_URL=http://127.0.0.1:")) return true;
213
+ return false;
214
+ }
215
+ function findLiminalExportsInProfile(profile) {
216
+ if (!existsSync2(profile.path)) return [];
217
+ try {
218
+ const content = readFileSync2(profile.path, "utf-8");
219
+ return content.split("\n").filter(isLiminalExportLine);
220
+ } catch {
221
+ return [];
222
+ }
223
+ }
224
+
135
225
  // src/ui/prompts.ts
136
226
  var ANSI = {
137
227
  HIDE_CURSOR: "\x1B[?25l",
@@ -190,23 +280,23 @@ function renderMultiSelect(options, cursorIndex, selected, message) {
190
280
  lines.push(` ${pointer} ${box} ${label}${desc}`);
191
281
  }
192
282
  lines.push("");
193
- lines.push(` ${ANSI.DIM}Space to toggle, Enter to confirm${ANSI.RESET}`);
283
+ 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
284
  lines.push("");
195
285
  return { text: lines.join("\n"), lineCount: lines.length };
196
286
  }
197
287
  function withRawMode(streams, handler) {
198
- const { stdin: stdin3, stdout: stdout3 } = streams;
288
+ const { stdin: stdin2, stdout: stdout2 } = streams;
199
289
  return new Promise((resolve, reject) => {
200
290
  let cleaned = false;
201
291
  function cleanup() {
202
292
  if (cleaned) return;
203
293
  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();
294
+ stdin2.removeListener("data", onData);
295
+ if (stdin2.setRawMode) stdin2.setRawMode(false);
296
+ if ("pause" in stdin2 && typeof stdin2.pause === "function") {
297
+ stdin2.pause();
208
298
  }
209
- stdout3.write(ANSI.SHOW_CURSOR);
299
+ stdout2.write(ANSI.SHOW_CURSOR);
210
300
  process.removeListener("exit", cleanup);
211
301
  }
212
302
  function onData(data) {
@@ -223,11 +313,11 @@ function withRawMode(streams, handler) {
223
313
  });
224
314
  process.on("exit", cleanup);
225
315
  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();
316
+ if (stdin2.setRawMode) stdin2.setRawMode(true);
317
+ stdout2.write(ANSI.HIDE_CURSOR);
318
+ stdin2.on("data", onData);
319
+ if ("resume" in stdin2 && typeof stdin2.resume === "function") {
320
+ stdin2.resume();
231
321
  }
232
322
  } catch (err) {
233
323
  cleanup();
@@ -575,59 +665,322 @@ async function runAuthFlow() {
575
665
  }
576
666
  }
577
667
 
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 };
668
+ // src/connectors/claude-code.ts
669
+ import { execSync } from "child_process";
670
+ var ENV_VAR = "ANTHROPIC_BASE_URL";
671
+ var INFO = {
672
+ id: "claude-code",
673
+ label: "Claude Code",
674
+ description: "Anthropic CLI for coding with Claude",
675
+ protocol: "anthropic-messages",
676
+ automatable: true
677
+ };
678
+ function isClaudeInstalled() {
679
+ try {
680
+ execSync("which claude", { stdio: "ignore" });
681
+ return true;
682
+ } catch {
683
+ return false;
585
684
  }
586
- if (shell.endsWith("/bash")) {
587
- const bashProfile = join2(home, ".bash_profile");
588
- if (existsSync2(bashProfile)) {
589
- return { name: "~/.bash_profile", path: bashProfile };
685
+ }
686
+ function getCurrentBaseUrl() {
687
+ return process.env[ENV_VAR] || void 0;
688
+ }
689
+ var claudeCodeConnector = {
690
+ info: INFO,
691
+ async detect() {
692
+ const installed = isClaudeInstalled();
693
+ const currentUrl = getCurrentBaseUrl();
694
+ const configured = currentUrl?.includes("127.0.0.1") ?? false;
695
+ if (!installed) {
696
+ return { installed, configured: false, detail: "Claude Code not found in PATH" };
590
697
  }
591
- return { name: "~/.bashrc", path: join2(home, ".bashrc") };
698
+ if (configured) {
699
+ return { installed, configured, detail: `Routing through ${currentUrl}` };
700
+ }
701
+ return { installed, configured, detail: "Installed but not routing through Liminal" };
702
+ },
703
+ getShellExports(port) {
704
+ return [`export ${ENV_VAR}=http://127.0.0.1:${port}`];
705
+ },
706
+ async setup(port) {
707
+ const exports = this.getShellExports(port);
708
+ return {
709
+ success: true,
710
+ shellExports: exports,
711
+ postSetupInstructions: [
712
+ "Claude Code will automatically route through Liminal.",
713
+ "Make sure to source your shell profile or restart your terminal."
714
+ ]
715
+ };
716
+ },
717
+ async teardown() {
718
+ return {
719
+ success: true,
720
+ manualSteps: [
721
+ `Remove the line \`export ${ENV_VAR}=...\` from your shell profile (~/.zshrc or ~/.bashrc).`,
722
+ "Restart your terminal or run: unset ANTHROPIC_BASE_URL"
723
+ ]
724
+ };
592
725
  }
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;
726
+ };
727
+
728
+ // src/connectors/codex.ts
729
+ import { execSync as execSync2 } from "child_process";
730
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
731
+ import { join as join3 } from "path";
732
+ import { homedir as homedir3 } from "os";
733
+ var ENV_VAR2 = "OPENAI_BASE_URL";
734
+ var CODEX_CONFIG_DIR = join3(homedir3(), ".codex");
735
+ var CODEX_CONFIG_FILE = join3(CODEX_CONFIG_DIR, "config.toml");
736
+ var INFO2 = {
737
+ id: "codex",
738
+ label: "Codex CLI",
739
+ description: "OpenAI CLI agent for coding (Responses API)",
740
+ protocol: "openai-responses",
741
+ automatable: true
742
+ };
743
+ function isCodexInstalled() {
744
+ try {
745
+ execSync2("which codex", { stdio: "ignore" });
746
+ return true;
747
+ } catch {
748
+ return false;
600
749
  }
601
- return null;
602
750
  }
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;
751
+ function getCurrentBaseUrl2() {
752
+ return process.env[ENV_VAR2] || void 0;
613
753
  }
614
- function lineExistsInFile(filePath, line) {
615
- if (!existsSync2(filePath)) return false;
754
+ function hasCodexConfig() {
755
+ return existsSync3(CODEX_CONFIG_FILE);
756
+ }
757
+ function codexConfigMentionsLiminal() {
758
+ if (!hasCodexConfig()) return false;
616
759
  try {
617
- const content = readFileSync2(filePath, "utf-8");
618
- return content.includes(line);
760
+ const content = readFileSync3(CODEX_CONFIG_FILE, "utf-8");
761
+ return content.includes("127.0.0.1") || content.includes("liminal");
619
762
  } catch {
620
763
  return false;
621
764
  }
622
765
  }
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");
766
+ var codexConnector = {
767
+ info: INFO2,
768
+ async detect() {
769
+ const installed = isCodexInstalled();
770
+ const currentUrl = getCurrentBaseUrl2();
771
+ const envConfigured = currentUrl?.includes("127.0.0.1") ?? false;
772
+ const tomlConfigured = codexConfigMentionsLiminal();
773
+ const configured = envConfigured || tomlConfigured;
774
+ if (!installed) {
775
+ return { installed, configured: false, detail: "Codex CLI not found in PATH" };
776
+ }
777
+ if (configured) {
778
+ const via = envConfigured ? ENV_VAR2 : "config.toml";
779
+ return { installed, configured, detail: `Routing through Liminal (via ${via})` };
780
+ }
781
+ return { installed, configured, detail: "Installed but not routing through Liminal" };
782
+ },
783
+ getShellExports(port) {
784
+ return [`export ${ENV_VAR2}=http://127.0.0.1:${port}/v1`];
785
+ },
786
+ async setup(port) {
787
+ const exports = this.getShellExports(port);
788
+ const instructions = [
789
+ "Codex CLI will automatically route through Liminal.",
790
+ "Make sure to source your shell profile or restart your terminal."
791
+ ];
792
+ instructions.push(
793
+ "Codex uses the OpenAI Responses API (/v1/responses) by default."
794
+ );
795
+ return {
796
+ success: true,
797
+ shellExports: exports,
798
+ postSetupInstructions: instructions
799
+ };
800
+ },
801
+ async teardown() {
802
+ const steps = [
803
+ `Remove the line \`export ${ENV_VAR2}=...\` from your shell profile (~/.zshrc or ~/.bashrc).`,
804
+ "Restart your terminal or run: unset OPENAI_BASE_URL"
805
+ ];
806
+ if (codexConfigMentionsLiminal()) {
807
+ steps.push(
808
+ `Remove the Liminal provider block from ${CODEX_CONFIG_FILE}.`
809
+ );
810
+ }
811
+ return { success: true, manualSteps: steps };
812
+ }
813
+ };
814
+
815
+ // src/connectors/cursor.ts
816
+ import { existsSync as existsSync4 } from "fs";
817
+ import { join as join4 } from "path";
818
+ import { homedir as homedir4 } from "os";
819
+ var INFO3 = {
820
+ id: "cursor",
821
+ label: "Cursor",
822
+ description: "AI-first code editor (GUI config required)",
823
+ protocol: "openai-chat",
824
+ automatable: false
825
+ };
826
+ function getCursorPaths() {
827
+ const platform = process.platform;
828
+ const home = homedir4();
829
+ if (platform === "darwin") {
830
+ return {
831
+ app: "/Applications/Cursor.app",
832
+ data: join4(home, "Library", "Application Support", "Cursor")
833
+ };
834
+ }
835
+ if (platform === "win32") {
836
+ const appData = process.env.APPDATA || join4(home, "AppData", "Roaming");
837
+ const localAppData = process.env.LOCALAPPDATA || join4(home, "AppData", "Local");
838
+ return {
839
+ app: join4(localAppData, "Programs", "Cursor", "Cursor.exe"),
840
+ data: join4(appData, "Cursor")
841
+ };
842
+ }
843
+ return {
844
+ app: "/usr/bin/cursor",
845
+ data: join4(home, ".config", "Cursor")
846
+ };
847
+ }
848
+ function isCursorInstalled() {
849
+ const { app, data } = getCursorPaths();
850
+ return existsSync4(app) || existsSync4(data);
851
+ }
852
+ function getSettingsDbPath() {
853
+ const { data } = getCursorPaths();
854
+ return join4(data, "User", "globalStorage", "state.vscdb");
855
+ }
856
+ var cursorConnector = {
857
+ info: INFO3,
858
+ async detect() {
859
+ const installed = isCursorInstalled();
860
+ const dbExists = existsSync4(getSettingsDbPath());
861
+ if (!installed) {
862
+ return { installed, configured: false, detail: "Cursor not found on this system" };
863
+ }
864
+ return {
865
+ installed,
866
+ configured: false,
867
+ detail: dbExists ? "Installed \u2014 configuration requires Cursor Settings GUI" : "Installed but settings database not found"
868
+ };
869
+ },
870
+ getShellExports(_port) {
871
+ return [];
872
+ },
873
+ async setup(port) {
874
+ const baseUrl = `http://127.0.0.1:${port}/v1`;
875
+ return {
876
+ success: true,
877
+ shellExports: [],
878
+ // No env vars — GUI only
879
+ postSetupInstructions: [
880
+ "Cursor requires manual configuration:",
881
+ "",
882
+ " 1. Open Cursor Settings (not VS Code settings)",
883
+ " 2. Go to Models",
884
+ ' 3. Enable "Override OpenAI Base URL (when using key)"',
885
+ ` 4. Set the base URL to: ${baseUrl}`,
886
+ ' 5. Enter any string as the API key (e.g., "liminal")',
887
+ " 6. Restart Cursor",
888
+ "",
889
+ "Cursor uses OpenAI format for all models, including Claude.",
890
+ "Both Chat Completions and Agent mode (Responses API) are supported."
891
+ ]
892
+ };
893
+ },
894
+ async teardown() {
895
+ return {
896
+ success: true,
897
+ manualSteps: [
898
+ "In Cursor Settings > Models:",
899
+ ' 1. Disable "Override OpenAI Base URL (when using key)"',
900
+ " 2. Clear the base URL field",
901
+ " 3. Restart Cursor"
902
+ ]
903
+ };
904
+ }
905
+ };
906
+
907
+ // src/connectors/openai-compatible.ts
908
+ var ENV_VAR3 = "OPENAI_BASE_URL";
909
+ var INFO4 = {
910
+ id: "openai-compatible",
911
+ label: "Other / OpenAI-compatible",
912
+ description: "Any tool that reads OPENAI_BASE_URL",
913
+ protocol: "openai-chat",
914
+ automatable: true
915
+ };
916
+ function getCurrentBaseUrl3() {
917
+ return process.env[ENV_VAR3] || void 0;
918
+ }
919
+ var openaiCompatibleConnector = {
920
+ info: INFO4,
921
+ async detect() {
922
+ const currentUrl = getCurrentBaseUrl3();
923
+ const configured = currentUrl?.includes("127.0.0.1") ?? false;
924
+ return {
925
+ installed: true,
926
+ // Generic — always "available"
927
+ configured,
928
+ detail: configured ? `OPENAI_BASE_URL \u2192 ${currentUrl}` : "OPENAI_BASE_URL not set to Liminal"
929
+ };
930
+ },
931
+ getShellExports(port) {
932
+ return [`export ${ENV_VAR3}=http://127.0.0.1:${port}/v1`];
933
+ },
934
+ async setup(port) {
935
+ const exports = this.getShellExports(port);
936
+ return {
937
+ success: true,
938
+ shellExports: exports,
939
+ postSetupInstructions: [
940
+ "Any tool that reads OPENAI_BASE_URL will route through Liminal.",
941
+ "Make sure to source your shell profile or restart your terminal.",
942
+ "",
943
+ "If your tool uses a different env var (e.g., OPENAI_API_BASE),",
944
+ `set it to: http://127.0.0.1:${port}/v1`
945
+ ]
946
+ };
947
+ },
948
+ async teardown() {
949
+ return {
950
+ success: true,
951
+ manualSteps: [
952
+ `Remove the line \`export ${ENV_VAR3}=...\` from your shell profile (~/.zshrc or ~/.bashrc).`,
953
+ "Restart your terminal or run: unset OPENAI_BASE_URL"
954
+ ]
955
+ };
956
+ }
957
+ };
958
+
959
+ // src/connectors/index.ts
960
+ var CONNECTORS = [
961
+ claudeCodeConnector,
962
+ codexConnector,
963
+ cursorConnector,
964
+ openaiCompatibleConnector
965
+ ];
966
+ function getConnector(id) {
967
+ const connector = CONNECTORS.find((c) => c.info.id === id);
968
+ if (!connector) {
969
+ throw new Error(`Unknown connector: ${id}`);
970
+ }
971
+ return connector;
630
972
  }
973
+ function getConnectors(ids) {
974
+ return ids.map(getConnector);
975
+ }
976
+
977
+ // src/commands/init.ts
978
+ var BOLD = "\x1B[1m";
979
+ var DIM = "\x1B[2m";
980
+ var CYAN = "\x1B[36m";
981
+ var GREEN = "\x1B[32m";
982
+ var YELLOW = "\x1B[33m";
983
+ var RESET = "\x1B[0m";
631
984
  async function initCommand() {
632
985
  printBanner();
633
986
  console.log(" Welcome to Liminal -- Your Transparency & Context Partner");
@@ -636,29 +989,38 @@ async function initCommand() {
636
989
  console.log();
637
990
  const apiKey = await runAuthFlow();
638
991
  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();
992
+ const port = DEFAULTS.port;
993
+ console.log(` ${BOLD}Detecting installed tools...${RESET}`);
994
+ console.log();
995
+ const detectionResults = await Promise.all(
996
+ CONNECTORS.map(async (c) => {
997
+ const status = await c.detect();
998
+ return { connector: c, status };
999
+ })
1000
+ );
1001
+ for (const { connector, status } of detectionResults) {
1002
+ const icon = status.installed ? `${GREEN}\u2713${RESET}` : `${DIM}\xB7${RESET}`;
1003
+ console.log(` ${icon} ${connector.info.label} ${DIM}${status.detail}${RESET}`);
650
1004
  }
651
1005
  console.log();
1006
+ const toolOptions = CONNECTORS.map((c) => {
1007
+ const detected = detectionResults.find((r) => r.connector.info.id === c.info.id);
1008
+ const installed = detected?.status.installed ?? false;
1009
+ let description = c.info.description;
1010
+ if (!installed) description += ` ${DIM}(not detected)${RESET}`;
1011
+ if (!c.info.automatable) description += ` ${DIM}(manual setup)${RESET}`;
1012
+ return {
1013
+ label: c.info.label,
1014
+ value: c.info.id,
1015
+ description,
1016
+ default: c.info.id === "claude-code" && installed
1017
+ };
1018
+ });
652
1019
  const toolsResult = await multiSelectPrompt({
653
1020
  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
- ]
1021
+ options: toolOptions
660
1022
  });
661
- const tools = toolsResult ?? ["claude-code"];
1023
+ const selectedIds = toolsResult ?? ["claude-code"];
662
1024
  console.log();
663
1025
  const learnResult = await selectPrompt({
664
1026
  message: "Learn from LLM responses?",
@@ -670,78 +1032,100 @@ async function initCommand() {
670
1032
  });
671
1033
  const learnFromResponses = learnResult ?? true;
672
1034
  console.log();
673
- const apiBaseUrl = DEFAULTS.apiBaseUrl;
674
1035
  ensureDirectories();
675
1036
  saveConfig({
676
1037
  apiKey,
677
- apiBaseUrl,
1038
+ apiBaseUrl: DEFAULTS.apiBaseUrl,
678
1039
  upstreamBaseUrl: DEFAULTS.upstreamBaseUrl,
679
1040
  anthropicUpstreamUrl: DEFAULTS.anthropicUpstreamUrl,
680
1041
  port,
681
1042
  learnFromResponses,
682
- tools,
1043
+ tools: selectedIds,
683
1044
  compressionThreshold: DEFAULTS.compressionThreshold,
684
1045
  compressRoles: DEFAULTS.compressRoles,
685
1046
  latencyBudgetMs: DEFAULTS.latencyBudgetMs,
686
1047
  enabled: DEFAULTS.enabled
687
1048
  });
1049
+ console.log(` ${GREEN}\u2713${RESET} Configuration saved to ${DIM}${CONFIG_FILE}${RESET}`);
688
1050
  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}`);
1051
+ const connectors = getConnectors(selectedIds);
1052
+ const allShellExports = [];
1053
+ const profile = detectShellProfile();
1054
+ console.log(` ${BOLD}Configuring ${connectors.length} tool${connectors.length > 1 ? "s" : ""}...${RESET}`);
1055
+ for (const connector of connectors) {
1056
+ const result = await connector.setup(port);
1057
+ const protocol = connector.info.protocol === "anthropic-messages" ? "Anthropic Messages API" : connector.info.protocol === "openai-responses" ? "Responses API" : "Chat Completions API";
1058
+ console.log();
1059
+ console.log(` ${CYAN}\u2500\u2500 ${connector.info.label} ${RESET}${DIM}(${protocol})${RESET}`);
1060
+ if (connector.info.automatable && result.shellExports.length > 0) {
1061
+ for (const line of result.shellExports) {
1062
+ console.log(` ${GREEN}\u2713${RESET} ${line}`);
1063
+ }
1064
+ allShellExports.push(...result.shellExports);
1065
+ }
1066
+ for (const line of result.postSetupInstructions) {
1067
+ if (line === "") {
1068
+ console.log();
699
1069
  } 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.`);
1070
+ if (line.includes("source your shell profile") || line.includes("restart your terminal")) continue;
1071
+ if (line.includes("will automatically route through Liminal")) {
1072
+ console.log(` ${DIM}${line}${RESET}`);
1073
+ } else if (line.startsWith(" ")) {
1074
+ console.log(` ${line}`);
720
1075
  } 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
- }
1076
+ console.log(` ${line}`);
727
1077
  }
728
1078
  }
1079
+ }
1080
+ if (!connector.info.automatable) {
1081
+ console.log(` ${YELLOW}\u26A0 Requires manual configuration (see steps above)${RESET}`);
1082
+ }
1083
+ }
1084
+ const uniqueExports = [...new Set(allShellExports)];
1085
+ if (uniqueExports.length > 0 && profile) {
1086
+ const allExist = uniqueExports.every((line) => lineExistsInFile(profile.path, line));
1087
+ console.log();
1088
+ if (allExist) {
1089
+ console.log(` ${GREEN}\u2713${RESET} Shell already configured in ${profile.name}`);
729
1090
  } else {
730
- console.log(" Add these to your shell profile:");
731
- console.log();
732
- for (const line of exportLines) {
733
- console.log(` ${line}`);
1091
+ const autoResult = await selectPrompt({
1092
+ message: `Add proxy exports to ${profile.name}?`,
1093
+ options: [
1094
+ { label: "Yes", value: true, description: "Automatic shell configuration" },
1095
+ { label: "No", value: false, description: "I'll set it up manually" }
1096
+ ],
1097
+ defaultIndex: 0
1098
+ });
1099
+ if (autoResult === true) {
1100
+ const added = appendToShellProfile(profile, uniqueExports);
1101
+ if (added.length > 0) {
1102
+ console.log();
1103
+ console.log(` ${GREEN}\u2713${RESET} Added to ${profile.name}`);
1104
+ console.log();
1105
+ console.log(` Run ${BOLD}source ${profile.name}${RESET} or restart your terminal.`);
1106
+ }
1107
+ } else {
1108
+ console.log();
1109
+ console.log(" Add these to your shell profile:");
1110
+ console.log();
1111
+ for (const line of uniqueExports) {
1112
+ console.log(` ${CYAN}${line}${RESET}`);
1113
+ }
734
1114
  }
735
1115
  }
736
- }
737
- if (hasCursor) {
1116
+ } else if (uniqueExports.length > 0) {
1117
+ console.log();
1118
+ console.log(" Add these to your shell profile:");
738
1119
  console.log();
739
- console.log(" Cursor setup (manual):");
740
- console.log(` Settings > Models > OpenAI API Base URL: http://127.0.0.1:${port}/v1`);
1120
+ for (const line of uniqueExports) {
1121
+ console.log(` ${CYAN}${line}${RESET}`);
1122
+ }
741
1123
  }
742
1124
  console.log();
1125
+ console.log(` ${BOLD}Setup complete!${RESET}`);
1126
+ console.log();
743
1127
  console.log(" Next step:");
744
- console.log(" liminal start");
1128
+ console.log(` ${BOLD}liminal start${RESET}`);
745
1129
  console.log();
746
1130
  }
747
1131
 
@@ -819,6 +1203,106 @@ import { RSCCircuitOpenError as RSCCircuitOpenError2 } from "@cognisos/rsc-sdk";
819
1203
 
820
1204
  // src/rsc/message-compressor.ts
821
1205
  import { RSCCircuitOpenError } from "@cognisos/rsc-sdk";
1206
+
1207
+ // src/rsc/content-segmenter.ts
1208
+ function segmentContent(text) {
1209
+ if (text.length === 0) return [{ type: "prose", text: "" }];
1210
+ const segments = [];
1211
+ const lines = text.split("\n");
1212
+ let i = 0;
1213
+ let proseBuf = [];
1214
+ function flushProse() {
1215
+ if (proseBuf.length > 0) {
1216
+ segments.push({ type: "prose", text: proseBuf.join("\n") });
1217
+ proseBuf = [];
1218
+ }
1219
+ }
1220
+ while (i < lines.length) {
1221
+ const line = lines[i];
1222
+ const fenceMatch = matchFenceOpen(line);
1223
+ if (fenceMatch) {
1224
+ if (proseBuf.length > 0) {
1225
+ flushProse();
1226
+ segments[segments.length - 1].text += "\n";
1227
+ }
1228
+ const codeBuf = [line];
1229
+ i++;
1230
+ let closed = false;
1231
+ while (i < lines.length) {
1232
+ codeBuf.push(lines[i]);
1233
+ if (matchFenceClose(lines[i], fenceMatch.char, fenceMatch.length)) {
1234
+ closed = true;
1235
+ i++;
1236
+ break;
1237
+ }
1238
+ i++;
1239
+ }
1240
+ let codeText = codeBuf.join("\n");
1241
+ if (i < lines.length) {
1242
+ codeText += "\n";
1243
+ }
1244
+ segments.push({ type: "code", text: codeText });
1245
+ continue;
1246
+ }
1247
+ if (isIndentedCodeLine(line)) {
1248
+ if (proseBuf.length > 0) {
1249
+ flushProse();
1250
+ segments[segments.length - 1].text += "\n";
1251
+ }
1252
+ const codeBuf = [line];
1253
+ i++;
1254
+ while (i < lines.length) {
1255
+ if (isIndentedCodeLine(lines[i])) {
1256
+ codeBuf.push(lines[i]);
1257
+ i++;
1258
+ } else if (lines[i].trim() === "" && i + 1 < lines.length && isIndentedCodeLine(lines[i + 1])) {
1259
+ codeBuf.push(lines[i]);
1260
+ i++;
1261
+ } else {
1262
+ break;
1263
+ }
1264
+ }
1265
+ let codeText = codeBuf.join("\n");
1266
+ if (i < lines.length) {
1267
+ codeText += "\n";
1268
+ }
1269
+ segments.push({ type: "code", text: codeText });
1270
+ continue;
1271
+ }
1272
+ if (proseBuf.length > 0) {
1273
+ proseBuf.push(line);
1274
+ } else {
1275
+ proseBuf.push(line);
1276
+ }
1277
+ i++;
1278
+ }
1279
+ if (proseBuf.length > 0) {
1280
+ flushProse();
1281
+ }
1282
+ if (segments.length === 0) {
1283
+ return [{ type: "prose", text }];
1284
+ }
1285
+ return segments;
1286
+ }
1287
+ function matchFenceOpen(line) {
1288
+ const match = line.match(/^( {0,3})((`{3,})|~{3,})(.*)$/);
1289
+ if (!match) return null;
1290
+ const fenceStr = match[2];
1291
+ const char = fenceStr[0];
1292
+ if (char === "`" && match[4] && match[4].includes("`")) return null;
1293
+ return { char, length: fenceStr.length };
1294
+ }
1295
+ function matchFenceClose(line, char, minLength) {
1296
+ const match = line.match(/^( {0,3})((`{3,})|(~{3,}))\s*$/);
1297
+ if (!match) return false;
1298
+ const fenceStr = match[2];
1299
+ return fenceStr[0] === char && fenceStr.length >= minLength;
1300
+ }
1301
+ function isIndentedCodeLine(line) {
1302
+ return (line.startsWith(" ") || line.startsWith(" ")) && line.trim().length > 0;
1303
+ }
1304
+
1305
+ // src/rsc/message-compressor.ts
822
1306
  async function compressMessages(messages, pipeline, session, compressRoles) {
823
1307
  let anyCompressed = false;
824
1308
  let totalTokensSaved = 0;
@@ -843,11 +1327,42 @@ async function compressMessages(messages, pipeline, session, compressRoles) {
843
1327
  return { messages: compressed, anyCompressed, totalTokensSaved };
844
1328
  }
845
1329
  async function compressStringContent(msg, pipeline, session, record) {
1330
+ const text = msg.content;
1331
+ const segments = segmentContent(text);
1332
+ const hasCode = segments.some((s) => s.type === "code");
1333
+ if (!hasCode) {
1334
+ try {
1335
+ const result = await pipeline.compressForLLM(text);
1336
+ session.recordCompression(result.metrics);
1337
+ record(!result.metrics.skipped, result.metrics.tokensSaved);
1338
+ return { ...msg, content: result.text };
1339
+ } catch (err) {
1340
+ if (err instanceof RSCCircuitOpenError) {
1341
+ session.recordFailure();
1342
+ throw err;
1343
+ }
1344
+ session.recordFailure();
1345
+ return msg;
1346
+ }
1347
+ }
846
1348
  try {
847
- const result = await pipeline.compressForLLM(msg.content);
848
- session.recordCompression(result.metrics);
849
- record(!result.metrics.skipped, result.metrics.tokensSaved);
850
- return { ...msg, content: result.text };
1349
+ const parts = await Promise.all(
1350
+ segments.map(async (seg) => {
1351
+ if (seg.type === "code") return seg.text;
1352
+ if (seg.text.trim().length === 0) return seg.text;
1353
+ try {
1354
+ const result = await pipeline.compressForLLM(seg.text);
1355
+ session.recordCompression(result.metrics);
1356
+ record(!result.metrics.skipped, result.metrics.tokensSaved);
1357
+ return result.text;
1358
+ } catch (err) {
1359
+ if (err instanceof RSCCircuitOpenError) throw err;
1360
+ session.recordFailure();
1361
+ return seg.text;
1362
+ }
1363
+ })
1364
+ );
1365
+ return { ...msg, content: parts.join("") };
851
1366
  } catch (err) {
852
1367
  if (err instanceof RSCCircuitOpenError) {
853
1368
  session.recordFailure();
@@ -857,16 +1372,41 @@ async function compressStringContent(msg, pipeline, session, record) {
857
1372
  return msg;
858
1373
  }
859
1374
  }
1375
+ async function compressTextWithSegmentation(text, pipeline, session, record) {
1376
+ const segments = segmentContent(text);
1377
+ const hasCode = segments.some((s) => s.type === "code");
1378
+ if (!hasCode) {
1379
+ const result = await pipeline.compressForLLM(text);
1380
+ session.recordCompression(result.metrics);
1381
+ record(!result.metrics.skipped, result.metrics.tokensSaved);
1382
+ return result.text;
1383
+ }
1384
+ const parts = await Promise.all(
1385
+ segments.map(async (seg) => {
1386
+ if (seg.type === "code") return seg.text;
1387
+ if (seg.text.trim().length === 0) return seg.text;
1388
+ try {
1389
+ const result = await pipeline.compressForLLM(seg.text);
1390
+ session.recordCompression(result.metrics);
1391
+ record(!result.metrics.skipped, result.metrics.tokensSaved);
1392
+ return result.text;
1393
+ } catch (err) {
1394
+ if (err instanceof RSCCircuitOpenError) throw err;
1395
+ session.recordFailure();
1396
+ return seg.text;
1397
+ }
1398
+ })
1399
+ );
1400
+ return parts.join("");
1401
+ }
860
1402
  async function compressArrayContent(msg, pipeline, session, record) {
861
1403
  const parts = msg.content;
862
1404
  const compressedParts = await Promise.all(
863
1405
  parts.map(async (part) => {
864
1406
  if (part.type === "text" && typeof part.text === "string") {
865
1407
  try {
866
- const result = await pipeline.compressForLLM(part.text);
867
- session.recordCompression(result.metrics);
868
- record(!result.metrics.skipped, result.metrics.tokensSaved);
869
- return { ...part, text: result.text };
1408
+ const compressed = await compressTextWithSegmentation(part.text, pipeline, session, record);
1409
+ return { ...part, text: compressed };
870
1410
  } catch (err) {
871
1411
  if (err instanceof RSCCircuitOpenError) {
872
1412
  session.recordFailure();
@@ -905,7 +1445,7 @@ function createStreamLearningBuffer(pipeline) {
905
1445
  }
906
1446
 
907
1447
  // src/proxy/streaming.ts
908
- async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete) {
1448
+ async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
909
1449
  clientRes.writeHead(200, {
910
1450
  "Content-Type": "text/event-stream",
911
1451
  "Cache-Control": "no-cache",
@@ -915,27 +1455,67 @@ async function pipeSSEResponse(upstreamResponse, clientRes, onContentDelta, onCo
915
1455
  const reader = upstreamResponse.body.getReader();
916
1456
  const decoder = new TextDecoder();
917
1457
  let lineBuf = "";
1458
+ const needsAdjustment = totalTokensSaved > 0;
918
1459
  try {
919
1460
  while (true) {
920
1461
  const { done, value } = await reader.read();
921
1462
  if (done) break;
922
1463
  const chunk = decoder.decode(value, { stream: true });
923
- clientRes.write(chunk);
1464
+ if (!needsAdjustment) {
1465
+ clientRes.write(chunk);
1466
+ lineBuf += chunk;
1467
+ const lines2 = lineBuf.split("\n");
1468
+ lineBuf = lines2.pop() || "";
1469
+ for (const line of lines2) {
1470
+ if (line.startsWith("data: ") && line !== "data: [DONE]") {
1471
+ try {
1472
+ const json = JSON.parse(line.slice(6));
1473
+ const content = json?.choices?.[0]?.delta?.content;
1474
+ if (typeof content === "string") {
1475
+ onContentDelta(content);
1476
+ }
1477
+ } catch {
1478
+ }
1479
+ }
1480
+ }
1481
+ continue;
1482
+ }
924
1483
  lineBuf += chunk;
925
1484
  const lines = lineBuf.split("\n");
926
1485
  lineBuf = lines.pop() || "";
1486
+ let adjusted = false;
1487
+ const outputLines = [];
927
1488
  for (const line of lines) {
928
1489
  if (line.startsWith("data: ") && line !== "data: [DONE]") {
929
1490
  try {
930
1491
  const json = JSON.parse(line.slice(6));
1492
+ if (json?.usage?.prompt_tokens != null) {
1493
+ json.usage.prompt_tokens += totalTokensSaved;
1494
+ if (json.usage.total_tokens != null) {
1495
+ json.usage.total_tokens += totalTokensSaved;
1496
+ }
1497
+ outputLines.push(`data: ${JSON.stringify(json)}`);
1498
+ adjusted = true;
1499
+ } else {
1500
+ outputLines.push(line);
1501
+ }
931
1502
  const content = json?.choices?.[0]?.delta?.content;
932
1503
  if (typeof content === "string") {
933
1504
  onContentDelta(content);
934
1505
  }
935
1506
  } catch {
1507
+ outputLines.push(line);
936
1508
  }
1509
+ } else {
1510
+ outputLines.push(line);
937
1511
  }
938
1512
  }
1513
+ if (adjusted) {
1514
+ const reconstructed = outputLines.join("\n") + "\n";
1515
+ clientRes.write(reconstructed);
1516
+ } else {
1517
+ clientRes.write(chunk);
1518
+ }
939
1519
  }
940
1520
  } finally {
941
1521
  clientRes.end();
@@ -976,6 +1556,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
976
1556
  }
977
1557
  let messages = request.messages;
978
1558
  let anyCompressed = false;
1559
+ let totalTokensSaved = 0;
979
1560
  if (config.enabled && !pipeline.isCircuitOpen()) {
980
1561
  try {
981
1562
  const compressRoles = new Set(config.compressRoles);
@@ -987,6 +1568,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
987
1568
  );
988
1569
  messages = result.messages;
989
1570
  anyCompressed = result.anyCompressed;
1571
+ totalTokensSaved = result.totalTokensSaved;
990
1572
  if (result.totalTokensSaved > 0) {
991
1573
  logger.log(`[COMPRESS] Saved ${result.totalTokensSaved} tokens`);
992
1574
  }
@@ -1029,14 +1611,30 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
1029
1611
  upstreamResponse,
1030
1612
  res,
1031
1613
  (text) => learningBuffer?.append(text),
1032
- () => learningBuffer?.flush()
1614
+ () => learningBuffer?.flush(),
1615
+ totalTokensSaved
1033
1616
  );
1034
1617
  return;
1035
1618
  }
1036
1619
  const responseBody = await upstreamResponse.text();
1620
+ let finalBody = responseBody;
1621
+ if (totalTokensSaved > 0) {
1622
+ try {
1623
+ const parsed = JSON.parse(responseBody);
1624
+ if (parsed?.usage?.prompt_tokens != null) {
1625
+ parsed.usage.prompt_tokens += totalTokensSaved;
1626
+ if (parsed.usage.total_tokens != null) {
1627
+ parsed.usage.total_tokens += totalTokensSaved;
1628
+ }
1629
+ finalBody = JSON.stringify(parsed);
1630
+ logger.log(`[TOKENS] Adjusted prompt_tokens by +${totalTokensSaved}`);
1631
+ }
1632
+ } catch {
1633
+ }
1634
+ }
1037
1635
  setCORSHeaders(res);
1038
1636
  res.writeHead(200, { "Content-Type": "application/json" });
1039
- res.end(responseBody);
1637
+ res.end(finalBody);
1040
1638
  if (anyCompressed) {
1041
1639
  try {
1042
1640
  const parsed = JSON.parse(responseBody);
@@ -1062,10 +1660,21 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger) {
1062
1660
  import { RSCCircuitOpenError as RSCCircuitOpenError3 } from "@cognisos/rsc-sdk";
1063
1661
 
1064
1662
  // src/proxy/anthropic-streaming.ts
1065
- async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete) {
1066
- clientRes.writeHead(200, {
1067
- "Content-Type": "text/event-stream",
1068
- "Cache-Control": "no-cache",
1663
+ function adjustMessageStartLine(dataLine, tokensSaved) {
1664
+ try {
1665
+ const json = JSON.parse(dataLine.slice(6));
1666
+ if (json?.message?.usage?.input_tokens != null) {
1667
+ json.message.usage.input_tokens += tokensSaved;
1668
+ return `data: ${JSON.stringify(json)}`;
1669
+ }
1670
+ } catch {
1671
+ }
1672
+ return null;
1673
+ }
1674
+ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
1675
+ clientRes.writeHead(200, {
1676
+ "Content-Type": "text/event-stream",
1677
+ "Cache-Control": "no-cache",
1069
1678
  "Connection": "keep-alive",
1070
1679
  "Access-Control-Allow-Origin": "*"
1071
1680
  });
@@ -1073,28 +1682,70 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
1073
1682
  const decoder = new TextDecoder();
1074
1683
  let lineBuf = "";
1075
1684
  let currentEvent = "";
1685
+ let usageAdjusted = false;
1686
+ const needsAdjustment = totalTokensSaved > 0;
1076
1687
  try {
1077
1688
  while (true) {
1078
1689
  const { done, value } = await reader.read();
1079
1690
  if (done) break;
1080
1691
  const chunk = decoder.decode(value, { stream: true });
1081
- clientRes.write(chunk);
1692
+ if (!needsAdjustment || usageAdjusted) {
1693
+ clientRes.write(chunk);
1694
+ lineBuf += chunk;
1695
+ const lines2 = lineBuf.split("\n");
1696
+ lineBuf = lines2.pop() || "";
1697
+ for (const line of lines2) {
1698
+ if (line.startsWith("event: ")) {
1699
+ currentEvent = line.slice(7).trim();
1700
+ } else if (line.startsWith("data: ") && currentEvent === "content_block_delta") {
1701
+ try {
1702
+ const json = JSON.parse(line.slice(6));
1703
+ if (json?.delta?.type === "text_delta" && typeof json.delta.text === "string") {
1704
+ onContentDelta(json.delta.text);
1705
+ }
1706
+ } catch {
1707
+ }
1708
+ }
1709
+ }
1710
+ continue;
1711
+ }
1082
1712
  lineBuf += chunk;
1083
1713
  const lines = lineBuf.split("\n");
1084
1714
  lineBuf = lines.pop() || "";
1715
+ let adjusted = false;
1716
+ const outputLines = [];
1085
1717
  for (const line of lines) {
1086
1718
  if (line.startsWith("event: ")) {
1087
1719
  currentEvent = line.slice(7).trim();
1088
- } else if (line.startsWith("data: ") && currentEvent === "content_block_delta") {
1089
- try {
1090
- const json = JSON.parse(line.slice(6));
1091
- if (json?.delta?.type === "text_delta" && typeof json.delta.text === "string") {
1092
- onContentDelta(json.delta.text);
1720
+ outputLines.push(line);
1721
+ } else if (line.startsWith("data: ") && currentEvent === "message_start" && !usageAdjusted) {
1722
+ const adjustedLine = adjustMessageStartLine(line, totalTokensSaved);
1723
+ if (adjustedLine) {
1724
+ outputLines.push(adjustedLine);
1725
+ usageAdjusted = true;
1726
+ adjusted = true;
1727
+ } else {
1728
+ outputLines.push(line);
1729
+ }
1730
+ } else {
1731
+ outputLines.push(line);
1732
+ if (line.startsWith("data: ") && currentEvent === "content_block_delta") {
1733
+ try {
1734
+ const json = JSON.parse(line.slice(6));
1735
+ if (json?.delta?.type === "text_delta" && typeof json.delta.text === "string") {
1736
+ onContentDelta(json.delta.text);
1737
+ }
1738
+ } catch {
1093
1739
  }
1094
- } catch {
1095
1740
  }
1096
1741
  }
1097
1742
  }
1743
+ if (adjusted) {
1744
+ const reconstructed = outputLines.join("\n") + "\n" + (lineBuf ? "" : "");
1745
+ clientRes.write(reconstructed);
1746
+ } else {
1747
+ clientRes.write(chunk);
1748
+ }
1098
1749
  }
1099
1750
  } finally {
1100
1751
  clientRes.end();
@@ -1154,6 +1805,7 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
1154
1805
  }
1155
1806
  let messages = request.messages;
1156
1807
  let anyCompressed = false;
1808
+ let totalTokensSaved = 0;
1157
1809
  if (config.enabled && !pipeline.isCircuitOpen()) {
1158
1810
  try {
1159
1811
  const compressRoles = new Set(config.compressRoles);
@@ -1166,6 +1818,7 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
1166
1818
  );
1167
1819
  messages = convertCompressedToAnthropic(result.messages);
1168
1820
  anyCompressed = result.anyCompressed;
1821
+ totalTokensSaved = result.totalTokensSaved;
1169
1822
  if (result.totalTokensSaved > 0) {
1170
1823
  logger.log(`[COMPRESS] Saved ${result.totalTokensSaved} tokens`);
1171
1824
  }
@@ -1213,14 +1866,27 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
1213
1866
  upstreamResponse,
1214
1867
  res,
1215
1868
  (text) => learningBuffer?.append(text),
1216
- () => learningBuffer?.flush()
1869
+ () => learningBuffer?.flush(),
1870
+ totalTokensSaved
1217
1871
  );
1218
1872
  return;
1219
1873
  }
1220
1874
  const responseBody = await upstreamResponse.text();
1875
+ let finalBody = responseBody;
1876
+ if (totalTokensSaved > 0) {
1877
+ try {
1878
+ const parsed = JSON.parse(responseBody);
1879
+ if (parsed?.usage?.input_tokens != null) {
1880
+ parsed.usage.input_tokens += totalTokensSaved;
1881
+ finalBody = JSON.stringify(parsed);
1882
+ logger.log(`[TOKENS] Adjusted input_tokens by +${totalTokensSaved}`);
1883
+ }
1884
+ } catch {
1885
+ }
1886
+ }
1221
1887
  setCORSHeaders2(res);
1222
1888
  res.writeHead(200, { "Content-Type": "application/json" });
1223
- res.end(responseBody);
1889
+ res.end(finalBody);
1224
1890
  if (anyCompressed) {
1225
1891
  try {
1226
1892
  const parsed = JSON.parse(responseBody);
@@ -1243,58 +1909,426 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger)
1243
1909
  }
1244
1910
  }
1245
1911
 
1246
- // src/proxy/handler.ts
1912
+ // src/proxy/responses.ts
1913
+ import { RSCCircuitOpenError as RSCCircuitOpenError4 } from "@cognisos/rsc-sdk";
1914
+
1915
+ // src/proxy/responses-streaming.ts
1916
+ async function pipeResponsesSSE(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
1917
+ clientRes.writeHead(200, {
1918
+ "Content-Type": "text/event-stream",
1919
+ "Cache-Control": "no-cache",
1920
+ "Connection": "keep-alive",
1921
+ "Access-Control-Allow-Origin": "*"
1922
+ });
1923
+ const reader = upstreamResponse.body.getReader();
1924
+ const decoder = new TextDecoder();
1925
+ let lineBuf = "";
1926
+ let currentEvent = "";
1927
+ let usageAdjusted = false;
1928
+ const needsAdjustment = totalTokensSaved > 0;
1929
+ try {
1930
+ while (true) {
1931
+ const { done, value } = await reader.read();
1932
+ if (done) break;
1933
+ const chunk = decoder.decode(value, { stream: true });
1934
+ if (!needsAdjustment || usageAdjusted) {
1935
+ clientRes.write(chunk);
1936
+ lineBuf += chunk;
1937
+ const lines2 = lineBuf.split("\n");
1938
+ lineBuf = lines2.pop() || "";
1939
+ for (const line of lines2) {
1940
+ if (line.startsWith("event: ")) {
1941
+ currentEvent = line.slice(7).trim();
1942
+ } else if (line.startsWith("data: ") && currentEvent === "response.output_text.delta") {
1943
+ try {
1944
+ const json = JSON.parse(line.slice(6));
1945
+ if (typeof json?.delta === "string") {
1946
+ onContentDelta(json.delta);
1947
+ }
1948
+ } catch {
1949
+ }
1950
+ }
1951
+ }
1952
+ continue;
1953
+ }
1954
+ lineBuf += chunk;
1955
+ const lines = lineBuf.split("\n");
1956
+ lineBuf = lines.pop() || "";
1957
+ let adjusted = false;
1958
+ const outputLines = [];
1959
+ for (const line of lines) {
1960
+ if (line.startsWith("event: ")) {
1961
+ currentEvent = line.slice(7).trim();
1962
+ outputLines.push(line);
1963
+ } else if (line.startsWith("data: ") && currentEvent === "response.completed" && !usageAdjusted) {
1964
+ try {
1965
+ const json = JSON.parse(line.slice(6));
1966
+ if (json?.response?.usage?.input_tokens != null) {
1967
+ json.response.usage.input_tokens += totalTokensSaved;
1968
+ if (json.response.usage.total_tokens != null) {
1969
+ json.response.usage.total_tokens += totalTokensSaved;
1970
+ }
1971
+ outputLines.push(`data: ${JSON.stringify(json)}`);
1972
+ usageAdjusted = true;
1973
+ adjusted = true;
1974
+ } else {
1975
+ outputLines.push(line);
1976
+ }
1977
+ } catch {
1978
+ outputLines.push(line);
1979
+ }
1980
+ } else {
1981
+ outputLines.push(line);
1982
+ if (line.startsWith("data: ") && currentEvent === "response.output_text.delta") {
1983
+ try {
1984
+ const json = JSON.parse(line.slice(6));
1985
+ if (typeof json?.delta === "string") {
1986
+ onContentDelta(json.delta);
1987
+ }
1988
+ } catch {
1989
+ }
1990
+ }
1991
+ }
1992
+ }
1993
+ if (adjusted) {
1994
+ const reconstructed = outputLines.join("\n") + "\n";
1995
+ clientRes.write(reconstructed);
1996
+ } else {
1997
+ clientRes.write(chunk);
1998
+ }
1999
+ }
2000
+ } finally {
2001
+ clientRes.end();
2002
+ onComplete();
2003
+ }
2004
+ }
2005
+
2006
+ // src/proxy/responses.ts
1247
2007
  function setCORSHeaders3(res) {
1248
2008
  res.setHeader("Access-Control-Allow-Origin", "*");
1249
2009
  res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
1250
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta");
1251
- res.setHeader("Access-Control-Max-Age", "86400");
2010
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
1252
2011
  }
1253
2012
  function sendJSON2(res, status, body) {
1254
2013
  setCORSHeaders3(res);
1255
2014
  res.writeHead(status, { "Content-Type": "application/json" });
1256
2015
  res.end(JSON.stringify(body));
1257
2016
  }
2017
+ function extractBearerToken2(req) {
2018
+ const auth = req.headers.authorization;
2019
+ if (!auth || !auth.startsWith("Bearer ")) return null;
2020
+ return auth.slice(7);
2021
+ }
2022
+ function isMessageItem(item) {
2023
+ return item.type === "message";
2024
+ }
2025
+ function inputToCompressibleMessages(input) {
2026
+ if (typeof input === "string") {
2027
+ return [{ role: "user", content: input }];
2028
+ }
2029
+ const messages = [];
2030
+ for (const item of input) {
2031
+ if (!isMessageItem(item)) continue;
2032
+ if (typeof item.content === "string") {
2033
+ const role = item.role === "developer" ? "system" : item.role;
2034
+ messages.push({ role, content: item.content });
2035
+ } else if (Array.isArray(item.content)) {
2036
+ const role = item.role === "developer" ? "system" : item.role;
2037
+ const parts = item.content.map((c) => {
2038
+ if (c.type === "input_text") {
2039
+ return { type: "text", text: c.text };
2040
+ }
2041
+ return c;
2042
+ });
2043
+ messages.push({ role, content: parts });
2044
+ }
2045
+ }
2046
+ return messages;
2047
+ }
2048
+ function applyCompressedToInput(originalInput, compressedMessages) {
2049
+ if (typeof originalInput === "string") {
2050
+ const first = compressedMessages[0];
2051
+ if (first && typeof first.content === "string") {
2052
+ return first.content;
2053
+ }
2054
+ return originalInput;
2055
+ }
2056
+ let msgIdx = 0;
2057
+ const result = [];
2058
+ for (const item of originalInput) {
2059
+ if (!isMessageItem(item)) {
2060
+ result.push(item);
2061
+ continue;
2062
+ }
2063
+ const compressed = compressedMessages[msgIdx];
2064
+ msgIdx++;
2065
+ if (!compressed) {
2066
+ result.push(item);
2067
+ continue;
2068
+ }
2069
+ if (typeof compressed.content === "string") {
2070
+ result.push({
2071
+ ...item,
2072
+ content: compressed.content
2073
+ });
2074
+ } else if (Array.isArray(compressed.content)) {
2075
+ const content = compressed.content.map((part) => {
2076
+ if (part.type === "text" && "text" in part) {
2077
+ return { type: "input_text", text: part.text };
2078
+ }
2079
+ return part;
2080
+ });
2081
+ result.push({
2082
+ ...item,
2083
+ content
2084
+ });
2085
+ } else {
2086
+ result.push(item);
2087
+ }
2088
+ }
2089
+ return result;
2090
+ }
2091
+ function extractOutputText(output) {
2092
+ const texts = [];
2093
+ for (const item of output) {
2094
+ if (item.type === "message") {
2095
+ const msg = item;
2096
+ for (const block of msg.content) {
2097
+ if (block.type === "output_text" && typeof block.text === "string") {
2098
+ texts.push(block.text);
2099
+ }
2100
+ }
2101
+ }
2102
+ }
2103
+ return texts.join("");
2104
+ }
2105
+ async function handleResponses(req, res, body, pipeline, config, logger) {
2106
+ const request = body;
2107
+ if (request.input === void 0 || request.input === null) {
2108
+ sendJSON2(res, 400, {
2109
+ error: { message: "input is required", type: "invalid_request_error" }
2110
+ });
2111
+ return;
2112
+ }
2113
+ const llmApiKey = extractBearerToken2(req);
2114
+ if (!llmApiKey) {
2115
+ sendJSON2(res, 401, {
2116
+ error: { message: "Authorization header with Bearer token is required", type: "authentication_error" }
2117
+ });
2118
+ return;
2119
+ }
2120
+ let compressedInput = request.input;
2121
+ let anyCompressed = false;
2122
+ let totalTokensSaved = 0;
2123
+ if (config.enabled && !pipeline.isCircuitOpen()) {
2124
+ try {
2125
+ const compressRoles = new Set(config.compressRoles);
2126
+ const compressible = inputToCompressibleMessages(request.input);
2127
+ if (compressible.length > 0) {
2128
+ const result = await compressMessages(
2129
+ compressible,
2130
+ pipeline.pipeline,
2131
+ pipeline.session,
2132
+ compressRoles
2133
+ );
2134
+ compressedInput = applyCompressedToInput(request.input, result.messages);
2135
+ anyCompressed = result.anyCompressed;
2136
+ totalTokensSaved = result.totalTokensSaved;
2137
+ if (result.totalTokensSaved > 0) {
2138
+ logger.log(`[COMPRESS] Responses API: saved ${result.totalTokensSaved} tokens`);
2139
+ }
2140
+ }
2141
+ } catch (err) {
2142
+ if (err instanceof RSCCircuitOpenError4) {
2143
+ logger.log("[DEGRADE] Circuit breaker open \u2014 passing through directly");
2144
+ } else {
2145
+ logger.log(`[ERROR] Compression failed: ${err instanceof Error ? err.message : String(err)}`);
2146
+ }
2147
+ compressedInput = request.input;
2148
+ }
2149
+ }
2150
+ const upstreamUrl = `${config.upstreamBaseUrl}/v1/responses`;
2151
+ const upstreamBody = { ...request, input: compressedInput };
2152
+ const upstreamHeaders = {
2153
+ "Authorization": `Bearer ${llmApiKey}`,
2154
+ "Content-Type": "application/json"
2155
+ };
2156
+ if (request.stream) {
2157
+ upstreamHeaders["Accept"] = "text/event-stream";
2158
+ }
2159
+ logger.log(`[RESPONSES] ${request.model} \u2192 ${upstreamUrl}`);
2160
+ try {
2161
+ const upstreamResponse = await fetch(upstreamUrl, {
2162
+ method: "POST",
2163
+ headers: upstreamHeaders,
2164
+ body: JSON.stringify(upstreamBody)
2165
+ });
2166
+ if (!upstreamResponse.ok) {
2167
+ const errorBody = await upstreamResponse.text();
2168
+ logger.log(`[RESPONSES] Upstream error ${upstreamResponse.status}: ${errorBody.slice(0, 500)}`);
2169
+ setCORSHeaders3(res);
2170
+ res.writeHead(upstreamResponse.status, {
2171
+ "Content-Type": upstreamResponse.headers.get("Content-Type") || "application/json"
2172
+ });
2173
+ res.end(errorBody);
2174
+ return;
2175
+ }
2176
+ if (request.stream && upstreamResponse.body) {
2177
+ const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
2178
+ await pipeResponsesSSE(
2179
+ upstreamResponse,
2180
+ res,
2181
+ (text) => learningBuffer?.append(text),
2182
+ () => learningBuffer?.flush(),
2183
+ totalTokensSaved
2184
+ );
2185
+ return;
2186
+ }
2187
+ const responseBody = await upstreamResponse.text();
2188
+ let finalBody = responseBody;
2189
+ if (totalTokensSaved > 0) {
2190
+ try {
2191
+ const parsed = JSON.parse(responseBody);
2192
+ if (parsed?.usage?.input_tokens != null) {
2193
+ parsed.usage.input_tokens += totalTokensSaved;
2194
+ if (parsed.usage.total_tokens != null) {
2195
+ parsed.usage.total_tokens += totalTokensSaved;
2196
+ }
2197
+ finalBody = JSON.stringify(parsed);
2198
+ logger.log(`[TOKENS] Adjusted input_tokens by +${totalTokensSaved}`);
2199
+ }
2200
+ } catch {
2201
+ }
2202
+ }
2203
+ setCORSHeaders3(res);
2204
+ res.writeHead(200, { "Content-Type": "application/json" });
2205
+ res.end(finalBody);
2206
+ if (anyCompressed) {
2207
+ try {
2208
+ const parsed = JSON.parse(responseBody);
2209
+ if (parsed?.output) {
2210
+ const text = extractOutputText(parsed.output);
2211
+ if (text.length > 0) {
2212
+ pipeline.pipeline.triggerLearning(text);
2213
+ }
2214
+ }
2215
+ } catch {
2216
+ }
2217
+ }
2218
+ } catch (err) {
2219
+ const message = err instanceof Error ? err.message : String(err);
2220
+ logger.log(`[ERROR] Upstream request failed: ${message}`);
2221
+ if (!res.headersSent) {
2222
+ sendJSON2(res, 502, {
2223
+ error: { message: `Failed to reach upstream LLM: ${message}`, type: "server_error" }
2224
+ });
2225
+ }
2226
+ }
2227
+ }
2228
+
2229
+ // src/proxy/handler.ts
2230
+ function setCORSHeaders4(res) {
2231
+ res.setHeader("Access-Control-Allow-Origin", "*");
2232
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH");
2233
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, anthropic-dangerous-direct-browser-access");
2234
+ res.setHeader("Access-Control-Max-Age", "86400");
2235
+ }
2236
+ function sendJSON3(res, status, body) {
2237
+ setCORSHeaders4(res);
2238
+ res.writeHead(status, { "Content-Type": "application/json" });
2239
+ res.end(JSON.stringify(body));
2240
+ }
1258
2241
  function readBody(req) {
1259
2242
  return new Promise((resolve, reject) => {
1260
2243
  const chunks = [];
1261
2244
  req.on("data", (chunk) => chunks.push(chunk));
1262
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
2245
+ req.on("end", () => resolve(Buffer.concat(chunks)));
1263
2246
  req.on("error", reject);
1264
2247
  });
1265
2248
  }
1266
- async function passthroughAnthropic(req, res, fullUrl, config, logger) {
1267
- const upstreamUrl = `${config.anthropicUpstreamUrl}${fullUrl}`;
1268
- const headers = {
1269
- "Content-Type": "application/json"
1270
- };
1271
- const xApiKey = req.headers["x-api-key"];
1272
- if (typeof xApiKey === "string") headers["x-api-key"] = xApiKey;
1273
- const auth = req.headers["authorization"];
1274
- if (typeof auth === "string") headers["Authorization"] = auth;
1275
- const version = req.headers["anthropic-version"];
1276
- if (typeof version === "string") headers["anthropic-version"] = version;
1277
- const beta = req.headers["anthropic-beta"];
1278
- if (typeof beta === "string") headers["anthropic-beta"] = beta;
2249
+ function detectUpstream(req, url) {
2250
+ if (req.headers["x-api-key"]) return "anthropic";
2251
+ if (req.headers["anthropic-version"]) return "anthropic";
2252
+ if (url.startsWith("/v1/messages") || url.startsWith("/messages")) return "anthropic";
2253
+ return "anthropic";
2254
+ }
2255
+ function getUpstreamBaseUrl(target, config) {
2256
+ return target === "anthropic" ? config.anthropicUpstreamUrl : config.upstreamBaseUrl;
2257
+ }
2258
+ var HOP_BY_HOP = /* @__PURE__ */ new Set([
2259
+ "host",
2260
+ "connection",
2261
+ "keep-alive",
2262
+ "transfer-encoding",
2263
+ "te",
2264
+ "trailer",
2265
+ "upgrade",
2266
+ "proxy-authorization",
2267
+ "proxy-authenticate"
2268
+ ]);
2269
+ function buildUpstreamHeaders(req) {
2270
+ const headers = {};
2271
+ for (const [key, value] of Object.entries(req.headers)) {
2272
+ if (HOP_BY_HOP.has(key)) continue;
2273
+ if (value === void 0) continue;
2274
+ headers[key] = Array.isArray(value) ? value.join(", ") : value;
2275
+ }
2276
+ return headers;
2277
+ }
2278
+ async function passthroughToUpstream(req, res, fullUrl, config, logger) {
2279
+ const target = detectUpstream(req, fullUrl);
2280
+ const upstreamBase = getUpstreamBaseUrl(target, config);
2281
+ const upstreamUrl = `${upstreamBase}${fullUrl}`;
2282
+ const method = req.method?.toUpperCase() ?? "GET";
2283
+ logger.log(`[PASSTHROUGH] ${method} ${fullUrl} \u2192 ${target} (${upstreamUrl})`);
2284
+ const headers = buildUpstreamHeaders(req);
1279
2285
  try {
1280
- const body = await readBody(req);
2286
+ const hasBody = method !== "GET" && method !== "HEAD";
2287
+ const body = hasBody ? await readBody(req) : void 0;
1281
2288
  const upstreamRes = await fetch(upstreamUrl, {
1282
- method: "POST",
2289
+ method,
1283
2290
  headers,
1284
2291
  body
1285
2292
  });
1286
- const responseBody = await upstreamRes.text();
1287
- setCORSHeaders3(res);
1288
- res.writeHead(upstreamRes.status, {
1289
- "Content-Type": upstreamRes.headers.get("Content-Type") || "application/json"
1290
- });
1291
- res.end(responseBody);
2293
+ const contentType = upstreamRes.headers.get("Content-Type") || "application/json";
2294
+ const isStreaming = contentType.includes("text/event-stream");
2295
+ if (isStreaming && upstreamRes.body) {
2296
+ setCORSHeaders4(res);
2297
+ res.writeHead(upstreamRes.status, {
2298
+ "Content-Type": contentType,
2299
+ "Cache-Control": "no-cache",
2300
+ "Connection": "keep-alive"
2301
+ });
2302
+ const reader = upstreamRes.body.getReader();
2303
+ try {
2304
+ while (true) {
2305
+ const { done, value } = await reader.read();
2306
+ if (done) break;
2307
+ res.write(value);
2308
+ }
2309
+ } finally {
2310
+ res.end();
2311
+ }
2312
+ } else {
2313
+ const responseBody = await upstreamRes.arrayBuffer();
2314
+ setCORSHeaders4(res);
2315
+ const responseHeaders = { "Content-Type": contentType };
2316
+ const reqId = upstreamRes.headers.get("request-id");
2317
+ if (reqId) responseHeaders["request-id"] = reqId;
2318
+ res.writeHead(upstreamRes.status, responseHeaders);
2319
+ res.end(Buffer.from(responseBody));
2320
+ }
1292
2321
  } catch (err) {
1293
2322
  const message = err instanceof Error ? err.message : String(err);
1294
- logger.log(`[ERROR] Passthrough failed: ${message}`);
1295
- setCORSHeaders3(res);
1296
- res.writeHead(502, { "Content-Type": "application/json" });
1297
- res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: `Failed to reach upstream: ${message}` } }));
2323
+ logger.log(`[ERROR] Passthrough to ${target} failed: ${message}`);
2324
+ if (!res.headersSent) {
2325
+ setCORSHeaders4(res);
2326
+ res.writeHead(502, { "Content-Type": "application/json" });
2327
+ res.end(JSON.stringify({
2328
+ type: "error",
2329
+ error: { type: "api_error", message: `Liminal proxy: failed to reach ${target} upstream: ${message}` }
2330
+ }));
2331
+ }
1298
2332
  }
1299
2333
  }
1300
2334
  function createRequestHandler(pipeline, config, logger) {
@@ -1307,14 +2341,14 @@ function createRequestHandler(pipeline, config, logger) {
1307
2341
  const authType = req.headers["x-api-key"] ? "x-api-key" : req.headers["authorization"] ? "bearer" : "none";
1308
2342
  logger.log(`[REQUEST] ${method} ${fullUrl} (auth: ${authType})`);
1309
2343
  if (method === "OPTIONS") {
1310
- setCORSHeaders3(res);
2344
+ setCORSHeaders4(res);
1311
2345
  res.writeHead(204);
1312
2346
  res.end();
1313
2347
  return;
1314
2348
  }
1315
2349
  if (method === "GET" && (url === "/health" || url === "/")) {
1316
2350
  const summary = pipeline.getSessionSummary();
1317
- sendJSON2(res, 200, {
2351
+ sendJSON3(res, 200, {
1318
2352
  status: "ok",
1319
2353
  version: config.rscApiKey ? "connected" : "no-api-key",
1320
2354
  rsc_connected: !pipeline.isCircuitOpen(),
@@ -1334,53 +2368,41 @@ function createRequestHandler(pipeline, config, logger) {
1334
2368
  });
1335
2369
  return;
1336
2370
  }
1337
- if (method === "GET" && (url === "/v1/models" || url === "/models")) {
1338
- const llmApiKey = req.headers.authorization?.slice(7);
1339
- if (!llmApiKey) {
1340
- sendJSON2(res, 401, {
1341
- error: { message: "Authorization header with Bearer token is required", type: "authentication_error" }
1342
- });
1343
- return;
1344
- }
2371
+ if (method === "POST" && (url === "/v1/chat/completions" || url === "/chat/completions")) {
2372
+ const body = await readBody(req);
2373
+ let parsed;
1345
2374
  try {
1346
- const upstreamRes = await fetch(`${config.upstreamBaseUrl}/v1/models`, {
1347
- headers: { "Authorization": `Bearer ${llmApiKey}` }
1348
- });
1349
- const body = await upstreamRes.text();
1350
- setCORSHeaders3(res);
1351
- res.writeHead(upstreamRes.status, {
1352
- "Content-Type": upstreamRes.headers.get("Content-Type") || "application/json"
1353
- });
1354
- res.end(body);
1355
- } catch (err) {
1356
- const message = err instanceof Error ? err.message : String(err);
1357
- sendJSON2(res, 502, {
1358
- error: { message: `Failed to reach upstream: ${message}`, type: "server_error" }
2375
+ parsed = JSON.parse(body.toString("utf-8"));
2376
+ } catch {
2377
+ sendJSON3(res, 400, {
2378
+ error: { message: "Invalid JSON body", type: "invalid_request_error" }
1359
2379
  });
2380
+ return;
1360
2381
  }
2382
+ await handleChatCompletions(req, res, parsed, pipeline, config, logger);
1361
2383
  return;
1362
2384
  }
1363
- if (method === "POST" && (url === "/v1/chat/completions" || url === "/chat/completions")) {
2385
+ if (method === "POST" && (url === "/v1/responses" || url === "/responses")) {
1364
2386
  const body = await readBody(req);
1365
2387
  let parsed;
1366
2388
  try {
1367
- parsed = JSON.parse(body);
2389
+ parsed = JSON.parse(body.toString("utf-8"));
1368
2390
  } catch {
1369
- sendJSON2(res, 400, {
2391
+ sendJSON3(res, 400, {
1370
2392
  error: { message: "Invalid JSON body", type: "invalid_request_error" }
1371
2393
  });
1372
2394
  return;
1373
2395
  }
1374
- await handleChatCompletions(req, res, parsed, pipeline, config, logger);
2396
+ await handleResponses(req, res, parsed, pipeline, config, logger);
1375
2397
  return;
1376
2398
  }
1377
2399
  if (method === "POST" && (url === "/v1/messages" || url === "/messages")) {
1378
2400
  const body = await readBody(req);
1379
2401
  let parsed;
1380
2402
  try {
1381
- parsed = JSON.parse(body);
2403
+ parsed = JSON.parse(body.toString("utf-8"));
1382
2404
  } catch {
1383
- sendJSON2(res, 400, {
2405
+ sendJSON3(res, 400, {
1384
2406
  type: "error",
1385
2407
  error: { type: "invalid_request_error", message: "Invalid JSON body" }
1386
2408
  });
@@ -1389,18 +2411,12 @@ function createRequestHandler(pipeline, config, logger) {
1389
2411
  await handleAnthropicMessages(req, res, parsed, pipeline, config, logger);
1390
2412
  return;
1391
2413
  }
1392
- if (method === "POST" && url.startsWith("/v1/messages/")) {
1393
- await passthroughAnthropic(req, res, fullUrl, config, logger);
1394
- return;
1395
- }
1396
- sendJSON2(res, 404, {
1397
- error: { message: `Not found: ${method} ${url}`, type: "invalid_request_error" }
1398
- });
2414
+ await passthroughToUpstream(req, res, fullUrl, config, logger);
1399
2415
  } catch (err) {
1400
2416
  const message = err instanceof Error ? err.message : String(err);
1401
2417
  logger.log(`[ERROR] Proxy handler error: ${message}`);
1402
2418
  if (!res.headersSent) {
1403
- sendJSON2(res, 500, {
2419
+ sendJSON3(res, 500, {
1404
2420
  error: { message: "Internal proxy error", type: "server_error" }
1405
2421
  });
1406
2422
  }
@@ -1467,7 +2483,7 @@ var ProxyServer = class {
1467
2483
  };
1468
2484
 
1469
2485
  // src/daemon/logger.ts
1470
- import { appendFileSync as appendFileSync2, statSync, renameSync, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
2486
+ import { appendFileSync as appendFileSync2, statSync, renameSync, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
1471
2487
  import { dirname as dirname2 } from "path";
1472
2488
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
1473
2489
  var MAX_BACKUPS = 2;
@@ -1478,7 +2494,7 @@ var FileLogger = class {
1478
2494
  this.logFile = options?.logFile ?? LOG_FILE;
1479
2495
  this.mirrorStdout = options?.mirrorStdout ?? false;
1480
2496
  const logDir = dirname2(this.logFile);
1481
- if (!existsSync3(logDir)) {
2497
+ if (!existsSync5(logDir)) {
1482
2498
  mkdirSync2(logDir, { recursive: true });
1483
2499
  }
1484
2500
  }
@@ -1503,7 +2519,7 @@ var FileLogger = class {
1503
2519
  for (let i = MAX_BACKUPS - 1; i >= 1; i--) {
1504
2520
  const from = `${this.logFile}.${i}`;
1505
2521
  const to = `${this.logFile}.${i + 1}`;
1506
- if (existsSync3(from)) renameSync(from, to);
2522
+ if (existsSync5(from)) renameSync(from, to);
1507
2523
  }
1508
2524
  renameSync(this.logFile, `${this.logFile}.1`);
1509
2525
  } catch {
@@ -1515,16 +2531,16 @@ var FileLogger = class {
1515
2531
  };
1516
2532
 
1517
2533
  // src/daemon/lifecycle.ts
1518
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, existsSync as existsSync4 } from "fs";
2534
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync6 } from "fs";
1519
2535
  import { fork } from "child_process";
1520
2536
  import { fileURLToPath } from "url";
1521
2537
  function writePidFile(pid) {
1522
- writeFileSync2(PID_FILE, String(pid), "utf-8");
2538
+ writeFileSync3(PID_FILE, String(pid), "utf-8");
1523
2539
  }
1524
2540
  function readPidFile() {
1525
- if (!existsSync4(PID_FILE)) return null;
2541
+ if (!existsSync6(PID_FILE)) return null;
1526
2542
  try {
1527
- const content = readFileSync3(PID_FILE, "utf-8").trim();
2543
+ const content = readFileSync4(PID_FILE, "utf-8").trim();
1528
2544
  const pid = parseInt(content, 10);
1529
2545
  return isNaN(pid) ? null : pid;
1530
2546
  } catch {
@@ -1533,7 +2549,7 @@ function readPidFile() {
1533
2549
  }
1534
2550
  function removePidFile() {
1535
2551
  try {
1536
- if (existsSync4(PID_FILE)) unlinkSync(PID_FILE);
2552
+ if (existsSync6(PID_FILE)) unlinkSync(PID_FILE);
1537
2553
  } catch {
1538
2554
  }
1539
2555
  }
@@ -1914,17 +2930,17 @@ async function configCommand(flags) {
1914
2930
  }
1915
2931
 
1916
2932
  // src/commands/logs.ts
1917
- import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync2, createReadStream } from "fs";
2933
+ import { readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync2, createReadStream } from "fs";
1918
2934
  import { watchFile, unwatchFile } from "fs";
1919
2935
  async function logsCommand(flags) {
1920
2936
  const follow = flags.has("follow") || flags.has("f");
1921
2937
  const linesFlag = flags.get("lines") ?? flags.get("n");
1922
2938
  const lines = typeof linesFlag === "string" ? parseInt(linesFlag, 10) : 50;
1923
- if (!existsSync5(LOG_FILE)) {
2939
+ if (!existsSync7(LOG_FILE)) {
1924
2940
  console.log('No log file found. Start the daemon with "liminal start" to generate logs.');
1925
2941
  return;
1926
2942
  }
1927
- const content = readFileSync4(LOG_FILE, "utf-8");
2943
+ const content = readFileSync5(LOG_FILE, "utf-8");
1928
2944
  const allLines = content.split("\n");
1929
2945
  const tail = allLines.slice(-lines - 1);
1930
2946
  process.stdout.write(tail.join("\n"));
@@ -1949,6 +2965,135 @@ async function logsCommand(flags) {
1949
2965
  });
1950
2966
  }
1951
2967
 
2968
+ // src/commands/uninstall.ts
2969
+ import { existsSync as existsSync8, rmSync, readFileSync as readFileSync6 } from "fs";
2970
+ var BOLD2 = "\x1B[1m";
2971
+ var DIM2 = "\x1B[2m";
2972
+ var GREEN2 = "\x1B[32m";
2973
+ var YELLOW2 = "\x1B[33m";
2974
+ var RESET2 = "\x1B[0m";
2975
+ function loadConfiguredTools() {
2976
+ if (!existsSync8(CONFIG_FILE)) return [];
2977
+ try {
2978
+ const raw = readFileSync6(CONFIG_FILE, "utf-8");
2979
+ const config = JSON.parse(raw);
2980
+ if (Array.isArray(config.tools)) return config.tools;
2981
+ } catch {
2982
+ }
2983
+ return [];
2984
+ }
2985
+ async function uninstallCommand() {
2986
+ console.log();
2987
+ console.log(` ${BOLD2}Liminal Uninstall${RESET2}`);
2988
+ console.log();
2989
+ const confirm = await selectPrompt({
2990
+ message: "Remove Liminal configuration and restore tool settings?",
2991
+ options: [
2992
+ { label: "Yes", value: true, description: "Undo all Liminal setup" },
2993
+ { label: "No", value: false, description: "Cancel" }
2994
+ ],
2995
+ defaultIndex: 1
2996
+ // Default to No for safety
2997
+ });
2998
+ if (confirm !== true) {
2999
+ console.log();
3000
+ console.log(" Cancelled.");
3001
+ console.log();
3002
+ return;
3003
+ }
3004
+ console.log();
3005
+ const state = isDaemonRunning();
3006
+ if (state.running && state.pid) {
3007
+ console.log(` Stopping Liminal daemon (PID ${state.pid})...`);
3008
+ try {
3009
+ process.kill(state.pid, "SIGTERM");
3010
+ for (let i = 0; i < 15; i++) {
3011
+ await sleep(200);
3012
+ if (!isProcessAlive(state.pid)) break;
3013
+ }
3014
+ if (isProcessAlive(state.pid)) {
3015
+ process.kill(state.pid, "SIGKILL");
3016
+ }
3017
+ } catch {
3018
+ }
3019
+ removePidFile();
3020
+ console.log(` ${GREEN2}\u2713${RESET2} Daemon stopped`);
3021
+ } else {
3022
+ console.log(` ${DIM2}\xB7${RESET2} Daemon not running`);
3023
+ }
3024
+ const profile = detectShellProfile();
3025
+ if (profile) {
3026
+ const existing = findLiminalExportsInProfile(profile);
3027
+ if (existing.length > 0) {
3028
+ const removed = removeLiminalFromShellProfile(profile);
3029
+ if (removed.length > 0) {
3030
+ console.log(` ${GREEN2}\u2713${RESET2} Removed ${removed.length} line${removed.length > 1 ? "s" : ""} from ${profile.name}:`);
3031
+ for (const line of removed) {
3032
+ const trimmed = line.trim();
3033
+ if (trimmed && trimmed !== "# Liminal \u2014 route AI tools through compression proxy") {
3034
+ console.log(` ${DIM2}${trimmed}${RESET2}`);
3035
+ }
3036
+ }
3037
+ }
3038
+ } else {
3039
+ console.log(` ${DIM2}\xB7${RESET2} No Liminal exports found in ${profile.name}`);
3040
+ }
3041
+ }
3042
+ const configuredTools = loadConfiguredTools();
3043
+ const allTools = configuredTools.length > 0 ? configuredTools : CONNECTORS.map((c) => c.info.id);
3044
+ const connectors = getConnectors(allTools);
3045
+ const manualSteps = [];
3046
+ for (const connector of connectors) {
3047
+ const result = await connector.teardown();
3048
+ if (result.manualSteps.length > 0 && !connector.info.automatable) {
3049
+ manualSteps.push({
3050
+ label: connector.info.label,
3051
+ steps: result.manualSteps
3052
+ });
3053
+ }
3054
+ }
3055
+ if (manualSteps.length > 0) {
3056
+ console.log();
3057
+ console.log(` ${YELLOW2}Manual steps needed:${RESET2}`);
3058
+ for (const { label, steps } of manualSteps) {
3059
+ console.log();
3060
+ console.log(` ${BOLD2}${label}:${RESET2}`);
3061
+ for (const step of steps) {
3062
+ console.log(` ${step}`);
3063
+ }
3064
+ }
3065
+ }
3066
+ if (existsSync8(LIMINAL_DIR)) {
3067
+ console.log();
3068
+ const removeData = await selectPrompt({
3069
+ message: "Remove ~/.liminal/ directory? (config, logs, PID file)",
3070
+ options: [
3071
+ { label: "Yes", value: true, description: "Delete all Liminal data" },
3072
+ { label: "No", value: false, description: "Keep config and logs" }
3073
+ ],
3074
+ defaultIndex: 1
3075
+ // Default to keep
3076
+ });
3077
+ if (removeData === true) {
3078
+ rmSync(LIMINAL_DIR, { recursive: true, force: true });
3079
+ console.log(` ${GREEN2}\u2713${RESET2} Removed ~/.liminal/`);
3080
+ } else {
3081
+ console.log(` ${DIM2}\xB7${RESET2} Kept ~/.liminal/`);
3082
+ }
3083
+ }
3084
+ console.log();
3085
+ console.log(` ${GREEN2}Liminal has been uninstalled.${RESET2}`);
3086
+ console.log();
3087
+ console.log(` ${DIM2}Your AI tools will connect directly to their APIs.${RESET2}`);
3088
+ console.log(` ${DIM2}Restart your terminal for shell changes to take effect.${RESET2}`);
3089
+ if (manualSteps.length > 0) {
3090
+ console.log(` ${YELLOW2}Don't forget the manual steps above for ${manualSteps.map((s) => s.label).join(", ")}.${RESET2}`);
3091
+ }
3092
+ console.log();
3093
+ console.log(` ${DIM2}To reinstall: npx @cognisos/liminal init${RESET2}`);
3094
+ console.log();
3095
+ }
3096
+
1952
3097
  // src/bin.ts
1953
3098
  var USAGE = `
1954
3099
  liminal v${VERSION} \u2014 Transparent LLM context compression proxy
@@ -1963,6 +3108,7 @@ var USAGE = `
1963
3108
  liminal summary Detailed session metrics
1964
3109
  liminal config [--set k=v] [--get k] View or edit configuration
1965
3110
  liminal logs [--follow] [--lines N] View proxy logs
3111
+ liminal uninstall Remove Liminal configuration
1966
3112
 
1967
3113
  Options:
1968
3114
  -h, --help Show this help message
@@ -2040,6 +3186,9 @@ async function main() {
2040
3186
  case "logs":
2041
3187
  await logsCommand(flags);
2042
3188
  break;
3189
+ case "uninstall":
3190
+ await uninstallCommand();
3191
+ break;
2043
3192
  case "":
2044
3193
  console.log(USAGE);
2045
3194
  process.exit(0);