{"id":51205583,"url":"https://github.com/jeffbai996/cc-discord-kit","last_synced_at":"2026-06-28T03:03:47.335Z","repository":{"id":358615635,"uuid":"1226976144","full_name":"jeffbai996/cc-discord-kit","owner":"jeffbai996","description":"Self-hosted multi-agent system: common memory, journal, personas, context compaction, semantic grep. Flask UI with ⌘K palette.","archived":false,"fork":false,"pushed_at":"2026-06-26T23:33:45.000Z","size":1910,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-27T01:20:15.127Z","etag":null,"topics":["agent-memory","claude","discord","flask","mcp","multi-agent","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jeffbai996.png","metadata":{"files":{"readme":"README.md","changelog":"history.py","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-02T03:40:06.000Z","updated_at":"2026-06-26T23:33:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jeffbai996/cc-discord-kit","commit_stats":null,"previous_names":["jeffbai996/multiagent-tools","jeffbai996/cc-discord-kit"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jeffbai996/cc-discord-kit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffbai996%2Fcc-discord-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffbai996%2Fcc-discord-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffbai996%2Fcc-discord-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffbai996%2Fcc-discord-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeffbai996","download_url":"https://codeload.github.com/jeffbai996/cc-discord-kit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffbai996%2Fcc-discord-kit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34875370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-memory","claude","discord","flask","mcp","multi-agent","python"],"created_at":"2026-06-28T03:03:45.851Z","updated_at":"2026-06-28T03:03:47.319Z","avatar_url":"https://github.com/jeffbai996.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cc-discord-kit\n\n**Give your Claude Code agents a shared memory — and watch them work from Discord.**\n\nTwo things in one kit:\n\n1. **Shared context** — a memory + journal + persona store that any number of Claude Code agents (across any number of machines) read and write through one CLI. Plain JSON, auditable by hand.\n2. **Claude Code → Discord** — a set of hooks that surface a running Claude Code session into a Discord channel: its narration, its tool calls, its turn status — and let you fire commands back at the host from your phone.\n\nSo you can step away from the terminal and still see what your agent is doing, what it remembered, and nudge it — all from Discord. Runs on your own box, over LAN or a private tunnel (tailscale). Never the public internet.\n\n---\n\n## Contents\n\n- [What it looks like](#what-it-looks-like)\n- [The two halves](#the-two-halves)\n- [What's in here](#whats-in-here)\n- [Install](#install)\n- [Configuration](#configuration)\n- [CLI](#cli)\n- [Web UI](#web-ui)\n- [Memory schema](#memory-schema)\n- [Saving from agents](#saving-from-agents)\n- [Hooks (Claude Code agents)](#hooks-claude-code-agents)\n- [Discord bot](#discord-bot)\n- [Spoken voice (TTS / STT)](#spoken-voice-tts--stt)\n- [Tests](#tests)\n- [Inventory probes](#inventory-probes)\n- [License](#license)\n\n## What it looks like\n\nYour agent is running a task in the terminal. Here's the same turn, in your Discord channel:\n\n**It reacts to your message as it works** — one emoji tracks the whole turn:\n\n\u003e 👀 → 🔧 → ✅  *(got it → editing → done)*\n\n**It narrates** — the prose it'd normally only print to the terminal shows up live:\n\n\u003e 🧠 ***Narrating…***\n\u003e \\\u003e Looking at the config now. The timeout's hardcoded — I'll pull it into an env var and drop the debug flag while I'm here.\n\n**It shows its tool calls** — from a one-line ticker up to full diffs:\n\n```diff\n+ ● Edit(src/config.py)\n  ⎿ [+3, -1]\n- DEBUG = True\n+ DEBUG = False\n+ TIMEOUT = int(os.environ.get(\"TIMEOUT\", 30))\n+ ● Bash(npm test)\n- ● Bash(npm run deploy) FAILED\n```\n\n**It shows its subagents** — when the agent fans work out, a live panel rides the bottom of the tool trace, edited in place every few seconds (top-left spinner pulses per update, each running dot blinks ○⇄◉, circles fill solid as agents land, tokens and elapsed from the real transcripts):\n\n```\n◓ agents · mybot · 2 running · 1 done · 16.5k tok\n\n  ◉  research bear case   sonnet  1m12s  12.3k\n  ◉  audit api handlers   sonnet  1m12s   3.9k\n  ●  verify margin math   haiku     31s   4.2k\n```\n\n**And you can talk back** — type a command in the channel, it runs on the host:\n\n\u003e **you:** `!git log --oneline -3`\n\u003e **bot:**\n\u003e ```\n\u003e 5b54edd docs(README): reframe positioning\n\u003e 8e98be7 feat(hooks): port tool-trace rework\n\u003e 3b62f9f feat: CF Worker + KV backend\n\u003e ```\n\n`!agents` works the same way — it replies with a snapshot of the panel above, in any mode, even when live surfacing is off.\n\n**Or open a live terminal pane** — send a bare `!` and one pinned message becomes a real scrollback, PATCHed in place every command instead of a new reply each time:\n\n\u003e **you:** `!`\n\u003e **bot:**\n\u003e ```\n\u003e $ ▏\n\u003e ```\n\u003e **you:** `!ls`\n\u003e **bot:** *(same message, edited)*\n\u003e ```\n\u003e $ ls\n\u003e README.md  cli.py  store.py  hooks/\n\u003e $ ▏\n\u003e ```\n\nClose it with `!exit` (or `!q`); an idle pane auto-expires after 30 minutes back to one-shot mode.\n\nAll of it is **opt-in per channel** and **off by default**. Pick how much you want to see — silent, just status emoji, narration, or full diffs.\n\nSee [Tool-trace modes](#tool-trace-by-example) for the full mode list, and [The two halves](#the-two-halves) for how it's built.\n\n---\n\n## The two halves\n\n### 1. Shared context store\nA JSON-backed store with three tiers — **memories** (durable atomic facts, cap 200), a **journal** (pinned moments, cap 1000, with optional titles), and **files** (shared documents/references/datasets/images the agents can read) — plus a freeform `SHARED.md` rules doc and per-agent **persona** files. Every agent uses it through one CLI — directly on a shared filesystem, or over HTTP against the bundled Flask server when the agent's on another machine. Optional semantic search hooks out to an external [vecgrep](#) service (no embedding model ships here). Last-writer-wins + plain JSON is deliberate: you can read and fix the store with a text editor, and a process dying mid-write can't corrupt it.\n\n### 2. Claude Code, surfaced into Discord\nThree independent hooks make a Claude Code turn **legible from a phone** — opt in per agent, per channel:\n\n| Hook | What it surfaces |\n| --- | --- |\n| **narration** (`narrate.py`) | the agent's between-tool prose, live as a `🧠` blockquote |\n| **tool-trace** (`tool_watcher.py`) | the actual tool calls — one-line ticker → full diffs → command output |\n| **emoji-state** (`react_hook.py`) | one reaction on your message tracking the turn: `👀` got it → `🔧` working → `✅` done |\n\nPlus a **command path back**: `discord_passthrough.py` lets you type `!ls` or a registered `/deploy` in Discord and have it run on the host, reply inline, and never cost a token. (Permission prompts are mirrored read-only — you still approve in the terminal.)\n\nSee [Tool-trace, by example](#tool-trace-by-example) below for what these actually look like in a channel.\n\n---\n\n**Discord isn't load-bearing for the store.** The store/CLI/server/inventory layers don't know Discord exists — they're transport-agnostic. Only the observability + command hooks are Discord-native (reactions, `\u003e\u003e\u003e` blockquotes, the 2000-char pagination guard). Swap substrates → reimplement the hook layer; the store underneath is unchanged.\n\nThe store/server layer originated here; the Claude Code hooks were developed alongside it and genericized for this kit.\n\n## What's in here\n\n**The store** (Discord-agnostic — works on its own)\n- `store.py` — the JSON memory + journal store. Atomic writes, last-writer-wins. Memories carry an optional `bot` whitelist (share/unshare per agent); journal entries carry an optional `title`. A freeform `SHARED.md` rules doc sits alongside, injected at session start.\n- `files_store.py` — a third tier: shared **files** (documents, references, datasets, images, PDFs) the whole set of agents can read. Inline text or on-disk blobs, size-capped, sha256'd, mime-typed.\n- `cli.py` — local CLI: `memory`/`journal`/`persona`/`files` × `list|show|add|edit|delete|search`, plus `recall` (semantic retrieve over the store; see [docs/retrieval-controller.md](docs/retrieval-controller.md)).\n- `client.py` — same CLI, but over HTTP to the server (set `CCDK_URL`) so remote agents use it transparently.\n- `server.py` — Flask web UI + JSON API. ⌘K palette, editors, markdown, pinning/trash/history/merge, a file browser with inline preview, optional semantic search.\n- `personas.py` — where each agent keeps its persona files (configured in `agents.yaml`); auto-commits if they live in a git repo.\n- `migrate.py` — one-shot importer: turn a directory of frontmatter markdown notes into memories/journal entries (`--memory-dir` / `--context-dir`).\n\n**The Discord layer**\n- `hooks/narrate.py`, `hooks/tool_watcher.py`, `hooks/react_hook.py` — the three observability lanes (narration / tool-trace / emoji-state).\n- `hooks/discord_passthrough.py` — run `!cmd` / `/cmd` from Discord on the host, reply inline, zero token spend. See `commands/README.md`.\n- `hooks/notify_hook.py` — mirror Claude Code permission prompts to Discord (read-only).\n- `discord_handler.py` — optional slash-command bot: `/mem`, `/journal`, `/persona`, `/bot`, `/squad`, `/vecgrep`.\n\n**Fleet management** (optional — for running several agents off one `agents.yaml`)\n- `bot_config.py` — single source of truth: reads `agents.yaml` and resolves which agent a given Claude Code session is (by config dir), with per-agent fields (kind, host, home channel, access.json path).\n- `bot_admin.py` — toggle per-channel Discord flags (requireMention / narrate / tool-watcher / allowed) for any agent in the registry; backs the `/bot` slash surface.\n- `bots_doctor.py` — validate every agent in `agents.yaml` against reality (persona files present, hooks wired, unit alive) and report problems.\n- `capabilities.py` — a capability matrix each agent self-reports into, so you can spot drift (one agent missing a hook — or an MCP-delivered capability like `browser` (the Playwright computer-use MCP) — the others have). Note: the matrix *tracks* which agents have a capability; it doesn't ship the implementation (e.g. the browser tooling itself lives outside this kit).\n- `new_bot.py` — scaffold a new agent: emits its `settings.json` (the kit's hook set), an `agents.yaml` entry, a launcher, and a presence file.\n- `facts.py` — a tiny key→value store for reusable literals (ports, IDs, paths) agents should look up rather than hallucinate.\n\n**Ops**\n- `inventory.py` — live read of hooks, crontab, systemd units, launchd agents across hosts (cached 30s, never writes).\n- `shot.py` — Playwright screenshot helper for visually verifying the web UI (`CCDK_HOST`/`CCDK_PORT`).\n- `digest.py` — pull recent channel history for review; optional Gemini summarize.\n\n## Operator — a Computer-Using Agent on top of the kit\n\n[**Operator**](https://github.com/jeffbai996/operator) is a companion project built on this kit: a live browser/computer-use cockpit where you watch an agent drive a real browser in real time, steer it yourself, or hand it the wheel. The agents are the same subscription-backed Claude/GPT sessions the kit already manages — Operator just adds a watch-and-steer surface in front of them.\n\n![Operator driving a browser](docs/img/operator-geoguessr.jpeg)\n\u003csub\u003e\u003ci\u003eOperator's GPT agent reasoning through a live GeoGuessr round — left: the interleaved thinking + action trace (Browsing / Reading / Clicking) with a live status card; right: the actual browser it's driving, streamed frame-by-frame.\u003c/i\u003e\u003c/sub\u003e\n\n---\n\nIt's its own repo (proprietary, source-visible); this kit is the substrate it runs on.\n\n## Install\n\n```bash\ngit clone https://github.com/\u003cyou\u003e/cc-discord-kit.git\ncd cc-discord-kit\npython3 -m venv venv \u0026\u0026 source venv/bin/activate\npip install -r requirements.txt\ncp agents.example.yaml ~/.config/cc-discord-kit/agents.yaml\n# Edit agents.yaml to point at your real persona-file paths.\n```\n\nThen either run the server:\n\n```bash\npython3 server.py\n# Open http://127.0.0.1:5005\n```\n\n…or the CLI:\n\n```bash\n./cli.py memory list\n./cli.py memory add \"Use direct, no glazing\" --type=feedback --name=\"comm style\"\n```\n\n## Configuration\n\nAll env vars optional unless noted.\n\n| Env var | Read by | Purpose |\n| --- | --- | --- |\n| `CCDK_DATA_DIR` | `store.py` | dir holding `memories.json` + `journal.json`. Default `~/.local/share/cc-discord-kit/`. |\n| `CCDK_AGENTS_FILE` | `personas.py` | path to `agents.yaml`. Default `~/.config/cc-discord-kit/agents.yaml`. |\n| `CCDK_URL` | `client.py` | when set, CLI runs in HTTP mode against this base URL instead of touching JSON files locally. |\n| `CCDK_HOST` / `CCDK_PORT` | `server.py` | Flask bind. Default `127.0.0.1:5005`. **Don't bind to 0.0.0.0** — this is a personal store, not a public service. |\n| `CCDK_URL_PREFIX` | `server.py` | for hosting under a path (e.g. `/cc-discord-kit` behind a reverse proxy). |\n| `CCDK_BOT` | `cli.py`, hooks | explicit agent identity. Otherwise auto-detected from `CLAUDE_CONFIG_DIR` last segment, then hostname. |\n| `CCDK_PRELOAD_BUDGET` | `hooks/session_start_hook.py` | token budget for the full-body memory preload at session start. `0` (default) = load every memory body; a positive value caps it (newest-first), the rest stay index-only and are reached via `recall`. See [docs/retrieval-controller.md](docs/retrieval-controller.md). |\n| `CCDK_DISCORD_TOKEN` | `digest.py`, `discord_handler.py`, `hooks/stop_hook.py` | bot token for the discord side. `stop_hook` uses it to post save/edit/delete confirmation cards back to the originating channel. |\n| `CCDK_GUILD_IDS` | `discord_handler.py` | optional CSV of Discord guild IDs for instant per-server slash command sync. Without this, slash commands sync globally (~1hr propagation). |\n| `CCDK_DIGEST_CHANNELS` | `digest.py` | comma-separated `name:id` pairs for digest pull. |\n| `CCDK_SETTINGS_PATHS` | `inventory.py` | optional CSV of extra Claude Code `settings.json` paths to probe for hook chains. |\n| `GEMINI_API_KEY` | `digest.py` | enables the optional auto-summarize button on the digest page. |\n| `VECGREP_URL` | `vecgrep_client.py` | optional vecgrep endpoint for semantic search. Default `http://127.0.0.1:8765`. |\n| `VECGREP_CORPUS_MEMORIES` / `VECGREP_CORPUS_JOURNAL` | `vecgrep_client.py` | optional corpus names. Default `cc-discord-kit`. |\n| `CCDK_OWNER_DISCORD_USER_ID` | `hooks/discord_passthrough.py` | the Discord `user_id` allowed to run `!cmd` and `/cmd` pass-through. Required for the hook to activate (fails closed). Alternatively place the same value in `~/.config/cc-discord-kit/owner_id`. |\n| `CCDK_OWNER_ID_FILE` | `hooks/discord_passthrough.py` | override the owner_id file path. Default `~/.config/cc-discord-kit/owner_id`. |\n| `CCDK_COMMANDS_DIR` | `hooks/discord_passthrough.py` | where to find `/cmd` registry scripts. Default `\u003crepo\u003e/commands/`. |\n| `CCDK_PASSTHROUGH_LOG` | `hooks/discord_passthrough.py` | log file path. Default `~/.local/state/cc-discord-kit/passthrough.log`. |\n| `CCDK_SESSION_STATE_FILE` | `hooks/discord_passthrough.py` | live-terminal session state (open pane's screen message + scrollback, per channel). Default `~/.cache/cc-discord-kit/passthrough_term.json`. |\n| `CCDK_RELAY_HELPER_ID` | `choice_card.py` | Discord user_id of the central helper bot that handles card taps. **Required for tap-to-act** — fail-closed if unset (no helper ⇒ relayed taps aren't trusted). |\n| `CCDK_HELPER_DISCORD_TOKEN` | `discord_card.py` | the helper bot's Discord token — cards are posted with it so the helper can edit them in place (cross-bot edits 403). Env or in `~/.config/cc-discord-kit/env`. |\n| `CCDK_ALLOWED_TAPPERS` | `vecgrep_confirm.py` | extra Discord user_ids (comma-separated) allowed to tap choice/veto/todo cards, beyond the owner. Vecgrep write-confirms stay owner-only. |\n| `CCDK_RELAY_CONFIG` / `CCDK_RELAY_SECRET` | `choice_card.py` | relay config + HMAC secret paths. Defaults `~/.config/cc-discord-kit/relay.json` + `relay_secret` (secret auto-generated on first use). |\n| `CCDK_RELAY_CONFIGS` | `choice_card.py` | multi-bot fan-out: `os.pathsep`-separated list of `relay.json` paths, one per bot state dir. Each bot's in-process plugin reads relay config from its OWN dir, so a multi-bot host must write all of them — set this so the helper writes every asking bot's config. Single-bot setups can ignore it (uses `CCDK_RELAY_CONFIG`). |\n| `CCDK_VECGREP_CONFIRM_CHANNEL` | `vecgrep_confirm.py` | the only channel a vecgrep write-confirm tap is honored in (owner's private channel). Fail-closed if unset. |\n| `CCDK_PLUGIN_DIRS` | `hooks/discord_plugin_patch.py` | extra config dirs (comma-separated, relative to `$HOME`) whose plugin copy to patch, for multi-agent hosts. Default: `CLAUDE_CONFIG_DIR` + `~/.claude`. |\n| `CCDK_THINK_SHOW_SEC` | `hooks/narrate.py` | ceiling (seconds) before the thinking indicator shows on a silent turn whose first output hasn't flushed. Default 6. |\n\nThe env file at `~/.config/cc-discord-kit/env` is checked as a fallback for any of the above. Shell-style:\n\n```\nCCDK_DISCORD_TOKEN=...\nCCDK_DIGEST_CHANNELS=general:111111111111111111,help:222222222222222222\nGEMINI_API_KEY=...\n```\n\n## CLI\n\n```bash\ncc-discord-kit memory list                          # all entries\ncc-discord-kit memory list --about user             # filter by subject\ncc-discord-kit memory list --type feedback          # filter by type\ncc-discord-kit memory show 42\ncc-discord-kit memory show 42 --body-only\ncc-discord-kit memory add \"...\" --type project --name \"X\" --tags a,b --about user\n\ncc-discord-kit journal list\ncc-discord-kit journal show 17\ncc-discord-kit journal add \"...\" --actor agent-1 --tags a,b --title \"X\"\ncc-discord-kit journal edit 17 \"updated body\" --tags a,b --title \"X\"\n\ncc-discord-kit files list                           # shared files\ncc-discord-kit files show 3\ncc-discord-kit files add ./notes.md --tags a,b      # text inline, binaries → blob\ncc-discord-kit files edit 3 --tags a,b\ncc-discord-kit files delete 3\ncc-discord-kit files search \"term\"\n\ncc-discord-kit memory share 42 --with agent-1       # scope a memory to an agent\ncc-discord-kit memory unshare 42 --with agent-1\n\ncc-discord-kit persona show agent-1 persona.md      # print file contents\ncc-discord-kit persona edit agent-1 persona.md      # opens $EDITOR; saves on exit\ncc-discord-kit persona write agent-1 persona.md \"\u003ctext\u003e\"  # write directly\n\n# Fleet management (needs agents.yaml + PyYAML)\ncc-discord-kit bots list                            # agents in the registry\ncc-discord-kit bots doctor                          # validate each against reality\ncc-discord-kit capabilities report                  # this agent self-reports its hooks/features\ncc-discord-kit capabilities show [agent]            # the capability matrix\ncc-discord-kit capabilities drift                   # who's missing what\ncc-discord-kit fact set \u003ckey\u003e \u003cvalue\u003e [--note ...]  # reusable literal store\ncc-discord-kit fact get \u003ckey\u003e\ncc-discord-kit fact list\ncc-discord-kit fact search \u003cterm\u003e\ncc-discord-kit fact delete \u003ckey\u003e\n```\n\n\nSet `CCDK_URL=https://your-host:8443/` to run the same commands against a remote server.\n\n## Web UI\n\n`python3 server.py` then open `http://127.0.0.1:5005`. Pages:\n\n| Path | What |\n| --- | --- |\n| `/` | memories index — search, optional semantic search, filter by type/about/bot, pin/trash |\n| `/journal` | journal entries timeline with literal or optional semantic search; optional title |\n| `/files` | shared file browser — colored type pills + hover legend, grid/list views, inline preview (images, syntax-highlighted code, markdown, JSON, CSV tables, PDF, audio/video), and an edit/preview toggle for text files |\n| `/context` | edit `SHARED.md` (the global rules doc injected at session start) + per-agent brain-file (CLAUDE.md) cards |\n| `/personas` | per-agent persona file editor |\n| `/digest` | recent Discord channel review (if configured) |\n| `/inventory` | live hooks/crons/services across configured hosts |\n| `/trash` | soft-deleted records, restore-able |\n\n`⌘K` (mac) / `ctrl+K` (everywhere else) opens the command palette. Filter type-ahead, ↑↓ to navigate, ↵ to fire, esc to close.\n\n## Memory schema\n\n```json\n{\n  \"id\": 42,\n  \"type\": \"feedback\",\n  \"name\": \"concise replies\",\n  \"text\": \"...\",\n  \"tags\": [\"communication\"],\n  \"about\": [\"user\"],\n  \"bot\": null,\n  \"ts\": \"2026-05-01T20:00:00Z\"\n}\n```\n\n- `type` — one of `user`, `feedback`, `project`, `reference`. Used for color coding + filter.\n- `name` — short title.\n- `text` — the body. Markdown rendered in the web UI.\n- `tags` — free-form labels.\n- `about` — subjects the entry concerns (e.g. `[\"user\"]`, `[\"domain-x\"]`). Filterable.\n- `bot` — if set (e.g. `[\"agent-1\"]`), only that agent includes the entry in default views; others must pass `--all` to see it. Default null = visible to all agents. Manage with `memory share`/`memory unshare`.\n\nJournal entries carry `id, ts, source, actor, text, tags, pinned` plus an optional `title` (short heading).\n\nFile records carry `id, ts, name, slug, type, mime, size, sha256, storage, content?/blob_path?, tags, about, bot?, actor`. `storage` is `inline` (text in the JSON) or `blob` (bytes on disk under `\u003cCCDK_DATA_DIR\u003e/files/`). Caps: 100 MB/file, 5 GB total. The web UI serves only provably-inert types inline (raster images, PDF, audio, video) — active types (SVG, HTML) are always forced to download.\n\n## Saving From Agents\n\nUse explicit CLI commands for real writes, especially when the request came from Discord:\n\n```bash\ncc-discord-kit memory add \\\n  --type feedback \\\n  --name \"short title\" \\\n  --tags \"tag1,tag2\" \\\n  --about \"subject1,subject2\" \\\n  --discord-chat-id \"\u003cchat_id\u003e\" \\\n  --discord-message-id \"\u003cmessage_id\u003e\" \\\n  \"body text\"\n```\n\nThe Discord flags are optional. When present, the CLI or HTTP API posts a confirmation card back to the originating channel. For terminal-only saves, omit them.\n\n## Hooks (Claude Code agents)\n\nThe `hooks/` directory has a full set of Claude Code hooks. Wire any subset into your `settings.json`. Each is independent — adopt only what you need.\n\n### Memory / journal integration (the original set)\n\n- **`session_start_hook.py`** (SessionStart) — injects full feedback memories, an index of other memories, and recent journal entries into context on session boot.\n- **`user_prompt_hook.py`** (UserPromptSubmit) — refreshes a compact memory index on each user prompt.\n- **`precompact_hook.py`** (PreCompact) — writes a \"what was the last conversation about\" snapshot before context compaction. Routes through `CCDK_URL` if set, else direct import.\n- **`stop_hook.py`** (Stop) — legacy tag-parser save path. **Use CLI commands as the recommended write path** (`cc-discord-kit memory add ...`). The Stop hook is retained for back-compat; see [Legacy save-intent gate](#legacy-save-intent-gate) for the syntax. See `SAVES.md` for the rationale and Discord card flow.\n\n### Discord pass-through + slash dispatch\n\n- **`discord_passthrough.py`** (UserPromptSubmit) — intercepts Discord-origin `!cmd` (raw shell) and `/cmd` (registered slash) messages from the configured owner, runs them on the host, replies directly to Discord, blocks the prompt from reaching the model (zero token spend). See `commands/README.md` for the dispatch contract. Owner check: `CCDK_OWNER_DISCORD_USER_ID` or `~/.config/cc-discord-kit/owner_id`. `!help` prints the command reference; `!agents help` prints the agent-view reference.\n\n  **Live-terminal mode.** Send a bare `!` to open a *terminal screen* — a single pinned Discord message that's PATCHed in place as you run commands, instead of a new reply per command. While the pane is open, each `!cmd` appends to a rolling scrollback (last 25 lines) rendered into that one message, so a channel reads like a real terminal. Close it with `!exit` (or `!q`) — the screen gets a final `Goodbye! 👋` frame. A pane left idle for 30 minutes auto-expires back to one-shot mode (so a forgotten session doesn't keep editing a message scrolled out of view). Session state lives in `CCDK_SESSION_STATE_FILE` (default `~/.cache/cc-discord-kit/passthrough_term.json`).\n\n### Voice surfacing — narrate + tool-watcher\n\n- **`narrate.py`** (PostToolUse `--mode watch` + Stop `--mode finalize`) — surfaces the agent's between-tool prose to Discord. Watcher tails the transcript for new `type:assistant` text blocks and posts/edits a `🧠 *Narrating…*` placeholder in the originating channel. Finalize fires on Stop — mode determines what happens to the placeholder.\n\n  Per-channel mode lives in `\u003cbot_root\u003e/channels/discord/narrate.json`:\n\n  ```json\n  { \"\u003cchat_id\u003e\": \"collapse\" | \"always\" | \"never\" }\n  ```\n\n  - **`collapse`** (alias: `auto`) — placeholder posted live, **deleted at Stop** after the real reply lands. Best for fast turns. (Legacy \"auto\" is migrated on read.)\n  - **`always`** — placeholder converted at Stop into a `🧠 **Narration**` quoted block kept **above** the real reply. Persistent, reviewable.\n  - **`never`** — no narration. Default.\n\n  Live placeholder uses Discord's `\u003e\u003e\u003e` multi-line blockquote. Triple-backticks in prose are neutralized so they don't break the outer fence. The watcher rotates segments on mid-turn reply landings.\n\n- **`tool_watcher.py`** (PostToolUse) — surfaces tool calls themselves into the same per-turn segment that narrate.py owns. Per-channel mode in `\u003cbot_root\u003e/channels/discord/tools.json`:\n\n  ```json\n  { \"\u003cchat_id\u003e\": \"off\" | \"collapse\" | \"ticker\" | \"diffs\" | \"full\" }\n  ```\n\n  - **`ticker`** — one-line header per call: `+ ● ToolName(short args)`. The `●` dot marks it as a tool invocation (vs a file-edit `+`/`-` line, which carry no dot). Errored calls render `- ● ToolName(...) FAILED` (red). Color is via a ` ```diff ` fence: Discord renders `+` lines green, `-` lines red — cross-platform. Persists past Stop.\n  - **`diffs`** — ticker + a ` ```diff ` unified diff for Edit/Write/MultiEdit, plus a grey summary line under the header: `  ⎿ [+N, -M]` (lines added/removed) for edits, `  ⎿ [N lines]` for Read.\n  - **`collapse`** — same as `diffs` while live (ticker + diffs + summaries), then the whole tool message is deleted at Stop. Symmetric with narrate's `collapse` — pair them for full visibility during the turn, clean channel after.\n  - **`full`** — diffs + ` ``` ` fenced Bash stdout (secret-stripped).\n  - **`off`** — disabled (default).\n\n  **Threads inherit the parent.** A Discord thread is a channel with its own id, but `narrate.json` / `tools.json` are keyed by the parent channel — so a thread with no entry of its own inherits the parent's mode (set it once on the channel, every thread under it follows). Set a mode on a thread's own id to override. The thread→parent lookup is one cached Discord API call per channel id, ever.\n\n- **`agent_view.py`** (PreToolUse / PostToolUse / PostToolUseFailure / Stop) — the **subagent panel**. PreToolUse on `Agent|Task` registers the spawn and forks a detached poller that tails each subagent's transcript (`\u003csession\u003e/subagents/agent-*.jsonl`) for tokens, model, and liveness, live-editing the panel every `CCDK_AGENT_VIEW_TICK` seconds (default 5) until every agent lands. Fully deterministic — hooks and a poller, no model in the display loop.\n\n  The panel renders as a **footer on the live tool-trace message**: Discord edits don't reorder messages, so it stays pinned at the visual bottom and migrates automatically when the trace rotates to a fresh segment. No trace to ride (quiet channel)? It posts standalone and reposts itself below anything that displaces it. Lifecycle follows the channel's `tools` mode — `off` no panel, `collapse` deleted with the trace at Stop, `ticker`/`diffs`/`full` frozen in place as the final summary.\n\n  Registry lives in `~/.local/state/cc-discord-kit/agent_view_state.json`; a silent running agent is marked lost after 15 min; the poller self-destructs after 2 h with a `stale` marker. `!agents` / `!agent` in any channel replies with a one-shot snapshot (reserved command — never hits the shell, works even in `off` mode). Subcommands: `!agents clear` drops finished rows and keeps runners, `!agents clear all` wipes the lot, `!agents help` prints the reference. Clear is view-only — it stops *tracking* agents, it never kills a running subagent.\n\n\u003ca name=\"tool-trace-by-example\"\u003e\u003c/a\u003e\n#### Tool-trace, by example\n\nWhat actually shows up in the channel as the agent works. Everything renders inside a ` ```diff ` fence so the `+`/`-` coloring works on desktop *and* mobile.\n\n**`ticker`** — one line per tool call, headers only:\n\n```diff\n+ ● Read(src/server.py)\n+ ● Edit(src/server.py)\n+ ● Bash(npm test)\n- ● Bash(npm run deploy) FAILED\n```\n\n**`diffs`** — same headers, plus a grey summary line and the actual edit diff:\n\n```diff\n+ ● Edit(src/config.py)\n  ⎿ [+3, -1]\n- DEBUG = True\n+ DEBUG = False\n+ LOG_LEVEL = \"info\"\n+ TIMEOUT = 30\n+ ● Read(README.md)\n  ⎿ [127 lines]\n```\n\nThe `●` dot marks a **tool invocation**; bare `+`/`-` lines (no dot) are the **file diff** itself — so a green `+ DEBUG` edit line never gets confused with the green `+ ●` header above it. `collapse` renders identically while the turn runs, then deletes the whole block at Stop for a clean channel.\n\n**`full`** adds the command's stdout below the header (secrets stripped):\n\n```diff\n+ ● Bash(git status)\n```\n```\nOn branch main\nnothing to commit, working tree clean\n```\n\n\u003ca name=\"agent-panel-by-example\"\u003e\u003c/a\u003e\n#### Agent panel, by example\n\nWhen the session fans work out to subagents, the panel appears as a footer\non the live tool-trace message and re-renders every ~5 seconds.\n\n**Mid-burst** — the top-left spinner advances one frame per update\n(◐ ◓ ◑ ◒) and each running task's dot pulses ○⇄◉ in step, so you can tell\nat a glance the panel is alive, not frozen. Two consecutive frames:\n\n```diff\n+ ● Agent({\"description\":\"research bear case\",\"prompt\":\"Research the…)\n+ ● Agent({\"description\":\"audit api handlers\",\"prompt\":\"Read every…)\n+ ● Agent({\"description\":\"verify margin math\",\"prompt\":\"Re-derive…)\n```\n```\n◑ agents · mybot · 2 running · 1 done · 16.5k tok\n\n  ○  research bear case   sonnet  1m12s  12.3k\n  ○  audit api handlers   sonnet  1m12s   3.9k\n  ●  verify margin math   haiku     31s   4.2k\n```\n```\n◒ agents · mybot · 2 running · 1 done · 16.7k tok\n\n  ◉  research bear case   sonnet  1m14s  12.4k\n  ◉  audit api handlers   sonnet  1m14s   3.9k\n  ●  verify margin math   haiku     31s   4.2k\n```\n\n**Burst complete** — the spinner settles to a solid ●, every dot fills,\nthe final summary freezes in place (`ticker`/`diffs`/`full` modes) or\nvanishes with the trace (`collapse`):\n\n```\n● agents · mybot · 0 running · 3 done · 31.8k tok\n\n  ●  research bear case   sonnet  4m07s  21.4k\n  ●  audit api handlers   sonnet  3m44s   6.2k\n  ●  verify margin math   haiku     31s   4.2k\n```\n\n**Something went wrong** — a failed call renders `✗`, and an agent whose\ntranscript goes silent for 15 minutes is marked lost rather than spinning\nforever:\n\n```\n◒ agents · mybot · 1 running · 2 done · 9.1k tok\n\n  ◉  research bear case   sonnet  2m02s   7.7k\n  ✗  audit api handlers   sonnet    44s   1.4k\n  ✗  verify margin math   haiku    15m0s     0\n```\n\n**On demand** — `!agents` replies with a snapshot of the same panel, in\nany channel, any mode. A snapshot can't animate, so the status dot sits\nstatic: ◉ while anything's live, ● once everything's done:\n\n\u003e **you:** `!agents`\n\u003e **bot:**\n\u003e ```\n\u003e ◉ agents · mybot · 1 running · 2 done · 28.7k tok\n\u003e\n\u003e   ◉  research bear case   sonnet  3m21s  18.3k\n\u003e   ●  audit api handlers   sonnet  3m44s   6.2k\n\u003e   ●  verify margin math   haiku     31s   4.2k\n\u003e ```\n\n**Reset** — `!agents clear` drops the finished rows once a burst has\nlanded (`clear all` wipes runners too). View-only: the subagents keep\nrunning, they just leave the panel.\n\n\u003e **you:** `!agents clear`\n\u003e **bot:** cleared — 2 finished dropped, 1 running kept\n\n### Discord echo + guardrails\n\n- **`react_hook.py`** — emoji reaction signaller. Called with `--mode received|working|replied|terminal|memorized|compacted|crosscheck|notified` from various Claude Code hook events. State partitioned per-agent so multiple agents sharing a host don't clobber each other. Emoji map:\n\n  | Mode       | Emoji | When                                         |\n  |---         |---    |---                                            |\n  | received   | 👀    | UserPromptSubmit — agent has the message     |\n  | working    | varies | PreToolUse — type of tool (🤔 think, 🔨 edit, 🔍 research, …) |\n  | replied    | ✅    | PostToolUse on Discord reply tool            |\n  | terminal   | 🖥️    | Stop — Discord-origin turn with no reply / no content react |\n  | memorized  | 💾    | Stop — turn wrote a memory/journal entry     |\n  | compacted  | 📝    | PreCompact — context was compacted           |\n  | crosscheck | 🔀    | PostToolUse on reply tool — chat_id doesn't match any inbound origin (cross-channel leak warning) |\n  | notified   | 🔔    | External — `notify_hook` mirrored a system notification |\n\n  Terminal-mode keeps one 🖥️ per channel (sliding-forward). Suppresses 🖥️ when an explicit content react was made (the react IS the response).\n\n- **`discord_echo_guard.py`** (Stop) — blocks turn end (exit 2) when a Discord-origin user message was responded to only in terminal — no reply / react. Forces the model to actually echo to Discord. Passes through when `stop_hook_active=true` so retries don't loop. Cooperates with react_hook's terminal mode to avoid premature 🖥️ stamps.\n\n- **`paginate_guard.py`** (PreToolUse) — rejects Discord `reply` calls whose `text` would auto-paginate a fenced code block. Discord chunks at 2000 chars by character boundary, butchering backticks. The guard tells the model to write the body to `/tmp/\u003cname\u003e.md` and attach instead.\n\n\n- **`discord_mention_resolver.py`** (UserPromptSubmit) — resolves `\u003c@USER_ID\u003e` mentions in inbound Discord messages to human-readable names. Roster loaded from `~/.config/cc-discord-kit/discord_roster.json` (or `CCDK_DISCORD_ROSTER`). The running agent's own ID comes from `CCDK_BOT_DISCORD_USER_ID`. Injects a `Discord mentions resolved:` block; adds an explicit warning when this agent was addressed.\n\n### Lifecycle + system\n\n- **`inject_time.py`** (UserPromptSubmit) — injects a one-line wall-clock stamp on every prompt. Compensates for stale `currentDate` in long-running sessions.\n- **`notify_hook.py`** (Notification) — mirrors Claude Code system notifications (permission prompts, elicitation dialogs) to Discord. Target channel via `NOTIFY_CHANNEL_ID` env, else the most recent Discord-origin chat. Best-effort drops a 🔔 reaction via `react_hook --mode notified`.\n\n### Env vars (per-hook overrides)\n\nAll log + state paths default under `~/.local/state/cc-discord-kit/`. Override individually:\n\n| Var | Hook | What |\n|---|---|---|\n| `CCDK_REACT_HOOK_LOG` / `CCDK_REACT_HOOK_STATE` | react_hook | log + state paths |\n| `CCDK_NARRATE_LOG` / `CCDK_NARRATE_STATE` | narrate | log + state paths |\n| `CCDK_TOOL_WATCHER_LOG` | tool_watcher | log path |\n| `CCDK_ECHO_GUARD_LOG` | discord_echo_guard | log path |\n| `CCDK_PAGINATE_GUARD_LOG` / `CCDK_PAGINATE_GUARD_LIMIT` | paginate_guard | log path + char limit (default 1900) |\n| `CCDK_NOTIFY_HOOK_LOG` | notify_hook | log path |\n| `CCDK_STOP_HOOK_LOG` | react_hook (memorized mode) | stop-hook log path to scan for 💾 trigger |\n| `CCDK_REACT_HOOK_BIN` | notify_hook | path to react_hook entrypoint for `--mode notified` |\n| `CCDK_DISCORD_ROSTER` | discord_mention_resolver | path to user_id → name JSON |\n| `CCDK_BOT_DISCORD_USER_ID` | discord_mention_resolver | running agent's own Discord user_id |\n| `DISCORD_STATE_DIR` | several | per-agent Discord plugin state dir override |\n\n### Legacy save-intent gate\n\nThe Stop hook only fires tag handlers when one of the user's last 5 messages contains a save-intent verb (`remember`, `save`, `memory`, `forget`, `delete`, `remove`, `nuke`, `edit`, `note`, `remind`, `journal`, `pin`, `stash`, `memo`). The 5-message window catches multi-turn save flows — e.g. user says \"save our address\" in turn N, replies with the actual address in turn N+1, assistant emits `[MEMORY:]` in response to N+1 — without it, the gate would scan only the address-only message and silently block.\n\nThis prevents meta-discussion of the tag syntax from triggering real writes. To talk *about* the tags without firing them, use the `[MEMORY-EXAMPLE: ...]` / `[JOURNAL-EXAMPLE: ...]` form — those get stripped before scanning.\n\n### Discord cards\n\nWhen an explicit CLI/API save includes Discord IDs, the app posts a rendered confirmation card to the same channel as a reply:\n\n```\n💾 Memory #42 saved\ntype: feedback · name: Communication style · tags: comm, voice · about: user\n\nBody text in italics, truncated past 600 chars.\nMulti-paragraph bodies render naturally with blank lines between.\n```\n\nCards cover save (`💾`), edit (`✏️`), and delete (`🗑️`) for both memory and journal. The hook reads `DISCORD_BOT_TOKEN` from `CCDK_DISCORD_TOKEN` first, then falls back to `$CLAUDE_PLUGIN_STATE_DIR/.env` and `~/.claude/channels/discord/.env` so the same setup as the rest of your Discord integration works without extra config.\n\nIf no Discord origin is in the user message (e.g. the save happened in a terminal session), no card is posted — the CLI's own `Saved #N` output is the confirmation in that case.\n\n### Tap-to-act: one-tap controls from Discord\n\nCards aren't just read-only confirmations — the owner can **tap** them to drive the agent. Built + verified 2026-06-20.\n\n- **The trusted relay (load-bearing).** The Claude Code Discord plugin drops every bot-authored message, so a reaction the helper bot hears can't reach the asking agent's `--channels` session. The HMAC-signed plugin patch (`hooks/discord_plugin_patch.py`) opens the filter only for the helper's signed `⟦vc-relay:\u003chex\u003e⟧` messages → delivered to the session as a normal prompt. **Without this, every tap dead-ends.** The helper signs via `choice_card.deliver_pick()` against a shared secret (`~/.config/cc-discord-kit/relay_secret`, auto-generated). The helper bot id must be set in `CCDK_RELAY_HELPER_ID` — fail-closed if unset (no helper ⇒ no relayed taps are trusted).\n- **Inline-collapse (every card).** A tap **collapses its outcome into the card in place** — the prompt/hint row inside the code block is replaced with the result (e.g. `✅ approved`, `↩️ backed out`) — instead of posting a separate reply. Shared `_settle_card_inline` drives this across choice, veto, todo, and vecgrep cards. One card, one final state.\n- **Veto cards** — every save/edit/delete card carries ✅ keep / ❌. ❌ **rejects** a save (hard-delete + free the id), **undoes** an edit (revert to the before-snapshot), or **undoes** a delete (restore). 1h window; the card sticky-bumps to the channel bottom so it stays visible; the decision collapses inline. So bots can write liberally — a bad write is one tap from reversal. (`memory_veto.py`)\n- **Choice cards** — `cc-discord-kit choice ask \"\u003cq\u003e\" \"\u003copt1\u003e\" \"\u003copt2\u003e\" …` posts a numbered tap card; the owner taps a number and the pick is relayed back into the session. Two escape taps: ✏️ *type something* (free-text your own answer) and ❌ *back out*. The channel-safe replacement for `AskUserQuestion` (which the `askuser_guard` PreToolUse hook hard-blocks in bot sessions, handing back the exact `choice ask` command).\n- **Todo cards** — `cc-discord-kit todo add \"\u003ctext\u003e\"` posts an actionable card: ✅ keep (acknowledge, stays active), 🚫 cancel (resolve), ⭐ flag (toggle starred). (`todo_card.py`)\n- **Who can tap.** Cards are owner-only by default (`CCDK_OWNER_DISCORD_USER_ID`). Add more tappers for choice/veto/todo cards via `CCDK_ALLOWED_TAPPERS` (comma-separated user IDs). The vecgrep write-confirm card stays owner-only regardless — it's gated by its own confirm-channel wall, so the allowlist never widens write-confirms.\n- **Interrupt + retry** — send a lone `❌` message to stop the bot's current turn (the patched plugin writes the stop flag the stop-check hook reads); tap 🔁 on the \"🛑 Stopped\" reply to relay a retry.\n- **Plugin-mod durability** — `hooks/discord_plugin_patch.py` keeps every local plugin edit (the relay, presence, etc.) alive across plugin updates: idempotent anchor-based fixes re-applied each `SessionStart`, with a self-test that screams (stderr + a sentinel JSON) if a marker goes missing. Targets the agent's `CLAUDE_CONFIG_DIR` + `~/.claude` plugin copy by default; multi-agent hosts can list extra dirs via `CCDK_PLUGIN_DIRS`.\n\n### Thinking indicator + tool surfacing\n\nBeyond the cards, the kit surfaces a running session live (`hooks/narrate.py`, `hooks/react_hook.py`, `hooks/tool_watcher.py`):\n\n- **Working react** — one emoji on the inbound message tracks the turn: 👀 received → 🔧/🌐/🤖 working → ✅ replied.\n- **Narration** — the agent's between-tool prose, surfaced as a `🧠 Narrating…` block (per-channel `narrate` mode: `off`/`collapse`/`always`).\n- **Tool trace** — tool calls as a one-line ticker up to full diffs (per-channel `tools` mode).\n- **Thinking indicator** — a standalone `🧠 ✻ Thinking…` message (animated, escalating) that settles to `🧠 ✓ Thought for Ns`. Shows whenever a turn does real work or reasoning (a thinking block **or** a real tool call) — decoupled from the extended-thinking toggle. The \"Thought for Ns\" is real think-only time (excludes tool execution). Anchored above the tool trace + reply via the first tool's PreToolUse. Spawned by `react_hook` when extended thinking is engaged; lives in `narrate.py`'s `run_think_updater`.\n\n## Discord bot\n\n`discord_handler.py` is an optional standalone bot exposing these slash\ncommand groups:\n\n- **`/mem`** — `list`/`show`/`add`/`search`/`edit`/`pin`/`delete`/`retag`/`reabout`/`dupes`, plus `trash`/`restore` (delete is a recoverable soft-delete).\n- **`/journal`** — `list`/`show`/`add`/`search`/`pin`/`delete`.\n- **`/persona`** — `list`/`show`/`edit` agent persona files.\n- **`/bot`** — `list`/`info`/`set`/`toggle`/`narrate`/`tools`/`doctor`: manage per-channel flags for the agents in `agents.yaml`. Admin-gated via `CCDK_ADMIN_ID`.\n- **`/squad`** — `status`/`services`/`restart`/`logs`/`presence`: systemd ops over the units in `CCDK_SERVICES` (a `unit:label` CSV; empty by default).\n- **`/vecgrep`** — semantic search over your corpora (needs `vecgrep_client.py` wired; extra corpora via `CCDK_VECGREP_CORPORA`).\n\nTo set up:\n\n1. Create a Discord application + bot at \u003chttps://discord.com/developers/applications\u003e.\n2. Under **OAuth2 → URL Generator**, select scopes `bot` and `applications.commands`. The bot only needs the **default** intents — no Message Content Intent required.\n3. Invite the bot to your server with the generated URL.\n4. Set `CCDK_DISCORD_TOKEN=\u003ctoken\u003e` in `~/.config/cc-discord-kit/env`.\n5. Optionally set `CCDK_GUILD_IDS=\u003ccsv of guild IDs\u003e` for instant slash-command sync (otherwise it's ~1hr global propagation). For the fleet commands, also set `CCDK_ADMIN_ID` (your Discord user id) and populate `agents.yaml`.\n6. Run `python3 discord_handler.py` (or enable the systemd unit installed by `install.sh`).\n\n## Spoken voice (TTS / STT)\n\nAudible voice — the agent *speaking* short replies into a Discord voice channel, and/or transcribing what you say back — is doable, but deliberately **not bundled here**. It's a different shape from everything above: it needs a voice-capable Discord client (the Claude Code plugin is text-only), a TTS provider (typically a paid API), and ffmpeg/opus for audio. That's a heavy, mostly-paid dependency most users of a text-bot kit won't want, so it lives outside the kit.\n\nThe working pattern, if you want it, is a small **standalone companion process** (its own repo) that:\n\n1. joins a Discord voice channel and exposes a one-shot CLI — `say \u003ctext-channel-id\u003e \"\u003cshort reply\u003e\"` streams TTS audio into the paired voice channel (~3s to first audio with a streaming model);\n2. is gated by an explicit *voice-mode* toggle — a small JSON config pairing a text channel ↔ a voice channel, with an allowlist and a master on/off switch. No always-on presence polling (a per-reply \"is anyone in the VC\" gateway check is more annoying than it's worth);\n3. is driven by a line in your `SHARED.md` rules doc, **not a hook**: *\"while voice mode is on, run `say` with a 1–3 sentence version of your reply, then echo that same short text to the channel; on error or empty playback, fall back to a normal text reply.\"*\n\nSo the integration is a **rules-doc protocol plus an external CLI call** — which is exactly why there's no `hooks/voice.py`. It isn't hook-shaped, and forcing it into the hook layer would only drag a paid API and a second Discord client into a text/observability kit. Keep the voice helper as its own repo; the two compose cleanly without coupling.\n\n## Tests\n\n```bash\npip install pytest\npytest tests/\n```\n\nTests are fully isolated from your real data dir (`CCDK_DATA_DIR` is\nset to a `tmp_path` in `conftest.py`) and do not touch the network.\n\n## Inventory probes\n\nThe `/inventory` page uses a transport abstraction to read hook chains, crontab, and service lists from each host. Out of the box:\n\n- `LocalTransport` — runs commands directly on the same host as the server.\n- Custom transports — drop a class with `run(cmd, timeout) → (rc, stdout, stderr)` into `inventory.py` to reach other hosts. Common patterns: SSH-with-restricted-`command=` wrapper, `kubectl exec`, `docker exec`.\n\nSource of truth (`settings.json`, `crontab`, `systemd` units) stays in its canonical location. This module just reads.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeffbai996%2Fcc-discord-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeffbai996%2Fcc-discord-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeffbai996%2Fcc-discord-kit/lists"}