# Phase 05 — util module (`/info`, `/help`) ## Context Links - Plan: [plan.md](plan.md) - Phase 04: [module framework](phase-04-module-framework.md) ## Overview - **Priority:** P1 - **Status:** pending - **Description:** fully-implemented `util` module with two public commands. `/info` reports chat/thread/sender IDs. `/help` iterates the registry and prints public+protected commands grouped by module. ## Key Insights - `/help` is a **renderer** over the registry — it does NOT hold its own command metadata. Single source of truth = registry. - Forum topics: `message_thread_id` may be absent for normal chats. Output "n/a" rather than omitting, so debug users know the field was checked. - Parse mode: **HTML** (decision locked). Easier escaping than MarkdownV2. Only 4 chars to escape: `&`, `<`, `>`, `"`. Write a small `escapeHtml()` util. - `/help` must access the registry. Use an exported getter from `src/modules/dispatcher.js` or `src/modules/registry.js` that returns the currently-built registry. The util module reads it inside its handler — not at module load time — so the registry exists by then. ## Requirements ### Functional - `/info` replies with: ``` chat id: 123 thread id: 456 (or "n/a" if undefined) sender id: 789 ``` Plain text, no parse mode needed. - `/help` output grouped by module: ```html util /info — Show chat/thread/sender IDs /help — Show this help wordle /wordle — Play wordle /wstats — Stats (protected) ... ``` - Modules with zero visible commands omitted entirely. - Private commands skipped. - Protected commands appended with `" (protected)"` suffix so users understand the distinction. - Module order: insertion order of `env.MODULES`. - Sent with `parse_mode: "HTML"`. - Both commands are **public** visibility. ### Non-functional - `src/modules/util/index.js` < 150 LOC. - No new deps. ## Architecture ``` src/modules/util/ ├── index.js # module default export ├── info-command.js # /info handler ├── help-command.js # /help handler + HTML renderer ``` Split by command file for clarity. Each command file < 80 LOC. Registry access: `src/modules/registry.js` exports `getCurrentRegistry()` returning the memoized instance (set by `buildRegistry`). `/help` calls this at handler time. ## Related Code Files ### Create - `src/modules/util/index.js` - `src/modules/util/info-command.js` - `src/modules/util/help-command.js` - `src/util/escape-html.js` (shared escaper) ### Modify - `src/modules/registry.js` — add `getCurrentRegistry()` exported getter ### Delete - none ## Implementation Steps 1. `src/util/escape-html.js`: ```js export function escapeHtml(s) { return String(s) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } ``` 2. `src/modules/registry.js`: - Add module-scope `let currentRegistry = null;` - `buildRegistry` assigns to it before returning. - `export function getCurrentRegistry() { if (!currentRegistry) throw new Error("registry not built yet"); return currentRegistry; }` 3. `src/modules/util/info-command.js`: - Exports `{ name: "info", visibility: "public", description: "Show chat/thread/sender IDs", handler }`. - Handler reads `ctx.chat?.id`, `ctx.message?.message_thread_id`, `ctx.from?.id`. - Reply: `\`chat id: ${chatId}\nthread id: ${threadId ?? "n/a"}\nsender id: ${senderId}\``. 4. `src/modules/util/help-command.js`: - Exports `{ name: "help", visibility: "public", description: "Show this help", handler }`. - Handler: - `const reg = getCurrentRegistry();` - Build `Map` of lines. - Iterate `reg.publicCommands` + `reg.protectedCommands` (in insertion order; `Map` preserves it). - For each entry, push `"/" + cmd.name + " — " + escapeHtml(cmd.description) + (visibility === "protected" ? " (protected)" : "")` under its module name. - Iterate `reg.modules` in order; for each with non-empty lines, emit `${escapeHtml(moduleName)}\n` + lines joined by `\n` + blank line. - `await ctx.reply(text, { parse_mode: "HTML" });` 5. `src/modules/util/index.js`: - `import info from "./info-command.js"; import help from "./help-command.js";` - `export default { name: "util", commands: [info, help] };` 6. Add `util` to `wrangler.toml` `MODULES` default if not already: `MODULES = "util,wordle,loldle,misc"`. 7. Lint. ## Todo List - [ ] `escape-html.js` - [ ] `getCurrentRegistry()` in registry.js - [ ] `info-command.js` - [ ] `help-command.js` renderer - [ ] `util/index.js` - [ ] Manual smoke test via `wrangler dev` - [ ] Lint clean ## Success Criteria - `/info` in a 1:1 chat shows chat id + "thread id: n/a" + sender id. - `/info` in a forum topic shows a real thread id. - `/help` shows `util` section with both commands, and stub module sections (after phase-06). - `/help` does NOT show private commands. - Protected commands show `(protected)` suffix. - HTML injection attempt in module description (e.g. `