Skip to main content

heartreverie-create-plugin

Create a new plugin for the HeartReverie plugin system. Use when the user wants to create a plugin, add a new plugin, scaffold a plugin, or build a plugin for this project. Guides through plugin type selection, manifest creation, prompt fragments, backend/frontend modules, tag configuration, and README generation.

Jump to install

Source facts

Repository
jim60105/HeartReverie
Last source activity
June 4, 2026 at 21:35
Detected SKILL.md language
English
Stars
4
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions ยท Read-only preview
name
heartreverie-create-plugin
description
Create a new plugin for the HeartReverie plugin system. Use when the user wants to create a plugin, add a new plugin, scaffold a plugin, or build a plugin for this project. Guides through plugin type selection, manifest creation, prompt fragments, backend/frontend modules, tag configuration, and README generation.
# Create Plugin Create a new plugin for the manifest-driven plugin system. Plugins live in `plugins/<name>/` with a `plugin.json` manifest that declares capabilities. For full manifest field reference, read `references/manifest-schema.md`. --- ## Step 1: Understand the Plugin Determine what the plugin does. Derive: - **Name**: kebab-case, e.g., `my-plugin`. Must be valid: no `..`, `\0`, `/`, `\`. - **Directory**: `plugins/<name>/` - **Purpose**: What it adds to the system ## Step 2: Determine Plugin Type Select type based on what the plugin needs: | Type | Use When | |------|----------| | `prompt-only` | Only injects text into the LLM system prompt | | `full-stack` | Needs any combination of: prompt fragments, backend hooks, frontend rendering | | `hook-only` | Only needs backend lifecycle hooks (no prompt injection) | | `frontend-only` | Only browser-side rendering | When uncertain, ask the user to choose from the four types. ## Step 3: Create the Manifest Create `plugins/<name>/plugin.json` with required fields: ```json { "name": "<name>", "displayName": "<ไบบ้กžๅฏ่ฎ€ๅ็จฑ>", "version": "1.0.0", "description": "Brief description", "type": "<type>" } ``` `name` is the slug (must match directory name); `displayName` is the label rendered in the reader sidebar and settings page heading. Both are **required** โ€” a plugin missing or with a blank `displayName` is rejected during load. Then add type-appropriate optional fields per the patterns below. ### Pattern: prompt-only ```json { "name": "my-plugin", "displayName": "ๆˆ‘็š„ๅค–ๆŽ›", "version": "1.0.0", "description": "My prompt instructions", "type": "prompt-only", "promptFragments": [ { "file": "./instructions.md", "variable": "my_plugin", "priority": 100 } ] } ``` ### Pattern: full-stack (prompt + frontend + tags) ```json { "name": "my-plugin", "displayName": "ๆˆ‘็š„ๅค–ๆŽ›", "version": "1.0.0", "description": "My full-stack plugin", "type": "full-stack", "promptFragments": [ { "file": "./instructions.md", "variable": "my_plugin", "priority": 100 } ], "frontendModule": "./frontend.js", "tags": ["mytag"], "promptStripTags": ["mytag"], "displayStripTags": ["mytag"], "hooks": [ { "stage": "frontend-render", "reads": ["text"], "writes": ["text", "placeholderMap"] } ] } ``` ### Pattern: full-stack (backend + frontend + tags, no prompt) ```json { "name": "my-plugin", "displayName": "ๆˆ‘็š„ๅค–ๆŽ›", "version": "1.0.0", "description": "My processing plugin", "type": "full-stack", "backendModule": "./handler.js", "frontendModule": "./frontend.js", "tags": ["mytag"], "promptStripTags": ["mytag"], "hooks": [ { "stage": "post-response", "parallel": true, "readOnly": true, "reads": ["usage", "endpoint", "source", "pluginName", "correlationId"] }, { "stage": "frontend-render" } ] } ``` ### Pattern: hook-only ```json { "name": "my-plugin", "displayName": "ๆˆ‘็š„ๅค–ๆŽ›", "version": "1.0.0", "description": "My backend hook plugin", "type": "hook-only", "backendModule": "./handler.js", "hooks": [ { "stage": "post-response", "writes": ["content"] } ] } ``` **Critical**: The `name` field must match the directory name exactly. The `displayName` field is the user-facing label (any non-empty Unicode string after trim); UI surfaces such as the reader sidebar and `/settings/plugins/<name>` heading render `displayName` rather than the slug. **`hooks` is mandatory for new plugins.** Enumerate every `hooks.register("<stage>", ...)` call in `register()` here. The loader compares manifest vs runtime registration on startup and **rolls back the plugin load with a `declaredOnly`/`registeredOnly` error** on mismatch. The check also powers the Hook Inspector page (`/settings/hook-inspector`) and the `deno task introspect:hooks` CLI. Use `reads`/`writes` to participate in conflict detection (C1: two plugins writing the same field; C2: read with no writer). Omitting `hooks` entirely puts the plugin in legacy mode (no validation) โ€” only use this for unmaintained third-party plugins during migration. For all fields and detailed examples, read `references/manifest-schema.md`. ## Step 4: Create Prompt Fragments (if applicable) For plugins with `promptFragments`: 1. Create each Markdown file declared in the manifest (e.g., `plugins/<name>/instructions.md`) 2. Write the LLM instructions content 3. If the fragment has a `variable`, add `{{ variable_name }}` to `system.md` at the desired position Priority guide: - `10` โ€” Start of prompt (framing) - `100` โ€” Normal (default) - `800` โ€” Reinforcement (re-emphasize late in prompt) - `900` โ€” End of prompt (final instructions) For reinforcement patterns (two fragments at different priorities), see the writestyle plugin in `references/manifest-schema.md`. ## Step 5: Configure Tags (if applicable) If the LLM outputs custom XML tags (e.g., `<mytag>...</mytag>`): 1. Add tag names to `tags` array 2. Add to `promptStripTags` โ€” strip from `previousContext` so tags don't echo back to LLM 3. Add to `displayStripTags` โ€” strip from frontend display (only if the tag should not be visible to readers) **Plain text** for simple tags: `"mytag"` โ†’ auto-wrapped as `<mytag>[\s\S]*?</mytag>` **Regex** for tags with attributes: ```json "/<mytag\\b[^>]+>[\\s\\S]*?<\\/mytag>/g" ``` Usually `promptStripTags` and `displayStripTags` use the same patterns. They differ when a tag should be stripped from the LLM prompt but kept visible in the reader (or vice versa). ## Step 6: Create Backend Module (if applicable) For plugins with `backendModule`, create the handler file. Backend modules register handlers via a context object. The module must export a `register` function that receives `{ hooks, logger, getSettings }` โ€” a `PluginHooks` wrapper, a scoped `Logger`, and a **zero-arg** `getSettings()` (own-plugin only). The same own-plugin `getSettings()` is also present on the `getDynamicVariables(context)` context. `registerRoutes(context)` additionally exposes `saveSettings(values)` (validates against the schema then persists). Backend `getSettings` is NOT cross-plugin โ€” only the frontend `hooks.getSettings(name?)` / `context.getSettings(name?)` can read other plugins' settings. **JavaScript (`handler.js`):** ```javascript export function register({ hooks, logger }) { hooks.register("post-response", async (context) => { const log = context.logger ?? logger; const { content, storyDir, rootDir } = context; log.info("Processing response", { contentLength: content.length }); // Process the LLM response }, 100); } ``` **TypeScript (`handler.ts`):** ```typescript import type { PluginRegisterContext } from "../../writer/types.ts"; export function register({ hooks, logger }: PluginRegisterContext): void { hooks.register("post-response", async (context) => { const log = context.logger ?? logger; const content = context.content as string; log.info("Processing response", { contentLength: content.length }); // Process the LLM response }, 100); } ``` For the active hook stages and their context parameters, read `references/hook-api.md`. Backend code style: ESM, **double quotes**, semicolons, `async/await`, JSDoc comments. Use `context.logger ?? logger` pattern in hook handlers for request-scoped logging. The same module MAY additionally export `registerRoutes(context)` (sync or async) to mount custom HTTP endpoints under `/api/plugins/<name>/*` โ€” useful for proxying external services or backing `x-options-url` dropdowns in the settings page. See [`references/hook-api.md`](./references/hook-api.md#registerroutes-export) for the full `PluginRouteContext` contract. ## Step 7: Create Frontend Module (if applicable) For plugins with `frontendModule`, create the module. The `register` function receives **two** arguments โ€” a per-plugin `hooks` proxy and a `context` object: ```javascript import { escapeHtml } from '../_shared/utils.js'; export function register(hooks, context) { hooks.register('frontend-render', (ctx) => { const settings = hooks.getSettings(); if (settings.enabled === false) return; let index = 0; ctx.text = ctx.text.replace( /<mytag>([\s\S]*?)<\/mytag>/gi, (_match, inner) => { const placeholder = `<!--MYTAG_BLOCK_${index++}-->`; ctx.placeholderMap.set(placeholder, `<div class="my-component">${escapeHtml(inner)}</div>`); return placeholder; } ); }, 100); } ``` Key points: - `register(hooks, context)` โ€” both args are provided. Older plugins that only declare `register(hooks)` keep working. - `hooks.register(stage, handler, priority?)` and the equivalent alias `hooks.on(stage, handler, priority?)` are both available on the per-plugin proxy. New code may use either; some community plugins feature-detect via `(hooks.on ?? hooks.register)`. - `hooks.getSettings(name?)` and `context.getSettings(name?)` both return the live settings snapshot for `name` (defaults to the calling plugin). The reader hydrates settings on boot and, after a ~50 ms debounce on `plugin-settings:changed`, bumps the chapter render epoch โ€” that re-runs render-pipeline hooks (`frontend-render`, `chapter:render:after`, `chapter:dom:ready`) and re-applies `displayStripTags`. The `notification` hook is NOT re-dispatched on settings change. - `register` MAY be `async` (the loader awaits it before flipping `pluginsReady`). - `hooks.register(stage, handler, priority?)` โ€” the `originPluginName` is auto-curried by the loader proxy; do NOT pass it manually. - Frontend handlers are **synchronous** for most stages; `action-button:click` is the exception (async dispatch). Async handlers on synchronous stages are **rejected** by the dispatcher and surface a startup mismatch in Hook Inspector. If you must `await` something inside a sync stage, fire-and-forget via an IIFE: ```javascript hooks.register('frontend-render', (ctx) => { // Sync work that affects ctx must happen here, synchronously. queueMicrotask(async () => { // Side-effects that don't block the render pipeline. await refreshCacheElsewhere(); }); }); ``` - Use unique placeholder names that include the plugin name to avoid collisions. - Shared utilities live under `/plugins/_shared/`. Import them via relative paths (e.g. `import { escapeHtml } from '../_shared/utils.js';`); the server only serves files under `_shared/` and each plugin's declared `frontendModule` / `frontendStyles` / `frontendImports`. - **Sibling `.js` imports must be declared in `manifest.frontendImports`.** If `frontend.js` does `import { foo } from './helper.js';`, add `"./helper.js"` to `frontendImports` or the browser request for `/plugins/<name>/helper.js` returns `404`. Imports from `../_shared/*` do not need to be declared. See [`references/manifest-schema.md`](./references/manifest-schema.md#frontend-imports-allowlist) for the validator rules. - Frontend code style: ESM, **single quotes**, no build step, no framework โ€” plugins ship as raw JS even though the reader itself is a Vue 3 + Vite SPA. ### Frontend Hook Stages (quick reference) | Stage | Mode | When | |-------|------|------| | `frontend-render` | sync | Custom XML extraction โ†’ placeholder map (Markdown not yet parsed) |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub