squeezr-ai 1.46.3 → 1.80.7
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 +199 -315
- package/bin/squeezr.js +2535 -2251
- package/dist/__tests__/aiRateLimit.test.d.ts +1 -0
- package/dist/__tests__/aiRateLimit.test.js +20 -0
- package/dist/__tests__/attachmentDedup.test.d.ts +1 -0
- package/dist/__tests__/attachmentDedup.test.js +89 -0
- package/dist/__tests__/compressibilityProbe.test.d.ts +1 -0
- package/dist/__tests__/compressibilityProbe.test.js +45 -0
- package/dist/__tests__/compressionGuard.test.d.ts +1 -0
- package/dist/__tests__/compressionGuard.test.js +57 -0
- package/dist/__tests__/compressor.test.js +104 -51
- package/dist/__tests__/diffRead.test.d.ts +1 -0
- package/dist/__tests__/diffRead.test.js +83 -0
- package/dist/__tests__/glossaryStore.test.d.ts +1 -0
- package/dist/__tests__/glossaryStore.test.js +37 -0
- package/dist/__tests__/glossarySub.test.d.ts +1 -0
- package/dist/__tests__/glossarySub.test.js +162 -0
- package/dist/__tests__/imageDedup.test.d.ts +1 -0
- package/dist/__tests__/imageDedup.test.js +80 -0
- package/dist/__tests__/largeBlock.test.d.ts +1 -0
- package/dist/__tests__/largeBlock.test.js +35 -0
- package/dist/__tests__/mcpFilter.test.d.ts +1 -0
- package/dist/__tests__/mcpFilter.test.js +87 -0
- package/dist/__tests__/newFeatures.test.d.ts +1 -0
- package/dist/__tests__/newFeatures.test.js +124 -0
- package/dist/__tests__/qualityHarness.test.d.ts +1 -0
- package/dist/__tests__/qualityHarness.test.js +98 -0
- package/dist/__tests__/rateLimitHeaders.test.js +6 -0
- package/dist/__tests__/requestCapture.test.d.ts +1 -0
- package/dist/__tests__/requestCapture.test.js +37 -0
- package/dist/__tests__/skillDedup.test.d.ts +1 -0
- package/dist/__tests__/skillDedup.test.js +57 -0
- package/dist/__tests__/staleTurns.test.d.ts +1 -0
- package/dist/__tests__/staleTurns.test.js +113 -0
- package/dist/__tests__/structuredGuard.test.d.ts +1 -0
- package/dist/__tests__/structuredGuard.test.js +72 -0
- package/dist/__tests__/toolDescComp.test.d.ts +1 -0
- package/dist/__tests__/toolDescComp.test.js +157 -0
- package/dist/__tests__/toolResultDedup.test.d.ts +1 -0
- package/dist/__tests__/toolResultDedup.test.js +40 -0
- package/dist/aiRateLimit.d.ts +19 -0
- package/dist/aiRateLimit.js +35 -0
- package/dist/aiToggle.d.ts +14 -0
- package/dist/aiToggle.js +53 -0
- package/dist/attachmentCompress.d.ts +9 -0
- package/dist/attachmentCompress.js +211 -0
- package/dist/attachmentDedup.d.ts +9 -0
- package/dist/attachmentDedup.js +89 -0
- package/dist/bypass.d.ts +6 -3
- package/dist/bypass.js +37 -5
- package/dist/cache.d.ts +3 -0
- package/dist/cache.js +10 -0
- package/dist/circuitBreaker.d.ts +4 -2
- package/dist/circuitBreaker.js +6 -3
- package/dist/compressibilityProbe.d.ts +8 -0
- package/dist/compressibilityProbe.js +47 -0
- package/dist/compressionGuard.d.ts +31 -0
- package/dist/compressionGuard.js +101 -0
- package/dist/compressor.d.ts +51 -1
- package/dist/compressor.js +599 -73
- package/dist/config.d.ts +21 -1
- package/dist/config.js +64 -4
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.js +621 -116
- package/dist/diffRead.d.ts +9 -0
- package/dist/diffRead.js +149 -0
- package/dist/expand.d.ts +2 -0
- package/dist/expand.js +6 -0
- package/dist/glossaryStore.d.ts +28 -0
- package/dist/glossaryStore.js +131 -0
- package/dist/glossarySub.d.ts +38 -0
- package/dist/glossarySub.js +123 -0
- package/dist/history.d.ts +35 -1
- package/dist/history.js +31 -5
- package/dist/identGlossary.d.ts +20 -0
- package/dist/identGlossary.js +215 -0
- package/dist/imageDedup.d.ts +12 -0
- package/dist/imageDedup.js +98 -0
- package/dist/index.js +7 -0
- package/dist/limits.d.ts +5 -2
- package/dist/limits.js +47 -4
- package/dist/logFeed.d.ts +10 -0
- package/dist/logFeed.js +42 -0
- package/dist/mcpFilter.d.ts +43 -0
- package/dist/mcpFilter.js +89 -0
- package/dist/mcpToolFilter.d.ts +32 -0
- package/dist/mcpToolFilter.js +140 -0
- package/dist/probePort.js +5 -1
- package/dist/promptCache.d.ts +44 -0
- package/dist/promptCache.js +121 -0
- package/dist/qualityGovernor.d.ts +11 -0
- package/dist/qualityGovernor.js +69 -0
- package/dist/requestCapture.d.ts +21 -0
- package/dist/requestCapture.js +79 -0
- package/dist/semanticRead.d.ts +9 -0
- package/dist/semanticRead.js +188 -0
- package/dist/server.js +447 -46
- package/dist/sessionCache.js +9 -2
- package/dist/skillDedup.d.ts +5 -0
- package/dist/skillDedup.js +89 -0
- package/dist/staleTurnSummary.d.ts +9 -0
- package/dist/staleTurnSummary.js +110 -0
- package/dist/staleTurns.d.ts +14 -0
- package/dist/staleTurns.js +80 -0
- package/dist/stats.d.ts +16 -3
- package/dist/stats.js +157 -21
- package/dist/stockToolDescs.d.ts +12 -0
- package/dist/stockToolDescs.js +69 -0
- package/dist/structuredGuard.d.ts +25 -0
- package/dist/structuredGuard.js +116 -0
- package/dist/systemPrompt.js +6 -2
- package/dist/systemSectioning.d.ts +21 -0
- package/dist/systemSectioning.js +111 -0
- package/dist/toolDescComp.d.ts +30 -0
- package/dist/toolDescComp.js +81 -0
- package/dist/toolResultDedup.d.ts +9 -0
- package/dist/toolResultDedup.js +88 -0
- package/package.json +69 -66
- package/squeezr.toml +18 -1
package/bin/squeezr.js
CHANGED
|
@@ -1,2251 +1,2535 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawn, execSync } from 'child_process'
|
|
4
|
-
import http from 'http'
|
|
5
|
-
import path from 'path'
|
|
6
|
-
import fs from 'fs'
|
|
7
|
-
import os from 'os'
|
|
8
|
-
import { fileURLToPath } from 'url'
|
|
9
|
-
import { createRequire } from 'module'
|
|
10
|
-
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
const require = createRequire(import.meta.url)
|
|
13
|
-
const ROOT = path.join(__dirname, '..')
|
|
14
|
-
const pkg = require(path.join(ROOT, 'package.json'))
|
|
15
|
-
|
|
16
|
-
const args = process.argv.slice(2)
|
|
17
|
-
const command = args[0]
|
|
18
|
-
|
|
19
|
-
// ── update check (non-blocking) ───────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
const UPDATE_CHECK_FILE = path.join(os.homedir(), '.squeezr', 'update-check.json')
|
|
22
|
-
const UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
|
|
23
|
-
|
|
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
|
-
|
|
38
|
-
const updateCheckPromise = (async () => {
|
|
39
|
-
try {
|
|
40
|
-
// Read cached check
|
|
41
|
-
let cached = null
|
|
42
|
-
try { cached = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf-8')) } catch {}
|
|
43
|
-
if (cached && Date.now() - cached.checkedAt < UPDATE_CHECK_INTERVAL) {
|
|
44
|
-
return newerVersionOrNull(cached.latest)
|
|
45
|
-
}
|
|
46
|
-
// Fetch latest from npm (with timeout)
|
|
47
|
-
const { get } = await import('https')
|
|
48
|
-
const latest = await new Promise((resolve, reject) => {
|
|
49
|
-
const req = get('https://registry.npmjs.org/squeezr-ai/latest', { timeout: 3000 }, res => {
|
|
50
|
-
let data = ''
|
|
51
|
-
res.on('data', chunk => { data += chunk })
|
|
52
|
-
res.on('end', () => {
|
|
53
|
-
try { resolve(JSON.parse(data).version) } catch { resolve(null) }
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
req.on('error', () => resolve(null))
|
|
57
|
-
req.setTimeout(3000, () => { req.destroy(); resolve(null) })
|
|
58
|
-
})
|
|
59
|
-
if (!latest) return null
|
|
60
|
-
// Cache result
|
|
61
|
-
const dir = path.dirname(UPDATE_CHECK_FILE)
|
|
62
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
63
|
-
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ latest, checkedAt: Date.now() }))
|
|
64
|
-
return newerVersionOrNull(latest)
|
|
65
|
-
} catch { return null }
|
|
66
|
-
})()
|
|
67
|
-
|
|
68
|
-
async function showUpdateBanner() {
|
|
69
|
-
try {
|
|
70
|
-
const latest = await Promise.race([updateCheckPromise, new Promise(r => setTimeout(() => r(null), 500))])
|
|
71
|
-
if (latest) {
|
|
72
|
-
console.log('')
|
|
73
|
-
console.log(` ╭─────────────────────────────────────────────────────────╮`)
|
|
74
|
-
console.log(` │ Update available: v${pkg.version} → v${latest}${' '.repeat(Math.max(0, 30 - pkg.version.length - latest.length))}│`)
|
|
75
|
-
console.log(` │ Run: squeezr update │`)
|
|
76
|
-
console.log(` ╰─────────────────────────────────────────────────────────╯`)
|
|
77
|
-
}
|
|
78
|
-
} catch {}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getPortFromToml() {
|
|
82
|
-
try {
|
|
83
|
-
const toml = fs.readFileSync(path.join(ROOT, 'squeezr.toml'), 'utf-8')
|
|
84
|
-
const m = toml.match(/^port\s*=\s*(\d+)/m)
|
|
85
|
-
if (m) return parseInt(m[1])
|
|
86
|
-
} catch {}
|
|
87
|
-
return null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Runtime info written by src/index.ts after a successful listen(). Reflects
|
|
91
|
-
// the *actual* bound port, which may differ from squeezr.toml when findFreePort
|
|
92
|
-
// drifted because the configured port was occupied.
|
|
93
|
-
const RUNTIME_FILE = path.join(os.homedir(), '.squeezr', 'runtime.json')
|
|
94
|
-
|
|
95
|
-
function readRuntimeInfo() {
|
|
96
|
-
try { return JSON.parse(fs.readFileSync(RUNTIME_FILE, 'utf-8')) } catch { return null }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getMitmPort(port) {
|
|
100
|
-
const envMitm = process.env.SQUEEZR_MITM_PORT
|
|
101
|
-
if (envMitm) return parseInt(envMitm)
|
|
102
|
-
const runtime = readRuntimeInfo()
|
|
103
|
-
if (runtime && runtime.mitmPort) return runtime.mitmPort
|
|
104
|
-
try {
|
|
105
|
-
const toml = fs.readFileSync(path.join(ROOT, 'squeezr.toml'), 'utf-8')
|
|
106
|
-
const m = toml.match(/^mitm_port\s*=\s*(\d+)/m)
|
|
107
|
-
if (m) return parseInt(m[1])
|
|
108
|
-
} catch {}
|
|
109
|
-
return Number(port) + 1
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getPort() {
|
|
113
|
-
if (process.env.SQUEEZR_PORT) return parseInt(process.env.SQUEEZR_PORT)
|
|
114
|
-
const runtime = readRuntimeInfo()
|
|
115
|
-
if (runtime && runtime.port) return runtime.port
|
|
116
|
-
return getPortFromToml() || 8080
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Verifies that whatever is listening on `port` is actually a squeezr instance
|
|
121
|
-
* (by checking the magic `identity` field in /squeezr/health), not an unrelated
|
|
122
|
-
* HTTP service that happens to answer 200. Returns the parsed health JSON, or
|
|
123
|
-
* null if the port is free, unreachable, or owned by a foreign service.
|
|
124
|
-
*/
|
|
125
|
-
function probeSqueezr(port, timeoutMs = 1500) {
|
|
126
|
-
return new Promise(resolve => {
|
|
127
|
-
const req = http.get({ host: '127.0.0.1', port, path: '/squeezr/health' }, res => {
|
|
128
|
-
let data = ''
|
|
129
|
-
res.on('data', chunk => { data += chunk })
|
|
130
|
-
res.on('end', () => {
|
|
131
|
-
if (res.statusCode !== 200) return resolve(null)
|
|
132
|
-
try {
|
|
133
|
-
const json = JSON.parse(data)
|
|
134
|
-
if (json && json.identity === 'squeezr') return resolve(json)
|
|
135
|
-
} catch {}
|
|
136
|
-
resolve(null)
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
req.on('error', () => resolve(null))
|
|
140
|
-
req.setTimeout(timeoutMs, () => { req.destroy(); resolve(null) })
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Install/update shell wrapper functions so env vars are auto-refreshed
|
|
146
|
-
* after squeezr start/setup/update (child processes can't modify parent env).
|
|
147
|
-
*/
|
|
148
|
-
function installShellWrapper() {
|
|
149
|
-
if (process.platform === 'win32') return installPowerShellWrapper()
|
|
150
|
-
if (isWSL() || process.platform === 'linux' || process.platform === 'darwin') return installBashWrapper()
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function installBashWrapper() {
|
|
154
|
-
const port = getPort()
|
|
155
|
-
const bundlePath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'bundle.crt')
|
|
156
|
-
const marker = '# squeezr shell wrapper'
|
|
157
|
-
const endMarker = '# end squeezr shell wrapper'
|
|
158
|
-
const wrapper = `${marker}
|
|
159
|
-
squeezr() {
|
|
160
|
-
command squeezr "$@"
|
|
161
|
-
case "$1" in
|
|
162
|
-
start|setup|update)
|
|
163
|
-
export ANTHROPIC_BASE_URL=http://localhost:${port}
|
|
164
|
-
export GEMINI_API_BASE_URL=http://localhost:${port}
|
|
165
|
-
export NODE_EXTRA_CA_CERTS=${bundlePath}
|
|
166
|
-
;;
|
|
167
|
-
esac
|
|
168
|
-
}
|
|
169
|
-
${endMarker}`
|
|
170
|
-
|
|
171
|
-
const profiles = [
|
|
172
|
-
path.join(os.homedir(), '.bashrc'),
|
|
173
|
-
path.join(os.homedir(), '.zshrc'),
|
|
174
|
-
]
|
|
175
|
-
let installed = false
|
|
176
|
-
for (const p of profiles) {
|
|
177
|
-
if (!fs.existsSync(p)) continue
|
|
178
|
-
try {
|
|
179
|
-
const content = fs.readFileSync(p, 'utf-8')
|
|
180
|
-
if (!content.includes(marker)) {
|
|
181
|
-
fs.appendFileSync(p, `\n${wrapper}\n`)
|
|
182
|
-
console.log(` [ok] Shell wrapper added to ${p}`)
|
|
183
|
-
installed = true
|
|
184
|
-
} else {
|
|
185
|
-
const updated = content.replace(new RegExp(`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), wrapper)
|
|
186
|
-
fs.writeFileSync(p, updated)
|
|
187
|
-
console.log(` [ok] Shell wrapper updated in ${p}`)
|
|
188
|
-
}
|
|
189
|
-
} catch {}
|
|
190
|
-
}
|
|
191
|
-
if (installed) {
|
|
192
|
-
console.log('')
|
|
193
|
-
console.log(' ╔═══════════════════════════════════════════════════════════════╗')
|
|
194
|
-
console.log(' ║ ONE-TIME SETUP: Close this terminal and open a new one. ║')
|
|
195
|
-
console.log(' ║ This loads the wrapper that auto-refreshes env vars. ║')
|
|
196
|
-
console.log(' ║ After that, you will NEVER need to do this again. ║')
|
|
197
|
-
console.log(' ╚═══════════════════════════════════════════════════════════════╝')
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function installPowerShellWrapper() {
|
|
202
|
-
try {
|
|
203
|
-
const psProfilePath = execSync('powershell -NoProfile -Command "[Environment]::GetFolderPath(\'MyDocuments\') + \'\\WindowsPowerShell\\Microsoft.PowerShell_profile.ps1\'"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
204
|
-
const psProfileDir = path.dirname(psProfilePath)
|
|
205
|
-
if (!fs.existsSync(psProfileDir)) fs.mkdirSync(psProfileDir, { recursive: true })
|
|
206
|
-
const psMarker = '# squeezr wrapper'
|
|
207
|
-
const psLines = []
|
|
208
|
-
psLines.push(psMarker)
|
|
209
|
-
psLines.push('function squeezr {')
|
|
210
|
-
psLines.push(' & squeezr.cmd @args')
|
|
211
|
-
psLines.push(' if (@("start","setup","update") -contains $args[0]) {')
|
|
212
|
-
psLines.push(' foreach ($k in @("ANTHROPIC_BASE_URL","GEMINI_API_BASE_URL","NODE_EXTRA_CA_CERTS")) {')
|
|
213
|
-
psLines.push(' $val = [Environment]::GetEnvironmentVariable($k, "User")')
|
|
214
|
-
psLines.push(' if ($val) { [Environment]::SetEnvironmentVariable($k, $val, "Process") }')
|
|
215
|
-
psLines.push(' }')
|
|
216
|
-
psLines.push(' }')
|
|
217
|
-
psLines.push('}')
|
|
218
|
-
psLines.push('# end squeezr wrapper')
|
|
219
|
-
const psFunction = psLines.join('\r\n')
|
|
220
|
-
const existing = fs.existsSync(psProfilePath) ? fs.readFileSync(psProfilePath, 'utf-8') : ''
|
|
221
|
-
if (!existing.includes(psMarker)) {
|
|
222
|
-
fs.appendFileSync(psProfilePath, `\n${psFunction}\n`)
|
|
223
|
-
console.log(` [ok] PowerShell wrapper added to ${psProfilePath}`)
|
|
224
|
-
console.log('')
|
|
225
|
-
console.log(' ╔═══════════════════════════════════════════════════════════════╗')
|
|
226
|
-
console.log(' ║ ONE-TIME SETUP: Close this terminal and open a new one. ║')
|
|
227
|
-
console.log(' ║ This loads the wrapper that auto-refreshes env vars. ║')
|
|
228
|
-
console.log(' ║ After that, you will NEVER need to do this again. ║')
|
|
229
|
-
console.log(' ╚═══════════════════════════════════════════════════════════════╝')
|
|
230
|
-
} else {
|
|
231
|
-
const updated = existing.replace(/# squeezr wrapper[\s\S]*?# end squeezr wrapper/, psFunction)
|
|
232
|
-
fs.writeFileSync(psProfilePath, updated)
|
|
233
|
-
console.log(` [ok] PowerShell wrapper updated in ${psProfilePath}`)
|
|
234
|
-
}
|
|
235
|
-
} catch {
|
|
236
|
-
console.log(` [skip] PowerShell profile wrapper could not be installed`)
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const HELP = `
|
|
241
|
-
Squeezr v${pkg.version} — AI context compressor for Claude Code, Codex, Aider, Gemini CLI and Ollama
|
|
242
|
-
|
|
243
|
-
Usage:
|
|
244
|
-
squeezr Start the proxy (default)
|
|
245
|
-
squeezr start Start the proxy
|
|
246
|
-
squeezr setup One-time setup: auto-start on login + configure all CLIs
|
|
247
|
-
squeezr stop Stop the running proxy
|
|
248
|
-
squeezr
|
|
249
|
-
squeezr
|
|
250
|
-
squeezr gain
|
|
251
|
-
squeezr
|
|
252
|
-
squeezr
|
|
253
|
-
squeezr
|
|
254
|
-
squeezr
|
|
255
|
-
squeezr mcp
|
|
256
|
-
squeezr
|
|
257
|
-
squeezr
|
|
258
|
-
squeezr
|
|
259
|
-
squeezr
|
|
260
|
-
squeezr desktop
|
|
261
|
-
squeezr desktop
|
|
262
|
-
squeezr desktop
|
|
263
|
-
squeezr
|
|
264
|
-
squeezr bypass
|
|
265
|
-
squeezr bypass --
|
|
266
|
-
squeezr
|
|
267
|
-
|
|
268
|
-
squeezr
|
|
269
|
-
squeezr
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.log(`
|
|
304
|
-
console.log(`
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
console.log(`
|
|
332
|
-
console.log(`
|
|
333
|
-
console.log(`
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
req.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
execSync(`
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
try {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
console.log(`Squeezr
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
console.log(`
|
|
502
|
-
|
|
503
|
-
console.log(`
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
console.log(`
|
|
511
|
-
console.log(`
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
console.log(
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
console.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
console.log(
|
|
613
|
-
console.log('')
|
|
614
|
-
console.log('
|
|
615
|
-
console.log('
|
|
616
|
-
console.log('
|
|
617
|
-
console.log('
|
|
618
|
-
console.log('
|
|
619
|
-
console.log('
|
|
620
|
-
console.log('
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
path.join(os.homedir(), '.
|
|
633
|
-
|
|
634
|
-
path.join(os.homedir(), '.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
console.log(
|
|
672
|
-
console.log(`
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
tomlContent = tomlContent.replace(
|
|
695
|
-
} else {
|
|
696
|
-
tomlContent = `[proxy]\nport = ${finalPort}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
try { execSync(`setx
|
|
714
|
-
try { execSync(`setx
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
path.join(os.homedir(), '.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
`export
|
|
727
|
-
`export
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
try { execSync(`"${setx}"
|
|
751
|
-
try { execSync(`"${setx}"
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
process
|
|
759
|
-
process.env.
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
path.join(os.homedir(), '.
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
try { execSync('
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
//
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
try { execSync('
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
|
|
848
|
-
try {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
console.log(' [
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
if
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
console.
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
'
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
console.
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
console.
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
console.log('
|
|
1014
|
-
console.log('
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
return
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
//
|
|
1054
|
-
// `squeezr desktop
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
console.log(`
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
console.log(`Desktop proxy ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
console.log(`
|
|
1152
|
-
console.log(`
|
|
1153
|
-
console.log(
|
|
1154
|
-
console.log(`
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
console.log(
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
console.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
//
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
console.log(`Desktop proxy
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
console.log(`
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
console.log(
|
|
1228
|
-
|
|
1229
|
-
console.log(
|
|
1230
|
-
console.log(`
|
|
1231
|
-
console.log(`
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
//
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
const
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
if
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
console.log(` [ok] Codex Desktop:
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
console.log(` [
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
//
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
execSync(`nssm
|
|
1351
|
-
|
|
1352
|
-
execSync(`nssm
|
|
1353
|
-
execSync(`nssm set ${serviceName}
|
|
1354
|
-
execSync(`nssm set ${serviceName}
|
|
1355
|
-
execSync(`nssm set ${serviceName}
|
|
1356
|
-
execSync(`nssm set ${serviceName}
|
|
1357
|
-
execSync(`nssm set ${serviceName}
|
|
1358
|
-
execSync(`nssm set ${serviceName}
|
|
1359
|
-
execSync(`nssm
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
console.log(` [warn] NSSM
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
`$
|
|
1380
|
-
|
|
1381
|
-
`$
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
'',
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
console.log(` [
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
//
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
console.log(` [
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
`export
|
|
1510
|
-
`export
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
`#
|
|
1514
|
-
`#
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
`
|
|
1518
|
-
`
|
|
1519
|
-
`
|
|
1520
|
-
`
|
|
1521
|
-
`
|
|
1522
|
-
`
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
`export
|
|
1533
|
-
`export
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
path.join(os.homedir(), '.
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
//
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
['
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
<
|
|
1597
|
-
<key>
|
|
1598
|
-
</
|
|
1599
|
-
</
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
console.log(` [
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
`
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
<
|
|
1632
|
-
<key>
|
|
1633
|
-
<
|
|
1634
|
-
<key>
|
|
1635
|
-
<key>
|
|
1636
|
-
</
|
|
1637
|
-
</
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
console.log(` [
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
console.log(` [
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
console.log(` [
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
return
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
//
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
`export
|
|
1747
|
-
`export
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
`#
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
`
|
|
1754
|
-
`
|
|
1755
|
-
`
|
|
1756
|
-
`
|
|
1757
|
-
`
|
|
1758
|
-
`
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
`export
|
|
1768
|
-
`export
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
path.join(os.homedir(), '.
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
console.log(` [
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
//
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
fs.
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
console.log(` [
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
const
|
|
1865
|
-
fs.
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
`$
|
|
1895
|
-
|
|
1896
|
-
`$
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
//
|
|
1951
|
-
//
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
console.log(
|
|
1993
|
-
console.log(` ║
|
|
1994
|
-
console.log(`
|
|
1995
|
-
console.log(` ║
|
|
1996
|
-
console.log(` ║
|
|
1997
|
-
console.log(` ║
|
|
1998
|
-
console.log(` ║
|
|
1999
|
-
console.log(` ║
|
|
2000
|
-
console.log(` ║
|
|
2001
|
-
console.log(` ║
|
|
2002
|
-
console.log(` ║
|
|
2003
|
-
console.log(` ║
|
|
2004
|
-
console.log(` ║
|
|
2005
|
-
console.log(` ║
|
|
2006
|
-
console.log(`
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
console.error(`
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
console.log(
|
|
2145
|
-
console.log(
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
console.log(
|
|
2248
|
-
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, execSync } from 'child_process'
|
|
4
|
+
import http from 'http'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
import os from 'os'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import { createRequire } from 'module'
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const require = createRequire(import.meta.url)
|
|
13
|
+
const ROOT = path.join(__dirname, '..')
|
|
14
|
+
const pkg = require(path.join(ROOT, 'package.json'))
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2)
|
|
17
|
+
const command = args[0]
|
|
18
|
+
|
|
19
|
+
// ── update check (non-blocking) ───────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const UPDATE_CHECK_FILE = path.join(os.homedir(), '.squeezr', 'update-check.json')
|
|
22
|
+
const UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
|
|
23
|
+
|
|
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
|
+
|
|
38
|
+
const updateCheckPromise = (async () => {
|
|
39
|
+
try {
|
|
40
|
+
// Read cached check
|
|
41
|
+
let cached = null
|
|
42
|
+
try { cached = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf-8')) } catch {}
|
|
43
|
+
if (cached && Date.now() - cached.checkedAt < UPDATE_CHECK_INTERVAL) {
|
|
44
|
+
return newerVersionOrNull(cached.latest)
|
|
45
|
+
}
|
|
46
|
+
// Fetch latest from npm (with timeout)
|
|
47
|
+
const { get } = await import('https')
|
|
48
|
+
const latest = await new Promise((resolve, reject) => {
|
|
49
|
+
const req = get('https://registry.npmjs.org/squeezr-ai/latest', { timeout: 3000 }, res => {
|
|
50
|
+
let data = ''
|
|
51
|
+
res.on('data', chunk => { data += chunk })
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
try { resolve(JSON.parse(data).version) } catch { resolve(null) }
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
req.on('error', () => resolve(null))
|
|
57
|
+
req.setTimeout(3000, () => { req.destroy(); resolve(null) })
|
|
58
|
+
})
|
|
59
|
+
if (!latest) return null
|
|
60
|
+
// Cache result
|
|
61
|
+
const dir = path.dirname(UPDATE_CHECK_FILE)
|
|
62
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
63
|
+
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ latest, checkedAt: Date.now() }))
|
|
64
|
+
return newerVersionOrNull(latest)
|
|
65
|
+
} catch { return null }
|
|
66
|
+
})()
|
|
67
|
+
|
|
68
|
+
async function showUpdateBanner() {
|
|
69
|
+
try {
|
|
70
|
+
const latest = await Promise.race([updateCheckPromise, new Promise(r => setTimeout(() => r(null), 500))])
|
|
71
|
+
if (latest) {
|
|
72
|
+
console.log('')
|
|
73
|
+
console.log(` ╭─────────────────────────────────────────────────────────╮`)
|
|
74
|
+
console.log(` │ Update available: v${pkg.version} → v${latest}${' '.repeat(Math.max(0, 30 - pkg.version.length - latest.length))}│`)
|
|
75
|
+
console.log(` │ Run: squeezr update │`)
|
|
76
|
+
console.log(` ╰─────────────────────────────────────────────────────────╯`)
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getPortFromToml() {
|
|
82
|
+
try {
|
|
83
|
+
const toml = fs.readFileSync(path.join(ROOT, 'squeezr.toml'), 'utf-8')
|
|
84
|
+
const m = toml.match(/^port\s*=\s*(\d+)/m)
|
|
85
|
+
if (m) return parseInt(m[1])
|
|
86
|
+
} catch {}
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Runtime info written by src/index.ts after a successful listen(). Reflects
|
|
91
|
+
// the *actual* bound port, which may differ from squeezr.toml when findFreePort
|
|
92
|
+
// drifted because the configured port was occupied.
|
|
93
|
+
const RUNTIME_FILE = path.join(os.homedir(), '.squeezr', 'runtime.json')
|
|
94
|
+
|
|
95
|
+
function readRuntimeInfo() {
|
|
96
|
+
try { return JSON.parse(fs.readFileSync(RUNTIME_FILE, 'utf-8')) } catch { return null }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getMitmPort(port) {
|
|
100
|
+
const envMitm = process.env.SQUEEZR_MITM_PORT
|
|
101
|
+
if (envMitm) return parseInt(envMitm)
|
|
102
|
+
const runtime = readRuntimeInfo()
|
|
103
|
+
if (runtime && runtime.mitmPort) return runtime.mitmPort
|
|
104
|
+
try {
|
|
105
|
+
const toml = fs.readFileSync(path.join(ROOT, 'squeezr.toml'), 'utf-8')
|
|
106
|
+
const m = toml.match(/^mitm_port\s*=\s*(\d+)/m)
|
|
107
|
+
if (m) return parseInt(m[1])
|
|
108
|
+
} catch {}
|
|
109
|
+
return Number(port) + 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getPort() {
|
|
113
|
+
if (process.env.SQUEEZR_PORT) return parseInt(process.env.SQUEEZR_PORT)
|
|
114
|
+
const runtime = readRuntimeInfo()
|
|
115
|
+
if (runtime && runtime.port) return runtime.port
|
|
116
|
+
return getPortFromToml() || 8080
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Verifies that whatever is listening on `port` is actually a squeezr instance
|
|
121
|
+
* (by checking the magic `identity` field in /squeezr/health), not an unrelated
|
|
122
|
+
* HTTP service that happens to answer 200. Returns the parsed health JSON, or
|
|
123
|
+
* null if the port is free, unreachable, or owned by a foreign service.
|
|
124
|
+
*/
|
|
125
|
+
function probeSqueezr(port, timeoutMs = 1500) {
|
|
126
|
+
return new Promise(resolve => {
|
|
127
|
+
const req = http.get({ host: '127.0.0.1', port, path: '/squeezr/health' }, res => {
|
|
128
|
+
let data = ''
|
|
129
|
+
res.on('data', chunk => { data += chunk })
|
|
130
|
+
res.on('end', () => {
|
|
131
|
+
if (res.statusCode !== 200) return resolve(null)
|
|
132
|
+
try {
|
|
133
|
+
const json = JSON.parse(data)
|
|
134
|
+
if (json && json.identity === 'squeezr') return resolve(json)
|
|
135
|
+
} catch {}
|
|
136
|
+
resolve(null)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
req.on('error', () => resolve(null))
|
|
140
|
+
req.setTimeout(timeoutMs, () => { req.destroy(); resolve(null) })
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Install/update shell wrapper functions so env vars are auto-refreshed
|
|
146
|
+
* after squeezr start/setup/update (child processes can't modify parent env).
|
|
147
|
+
*/
|
|
148
|
+
function installShellWrapper() {
|
|
149
|
+
if (process.platform === 'win32') return installPowerShellWrapper()
|
|
150
|
+
if (isWSL() || process.platform === 'linux' || process.platform === 'darwin') return installBashWrapper()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function installBashWrapper() {
|
|
154
|
+
const port = getPort()
|
|
155
|
+
const bundlePath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'bundle.crt')
|
|
156
|
+
const marker = '# squeezr shell wrapper'
|
|
157
|
+
const endMarker = '# end squeezr shell wrapper'
|
|
158
|
+
const wrapper = `${marker}
|
|
159
|
+
squeezr() {
|
|
160
|
+
command squeezr "$@"
|
|
161
|
+
case "$1" in
|
|
162
|
+
start|setup|update)
|
|
163
|
+
export ANTHROPIC_BASE_URL=http://localhost:${port}
|
|
164
|
+
export GEMINI_API_BASE_URL=http://localhost:${port}
|
|
165
|
+
export NODE_EXTRA_CA_CERTS=${bundlePath}
|
|
166
|
+
;;
|
|
167
|
+
esac
|
|
168
|
+
}
|
|
169
|
+
${endMarker}`
|
|
170
|
+
|
|
171
|
+
const profiles = [
|
|
172
|
+
path.join(os.homedir(), '.bashrc'),
|
|
173
|
+
path.join(os.homedir(), '.zshrc'),
|
|
174
|
+
]
|
|
175
|
+
let installed = false
|
|
176
|
+
for (const p of profiles) {
|
|
177
|
+
if (!fs.existsSync(p)) continue
|
|
178
|
+
try {
|
|
179
|
+
const content = fs.readFileSync(p, 'utf-8')
|
|
180
|
+
if (!content.includes(marker)) {
|
|
181
|
+
fs.appendFileSync(p, `\n${wrapper}\n`)
|
|
182
|
+
console.log(` [ok] Shell wrapper added to ${p}`)
|
|
183
|
+
installed = true
|
|
184
|
+
} else {
|
|
185
|
+
const updated = content.replace(new RegExp(`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), wrapper)
|
|
186
|
+
fs.writeFileSync(p, updated)
|
|
187
|
+
console.log(` [ok] Shell wrapper updated in ${p}`)
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
if (installed) {
|
|
192
|
+
console.log('')
|
|
193
|
+
console.log(' ╔═══════════════════════════════════════════════════════════════╗')
|
|
194
|
+
console.log(' ║ ONE-TIME SETUP: Close this terminal and open a new one. ║')
|
|
195
|
+
console.log(' ║ This loads the wrapper that auto-refreshes env vars. ║')
|
|
196
|
+
console.log(' ║ After that, you will NEVER need to do this again. ║')
|
|
197
|
+
console.log(' ╚═══════════════════════════════════════════════════════════════╝')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function installPowerShellWrapper() {
|
|
202
|
+
try {
|
|
203
|
+
const psProfilePath = execSync('powershell -NoProfile -Command "[Environment]::GetFolderPath(\'MyDocuments\') + \'\\WindowsPowerShell\\Microsoft.PowerShell_profile.ps1\'"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
204
|
+
const psProfileDir = path.dirname(psProfilePath)
|
|
205
|
+
if (!fs.existsSync(psProfileDir)) fs.mkdirSync(psProfileDir, { recursive: true })
|
|
206
|
+
const psMarker = '# squeezr wrapper'
|
|
207
|
+
const psLines = []
|
|
208
|
+
psLines.push(psMarker)
|
|
209
|
+
psLines.push('function squeezr {')
|
|
210
|
+
psLines.push(' & squeezr.cmd @args')
|
|
211
|
+
psLines.push(' if (@("start","setup","update") -contains $args[0]) {')
|
|
212
|
+
psLines.push(' foreach ($k in @("ANTHROPIC_BASE_URL","GEMINI_API_BASE_URL","NODE_EXTRA_CA_CERTS")) {')
|
|
213
|
+
psLines.push(' $val = [Environment]::GetEnvironmentVariable($k, "User")')
|
|
214
|
+
psLines.push(' if ($val) { [Environment]::SetEnvironmentVariable($k, $val, "Process") }')
|
|
215
|
+
psLines.push(' }')
|
|
216
|
+
psLines.push(' }')
|
|
217
|
+
psLines.push('}')
|
|
218
|
+
psLines.push('# end squeezr wrapper')
|
|
219
|
+
const psFunction = psLines.join('\r\n')
|
|
220
|
+
const existing = fs.existsSync(psProfilePath) ? fs.readFileSync(psProfilePath, 'utf-8') : ''
|
|
221
|
+
if (!existing.includes(psMarker)) {
|
|
222
|
+
fs.appendFileSync(psProfilePath, `\n${psFunction}\n`)
|
|
223
|
+
console.log(` [ok] PowerShell wrapper added to ${psProfilePath}`)
|
|
224
|
+
console.log('')
|
|
225
|
+
console.log(' ╔═══════════════════════════════════════════════════════════════╗')
|
|
226
|
+
console.log(' ║ ONE-TIME SETUP: Close this terminal and open a new one. ║')
|
|
227
|
+
console.log(' ║ This loads the wrapper that auto-refreshes env vars. ║')
|
|
228
|
+
console.log(' ║ After that, you will NEVER need to do this again. ║')
|
|
229
|
+
console.log(' ╚═══════════════════════════════════════════════════════════════╝')
|
|
230
|
+
} else {
|
|
231
|
+
const updated = existing.replace(/# squeezr wrapper[\s\S]*?# end squeezr wrapper/, psFunction)
|
|
232
|
+
fs.writeFileSync(psProfilePath, updated)
|
|
233
|
+
console.log(` [ok] PowerShell wrapper updated in ${psProfilePath}`)
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
console.log(` [skip] PowerShell profile wrapper could not be installed`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const HELP = `
|
|
241
|
+
Squeezr v${pkg.version} — AI context compressor for Claude Code, Codex, Aider, Gemini CLI and Ollama
|
|
242
|
+
|
|
243
|
+
Usage:
|
|
244
|
+
squeezr Start the proxy (default)
|
|
245
|
+
squeezr start Start the proxy
|
|
246
|
+
squeezr setup One-time setup: auto-start on login + configure all CLIs
|
|
247
|
+
squeezr stop Stop the running proxy
|
|
248
|
+
squeezr restart Stop and restart the proxy (reloads config)
|
|
249
|
+
squeezr logs Show last 50 lines of the log file
|
|
250
|
+
squeezr gain Show token savings stats
|
|
251
|
+
squeezr gain --reset Reset saved stats
|
|
252
|
+
squeezr discover Show pattern coverage report (proxy must be running)
|
|
253
|
+
squeezr status Check if proxy is running
|
|
254
|
+
squeezr config Print config file path and current settings
|
|
255
|
+
squeezr mcp install Register Squeezr MCP server in Claude Code, Cursor, Windsurf & Cline
|
|
256
|
+
squeezr mcp uninstall Remove Squeezr MCP registration
|
|
257
|
+
squeezr ports Change HTTP and MITM proxy ports
|
|
258
|
+
squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
|
|
259
|
+
squeezr enable-claude-desktop Enable hosts-file redirect for Claude Desktop (admin once)
|
|
260
|
+
squeezr disable-claude-desktop Disable hosts-file redirect for Claude Desktop
|
|
261
|
+
squeezr desktop start Start SEPARATE proxy for Claude/Codex Desktop (ports 8443+8088)
|
|
262
|
+
squeezr desktop stop Stop the desktop proxy (does NOT affect main proxy)
|
|
263
|
+
squeezr desktop status Show desktop proxy status
|
|
264
|
+
squeezr bypass Toggle bypass mode (skip compression, keep logging)
|
|
265
|
+
squeezr bypass --on Enable bypass (disable compression)
|
|
266
|
+
squeezr bypass --off Disable bypass (resume compression)
|
|
267
|
+
squeezr zest Install Zest local AI compression model (guided wizard)
|
|
268
|
+
squeezr update Kill old processes, install latest from npm, restart
|
|
269
|
+
squeezr uninstall Remove Squeezr completely (env vars, CA, auto-start, logs)
|
|
270
|
+
squeezr version Print version
|
|
271
|
+
squeezr help Show this help
|
|
272
|
+
`
|
|
273
|
+
|
|
274
|
+
function runNode(script, extraArgs = []) {
|
|
275
|
+
const distPath = path.join(ROOT, 'dist', script)
|
|
276
|
+
if (!fs.existsSync(distPath)) {
|
|
277
|
+
console.error(`Error: ${distPath} not found. Run 'npm run build' first.`)
|
|
278
|
+
process.exit(1)
|
|
279
|
+
}
|
|
280
|
+
const child = spawn(process.execPath, [distPath, ...extraArgs], {
|
|
281
|
+
stdio: 'inherit',
|
|
282
|
+
cwd: ROOT,
|
|
283
|
+
})
|
|
284
|
+
child.on('exit', code => process.exit(code ?? 0))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function startDaemon() {
|
|
288
|
+
const distIndex = path.join(ROOT, 'dist', 'index.js')
|
|
289
|
+
if (!fs.existsSync(distIndex)) {
|
|
290
|
+
console.error(`Error: ${distIndex} not found. Run 'npm run build' first.`)
|
|
291
|
+
process.exit(1)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check if already running — and if the version matches. We use probeSqueezr
|
|
295
|
+
// (which validates the `identity` field) so we don't mistake an unrelated
|
|
296
|
+
// HTTP service squatting on this port for a real squeezr instance.
|
|
297
|
+
const port = getPort()
|
|
298
|
+
const running = await probeSqueezr(port)
|
|
299
|
+
const runningVersion = running ? running.version : null
|
|
300
|
+
if (runningVersion) {
|
|
301
|
+
if (runningVersion === pkg.version) {
|
|
302
|
+
const mitmPort = getMitmPort(port)
|
|
303
|
+
console.log(`Squeezr is already running (v${pkg.version})`)
|
|
304
|
+
console.log(` HTTP proxy (Claude/Aider/Gemini): http://localhost:${port}`)
|
|
305
|
+
console.log(` MITM proxy (Codex): http://localhost:${mitmPort}`)
|
|
306
|
+
console.log(` Dashboard: http://localhost:${port}/squeezr/dashboard`)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
// Version mismatch — old process from before npm update. Kill and restart.
|
|
310
|
+
console.log(`Squeezr v${runningVersion} is running but v${pkg.version} is installed. Restarting...`)
|
|
311
|
+
stopProxy()
|
|
312
|
+
// Wait for ports to free up
|
|
313
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Launch detached background process
|
|
317
|
+
const logDir = path.join(os.homedir(), '.squeezr')
|
|
318
|
+
const logFile = path.join(logDir, 'squeezr.log')
|
|
319
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
320
|
+
const logFd = fs.openSync(logFile, 'a')
|
|
321
|
+
const child = spawn(process.execPath, [distIndex], {
|
|
322
|
+
detached: true,
|
|
323
|
+
stdio: ['ignore', logFd, logFd],
|
|
324
|
+
windowsHide: true,
|
|
325
|
+
cwd: ROOT,
|
|
326
|
+
env: { ...process.env, SQUEEZR_DAEMON: '1' },
|
|
327
|
+
})
|
|
328
|
+
child.unref()
|
|
329
|
+
fs.closeSync(logFd)
|
|
330
|
+
const mitmPort = getMitmPort(port)
|
|
331
|
+
console.log(`Squeezr started (pid ${child.pid})`)
|
|
332
|
+
console.log(` HTTP proxy (Claude/Aider/Gemini): http://localhost:${port}`)
|
|
333
|
+
console.log(` MITM proxy (Codex): http://localhost:${mitmPort}`)
|
|
334
|
+
console.log(` Dashboard: http://localhost:${port}/squeezr/dashboard`)
|
|
335
|
+
console.log(` Logs: ${logFile}`)
|
|
336
|
+
|
|
337
|
+
// ── Also start the SEPARATE Desktop proxy (independent process) ────────────
|
|
338
|
+
// This is the proxy that serves Claude Desktop and Codex Desktop. It runs in
|
|
339
|
+
// its own Node process on ports 8443 + 8088. If it crashes, the main proxy
|
|
340
|
+
// (just started above) keeps running. Failures here are non-fatal — we just
|
|
341
|
+
// warn and continue.
|
|
342
|
+
try {
|
|
343
|
+
await desktopProxyStart()
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.warn(`Desktop proxy did not start: ${e?.message ?? e}`)
|
|
346
|
+
console.warn(`(The main proxy is fine. Try \`squeezr desktop start\` separately.)`)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function showLogs() {
|
|
351
|
+
const logFile = path.join(os.homedir(), '.squeezr', 'squeezr.log')
|
|
352
|
+
if (!fs.existsSync(logFile)) {
|
|
353
|
+
console.log('No log file yet. Run: squeezr setup')
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
// Show last 50 lines
|
|
357
|
+
const lines = fs.readFileSync(logFile, 'utf-8').split('\n').filter(Boolean)
|
|
358
|
+
const tail = lines.slice(-50)
|
|
359
|
+
if (tail.length === 0) {
|
|
360
|
+
console.log('Log file is empty — no requests yet.')
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
console.log(`=== ${logFile} (last ${tail.length} lines) ===\n`)
|
|
364
|
+
console.log(tail.join('\n'))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function killMcpProcesses() {
|
|
368
|
+
if (process.platform === 'win32') {
|
|
369
|
+
try {
|
|
370
|
+
execSync(
|
|
371
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name='node.exe'\\" | Where-Object { $_.CommandLine -like '*squeezr*mcp*' -or $_.CommandLine -like '*mcp.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"`,
|
|
372
|
+
{ stdio: 'pipe' }
|
|
373
|
+
)
|
|
374
|
+
} catch {}
|
|
375
|
+
} else {
|
|
376
|
+
try { execSync(`pkill -f 'squeezr.*mcp' 2>/dev/null`, { stdio: 'pipe' }) } catch {}
|
|
377
|
+
try { execSync(`pkill -f 'mcp\\.js' 2>/dev/null`, { stdio: 'pipe' }) } catch {}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function stopProxy() {
|
|
382
|
+
const port = getPort()
|
|
383
|
+
const mitmPort = getMitmPort(port)
|
|
384
|
+
|
|
385
|
+
// ── Step 0: Stop the SEPARATE Desktop proxy first (independent process) ────
|
|
386
|
+
// It has its own PID file. Failure here doesn't block main-proxy shutdown.
|
|
387
|
+
try {
|
|
388
|
+
const desktopPid = readDesktopPid()
|
|
389
|
+
if (desktopPid) {
|
|
390
|
+
try { process.kill(desktopPid, 'SIGTERM') } catch {}
|
|
391
|
+
// Give it a moment to exit gracefully
|
|
392
|
+
for (let i = 0; i < 20; i++) {
|
|
393
|
+
try { process.kill(desktopPid, 0) } catch { break }
|
|
394
|
+
execSync(process.platform === 'win32' ? `ping -n 1 127.0.0.1 > nul` : `sleep 0.1`, { stdio: 'pipe' })
|
|
395
|
+
}
|
|
396
|
+
try { process.kill(desktopPid, 'SIGKILL') } catch {}
|
|
397
|
+
try { fs.unlinkSync(DESKTOP_PID_FILE) } catch {}
|
|
398
|
+
// Also clean up any orphan listeners on the desktop ports
|
|
399
|
+
for (const dp of [DESKTOP_HTTPS_PORT, DESKTOP_HTTP_PORT]) {
|
|
400
|
+
try {
|
|
401
|
+
if (process.platform === 'win32') {
|
|
402
|
+
const out = execSync(`netstat -ano | findstr ":${dp} "`, { encoding: 'utf-8', stdio: 'pipe' })
|
|
403
|
+
const matches = [...out.matchAll(/LISTENING\s+(\d+)/g)]
|
|
404
|
+
for (const m of matches) {
|
|
405
|
+
try { execSync(`taskkill /F /PID ${m[1]}`, { stdio: 'pipe' }) } catch {}
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
try { execSync(`lsof -ti :${dp} -sTCP:LISTEN | xargs -r kill -9`, { stdio: 'pipe' }) } catch {}
|
|
409
|
+
}
|
|
410
|
+
} catch {}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch {}
|
|
414
|
+
|
|
415
|
+
// ── Step 1: Graceful shutdown via HTTP — persists history/cache before exit ──
|
|
416
|
+
// /squeezr/control/stop emits SIGTERM which calls persistAndExit().
|
|
417
|
+
// This prevents losing the current session's savings history on stop.
|
|
418
|
+
let gracefulOk = false
|
|
419
|
+
try {
|
|
420
|
+
const req = http.request({ hostname: 'localhost', port, path: '/squeezr/control/stop', method: 'POST', timeout: 2000 })
|
|
421
|
+
req.on('error', () => {})
|
|
422
|
+
req.end()
|
|
423
|
+
gracefulOk = true
|
|
424
|
+
// Give the process time to persist and exit cleanly
|
|
425
|
+
execSync(process.platform === 'win32'
|
|
426
|
+
? `ping -n 2 127.0.0.1 > nul` // ~1s sleep on Windows
|
|
427
|
+
: `sleep 1`, { stdio: 'pipe' })
|
|
428
|
+
} catch {}
|
|
429
|
+
|
|
430
|
+
// ── Step 2: Force-kill anything still listening (fallback) ──────────────────
|
|
431
|
+
const ports = [port, mitmPort]
|
|
432
|
+
let killed = gracefulOk // count graceful as "killed"
|
|
433
|
+
|
|
434
|
+
for (const p of ports) {
|
|
435
|
+
try {
|
|
436
|
+
let pids = []
|
|
437
|
+
if (process.platform === 'win32') {
|
|
438
|
+
const out = execSync(`netstat -ano | findstr ":${p} "`, { encoding: 'utf-8', stdio: 'pipe' })
|
|
439
|
+
const matches = [...out.matchAll(/LISTENING\s+(\d+)/g)]
|
|
440
|
+
pids = [...new Set(matches.map(m => m[1]))]
|
|
441
|
+
} else {
|
|
442
|
+
try {
|
|
443
|
+
const out = execSync(`lsof -ti :${p} -sTCP:LISTEN`, { encoding: 'utf-8', stdio: 'pipe' }).trim()
|
|
444
|
+
pids = out.split(/\s+/).filter(Boolean)
|
|
445
|
+
} catch {
|
|
446
|
+
try {
|
|
447
|
+
const out = execSync(`fuser ${p}/tcp 2>/dev/null`, { encoding: 'utf-8', stdio: 'pipe' }).trim()
|
|
448
|
+
pids = out.split(/\s+/).filter(Boolean)
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const pid of pids) {
|
|
453
|
+
try {
|
|
454
|
+
if (process.platform === 'win32') {
|
|
455
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' })
|
|
456
|
+
} else {
|
|
457
|
+
execSync(`kill -9 ${pid}`, { stdio: 'pipe' })
|
|
458
|
+
}
|
|
459
|
+
killed = true
|
|
460
|
+
} catch {}
|
|
461
|
+
}
|
|
462
|
+
} catch {}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Also stop the MCP server process
|
|
466
|
+
killMcpProcesses()
|
|
467
|
+
|
|
468
|
+
// Clear HTTPS_PROXY so npm and other tools don't try to use the dead proxy
|
|
469
|
+
if (process.platform === 'win32') {
|
|
470
|
+
try { execSync('setx HTTPS_PROXY ""', { stdio: 'pipe' }) } catch {}
|
|
471
|
+
} else if (isWSL()) {
|
|
472
|
+
try {
|
|
473
|
+
const setxExe = '/mnt/c/Windows/System32/setx.exe'
|
|
474
|
+
if (fs.existsSync(setxExe)) execSync(`"${setxExe}" HTTPS_PROXY ""`, { stdio: 'pipe' })
|
|
475
|
+
} catch {}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (killed) {
|
|
479
|
+
console.log(`Squeezr stopped`)
|
|
480
|
+
} else {
|
|
481
|
+
console.log(`Squeezr is not running`)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function checkStatus() {
|
|
486
|
+
const port = getPort()
|
|
487
|
+
const mitmPort = getMitmPort(port)
|
|
488
|
+
const json = await probeSqueezr(port, 2000)
|
|
489
|
+
if (!json) {
|
|
490
|
+
// Distinguish "nothing here" from "something foreign here" so the user gets
|
|
491
|
+
// an actionable error instead of a misleading "not running".
|
|
492
|
+
const occupied = await new Promise(resolve => {
|
|
493
|
+
const req = http.get(`http://localhost:${port}/`, res => {
|
|
494
|
+
resolve({ status: res.statusCode, server: res.headers.server })
|
|
495
|
+
res.resume()
|
|
496
|
+
})
|
|
497
|
+
req.on('error', () => resolve(null))
|
|
498
|
+
req.setTimeout(1500, () => { req.destroy(); resolve(null) })
|
|
499
|
+
})
|
|
500
|
+
if (occupied) {
|
|
501
|
+
console.log(`Squeezr is NOT running on port ${port}, but a foreign service is.`)
|
|
502
|
+
console.log(` Foreign response: HTTP ${occupied.status}${occupied.server ? ` (Server: ${occupied.server})` : ''}`)
|
|
503
|
+
console.log(` Stop it or change squeezr.toml port, then run: squeezr start`)
|
|
504
|
+
} else {
|
|
505
|
+
console.log(`Squeezr is NOT running`)
|
|
506
|
+
console.log('Start it with: squeezr start')
|
|
507
|
+
}
|
|
508
|
+
return false
|
|
509
|
+
}
|
|
510
|
+
console.log(`Squeezr is running (v${json.version})`)
|
|
511
|
+
console.log(` HTTP proxy (Claude Code, Claude Desktop, Codex Desktop, Aider, Gemini): http://localhost:${port}`)
|
|
512
|
+
console.log(` MITM proxy (Codex CLI TLS): http://localhost:${mitmPort}`)
|
|
513
|
+
console.log(` Dashboard: http://localhost:${port}/squeezr/dashboard`)
|
|
514
|
+
if (json.mode) console.log(` Mode: ${json.mode}`)
|
|
515
|
+
if (json.uptime_seconds != null) {
|
|
516
|
+
const s = json.uptime_seconds
|
|
517
|
+
const fmt = s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s/60)}m ${s%60}s` : `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`
|
|
518
|
+
console.log(` Uptime: ${fmt}`)
|
|
519
|
+
}
|
|
520
|
+
if (json.bypassed) console.log(` ⚠ Bypass mode is ON (compression disabled)`)
|
|
521
|
+
if (json.circuit_breaker) {
|
|
522
|
+
const cb = json.circuit_breaker
|
|
523
|
+
const icons = { closed: '🟢 OK', open: '🔴 OPEN', 'half-open': '🟡 PROBING' }
|
|
524
|
+
console.log(` Circuit: ${icons[cb.state] || cb.state}${cb.total_trips ? ` (${cb.total_trips} trip${cb.total_trips > 1 ? 's' : ''})` : ''}`)
|
|
525
|
+
}
|
|
526
|
+
return true
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function showConfig() {
|
|
530
|
+
const tomlPath = path.join(ROOT, 'squeezr.toml')
|
|
531
|
+
console.log(`Config file: ${tomlPath}`)
|
|
532
|
+
if (fs.existsSync(tomlPath)) {
|
|
533
|
+
console.log('\nCurrent config:')
|
|
534
|
+
console.log(fs.readFileSync(tomlPath, 'utf-8'))
|
|
535
|
+
} else {
|
|
536
|
+
console.log('No squeezr.toml found. Using defaults.')
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
// ── squeezr mcp ───────────────────────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
async function mcpInstall() {
|
|
544
|
+
const mcpServerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'mcp.js')
|
|
545
|
+
const entry = {
|
|
546
|
+
type: 'stdio',
|
|
547
|
+
command: 'node',
|
|
548
|
+
args: [mcpServerPath],
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Claude Desktop config path varies by platform
|
|
552
|
+
const claudeDesktopConfig = process.platform === 'win32'
|
|
553
|
+
? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
|
|
554
|
+
: process.platform === 'darwin'
|
|
555
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
556
|
+
: path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json')
|
|
557
|
+
|
|
558
|
+
const targets = [
|
|
559
|
+
{
|
|
560
|
+
name: 'Claude Code',
|
|
561
|
+
file: path.join(os.homedir(), '.claude.json'),
|
|
562
|
+
key: 'mcpServers',
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: 'Claude Desktop',
|
|
566
|
+
file: claudeDesktopConfig,
|
|
567
|
+
key: 'mcpServers',
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'Cursor',
|
|
571
|
+
file: path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
572
|
+
key: 'mcpServers',
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: 'Windsurf',
|
|
576
|
+
file: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
577
|
+
key: 'mcpServers',
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: 'Cline / Roo-Cline',
|
|
581
|
+
file: path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
|
|
582
|
+
key: 'mcpServers',
|
|
583
|
+
},
|
|
584
|
+
]
|
|
585
|
+
|
|
586
|
+
let installed = 0
|
|
587
|
+
|
|
588
|
+
for (const target of targets) {
|
|
589
|
+
try {
|
|
590
|
+
// Only install into configs that already exist (user has that tool)
|
|
591
|
+
if (!fs.existsSync(target.file) && target.name !== 'Claude Code') continue
|
|
592
|
+
|
|
593
|
+
let cfg = {}
|
|
594
|
+
if (fs.existsSync(target.file)) {
|
|
595
|
+
try { cfg = JSON.parse(fs.readFileSync(target.file, 'utf-8')) } catch { cfg = {} }
|
|
596
|
+
}
|
|
597
|
+
cfg[target.key] = cfg[target.key] || {}
|
|
598
|
+
cfg[target.key].squeezr = entry
|
|
599
|
+
|
|
600
|
+
const dir = path.dirname(target.file)
|
|
601
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
602
|
+
fs.writeFileSync(target.file, JSON.stringify(cfg, null, 2))
|
|
603
|
+
installed++
|
|
604
|
+
console.log()
|
|
605
|
+
console.log(' ok ' + target.name + ': ' + target.file)
|
|
606
|
+
} catch (e) {
|
|
607
|
+
console.warn()
|
|
608
|
+
console.warn(' warn ' + target.name + ': ' + (e.message || e))
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
console.log()
|
|
613
|
+
console.log('MCP server registered in ' + installed + ' client(s).')
|
|
614
|
+
console.log('Server binary: ' + mcpServerPath)
|
|
615
|
+
console.log('')
|
|
616
|
+
console.log('Available tools in Claude Desktop, Claude Code, Codex Desktop, Cursor…:')
|
|
617
|
+
console.log(' squeezr_status — Check if Squeezr is running')
|
|
618
|
+
console.log(' squeezr_stats — Real-time token savings')
|
|
619
|
+
console.log(' squeezr_set_mode — Change compression aggressiveness')
|
|
620
|
+
console.log(' squeezr_config — Current configuration')
|
|
621
|
+
console.log(' squeezr_habits — Wasteful pattern report')
|
|
622
|
+
console.log(' squeezr_open_dashboard — Open the Squeezr dashboard in your browser')
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function mcpUninstall() {
|
|
626
|
+
const claudeDesktopConfig = process.platform === 'win32'
|
|
627
|
+
? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
|
|
628
|
+
: process.platform === 'darwin'
|
|
629
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
630
|
+
: path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json')
|
|
631
|
+
const files = [
|
|
632
|
+
path.join(os.homedir(), '.claude.json'),
|
|
633
|
+
claudeDesktopConfig,
|
|
634
|
+
path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
635
|
+
path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
636
|
+
path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
|
|
637
|
+
]
|
|
638
|
+
let removed = 0
|
|
639
|
+
for (const file of files) {
|
|
640
|
+
if (!fs.existsSync(file)) continue
|
|
641
|
+
try {
|
|
642
|
+
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
643
|
+
if (cfg.mcpServers?.squeezr) {
|
|
644
|
+
delete cfg.mcpServers.squeezr
|
|
645
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2))
|
|
646
|
+
console.log()
|
|
647
|
+
removed++
|
|
648
|
+
}
|
|
649
|
+
} catch { /* ignore */ }
|
|
650
|
+
}
|
|
651
|
+
if (removed === 0) console.log('Squeezr MCP not found in any config.')
|
|
652
|
+
else console.log()
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ── squeezr ports ─────────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
async function configurePorts() {
|
|
658
|
+
const { createInterface } = await import('readline')
|
|
659
|
+
const tomlPath = path.join(ROOT, 'squeezr.toml')
|
|
660
|
+
let tomlContent = fs.existsSync(tomlPath) ? fs.readFileSync(tomlPath, 'utf-8') : ''
|
|
661
|
+
|
|
662
|
+
// Read current ports from toml
|
|
663
|
+
const portMatch = tomlContent.match(/^port\s*=\s*(\d+)/m)
|
|
664
|
+
const mitmMatch = tomlContent.match(/^mitm_port\s*=\s*(\d+)/m)
|
|
665
|
+
const currentPort = portMatch ? parseInt(portMatch[1]) : 8080
|
|
666
|
+
const currentMitm = mitmMatch ? parseInt(mitmMatch[1]) : currentPort + 1
|
|
667
|
+
|
|
668
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
669
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve))
|
|
670
|
+
|
|
671
|
+
console.log(`\nCurrent ports:`)
|
|
672
|
+
console.log(` HTTP proxy (Claude/Aider/Gemini): ${currentPort}`)
|
|
673
|
+
console.log(` MITM proxy (Codex): ${currentMitm}`)
|
|
674
|
+
console.log(` Dashboard: ${currentPort}/squeezr/dashboard (same port as proxy)\n`)
|
|
675
|
+
|
|
676
|
+
const newPort = await ask(`HTTP proxy port [${currentPort}]: `)
|
|
677
|
+
const newMitm = await ask(`MITM proxy port [${currentMitm}]: `)
|
|
678
|
+
rl.close()
|
|
679
|
+
|
|
680
|
+
const finalPort = newPort.trim() ? parseInt(newPort.trim()) : currentPort
|
|
681
|
+
const finalMitm = newMitm.trim() ? parseInt(newMitm.trim()) : currentMitm
|
|
682
|
+
|
|
683
|
+
if (isNaN(finalPort) || isNaN(finalMitm) || finalPort < 1 || finalMitm < 1 || finalPort > 65535 || finalMitm > 65535) {
|
|
684
|
+
console.error('Invalid port number. Must be between 1 and 65535.')
|
|
685
|
+
process.exit(1)
|
|
686
|
+
}
|
|
687
|
+
if (finalPort === finalMitm) {
|
|
688
|
+
console.error('HTTP and MITM ports must be different.')
|
|
689
|
+
process.exit(1)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Update toml
|
|
693
|
+
if (portMatch) {
|
|
694
|
+
tomlContent = tomlContent.replace(/^port\s*=\s*\d+/m, `port = ${finalPort}`)
|
|
695
|
+
} else if (tomlContent.includes('[proxy]')) {
|
|
696
|
+
tomlContent = tomlContent.replace('[proxy]', `[proxy]\nport = ${finalPort}`)
|
|
697
|
+
} else {
|
|
698
|
+
tomlContent = `[proxy]\nport = ${finalPort}\n` + tomlContent
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (mitmMatch) {
|
|
702
|
+
tomlContent = tomlContent.replace(/^mitm_port\s*=\s*\d+/m, `mitm_port = ${finalMitm}`)
|
|
703
|
+
} else {
|
|
704
|
+
// Add after port line
|
|
705
|
+
tomlContent = tomlContent.replace(/^(port\s*=\s*\d+)/m, `$1\nmitm_port = ${finalMitm}`)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
fs.writeFileSync(tomlPath, tomlContent)
|
|
709
|
+
console.log(`\nSaved to ${tomlPath}`)
|
|
710
|
+
|
|
711
|
+
// Update env vars
|
|
712
|
+
if (process.platform === 'win32') {
|
|
713
|
+
try { execSync(`setx SQUEEZR_PORT "${finalPort}"`, { stdio: 'pipe' }) } catch {}
|
|
714
|
+
try { execSync(`setx SQUEEZR_MITM_PORT "${finalMitm}"`, { stdio: 'pipe' }) } catch {}
|
|
715
|
+
try { execSync(`setx ANTHROPIC_BASE_URL "http://localhost:${finalPort}"`, { stdio: 'pipe' }) } catch {}
|
|
716
|
+
try { execSync(`setx GEMINI_API_BASE_URL "http://localhost:${finalPort}"`, { stdio: 'pipe' }) } catch {}
|
|
717
|
+
console.log('Environment variables updated. Restart your terminal for changes to take effect.')
|
|
718
|
+
} else {
|
|
719
|
+
// Update shell profiles directly
|
|
720
|
+
const profiles = [
|
|
721
|
+
path.join(os.homedir(), '.zshrc'),
|
|
722
|
+
path.join(os.homedir(), '.bashrc'),
|
|
723
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
724
|
+
]
|
|
725
|
+
const envBlock = [
|
|
726
|
+
`export SQUEEZR_PORT=${finalPort}`,
|
|
727
|
+
`export SQUEEZR_MITM_PORT=${finalMitm}`,
|
|
728
|
+
`export ANTHROPIC_BASE_URL=http://localhost:${finalPort}`,
|
|
729
|
+
`export GEMINI_API_BASE_URL=http://localhost:${finalPort}`,
|
|
730
|
+
].join('\n')
|
|
731
|
+
for (const p of profiles) {
|
|
732
|
+
try {
|
|
733
|
+
let content = fs.readFileSync(p, 'utf-8')
|
|
734
|
+
if (content.includes('# squeezr env vars')) {
|
|
735
|
+
// Replace existing block (from marker to the closing fi)
|
|
736
|
+
content = content.replace(
|
|
737
|
+
/# squeezr env vars[\s\S]*?(?:fi|unset -f _squeezr_alive)/,
|
|
738
|
+
`# squeezr env vars\n${envBlock}\n# squeezr auto-heal (validates identity, not just HTTP 200)\n_squeezr_alive() {\n curl -sf --max-time 2 "http://localhost:${finalPort}/squeezr/health" 2>/dev/null | grep -q '"identity":"squeezr"'\n}\nif ! _squeezr_alive; then squeezr start > /dev/null 2>&1; fi\nunset -f _squeezr_alive`
|
|
739
|
+
)
|
|
740
|
+
fs.writeFileSync(p, content)
|
|
741
|
+
console.log(` [ok] Updated ${p}`)
|
|
742
|
+
}
|
|
743
|
+
} catch {}
|
|
744
|
+
}
|
|
745
|
+
// Also update env for WSL setx if on WSL
|
|
746
|
+
try {
|
|
747
|
+
const procVersion = fs.readFileSync('/proc/version', 'utf-8')
|
|
748
|
+
if (/microsoft|wsl/i.test(procVersion)) {
|
|
749
|
+
const setx = '/mnt/c/Windows/System32/setx.exe'
|
|
750
|
+
try { execSync(`"${setx}" SQUEEZR_PORT "${finalPort}"`, { stdio: 'pipe' }) } catch {}
|
|
751
|
+
try { execSync(`"${setx}" SQUEEZR_MITM_PORT "${finalMitm}"`, { stdio: 'pipe' }) } catch {}
|
|
752
|
+
try { execSync(`"${setx}" ANTHROPIC_BASE_URL "http://localhost:${finalPort}"`, { stdio: 'pipe' }) } catch {}
|
|
753
|
+
try { execSync(`"${setx}" GEMINI_API_BASE_URL "http://localhost:${finalPort}"`, { stdio: 'pipe' }) } catch {}
|
|
754
|
+
}
|
|
755
|
+
} catch {}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Apply to current process so stop/start works immediately
|
|
759
|
+
process.env.SQUEEZR_PORT = String(finalPort)
|
|
760
|
+
process.env.SQUEEZR_MITM_PORT = String(finalMitm)
|
|
761
|
+
process.env.ANTHROPIC_BASE_URL = `http://localhost:${finalPort}`
|
|
762
|
+
|
|
763
|
+
// Auto stop + start
|
|
764
|
+
console.log('')
|
|
765
|
+
stopProxy()
|
|
766
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
767
|
+
await startDaemon()
|
|
768
|
+
console.log(`\nOpen a new terminal for env vars to apply to other tools.`)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ── squeezr uninstall ─────────────────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
async function uninstall() {
|
|
774
|
+
const { createInterface } = await import('readline')
|
|
775
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
776
|
+
const answer = await new Promise(resolve => rl.question(
|
|
777
|
+
'This will remove Squeezr completely: stop proxy, remove env vars, CA certs, auto-start, config, and logs.\nContinue? [y/N] ', resolve
|
|
778
|
+
))
|
|
779
|
+
rl.close()
|
|
780
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
781
|
+
console.log('Cancelled.')
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
console.log('\nUninstalling Squeezr...\n')
|
|
786
|
+
|
|
787
|
+
// 1. Stop proxy
|
|
788
|
+
stopProxy()
|
|
789
|
+
|
|
790
|
+
// 2. Remove env vars
|
|
791
|
+
if (process.platform === 'win32') {
|
|
792
|
+
const vars = ['ANTHROPIC_BASE_URL', 'GEMINI_API_BASE_URL', 'HTTPS_PROXY', 'NODE_EXTRA_CA_CERTS', 'SQUEEZR_PORT', 'SQUEEZR_MITM_PORT', 'openai_base_url', 'NO_PROXY']
|
|
793
|
+
for (const v of vars) {
|
|
794
|
+
try { execSync(`reg delete "HKCU\\Environment" /v ${v} /f`, { stdio: 'pipe' }) } catch {}
|
|
795
|
+
}
|
|
796
|
+
console.log(' [ok] Windows env vars removed')
|
|
797
|
+
} else {
|
|
798
|
+
// Remove squeezr block from shell profiles
|
|
799
|
+
const profiles = [
|
|
800
|
+
path.join(os.homedir(), '.zshrc'),
|
|
801
|
+
path.join(os.homedir(), '.bashrc'),
|
|
802
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
803
|
+
]
|
|
804
|
+
for (const p of profiles) {
|
|
805
|
+
try {
|
|
806
|
+
const content = fs.readFileSync(p, 'utf-8')
|
|
807
|
+
if (content.includes('# squeezr env vars')) {
|
|
808
|
+
const cleaned = content.replace(/\n?# squeezr env vars[\s\S]*?fi\n?/g, '\n')
|
|
809
|
+
fs.writeFileSync(p, cleaned)
|
|
810
|
+
console.log(` [ok] Cleaned ${p}`)
|
|
811
|
+
}
|
|
812
|
+
} catch {}
|
|
813
|
+
}
|
|
814
|
+
// Also clean .profile
|
|
815
|
+
const profilePath = path.join(os.homedir(), '.profile')
|
|
816
|
+
try {
|
|
817
|
+
const content = fs.readFileSync(profilePath, 'utf-8')
|
|
818
|
+
if (content.includes('# squeezr env vars')) {
|
|
819
|
+
const cleaned = content.replace(/\n?# squeezr env vars[^\n]*(\nexport [^\n]*)*/g, '')
|
|
820
|
+
fs.writeFileSync(profilePath, cleaned)
|
|
821
|
+
console.log(` [ok] Cleaned ${profilePath}`)
|
|
822
|
+
}
|
|
823
|
+
} catch {}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 3. Remove CA from certificate stores
|
|
827
|
+
if (process.platform === 'win32') {
|
|
828
|
+
try { execSync('certutil -delstore -user Root "Squeezr-MITM-CA"', { stdio: 'pipe' }); console.log(' [ok] CA removed from user certificate store') } catch {}
|
|
829
|
+
try { execSync('certutil -delstore Root "Squeezr-MITM-CA"', { stdio: 'pipe' }) } catch {}
|
|
830
|
+
} else if (process.platform === 'darwin') {
|
|
831
|
+
try { execSync('security delete-certificate -c "Squeezr-MITM-CA" ~/Library/Keychains/login.keychain-db', { stdio: 'pipe' }); console.log(' [ok] CA removed from Keychain') } catch {}
|
|
832
|
+
}
|
|
833
|
+
// On Linux, CA is only in bundle.crt which gets deleted with ~/.squeezr below
|
|
834
|
+
|
|
835
|
+
// 4. Remove auto-start
|
|
836
|
+
if (process.platform === 'win32') {
|
|
837
|
+
try { execSync('nssm stop SqueezrProxy', { stdio: 'pipe' }) } catch {}
|
|
838
|
+
try { execSync('nssm remove SqueezrProxy confirm', { stdio: 'pipe' }) } catch {}
|
|
839
|
+
try { execSync('schtasks /Delete /TN "Squeezr" /F', { stdio: 'pipe' }); console.log(' [ok] Removed scheduled task') } catch {}
|
|
840
|
+
// Remove Startup folder VBS (fallback auto-start)
|
|
841
|
+
const startupVbs = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'squeezr-start.vbs')
|
|
842
|
+
try { fs.unlinkSync(startupVbs); console.log(' [ok] Removed startup VBS script') } catch {}
|
|
843
|
+
} else if (process.platform === 'darwin') {
|
|
844
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.squeezr.plist')
|
|
845
|
+
try { execSync(`launchctl unload "${plistPath}"`, { stdio: 'pipe' }) } catch {}
|
|
846
|
+
try { fs.unlinkSync(plistPath); console.log(' [ok] Removed launchd plist') } catch {}
|
|
847
|
+
} else {
|
|
848
|
+
try { execSync('systemctl --user disable --now squeezr', { stdio: 'pipe' }) } catch {}
|
|
849
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'squeezr.service')
|
|
850
|
+
try { fs.unlinkSync(servicePath); console.log(' [ok] Removed systemd service') } catch {}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// 5. Remove ~/.squeezr (logs, cache, CA, stats)
|
|
854
|
+
const squeezrDir = path.join(os.homedir(), '.squeezr')
|
|
855
|
+
try {
|
|
856
|
+
fs.rmSync(squeezrDir, { recursive: true, force: true })
|
|
857
|
+
console.log(` [ok] Removed ${squeezrDir}`)
|
|
858
|
+
} catch {}
|
|
859
|
+
|
|
860
|
+
// 6. Remove global config
|
|
861
|
+
const tomlPath = path.join(ROOT, 'squeezr.toml')
|
|
862
|
+
try { fs.unlinkSync(tomlPath) } catch {}
|
|
863
|
+
|
|
864
|
+
// 7. Remove shell wrapper functions from profiles
|
|
865
|
+
if (process.platform === 'win32') {
|
|
866
|
+
try {
|
|
867
|
+
const psProfilePath = execSync('powershell -NoProfile -Command "[Environment]::GetFolderPath(\'MyDocuments\') + \'\\WindowsPowerShell\\Microsoft.PowerShell_profile.ps1\'"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
868
|
+
if (fs.existsSync(psProfilePath)) {
|
|
869
|
+
const content = fs.readFileSync(psProfilePath, 'utf-8')
|
|
870
|
+
if (content.includes('# squeezr wrapper')) {
|
|
871
|
+
const cleaned = content.replace(/\n?# squeezr wrapper[\s\S]*?# end squeezr wrapper\n?/g, '')
|
|
872
|
+
fs.writeFileSync(psProfilePath, cleaned)
|
|
873
|
+
console.log(` [ok] Removed PowerShell wrapper from ${psProfilePath}`)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
} catch {}
|
|
877
|
+
}
|
|
878
|
+
// Remove bash/zsh wrapper
|
|
879
|
+
for (const p of [path.join(os.homedir(), '.bashrc'), path.join(os.homedir(), '.zshrc')]) {
|
|
880
|
+
try {
|
|
881
|
+
const content = fs.readFileSync(p, 'utf-8')
|
|
882
|
+
if (content.includes('# squeezr shell wrapper')) {
|
|
883
|
+
const cleaned = content.replace(/\n?# squeezr shell wrapper[\s\S]*?# end squeezr shell wrapper\n?/g, '')
|
|
884
|
+
fs.writeFileSync(p, cleaned)
|
|
885
|
+
console.log(` [ok] Removed shell wrapper from ${p}`)
|
|
886
|
+
}
|
|
887
|
+
} catch {}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// 8. Remove MCP registrations
|
|
891
|
+
console.log(' [..] Removing MCP registrations...')
|
|
892
|
+
try { await mcpUninstall() } catch {}
|
|
893
|
+
|
|
894
|
+
// 9. npm uninstall -g (clear HTTPS_PROXY first so npm doesn't hit dead proxy)
|
|
895
|
+
console.log(' [..] Uninstalling npm package...')
|
|
896
|
+
const cleanEnv = { ...process.env, HTTPS_PROXY: '', https_proxy: '', HTTP_PROXY: '', http_proxy: '' }
|
|
897
|
+
try {
|
|
898
|
+
execSync('npm uninstall -g squeezr-ai', { stdio: 'inherit', env: cleanEnv })
|
|
899
|
+
console.log(' [ok] npm package removed')
|
|
900
|
+
} catch {
|
|
901
|
+
try {
|
|
902
|
+
execSync('sudo npm uninstall -g squeezr-ai', { stdio: 'inherit', env: cleanEnv })
|
|
903
|
+
console.log(' [ok] npm package removed')
|
|
904
|
+
} catch {
|
|
905
|
+
console.log(' [warn] Could not uninstall npm package. Run manually: npm uninstall -g squeezr-ai')
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
console.log('\nDone! Squeezr has been completely removed.\n')
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ── Claude Desktop hosts file + TLS intercept ────────────────────────────────
|
|
913
|
+
// Claude Desktop ignores ANTHROPIC_BASE_URL (it's an Electron GUI app), so the
|
|
914
|
+
// only way to intercept is at DNS level: hosts file redirects api.anthropic.com
|
|
915
|
+
// → 127.0.0.1, and Squeezr listens on :443 with TLS using its local CA cert.
|
|
916
|
+
//
|
|
917
|
+
// This function adds/removes the hosts file entry + firewall rule. Requires
|
|
918
|
+
// admin. On Windows, re-launches itself elevated via PowerShell Start-Process -Verb RunAs.
|
|
919
|
+
|
|
920
|
+
const HOSTS_FILE_WIN = 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
|
921
|
+
const HOSTS_FILE_UNIX = '/etc/hosts'
|
|
922
|
+
const HOSTS_MARKER_BEGIN = '# squeezr-claude-desktop BEGIN'
|
|
923
|
+
const HOSTS_MARKER_END = '# squeezr-claude-desktop END'
|
|
924
|
+
const INTERCEPTED_DOMAINS = ['api.anthropic.com']
|
|
925
|
+
const CLAUDE_DESKTOP_FLAG_FILE = path.join(os.homedir(), '.squeezr', 'claude-desktop-enabled')
|
|
926
|
+
const MITM_INTERNAL_PORT = 8443
|
|
927
|
+
|
|
928
|
+
async function toggleClaudeDesktopIntercept(enable) {
|
|
929
|
+
const isWin = process.platform === 'win32'
|
|
930
|
+
const hostsPath = isWin ? HOSTS_FILE_WIN : HOSTS_FILE_UNIX
|
|
931
|
+
|
|
932
|
+
// Check if running as admin
|
|
933
|
+
const isAdmin = await checkIsAdmin()
|
|
934
|
+
if (!isAdmin) {
|
|
935
|
+
if (isWin) {
|
|
936
|
+
console.log('Admin rights required to modify hosts file and bind to port 443.')
|
|
937
|
+
console.log('Relaunching elevated...\n')
|
|
938
|
+
const verb = enable ? 'enable-claude-desktop' : 'disable-claude-desktop'
|
|
939
|
+
try {
|
|
940
|
+
execSync(
|
|
941
|
+
`powershell -NoProfile -Command "Start-Process -FilePath '${process.execPath}' -ArgumentList '${process.argv[1]}','${verb}' -Verb RunAs -Wait"`,
|
|
942
|
+
{ stdio: 'inherit' }
|
|
943
|
+
)
|
|
944
|
+
console.log('\nDone. Restart Squeezr to apply: squeezr stop && squeezr start')
|
|
945
|
+
} catch (e) {
|
|
946
|
+
console.error('Failed to launch elevated process: ' + e.message)
|
|
947
|
+
}
|
|
948
|
+
return
|
|
949
|
+
} else {
|
|
950
|
+
console.error('Run with sudo: sudo squeezr ' + (enable ? 'enable' : 'disable') + '-claude-desktop')
|
|
951
|
+
process.exit(1)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Read hosts file
|
|
956
|
+
let content = ''
|
|
957
|
+
try { content = fs.readFileSync(hostsPath, 'utf-8') } catch (e) {
|
|
958
|
+
console.error('Could not read hosts file: ' + e.message)
|
|
959
|
+
process.exit(1)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Strip any existing squeezr block
|
|
963
|
+
const blockRegex = new RegExp(
|
|
964
|
+
`\\r?\\n?${HOSTS_MARKER_BEGIN}[\\s\\S]*?${HOSTS_MARKER_END}\\r?\\n?`,
|
|
965
|
+
'g'
|
|
966
|
+
)
|
|
967
|
+
content = content.replace(blockRegex, '')
|
|
968
|
+
|
|
969
|
+
if (enable) {
|
|
970
|
+
const block = [
|
|
971
|
+
'',
|
|
972
|
+
HOSTS_MARKER_BEGIN,
|
|
973
|
+
'# Redirects Claude Desktop to Squeezr for token compression.',
|
|
974
|
+
'# Remove this block (or run `squeezr disable-claude-desktop`) to undo.',
|
|
975
|
+
...INTERCEPTED_DOMAINS.map(d => `127.0.0.1 ${d}`),
|
|
976
|
+
HOSTS_MARKER_END,
|
|
977
|
+
'',
|
|
978
|
+
].join('\n')
|
|
979
|
+
content += block
|
|
980
|
+
|
|
981
|
+
fs.writeFileSync(hostsPath, content)
|
|
982
|
+
console.log('[1/4] Hosts file updated: api.anthropic.com → 127.0.0.1')
|
|
983
|
+
|
|
984
|
+
if (isWin) {
|
|
985
|
+
// 2. netsh portproxy 443 → 8443 (so Squeezr listens on 8443 without admin)
|
|
986
|
+
try {
|
|
987
|
+
execSync(`netsh interface portproxy delete v4tov4 listenport=443 listenaddress=127.0.0.1`, { stdio: 'pipe' })
|
|
988
|
+
} catch {}
|
|
989
|
+
try {
|
|
990
|
+
execSync(`netsh interface portproxy add v4tov4 listenport=443 listenaddress=127.0.0.1 connectport=${MITM_INTERNAL_PORT} connectaddress=127.0.0.1`, { stdio: 'pipe' })
|
|
991
|
+
console.log(`[2/4] Port forwarding 127.0.0.1:443 → 127.0.0.1:${MITM_INTERNAL_PORT} (netsh portproxy)`)
|
|
992
|
+
} catch (e) {
|
|
993
|
+
console.warn('Could not add netsh portproxy: ' + e.message)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// 3. Firewall: open inbound 443
|
|
997
|
+
try {
|
|
998
|
+
execSync('netsh advfirewall firewall delete rule name="Squeezr-Claude-Desktop-443"', { stdio: 'pipe' })
|
|
999
|
+
} catch {}
|
|
1000
|
+
try {
|
|
1001
|
+
execSync('netsh advfirewall firewall add rule name="Squeezr-Claude-Desktop-443" dir=in action=allow protocol=TCP localport=443', { stdio: 'pipe' })
|
|
1002
|
+
console.log('[3/4] Firewall rule added for port 443.')
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
console.warn('Could not add firewall rule: ' + e.message)
|
|
1005
|
+
}
|
|
1006
|
+
// Flush DNS cache so the change takes effect immediately
|
|
1007
|
+
try { execSync('ipconfig /flushdns', { stdio: 'pipe' }) } catch {}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Note: no persistent flag file needed — Squeezr auto-detects the hosts file
|
|
1011
|
+
// entry on startup and activates the MITM listener accordingly.
|
|
1012
|
+
|
|
1013
|
+
console.log('\n[NEXT] Restart Squeezr: squeezr stop && squeezr start')
|
|
1014
|
+
console.log(' Close Claude Desktop completely (including system tray).')
|
|
1015
|
+
console.log(' Reopen Claude Desktop and try any query.')
|
|
1016
|
+
console.log(' Verify in dashboard "By client" section → should show "claude_desktop".')
|
|
1017
|
+
} else {
|
|
1018
|
+
fs.writeFileSync(hostsPath, content)
|
|
1019
|
+
console.log('[1/4] Hosts file cleaned: api.anthropic.com restored to normal DNS.')
|
|
1020
|
+
if (isWin) {
|
|
1021
|
+
try {
|
|
1022
|
+
execSync(`netsh interface portproxy delete v4tov4 listenport=443 listenaddress=127.0.0.1`, { stdio: 'pipe' })
|
|
1023
|
+
console.log('[2/4] Port forwarding rule removed.')
|
|
1024
|
+
} catch {}
|
|
1025
|
+
try {
|
|
1026
|
+
execSync('netsh advfirewall firewall delete rule name="Squeezr-Claude-Desktop-443"', { stdio: 'pipe' })
|
|
1027
|
+
console.log('[3/4] Firewall rule removed.')
|
|
1028
|
+
} catch {}
|
|
1029
|
+
try { execSync('ipconfig /flushdns', { stdio: 'pipe' }) } catch {}
|
|
1030
|
+
}
|
|
1031
|
+
// Legacy flag cleanup (if exists from older version)
|
|
1032
|
+
try {
|
|
1033
|
+
const legacyFlag = path.join(os.homedir(), '.squeezr', 'claude-desktop-enabled')
|
|
1034
|
+
if (fs.existsSync(legacyFlag)) fs.unlinkSync(legacyFlag)
|
|
1035
|
+
} catch {}
|
|
1036
|
+
console.log('\n[NEXT] Restart Squeezr: squeezr stop && squeezr start')
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async function checkIsAdmin() {
|
|
1041
|
+
if (process.platform === 'win32') {
|
|
1042
|
+
try {
|
|
1043
|
+
execSync('net session', { stdio: 'pipe' })
|
|
1044
|
+
return true
|
|
1045
|
+
} catch {
|
|
1046
|
+
return false
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
return process.getuid && process.getuid() === 0
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// ── Desktop proxy lifecycle (TOTALLY SEPARATE from main proxy on 8080) ───────
|
|
1054
|
+
// `squeezr desktop start` → spawns dist/desktopProxy.js detached
|
|
1055
|
+
// `squeezr desktop stop` → kills via PID file (does NOT touch port 8080)
|
|
1056
|
+
// `squeezr desktop status` → prints state
|
|
1057
|
+
//
|
|
1058
|
+
// Critical: this proxy lives or dies independently. Crashing it cannot affect
|
|
1059
|
+
// the main proxy on 8080 (Claude Code, Codex CLI, Aider, Gemini CLI).
|
|
1060
|
+
|
|
1061
|
+
const DESKTOP_PID_FILE = path.join(os.homedir(), '.squeezr', 'desktop-proxy.pid')
|
|
1062
|
+
const DESKTOP_LOG_FILE = path.join(os.homedir(), '.squeezr', 'desktop-proxy.log')
|
|
1063
|
+
const DESKTOP_HTTPS_PORT = 8443
|
|
1064
|
+
const DESKTOP_HTTP_PORT = 8088
|
|
1065
|
+
|
|
1066
|
+
function readDesktopPid() {
|
|
1067
|
+
try {
|
|
1068
|
+
const raw = fs.readFileSync(DESKTOP_PID_FILE, 'utf-8').trim()
|
|
1069
|
+
const pid = Number(raw)
|
|
1070
|
+
if (!pid || Number.isNaN(pid)) return null
|
|
1071
|
+
// Check process actually exists
|
|
1072
|
+
try { process.kill(pid, 0); return pid } catch { return null }
|
|
1073
|
+
} catch { return null }
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Probe whether the desktop proxy listener is actually answering. Used to
|
|
1077
|
+
// detect orphan processes — situations where the PID file is stale but a
|
|
1078
|
+
// previous desktop proxy is still bound to the ports.
|
|
1079
|
+
async function isDesktopPortBound(port) {
|
|
1080
|
+
return new Promise(resolve => {
|
|
1081
|
+
const net = require('node:net')
|
|
1082
|
+
const sock = net.connect({ host: '127.0.0.1', port, timeout: 500 }, () => {
|
|
1083
|
+
sock.end()
|
|
1084
|
+
resolve(true)
|
|
1085
|
+
})
|
|
1086
|
+
sock.once('error', () => resolve(false))
|
|
1087
|
+
sock.once('timeout', () => { sock.destroy(); resolve(false) })
|
|
1088
|
+
})
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Find the PID owning a local TCP listener on Windows via `netstat -ano`. We
|
|
1092
|
+
// use this only to clean up orphan desktop-proxy processes; it is a best
|
|
1093
|
+
// effort and returns null if it can't parse the output.
|
|
1094
|
+
function findPidByPort(port) {
|
|
1095
|
+
if (process.platform !== 'win32') return null
|
|
1096
|
+
try {
|
|
1097
|
+
const out = require('node:child_process').execSync(`netstat -ano -p TCP`, { encoding: 'utf-8' })
|
|
1098
|
+
for (const line of out.split(/\r?\n/)) {
|
|
1099
|
+
const m = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/)
|
|
1100
|
+
if (m && Number(m[1]) === port) return Number(m[2])
|
|
1101
|
+
}
|
|
1102
|
+
} catch { /* ignore */ }
|
|
1103
|
+
return null
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function desktopProxyStart() {
|
|
1107
|
+
const existing = readDesktopPid()
|
|
1108
|
+
if (existing) {
|
|
1109
|
+
console.log(`Desktop proxy already running (pid=${existing}).`)
|
|
1110
|
+
console.log(` HTTPS: https://127.0.0.1:${DESKTOP_HTTPS_PORT} (Claude Desktop)`)
|
|
1111
|
+
console.log(` HTTP: http://127.0.0.1:${DESKTOP_HTTP_PORT} (Codex Desktop)`)
|
|
1112
|
+
return
|
|
1113
|
+
}
|
|
1114
|
+
// Detect orphan listener (PID file dead but listener still up) — common
|
|
1115
|
+
// after an ungraceful restart. Reclaim it by adopting the running PID,
|
|
1116
|
+
// otherwise the spawn below would crash with EADDRINUSE.
|
|
1117
|
+
if (await isDesktopPortBound(DESKTOP_HTTPS_PORT) || await isDesktopPortBound(DESKTOP_HTTP_PORT)) {
|
|
1118
|
+
const orphanPid = findPidByPort(DESKTOP_HTTPS_PORT) ?? findPidByPort(DESKTOP_HTTP_PORT)
|
|
1119
|
+
if (orphanPid) {
|
|
1120
|
+
try { fs.writeFileSync(DESKTOP_PID_FILE, String(orphanPid), 'utf-8') } catch {}
|
|
1121
|
+
console.log(`Desktop proxy is already bound to ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} (pid=${orphanPid}, adopted).`)
|
|
1122
|
+
} else {
|
|
1123
|
+
console.log(`Desktop proxy ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} are already in use by an unknown process. Use 'squeezr desktop stop' first.`)
|
|
1124
|
+
}
|
|
1125
|
+
return
|
|
1126
|
+
}
|
|
1127
|
+
const distPath = path.join(ROOT, 'dist', 'desktopProxy.js')
|
|
1128
|
+
if (!fs.existsSync(distPath)) {
|
|
1129
|
+
console.error(`Error: ${distPath} not found. Run 'npm run build' first.`)
|
|
1130
|
+
process.exit(1)
|
|
1131
|
+
}
|
|
1132
|
+
try { fs.mkdirSync(path.dirname(DESKTOP_LOG_FILE), { recursive: true }) } catch {}
|
|
1133
|
+
const out = fs.openSync(DESKTOP_LOG_FILE, 'a')
|
|
1134
|
+
const err = fs.openSync(DESKTOP_LOG_FILE, 'a')
|
|
1135
|
+
const child = spawn(process.execPath, [distPath], {
|
|
1136
|
+
detached: true,
|
|
1137
|
+
stdio: ['ignore', out, err],
|
|
1138
|
+
env: {
|
|
1139
|
+
...process.env,
|
|
1140
|
+
SQUEEZR_DESKTOP_HTTPS_PORT: String(DESKTOP_HTTPS_PORT),
|
|
1141
|
+
SQUEEZR_DESKTOP_HTTP_PORT: String(DESKTOP_HTTP_PORT),
|
|
1142
|
+
},
|
|
1143
|
+
})
|
|
1144
|
+
child.unref()
|
|
1145
|
+
// Wait briefly to confirm it didn't immediately exit
|
|
1146
|
+
await new Promise(r => setTimeout(r, 800))
|
|
1147
|
+
if (child.exitCode !== null) {
|
|
1148
|
+
console.error(`Desktop proxy failed to start (exit ${child.exitCode}). Check log: ${DESKTOP_LOG_FILE}`)
|
|
1149
|
+
process.exit(1)
|
|
1150
|
+
}
|
|
1151
|
+
console.log(`Desktop proxy started (pid=${child.pid}).`)
|
|
1152
|
+
console.log(` HTTPS: https://127.0.0.1:${DESKTOP_HTTPS_PORT} (Claude Desktop)`)
|
|
1153
|
+
console.log(` HTTP: http://127.0.0.1:${DESKTOP_HTTP_PORT} (Codex Desktop)`)
|
|
1154
|
+
console.log(` Log: ${DESKTOP_LOG_FILE}`)
|
|
1155
|
+
console.log(``)
|
|
1156
|
+
console.log(`Note: the main Squeezr proxy on 8080 is unaffected by this command.`)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function desktopProxyStop() {
|
|
1160
|
+
let pid = readDesktopPid()
|
|
1161
|
+
if (!pid) {
|
|
1162
|
+
// PID file is missing or its pid is dead — but a listener may still be
|
|
1163
|
+
// bound by an orphan. Try to locate it by port and kill that instead.
|
|
1164
|
+
if (await isDesktopPortBound(DESKTOP_HTTPS_PORT) || await isDesktopPortBound(DESKTOP_HTTP_PORT)) {
|
|
1165
|
+
pid = findPidByPort(DESKTOP_HTTPS_PORT) ?? findPidByPort(DESKTOP_HTTP_PORT)
|
|
1166
|
+
if (!pid) {
|
|
1167
|
+
console.log(`Desktop proxy ports ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} are in use but PID cannot be resolved. Stop the owning process manually.`)
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
console.log(`Recovered orphan desktop proxy pid=${pid} from listener.`)
|
|
1171
|
+
} else {
|
|
1172
|
+
console.log('Desktop proxy is not running.')
|
|
1173
|
+
return
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
process.kill(pid, 'SIGTERM')
|
|
1178
|
+
// Wait up to 3s for graceful exit
|
|
1179
|
+
for (let i = 0; i < 30; i++) {
|
|
1180
|
+
try { process.kill(pid, 0) } catch { break }
|
|
1181
|
+
await new Promise(r => setTimeout(r, 100))
|
|
1182
|
+
}
|
|
1183
|
+
// Force kill if still alive
|
|
1184
|
+
try { process.kill(pid, 'SIGKILL') } catch {}
|
|
1185
|
+
try { fs.unlinkSync(DESKTOP_PID_FILE) } catch {}
|
|
1186
|
+
console.log(`Desktop proxy stopped (was pid=${pid}).`)
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
console.error(`Failed to stop desktop proxy: ${e.message}`)
|
|
1189
|
+
process.exit(1)
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function desktopProxyStatus() {
|
|
1194
|
+
let pid = readDesktopPid()
|
|
1195
|
+
// Fallback when PID file is stale: probe the desktop ports. If the listener
|
|
1196
|
+
// answers, the proxy IS running — just under a different PID than the one
|
|
1197
|
+
// recorded. This is the common shape after an ungraceful restart.
|
|
1198
|
+
if (!pid) {
|
|
1199
|
+
const bound = (await isDesktopPortBound(DESKTOP_HTTPS_PORT))
|
|
1200
|
+
|| (await isDesktopPortBound(DESKTOP_HTTP_PORT))
|
|
1201
|
+
if (!bound) {
|
|
1202
|
+
console.log('Desktop proxy is NOT running.')
|
|
1203
|
+
console.log('Start it with: squeezr desktop start')
|
|
1204
|
+
return
|
|
1205
|
+
}
|
|
1206
|
+
const orphanPid = findPidByPort(DESKTOP_HTTPS_PORT) ?? findPidByPort(DESKTOP_HTTP_PORT)
|
|
1207
|
+
if (orphanPid) {
|
|
1208
|
+
try { fs.writeFileSync(DESKTOP_PID_FILE, String(orphanPid), 'utf-8') } catch {}
|
|
1209
|
+
pid = orphanPid
|
|
1210
|
+
console.log(`Desktop proxy is running (pid=${pid}, recovered from orphan listener).`)
|
|
1211
|
+
} else {
|
|
1212
|
+
console.log(`Desktop proxy listener is bound on ${DESKTOP_HTTPS_PORT}/${DESKTOP_HTTP_PORT} but its PID could not be resolved.`)
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
console.log(`Desktop proxy is running (pid=${pid}).`)
|
|
1216
|
+
}
|
|
1217
|
+
console.log(` HTTPS: https://127.0.0.1:${DESKTOP_HTTPS_PORT} (Claude Desktop)`)
|
|
1218
|
+
console.log(` HTTP: http://127.0.0.1:${DESKTOP_HTTP_PORT} (Codex Desktop)`)
|
|
1219
|
+
console.log(` Log: ${DESKTOP_LOG_FILE}`)
|
|
1220
|
+
// Surface the activation state of the Claude Desktop hosts redirect — the
|
|
1221
|
+
// most common reason "nothing appears in the logs" even when this listener
|
|
1222
|
+
// is bound is that the user never ran `enable-claude-desktop`, so Claude
|
|
1223
|
+
// Desktop's traffic goes directly to api.anthropic.com.
|
|
1224
|
+
const hostsActive = await isClaudeDesktopHostsActive()
|
|
1225
|
+
if (hostsActive) {
|
|
1226
|
+
console.log(``)
|
|
1227
|
+
console.log(` Claude Desktop interception: ACTIVE (hosts redirect set)`)
|
|
1228
|
+
} else {
|
|
1229
|
+
console.log(``)
|
|
1230
|
+
console.log(` Claude Desktop interception: NOT SET`)
|
|
1231
|
+
console.log(` → Run as admin: squeezr enable-claude-desktop`)
|
|
1232
|
+
console.log(` Without this, Claude Desktop talks to api.anthropic.com directly`)
|
|
1233
|
+
console.log(` and no traffic will reach this proxy.`)
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Best-effort detection of whether the hosts file currently redirects
|
|
1238
|
+
// api.anthropic.com to 127.0.0.1 (the prerequisite for Claude Desktop
|
|
1239
|
+
// traffic to reach the desktop proxy).
|
|
1240
|
+
async function isClaudeDesktopHostsActive() {
|
|
1241
|
+
try {
|
|
1242
|
+
const hostsPath = process.platform === 'win32'
|
|
1243
|
+
? 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
|
1244
|
+
: '/etc/hosts'
|
|
1245
|
+
const txt = fs.readFileSync(hostsPath, 'utf-8')
|
|
1246
|
+
return /^\s*127\.0\.0\.1\s+api\.anthropic\.com\b/m.test(txt)
|
|
1247
|
+
|| txt.includes('# squeezr-claude-desktop BEGIN')
|
|
1248
|
+
} catch { return false }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ── Codex Desktop config helper ──────────────────────────────────────────────
|
|
1252
|
+
// Writes openai_base_url to ~/.codex/config.toml so Codex Desktop routes
|
|
1253
|
+
// through Squeezr's HTTP proxy (no MITM needed — uses standard base URL).
|
|
1254
|
+
|
|
1255
|
+
function configureCodexDesktop(port) {
|
|
1256
|
+
const codexDir = path.join(os.homedir(), '.codex')
|
|
1257
|
+
const codexToml = path.join(codexDir, 'config.toml')
|
|
1258
|
+
const url = `http://localhost:${port}/v1`
|
|
1259
|
+
const marker = 'openai_base_url'
|
|
1260
|
+
const line = `openai_base_url = "${url}"`
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
fs.mkdirSync(codexDir, { recursive: true })
|
|
1264
|
+
if (fs.existsSync(codexToml)) {
|
|
1265
|
+
let content = fs.readFileSync(codexToml, 'utf-8')
|
|
1266
|
+
// Match openai_base_url = "anything" — including empty "" which Codex Desktop
|
|
1267
|
+
// sometimes writes back to its config (bug we observed in the wild).
|
|
1268
|
+
// Force-overwrite any existing value, even if empty.
|
|
1269
|
+
const re = /openai_base_url\s*=\s*"[^"]*"/
|
|
1270
|
+
if (re.test(content)) {
|
|
1271
|
+
const before = content.match(re)?.[0] ?? ''
|
|
1272
|
+
content = content.replace(re, line)
|
|
1273
|
+
fs.writeFileSync(codexToml, content)
|
|
1274
|
+
if (before === `openai_base_url = ""`) {
|
|
1275
|
+
console.log(` [ok] Codex Desktop: FIXED empty openai_base_url in ${codexToml}`)
|
|
1276
|
+
} else {
|
|
1277
|
+
console.log(` [ok] Codex Desktop: updated ${codexToml}`)
|
|
1278
|
+
}
|
|
1279
|
+
} else {
|
|
1280
|
+
fs.appendFileSync(codexToml, `\n# Squeezr: route Codex Desktop through the compression proxy\n${line}\n`)
|
|
1281
|
+
console.log(` [ok] Codex Desktop: configured ${codexToml}`)
|
|
1282
|
+
}
|
|
1283
|
+
} else {
|
|
1284
|
+
fs.writeFileSync(codexToml, `# Squeezr: route Codex Desktop through the compression proxy\n${line}\n`)
|
|
1285
|
+
console.log(` [ok] Codex Desktop: created ${codexToml}`)
|
|
1286
|
+
}
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
console.log(` [warn] Codex Desktop config: ${err.message}`)
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// ── squeezr setup ─────────────────────────────────────────────────────────────
|
|
1293
|
+
|
|
1294
|
+
async function setupWindows() {
|
|
1295
|
+
const squeezrBin = process.argv[1]
|
|
1296
|
+
const nodeExe = process.execPath
|
|
1297
|
+
const distIndex = path.join(ROOT, 'dist', 'index.js')
|
|
1298
|
+
|
|
1299
|
+
console.log('Setting up Squeezr for Windows...\n')
|
|
1300
|
+
|
|
1301
|
+
// 1. Set env vars permanently via setx (user scope, no admin needed)
|
|
1302
|
+
const port = getPort()
|
|
1303
|
+
const mitmPort = getMitmPort(port)
|
|
1304
|
+
const caPath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'ca.crt')
|
|
1305
|
+
const vars = {
|
|
1306
|
+
ANTHROPIC_BASE_URL: `http://localhost:${port}`,
|
|
1307
|
+
// openai_base_url NOT set — Codex uses WebSocket and must go via HTTPS_PROXY/MITM,
|
|
1308
|
+
// not through the HTTP proxy. Setting it breaks Codex's ws:// connections.
|
|
1309
|
+
GEMINI_API_BASE_URL: `http://localhost:${port}`,
|
|
1310
|
+
// HTTPS_PROXY intentionally NOT set globally — it routes ALL HTTPS traffic through
|
|
1311
|
+
// the MITM proxy which breaks Claude Code, npm, and other tools. Only Codex needs it.
|
|
1312
|
+
// Users who need Codex MITM can set it per-session: $env:HTTPS_PROXY="http://localhost:8081"
|
|
1313
|
+
NODE_EXTRA_CA_CERTS: caPath,
|
|
1314
|
+
}
|
|
1315
|
+
// Clean up HTTPS_PROXY from registry if set by older versions
|
|
1316
|
+
try { execSync('reg delete "HKCU\\Environment" /v HTTPS_PROXY /f', { stdio: 'pipe' }) } catch {}
|
|
1317
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1318
|
+
try {
|
|
1319
|
+
execSync(`setx ${key} "${value}"`, { stdio: 'pipe' })
|
|
1320
|
+
console.log(` [ok] ${key}=${value}`)
|
|
1321
|
+
} catch {
|
|
1322
|
+
console.log(` [skip] ${key} could not be set`)
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// 1b. Configure Codex Desktop (~/.codex/config.toml → openai_base_url)
|
|
1327
|
+
// On Windows, ANTHROPIC_BASE_URL from setx is already visible to all GUI apps
|
|
1328
|
+
// (including Claude Desktop) since user-level env vars propagate to new processes.
|
|
1329
|
+
configureCodexDesktop(port)
|
|
1330
|
+
|
|
1331
|
+
// 1c. Register MCP server in Claude Desktop + Codex Desktop automatically
|
|
1332
|
+
await mcpInstall()
|
|
1333
|
+
|
|
1334
|
+
// 1c. Install PowerShell wrapper so env vars auto-refresh after start/setup/update
|
|
1335
|
+
installShellWrapper()
|
|
1336
|
+
|
|
1337
|
+
// 2. Auto-start: try NSSM (Windows service, survives crashes) → fallback to Task Scheduler
|
|
1338
|
+
const logDir = path.join(os.homedir(), '.squeezr')
|
|
1339
|
+
const serviceName = 'SqueezrProxy'
|
|
1340
|
+
let autoStartOk = false
|
|
1341
|
+
|
|
1342
|
+
const nssmAvailable = (() => {
|
|
1343
|
+
try { execSync('where nssm', { stdio: 'pipe' }); return true } catch { return false }
|
|
1344
|
+
})()
|
|
1345
|
+
|
|
1346
|
+
if (nssmAvailable) {
|
|
1347
|
+
try {
|
|
1348
|
+
// Remove existing service if present (ignore errors)
|
|
1349
|
+
try { execSync(`nssm stop ${serviceName}`, { stdio: 'pipe' }) } catch {}
|
|
1350
|
+
try { execSync(`nssm remove ${serviceName} confirm`, { stdio: 'pipe' }) } catch {}
|
|
1351
|
+
|
|
1352
|
+
execSync(`nssm install ${serviceName} "${nodeExe}" "${distIndex}"`, { stdio: 'pipe' })
|
|
1353
|
+
execSync(`nssm set ${serviceName} AppDirectory "${ROOT}"`, { stdio: 'pipe' })
|
|
1354
|
+
execSync(`nssm set ${serviceName} AppStdout "${logDir}\\service-stdout.log"`, { stdio: 'pipe' })
|
|
1355
|
+
execSync(`nssm set ${serviceName} AppStderr "${logDir}\\service-stderr.log"`, { stdio: 'pipe' })
|
|
1356
|
+
execSync(`nssm set ${serviceName} AppRotateFiles 1`, { stdio: 'pipe' })
|
|
1357
|
+
execSync(`nssm set ${serviceName} AppRotateSeconds 86400`, { stdio: 'pipe' })
|
|
1358
|
+
execSync(`nssm set ${serviceName} AppExit Default Restart`, { stdio: 'pipe' })
|
|
1359
|
+
execSync(`nssm set ${serviceName} AppRestartDelay 3000`, { stdio: 'pipe' })
|
|
1360
|
+
execSync(`nssm set ${serviceName} Description "Squeezr AI token compression proxy on port 8080"`, { stdio: 'pipe' })
|
|
1361
|
+
execSync(`nssm start ${serviceName}`, { stdio: 'pipe' })
|
|
1362
|
+
console.log(` [ok] Auto-start registered as Windows service via NSSM (auto-restart on crash)`)
|
|
1363
|
+
autoStartOk = true
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
const msg = err.stderr?.toString() || err.message || ''
|
|
1366
|
+
if (msg.includes('Access') || msg.includes('admin') || msg.includes('5')) {
|
|
1367
|
+
console.log(` [warn] NSSM requires admin — run as Administrator for service install`)
|
|
1368
|
+
} else {
|
|
1369
|
+
console.log(` [warn] NSSM install failed: ${msg.trim().split('\n')[0]}`)
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (!autoStartOk) {
|
|
1375
|
+
// Fallback: Task Scheduler (no crash recovery, but works without admin)
|
|
1376
|
+
const taskName = 'Squeezr'
|
|
1377
|
+
const nodeArg = `${nodeExe} \`"${distIndex}\`"`
|
|
1378
|
+
const ps = [
|
|
1379
|
+
`$e = Get-ScheduledTask -TaskName '${taskName}' -ErrorAction SilentlyContinue`,
|
|
1380
|
+
`if ($e) { Unregister-ScheduledTask -TaskName '${taskName}' -Confirm:$false }`,
|
|
1381
|
+
`$a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-WindowStyle Hidden -NonInteractive -Command "${nodeArg}"' -WorkingDirectory '${ROOT}'`,
|
|
1382
|
+
`$t = New-ScheduledTaskTrigger -AtLogon`,
|
|
1383
|
+
`$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit 0 -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1)`,
|
|
1384
|
+
`Register-ScheduledTask -TaskName '${taskName}' -Action $a -Trigger $t -Settings $s -Force | Out-Null`,
|
|
1385
|
+
].join('; ')
|
|
1386
|
+
try {
|
|
1387
|
+
execSync(`powershell -NoProfile -Command "${ps}"`, { stdio: 'pipe' })
|
|
1388
|
+
console.log(` [ok] Auto-start registered in Task Scheduler (install NSSM for crash recovery)`)
|
|
1389
|
+
autoStartOk = true
|
|
1390
|
+
} catch {
|
|
1391
|
+
// ignore — will fall through to Startup folder VBS
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (!autoStartOk) {
|
|
1396
|
+
// Final fallback: VBS script in user Startup folder (no admin, no special tools)
|
|
1397
|
+
try {
|
|
1398
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
1399
|
+
const squeezrCmd = path.join(appData, 'npm', 'squeezr.cmd')
|
|
1400
|
+
const startupDir = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
|
|
1401
|
+
const vbsPath = path.join(startupDir, 'squeezr-start.vbs')
|
|
1402
|
+
const cmdToRun = fs.existsSync(squeezrCmd) ? squeezrCmd : nodeExe
|
|
1403
|
+
const cmdArg = fs.existsSync(squeezrCmd) ? 'start' : `"${distIndex}"`
|
|
1404
|
+
const vbsContent = [
|
|
1405
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
1406
|
+
`WshShell.Run """${cmdToRun}"" ${cmdArg}", 0, False`,
|
|
1407
|
+
'',
|
|
1408
|
+
].join('\r\n')
|
|
1409
|
+
fs.mkdirSync(startupDir, { recursive: true })
|
|
1410
|
+
fs.writeFileSync(vbsPath, vbsContent)
|
|
1411
|
+
console.log(` [ok] Auto-start registered in Startup folder (${vbsPath})`)
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
console.log(` [warn] Auto-start failed — run as admin or install NSSM: https://nssm.cc`)
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// 3. Start Squeezr right now as a detached background process (no window)
|
|
1418
|
+
// Logs go to ~/.squeezr/squeezr.log
|
|
1419
|
+
const logFile = path.join(logDir, 'squeezr.log')
|
|
1420
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
1421
|
+
const logFd = fs.openSync(logFile, 'a')
|
|
1422
|
+
const child = spawn(nodeExe, [distIndex], {
|
|
1423
|
+
detached: true,
|
|
1424
|
+
stdio: ['ignore', logFd, logFd],
|
|
1425
|
+
windowsHide: true,
|
|
1426
|
+
cwd: ROOT,
|
|
1427
|
+
})
|
|
1428
|
+
child.unref()
|
|
1429
|
+
fs.closeSync(logFd)
|
|
1430
|
+
console.log(` [ok] Squeezr started in background (pid ${child.pid})`)
|
|
1431
|
+
console.log(` [ok] Logs → ${logFile}`)
|
|
1432
|
+
|
|
1433
|
+
// 4. Trust MITM CA in Windows Certificate Store (for Rust apps like Codex)
|
|
1434
|
+
// Node.js apps use NODE_EXTRA_CA_CERTS; Rust/native apps need the cert store.
|
|
1435
|
+
// The CA is generated on first proxy start — wait briefly for it to appear.
|
|
1436
|
+
const waitForCa = (retries = 10, interval = 500) => new Promise(resolve => {
|
|
1437
|
+
const check = (n) => {
|
|
1438
|
+
if (fs.existsSync(caPath)) return resolve(true)
|
|
1439
|
+
if (n <= 0) return resolve(false)
|
|
1440
|
+
setTimeout(() => check(n - 1), interval)
|
|
1441
|
+
}
|
|
1442
|
+
check(retries)
|
|
1443
|
+
})
|
|
1444
|
+
|
|
1445
|
+
waitForCa().then(found => {
|
|
1446
|
+
if (!found) {
|
|
1447
|
+
console.log(` [warn] MITM CA not found yet — run 'squeezr setup' again after first start`)
|
|
1448
|
+
printDone()
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
// Try machine store (admin) first, fall back to user store (no admin)
|
|
1452
|
+
try {
|
|
1453
|
+
execSync(`certutil -addstore -f Root "${caPath}"`, { stdio: 'pipe' })
|
|
1454
|
+
console.log(` [ok] MITM CA trusted in Windows Certificate Store (machine-level)`)
|
|
1455
|
+
} catch {
|
|
1456
|
+
try {
|
|
1457
|
+
execSync(`certutil -addstore -user Root "${caPath}"`, { stdio: 'pipe' })
|
|
1458
|
+
console.log(` [ok] MITM CA trusted in Windows Certificate Store (user-level)`)
|
|
1459
|
+
} catch {
|
|
1460
|
+
console.log(` [warn] Could not trust MITM CA — trust manually:`)
|
|
1461
|
+
console.log(` certutil -addstore -user Root "${caPath}"`)
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
printDone()
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
function printDone() {
|
|
1468
|
+
console.log(`
|
|
1469
|
+
Done!
|
|
1470
|
+
|
|
1471
|
+
Squeezr is running on http://localhost:${port}
|
|
1472
|
+
MITM proxy on http://localhost:${mitmPort} (Codex CLI TLS interception)
|
|
1473
|
+
|
|
1474
|
+
Configured:
|
|
1475
|
+
Claude Code ANTHROPIC_BASE_URL=http://localhost:${port}
|
|
1476
|
+
Claude Desktop same — setx env var is visible to all GUI apps
|
|
1477
|
+
Codex Desktop ~/.codex/config.toml openai_base_url set
|
|
1478
|
+
Codex CLI HTTPS_PROXY=http://localhost:${mitmPort} codex (per-session)
|
|
1479
|
+
Aider / OpenCode ANTHROPIC_BASE_URL + openai_base_url set
|
|
1480
|
+
Gemini CLI GEMINI_API_BASE_URL=http://localhost:${port}
|
|
1481
|
+
|
|
1482
|
+
squeezr status — check it's running
|
|
1483
|
+
squeezr gain — see token savings
|
|
1484
|
+
`)
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function setupUnix() {
|
|
1489
|
+
const squeezrBin = process.argv[1]
|
|
1490
|
+
const nodeExe = process.execPath
|
|
1491
|
+
const platform = process.platform
|
|
1492
|
+
|
|
1493
|
+
console.log(`Setting up Squeezr for ${platform === 'darwin' ? 'macOS' : 'Linux'}...\n`)
|
|
1494
|
+
|
|
1495
|
+
// 1. Set env vars + auto-heal guard in shell profile
|
|
1496
|
+
const distIndex = path.join(ROOT, 'dist', 'index.js')
|
|
1497
|
+
const port = getPort()
|
|
1498
|
+
const mitmPort = getMitmPort(port)
|
|
1499
|
+
const bundlePath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'bundle.crt')
|
|
1500
|
+
// The auto-heal validates that whatever answers on the configured port is
|
|
1501
|
+
// actually squeezr (by checking the magic `identity` field). A bare
|
|
1502
|
+
// `curl -sf .../squeezr/health` is NOT enough because curl returns success on
|
|
1503
|
+
// 3xx redirects, so a foreign service (e.g. an Apache+WordPress container on
|
|
1504
|
+
// 8080) would be mistaken for a healthy squeezr — and Claude Code would then
|
|
1505
|
+
// route its API requests into the wrong service, producing cryptic errors
|
|
1506
|
+
// like `undefined is not an object (evaluating '$.speed')`.
|
|
1507
|
+
const shellBlock = [
|
|
1508
|
+
`# squeezr env vars`,
|
|
1509
|
+
`export ANTHROPIC_BASE_URL=http://localhost:${port}`,
|
|
1510
|
+
`export openai_base_url=http://localhost:${port}`,
|
|
1511
|
+
`export GEMINI_API_BASE_URL=http://localhost:${port}`,
|
|
1512
|
+
`export NODE_EXTRA_CA_CERTS=${bundlePath}`,
|
|
1513
|
+
`# NOTE: HTTPS_PROXY is intentionally NOT set globally — it would route ALL HTTPS`,
|
|
1514
|
+
`# (including Claude Code) through the MITM proxy and cause 502 errors.`,
|
|
1515
|
+
`# For Codex, set it per-session only: HTTPS_PROXY=http://localhost:${mitmPort} codex`,
|
|
1516
|
+
`# squeezr auto-heal: start proxy if not running (validates identity, not just HTTP 200)`,
|
|
1517
|
+
`_squeezr_alive() {`,
|
|
1518
|
+
` curl -sf --max-time 2 "http://localhost:${port}/squeezr/health" 2>/dev/null | grep -q '"identity":"squeezr"'`,
|
|
1519
|
+
`}`,
|
|
1520
|
+
`if ! _squeezr_alive; then`,
|
|
1521
|
+
` nohup ${nodeExe} ${distIndex} >> "${os.homedir()}/.squeezr/squeezr.log" 2>&1 &`,
|
|
1522
|
+
` disown`,
|
|
1523
|
+
`fi`,
|
|
1524
|
+
`unset -f _squeezr_alive`,
|
|
1525
|
+
].join('\n')
|
|
1526
|
+
const marker = '# squeezr env vars'
|
|
1527
|
+
|
|
1528
|
+
// Env-only block (no auto-heal) for .profile — loaded by login shells
|
|
1529
|
+
// before .bashrc's "case $-" interactive guard
|
|
1530
|
+
const envOnlyBlock = [
|
|
1531
|
+
`# squeezr env vars`,
|
|
1532
|
+
`export ANTHROPIC_BASE_URL=http://localhost:${port}`,
|
|
1533
|
+
`export openai_base_url=http://localhost:${port}`,
|
|
1534
|
+
`export GEMINI_API_BASE_URL=http://localhost:${port}`,
|
|
1535
|
+
`export NODE_EXTRA_CA_CERTS=${bundlePath}`,
|
|
1536
|
+
].join('\n')
|
|
1537
|
+
|
|
1538
|
+
// Write env vars to ~/.profile (login shell — always loaded)
|
|
1539
|
+
const profilePath = path.join(os.homedir(), '.profile')
|
|
1540
|
+
try {
|
|
1541
|
+
const profileContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf-8') : ''
|
|
1542
|
+
if (!profileContent.includes(marker)) {
|
|
1543
|
+
fs.appendFileSync(profilePath, `\n${envOnlyBlock}\n`)
|
|
1544
|
+
console.log(` [ok] Env vars added to ${profilePath}`)
|
|
1545
|
+
} else {
|
|
1546
|
+
const updated = profileContent.replace(/# squeezr env vars[\s\S]*?(?=\n(?!export )|\n*$)/, envOnlyBlock)
|
|
1547
|
+
fs.writeFileSync(profilePath, updated)
|
|
1548
|
+
console.log(` [ok] Env vars updated in ${profilePath}`)
|
|
1549
|
+
}
|
|
1550
|
+
} catch {}
|
|
1551
|
+
|
|
1552
|
+
// Write full block (env + auto-heal) to interactive shell profile
|
|
1553
|
+
const profiles = [
|
|
1554
|
+
path.join(os.homedir(), '.zshrc'),
|
|
1555
|
+
path.join(os.homedir(), '.bashrc'),
|
|
1556
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
1557
|
+
]
|
|
1558
|
+
const profile = profiles.find(p => fs.existsSync(p)) ?? profiles[0]
|
|
1559
|
+
const existing = fs.existsSync(profile) ? fs.readFileSync(profile, 'utf-8') : ''
|
|
1560
|
+
if (!existing.includes(marker)) {
|
|
1561
|
+
fs.appendFileSync(profile, `\n${shellBlock}\n`)
|
|
1562
|
+
console.log(` [ok] Env vars + auto-heal added to ${profile}`)
|
|
1563
|
+
} else {
|
|
1564
|
+
const updatedContent = existing.replace(
|
|
1565
|
+
/# squeezr env vars[\s\S]*?fi\n?/,
|
|
1566
|
+
shellBlock + '\n'
|
|
1567
|
+
)
|
|
1568
|
+
fs.writeFileSync(profile, updatedContent)
|
|
1569
|
+
console.log(` [ok] Env vars + auto-heal updated in ${profile}`)
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// 2. Configure Codex Desktop + Claude Desktop
|
|
1573
|
+
// Codex Desktop reads ~/.codex/config.toml (openai_base_url key).
|
|
1574
|
+
configureCodexDesktop(port)
|
|
1575
|
+
|
|
1576
|
+
// Register MCP server in Claude Desktop and Codex Desktop automatically
|
|
1577
|
+
await mcpInstall()
|
|
1578
|
+
|
|
1579
|
+
// Claude Desktop (GUI app) does not read shell env vars.
|
|
1580
|
+
// macOS: inject via a launchd env-setter plist (persists across reboots).
|
|
1581
|
+
// Linux: write to ~/.config/environment.d/ (systemd user env, read by GUI apps).
|
|
1582
|
+
if (platform === 'darwin') {
|
|
1583
|
+
const envPlistDir = path.join(os.homedir(), 'Library', 'LaunchAgents')
|
|
1584
|
+
const envPlistPath = path.join(envPlistDir, 'com.squeezr.env.plist')
|
|
1585
|
+
fs.mkdirSync(envPlistDir, { recursive: true })
|
|
1586
|
+
const envVars = [
|
|
1587
|
+
['ANTHROPIC_BASE_URL', `http://localhost:${port}`],
|
|
1588
|
+
['GEMINI_API_BASE_URL', `http://localhost:${port}`],
|
|
1589
|
+
['NODE_EXTRA_CA_CERTS', bundlePath],
|
|
1590
|
+
]
|
|
1591
|
+
const envSetCmds = envVars.map(([k, v]) => `launchctl setenv ${k} "${v}"`).join(' && ')
|
|
1592
|
+
fs.writeFileSync(envPlistPath, `<?xml version="1.0" encoding="UTF-8"?>
|
|
1593
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1594
|
+
<plist version="1.0">
|
|
1595
|
+
<dict>
|
|
1596
|
+
<key>Label</key><string>com.squeezr.env</string>
|
|
1597
|
+
<key>ProgramArguments</key>
|
|
1598
|
+
<array><string>/bin/sh</string><string>-c</string><string>${envSetCmds}</string></array>
|
|
1599
|
+
<key>RunAtLoad</key><true/>
|
|
1600
|
+
</dict>
|
|
1601
|
+
</plist>`)
|
|
1602
|
+
try {
|
|
1603
|
+
execSync(`launchctl unload "${envPlistPath}" 2>/dev/null; launchctl load -w "${envPlistPath}"`, { stdio: 'pipe' })
|
|
1604
|
+
console.log(` [ok] Claude Desktop: env vars set via launchctl (visible to all GUI apps)`)
|
|
1605
|
+
} catch {
|
|
1606
|
+
console.log(` [warn] Claude Desktop: launchctl env plist failed — restart Claude Desktop manually after setup`)
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
// Linux: systemd user environment.d — read by all user processes incl. GUI apps
|
|
1610
|
+
const envDDir = path.join(os.homedir(), '.config', 'environment.d')
|
|
1611
|
+
const envDPath = path.join(envDDir, 'squeezr.conf')
|
|
1612
|
+
fs.mkdirSync(envDDir, { recursive: true })
|
|
1613
|
+
fs.writeFileSync(envDPath, [
|
|
1614
|
+
`# Squeezr — visible to all GUI apps (Claude Desktop, etc.)`,
|
|
1615
|
+
`ANTHROPIC_BASE_URL=http://localhost:${port}`,
|
|
1616
|
+
`GEMINI_API_BASE_URL=http://localhost:${port}`,
|
|
1617
|
+
`NODE_EXTRA_CA_CERTS=${bundlePath}`,
|
|
1618
|
+
].join('\n') + '\n')
|
|
1619
|
+
console.log(` [ok] Claude Desktop: env vars written to ${envDPath} (effective after next login)`)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// 3a. macOS — launchd
|
|
1623
|
+
if (platform === 'darwin') {
|
|
1624
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents')
|
|
1625
|
+
const plistPath = path.join(plistDir, 'com.squeezr.plist')
|
|
1626
|
+
fs.mkdirSync(plistDir, { recursive: true })
|
|
1627
|
+
fs.writeFileSync(plistPath, `<?xml version="1.0" encoding="UTF-8"?>
|
|
1628
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1629
|
+
<plist version="1.0">
|
|
1630
|
+
<dict>
|
|
1631
|
+
<key>Label</key><string>com.squeezr</string>
|
|
1632
|
+
<key>ProgramArguments</key>
|
|
1633
|
+
<array><string>${nodeExe}</string><string>${squeezrBin}</string></array>
|
|
1634
|
+
<key>RunAtLoad</key><true/>
|
|
1635
|
+
<key>KeepAlive</key><true/>
|
|
1636
|
+
<key>StandardOutPath</key><string>${os.homedir()}/.squeezr/squeezr.log</string>
|
|
1637
|
+
<key>StandardErrorPath</key><string>${os.homedir()}/.squeezr/squeezr.log</string>
|
|
1638
|
+
</dict>
|
|
1639
|
+
</plist>`)
|
|
1640
|
+
try {
|
|
1641
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null; launchctl load "${plistPath}"`, { stdio: 'pipe' })
|
|
1642
|
+
console.log(` [ok] Auto-start registered via launchd`)
|
|
1643
|
+
console.log(` [ok] Squeezr started now`)
|
|
1644
|
+
} catch {
|
|
1645
|
+
console.log(` [warn] launchctl failed — starting in background`)
|
|
1646
|
+
spawn(nodeExe, [squeezrBin], { detached: true, stdio: 'ignore' }).unref()
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Trust MITM CA in macOS Keychain (for Codex TLS interception)
|
|
1650
|
+
// CA is generated on first proxy start — wait briefly for it to appear
|
|
1651
|
+
const caPath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'ca.crt')
|
|
1652
|
+
const waitForCa = (retries = 10, interval = 500) => new Promise(resolve => {
|
|
1653
|
+
const check = (n) => {
|
|
1654
|
+
if (fs.existsSync(caPath)) return resolve(true)
|
|
1655
|
+
if (n <= 0) return resolve(false)
|
|
1656
|
+
setTimeout(() => check(n - 1), interval)
|
|
1657
|
+
}
|
|
1658
|
+
check(retries)
|
|
1659
|
+
})
|
|
1660
|
+
waitForCa().then(found => {
|
|
1661
|
+
if (found) {
|
|
1662
|
+
try {
|
|
1663
|
+
execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${caPath}" 2>/dev/null`, { stdio: 'pipe' })
|
|
1664
|
+
console.log(` [ok] MITM CA trusted in macOS Keychain`)
|
|
1665
|
+
} catch {
|
|
1666
|
+
console.log(` [info] To trust MITM CA for Codex: security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${caPath}"`)
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
})
|
|
1670
|
+
|
|
1671
|
+
// 3b. Linux — systemd
|
|
1672
|
+
} else {
|
|
1673
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user')
|
|
1674
|
+
const servicePath = path.join(serviceDir, 'squeezr.service')
|
|
1675
|
+
fs.mkdirSync(serviceDir, { recursive: true })
|
|
1676
|
+
fs.writeFileSync(servicePath, `[Unit]
|
|
1677
|
+
Description=Squeezr AI proxy
|
|
1678
|
+
After=network.target
|
|
1679
|
+
|
|
1680
|
+
[Service]
|
|
1681
|
+
ExecStart=${nodeExe} ${squeezrBin}
|
|
1682
|
+
Restart=always
|
|
1683
|
+
RestartSec=5
|
|
1684
|
+
|
|
1685
|
+
[Install]
|
|
1686
|
+
WantedBy=default.target
|
|
1687
|
+
`)
|
|
1688
|
+
try {
|
|
1689
|
+
execSync('systemctl --user daemon-reload && systemctl --user enable --now squeezr', { stdio: 'pipe' })
|
|
1690
|
+
console.log(` [ok] Auto-start registered via systemd`)
|
|
1691
|
+
console.log(` [ok] Squeezr started now`)
|
|
1692
|
+
} catch {
|
|
1693
|
+
console.log(` [warn] systemctl failed — starting in background`)
|
|
1694
|
+
spawn(nodeExe, [squeezrBin], { detached: true, stdio: 'ignore' }).unref()
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
console.log(`
|
|
1699
|
+
Done!
|
|
1700
|
+
|
|
1701
|
+
Squeezr is running on http://localhost:${port}
|
|
1702
|
+
|
|
1703
|
+
Configured:
|
|
1704
|
+
Claude Code ANTHROPIC_BASE_URL=http://localhost:${port}
|
|
1705
|
+
Claude Desktop ${platform === 'darwin' ? 'env vars set via launchctl (restart app once)' : 'env vars in ~/.config/environment.d/ (re-login to activate)'}
|
|
1706
|
+
Codex Desktop ~/.codex/config.toml openai_base_url set
|
|
1707
|
+
Codex CLI HTTPS_PROXY=http://localhost:${mitmPort} codex (per-session)
|
|
1708
|
+
Aider / OpenCode ANTHROPIC_BASE_URL + openai_base_url set
|
|
1709
|
+
Gemini CLI GEMINI_API_BASE_URL=http://localhost:${port}
|
|
1710
|
+
|
|
1711
|
+
Run: source ${profile} (or open a new terminal)
|
|
1712
|
+
squeezr status — check it's running
|
|
1713
|
+
squeezr gain — see token savings
|
|
1714
|
+
`)
|
|
1715
|
+
installShellWrapper()
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// ── WSL2 detection ───────────────────────────────────────────────────────────
|
|
1719
|
+
|
|
1720
|
+
function isWSL() {
|
|
1721
|
+
try {
|
|
1722
|
+
const release = fs.readFileSync('/proc/version', 'utf-8')
|
|
1723
|
+
return /microsoft|wsl/i.test(release)
|
|
1724
|
+
} catch {
|
|
1725
|
+
return false
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// ── squeezr setup — WSL2 ────────────────────────────────────────────────────
|
|
1730
|
+
|
|
1731
|
+
async function setupWSL() {
|
|
1732
|
+
const nodeExe = process.execPath
|
|
1733
|
+
const distIndex = path.join(ROOT, 'dist', 'index.js')
|
|
1734
|
+
|
|
1735
|
+
console.log('Setting up Squeezr for WSL2...\n')
|
|
1736
|
+
|
|
1737
|
+
// 1. Set env vars + auto-heal guard in WSL shell profile (.bashrc / .zshrc)
|
|
1738
|
+
// The guard checks if the proxy is alive on terminal open. If not, it starts
|
|
1739
|
+
// it in the background. This is the safety net for WSL2 where systemd and
|
|
1740
|
+
// Task Scheduler may both fail.
|
|
1741
|
+
const port = getPort()
|
|
1742
|
+
const mitmPort = getMitmPort(port)
|
|
1743
|
+
const bundlePath = path.join(os.homedir(), '.squeezr', 'mitm-ca', 'bundle.crt')
|
|
1744
|
+
const shellBlock = [
|
|
1745
|
+
`# squeezr env vars`,
|
|
1746
|
+
`export ANTHROPIC_BASE_URL=http://localhost:${port}`,
|
|
1747
|
+
`export openai_base_url=http://localhost:${port}`,
|
|
1748
|
+
`export GEMINI_API_BASE_URL=http://localhost:${port}`,
|
|
1749
|
+
`export NODE_EXTRA_CA_CERTS=${bundlePath}`,
|
|
1750
|
+
`# NOTE: HTTPS_PROXY is intentionally NOT set globally — set per-session for Codex only:`,
|
|
1751
|
+
`# HTTPS_PROXY=http://localhost:${mitmPort} codex`,
|
|
1752
|
+
`# squeezr auto-heal: start proxy if not running (validates identity, not just HTTP 200)`,
|
|
1753
|
+
`_squeezr_alive() {`,
|
|
1754
|
+
` curl -sf --max-time 2 "http://localhost:${port}/squeezr/health" 2>/dev/null | grep -q '"identity":"squeezr"'`,
|
|
1755
|
+
`}`,
|
|
1756
|
+
`if ! _squeezr_alive; then`,
|
|
1757
|
+
` nohup ${nodeExe} ${distIndex} >> "${os.homedir()}/.squeezr/squeezr.log" 2>&1 &`,
|
|
1758
|
+
` disown`,
|
|
1759
|
+
`fi`,
|
|
1760
|
+
`unset -f _squeezr_alive`,
|
|
1761
|
+
].join('\n')
|
|
1762
|
+
const marker = '# squeezr env vars'
|
|
1763
|
+
|
|
1764
|
+
// Env-only block for .profile (loaded before .bashrc's interactive guard)
|
|
1765
|
+
const envOnlyBlock = [
|
|
1766
|
+
`# squeezr env vars`,
|
|
1767
|
+
`export ANTHROPIC_BASE_URL=http://localhost:${port}`,
|
|
1768
|
+
`export openai_base_url=http://localhost:${port}`,
|
|
1769
|
+
`export GEMINI_API_BASE_URL=http://localhost:${port}`,
|
|
1770
|
+
`export NODE_EXTRA_CA_CERTS=${bundlePath}`,
|
|
1771
|
+
].join('\n')
|
|
1772
|
+
|
|
1773
|
+
const profilePath = path.join(os.homedir(), '.profile')
|
|
1774
|
+
try {
|
|
1775
|
+
const profileContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf-8') : ''
|
|
1776
|
+
if (!profileContent.includes(marker)) {
|
|
1777
|
+
fs.appendFileSync(profilePath, `\n${envOnlyBlock}\n`)
|
|
1778
|
+
console.log(` [ok] Env vars added to ${profilePath}`)
|
|
1779
|
+
} else {
|
|
1780
|
+
const updated = profileContent.replace(/# squeezr env vars[\s\S]*?(?=\n(?!export )|\n*$)/, envOnlyBlock)
|
|
1781
|
+
fs.writeFileSync(profilePath, updated)
|
|
1782
|
+
console.log(` [ok] Env vars updated in ${profilePath}`)
|
|
1783
|
+
}
|
|
1784
|
+
} catch {}
|
|
1785
|
+
|
|
1786
|
+
// Full block (env + auto-heal) in interactive shell profile
|
|
1787
|
+
const profiles = [
|
|
1788
|
+
path.join(os.homedir(), '.zshrc'),
|
|
1789
|
+
path.join(os.homedir(), '.bashrc'),
|
|
1790
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
1791
|
+
]
|
|
1792
|
+
const profile = profiles.find(p => fs.existsSync(p)) ?? profiles[1]
|
|
1793
|
+
const existing = fs.existsSync(profile) ? fs.readFileSync(profile, 'utf-8') : ''
|
|
1794
|
+
if (!existing.includes(marker)) {
|
|
1795
|
+
fs.appendFileSync(profile, `\n${shellBlock}\n`)
|
|
1796
|
+
console.log(` [ok] Env vars + auto-heal added to ${profile}`)
|
|
1797
|
+
} else {
|
|
1798
|
+
const updatedContent = existing.replace(
|
|
1799
|
+
/# squeezr env vars[\s\S]*?fi\n?/,
|
|
1800
|
+
shellBlock + '\n'
|
|
1801
|
+
)
|
|
1802
|
+
fs.writeFileSync(profile, updatedContent)
|
|
1803
|
+
console.log(` [ok] Env vars + auto-heal updated in ${profile}`)
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// 2. Set Windows env vars via setx.exe (so Windows-launched CLIs see them)
|
|
1807
|
+
// ANTHROPIC_BASE_URL via setx is also visible to Claude Desktop (GUI app).
|
|
1808
|
+
const setxExe = '/mnt/c/Windows/System32/setx.exe'
|
|
1809
|
+
const winVars = {
|
|
1810
|
+
ANTHROPIC_BASE_URL: 'http://localhost:8080',
|
|
1811
|
+
openai_base_url: 'http://localhost:8080',
|
|
1812
|
+
GEMINI_API_BASE_URL: 'http://localhost:8080',
|
|
1813
|
+
}
|
|
1814
|
+
if (fs.existsSync(setxExe)) {
|
|
1815
|
+
for (const [key, value] of Object.entries(winVars)) {
|
|
1816
|
+
try {
|
|
1817
|
+
execSync(`"${setxExe}" ${key} "${value}"`, { stdio: 'pipe' })
|
|
1818
|
+
console.log(` [ok] Windows env: ${key}=${value}`)
|
|
1819
|
+
} catch {
|
|
1820
|
+
console.log(` [skip] Windows env: ${key} could not be set`)
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
} else {
|
|
1824
|
+
console.log(' [skip] setx.exe not found — Windows env vars not set')
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// 3. Configure Codex Desktop + MCP
|
|
1828
|
+
// WSL-side ~/.codex/config.toml (for Codex Desktop running in WSL)
|
|
1829
|
+
configureCodexDesktop(port)
|
|
1830
|
+
|
|
1831
|
+
// Register MCP server in Claude Desktop and Codex Desktop automatically
|
|
1832
|
+
await mcpInstall()
|
|
1833
|
+
// Windows-side %USERPROFILE%\.codex\config.toml (for Codex Desktop on Windows)
|
|
1834
|
+
try {
|
|
1835
|
+
const winHome = execSync('cmd.exe /c echo %USERPROFILE%', { stdio: 'pipe' }).toString().trim().replace(/\r/g, '')
|
|
1836
|
+
const winCodexDir = winHome + '\\.codex'
|
|
1837
|
+
const winMountedDir = winCodexDir.replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_, d) => `/mnt/${d.toLowerCase()}`)
|
|
1838
|
+
const winMountedToml = winMountedDir + '/config.toml'
|
|
1839
|
+
const winUrl = `http://localhost:${port}/v1`
|
|
1840
|
+
const winLine = `openai_base_url = "${winUrl}"`
|
|
1841
|
+
fs.mkdirSync(winMountedDir, { recursive: true })
|
|
1842
|
+
if (fs.existsSync(winMountedToml)) {
|
|
1843
|
+
let content = fs.readFileSync(winMountedToml, 'utf-8')
|
|
1844
|
+
if (content.includes('openai_base_url')) {
|
|
1845
|
+
content = content.replace(/openai_base_url\s*=\s*"[^"]*"/, winLine)
|
|
1846
|
+
fs.writeFileSync(winMountedToml, content)
|
|
1847
|
+
} else {
|
|
1848
|
+
fs.appendFileSync(winMountedToml, `\n# Squeezr\n${winLine}\n`)
|
|
1849
|
+
}
|
|
1850
|
+
} else {
|
|
1851
|
+
fs.writeFileSync(winMountedToml, `# Squeezr\n${winLine}\n`)
|
|
1852
|
+
}
|
|
1853
|
+
console.log(` [ok] Codex Desktop (Windows): ${winCodexDir}\\config.toml`)
|
|
1854
|
+
} catch {
|
|
1855
|
+
console.log(` [skip] Codex Desktop (Windows): could not write config`)
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// 3. Auto-start: try systemd first (WSL2 with systemd enabled), fallback to
|
|
1859
|
+
// Windows Task Scheduler, then plain background process
|
|
1860
|
+
let autoStartDone = false
|
|
1861
|
+
|
|
1862
|
+
// 3a. Try systemd (works on newer WSL2 with [boot] systemd=true in wsl.conf)
|
|
1863
|
+
try {
|
|
1864
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user')
|
|
1865
|
+
fs.mkdirSync(serviceDir, { recursive: true })
|
|
1866
|
+
const servicePath = path.join(serviceDir, 'squeezr.service')
|
|
1867
|
+
fs.writeFileSync(servicePath, `[Unit]
|
|
1868
|
+
Description=Squeezr AI proxy
|
|
1869
|
+
After=network.target
|
|
1870
|
+
|
|
1871
|
+
[Service]
|
|
1872
|
+
ExecStart=${nodeExe} ${distIndex}
|
|
1873
|
+
Restart=always
|
|
1874
|
+
RestartSec=5
|
|
1875
|
+
WorkingDirectory=${ROOT}
|
|
1876
|
+
|
|
1877
|
+
[Install]
|
|
1878
|
+
WantedBy=default.target
|
|
1879
|
+
`)
|
|
1880
|
+
execSync('systemctl --user daemon-reload && systemctl --user enable --now squeezr', { stdio: 'pipe' })
|
|
1881
|
+
console.log(' [ok] Auto-start registered via systemd')
|
|
1882
|
+
autoStartDone = true
|
|
1883
|
+
} catch {
|
|
1884
|
+
// systemd not available — try Windows Task Scheduler
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// 3b. Fallback: Windows Task Scheduler via powershell.exe
|
|
1888
|
+
if (!autoStartDone) {
|
|
1889
|
+
const winNodeExe = execSync('wslpath -w "$(which node)"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
1890
|
+
const winDistIndex = execSync(`wslpath -w "${distIndex}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
1891
|
+
const winRoot = execSync(`wslpath -w "${ROOT}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
1892
|
+
const taskName = 'Squeezr'
|
|
1893
|
+
const ps = [
|
|
1894
|
+
`$e = Get-ScheduledTask -TaskName '${taskName}' -ErrorAction SilentlyContinue`,
|
|
1895
|
+
`if ($e) { Unregister-ScheduledTask -TaskName '${taskName}' -Confirm:$false }`,
|
|
1896
|
+
`$a = New-ScheduledTaskAction -Execute 'wsl.exe' -Argument '-d ${os.hostname()} -- ${nodeExe} ${distIndex}' -WorkingDirectory '${winRoot}'`,
|
|
1897
|
+
`$t = New-ScheduledTaskTrigger -AtLogon`,
|
|
1898
|
+
`$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit 0 -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1)`,
|
|
1899
|
+
`Register-ScheduledTask -TaskName '${taskName}' -Action $a -Trigger $t -Settings $s -Force | Out-Null`,
|
|
1900
|
+
].join('; ')
|
|
1901
|
+
|
|
1902
|
+
try {
|
|
1903
|
+
execSync(`powershell.exe -NoProfile -Command "${ps}"`, { stdio: 'pipe' })
|
|
1904
|
+
console.log(' [ok] Auto-start registered via Windows Task Scheduler')
|
|
1905
|
+
autoStartDone = true
|
|
1906
|
+
} catch {
|
|
1907
|
+
console.log(' [warn] Task Scheduler failed — run PowerShell as admin for auto-start')
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// 4. Start proxy now as a detached background process
|
|
1912
|
+
const logDir = path.join(os.homedir(), '.squeezr')
|
|
1913
|
+
const logFile = path.join(logDir, 'squeezr.log')
|
|
1914
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
1915
|
+
const logFd = fs.openSync(logFile, 'a')
|
|
1916
|
+
const child = spawn(nodeExe, [distIndex], {
|
|
1917
|
+
detached: true,
|
|
1918
|
+
stdio: ['ignore', logFd, logFd],
|
|
1919
|
+
cwd: ROOT,
|
|
1920
|
+
})
|
|
1921
|
+
child.unref()
|
|
1922
|
+
fs.closeSync(logFd)
|
|
1923
|
+
console.log(` [ok] Squeezr started in background (pid ${child.pid})`)
|
|
1924
|
+
console.log(` [ok] Logs → ${logFile}`)
|
|
1925
|
+
|
|
1926
|
+
const setupPort = getPort()
|
|
1927
|
+
const setupMitmPort = getMitmPort(setupPort)
|
|
1928
|
+
console.log(`
|
|
1929
|
+
Done!
|
|
1930
|
+
|
|
1931
|
+
Squeezr is running on http://localhost:${setupPort}
|
|
1932
|
+
|
|
1933
|
+
Configured:
|
|
1934
|
+
Claude Code ANTHROPIC_BASE_URL=http://localhost:${setupPort}
|
|
1935
|
+
Claude Desktop Windows setx env var set (restart app once to pick it up)
|
|
1936
|
+
Codex Desktop ~/.codex/config.toml + Windows %USERPROFILE%\\.codex\\config.toml
|
|
1937
|
+
Codex CLI HTTPS_PROXY=http://localhost:${setupMitmPort} codex (per-session)
|
|
1938
|
+
Aider / OpenCode ANTHROPIC_BASE_URL + openai_base_url set
|
|
1939
|
+
Gemini CLI GEMINI_API_BASE_URL=http://localhost:${setupPort}
|
|
1940
|
+
|
|
1941
|
+
Windows env vars are set (effective in new terminals immediately).
|
|
1942
|
+
WSL env vars added to ${profile}.
|
|
1943
|
+
|
|
1944
|
+
squeezr status — check it's running
|
|
1945
|
+
squeezr gain — see token savings
|
|
1946
|
+
`)
|
|
1947
|
+
installShellWrapper()
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// ── squeezr tunnel ────────────────────────────────────────────────────────────
|
|
1951
|
+
// Exposes the local proxy via a Cloudflare Quick Tunnel (free, no account needed).
|
|
1952
|
+
// Cursor IDE requires a public HTTPS URL because its servers call the endpoint
|
|
1953
|
+
// from Cloudflare's infrastructure — localhost is unreachable from there.
|
|
1954
|
+
|
|
1955
|
+
async function startTunnel() {
|
|
1956
|
+
const port = getPort()
|
|
1957
|
+
|
|
1958
|
+
// Verify proxy is running first
|
|
1959
|
+
const running = await new Promise(resolve => {
|
|
1960
|
+
const req = http.get(`http://localhost:${port}/squeezr/health`, res => {
|
|
1961
|
+
resolve(res.statusCode === 200)
|
|
1962
|
+
})
|
|
1963
|
+
req.on('error', () => resolve(false))
|
|
1964
|
+
req.setTimeout(2000, () => { req.destroy(); resolve(false) })
|
|
1965
|
+
})
|
|
1966
|
+
|
|
1967
|
+
if (!running) {
|
|
1968
|
+
console.error(`Squeezr proxy is not running on port ${port}.`)
|
|
1969
|
+
console.error(`Start it first: squeezr start`)
|
|
1970
|
+
process.exit(1)
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
console.log(`Starting Cloudflare Quick Tunnel for http://localhost:${port}...`)
|
|
1974
|
+
console.log(`(free, no account needed — powered by trycloudflare.com)\n`)
|
|
1975
|
+
|
|
1976
|
+
// Try cloudflared binary, fall back to npx
|
|
1977
|
+
let tunnelCmd, tunnelArgs
|
|
1978
|
+
try {
|
|
1979
|
+
execSync('cloudflared --version', { stdio: 'pipe' })
|
|
1980
|
+
tunnelCmd = 'cloudflared'
|
|
1981
|
+
tunnelArgs = ['tunnel', '--url', `http://localhost:${port}`]
|
|
1982
|
+
} catch {
|
|
1983
|
+
tunnelCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
1984
|
+
tunnelArgs = ['cloudflared@latest', 'tunnel', '--url', `http://localhost:${port}`]
|
|
1985
|
+
console.log(`cloudflared not installed — using npx cloudflared (may take a moment to download)\n`)
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
let tunnelUrl = null
|
|
1989
|
+
const child = spawn(tunnelCmd, tunnelArgs, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
1990
|
+
|
|
1991
|
+
const printInstructions = (url) => {
|
|
1992
|
+
console.log(`\n ╔══════════════════════════════════════════════════════════════════╗`)
|
|
1993
|
+
console.log(` ║ Tunnel active: ${url.padEnd(49)}║`)
|
|
1994
|
+
console.log(` ╠══════════════════════════════════════════════════════════════════╣`)
|
|
1995
|
+
console.log(` ║ CURSOR SETUP ║`)
|
|
1996
|
+
console.log(` ║ ║`)
|
|
1997
|
+
console.log(` ║ 1. Cursor → Settings → Models ║`)
|
|
1998
|
+
console.log(` ║ 2. Add your OpenAI or Anthropic API key ║`)
|
|
1999
|
+
console.log(` ║ 3. Enable "Override OpenAI Base URL" ║`)
|
|
2000
|
+
console.log(` ║ 4. Set URL to: ${(url + '/v1').padEnd(49)}║`)
|
|
2001
|
+
console.log(` ║ 5. Disable all built-in Cursor models ║`)
|
|
2002
|
+
console.log(` ║ 6. Add a custom model pointing to the same URL ║`)
|
|
2003
|
+
console.log(` ║ ║`)
|
|
2004
|
+
console.log(` ║ CONTINUE EXTENSION (VS Code / JetBrains) ║`)
|
|
2005
|
+
console.log(` ║ No tunnel needed — use http://localhost:${port} directly ${' '.repeat(Math.max(0, 17 - String(port).length))}║`)
|
|
2006
|
+
console.log(` ║ ║`)
|
|
2007
|
+
console.log(` ║ Press Ctrl+C to stop the tunnel ║`)
|
|
2008
|
+
console.log(` ╚══════════════════════════════════════════════════════════════════╝\n`)
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const parseUrl = (line) => {
|
|
2012
|
+
const m = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)
|
|
2013
|
+
return m ? m[0] : null
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
child.stdout.on('data', (chunk) => {
|
|
2017
|
+
const text = chunk.toString()
|
|
2018
|
+
if (!tunnelUrl) {
|
|
2019
|
+
const found = parseUrl(text)
|
|
2020
|
+
if (found) { tunnelUrl = found; printInstructions(tunnelUrl) }
|
|
2021
|
+
}
|
|
2022
|
+
process.stdout.write(text)
|
|
2023
|
+
})
|
|
2024
|
+
|
|
2025
|
+
child.stderr.on('data', (chunk) => {
|
|
2026
|
+
const text = chunk.toString()
|
|
2027
|
+
if (!tunnelUrl) {
|
|
2028
|
+
const found = parseUrl(text)
|
|
2029
|
+
if (found) { tunnelUrl = found; printInstructions(tunnelUrl) }
|
|
2030
|
+
}
|
|
2031
|
+
// Only show cloudflared logs if no URL yet (suppress verbose after)
|
|
2032
|
+
if (!tunnelUrl) process.stderr.write(text)
|
|
2033
|
+
})
|
|
2034
|
+
|
|
2035
|
+
child.on('error', (err) => {
|
|
2036
|
+
if (err.code === 'ENOENT') {
|
|
2037
|
+
console.error(`Could not start tunnel. Install cloudflared: https://developers.cloudflare.com/cloudflared/downloads`)
|
|
2038
|
+
} else {
|
|
2039
|
+
console.error(`Tunnel error: ${err.message}`)
|
|
2040
|
+
}
|
|
2041
|
+
process.exit(1)
|
|
2042
|
+
})
|
|
2043
|
+
|
|
2044
|
+
child.on('exit', (code) => {
|
|
2045
|
+
if (code !== 0) console.log(`\nTunnel stopped (exit ${code})`)
|
|
2046
|
+
process.exit(0)
|
|
2047
|
+
})
|
|
2048
|
+
|
|
2049
|
+
process.on('SIGINT', () => { child.kill(); process.exit(0) })
|
|
2050
|
+
process.on('SIGTERM', () => { child.kill(); process.exit(0) })
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// ── squeezr zest — guided install wizard ─────────────────────────────────────
|
|
2054
|
+
|
|
2055
|
+
async function installZest() {
|
|
2056
|
+
const { createInterface } = await import('readline')
|
|
2057
|
+
const { get } = await import('https')
|
|
2058
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
2059
|
+
const ask = (q) => new Promise(res => rl.question(q, res))
|
|
2060
|
+
const ZEST_DIR = path.join(os.homedir(), '.squeezr', 'zest')
|
|
2061
|
+
const GGUF_PATH = path.join(ZEST_DIR, 'zest-Q4_K_M.gguf')
|
|
2062
|
+
const MODELFILE_PATH = path.join(ZEST_DIR, 'Modelfile.zest')
|
|
2063
|
+
// GGUF download URL — hosted on HuggingFace (replace with actual URL when published)
|
|
2064
|
+
const GGUF_URL = 'https://huggingface.co/ramosvs/zest/resolve/main/zest-Q4_K_M.gguf'
|
|
2065
|
+
const COMPRESS_SYSTEM = 'You are compressing a coding tool output to save tokens. Extract ONLY what is essential: errors, file paths, function names, test failures, key values, warnings. Be extremely concise, target under 150 tokens. Output only the compressed content, nothing else.'
|
|
2066
|
+
|
|
2067
|
+
console.log('\n╔══════════════════════════════════════════════════════╗')
|
|
2068
|
+
console.log('║ Zest — Local AI Compression Model for Squeezr ║')
|
|
2069
|
+
console.log('╚══════════════════════════════════════════════════════╝')
|
|
2070
|
+
console.log('\nZest is a fine-tuned 0.8B model that compresses coding tool')
|
|
2071
|
+
console.log('outputs locally — zero cost, zero API calls, zero latency added.')
|
|
2072
|
+
console.log('Runs via Ollama on your machine.\n')
|
|
2073
|
+
|
|
2074
|
+
// ── Step 1: Check / install Ollama + verify compatible version ────────────
|
|
2075
|
+
// Zest is based on Qwen3.5 which requires Ollama ≥ 0.6.0 (added qwen3.5 arch)
|
|
2076
|
+
const OLLAMA_MIN_MAJOR = 0
|
|
2077
|
+
const OLLAMA_MIN_MINOR = 6
|
|
2078
|
+
console.log('[ 1 / 4 ] Checking Ollama...')
|
|
2079
|
+
let ollamaOk = false
|
|
2080
|
+
let ollamaVersion = null
|
|
2081
|
+
try {
|
|
2082
|
+
const vOut = execSync('ollama --version', { stdio: 'pipe', encoding: 'utf-8' })
|
|
2083
|
+
const m = vOut.match(/(\d+)\.(\d+)\.(\d+)/)
|
|
2084
|
+
if (m) {
|
|
2085
|
+
ollamaVersion = `${m[1]}.${m[2]}.${m[3]}`
|
|
2086
|
+
const major = parseInt(m[1]), minor = parseInt(m[2])
|
|
2087
|
+
ollamaOk = major > OLLAMA_MIN_MAJOR || (major === OLLAMA_MIN_MAJOR && minor >= OLLAMA_MIN_MINOR)
|
|
2088
|
+
if (!ollamaOk) {
|
|
2089
|
+
console.log(`\n Ollama ${ollamaVersion} is too old. Zest requires Ollama ≥ ${OLLAMA_MIN_MAJOR}.${OLLAMA_MIN_MINOR}.0`)
|
|
2090
|
+
console.log(' (Qwen3.5 architecture support was added in 0.6.0)\n')
|
|
2091
|
+
const ans = await ask(' Update Ollama now? [Y/n] ')
|
|
2092
|
+
if (ans.toLowerCase() === 'n') {
|
|
2093
|
+
console.log('\n Download the latest from https://ollama.com/download then run: squeezr zest\n')
|
|
2094
|
+
rl.close(); return
|
|
2095
|
+
}
|
|
2096
|
+
// Update Ollama
|
|
2097
|
+
if (process.platform === 'win32') {
|
|
2098
|
+
console.log('\n Downloading latest Ollama installer...')
|
|
2099
|
+
const installerPath = path.join(os.tmpdir(), 'OllamaSetup.exe')
|
|
2100
|
+
await new Promise((resolve, reject) => {
|
|
2101
|
+
const file = require('fs').createWriteStream(installerPath)
|
|
2102
|
+
require('https').get('https://ollama.com/download/OllamaSetup.exe', { headers: { 'User-Agent': 'squeezr-zest-installer' } }, res => {
|
|
2103
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
2104
|
+
require('https').get(res.headers.location, { headers: { 'User-Agent': 'squeezr-zest-installer' } }, res2 => { res2.pipe(file); file.on('finish', resolve) }).on('error', reject)
|
|
2105
|
+
} else { res.pipe(file); file.on('finish', resolve) }
|
|
2106
|
+
}).on('error', reject)
|
|
2107
|
+
})
|
|
2108
|
+
console.log(' Installing (may show a UAC prompt)...')
|
|
2109
|
+
try {
|
|
2110
|
+
execSync(`"${installerPath}" /S`, { stdio: 'inherit' })
|
|
2111
|
+
ollamaOk = true
|
|
2112
|
+
console.log(' ✓ Ollama updated')
|
|
2113
|
+
} catch {
|
|
2114
|
+
console.log('\n Automatic update failed. Download manually: https://ollama.com/download/windows')
|
|
2115
|
+
const wait = await ask(' Press Enter once Ollama is updated... ')
|
|
2116
|
+
}
|
|
2117
|
+
} else {
|
|
2118
|
+
console.log(' → Updating via official script...')
|
|
2119
|
+
try { execSync('curl -fsSL https://ollama.com/install.sh | sh', { stdio: 'inherit' }); ollamaOk = true } catch {}
|
|
2120
|
+
}
|
|
2121
|
+
// Re-check version
|
|
2122
|
+
try {
|
|
2123
|
+
const vOut2 = execSync('ollama --version', { stdio: 'pipe', encoding: 'utf-8' })
|
|
2124
|
+
const m2 = vOut2.match(/(\d+)\.(\d+)/)
|
|
2125
|
+
if (m2) {
|
|
2126
|
+
ollamaOk = parseInt(m2[1]) > OLLAMA_MIN_MAJOR || (parseInt(m2[1]) === OLLAMA_MIN_MAJOR && parseInt(m2[2]) >= OLLAMA_MIN_MINOR)
|
|
2127
|
+
}
|
|
2128
|
+
} catch {}
|
|
2129
|
+
if (!ollamaOk) {
|
|
2130
|
+
console.log('\n Please update Ollama manually from https://ollama.com/download then run: squeezr zest\n')
|
|
2131
|
+
rl.close(); return
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
} else {
|
|
2135
|
+
ollamaOk = true // version string not parseable — assume ok
|
|
2136
|
+
}
|
|
2137
|
+
} catch {}
|
|
2138
|
+
|
|
2139
|
+
if (!ollamaOk && !ollamaVersion) {
|
|
2140
|
+
console.log('\n Ollama is not installed.')
|
|
2141
|
+
console.log(' Ollama is a free, open-source local model runner.\n')
|
|
2142
|
+
const ans = await ask(' Install Ollama now? [Y/n] ')
|
|
2143
|
+
if (ans.toLowerCase() === 'n') {
|
|
2144
|
+
console.log('\n Skipping. Install Ollama manually from https://ollama.com/download')
|
|
2145
|
+
console.log(' Then run: squeezr zest\n')
|
|
2146
|
+
rl.close(); return
|
|
2147
|
+
}
|
|
2148
|
+
console.log('\n Installing Ollama...')
|
|
2149
|
+
if (process.platform === 'win32') {
|
|
2150
|
+
// winget first, fallback to direct download guidance
|
|
2151
|
+
try {
|
|
2152
|
+
execSync('winget install -e --id Ollama.Ollama --accept-package-agreements --accept-source-agreements', { stdio: 'inherit' })
|
|
2153
|
+
ollamaOk = true
|
|
2154
|
+
} catch {
|
|
2155
|
+
console.log('\n winget install failed. Please download Ollama manually:')
|
|
2156
|
+
console.log(' https://ollama.com/download/windows')
|
|
2157
|
+
const wait = await ask('\n Press Enter once Ollama is installed... ')
|
|
2158
|
+
try { execSync('ollama --version', { stdio: 'pipe' }); ollamaOk = true } catch {}
|
|
2159
|
+
}
|
|
2160
|
+
} else if (process.platform === 'darwin') {
|
|
2161
|
+
console.log(' → brew install ollama')
|
|
2162
|
+
try { execSync('brew install ollama', { stdio: 'inherit' }); ollamaOk = true } catch {}
|
|
2163
|
+
} else {
|
|
2164
|
+
// Linux
|
|
2165
|
+
try {
|
|
2166
|
+
execSync('curl -fsSL https://ollama.com/install.sh | sh', { stdio: 'inherit' })
|
|
2167
|
+
ollamaOk = true
|
|
2168
|
+
} catch {}
|
|
2169
|
+
}
|
|
2170
|
+
if (!ollamaOk) {
|
|
2171
|
+
console.log('\n Could not install Ollama automatically.')
|
|
2172
|
+
console.log(' Install manually from https://ollama.com/download then run: squeezr zest\n')
|
|
2173
|
+
rl.close(); return
|
|
2174
|
+
}
|
|
2175
|
+
// Start ollama service
|
|
2176
|
+
try { execSync('ollama serve &', { stdio: 'pipe', shell: true }) } catch {}
|
|
2177
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
2178
|
+
}
|
|
2179
|
+
try { execSync('ollama --version', { stdio: 'pipe' }) } catch {
|
|
2180
|
+
console.log('\n Ollama installed but not in PATH. Open a new terminal and run: squeezr zest\n')
|
|
2181
|
+
rl.close(); return
|
|
2182
|
+
}
|
|
2183
|
+
console.log(' ✓ Ollama ready')
|
|
2184
|
+
|
|
2185
|
+
// ── Step 2: Check if zest model already exists in Ollama ──────────────────
|
|
2186
|
+
console.log('\n[ 2 / 4 ] Checking Zest model in Ollama...')
|
|
2187
|
+
let modelExists = false
|
|
2188
|
+
try {
|
|
2189
|
+
const list = execSync('ollama list', { stdio: 'pipe', encoding: 'utf-8' })
|
|
2190
|
+
modelExists = list.includes('zest')
|
|
2191
|
+
} catch {}
|
|
2192
|
+
|
|
2193
|
+
if (modelExists) {
|
|
2194
|
+
console.log(' ✓ Zest model already installed in Ollama')
|
|
2195
|
+
} else {
|
|
2196
|
+
// Download GGUF if not present
|
|
2197
|
+
if (!fs.existsSync(GGUF_PATH)) {
|
|
2198
|
+
console.log(`\n Downloading Zest model (~500 MB)...`)
|
|
2199
|
+
console.log(` From: ${GGUF_URL}`)
|
|
2200
|
+
console.log(` To: ${GGUF_PATH}\n`)
|
|
2201
|
+
fs.mkdirSync(ZEST_DIR, { recursive: true })
|
|
2202
|
+
// Stream download with progress
|
|
2203
|
+
await new Promise((resolve, reject) => {
|
|
2204
|
+
const file = fs.createWriteStream(GGUF_PATH)
|
|
2205
|
+
const request = (url, redirects = 0) => {
|
|
2206
|
+
if (redirects > 5) return reject(new Error('Too many redirects'))
|
|
2207
|
+
const urlObj = new URL(url)
|
|
2208
|
+
const mod = urlObj.protocol === 'https:' ? require('https') : require('http')
|
|
2209
|
+
mod.get(url, { headers: { 'User-Agent': 'squeezr-zest-installer' } }, res => {
|
|
2210
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
2211
|
+
return request(res.headers.location, redirects + 1)
|
|
2212
|
+
}
|
|
2213
|
+
if (res.statusCode !== 200) {
|
|
2214
|
+
file.close()
|
|
2215
|
+
fs.unlinkSync(GGUF_PATH)
|
|
2216
|
+
return reject(new Error(`HTTP ${res.statusCode}`))
|
|
2217
|
+
}
|
|
2218
|
+
const total = parseInt(res.headers['content-length'] || '0')
|
|
2219
|
+
let downloaded = 0
|
|
2220
|
+
res.on('data', chunk => {
|
|
2221
|
+
downloaded += chunk.length
|
|
2222
|
+
if (total > 0) {
|
|
2223
|
+
const pct = Math.round(downloaded / total * 100)
|
|
2224
|
+
process.stdout.write(`\r Downloading... ${pct}% (${Math.round(downloaded/1024/1024)}/${Math.round(total/1024/1024)} MB)`)
|
|
2225
|
+
}
|
|
2226
|
+
})
|
|
2227
|
+
res.pipe(file)
|
|
2228
|
+
file.on('finish', () => { file.close(); console.log('\n ✓ Download complete'); resolve() })
|
|
2229
|
+
}).on('error', err => { file.close(); fs.unlinkSync(GGUF_PATH); reject(err) })
|
|
2230
|
+
}
|
|
2231
|
+
request(GGUF_URL)
|
|
2232
|
+
}).catch(err => {
|
|
2233
|
+
console.log(`\n ✗ Download failed: ${err.message}`)
|
|
2234
|
+
console.log(' The model will be available at https://huggingface.co/ramosvs/zest')
|
|
2235
|
+
console.log(' Download the GGUF manually and place it at:')
|
|
2236
|
+
console.log(` ${GGUF_PATH}`)
|
|
2237
|
+
console.log(' Then run: squeezr zest\n')
|
|
2238
|
+
rl.close(); process.exit(1)
|
|
2239
|
+
})
|
|
2240
|
+
} else {
|
|
2241
|
+
console.log(' ✓ GGUF already downloaded')
|
|
2242
|
+
}
|
|
2243
|
+
// Write Modelfile
|
|
2244
|
+
const modelfileContent = `FROM ${GGUF_PATH}\nSYSTEM """${COMPRESS_SYSTEM}"""\nPARAMETER temperature 0\nPARAMETER top_p 1\nPARAMETER top_k 1\nPARAMETER num_predict 300\nPARAMETER num_ctx 2048\n`
|
|
2245
|
+
fs.writeFileSync(MODELFILE_PATH, modelfileContent)
|
|
2246
|
+
// Create Ollama model
|
|
2247
|
+
console.log('\n Creating Zest model in Ollama...')
|
|
2248
|
+
try {
|
|
2249
|
+
execSync(`ollama create zest -f "${MODELFILE_PATH}"`, { stdio: 'inherit' })
|
|
2250
|
+
console.log(' ✓ Zest model created')
|
|
2251
|
+
} catch (e) {
|
|
2252
|
+
console.log(`\n ✗ Failed to create Ollama model: ${e.message}`)
|
|
2253
|
+
rl.close(); return
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// ── Step 3: Smoke test ────────────────────────────────────────────────────
|
|
2258
|
+
console.log('\n[ 3 / 4 ] Smoke test...')
|
|
2259
|
+
const testInput = 'npm warn deprecated inflight@1.0.6: not supported, leaks memory. added 847 packages in 14s. 3 vulnerabilities (1 moderate, 2 high).'
|
|
2260
|
+
let testOutput = ''
|
|
2261
|
+
try {
|
|
2262
|
+
testOutput = execSync(`ollama run zest "${testInput}"`, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 }).trim()
|
|
2263
|
+
const ratio = Math.round((1 - testOutput.length / testInput.length) * 100)
|
|
2264
|
+
console.log(`\n Input (${testInput.length} chars): ${testInput}`)
|
|
2265
|
+
console.log(` Output (${testOutput.length} chars): ${testOutput}`)
|
|
2266
|
+
if (ratio > 0) {
|
|
2267
|
+
console.log(`\n ✓ Compression working — ${ratio}% savings on test input`)
|
|
2268
|
+
} else {
|
|
2269
|
+
console.log(`\n ⚠ Output is larger than input on this test — this is normal for very short inputs`)
|
|
2270
|
+
console.log(' Zest shines on real tool outputs (≥1500 chars). Short test inputs may expand.')
|
|
2271
|
+
}
|
|
2272
|
+
} catch (e) {
|
|
2273
|
+
console.log(`\n ✗ Smoke test failed: ${e.message}`)
|
|
2274
|
+
console.log(' The model may still work. Continuing setup.')
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// ── Step 4: Configure Squeezr ─────────────────────────────────────────────
|
|
2278
|
+
console.log('\n[ 4 / 4 ] Configuring Squeezr...')
|
|
2279
|
+
const userToml = path.join(os.homedir(), '.squeezr', 'squeezr.toml')
|
|
2280
|
+
let tomlContent = ''
|
|
2281
|
+
try { tomlContent = fs.readFileSync(userToml, 'utf-8') } catch {}
|
|
2282
|
+
|
|
2283
|
+
const alreadyConfigured = tomlContent.includes('compression_model') && tomlContent.includes('zest')
|
|
2284
|
+
if (alreadyConfigured) {
|
|
2285
|
+
console.log(' ✓ Squeezr already configured to use Zest')
|
|
2286
|
+
} else {
|
|
2287
|
+
const ans = await ask('\n Configure Squeezr to use Zest as AI compression backend? [Y/n] ')
|
|
2288
|
+
if (ans.toLowerCase() !== 'n') {
|
|
2289
|
+
// Add [local] section and enable ai_compression
|
|
2290
|
+
let newToml = tomlContent
|
|
2291
|
+
if (!newToml.includes('[local]')) {
|
|
2292
|
+
newToml += '\n[local]\nenabled = true\nupstream_url = "http://localhost:11434"\ncompression_model = "zest"\n'
|
|
2293
|
+
}
|
|
2294
|
+
if (!newToml.includes('ai_compression')) {
|
|
2295
|
+
// Add under [compression] or create section
|
|
2296
|
+
if (newToml.includes('[compression]')) {
|
|
2297
|
+
newToml = newToml.replace('[compression]', '[compression]\nai_compression = true\nai_min_chars = 1500')
|
|
2298
|
+
} else {
|
|
2299
|
+
newToml += '\n[compression]\nai_compression = true\nai_min_chars = 1500\n'
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
fs.writeFileSync(userToml, newToml)
|
|
2303
|
+
console.log(' ✓ Squeezr configured')
|
|
2304
|
+
// Restart proxy
|
|
2305
|
+
console.log('\n Restarting Squeezr to apply changes...')
|
|
2306
|
+
try {
|
|
2307
|
+
execSync('squeezr restart', { stdio: 'inherit' })
|
|
2308
|
+
} catch {}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
rl.close()
|
|
2313
|
+
console.log('\n╔══════════════════════════════════════════════════════╗')
|
|
2314
|
+
console.log('║ ✓ Zest is ready! ║')
|
|
2315
|
+
console.log('╚══════════════════════════════════════════════════════╝')
|
|
2316
|
+
console.log('\nSqueezr will now use Zest for AI compression locally.')
|
|
2317
|
+
console.log('No API calls, no cost — everything runs on your machine.')
|
|
2318
|
+
console.log('\nDashboard: http://localhost:8080/squeezr/dashboard')
|
|
2319
|
+
console.log('To disable: set ai_compression = false in ~/.squeezr/squeezr.toml\n')
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// ── CLI router ────────────────────────────────────────────────────────────────
|
|
2323
|
+
|
|
2324
|
+
switch (command) {
|
|
2325
|
+
case undefined:
|
|
2326
|
+
case 'start':
|
|
2327
|
+
startDaemon()
|
|
2328
|
+
break
|
|
2329
|
+
|
|
2330
|
+
case 'setup':
|
|
2331
|
+
if (process.platform === 'win32') setupWindows()
|
|
2332
|
+
else if (isWSL()) setupWSL()
|
|
2333
|
+
else setupUnix()
|
|
2334
|
+
break
|
|
2335
|
+
|
|
2336
|
+
case 'update':
|
|
2337
|
+
await (async () => {
|
|
2338
|
+
console.log('Stopping Squeezr...')
|
|
2339
|
+
stopProxy() // also kills MCP via killMcpProcesses()
|
|
2340
|
+
console.log('Releasing file locks...')
|
|
2341
|
+
killMcpProcesses() // double-kill in case stopProxy was too fast
|
|
2342
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
2343
|
+
|
|
2344
|
+
console.log('Installing latest version...')
|
|
2345
|
+
const cleanEnv = { ...process.env, HTTPS_PROXY: '', https_proxy: '', HTTP_PROXY: '', http_proxy: '' }
|
|
2346
|
+
let installed = false
|
|
2347
|
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
2348
|
+
try {
|
|
2349
|
+
execSync('npm install -g squeezr-ai@latest', { stdio: 'inherit', env: cleanEnv })
|
|
2350
|
+
installed = true
|
|
2351
|
+
break
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
const msg = String(err?.stderr || err?.message || '')
|
|
2354
|
+
if ((msg.includes('EBUSY') || msg.includes('EPERM')) && attempt < 4) {
|
|
2355
|
+
console.log(` Files still locked, retrying in 3s (attempt ${attempt}/4)...`)
|
|
2356
|
+
// Try harder to release locks on retry
|
|
2357
|
+
killMcpProcesses()
|
|
2358
|
+
await new Promise(r => setTimeout(r, 3000))
|
|
2359
|
+
} else if (!msg.includes('EBUSY') && !msg.includes('EPERM') && process.platform !== 'win32') {
|
|
2360
|
+
// On Unix, try sudo as fallback (not useful on Windows)
|
|
2361
|
+
try {
|
|
2362
|
+
execSync('sudo npm install -g squeezr-ai@latest', { stdio: 'inherit', env: cleanEnv })
|
|
2363
|
+
installed = true
|
|
2364
|
+
} catch {}
|
|
2365
|
+
break
|
|
2366
|
+
} else {
|
|
2367
|
+
break
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!installed) {
|
|
2372
|
+
console.error('\nUpdate failed: files are still locked.')
|
|
2373
|
+
console.error('Fix: close Claude Code completely (this releases the MCP server lock), then run "squeezr update" again.')
|
|
2374
|
+
process.exit(1)
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// Clear update check cache
|
|
2378
|
+
try { fs.unlinkSync(UPDATE_CHECK_FILE) } catch {}
|
|
2379
|
+
|
|
2380
|
+
// Resolve the NEW package root from npm global modules
|
|
2381
|
+
let newRoot = ROOT
|
|
2382
|
+
try {
|
|
2383
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
2384
|
+
const candidate = path.join(npmRoot, 'squeezr-ai')
|
|
2385
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) newRoot = candidate
|
|
2386
|
+
} catch {}
|
|
2387
|
+
|
|
2388
|
+
// Read the new version and write cache so no stale banner appears
|
|
2389
|
+
try {
|
|
2390
|
+
const newPkg = JSON.parse(fs.readFileSync(path.join(newRoot, 'package.json'), 'utf-8'))
|
|
2391
|
+
const dir = path.dirname(UPDATE_CHECK_FILE)
|
|
2392
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
2393
|
+
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ latest: newPkg.version, checkedAt: Date.now() }))
|
|
2394
|
+
console.log(`\nUpdated to v${newPkg.version}`)
|
|
2395
|
+
} catch {}
|
|
2396
|
+
|
|
2397
|
+
// Start the daemon directly from the new dist/index.js (no re-exec of old binary)
|
|
2398
|
+
console.log('Starting Squeezr...')
|
|
2399
|
+
const newDistIndex = path.join(newRoot, 'dist', 'index.js')
|
|
2400
|
+
const startPort = getPort()
|
|
2401
|
+
const startMitmPort = getMitmPort(startPort)
|
|
2402
|
+
const logDir = path.join(os.homedir(), '.squeezr')
|
|
2403
|
+
const logFile = path.join(logDir, 'squeezr.log')
|
|
2404
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
2405
|
+
const logFd = fs.openSync(logFile, 'a')
|
|
2406
|
+
const child = spawn(process.execPath, [newDistIndex], {
|
|
2407
|
+
detached: true,
|
|
2408
|
+
stdio: ['ignore', logFd, logFd],
|
|
2409
|
+
cwd: newRoot,
|
|
2410
|
+
env: { ...process.env, SQUEEZR_DAEMON: '1' },
|
|
2411
|
+
})
|
|
2412
|
+
child.unref()
|
|
2413
|
+
fs.closeSync(logFd)
|
|
2414
|
+
console.log(`Squeezr started (pid ${child.pid})`)
|
|
2415
|
+
console.log(` HTTP proxy (Claude/Aider/Gemini): http://localhost:${startPort}`)
|
|
2416
|
+
console.log(` MITM proxy (Codex): http://localhost:${startMitmPort}`)
|
|
2417
|
+
console.log(` Dashboard: http://localhost:${startPort}/squeezr/dashboard`)
|
|
2418
|
+
console.log(` Logs: ${logFile}`)
|
|
2419
|
+
|
|
2420
|
+
// Ensure PowerShell wrapper is installed (so env vars refresh automatically)
|
|
2421
|
+
installShellWrapper()
|
|
2422
|
+
})()
|
|
2423
|
+
break
|
|
2424
|
+
case 'restart':
|
|
2425
|
+
await (async () => {
|
|
2426
|
+
console.log('Stopping Squeezr...')
|
|
2427
|
+
stopProxy()
|
|
2428
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
2429
|
+
await startDaemon()
|
|
2430
|
+
})()
|
|
2431
|
+
break
|
|
2432
|
+
|
|
2433
|
+
case 'stop':
|
|
2434
|
+
stopProxy()
|
|
2435
|
+
break
|
|
2436
|
+
|
|
2437
|
+
case 'logs':
|
|
2438
|
+
showLogs()
|
|
2439
|
+
break
|
|
2440
|
+
|
|
2441
|
+
case 'gain':
|
|
2442
|
+
runNode('gain.js', args.slice(1))
|
|
2443
|
+
break
|
|
2444
|
+
|
|
2445
|
+
case 'discover':
|
|
2446
|
+
runNode('discover.js', args.slice(1))
|
|
2447
|
+
break
|
|
2448
|
+
|
|
2449
|
+
case 'status':
|
|
2450
|
+
checkStatus()
|
|
2451
|
+
break
|
|
2452
|
+
|
|
2453
|
+
case 'ports':
|
|
2454
|
+
await configurePorts()
|
|
2455
|
+
break
|
|
2456
|
+
|
|
2457
|
+
case 'tunnel':
|
|
2458
|
+
await startTunnel()
|
|
2459
|
+
break
|
|
2460
|
+
|
|
2461
|
+
case 'bypass':
|
|
2462
|
+
await (async () => {
|
|
2463
|
+
const port = getPort()
|
|
2464
|
+
const body = args[1] === '--on' ? JSON.stringify({ enabled: true })
|
|
2465
|
+
: args[1] === '--off' ? JSON.stringify({ enabled: false })
|
|
2466
|
+
: '{}'
|
|
2467
|
+
try {
|
|
2468
|
+
const res = await fetch(`http://localhost:${port}/squeezr/bypass`, {
|
|
2469
|
+
method: 'POST',
|
|
2470
|
+
headers: { 'content-type': 'application/json' },
|
|
2471
|
+
body,
|
|
2472
|
+
})
|
|
2473
|
+
const json = await res.json()
|
|
2474
|
+
if (json.bypassed) {
|
|
2475
|
+
console.log('⏸️ Bypass mode ON — compression disabled')
|
|
2476
|
+
console.log(' Requests pass through uncompressed but are still logged.')
|
|
2477
|
+
console.log(' Turn off: squeezr bypass --off')
|
|
2478
|
+
} else {
|
|
2479
|
+
console.log('▶️ Bypass mode OFF — compression active')
|
|
2480
|
+
}
|
|
2481
|
+
} catch {
|
|
2482
|
+
console.log('Squeezr is NOT running')
|
|
2483
|
+
console.log('Start it with: squeezr start')
|
|
2484
|
+
}
|
|
2485
|
+
})()
|
|
2486
|
+
break
|
|
2487
|
+
|
|
2488
|
+
case 'uninstall':
|
|
2489
|
+
await uninstall()
|
|
2490
|
+
break
|
|
2491
|
+
case 'enable-claude-desktop':
|
|
2492
|
+
case 'disable-claude-desktop':
|
|
2493
|
+
await toggleClaudeDesktopIntercept(command === 'enable-claude-desktop')
|
|
2494
|
+
break
|
|
2495
|
+
case 'desktop': {
|
|
2496
|
+
const sub = args[1] ?? 'status'
|
|
2497
|
+
if (sub === 'start') await desktopProxyStart()
|
|
2498
|
+
else if (sub === 'stop') await desktopProxyStop()
|
|
2499
|
+
else if (sub === 'status') await desktopProxyStatus()
|
|
2500
|
+
else { console.error(`Unknown desktop subcommand: ${sub}. Use start|stop|status`); process.exit(1) }
|
|
2501
|
+
break
|
|
2502
|
+
}
|
|
2503
|
+
case 'config':
|
|
2504
|
+
showConfig()
|
|
2505
|
+
break
|
|
2506
|
+
|
|
2507
|
+
case 'mcp': {
|
|
2508
|
+
const subCmd = args[1] ?? 'install'
|
|
2509
|
+
if (subCmd === 'uninstall') await mcpUninstall()
|
|
2510
|
+
else await mcpInstall()
|
|
2511
|
+
break
|
|
2512
|
+
}
|
|
2513
|
+
case 'zest':
|
|
2514
|
+
await installZest()
|
|
2515
|
+
break
|
|
2516
|
+
|
|
2517
|
+
case 'version':
|
|
2518
|
+
case '--version':
|
|
2519
|
+
case '-v':
|
|
2520
|
+
console.log(pkg.version)
|
|
2521
|
+
break
|
|
2522
|
+
|
|
2523
|
+
case '--help':
|
|
2524
|
+
case '-h':
|
|
2525
|
+
case 'help':
|
|
2526
|
+
console.log(HELP)
|
|
2527
|
+
break
|
|
2528
|
+
|
|
2529
|
+
default:
|
|
2530
|
+
console.error(`Unknown command: ${command}`)
|
|
2531
|
+
console.log(HELP)
|
|
2532
|
+
process.exit(1)
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
if (command !== 'update') await showUpdateBanner()
|