feat(skills): add OpenClaw/AgentSkills brewpage-publish variant; bring standalone to parity

- openclaw/brewpage-publish: AgentSkills-format SKILL.md (name/description/homepage/user-invocable only; drops Claude-only model/argument-hint/allowed-tools). Supports HTML, markdown, files, multi-file sites (no JSON/KV). Directory auto-zip is primary with junk/secret excludes (.git/.env/node_modules/...) + built-static guard.
- skills/brewpage-publish: brought to parity with brewdoc max version (sites dir+zip, PASS_H password array, archive filters, guard); kept JSON.
- README + brewdoc/README: link both portable variants.

Security: ownerToken never echoed (history-file only); password via quoted PASS_H array; .env/.git excluded from site archives.
This commit is contained in:
kochetkov-ma
2026-05-31 17:48:14 +01:00
parent 4beac38dc9
commit 333bf7c064
6 changed files with 547 additions and 89 deletions
+9
View File
@@ -239,6 +239,15 @@ After `/brewcode:setup`, each task follows the cycle: `spec` -> `plan` -> `start
| `/brewui:image-gen` | AI image generation via 5 providers with anti-slop controls |
| `/brewui:glm-design-to-code` | GLM vision design-to-code: image/text/HTML/URL to multi-framework code |
### Portable skills (standalone)
Self-contained `SKILL.md` folders that ship outside the four plugins -- drop them into any compatible agent runtime. Both publish content to [brewpage.app](https://brewpage.app) (text, markdown, files, multi-file sites) and return a public URL.
| Skill | Runtime | Path |
|-------|---------|------|
| `brewpage-publish` | Claude Code | [`skills/brewpage-publish`](skills/brewpage-publish/) |
| `brewpage-publish` | OpenClaw / AgentSkills | [`openclaw/brewpage-publish`](openclaw/brewpage-publish/) |
## Agents (16 total)
| Agent | Model | Purpose |
+2
View File
@@ -85,6 +85,8 @@ claude --plugin-dir ./brewdoc
| [`/brewdoc:publish`](skills/publish/README.md) | Publish to brewpage.app -- returns public URL | haiku | `<text\|file\|json> [--ttl N]` |
| [`/brewdoc:guide`](skills/guide/README.md) | Interactive tutorial for the plugin suite | haiku | `[topic]` |
> Need a portable, plugin-free version? See the standalone [`brewpage-publish`](../skills/brewpage-publish/) (Claude Code) and [`openclaw/brewpage-publish`](../openclaw/brewpage-publish/) (OpenClaw / AgentSkills) skills.
## Agent
| Agent | Model | Purpose |
+94
View File
@@ -0,0 +1,94 @@
# brewpage-publish (OpenClaw skill)
Publish text, markdown, any file, or a whole multi-file site to [brewpage.app](https://brewpage.app) — get a public URL instantly. No sign-up.
This is an [AgentSkills](https://docs.openclaw.ai/tools/skills)-standard skill for **OpenClaw**.
## What It Does
1. **Detects content type** — a directory/ZIP becomes a multi-file site, a single file becomes a file upload, everything else becomes HTML (markdown rendered)
2. **Asks namespace** — interactive prompt for a short, human-readable namespace (URL slug)
3. **Asks password** — optional password protection (hides the page from the gallery)
4. **Calls API** — sends content to brewpage.app
5. **Returns URL** — public link ready to share
6. **Saves token** — owner token written to `./brewpage-history.md`
## API Coverage
| Content | Type | Endpoint |
|---------|------|----------|
| Text / markdown | HTML | `POST /api/html?format=markdown` |
| Local file | File | `POST /api/files` |
| Directory (built static) | Site | `POST /api/sites` (auto-zipped — primary path) |
| `.zip` archive | Site | `POST /api/sites` (pre-built) |
## Install / Placement
OpenClaw discovers skills from two locations. Drop the `brewpage-publish/` folder (containing `SKILL.md`) into either:
```
<workspace>/skills/brewpage-publish/SKILL.md # project-local skill
~/.openclaw/skills/brewpage-publish/SKILL.md # user-global skill
```
The folder/`name` (`brewpage-publish`) drives the slash command and the allowlist key. The name uses lowercase + a hyphen — colon/uppercase forms like `BrewPage::publish` are **invalid** in OpenClaw.
## How to Invoke
Via slash command:
```
/brewpage-publish "Hello, world!"
/brewpage-publish report.md
/brewpage-publish screenshot.png --ttl 1
/brewpage-publish ./dist --entry index.html
/brewpage-publish site.zip
```
Or via natural language:
```
Publish this to brewpage
Upload report.md to brewpage.app
Deploy this directory as a site
```
The model detects the content type, asks for a namespace and password, calls the brewpage.app API, and returns the public URL.
## Sites
`POST /api/sites` accepts **only a `.zip` archive** — there is no raw-folder upload.
- **Directory (primary):** point at a **built** static directory; the skill auto-zips it and uploads. Archive sealing keeps relative paths intact.
- **Pre-built `.zip` (alternative):** uploaded as-is.
The auto-zip **excludes** `.git/`, `.env`/`.env.*`, `node_modules/`, `.DS_Store`, `Thumbs.db`, `.idea/`, `.vscode/`, `.cache/`, `*.map` and `*.log` — only real built assets ship; secrets and VCS data never leak into the public archive.
**Built-static guard:** publish build output, not sources. No `.html` in the directory → the skill fails and tells you to build first. A source tree (`package.json` + `src/`, no top-level `.html`) → the skill asks you to point at the build output (`dist/`, `build/`, `out/`, `_site/`, `public/`).
Entry file: `--entry` override > `index.html` > first `.html` alphabetically.
**No trailing slash.** The API returns `https://brewpage.app/public/<id>` without a trailing `/`. Appending `/` routes to brewpage.app's landing page and breaks the link. Site URLs cannot be verified with plain `curl` — verify with a real browser or fetch `<url>/index.html`.
## TTL
Default time-to-live is **15 days** (max 30). Override with `--ttl N` (days).
## Owner Token & Privacy
The owner token is **never printed in conversation** — the skill's shell blocks curl the API, parse the token, and append it to `./brewpage-history.md` directly. Only the public URL is shown.
`./brewpage-history.md` is a **private file** (keep it out of version control). Use the token to delete a page:
```bash
# html / json / kv
curl -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: {token}"
# site
curl -X DELETE "https://brewpage.app/api/sites/{ns}/{id}" -H "X-Owner-Token: {token}"
```
## Requirements
`curl`, `jq`, and `zip` available on the host shell.
## License
MIT
+272
View File
@@ -0,0 +1,272 @@
---
name: brewpage-publish
description: "Publish content to brewpage.app — text, markdown, any 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, сделай публичную ссылку, опубликуй."
homepage: https://brewpage.app
user-invocable: true
---
# brewpage-publish
Publish content to **brewpage.app** — free instant hosting for HTML pages, files, and multi-file sites. No sign-up required.
## Workflow
### Step 1: Parse Arguments
Extract from the arguments string:
- `--ttl N` → TTL in days (default: `15`)
- `--entry <filename>` → entry file for SITE uploads (default: auto-detect)
- Remaining text → `content_arg`
### Step 2: Detect Content Type
| 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) |
| Anything else | HTML | `POST /api/html` (format=markdown) |
Mode rule: directory/ZIP → SITE. Single file → FILE. Everything else → HTML (markdown). `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: char count.
### Step 3: Show Pre-Publish Stats
For HTML/FILE:
```
Content: <type description> · <size> · <api endpoint>
TTL: <N> days
```
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:
- If the directory contains no `.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.
- If the directory looks like un-built sources (has `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.
```
Content: site · <N> files · <total_size> · POST /api/sites
Entry: <entry_file>
TTL: <N> days
```
### Step 4: Ask Namespace
Ask the user:
```
Namespace determines the URL prefix and gallery visibility on brewpage.app.
Options:
1) public — visible in gallery (default)
2) {auto-suggested 6-8 char slug}
3) Enter custom namespace
4) Skip → use public
Reply with a number or your custom namespace (alphanumeric, 3-32 chars).
```
Auto-suggest: generate a **meaningful short slug** (3-16 chars, lowercase alphanumeric + hyphens) from content context:
- File → topic/purpose of the file (e.g. `api-docs`, `login-page`, `report-q2`)
- Text/HTML → main subject or title (e.g. `pricing`, `team-intro`, `changelog`)
- Site → site title or directory name (e.g. `portfolio`, `docs-site`)
- Fallback → project name or directory name if content is ambiguous
Never use random strings or truncated filenames — the slug should be human-readable and describe what's being published.
Resolution:
- `1`, `4`, or empty → `public`
- `2` → suggested slug
- `3` or any other string → use as-is
### Step 5: Ask Password
Ask the user:
```
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.
```
Generate random password — run with the shell tool:
```bash
LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c6 2>/dev/null
```
Resolution:
- `1`, `4`, or empty → no password
- `2` → use generated random password
- `3` or custom text → use as-is
### Step 6: Publish and Save Token (secure)
> **SECURITY:** The ownerToken MUST NEVER appear in conversation output. The bash blocks below handle curl + token parsing + history save atomically; the model sees only the URL. Each block sets `PASS_H` first (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.
History file is workspace-relative: `./brewpage-history.md`.
**6a. Init history file (run once, before the publish block)** — run with the shell tool:
```bash
HISTORY_FILE="./brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
cat > "$HISTORY_FILE" <<'HEADER'
# brewpage.app — Published Pages
> PRIVATE FILE — keep this out of version control and never share it.
> Owner tokens allow delete (no in-place PUT for sites; html/json/kv 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"`
| Date | URL | Owner Token | TTL | Type |
|------|-----|-------------|-----|------|
HEADER
fi
```
Then run ONE of the following publish blocks based on detected type. Each assumes `HISTORY_FILE` already exists from 6a.
**HTML/Markdown text** — run with the shell tool:
```bash
HISTORY_FILE="./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"
fi
```
**File** — run with the shell tool:
```bash
HISTORY_FILE="./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"
fi
```
**Site (directory)** — run with the shell tool:
```bash
HISTORY_FILE="./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: OpenClaw/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"
fi
```
**Site (ZIP file)** — run with the shell tool:
```bash
HISTORY_FILE="./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: OpenClaw/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"
fi
```
### Step 7: Output Result
**Success** (bash printed `OK {url}`):
```
Published: {url from bash output}
Owner token saved to ./brewpage-history.md
```
**Success for SITE** (bash printed `OK {url} | Files: {count}`):
```
Published site: {url from bash output}
Entry: {entry_file} | Files: {count}
Owner token saved to ./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.
```
**NEVER print the ownerToken in conversation.** The token lives only in the history file.
**Error** (bash printed `FAILED: ...`):
```
Publish failed.
```
## Notes
- Always use absolute file paths with curl `-F "file=@..."`.
- Use `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.
- TTL default is `15` days. Namespace must be alphanumeric (3-32 chars), default `public`.
- To **delete** a published page, find the owner token in `./brewpage-history.md` and use the delete command shown in that file's header.
- **Sites: directory is the primary input** — it is auto-zipped (the only thing `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.
- The auto-zip excludes `.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 file detection: `--entry` override > `index.html` > first `.html` alphabetically.
- **SITE URL — NO trailing slash.** API returns `.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.
- **SITE verification cannot be done via `curl`.** The no-slash URL serves the BrewPage landing HTML with an inline JS redirect that only executes in a real browser. Verify with a real browser, or fetch `<url>/index.html` explicitly.
---
## Powered by
| | |
|-|-|
| **[brewpage.app](https://brewpage.app)** | Free instant hosting — HTML, files, multi-file sites. No sign-up. |
| **[brewcode](https://github.com/kochetkov-ma/claude-brewcode)** | Plugin & skill suite — infinite tasks, code review, skills, hooks. |
+41 -24
View File
@@ -1,6 +1,6 @@
# Brewpage
# Brewpage Publish
Publish text, markdown, JSON, or files to [brewpage.app](https://brewpage.app) — get a public URL instantly. No sign-up.
Publish text, markdown, JSON, files, or whole multi-file sites to [brewpage.app](https://brewpage.app) — get a public URL instantly. No sign-up.
## Quick Start
@@ -11,25 +11,28 @@ Publish text, markdown, JSON, or files to [brewpage.app](https://brewpage.app)
2. Use via slash command:
```
/brewpage "Hello, world!"
/brewpage report.md
/brewpage '{"status": "ok"}'
/brewpage screenshot.png --ttl 1
/brewpage-publish "Hello, world!"
/brewpage-publish report.md
/brewpage-publish '{"status": "ok"}'
/brewpage-publish screenshot.png --ttl 1
/brewpage-publish ./my-site --entry index.html
/brewpage-publish site.zip
```
Or via natural language prompt:
Or via natural language:
```
Publish this to brewpage
Upload report.md to brewpage.app
Deploy this directory as a site
```
Claude detects the content type, asks for a namespace and password interactively, calls the brewpage.app API, and returns a public URL. The owner token is saved to `.claude/brewpage-history.md` for later deletion.
## What It Does
1. **Detects content type** — text/markdown becomes HTML, objects/arrays become JSON, file paths become file uploads
2. **Asks namespace** — interactive prompt for a short namespace (URL slug)
3. **Asks password** — optional password protection for the published page
1. **Detects content type** — directory/ZIP becomes a multi-file site, single file becomes a file upload, objects/arrays become JSON, everything else becomes HTML (markdown rendered)
2. **Asks namespace** — interactive prompt for a short, human-readable namespace (URL slug)
3. **Asks password** — optional password protection (hides the page from the gallery)
4. **Calls API** — sends content to brewpage.app
5. **Returns URL** — public link ready to share
6. **Saves token** — owner token written to `.claude/brewpage-history.md`
@@ -38,38 +41,52 @@ Claude detects the content type, asks for a namespace and password interactively
| Content | Type | Endpoint |
|---------|------|----------|
| Text / markdown | HTML | `POST /api/html` |
| Text / markdown | HTML | `POST /api/html?format=markdown` |
| JSON object/array | JSON | `POST /api/json` |
| Local file | File | `POST /api/files` |
| Directory | Site | `POST /api/sites` (zipped on the fly) |
| `.zip` archive | Site | `POST /api/sites` |
## Sites
`POST /api/sites` accepts **only a `.zip` archive** — there is no raw-folder upload.
- **Directory (primary):** point at a **built** static directory; the skill auto-zips it and uploads. Archive sealing keeps relative paths intact.
- **Pre-built `.zip` (alternative):** uploaded as-is.
The auto-zip **excludes** `.git/`, `.env`/`.env.*`, `node_modules/`, `.DS_Store`, `Thumbs.db`, `.idea/`, `.vscode/`, `.cache/`, `*.map` and `*.log` — only real built assets ship; secrets and VCS data never leak into the public archive.
**Built-static guard:** publish build output, not sources. No `.html` in the directory → the skill fails and tells you to build first. A source tree (`package.json` + `src/`, no top-level `.html`) → the skill asks you to point at the build output (`dist/`, `build/`, `out/`, `_site/`, `public/`).
Entry file: `--entry` override > `index.html` > first `.html` alphabetically.
**No trailing slash.** The API returns `https://brewpage.app/public/<id>` without a trailing `/`. Appending `/` routes to brewpage.app's landing page and breaks the link. Share the URL exactly as printed. Site URLs cannot be verified with plain `curl` (the no-slash URL serves an inline JS redirect that only runs in a browser) — verify with a real browser or fetch `<url>/index.html`.
## TTL
Default time-to-live is **5 days**. Override with `--ttl N` (days):
Default time-to-live is **15 days** (max 30). Override with `--ttl N` (days):
```
/brewpage report.md --ttl 1
/brewpage '{"data": [1,2,3]}' --ttl 30
/brewpage-publish report.md --ttl 1
/brewpage-publish '{"data": [1,2,3]}' --ttl 30
```
## Owner Token
## Owner Token & Privacy
Every publish saves an entry to `.claude/brewpage-history.md`:
The owner token is **never printed in conversation** — the skill's bash blocks curl the API, parse the token, and append it to `.claude/brewpage-history.md` directly. Only the public URL is shown.
```markdown
| Namespace | ID | URL | Token | Created |
|-----------|-----|-----|-------|---------|
| my-ns | abc123 | https://brewpage.app/my-ns/abc123 | tok_... | 2026-03-31 |
```
Use the token to delete a page:
`.claude/brewpage-history.md` is a **private file** (keep it out of version control). Use the token to delete a page:
```bash
# html / json / kv
curl -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: {token}"
# site
curl -X DELETE "https://brewpage.app/api/sites/{ns}/{id}" -H "X-Owner-Token: {token}"
```
## Part of Brewcode
This skill is extracted from [brewcode](https://github.com/kochetkov-ma/claude-brewcode) — a development platform for Claude Code with infinite focus tasks, 14 agents, quorum reviews, and knowledge persistence.
This skill is part of [brewcode](https://github.com/kochetkov-ma/claude-brewcode) — a development platform for Claude Code with infinite focus tasks, agents, quorum reviews, and knowledge persistence.
```bash
claude plugin marketplace add https://github.com/kochetkov-ma/claude-brewcode
+129 -65
View File
@@ -1,15 +1,15 @@
---
name: brewpage-publish
description: "Publish content to brewpage.app — text, markdown, JSON, or file. Asks namespace and password, returns public URL. Triggers: publish, share link, upload to brewpage, host page, brewpage."
argument-hint: "<text|file_path|json> [--ttl N]"
description: "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, сделай публичную ссылку, опубликуй."
argument-hint: "<text|file_path|directory_path|zip_path> [--ttl N] [--entry filename]"
user-invocable: true
allowed-tools: Read, Bash, AskUserQuestion, Glob
model: haiku
---
# brewpage
# brewpage-publish
Publish content to **brewpage.app** — free instant hosting for HTML pages, JSON documents, and files. No sign-up required.
Publish content to **brewpage.app** — free instant hosting for HTML pages, JSON documents, files, and multi-file sites. No sign-up required.
## Workflow
@@ -17,24 +17,38 @@ Publish content to **brewpage.app** — free instant hosting for HTML pages, JSO
Extract from `$ARGUMENTS`:
- `--ttl N` → TTL in days (default: `15`)
- `--entry <filename>` → entry file for SITE uploads (default: auto-detect)
- Remaining text → `content_arg`
### Step 2: Detect Content Type
| Input | Type | API |
|-------|------|-----|
| `content_arg` is a path AND file exists (`test -f`) | FILE | `POST /api/files` (multipart) |
| `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) |
For FILE: get file size and MIME type via Bash (`file --mime-type -b`).
For TEXT/JSON: count characters.
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.
### Step 3: Show Pre-Publish Stats
For HTML/JSON/FILE:
```
📊 Content: <type description> · <size> · <api endpoint>
TTL: <N> days
Content: <type description> · <size> · <api endpoint>
TTL: <N> days
```
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:
- If the directory contains no `.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.
- If the directory looks like un-built sources (has `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.
```
Content: site · <N> files · <total_size> · POST /api/sites
Entry: <entry_file>
TTL: <N> days
```
### Step 4: Ask Namespace
@@ -93,9 +107,9 @@ Resolution:
### Step 6: Publish and Save Token (secure)
> **SECURITY:** The ownerToken MUST never appear in conversation output. The bash block below handles curl, token parsing, and history saving atomically. The LLM only sees the URL.
> **SECURITY:** 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_H` first (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.
**HTML/Markdown text****EXECUTE** using Bash tool:
**6a. Init history file (run once, before the publish block)****EXECUTE** using Bash tool:
```bash
HISTORY_FILE=".claude/brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
@@ -103,123 +117,173 @@ if [ ! -f "$HISTORY_FILE" ]; then
cat > "$HISTORY_FILE" <<'HEADER'
# brewpage.app — Published Pages
> Owner tokens allow update/delete. Keep this file private.
> Delete: `curl -s -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
> PRIVATE FILE — keep this out of version control and never share it.
> Owner tokens allow delete (no in-place PUT for sites; html/json/kv 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"`
| Date | URL | Owner Token | TTL |
|------|-----|-------------|-----|
| Date | URL | Owner Token | TTL | Type |
|------|-----|-------------|-----|------|
HEADER
fi
```
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:
```bash
HISTORY_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" \
{password_header} \
"${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 |" >> "$HISTORY_FILE"
echo " $URL"
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | html |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: $RESPONSE"
echo "FAILED: $RESPONSE"
fi
```
**JSON****EXECUTE** using Bash tool:
```bash
HISTORY_FILE=".claude/brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
mkdir -p "$(dirname "$HISTORY_FILE")"
cat > "$HISTORY_FILE" <<'HEADER'
# brewpage.app — Published Pages
> Owner tokens allow update/delete. Keep this file private.
> Delete: `curl -s -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
| Date | URL | Owner Token | TTL |
|------|-----|-------------|-----|
HEADER
fi
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" \
{password_header} \
"${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 |" >> "$HISTORY_FILE"
echo " $URL"
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | json |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: $RESPONSE"
echo "FAILED: $RESPONSE"
fi
```
**File****EXECUTE** using Bash tool:
```bash
HISTORY_FILE=".claude/brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
mkdir -p "$(dirname "$HISTORY_FILE")"
cat > "$HISTORY_FILE" <<'HEADER'
# brewpage.app — Published Pages
> Owner tokens allow update/delete. Keep this file private.
> Delete: `curl -s -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
| Date | URL | Owner Token | TTL |
|------|-----|-------------|-----|
HEADER
fi
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/files?ns={ns}&ttl={days}" \
{password_header} \
"${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 |" >> "$HISTORY_FILE"
echo " $URL"
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | file |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: $RESPONSE"
echo "FAILED: $RESPONSE"
fi
```
Replace `{password_header}` with `-H "X-Password: {pass}"` only when password was set; otherwise remove it entirely.
**Site (directory)****EXECUTE** using Bash tool:
```bash
HISTORY_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"
fi
```
**Site (ZIP file)****EXECUTE** using Bash tool:
```bash
HISTORY_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"
fi
```
### Step 7: Output Result
**Success** (bash printed ` {url}`):
**Success** (bash printed `OK {url}`):
```
Published!
🔗 {url from bash output}
📁 Owner token saved to .claude/brewpage-history.md
Published: {url from bash output}
Owner token saved to .claude/brewpage-history.md
```
**NEVER print ownerToken in conversation.** The token is only in the history file.
**Error** (bash printed `❌ FAILED: ...`):
**Success for SITE** (bash printed `OK {url} | Files: {count}`):
```
Publish failed.
Published 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.
```
**NEVER print the ownerToken in conversation.** The token lives only in the history file.
**Error** (bash printed `FAILED: ...`):
```
Publish failed.
```
## Notes
- Always use absolute file paths with curl `-F "file=@..."`.
- Use `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.
- TTL default is `15` days.
- Namespace must be alphanumeric (3-32 chars). Default: `public`.
- TTL default is `15` days. Namespace must be alphanumeric (3-32 chars), default `public`.
- To **delete** a published page, find the owner token in `.claude/brewpage-history.md` and use the delete command shown in that file's header.
- **Sites: directory is the primary input** — it is auto-zipped (the only thing `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.
- The auto-zip excludes `.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 file detection: `--entry` override > `index.html` > first `.html` alphabetically.
- **SITE URL — NO trailing slash.** API returns `.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.
- **SITE verification cannot be done via `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.
---
@@ -227,5 +291,5 @@ Replace `{password_header}` with `-H "X-Password: {pass}"` only when password wa
| | |
|-|-|
| **[brewpage.app](https://brewpage.app)** | Free instant hosting — HTML, JSON, files, KV. No sign-up. |
| **[brewpage.app](https://brewpage.app)** | Free instant hosting — HTML, JSON, files, sites, KV. No sign-up. |
| **[brewcode](https://github.com/kochetkov-ma/claude-brewcode)** | Claude Code plugin suite — infinite tasks, code review, skills, hooks. |