@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.
- package/dist/bin.js +1384 -235
- package/dist/bin.js.map +1 -1
- 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.
|
|
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://
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
205
|
-
if (
|
|
206
|
-
if ("pause" in
|
|
207
|
-
|
|
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
|
-
|
|
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 (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if ("resume" in
|
|
230
|
-
|
|
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/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
const lines = [];
|
|
606
|
-
if (tools.includes("claude-code")) {
|
|
607
|
-
lines.push(`export ANTHROPIC_BASE_URL=${base}`);
|
|
608
|
-
}
|
|
609
|
-
if (tools.includes("codex") || tools.includes("openai-compatible")) {
|
|
610
|
-
lines.push(`export OPENAI_BASE_URL=${base}/v1`);
|
|
611
|
-
}
|
|
612
|
-
return lines;
|
|
751
|
+
function getCurrentBaseUrl2() {
|
|
752
|
+
return process.env[ENV_VAR2] || void 0;
|
|
613
753
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
754
|
+
function hasCodexConfig() {
|
|
755
|
+
return existsSync3(CODEX_CONFIG_FILE);
|
|
756
|
+
}
|
|
757
|
+
function codexConfigMentionsLiminal() {
|
|
758
|
+
if (!hasCodexConfig()) return false;
|
|
616
759
|
try {
|
|
617
|
-
const content =
|
|
618
|
-
return content.includes(
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
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
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
],
|
|
706
|
-
defaultIndex: 0
|
|
707
|
-
});
|
|
708
|
-
if (autoResult === true) {
|
|
709
|
-
const newLines = exportLines.filter((line) => !lineExistsInFile(profile.path, line));
|
|
710
|
-
if (newLines.length > 0) {
|
|
711
|
-
appendToShellProfile(profile, newLines);
|
|
712
|
-
}
|
|
713
|
-
console.log();
|
|
714
|
-
console.log(` Added to ${profile.name}:`);
|
|
715
|
-
for (const line of exportLines) {
|
|
716
|
-
console.log(` ${line}`);
|
|
717
|
-
}
|
|
718
|
-
console.log();
|
|
719
|
-
console.log(` Run \x1B[1msource ${profile.name}\x1B[0m or restart your terminal to apply.`);
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
1116
|
+
} else if (uniqueExports.length > 0) {
|
|
1117
|
+
console.log();
|
|
1118
|
+
console.log(" Add these to your shell profile:");
|
|
738
1119
|
console.log();
|
|
739
|
-
|
|
740
|
-
|
|
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(
|
|
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
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
|
867
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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(
|
|
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/
|
|
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
|
|
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)
|
|
2245
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1263
2246
|
req.on("error", reject);
|
|
1264
2247
|
});
|
|
1265
2248
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
|
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
|
|
2289
|
+
method,
|
|
1283
2290
|
headers,
|
|
1284
2291
|
body
|
|
1285
2292
|
});
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
1338
|
-
const
|
|
1339
|
-
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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/
|
|
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
|
-
|
|
2391
|
+
sendJSON3(res, 400, {
|
|
1370
2392
|
error: { message: "Invalid JSON body", type: "invalid_request_error" }
|
|
1371
2393
|
});
|
|
1372
2394
|
return;
|
|
1373
2395
|
}
|
|
1374
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
-
|
|
2538
|
+
writeFileSync3(PID_FILE, String(pid), "utf-8");
|
|
1523
2539
|
}
|
|
1524
2540
|
function readPidFile() {
|
|
1525
|
-
if (!
|
|
2541
|
+
if (!existsSync6(PID_FILE)) return null;
|
|
1526
2542
|
try {
|
|
1527
|
-
const content =
|
|
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 (
|
|
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
|
|
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 (!
|
|
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 =
|
|
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);
|