{"id":51048332,"url":"https://github.com/pepperonas/pigeon","last_synced_at":"2026-06-22T15:02:00.860Z","repository":{"id":362237127,"uuid":"1257713730","full_name":"pepperonas/pigeon","owner":"pepperonas","description":"🐦 Forward browser console errors to Claude Code via MCP — Chrome MV3 extension → WebSocket bridge → MCP stdio server.","archived":false,"fork":false,"pushed_at":"2026-06-03T09:22:24.000Z","size":111,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T09:23:30.671Z","etag":null,"topics":["chrome-extension","claude-code","debugging","devtools","manifest-v3","mcp","model-context-protocol","typescript","websocket"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pepperonas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T00:10:23.000Z","updated_at":"2026-06-03T09:22:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pepperonas/pigeon","commit_stats":null,"previous_names":["pepperonas/pigeon"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/pepperonas/pigeon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pepperonas%2Fpigeon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pepperonas%2Fpigeon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pepperonas%2Fpigeon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pepperonas%2Fpigeon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pepperonas","download_url":"https://codeload.github.com/pepperonas/pigeon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pepperonas%2Fpigeon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34653715,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chrome-extension","claude-code","debugging","devtools","manifest-v3","mcp","model-context-protocol","typescript","websocket"],"created_at":"2026-06-22T15:01:59.775Z","updated_at":"2026-06-22T15:02:00.855Z","avatar_url":"https://github.com/pepperonas.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🐦 Pigeon\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Pigeon-console%20errors%20%E2%86%92%20Claude%20Code-7C3AED?style=for-the-badge\u0026logo=anthropic\u0026logoColor=white\" alt=\"Pigeon\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/pepperonas/pigeon/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/pepperonas/pigeon/actions/workflows/ci.yml/badge.svg\" alt=\"CI\" /\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-green.svg?style=flat-square\" alt=\"License: MIT\" /\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/version-0.2.0-blue.svg?style=flat-square\" alt=\"Version\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/status-v1%20minimal-orange.svg?style=flat-square\" alt=\"Status\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square\" alt=\"PRs welcome\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/TypeScript-5.x-3178C6?style=flat-square\u0026logo=typescript\u0026logoColor=white\" alt=\"TypeScript\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Node.js-18%2B-339933?style=flat-square\u0026logo=node.js\u0026logoColor=white\" alt=\"Node 18+\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/MCP-stdio-000000?style=flat-square\u0026logo=anthropic\u0026logoColor=white\" alt=\"Model Context Protocol\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/WebSocket-ws-010101?style=flat-square\u0026logo=socketdotio\u0026logoColor=white\" alt=\"WebSocket\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Chrome-Manifest%20V3-4285F4?style=flat-square\u0026logo=googlechrome\u0026logoColor=white\" alt=\"Chrome MV3\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/bundler-esbuild-FFCF00?style=flat-square\u0026logo=esbuild\u0026logoColor=black\" alt=\"esbuild\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Claude%20Code-ready-D97757?style=flat-square\u0026logo=claude\u0026logoColor=white\" alt=\"Claude Code\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/platform-localhost-lightgrey?style=flat-square\" alt=\"localhost\" /\u003e\n\u003c/p\u003e\n\nForward **browser console errors** straight to **Claude Code** — no more copy‑pasting\nstack traces. Pigeon captures `console.error`/`console.warn`, uncaught exceptions,\nunhandled promise rejections, **and failed network requests** (`fetch`/XHR) from your dev\npages and exposes them to Claude Code over the **Model Context Protocol (MCP)**. Minified\nstack traces are **resolved back to original source** via your dev server's source maps.\n\n```\nBrowser → Extension → WebSocket ┐\n                                 ▼\n                         Bridge daemon  ──control channel──►  MCP proxy (per session) → Claude Code\n                         (one, shared)                        MCP proxy (per session) → Claude Code\n```\n\nA browser extension can't talk to a CLI process directly, hence the chain. A single\n**bridge daemon** owns the WebSocket the extension connects to plus the error buffer; it's\nstarted automatically and shared by every session. Each Claude Code session runs a thin\n**MCP proxy** that forwards tool calls to the daemon over a local control channel — so\n**multiple sessions and projects can use Pigeon at once** (scope each to its dev server\nwith `get_recent_errors({ pageUrl })`).\n\n## Layout\n\n```\npigeon/\n  extension/   # Chrome Manifest V3 extension (TypeScript, esbuild)\n  server/      # Bridge daemon (WebSocket + buffer) + per-session MCP proxy (TypeScript)\n  README.md\n```\n\n## What Claude gets\n\n**Tools**\n\n| Tool | Purpose |\n|------|---------|\n| `get_recent_errors({ limit?, level?, pageUrl?, since? })` | Buffered errors, newest first; filter by `level` (`error`/`warn`/`network`), `pageUrl` substring, or `since` timestamp. |\n| `wait_for_next_error({ timeout_ms? })` | Block until the next error arrives — *“reproduce in the browser, then check.”* |\n| `get_error_stats()` | Counts per level + newest/oldest timestamps. |\n| `clear_errors()` | Empty the buffer. |\n| `get_error_history({ limit?, level?, since? })` | **When `PIGEON_DB` is set.** Query the on-disk history — spans restarts, beyond the 200-entry buffer. |\n| `reload_tab({ tabId? })` | Reload a dev tab (active localhost tab by default) — re-trigger an error after a fix. |\n| `eval_in_page({ expression, tabId?, timeout_ms? })` | **Gated.** Run JS in the page's MAIN world and return the result. Off unless explicitly enabled (see below). |\n\n**Prompts** (appear as slash-commands in Claude Code's `/` menu)\n\n| Prompt | Purpose |\n|--------|---------|\n| `analyze_browser_errors({ limit?, level?, pageUrl? })` | Embeds the recent errors and asks for root‑cause analysis + concrete fixes. |\n| `fix_latest_error()` | Focuses on the single newest error (with its resolved stack) and proposes a fix. |\n\n**Resources**\n\n| Resource | Contents |\n|----------|----------|\n| `pigeon://errors` | Live JSON snapshot of the buffer. |\n| `pigeon://errors/{id}/screenshot` | JPEG of the page when an uncaught error fired. |\n| `pigeon://errors/{id}/dom` | `outerHTML` of the page at error time. |\n\nOn **uncaught errors / unhandled rejections**, Pigeon also captures a screenshot\n(rate-limited, best-effort — the error's tab must be the visible one) and a DOM snapshot.\nEntries gain `hasScreenshot`/`hasDom` flags plus `screenshotUri`/`domUri`. Toggle this off\nin the popup (\"Snapshots on errors\"); console/network events never carry snapshots.\n\nEach captured error carries `level`, `message`, `stack`, `source`, `line`, `col`,\n`pageUrl`, `origin`, `timestamp`, plus `tabId`/`tabTitle` (which tab it came from) and,\nfor network events, `status`. When a source map is available, a `resolvedStack` field is\nadded with the original `file:line:col` frames.\n\nThe server keeps a **ring buffer** of the latest 200 errors and **deduplicates**\nidentical `message`+`stack` pairs seen within 2 seconds (collapsed into one entry with a\n`count`).\n\n---\n\n## Setup\n\n### 1. Build \u0026 start the bridge server\n\n```bash\ncd pigeon\nnpm run install:all      # installs server/ and extension/ deps\nnpm run build            # builds both\n```\n\nStart the bridge. Normally Claude Code launches it for you (see step 3) — but you can run\nit standalone for testing:\n\n```bash\nnpm start                # = node server/dist/index.js\n```\n\n\u003e The bridge listens on `ws://127.0.0.1:8765` for the extension and speaks MCP over\n\u003e **stdio**. All logs go to **stderr** (or `$PIGEON_LOG_FILE`) — never stdout, which is\n\u003e reserved for the MCP JSON‑RPC protocol.\n\nSmoke‑test without the extension:\n\n```bash\nnpm test                 # spawns the server, pushes a fake error, asserts the MCP tools\n# or, against an already-running server:\nnpm --prefix server run test:client\n```\n\n### 2. Load the extension in Chrome\n\n1. Open `chrome://extensions`.\n2. Enable **Developer mode** (top right).\n3. Click **Load unpacked** and select **`pigeon/extension/dist`**.\n4. The 🐦 icon appears. Click it: the popup shows connection status, the number of\n   buffered errors, and three toggles — **forwarding** on/off, **Snapshots on errors**,\n   and **Allow remote eval ⚠️** (for `eval_in_page`, off by default).\n\nThe extension only activates on `http://localhost/*` and `http://127.0.0.1/*` (your dev\nservers). It connects to the bridge automatically; if the bridge isn't running yet, it\nreconnects with exponential backoff and buffers errors in the meantime.\n\n\u003e Rebuild after changes with `npm run dev:extension` (watch) or `npm --prefix extension\n\u003e run build`, then hit **Reload** on the extension card.\n\n### 3. Register the MCP server with Claude Code\n\nUse the absolute path to the built entry point. From the `pigeon/` directory:\n\n```bash\nclaude mcp add pigeon -- node \"$(pwd)/server/dist/index.js\"\n```\n\nAdd `-s user` to register it **globally** (available in every project, not just this one) —\nrecommended if you do web dev across several repos:\n\n```bash\nclaude mcp add -s user pigeon -- node \"$(pwd)/server/dist/index.js\"\n```\n\nOr add it to a `.mcp.json` in your project:\n\n```json\n{\n  \"mcpServers\": {\n    \"pigeon\": {\n      \"command\": \"node\",\n      \"args\": [\"/Users/martin/claude/pigeon/server/dist/index.js\"]\n    }\n  }\n}\n```\n\nTo enable the optional features, pass the env vars at registration. With the CLI:\n\n```bash\nclaude mcp add pigeon \\\n  --env PIGEON_DB=\"$HOME/.pigeon/history.jsonl\" \\\n  --env PIGEON_ALLOW_EVAL=1 \\\n  -- node \"$(pwd)/server/dist/index.js\"\n```\n\n…or in `.mcp.json`, add an `\"env\"` block alongside `command`/`args`:\n\n```json\n\"env\": { \"PIGEON_DB\": \"/abs/path/history.jsonl\", \"PIGEON_ALLOW_EVAL\": \"1\" }\n```\n\nThen, inside Claude Code, verify with `/mcp` — you should see `pigeon` connected with its\ntools and the `pigeon://errors` resource. (How many tools depends on gating: the four\nbuffer tools plus `reload_tab` are always there; `get_error_history` appears with\n`PIGEON_DB` set and `eval_in_page` with `PIGEON_ALLOW_EVAL=1`.)\n\n---\n\n## Using Pigeon in your Claude Code workflow\n\nOnce registered (ideally `-s user`, so it's there in every project) and the extension shows\n**Connected**, run your dev server on `localhost` and work as usual. Verify with `/mcp`\n(pigeon connected) and the popup's green dot.\n\n### Recipes\n\n- **Reactive — “what's broken?”** → *“What errors are in the browser right now?”* Claude calls\n  `get_recent_errors` and reads the message + **source-mapped** stack (original file, not minified).\n- **Repro-driven — the core loop.** *“I'll reproduce it, wait for the error.”* Claude calls\n  `wait_for_next_error`; you trigger it in the browser; it streams in and Claude fixes it. After the\n  fix: *“reload the tab”* → `reload_tab` → wait again. That's **edit → reload → verify** without\n  leaving the CLI.\n- **Slash-commands.** The prompts appear in the `/` menu: `/analyze_browser_errors` (group all\n  current errors by root cause) and `/fix_latest_error` (focus the newest one).\n- **Visual / state bugs.** Uncaught errors carry a screenshot + DOM snapshot —\n  *“look at the screenshot from the last error”* (`pigeon://errors/{id}/screenshot` and `/dom`).\n- **Inspect live (opt-in).** With eval enabled (see Browser control below): *“evaluate\n  `window.__store.getState()` in the page”* → `eval_in_page`.\n- **Recurring errors.** With `PIGEON_DB` set: *“has this error happened before?”* → `get_error_history`\n  (spans restarts).\n\n### Make it smoother\n\n**Let Claude reach for Pigeon on its own** — add to a project's `CLAUDE.md`:\n\n```markdown\n## Debugging\nFor runtime errors in the browser, use the `pigeon` MCP tools\n(`get_recent_errors`, `wait_for_next_error`) instead of asking me to paste console output.\n```\n\n**Skip permission prompts** for the read-only tools — in `settings.json`:\n\n```json\n{ \"permissions\": { \"allow\": [\n  \"mcp__pigeon__get_recent_errors\",\n  \"mcp__pigeon__get_error_stats\",\n  \"mcp__pigeon__wait_for_next_error\",\n  \"mcp__pigeon__get_error_history\"\n] } }\n```\n\nLeave `reload_tab`, `eval_in_page`, and `clear_errors` to prompt.\n\n### Multiple sessions \u0026 projects\n\nThis works out of the box. A single **bridge daemon** is auto-started on first use and shared\nby every Claude Code session — open as many as you like across different projects. It picks\nfree ports automatically (no conflict even if `8765`/`8766` are taken) and advertises them via\n`~/.pigeon/runtime.json`. All localhost tabs feed the same buffer; scope each session to its\nown dev server with the `pageUrl` filter.\n\n**What \"scope with `pageUrl`\" means.** Because every project's tabs feed one shared buffer, you\nnarrow a session to its own errors by filtering on the page URL — a case-insensitive substring\nmatch against each error's URL. You don't type the `{ pageUrl: … }` syntax; you just ask in plain\nlanguage and Claude sets the filter:\n\n\u003e *“What errors are coming from viacamp?”* / *“Show the errors from localhost:3000.”*\n\u003e → Claude calls `get_recent_errors({ pageUrl: \"3000\" })`\n\nNotes:\n- The filter value is whatever is in the tab's **address bar** that uniquely identifies it —\n  usually the port (`\"3000\"`, `\"5173\"`) or `\"localhost:3000\"`.\n- If only one project is running, you don't need the filter at all — the buffer only holds that\n  project's errors.\n- Drop the proactive snippet from [Make it smoother](#make-it-smoother) into each project's\n  `CLAUDE.md` and the session scopes itself automatically.\n\nThe daemon keeps running in the background after sessions close (it owns the browser feed) —\nnormally just leave it. To stop it: `pkill -f dist/bridge.js`. The **first** session's env\n(`PIGEON_DB`, `PIGEON_ALLOW_EVAL`) configures the daemon, so set those consistently in your\nuser-scope registration; later sessions reuse the already-running daemon as-is.\n\n## Dashboard \u0026 CLI\n\nThe daemon comes with two ways to see and manage what's going on across all your sessions.\n\n### `pigeon` CLI\n\nA small terminal tool for the shared daemon (no browser needed):\n\n```bash\nnode server/dist/cli.js status      # or: npm --prefix server run cli -- status\n```\n\n| Command | Shows / does |\n|---------|--------------|\n| `pigeon status` | Daemon health (pid, version, uptime), ports, whether the extension is connected, eval/history flags, buffer counts, and the **connected Claude Code sessions** — each with its project name and the dev-server it's scoped to. |\n| `pigeon doctor` | PASS/WARN/FAIL checks with actionable hints (Node version, build present, daemon up, extension connected, ports reachable, eval gating). The first thing to run when something feels off. |\n| `pigeon stop` | Shut the daemon down gracefully (sessions just respawn it on next use). |\n| `pigeon dashboard` | Open the web dashboard in your browser. |\n\n`pigeon status` when nothing is running tells you so (it never spawns a daemon). Tip: register the\nbin once with `npm --prefix server link` (or add `server/dist/cli.js` to your PATH) to type\n`pigeon` directly.\n\n### Web dashboard\n\nThe daemon serves a live dashboard on `http://127.0.0.1:8767` (first free port from there; see it in\n`pigeon status` or the daemon log). It shows daemon health + ports, the **connected sessions** (which\nproject, since when, which dev-server they're scoped to), and an error feed (level-coloured, click to\nexpand the source-mapped stack + screenshot). The feed has two tabs: **Live** (the in-memory buffer)\nand **History** (the persisted JSONL — only when `PIGEON_DB` is set; it outlives restarts and the\nbuffer). Buttons clear the buffer or stop the daemon.\n\nThe dashboard never pollutes its own data: its page marks itself so the extension skips capturing it\n(otherwise, running on localhost, it would log its own API traffic into the buffer it displays).\n\nIt's **on by default**; disable with `PIGEON_DASHBOARD=0`. Security: it binds `127.0.0.1` only,\nvalidates the `Host` header (anti-DNS-rebinding), and gates every API call on a per-daemon token kept\nin the mode-600 `~/.pigeon/runtime.json` — a random web page can't read the token, so it can't drive\nthe dashboard. It **shows** the eval-gating state but never toggles it (that stays the explicit\ndouble opt-in below).\n\n## Browser control \u0026 security\n\nPigeon can also drive the browser, so Claude can *reproduce* a bug rather than only read it:\n\n- **`reload_tab`** — always available; reloads the target tab.\n- **`eval_in_page`** — runs **arbitrary JavaScript** in the page. This is powerful and\n  dangerous, so it is **off by default** behind a **double opt-in** — both must be true:\n  1. Start the server with `PIGEON_ALLOW_EVAL=1` (otherwise the tool isn't even exposed).\n  2. Turn on **\"Allow remote eval ⚠️\"** in the extension popup (otherwise the extension\n     refuses every eval command).\n\n  It only targets `localhost`/`127.0.0.1` tabs. Leave both off unless you actively want\n  Claude to execute code in your dev page.\n\n## Configuration\n\n| Env var | Default | Where | Meaning |\n|---------|---------|-------|---------|\n| `PIGEON_WS_PORT` | `8765` | daemon | **Base** extension WebSocket port. The daemon binds the first free port from here; the extension scans the next 16. |\n| `PIGEON_CONTROL_PORT` | `8766` | daemon | **Base** control-channel port (first free from here). Proxies discover the actual port via the runtime file. |\n| `PIGEON_DASHBOARD_PORT` | `8767` | daemon | **Base** web-dashboard HTTP port (first free from here). |\n| `PIGEON_DASHBOARD` | `1` (on) | daemon | Set to `0` to disable the local web dashboard entirely. |\n| `PIGEON_LOG_FILE` | — | both | If set, mirror stderr logs to this file (handy to see daemon logs). |\n| `PIGEON_RUNTIME_DIR` | `~/.pigeon` | both | Where the daemon writes its discovery file (`runtime.json`) + lock. |\n| `PIGEON_SOURCEMAPS` | `1` | server | Set to `0` to disable source-map resolution of stacks. |\n| `PIGEON_ALLOW_EVAL` | — | server | Set to `1` to expose the `eval_in_page` tool (also needs the popup toggle). |\n| `PIGEON_DB` | — | server | Path to a JSONL file; enables persistent history + the `get_error_history` tool. |\n\nPorts are chosen automatically: the daemon takes the first free port at/after each base and\nrecords it in `~/.pigeon/runtime.json`; proxies read that file and the extension scans the\nrange — so Pigeon runs out of the box even if `8765`/`8766` are already taken. You only need\n`PIGEON_WS_PORT` if you want a different base (then also bump `WS_BASE` in\n`extension/src/background.ts`, since the extension scans from there).\n\n## What's captured\n\n- **Console:** `console.error` / `console.warn` (wrapped, then passed through unchanged).\n  The hooks are installed by a `MAIN`-world content script at `document_start`, so they run\n  before any page script and catch even synchronous errors during initial load.\n- **Uncaught exceptions** (`window` `error`) and **unhandled promise rejections**.\n- **Failed network requests:** `fetch` and `XMLHttpRequest` responses with status ≥ 400 or\n  a transport failure (status `0`). Intentional `abort`s are ignored. Original semantics\n  are preserved — Pigeon never swallows a response or rejection.\n\n## Notes \u0026 limits\n\n- **Source maps** are fetched from the dev server on demand and cached briefly (5 s, so\n  hot-reloads stay accurate). Resolution is best-effort: no map → the raw stack is kept.\n- Only `localhost` / `127.0.0.1` are matched, by design (your dev servers).\n- The MV3 service worker sleeps after ~30s idle; Pigeon reconnects on wake (incoming\n  messages and a 30s `alarms` heartbeat) and persists the pending queue in\n  `chrome.storage.session`, so errors aren't lost across an eviction.\n- One browser, one bridge: the buffer is shared across all matched tabs (filter with\n  `get_recent_errors({ pageUrl })`).\n- **History:** with `PIGEON_DB=/path/to/history.jsonl`, new errors are appended as JSONL and\n  reloaded on startup. The file is append-only and excludes screenshots/DOM (those stay\n  in-memory only) — rotate or delete it yourself.\n- Lighthouse / performance metrics remain out of scope for now.\n\n## Development\n\n```bash\n# server\nnpm --prefix server run dev             # tsc --watch\nnpm --prefix server run test:e2e        # full MCP proxy/daemon smoke test\nnpm --prefix server run test:multi      # two sessions sharing one daemon + pageUrl scoping + session identity\nnpm --prefix server run test:ports      # daemon shifts ports when 8765/8766 are taken\nnpm --prefix server run test:dashboard  # dashboard HTTP API: token gating, /api/state, clear\nnpm --prefix server run test:sourcemap  # source-map resolution test\n\n# extension\nnpm --prefix extension run dev           # esbuild --watch\nnpm --prefix extension run typecheck     # tsc --noEmit\nnpm --prefix extension run test:unit     # pure serialization unit tests\nnpm --prefix extension run test:browser  # real-browser smoke test (loads the extension)\n```\n\n`test:browser` loads the built extension into **Chrome for Testing** (system Google Chrome\nblocks unpacked extensions). Install it once:\n\n```bash\ncd extension \u0026\u0026 node node_modules/playwright-core/cli.js install chromium\n```\n\nCI (`.github/workflows/ci.yml`) runs everything on each push: a `build-test` job (build,\ntypecheck, unit + source-map + MCP E2E + multi-session + port-fallback + dashboard) and a\n`browser-e2e` job (real-browser smoke test).\nSee [`CLAUDE.md`](CLAUDE.md) for the architecture and the invariants worth not regressing.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpepperonas%2Fpigeon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpepperonas%2Fpigeon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpepperonas%2Fpigeon/lists"}