squeezr-ai 1.23.0 → 1.46.2
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 +10 -8
- package/bin/squeezr.js +622 -26
- package/dist/__tests__/rateLimitHeaders.test.d.ts +8 -0
- package/dist/__tests__/rateLimitHeaders.test.js +96 -0
- package/dist/anthropicDirectFetch.d.ts +7 -0
- package/dist/anthropicDirectFetch.js +186 -0
- package/dist/anthropicMitm.d.ts +33 -0
- package/dist/anthropicMitm.js +401 -0
- package/dist/claudeDesktopWorker.d.ts +1 -0
- package/dist/claudeDesktopWorker.js +153 -0
- package/dist/compressor.js +430 -35
- package/dist/config.d.ts +13 -0
- package/dist/config.js +63 -5
- package/dist/dashboard.d.ts +3 -3
- package/dist/dashboard.js +1657 -1471
- package/dist/desktopProxy.d.ts +1 -0
- package/dist/desktopProxy.js +324 -0
- package/dist/deterministic.d.ts +1 -0
- package/dist/deterministic.js +73 -0
- package/dist/history.d.ts +2 -1
- package/dist/history.js +4 -1
- package/dist/index.js +10 -0
- package/dist/limits.d.ts +2 -2
- package/dist/limits.js +9 -2
- package/dist/mcp.js +33 -0
- package/dist/server.js +1001 -736
- package/dist/stats.d.ts +29 -1
- package/dist/stats.js +78 -1
- package/dist/systemPrompt.js +29 -8
- package/package.json +3 -3
- package/squeezr.toml +40 -38
package/bin/squeezr.js
CHANGED
|
@@ -22,13 +22,26 @@ const UPDATE_CHECK_FILE = path.join(os.homedir(), '.squeezr', 'update-check.json
|
|
|
22
22
|
const UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
|
|
23
23
|
|
|
24
24
|
// Fire and forget — runs in background, never blocks CLI
|
|
25
|
+
// Convert semver string to comparable number (major*1M + minor*1k + patch)
|
|
26
|
+
function semverToNum(v) {
|
|
27
|
+
if (!v || typeof v !== 'string') return 0
|
|
28
|
+
const parts = v.split('.').map(p => parseInt(p) || 0)
|
|
29
|
+
return (parts[0] || 0) * 1000000 + (parts[1] || 0) * 1000 + (parts[2] || 0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Returns the npm version IF it's strictly newer than the local version, else null
|
|
33
|
+
function newerVersionOrNull(latest) {
|
|
34
|
+
if (!latest || latest === pkg.version) return null
|
|
35
|
+
return semverToNum(latest) > semverToNum(pkg.version) ? latest : null
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
const updateCheckPromise = (async () => {
|
|
26
39
|
try {
|
|
27
40
|
// Read cached check
|
|
28
41
|
let cached = null
|
|
29
42
|
try { cached = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf-8')) } catch {}
|
|
30
43
|
if (cached && Date.now() - cached.checkedAt < UPDATE_CHECK_INTERVAL) {
|
|
31
|
-
return cached.latest
|
|
44
|
+
return newerVersionOrNull(cached.latest)
|
|
32
45
|
}
|
|
33
46
|
// Fetch latest from npm (with timeout)
|
|
34
47
|
const { get } = await import('https')
|
|
@@ -48,7 +61,7 @@ const updateCheckPromise = (async () => {
|
|
|
48
61
|
const dir = path.dirname(UPDATE_CHECK_FILE)
|
|
49
62
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
50
63
|
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ latest, checkedAt: Date.now() }))
|
|
51
|
-
return latest
|
|
64
|
+
return newerVersionOrNull(latest)
|
|
52
65
|
} catch { return null }
|
|
53
66
|
})()
|
|
54
67
|
|
|
@@ -242,6 +255,11 @@ Usage:
|
|
|
242
255
|
squeezr mcp uninstall Remove Squeezr MCP registration
|
|
243
256
|
squeezr ports Change HTTP and MITM proxy ports
|
|
244
257
|
squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
|
|
258
|
+
squeezr enable-claude-desktop Enable hosts-file redirect for Claude Desktop (admin once)
|
|
259
|
+
squeezr disable-claude-desktop Disable hosts-file redirect for Claude Desktop
|
|
260
|
+
squeezr desktop start Start SEPARATE proxy for Claude/Codex Desktop (ports 8443+8088)
|
|
261
|
+
squeezr desktop stop Stop the desktop proxy (does NOT affect main proxy)
|
|
262
|
+
squeezr desktop status Show desktop proxy status
|
|
245
263
|
squeezr bypass Toggle bypass mode (skip compression, keep logging)
|
|
246
264
|
squeezr bypass --on Enable bypass (disable compression)
|
|
247
265
|
squeezr bypass --off Disable bypass (resume compression)
|
|
@@ -314,6 +332,17 @@ async function startDaemon() {
|
|
|
314
332
|
console.log(` Dashboard: http://localhost:${port}/squeezr/dashboard`)
|
|
315
333
|
console.log(` Logs: ${logFile}`)
|
|
316
334
|
|
|
335
|
+
// ── Also start the SEPARATE Desktop proxy (independent process) ────────────
|
|
336
|
+
// This is the proxy that serves Claude Desktop and Codex Desktop. It runs in
|
|
337
|
+
// its own Node process on ports 8443 + 8088. If it crashes, the main proxy
|
|
338
|
+
// (just started above) keeps running. Failures here are non-fatal — we just
|
|
339
|
+
// warn and continue.
|
|
340
|
+
try {
|
|
341
|
+
await desktopProxyStart()
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.warn(`Desktop proxy did not start: ${e?.message ?? e}`)
|
|
344
|
+
console.warn(`(The main proxy is fine. Try \`squeezr desktop start\` separately.)`)
|
|
345
|
+
}
|
|
317
346
|
}
|
|
318
347
|
|
|
319
348
|
function showLogs() {
|
|
@@ -350,8 +379,55 @@ function killMcpProcesses() {
|
|
|
350
379
|
function stopProxy() {
|
|
351
380
|
const port = getPort()
|
|
352
381
|
const mitmPort = getMitmPort(port)
|
|
382
|
+
|
|
383
|
+
// ── Step 0: Stop the SEPARATE Desktop proxy first (independent process) ────
|
|
384
|
+
// It has its own PID file. Failure here doesn't block main-proxy shutdown.
|
|
385
|
+
try {
|
|
386
|
+
const desktopPid = readDesktopPid()
|
|
387
|
+
if (desktopPid) {
|
|
388
|
+
try { process.kill(desktopPid, 'SIGTERM') } catch {}
|
|
389
|
+
// Give it a moment to exit gracefully
|
|
390
|
+
for (let i = 0; i < 20; i++) {
|
|
391
|
+
try { process.kill(desktopPid, 0) } catch { break }
|
|
392
|
+
execSync(process.platform === 'win32' ? `ping -n 1 127.0.0.1 > nul` : `sleep 0.1`, { stdio: 'pipe' })
|
|
393
|
+
}
|
|
394
|
+
try { process.kill(desktopPid, 'SIGKILL') } catch {}
|
|
395
|
+
try { fs.unlinkSync(DESKTOP_PID_FILE) } catch {}
|
|
396
|
+
// Also clean up any orphan listeners on the desktop ports
|
|
397
|
+
for (const dp of [DESKTOP_HTTPS_PORT, DESKTOP_HTTP_PORT]) {
|
|
398
|
+
try {
|
|
399
|
+
if (process.platform === 'win32') {
|
|
400
|
+
const out = execSync(`netstat -ano | findstr ":${dp} "`, { encoding: 'utf-8', stdio: 'pipe' })
|
|
401
|
+
const matches = [...out.matchAll(/LISTENING\s+(\d+)/g)]
|
|
402
|
+
for (const m of matches) {
|
|
403
|
+
try { execSync(`taskkill /F /PID ${m[1]}`, { stdio: 'pipe' }) } catch {}
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
try { execSync(`lsof -ti :${dp} -sTCP:LISTEN | xargs -r kill -9`, { stdio: 'pipe' }) } catch {}
|
|
407
|
+
}
|
|
408
|
+
} catch {}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch {}
|
|
412
|
+
|
|
413
|
+
// ── Step 1: Graceful shutdown via HTTP — persists history/cache before exit ──
|
|
414
|
+
// /squeezr/control/stop emits SIGTERM which calls persistAndExit().
|
|
415
|
+
// This prevents losing the current session's savings history on stop.
|
|
416
|
+
let gracefulOk = false
|
|
417
|
+
try {
|
|
418
|
+
const req = http.request({ hostname: 'localhost', port, path: '/squeezr/control/stop', method: 'POST', timeout: 2000 })
|
|
419
|
+
req.on('error', () => {})
|
|
420
|
+
req.end()
|
|
421
|
+
gracefulOk = true
|
|
422
|
+
// Give the process time to persist and exit cleanly
|
|
423
|
+
execSync(process.platform === 'win32'
|
|
424
|
+
? `ping -n 2 127.0.0.1 > nul` // ~1s sleep on Windows
|
|
425
|
+
: `sleep 1`, { stdio: 'pipe' })
|
|
426
|
+
} catch {}
|
|
427
|
+
|
|
428
|
+
// ── Step 2: Force-kill anything still listening (fallback) ──────────────────
|
|
353
429
|
const ports = [port, mitmPort]
|
|
354
|
-
let killed =
|
|
430
|
+
let killed = gracefulOk // count graceful as "killed"
|
|
355
431
|
|
|
356
432
|
for (const p of ports) {
|
|
357
433
|
try {
|
|
@@ -430,9 +506,9 @@ async function checkStatus() {
|
|
|
430
506
|
return false
|
|
431
507
|
}
|
|
432
508
|
console.log(`Squeezr is running (v${json.version})`)
|
|
433
|
-
console.log(` HTTP proxy (Claude
|
|
434
|
-
console.log(` MITM proxy (Codex):
|
|
435
|
-
console.log(` Dashboard:
|
|
509
|
+
console.log(` HTTP proxy (Claude Code, Claude Desktop, Codex Desktop, Aider, Gemini): http://localhost:${port}`)
|
|
510
|
+
console.log(` MITM proxy (Codex CLI TLS): http://localhost:${mitmPort}`)
|
|
511
|
+
console.log(` Dashboard: http://localhost:${port}/squeezr/dashboard`)
|
|
436
512
|
if (json.mode) console.log(` Mode: ${json.mode}`)
|
|
437
513
|
if (json.uptime_seconds != null) {
|
|
438
514
|
const s = json.uptime_seconds
|
|
@@ -470,12 +546,24 @@ async function mcpInstall() {
|
|
|
470
546
|
args: [mcpServerPath],
|
|
471
547
|
}
|
|
472
548
|
|
|
549
|
+
// Claude Desktop config path varies by platform
|
|
550
|
+
const claudeDesktopConfig = process.platform === 'win32'
|
|
551
|
+
? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
|
|
552
|
+
: process.platform === 'darwin'
|
|
553
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
554
|
+
: path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json')
|
|
555
|
+
|
|
473
556
|
const targets = [
|
|
474
557
|
{
|
|
475
558
|
name: 'Claude Code',
|
|
476
559
|
file: path.join(os.homedir(), '.claude.json'),
|
|
477
560
|
key: 'mcpServers',
|
|
478
561
|
},
|
|
562
|
+
{
|
|
563
|
+
name: 'Claude Desktop',
|
|
564
|
+
file: claudeDesktopConfig,
|
|
565
|
+
key: 'mcpServers',
|
|
566
|
+
},
|
|
479
567
|
{
|
|
480
568
|
name: 'Cursor',
|
|
481
569
|
file: path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
@@ -523,17 +611,24 @@ async function mcpInstall() {
|
|
|
523
611
|
console.log('MCP server registered in ' + installed + ' client(s).')
|
|
524
612
|
console.log('Server binary: ' + mcpServerPath)
|
|
525
613
|
console.log('')
|
|
526
|
-
console.log('Available tools in Claude
|
|
527
|
-
console.log(' squeezr_status
|
|
528
|
-
console.log(' squeezr_stats
|
|
529
|
-
console.log(' squeezr_set_mode
|
|
530
|
-
console.log(' squeezr_config
|
|
531
|
-
console.log(' squeezr_habits
|
|
614
|
+
console.log('Available tools in Claude Desktop, Claude Code, Codex Desktop, Cursor…:')
|
|
615
|
+
console.log(' squeezr_status — Check if Squeezr is running')
|
|
616
|
+
console.log(' squeezr_stats — Real-time token savings')
|
|
617
|
+
console.log(' squeezr_set_mode — Change compression aggressiveness')
|
|
618
|
+
console.log(' squeezr_config — Current configuration')
|
|
619
|
+
console.log(' squeezr_habits — Wasteful pattern report')
|
|
620
|
+
console.log(' squeezr_open_dashboard — Open the Squeezr dashboard in your browser')
|
|
532
621
|
}
|
|
533
622
|
|
|
534
623
|
async function mcpUninstall() {
|
|
624
|
+
const claudeDesktopConfig = process.platform === 'win32'
|
|
625
|
+
? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
|
|
626
|
+
: process.platform === 'darwin'
|
|
627
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
628
|
+
: path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json')
|
|
535
629
|
const files = [
|
|
536
630
|
path.join(os.homedir(), '.claude.json'),
|
|
631
|
+
claudeDesktopConfig,
|
|
537
632
|
path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
538
633
|
path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
539
634
|
path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
|
|
@@ -812,9 +907,389 @@ async function uninstall() {
|
|
|
812
907
|
console.log('\nDone! Squeezr has been completely removed.\n')
|
|
813
908
|
}
|
|
814
909
|
|
|
910
|
+
// ── Claude Desktop hosts file + TLS intercept ────────────────────────────────
|
|
911
|
+
// Claude Desktop ignores ANTHROPIC_BASE_URL (it's an Electron GUI app), so the
|
|
912
|
+
// only way to intercept is at DNS level: hosts file redirects api.anthropic.com
|
|
913
|
+
// → 127.0.0.1, and Squeezr listens on :443 with TLS using its local CA cert.
|
|
914
|
+
//
|
|
915
|
+
// This function adds/removes the hosts file entry + firewall rule. Requires
|
|
916
|
+
// admin. On Windows, re-launches itself elevated via PowerShell Start-Process -Verb RunAs.
|
|
917
|
+
|
|
918
|
+
const HOSTS_FILE_WIN = 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
|
919
|
+
const HOSTS_FILE_UNIX = '/etc/hosts'
|
|
920
|
+
const HOSTS_MARKER_BEGIN = '# squeezr-claude-desktop BEGIN'
|
|
921
|
+
const HOSTS_MARKER_END = '# squeezr-claude-desktop END'
|
|
922
|
+
const INTERCEPTED_DOMAINS = ['api.anthropic.com']
|
|
923
|
+
const CLAUDE_DESKTOP_FLAG_FILE = path.join(os.homedir(), '.squeezr', 'claude-desktop-enabled')
|
|
924
|
+
const MITM_INTERNAL_PORT = 8443
|
|
925
|
+
|
|
926
|
+
async function toggleClaudeDesktopIntercept(enable) {
|
|
927
|
+
const isWin = process.platform === 'win32'
|
|
928
|
+
const hostsPath = isWin ? HOSTS_FILE_WIN : HOSTS_FILE_UNIX
|
|
929
|
+
|
|
930
|
+
// Check if running as admin
|
|
931
|
+
const isAdmin = await checkIsAdmin()
|
|
932
|
+
if (!isAdmin) {
|
|
933
|
+
if (isWin) {
|
|
934
|
+
console.log('Admin rights required to modify hosts file and bind to port 443.')
|
|
935
|
+
console.log('Relaunching elevated...\n')
|
|
936
|
+
const verb = enable ? 'enable-claude-desktop' : 'disable-claude-desktop'
|
|
937
|
+
try {
|
|
938
|
+
execSync(
|
|
939
|
+
`powershell -NoProfile -Command "Start-Process -FilePath '${process.execPath}' -ArgumentList '${process.argv[1]}','${verb}' -Verb RunAs -Wait"`,
|
|
940
|
+
{ stdio: 'inherit' }
|
|
941
|
+
)
|
|
942
|
+
console.log('\nDone. Restart Squeezr to apply: squeezr stop && squeezr start')
|
|
943
|
+
} catch (e) {
|
|
944
|
+
console.error('Failed to launch elevated process: ' + e.message)
|
|
945
|
+
}
|
|
946
|
+
return
|
|
947
|
+
} else {
|
|
948
|
+
console.error('Run with sudo: sudo squeezr ' + (enable ? 'enable' : 'disable') + '-claude-desktop')
|
|
949
|
+
process.exit(1)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Read hosts file
|
|
954
|
+
let content = ''
|
|
955
|
+
try { content = fs.readFileSync(hostsPath, 'utf-8') } catch (e) {
|
|
956
|
+
console.error('Could not read hosts file: ' + e.message)
|
|
957
|
+
process.exit(1)
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Strip any existing squeezr block
|
|
961
|
+
const blockRegex = new RegExp(
|
|
962
|
+
`\\r?\\n?${HOSTS_MARKER_BEGIN}[\\s\\S]*?${HOSTS_MARKER_END}\\r?\\n?`,
|
|
963
|
+
'g'
|
|
964
|
+
)
|
|
965
|
+
content = content.replace(blockRegex, '')
|
|
966
|
+
|
|
967
|
+
if (enable) {
|
|
968
|
+
const block = [
|
|
969
|
+
'',
|
|
970
|
+
HOSTS_MARKER_BEGIN,
|
|
971
|
+
'# Redirects Claude Desktop to Squeezr for token compression.',
|
|
972
|
+
'# Remove this block (or run `squeezr disable-claude-desktop`) to undo.',
|
|
973
|
+
...INTERCEPTED_DOMAINS.map(d => `127.0.0.1 ${d}`),
|
|
974
|
+
HOSTS_MARKER_END,
|
|
975
|
+
'',
|
|
976
|
+
].join('\n')
|
|
977
|
+
content += block
|
|
978
|
+
|
|
979
|
+
fs.writeFileSync(hostsPath, content)
|
|
980
|
+
console.log('[1/4] Hosts file updated: api.anthropic.com → 127.0.0.1')
|
|
981
|
+
|
|
982
|
+
if (isWin) {
|
|
983
|
+
// 2. netsh portproxy 443 → 8443 (so Squeezr listens on 8443 without admin)
|
|
984
|
+
try {
|
|
985
|
+
execSync(`netsh interface portproxy delete v4tov4 listenport=443 listenaddress=127.0.0.1`, { stdio: 'pipe' })
|
|
986
|
+
} catch {}
|
|
987
|
+
try {
|
|
988
|
+
execSync(`netsh interface portproxy add v4tov4 listenport=443 listenaddress=127.0.0.1 connectport=${MITM_INTERNAL_PORT} connectaddress=127.0.0.1`, { stdio: 'pipe' })
|
|
989
|
+
console.log(`[2/4] Port forwarding 127.0.0.1:443 → 127.0.0.1:${MITM_INTERNAL_PORT} (netsh portproxy)`)
|
|
990
|
+
} catch (e) {
|
|
991
|
+
console.warn('Could not add netsh portproxy: ' + e.message)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// 3. Firewall: open inbound 443
|
|
995
|
+
try {
|
|
996
|
+
execSync('netsh advfirewall firewall delete rule name="Squeezr-Claude-Desktop-443"', { stdio: 'pipe' })
|
|
997
|
+
} catch {}
|
|
998
|
+
try {
|
|
999
|
+
execSync('netsh advfirewall firewall add rule name="Squeezr-Claude-Desktop-443" dir=in action=allow protocol=TCP localport=443', { stdio: 'pipe' })
|
|
1000
|
+
console.log('[3/4] Firewall rule added for port 443.')
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
console.warn('Could not add firewall rule: ' + e.message)
|
|
1003
|
+
}
|
|
1004
|
+
// Flush DNS cache so the change takes effect immediately
|
|
1005
|
+
try { execSync('ipconfig /flushdns', { stdio: 'pipe' }) } catch {}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Note: no persistent flag file needed — Squeezr auto-detects the hosts file
|
|
1009
|
+
// entry on startup and activates the MITM listener accordingly.
|
|
1010
|
+
|
|
1011
|
+
console.log('\n[NEXT] Restart Squeezr: squeezr stop && squeezr start')
|
|
1012
|
+
console.log(' Close Claude Desktop completely (including system tray).')
|
|
1013
|
+
console.log(' Reopen Claude Desktop and try any query.')
|
|
1014
|
+
console.log(' Verify in dashboard "By client" section → should show "claude_desktop".')
|
|
1015
|
+
} else {
|
|
1016
|
+
fs.writeFileSync(hostsPath, content)
|
|
1017
|
+
console.log('[1/4] Hosts file cleaned: api.anthropic.com restored to normal DNS.')
|
|
1018
|
+
if (isWin) {
|
|
1019
|
+
try {
|
|
1020
|
+
execSync(`netsh interface portproxy delete v4tov4 listenport=443 listenaddress=127.0.0.1`, { stdio: 'pipe' })
|
|
1021
|
+
console.log('[2/4] Port forwarding rule removed.')
|
|
1022
|
+
} catch {}
|
|
1023
|
+
try {
|
|
1024
|
+
execSync('netsh advfirewall firewall delete rule name="Squeezr-Claude-Desktop-443"', { stdio: 'pipe' })
|
|
1025
|
+
console.log('[3/4] Firewall rule removed.')
|
|
1026
|
+
} catch {}
|
|
1027
|
+
try { execSync('ipconfig /flushdns', { stdio: 'pipe' }) } catch {}
|
|
1028
|
+
}
|
|
1029
|
+
// Legacy flag cleanup (if exists from older version)
|
|
1030
|
+
try {
|
|
1031
|
+
const legacyFlag = path.join(os.homedir(), '.squeezr', 'claude-desktop-enabled')
|
|
1032
|
+
if (fs.existsSync(legacyFlag)) fs.unlinkSync(legacyFlag)
|
|
1033
|
+
} catch {}
|
|
1034
|
+
console.log('\n[NEXT] Restart Squeezr: squeezr stop && squeezr start')
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function checkIsAdmin() {
|
|
1039
|
+
if (process.platform === 'win32') {
|
|
1040
|
+
try {
|
|
1041
|
+
execSync('net session', { stdio: 'pipe' })
|
|
1042
|
+
return true
|
|
1043
|
+
} catch {
|
|
1044
|
+
return false
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
return process.getuid && process.getuid() === 0
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// ── Desktop proxy lifecycle (TOTALLY SEPARATE from main proxy on 8080) ───────
|
|
1052
|
+
// `squeezr desktop start` → spawns dist/desktopProxy.js detached
|
|
1053
|
+
// `squeezr desktop stop` → kills via PID file (does NOT touch port 8080)
|
|
1054
|
+
// `squeezr desktop status` → prints state
|
|
1055
|
+
//
|
|
1056
|
+
// Critical: this proxy lives or dies independently. Crashing it cannot affect
|
|
1057
|
+
// the main proxy on 8080 (Claude Code, Codex CLI, Aider, Gemini CLI).
|
|
1058
|
+
|
|
1059
|
+
const DESKTOP_PID_FILE = path.join(os.homedir(), '.squeezr', 'desktop-proxy.pid')
|
|
1060
|
+
const DESKTOP_LOG_FILE = path.join(os.homedir(), '.squeezr', 'desktop-proxy.log')
|
|
1061
|
+
const DESKTOP_HTTPS_PORT = 8443
|
|
1062
|
+
const DESKTOP_HTTP_PORT = 8088
|
|
1063
|
+
|
|
1064
|
+
function readDesktopPid() {
|
|
1065
|
+
try {
|
|
1066
|
+
const raw = fs.readFileSync(DESKTOP_PID_FILE, 'utf-8').trim()
|
|
1067
|
+
const pid = Number(raw)
|
|
1068
|
+
if (!pid || Number.isNaN(pid)) return null
|
|
1069
|
+
// Check process actually exists
|
|
1070
|
+
try { process.kill(pid, 0); return pid } catch { return null }
|
|
1071
|
+
} catch { return null }
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Probe whether the desktop proxy listener is actually answering. Used to
|
|
1075
|
+
// detect orphan processes — situations where the PID file is stale but a
|
|
1076
|
+
// previous desktop proxy is still bound to the ports.
|
|
1077
|
+
async function isDesktopPortBound(port) {
|
|
1078
|
+
return new Promise(resolve => {
|
|
1079
|
+
const net = require('node:net')
|
|
1080
|
+
const sock = net.connect({ host: '127.0.0.1', port, timeout: 500 }, () => {
|
|
1081
|
+
sock.end()
|
|
1082
|
+
resolve(true)
|
|
1083
|
+
})
|
|
1084
|
+
sock.once('error', () => resolve(false))
|
|
1085
|
+
sock.once('timeout', () => { sock.destroy(); resolve(false) })
|
|
1086
|
+
})
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Find the PID owning a local TCP listener on Windows via `netstat -ano`. We
|
|
1090
|
+
// use this only to clean up orphan desktop-proxy processes; it is a best
|
|
1091
|
+
// effort and returns null if it can't parse the output.
|
|
1092
|
+
function findPidByPort(port) {
|
|
1093
|
+
if (process.platform !== 'win32') return null
|
|
1094
|
+
try {
|
|
1095
|
+
const out = require('node:child_process').execSync(`netstat -ano -p TCP`, { encoding: 'utf-8' })
|
|
1096
|
+
for (const line of out.split(/\r?\n/)) {
|
|
1097
|
+
const m = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/)
|
|
1098
|
+
if (m && Number(m[1]) === port) return Number(m[2])
|
|
1099
|
+
}
|
|
1100
|
+
} catch { /* ignore */ }
|
|
1101
|
+
return null
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function desktopProxyStart() {
|
|
1105
|
+
const existing = readDesktopPid()
|
|
1106
|
+
if (existing) {
|
|
1107
|
+
console.log(`Desktop proxy already running (pid=${existing}).`)
|
|
1108
|
+
console.log(` HTTPS: https://127.0.0.1:${DESKTOP_HTTPS_PORT} (Claude Desktop)`)
|
|
1109
|
+
console.log(` HTTP: http://127.0.0.1:${DESKTOP_HTTP_PORT} (Codex Desktop)`)
|
|
1110
|
+
return
|
|
1111
|
+
}
|
|
1112
|
+
// Detect orphan listener (PID file dead but listener still up) — common
|
|
1113
|
+
// after an ungraceful restart. Reclaim it by adopting the running PID,
|
|
1114
|
+
// otherwise the spawn below would crash with EADDRINUSE.
|
|
1115
|
+
if (await isDesktopPortBound(DESKTOP_HTTPS_PORT) || await isDesktopPortBound(DESKTOP_HTTP_PORT)) {
|
|
1116
|
+
const orphanPid = findPidByPort(DESKTOP_HTTPS_PORT) ?? findPidByPort(DESKTOP_HTTP_PORT)
|
|
1117
|
+
if (orphanPid) {
|
|
1118
|
+
try { fs.writeFileSync(DESKTOP_PID_FILE, String(orphanPid), 'utf-8') } catch {}
|
|
1119
|
+
console.log(`Desktop proxy is already bound to ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} (pid=${orphanPid}, adopted).`)
|
|
1120
|
+
} else {
|
|
1121
|
+
console.log(`Desktop proxy ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} are already in use by an unknown process. Use 'squeezr desktop stop' first.`)
|
|
1122
|
+
}
|
|
1123
|
+
return
|
|
1124
|
+
}
|
|
1125
|
+
const distPath = path.join(ROOT, 'dist', 'desktopProxy.js')
|
|
1126
|
+
if (!fs.existsSync(distPath)) {
|
|
1127
|
+
console.error(`Error: ${distPath} not found. Run 'npm run build' first.`)
|
|
1128
|
+
process.exit(1)
|
|
1129
|
+
}
|
|
1130
|
+
try { fs.mkdirSync(path.dirname(DESKTOP_LOG_FILE), { recursive: true }) } catch {}
|
|
1131
|
+
const out = fs.openSync(DESKTOP_LOG_FILE, 'a')
|
|
1132
|
+
const err = fs.openSync(DESKTOP_LOG_FILE, 'a')
|
|
1133
|
+
const child = spawn(process.execPath, [distPath], {
|
|
1134
|
+
detached: true,
|
|
1135
|
+
stdio: ['ignore', out, err],
|
|
1136
|
+
env: {
|
|
1137
|
+
...process.env,
|
|
1138
|
+
SQUEEZR_DESKTOP_HTTPS_PORT: String(DESKTOP_HTTPS_PORT),
|
|
1139
|
+
SQUEEZR_DESKTOP_HTTP_PORT: String(DESKTOP_HTTP_PORT),
|
|
1140
|
+
},
|
|
1141
|
+
})
|
|
1142
|
+
child.unref()
|
|
1143
|
+
// Wait briefly to confirm it didn't immediately exit
|
|
1144
|
+
await new Promise(r => setTimeout(r, 800))
|
|
1145
|
+
if (child.exitCode !== null) {
|
|
1146
|
+
console.error(`Desktop proxy failed to start (exit ${child.exitCode}). Check log: ${DESKTOP_LOG_FILE}`)
|
|
1147
|
+
process.exit(1)
|
|
1148
|
+
}
|
|
1149
|
+
console.log(`Desktop proxy started (pid=${child.pid}).`)
|
|
1150
|
+
console.log(` HTTPS: https://127.0.0.1:${DESKTOP_HTTPS_PORT} (Claude Desktop)`)
|
|
1151
|
+
console.log(` HTTP: http://127.0.0.1:${DESKTOP_HTTP_PORT} (Codex Desktop)`)
|
|
1152
|
+
console.log(` Log: ${DESKTOP_LOG_FILE}`)
|
|
1153
|
+
console.log(``)
|
|
1154
|
+
console.log(`Note: the main Squeezr proxy on 8080 is unaffected by this command.`)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function desktopProxyStop() {
|
|
1158
|
+
let pid = readDesktopPid()
|
|
1159
|
+
if (!pid) {
|
|
1160
|
+
// PID file is missing or its pid is dead — but a listener may still be
|
|
1161
|
+
// bound by an orphan. Try to locate it by port and kill that instead.
|
|
1162
|
+
if (await isDesktopPortBound(DESKTOP_HTTPS_PORT) || await isDesktopPortBound(DESKTOP_HTTP_PORT)) {
|
|
1163
|
+
pid = findPidByPort(DESKTOP_HTTPS_PORT) ?? findPidByPort(DESKTOP_HTTP_PORT)
|
|
1164
|
+
if (!pid) {
|
|
1165
|
+
console.log(`Desktop proxy ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} are in use but PID cannot be resolved. Stop the owning process manually.`)
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
console.log(`Recovered orphan desktop proxy pid=${pid} from listener.`)
|
|
1169
|
+
} else {
|
|
1170
|
+
console.log('Desktop proxy is not running.')
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
process.kill(pid, 'SIGTERM')
|
|
1176
|
+
// Wait up to 3s for graceful exit
|
|
1177
|
+
for (let i = 0; i < 30; i++) {
|
|
1178
|
+
try { process.kill(pid, 0) } catch { break }
|
|
1179
|
+
await new Promise(r => setTimeout(r, 100))
|
|
1180
|
+
}
|
|
1181
|
+
// Force kill if still alive
|
|
1182
|
+
try { process.kill(pid, 'SIGKILL') } catch {}
|
|
1183
|
+
try { fs.unlinkSync(DESKTOP_PID_FILE) } catch {}
|
|
1184
|
+
console.log(`Desktop proxy stopped (was pid=${pid}).`)
|
|
1185
|
+
} catch (e) {
|
|
1186
|
+
console.error(`Failed to stop desktop proxy: ${e.message}`)
|
|
1187
|
+
process.exit(1)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async function desktopProxyStatus() {
|
|
1192
|
+
let pid = readDesktopPid()
|
|
1193
|
+
// Fallback when PID file is stale: probe the desktop ports. If the listener
|
|
1194
|
+
// answers, the proxy IS running — just under a different PID than the one
|
|
1195
|
+
// recorded. This is the common shape after an ungraceful restart.
|
|
1196
|
+
if (!pid) {
|
|
1197
|
+
const bound = (await isDesktopPortBound(DESKTOP_HTTPS_PORT))
|
|
1198
|
+
|| (await isDesktopPortBound(DESKTOP_HTTP_PORT))
|
|
1199
|
+
if (!bound) {
|
|
1200
|
+
console.log('Desktop proxy is NOT running.')
|
|
1201
|
+
console.log('Start it with: squeezr desktop start')
|
|
1202
|
+
return
|
|
1203
|
+
}
|
|
1204
|
+
const orphanPid = findPidByPort(DESKTOP_HTTPS_PORT) ?? findPidByPort(DESKTOP_HTTP_PORT)
|
|
1205
|
+
if (orphanPid) {
|
|
1206
|
+
try { fs.writeFileSync(DESKTOP_PID_FILE, String(orphanPid), 'utf-8') } catch {}
|
|
1207
|
+
pid = orphanPid
|
|
1208
|
+
console.log(`Desktop proxy is running (pid=${pid}, recovered from orphan listener).`)
|
|
1209
|
+
} else {
|
|
1210
|
+
console.log(`Desktop proxy listener is bound on ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} but its PID could not be resolved.`)
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
console.log(`Desktop proxy is running (pid=${pid}).`)
|
|
1214
|
+
}
|
|
1215
|
+
console.log(` HTTPS: https://127.0.0.1:${DESKTOP_HTTPS_PORT} (Claude Desktop)`)
|
|
1216
|
+
console.log(` HTTP: http://127.0.0.1:${DESKTOP_HTTP_PORT} (Codex Desktop)`)
|
|
1217
|
+
console.log(` Log: ${DESKTOP_LOG_FILE}`)
|
|
1218
|
+
// Surface the activation state of the Claude Desktop hosts redirect — the
|
|
1219
|
+
// most common reason "nothing appears in the logs" even when this listener
|
|
1220
|
+
// is bound is that the user never ran `enable-claude-desktop`, so Claude
|
|
1221
|
+
// Desktop's traffic goes directly to api.anthropic.com.
|
|
1222
|
+
const hostsActive = await isClaudeDesktopHostsActive()
|
|
1223
|
+
if (hostsActive) {
|
|
1224
|
+
console.log(``)
|
|
1225
|
+
console.log(` Claude Desktop interception: ACTIVE (hosts redirect set)`)
|
|
1226
|
+
} else {
|
|
1227
|
+
console.log(``)
|
|
1228
|
+
console.log(` Claude Desktop interception: NOT SET`)
|
|
1229
|
+
console.log(` → Run as admin: squeezr enable-claude-desktop`)
|
|
1230
|
+
console.log(` Without this, Claude Desktop talks to api.anthropic.com directly`)
|
|
1231
|
+
console.log(` and no traffic will reach this proxy.`)
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Best-effort detection of whether the hosts file currently redirects
|
|
1236
|
+
// api.anthropic.com to 127.0.0.1 (the prerequisite for Claude Desktop
|
|
1237
|
+
// traffic to reach the desktop proxy).
|
|
1238
|
+
async function isClaudeDesktopHostsActive() {
|
|
1239
|
+
try {
|
|
1240
|
+
const hostsPath = process.platform === 'win32'
|
|
1241
|
+
? 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
|
1242
|
+
: '/etc/hosts'
|
|
1243
|
+
const txt = fs.readFileSync(hostsPath, 'utf-8')
|
|
1244
|
+
return /^\s*127\.0\.0\.1\s+api\.anthropic\.com\b/m.test(txt)
|
|
1245
|
+
|| txt.includes('# squeezr-claude-desktop BEGIN')
|
|
1246
|
+
} catch { return false }
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// ── Codex Desktop config helper ──────────────────────────────────────────────
|
|
1250
|
+
// Writes openai_base_url to ~/.codex/config.toml so Codex Desktop routes
|
|
1251
|
+
// through Squeezr's HTTP proxy (no MITM needed — uses standard base URL).
|
|
1252
|
+
|
|
1253
|
+
function configureCodexDesktop(port) {
|
|
1254
|
+
const codexDir = path.join(os.homedir(), '.codex')
|
|
1255
|
+
const codexToml = path.join(codexDir, 'config.toml')
|
|
1256
|
+
const url = `http://localhost:${port}/v1`
|
|
1257
|
+
const marker = 'openai_base_url'
|
|
1258
|
+
const line = `openai_base_url = "${url}"`
|
|
1259
|
+
|
|
1260
|
+
try {
|
|
1261
|
+
fs.mkdirSync(codexDir, { recursive: true })
|
|
1262
|
+
if (fs.existsSync(codexToml)) {
|
|
1263
|
+
let content = fs.readFileSync(codexToml, 'utf-8')
|
|
1264
|
+
// Match openai_base_url = "anything" — including empty "" which Codex Desktop
|
|
1265
|
+
// sometimes writes back to its config (bug we observed in the wild).
|
|
1266
|
+
// Force-overwrite any existing value, even if empty.
|
|
1267
|
+
const re = /openai_base_url\s*=\s*"[^"]*"/
|
|
1268
|
+
if (re.test(content)) {
|
|
1269
|
+
const before = content.match(re)?.[0] ?? ''
|
|
1270
|
+
content = content.replace(re, line)
|
|
1271
|
+
fs.writeFileSync(codexToml, content)
|
|
1272
|
+
if (before === `openai_base_url = ""`) {
|
|
1273
|
+
console.log(` [ok] Codex Desktop: FIXED empty openai_base_url in ${codexToml}`)
|
|
1274
|
+
} else {
|
|
1275
|
+
console.log(` [ok] Codex Desktop: updated ${codexToml}`)
|
|
1276
|
+
}
|
|
1277
|
+
} else {
|
|
1278
|
+
fs.appendFileSync(codexToml, `\n# Squeezr: route Codex Desktop through the compression proxy\n${line}\n`)
|
|
1279
|
+
console.log(` [ok] Codex Desktop: configured ${codexToml}`)
|
|
1280
|
+
}
|
|
1281
|
+
} else {
|
|
1282
|
+
fs.writeFileSync(codexToml, `# Squeezr: route Codex Desktop through the compression proxy\n${line}\n`)
|
|
1283
|
+
console.log(` [ok] Codex Desktop: created ${codexToml}`)
|
|
1284
|
+
}
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
console.log(` [warn] Codex Desktop config: ${err.message}`)
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
815
1290
|
// ── squeezr setup ─────────────────────────────────────────────────────────────
|
|
816
1291
|
|
|
817
|
-
function setupWindows() {
|
|
1292
|
+
async function setupWindows() {
|
|
818
1293
|
const squeezrBin = process.argv[1]
|
|
819
1294
|
const nodeExe = process.execPath
|
|
820
1295
|
const distIndex = path.join(ROOT, 'dist', 'index.js')
|
|
@@ -846,7 +1321,15 @@ function setupWindows() {
|
|
|
846
1321
|
}
|
|
847
1322
|
}
|
|
848
1323
|
|
|
849
|
-
// 1b.
|
|
1324
|
+
// 1b. Configure Codex Desktop (~/.codex/config.toml → openai_base_url)
|
|
1325
|
+
// On Windows, ANTHROPIC_BASE_URL from setx is already visible to all GUI apps
|
|
1326
|
+
// (including Claude Desktop) since user-level env vars propagate to new processes.
|
|
1327
|
+
configureCodexDesktop(port)
|
|
1328
|
+
|
|
1329
|
+
// 1c. Register MCP server in Claude Desktop + Codex Desktop automatically
|
|
1330
|
+
await mcpInstall()
|
|
1331
|
+
|
|
1332
|
+
// 1c. Install PowerShell wrapper so env vars auto-refresh after start/setup/update
|
|
850
1333
|
installShellWrapper()
|
|
851
1334
|
|
|
852
1335
|
// 2. Auto-start: try NSSM (Windows service, survives crashes) → fallback to Task Scheduler
|
|
@@ -984,8 +1467,15 @@ function setupWindows() {
|
|
|
984
1467
|
Done!
|
|
985
1468
|
|
|
986
1469
|
Squeezr is running on http://localhost:${port}
|
|
987
|
-
MITM proxy on http://localhost:${mitmPort} (Codex TLS interception)
|
|
988
|
-
|
|
1470
|
+
MITM proxy on http://localhost:${mitmPort} (Codex CLI TLS interception)
|
|
1471
|
+
|
|
1472
|
+
Configured:
|
|
1473
|
+
Claude Code ANTHROPIC_BASE_URL=http://localhost:${port}
|
|
1474
|
+
Claude Desktop same — setx env var is visible to all GUI apps
|
|
1475
|
+
Codex Desktop ~/.codex/config.toml openai_base_url set
|
|
1476
|
+
Codex CLI HTTPS_PROXY=http://localhost:${mitmPort} codex (per-session)
|
|
1477
|
+
Aider / OpenCode ANTHROPIC_BASE_URL + openai_base_url set
|
|
1478
|
+
Gemini CLI GEMINI_API_BASE_URL=http://localhost:${port}
|
|
989
1479
|
|
|
990
1480
|
squeezr status — check it's running
|
|
991
1481
|
squeezr gain — see token savings
|
|
@@ -993,7 +1483,7 @@ Done!
|
|
|
993
1483
|
}
|
|
994
1484
|
}
|
|
995
1485
|
|
|
996
|
-
function setupUnix() {
|
|
1486
|
+
async function setupUnix() {
|
|
997
1487
|
const squeezrBin = process.argv[1]
|
|
998
1488
|
const nodeExe = process.execPath
|
|
999
1489
|
const platform = process.platform
|
|
@@ -1077,7 +1567,57 @@ function setupUnix() {
|
|
|
1077
1567
|
console.log(` [ok] Env vars + auto-heal updated in ${profile}`)
|
|
1078
1568
|
}
|
|
1079
1569
|
|
|
1080
|
-
//
|
|
1570
|
+
// 2. Configure Codex Desktop + Claude Desktop
|
|
1571
|
+
// Codex Desktop reads ~/.codex/config.toml (openai_base_url key).
|
|
1572
|
+
configureCodexDesktop(port)
|
|
1573
|
+
|
|
1574
|
+
// Register MCP server in Claude Desktop and Codex Desktop automatically
|
|
1575
|
+
await mcpInstall()
|
|
1576
|
+
|
|
1577
|
+
// Claude Desktop (GUI app) does not read shell env vars.
|
|
1578
|
+
// macOS: inject via a launchd env-setter plist (persists across reboots).
|
|
1579
|
+
// Linux: write to ~/.config/environment.d/ (systemd user env, read by GUI apps).
|
|
1580
|
+
if (platform === 'darwin') {
|
|
1581
|
+
const envPlistDir = path.join(os.homedir(), 'Library', 'LaunchAgents')
|
|
1582
|
+
const envPlistPath = path.join(envPlistDir, 'com.squeezr.env.plist')
|
|
1583
|
+
fs.mkdirSync(envPlistDir, { recursive: true })
|
|
1584
|
+
const envVars = [
|
|
1585
|
+
['ANTHROPIC_BASE_URL', `http://localhost:${port}`],
|
|
1586
|
+
['GEMINI_API_BASE_URL', `http://localhost:${port}`],
|
|
1587
|
+
['NODE_EXTRA_CA_CERTS', bundlePath],
|
|
1588
|
+
]
|
|
1589
|
+
const envSetCmds = envVars.map(([k, v]) => `launchctl setenv ${k} "${v}"`).join(' && ')
|
|
1590
|
+
fs.writeFileSync(envPlistPath, `<?xml version="1.0" encoding="UTF-8"?>
|
|
1591
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1592
|
+
<plist version="1.0">
|
|
1593
|
+
<dict>
|
|
1594
|
+
<key>Label</key><string>com.squeezr.env</string>
|
|
1595
|
+
<key>ProgramArguments</key>
|
|
1596
|
+
<array><string>/bin/sh</string><string>-c</string><string>${envSetCmds}</string></array>
|
|
1597
|
+
<key>RunAtLoad</key><true/>
|
|
1598
|
+
</dict>
|
|
1599
|
+
</plist>`)
|
|
1600
|
+
try {
|
|
1601
|
+
execSync(`launchctl unload "${envPlistPath}" 2>/dev/null; launchctl load -w "${envPlistPath}"`, { stdio: 'pipe' })
|
|
1602
|
+
console.log(` [ok] Claude Desktop: env vars set via launchctl (visible to all GUI apps)`)
|
|
1603
|
+
} catch {
|
|
1604
|
+
console.log(` [warn] Claude Desktop: launchctl env plist failed — restart Claude Desktop manually after setup`)
|
|
1605
|
+
}
|
|
1606
|
+
} else {
|
|
1607
|
+
// Linux: systemd user environment.d — read by all user processes incl. GUI apps
|
|
1608
|
+
const envDDir = path.join(os.homedir(), '.config', 'environment.d')
|
|
1609
|
+
const envDPath = path.join(envDDir, 'squeezr.conf')
|
|
1610
|
+
fs.mkdirSync(envDDir, { recursive: true })
|
|
1611
|
+
fs.writeFileSync(envDPath, [
|
|
1612
|
+
`# Squeezr — visible to all GUI apps (Claude Desktop, etc.)`,
|
|
1613
|
+
`ANTHROPIC_BASE_URL=http://localhost:${port}`,
|
|
1614
|
+
`GEMINI_API_BASE_URL=http://localhost:${port}`,
|
|
1615
|
+
`NODE_EXTRA_CA_CERTS=${bundlePath}`,
|
|
1616
|
+
].join('\n') + '\n')
|
|
1617
|
+
console.log(` [ok] Claude Desktop: env vars written to ${envDPath} (effective after next login)`)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// 3a. macOS — launchd
|
|
1081
1621
|
if (platform === 'darwin') {
|
|
1082
1622
|
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents')
|
|
1083
1623
|
const plistPath = path.join(plistDir, 'com.squeezr.plist')
|
|
@@ -1126,7 +1666,7 @@ function setupUnix() {
|
|
|
1126
1666
|
}
|
|
1127
1667
|
})
|
|
1128
1668
|
|
|
1129
|
-
//
|
|
1669
|
+
// 3b. Linux — systemd
|
|
1130
1670
|
} else {
|
|
1131
1671
|
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user')
|
|
1132
1672
|
const servicePath = path.join(serviceDir, 'squeezr.service')
|
|
@@ -1156,12 +1696,17 @@ WantedBy=default.target
|
|
|
1156
1696
|
console.log(`
|
|
1157
1697
|
Done!
|
|
1158
1698
|
|
|
1159
|
-
Squeezr is running on http://localhost
|
|
1160
|
-
All CLIs (Claude Code, Codex, Aider, Gemini, Ollama) are configured.
|
|
1699
|
+
Squeezr is running on http://localhost:${port}
|
|
1161
1700
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1701
|
+
Configured:
|
|
1702
|
+
Claude Code ANTHROPIC_BASE_URL=http://localhost:${port}
|
|
1703
|
+
Claude Desktop ${platform === 'darwin' ? 'env vars set via launchctl (restart app once)' : 'env vars in ~/.config/environment.d/ (re-login to activate)'}
|
|
1704
|
+
Codex Desktop ~/.codex/config.toml openai_base_url set
|
|
1705
|
+
Codex CLI HTTPS_PROXY=http://localhost:${mitmPort} codex (per-session)
|
|
1706
|
+
Aider / OpenCode ANTHROPIC_BASE_URL + openai_base_url set
|
|
1707
|
+
Gemini CLI GEMINI_API_BASE_URL=http://localhost:${port}
|
|
1164
1708
|
|
|
1709
|
+
Run: source ${profile} (or open a new terminal)
|
|
1165
1710
|
squeezr status — check it's running
|
|
1166
1711
|
squeezr gain — see token savings
|
|
1167
1712
|
`)
|
|
@@ -1181,7 +1726,7 @@ function isWSL() {
|
|
|
1181
1726
|
|
|
1182
1727
|
// ── squeezr setup — WSL2 ────────────────────────────────────────────────────
|
|
1183
1728
|
|
|
1184
|
-
function setupWSL() {
|
|
1729
|
+
async function setupWSL() {
|
|
1185
1730
|
const nodeExe = process.execPath
|
|
1186
1731
|
const distIndex = path.join(ROOT, 'dist', 'index.js')
|
|
1187
1732
|
|
|
@@ -1257,6 +1802,7 @@ function setupWSL() {
|
|
|
1257
1802
|
}
|
|
1258
1803
|
|
|
1259
1804
|
// 2. Set Windows env vars via setx.exe (so Windows-launched CLIs see them)
|
|
1805
|
+
// ANTHROPIC_BASE_URL via setx is also visible to Claude Desktop (GUI app).
|
|
1260
1806
|
const setxExe = '/mnt/c/Windows/System32/setx.exe'
|
|
1261
1807
|
const winVars = {
|
|
1262
1808
|
ANTHROPIC_BASE_URL: 'http://localhost:8080',
|
|
@@ -1276,6 +1822,37 @@ function setupWSL() {
|
|
|
1276
1822
|
console.log(' [skip] setx.exe not found — Windows env vars not set')
|
|
1277
1823
|
}
|
|
1278
1824
|
|
|
1825
|
+
// 3. Configure Codex Desktop + MCP
|
|
1826
|
+
// WSL-side ~/.codex/config.toml (for Codex Desktop running in WSL)
|
|
1827
|
+
configureCodexDesktop(port)
|
|
1828
|
+
|
|
1829
|
+
// Register MCP server in Claude Desktop and Codex Desktop automatically
|
|
1830
|
+
await mcpInstall()
|
|
1831
|
+
// Windows-side %USERPROFILE%\.codex\config.toml (for Codex Desktop on Windows)
|
|
1832
|
+
try {
|
|
1833
|
+
const winHome = execSync('cmd.exe /c echo %USERPROFILE%', { stdio: 'pipe' }).toString().trim().replace(/\r/g, '')
|
|
1834
|
+
const winCodexDir = winHome + '\\.codex'
|
|
1835
|
+
const winMountedDir = winCodexDir.replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_, d) => `/mnt/${d.toLowerCase()}`)
|
|
1836
|
+
const winMountedToml = winMountedDir + '/config.toml'
|
|
1837
|
+
const winUrl = `http://localhost:${port}/v1`
|
|
1838
|
+
const winLine = `openai_base_url = "${winUrl}"`
|
|
1839
|
+
fs.mkdirSync(winMountedDir, { recursive: true })
|
|
1840
|
+
if (fs.existsSync(winMountedToml)) {
|
|
1841
|
+
let content = fs.readFileSync(winMountedToml, 'utf-8')
|
|
1842
|
+
if (content.includes('openai_base_url')) {
|
|
1843
|
+
content = content.replace(/openai_base_url\s*=\s*"[^"]*"/, winLine)
|
|
1844
|
+
fs.writeFileSync(winMountedToml, content)
|
|
1845
|
+
} else {
|
|
1846
|
+
fs.appendFileSync(winMountedToml, `\n# Squeezr\n${winLine}\n`)
|
|
1847
|
+
}
|
|
1848
|
+
} else {
|
|
1849
|
+
fs.writeFileSync(winMountedToml, `# Squeezr\n${winLine}\n`)
|
|
1850
|
+
}
|
|
1851
|
+
console.log(` [ok] Codex Desktop (Windows): ${winCodexDir}\\config.toml`)
|
|
1852
|
+
} catch {
|
|
1853
|
+
console.log(` [skip] Codex Desktop (Windows): could not write config`)
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1279
1856
|
// 3. Auto-start: try systemd first (WSL2 with systemd enabled), fallback to
|
|
1280
1857
|
// Windows Task Scheduler, then plain background process
|
|
1281
1858
|
let autoStartDone = false
|
|
@@ -1350,7 +1927,14 @@ WantedBy=default.target
|
|
|
1350
1927
|
Done!
|
|
1351
1928
|
|
|
1352
1929
|
Squeezr is running on http://localhost:${setupPort}
|
|
1353
|
-
|
|
1930
|
+
|
|
1931
|
+
Configured:
|
|
1932
|
+
Claude Code ANTHROPIC_BASE_URL=http://localhost:${setupPort}
|
|
1933
|
+
Claude Desktop Windows setx env var set (restart app once to pick it up)
|
|
1934
|
+
Codex Desktop ~/.codex/config.toml + Windows %USERPROFILE%\\.codex\\config.toml
|
|
1935
|
+
Codex CLI HTTPS_PROXY=http://localhost:${setupMitmPort} codex (per-session)
|
|
1936
|
+
Aider / OpenCode ANTHROPIC_BASE_URL + openai_base_url set
|
|
1937
|
+
Gemini CLI GEMINI_API_BASE_URL=http://localhost:${setupPort}
|
|
1354
1938
|
|
|
1355
1939
|
Windows env vars are set (effective in new terminals immediately).
|
|
1356
1940
|
WSL env vars added to ${profile}.
|
|
@@ -1624,12 +2208,24 @@ switch (command) {
|
|
|
1624
2208
|
case 'uninstall':
|
|
1625
2209
|
await uninstall()
|
|
1626
2210
|
break
|
|
2211
|
+
case 'enable-claude-desktop':
|
|
2212
|
+
case 'disable-claude-desktop':
|
|
2213
|
+
await toggleClaudeDesktopIntercept(command === 'enable-claude-desktop')
|
|
2214
|
+
break
|
|
2215
|
+
case 'desktop': {
|
|
2216
|
+
const sub = args[1] ?? 'status'
|
|
2217
|
+
if (sub === 'start') await desktopProxyStart()
|
|
2218
|
+
else if (sub === 'stop') await desktopProxyStop()
|
|
2219
|
+
else if (sub === 'status') await desktopProxyStatus()
|
|
2220
|
+
else { console.error(`Unknown desktop subcommand: ${sub}. Use start|stop|status`); process.exit(1) }
|
|
2221
|
+
break
|
|
2222
|
+
}
|
|
1627
2223
|
case 'config':
|
|
1628
2224
|
showConfig()
|
|
1629
2225
|
break
|
|
1630
2226
|
|
|
1631
2227
|
case 'mcp': {
|
|
1632
|
-
const subCmd = args[
|
|
2228
|
+
const subCmd = args[1] ?? 'install'
|
|
1633
2229
|
if (subCmd === 'uninstall') await mcpUninstall()
|
|
1634
2230
|
else await mcpInstall()
|
|
1635
2231
|
break
|