mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 13:15:24 +02:00
fix: compiled bun binary cannot posix_spawn — use external agent process
Compiled bun binaries fail posix_spawn on ALL executables (even /bin/bash). The server now writes to an agent queue file, and a separate non-compiled bun process (sidebar-agent.ts) reads the queue, spawns claude, and POSTs events back via /sidebar-agent/event. Changes: - server.ts: spawnClaude writes to queue file instead of spawning directly - server.ts: new /sidebar-agent/event endpoint for agent → server relay - server.ts: fix result event field name (event.text vs event.result) - sidebar-agent.ts: rewritten to poll queue file, relay events via HTTP - cli.ts: $B connect auto-starts sidebar-agent as non-compiled bun process Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -414,6 +414,31 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
||||
});
|
||||
const status = await resp.text();
|
||||
console.log(`Connected to real Chrome\n${status}`);
|
||||
|
||||
// Auto-start sidebar agent (non-compiled bun process)
|
||||
const agentScript = path.resolve(__dirname, 'sidebar-agent.ts');
|
||||
const agentLogFile = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent.log');
|
||||
try {
|
||||
// Clear old agent queue
|
||||
const agentQueue = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
|
||||
try { fs.writeFileSync(agentQueue, ''); } catch {}
|
||||
|
||||
const agentProc = Bun.spawn(['bun', 'run', agentScript], {
|
||||
cwd: config.projectDir,
|
||||
env: {
|
||||
...process.env,
|
||||
BROWSE_BIN: path.resolve(__dirname, '..', 'dist', 'browse'),
|
||||
BROWSE_STATE_FILE: config.stateFile,
|
||||
BROWSE_SERVER_PORT: String(newState.port),
|
||||
},
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
agentProc.unref();
|
||||
console.log(`[browse] Sidebar agent started (PID: ${agentProc.pid})`);
|
||||
} catch (err: any) {
|
||||
console.error(`[browse] Sidebar agent failed to start: ${err.message}`);
|
||||
console.error(`[browse] Run manually: bun run ${agentScript}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[browse] Connect failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
|
||||
+57
-59
@@ -22,7 +22,8 @@ import { COMMAND_DESCRIPTIONS } from './commands';
|
||||
import { SNAPSHOT_FLAGS } from './snapshot';
|
||||
import { resolveConfig, ensureStateDir, readVersionHash } from './config';
|
||||
import { emitActivity, subscribe, getActivityAfter, getActivityHistory, getSubscriberCount } from './activity';
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
// Bun.spawn used instead of child_process.spawn (compiled bun binaries
|
||||
// fail posix_spawn on all executables including /bin/bash)
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
@@ -365,7 +366,7 @@ function processAgentEvent(event: any): void {
|
||||
}
|
||||
|
||||
if (event.type === 'result') {
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'result', text: event.result || '' });
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'result', text: event.text || event.result || '' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,68 +399,34 @@ function spawnClaude(userMessage: string): void {
|
||||
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_start' });
|
||||
|
||||
// Resolve claude binary — daemon process may not have user's PATH
|
||||
const claudeBin = findClaudeBin();
|
||||
if (!claudeBin) {
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_error', error: 'Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code' });
|
||||
// Compiled bun binaries CANNOT spawn external processes (posix_spawn
|
||||
// fails with ENOENT on everything, including /bin/bash). Instead,
|
||||
// write the command to a queue file that the sidebar-agent process
|
||||
// (running as non-compiled bun) picks up and spawns claude.
|
||||
const gstackDir = path.join(process.env.HOME || '/tmp', '.gstack');
|
||||
const agentQueue = path.join(gstackDir, 'sidebar-agent-queue.jsonl');
|
||||
const entry = JSON.stringify({
|
||||
ts: new Date().toISOString(),
|
||||
message: userMessage,
|
||||
prompt,
|
||||
args,
|
||||
stateFile: config.stateFile,
|
||||
cwd: (sidebarSession as any)?.worktreePath || process.cwd(),
|
||||
sessionId: sidebarSession?.claudeSessionId || null,
|
||||
});
|
||||
try {
|
||||
fs.mkdirSync(gstackDir, { recursive: true });
|
||||
fs.appendFileSync(agentQueue, entry + '\n');
|
||||
} catch (err: any) {
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_error', error: `Failed to queue: ${err.message}` });
|
||||
agentStatus = 'idle';
|
||||
agentStartTime = null;
|
||||
currentMessage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const proc = spawn(claudeBin, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: (sidebarSession as any)?.worktreePath || process.cwd(),
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile },
|
||||
} as any);
|
||||
proc.stdin?.end();
|
||||
agentProcess = proc;
|
||||
|
||||
let buffer = '';
|
||||
|
||||
proc.stdout?.on('data', (data: Buffer) => {
|
||||
buffer += data.toString();
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try { processAgentEvent(JSON.parse(line)); } catch {}
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr?.on('data', () => {}); // Claude logs to stderr, ignore
|
||||
|
||||
proc.on('close', () => {
|
||||
if (buffer.trim()) {
|
||||
try { processAgentEvent(JSON.parse(buffer)); } catch {}
|
||||
}
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_done' });
|
||||
agentProcess = null;
|
||||
agentStartTime = null;
|
||||
currentMessage = null;
|
||||
|
||||
// Process next queued message (set status synchronously first — race condition guard)
|
||||
if (messageQueue.length > 0) {
|
||||
const next = messageQueue.shift()!;
|
||||
spawnClaude(next.message);
|
||||
} else {
|
||||
agentStatus = 'idle';
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_error', error: err.message });
|
||||
agentProcess = null;
|
||||
agentStartTime = null;
|
||||
currentMessage = null;
|
||||
agentStatus = 'idle';
|
||||
// Try next in queue even after error
|
||||
if (messageQueue.length > 0) {
|
||||
const next = messageQueue.shift()!;
|
||||
spawnClaude(next.message);
|
||||
}
|
||||
});
|
||||
// The sidebar-agent.ts process polls this file and spawns claude.
|
||||
// It POST events back via /sidebar-event which processAgentEvent handles.
|
||||
// Agent status transitions happen when we receive agent_done/agent_error events.
|
||||
}
|
||||
|
||||
function killAgent(): void {
|
||||
@@ -1042,6 +1009,37 @@ async function start() {
|
||||
});
|
||||
}
|
||||
|
||||
// Agent event relay — sidebar-agent.ts POSTs events here
|
||||
if (url.pathname === '/sidebar-agent/event' && req.method === 'POST') {
|
||||
if (!validateAuth(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
const body = await req.json();
|
||||
processAgentEvent(body);
|
||||
// Handle agent lifecycle events
|
||||
if (body.type === 'agent_done' || body.type === 'agent_error') {
|
||||
agentProcess = null;
|
||||
agentStartTime = null;
|
||||
currentMessage = null;
|
||||
if (body.type === 'agent_done') {
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_done' });
|
||||
}
|
||||
// Process next queued message
|
||||
if (messageQueue.length > 0) {
|
||||
const next = messageQueue.shift()!;
|
||||
spawnClaude(next.message);
|
||||
} else {
|
||||
agentStatus = 'idle';
|
||||
}
|
||||
}
|
||||
// Capture claude session ID for --resume
|
||||
if (body.claudeSessionId && sidebarSession && !sidebarSession.claudeSessionId) {
|
||||
sidebarSession.claudeSessionId = body.claudeSessionId;
|
||||
saveSession();
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
|
||||
// ─── Auth-required endpoints ──────────────────────────────────
|
||||
|
||||
if (!validateAuth(req)) {
|
||||
|
||||
+117
-216
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Sidebar Agent — watches sidebar-commands.jsonl, spawns claude -p for each
|
||||
* message, streams live events back to the sidebar.
|
||||
* Sidebar Agent — polls agent-queue from server, spawns claude -p for each
|
||||
* message, streams live events back to the server via /sidebar-agent/event.
|
||||
*
|
||||
* This runs as a NON-COMPILED bun process because compiled bun binaries
|
||||
* cannot posix_spawn external executables. The server writes to the queue
|
||||
* file, this process reads it and spawns claude.
|
||||
*
|
||||
* Usage: BROWSE_BIN=/path/to/browse bun run browse/src/sidebar-agent.ts
|
||||
*/
|
||||
@@ -9,13 +13,15 @@ import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const QUEUE = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-commands.jsonl');
|
||||
const SERVER_URL = 'http://127.0.0.1:34567';
|
||||
const POLL_MS = 1500;
|
||||
const QUEUE = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
|
||||
const SERVER_PORT = parseInt(process.env.BROWSE_SERVER_PORT || '34567', 10);
|
||||
const SERVER_URL = `http://127.0.0.1:${SERVER_PORT}`;
|
||||
const POLL_MS = 500; // Fast polling — server already did the user-facing response
|
||||
const B = process.env.BROWSE_BIN || path.resolve(__dirname, '../../.claude/skills/gstack/browse/dist/browse');
|
||||
|
||||
let lastLine = 0;
|
||||
let authToken: string | null = null;
|
||||
let isProcessing = false;
|
||||
|
||||
// ─── Auth ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -31,14 +37,14 @@ async function refreshToken(): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Event streaming to sidebar ──────────────────────────────────
|
||||
// ─── Event relay to server ──────────────────────────────────────
|
||||
|
||||
async function sendEvent(event: Record<string, any>): Promise<void> {
|
||||
if (!authToken) await refreshToken();
|
||||
if (!authToken) return;
|
||||
|
||||
try {
|
||||
await fetch(`${SERVER_URL}/sidebar-event`, {
|
||||
await fetch(`${SERVER_URL}/sidebar-agent/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -51,159 +57,7 @@ async function sendEvent(event: Record<string, any>): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Claude subprocess with live streaming ───────────────────────
|
||||
|
||||
async function askClaude(userMessage: string): Promise<void> {
|
||||
// Get current page context
|
||||
let pageContext = '';
|
||||
try {
|
||||
const statusResp = await fetch(`${SERVER_URL}/health`, { signal: AbortSignal.timeout(3000) });
|
||||
if (statusResp.ok) {
|
||||
const status = await statusResp.json() as any;
|
||||
pageContext = `Current browser: ${status.currentUrl || 'about:blank'} (${status.tabs || 1} tabs, mode: ${status.mode})`;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const systemPrompt = [
|
||||
'You are a browser assistant running in a Chrome sidebar.',
|
||||
'You control a browser via the browse CLI.',
|
||||
'',
|
||||
`Browse binary: ${B}`,
|
||||
pageContext,
|
||||
'',
|
||||
'Available commands (run via bash):',
|
||||
` ${B} goto <url> — navigate`,
|
||||
` ${B} click <@ref> — click element`,
|
||||
` ${B} fill <@ref> <text> — fill input`,
|
||||
` ${B} snapshot -i — get element refs`,
|
||||
` ${B} text — page text`,
|
||||
` ${B} screenshot — screenshot`,
|
||||
` ${B} back / forward / reload`,
|
||||
` ${B} status — current URL`,
|
||||
'',
|
||||
'Rules:',
|
||||
'- Before clicking, run snapshot -i to get fresh refs.',
|
||||
'- Keep responses SHORT — narrow sidebar.',
|
||||
'- You can also read/write files, run git, etc.',
|
||||
].join('\n');
|
||||
|
||||
const prompt = `${systemPrompt}\n\nUser: ${userMessage}`;
|
||||
|
||||
// Signal that Claude is starting
|
||||
await sendEvent({ type: 'agent_start' });
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('claude', [
|
||||
'-p', prompt,
|
||||
'--output-format', 'stream-json',
|
||||
'--verbose',
|
||||
'--allowedTools', 'Bash,Read,Glob,Grep',
|
||||
], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env },
|
||||
});
|
||||
|
||||
let buffer = '';
|
||||
|
||||
// Close stdin immediately so claude doesn't wait for input
|
||||
proc.stdin.end();
|
||||
|
||||
proc.stdout.on('data', (data: Buffer) => {
|
||||
buffer += data.toString();
|
||||
const lines = buffer.split('\n');
|
||||
// Keep last potentially incomplete line in buffer
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
handleStreamEvent(event);
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data: Buffer) => {
|
||||
console.error('[sidebar-agent] stderr:', data.toString().slice(0, 200));
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
console.log(`[sidebar-agent] claude exited with code ${code}`);
|
||||
// Process any remaining buffer
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
handleStreamEvent(JSON.parse(buffer));
|
||||
} catch {}
|
||||
}
|
||||
sendEvent({ type: 'agent_done' }).then(resolve);
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
sendEvent({ type: 'agent_error', error: err.message }).then(resolve);
|
||||
});
|
||||
|
||||
// Timeout after 90 seconds
|
||||
setTimeout(() => {
|
||||
proc.kill();
|
||||
sendEvent({ type: 'agent_error', error: 'Timed out after 90s' }).then(resolve);
|
||||
}, 90000);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleStreamEvent(event: any): Promise<void> {
|
||||
console.log(`[sidebar-agent] event: ${event.type}`, event.type === 'result' ? event.result?.slice(0, 80) : '');
|
||||
// claude stream-json event types:
|
||||
// - { type: "assistant", message: { content: [{ type: "text", text: "..." }, { type: "tool_use", name: "...", input: {...} }] } }
|
||||
// - { type: "content_block_start", content_block: { type: "tool_use", name: "Bash", ... } }
|
||||
// - { type: "content_block_delta", delta: { type: "text_delta", text: "..." } }
|
||||
// - { type: "result", result: "final text", ... }
|
||||
|
||||
if (event.type === 'assistant' && event.message?.content) {
|
||||
for (const block of event.message.content) {
|
||||
if (block.type === 'tool_use') {
|
||||
// Tool call starting
|
||||
await sendEvent({
|
||||
type: 'tool_use',
|
||||
tool: block.name,
|
||||
input: summarizeToolInput(block.name, block.input),
|
||||
});
|
||||
} else if (block.type === 'text' && block.text) {
|
||||
await sendEvent({
|
||||
type: 'text',
|
||||
text: block.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'content_block_start' && event.content_block) {
|
||||
if (event.content_block.type === 'tool_use') {
|
||||
await sendEvent({
|
||||
type: 'tool_use',
|
||||
tool: event.content_block.name,
|
||||
input: summarizeToolInput(event.content_block.name, event.content_block.input),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'content_block_delta' && event.delta) {
|
||||
if (event.delta.type === 'text_delta' && event.delta.text) {
|
||||
await sendEvent({
|
||||
type: 'text_delta',
|
||||
text: event.delta.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'result') {
|
||||
await sendEvent({
|
||||
type: 'result',
|
||||
text: event.result || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
// ─── Claude subprocess ──────────────────────────────────────────
|
||||
|
||||
function shorten(str: string): string {
|
||||
return str
|
||||
@@ -228,49 +82,129 @@ function summarizeToolInput(tool: string, input: any): string {
|
||||
try { return shorten(JSON.stringify(input)).slice(0, 60); } catch { return ''; }
|
||||
}
|
||||
|
||||
async function handleStreamEvent(event: any): Promise<void> {
|
||||
if (event.type === 'system' && event.session_id) {
|
||||
// Relay claude session ID for --resume support
|
||||
await sendEvent({ type: 'system', claudeSessionId: event.session_id });
|
||||
}
|
||||
|
||||
if (event.type === 'assistant' && event.message?.content) {
|
||||
for (const block of event.message.content) {
|
||||
if (block.type === 'tool_use') {
|
||||
await sendEvent({ type: 'tool_use', tool: block.name, input: summarizeToolInput(block.name, block.input) });
|
||||
} else if (block.type === 'text' && block.text) {
|
||||
await sendEvent({ type: 'text', text: block.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
||||
await sendEvent({ type: 'tool_use', tool: event.content_block.name, input: summarizeToolInput(event.content_block.name, event.content_block.input) });
|
||||
}
|
||||
|
||||
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta' && event.delta.text) {
|
||||
await sendEvent({ type: 'text_delta', text: event.delta.text });
|
||||
}
|
||||
|
||||
if (event.type === 'result') {
|
||||
await sendEvent({ type: 'result', text: event.result || '' });
|
||||
}
|
||||
}
|
||||
|
||||
async function askClaude(queueEntry: any): Promise<void> {
|
||||
const { prompt, args, stateFile, cwd } = queueEntry;
|
||||
|
||||
isProcessing = true;
|
||||
await sendEvent({ type: 'agent_start' });
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Build args fresh — don't trust --resume from queue (session may be stale)
|
||||
let claudeArgs = ['-p', prompt, '--output-format', 'stream-json', '--verbose',
|
||||
'--allowedTools', 'Bash,Read,Glob,Grep'];
|
||||
|
||||
const proc = spawn('claude', claudeArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: cwd || process.cwd(),
|
||||
env: { ...process.env, BROWSE_STATE_FILE: stateFile || '' },
|
||||
});
|
||||
|
||||
proc.stdin.end();
|
||||
|
||||
let buffer = '';
|
||||
|
||||
proc.stdout.on('data', (data: Buffer) => {
|
||||
buffer += data.toString();
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try { handleStreamEvent(JSON.parse(line)); } catch {}
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr.on('data', () => {}); // Claude logs to stderr, ignore
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (buffer.trim()) {
|
||||
try { handleStreamEvent(JSON.parse(buffer)); } catch {}
|
||||
}
|
||||
sendEvent({ type: 'agent_done' }).then(() => {
|
||||
isProcessing = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
sendEvent({ type: 'agent_error', error: err.message }).then(() => {
|
||||
isProcessing = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Timeout after 120 seconds
|
||||
setTimeout(() => {
|
||||
try { proc.kill(); } catch {}
|
||||
sendEvent({ type: 'agent_error', error: 'Timed out after 120s' }).then(() => {
|
||||
isProcessing = false;
|
||||
resolve();
|
||||
});
|
||||
}, 120000);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Poll loop ───────────────────────────────────────────────────
|
||||
|
||||
function countLines(): number {
|
||||
try {
|
||||
const content = fs.readFileSync(QUEUE, 'utf-8');
|
||||
return content.split('\n').filter(Boolean).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
return fs.readFileSync(QUEUE, 'utf-8').split('\n').filter(Boolean).length;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
function readLine(n: number): string | null {
|
||||
try {
|
||||
const lines = fs.readFileSync(QUEUE, 'utf-8').split('\n').filter(Boolean);
|
||||
return lines[n - 1] || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
if (isProcessing) return; // One at a time — server handles queuing
|
||||
|
||||
const current = countLines();
|
||||
if (current <= lastLine) return;
|
||||
|
||||
while (lastLine < current) {
|
||||
while (lastLine < current && !isProcessing) {
|
||||
lastLine++;
|
||||
const line = readLine(lastLine);
|
||||
if (!line) continue;
|
||||
|
||||
let message: string;
|
||||
let entry: any;
|
||||
try { entry = JSON.parse(line); } catch { continue; }
|
||||
if (!entry.message && !entry.prompt) continue;
|
||||
|
||||
console.log(`[sidebar-agent] Processing: "${entry.message}"`);
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
message = parsed.message;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!message) continue;
|
||||
|
||||
console.log(`[sidebar-agent] Processing: "${message}"`);
|
||||
|
||||
try {
|
||||
await askClaude(message);
|
||||
await askClaude(entry);
|
||||
} catch (err) {
|
||||
console.error(`[sidebar-agent] Error:`, err);
|
||||
await sendEvent({ type: 'agent_error', error: String(err) });
|
||||
@@ -280,31 +214,6 @@ async function poll() {
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────────────
|
||||
|
||||
async function ensureStateFile(): Promise<string | null> {
|
||||
// Write a state file pointing to the CDP server so claude -p's $B commands
|
||||
// connect to the right browser (not a stale headless server).
|
||||
try {
|
||||
const resp = await fetch(`${SERVER_URL}/health`, { signal: AbortSignal.timeout(3000) });
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json() as any;
|
||||
if (!data.token) return null;
|
||||
|
||||
const stateDir = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent');
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
const stateFile = path.join(stateDir, 'browse.json');
|
||||
fs.writeFileSync(stateFile, JSON.stringify({
|
||||
pid: process.pid,
|
||||
port: 34567,
|
||||
token: data.token,
|
||||
startedAt: new Date().toISOString(),
|
||||
mode: 'cdp',
|
||||
}, null, 2));
|
||||
return stateFile;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dir = path.dirname(QUEUE);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
@@ -313,17 +222,9 @@ async function main() {
|
||||
lastLine = countLines();
|
||||
await refreshToken();
|
||||
|
||||
// Write a state file that points claude -p at the CDP server
|
||||
const stateFile = await ensureStateFile();
|
||||
if (stateFile) {
|
||||
// Set env so all claude -p subprocesses find the right browse server
|
||||
process.env.BROWSE_STATE_FILE = stateFile;
|
||||
console.log(`[sidebar-agent] State file: ${stateFile}`);
|
||||
}
|
||||
|
||||
console.log(`[sidebar-agent] Started. Watching ${QUEUE} from line ${lastLine}`);
|
||||
console.log(`[sidebar-agent] Browse binary: ${B}`);
|
||||
console.log(`[sidebar-agent] Server: ${SERVER_URL}`);
|
||||
console.log(`[sidebar-agent] Browse binary: ${B}`);
|
||||
|
||||
setInterval(poll, POLL_MS);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user