Files
sechenov/server.js

101 lines
3.5 KiB
JavaScript

const express = require('express');
const { spawn, execSync } = require('child_process');
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')));
// 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) => {
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');
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders(); // establish SSE connection immediately
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', 'stream-json', '--verbose'], {
env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let buf = '';
proc.stdout.on('data', (chunk) => {
buf += chunk.toString();
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
if (obj.type === 'assistant' && obj.message?.content) {
for (const block of obj.message.content) {
if (block.type === 'text') res.write(`data: ${JSON.stringify({ text: block.text })}\n\n`);
}
} else if (obj.type === 'result' && obj.is_error) {
res.write(`data: ${JSON.stringify({ error: obj.result || 'error' })}\n\n`);
}
} catch (e) {}
}
});
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`);
}
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'));