npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

appshotr-mcp

v0.2.4

Published

appshotr MCP — drives your logged-in ChatGPT in a real Chrome: attach an image, prompt, wait for the generated image, save it, next chat.

Downloads

1,183

Readme

appshotr-mcp

Drives your own ChatGPT — your login, your subscription — inside a real Chrome window, from Claude Code.

The loop it was built for:

fresh chat → attach a source screenshot → send the prompt → wait for the generated image → save it to disk → next chat, next prompt.

No OpenAI API key and no per-image API bill: it is your browser session, typing and clicking like you would.


Install

npm install -g appshotr-mcp
appshotr setup

setup does the three things in order and skips what is already done: registers the server in every MCP client it finds on the machine — Claude Code, Codex CLI, Gemini CLI, Claude Desktop, Cursor, Windsurf, VS Code — links this machine to your appshotr.com account (a URL and a short code you approve in a browser you are already signed into), and opens a dedicated Chrome window for you to sign in to ChatGPT once.

The registration is the client's own: claude mcp add, codex mcp add, gemini mcp add where the client has a CLI; a merge into the JSON file it reads (claude_desktop_config.json, ~/.cursor/mcp.json, ~/.codeium/windsurf/mcp_config.json, VS Code's user mcp.json) where it does not — one key added, nothing else touched, a .bak next to the file. appshotr install runs just that step (--client codex cursor to pick, --list to see what was found, --remove to take it out again). In Claude Code the flows are slash commands; in any other agent you just ask — "make the App Store screenshots for this app with appshotr" — and the server's instructions walk the same stops.

Nothing runs without an active Companion plan: every tool and every command checks it with appshotr's server first (a 15-minute access token, so one request per quarter hour), and stops the moment the plan lapses or the machine is unlinked from Settings. Only setup, link, doctor and install work unlicensed, because they are how you get licensed.

Two logins, on purpose: appshotr is the plan (it holds the style catalogues the prompts are built from), ChatGPT is the session that draws. appshotr doctor shows the state of both. Each step is also its own command — appshotr link, appshotr login — and appshotr unlink forgets the plan's secret on this machine (the device slot stays until you revoke it in Settings). The secret is never printed.

Then, in Claude Code, type / and pick:

/mcp__appshotr__screenshots     an App Store set — it finds your screenshots
/mcp__appshotr__icon            an app icon

You do not have to say where the screenshots are: Claude looks in the project, on the Desktop, in Downloads and Pictures for real device captures (Simulator, AirDrop, fastlane) and asks "these?". Both commands walk Claude through the whole job and show you the plan before anything is generated — the style, the headline per slot, which screenshot goes where — and wait for your yes. Claude reads your project first (README, store metadata, onboarding strings) so the headlines describe your app, not a generic one, and it never invents ratings, awards or numbers that are not in the product.

The set's memory: .appshotr/ in your repo

The first run writes it; every run after that starts from it, so "change headline 3", "I retook screen 2" or "also export 6.5" is one change, not a new set. It is meant to be committed, like fastlane's screenshots — a teammate inherits the plan.

.appshotr/
  plan.json                          the brief and its evidence, the style, the size, and per image:
                                     capture, headline, and which generated image it delivers
  screenshots/                       a copy of the captures used (Downloads is not a place to keep them)
  generations/                       every image ChatGPT ever made for this app — never moved or overwritten
    screenshots/repost-2026-08-22-k7m2/     one set: style · day · a short id so two sets never collide
      set.json                             what was generated, with which prompt, in which chat
      01-throw-a-real-punch/               one image: prompt.txt, raw-1.png (+ raw-2.png when ChatGPT
      01-throw-a-real-punch-2/             offered a pair); a regeneration gets its own folder
    icons/iridescent-bolt-2026-08-23-f3q8/ one icon per folder, same idea
  out/                               exactly what you upload, rebuilt from the plan at any time
    iphone-6.9/01-throw-a-real-punch.png
    app-icon/icon-1024.png

plan.json is plain JSON with paths relative to the repo: edit a headline by hand if you like, Claude reads it back. out/ is derived — delete it, or point a slot's source at another variant or an older generation, and plan_export (or appshotr plan export, also --size iphone-6.5) rebuilds it with no browser involved. generations/ and out/ are git-ignored by default (.appshotr/.gitignore, one line to change); the plan and the captures are what a teammate needs.

From a clone instead:

npm install
npm run appshotr -- setup     # same commands, prefixed with `npm run appshotr --`

A dedicated Chrome profile opens (%LOCALAPPDATA%\appshotr-mcp\chrome-profile). Sign in there. The session is remembered — it is a separate profile from your everyday Chrome and from other tools'.

Only one process can own that profile at a time. Stop the MCP server (or close its Chrome window) before running appshotr commands in a terminal, and vice versa.

Use it from Claude Code

Tools exposed by the appshotr MCP server:

| Tool | What it does | | --- | --- | | chatgpt_status | Chrome, profile, signed-in state, default output folder. Call first. | | chatgpt_login | Opens the login page and waits for you to sign in. | | generate_image | Fresh chat → attach input_imagesprompt → wait for the image → save to out_dir. One call per image; saves 2 files when ChatGPT runs its A/B variant mode. | | ask | Same machinery, text reply as markdown. | | collect_images | Rescue path: download the images of a chat that already rendered (chat_url). | | export_apple | Convert existing images to App Store Connect spec (exact size, RGB, no alpha). No browser. | | chatgpt_probe | Diagnostic: which UI selector currently resolves for each role. |

Typical ask in Claude Code:

"Take every PNG in ./shots, and for each one ask ChatGPT for an App Store screenshot with the phone frame and a headline, saving the results in ./out."

Claude calls generate_image once per file. Each call is its own chat, so nothing bleeds between screenshots, and the tool blocks until the image is on disk (30–120 s each).

Use it from the terminal

npm run appshotr -- gen "pon esta pantalla en un mockup de iPhone 16 con fondo degradado" \
  -i ./shots/home.png -o ./out -n home

npm run appshotr -- batch jobs.json -o ./out     # [{ "prompt": "...", "images": ["..."], "name": "home" }, …]
npm run appshotr -- ask "resume esto en 5 bullets" -i ./shots/home.png
npm run appshotr -- collect https://chatgpt.com/c/<id> -o ./out
npm run appshotr -- export ./out/*.png --apple iphone-6.9    # existing files → App Store spec
npm run appshotr -- doctor --page                # health + live selector probe

App Store output

ChatGPT never returns an App Store size on its own, so a raw generated PNG cannot be uploaded. Pass apple_size (tool) or --apple (CLI) and you get a second file at an exact accepted size, RGB, no alpha — iphone-6.9 (1260x2736) by default, plus 6.5", 6.3", 6.1" and iPad 13".

The export never invents and never distorts. What you upload is what ChatGPT drew:

  1. Before generating, the source is letterboxed onto a canvas with the slot's ratio and attached, and the prompt states the exact pixel size. ChatGPT takes its output shape from what you attach, so this pulls it to ~1:2.17 instead of its default 2:3.
  2. After downloading, in order:
    • shape already Apple's (±0.5%) → pure LANCZOS scale, every pixel ChatGPT's. Reported exact.
    • otherwise, measure the band that would have to come off the sides. If it is empty background — per-row variation under 14/255, so a vertical or diagonal gradient still counts as empty — trim it. Reported cropped, with how many px went.
    • artwork reaching the edges → regenerate (up to attempts, default 3) rather than ship a padded or squashed image.
  3. Among the sizes Apple accepts for the slot, it exports to the one that needs no alteration if there is one, and otherwise to the smallest — the least upscaling from ChatGPT's ~900px width.

--fit pad (extend the background), --fit crop (always trim) and --fit stretch are there when you want to override that, but auto is the default and it never fakes pixels.

When something hangs

APPSHOTR_TRACE=1 logs every poll of the wait loop, so a hang explains itself instead of needing a guess:

[appshotr] trace 54s streaming=true turns=1 imgs=1/1 text=77 idle=- stable=2s imgStable=2s

streaming = ChatGPT's stop button is up. imgs=a/b = images found / fully decoded. idle = how long the stream has been closed. imgStable = how long the image set has been unchanged. If it ever waits with imgs=1/1 for more than imageStableMs, that is a bug — send that line.

When ChatGPT's UI changes

Everything the automation touches is one list of candidate selectors per role in src/chatgpt/selectors.ts. Run appshotr doctor --page (or the chatgpt_probe tool): it prints which candidate resolved for each role, and - none - for the one that broke. Add the new selector at the top of that role's list — nothing else needs to change.

What ChatGPT's UI actually does (learned the hard way, live)

Each of these cost a debugging round; they are why the code looks the way it does.

  • Attachments must be DROPPED, not set on the file input. setInputFiles('#upload-files') leaves input.files.length === 1 and fires zero upload requests — ChatGPT ignores the synthetic change event, the prompt is sent without the image, and the model happily invents something unrelated. attach() dispatches a CDP Input.dispatchDragEvent with the file paths instead: the browser itself produces the drop, so the app sees a real one. The + menu is not a fallback — it does not open under a programmatic click.
  • The composer keeps an unsent draft across navigation, even into a brand-new chat. Without clearComposer() the previous prompt prefixes the next one. waitForComposer() clears it.
  • Turns are section[data-testid="conversation-turn-N"][data-turn="user"|"assistant"]. data-message-author-role exists only on the USER turn in the current build, so anchoring the reply on [data-message-author-role="assistant"] finds nothing at all.
  • While ChatGPT "thinks" there is no assistant turn in the DOM — an image request sits at "Pensando…" for a minute or more with only the user turn present. So the wait is two-phase: first confirm the prompt was accepted (user turn appeared / stop button showed up), then wait for the answer to land.
  • A finished image does not mean a finished stream. ChatGPT keeps the stop button up after the picture is done — 4 s in one traced run, and indefinitely when its A/B prompt ("¿Qué imagen te gusta más?") is waiting for a click. Waiting on that button alone burns the entire timeout with the result already on screen. An image set that has not changed for imageStableMs is treated as finished no matter what the button says.
  • A reply with no image is not a reason to wait either. If the stream ends, a turn exists and no image ever showed up, ChatGPT answered in text or asked something: fail after noImageGraceMs with what it actually said, instead of sitting out the five-minute image timeout.
  • Progressive previews are invisible to the DOM query: while the picture visibly renders in Chrome, no <img> of usable size exists yet. So "I can see it, why are you waiting?" is expected for a few seconds — the element only appears when the final image lands.
  • The same image is rendered several times inside one turn (thumbnail, comparison, full view) — dedupe by src or you save three copies of one image.
  • A/B mode: ChatGPT sometimes generates TWO variants and asks "¿Qué imagen te gusta más?" ([data-testid="image-paragen-multigen"]). Both are final renders, so both get saved (-1, -2).
  • Opening a saved chat renders the turns before the images finish loading, so collect_images polls for decoded images instead of sleeping.
  • The output size is NOT under your control. appshotr's backend asks the API for 1024x2224 (already Apple's 1:2.17) and always gets it, so a plain resize is enough. The web UI gives the model the choice and it wanders: measured, 1024x1536 (2:3), 941x1672 (16:9), 923x1704 (1:1.847), then 851x1849 and 851x1848 (1:2.173 and 1:2.1716 — a 0.01% miss) once the attached canvas carried the slot's shape AND the prompt named the exact pixel size. So the canvas trick works but is not deterministic: the retry loop is what closes the gap.
  • Resolution is lower too: ~850-940 px wide from the browser vs 1024 from the API, so the export upscales ~45% instead of ~23%. Slightly softer; still sharp enough at App Store viewing size.
  • A killed process leaves Chrome holding the profile, and the next launch just hands off to it and dies ("Target page, context or browser has been closed"). getContext() detects that, kills only the processes whose command line points at the appshotr profile, and retries once — appshotr kill does it by hand.

Two things that will bite you if you forget them

  1. Browser-side code must be a string. Anything running inside the page lives in src/chatgpt/pagefns.ts as a string snippet. A function passed to page.evaluate() is serialized to source, and tsx/esbuild's keepNames wraps declarations in a __name(…) helper that does not exist in the page → ReferenceError: __name is not defined, only when running from source. A test in tests/pagefns.test.ts fails the build if a function-form evaluate sneaks back in.
  2. One browser, one prompt at a time. Two concurrent calls would type into the same composer. The second one fails fast with busy:.

Config (env)

| Variable | Default | | | --- | --- | --- | | APPSHOTR_HOME | %LOCALAPPDATA%\appshotr-mcp | data root: profile + saved images | | APPSHOTR_CHROME | auto-detected | path to chrome.exe | | APPSHOTR_CHATGPT_URL | https://chatgpt.com | | | APPSHOTR_IMAGE_TIMEOUT_S | 300 | wait for a generated image | | APPSHOTR_REPLY_TIMEOUT_S | 180 | wait for a text reply | | APPSHOTR_UPLOAD_TIMEOUT_S | 120 | wait for an attachment to finish uploading | | APPSHOTR_BROWSER_IDLE_MS | 600000 | close the idle Chrome (reopens on demand); 0 disables | | APPSHOTR_MIN_IMAGE_WIDTH | 256 | ignore smaller <img> in a reply (avatars, icons) |

Layout

src/
  cli.ts                 commands: setup, login, doctor, gen, batch, ask, collect, mcp serve
  config.ts              paths, timeouts, env overrides
  browser/chrome.ts      real Chrome, persistent profile, idle reaper, session cookie check
  chatgpt/selectors.ts   every DOM hook, one candidate list per role
  chatgpt/pagefns.ts     browser-side snippets (strings — see above)
  chatgpt/page.ts        attach → type → send → wait for the reply → download the images
  chatgpt/session.ts     one-at-a-time lock, status/login/generateImage/ask/collectImages
  mcp/server.ts          the MCP tools (stdio) + Claude Code registration