task logs: migrate from filesystem to postgresql; refactor sidecars into submodules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import type { SidecarCommand, SidecarEvent } from '../protocol';
|
||||
import { loadState, flushAndSave, acquireLock, releaseLock, getState } from './state';
|
||||
import { startAnthropicProxy, getProxySecret, ensureProxySecret } from './proxy';
|
||||
import * as claudeManager from './claude-manager';
|
||||
import { createSidecarConnector } from '../connect';
|
||||
|
||||
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
|
||||
|
||||
// ── Startup ──
|
||||
|
||||
if (!acquireLock()) {
|
||||
console.error('[claude] another instance is already running (lock file exists with live PID)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
loadState();
|
||||
ensureProxySecret();
|
||||
|
||||
// Start Anthropic proxy
|
||||
try {
|
||||
startAnthropicProxy();
|
||||
} catch (err) {
|
||||
console.error('[claude] failed to start Anthropic proxy:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
|
||||
// ── Command handlers ──
|
||||
|
||||
type ReplyFn = (msg: SidecarEvent) => void;
|
||||
|
||||
async function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
|
||||
switch (cmd.type) {
|
||||
case 'ping':
|
||||
reply({ type: 'pong', id: cmd.id });
|
||||
break;
|
||||
|
||||
case 'state:sync':
|
||||
reply({
|
||||
type: 'state:sync',
|
||||
id: cmd.id,
|
||||
state: {
|
||||
proxySecret: getProxySecret(),
|
||||
claudeSessions: { ...getState().claudeSessions },
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case 'proxy:secret':
|
||||
reply({ type: 'proxy:secret', id: cmd.id, secret: getProxySecret() });
|
||||
break;
|
||||
|
||||
case 'claude:spawn': {
|
||||
try {
|
||||
const result = await claudeManager.spawnClaude(cmd.params);
|
||||
reply({ type: 'claude:result', id: cmd.id, result });
|
||||
} catch (err) {
|
||||
reply({ type: 'claude:error', id: cmd.id, error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'claude:spawn-streaming': {
|
||||
reply({ type: 'claude:spawned', id: cmd.id, sessionKey: cmd.params.sessionKey });
|
||||
|
||||
const onEvent = (event: import('../../api/pi/types').PiEvent) => {
|
||||
connection.send({ type: 'claude:event', sessionKey: cmd.params.sessionKey, event });
|
||||
};
|
||||
|
||||
claudeManager.spawnClaudeStreaming(cmd.params, onEvent).catch((err) => {
|
||||
connection.send({
|
||||
type: 'claude:event',
|
||||
sessionKey: cmd.params.sessionKey,
|
||||
event: { type: 'error', message: err instanceof Error ? err.message : String(err) },
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'claude:kill':
|
||||
claudeManager.killClaudeSession(cmd.sessionKey);
|
||||
reply({ type: 'claude:killed', id: cmd.id });
|
||||
break;
|
||||
|
||||
case 'claude:clear-session':
|
||||
claudeManager.clearSession(cmd.sessionKey);
|
||||
reply({ type: 'claude:session-cleared', id: cmd.id });
|
||||
break;
|
||||
|
||||
default:
|
||||
reply({
|
||||
type: 'error',
|
||||
id: (cmd as SidecarCommand).id,
|
||||
error: `Unknown command type: ${(cmd as Record<string, unknown>).type}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connect to API server ──
|
||||
|
||||
const connection = createSidecarConnector({
|
||||
apiUrl: `${API_URL}/api/sidecar/register`,
|
||||
name: 'claude',
|
||||
capabilities: ['claude', 'proxy'],
|
||||
onCommand(cmd, reply) {
|
||||
handleCommand(cmd as SidecarCommand, reply as ReplyFn);
|
||||
},
|
||||
});
|
||||
|
||||
// ── Graceful shutdown ──
|
||||
|
||||
async function shutdown(signal: string) {
|
||||
console.log(`[claude] ${signal} received, saving state...`);
|
||||
connection.destroy();
|
||||
await flushAndSave();
|
||||
releaseLock();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
Reference in New Issue
Block a user