import { spawn } from 'child_process' import { createServer } from 'http' import { resolve } from 'path' const PORT = 3141 const CLAUDE_BIN = resolve(process.env.HOME ?? '', '.local/bin/claude') const server = createServer((req, res) => { if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }) res.end() return } if (req.method !== 'POST' || req.url !== '/v1/messages') { res.writeHead(404, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: 'Not found' })) return } let body = '' req.on('data', (chunk) => { body += chunk }) req.on('end', () => { try { const { model, messages, system } = JSON.parse(body) const modelFlag = model?.includes('opus') ? 'opus' : model?.includes('haiku') ? 'haiku' : 'sonnet' const history = (messages ?? []) as { role: string; content: string }[] const userMessages = history.filter((m) => m.role === 'user') const lastUserMsg = userMessages[userMessages.length - 1]?.content ?? '' const contextParts: string[] = [] if (system) contextParts.push(system) const prior = history.slice(0, -1) if (prior.length > 0) { contextParts.push( 'Conversation so far:\n' + prior.map((m) => `${m.role}: ${m.content}`).join('\n') ) } const systemPrompt = contextParts.length > 0 ? contextParts.join('\n\n') : undefined const args = ['-p', '--model', modelFlag] if (systemPrompt) args.push('--system-prompt', systemPrompt) args.push('--', lastUserMsg) console.log(`[proxy] → claude -p --model ${modelFlag} "${lastUserMsg.slice(0, 60)}..."`) const proc = spawn(CLAUDE_BIN, args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' }, detached: false, }) proc.stdin.end('') res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'X-Accel-Buffering': 'no', }) let fullOutput = '' let clientDisconnected = false proc.stdout.on('data', (chunk: Buffer) => { const text = chunk.toString() fullOutput += text console.log(`[proxy] stdout +${text.length}b total=${fullOutput.length}b`) if (!clientDisconnected) { const sseData = { type: 'content_block_delta', delta: { type: 'text_delta', text }, } res.write(`data: ${JSON.stringify(sseData)}\n\n`) } }) proc.stderr.on('data', (chunk: Buffer) => { const msg = chunk.toString().trim() if (msg) console.error('[proxy] stderr:', msg) }) proc.on('error', (err) => { console.error('[proxy] spawn error:', err) if (!clientDisconnected) { const errData = { type: 'error', error: { message: `Spawn error: ${err.message}` }, } res.write(`data: ${JSON.stringify(errData)}\n\n`) res.write('data: [DONE]\n\n') res.end() } }) proc.on('close', (code, signal) => { console.log(`[proxy] ← exit code=${code} signal=${signal} output=${fullOutput.length}b`) if (!clientDisconnected) { res.write('data: [DONE]\n\n') res.end() } }) res.on('close', () => { clientDisconnected = true if (proc.exitCode === null && !proc.killed) { console.log('[proxy] Client disconnected, killing process') proc.kill('SIGTERM') } }) } catch (err) { console.error('[proxy] Parse error:', err) res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }) res.end(JSON.stringify({ error: String(err) })) } }) }) server.listen(PORT, () => { console.log(`\n Claude proxy → http://localhost:${PORT}`) console.log(` Binary: ${CLAUDE_BIN}`) console.log(` Using your Max subscription\n`) })