From 5d0db75ba3ef6ed8a94d119e68fed44faa1744c5 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 2 Apr 2026 23:33:10 -0700 Subject: [PATCH] feat: add authenticated screenshot utility for Paperclip UI Reads board token from ~/.paperclip/auth.json and injects it as a Bearer header via Playwright, enabling agents to take screenshots of authenticated pages without storing email/password credentials. Co-Authored-By: Paperclip --- scripts/screenshot.cjs | 86 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 scripts/screenshot.cjs diff --git a/scripts/screenshot.cjs b/scripts/screenshot.cjs new file mode 100644 index 00000000000..052c5f4922d --- /dev/null +++ b/scripts/screenshot.cjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Screenshot utility for Paperclip UI. + * + * Reads the board token from ~/.paperclip/auth.json and injects it as a + * Bearer header so Playwright can access authenticated pages. + * + * Usage: + * node scripts/screenshot.cjs [output.png] [--width 1280] [--height 800] [--wait 2000] + * + * Examples: + * node scripts/screenshot.cjs /PAPA/agents/cto/instructions /tmp/shot.png + * node scripts/screenshot.cjs http://localhost:5173/PAPA/agents/cto/instructions + */ + +const fs = require("fs"); +const path = require("path"); +const os = require("os"); + +// --- CLI args ----------------------------------------------------------- +const args = process.argv.slice(2); +function flag(name, fallback) { + const i = args.indexOf(`--${name}`); + if (i === -1) return fallback; + const val = args.splice(i, 2)[1]; + return Number(val) || fallback; +} +const width = flag("width", 1280); +const height = flag("height", 800); +const waitMs = flag("wait", 2000); + +const rawUrl = args[0]; +const outPath = args[1] || "/tmp/paperclip-screenshot.png"; + +if (!rawUrl) { + console.error("Usage: node scripts/screenshot.cjs [output.png]"); + process.exit(1); +} + +// --- Auth ---------------------------------------------------------------- +function loadBoardToken() { + const authPath = path.resolve(os.homedir(), ".paperclip/auth.json"); + try { + const auth = JSON.parse(fs.readFileSync(authPath, "utf-8")); + const creds = auth.credentials || {}; + const entry = Object.values(creds)[0]; + if (entry && entry.token) return { token: entry.token, apiBase: entry.apiBase }; + } catch (_) { + // ignore + } + return null; +} + +const cred = loadBoardToken(); +if (!cred) { + console.error("No board token found in ~/.paperclip/auth.json"); + process.exit(1); +} + +// Resolve URL — if it starts with / treat as path relative to apiBase +const url = rawUrl.startsWith("http") ? rawUrl : `${cred.apiBase}${rawUrl}`; + +// --- Screenshot ---------------------------------------------------------- +(async () => { + const { chromium } = require("playwright"); + const browser = await chromium.launch(); + const context = await browser.newContext({ + viewport: { width, height }, + extraHTTPHeaders: { + Authorization: `Bearer ${cred.token}`, + }, + }); + + const page = await context.newPage(); + try { + await page.goto(url, { waitUntil: "networkidle", timeout: 20000 }); + await page.waitForTimeout(waitMs); + await page.screenshot({ path: outPath, fullPage: false }); + console.log(`Saved: ${outPath}`); + } catch (err) { + console.error(`Screenshot failed: ${err.message}`); + process.exit(1); + } finally { + await browser.close(); + } +})();