mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat: sidebar Write tool, stderr capture, cross-platform URL opener (#584)
Add Write to sidebar allowedTools (both sidebar-agent.ts and server.ts). Write doesn't expand attack surface beyond what Bash already provides. Replace empty stderr handler with buffer capture for better error diagnostics. New bin/gstack-open-url for cross-platform URL opening. Does NOT include Search Before Building intro flow (deferred). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-open-url — cross-platform URL opener
|
||||||
|
#
|
||||||
|
# Usage: gstack-open-url <url>
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
URL="${1:?Usage: gstack-open-url <url>}"
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) open "$URL" ;;
|
||||||
|
Linux) xdg-open "$URL" 2>/dev/null || echo "$URL" ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) start "$URL" ;;
|
||||||
|
*) echo "$URL" ;;
|
||||||
|
esac
|
||||||
@@ -404,7 +404,7 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null): void {
|
|||||||
|
|
||||||
const prompt = `${systemPrompt}\n\nUser: ${userMessage}`;
|
const prompt = `${systemPrompt}\n\nUser: ${userMessage}`;
|
||||||
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose',
|
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose',
|
||||||
'--allowedTools', 'Bash,Read,Glob,Grep'];
|
'--allowedTools', 'Bash,Read,Glob,Grep,Write'];
|
||||||
if (sidebarSession?.claudeSessionId) {
|
if (sidebarSession?.claudeSessionId) {
|
||||||
args.push('--resume', sidebarSession.claudeSessionId);
|
args.push('--resume', sidebarSession.claudeSessionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,8 +160,10 @@ async function askClaude(queueEntry: any): Promise<void> {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Build args fresh — don't trust --resume from queue (session may be stale)
|
// Build args fresh — don't trust --resume from queue (session may be stale)
|
||||||
|
// Write doesn't expand attack surface beyond what Bash already provides.
|
||||||
|
// The security boundary is the localhost-only message path, not the tool allowlist.
|
||||||
let claudeArgs = ['-p', prompt, '--output-format', 'stream-json', '--verbose',
|
let claudeArgs = ['-p', prompt, '--output-format', 'stream-json', '--verbose',
|
||||||
'--allowedTools', 'Bash,Read,Glob,Grep'];
|
'--allowedTools', 'Bash,Read,Glob,Grep,Write'];
|
||||||
|
|
||||||
// Validate cwd exists — queue may reference a stale worktree
|
// Validate cwd exists — queue may reference a stale worktree
|
||||||
let effectiveCwd = cwd || process.cwd();
|
let effectiveCwd = cwd || process.cwd();
|
||||||
@@ -187,20 +189,30 @@ async function askClaude(queueEntry: any): Promise<void> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.stderr.on('data', () => {}); // Claude logs to stderr, ignore
|
let stderrBuffer = '';
|
||||||
|
proc.stderr.on('data', (data: Buffer) => {
|
||||||
|
stderrBuffer += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
proc.on('close', (code) => {
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
try { handleStreamEvent(JSON.parse(buffer)); } catch {}
|
try { handleStreamEvent(JSON.parse(buffer)); } catch {}
|
||||||
}
|
}
|
||||||
sendEvent({ type: 'agent_done' }).then(() => {
|
const doneEvent: Record<string, any> = { type: 'agent_done' };
|
||||||
|
if (code !== 0 && stderrBuffer.trim()) {
|
||||||
|
doneEvent.stderr = stderrBuffer.trim().slice(-500);
|
||||||
|
}
|
||||||
|
sendEvent(doneEvent).then(() => {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
proc.on('error', (err) => {
|
||||||
sendEvent({ type: 'agent_error', error: err.message }).then(() => {
|
const errorMsg = stderrBuffer.trim()
|
||||||
|
? `${err.message}\nstderr: ${stderrBuffer.trim().slice(-500)}`
|
||||||
|
: err.message;
|
||||||
|
sendEvent({ type: 'agent_error', error: errorMsg }).then(() => {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@@ -210,7 +222,10 @@ async function askClaude(queueEntry: any): Promise<void> {
|
|||||||
const timeoutMs = parseInt(process.env.SIDEBAR_AGENT_TIMEOUT || '300000', 10);
|
const timeoutMs = parseInt(process.env.SIDEBAR_AGENT_TIMEOUT || '300000', 10);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try { proc.kill(); } catch {}
|
try { proc.kill(); } catch {}
|
||||||
sendEvent({ type: 'agent_error', error: `Timed out after ${timeoutMs / 1000}s` }).then(() => {
|
const timeoutMsg = stderrBuffer.trim()
|
||||||
|
? `Timed out after ${timeoutMs / 1000}s\nstderr: ${stderrBuffer.trim().slice(-500)}`
|
||||||
|
: `Timed out after ${timeoutMs / 1000}s`;
|
||||||
|
sendEvent({ type: 'agent_error', error: timeoutMsg }).then(() => {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user