Agentpub publish
agentpub agent skills — publish, review, blueprints, and onboarding for agentpub.io
npx -y skills add agentpub-io/skill --skill agentpub-publishAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when asked to publish, host, deploy, or share an HTML page, report, dashboard, visualization, or static files to a live URL, or when agentpub is mentioned. Static sites only (no server-side compute).
SKILL.md
14.3 KB, as published. Nobody here has run it
agentpub publishing
agentpub turns local HTML/static files into a live URL via a three-step API: create → upload → finalize. Base: https://agentpub.io.
When to use / when NOT
Use to put static HTML, CSS, JS, images, PDFs, or other static assets online fast. Do NOT use for server-side compute, databases, or dynamic backends — served sites are static only.
Step 0 — resolve a key and authenticate by default (DO THIS FIRST)
The single most common mistake is publishing anonymously by accident — that creates a throwaway 24-hour site and forces a separate claim. Before the three calls, resolve a key and decide ownership:
- Resolve a key, first match wins: (1)
AGENTPUB_API_KEYenv var; (2)~/.config/agentpub/credentials(file mode0600). - If a key resolves → publish authenticated (send
Authorization: Bearer <key>on the create call). The site is owned at creation: permanent, in the dashboard immediately, no per-page claim, and share-safe (no claim link a recipient could hijack). - If no key resolves: if the user wants to keep the work, acquire one once (see "First use" below) and publish authenticated. Only publish anonymous for a deliberate zero-signup throwaway/trial.
- Anonymous is an explicit choice, never a fallback. Don't quietly create a 24h site because no key resolved — if the user wants to keep the work, acquire a key. Only go anonymous when the user explicitly asked for a throwaway. For durable/automated workflows set
AGENTPUB_REQUIRE_AUTH=1to forbid anonymous entirely (belt-and-suspenders).
Shortcut: the bundled agentpub.sh encodes all of this. ./agentpub.sh publish ./dir publishes owned when a key exists and hard-stops when none does (telling you to login or pass --anonymous) — it never creates a silent/accidental anonymous site. --anonymous is the explicit throwaway; AGENTPUB_REQUIRE_AUTH=1 forbids anonymous even then. MCP-host agents (Claude, Cursor) can instead use the agentpub.io/mcp endpoint, where the host manages auth.
The three calls
- Create — declare each file with its exact byte size. Authenticated by default:
curl -sX POST https://agentpub.io/api/v1/publish \
-H "authorization: Bearer $AGENTPUB_API_KEY" \
-H 'content-type: application/json' \
-d '{"files":[{"path":"index.html","size":1234,"contentType":"text/html"}]}'
Returns upload.versionId, upload.uploads[] (each {path, method:"PUT", url}), upload.finalizeUrl, and the ownership state authenticated/anonymous/expiresAt. Omit the authorization header only for a deliberate anonymous site — anonymous responses also include claimToken, claimUrl, warning.
- Upload — PUT each file's bytes to its presigned
url, sending content-type:
curl -sX PUT '<upload.uploads[].url>' \
-H 'content-type: text/html' --data-binary @index.html
- Finalize — flip it live:
curl -sX POST '<upload.finalizeUrl>' \
-H 'content-type: application/json' \
-d '{"versionId":"<upload.versionId>"}'
Live at https://{slug}.agentpub.io/.
Name the site for humans. Pass "artifact":{"title":"…","description":"…"} on the create body so the owner's dashboard and list_my_sites show a meaningful label (not just the slug) and it's searchable. Omitted? The title auto-derives from the page's <title> (or first <h1>) and the description from <meta name="description"> — so ship a good <title>/meta-description either way.
Name it so you can update it in place (stable addressing)
Pass a name on the create body — an account-scoped handle YOU choose (e.g. "name":"ads-daily"):
curl -sX POST https://agentpub.io/api/v1/publish \
-H "authorization: Bearer $AGENTPUB_API_KEY" -H 'content-type: application/json' \
-d '{"name":"ads-daily","files":[{"path":"index.html","size":1234,"contentType":"text/html"}]}'
- Re-publishing with the same
nameupdates that site in place — you never have to discover, store, or guess the random slug. This is the right way to run any "updates daily/in place" routine: re-derive yournamefrom context each run, no out-of-band slug tracking. - Resolve a name → slug anytime:
GET /api/v1/publish/by-name/{name}(Bearer) →{slug, name}.list_my_sitesalso shows each site'sname. nameis lowercase letters/digits/hyphens, unique per account; doing a fresh create with a name you already own returns409(publish-by-name to update it instead).name≠title:nameis the stable machine handle you address by;title(inartifact) is the human display label. The public URL stays a random{slug}.agentpub.io(random by design — not enumerable);nameis how you find it, not the public link.
Confirm ownership after finalize (surface it loudly)
Every publish response carries the ownership state — check it and report it to the user so an accidental anonymous site is caught immediately:
authenticated: true
anonymous: false
expiresAt: null
If you see authenticated: false / anonymous: true / a non-null expiresAt when the user wanted to keep the site, you published the unsafe path — acquire a key and republish (or claim via the returned claimUrl).
First use — acquire and persist a key once (no browser)
Preferred for headless agents: device pairing. Run ./agentpub.sh pair (or use the /api/v1/pair/* endpoints directly): the agent starts a pairing, the human approves in their browser, and the key is delivered out-of-band on poll — so neither the user code nor the API key ever passes through chat. The deviceSecret stays with the agent and agentpub.sh pair persists the key to ~/.config/agentpub/credentials (0600). For headless integration, run agentpub.sh pair --json — machine-readable events go to stdout (start / poll pending|slow_down / approved with keyId/keyName, never the key), human text to stderr; exit codes: 0 ok, 2 denied, 3 expired, 4 timed out. The email-code flow below remains the fallback when a browser approve link is impractical.
When no key resolves and the user wants to keep their work:
POST /api/auth/agent/request-code{"email":"[email protected]"}→ a 6-digit code is emailed.- User reads the code back to you →
POST /api/auth/agent/verify-code{"email":"[email protected]","code":"482913"}→ returns{"apiKey":"...","accountCreated":true|false}. - Persist it so you never claim again:
mkdir -p ~/.config/agentpub && umask 177
printf '%s\n' "$APIKEY" > ~/.config/agentpub/credentials
chmod 600 ~/.config/agentpub/credentials
- Name the key for the tool holding it:
POST /api/v1/keys{"name":"claude"}(useclaude,cursor,hermes, …) — each tool keeps its own revocable key on the one account (least privilege; revoke one without breaking the others). - Never echo, log, commit, or paste the key into chat history, code, or shared docs — only the
0600file or env. Treat it like a password.
Anonymous (only when explicitly intended)
- No auth → 24h site + a one-time
claimToken/claimUrl. SURFACE theclaimUrlto the user IMMEDIATELY and prominently — it is shown only once and is the only way to keep the site past 24h. Never log or paste theclaimTokenanywhere else.
Updating — full replace vs patch
Address an existing site by slug (or resolve it from your name). Two ways to update:
- Full replace —
PUT /api/v1/publish/{slug}(same body shape as create). Authorize with the ownerBearer(or{"claimToken":"..."}for anonymous). Send a per-filehash(sha256 lowercase hex —shasum -a 256 file) so unchanged files skip upload (returned underupload.carried); only changed files get presigned URLs. Then finalize. Sendhashon every publish incl. the first, or dedup can't compare. - Patch (overlay just some files) —
POST /api/v1/publish/{slug}/patchwith only the files you want to change (same{files:[...]}shape; Bearer or{"claimToken"}). Unmentioned files are kept (carried forward byte-identical); patch never deletes. Then finalize. This is the cheap path for incremental updates — e.g. swapping onedata.jsonwithout resending the rest of the site. MCP:patch_site.
Recurring reports / dashboards that update in place daily
The robust pattern for a page a scheduled agent refreshes (daily report, live dashboard):
- Publish once with a
nameand a staticindex.htmlwhose markup never changes — itfetch('data.json')s its numbers at load and renders them client-side. - Each run, patch only the data:
POST /api/v1/publish/{slug}/patchwith just[{"path":"data.json",...}], then finalize. The design stays byte-identical every day (no drift) and the payload is a few KB. - Read back to compute deltas vs the last run:
GET /api/v1/publish/{slug}/content?path=data.json(Bearer) → current file contents. MCP:get_site_content.
Why this matters: if a model re-emits a large HTML document each run it will drift (spacing, layout, bar heights wobble) — models reconstruct markup rather than reproduce it byte-for-byte. Static design + data-only patch eliminates that. (Generating the HTML with deterministic code also avoids drift — but patching only the data is still cheaper and is what get_site_content + patch_site are built for.)
Tips: agentpub is static hosting only — "live daily numbers" need an external scheduler (your cron/agent runner) calling patch+finalize. And don't bake the page's own public URL into the markup (you don't know the slug until after create) — read location.href client-side instead.
Quick reference
| Action | Call |
|---|---|
| Site status | GET /api/v1/sites/{slug} |
| List my sites | GET /api/v1/sites (Bearer) — includes each site's name + versionNumber |
| Resolve a name | GET /api/v1/publish/by-name/{name} (Bearer) → {slug, name} |
| Patch (overlay) | POST /api/v1/publish/{slug}/patch — change some files, keep the rest — MCP patch_site |
| Read content | GET /api/v1/publish/{slug}/content[?path=&version=] (Bearer) — MCP get_site_content |
| Versions | GET /api/v1/publish/{slug}/versions — each has a vN number — (Bearer or ?claimToken=) |
| Rollback | POST /api/v1/publish/{slug}/rollback {"version":"v2"} or {"versionId":"..."} |
| Delete | DELETE /api/v1/publish/{slug} (Bearer, or {"claimToken":"..."}) |
| List keys | GET /api/v1/keys (Bearer) |
| Mint named key | POST /api/v1/keys {"name":"cursor"} (Bearer) |
| Revoke key | DELETE /api/v1/keys/{id} (Bearer) |
| Enable review | POST /api/v1/publish/{slug}/review {"enabled":true} (Bearer) — MCP enable_review |
| Get feedback | GET /api/v1/sites/{slug}/comments → {slug, approved, comments[]} — MCP get_feedback |
| Mark addressed | POST /api/v1/sites/{slug}/comments/{id}/addressed (Bearer) — MCP mark_addressed |
Enabling review mode turns on a feedback widget on the served page where reviewers leave page-level comments and approve. Comments can target a specific element via an anchor {selector, tag, text}, surfaced by get_feedback so the agent can locate and edit that exact element.
Applying review feedback
Run this loop whenever asked to "apply the agentpub comments for {slug}":
-
Fetch comments — MCP
get_feedback(slug)orGET /api/v1/sites/{slug}/comments(Bearer). Returns{slug, approved, approvedVersionId, comments[]}. Each comment hasstatus("open"|"addressed"),body(the requested change), andanchor: {selector, tag, text} | null. -
Filter — work only on comments where
status === "open". -
Fetch the live page source —
GET https://{slug}.agentpub.io/(or whichever file the comment targets). Strip the review widget before editing: the served HTML of a review-enabled site has a block injected immediately before</body>that begins with<button id="apb-approve-top". Remove everything from that<button>to and including</body>, then append a clean</body>— do NOT bake the widget back into the published source. -
Apply each change — for comments with an anchor, locate the element by matching
tag+textfirst; useselectorto disambiguate when multiple elements match. For anchorless comments, apply the change to the page as a whole. Edit the in-memory source. -
Republish — standard three-call flow against
PUT /api/v1/publish/{slug}(Bearer$AGENTPUB_API_KEY): declare files with exact byte sizes and sha256 hashes → upload changed files → finalize. -
Mark addressed — for each comment you handled: MCP
mark_addressed(slug, commentId)orPOST /api/v1/sites/{slug}/comments/{commentId}/addressed(Bearer). Call after a successful finalize.
Gotchas
sizeis validated for format and limits at create (must be an integer, ≤ 25 MB), but a mismatch between declared and actual bytes is not enforced at upload — still declare exact bytes (wc -c < file) because the manifest is recorded metadata.- Send the
content-typeon the upload PUT — R2 records it and serves it back. - Finalize
409means a declared file didn't upload — re-PUT that file, then re-finalize. 429→ respect theRetry-Afterheader before retrying.- Anonymous sites show a claim badge; claiming (via
claimUrl) removes it.
Full reference: https://agentpub.io/llms.txt and https://agentpub.io/openapi.json.