squeezr-ai 1.17.4 → 1.17.6
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 +19 -1
- package/bin/squeezr.js +102 -0
- package/dist/__tests__/cursorMitm.test.d.ts +1 -0
- package/dist/__tests__/cursorMitm.test.js +313 -0
- package/dist/compressor.js +10 -9
- package/dist/config.d.ts +15 -0
- package/dist/config.js +26 -0
- package/dist/cursorMitm.d.ts +18 -0
- package/dist/cursorMitm.js +846 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.js +428 -0
- package/dist/index.js +24 -2
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +380 -0
- package/dist/server.js +32 -3
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ Squeezr uses cheap/free models for AI compression (the deterministic layer is pu
|
|
|
174
174
|
## CLI commands
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
|
-
squeezr setup # configure env vars, auto-start, CA trust
|
|
177
|
+
squeezr setup # configure env vars, auto-start, CA trust, install MCP server
|
|
178
178
|
squeezr start # start the proxy (auto-restarts if version mismatch after update)
|
|
179
179
|
squeezr update # kill old processes, install latest from npm, restart
|
|
180
180
|
squeezr stop # stop the proxy
|
|
@@ -184,10 +184,28 @@ squeezr config # print current config
|
|
|
184
184
|
squeezr ports # change HTTP and MITM proxy ports
|
|
185
185
|
squeezr gain # estimate token savings for a directory
|
|
186
186
|
squeezr discover # detect which AI CLIs are installed
|
|
187
|
+
squeezr mcp install # register MCP server in Claude Code, Cursor, Windsurf, Cline
|
|
188
|
+
squeezr mcp uninstall # remove MCP server registration
|
|
187
189
|
squeezr uninstall # remove Squeezr completely (env vars, CA, auto-start, logs)
|
|
188
190
|
squeezr version # print version
|
|
189
191
|
```
|
|
190
192
|
|
|
193
|
+
## MCP server
|
|
194
|
+
|
|
195
|
+
Squeezr ships with a built-in MCP server (`squeezr-mcp`) that gives any MCP-capable AI CLI real-time awareness of Squeezr's state and control over it.
|
|
196
|
+
|
|
197
|
+
**Installed automatically** by `squeezr setup` into Claude Code, Cursor, Windsurf, and Cline.
|
|
198
|
+
|
|
199
|
+
Available MCP tools:
|
|
200
|
+
|
|
201
|
+
| Tool | Description |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `squeezr_status` | Is proxy running? Version, port, uptime, mode |
|
|
204
|
+
| `squeezr_stats` | Token savings, compression %, cost saved, per-tool breakdown |
|
|
205
|
+
| `squeezr_set_mode` | Change compression mode instantly (soft / normal / aggressive / critical) |
|
|
206
|
+
| `squeezr_config` | Current thresholds, keepRecent, cache sizes |
|
|
207
|
+
| `squeezr_habits` | Detect wasteful patterns this session (duplicate reads, high Bash count, cache efficiency) |
|
|
208
|
+
|
|
191
209
|
## Requirements
|
|
192
210
|
|
|
193
211
|
- Node.js 18+ (compatible with Node.js 24)
|
package/bin/squeezr.js
CHANGED
|
@@ -199,6 +199,8 @@ Usage:
|
|
|
199
199
|
squeezr discover Show pattern coverage report (proxy must be running)
|
|
200
200
|
squeezr status Check if proxy is running
|
|
201
201
|
squeezr config Print config file path and current settings
|
|
202
|
+
squeezr mcp install Register Squeezr MCP server in Claude Code, Cursor, Windsurf & Cline
|
|
203
|
+
squeezr mcp uninstall Remove Squeezr MCP registration
|
|
202
204
|
squeezr ports Change HTTP and MITM proxy ports
|
|
203
205
|
squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
|
|
204
206
|
squeezr update Kill old processes, install latest from npm, restart
|
|
@@ -391,6 +393,100 @@ function showConfig() {
|
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
|
|
396
|
+
|
|
397
|
+
// ── squeezr mcp ───────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
async function mcpInstall() {
|
|
400
|
+
const mcpServerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'mcp.js')
|
|
401
|
+
const entry = {
|
|
402
|
+
type: 'stdio',
|
|
403
|
+
command: 'node',
|
|
404
|
+
args: [mcpServerPath],
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const targets = [
|
|
408
|
+
{
|
|
409
|
+
name: 'Claude Code',
|
|
410
|
+
file: path.join(os.homedir(), '.claude.json'),
|
|
411
|
+
key: 'mcpServers',
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: 'Cursor',
|
|
415
|
+
file: path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
416
|
+
key: 'mcpServers',
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: 'Windsurf',
|
|
420
|
+
file: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
421
|
+
key: 'mcpServers',
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: 'Cline / Roo-Cline',
|
|
425
|
+
file: path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
|
|
426
|
+
key: 'mcpServers',
|
|
427
|
+
},
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
let installed = 0
|
|
431
|
+
|
|
432
|
+
for (const target of targets) {
|
|
433
|
+
try {
|
|
434
|
+
// Only install into configs that already exist (user has that tool)
|
|
435
|
+
if (!fs.existsSync(target.file) && target.name !== 'Claude Code') continue
|
|
436
|
+
|
|
437
|
+
let cfg = {}
|
|
438
|
+
if (fs.existsSync(target.file)) {
|
|
439
|
+
try { cfg = JSON.parse(fs.readFileSync(target.file, 'utf-8')) } catch { cfg = {} }
|
|
440
|
+
}
|
|
441
|
+
cfg[target.key] = cfg[target.key] || {}
|
|
442
|
+
cfg[target.key].squeezr = entry
|
|
443
|
+
|
|
444
|
+
const dir = path.dirname(target.file)
|
|
445
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
446
|
+
fs.writeFileSync(target.file, JSON.stringify(cfg, null, 2))
|
|
447
|
+
console.log()
|
|
448
|
+
console.log(' ok ' + target.name + ': ' + target.file)
|
|
449
|
+
} catch (e) {
|
|
450
|
+
console.warn()
|
|
451
|
+
console.warn(' warn ' + target.name + ': ' + (e.message || e))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log()
|
|
455
|
+
console.log('MCP server registered in ' + installed + ' client(s).')
|
|
456
|
+
console.log('Server binary: ' + mcpServerPath)
|
|
457
|
+
console.log('')
|
|
458
|
+
console.log('Available tools in Claude/Codex/Cursor:')
|
|
459
|
+
console.log(' squeezr_status — Check if Squeezr is running')
|
|
460
|
+
console.log(' squeezr_stats — Real-time token savings')
|
|
461
|
+
console.log(' squeezr_set_mode — Change compression aggressiveness')
|
|
462
|
+
console.log(' squeezr_config — Current configuration')
|
|
463
|
+
console.log(' squeezr_habits — Wasteful pattern report')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function mcpUninstall() {
|
|
467
|
+
const files = [
|
|
468
|
+
path.join(os.homedir(), '.claude.json'),
|
|
469
|
+
path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
470
|
+
path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
471
|
+
path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
|
|
472
|
+
]
|
|
473
|
+
let removed = 0
|
|
474
|
+
for (const file of files) {
|
|
475
|
+
if (!fs.existsSync(file)) continue
|
|
476
|
+
try {
|
|
477
|
+
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
478
|
+
if (cfg.mcpServers?.squeezr) {
|
|
479
|
+
delete cfg.mcpServers.squeezr
|
|
480
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2))
|
|
481
|
+
console.log()
|
|
482
|
+
removed++
|
|
483
|
+
}
|
|
484
|
+
} catch { /* ignore */ }
|
|
485
|
+
}
|
|
486
|
+
if (removed === 0) console.log('Squeezr MCP not found in any config.')
|
|
487
|
+
else console.log()
|
|
488
|
+
}
|
|
489
|
+
|
|
394
490
|
// ── squeezr ports ─────────────────────────────────────────────────────────────
|
|
395
491
|
|
|
396
492
|
async function configurePorts() {
|
|
@@ -1378,6 +1474,12 @@ switch (command) {
|
|
|
1378
1474
|
showConfig()
|
|
1379
1475
|
break
|
|
1380
1476
|
|
|
1477
|
+
case 'mcp': {
|
|
1478
|
+
const subCmd = args[0] ?? 'install'
|
|
1479
|
+
if (subCmd === 'uninstall') await mcpUninstall()
|
|
1480
|
+
else await mcpInstall()
|
|
1481
|
+
break
|
|
1482
|
+
}
|
|
1381
1483
|
case 'version':
|
|
1382
1484
|
case '--version':
|
|
1383
1485
|
case '-v':
|
|
@@ -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
|
+
});
|
package/dist/compressor.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|