squeezr-ai 1.17.4 → 1.17.5

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/bin/squeezr.js CHANGED
@@ -201,6 +201,8 @@ Usage:
201
201
  squeezr config Print config file path and current settings
202
202
  squeezr ports Change HTTP and MITM proxy ports
203
203
  squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
204
+ squeezr cursor Start Cursor subscription MITM proxy (no BYOK needed)
205
+ squeezr cursor stop Stop Cursor proxy and clean up system proxy settings
204
206
  squeezr update Kill old processes, install latest from npm, restart
205
207
  squeezr uninstall Remove Squeezr completely (env vars, CA, auto-start, logs)
206
208
  squeezr version Print version
@@ -1254,6 +1256,162 @@ async function startTunnel() {
1254
1256
  process.on('SIGTERM', () => { child.kill(); process.exit(0) })
1255
1257
  }
1256
1258
 
1259
+ // ── squeezr cursor ───────────────────────────────────────────────────────────
1260
+
1261
+ async function startCursorProxy() {
1262
+ const port = getPort()
1263
+ const mitmPort = getMitmPort(port)
1264
+
1265
+ // Verify main proxy is running first
1266
+ const running = await new Promise(resolve => {
1267
+ const req = http.get(`http://localhost:${port}/squeezr/health`, res => {
1268
+ resolve(res.statusCode === 200)
1269
+ })
1270
+ req.on('error', () => resolve(false))
1271
+ req.setTimeout(2000, () => { req.destroy(); resolve(false) })
1272
+ })
1273
+
1274
+ if (!running) {
1275
+ console.error(`Squeezr proxy is not running on port ${port}.`)
1276
+ console.error('Start it first: squeezr start')
1277
+ process.exit(1)
1278
+ }
1279
+
1280
+ // Verify CA exists
1281
+ const caDir = path.join(os.homedir(), '.squeezr', 'mitm-ca')
1282
+ const caCertPath = path.join(caDir, 'ca.crt')
1283
+ if (!fs.existsSync(caCertPath)) {
1284
+ console.error('MITM CA certificate not found. Run `squeezr setup` first.')
1285
+ process.exit(1)
1286
+ }
1287
+
1288
+ console.log('Starting Cursor MITM proxy...')
1289
+
1290
+ const distPath = path.join(ROOT, 'dist', 'cursorMitm.js')
1291
+ if (!fs.existsSync(distPath)) {
1292
+ console.error(`Error: ${distPath} not found. Run 'npm run build' first.`)
1293
+ process.exit(1)
1294
+ }
1295
+
1296
+ const distUrl = process.platform === 'win32' ? 'file:///' + distPath.replace(/\\/g, '/') : distPath
1297
+ const { startCursorMitm, getCursorMitmPort, getCursorStats } = await import(distUrl)
1298
+
1299
+ try {
1300
+ await startCursorMitm()
1301
+ } catch (err) {
1302
+ console.error('Failed to start Cursor proxy:', err.message)
1303
+ process.exit(1)
1304
+ }
1305
+
1306
+ const actualPort = getCursorMitmPort()
1307
+ const proxyConfigured = configureSystemProxy(actualPort)
1308
+
1309
+ console.log('')
1310
+ console.log('╔══════════════════════════════════════════════════════════════════╗')
1311
+ console.log(`║ Cursor MITM proxy active on port ${actualPort} ║`)
1312
+ console.log('╠══════════════════════════════════════════════════════════════════╣')
1313
+ console.log('║ ║')
1314
+ console.log('║ Intercepting: api2.cursor.sh (chat, agent, composer) ║')
1315
+ console.log('║ Compressing: conversation context via cursor-small ║')
1316
+ console.log('║ Everything else: transparent pass-through ║')
1317
+ console.log('║ ║')
1318
+ if (proxyConfigured) {
1319
+ console.log('║ System proxy configured automatically. ║')
1320
+ console.log('║ Cursor will route through Squeezr on next request. ║')
1321
+ } else {
1322
+ console.log('║ ⚠ Could not set system proxy automatically. ║')
1323
+ console.log('║ Set it manually: ║')
1324
+ if (process.platform === 'win32') {
1325
+ console.log(`║ Settings > Network > Proxy > Manual: 127.0.0.1:${actualPort} ║`)
1326
+ } else if (process.platform === 'darwin') {
1327
+ console.log(`║ networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 ${actualPort} ║`)
1328
+ } else {
1329
+ console.log(`║ export HTTPS_PROXY=http://127.0.0.1:${actualPort} ║`)
1330
+ }
1331
+ }
1332
+ console.log('║ ║')
1333
+ console.log('║ Press Ctrl+C to stop and clean up proxy settings ║')
1334
+ console.log('╚══════════════════════════════════════════════════════════════════╝')
1335
+ console.log('')
1336
+
1337
+ const statsInterval = setInterval(() => {
1338
+ const s = getCursorStats()
1339
+ if (s.requests > 0) {
1340
+ console.log(`[squeezr/cursor] Stats: ${s.requests} requests, ${s.compressed} compressed, -${s.charsSaved.toLocaleString()} chars saved`)
1341
+ }
1342
+ }, 30_000)
1343
+
1344
+ const cleanup = () => {
1345
+ clearInterval(statsInterval)
1346
+ console.log('\nStopping Cursor proxy...')
1347
+ cleanSystemProxy()
1348
+ const s = getCursorStats()
1349
+ console.log(`Session: ${s.requests} requests, ${s.compressed} compressed, -${s.charsSaved.toLocaleString()} chars saved`)
1350
+ process.exit(0)
1351
+ }
1352
+
1353
+ process.on('SIGINT', cleanup)
1354
+ process.on('SIGTERM', cleanup)
1355
+ }
1356
+
1357
+ function configureSystemProxy(port) {
1358
+ try {
1359
+ if (process.platform === 'win32') {
1360
+ execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f`, { stdio: 'pipe' })
1361
+ execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer /t REG_SZ /d "127.0.0.1:${port}" /f`, { stdio: 'pipe' })
1362
+ execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyOverride /t REG_SZ /d "<local>;localhost;127.0.0.1" /f`, { stdio: 'pipe' })
1363
+ return true
1364
+ } else if (process.platform === 'darwin') {
1365
+ try {
1366
+ const services = execSync('networksetup -listallnetworkservices', { encoding: 'utf-8' })
1367
+ .split('\n')
1368
+ .filter(s => s.trim() && !s.startsWith('*') && !s.startsWith('An asterisk'))
1369
+ for (const svc of services) {
1370
+ try {
1371
+ execSync(`networksetup -setsecurewebproxy "${svc.trim()}" 127.0.0.1 ${port}`, { stdio: 'pipe' })
1372
+ } catch {}
1373
+ }
1374
+ return true
1375
+ } catch { return false }
1376
+ } else {
1377
+ try {
1378
+ execSync(`gsettings set org.gnome.system.proxy mode 'manual'`, { stdio: 'pipe' })
1379
+ execSync(`gsettings set org.gnome.system.proxy.https host '127.0.0.1'`, { stdio: 'pipe' })
1380
+ execSync(`gsettings set org.gnome.system.proxy.https port ${port}`, { stdio: 'pipe' })
1381
+ return true
1382
+ } catch { return false }
1383
+ }
1384
+ } catch {
1385
+ return false
1386
+ }
1387
+ }
1388
+
1389
+ function cleanSystemProxy() {
1390
+ try {
1391
+ if (process.platform === 'win32') {
1392
+ execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f`, { stdio: 'pipe' })
1393
+ } else if (process.platform === 'darwin') {
1394
+ try {
1395
+ const services = execSync('networksetup -listallnetworkservices', { encoding: 'utf-8' })
1396
+ .split('\n')
1397
+ .filter(s => s.trim() && !s.startsWith('*') && !s.startsWith('An asterisk'))
1398
+ for (const svc of services) {
1399
+ try {
1400
+ execSync(`networksetup -setsecurewebproxystate "${svc.trim()}" off`, { stdio: 'pipe' })
1401
+ } catch {}
1402
+ }
1403
+ } catch {}
1404
+ } else {
1405
+ try {
1406
+ execSync(`gsettings set org.gnome.system.proxy mode 'none'`, { stdio: 'pipe' })
1407
+ } catch {}
1408
+ }
1409
+ console.log('System proxy settings cleaned up.')
1410
+ } catch {
1411
+ console.warn('Could not clean system proxy settings. You may need to disable manually.')
1412
+ }
1413
+ }
1414
+
1257
1415
  // ── CLI router ────────────────────────────────────────────────────────────────
1258
1416
 
1259
1417
  switch (command) {
@@ -1371,6 +1529,14 @@ switch (command) {
1371
1529
  await startTunnel()
1372
1530
  break
1373
1531
 
1532
+ case 'cursor':
1533
+ if (args[1] === 'stop') {
1534
+ cleanSystemProxy()
1535
+ } else {
1536
+ await startCursorProxy()
1537
+ }
1538
+ break
1539
+
1374
1540
  case 'uninstall':
1375
1541
  await uninstall()
1376
1542
  break
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,313 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ // We test the exported utility functions and the ConnectRPC/proto layer.
3
+ // Since cursorMitm.ts has many private functions, we use a test helper approach
4
+ // by importing the module and testing the public API + running integration checks.
5
+ // For testing the proto encoder/decoder, we replicate the minimal logic here
6
+ // since the functions are module-private. We'll also test the full flow.
7
+ // ── ConnectRPC frame tests ───────────────────────────────────────────────────
8
+ function buildConnectFrame(payload, flag = 0) {
9
+ const header = Buffer.alloc(5);
10
+ header[0] = flag;
11
+ header.writeUInt32BE(payload.length, 1);
12
+ return Buffer.concat([header, payload]);
13
+ }
14
+ function parseConnectFrame(buf) {
15
+ if (buf.length < 5)
16
+ return null;
17
+ const flag = buf[0];
18
+ const length = buf.readUInt32BE(1);
19
+ if (buf.length < 5 + length)
20
+ return null;
21
+ return { flag, payload: buf.subarray(5, 5 + length), total: 5 + length };
22
+ }
23
+ describe('ConnectRPC frame encoding', () => {
24
+ it('should round-trip a simple payload', () => {
25
+ const payload = Buffer.from('hello world');
26
+ const frame = buildConnectFrame(payload);
27
+ expect(frame.length).toBe(5 + payload.length);
28
+ expect(frame[0]).toBe(0); // uncompressed flag
29
+ const parsed = parseConnectFrame(frame);
30
+ expect(parsed).not.toBeNull();
31
+ expect(parsed.flag).toBe(0);
32
+ expect(parsed.payload.toString()).toBe('hello world');
33
+ expect(parsed.total).toBe(frame.length);
34
+ });
35
+ it('should handle gzip flag', () => {
36
+ const payload = Buffer.from('compressed data');
37
+ const frame = buildConnectFrame(payload, 1);
38
+ const parsed = parseConnectFrame(frame);
39
+ expect(parsed.flag).toBe(1);
40
+ expect(parsed.payload.toString()).toBe('compressed data');
41
+ });
42
+ it('should return null for incomplete frames', () => {
43
+ expect(parseConnectFrame(Buffer.alloc(3))).toBeNull();
44
+ // Header says 100 bytes but only 10 present
45
+ const incomplete = Buffer.alloc(15);
46
+ incomplete.writeUInt32BE(100, 1);
47
+ expect(parseConnectFrame(incomplete)).toBeNull();
48
+ });
49
+ it('should handle empty payload', () => {
50
+ const frame = buildConnectFrame(Buffer.alloc(0));
51
+ expect(frame.length).toBe(5);
52
+ const parsed = parseConnectFrame(frame);
53
+ expect(parsed.payload.length).toBe(0);
54
+ });
55
+ it('should handle large payloads', () => {
56
+ const payload = Buffer.alloc(65536, 0x42);
57
+ const frame = buildConnectFrame(payload);
58
+ const parsed = parseConnectFrame(frame);
59
+ expect(parsed.payload.length).toBe(65536);
60
+ expect(parsed.payload[0]).toBe(0x42);
61
+ });
62
+ });
63
+ // ── Protobuf varint tests ────────────────────────────────────────────────────
64
+ function encodeVarint(value) {
65
+ const bytes = [];
66
+ while (value > 0x7F) {
67
+ bytes.push((value & 0x7F) | 0x80);
68
+ value >>>= 7;
69
+ }
70
+ bytes.push(value & 0x7F);
71
+ return Buffer.from(bytes);
72
+ }
73
+ function decodeVarint(buf, offset) {
74
+ let value = 0;
75
+ let shift = 0;
76
+ let bytesRead = 0;
77
+ while (offset + bytesRead < buf.length) {
78
+ const byte = buf[offset + bytesRead];
79
+ value |= (byte & 0x7F) << shift;
80
+ bytesRead++;
81
+ if ((byte & 0x80) === 0)
82
+ break;
83
+ shift += 7;
84
+ }
85
+ return { value, bytesRead };
86
+ }
87
+ describe('Protobuf varint encoding', () => {
88
+ it('should encode small numbers', () => {
89
+ expect(encodeVarint(0)).toEqual(Buffer.from([0]));
90
+ expect(encodeVarint(1)).toEqual(Buffer.from([1]));
91
+ expect(encodeVarint(127)).toEqual(Buffer.from([127]));
92
+ });
93
+ it('should encode multi-byte varints', () => {
94
+ expect(encodeVarint(128)).toEqual(Buffer.from([0x80, 0x01]));
95
+ expect(encodeVarint(300)).toEqual(Buffer.from([0xAC, 0x02]));
96
+ });
97
+ it('should round-trip varints', () => {
98
+ for (const val of [0, 1, 127, 128, 300, 16383, 16384, 65535]) {
99
+ const buf = encodeVarint(val);
100
+ const { value } = decodeVarint(buf, 0);
101
+ expect(value).toBe(val);
102
+ }
103
+ });
104
+ });
105
+ // ── Protobuf message building/parsing ────────────────────────────────────────
106
+ function encodeTag(fieldNumber, wireType) {
107
+ return encodeVarint((fieldNumber << 3) | wireType);
108
+ }
109
+ function encodeLengthDelimited(fieldNumber, data) {
110
+ const tag = encodeTag(fieldNumber, 2);
111
+ const len = encodeVarint(data.length);
112
+ return Buffer.concat([tag, len, data]);
113
+ }
114
+ function encodeString(fieldNumber, str) {
115
+ return encodeLengthDelimited(fieldNumber, Buffer.from(str, 'utf-8'));
116
+ }
117
+ function encodeVarintField(fieldNumber, value) {
118
+ const tag = encodeTag(fieldNumber, 0);
119
+ const val = encodeVarint(value);
120
+ return Buffer.concat([tag, val]);
121
+ }
122
+ function parseProtoFields(buf) {
123
+ const fields = [];
124
+ let offset = 0;
125
+ while (offset < buf.length) {
126
+ const tagStart = offset;
127
+ const { value: tag, bytesRead: tagBytes } = decodeVarint(buf, offset);
128
+ offset += tagBytes;
129
+ const fieldNumber = tag >>> 3;
130
+ const wireType = tag & 0x07;
131
+ let fieldEnd = offset;
132
+ switch (wireType) {
133
+ case 0: {
134
+ while (fieldEnd < buf.length && (buf[fieldEnd] & 0x80) !== 0)
135
+ fieldEnd++;
136
+ fieldEnd++;
137
+ break;
138
+ }
139
+ case 1: {
140
+ fieldEnd += 8;
141
+ break;
142
+ }
143
+ case 2: {
144
+ const { value: len, bytesRead: lenBytes } = decodeVarint(buf, offset);
145
+ fieldEnd = offset + lenBytes + len;
146
+ break;
147
+ }
148
+ case 5: {
149
+ fieldEnd += 4;
150
+ break;
151
+ }
152
+ default: fieldEnd = buf.length;
153
+ }
154
+ fields.push({ fieldNumber, wireType, data: buf.subarray(tagStart, fieldEnd) });
155
+ offset = fieldEnd;
156
+ }
157
+ return fields;
158
+ }
159
+ function extractPayload(rawField) {
160
+ let offset = 0;
161
+ while (offset < rawField.length && (rawField[offset] & 0x80) !== 0)
162
+ offset++;
163
+ offset++;
164
+ const { value: len, bytesRead } = decodeVarint(rawField, offset);
165
+ offset += bytesRead;
166
+ return rawField.subarray(offset, offset + len);
167
+ }
168
+ describe('Protobuf field encoding/parsing', () => {
169
+ it('should encode and parse a string field', () => {
170
+ const encoded = encodeString(2, 'Hello, world!');
171
+ const fields = parseProtoFields(encoded);
172
+ expect(fields.length).toBe(1);
173
+ expect(fields[0].fieldNumber).toBe(2);
174
+ expect(fields[0].wireType).toBe(2);
175
+ const payload = extractPayload(fields[0].data);
176
+ expect(payload.toString('utf-8')).toBe('Hello, world!');
177
+ });
178
+ it('should encode and parse a varint field', () => {
179
+ const encoded = encodeVarintField(1, 42);
180
+ const fields = parseProtoFields(encoded);
181
+ expect(fields.length).toBe(1);
182
+ expect(fields[0].fieldNumber).toBe(1);
183
+ expect(fields[0].wireType).toBe(0);
184
+ });
185
+ it('should encode and parse a ConversationMessage-like structure', () => {
186
+ // ConversationMessage: field 1 = role (varint), field 2 = text (string)
187
+ const msgPayload = Buffer.concat([
188
+ encodeVarintField(1, 1), // role = HUMAN
189
+ encodeString(2, 'What is 2+2?'),
190
+ ]);
191
+ const conversationField = encodeLengthDelimited(2, msgPayload);
192
+ const outerFields = parseProtoFields(conversationField);
193
+ expect(outerFields.length).toBe(1);
194
+ expect(outerFields[0].fieldNumber).toBe(2);
195
+ const innerPayload = extractPayload(outerFields[0].data);
196
+ const innerFields = parseProtoFields(innerPayload);
197
+ expect(innerFields.length).toBe(2);
198
+ expect(innerFields[0].fieldNumber).toBe(1); // role
199
+ expect(innerFields[1].fieldNumber).toBe(2); // text
200
+ const text = extractPayload(innerFields[1].data).toString('utf-8');
201
+ expect(text).toBe('What is 2+2?');
202
+ });
203
+ it('should handle a full GetChatRequest with multiple conversation messages', () => {
204
+ // Build a fake GetChatRequest:
205
+ // field 2 (conversation) repeated, field 5 (workspace_root_path)
206
+ const msg1 = encodeLengthDelimited(2, Buffer.concat([
207
+ encodeVarintField(1, 1),
208
+ encodeString(2, 'Please review my code'),
209
+ ]));
210
+ const msg2 = encodeLengthDelimited(2, Buffer.concat([
211
+ encodeVarintField(1, 2),
212
+ encodeString(2, 'Sure, I see several issues with your implementation...'),
213
+ ]));
214
+ const msg3 = encodeLengthDelimited(2, Buffer.concat([
215
+ encodeVarintField(1, 1),
216
+ encodeString(2, 'Can you fix them?'),
217
+ ]));
218
+ const workspacePath = encodeString(5, '/home/user/project');
219
+ const request = Buffer.concat([msg1, msg2, msg3, workspacePath]);
220
+ const fields = parseProtoFields(request);
221
+ // Should have 4 fields: 3 conversation + 1 workspace
222
+ expect(fields.length).toBe(4);
223
+ const conversationFields = fields.filter(f => f.fieldNumber === 2);
224
+ expect(conversationFields.length).toBe(3);
225
+ const otherFields = fields.filter(f => f.fieldNumber !== 2);
226
+ expect(otherFields.length).toBe(1);
227
+ expect(otherFields[0].fieldNumber).toBe(5);
228
+ });
229
+ it('should preserve round-trip of unknown fields', () => {
230
+ // Fields we don't know should survive parse → concat(data) unchanged
231
+ const msg1 = encodeLengthDelimited(2, Buffer.concat([
232
+ encodeVarintField(1, 1),
233
+ encodeString(2, 'hello'),
234
+ ]));
235
+ const unknownField1 = encodeString(7, 'some-request-id');
236
+ const unknownField2 = encodeVarintField(27, 1); // is_composer
237
+ const unknownField3 = encodeString(15, 'conv-id-123');
238
+ const original = Buffer.concat([msg1, unknownField1, unknownField2, unknownField3]);
239
+ const fields = parseProtoFields(original);
240
+ // Reconstructing from raw data should give same bytes
241
+ const reconstructed = Buffer.concat(fields.map(f => f.data));
242
+ expect(reconstructed).toEqual(original);
243
+ });
244
+ });
245
+ // ── Integration: ConnectRPC + Protobuf ───────────────────────────────────────
246
+ describe('ConnectRPC + Protobuf integration', () => {
247
+ it('should encode a full request frame and parse it back', () => {
248
+ const msg1 = encodeLengthDelimited(2, Buffer.concat([
249
+ encodeVarintField(1, 1),
250
+ encodeString(2, 'User question'),
251
+ ]));
252
+ const msg2 = encodeLengthDelimited(2, Buffer.concat([
253
+ encodeVarintField(1, 2),
254
+ encodeString(2, 'AI response with lots of context and details'),
255
+ ]));
256
+ const requestPayload = Buffer.concat([msg1, msg2]);
257
+ const frame = buildConnectFrame(requestPayload);
258
+ // Parse frame
259
+ const parsed = parseConnectFrame(frame);
260
+ expect(parsed).not.toBeNull();
261
+ // Parse proto
262
+ const fields = parseProtoFields(parsed.payload);
263
+ expect(fields.filter(f => f.fieldNumber === 2).length).toBe(2);
264
+ });
265
+ });
266
+ // ── Deterministic compression ────────────────────────────────────────────────
267
+ function deterministicCompress(text) {
268
+ let out = text;
269
+ out = out.replace(/\n{3,}/g, '\n\n');
270
+ out = out.replace(/[ \t]+$/gm, '');
271
+ const lines = out.split('\n');
272
+ const result = [];
273
+ let lastLine = '';
274
+ let repeatCount = 0;
275
+ for (const line of lines) {
276
+ if (line === lastLine && line.trim().length > 0) {
277
+ repeatCount++;
278
+ }
279
+ else {
280
+ if (repeatCount > 0) {
281
+ result.push(` ... (repeated ${repeatCount}x)`);
282
+ repeatCount = 0;
283
+ }
284
+ result.push(line);
285
+ lastLine = line;
286
+ }
287
+ }
288
+ if (repeatCount > 0)
289
+ result.push(` ... (repeated ${repeatCount}x)`);
290
+ return result.join('\n');
291
+ }
292
+ describe('Deterministic compression', () => {
293
+ it('should collapse blank lines', () => {
294
+ const input = 'a\n\n\n\n\nb';
295
+ expect(deterministicCompress(input)).toBe('a\n\nb');
296
+ });
297
+ it('should strip trailing whitespace', () => {
298
+ const input = 'hello \nworld\t\t';
299
+ expect(deterministicCompress(input)).toBe('hello\nworld');
300
+ });
301
+ it('should collapse repeated lines', () => {
302
+ const input = 'error: connection failed\nerror: connection failed\nerror: connection failed\nok';
303
+ const result = deterministicCompress(input);
304
+ expect(result).toContain('error: connection failed');
305
+ expect(result).toContain('repeated 2x');
306
+ expect(result).toContain('ok');
307
+ });
308
+ it('should not collapse empty repeated lines', () => {
309
+ const input = 'a\n\n\nb';
310
+ const result = deterministicCompress(input);
311
+ expect(result).not.toContain('repeated');
312
+ });
313
+ });
@@ -4,6 +4,7 @@ import { CompressionCache } from './cache.js';
4
4
  import { preprocess, preprocessForTool, hitPattern } from './deterministic.js';
5
5
  import { storeOriginal } from './expand.js';
6
6
  import { hashText, getBlock, setBlock } from './sessionCache.js';
7
+ import { effectiveThreshold, effectiveKeepRecent, aiEnabled } from './config.js';
7
8
  const COMPRESS_PROMPT = 'You are compressing a coding tool output to save tokens. ' +
8
9
  'Extract ONLY what is essential: errors, file paths, function names, ' +
9
10
  'test failures, key values, warnings. ' +
@@ -138,7 +139,7 @@ export async function compressAnthropicMessages(messages, apiKey, config, system
138
139
  if (config.disabled)
139
140
  return [messages, emptySavings()];
140
141
  const pressure = estimatePressure(messages, systemExtraChars);
141
- const threshold = config.thresholdForPressure(pressure);
142
+ const threshold = effectiveThreshold(config, pressure);
142
143
  const { nameMap: toolIdMap, skipIds } = buildAnthropicToolIdMap(messages);
143
144
  const allResults = extractAnthropicToolResults(messages, toolIdMap)
144
145
  .filter(r => !skipIds.has(r.toolUseId) && !config.shouldSkipTool(r.tool));
@@ -199,7 +200,7 @@ export async function compressAnthropicMessages(messages, apiKey, config, system
199
200
  console.log(`[squeezr/det] Deterministic: -${detSaved.toLocaleString()} chars (~${tokens} tokens) across ${allResults.length} block(s)`);
200
201
  }
201
202
  // ── Step 2: AI compression for old blocks above threshold ─────────────────
202
- const candidates = allResults.slice(0, Math.max(0, allResults.length - config.keepRecent));
203
+ const candidates = allResults.slice(0, Math.max(0, allResults.length - effectiveKeepRecent(config)));
203
204
  const toProcess = candidates.filter(c => c.text.length >= threshold && !dedupedSet.has(`${c.index}:${c.subIndex}`));
204
205
  if (toProcess.length === 0)
205
206
  return [msgs, emptySavings()];
@@ -217,7 +218,7 @@ export async function compressAnthropicMessages(messages, apiKey, config, system
217
218
  if (cached) {
218
219
  sessionHits.push({ index: c.index, subIndex: c.subIndex, tool: c.tool, block: cached });
219
220
  }
220
- else if (c.index === lastMsgIdx && !config.aiSkipTools.has(c.tool.toLowerCase())) {
221
+ else if (aiEnabled() && c.index === lastMsgIdx && !config.aiSkipTools.has(c.tool.toLowerCase())) {
221
222
  // Only AI-compress genuinely new blocks (from the last user message).
222
223
  // Historical uncached blocks skip AI compression → prevents burst on first activation.
223
224
  toCompress.push(c);
@@ -284,7 +285,7 @@ export async function compressOpenAIMessages(messages, apiKey, config, isLocal =
284
285
  if (config.disabled)
285
286
  return [messages, emptySavings()];
286
287
  const pressure = estimatePressure(messages);
287
- const threshold = config.thresholdForPressure(pressure);
288
+ const threshold = effectiveThreshold(config, pressure);
288
289
  const allResults = extractOpenAIToolResults(messages)
289
290
  .filter(r => !r.skip && !config.shouldSkipTool(r.tool));
290
291
  if (allResults.length === 0)
@@ -333,7 +334,7 @@ export async function compressOpenAIMessages(messages, apiKey, config, isLocal =
333
334
  console.log(`[squeezr/det/${tag}] Deterministic: -${detSaved.toLocaleString()} chars across ${allResults.length} block(s)`);
334
335
  }
335
336
  // Step 2: AI compression for old blocks above threshold
336
- const candidates = allResults.slice(0, Math.max(0, allResults.length - config.keepRecent));
337
+ const candidates = allResults.slice(0, Math.max(0, allResults.length - effectiveKeepRecent(config)));
337
338
  const toProcess = candidates.filter(c => c.text.length >= threshold && !dedupedIndices.has(c.index));
338
339
  if (toProcess.length === 0)
339
340
  return [msgs, emptySavings()];
@@ -352,7 +353,7 @@ export async function compressOpenAIMessages(messages, apiKey, config, isLocal =
352
353
  if (cached) {
353
354
  sessionHits.push({ index: c.index, tool: c.tool, block: cached });
354
355
  }
355
- else if (c.index > newStartIdx && !config.aiSkipTools.has(c.tool.toLowerCase())) {
356
+ else if (aiEnabled() && c.index > newStartIdx && !config.aiSkipTools.has(c.tool.toLowerCase())) {
356
357
  // Only AI-compress new tool results (after last assistant turn) — prevents burst on first activation.
357
358
  toCompress.push(c);
358
359
  }
@@ -390,7 +391,7 @@ export async function compressGeminiContents(contents, apiKey, config) {
390
391
  if (config.disabled)
391
392
  return [contents, emptySavings()];
392
393
  const pressure = estimatePressure(contents);
393
- const threshold = config.thresholdForPressure(pressure);
394
+ const threshold = effectiveThreshold(config, pressure);
394
395
  const allResults = [];
395
396
  for (let i = 0; i < contents.length; i++) {
396
397
  if (contents[i].role !== 'user')
@@ -453,7 +454,7 @@ export async function compressGeminiContents(contents, apiKey, config) {
453
454
  if (detSaved > 0)
454
455
  console.log(`[squeezr/det/gemini] Deterministic: -${detSaved.toLocaleString()} chars across ${allResults.length} block(s)`);
455
456
  // Step 2: AI compression for old blocks above threshold
456
- const candidates = allResults.slice(0, Math.max(0, allResults.length - config.keepRecent))
457
+ const candidates = allResults.slice(0, Math.max(0, allResults.length - effectiveKeepRecent(config)))
457
458
  .filter(c => c.text.length >= threshold && !geminiDedupedSet.has(`${c.index}:${c.subIndex}`));
458
459
  if (candidates.length === 0)
459
460
  return [cts, emptySavings()];
@@ -467,7 +468,7 @@ export async function compressGeminiContents(contents, apiKey, config) {
467
468
  const cached = getBlock(hashText(c.text));
468
469
  if (cached)
469
470
  sessionHits.push({ index: c.index, subIndex: c.subIndex, tool: c.tool, block: cached });
470
- else
471
+ else if (aiEnabled())
471
472
  toCompress.push(c);
472
473
  }
473
474
  const freshlyCompressed = toCompress.length > 0
package/dist/config.d.ts CHANGED
@@ -26,4 +26,19 @@ export declare class Config {
26
26
  shouldSkipTool(toolName: string): boolean;
27
27
  isLocalKey(key: string): boolean;
28
28
  }
29
+ export type CompressionMode = 'soft' | 'normal' | 'aggressive' | 'critical';
30
+ export interface RuntimeOverrides {
31
+ mode: CompressionMode;
32
+ threshold?: number;
33
+ keepRecent?: number;
34
+ aiEnabled?: boolean;
35
+ }
36
+ export declare const runtimeOverrides: RuntimeOverrides;
37
+ export declare function applyMode(mode: CompressionMode): void;
38
+ /** Effective threshold — runtime override wins over TOML adaptive threshold */
39
+ export declare function effectiveThreshold(config: Config, pressure: number): number;
40
+ /** Effective keepRecent — runtime override wins */
41
+ export declare function effectiveKeepRecent(config: Config): number;
42
+ /** Whether AI compression is enabled right now */
43
+ export declare function aiEnabled(): boolean;
29
44
  export declare const config: Config;
package/dist/config.js CHANGED
@@ -117,4 +117,30 @@ export class Config {
117
117
  return this.localDummyKeys.has(k) || (k.length > 0 && !k.startsWith('sk-') && !k.startsWith('aiza') && !k.startsWith('eyj'));
118
118
  }
119
119
  }
120
+ const MODES = {
121
+ soft: { threshold: 3000, keepRecent: 10, aiEnabled: false },
122
+ normal: { threshold: 800, keepRecent: 3, aiEnabled: true },
123
+ aggressive: { threshold: 200, keepRecent: 1, aiEnabled: true },
124
+ critical: { threshold: 50, keepRecent: 0, aiEnabled: true },
125
+ };
126
+ export const runtimeOverrides = { mode: 'normal' };
127
+ export function applyMode(mode) {
128
+ const preset = MODES[mode];
129
+ Object.assign(runtimeOverrides, { mode, ...preset });
130
+ console.log(`[squeezr] Mode → ${mode} (threshold=${preset.threshold}, keepRecent=${preset.keepRecent}, ai=${preset.aiEnabled})`);
131
+ }
132
+ /** Effective threshold — runtime override wins over TOML adaptive threshold */
133
+ export function effectiveThreshold(config, pressure) {
134
+ if (runtimeOverrides.threshold !== undefined)
135
+ return runtimeOverrides.threshold;
136
+ return config.thresholdForPressure(pressure);
137
+ }
138
+ /** Effective keepRecent — runtime override wins */
139
+ export function effectiveKeepRecent(config) {
140
+ return runtimeOverrides.keepRecent ?? config.keepRecent;
141
+ }
142
+ /** Whether AI compression is enabled right now */
143
+ export function aiEnabled() {
144
+ return runtimeOverrides.aiEnabled ?? true;
145
+ }
120
146
  export const config = new Config();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Cursor Subscription MITM Proxy
3
+ *
4
+ * Intercepts Cursor IDE (Electron/Chromium) traffic to api2.cursor.sh,
5
+ * compresses the conversation context using Cursor's own models (cursor-small),
6
+ * and forwards the compressed request transparently.
7
+ *
8
+ * Protocol: ConnectRPC over HTTP/2 with binary Protobuf
9
+ * Target: api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools
10
+ */
11
+ export declare function getCursorStats(): {
12
+ requests: number;
13
+ compressed: number;
14
+ charsSaved: number;
15
+ };
16
+ export declare function getCursorMitmPort(): number;
17
+ export declare function startCursorMitm(): Promise<void>;
18
+ export declare function stopCursorMitm(): void;