diff --git a/CLAUDE.md b/CLAUDE.md index ff7987e..cb5db8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ npm run dev # Development with hot-reload - **TypeScript + ESM** - Modern Node.js with ES modules - **MCP SDK** - Uses `@modelcontextprotocol/sdk` for MCP protocol - **PTY** - Uses `node-pty` for pseudo-terminal management -- **File Server** - HTTP file server runs on port 7498 regardless of MCP transport mode (stdio or HTTP). Tool results include `download_url` which is always valid. +- **File Server** - HTTP file server runs on port 7498 regardless of MCP transport mode (stdio or HTTP). Tool results include format-specific download URLs (`download_png_url`, `download_svg_url`, `download_gif_url`) which are always valid. ## MCP Tools diff --git a/README.md b/README.md index 25f23c3..b384897 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ Some configuration can also be provided by the LLM, simply prompt for it: | [`shell_start`](#shell_start) | Start a new PTY session | | [`shell_send`](#shell_send) | Send input to a session | | [`shell_read`](#shell_read) | Read the terminal buffer | -| [`shell_screenshot`](#shell_screenshot) | Capture terminal as PNG | +| [`shell_screenshot`](#shell_screenshot) | Capture terminal as PNG and SVG | | [`shell_record_start`](#shell_record_start) | Start recording for GIF export | | [`shell_record_stop`](#shell_record_stop) | Stop recording and save GIF | | [`shell_stop`](#shell_stop) | Stop a PTY session | @@ -286,7 +286,7 @@ drwxr-xr-x 10 user staff 320 Dec 18 09:00 .. ### **shell_screenshot** -Capture terminal as PNG. Also saves SVG, ANSI, and plain text versions. Pass `name` without extension (`.png` is added automatically). Optionally add a macOS-style window border (off by default): +Capture terminal as PNG and SVG. Also saves ANSI and plain text versions. Pass `name` without extension (`.png` is added automatically). Optionally add a macOS-style window border (off by default): ```json { @@ -296,13 +296,14 @@ Capture terminal as PNG. Also saves SVG, ANSI, and plain text versions. Pass `na } ``` -The response contains a `download_url` for curl to save the file locally: +The response contains `download_png_url` and `download_svg_url` for curl to save the files locally: ```json { "filename": "my-screenshot.png", - "download_url": "http://localhost:7498/files/mcp-.../screenshots/my-screenshot.png", - "hint": "Use curl -o to save the file" + "download_png_url": "http://localhost:7498/files/mcp-.../screenshots/my-screenshot.png", + "download_svg_url": "http://localhost:7498/files/mcp-.../screenshots/my-screenshot.svg", + "hint": "Use curl -o to save the file" } ``` @@ -339,13 +340,13 @@ Stop recording and render frames to GIF: } ``` -The response contains a `download_url` for curl to save the file locally: +The response contains a `download_gif_url` for curl to save the file locally: ```json { "filename": "my-recording.gif", - "download_url": "http://localhost:7498/files/mcp-.../recordings/my-recording.gif", - "hint": "Use curl -o to save the file", + "download_gif_url": "http://localhost:7498/files/mcp-.../recordings/my-recording.gif", + "hint": "Use curl -o to save the file", "frame_count": 42, "duration_ms": 4200 } diff --git a/docs/architecture.md b/docs/architecture.md index cafa9b8..1643bd7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -35,13 +35,13 @@ ## File Download Flow -Screenshots and recordings return a `download_url` instead of base64 data: +Screenshots and recordings return format-specific download URLs instead of base64 data: ``` 1. LLM calls shell_screenshot() or shell_record_stop() 2. Server saves file to temp directory -3. Server returns: { filename, download_url, ... } -4. LLM uses curl to download: curl -o file.png +3. Server returns: { filename, download_png_url, download_svg_url, ... } or { filename, download_gif_url, ... } +4. LLM uses curl to download: curl -o file.png ``` This avoids token overflow from large base64 payloads. diff --git a/docs/examples/vim-close-bordered.gif b/docs/examples/vim-close-bordered.gif new file mode 100644 index 0000000..6ba8548 Binary files /dev/null and b/docs/examples/vim-close-bordered.gif differ diff --git a/evaluations/run.ts b/evaluations/run.ts index 9386182..1e298d9 100644 --- a/evaluations/run.ts +++ b/evaluations/run.ts @@ -88,7 +88,7 @@ ${prompt}`, } } } else if (message.type === "user") { - // Capture tool results to extract download_url + // Capture tool results to extract download URLs for (const block of message.message.content) { if (block.type === "tool_result") { const content = typeof block.content === "string" @@ -102,11 +102,22 @@ ${prompt}`, } catch { continue; // Not JSON, skip } - if (parsed.download_url && parsed.filename) { - const dest = path.join(scenarioPath, parsed.filename); - console.log(` Downloading: ${parsed.download_url} → ${parsed.filename}`); - await downloadArtifact(parsed.download_url, dest); - artifacts.push({ filename: parsed.filename, localPath: dest }); + const downloadUrls: { url: string; filename: string }[] = []; + if (parsed.download_png_url && parsed.filename) { + downloadUrls.push({ url: parsed.download_png_url, filename: parsed.filename }); + } + if (parsed.download_svg_url && parsed.filename) { + const svgFilename = parsed.filename.replace(/\.png$/i, ".svg"); + downloadUrls.push({ url: parsed.download_svg_url, filename: svgFilename }); + } + if (parsed.download_gif_url && parsed.filename) { + downloadUrls.push({ url: parsed.download_gif_url, filename: parsed.filename }); + } + for (const { url, filename } of downloadUrls) { + const dest = path.join(scenarioPath, filename); + console.log(` Downloading: ${url} → ${filename}`); + await downloadArtifact(url, dest); + artifacts.push({ filename, localPath: dest }); console.log(` ✓ Artifact saved: ${dest}`); } } diff --git a/src/index.ts b/src/index.ts index cee2658..b6c5123 100644 --- a/src/index.ts +++ b/src/index.ts @@ -202,7 +202,7 @@ Tips: server.tool( "shell_screenshot", - "Capture terminal screenshot as PNG. Optionally add a macOS-style window border with border: { style: \"macos\", title: \"...\" } (off by default). Returns a download_url - use curl to save the file locally (e.g., curl -o screenshot.png )", + "Capture terminal screenshot as PNG and SVG. Optionally add a macOS-style window border with border: { style: \"macos\", title: \"...\" } (off by default). Returns download_png_url and download_svg_url - use curl to save (e.g., curl -o screenshot.png )", shellScreenshotSchema, async (params) => shellScreenshot(params, toolContext) ); @@ -223,7 +223,7 @@ Tips: server.tool( "shell_record_stop", - "Stop recording and save GIF. Returns a download_url - use curl to save the file locally (e.g., curl -o recording.gif )", + "Stop recording and save GIF. Returns download_gif_url - use curl to save the file locally (e.g., curl -o recording.gif )", shellRecordStopSchema, async (params) => shellRecordStop(params, toolContext) ); diff --git a/src/lib/buffer-to-svg.ts b/src/lib/buffer-to-svg.ts index 8a6319a..a48fcaa 100644 --- a/src/lib/buffer-to-svg.ts +++ b/src/lib/buffer-to-svg.ts @@ -164,7 +164,7 @@ export function bufferToSvg( } // Render cursor if visible - if (terminal.modes.showCursor) { + if ((terminal.modes as { showCursor?: boolean }).showCursor) { const cursorX = buffer.cursorX; const cursorY = buffer.cursorY; diff --git a/src/tools/shell-record-stop.ts b/src/tools/shell-record-stop.ts index e1f817a..b681148 100644 --- a/src/tools/shell-record-stop.ts +++ b/src/tools/shell-record-stop.ts @@ -47,10 +47,10 @@ export async function shellRecordStop( const downloadUrl = context.getDownloadUrl(context.getMcpSessionId(), session_id, "recordings", filename); const output = { filename, - download_url: downloadUrl, + download_gif_url: downloadUrl, frame_count: result.frameCount, duration_ms: durationMs, - hint: "Use curl -o to save the file" + hint: "Use curl -o to save the file" }; context.logToolCall("shell_record_stop", { session_id, name }, output); diff --git a/src/tools/shell-screenshot.ts b/src/tools/shell-screenshot.ts index f212f70..da30629 100644 --- a/src/tools/shell-screenshot.ts +++ b/src/tools/shell-screenshot.ts @@ -58,8 +58,15 @@ export async function shellScreenshot( context.log(`[shellwright] Screenshot saved: ${screenshotDir}/${baseName}.{png,svg,ansi,txt}`); - const downloadUrl = context.getDownloadUrl(context.getMcpSessionId(), session_id, "screenshots", filename); - const output = { filename, download_url: downloadUrl, hint: "Use curl -o to save the file" }; + const mcpSessionId = context.getMcpSessionId(); + const pngUrl = context.getDownloadUrl(mcpSessionId, session_id, "screenshots", `${baseName}.png`); + const svgUrl = context.getDownloadUrl(mcpSessionId, session_id, "screenshots", `${baseName}.svg`); + const output = { + filename, + download_png_url: pngUrl, + download_svg_url: svgUrl, + hint: "Use curl -o to save the file", + }; context.logToolCall("shell_screenshot", { session_id, name }, output); return {