const express = require('express'); const path = require('path'); const fs = require('fs'); const os = require('os'); 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.", 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'));