squeezr-ai 1.17.0 → 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 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
- [![npm](https://img.shields.io/npm/v/squeezr-ai)](https://www.npmjs.com/package/squeezr-ai) [![license](https://img.shields.io/npm/l/squeezr-ai)](LICENSE) [![tests](https://img.shields.io/badge/tests-219%20passing-brightgreen)]()
5
+ [![npm](https://img.shields.io/npm/v/squeezr-ai)](https://www.npmjs.com/package/squeezr-ai) [![license](https://img.shields.io/npm/l/squeezr-ai)](LICENSE) [![tests](https://img.shields.io/badge/tests-237%20passing-brightgreen)]()
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"] # never compress these tools
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
@@ -100,7 +100,6 @@ function installShellWrapper() {
100
100
 
101
101
  function installBashWrapper() {
102
102
  const port = getPort()
103
- const mitmPort = getMitmPort(port)
104
103
  const bundlePath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'bundle.crt')
105
104
  const marker = '# squeezr shell wrapper'
106
105
  const endMarker = '# end squeezr shell wrapper'
@@ -111,11 +110,7 @@ squeezr() {
111
110
  start|setup|update)
112
111
  export ANTHROPIC_BASE_URL=http://localhost:${port}
113
112
  export GEMINI_API_BASE_URL=http://localhost:${port}
114
- export HTTPS_PROXY=http://localhost:${mitmPort}
115
- export SSL_CERT_FILE=${bundlePath}
116
- ;;
117
- stop)
118
- unset HTTPS_PROXY
113
+ export NODE_EXTRA_CA_CERTS=${bundlePath}
119
114
  ;;
120
115
  esac
121
116
  }
@@ -205,6 +200,7 @@ Usage:
205
200
  squeezr status Check if proxy is running
206
201
  squeezr config Print config file path and current settings
207
202
  squeezr ports Change HTTP and MITM proxy ports
203
+ squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
208
204
  squeezr update Kill old processes, install latest from npm, restart
209
205
  squeezr uninstall Remove Squeezr completely (env vars, CA, auto-start, logs)
210
206
  squeezr version Print version
@@ -469,7 +465,6 @@ async function configurePorts() {
469
465
  `export SQUEEZR_MITM_PORT=${finalMitm}`,
470
466
  `export ANTHROPIC_BASE_URL=http://localhost:${finalPort}`,
471
467
  `export GEMINI_API_BASE_URL=http://localhost:${finalPort}`,
472
- `export HTTPS_PROXY=http://localhost:${finalMitm}`,
473
468
  ].join('\n')
474
469
  for (const p of profiles) {
475
470
  try {
@@ -494,7 +489,6 @@ async function configurePorts() {
494
489
  try { execSync(`"${setx}" SQUEEZR_MITM_PORT "${finalMitm}"`, { stdio: 'pipe' }) } catch {}
495
490
  try { execSync(`"${setx}" ANTHROPIC_BASE_URL "http://localhost:${finalPort}"`, { stdio: 'pipe' }) } catch {}
496
491
  try { execSync(`"${setx}" GEMINI_API_BASE_URL "http://localhost:${finalPort}"`, { stdio: 'pipe' }) } catch {}
497
- try { execSync(`"${setx}" HTTPS_PROXY "http://localhost:${finalMitm}"`, { stdio: 'pipe' }) } catch {}
498
492
  }
499
493
  } catch {}
500
494
  }
@@ -503,7 +497,6 @@ async function configurePorts() {
503
497
  process.env.SQUEEZR_PORT = String(finalPort)
504
498
  process.env.SQUEEZR_MITM_PORT = String(finalMitm)
505
499
  process.env.ANTHROPIC_BASE_URL = `http://localhost:${finalPort}`
506
- process.env.HTTPS_PROXY = `http://localhost:${finalMitm}`
507
500
 
508
501
  // Auto stop + start
509
502
  console.log('')
@@ -822,9 +815,10 @@ function setupUnix() {
822
815
  `export ANTHROPIC_BASE_URL=http://localhost:${port}`,
823
816
  `export openai_base_url=http://localhost:${port}`,
824
817
  `export GEMINI_API_BASE_URL=http://localhost:${port}`,
825
- `# squeezr MITM proxy for Codex (TLS interception)`,
826
- `export HTTPS_PROXY=http://localhost:${mitmPort}`,
827
- `export SSL_CERT_FILE=${bundlePath}`,
818
+ `export NODE_EXTRA_CA_CERTS=${bundlePath}`,
819
+ `# NOTE: HTTPS_PROXY is intentionally NOT set globally — it would route ALL HTTPS`,
820
+ `# (including Claude Code) through the MITM proxy and cause 502 errors.`,
821
+ `# For Codex, set it per-session only: HTTPS_PROXY=http://localhost:${mitmPort} codex`,
828
822
  `# squeezr auto-heal: start proxy if not running`,
829
823
  `if ! curl -sf http://localhost:${port}/squeezr/health >/dev/null 2>&1; then`,
830
824
  ` nohup ${nodeExe} ${distIndex} >> "${os.homedir()}/.squeezr/squeezr.log" 2>&1 &`,
@@ -840,8 +834,7 @@ function setupUnix() {
840
834
  `export ANTHROPIC_BASE_URL=http://localhost:${port}`,
841
835
  `export openai_base_url=http://localhost:${port}`,
842
836
  `export GEMINI_API_BASE_URL=http://localhost:${port}`,
843
- `export HTTPS_PROXY=http://localhost:${mitmPort}`,
844
- `export SSL_CERT_FILE=${bundlePath}`,
837
+ `export NODE_EXTRA_CA_CERTS=${bundlePath}`,
845
838
  ].join('\n')
846
839
 
847
840
  // Write env vars to ~/.profile (login shell — always loaded)
@@ -905,6 +898,28 @@ function setupUnix() {
905
898
  spawn(nodeExe, [squeezrBin], { detached: true, stdio: 'ignore' }).unref()
906
899
  }
907
900
 
901
+ // Trust MITM CA in macOS Keychain (for Codex TLS interception)
902
+ // CA is generated on first proxy start — wait briefly for it to appear
903
+ const caPath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'ca.crt')
904
+ const waitForCa = (retries = 10, interval = 500) => new Promise(resolve => {
905
+ const check = (n) => {
906
+ if (fs.existsSync(caPath)) return resolve(true)
907
+ if (n <= 0) return resolve(false)
908
+ setTimeout(() => check(n - 1), interval)
909
+ }
910
+ check(retries)
911
+ })
912
+ waitForCa().then(found => {
913
+ if (found) {
914
+ try {
915
+ execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${caPath}" 2>/dev/null`, { stdio: 'pipe' })
916
+ console.log(` [ok] MITM CA trusted in macOS Keychain`)
917
+ } catch {
918
+ console.log(` [info] To trust MITM CA for Codex: security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${caPath}"`)
919
+ }
920
+ }
921
+ })
922
+
908
923
  // 2b. Linux — systemd
909
924
  } else {
910
925
  const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user')
@@ -978,8 +993,9 @@ function setupWSL() {
978
993
  `export ANTHROPIC_BASE_URL=http://localhost:${port}`,
979
994
  `export openai_base_url=http://localhost:${port}`,
980
995
  `export GEMINI_API_BASE_URL=http://localhost:${port}`,
981
- `export HTTPS_PROXY=http://localhost:${mitmPort}`,
982
- `export SSL_CERT_FILE=${bundlePath}`,
996
+ `export NODE_EXTRA_CA_CERTS=${bundlePath}`,
997
+ `# NOTE: HTTPS_PROXY is intentionally NOT set globally — set per-session for Codex only:`,
998
+ `# HTTPS_PROXY=http://localhost:${mitmPort} codex`,
983
999
  `# squeezr auto-heal: start proxy if not running`,
984
1000
  `if ! curl -sf http://localhost:${port}/squeezr/health >/dev/null 2>&1; then`,
985
1001
  ` nohup ${nodeExe} ${distIndex} >> "${os.homedir()}/.squeezr/squeezr.log" 2>&1 &`,
@@ -994,8 +1010,7 @@ function setupWSL() {
994
1010
  `export ANTHROPIC_BASE_URL=http://localhost:${port}`,
995
1011
  `export openai_base_url=http://localhost:${port}`,
996
1012
  `export GEMINI_API_BASE_URL=http://localhost:${port}`,
997
- `export HTTPS_PROXY=http://localhost:${mitmPort}`,
998
- `export SSL_CERT_FILE=${bundlePath}`,
1013
+ `export NODE_EXTRA_CA_CERTS=${bundlePath}`,
999
1014
  ].join('\n')
1000
1015
 
1001
1016
  const profilePath = path.join(os.homedir(), '.profile')
@@ -1136,6 +1151,109 @@ Done!
1136
1151
  installShellWrapper()
1137
1152
  }
1138
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
+
1139
1257
  // ── CLI router ────────────────────────────────────────────────────────────────
1140
1258
 
1141
1259
  switch (command) {
@@ -1248,6 +1366,11 @@ switch (command) {
1248
1366
  case 'ports':
1249
1367
  await configurePorts()
1250
1368
  break
1369
+
1370
+ case 'tunnel':
1371
+ await startTunnel()
1372
+ break
1373
+
1251
1374
  case 'uninstall':
1252
1375
  await uninstall()
1253
1376
  break
@@ -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;
@@ -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
- else
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
- else
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
- process.exit(0);
39
+ persistAndExit(0);
28
40
  });
29
41
  }
30
- process.on('SIGTERM', () => process.exit(0));
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 && typeof body.system === 'string') {
76
- body.system = await compressSystemPrompt(body.system, apiKey, 'haiku');
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 [compressedMsgs, savings] = await compressAnthropicMessages(messages, apiKey, config);
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);
@@ -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;
@@ -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.0",
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
- "@hono/node-server": "^1.13.7",
45
- "hono": "^4.7.5",
46
- "node-forge": "^1.4.0",
47
- "openai": "^4.93.0",
48
- "smol-toml": "^1.3.1"
49
- },
50
- "devDependencies": {
51
- "@types/node": "^22.14.0",
52
- "@types/node-forge": "^1.3.14",
53
- "@vitest/coverage-v8": "^4.1.2",
54
- "tsx": "^4.19.3",
55
- "typescript": "^5.8.3",
56
- "vitest": "^3.1.1"
57
- },
58
- "engines": {
59
- "node": ">=18"
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
+ }