- Updated the app to support light and dark themes with appropriate CSS classes. - Enhanced PWA configuration with manifest details and caching strategies. - Improved the chat UI with dynamic theme adjustments for various components. - Added new meta tags for better mobile web app experience. - Refactored environment variables to include new Anthropic token. - Updated package dependencies for better compatibility and performance. Made-with: Cursor
140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
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`)
|
|
})
|