squeezr-ai 1.17.1 → 1.17.4
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/README.md +5 -2
- package/bin/squeezr.js +109 -0
- package/dist/compressor.d.ts +1 -1
- package/dist/compressor.js +19 -8
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/expand.d.ts +2 -0
- package/dist/expand.js +26 -0
- package/dist/index.js +16 -4
- package/dist/server.js +35 -4
- package/dist/sessionCache.d.ts +2 -0
- package/dist/sessionCache.js +26 -0
- package/package.json +62 -61
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Token compression proxy for AI coding CLIs.** Sits between your CLI and the API, compresses context on the fly, saves thousands of tokens per session.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/squeezr-ai) [](LICENSE) [](https://www.npmjs.com/package/squeezr-ai) [](LICENSE) []()
|
|
6
6
|
|
|
7
7
|
## Supported CLIs
|
|
8
8
|
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
| Gemini CLI | HTTP to Gemini API | `GEMINI_API_BASE_URL=http://localhost:8080` |
|
|
15
15
|
| Ollama | HTTP (local) | Transparent via dummy API key detection |
|
|
16
16
|
| **Codex** | **WebSocket to chatgpt.com** | **TLS-terminating MITM proxy on :8081** |
|
|
17
|
+
| **Cursor IDE** | **ConnectRPC/HTTP2 to api2.cursor.sh** | **`squeezr cursor` — MITM proxy on :8082** |
|
|
18
|
+
| Continue (VS Code) | HTTP to OpenAI-compat | `apiBase: http://localhost:8080/v1` |
|
|
17
19
|
|
|
18
20
|
## Quick start
|
|
19
21
|
|
|
@@ -109,8 +111,9 @@ threshold = 800 # min chars to trigger compression
|
|
|
109
111
|
keep_recent = 3 # last N results left uncompressed
|
|
110
112
|
compress_system_prompt = true
|
|
111
113
|
compress_conversation = false # aggressive: compress assistant messages too
|
|
112
|
-
# skip_tools = ["Read"] #
|
|
114
|
+
# skip_tools = ["Read"] # skip ALL compression for these tools (deterministic + AI)
|
|
113
115
|
# only_tools = ["Bash"] # only compress these tools
|
|
116
|
+
ai_skip_tools = ["Read"] # skip AI compression only (default); deterministic still runs
|
|
114
117
|
|
|
115
118
|
[cache]
|
|
116
119
|
enabled = true
|
package/bin/squeezr.js
CHANGED
|
@@ -200,6 +200,7 @@ Usage:
|
|
|
200
200
|
squeezr status Check if proxy is running
|
|
201
201
|
squeezr config Print config file path and current settings
|
|
202
202
|
squeezr ports Change HTTP and MITM proxy ports
|
|
203
|
+
squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
|
|
203
204
|
squeezr update Kill old processes, install latest from npm, restart
|
|
204
205
|
squeezr uninstall Remove Squeezr completely (env vars, CA, auto-start, logs)
|
|
205
206
|
squeezr version Print version
|
|
@@ -1150,6 +1151,109 @@ Done!
|
|
|
1150
1151
|
installShellWrapper()
|
|
1151
1152
|
}
|
|
1152
1153
|
|
|
1154
|
+
// ── squeezr tunnel ────────────────────────────────────────────────────────────
|
|
1155
|
+
// Exposes the local proxy via a Cloudflare Quick Tunnel (free, no account needed).
|
|
1156
|
+
// Cursor IDE requires a public HTTPS URL because its servers call the endpoint
|
|
1157
|
+
// from Cloudflare's infrastructure — localhost is unreachable from there.
|
|
1158
|
+
|
|
1159
|
+
async function startTunnel() {
|
|
1160
|
+
const port = getPort()
|
|
1161
|
+
|
|
1162
|
+
// Verify proxy is running first
|
|
1163
|
+
const running = await new Promise(resolve => {
|
|
1164
|
+
const req = http.get(`http://localhost:${port}/squeezr/health`, res => {
|
|
1165
|
+
resolve(res.statusCode === 200)
|
|
1166
|
+
})
|
|
1167
|
+
req.on('error', () => resolve(false))
|
|
1168
|
+
req.setTimeout(2000, () => { req.destroy(); resolve(false) })
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
if (!running) {
|
|
1172
|
+
console.error(`Squeezr proxy is not running on port ${port}.`)
|
|
1173
|
+
console.error(`Start it first: squeezr start`)
|
|
1174
|
+
process.exit(1)
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
console.log(`Starting Cloudflare Quick Tunnel for http://localhost:${port}...`)
|
|
1178
|
+
console.log(`(free, no account needed — powered by trycloudflare.com)\n`)
|
|
1179
|
+
|
|
1180
|
+
// Try cloudflared binary, fall back to npx
|
|
1181
|
+
let tunnelCmd, tunnelArgs
|
|
1182
|
+
try {
|
|
1183
|
+
execSync('cloudflared --version', { stdio: 'pipe' })
|
|
1184
|
+
tunnelCmd = 'cloudflared'
|
|
1185
|
+
tunnelArgs = ['tunnel', '--url', `http://localhost:${port}`]
|
|
1186
|
+
} catch {
|
|
1187
|
+
tunnelCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
1188
|
+
tunnelArgs = ['cloudflared@latest', 'tunnel', '--url', `http://localhost:${port}`]
|
|
1189
|
+
console.log(`cloudflared not installed — using npx cloudflared (may take a moment to download)\n`)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
let tunnelUrl = null
|
|
1193
|
+
const child = spawn(tunnelCmd, tunnelArgs, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
1194
|
+
|
|
1195
|
+
const printInstructions = (url) => {
|
|
1196
|
+
console.log(`\n ╔══════════════════════════════════════════════════════════════════╗`)
|
|
1197
|
+
console.log(` ║ Tunnel active: ${url.padEnd(49)}║`)
|
|
1198
|
+
console.log(` ╠══════════════════════════════════════════════════════════════════╣`)
|
|
1199
|
+
console.log(` ║ CURSOR SETUP ║`)
|
|
1200
|
+
console.log(` ║ ║`)
|
|
1201
|
+
console.log(` ║ 1. Cursor → Settings → Models ║`)
|
|
1202
|
+
console.log(` ║ 2. Add your OpenAI or Anthropic API key ║`)
|
|
1203
|
+
console.log(` ║ 3. Enable "Override OpenAI Base URL" ║`)
|
|
1204
|
+
console.log(` ║ 4. Set URL to: ${(url + '/v1').padEnd(49)}║`)
|
|
1205
|
+
console.log(` ║ 5. Disable all built-in Cursor models ║`)
|
|
1206
|
+
console.log(` ║ 6. Add a custom model pointing to the same URL ║`)
|
|
1207
|
+
console.log(` ║ ║`)
|
|
1208
|
+
console.log(` ║ CONTINUE EXTENSION (VS Code / JetBrains) ║`)
|
|
1209
|
+
console.log(` ║ No tunnel needed — use http://localhost:${port} directly ${' '.repeat(Math.max(0, 17 - String(port).length))}║`)
|
|
1210
|
+
console.log(` ║ ║`)
|
|
1211
|
+
console.log(` ║ Press Ctrl+C to stop the tunnel ║`)
|
|
1212
|
+
console.log(` ╚══════════════════════════════════════════════════════════════════╝\n`)
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const parseUrl = (line) => {
|
|
1216
|
+
const m = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)
|
|
1217
|
+
return m ? m[0] : null
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
child.stdout.on('data', (chunk) => {
|
|
1221
|
+
const text = chunk.toString()
|
|
1222
|
+
if (!tunnelUrl) {
|
|
1223
|
+
const found = parseUrl(text)
|
|
1224
|
+
if (found) { tunnelUrl = found; printInstructions(tunnelUrl) }
|
|
1225
|
+
}
|
|
1226
|
+
process.stdout.write(text)
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
child.stderr.on('data', (chunk) => {
|
|
1230
|
+
const text = chunk.toString()
|
|
1231
|
+
if (!tunnelUrl) {
|
|
1232
|
+
const found = parseUrl(text)
|
|
1233
|
+
if (found) { tunnelUrl = found; printInstructions(tunnelUrl) }
|
|
1234
|
+
}
|
|
1235
|
+
// Only show cloudflared logs if no URL yet (suppress verbose after)
|
|
1236
|
+
if (!tunnelUrl) process.stderr.write(text)
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
child.on('error', (err) => {
|
|
1240
|
+
if (err.code === 'ENOENT') {
|
|
1241
|
+
console.error(`Could not start tunnel. Install cloudflared: https://developers.cloudflare.com/cloudflared/downloads`)
|
|
1242
|
+
} else {
|
|
1243
|
+
console.error(`Tunnel error: ${err.message}`)
|
|
1244
|
+
}
|
|
1245
|
+
process.exit(1)
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
child.on('exit', (code) => {
|
|
1249
|
+
if (code !== 0) console.log(`\nTunnel stopped (exit ${code})`)
|
|
1250
|
+
process.exit(0)
|
|
1251
|
+
})
|
|
1252
|
+
|
|
1253
|
+
process.on('SIGINT', () => { child.kill(); process.exit(0) })
|
|
1254
|
+
process.on('SIGTERM', () => { child.kill(); process.exit(0) })
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1153
1257
|
// ── CLI router ────────────────────────────────────────────────────────────────
|
|
1154
1258
|
|
|
1155
1259
|
switch (command) {
|
|
@@ -1262,6 +1366,11 @@ switch (command) {
|
|
|
1262
1366
|
case 'ports':
|
|
1263
1367
|
await configurePorts()
|
|
1264
1368
|
break
|
|
1369
|
+
|
|
1370
|
+
case 'tunnel':
|
|
1371
|
+
await startTunnel()
|
|
1372
|
+
break
|
|
1373
|
+
|
|
1265
1374
|
case 'uninstall':
|
|
1266
1375
|
await uninstall()
|
|
1267
1376
|
break
|
package/dist/compressor.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ interface AnthropicMessage {
|
|
|
21
21
|
content?: unknown;
|
|
22
22
|
}>;
|
|
23
23
|
}
|
|
24
|
-
export declare function compressAnthropicMessages(messages: AnthropicMessage[], apiKey: string, config: Config): Promise<[AnthropicMessage[], Savings]>;
|
|
24
|
+
export declare function compressAnthropicMessages(messages: AnthropicMessage[], apiKey: string, config: Config, systemExtraChars?: number): Promise<[AnthropicMessage[], Savings]>;
|
|
25
25
|
interface OpenAIMessage {
|
|
26
26
|
role: string;
|
|
27
27
|
content?: string | null;
|
package/dist/compressor.js
CHANGED
|
@@ -15,8 +15,8 @@ export function getCache(config) {
|
|
|
15
15
|
_cache = new CompressionCache(config.cacheMaxEntries);
|
|
16
16
|
return _cache;
|
|
17
17
|
}
|
|
18
|
-
function estimatePressure(messages) {
|
|
19
|
-
const chars = JSON.stringify(messages).length;
|
|
18
|
+
function estimatePressure(messages, extraChars = 0) {
|
|
19
|
+
const chars = JSON.stringify(messages).length + extraChars;
|
|
20
20
|
return Math.min(chars / 800_000, 1.0);
|
|
21
21
|
}
|
|
22
22
|
// ── Compression backends ──────────────────────────────────────────────────────
|
|
@@ -134,10 +134,10 @@ function buildAnthropicToolIdMap(messages) {
|
|
|
134
134
|
}
|
|
135
135
|
return { nameMap, skipIds };
|
|
136
136
|
}
|
|
137
|
-
export async function compressAnthropicMessages(messages, apiKey, config) {
|
|
137
|
+
export async function compressAnthropicMessages(messages, apiKey, config, systemExtraChars = 0) {
|
|
138
138
|
if (config.disabled)
|
|
139
139
|
return [messages, emptySavings()];
|
|
140
|
-
const pressure = estimatePressure(messages);
|
|
140
|
+
const pressure = estimatePressure(messages, systemExtraChars);
|
|
141
141
|
const threshold = config.thresholdForPressure(pressure);
|
|
142
142
|
const { nameMap: toolIdMap, skipIds } = buildAnthropicToolIdMap(messages);
|
|
143
143
|
const allResults = extractAnthropicToolResults(messages, toolIdMap)
|
|
@@ -211,12 +211,17 @@ export async function compressAnthropicMessages(messages, apiKey, config) {
|
|
|
211
211
|
// Differential: split session cache hits from uncached
|
|
212
212
|
const sessionHits = [];
|
|
213
213
|
const toCompress = [];
|
|
214
|
+
const lastMsgIdx = messages.length - 1;
|
|
214
215
|
for (const c of toProcess) {
|
|
215
216
|
const cached = getBlock(hashText(c.text));
|
|
216
|
-
if (cached)
|
|
217
|
+
if (cached) {
|
|
217
218
|
sessionHits.push({ index: c.index, subIndex: c.subIndex, tool: c.tool, block: cached });
|
|
218
|
-
|
|
219
|
+
}
|
|
220
|
+
else if (c.index === lastMsgIdx && !config.aiSkipTools.has(c.tool.toLowerCase())) {
|
|
221
|
+
// Only AI-compress genuinely new blocks (from the last user message).
|
|
222
|
+
// Historical uncached blocks skip AI compression → prevents burst on first activation.
|
|
219
223
|
toCompress.push(c);
|
|
224
|
+
}
|
|
220
225
|
}
|
|
221
226
|
const freshlyCompressed = toCompress.length > 0
|
|
222
227
|
? await runCompression(toCompress, t => compressWithHaiku(t, apiKey), config)
|
|
@@ -339,12 +344,18 @@ export async function compressOpenAIMessages(messages, apiKey, config, isLocal =
|
|
|
339
344
|
}
|
|
340
345
|
const sessionHits = [];
|
|
341
346
|
const toCompress = [];
|
|
347
|
+
const lastOAIMsgIdx = messages.length - 1;
|
|
348
|
+
const lastAssistantIdx = messages.reduce((best, m, i) => (m.role === 'assistant' ? i : best), -1);
|
|
349
|
+
const newStartIdx = lastAssistantIdx >= 0 ? lastAssistantIdx : lastOAIMsgIdx;
|
|
342
350
|
for (const c of toProcess) {
|
|
343
351
|
const cached = getBlock(hashText(c.text));
|
|
344
|
-
if (cached)
|
|
352
|
+
if (cached) {
|
|
345
353
|
sessionHits.push({ index: c.index, tool: c.tool, block: cached });
|
|
346
|
-
|
|
354
|
+
}
|
|
355
|
+
else if (c.index > newStartIdx && !config.aiSkipTools.has(c.tool.toLowerCase())) {
|
|
356
|
+
// Only AI-compress new tool results (after last assistant turn) — prevents burst on first activation.
|
|
347
357
|
toCompress.push(c);
|
|
358
|
+
}
|
|
348
359
|
}
|
|
349
360
|
const compressFn = isLocal
|
|
350
361
|
? t => compressWithOllama(t, config.localUpstreamUrl, config.localCompressionModel)
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class Config {
|
|
|
9
9
|
readonly dryRun: boolean;
|
|
10
10
|
readonly skipTools: Set<string>;
|
|
11
11
|
readonly onlyTools: Set<string>;
|
|
12
|
+
readonly aiSkipTools: Set<string>;
|
|
12
13
|
readonly cacheEnabled: boolean;
|
|
13
14
|
readonly cacheMaxEntries: number;
|
|
14
15
|
readonly adaptiveEnabled: boolean;
|
package/dist/config.js
CHANGED
|
@@ -49,6 +49,7 @@ export class Config {
|
|
|
49
49
|
dryRun;
|
|
50
50
|
skipTools;
|
|
51
51
|
onlyTools;
|
|
52
|
+
aiSkipTools;
|
|
52
53
|
cacheEnabled;
|
|
53
54
|
cacheMaxEntries;
|
|
54
55
|
adaptiveEnabled;
|
|
@@ -77,6 +78,7 @@ export class Config {
|
|
|
77
78
|
this.dryRun = env('SQUEEZR_DRY_RUN', '') === '1';
|
|
78
79
|
this.skipTools = new Set((c.skip_tools ?? []).map(t => t.toLowerCase()));
|
|
79
80
|
this.onlyTools = new Set((c.only_tools ?? []).map(t => t.toLowerCase()));
|
|
81
|
+
this.aiSkipTools = new Set((c.ai_skip_tools ?? ['read']).map(t => t.toLowerCase()));
|
|
80
82
|
this.cacheEnabled = ca.enabled ?? true;
|
|
81
83
|
this.cacheMaxEntries = ca.max_entries ?? 1000;
|
|
82
84
|
this.adaptiveEnabled = ad.enabled ?? true;
|
package/dist/expand.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export declare function storeOriginal(original: string): string;
|
|
|
2
2
|
export declare function retrieveOriginal(id: string): string | undefined;
|
|
3
3
|
export declare function expandStoreSize(): number;
|
|
4
4
|
export declare function clearExpandStore(): void;
|
|
5
|
+
export declare function loadExpandStore(): void;
|
|
6
|
+
export declare function persistExpandStore(): void;
|
|
5
7
|
export declare const EXPAND_TOOL_ANTHROPIC: {
|
|
6
8
|
name: string;
|
|
7
9
|
description: string;
|
package/dist/expand.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
const EXPAND_STORE_PATH = join(homedir(), '.squeezr', 'expand_store.json');
|
|
2
6
|
/**
|
|
3
7
|
* Expand store — keeps original tool results so the model can retrieve
|
|
4
8
|
* them if it needs more detail than the compressed summary provides.
|
|
@@ -33,6 +37,28 @@ export function expandStoreSize() {
|
|
|
33
37
|
export function clearExpandStore() {
|
|
34
38
|
store.clear();
|
|
35
39
|
}
|
|
40
|
+
export function loadExpandStore() {
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync(EXPAND_STORE_PATH)) {
|
|
43
|
+
const raw = JSON.parse(readFileSync(EXPAND_STORE_PATH, 'utf-8'));
|
|
44
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
45
|
+
store.set(k, v);
|
|
46
|
+
}
|
|
47
|
+
if (store.size > 0)
|
|
48
|
+
console.log(`[squeezr] Loaded ${store.size} expand store entries from disk`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch { /* ignore */ }
|
|
52
|
+
}
|
|
53
|
+
export function persistExpandStore() {
|
|
54
|
+
try {
|
|
55
|
+
const dir = join(homedir(), '.squeezr');
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
writeFileSync(EXPAND_STORE_PATH, JSON.stringify(Object.fromEntries(store)));
|
|
59
|
+
}
|
|
60
|
+
catch { /* ignore */ }
|
|
61
|
+
}
|
|
36
62
|
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
37
63
|
export const EXPAND_TOOL_ANTHROPIC = {
|
|
38
64
|
name: 'squeezr_expand',
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,15 @@ import { app, stats } from './server.js';
|
|
|
3
3
|
import { config } from './config.js';
|
|
4
4
|
import { VERSION } from './version.js';
|
|
5
5
|
import { startMitmProxy } from './codexMitm.js';
|
|
6
|
+
import { loadSessionCache, persistSessionCache } from './sessionCache.js';
|
|
7
|
+
import { loadExpandStore, persistExpandStore } from './expand.js';
|
|
8
|
+
// Load persisted caches before accepting requests
|
|
9
|
+
loadSessionCache();
|
|
10
|
+
loadExpandStore();
|
|
6
11
|
const PORT = config.port;
|
|
7
12
|
const httpServer = createAdaptorServer({ fetch: app.fetch });
|
|
13
|
+
// Persist caches every 60s so a crash doesn't lose more than a minute of work
|
|
14
|
+
setInterval(() => { persistSessionCache(); persistExpandStore(); }, 60_000).unref();
|
|
8
15
|
httpServer.listen(PORT, () => {
|
|
9
16
|
console.log(`Squeezr v${VERSION} listening on http://localhost:${PORT}`);
|
|
10
17
|
console.log(`Mode: ${config.dryRun ? 'dry-run' : 'active'}`);
|
|
@@ -16,15 +23,20 @@ httpServer.listen(PORT, () => {
|
|
|
16
23
|
// Start MITM proxy for Codex OAuth (chatgpt.com/backend-api)
|
|
17
24
|
startMitmProxy();
|
|
18
25
|
const isDaemon = !!process.env.SQUEEZR_DAEMON;
|
|
26
|
+
function persistAndExit(code = 0) {
|
|
27
|
+
persistSessionCache();
|
|
28
|
+
persistExpandStore();
|
|
29
|
+
process.exit(code);
|
|
30
|
+
}
|
|
19
31
|
if (isDaemon) {
|
|
20
|
-
process.on('SIGINT', () => { });
|
|
21
|
-
process.on('SIGHUP', () => { });
|
|
32
|
+
process.on('SIGINT', () => { persistAndExit(0); });
|
|
33
|
+
process.on('SIGHUP', () => { persistAndExit(0); });
|
|
22
34
|
}
|
|
23
35
|
else {
|
|
24
36
|
process.on('SIGINT', () => {
|
|
25
37
|
const s = stats.summary();
|
|
26
38
|
console.log(`\n[squeezr] Session summary: ${s.requests} requests | -${s.total_saved_chars.toLocaleString()} chars (~${s.total_saved_tokens.toLocaleString()} tokens, ${s.savings_pct}% saved)`);
|
|
27
|
-
|
|
39
|
+
persistAndExit(0);
|
|
28
40
|
});
|
|
29
41
|
}
|
|
30
|
-
process.on('SIGTERM', () =>
|
|
42
|
+
process.on('SIGTERM', () => { persistAndExit(0); });
|
package/dist/server.js
CHANGED
|
@@ -62,6 +62,23 @@ async function proxyStream(upstream, body, headers, params) {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
export const app = new Hono();
|
|
65
|
+
// ── CORS middleware (required for Cursor IDE and browser-based tools) ─────────
|
|
66
|
+
// Cursor's Electron renderer sends OPTIONS preflight before every POST.
|
|
67
|
+
// Without this the request is blocked and Cursor shows a network error.
|
|
68
|
+
app.use('*', async (c, next) => {
|
|
69
|
+
if (c.req.method === 'OPTIONS') {
|
|
70
|
+
return c.body(null, 204, {
|
|
71
|
+
'Access-Control-Allow-Origin': '*',
|
|
72
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
73
|
+
'Access-Control-Allow-Headers': '*',
|
|
74
|
+
'Access-Control-Max-Age': '86400',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
await next();
|
|
78
|
+
c.res.headers.set('Access-Control-Allow-Origin', '*');
|
|
79
|
+
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
80
|
+
c.res.headers.set('Access-Control-Allow-Headers', '*');
|
|
81
|
+
});
|
|
65
82
|
// ── Anthropic / Claude Code ───────────────────────────────────────────────────
|
|
66
83
|
app.post('/v1/messages', async (c) => {
|
|
67
84
|
const body = await c.req.json();
|
|
@@ -71,13 +88,27 @@ app.post('/v1/messages', async (c) => {
|
|
|
71
88
|
?? c.req.header('authorization')?.replace(/^bearer\s+/i, '').trim()
|
|
72
89
|
?? process.env.ANTHROPIC_API_KEY
|
|
73
90
|
?? '';
|
|
74
|
-
// System prompt compression
|
|
75
|
-
if (config.compressSystemPrompt && !config.dryRun
|
|
76
|
-
|
|
91
|
+
// System prompt compression (handles both string and array formats — Claude Code sends array)
|
|
92
|
+
if (config.compressSystemPrompt && !config.dryRun) {
|
|
93
|
+
if (typeof body.system === 'string') {
|
|
94
|
+
body.system = await compressSystemPrompt(body.system, apiKey, 'haiku');
|
|
95
|
+
}
|
|
96
|
+
else if (Array.isArray(body.system)) {
|
|
97
|
+
for (const block of body.system) {
|
|
98
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
99
|
+
block.text = await compressSystemPrompt(block.text, apiKey, 'haiku');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
77
103
|
}
|
|
78
104
|
const messages = (body.messages ?? []);
|
|
79
105
|
const originalChars = estimateChars(messages);
|
|
80
|
-
const
|
|
106
|
+
const systemExtraChars = typeof body.system === 'string'
|
|
107
|
+
? body.system.length
|
|
108
|
+
: Array.isArray(body.system)
|
|
109
|
+
? body.system.reduce((s, b) => s + (b.text?.length ?? 0), 0)
|
|
110
|
+
: 0;
|
|
111
|
+
const [compressedMsgs, savings] = await compressAnthropicMessages(messages, apiKey, config, systemExtraChars);
|
|
81
112
|
body.messages = compressedMsgs;
|
|
82
113
|
// Inject expand tool
|
|
83
114
|
injectExpandToolAnthropic(body);
|
package/dist/sessionCache.d.ts
CHANGED
|
@@ -28,3 +28,5 @@ export declare function getBlock(hash: string): SessionBlock | undefined;
|
|
|
28
28
|
export declare function setBlock(hash: string, block: SessionBlock): void;
|
|
29
29
|
export declare function sessionCacheSize(): number;
|
|
30
30
|
export declare function clearSessionCache(): void;
|
|
31
|
+
export declare function loadSessionCache(): void;
|
|
32
|
+
export declare function persistSessionCache(): void;
|
package/dist/sessionCache.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
const SESSION_CACHE_PATH = join(homedir(), '.squeezr', 'session_cache.json');
|
|
2
6
|
const cache = new Map();
|
|
3
7
|
export function hashText(text) {
|
|
4
8
|
return createHash('md5').update(text).digest('hex');
|
|
@@ -15,3 +19,25 @@ export function sessionCacheSize() {
|
|
|
15
19
|
export function clearSessionCache() {
|
|
16
20
|
cache.clear();
|
|
17
21
|
}
|
|
22
|
+
export function loadSessionCache() {
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(SESSION_CACHE_PATH)) {
|
|
25
|
+
const raw = JSON.parse(readFileSync(SESSION_CACHE_PATH, 'utf-8'));
|
|
26
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
27
|
+
cache.set(k, v);
|
|
28
|
+
}
|
|
29
|
+
if (cache.size > 0)
|
|
30
|
+
console.log(`[squeezr] Loaded ${cache.size} session cache entries from disk`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
export function persistSessionCache() {
|
|
36
|
+
try {
|
|
37
|
+
const dir = join(homedir(), '.squeezr');
|
|
38
|
+
if (!existsSync(dir))
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
writeFileSync(SESSION_CACHE_PATH, JSON.stringify(Object.fromEntries(cache)));
|
|
41
|
+
}
|
|
42
|
+
catch { /* ignore */ }
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,61 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "squeezr-ai",
|
|
3
|
-
"version": "1.17.
|
|
4
|
-
"description": "AI proxy that compresses Claude Code, Codex, Aider, Gemini CLI and Ollama context windows to save thousands of tokens per session",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"claude",
|
|
7
|
-
"claude-code",
|
|
8
|
-
"codex",
|
|
9
|
-
"ollama",
|
|
10
|
-
"aider",
|
|
11
|
-
"gemini",
|
|
12
|
-
"token",
|
|
13
|
-
"compression",
|
|
14
|
-
"proxy",
|
|
15
|
-
"llm",
|
|
16
|
-
"ai"
|
|
17
|
-
],
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/sergioramosv/Squeezr.git"
|
|
22
|
-
},
|
|
23
|
-
"homepage": "https://github.com/sergioramosv/Squeezr#readme",
|
|
24
|
-
"type": "module",
|
|
25
|
-
"bin": {
|
|
26
|
-
"squeezr": "bin/squeezr.js"
|
|
27
|
-
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsc",
|
|
30
|
-
"dev": "tsx src/index.ts",
|
|
31
|
-
"start": "node dist/index.js",
|
|
32
|
-
"gain": "node dist/gain.js",
|
|
33
|
-
"discover": "node dist/discover.js",
|
|
34
|
-
"test": "vitest run",
|
|
35
|
-
"test:watch": "vitest"
|
|
36
|
-
},
|
|
37
|
-
"files": [
|
|
38
|
-
"bin/",
|
|
39
|
-
"dist/",
|
|
40
|
-
"squeezr.toml"
|
|
41
|
-
],
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@anthropic-ai/sdk": "^0.39.0",
|
|
44
|
-
"@
|
|
45
|
-
"hono": "^
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"@types/node
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "squeezr-ai",
|
|
3
|
+
"version": "1.17.4",
|
|
4
|
+
"description": "AI proxy that compresses Claude Code, Codex, Aider, Gemini CLI and Ollama context windows to save thousands of tokens per session",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex",
|
|
9
|
+
"ollama",
|
|
10
|
+
"aider",
|
|
11
|
+
"gemini",
|
|
12
|
+
"token",
|
|
13
|
+
"compression",
|
|
14
|
+
"proxy",
|
|
15
|
+
"llm",
|
|
16
|
+
"ai"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/sergioramosv/Squeezr.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/sergioramosv/Squeezr#readme",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"squeezr": "bin/squeezr.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsx src/index.ts",
|
|
31
|
+
"start": "node dist/index.js",
|
|
32
|
+
"gain": "node dist/gain.js",
|
|
33
|
+
"discover": "node dist/discover.js",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"bin/",
|
|
39
|
+
"dist/",
|
|
40
|
+
"squeezr.toml"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
44
|
+
"@bufbuild/protobuf": "^2.11.0",
|
|
45
|
+
"@hono/node-server": "^1.13.7",
|
|
46
|
+
"hono": "^4.7.5",
|
|
47
|
+
"node-forge": "^1.4.0",
|
|
48
|
+
"openai": "^4.93.0",
|
|
49
|
+
"smol-toml": "^1.3.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.14.0",
|
|
53
|
+
"@types/node-forge": "^1.3.14",
|
|
54
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
55
|
+
"tsx": "^4.19.3",
|
|
56
|
+
"typescript": "^5.8.3",
|
|
57
|
+
"vitest": "^3.1.1"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18"
|
|
61
|
+
}
|
|
62
|
+
}
|