npx skills add ...
npx skills add kochetkov-ma/claude-brewcode --skill brewpage-publish
Publish content to brewpage.app — text, markdown, JSON, file, or multi-file site. Asks namespace and password, returns public URL. Triggers: publish, share link, upload to brewpage, host page, brewpage, publish site, upload site, upload directory, deploy site, сделай публичную ссылку, опубликуй.
npx skills add kochetkov-ma/claude-brewcode --skill brewpage-publish
Publish content to brewpage.app — free instant hosting for HTML pages, JSON documents, files, and multi-file sites. No sign-up required.
Extract from $ARGUMENTS:
--ttl N → TTL in days (default: 15)--entry <filename> → entry file for SITE uploads (default: auto-detect)content_arg| Input | Type | API |
|---|---|---|
content_arg is a directory (test -d) | SITE | POST /api/sites (dir auto-zipped — primary path) |
content_arg ends with .zip AND file exists (test -f) | SITE | POST /api/sites (pre-built archive upload) |
content_arg is a file path AND file exists (test -f) | FILE | POST /api/files (multipart) |
content_arg starts with { or [ | JSON | POST /api/json |
| Anything else | HTML | POST /api/html (format=markdown) |
Mode rule: directory/ZIP → SITE. Single file → FILE. POST /api/sites accepts ONLY a multipart archive=@file.zip — there is no raw-folder upload, so a directory is auto-zipped on the fly (the robust default; archive sealing keeps relative paths intact). Stats per type — SITE (dir): HTML count, total size, entry file. SITE (ZIP): file size, entry override. FILE: size + MIME via file --mime-type -b. TEXT/JSON: char count.
For HTML/JSON/FILE:
For SITE: detect entry file using priority: 1) --entry flag, 2) index.html exists, 3) first .html file alphabetically.
Built-static guard (run BEFORE zipping). Publish BUILT output, never project sources:
.html file at all → FAIL with an explicit error: "No .html found — build the site first, then point at the build output directory." Do not guess an entry.package.json + src/ but no top-level .html) → warn and ask the user to point at the build output instead (dist/, build/, out/, _site/, or public/). Do not zip the source tree.Use AskUserQuestion:
Auto-suggest: generate a meaningful short slug (3-16 chars, lowercase alphanumeric + hyphens) from content context:
api-docs, login-page, report-q2)pricing, team-intro, changelog)user-config, metrics)Resolution:
2, 4, or empty → suggested slug1 → public3 or any other string → use as-isUse AskUserQuestion:
Generate random password EXECUTE using Bash tool:
Resolution:
1, 4, or empty → no password2 → use generated random password3 or custom text → use as-isSECURITY: The ownerToken MUST NEVER appear in conversation output. The bash blocks below handle curl + token parsing + history save atomically; the LLM sees only the URL. Each block sets
PASS_Hfirst (empty array when no password) and uses"${PASS_H[@]}"quoted — passwords are never string-interpolated into the command. The site-dir zip excludes (.git/,.env*, etc.) are also a secret-leak safeguard — they keep credentials and VCS data out of the published archive.
6a. Init history file (run once, before the publish block) — EXECUTE using Bash tool:
Then run ONE of the following publish blocks based on detected type. Each assumes HISTORY_FILE already exists from 6a.
HTML/Markdown text — EXECUTE using Bash tool:
JSON — EXECUTE using Bash tool:
File — EXECUTE using Bash tool:
Site (directory) — EXECUTE using Bash tool:
Site (ZIP file) — EXECUTE using Bash tool:
Success (bash printed OK {url}):
Success for SITE (bash printed OK {url} | Files: {count}):
For a private (non-public) namespace, append one short line after the link (skip if the reply must stay ultra-brief): Unlisted link — anyone who has it can open it, but it's not in the gallery or search. Want it discoverable? Publish to public.
NEVER print the ownerToken in conversation. The token lives only in the history file.
Error (bash printed FAILED: ...):
-F "file=@...".jq -n --arg c "$CONTENT" '{content: $c}' to safely encode text content. format is a query param, not a body field — /api/html ignores any format key inside the JSON body and reads only ?format= from the URL. Wrong location = server applies default html and stores your markdown as raw text.15 days. Namespace must be alphanumeric (3-32 chars), default: suggested (private) namespace..claude/brewpage-history.md and use the delete command shown in that file's header.PUT the new bundle to the same site URL (PUT /api/sites/{ns}/{id}) with your X-Owner-Token — the uploaded bundle fully replaces the file set (adds new files, removes absent ones, overwrites matching) and the link never changes. No DELETE-then-POST needed.POST /api/sites accepts), which seals relative paths. A pre-built .zip is the alternative input, uploaded as-is. Always publish BUILT output (dist/, build/, out/, _site/, public/), never project sources..git/, .env/.env.*, node_modules/, editor/OS junk, sourcemaps and logs — a deliberate secret-leak safeguard so credentials and VCS history never reach the public archive.--entry override > index.html > first .html alphabetically..link = "https://brewpage.app/public/<id>" without trailing /. Appending / routes to brewpage.app's own landing page; the JS redirect that rescues the no-slash form does NOT fire for the slash-dir form → site becomes inaccessible.curl. The no-slash URL serves the BrewPage landing HTML with an inline JS redirect that only executes in a real browser. Verify with Playwright / browser_navigate, or fetch <url>/index.html explicitly.| brewpage.app | Free instant hosting — HTML, JSON, files, sites, KV. No sign-up. |
| brewcode | Claude Code plugin suite — infinite tasks, code review, skills, hooks. |
Password protection (if set, page is hidden from gallery):
Options:
1) No password (default)
2) Random: {generated 6-char password, e.g. "kx7p2m"}
3) Enter custom password (min 4 chars)
4) Skip → no password
Reply with a number or your custom password.LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c6 2>/dev/nullHISTORY_FILE=".claude/brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
mkdir -p "$(dirname "$HISTORY_FILE")"
cat > "$HISTORY_FILE" <<'HEADER'
# brewpage.app — Published Pages
> PRIVATE FILE — keep this out of version control and never share it.
> Owner tokens allow delete and in-place republish (html/json/kv/sites all support PUT).
> Delete html/json/kv: `curl -s -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
> Delete site: `curl -s -X DELETE "https://brewpage.app/api/sites/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
> Update site (keep same URL): `PUT /api/sites/{ns}/{id}` with `X-Owner-Token: TOKEN` + the new bundle — the uploaded bundle fully replaces the file set (adds new files, removes absent ones, overwrites matching). The link never changes.
| Date | URL | Owner Token | TTL | Type |
|------|-----|-------------|-----|------|
HEADER
fiHISTORY_FILE=".claude/brewpage-history.md"
CONTENT=$(cat <<'BREWPAGE_EOF'
{content}
BREWPAGE_EOF
)
PAYLOAD=$(jq -n --arg c "$CONTENT" '{content: $c}')
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/html?ns={ns}&ttl={days}&format=markdown" \
-H "Content-Type: application/json" \
"${PASS_H[@]}" \
-d "$PAYLOAD")
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | html |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: $RESPONSE"
fiHISTORY_FILE=".claude/brewpage-history.md"
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/json?ns={ns}&ttl={days}" \
-H "Content-Type: application/json" \
"${PASS_H[@]}" \
-d '{original_json}')
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | json |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: $RESPONSE"
fiHISTORY_FILE=".claude/brewpage-history.md"
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/files?ns={ns}&ttl={days}" \
"${PASS_H[@]}" \
-F "file=@/absolute/path/to/file")
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | file |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: $RESPONSE"
fiHISTORY_FILE=".claude/brewpage-history.md"
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
TMPZIP=$(mktemp /tmp/brewpage-site-XXXXXX.zip)
# Exclude VCS, secrets, deps, editor + OS junk and sourcemaps — publish only built static assets.
(cd "{directory_path}" && zip -r "$TMPZIP" . -x '.git/*' '*/.git/*' '.env' '.env.*' '*/.env' '*/.env.*' 'node_modules/*' '*/node_modules/*' '.DS_Store' '*/.DS_Store' 'Thumbs.db' '.idea/*' '*/.idea/*' '.vscode/*' '*/.vscode/*' '.cache/*' '*/.cache/*' '*.map' '*.log')
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/sites?ns={ns}&ttl={days}&entry={entry}" \
-H "User-Agent: ClaudeCode/1.0" \
"${PASS_H[@]}" \
-F "archive=@$TMPZIP")
rm -f "$TMPZIP"
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
URL="${URL%/}" # strip any trailing slash — /public/<id>/ routes to brewpage landing
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
FCOUNT=$(echo "$RESPONSE" | jq -r '.fileCount // "?"')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | site ($FCOUNT files) |" >> "$HISTORY_FILE"
echo "OK $URL | Files: $FCOUNT"
else
echo "FAILED: $RESPONSE"
fiHISTORY_FILE=".claude/brewpage-history.md"
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/sites?ns={ns}&ttl={days}&entry={entry}" \
-H "User-Agent: ClaudeCode/1.0" \
"${PASS_H[@]}" \
-F "archive=@{zip_file_path}")
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
URL="${URL%/}" # strip any trailing slash — /public/<id>/ routes to brewpage landing
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
FCOUNT=$(echo "$RESPONSE" | jq -r '.fileCount // "?"')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | site ($FCOUNT files) |" >> "$HISTORY_FILE"
echo "OK $URL | Files: $FCOUNT"
else
echo "FAILED: $RESPONSE"
fiPublished: {url from bash output}
Owner token saved to .claude/brewpage-history.mdPublished site: {url from bash output}
Entry: {entry_file} | Files: {count}
Owner token saved to .claude/brewpage-history.md
⚠ Share the URL exactly as printed — DO NOT append a trailing slash.
brewpage.app routes "/public/<id>/" to its own landing page, and the
redirect that saves the no-slash form does not fire for the slash-dir form.Publish failed.