squeezr-ai 1.17.5 → 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 -166
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +380 -0
- 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,10 +199,10 @@ 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
|
-
squeezr cursor Start Cursor subscription MITM proxy (no BYOK needed)
|
|
205
|
-
squeezr cursor stop Stop Cursor proxy and clean up system proxy settings
|
|
206
206
|
squeezr update Kill old processes, install latest from npm, restart
|
|
207
207
|
squeezr uninstall Remove Squeezr completely (env vars, CA, auto-start, logs)
|
|
208
208
|
squeezr version Print version
|
|
@@ -393,6 +393,100 @@ function showConfig() {
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
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
|
+
|
|
396
490
|
// ── squeezr ports ─────────────────────────────────────────────────────────────
|
|
397
491
|
|
|
398
492
|
async function configurePorts() {
|
|
@@ -1256,162 +1350,6 @@ async function startTunnel() {
|
|
|
1256
1350
|
process.on('SIGTERM', () => { child.kill(); process.exit(0) })
|
|
1257
1351
|
}
|
|
1258
1352
|
|
|
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
|
-
|
|
1415
1353
|
// ── CLI router ────────────────────────────────────────────────────────────────
|
|
1416
1354
|
|
|
1417
1355
|
switch (command) {
|
|
@@ -1529,14 +1467,6 @@ switch (command) {
|
|
|
1529
1467
|
await startTunnel()
|
|
1530
1468
|
break
|
|
1531
1469
|
|
|
1532
|
-
case 'cursor':
|
|
1533
|
-
if (args[1] === 'stop') {
|
|
1534
|
-
cleanSystemProxy()
|
|
1535
|
-
} else {
|
|
1536
|
-
await startCursorProxy()
|
|
1537
|
-
}
|
|
1538
|
-
break
|
|
1539
|
-
|
|
1540
1470
|
case 'uninstall':
|
|
1541
1471
|
await uninstall()
|
|
1542
1472
|
break
|
|
@@ -1544,6 +1474,12 @@ switch (command) {
|
|
|
1544
1474
|
showConfig()
|
|
1545
1475
|
break
|
|
1546
1476
|
|
|
1477
|
+
case 'mcp': {
|
|
1478
|
+
const subCmd = args[0] ?? 'install'
|
|
1479
|
+
if (subCmd === 'uninstall') await mcpUninstall()
|
|
1480
|
+
else await mcpInstall()
|
|
1481
|
+
break
|
|
1482
|
+
}
|
|
1547
1483
|
case 'version':
|
|
1548
1484
|
case '--version':
|
|
1549
1485
|
case '-v':
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Squeezr MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Gives any MCP-compatible AI CLI (Claude Code, Cursor, Windsurf, Cline…)
|
|
6
|
+
* real-time awareness of Squeezr's state and control over it.
|
|
7
|
+
*
|
|
8
|
+
* Transport: stdio (universal — works with all MCP clients)
|
|
9
|
+
* Queries the Squeezr proxy via HTTP on localhost.
|
|
10
|
+
*
|
|
11
|
+
* Tools exposed:
|
|
12
|
+
* squeezr_status — Is proxy running? Port, version, uptime
|
|
13
|
+
* squeezr_stats — Token savings, compression %, by-tool breakdown
|
|
14
|
+
* squeezr_set_mode — Change compression aggressiveness instantly
|
|
15
|
+
* squeezr_config — Current thresholds and settings
|
|
16
|
+
* squeezr_habits — Detected wasteful patterns this session
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Squeezr MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Gives any MCP-compatible AI CLI (Claude Code, Cursor, Windsurf, Cline…)
|
|
6
|
+
* real-time awareness of Squeezr's state and control over it.
|
|
7
|
+
*
|
|
8
|
+
* Transport: stdio (universal — works with all MCP clients)
|
|
9
|
+
* Queries the Squeezr proxy via HTTP on localhost.
|
|
10
|
+
*
|
|
11
|
+
* Tools exposed:
|
|
12
|
+
* squeezr_status — Is proxy running? Port, version, uptime
|
|
13
|
+
* squeezr_stats — Token savings, compression %, by-tool breakdown
|
|
14
|
+
* squeezr_set_mode — Change compression aggressiveness instantly
|
|
15
|
+
* squeezr_config — Current thresholds and settings
|
|
16
|
+
* squeezr_habits — Detected wasteful patterns this session
|
|
17
|
+
*/
|
|
18
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { dirname } from 'node:path';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
// ── Resolve proxy port from squeezr.toml ─────────────────────────────────────
|
|
31
|
+
function getProxyPort() {
|
|
32
|
+
try {
|
|
33
|
+
const tomlPath = join(__dirname, '..', 'squeezr.toml');
|
|
34
|
+
if (existsSync(tomlPath)) {
|
|
35
|
+
const toml = readFileSync(tomlPath, 'utf-8');
|
|
36
|
+
const m = toml.match(/^\s*port\s*=\s*(\d+)/m);
|
|
37
|
+
if (m)
|
|
38
|
+
return parseInt(m[1]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { /* ignore */ }
|
|
42
|
+
return parseInt(process.env.SQUEEZR_PORT ?? '8080');
|
|
43
|
+
}
|
|
44
|
+
const BASE_URL = process.env.SQUEEZR_URL ?? `http://localhost:${getProxyPort()}`;
|
|
45
|
+
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
46
|
+
async function proxyGet(path) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${BASE_URL}${path}`, { signal: AbortSignal.timeout(3000) });
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
return null;
|
|
51
|
+
return await res.json();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function proxyPost(path, body) {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
signal: AbortSignal.timeout(3000),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
return null;
|
|
67
|
+
return await res.json();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function fmtNum(n) {
|
|
74
|
+
if (n >= 1_000_000)
|
|
75
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
76
|
+
if (n >= 1_000)
|
|
77
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
78
|
+
return String(n);
|
|
79
|
+
}
|
|
80
|
+
function fmtUptime(s) {
|
|
81
|
+
if (s < 60)
|
|
82
|
+
return `${s}s`;
|
|
83
|
+
if (s < 3600)
|
|
84
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
85
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
86
|
+
}
|
|
87
|
+
// ── MCP Server setup ──────────────────────────────────────────────────────────
|
|
88
|
+
const pkg = require('../package.json');
|
|
89
|
+
const server = new Server({ name: 'squeezr', version: pkg.version }, { capabilities: { tools: {} } });
|
|
90
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
91
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
92
|
+
tools: [
|
|
93
|
+
{
|
|
94
|
+
name: 'squeezr_status',
|
|
95
|
+
description: 'Check if the Squeezr proxy is running. Returns version, port, uptime, ' +
|
|
96
|
+
'compression mode, and whether dry-run is active. ' +
|
|
97
|
+
'Call this first to confirm Squeezr is active before querying other tools.',
|
|
98
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'squeezr_stats',
|
|
102
|
+
description: 'Get real-time token compression statistics for the current Squeezr session. ' +
|
|
103
|
+
'Returns: tokens saved, chars saved, compression %, total requests, ' +
|
|
104
|
+
'session cache hits, cost saved estimate, and per-tool breakdown. ' +
|
|
105
|
+
'Use this to understand how much context Squeezr is saving right now.',
|
|
106
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'squeezr_set_mode',
|
|
110
|
+
description: 'Change Squeezr compression aggressiveness instantly without restarting. ' +
|
|
111
|
+
'Takes effect on the next request. Use "aggressive" or "critical" when ' +
|
|
112
|
+
'approaching context limits. Use "soft" when you need full fidelity on outputs.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
mode: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
enum: ['soft', 'normal', 'aggressive', 'critical'],
|
|
119
|
+
description: 'soft: minimal compression, last 10 results kept, no AI compression. ' +
|
|
120
|
+
'normal: default (threshold 800 chars, last 3 kept). ' +
|
|
121
|
+
'aggressive: threshold 200 chars, last 1 kept, AI compression on. ' +
|
|
122
|
+
'critical: threshold 50 chars, everything compressed, max savings.',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
required: ['mode'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'squeezr_config',
|
|
130
|
+
description: 'Get the current Squeezr configuration: active compression mode, ' +
|
|
131
|
+
'threshold values, keepRecent setting, which tools are AI-skipped, ' +
|
|
132
|
+
'and whether AI compression is currently enabled.',
|
|
133
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'squeezr_habits',
|
|
137
|
+
description: 'Get a report of detected token-wasteful patterns in the current session. ' +
|
|
138
|
+
'Shows which deterministic patterns fired (duplicate reads, lock files, ' +
|
|
139
|
+
'repeated errors, large outputs) and how many tokens they saved or wasted. ' +
|
|
140
|
+
'Useful for improving how you use Claude to spend fewer tokens.',
|
|
141
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
}));
|
|
145
|
+
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
146
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
147
|
+
const { name, arguments: args } = req.params;
|
|
148
|
+
// ── squeezr_status ──────────────────────────────────────────────────────────
|
|
149
|
+
if (name === 'squeezr_status') {
|
|
150
|
+
const health = await proxyGet('/squeezr/health');
|
|
151
|
+
const stats = await proxyGet('/squeezr/stats');
|
|
152
|
+
if (!health) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: [
|
|
157
|
+
'❌ Squeezr proxy is NOT running.',
|
|
158
|
+
'',
|
|
159
|
+
`Expected at: ${BASE_URL}`,
|
|
160
|
+
'Start it with: squeezr start',
|
|
161
|
+
'',
|
|
162
|
+
'Without Squeezr, your tool results are sent to the API uncompressed.',
|
|
163
|
+
].join('\n'),
|
|
164
|
+
}],
|
|
165
|
+
isError: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const uptime = stats ? fmtUptime(stats.uptime_seconds ?? 0) : '?';
|
|
169
|
+
const mode = stats ? (stats.mode ?? 'normal') : 'normal';
|
|
170
|
+
const dryRun = stats ? stats.dry_run : false;
|
|
171
|
+
return {
|
|
172
|
+
content: [{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: [
|
|
175
|
+
`✅ Squeezr v${health.version ?? '?'} is running`,
|
|
176
|
+
` Port : ${BASE_URL}`,
|
|
177
|
+
` Uptime : ${uptime}`,
|
|
178
|
+
` Mode : ${mode}${dryRun ? ' (dry-run)' : ''}`,
|
|
179
|
+
` Dashboard: ${BASE_URL}/squeezr/dashboard`,
|
|
180
|
+
].join('\n'),
|
|
181
|
+
}],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ── squeezr_stats ───────────────────────────────────────────────────────────
|
|
185
|
+
if (name === 'squeezr_stats') {
|
|
186
|
+
const data = await proxyGet('/squeezr/stats');
|
|
187
|
+
if (!data) {
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
|
|
190
|
+
isError: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const savedTokens = data.total_saved_tokens ?? 0;
|
|
194
|
+
const savedChars = data.total_saved_chars ?? 0;
|
|
195
|
+
const pct = data.savings_pct ?? 0;
|
|
196
|
+
const requests = data.requests ?? 0;
|
|
197
|
+
const compressions = data.compressions ?? 0;
|
|
198
|
+
const cacheHits = data.session_cache_hits ?? 0;
|
|
199
|
+
const uptime = fmtUptime(data.uptime_seconds ?? 0);
|
|
200
|
+
const costUsd = (savedTokens / 1_000_000) * 3;
|
|
201
|
+
const byTool = data.by_tool ?? {};
|
|
202
|
+
const sessionCache = data.session_cache_size ?? 0;
|
|
203
|
+
const patternHits = data.pattern_hits ?? {};
|
|
204
|
+
const totalPatterns = Object.values(patternHits).reduce((s, v) => s + v, 0);
|
|
205
|
+
const toolLines = Object.entries(byTool)
|
|
206
|
+
.sort((a, b) => b[1].saved_tokens - a[1].saved_tokens)
|
|
207
|
+
.slice(0, 8)
|
|
208
|
+
.map(([tool, t]) => ` ${tool.padEnd(16)} ${fmtNum(t.saved_tokens).padStart(7)} tokens ${t.avg_pct}% avg ×${t.count}`);
|
|
209
|
+
return {
|
|
210
|
+
content: [{
|
|
211
|
+
type: 'text',
|
|
212
|
+
text: [
|
|
213
|
+
'📊 Squeezr Session Stats',
|
|
214
|
+
'─'.repeat(40),
|
|
215
|
+
`Tokens saved : ${fmtNum(savedTokens)} (~${savedChars.toLocaleString()} chars)`,
|
|
216
|
+
`Compression : ${pct}% of tool results`,
|
|
217
|
+
`Cost saved : $${costUsd.toFixed(3)} (@ $3/MTok)`,
|
|
218
|
+
`Requests : ${requests} (${compressions} compressions)`,
|
|
219
|
+
`Session cache : ${cacheHits} hits (${sessionCache} entries)`,
|
|
220
|
+
`Pattern hits : ${totalPatterns.toLocaleString()}`,
|
|
221
|
+
`Uptime : ${uptime}`,
|
|
222
|
+
'',
|
|
223
|
+
toolLines.length > 0 ? 'By tool:\n' + toolLines.join('\n') : 'No tool results compressed yet.',
|
|
224
|
+
].join('\n'),
|
|
225
|
+
}],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ── squeezr_set_mode ────────────────────────────────────────────────────────
|
|
229
|
+
if (name === 'squeezr_set_mode') {
|
|
230
|
+
const parsed = z.object({
|
|
231
|
+
mode: z.enum(['soft', 'normal', 'aggressive', 'critical']),
|
|
232
|
+
}).parse(args);
|
|
233
|
+
const modeInfo = {
|
|
234
|
+
soft: 'threshold=3000 chars, last 10 results uncompressed, AI off',
|
|
235
|
+
normal: 'threshold=800 chars, last 3 results uncompressed, AI on new blocks',
|
|
236
|
+
aggressive: 'threshold=200 chars, last 1 result uncompressed, AI on',
|
|
237
|
+
critical: 'threshold=50 chars, all results compressed, maximum savings',
|
|
238
|
+
};
|
|
239
|
+
const result = await proxyPost('/squeezr/config', { mode: parsed.mode });
|
|
240
|
+
if (!result) {
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
|
|
243
|
+
isError: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
content: [{
|
|
248
|
+
type: 'text',
|
|
249
|
+
text: [
|
|
250
|
+
`✅ Compression mode set to: ${parsed.mode}`,
|
|
251
|
+
` ${modeInfo[parsed.mode]}`,
|
|
252
|
+
'',
|
|
253
|
+
parsed.mode === 'critical'
|
|
254
|
+
? '⚠️ Critical mode compresses everything aggressively. Switch back to normal when context pressure drops.'
|
|
255
|
+
: parsed.mode === 'aggressive'
|
|
256
|
+
? '🔥 Aggressive mode active. Use when context is above 70%.'
|
|
257
|
+
: '',
|
|
258
|
+
].filter(Boolean).join('\n'),
|
|
259
|
+
}],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// ── squeezr_config ──────────────────────────────────────────────────────────
|
|
263
|
+
if (name === 'squeezr_config') {
|
|
264
|
+
const data = await proxyGet('/squeezr/stats');
|
|
265
|
+
if (!data) {
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
|
|
268
|
+
isError: false,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const mode = data.mode ?? 'normal';
|
|
272
|
+
const dryRun = data.dry_run ?? false;
|
|
273
|
+
const cacheStats = data.cache;
|
|
274
|
+
return {
|
|
275
|
+
content: [{
|
|
276
|
+
type: 'text',
|
|
277
|
+
text: [
|
|
278
|
+
'⚙️ Squeezr Configuration',
|
|
279
|
+
'─'.repeat(40),
|
|
280
|
+
`Compression mode : ${mode}`,
|
|
281
|
+
`Dry-run : ${dryRun ? 'yes (no actual compression)' : 'no'}`,
|
|
282
|
+
`LRU cache : ${cacheStats?.size ?? '?'} entries | ${cacheStats?.hit_rate_pct ?? '?'}% hit rate`,
|
|
283
|
+
`Session cache : ${data.session_cache_size ?? '?'} entries`,
|
|
284
|
+
`Expand store : ${data.expand_store_size ?? '?'} entries`,
|
|
285
|
+
'',
|
|
286
|
+
'Modes available:',
|
|
287
|
+
' soft — minimal, no AI compression',
|
|
288
|
+
' normal — default balanced',
|
|
289
|
+
' aggressive — max useful compression',
|
|
290
|
+
' critical — compress everything',
|
|
291
|
+
'',
|
|
292
|
+
`Change with: squeezr_set_mode({ mode: "aggressive" })`,
|
|
293
|
+
].join('\n'),
|
|
294
|
+
}],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// ── squeezr_habits ──────────────────────────────────────────────────────────
|
|
298
|
+
if (name === 'squeezr_habits') {
|
|
299
|
+
const data = await proxyGet('/squeezr/stats');
|
|
300
|
+
if (!data) {
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
|
|
303
|
+
isError: false,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const patternHits = data.pattern_hits ?? {};
|
|
307
|
+
const byTool = data.by_tool ?? {};
|
|
308
|
+
const habits = [];
|
|
309
|
+
// Detect lock file reads
|
|
310
|
+
const readTool = byTool['Read'] ?? byTool['read'];
|
|
311
|
+
if (readTool && readTool.count > 5) {
|
|
312
|
+
habits.push({
|
|
313
|
+
level: readTool.count > 20 ? '🔴' : '🟡',
|
|
314
|
+
msg: `Read tool called ${readTool.count}× this session. Check for lock file reads or repeated file reads.`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// Detect read-dedup pattern hits (same file read multiple times)
|
|
318
|
+
if ((patternHits['readDedup'] ?? 0) > 0) {
|
|
319
|
+
habits.push({
|
|
320
|
+
level: '🟡',
|
|
321
|
+
msg: `${patternHits['readDedup']} duplicate file read(s) detected and collapsed. Consider using @file to pin frequently needed files.`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// Detect high bash call count
|
|
325
|
+
const bashTool = byTool['Bash'] ?? byTool['bash'];
|
|
326
|
+
if (bashTool && bashTool.count > 30) {
|
|
327
|
+
habits.push({
|
|
328
|
+
level: '🟡',
|
|
329
|
+
msg: `Bash called ${bashTool.count}× — if repeatedly running the same command, consider using --watch mode.`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// Detect low compression on Read (code quality concern)
|
|
333
|
+
if (readTool && readTool.avg_pct < 10 && readTool.count > 3) {
|
|
334
|
+
habits.push({
|
|
335
|
+
level: '🟢',
|
|
336
|
+
msg: `Read tool results have ${readTool.avg_pct}% compression (code files protected from AI summarization — this is correct).`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Session cache effectiveness
|
|
340
|
+
const cacheHits = data.session_cache_hits ?? 0;
|
|
341
|
+
const compressions = data.compressions ?? 0;
|
|
342
|
+
if (compressions > 0) {
|
|
343
|
+
const hitRate = Math.round((cacheHits / (cacheHits + compressions)) * 100);
|
|
344
|
+
if (hitRate > 50) {
|
|
345
|
+
habits.push({
|
|
346
|
+
level: '🟢',
|
|
347
|
+
msg: `Session cache ${hitRate}% hit rate — repeated tool results are being reused efficiently.`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const totalSaved = data.total_saved_tokens ?? 0;
|
|
352
|
+
const requests = data.requests ?? 0;
|
|
353
|
+
if (habits.length === 0) {
|
|
354
|
+
habits.push({ level: '🟢', msg: 'No significant wasteful patterns detected this session.' });
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
content: [{
|
|
358
|
+
type: 'text',
|
|
359
|
+
text: [
|
|
360
|
+
'🔍 Squeezr Habit Report (current session)',
|
|
361
|
+
'─'.repeat(40),
|
|
362
|
+
`Session: ${requests} requests | ${fmtNum(totalSaved)} tokens saved`,
|
|
363
|
+
'',
|
|
364
|
+
...habits.map(h => `${h.level} ${h.msg}`),
|
|
365
|
+
'',
|
|
366
|
+
'Tip: use squeezr_set_mode to control compression aggressiveness.',
|
|
367
|
+
].join('\n'),
|
|
368
|
+
}],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
373
|
+
isError: true,
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
// ── Start server ──────────────────────────────────────────────────────────────
|
|
377
|
+
const transport = new StdioServerTransport();
|
|
378
|
+
await server.connect(transport);
|
|
379
|
+
// stderr only — stdout is reserved for MCP protocol
|
|
380
|
+
process.stderr.write(`[squeezr-mcp] Squeezr MCP server v${pkg.version} ready (proxy: ${BASE_URL})\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squeezr-ai",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.6",
|
|
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
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"homepage": "https://github.com/sergioramosv/Squeezr#readme",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
26
|
-
"squeezr": "bin/squeezr.js"
|
|
26
|
+
"squeezr": "bin/squeezr.js",
|
|
27
|
+
"squeezr-mcp": "dist/mcp.js"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsc",
|
|
@@ -43,10 +44,12 @@
|
|
|
43
44
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
44
45
|
"@bufbuild/protobuf": "^2.11.0",
|
|
45
46
|
"@hono/node-server": "^1.13.7",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
48
|
"hono": "^4.7.5",
|
|
47
49
|
"node-forge": "^1.4.0",
|
|
48
50
|
"openai": "^4.93.0",
|
|
49
|
-
"smol-toml": "^1.3.1"
|
|
51
|
+
"smol-toml": "^1.3.1",
|
|
52
|
+
"zod": "^3.24.0"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@types/node": "^22.14.0",
|