From ecffb4e5981ad119acfe7a2996be4bf5fa74eeb0 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Mon, 10 Aug 2026 09:49:22 -0500 Subject: [PATCH] refactor(tooling): merge discourse-cli into tooling-cli/discourse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tooling-cli/KNELCredsManager/README.md | 2 +- .../discourse}/.env.example | 0 tooling-cli/discourse/.gitignore | 8 + tooling-cli/discourse/AGENTS.md | 346 ++++++++++++++++++ .../discourse}/Dockerfile | 0 .../discourse}/README.md | 56 ++- .../discourse}/requirements.txt | 0 .../discourse}/src/discourse_cli.py | 0 .../discourse}/validate.sh | 19 +- 9 files changed, 405 insertions(+), 26 deletions(-) rename {discourse-cli => tooling-cli/discourse}/.env.example (100%) create mode 100644 tooling-cli/discourse/.gitignore create mode 100644 tooling-cli/discourse/AGENTS.md rename {discourse-cli => tooling-cli/discourse}/Dockerfile (100%) rename {discourse-cli => tooling-cli/discourse}/README.md (63%) rename {discourse-cli => tooling-cli/discourse}/requirements.txt (100%) rename {discourse-cli => tooling-cli/discourse}/src/discourse_cli.py (100%) rename {discourse-cli => tooling-cli/discourse}/validate.sh (93%) diff --git a/tooling-cli/KNELCredsManager/README.md b/tooling-cli/KNELCredsManager/README.md index 3649a7d..237f1c2 100644 --- a/tooling-cli/KNELCredsManager/README.md +++ b/tooling-cli/KNELCredsManager/README.md @@ -28,7 +28,7 @@ All credential consumers source from `~/.creds/`: |---|---|---| | Redmine | `~/daytoday/redmine/bin/redmine` | `--env-file ~/.creds/redmine.env` | | Redmine MCP | `mcp-redmine-wrapper.sh` | `set -a; . ~/.creds/redmine.env; set +a` | -| Discourse | `~/daytoday/discourse/bin/discourse` | `--env-file ~/.creds/discourse.env` | +| Discourse | `discourse-cli` container (`tooling-cli/discourse/`) | `docker run --env-file ~/.creds/discourse.env` | | Discourse MCP | `mcp-discourse-wrapper.sh` | `set -a; . ~/.creds/discourse.env; set +a` | | Beszel MCP | `mcp-beszel-wrapper.sh` | `set -a; . ~/.creds/beszel.env; set +a` | | Uptime Kuma | (no active consumer yet) | Direct env reference | diff --git a/discourse-cli/.env.example b/tooling-cli/discourse/.env.example similarity index 100% rename from discourse-cli/.env.example rename to tooling-cli/discourse/.env.example diff --git a/tooling-cli/discourse/.gitignore b/tooling-cli/discourse/.gitignore new file mode 100644 index 0000000..78099e7 --- /dev/null +++ b/tooling-cli/discourse/.gitignore @@ -0,0 +1,8 @@ +# Secrets - never commit +.env + +# OS / editor cruft +.DS_Store +*.swp +*.swo +*~ diff --git a/tooling-cli/discourse/AGENTS.md b/tooling-cli/discourse/AGENTS.md new file mode 100644 index 0000000..96805cd --- /dev/null +++ b/tooling-cli/discourse/AGENTS.md @@ -0,0 +1,346 @@ +# 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`): + +```bash +docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest +``` + +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): + +```bash +alias discourse='docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest' +``` + +The examples below write `discourse ` 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 + +```bash +# 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 ` | Show details of a single category. | +| `topics` (`ls`) | List topics (latest, or filtered). Filters below. | +| `show ` | Full topic detail incl. all posts (HTML stripped to text). | +| `create` | Create a new topic (first post). | +| `reply ` | Reply to a topic. | +| `update ` | Edit an existing post's body. | +| `delete ` | Delete a post. | +| `search ` | Search the forum. | +| `notifications` | List your notifications. | + +### `topics` / `ls` filters +- `-c, --category ` — only topics in a category +- `-n, --new` — only new (to you) topics +- `-u, --unread` — only unread topics +- `-p, --page ` — page number (default 0) + +### `create` options +- `-t, --title` **required** +- `-b, --body` **required** (raw markdown/text — this becomes the first post) +- `-c, --category ` +- `--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 ` — 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). + +### `search` +- `` (positional, required) +- `-p, --page ` — page number (default 1) + +### `notifications` +- `-l, --limit ` — 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 # (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): + +```bash +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/.json` — full topic incl. `post_stream.posts[]` (each has + `id`, `post_number`, `username`, `cooked`, `raw`). +- `GET /categories.json` — all categories. +- `GET /c/.json` — topics in a category. +- `POST /posts.json` — create topic (`title`+`raw`+`category`) or reply + (`topic_id`+`raw`). +- `PUT /posts/.json` — edit (`{"post":{"raw":"..."}}`). +- `DELETE /posts/.json` — delete. +- `GET /search.json?q=` — search. +- `GET /notifications.json` — your notifications. + +## Patterns + +### Pattern: review & respond to a topic +1. `discourse show ` — 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 -b "your markdown" [-r ]`. +5. Verify with `discourse show `. + +### Pattern: before you act +- Always `discourse show ` 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): +```python +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 `, not +`git add -A`) so unrelated changes don't get bundled. + +### Conventional commit messages +Use the [Conventional Commits](https://www.conventionalcommits.org/) format: + +``` +(): + + +``` + +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. diff --git a/discourse-cli/Dockerfile b/tooling-cli/discourse/Dockerfile similarity index 100% rename from discourse-cli/Dockerfile rename to tooling-cli/discourse/Dockerfile diff --git a/discourse-cli/README.md b/tooling-cli/discourse/README.md similarity index 63% rename from discourse-cli/README.md rename to tooling-cli/discourse/README.md index 5e992d9..666c25f 100644 --- a/discourse-cli/README.md +++ b/tooling-cli/discourse/README.md @@ -13,20 +13,22 @@ the Discourse REST API. A small Python CLI built on `requests`. ## Configuration -Copy the env template and fill in real values (the `.env` is gitignored): +Credentials live in the **centralized credential store** at +`~/.creds/discourse.env` (see `tooling-cli/KNELCredsManager`). It holds: -```bash -cp .env.example .env -# then edit .env: -# DISCOURSE_URL=https://discourse.example.com -# DISCOURSE_API_KEY=abc123... -# DISCOURSE_API_USERNAME=your-username ``` +DISCOURSE_URL=https://discourse.example.com +DISCOURSE_API_KEY=abc123... +DISCOURSE_API_USERNAME=your-username +``` + +Use `.env.example` in this directory as a template if you need to create one. +Permissions: directory `700`, the env file `600` (owner read/write only). ## Build ```bash -docker build -t kneldevstack-aimiddleware-discourse-cli . +docker build -t kneldevstack-aimiddleware-discourse-cli:latest . ``` A prebuilt image is also available in the Gitea registry: @@ -37,16 +39,29 @@ git.knownelement.com/reachableceo/discourse-cli:latest ## Usage -### Via docker directly (from this directory) +Invoke the container directly with `docker run`. Pass credentials from the +centralized store via `--env-file`: ```bash -docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli whoami -docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli ls -docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli show 42 -docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli create -c 5 -t "Hello" -b "Body text" -docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli reply 42 -b "Nice point" +docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest whoami + +docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest ls + +docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest show 42 + +docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest create -c 5 -t "Hello" -b "Body text" + +docker run --rm --env-file ~/.creds/discourse.env \ + git.knownelement.com/reachableceo/discourse-cli:latest reply 42 -b "Nice point" ``` +To use a locally built image instead of the registry image, either set +`DISCOURSE_CLI_IMAGE` or swap the image tag for `kneldevstack-aimiddleware-discourse-cli:latest`. + ## Commands | Command | Description | @@ -59,7 +74,7 @@ docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli reply 42 | `create` | Create a topic (`--category`, `--title`, `--body`, `--tags`). | | `reply ` | Reply to a topic (`--body`, optional `--reply-to`). | | `update ` | Update a post (`--body`). | -| `delete ` | Delete a post. | +| `delete ` | Delete a post. | | `search ` | Search the forum. | | `notifications` | List your notifications. | @@ -89,3 +104,14 @@ docker run --rm --env-file .env kneldevstack-aimiddleware-discourse-cli reply 42 | `DISCOURSE_URL` | yes | Base URL of the Discourse instance. | | `DISCOURSE_API_KEY` | yes | API key ("All users" or "Single user"). | | `DISCOURSE_API_USERNAME`| yes | Username the API key acts as. | + +## Validation + +```bash +./validate.sh +``` + +Runs a full live read/write cycle against the configured instance (credential +check, whoami, categories, topics, show, search, create+reply+update+delete +cleanup). Credentials are read from `~/.creds/discourse.env` by default; +override with `DISCOURSE_ENV_FILE`. diff --git a/discourse-cli/requirements.txt b/tooling-cli/discourse/requirements.txt similarity index 100% rename from discourse-cli/requirements.txt rename to tooling-cli/discourse/requirements.txt diff --git a/discourse-cli/src/discourse_cli.py b/tooling-cli/discourse/src/discourse_cli.py similarity index 100% rename from discourse-cli/src/discourse_cli.py rename to tooling-cli/discourse/src/discourse_cli.py diff --git a/discourse-cli/validate.sh b/tooling-cli/discourse/validate.sh similarity index 93% rename from discourse-cli/validate.sh rename to tooling-cli/discourse/validate.sh index f13e64b..0334d36 100755 --- a/discourse-cli/validate.sh +++ b/tooling-cli/discourse/validate.sh @@ -2,7 +2,7 @@ # discourse-cli validation script. # Runs everything inside containers so no host tooling (curl, etc.) is needed. # -# Usage: ./validate.sh (reads .env from the discourse-cli directory) +# Usage: ./validate.sh # # Exit codes: 0 = all checks passed, 1 = one or more checks failed set -eu @@ -10,15 +10,18 @@ set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" -ENV_FILE="${DISCOURSE_ENV_FILE:-/home/reachableceo/.creds/discourse.env}" +# Credentials live in the centralized ~/.creds/ store (see KNELCredsManager). +# Override with DISCOURSE_ENV_FILE if you need a different location. +ENV_FILE="${DISCOURSE_ENV_FILE:-$HOME/.creds/discourse.env}" # Ensure env file exists if [ ! -f "$ENV_FILE" ]; then echo "FAIL: env file not found: $ENV_FILE" + echo " Set DISCOURSE_ENV_FILE or create it in ~/.creds/discourse.env" exit 1 fi -IMAGE="${DISCOURSE_CLI_IMAGE:-kneldevstack-aimiddleware-discourse-cli}" +IMAGE="${DISCOURSE_CLI_IMAGE:-git.knownelement.com/reachableceo/discourse-cli:latest}" PASS=0 FAIL=0 TOTAL=0 @@ -37,7 +40,7 @@ echo "" # ---------------------------------------------------------------- # echo "--- [0] Credential check (raw HTTP via containerized curl) ---" -# Load env vars from .env for the raw test +# Load env vars from the creds file for the raw test DISCOURSE_URL=$(grep -E '^DISCOURSE_URL=' "$ENV_FILE" | cut -d= -f2-) DISCOURSE_API_KEY=$(grep -E '^DISCOURSE_API_KEY=' "$ENV_FILE" | cut -d= -f2-) DISCOURSE_API_USERNAME=$(grep -E '^DISCOURSE_API_USERNAME=' "$ENV_FILE" | cut -d= -f2-) @@ -199,13 +202,9 @@ fi echo "" # ---------------------------------------------------------------- # -# Summary +# Results # ---------------------------------------------------------------- # echo "============================================" echo " RESULTS: ${PASS}/${TOTAL} passed, ${FAIL} failed" echo "============================================" - -if [ "$FAIL" -gt 0 ]; then - exit 1 -fi -exit 0 +[ "$FAIL" -eq 0 ]