diff --git a/package.json b/package.json index 0f57120..1c55221 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "start": "node server.js" }, "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", "express": "^4.19.2" } } diff --git a/server.js b/server.js index 60149fc..4765fef 100644 --- a/server.js +++ b/server.js @@ -1,84 +1,79 @@ const express = require('express'); -const { spawn, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); +const { createRequire } = require('module'); const app = express(); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, 'public'))); -// Resolve claude binary -let CLAUDE_BIN = 'claude'; -{ - const local = '/app/claude-bin/node_modules/.bin/claude'; - if (fs.existsSync(local)) { - CLAUDE_BIN = local; - } else { - try { - CLAUDE_BIN = execSync('which claude 2>/dev/null || echo claude', { encoding: 'utf8' }).trim(); - } catch (e) {} - } -} -console.log(`✓ Claude binary: ${CLAUDE_BIN}`); - const MODELS = { sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001', }; -app.post('/api/chat', (req, res) => { +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; + } +} + +function createClient(accessToken) { + const Anthropic = require('@anthropic-ai/sdk'); + // OAuth token: use as apiKey — SDK sends x-api-key header + // Some versions also support authToken for Bearer auth + try { + return new Anthropic({ authToken: accessToken }); + } catch (e) { + return new Anthropic({ apiKey: accessToken }); + } +} + +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 configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); + const accessToken = getAccessToken(); + + if (!accessToken) { + return res.status(500).json({ error: 'No credentials found 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(); // establish SSE connection immediately + res.flushHeaders(); - const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir }; - - // heartbeat to keep connection alive during long generation const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000); - const proc = spawn(CLAUDE_BIN, ['-p', text, '--model', modelId, '--output-format', 'text'], { - env, - stdio: ['ignore', 'pipe', 'pipe'], - }); + try { + const client = createClient(accessToken); + const stream = await client.messages.stream({ + model: modelId, + max_tokens: 8192, + messages: [{ role: 'user', content: text }], + }); - proc.stdout.on('data', (chunk) => { - res.write(`data: ${JSON.stringify({ text: chunk.toString() })}\n\n`); - }); - - let stderrBuf = ''; - proc.stderr.on('data', (chunk) => { - const s = chunk.toString(); - stderrBuf += s; - console.error('[claude stderr]', s.substring(0, 500)); - }); - - proc.on('error', (err) => { - console.error('[spawn error]', err.message); - res.write(`data: ${JSON.stringify({ error: `spawn: ${err.message}` })}\n\n`); - }); - - proc.on('close', (code, signal) => { - clearInterval(heartbeat); - console.log(`[claude close] code=${code} signal=${signal} stderr=${stderrBuf.substring(0,300)}`); - if (code !== 0) { - const msg = stderrBuf.trim() || `code=${code} signal=${signal}`; - res.write(`data: ${JSON.stringify({ error: msg.substring(0, 500) })}\n\n`); + for await (const event of stream) { + if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') { + res.write(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`); + } } + } 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(); - }); - - let finished = false; - proc.on('close', () => { finished = true; }); - res.on('close', () => { clearInterval(heartbeat); if (!finished) try { proc.kill(); } catch (e) {} }); + } }); app.listen(8108, () => console.log('sechenov :8108'));