mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-07 22:16:52 +02:00
feat: v0.3.2 — project-local state, diff-aware QA, Greptile integration (#36)
* fix: cookie import picker returns JSON instead of HTML jsonResponse() was defined at module scope but referenced `url` which only existed as a parameter of handleCookiePickerRoute(). Every API call crashed, the catch block also crashed, and Bun returned a default HTML page that the frontend couldn't parse as JSON. Thread port via corsOrigin() helper and options objects. Add route-level tests to prevent this class of bug from shipping again. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add help command to browse server Agents that don't have SKILL.md loaded (or misread flags) had no way to self-discover the CLI. The help command returns a formatted reference of all commands and snapshot flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: version-aware find-browse with META signal protocol Agents in other workspaces found stale browse binaries that were missing newer flags. find-browse now compares the local binary's git SHA against origin/main via git ls-remote (4hr cache), and emits META:UPDATE_AVAILABLE when behind. SKILL.md setup checks parse META signals and prompt the user to update. - New compiled binary: browse/dist/find-browse (TypeScript, testable) - Bash shim at browse/bin/find-browse delegates to compiled binary - .version file written at build time with git commit SHA - Build script compiles both browse and find-browse binaries - Graceful degradation: offline, missing .version, corrupt cache all skip check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: clean up .bun-build temp files after compile bun build --compile leaves ~58MB temp files in the working directory. Add rm -f .*.bun-build to the build script to clean up after each build. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make help command reachable by removing it from META_COMMANDS help was in META_COMMANDS, so it dispatched to handleMetaCommand() which threw "Unknown meta command: help". Removing it from the set lets the dedicated else-if handler in handleCommand() execute correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump version and changelog (v0.3.2) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add shared Greptile comment triage reference doc Shared reference for fetching, filtering, and classifying Greptile review comments on GitHub PRs. Used by both /review and /ship skills. Includes parallel API fetching, suppressions check, classification logic, reply APIs, and history file writes. * feat: make /review and /ship Greptile-aware /review: Step 2.5 fetches and classifies Greptile comments, Step 5 resolves them with AskUserQuestion for valid issues and false positives. /ship: Step 3.75 triages Greptile comments between pre-landing review and version bump. Adds Greptile Review section to PR body in Step 8. Re-runs tests if any Greptile fixes are applied. * feat: add Greptile batting average to /retro Reads ~/.gstack/greptile-history.md, computes signal ratio (valid catches vs false positives), includes in metrics table, JSON snapshot, and Code Quality Signals narrative. * docs: add Greptile integration section to README Personal endorsement, two-layer review narrative, full UX walkthrough transcript, skills table updates. Add Greptile training feedback loop to TODO.md future ideas. * feat: add local dev mode for testing skills from within the repo bin/dev-setup creates .claude/skills/gstack symlink to the working tree so Claude Code discovers skills locally. bin/dev-teardown cleans up. DEVELOPING_GSTACK.md documents the workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: narrow gitignore to .claude/skills/ instead of all .claude/ Avoids ignoring legitimate Claude Code config like settings.json or CLAUDE.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: rename DEVELOPING_GSTACK.md to CONTRIBUTING.md Rewritten as a contributor-friendly guide instead of a dry plan doc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: explain why dev-setup is needed in CONTRIBUTING.md quick start Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add browser interaction guidance to CLAUDE.md Prevents Claude from using mcp__claude-in-chrome__* tools instead of /browse. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add shared config module for project-local browse state Centralizes path resolution (git root detection, state dir, log paths) into config.ts. Both cli.ts and server.ts import from it, eliminating duplicated PORT_OFFSET/BROWSE_PORT/STATE_FILE logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: rewrite port selection to use random ports Replace CONDUCTOR_PORT magic offset and 9400-9409 scan with random port 10000-60000. Atomic state file writes, log paths from config module, binaryVersion field for auto-restart on update. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: move browse state from /tmp to project-local .gstack/ CLI now uses config module for state paths, passes BROWSE_STATE_FILE to spawned server. Adds version mismatch auto-restart, legacy /tmp cleanup with PID verification, and removes stale global install fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update crash log path reference to .gstack/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add config tests and update CLI lifecycle test 14 new tests for config resolution, ensureStateDir, readVersionHash, resolveServerScript, and version mismatch detection. Remove obsolete CONDUCTOR_PORT/BROWSE_PORT filtering from commands.test.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update BROWSER.md and TODO.md for project-local state Replace /tmp paths with .gstack/, remove CONDUCTOR_PORT docs, document random port selection and per-project isolation. Add server bundling TODO. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update README, CHANGELOG, and CONTRIBUTING for v0.3.2 - README: replace Conductor-aware language with project-local isolation, add Greptile setup note - CHANGELOG: comprehensive v0.3.2 entry with all state management changes - CONTRIBUTING: add instructions for testing branches in other repos Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add diff-aware mode to /qa — auto-tests affected pages from branch diff When on a feature branch, /qa now reads git diff main, identifies affected pages/routes from changed files, and tests them automatically. No URL required. The most natural flow: write code, /ship, /qa. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for complete v0.3.2 coverage Add missing entries: diff-aware QA mode, Greptile integration, local dev mode, crash log path fix, README/SKILL.md updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+60
-21
@@ -6,6 +6,11 @@
|
||||
* Console/network/dialog buffers: CircularBuffer in-memory + async disk flush
|
||||
* Chromium crash → server EXITS with clear error (CLI auto-restarts)
|
||||
* Auto-shutdown after BROWSE_IDLE_TIMEOUT (default 30 min)
|
||||
*
|
||||
* State:
|
||||
* State file: <project-root>/.gstack/browse.json (set via BROWSE_STATE_FILE env)
|
||||
* Log files: <project-root>/.gstack/browse-{console,network,dialog}.log
|
||||
* Port: random 10000-60000 (or BROWSE_PORT env for debug override)
|
||||
*/
|
||||
|
||||
import { BrowserManager } from './browser-manager';
|
||||
@@ -13,18 +18,18 @@ import { handleReadCommand } from './read-commands';
|
||||
import { handleWriteCommand } from './write-commands';
|
||||
import { handleMetaCommand } from './meta-commands';
|
||||
import { handleCookiePickerRoute } from './cookie-picker-routes';
|
||||
import { resolveConfig, ensureStateDir, readVersionHash } from './config';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
// ─── Auth (inline) ─────────────────────────────────────────────
|
||||
// ─── Config ─────────────────────────────────────────────────────
|
||||
const config = resolveConfig();
|
||||
ensureStateDir(config);
|
||||
|
||||
// ─── Auth ───────────────────────────────────────────────────────
|
||||
const AUTH_TOKEN = crypto.randomUUID();
|
||||
const PORT_OFFSET = 45600;
|
||||
const BROWSE_PORT = process.env.CONDUCTOR_PORT
|
||||
? parseInt(process.env.CONDUCTOR_PORT, 10) - PORT_OFFSET
|
||||
: parseInt(process.env.BROWSE_PORT || '0', 10); // 0 = auto-scan
|
||||
const INSTANCE_SUFFIX = BROWSE_PORT ? `-${BROWSE_PORT}` : '';
|
||||
const STATE_FILE = process.env.BROWSE_STATE_FILE || `/tmp/browse-server${INSTANCE_SUFFIX}.json`;
|
||||
const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
|
||||
const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
|
||||
|
||||
function validateAuth(req: Request): boolean {
|
||||
@@ -36,9 +41,9 @@ function validateAuth(req: Request): boolean {
|
||||
import { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry } from './buffers';
|
||||
export { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry };
|
||||
|
||||
const CONSOLE_LOG_PATH = `/tmp/browse-console${INSTANCE_SUFFIX}.log`;
|
||||
const NETWORK_LOG_PATH = `/tmp/browse-network${INSTANCE_SUFFIX}.log`;
|
||||
const DIALOG_LOG_PATH = `/tmp/browse-dialog${INSTANCE_SUFFIX}.log`;
|
||||
const CONSOLE_LOG_PATH = config.consoleLog;
|
||||
const NETWORK_LOG_PATH = config.networkLog;
|
||||
const DIALOG_LOG_PATH = config.dialogLog;
|
||||
let lastConsoleFlushed = 0;
|
||||
let lastNetworkFlushed = 0;
|
||||
let lastDialogFlushed = 0;
|
||||
@@ -132,22 +137,25 @@ export const META_COMMANDS = new Set([
|
||||
const browserManager = new BrowserManager();
|
||||
let isShuttingDown = false;
|
||||
|
||||
// Find port: deterministic from CONDUCTOR_PORT, or scan range
|
||||
// Find port: explicit BROWSE_PORT, or random in 10000-60000
|
||||
async function findPort(): Promise<number> {
|
||||
// Deterministic port from CONDUCTOR_PORT (e.g., 55040 - 45600 = 9440)
|
||||
// Explicit port override (for debugging)
|
||||
if (BROWSE_PORT) {
|
||||
try {
|
||||
const testServer = Bun.serve({ port: BROWSE_PORT, fetch: () => new Response('ok') });
|
||||
testServer.stop();
|
||||
return BROWSE_PORT;
|
||||
} catch {
|
||||
throw new Error(`[browse] Port ${BROWSE_PORT} (from CONDUCTOR_PORT ${process.env.CONDUCTOR_PORT}) is in use`);
|
||||
throw new Error(`[browse] Port ${BROWSE_PORT} (from BROWSE_PORT env) is in use`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: scan range
|
||||
const start = parseInt(process.env.BROWSE_PORT_START || '9400', 10);
|
||||
for (let port = start; port < start + 10; port++) {
|
||||
// Random port with retry
|
||||
const MIN_PORT = 10000;
|
||||
const MAX_PORT = 60000;
|
||||
const MAX_RETRIES = 5;
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
const port = MIN_PORT + Math.floor(Math.random() * (MAX_PORT - MIN_PORT));
|
||||
try {
|
||||
const testServer = Bun.serve({ port, fetch: () => new Response('ok') });
|
||||
testServer.stop();
|
||||
@@ -156,7 +164,7 @@ async function findPort(): Promise<number> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error(`[browse] No available port in range ${start}-${start + 9}`);
|
||||
throw new Error(`[browse] No available port after ${MAX_RETRIES} attempts in range ${MIN_PORT}-${MAX_PORT}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +209,34 @@ async function handleCommand(body: any): Promise<Response> {
|
||||
result = await handleWriteCommand(command, args, browserManager);
|
||||
} else if (META_COMMANDS.has(command)) {
|
||||
result = await handleMetaCommand(command, args, browserManager, shutdown);
|
||||
} else if (command === 'help') {
|
||||
const helpText = [
|
||||
'gstack browse — headless browser for AI agents',
|
||||
'',
|
||||
'Commands:',
|
||||
' Navigation: goto <url>, back, forward, reload',
|
||||
' Interaction: click <sel>, fill <sel> <text>, select <sel> <val>, hover, type, press, scroll, wait',
|
||||
' Read: text [sel], html [sel], links, forms, accessibility, cookies, storage, console, network, perf',
|
||||
' Evaluate: js <expr>, eval <expr>, css <sel> <prop>, attrs <sel>, is <sel> <state>',
|
||||
' Snapshot: snapshot [-i] [-c] [-d N] [-s sel] [-D] [-a] [-o path] [-C]',
|
||||
' Screenshot: screenshot [path], pdf [path], responsive <widths>',
|
||||
' Tabs: tabs, tab <id>, newtab [url], closetab [id]',
|
||||
' State: cookie <set|get|clear>, cookie-import <json>, cookie-import-browser [browser]',
|
||||
' Headers: header <set|clear> [name] [value], useragent [string]',
|
||||
' Upload: upload <sel> <file1> [file2...]',
|
||||
' Dialogs: dialog, dialog-accept [text], dialog-dismiss',
|
||||
' Meta: status, stop, restart, diff, chain, help',
|
||||
'',
|
||||
'Snapshot flags:',
|
||||
' -i interactive only -c compact (remove empty nodes)',
|
||||
' -d N limit depth -s sel scope to CSS selector',
|
||||
' -D diff vs previous -a annotated screenshot with ref labels',
|
||||
' -o path output file -C cursor-interactive elements',
|
||||
].join('\n');
|
||||
return new Response(helpText, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
} else {
|
||||
return new Response(JSON.stringify({
|
||||
error: `Unknown command: ${command}`,
|
||||
@@ -235,7 +271,7 @@ async function shutdown() {
|
||||
await browserManager.close();
|
||||
|
||||
// Clean up state file
|
||||
try { fs.unlinkSync(STATE_FILE); } catch {}
|
||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -301,19 +337,22 @@ async function start() {
|
||||
},
|
||||
});
|
||||
|
||||
// Write state file
|
||||
// Write state file (atomic: write .tmp then rename)
|
||||
const state = {
|
||||
pid: process.pid,
|
||||
port,
|
||||
token: AUTH_TOKEN,
|
||||
startedAt: new Date().toISOString(),
|
||||
serverPath: path.resolve(import.meta.dir, 'server.ts'),
|
||||
binaryVersion: readVersionHash() || undefined,
|
||||
};
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
|
||||
const tmpFile = config.stateFile + '.tmp';
|
||||
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), { mode: 0o600 });
|
||||
fs.renameSync(tmpFile, config.stateFile);
|
||||
|
||||
browserManager.serverPort = port;
|
||||
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
||||
console.log(`[browse] State file: ${STATE_FILE}`);
|
||||
console.log(`[browse] State file: ${config.stateFile}`);
|
||||
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1000}s`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user