Consolidate the discourse-cli source into tooling-cli/discourse/
(CLI source, Dockerfile, README, AGENTS.md, validate.sh) with all
documentation rewritten to invoke the container via raw docker run
and credentials from the centralized ~/.creds/discourse.env store.
No bin/ wrapper, no system-dependent paths in the docs.
Removes the old discourse-cli/ subdirectory and updates the
KNELCredsManager consumer table to reference the container directly.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
14 KiB
AGENTS.md — Discourse CLI tooling
This directory holds the discourse-cli Docker container — a thin Python CLI
over the Discourse REST API. Every Crush session that works on or with this
tool should read this file first: how to invoke it, what commands exist, what
gotchas to avoid, and the patterns to follow for common forum operations.
Connection & invocation
Invoke the real container with docker run. Credentials come from the
centralized credential store (see tooling-cli/KNELCredsManager):
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest <command>
There is intentionally no bin/ wrapper — invoke the real container, as
the project-wide rules require. For readability in an interactive session you
may define a throwaway shell alias (do not commit one):
alias discourse='docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest'
The examples below write discourse <cmd> for brevity; expand it to the full
docker run line (or set the alias) before running.
| Item | Value |
|---|---|
| Instance | https://community.turnsys.com |
| API user | Charles N Wyble (id 2, username reachableceo, trust level 4) |
| Admin? | No — trust 4 (Leader) but not staff/admin. Admin-only endpoints will 403. |
| Credentials | ~/.creds/discourse.env (DISCOURSE_URL, DISCOURSE_API_KEY, DISCOURSE_API_USERNAME) |
| CLI image | git.knownelement.com/reachableceo/discourse-cli:latest |
| Source | src/discourse_cli.py in this directory |
To use a locally built image instead of the registry image, either set
DISCOURSE_CLI_IMAGE or swap the tag for kneldevstack-aimiddleware-discourse-cli:latest.
Quick start
# Connection sanity check (run this first in any session):
discourse whoami
# List categories:
discourse categories
# Latest topics (or in a category):
discourse ls
discourse ls -c general
# Look at a topic:
discourse show 42
Command reference
| Command | Description |
|---|---|
whoami |
Authenticated user + connection test. |
categories |
List categories (id, slug, topic count, name). |
cat-info <id|slug> |
Show details of a single category. |
topics (ls) |
List topics (latest, or filtered). Filters below. |
show <topic_id> |
Full topic detail incl. all posts (HTML stripped to text). |
create |
Create a new topic (first post). |
reply <topic_id> |
Reply to a topic. |
update <post_id> |
Edit an existing post's body. |
delete <post_id> |
Delete a post. |
search <query> |
Search the forum. |
notifications |
List your notifications. |
topics / ls filters
-c, --category <id|slug>— only topics in a category-n, --new— only new (to you) topics-u, --unread— only unread topics-p, --page <n>— page number (default 0)
create options
-t, --titlerequired-b, --bodyrequired (raw markdown/text — this becomes the first post)-c, --category <id|slug>--tags— comma-separated tag list
reply options
topic_id(positional, required) — the topic to reply in-b, --bodyrequired (raw markdown/text)-r, --reply-to <post_number>— reply to a specific post number (not id)
update / delete
- Both operate on a post id (positional, required), not a topic id or
post number. Find the post id via
showor the raw API (see gotcha below). update -b, --bodyrequired (new raw body).
search
<query>(positional, required)-p, --page <n>— page number (default 1)
notifications
-l, --limit <n>— max results (default 20)
Key categories
| ID | Slug | Name | Notes |
|---|---|---|---|
| 4 | general |
General | Open discussion |
| 23 | reachableceo |
ReachableCEO | Personal |
| 6 | chiefoperationsandfinanceofficer |
ChiefOperationsOfficer | COO seat — parent for VP subcategories |
| 3 | staff |
Staff | May be restricted |
| 74 | vp-techops |
VP TechOps | ChiefOperationsOfficer (6) — 11 wiki topics migrated from PFVCluster |
| 75 | vp-compliance |
VP Compliance | ChiefOperationsOfficer (6) — awaiting content |
| 76 | board |
Board | (top-level, future) |
Run discourse categories for the full, current list.
Key topics (VP TechOps — category 74)
Eleven wiki topics migrated from PFVCluster (Discourse is source of truth):
| Topic | Title | Pattern |
|---|---|---|
| 296 | PFVCluster Project Overview | Pinned wiki |
| 297 | Operations Status | Pinned wiki (updated in place) |
| 298 | Infrastructure Audit Log | Wiki index + dated replies |
| 299 | Network Topology | Wiki |
| 300 | Storage Architecture | Wiki |
| 301 | Data Center Infrastructure | Wiki |
| 302 | Automation and Provisioning | Wiki |
| 303 | Security Architecture and Hardening | Wiki |
| 304 | Proxmox Fleet Reference | Wiki + replies |
| 305 | Kubernetes Platform | Wiki + replies |
| 306 | DNS and DHCP Services | Wiki + replies |
Gotcha: post id vs post number
Discourse distinguishes a post number (1, 2, 3... within a topic — the first post is always #1) from a post id (a globally unique integer).
reply -rtakes a post number.update/deletetake a post id.
show prints headers like --- #3 [3] author ... where the value in brackets
is the post number (here they often coincide for simple topics, but they are
not the same thing). To reliably get a post's id, either read the raw
JSON via the escape hatch below, or note that create/reply print the id
of the post they just made (Posted reply #<num> (id=<id>)).
Gotcha: not an admin (user key)
The default API user is trust level 4 (Leader) but not an admin/staff member. With the user-level key:
- Cannot create categories, set wiki posts, configure site settings, or
access
/admin/...endpoints (all 403). - Can create topics, reply, edit own posts, search, list notifications.
For admin operations (category creation, wiki flagging, docs plugin config),
an admin-scoped API key is needed. Set it via DISCOURSE_ADMIN_KEY in the
credential file and pass it explicitly in raw requests calls.
Things that will fail or be restricted without admin:
- Deleting other users' posts.
- Creating topics in staff-only or restricted categories.
- Moving/merging/recategorizing topics.
- Setting the wiki flag on posts.
If an operation 403s, that's expected — surface it to the user rather than retrying.
Gotcha: bulk operations & raw API
For anything beyond a single create/reply/update, or to read fields the
CLI doesn't print (e.g. exact post ids, category permissions, user lists),
run Python directly inside the container with requests. The CLI uses raw
requests against the Discourse REST API (no heavyweight SDK).
Pattern (mount a script and run it in the same image):
cat > /tmp/script.py <<'PY'
import os, requests
URL = os.environ["DISCOURSE_URL"].rstrip("/")
H = {
"Api-Key": os.environ["DISCOURSE_API_KEY"],
"Api-Username": os.environ["DISCOURSE_API_USERNAME"],
"Accept": "application/json",
}
# Example: get post ids for topic 42
r = requests.get(f"{URL}/t/42.json", headers=H, timeout=30)
r.raise_for_status()
for p in r.json()["post_stream"]["posts"]:
print(p["post_number"], "id=", p["id"], "by", p["username"])
PY
docker run --rm --env-file ~/.creds/discourse.env \
--entrypoint python \
-v /tmp/script.py:/tmp/script.py \
git.knownelement.com/reachableceo/discourse-cli:latest \
/tmp/script.py
Useful raw endpoints:
GET /t/<topic_id>.json— full topic incl.post_stream.posts[](each hasid,post_number,username,cooked,raw).GET /categories.json— all categories.GET /c/<cat_id>.json— topics in a category.POST /posts.json— create topic (title+raw+category) or reply (topic_id+raw).PUT /posts/<post_id>.json— edit ({"post":{"raw":"..."}}).DELETE /posts/<post_id>.json— delete.GET /search.json?q=<query>— search.GET /notifications.json— your notifications.
Patterns
Pattern: review & respond to a topic
discourse show <topic_id>— read the topic and all replies.- Identify the post you're responding to; note its post number (for
-r) and the overall context. - Draft a reply.
create/replytake raw markdown — links, lists, code fences all work. discourse reply <topic_id> -b "your markdown" [-r <post_number>].- Verify with
discourse show <topic_id>.
Pattern: before you act
- Always
discourse show <topic_id>before replying/editing — confirm the current state so you don't duplicate or contradict prior posts. - Never delete a post unless the user explicitly asks.
- Prefer replying over editing someone else's post (and editing others' posts will likely 403 anyway as a non-admin).
- When unsure which category to post in,
discourse categoriesand pick the closest match, or ask the user.
Pattern: posting conventions
- Bodies are raw markdown — Discourse renders them. Use fenced code blocks for commands/output, headings, tables, and bullet lists freely.
- Keep titles concise and descriptive.
- Use tags where the category supports them (
--tags a,b,c).
Pattern: wiki topics (living documents)
Wiki topics are the core anti-sprawl primitive. A wiki topic's first post is editable by anyone with permission (not just the original author), and Discourse preserves the full edit history automatically.
When to use a wiki topic:
- Living references — inventories, host lists, network topology, storage maps. Updated in place as facts change.
- Collaborative documents — policies, design docs, specs where multiple agents/humans contribute. First post = the document; replies = discussion.
- Status/index pages — operations status, documentation indexes.
When NOT to use a wiki topic:
- Discussion threads — regular topics where each reply is a distinct voice. Wiki-editing the first post would destroy the conversation.
- Point-in-time reports — these go as dated replies inside a wiki "log" topic (see audit pattern below).
To mark a post as wiki via the API (requires admin/moderator):
requests.put(f"{URL}/posts/{post_id}.json", headers=H, json={"wiki": True})
Pattern: post lifecycle (comment vs replace vs new)
The golden rule: never create a new topic for an update to existing knowledge. One wiki topic per subsystem, updated in place.
| Change type | Action | Why |
|---|---|---|
| Fact update (new VM, IP changed) | Edit the wiki post in place | Edit history preserves old state |
| New snapshot (audit, drift report) | New reply in the existing log topic | One topic accumulates history |
| Question/discussion about content | Reply to the relevant post | Threaded, doesn't mutate the doc |
| Major restructure | Edit wiki + reply noting why | Edit log = what; reply = why |
Pattern: audit/snapshot logs
Point-in-time reports (audits, drift reports, status snapshots) do NOT each get their own topic. Instead, create ONE wiki "log" topic per domain and append each snapshot as a dated reply:
[Wiki post #1] Index table (date | scope | link | superseded-by)
+ latest snapshot summary
[Reply #2] Audit 2026-07-29 — full content
[Reply #3] Audit 2026-07-30 — full content (supersedes #2)
[Reply #4] Audit 2026-08-05 — full content (current)
When a new snapshot arrives: add a reply with full content, then edit the wiki first post to point at the latest reply as "current."
Pattern: tag taxonomy
Tags are cross-cutting classifiers that prevent category multiplication:
| Tag | Meaning |
|---|---|
reference |
Living reference doc (inventories, topology, host lists) |
runbook |
Operational procedure (deploy, recover, configure) |
architecture |
Design doc, system architecture, capacity model |
policy |
Naming conventions, security policies, standards |
decision |
Resolved decision (ADR, distro choice, analysis outcome) |
audit |
Point-in-time snapshot |
security |
Security/hardening topic |
Pattern: category taxonomy
Org-based hierarchy mirroring Known Element's VP seats:
ChiefOperationsOfficer (id 6)
├── vp-techops — infrastructure, network, compute, security ops
└── vp-compliance — frameworks, evidence, audit response
Board (future)
Security operations (SecOps) lives under vp-techops (security is operational). Compliance covers frameworks (CMMC/STIG), evidence, and audit response.
Git workflow
Atomic commits
Each commit is one logical change — one feature, one fix, one doc update.
If you're tempted to write "and also..." in a commit message, that's a sign
to split it into two commits. Stage precisely (git add <file>, not
git add -A) so unrelated changes don't get bundled.
Conventional commit messages
Use the Conventional Commits format:
<type>(<optional scope>): <imperative subject>
<optional body — why, not what>
Types used in this repo: feat, fix, docs, refactor, chore, style.
Rules:
- Subject line under 72 chars, lowercase, imperative mood.
- No period at end of subject.
- Body wrapped at 72 chars, explains why the change exists.
Commit cadence
- Commit early and often. Don't accumulate a pile of unrelated changes.
- Commit without asking — if you made a coherent change, commit it.
- Every task (topic review, reply, script addition, doc update) ends with the relevant files committed.
- Run
git statusbefore committing to stage only what belongs together.