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

easy-cast

v0.1.0

Published

Attach visual evidence — screenshots and video — to a GitHub issue or pull request from the command line

Downloads

22

Readme

easy-cast

Attach visual evidence — screenshots and video — to a GitHub issue or pull request from the command line.

An agent that changes the front end can only report in prose: "done, checked it." A reviewer gets a claim, not proof. easy-cast is the missing step between the two: it takes a file that already exists on disk, uploads it, and posts it into a comment the tool owns and can find again. Capture is not its job: the playwright-cli skill records the screenshot or the video, easy-cast delivers it.

Its primary consumer is an agent rather than a person, which is why the output is a machine contract: --json emits exactly one JSON object whatever happens, and every failure carries a nextAction rather than leaving an exit code to be guessed at.


Status: every command works; not yet published

Stage 0 — the probe that establishes the undocumented endpoint's wire format and response semantics — has run. What it found is in docs/endpoint-semantics.md, and the raw evidence is in fixtures/endpoint/observations.jsonl.

| Command | State today | | --- | --- | | attach | works. Verified end to end against a real repository, for a screenshot and for a video: dry run, real attach, then the identical command again — which reused the asset, created nothing, and updated the comment rather than adding one | | upload | works. Verified live: one URL back, no comment, no journal, and no deduplication — which is the point of it, not a gap | | recover | works. It only ever reads the local journal | | report | works. Verified live: a four-artifact report with a title, headings, a side-by-side comparison and a fold, rendered by GitHub as <h3>, <h4>, <video>, <table> and <details> | | harvest, compose | work, and upload nothing — by design, permanently | | --dry-run | works, and is meaningful: it produces a real plan and a real --confirm-plan token |

Not published yet: the remaining work is release mechanics, not evidence. docs/release-readiness.md has the checklist, the four live smoke results, and the list of what is still not established — .webm and .mov, the exact size boundary, installation tokens and abuse detection, each for a stated reason rather than by oversight.


Uploads are irreversible

An uploaded attachment cannot be deleted — ever. Removing the markdown from a comment does not remove the asset. Editing the comment does not. Deleting the comment does not. Closing the issue does not. The bytes stay on GitHub's storage, and anyone who can read a comment quoting the asset can still fetch them.

Every design decision in this tool follows from that one fact:

  • sanitised source data is the primary defence; masking is secondary and leaks on anything that moves;
  • consent for anything irreversible is collected before the bytes leave the machine;
  • nothing is ever retried automatically when the outcome is unknown.

Install

Not published yet. From a clone:

pnpm install
pnpm build
node dist/src/cli.js --help

Requires Node 22 or newer, gh on PATH for everything except the upload itself, and ffmpeg if you want video converted to a widely playable mp4 rather than uploaded as-is.


The commands

easy-cast attach <file...> --to pr | pr:<n> | issue:<n>
easy-cast upload <file...> [--repo owner/name]
easy-cast recover

easy-cast harvest <dir...> [--out spec.json]      find media, write a report spec
easy-cast compose --spec spec.json                print the comment it would post
easy-cast report  --spec spec.json --to pr        upload and post that report

| Command | What it does | Can it create something permanent? | | --- | --- | --- | | attach | uploads the files and writes them into a comment on a pull request or issue. The comment is identified by a hidden marker, so a repeat run updates that comment instead of adding another | yes | | upload | uploads and prints the URLs, posting nothing. No comment, no journal, no ledger, and no deduplication whatsoever — a repeat uploads again, permanently. It also has no --allow-public gate, and refuses the flag rather than pretending to honour it: exposure follows wherever you paste the URL, not the repository the bytes went to (ADR 023) | yes | | report | attach with a composed body: several artifacts arranged by a spec you wrote — headings, per-artifact labels, before/after side by side, folds. Same marker, same ledger, same handshake | yes | | recover | prints the local write-ahead journal: what was uploaded, and what is recorded locally but not yet referenced by any comment. It only reads — repair belongs to a real attach run, which is the only thing that knows the target | no | | harvest | walks directories, finds media, and writes a report spec with every label blank. It uploads nothing, and by design never will | no | | compose | renders the exact comment a spec would post, with unmistakable placeholder URLs, so a person can read it before it exists | no |

Reports

A report is several artifacts in one structured comment instead of a flat stack under one caption:

easy-cast harvest ./screenshots --out report.json   # labels come back blank, on purpose
$EDITOR report.json                                 # open each file, write what it shows
easy-cast compose --spec report.json                # read the comment before it exists
easy-cast report --spec report.json --to pr --dry-run --json
easy-cast report --spec report.json --to pr --confirm-plan=<token>

harvest never uploads, and that is the point rather than a limitation: a command that swept a directory and posted what it found would put a bulk irreversible upload one keystroke away, and would defeat the one obligation the CLI cannot enforce — that somebody looked at each frame. The blank labels are the mechanism: filling one in requires opening the file. A spec that still carries harvest's placeholder text is refused by report. See ADR 024.

For a report the plan token covers the spec as well as the files, so rewriting a label after obtaining a token invalidates it. That is why the token format is v2.

Flags, in full, are easy-cast --help. The ones that change what is at stake:

| Flag | Effect | | --- | --- | | --to pr | resolves the pull request open for the current branch. If there is none, exit 3 — the tool never opens one | | --repo owner/name | overrides the repository inferred from the git remote. The only way to eliminate the wrong-directory class of error outright | | --key <key> | addresses a specific comment. Defaults to a hash of the source files | | --dry-run | plans and changes nothing: no upload, no mutating GitHub call, no temporary file, no repair. Prints a --confirm-plan token | | --json | exactly one JSON object on stdout, whatever the outcome, including an unhandled exception | | --no-convert | uploads video as-is instead of converting it. Toggling this back and forth costs one irreversible upload, because the conversion profile is part of the deduplication key | | --allow-public | attach only. Consent to uploading into a public repository; without it a public target is refused with exit 6 before any byte moves. upload refuses the flag rather than accepting it, because it has no gate for the flag to satisfy — see below |


The --dry-run → --confirm-plan handshake

A real upload requires a token that was computed for the same plan:

easy-cast attach shot.png --to pr --dry-run --json     # exit 0, reason dry_run, prints planToken
easy-cast attach shot.png --to pr --confirm-plan=<token>

Without a token: exit 7, plan_missing. With a token computed for a different plan: exit 7, plan_mismatch, and planContext.changed names what moved.

What the token proves. That what leaves the machine is exactly: those source bytes, identified by hash, in that order, to that repository, that issue or pull request, under that --key and that conversion policy. If any of those changed after the token was issued, the run is refused rather than performed.

What the token does not prove, and cannot:

| Claim | Why the token cannot carry it | | --- | --- | | "--dry-run was executed" | the token is deterministic — it is computable from the plan without ever running --dry-run | | "a plan was displayed to anyone" | nothing about display is an input to the token | | "a human agreed" | consent is not observable by a CLI | | "the file was looked at" | the token is computed over bytes, not over pixels anybody saw |

So an automated caller can satisfy the handshake completely while having seen nothing. That is a conceded limit, not an oversight — see ADR 010. The obligation to look at the file before publishing it lives in the skill, and the CLI cannot enforce it.

What the CLI does own is the rendering: --dry-run prints a human-readable plan — the repository and how it was inferred, every file with its size and size verdict, whether ffmpeg was found and therefore whether video will be converted, and then the token. Without that, the handshake would be two automated calls with nothing shown in between.


Authorisation

| Source | Supported | Notes | | --- | --- | --- | | GH_TOKEN | yes | read first. When it is the source, it is also forwarded to gh, so both sides act as the same account | | gh auth login | yes | used when GH_TOKEN is unset: the token comes from gh auth token | | GITHUB_TOKEN | no — deliberately unsupported | never read, and always stripped from the environment of any child process |

Why GITHUB_TOKEN is unsupported: it normally arrives from GitHub Actions as an installation token, CI is explicitly out of scope for this iteration, and the upload endpoint's behaviour under an installation token has never been observed. Silently picking up a credential whose behaviour against an irreversible endpoint is unknown is worse than reporting that no token was found. With no credential, the tool exits 2 token_not_found and says exactly what to do.

GITHUB_TOKEN is also stripped from gh's environment on every call, because gh auth token prints whatever GITHUB_TOKEN holds when it is set — leaving it in place would silently switch the identity out from under the check below.

Both sides must be the same account. Uploading under one identity and commenting under another would attribute a permanent attachment to an account the caller never named, so attach compares the two before anything is uploaded and exits 2 identity_mismatch with zero uploads on a mismatch. See ADR 013.

Stated precisely, because the check is weaker than it looks: both logins are currently read through gh, and GH_TOKEN is forwarded to gh whenever the token came from there — so the two are the same identity by construction and the comparison cannot presently fail. It documents the invariant rather than verifying it. Making it a real check means reading the identity with the upload token directly; that is recorded as outstanding in docs/release-readiness.md rather than left to be discovered.


Outcomes

Exit code alone is ambiguous on purpose; the pair (exitCode, reason) is the contract, and each pair carries its own nextAction.

| Exit | Means | | --- | --- | | 0 | done — or, for dry_run, a plan awaiting --confirm-plan | | 1 | the run failed with the remote state known (except internal_error, which is unknown) | | 2 | the call or the environment is wrong; nothing was uploaded | | 3 | no such pull request, issue or repository. Nothing was created, and nothing is guessed | | 4 | partial upload — some files landed. Safe to repeat only when reason is partial_upload_retryable | | 5 | the endpoint could not be spoken to, or answered unclassifiably. State unknown; never repeat | | 6 | public repository without --allow-public. Refused before any byte moved | | 7 | the plan token is missing or no longer matches |

The full pair-by-pair table, with the retry rules and what to say to a person for each one, is in skills/easy-cast/references/failures.md.


Not guaranteed

Stated in full, because each line is a way this tool can cost you something irreversible:

  • deletion of an uploaded asset — never, under any exit code;
  • that a human saw the file: --confirm-plan proves the plan matched, not consent;
  • that a plan was ever shown at all: computePlanToken is deterministic, so a caller can compute a valid token without ever running --dry-run. What is guaranteed is that execution matches the plan — not that the plan was demonstrated to anybody;
  • that the repository was inferred correctly relative to your intent (D2″/DG5): the repository is taken from the git remote of the directory the command ran in. Running from the wrong directory results in an irreversible upload into a different repository. The tool prints the resolved owner/repo and the basis of the inference on every inference, but it cannot prevent that output being ignored. To eliminate the class of error entirely, pass --repo explicitly;
  • idempotency under concurrent runs: the behaviour is pinned by a test, not fixed;
  • reuse across targets or across --key values; and upload does not deduplicate at all;
  • preservation of someone else's markdown: on the compose path the comment body is overwritten whole; on the ledger-only path preservation is incidental, not promised (D15′);
  • behaviour when several of the tool's own comments carry the same marker: the oldest is rewritten, the rest stay visible with stale URLs, and their ledgers are not read — on another machine that costs one irreversible re-upload;
  • the journal is bound to the directory the command ran in. It can be committed, and then travels with the repository (which is why .gitattributes sets merge=union on it) — but only commit it to a private repository. Stage 0 established that an attachment URL is a capability: anyone who can read a comment quoting it gets a working signed link to the bytes. A journal in a public repository therefore publishes every asset it lists, which is why this repository ignores its own. If it is not committed, then another machine, CI, a run from a different directory, or rm -rf .easy-cast all mean no deduplication — and, on a repeat, irreversible duplicates;
  • atomicity of O_APPEND on network filesystems (NFS, a networked $HOME) is not guaranteed — the test is green on a local filesystem only;
  • switching --no-convert on and off costs one irreversible upload;
  • eviction from a comment's ledger past 64 distinct files costs one irreversible upload (on the same machine this is softened by the local write-ahead journal);
  • the correctness of any endpoint behaviour Stage 0 did not measure — the open items are listed in docs/endpoint-semantics.md rather than left implicit.

One point is worth naming separately, because Stage 0 answered it and the answer is not the reassuring one. A private repository does not protect the file. The URL this tool prints never serves the image to anyone; GitHub substitutes a short-lived signed URL when it renders the comment, and that signed URL needs no credentials at all. An asset uploaded against the private probe repository and quoted in a public issue was fetched anonymously during the probe.

So access follows who can read the comment, not which repository the file was uploaded against. --allow-public still protects attach, but only because attach posts into the same repository it uploaded against, so the two audiences coincide — see ADR 022. A URL that leaves a private comment for a public one is public from that moment. Treat every uploaded frame as readable by a stranger.


Design decisions

The reasoning behind each of the above lives in docs/decisions/ — records 003 to 024, one per decision, with the alternatives that were considered and what each choice costs. The index is docs/decisions/README.md. Rather than restating them here: start with 003 for why the endpoint is quarantined, 010 for the plan handshake, 011 and 017 for deduplication and repair, 022 for what Stage 0 changed about the public-repository gate, and 023 for why upload records nothing, and 024 for why harvest never uploads.

Two further documents: docs/skill-derived-requirements.md records the requirements the skill imposed on the CLI surface and how each was closed or rejected, and docs/release-readiness.md records what has to be true before this is published — and why it is not publishable today.


Licence

MIT. See LICENSE.