remove seed directory, clean up provisioning and sync modules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { ServerWebSocket } from 'bun';
|
||||
import type { Subprocess } from 'bun';
|
||||
import type {
|
||||
SidecarCommand,
|
||||
SidecarEvent,
|
||||
@@ -111,6 +113,13 @@ function findSidecarByCapability(cap: string): RegisteredSidecar | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findSidecarByName(name: string): RegisteredSidecar | undefined {
|
||||
for (const sc of sidecars.values()) {
|
||||
if (sc.name === name) return sc;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── Event dispatch ──
|
||||
|
||||
function dispatchEvent(msg: SidecarEvent | PtyEvent) {
|
||||
@@ -166,6 +175,105 @@ function sendFire(cap: string, cmd: SidecarCommand | PtyCommand): void {
|
||||
}
|
||||
}
|
||||
|
||||
function sendCommandToSidecar(
|
||||
sc: RegisteredSidecar,
|
||||
cmd: SidecarCommand | PtyCommand,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete((cmd as any).id);
|
||||
reject(new Error(`Sidecar command ${cmd.type} timed out`));
|
||||
}, timeoutMs);
|
||||
|
||||
pending.set((cmd as any).id, { resolve, reject, timer });
|
||||
sc.ws.send(JSON.stringify(cmd));
|
||||
});
|
||||
}
|
||||
|
||||
function sendFireToSidecar(sc: RegisteredSidecar, cmd: SidecarCommand | PtyCommand): void {
|
||||
sc.ws.send(JSON.stringify(cmd));
|
||||
}
|
||||
|
||||
// ── On-demand Claude sidecar spawning ──
|
||||
|
||||
const USER_INSTANCE_SCRIPT = resolve(import.meta.dir, 'sidecar/claude/user-instance.ts');
|
||||
const SIDECAR_SPAWN_TIMEOUT_MS = 15_000;
|
||||
|
||||
const claudeProcs = new Map<string, Subprocess>();
|
||||
const claudeSpawnWaiters = new Map<string, Promise<RegisteredSidecar>>();
|
||||
|
||||
async function ensureClaudeSidecar(email: string): Promise<RegisteredSidecar> {
|
||||
const name = `claude:${email}`;
|
||||
|
||||
// Already registered?
|
||||
const existing = findSidecarByName(name);
|
||||
if (existing) return existing;
|
||||
|
||||
// Already spawning?
|
||||
const waiter = claudeSpawnWaiters.get(email);
|
||||
if (waiter) return waiter;
|
||||
|
||||
// Spawn and wait for registration
|
||||
const promise = spawnAndWaitForRegistration(email, name);
|
||||
claudeSpawnWaiters.set(email, promise);
|
||||
try {
|
||||
return await promise;
|
||||
} finally {
|
||||
claudeSpawnWaiters.delete(email);
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnAndWaitForRegistration(email: string, name: string): Promise<RegisteredSidecar> {
|
||||
// Get proxy secret for auth
|
||||
const proxySecret = await getProxySecret();
|
||||
const proxyPort = process.env.ANTHROPIC_PROXY_PORT ?? '5051';
|
||||
|
||||
const env: Record<string, string> = {
|
||||
...(process.env as Record<string, string>),
|
||||
CLAUDE_USER_EMAIL: email,
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxyPort}`,
|
||||
ANTHROPIC_API_KEY: proxySecret,
|
||||
};
|
||||
|
||||
const proc = Bun.spawn(['bun', 'run', USER_INSTANCE_SCRIPT], {
|
||||
env,
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
});
|
||||
|
||||
claudeProcs.set(email, proc);
|
||||
|
||||
// Clean up on exit
|
||||
proc.exited.then(() => {
|
||||
claudeProcs.delete(email);
|
||||
});
|
||||
|
||||
// Wait for the sidecar to register
|
||||
return new Promise<RegisteredSidecar>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
unsub();
|
||||
reject(new Error(`Claude sidecar for ${email} failed to register within ${SIDECAR_SPAWN_TIMEOUT_MS}ms`));
|
||||
}, SIDECAR_SPAWN_TIMEOUT_MS);
|
||||
|
||||
// Poll for registration (the sidecar connects via WebSocket and registerSidecar is called)
|
||||
const check = () => {
|
||||
const sc = findSidecarByName(name);
|
||||
if (sc) {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
resolve(sc);
|
||||
}
|
||||
};
|
||||
const interval = setInterval(check, 50);
|
||||
|
||||
const unsub = () => {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
export function isConnected(): boolean {
|
||||
@@ -196,28 +304,41 @@ export function getProxySecretSync(): string {
|
||||
return cachedState?.proxySecret ?? '';
|
||||
}
|
||||
|
||||
// ── Claude Code ──
|
||||
// ── Claude Code (per-user routing) ──
|
||||
|
||||
export async function spawnClaude(params: ClaudeSpawnParams): Promise<ClaudeCodeResult> {
|
||||
const res = await sendCommand('claude', { type: 'claude:spawn', id: nextId(), params }, LONG_TIMEOUT_MS);
|
||||
const sc = await ensureClaudeSidecar(params.email);
|
||||
const res = await sendCommandToSidecar(sc, { type: 'claude:spawn', id: nextId(), params }, LONG_TIMEOUT_MS);
|
||||
if (res.type === 'claude:result') return res.result;
|
||||
if (res.type === 'claude:error') throw new Error(res.error);
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export async function spawnClaudeStreaming(params: ClaudeSpawnStreamingParams): Promise<void> {
|
||||
const res = await sendCommand('claude', { type: 'claude:spawn-streaming', id: nextId(), params });
|
||||
const sc = await ensureClaudeSidecar(params.email);
|
||||
const res = await sendCommandToSidecar(sc, { type: 'claude:spawn-streaming', id: nextId(), params });
|
||||
if (res.type === 'claude:spawned') return;
|
||||
if (res.type === 'claude:error') throw new Error(res.error);
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export function killClaude(sessionKey: string): void {
|
||||
sendFire('claude', { type: 'claude:kill', id: nextId(), sessionKey });
|
||||
export function killClaude(sessionKey: string, email: string): void {
|
||||
const sc = findSidecarByName(`claude:${email}`);
|
||||
if (sc) sendFireToSidecar(sc, { type: 'claude:kill', id: nextId(), sessionKey });
|
||||
}
|
||||
|
||||
export function clearClaudeSession(sessionKey: string): void {
|
||||
sendFire('claude', { type: 'claude:clear-session', id: nextId(), sessionKey });
|
||||
export function clearClaudeSession(sessionKey: string, email?: string): void {
|
||||
if (email) {
|
||||
const sc = findSidecarByName(`claude:${email}`);
|
||||
if (sc) sendFireToSidecar(sc, { type: 'claude:clear-session', id: nextId(), sessionKey });
|
||||
} else {
|
||||
// Broadcast to all claude sidecars (used when email is not available)
|
||||
for (const sc of sidecars.values()) {
|
||||
if (sc.capabilities.includes('claude')) {
|
||||
sendFireToSidecar(sc, { type: 'claude:clear-session', id: nextId(), sessionKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onClaudeEvent(handler: (sessionKey: string, event: PiEvent) => void): () => void {
|
||||
|
||||
Reference in New Issue
Block a user