[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JZ4HA9NE3QSHFF3WASEBVP":3,"skill-same:01M6JZ4HA9NE3QSHFF3WASEBVP":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":13,"repositoryHtmlUrl":46,"repositoryHomepageUrl":47,"ownerHtmlUrl":48,"ownerBlogUrl":47,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M6JZ4HA9NE3QSHFF3WASEBVP","discord-bridge",null,"Work on the Discord-as-client bridge (`packages\u002Fdiscord-bridge`) — the WebSocket-proxy bot that lets a Discord user play MUDdown from their DM thread. Covers the renderer invariants (envelope → embed + components), button `custom_id` encoding, the no-auto-message rule, the multi-character picker flow, and the public `\u002Ffeed` subscriber.","MUDdown\u002FMUDdown","MUDdown","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F271809841?v=4","01M6JZ2ZZ19Z0NRQS2YHZ57AHZ","01M6JZ303FSH41JFNGAAJSQGDN",2,"MIT","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fdiscord-bridge\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"discord","bridge","discord bridge","custom","bot","thread","renderer","multi","embed","id","as","message","auto","work","work on","websocket","proxy","play","packages","components","2026-11-10 10:12:30",false,"---\nname: discord-bridge\ndescription: Work on the Discord-as-client bridge (`packages\u002Fdiscord-bridge`) — the WebSocket-proxy bot that lets a Discord user play MUDdown from their DM thread. Covers the renderer invariants (envelope → embed + components), button `custom_id` encoding, the no-auto-message rule, the multi-character picker flow, and the public `\u002Ffeed` subscriber.\n---\n\n# Discord Bridge Skill\n\nYou are working on `packages\u002Fdiscord-bridge`, a stateless proxy that holds **one WebSocket session per Discord user** and translates between Discord messages and MUDdown wire envelopes. It is parallel in shape to `packages\u002Fbridge` (telnet) but speaks Discord embeds + components instead of ANSI\u002FOSC 8.\n\nThe primary play surface is the bot's **DM with each player**. Public-channel slash commands (`\u002Fplay`, `\u002Fwho`, `\u002Fswitch`, `\u002Fquit`) are convenience entry points that funnel the player into their DM session. A second, optional surface is the **public feed channel** — a one-way mirror of `:::system{scope=\"world\"}` envelopes posted by `feed-subscriber.ts`.\n\n## Files at a glance\n\n| File | Responsibility |\n|------|----------------|\n| `src\u002Fmain.ts` | Entry point — dynamic import of `bridge.ts` |\n| `src\u002Fbridge.ts` | Discord client lifecycle, interaction handlers, DM intake, slash commands, `feedSubscriber` wiring |\n| `src\u002Fbridge-policy.ts` | Pure policy functions extracted from `bridge.ts` for unit testability: `resolveGameplayInteractionCommand`, `dispatchGameplayCommand`, `formatWhoStatus`, `recordActivityIfDispatched`, `refreshReconnectTicket`, `handleSocketClose`, `handleReconnectError` |\n| `src\u002Frender.ts` | Pure `ServerMessage` → `RenderedMessage` (embeds + components). No discord.js coupling — exercised in unit tests by inspecting the returned shape |\n| `src\u002Fsessions.ts` | Discord user ID → `DiscordSession` registry (`open`\u002F`close`\u002F`touch`\u002F`values`). Connection objects are held separately in `bridge.ts` |\n| `src\u002Fcommands.ts` | Slash-command registration (`\u002Fplay`, `\u002Fwho`, `\u002Fswitch`, `\u002Fquit`) |\n| `src\u002Fidle-policy.ts` | Idle-sweep logic and tunable defaults (`IDLE_TIMEOUT_MS`, `IDLE_CHECK_INTERVAL_MS`, `findIdleSessions`) |\n| `src\u002Fdelivery-policy.ts` | Envelope delivery retry logic and defaults (`gameplayDeliveryBackoffMs`, `nextDeliveryFailure`) |\n| `src\u002Freconnect-notifier.ts` | Per-user reconnect state tracking |\n| `src\u002Ffeed.ts` | `isWorldScopeEnvelope()` defense-in-depth filter and `stripInteractiveLinks()` |\n| `src\u002Ffeed-subscriber.ts` | Read-only WS client to `\u002Ffeed`, full-jitter backoff, posts world-scope envelopes to `feedChannelId` |\n| `src\u002Fconfig.ts` | Env parsing, snowflake validation, `feedChannelId` opt-in |\n\n## Renderer invariants\n\n`render.ts` is the single source of truth for envelope → Discord shape. Keep it pure (no discord.js client, no I\u002FO) so tests assert on the returned object directly.\n\n- **One embed per envelope**, except long bodies split across multiple embeds in the same message.\n- **Embed color by block type**, mirroring the spec's ARIA-role intent:\n  - `room` → blue `0x3b82f6`\n  - `system` → red `0xef4444`\n  - `combat` → orange `0xf97316`\n  - `dialogue` → green `0x22c55e`\n  - `narrative` → neutral gray `0x9ca3af`\n- **Hard limits** (from Discord's API; codified in `DISCORD_LIMITS`):\n  - Embed description: 4096 chars (chunk via `chunkDescription`, prefer paragraph breaks then whitespace; falls back to a hard cut at 4096 when neither a `\\n\\n` nor a space boundary exists past the midpoint)\n  - Buttons per action row: 5\n  - Action rows per message: 5 (25 buttons total; overflow → select menu)\n  - Button `custom_id`: ≤ 100 chars\n- **Frontmatter and outer `:::` fences are stripped** (`stripContainerScaffolding`). Anchors are start-\u002Fend-of-string, not line, so an inner `:::` fence isn't matched as the outer close.\n- **Interactive links become components**, not embedded URLs. External URLs (http(s)) stay inline as Markdown links. Game schemes (`go:`, `cmd:`, `item:`, `npc:`, `player:`, `help:`) are pulled out and re-emitted as buttons via `resolveGameLink`.\n- **De-duplicate** same-label\u002Fsame-target buttons (`label|customId` key) so a list of exits doesn't render the same button twice.\n\n## Button `custom_id` encoding\n\nThe wire format for an interactive-link button:\n\n```\nmuddown-link:\u003Cpercent-encoded command line>\n```\n\nEncode with `encodeLinkCustomId(command)` and decode with `decodeLinkCustomId(customId)`. The encoder rejects results longer than `DISCORD_LIMITS.customIdLength` (100) by returning `undefined` — callers must skip those links rather than truncate.\n\nThe character picker uses a separate, non-collision-prone literal: `muddown-character-select`. Don't reuse `muddown-link:` for non-game-link interactions.\n\nWhen more than 25 game links are present, the renderer places the first 20 as buttons (4 rows × 5) and the next up to 25 in a string-select component with `customId = \"muddown-link-select\"` and the same `muddown-link:\u003C…>` value per option. Links beyond 45 total are dropped with a `console.warn`. Both shapes are resolved via `resolveGameplayInteractionCommand` (defined in `bridge-policy.ts`, called from `bridge.ts`).\n\n## No-auto-message rule\n\nThe bridge never sends a DM the user did not ask for.\n\n- A new player gets their first DM only after invoking `\u002Fplay` in the public hub channel — never as a side effect of joining the server, posting elsewhere, etc.\n- Reconnect \u002F idle-eviction notices go to the user **only when there is an active session that the user opened**.\n- Slash commands answer in the channel they were invoked in (or as an ephemeral reply); the bot doesn't follow up by DM unless the command's contract says so.\n- The public feed channel is the only surface that posts without a per-user trigger, and it never @-mentions or pings.\n\nThis is a hard constraint, not a courtesy. Discord aggressively rate-limits and eventually disables bots that DM unprompted.\n\n## Multi-character picker flow\n\nThe bridge supports a multi-character account model via `CharacterEntry` (local interface in `bridge.ts`). Characters are fetched from the server's `\u002Fauth\u002Fcharacters` endpoint.\n\n- **First DM**: bridge sends a \"Select the character to play:\" embed with a string-select menu, one option per character on the linked account. Selecting one opens the WebSocket session.\n- **`\u002Fswitch`** (or sending `quit` then re-DMing): tears down the active session, returns to the picker, and starts a new session under the chosen character.\n- **Single-active invariant**: only one active character per Discord user at a time, matching the WebSocket \"one session per connection\" rule on the server.\n- **Single-character accounts** skip the picker entirely and open the session directly.\n\nThe picker uses `CHARACTER_SELECT_CUSTOM_ID = \"muddown-character-select\"`. Character IDs travel as the option `value`, not encoded into the `custom_id`, because Discord's 100-char `custom_id` limit is too tight for multi-character UUIDs.\n\n## Public feed channel\n\n`feed-subscriber.ts` opens a dedicated WS to `${MUDDOWN_SERVER_URL}` with the path overwritten to `\u002Ffeed`. It only activates when `config.feedChannelId !== undefined`.\n\n- **Read-only by construction**: the server never delivers `scope=\"player\"` traffic to `\u002Ffeed`, but the subscriber **also** runs every envelope through `isWorldScopeEnvelope()` as defense-in-depth. Anything else is dropped.\n- **Interactive links are stripped** by `stripInteractiveLinks()` before the embed is built — the public channel has no per-user session, so buttons would resolve to nothing. Visible link text is retained; the `components` array is discarded.\n- **Backoff**: exponential 1s → 30s with full jitter. The client never spams reconnects.\n- **URL hygiene**: `deriveFeedUrl()` clears `search` (prevents `?ticket=…` leakage to the unauthenticated endpoint per spec §6.3.1) and `hash`, and rewrites pathname to `\u002Ffeed`.\n- **Lifecycle race guard**: in `bridge.ts`, after `await client.channels.fetch(config.feedChannelId)`, check `if (this.client !== client)` — if true, bail out without creating the subscriber. This guards against a `shutdown()` or `reset()` that ran during the fetch.\n\n## Testing\n\nVitest. 200+ tests across 11 files.\n\n- `tests\u002Frender.test.ts` — fixture-driven envelope → expected embed\u002Fcomponents shape\n- `tests\u002Fsessions.test.ts` — connection manager, idle eviction\n- `tests\u002Ffeed.test.ts` — `isWorldScopeEnvelope` and `stripInteractiveLinks` (covers `scope=\"player\"` rejection, mixed link cases, case-sensitive scheme matching)\n- `tests\u002Ffeed-subscriber.test.ts` — reconnect\u002Fbackoff, URL hygiene, lifecycle\n\nRun from the repo root:\n\n```bash\nnpx turbo run test --filter=@muddown\u002Fdiscord-bridge\n```\n\nRenderer fixtures should compare the **structural shape**, not stringified discord.js builders. The renderer never imports a discord.js builder for this reason.\n\n## What NOT to do\n\n- Don't import discord.js into `render.ts`. The renderer stays pure.\n- Don't auto-DM users who haven't opened a session.\n- Don't widen `isWorldScopeEnvelope` to accept `scope=\"player\"`. The defense-in-depth check is the second wall; relaxing it is the bug.\n- Don't reuse `muddown-link:` for non-game-link interactions. Pick a new literal.\n- Don't truncate a too-long `custom_id` — return `undefined` and skip the button.\n- Don't ship changes to bridge DM-session behaviour or `discord_rich_presence` without updating `MUDdown.wiki\u002FDiscord-Bridge.md` and `MUDdown.wiki\u002FDesktop-App.md` (per AGENTS.md).\n","# Discord Bridge Skill\n\nYou are working on `packages\u002Fdiscord-bridge`, a stateless proxy that holds **one WebSocket session per Discord user** and translates between Discord messages and MUDdown wire envelopes. It is parallel in shape to `packages\u002Fbridge` (telnet) but speaks Discord embeds + components instead of ANSI\u002FOSC 8.\n\nThe primary play surface is the bot's **DM with each player**. Public-channel slash commands (`\u002Fplay`, `\u002Fwho`, `\u002Fswitch`, `\u002Fquit`) are convenience entry points that funnel the player into their DM session. A second, optional surface is the **public feed channel** — a one-way mirror of `:::system{scope=\"world\"}` envelopes posted by `feed-subscriber.ts`.\n\n## Files at a glance\n\n| File | Responsibility |\n|------|----------------|\n| `src\u002Fmain.ts` | Entry point — dynamic import of `bridge.ts` |\n| `src\u002Fbridge.ts` | Discord client lifecycle, interaction handlers, DM intake, slash commands, `feedSubscriber` wiring |\n| `src\u002Fbridge-policy.ts` | Pure policy functions extracted from `bridge.ts` for unit testability: `resolveGameplayInteractionCommand`, `dispatchGameplayCommand`, `formatWhoStatus`, `recordActivityIfDispatched`, `refreshReconnectTicket`, `handleSocketClose`, `handleReconnectError` |\n| `src\u002Frender.ts` | Pure `ServerMessage` → `RenderedMessage` (embeds + components). No discord.js coupling — exercised in unit tests by inspecting the returned shape |\n| `src\u002Fsessions.ts` | Discord user ID → `DiscordSession` registry (`open`\u002F`close`\u002F`touch`\u002F`values`). Connection objects are held separately in `bridge.ts` |\n| `src\u002Fcommands.ts` | Slash-command registration (`\u002Fplay`, `\u002Fwho`, `\u002Fswitch`, `\u002Fquit`) |\n| `src\u002Fidle-policy.ts` | Idle-sweep logic and tunable defaults (`IDLE_TIMEOUT_MS`, `IDLE_CHECK_INTERVAL_MS`, `findIdleSessions`) |\n| `src\u002Fdelivery-policy.ts` | Envelope delivery retry logic and defaults (`gameplayDeliveryBackoffMs`, `nextDeliveryFailure`) |\n| `src\u002Freconnect-notifier.ts` | Per-user reconnect state tracking |\n| `src\u002Ffeed.ts` | `isWorldScopeEnvelope()` defense-in-depth filter and `stripInteractiveLinks()` |\n| `src\u002Ffeed-subscriber.ts` | Read-only WS client to `\u002Ffeed`, full-jitter backoff, posts world-scope envelopes to `feedChannelId` |\n| `src\u002Fconfig.ts` | Env parsing, snowflake validation, `feedChannelId` opt-in |\n\n## Renderer invariants\n\n`render.ts` is the single source of truth for envelope → Discord shape. Keep it pure (no discord.js client, no I\u002FO) so tests assert on the returned object directly.\n\n- **One embed per envelope**, except long bodies split across multiple embeds in the same message.\n- **Embed color by block type**, mirroring the spec's ARIA-role intent:\n  - `room` → blue `0x3b82f6`\n  - `system` → red `0xef4444`\n  - `combat` → orange `0xf97316`\n  - `dialogue` → green `0x22c55e`\n  - `narrative` → neutral gray `0x9ca3af`\n- **Hard limits** (from Discord's API; codified in `DISCORD_LIMITS`):\n  - Embed description: 4096 chars (chunk via `chunkDescription`, prefer paragraph breaks then whitespace; falls back to a hard cut at 4096 when neither a `\\n\\n` nor a space boundary exists past the midpoint)\n  - Buttons per action row: 5\n  - Action rows per message: 5 (25 buttons total; overflow → select menu)\n  - Button `custom_id`: ≤ 100 chars\n- **Frontmatter and outer `:::` fences are stripped** (`stripContainerScaffolding`). Anchors are start-\u002Fend-of-string, not line, so an inner `:::` fence isn't matched as the outer close.\n- **Interactive links become components**, not embedded URLs. External URLs (http(s)) stay inline as Markdown links. Game schemes (`go:`, `cmd:`, `item:`, `npc:`, `player:`, `help:`) are pulled out and re-emitted as buttons via `resolveGameLink`.\n- **De-duplicate** same-label\u002Fsame-target buttons (`label|customId` key) so a list of exits doesn't render the same button twice.\n\n## Button `custom_id` encoding\n\nThe wire format for an interactive-link button:\n\n```\nmuddown-link:\u003Cpercent-encoded command line>\n```\n\nEncode with `encodeLinkCustomId(command)` and decode with `decodeLinkCustomId(customId)`. The encoder rejects results longer than `DISCORD_LIMITS.customIdLength` (100) by returning `undefined` — callers must skip those links rather than truncate.\n\nThe character picker uses a separate, non-collision-prone literal: `muddown-character-select`. Don't reuse `muddown-link:` for non-game-link interactions.\n\nWhen more than 25 game links are present, the renderer places the first 20 as buttons (4 rows × 5) and the next up to 25 in a string-select component with `customId = \"muddown-link-select\"` and the same `muddown-link:\u003C…>` value per option. Links beyond 45 total are dropped with a `console.warn`. Both shapes are resolved via `resolveGameplayInteractionCommand` (defined in `bridge-policy.ts`, called from `bridge.ts`).\n\n## No-auto-message rule\n\nThe bridge never sends a DM the user did not ask for.\n\n- A new player gets their first DM only after invoking `\u002Fplay` in the public hub channel — never as a side effect of joining the server, posting elsewhere, etc.\n- Reconnect \u002F idle-eviction notices go to the user **only when there is an active session that the user opened**.\n- Slash commands answer in the channel they were invoked in (or as an ephemeral reply); the bot doesn't follow up by DM unless the command's contract says so.\n- The public feed channel is the only surface that posts without a per-user trigger, and it never @-mentions or pings.\n\nThis is a hard constraint, not a courtesy. Discord aggressively rate-limits and eventually disables bots that DM unprompted.\n\n## Multi-character picker flow\n\nThe bridge supports a multi-character account model via `CharacterEntry` (local interface in `bridge.ts`). Characters are fetched from the server's `\u002Fauth\u002Fcharacters` endpoint.\n\n- **First DM**: bridge sends a \"Select the character to play:\" embed with a string-select menu, one option per character on the linked account. Selecting one opens the WebSocket session.\n- **`\u002Fswitch`** (or sending `quit` then re-DMing): tears down the active session, returns to the picker, and starts a new session under the chosen character.\n- **Single-active invariant**: only one active character per Discord user at a time, matching the WebSocket \"one session per connection\" rule on the server.\n- **Single-character accounts** skip the picker entirely and open the session directly.\n\nThe picker uses `CHARACTER_SELECT_CUSTOM_ID = \"muddown-character-select\"`. Character IDs travel as the option `value`, not encoded into the `custom_id`, because Discord's 100-char `custom_id` limit is too tight for multi-character UUIDs.\n\n## Public feed channel\n\n`feed-subscriber.ts` opens a dedicated WS to `${MUDDOWN_SERVER_URL}` with the path overwritten to `\u002Ffeed`. It only activates when `config.feedChannelId !== undefined`.\n\n- **Read-only by construction**: the server never delivers `scope=\"player\"` traffic to `\u002Ffeed`, but the subscriber **also** runs every envelope through `isWorldScopeEnvelope()` as defense-in-depth. Anything else is dropped.\n- **Interactive links are stripped** by `stripInteractiveLinks()` before the embed is built — the public channel has no per-user session, so buttons would resolve to nothing. Visible link text is retained; the `components` array is discarded.\n- **Backoff**: exponential 1s → 30s with full jitter. The client never spams reconnects.\n- **URL hygiene**: `deriveFeedUrl()` clears `search` (prevents `?ticket=…` leakage to the unauthenticated endpoint per spec §6.3.1) and `hash`, and rewrites pathname to `\u002Ffeed`.\n- **Lifecycle race guard**: in `bridge.ts`, after `await client.channels.fetch(config.feedChannelId)`, check `if (this.client !== client)` — if true, bail out without creating the subscriber. This guards against a `shutdown()` or `reset()` that ran during the fetch.\n\n## Testing\n\nVitest. 200+ tests across 11 files.\n\n- `tests\u002Frender.test.ts` — fixture-driven envelope → expected embed\u002Fcomponents shape\n- `tests\u002Fsessions.test.ts` — connection manager, idle eviction\n- `tests\u002Ffeed.test.ts` — `isWorldScopeEnvelope` and `stripInteractiveLinks` (covers `scope=\"player\"` rejection, mixed link cases, case-sensitive scheme matching)\n- `tests\u002Ffeed-subscriber.test.ts` — reconnect\u002Fbackoff, URL hygiene, lifecycle\n\nRun from the repo root:\n\n```bash\nnpx turbo run test --filter=@muddown\u002Fdiscord-bridge\n```\n\nRenderer fixtures should compare the **structural shape**, not stringified discord.js builders. The renderer never imports a discord.js builder for this reason.\n\n## What NOT to do\n\n- Don't import discord.js into `render.ts`. The renderer stays pure.\n- Don't auto-DM users who haven't opened a session.\n- Don't widen `isWorldScopeEnvelope` to accept `scope=\"player\"`. The defense-in-depth check is the second wall; relaxing it is the bug.\n- Don't reuse `muddown-link:` for non-game-link interactions. Pick a new literal.\n- Don't truncate a too-long `custom_id` — return `undefined` and skip the button.\n- Don't ship changes to bridge DM-session behaviour or `discord_rich_presence` without updating `MUDdown.wiki\u002FDiscord-Bridge.md` and `MUDdown.wiki\u002FDesktop-App.md` (per AGENTS.md).\n",{"name":5,"description":7},"main","003024ca8f1bd677ce1cf158084703aa98867375",".github\u002Fskills\u002Fdiscord-bridge","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown","https:\u002F\u002Fwww.muddown.com","https:\u002F\u002Fgithub.com\u002FMUDdown","2026-08-06 08:37:36",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add MUDdown\u002FMUDdown --skill discord-bridge",{"tool":56,"command":57},"bunx","bunx skills add MUDdown\u002FMUDdown --skill discord-bridge",{"tool":59,"command":60},"pnpm","pnpm dlx skills add MUDdown\u002FMUDdown --skill discord-bridge","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fdiscord-bridge\u002FSKILL.md",true,[],[65,92,112,133,155,180],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":79,"publishedAt":38},"01M6JZ5NT4RRHMMEDQTJE0BQFK","testing","Write and maintain tests for MUDdown packages. Covers vitest for the server and parser, fixture helpers, world integrity tests, and testing conventions.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Ftesting\u002FSKILL.md",[71,76],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":77,"categoryName":78,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000010","后端与接口",[67,80,81,82,83,84,85,86,87,88,89,90,91,36],"integrity","for","tests","testing conventions","fixture","conventions","world","maintain","write","parser","vitest","server",{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":38},"01M6JZ5JM4ZZHTZ2S93S00GCKY","room-creation","Create and modify MUDdown room files. Covers the Markdown+YAML structure, frontmatter fields, container blocks, bidirectional exits, item\u002FNPC references, lighting, and region organization.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Froom-creation\u002FSKILL.md",[],[99,100,101,102,103,104,105,106,107,108,109,110,111],"room creation","creation","create","yaml","markdown","container","frontmatter","blocks","lighting","structure","organization","references","files",{"skillNo":113,"name":114,"nameZh":6,"description":115,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":123,"publishedAt":38},"01M6JZ5FHRMJ7DFRWS3H3TQY6Y","privacy","Audit and maintain privacy compliance for MUDdown. Covers the privacy policy, data collection practices, cookie\u002Fstorage usage, and compliance testing.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fprivacy\u002FSKILL.md",[118],{"categoryNo":119,"categoryName":120,"taxonomyType":121,"taxonomyTypeDesc":122},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[114,124,125,81,126,127,87,128,129,130,131,132,67],"privacy policy","collection","usage","data collection","data","compliance","audit","storage","policy",{"skillNo":134,"name":135,"nameZh":6,"description":136,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":137,"categories":138,"keywords":140,"publishedAt":38},"01M6JZ5C6GZ0H6AD33FJY399M3","persistence","Work with the database abstraction layer and account\u002Fcharacter\u002Fworld persistence. Covers the GameDatabase interface, SQLite adapter, save\u002Fload lifecycle, auth sessions, and testing patterns.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fpersistence\u002FSKILL.md",[139],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},[135,141,142,143,144,145,146,86,147,31,148,149,150,151,152,153,154,67],"interface","layer","load","patterns","auth","adapter","character","save","sqlite","lifecycle","testing patterns","database","with","sessions",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":159,"categories":160,"keywords":165,"publishedAt":38},"01M6JZ5836SXKR67RCMC83VRDJ","osc8-bridge","Add or modify OSC 8 hyperlink capabilities in the telnet bridge — NEW-ENVIRON (RFC 1572) negotiation, Mudlet\u002FFado\u002FMudForge `send:` URIs, and per-scheme tooltip\u002Fmenu metadata. Covers the capability pipeline from telnet negotiation through renderer output, the sanitization contract, and the word-wrap invariant.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fosc8-bridge\u002FSKILL.md",[161,164],{"categoryNo":162,"categoryName":163,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000002","文档处理",{"categoryNo":119,"categoryName":120,"taxonomyType":121,"taxonomyTypeDesc":122},[166,19,167,168,169,170,171,172,173,24,174,175,176,177,178,179],"osc8 bridge","Word","RFC","pipeline","negotiation","metadata","capabilities","wrap","sanitization","scheme","output","add","osc","contract",{"skillNo":181,"name":182,"nameZh":6,"description":183,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":184,"categories":185,"keywords":186,"publishedAt":38},"01M6JZ548282CBBAQBAJ8HNPG2","oauth-provider","Add a new OAuth\u002FOIDC identity provider to MUDdown. Covers the shared type, auth switch statements, server config, login page button, env vars, tests, and local verification.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Foauth-provider\u002FSKILL.md",[],[187,188,189,82,190,177,191,192,193,194,195,196,197,198,91,199,200,201,145],"provider","oauth provider","OAuth","login","env","type","shared","local","switch","page","oidc","env vars","verification","identity","config"]