const express = require('express'); const path = require('path'); const fs = require('fs'); const os = require('os'); const { ProxyAgent, setGlobalDispatcher } = require('undici'); const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; if (proxyUrl) { setGlobalDispatcher(new ProxyAgent(proxyUrl)); console.log(`✓ Using proxy: ${proxyUrl}`); } const app = express(); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, 'public'))); const MODELS = { sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001', }; function getAccessToken() { const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); const credsPath = path.join(configDir, '.credentials.json'); try { const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8')); return creds?.claudeAiOauth?.accessToken || null; } catch (e) { return null; } } app.post('/api/chat', async (req, res) => { const { text, model = 'sonnet' } = req.body; if (!text?.trim()) return res.status(400).json({ error: 'text required' }); const modelId = MODELS[model] || MODELS.sonnet; const accessToken = getAccessToken(); if (!accessToken) { return res.status(500).json({ error: 'No credentials in ' + (process.env.CLAUDE_CONFIG_DIR || '~/.claude') }); } res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000); try { const apiResp = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'anthropic-version': '2023-06-01', 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14', 'x-app': 'cli', 'User-Agent': 'claude-cli/2.1.114', }, body: JSON.stringify({ model: modelId, max_tokens: 8192, stream: true, system: "You are Claude Code, Anthropic's official CLI for Claude.\n\nAnswer in the same language as the user. Give the shortest possible correct answer. No greetings, no filler, no emojis, no markdown formatting unless the user explicitly asks for code. Do not restate the question. Do not add caveats or disclaimers. Respond with just the answer — nothing else.", messages: [{ role: 'user', content: text }], }), }); if (!apiResp.ok) { const errText = await apiResp.text(); res.write(`data: ${JSON.stringify({ error: `${apiResp.status}: ${errText.substring(0, 500)}` })}\n\n`); return; } const reader = apiResp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const events = buf.split('\n\n'); buf = events.pop(); for (const ev of events) { const dataLine = ev.split('\n').find(l => l.startsWith('data: ')); if (!dataLine) continue; const payload = dataLine.slice(6); if (payload === '[DONE]') continue; try { const obj = JSON.parse(payload); if (obj.type === 'content_block_delta' && obj.delta?.type === 'text_delta') { res.write(`data: ${JSON.stringify({ text: obj.delta.text })}\n\n`); } } catch (e) {} } } } catch (err) { console.error('[api error]', err.message); res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); } finally { clearInterval(heartbeat); res.write('data: [DONE]\n\n'); res.end(); } }); app.listen(8108, () => console.log('sechenov :8108'));