Files
mrcharles ecffb4e598 refactor(tooling): merge discourse-cli into tooling-cli/discourse
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
2026-08-10 09:49:22 -05:00

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, --title required
  • -b, --body required (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, --body required (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 show or the raw API (see gotcha below).
  • update -b, --body required (new raw body).
  • <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 -r takes a post number.
  • update / delete take 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 has id, 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

  1. discourse show <topic_id> — read the topic and all replies.
  2. Identify the post you're responding to; note its post number (for -r) and the overall context.
  3. Draft a reply. create/reply take raw markdown — links, lists, code fences all work.
  4. discourse reply <topic_id> -b "your markdown" [-r <post_number>].
  5. 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 categories and 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 status before committing to stage only what belongs together.