diff --git a/README.md b/README.md index b719fdf..1b7d694 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/brewdoc/README.md b/brewdoc/README.md index bef5d84..988ac29 100644 --- a/brewdoc/README.md +++ b/brewdoc/README.md @@ -85,6 +85,8 @@ claude --plugin-dir ./brewdoc | [`/brewdoc:publish`](skills/publish/README.md) | Publish to brewpage.app -- returns public URL | haiku | ` [--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 | diff --git a/openclaw/brewpage-publish/README.md b/openclaw/brewpage-publish/README.md new file mode 100644 index 0000000..08339e2 --- /dev/null +++ b/openclaw/brewpage-publish/README.md @@ -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: + +``` +/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/` 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 `/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 diff --git a/openclaw/brewpage-publish/SKILL.md b/openclaw/brewpage-publish/SKILL.md new file mode 100644 index 0000000..4221cdb --- /dev/null +++ b/openclaw/brewpage-publish/SKILL.md @@ -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 ` → 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: · · +TTL: 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 · files · · POST /api/sites +Entry: +TTL: 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// 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// 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//" 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/"` 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 `/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. | diff --git a/skills/brewpage-publish/README.md b/skills/brewpage-publish/README.md index 79f9195..a5771ca 100644 --- a/skills/brewpage-publish/README.md +++ b/skills/brewpage-publish/README.md @@ -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/` 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 `/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 diff --git a/skills/brewpage-publish/SKILL.md b/skills/brewpage-publish/SKILL.md index 9e6b156..ab4f906 100644 --- a/skills/brewpage-publish/SKILL.md +++ b/skills/brewpage-publish/SKILL.md @@ -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: " [--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: " [--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 ` → 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: · · - TTL: days +Content: · · +TTL: 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 · files · · POST /api/sites +Entry: +TTL: 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// 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// 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//" 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/"` 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 `/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. |