git:20260925.45c2e9c to git:20260926.a2804ed

49 added, 216 removed. Audit A to A.

---
name: sandock-community
license: MIT
description: Read, answer and post on the Sandock community forum at community.sandock.ai through its /api/v1/community REST API. Lists categories and posts, reads a thread with its replies, searches, and publishes posts and replies behind an explicit confirmation. Use when the user invokes $sandock-community or /sandock-community, asks what is happening in the Sandock community, wants an unanswered question triaged or answered, or wants something posted or announced there.
disable-model-invocation: false
allowed-tools: Bash(node:*), Read, Write, Edit, Grep, Glob
user-invocable: true
metadata:
category: platform
tags:
- risk:gated-write
- surface:sandock
---
# Sandock Community
- One zero-dependency CLI over the forum's own REST API. No UI, no local mirror of
- the forum, no scraping — `community.sandock.ai` is the source of truth and the
- API is the only way this skill touches it.
+ A zero-dependency CLI over the forum's own REST API: read, post, reply, edit
+ what you wrote, and upload images. `community.sandock.ai` is the source of truth; this
+ skill keeps no local copy.
```bash
node skills/sandock-community/scripts/community.mjs <command> [--json]
```
- ## When to use
-
- - "社区里有什么新帖 / 有没有没人回的问题" → `posts --unanswered`
- - "看一下这个帖子" → `post <slug>`
- - "帮我回复他" → read the thread first, draft in chat, then `reply` after approval
- - "发个公告 / 提个 feature request" → `new` after approval
-
- Not for: emailing, tweeting, or announcing anywhere else. That is a different
- skill and a separate approval.
-
## Setup
```bash
printf 'SANDOCK_API_KEY=%s\n' '<key>' >> skills/sandock-community/.env
chmod 600 skills/sandock-community/.env
- node skills/sandock-community/scripts/community.mjs setup # is it configured?
- node skills/sandock-community/scripts/community.mjs whoami # is the key still valid?
+ node skills/sandock-community/scripts/community.mjs whoami
```
- The skill reads its own env file — no `source`, no exporting. Files are read
- **nearest-first**, and a variable already exported in the shell always wins over
- every file:
+ Create the key at <https://sandock.ai/docs/api-keys>, signed in with the account you use on the
+ forum. Values are read in this order, first one wins: an exported variable,
+ `$SANDOCK_ENV_FILE`, `skills/sandock-community/.env` (gitignored), `~/.sandock/.env`.
- | Order | Where |
+ | Variable | What it is |
| --- | --- |
- | 1 | `$SANDOCK_ENV_FILE`, if set |
- | 2 | `skills/sandock-community/.env.local` |
- | 3 | `skills/sandock-community/.env` |
- | 4 | `~/.config/sandock-community/.env` |
- | 5 | `~/.sandock/.env` — shared with the other Sandock tooling |
-
- `.env` and `.env.local` are gitignored (`skills/*/.env` in the repo's
- `.gitignore`), and nothing in this directory is tracked. `setup` prints which
- files were read and which variable names came from each — never a value.
-
- | Variable | Default | What it is |
- | --- | --- | --- |
- | `SANDOCK_API_KEY` | — | Required. Bearer token for the default account |
- | `SANDOCK_API_KEY_<NAME>` | — | A second account, reached with `--as <name>` |
- | `SANDOCK_ACCOUNT` | `default` | Which account to use when `--as` is absent |
- | `SANDOCK_COMMUNITY_API_URL` | `https://sandock.ai` | API origin |
- | `SANDOCK_COMMUNITY_URL` | `https://community.sandock.ai` | Forum origin, for links |
- | `SANDOCK_ENV_FILE` | — | Optional. Read this file first, ahead of the defaults |
-
- ### First run — when the key is missing
-
- Every command exits with the setup instructions when `SANDOCK_API_KEY` is
- unset. **Do not work around that**: do not guess a key, do not fall back to
- scraping the public site, and do not go quiet. Walk the user through it:
-
- 1. Run `setup` and show them its output.
- 2. Tell them where the key comes from — <https://sandock.ai/docs/api-keys>,
- signed in with the same account they use on the forum.
- 3. Tell them to put it in `skills/sandock-community/.env` (`chmod 600`).
- It is gitignored and read automatically.
- 4. Ask them to say when it is set, then confirm with `whoami` before doing
- anything else.
+ | `SANDOCK_API_KEY` | Required. The default account |
+ | `SANDOCK_API_KEY_<NAME>` | Another account, used with `--as <name>` |
+ | `SANDOCK_ACCOUNT` | Default account name when `--as` is absent |
+ | `SANDOCK_COMMUNITY_API_URL` | API origin (default `https://sandock.ai`) |
+ | `SANDOCK_COMMUNITY_URL` | Forum origin for links (default `https://community.sandock.ai`) |
- Never ask the user to paste the key into chat, never write it into a tracked
- file, and never echo it in output — `setup` prints only its length. When you
- write the file **for** them, write only the variable they gave you and never
- print the line back.
+ `community.sandock.ai/api/v1/*` only redirects to `sandock.ai`, and `fetch` drops the `Authorization` header on a cross-origin redirect — so the API origin must stay `https://sandock.ai`.
- **Why the API origin is not the forum origin.** `community.sandock.ai/api/v1/*`
- only 307s to `sandock.ai`, and `fetch` strips the `Authorization` header across
- a cross-origin redirect — so calling the community host would always come back
- `UNAUTHORIZED` no matter how good the key is. Do not "fix" that by pointing
- `SANDOCK_COMMUNITY_API_URL` back at the community host.
+ If the key is missing, every command prints these instructions. Never ask for
+ the key in chat, never write it to a tracked file, never print it — `setup`
+ shows only its length.
## Commands
| Command | What it does |
| --- | --- |
- | `setup` | Report whether the key is configured, or print how to configure it |
- | `accounts [--verify]` | List the configured accounts; `--verify` checks each against the server |
+ | `setup` | Is it configured, and from which files |
+ | `accounts [--verify]` | List configured accounts; `--verify` checks each one |
| `whoami` | Verify the key and print the API/forum origins (Sandock serves no `users/me`, so this probes `categories`) |
| `categories` | Category slugs, kinds and post counts |
| `posts [--category S] [--sort active\|latest\|top] [--unanswered] [--q TEXT] [--lang XX] [--limit N] [--offset N]` | List posts |
| `post <slug>` | One post with its replies |
| `new --category S --title T --body B [--lang XX] [--yes]` | Publish a post |
| `reply <postId> --body B [--yes]` | Publish a reply |
- | `edit <postId> [--title T] [--body B] [--lang XX] [--yes]` | Edit a post the selected account wrote. The URL stays; the thread shows "edited" |
+ | `edit <postId> [--title T] [--body B] [--lang XX] [--yes]` | Edit a post the selected account wrote |
| `edit-reply <replyId> --body B [--yes]` | Edit a reply the selected account wrote |
- | `upload <file.png>` | Store an image and print the markdown line that points at it |
-
- Every command takes `--json`. Use it when you are going to reason over the
- result; use the plain output when you are showing it to the user.
-
- ## Images
-
- A forum image is not an attachment on the post. The post carries one piece of
- content — its markdown `body` — so a picture reaches a thread as a link inside
- that markdown, which is exactly what the web composer writes too.
-
- So it is two steps, in this order:
-
- ```bash
- node scripts/community.mjs upload ./screenshot.png
- # → ![](https://…/attachments/blobs/sha256/ab/abcd….png)
+ | `upload <file.png>` | Store an image and print its markdown line |
- node scripts/community.mjs new --category ask --title "…" \
- --body "$(cat <<'EOF'
- Here is what I see:
+ `--category` takes a slug (run `categories` first). `post` takes a slug;
+ `reply` and `edit` take the post **id** from `post <slug>`.
- ![](https://…/attachments/blobs/sha256/ab/abcd….png)
- EOF
- )" --yes
- ```
+ ## Publishing is immediate
- `upload` takes png, jpg, jpeg, gif or webp, up to 10MB. It is not behind
- `--yes`: the bytes do nothing until a post points at them, and the gate that
- matters is the one on publishing the post.
+ This API has no draft or review state, so the gate is local:
- Uploading the same file twice costs nothing and returns the same URL — storage
- is keyed by the content's hash, so identical bytes are stored once. That also
- means a URL is not a secret you can rotate: anyone holding it can fetch the
- image. Do not upload a screenshot you would not post.
+ 1. `new`, `reply`, `edit` and `edit-reply` without `--yes` print the exact
+ payload and send nothing.
+ 2. Show the user that payload verbatim.
+ 3. Re-run with `--yes` only after they approve it, in the same turn.
- ## Several accounts
+ Read the thread before answering it, post in the thread's language, and publish
+ the user's wording as given. A write is done only when the command prints the
+ new id or URL — quote it back.
- One forum, several identities. The unnamed `SANDOCK_API_KEY` is the default
- account; anything under `SANDOCK_API_KEY_<NAME>` is another one.
+ ## Images
- ```bash
- # skills/sandock-community/.env
- SANDOCK_API_KEY=... # default
- SANDOCK_API_KEY_ALT=... # --as alt
- SANDOCK_API_KEY_SUPPORT=... # --as support
- ```
+ An image is a markdown link in the body, not an attachment:
```bash
- accounts --verify # who is configured, and does each still work
- whoami --as support # which identity is that, really
- reply <postId> --body "…" --as alt --yes
+ node scripts/community.mjs upload ./screenshot.png # → ![](…/attachments/blobs/sha256/….png)
+ node scripts/community.mjs new --category ask --title "…" --body "…![](…)…" --yes
```
- `--as` works on every command. `SANDOCK_ACCOUNT=alt` changes the default for a
- whole shell. `--as` always wins over it.
-
- Rules that matter more here than anywhere else in this skill:
-
- - **Say which account, every time.** `whoami`, the `--yes` preview, and the
- published confirmation all name the account. Quote that name back to the user
- — "posted as `alt`", not "posted".
- - **Never substitute an account.** A name that is not configured is an error,
- not a fall back to the default. Publishing under the wrong identity is not
- something the user can undo.
- - **Ask which account** when the user has more than one configured and has not
- said which to use. Do not assume the default just because it is the default.
-
- `--category` takes a **slug** (`ask`, `feature-requests`, …) — run `categories`
- first rather than guessing. `post` takes a **slug**; `reply` takes the post
- **id** from `post <slug>`, which is not the same string.
-
- <!--
- INTERNAL — not part of the published docs. Do not surface this section, or the
- existence of `SANDOCK_SYSTEMADMIN_KEY`, unless the user raises it first.
-
- `admin` drives /api/v1/system-admin/community/* — the moderation surface behind
- the deployment's own SYSTEM_ADMIN_API_SECRET_KEY (env: SANDOCK_SYSTEMADMIN_KEY,
- or a shared SYSTEMADMIN_KEY). Not a per-user token: it can hide, move and
- permanently delete anyone's content. Unset -> `admin` stops with instructions
- and nothing else is affected. Some deployments do not serve the route at all;
- the first call says so.
-
- read overview · posts · replies · reports · categories
- moderate moderate-post · take-down-post · take-down-reply · move-post
- feature-status · restore-post · restore-reply
- purge delete-post · delete-reply IRREVERSIBLE
- taxonomy create-category · update-category · archive-category
- reorder-categories
- reports handle-report
-
- admin posts --status hidden --include-deleted --limit 20
- admin take-down-post --post-id <id> --yes
- admin moderate-post --post-id <id> --is-locked true --yes
- admin feature-status --post-id <id> --feature-status planned --yes # none clears
- admin create-category --slug how-to --kind question --name "How to" --name "zh-CN=怎么做" --yes
-
- Flags are the API's field names in kebab-case. A category's text repeats once
- per locale: bare --name is English, --name "zh-CN=…" is that locale.
-
- Same gate as `new`/`reply`: payload printed, nothing sent, until --yes.
-
- `take-down-post` is the SOFT delete — row survives, links hold, `restore-post`
- undoes it. `moderate-post` does NOT take anything down; it pins, locks and
- backdates, and the server's schema has no status field for it to honour. `delete-post` purges the post, its replies, reactions
- and reports with no undo. Reach for the soft one unless the user asked for
- destruction in as many words, and quote the irreversible warning back first.
-
- Contract: references/api.md, in the commented block at the end.
- -->
-
- ## Permission boundary
-
- **Publishing is immediate, public, and has no draft or review state on this
- API.** There is no change request to stage and no merge to hold back.
-
- So the gate is local and mandatory:
-
- 1. `new` and `reply` without `--yes` print the exact JSON payload and send
- nothing. That is the default.
- 2. Show the user that payload — the real title and body, verbatim, not a summary.
- 3. Re-run with `--yes` only after they say yes, in that same turn.
-
- Never pass `--yes` on your own initiative, never on the first attempt, and never
- because the user approved a *different* post earlier in the conversation. A
- published post is visible to everyone immediately and this API has no delete.
-
- Other rules:
-
- - Read the thread before answering it. `post <slug>` exists for that.
- - Post in the language of the thread. `--lang` is the post's declared language
- and a post has exactly one; there is no "all".
- - When the user dictates wording, publish their wording. Do not improve it.
- - One topic per post. Do not batch unrelated answers into one reply.
-
- ## Evidence of completion
-
- A post or reply counts as done only when the command returned and printed the
- new id and URL. Quote them back. If the command printed a payload preview, the
- work is **not** done — nothing was sent.
-
- ## Reference
-
- `references/api.md` — the endpoint contract (paths, parameters, payload limits,
- response fields). Read it before changing the script.
+ png, jpg, gif or webp, up to 10MB. Identical bytes are stored once and anyone
+ with the URL can fetch it — do not upload what you would not post.
- ## Tests
+ ## Several accounts
```bash
- npm run test:community # all three forum skills, in CI too
- node --test skills/sandock-community/test/*.test.mjs # just this one
+ # skills/sandock-community/.env
+ SANDOCK_API_KEY=... # default
+ SANDOCK_API_KEY_ALT=... # --as alt
```
- `node --test <dir>` does not expand a directory on Node 24 — name the files.
-
- Two files per skill, both offline — no key, no network, runnable on a fresh
- clone:
+ `--as <name>` works on every command; `SANDOCK_ACCOUNT` sets the default for a
+ shell. An unknown name is an error, never a fallback to the default. Every
+ preview and confirmation names the account — say which one was used, and ask
+ when the user has several and has not said.
- - `community.test.mjs` — the pure helpers: argument parsing, URL building,
- config and account resolution, the dotenv reader and its precedence, error
- envelopes, the onboarding text, CJK-aware padding.
- - `commands.test.mjs` — the commands themselves, driven through `run()` with a
- recording `fetch`. This is where the dangerous behaviour is pinned down:
- that a write without `--yes` sends **nothing**, that reads use the user's
- credential and moderation uses the operator's, that an unknown `--as` account
- refuses instead of publishing as somebody else, that an id in a path is
- encoded, and that a listing never prints a key.
+ ## Reference and tests
- The second file exists because a helper test cannot answer "did this send
- anything". Both are wired into CI (`npm run test:community`).
- They need no key and make no network calls, so they stay runnable in CI and on
- a fresh clone.
+ `references/api.md` is the endpoint contract. Tests are offline (no key, no
+ network): `npm run test:community`.