{"id":50929431,"url":"https://github.com/pyyupsk/pyyupsk-wiki-bot","last_synced_at":"2026-06-17T02:31:20.032Z","repository":{"id":353504222,"uuid":"1219701086","full_name":"pyyupsk/pyyupsk-wiki-bot","owner":"pyyupsk","description":"Self-hosted Discord bot that answers questions from a personal wiki via Claude","archived":false,"fork":false,"pushed_at":"2026-04-24T06:32:07.000Z","size":64,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-24T08:25:07.265Z","etag":null,"topics":["bun","claude","claude-code","discord-bot","discord-js","knowledge-base","llm","personal-assistant","second-brain","self-hosted","typescript","wiki"],"latest_commit_sha":null,"homepage":"https://fasu.dev/writings/the-llm-wiki-pattern-a-second-brain-that-compounds","language":"TypeScript","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/pyyupsk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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-04-24T06:18:43.000Z","updated_at":"2026-04-24T06:32:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pyyupsk/pyyupsk-wiki-bot","commit_stats":null,"previous_names":["pyyupsk/pyyupsk-wiki-bot"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/pyyupsk/pyyupsk-wiki-bot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyyupsk%2Fpyyupsk-wiki-bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyyupsk%2Fpyyupsk-wiki-bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyyupsk%2Fpyyupsk-wiki-bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyyupsk%2Fpyyupsk-wiki-bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pyyupsk","download_url":"https://codeload.github.com/pyyupsk/pyyupsk-wiki-bot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyyupsk%2Fpyyupsk-wiki-bot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34431810,"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-17T02:00:05.408Z","response_time":127,"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":["bun","claude","claude-code","discord-bot","discord-js","knowledge-base","llm","personal-assistant","second-brain","self-hosted","typescript","wiki"],"created_at":"2026-06-17T02:31:19.383Z","updated_at":"2026-06-17T02:31:20.026Z","avatar_url":"https://github.com/pyyupsk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Local Wiki LLM\n\nSelf-hosted Discord bot that answers questions from a personal Obsidian-style wiki via the Claude CLI. Replies as plain text or Discord embeds depending on the answer shape.\n\n\u003e Built around the wiki-as-second-brain pattern — see [The LLM Wiki Pattern: A Second Brain That Compounds](https://fasu.dev/writings/the-llm-wiki-pattern-a-second-brain-that-compounds) for the motivation.\n\n## Features\n\n- **`/ask prompt:\u003ctext\u003e`** — query the wiki, get text or embed reply\n- **`@mention`** in any channel — same as `/ask`, with **reply-chain memory** (walks up to 6 replies back for context)\n- **`/stats [range:24h|7d|30d|all]`** — cost (USD + THB), token usage, cache hit rate, top users, model breakdown\n- **`/cleanup count:\u003cN\u003e [target:bot|user|all]`** — bulk delete recent messages; falls back to slow-delete for \u003e14-day messages\n- **`/allow add|remove|list user:\u003c@user\u003e`** — owner-managed allowlist (see Access Control)\n- **`/config get|set|reset`** — runtime-mutable settings (model, THB rate, toggles)\n- **🗑️ reaction** on any bot reply → delete it (owner or original asker)\n- **👀 reaction** auto-added on mention messages when processing starts\n- Tracks every query to sqlite with per-user attribution + cost in USD and THB\n\n## Requirements\n\n- [Bun](https://bun.sh) (runtime + package manager)\n- [Claude Code CLI](https://github.com/anthropics/claude-code) (`claude` on `PATH`)\n- A Claude subscription (Pro or Max plan, via OAuth) — this bot does **not** need an API key\n\n\u003e **Why spawn the CLI instead of calling the API?** The Anthropic API is cheaper per token, but billing is separate from the subscription — you'd pay for both a plan (for Claude Code) _and_ API credits. Spawning `claude -p` reuses the subscription OAuth auth, so queries are covered by the plan you already pay for. If you're willing to pay API credits on top, the `--bare` mode path (not enabled here) would be ~40× cheaper per query.\n- A Discord application + bot token ([Developer Portal](https://discord.com/developers/applications))\n- A local wiki directory (e.g. `~/Obsidian/pyyupsk/wiki`) with a `hotcache.md` file\n\n## Setup\n\n### 1. Clone + install\n\n```sh\ngit clone git@github.com:pyyupsk/pyyupsk-wiki-bot.git\ncd pyyupsk-wiki-bot\nbun install\n```\n\n### 2. Create the Discord app\n\nIn the [Developer Portal](https://discord.com/developers/applications):\n\n- Create a new application\n- **Bot** tab → enable **MESSAGE CONTENT INTENT** (privileged) → Save\n- Copy the **bot token** (reset if needed)\n- Copy the **Application ID** (client ID)\n\n### 3. Invite the bot\n\nOAuth2 → URL Generator:\n\n- **Scopes**: `bot`, `applications.commands`\n- **Bot Permissions**: `Send Messages`, `Read Message History`, `Manage Messages`, `Add Reactions`, `Use Slash Commands`\n- Open the generated URL, pick a server, authorize\n\n### 4. Configure env\n\n```sh\ncp .env.example .env\n```\n\nFill in `.env`:\n\n```sh\nDISCORD_TOKEN=...\nDISCORD_CLIENT_ID=...                        # Application ID\nDISCORD_GUILD_ID=...                         # optional; guild-scoped commands deploy instantly\nDISCORD_OWNER_ID=...                         # your Discord user ID (optional, enables access control)\n\nCLAUDE_BIN=claude                            # path to the claude CLI\nCLAUDE_MODEL=haiku                           # default model (haiku | sonnet | opus)\nWIKI_DIR=/home/you/Obsidian/your-wiki        # absolute path; ~/ is also expanded\n```\n\n### 5. Run\n\n```sh\nbun run dev      # watch mode (auto-restart on file change)\nbun run start    # plain run\nbun run deploy   # force-redeploy slash commands (rarely needed; syncs automatically on boot)\n```\n\nOn boot you should see: `[bot] ℹ logged in as YourBot#1234`.\n\n## Keep it alive\n\nPick one:\n\n- **tmux/zellij session** — simplest, `bun run dev` in a detached pane\n- **systemd user unit** — recommended for always-on; see below\n- **pm2** — `bun add -g pm2 \u0026\u0026 pm2 start \"bun src/index.ts\" --name wiki-bot`\n\n### systemd user unit\n\nCreate `~/.config/systemd/user/wiki-bot.service`:\n\n```ini\n[Unit]\nDescription=Wiki Discord Bot\nAfter=network.target\n\n[Service]\nWorkingDirectory=/absolute/path/to/discord-bot\nExecStart=/home/you/.bun/bin/bun src/index.ts\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n```\n\n```sh\nsystemctl --user daemon-reload\nsystemctl --user enable --now wiki-bot\njournalctl --user -u wiki-bot -f   # tail logs\n```\n\n## Access control\n\n- **`DISCORD_OWNER_ID` unset** → everyone can use all commands (dev mode)\n- **`DISCORD_OWNER_ID` set** → only the owner + allowlisted users can use commands. Unauthorized mentions are silently ignored.\n\nThe owner manages the allowlist via `/allow`:\n\n- `/allow add user:\u003c@user\u003e` — grant access\n- `/allow remove user:\u003c@user\u003e` — revoke access\n- `/allow list` — show current entries with timestamps\n\n## Runtime config\n\n`/config` (owner only) lets you change behavior without restarting:\n\n| Key               | Type                  | Default            | Effect                         |\n| ----------------- | --------------------- | ------------------ | ------------------------------ |\n| `claude_model`    | `haiku\\|sonnet\\|opus` | env `CLAUDE_MODEL` | model for new queries          |\n| `thb_rate`        | number                | `34`               | USD→THB conversion in `/stats` |\n| `reply_chain`     | boolean               | `true`             | walk reply chain for @mentions |\n| `lookup_reaction` | boolean               | `true`             | auto-react with 👀 on mentions  |\n\nSubcommands:\n\n- `/config get` — show all keys with `[override]` or `[default]` badges\n- `/config set key:\u003cchoice\u003e value:\u003ctext\u003e` — validates + stores\n- `/config reset key:\u003cchoice\u003e` — remove override\n\nOverrides persist in sqlite (`.local/bot.db`).\n\n## How it works\n\nEach query spawns `claude -p` with a minimal system prompt + your `hotcache.md` inlined. Skills, MCP servers, and CLAUDE.md auto-loading are disabled via `--setting-sources \"\"` + `--strict-mcp-config` + `--system-prompt` (replaces default). Tools are restricted to `Read,Glob,Grep` over `WIKI_DIR`.\n\nClaude returns structured JSON matching a discriminated-union schema (`{type: \"text\", content}` or `{type: \"embed\", title, description, fields, ...}`) enforced by `--json-schema`. The bot parses, validates (zod), and renders with `EmbedBuilder` or plain content.\n\n`hotcache.md` is read once per query with an mtime check — edits to the wiki are picked up automatically, but unchanged runs reuse the in-memory copy.\n\n## Project structure\n\n```tree\nsrc/\n├── index.ts                  # entry: create client, register events/commands, login\n├── env.ts                    # validated env (zod)\n├── client.ts                 # Discord client factory + SlashCommand type\n├── lib/\n│   ├── chain.ts              # reply-chain walker\n│   ├── logger.ts             # consola wrapper\n│   ├── reply.ts              # ephemeral() helper\n│   └── safe.ts               # Go-style error tuple\n├── events/\n│   ├── ready.ts              # login confirmation\n│   ├── interactionCreate.ts  # slash command dispatcher + allowlist gate\n│   ├── messageCreate.ts      # @mention handler\n│   ├── messageReactionAdd.ts # 🗑️ delete-on-reaction\n│   └── index.ts              # registerEvents()\n├── commands/\n│   ├── ask.ts                # /ask\n│   ├── stats.ts              # /stats\n│   ├── cleanup.ts            # /cleanup\n│   ├── allow.ts              # /allow\n│   ├── config.ts             # /config\n│   └── index.ts              # registerCommands()\n├── services/\n│   ├── db.ts                 # shared bun:sqlite connection + schema\n│   ├── stats.ts              # query recording + summary queries\n│   ├── allowlist.ts          # isOwner / isAllowed + mutations\n│   ├── config.ts             # runtime config getters/setters\n│   ├── deploy.ts             # slash command sync with hash cache\n│   ├── render.ts             # WikiReply → Discord message\n│   └── wiki/\n│       ├── index.ts          # askWiki() — spawns claude, parses reply\n│       ├── schemas.ts        # zod + JSON schema for structured output\n│       └── prompt.ts         # SYSTEM prompt + hotcache reader\n└── scripts/\n    └── deploy.ts             # standalone deploy entrypoint\n```\n\n## Scripts\n\n| Script              | What                                     |\n| ------------------- | ---------------------------------------- |\n| `bun run dev`       | Watch mode — auto-restart on file change |\n| `bun run start`     | Plain run                                |\n| `bun run deploy`    | Force-redeploy slash commands            |\n| `bun run check`     | Biome lint + format                      |\n| `bun run typecheck` | `tsc --noEmit`                           |\n\n## Git hooks\n\n[@pyyupsk/nit](https://www.npmjs.com/package/@pyyupsk/nit) installs hooks on `bun install`:\n\n- **pre-commit** — `biome check --write` on staged `.ts`/`.json`\n- **pre-push** — `bun run typecheck`\n- **commit-msg** — [commitlint](https://commitlint.js.org) with a custom rule: `@` only allowed inside inline code (e.g. `` `@types/bun` ``)\n\n## Cost notes\n\nEach query runs `claude -p` which loads a small system prompt + the hotcache. Typical cost on haiku: **~$0.005-0.02/query** (mostly cache creation for the first call, cached reads after). Subsequent calls within 5 minutes hit the ephemeral cache and cost even less.\n\n`/stats` shows the running total in both USD and THB (configurable rate).\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyyupsk%2Fpyyupsk-wiki-bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpyyupsk%2Fpyyupsk-wiki-bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyyupsk%2Fpyyupsk-wiki-bot/lists"}