mirror of
https://github.com/evdboom/Bindery.git
synced 2026-07-22 06:49:36 +00:00
feat: Enhance Bindery MCP with new translation and memory management tools
- Updated README.md to reflect new features including session memory and enhanced translation management. - Modified manifest.json to include new tools: init_workspace, setup_ai_files, get_translation, memory_list, memory_append, and memory_compact. - Added activation event for VS Code extension on startup. - Implemented new AI setup features in ai-setup.ts, including versioning for AI files and template rendering. - Updated extension.ts to check for AI setup version and prompt users to update if necessary. - Expanded mcp.ts to register new tools for translation and memory management, enhancing the overall functionality of the Bindery MCP.
This commit is contained in:
parent
00c71050b4
commit
eb73ef7e5c
13 changed files with 1518 additions and 475 deletions
176
.github/copilot-instructions.md
vendored
Normal file
176
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# GitHub Copilot — Bindery
|
||||
|
||||
Bindery is a **VS Code extension** + **MCP server** for markdown book authoring.
|
||||
TypeScript throughout. Two independent packages — keep them in sync when adding features.
|
||||
|
||||
---
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
vscode-ext/ ← VS Code extension (published to Marketplace)
|
||||
src/
|
||||
extension.ts ← activation, all commands, format-on-save handler
|
||||
workspace.ts ← reads/writes .bindery/settings.json + translations.json
|
||||
merge.ts ← chapter discovery, markdown assembly, pandoc/LibreOffice
|
||||
format.ts ← typography transforms (curly quotes, em-dash, ellipsis)
|
||||
mcp.ts ← vscode.lm.registerTool registrations + mcp.json writer
|
||||
ai-setup.ts ← generates CLAUDE.md, copilot-instructions.md, skills, etc.
|
||||
|
||||
mcp-ts/ ← Standalone MCP server (also bundled inside vscode-ext)
|
||||
src/
|
||||
index.ts ← McpServer entry point, all server.registerTool() calls
|
||||
tools.ts ← one exported function per tool (pure: root + args → string)
|
||||
registry.ts ← book registry (--book flags + BINDERY_BOOKS env var)
|
||||
search.ts ← BM25 index build/load/search + optional Ollama reranking
|
||||
docstore.ts ← file discovery and chunking
|
||||
format.ts ← typography (shared logic, duplicated from vscode-ext)
|
||||
|
||||
mcpb/ ← Claude Desktop extension package (mcpb manifest + bundled server)
|
||||
manifest.json ← mcpb manifest v0.3 — tools list, user_config, privacy_policies
|
||||
server/ ← bundled output (copy of mcp-ts/out/) — DO NOT hand-edit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP server — rules for every tool change
|
||||
|
||||
The MCP server is published to the Anthropic MCPB directory. These rules are hard requirements to stay publishable.
|
||||
|
||||
### Every tool MUST have exactly one annotation
|
||||
```typescript
|
||||
annotations: { readOnlyHint: true } // reads only — never writes files
|
||||
annotations: { destructiveHint: true } // writes, creates, or modifies files
|
||||
```
|
||||
No annotation = mcpb submission rejection. Both annotations on one tool = also wrong.
|
||||
|
||||
| Tool behaviour | Annotation |
|
||||
|---|---|
|
||||
| list, get, read, search, status checks | `readOnlyHint: true` |
|
||||
| write, append, overwrite, format, commit, create | `destructiveHint: true` |
|
||||
|
||||
### Adding a new tool — checklist
|
||||
When adding a tool, touch **all four** of these: missing any breaks one surface.
|
||||
|
||||
1. **`mcp-ts/src/tools.ts`** — add the implementation function (`toolXxx(root, args): string`)
|
||||
2. **`mcp-ts/src/index.ts`** — `server.registerTool(...)` with Zod schema, description, and ONE annotation
|
||||
3. **`vscode-ext/src/mcp.ts`** — `vscode.lm.registerTool('bindery_xxx', ...)` + add input interface + add to `McpTools`
|
||||
4. **`vscode-ext/package.json`** — add entry to `languageModelTools[]` with JSON Schema input
|
||||
5. **`mcpb/manifest.json`** — add `{ "name": "xxx", "description": "..." }` to `tools[]`
|
||||
|
||||
### mcpb manifest rules
|
||||
- `manifest_version` must stay `"0.3"` or higher
|
||||
- `privacy_policies` array must contain a valid HTTPS URL — do not remove it
|
||||
- `tools[]` is a flat list of `{ name, description }` — one entry per tool, no inputSchema here
|
||||
- The `server/` folder is a bundled copy of `mcp-ts/out/` — update it when releasing
|
||||
|
||||
---
|
||||
|
||||
## MCP tools reference
|
||||
|
||||
All tools take a `book` argument (string name from `--book` config). The VS Code surface has no `book` arg — it uses the open workspace root.
|
||||
|
||||
| Tool | Annotation | Description |
|
||||
|---|---|---|
|
||||
| `list_books` | readOnly | List configured books |
|
||||
| `identify_book` | readOnly | Match a working directory to a book |
|
||||
| `health` | readOnly | Server/index/embedding status |
|
||||
| `init_workspace` | destructive | Create or update .bindery/settings.json + translations.json |
|
||||
| `index_build` | destructive | Build/rebuild BM25 search index |
|
||||
| `index_status` | readOnly | Index chunk count and build time |
|
||||
| `get_text` | readOnly | Read file by relative path, optional line range |
|
||||
| `get_chapter` | readOnly | Full chapter by number + language |
|
||||
| `get_overview` | readOnly | Chapter structure (acts, chapters, titles) |
|
||||
| `get_notes` | readOnly | Notes/ and Details_*.md, filterable |
|
||||
| `search` | readOnly | BM25 full-text search with ranked snippets |
|
||||
| `retrieve_context` | readOnly | Semantic passage retrieval |
|
||||
| `format` | destructive | Apply typography formatting to file/folder |
|
||||
| `get_review_text` | destructive | Git diff with optional autoStage |
|
||||
| `git_snapshot` | destructive | Git commit of story/notes/arc changes |
|
||||
| `get_translation` | readOnly | List all rules for a language, or look up a specific word (forgiving) |
|
||||
| `add_translation` | destructive | Add/update .bindery/translations.json rule |
|
||||
| `memory_list` | readOnly | List Notes/Memories/ files with line counts |
|
||||
| `memory_append` | destructive | Append dated session entry to a memory file |
|
||||
| `memory_compact` | destructive | Overwrite memory file with summary (backs up original) |
|
||||
|
||||
---
|
||||
|
||||
## VS Code extension — key design rules
|
||||
|
||||
- **Generic**: no project-specific strings in source (use "Book", not a title)
|
||||
- **Config priority**: `.bindery/settings.json` → VS Code workspace settings → VS Code user settings → code defaults
|
||||
- **Machine paths only in user settings**: `pandocPath`, `libreOfficePath` — never in workspace settings
|
||||
- **Substitution tiers**: built-in (`merge.ts`) → user-general (`bindery.generalSubstitutions`) → project (`.bindery/translations.json`). Later tiers win.
|
||||
- **formatOnSave**: only fires for files inside the configured `storyFolder`
|
||||
- **Activation**: `onStartupFinished` (to ensure LM tools register at launch) + `onLanguage:markdown`
|
||||
- **Command namespace**: all commands are `bindery.*` — must match in both `package.json` and `extension.ts`
|
||||
|
||||
## VS Code extension — commands
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `bindery.init` | Create `.bindery/settings.json` + `translations.json` |
|
||||
| `bindery.setupAI` | Generate CLAUDE.md / copilot-instructions.md / skills / AGENTS.md |
|
||||
| `bindery.formatDocument` / `bindery.formatFolder` | Typography formatting |
|
||||
| `bindery.mergeMarkdown/Docx/Epub/Pdf/All` | Merge chapters → output format |
|
||||
| `bindery.findProbableUsToUkWords` | Surface probable US spellings in EN source |
|
||||
| `bindery.addUkReplacement` | Add a substitution rule |
|
||||
| `bindery.openTranslations` | Open translations.json |
|
||||
| `bindery.registerMcp` | Write .vscode/mcp.json for Claude/Codex MCP discovery |
|
||||
|
||||
---
|
||||
|
||||
## AI setup (`ai-setup.ts`)
|
||||
|
||||
`setupAiFiles()` generates per-target instruction files. Each target is independent.
|
||||
|
||||
| Target | Output |
|
||||
|---|---|
|
||||
| `claude` | `CLAUDE.md` + `.claude/skills/<skill>/SKILL.md` for each selected skill |
|
||||
| `copilot` | `.github/copilot-instructions.md` |
|
||||
| `cursor` | `.cursor/rules` |
|
||||
| `agents` | `AGENTS.md` |
|
||||
|
||||
Skills: `review`, `brainstorm`, `memory`, `translate`, `status`, `continuity`, `read_aloud`.
|
||||
|
||||
The **memory skill** uses `memory_list` → `memory_append` → `memory_compact`. Do not fall back to `get_text` + Edit tool for memory writes.
|
||||
|
||||
### AI setup versioning
|
||||
`AI_SETUP_VERSION` (integer constant in `ai-setup.ts`) controls staleness detection.
|
||||
|
||||
- `setupAiFiles()` stamps `.bindery/ai-version.json` with the current version after every run.
|
||||
- On extension activation, if `.bindery/settings.json` exists and the stamped version is older than `AI_SETUP_VERSION`, the user is notified and offered an "Update now" button that opens `bindery.setupAI`.
|
||||
- **Bump `AI_SETUP_VERSION` by 1 whenever skill templates or instruction file templates change significantly** (i.e. existing users should regenerate). Small copy fixes do not require a bump.
|
||||
|
||||
---
|
||||
|
||||
## Memory system
|
||||
|
||||
Memory files live in `.bindery/memories/` inside the book root.
|
||||
- `global.md` — cross-chapter decisions
|
||||
- `chXX.md` — per-chapter notes (e.g. `ch10.md`)
|
||||
- `archive/` — compacted originals (auto-created by `memory_compact`)
|
||||
|
||||
Format of an appended entry (stamped by `memory_append`, not the caller):
|
||||
```
|
||||
## Session YYYY-MM-DD — [title]
|
||||
[content lines]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# MCP server
|
||||
cd mcp-ts && npm run build # outputs to mcp-ts/out/
|
||||
|
||||
# VS Code extension
|
||||
cd vscode-ext && npm run compile # outputs to vscode-ext/out/
|
||||
|
||||
# Type-check only (no emit)
|
||||
cd mcp-ts && npx tsc --noEmit
|
||||
cd vscode-ext && npx tsc --noEmit
|
||||
```
|
||||
|
||||
Before releasing mcpb: copy `mcp-ts/out/` → `mcpb/server/`.
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -76,6 +76,9 @@ jobs:
|
|||
npm ci
|
||||
npm run compile
|
||||
|
||||
- name: Sync templates to vscode-ext
|
||||
run: cp mcp-ts/src/templates.ts vscode-ext/src/ai-setup-templates.ts
|
||||
|
||||
- name: Set version from tag and publish extension
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -26,3 +26,7 @@ Thumbs.db
|
|||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# misc
|
||||
.FluxCoreInfo/
|
||||
ai-setup-templates.ts
|
||||
147
mcp-ts/src/aisetup.ts
Normal file
147
mcp-ts/src/aisetup.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* AI instruction file generation for Bindery (MCP server).
|
||||
*
|
||||
* Generates CLAUDE.md, .github/copilot-instructions.md, .cursor/rules,
|
||||
* AGENTS.md, and .claude/skills/<skill>/SKILL.md from the book's
|
||||
* .bindery/settings.json.
|
||||
*
|
||||
* Templates live in templates.ts — the single source of truth.
|
||||
* vscode-ext/src/ai-setup.ts imports its copy via ai-setup-templates.ts.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { renderTemplate, type TemplateContext } from './templates.js';
|
||||
|
||||
// ─── Public types ─────────────────────────────────────────────────────────────
|
||||
|
||||
export type AiTarget = 'claude' | 'copilot' | 'cursor' | 'agents';
|
||||
|
||||
export type SkillTemplate =
|
||||
| 'review'
|
||||
| 'brainstorm'
|
||||
| 'memory'
|
||||
| 'translate'
|
||||
| 'status'
|
||||
| 'continuity'
|
||||
| 'read_aloud';
|
||||
|
||||
export const ALL_SKILLS: SkillTemplate[] = [
|
||||
'review', 'brainstorm', 'memory', 'translate', 'status', 'continuity', 'read_aloud',
|
||||
];
|
||||
|
||||
export interface AiSetupOptions {
|
||||
root: string;
|
||||
targets: AiTarget[];
|
||||
skills?: SkillTemplate[];
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
export interface AiSetupResult {
|
||||
created: string[];
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bump this integer whenever templates change significantly enough that
|
||||
* existing users should regenerate their AI files.
|
||||
* Must be kept in sync with AI_SETUP_VERSION in vscode-ext/src/ai-setup.ts.
|
||||
*/
|
||||
export const AI_SETUP_VERSION = 4;
|
||||
|
||||
// ─── Settings types ───────────────────────────────────────────────────────────
|
||||
|
||||
interface Settings {
|
||||
bookTitle?: string | Record<string, string>;
|
||||
author?: string;
|
||||
description?: string;
|
||||
genre?: string;
|
||||
targetAudience?: string;
|
||||
storyFolder?: string;
|
||||
languages?: Array<{ code: string; folderName: string }>;
|
||||
}
|
||||
|
||||
// ─── Context builder ──────────────────────────────────────────────────────────
|
||||
|
||||
function buildContext(s: Settings): TemplateContext {
|
||||
const title = (typeof s.bookTitle === 'string' ? s.bookTitle : undefined) ?? 'Untitled';
|
||||
const author = s.author ?? '';
|
||||
const description = s.description ?? '';
|
||||
const genre = s.genre ?? '';
|
||||
const audience = s.targetAudience ?? '';
|
||||
const storyFolder = s.storyFolder ?? 'Story';
|
||||
const languages = s.languages ?? [];
|
||||
|
||||
const langList = languages.length > 0
|
||||
? languages.map((l, i) => i === 0 ? `${l.code} (source)` : `${l.code} (translation)`).join(', ')
|
||||
: 'EN (source)';
|
||||
|
||||
return {
|
||||
title, author, description, genre, audience,
|
||||
storyFolder, notesFolder: 'Notes', arcFolder: 'Arc',
|
||||
memoriesFolder: '.bindery/memories',
|
||||
languages, langList,
|
||||
hasMultiLang: languages.length > 1,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
|
||||
const { root, targets, skills = ALL_SKILLS, overwrite = false } = options;
|
||||
|
||||
const settingsPath = path.join(root, '.bindery', 'settings.json');
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
throw new Error('settings.json not found — run init_workspace first.');
|
||||
}
|
||||
const settings: Settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as Settings;
|
||||
const ctx = buildContext(settings);
|
||||
|
||||
const result: AiSetupResult = { created: [], skipped: [] };
|
||||
|
||||
for (const target of targets) {
|
||||
switch (target) {
|
||||
case 'claude':
|
||||
writeFile(root, 'CLAUDE.md', renderTemplate('claude', ctx), overwrite, result);
|
||||
for (const skill of skills) {
|
||||
writeFile(root, path.join('.claude', 'skills', skill, 'SKILL.md'), renderTemplate(skill, ctx), overwrite, result);
|
||||
}
|
||||
break;
|
||||
case 'copilot':
|
||||
writeFile(root, path.join('.github', 'copilot-instructions.md'), renderTemplate('copilot', ctx), overwrite, result);
|
||||
break;
|
||||
case 'cursor':
|
||||
writeFile(root, path.join('.cursor', 'rules'), renderTemplate('cursor', ctx), overwrite, result);
|
||||
break;
|
||||
case 'agents':
|
||||
writeFile(root, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stampAiVersion(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function writeFile(root: string, relPath: string, content: string, overwrite: boolean, result: AiSetupResult): void {
|
||||
const full = path.join(root, relPath);
|
||||
if (fs.existsSync(full) && !overwrite) {
|
||||
result.skipped.push(relPath);
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||
fs.writeFileSync(full, content, 'utf-8');
|
||||
result.created.push(relPath);
|
||||
}
|
||||
|
||||
function stampAiVersion(root: string): void {
|
||||
const dir = path.join(root, '.bindery');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'ai-version.json'),
|
||||
JSON.stringify({ version: AI_SETUP_VERSION }, null, 2) + '\n',
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
|
@ -27,7 +27,13 @@ import {
|
|||
toolFormat,
|
||||
toolGetReviewText,
|
||||
toolGitSnapshot,
|
||||
toolGetTranslation,
|
||||
toolAddTranslation,
|
||||
toolInitWorkspace,
|
||||
toolSetupAiFiles,
|
||||
toolMemoryList,
|
||||
toolMemoryAppend,
|
||||
toolMemoryCompact,
|
||||
} from './tools.js';
|
||||
import { resolveBook, listBooks, findBookByPath } from './registry.js';
|
||||
|
||||
|
|
@ -249,6 +255,22 @@ server.registerTool('git_snapshot', {
|
|||
try { return ok(toolGitSnapshot(resolveBook(book).root, { message })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('get_translation', {
|
||||
title: 'Get Translation',
|
||||
description:
|
||||
'Look up translation/substitution rules in .bindery/translations.json. ' +
|
||||
'Without a word, lists all rules for the language. ' +
|
||||
'With a word, does a forgiving case-insensitive lookup including plural and inflected forms.',
|
||||
inputSchema: {
|
||||
book: bookSchema,
|
||||
language: z.string().describe('Language key, label, or code (e.g. "nl", "en-gb", "British English")'),
|
||||
word: z.string().optional().describe('Word or term to look up (optional — omit to list all rules)'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, language, word }) => {
|
||||
try { return ok(toolGetTranslation(resolveBook(book).root, { language, word })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('add_translation', {
|
||||
title: 'Add Translation',
|
||||
description:
|
||||
|
|
@ -266,6 +288,86 @@ server.registerTool('add_translation', {
|
|||
try { return ok(toolAddTranslation(resolveBook(book).root, { langKey, from, to })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('init_workspace', {
|
||||
title: 'Init Workspace',
|
||||
description:
|
||||
'Create or update .bindery/settings.json and .bindery/translations.json. ' +
|
||||
'All arguments are optional — smart defaults are used for any omitted values. ' +
|
||||
'Safe to run on an existing workspace: existing settings are preserved unless explicitly overridden. ' +
|
||||
'Detects language folders in the story directory automatically.',
|
||||
inputSchema: {
|
||||
book: bookSchema,
|
||||
bookTitle: z.string().optional().describe('Book title (defaults to folder name)'),
|
||||
author: z.string().optional().describe('Author name'),
|
||||
storyFolder: z.string().optional().describe('Story folder name relative to root (default: Story)'),
|
||||
genre: z.string().optional().describe('Genre, e.g. sci-fi/fantasy'),
|
||||
description: z.string().optional().describe('One-line description used in AI instruction files'),
|
||||
targetAudience: z.string().optional().describe('Target audience, e.g. 12+ or adults'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, bookTitle, author, storyFolder, genre, description, targetAudience }) => {
|
||||
try { return ok(toolInitWorkspace(resolveBook(book).root, { bookTitle, author, storyFolder, genre, description, targetAudience })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('setup_ai_files', {
|
||||
title: 'Setup AI Files',
|
||||
description:
|
||||
'Generate AI assistant instruction files (CLAUDE.md, .github/copilot-instructions.md, ' +
|
||||
'.cursor/rules, AGENTS.md) and Claude skill templates from .bindery/settings.json. ' +
|
||||
'Run init_workspace first. Safe to run multiple times — skips existing files unless overwrite is true.',
|
||||
inputSchema: {
|
||||
book: bookSchema,
|
||||
targets: z.array(z.string()).optional().describe('Which files to generate: claude, copilot, cursor, agents. Default: all.'),
|
||||
skills: z.array(z.string()).optional().describe('Which Claude skills to generate: review, brainstorm, memory, translate, status, continuity, read_aloud. Default: all.'),
|
||||
overwrite: z.boolean().optional().describe('Overwrite existing files? Default false (skip existing).'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, targets, skills, overwrite }) => {
|
||||
try { return ok(toolSetupAiFiles(resolveBook(book).root, { targets, skills, overwrite })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('memory_list', {
|
||||
title: 'Memory List',
|
||||
description: 'List all session memory files in .bindery/memories/. Returns each filename and its line count.',
|
||||
inputSchema: { book: bookSchema },
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book }) => {
|
||||
try { return ok(toolMemoryList(resolveBook(book).root)); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('memory_append', {
|
||||
title: 'Memory Append',
|
||||
description:
|
||||
'Append a dated session entry to a memory file in .bindery/memories/. ' +
|
||||
'Creates the file if it does not exist. ' +
|
||||
'The tool stamps the current date; supply a short title and the content to record.',
|
||||
inputSchema: {
|
||||
book: bookSchema,
|
||||
file: z.string().describe('Filename within .bindery/memories/, e.g. global.md or ch10.md'),
|
||||
title: z.string().describe('Short session title describing the topic'),
|
||||
content: z.string().describe('Text to record under this session entry'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, file, title, content }) => {
|
||||
try { return ok(toolMemoryAppend(resolveBook(book).root, { file, title, content })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
server.registerTool('memory_compact', {
|
||||
title: 'Memory Compact',
|
||||
description:
|
||||
'Overwrite a memory file with a compacted version supplied by the model. ' +
|
||||
'The original is backed up to .bindery/memories/archive/ before overwriting. ' +
|
||||
'Use this when a memory file has grown too large and needs to be summarised.',
|
||||
inputSchema: {
|
||||
book: bookSchema,
|
||||
file: z.string().describe('Filename within .bindery/memories/, e.g. global.md'),
|
||||
compacted_content: z.string().describe('Full replacement content (model-supplied summary)'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, file, compacted_content }) => {
|
||||
try { return ok(toolMemoryCompact(resolveBook(book).root, { file, compacted_content })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
// ─── Start ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
|
|
|||
533
mcp-ts/src/templates.ts
Normal file
533
mcp-ts/src/templates.ts
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
/**
|
||||
* Bindery AI instruction file templates.
|
||||
*
|
||||
* SINGLE SOURCE OF TRUTH — do not edit the copy in vscode-ext/src/.
|
||||
* The copy at vscode-ext/src/ai-setup-templates.ts is generated automatically:
|
||||
* cp mcp-ts/src/templates.ts vscode-ext/src/ai-setup-templates.ts
|
||||
*
|
||||
* This file has zero imports. It exports only TemplateContext and renderTemplate.
|
||||
*/
|
||||
|
||||
// ─── Context ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface TemplateContext {
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
genre: string;
|
||||
audience: string;
|
||||
storyFolder: string;
|
||||
notesFolder: string;
|
||||
arcFolder: string;
|
||||
memoriesFolder: string;
|
||||
languages: Array<{ code: string; folderName: string }>;
|
||||
langList: string;
|
||||
hasMultiLang: boolean;
|
||||
}
|
||||
|
||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a named template with the given context.
|
||||
*
|
||||
* Top-level file templates: 'claude', 'copilot', 'cursor', 'agents'
|
||||
* Skill templates: 'review', 'brainstorm', 'memory', 'translate',
|
||||
* 'status', 'continuity', 'read_aloud'
|
||||
*/
|
||||
export function renderTemplate(name: string, ctx: TemplateContext): string {
|
||||
switch (name) {
|
||||
case 'claude': return claudeMd(ctx);
|
||||
case 'copilot': return copilotMd(ctx);
|
||||
case 'cursor': return cursorRules(ctx);
|
||||
case 'agents': return agentsMd(ctx);
|
||||
case 'review': return skillReview(ctx);
|
||||
case 'brainstorm': return skillBrainstorm(ctx);
|
||||
case 'memory': return skillMemory(ctx);
|
||||
case 'translate': return skillTranslate(ctx);
|
||||
case 'status': return skillStatus(ctx);
|
||||
case 'continuity': return skillContinuity(ctx);
|
||||
case 'read_aloud': return skillReadAloud(ctx);
|
||||
default: return `Unknown template: ${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function audienceNote(ctx: TemplateContext): string {
|
||||
return ctx.audience ? `Target audience: ${ctx.audience}.` : '';
|
||||
}
|
||||
|
||||
function languageSection(ctx: TemplateContext): string {
|
||||
if (!ctx.hasMultiLang) { return ''; }
|
||||
return `\nLanguages: ${ctx.langList}.\n`;
|
||||
}
|
||||
|
||||
// ─── Top-level templates ──────────────────────────────────────────────────────
|
||||
|
||||
function claudeMd(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, memoriesFolder } = ctx;
|
||||
const lines: string[] = [
|
||||
`# Claude — ${title}`,
|
||||
'',
|
||||
'## Project',
|
||||
];
|
||||
if (genre) { lines.push(`Genre: ${genre}.`); }
|
||||
if (description) { lines.push(description); }
|
||||
if (ctx.audience){ lines.push(audienceNote(ctx)); }
|
||||
if (author) { lines.push(`Author: ${author}.`); }
|
||||
lines.push(languageSection(ctx));
|
||||
|
||||
lines.push(
|
||||
'## Start of session',
|
||||
`1. Read COWORK.md (if present) for current focus and context.`,
|
||||
`2. Read ${memoriesFolder}/global.md for cross-chapter decisions.`,
|
||||
`3. If working on a specific chapter, read ${memoriesFolder}/chXX.md if it exists.`,
|
||||
'',
|
||||
'## Repo layout',
|
||||
'```',
|
||||
`${arcFolder}/ ← story arc files (Overall.md, Act_I_*.md, Act_II_*.md, Act_III_*.md)`,
|
||||
`${notesFolder}/ ← story bible (characters, world, translation table, memories)`,
|
||||
`${storyFolder}/`,
|
||||
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters (one .md per chapter)`),
|
||||
'```',
|
||||
'',
|
||||
'## Writing rules',
|
||||
'- Never rewrite paragraphs unless explicitly asked. Suggest edits only.',
|
||||
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not prose.',
|
||||
'- Quotation marks and dashes in chapter files are managed by the Bindery extension. Do not flag these as formatting errors.',
|
||||
);
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Content is aimed at ${ctx.audience}. Keep language accessible and themes age-appropriate.`);
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'## Key reference files',
|
||||
'| File | Contains |',
|
||||
'|---|---|',
|
||||
`| \`${arcFolder}/Overall.md\` | Full story arc |`,
|
||||
`| \`${arcFolder}/Act_I_*.md\`, \`Act_II_*.md\`, \`Act_III_*.md\` | Per-act arc details |`,
|
||||
`| \`${notesFolder}/Details_Characters.md\` | Character profiles |`,
|
||||
`| \`${notesFolder}/Details_World_and_Magic.md\` | World rules and magic system |`,
|
||||
`| \`${notesFolder}/Details_Translation_notes.md\` | Term translations / glossary |`,
|
||||
`| \`${memoriesFolder}/global.md\` | Cross-session decisions |`,
|
||||
'',
|
||||
'## Available skills',
|
||||
'Use these slash commands to trigger structured workflows:',
|
||||
'| Command | Purpose |',
|
||||
'|---|---|',
|
||||
'| `/review` | Review a chapter for language, arc consistency, and age-appropriateness |',
|
||||
'| `/brainstorm` | Generate plot/character/scene ideas |',
|
||||
'| `/memory` | Update memory files and compact if needed |',
|
||||
'| `/translate` | Assist with chapter translation |',
|
||||
'| `/status` | Book progress snapshot |',
|
||||
'| `/continuity` | Check a chapter for consistency errors |',
|
||||
'| `/read-aloud` | Test how a passage reads when spoken |',
|
||||
);
|
||||
return lines.filter(l => l !== '\n').join('\n') + '\n';
|
||||
}
|
||||
|
||||
function copilotMd(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder } = ctx;
|
||||
const lines: string[] = [`# GitHub Copilot — ${title}`, ''];
|
||||
if (genre || description || ctx.audience) {
|
||||
lines.push('## Project');
|
||||
if (genre) { lines.push(`${genre} novel.`); }
|
||||
if (description) { lines.push(description); }
|
||||
if (ctx.audience){ lines.push(audienceNote(ctx)); }
|
||||
if (author) { lines.push(`Author: ${author}.`); }
|
||||
lines.push(languageSection(ctx), '');
|
||||
}
|
||||
lines.push(
|
||||
'## Repo layout',
|
||||
'```',
|
||||
`${arcFolder}/ ← story arc files`,
|
||||
`${notesFolder}/ ← story bible, translation table, memories`,
|
||||
`${storyFolder}/`,
|
||||
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters`),
|
||||
'```',
|
||||
'',
|
||||
'## Writing guidelines',
|
||||
'- HTML comments `<!-- -->` in chapter files are writer notes — treat as context only.',
|
||||
'- Quotation marks and dashes are managed by the Bindery VS Code extension. Do not normalise them.',
|
||||
'- Check `Notes/Details_Translation_notes.md` before using or translating world-specific terms.',
|
||||
);
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Content targets ${ctx.audience}. Keep vocabulary accessible and themes appropriate.`);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function cursorRules(ctx: TemplateContext): string {
|
||||
const { title, storyFolder, notesFolder, arcFolder, memoriesFolder } = ctx;
|
||||
const lines: string[] = [
|
||||
`# Cursor rules — ${title}`,
|
||||
'',
|
||||
`Story folder: \`${storyFolder}/\``,
|
||||
`Notes folder: \`${notesFolder}/\``,
|
||||
`Arc folder: \`${arcFolder}/\` (Overall.md, Act_I_*.md, Act_II_*.md, Act_III_*.md)`,
|
||||
'',
|
||||
'## Context files to read',
|
||||
`- \`${memoriesFolder}/global.md\` — cross-chapter decisions (read at start of session)`,
|
||||
`- \`${arcFolder}/Overall.md\` — full story arc`,
|
||||
`- \`${notesFolder}/Details_Characters.md\` — character profiles`,
|
||||
`- \`${notesFolder}/Details_World_and_Magic.md\` — world rules`,
|
||||
`- \`${notesFolder}/Details_Translation_notes.md\` — term translations`,
|
||||
'',
|
||||
'## Rules',
|
||||
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not story content.',
|
||||
'- Do not normalise quotation marks or dashes — these are managed by the Bindery extension.',
|
||||
'- Do not rewrite prose unless explicitly asked. Suggest edits only.',
|
||||
];
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Target audience is ${ctx.audience}. Flag content that is too complex or inappropriate.`);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function agentsMd(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, memoriesFolder } = ctx;
|
||||
const lines: string[] = [`# Agent Instructions — ${title}`, ''];
|
||||
lines.push('## Project overview');
|
||||
if (genre) { lines.push(`${genre} novel.`); }
|
||||
if (description) { lines.push(description); }
|
||||
if (ctx.audience){ lines.push(audienceNote(ctx)); }
|
||||
if (author) { lines.push(`Author: ${author}.`); }
|
||||
lines.push(languageSection(ctx), '');
|
||||
lines.push(
|
||||
'## Start of session',
|
||||
`1. Read \`${memoriesFolder}/global.md\` for cross-chapter context.`,
|
||||
`2. If working on a specific chapter, read \`${memoriesFolder}/chXX.md\` if it exists.`,
|
||||
`3. Check \`${notesFolder}/Details_Translation_notes.md\` before using or translating world-specific terms.`,
|
||||
'',
|
||||
'## Story files',
|
||||
`- Chapter files are \`.md\` files in \`${storyFolder}/\`, organised in act subfolders.`,
|
||||
'- HTML comments `<!-- -->` are writer notes — treat as context only, not prose.',
|
||||
'- Quotation marks and em-dashes are managed by the Bindery extension. Do not normalise them.',
|
||||
'',
|
||||
'## Writing guidelines',
|
||||
'- Do not rewrite paragraphs unless explicitly asked. Suggest edits only.',
|
||||
);
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Audience is ${ctx.audience}. Keep vocabulary clear and themes age-appropriate.`);
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'## Key reference files',
|
||||
'| File | Contains |',
|
||||
'|---|---|',
|
||||
`| \`${arcFolder}/Overall.md\` | Full story arc |`,
|
||||
`| \`${arcFolder}/Act_I_*.md\`, \`Act_II_*.md\`, \`Act_III_*.md\` | Per-act arc details |`,
|
||||
`| \`${notesFolder}/Details_Characters.md\` | Character profiles |`,
|
||||
`| \`${notesFolder}/Details_World_and_Magic.md\` | World rules |`,
|
||||
`| \`${notesFolder}/Details_Translation_notes.md\` | EN ↔ translation term table |`,
|
||||
`| \`${memoriesFolder}/global.md\` | Cross-session decisions |`,
|
||||
);
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
// ─── Skill templates ──────────────────────────────────────────────────────────
|
||||
|
||||
function skillReview(ctx: TemplateContext): string {
|
||||
const { title, arcFolder, memoriesFolder } = ctx;
|
||||
const audienceStr = ctx.audience || 'the target audience';
|
||||
return `# Skill: /review
|
||||
|
||||
Review a chapter of "${title}" and give structured feedback.
|
||||
|
||||
## Trigger
|
||||
User says \`/review\`, "review chapter X", "quick review", or "review my changes".
|
||||
|
||||
## Clarify first
|
||||
- Which chapter number?
|
||||
- Type: **Full** (language + arc + age-appropriateness) or **Quick** (language and typos only)?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools to gather context:
|
||||
- \`get_review_text(language)\` — get the git diff of uncommitted changes (for "review my changes")
|
||||
- \`get_chapter(chapterNumber, language)\` — read the full chapter text
|
||||
- \`get_notes(category, name)\` — look up character profiles (\`category: "Characters"\`) or world rules
|
||||
- \`retrieve_context(query, language)\` — find related passages across the book
|
||||
- \`git_snapshot(message)\` — after a successful review, suggest saving a snapshot
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load context
|
||||
- Read \`${memoriesFolder}/global.md\`
|
||||
- Use \`get_chapter\` to load the chapter
|
||||
- For a Full review, read the relevant arc file: \`${arcFolder}/Act_I_[X].md\`, \`Act_II_[X].md\`, or \`Act_III_[X].md\`
|
||||
- For "review my changes", use \`get_review_text\` to get the diff
|
||||
|
||||
### 2. Perform the review
|
||||
|
||||
**Quick** — language and typos only.
|
||||
|
||||
**Full** — adds:
|
||||
- Arc consistency with the arc file
|
||||
- Age-appropriateness for ${audienceStr}
|
||||
- Character consistency (use \`get_notes(category: "Characters")\`)
|
||||
|
||||
### 3. Output format
|
||||
|
||||
| Location | Before | Suggested | Reason |
|
||||
|---|---|---|---|
|
||||
| Line X | ...original... | ...suggestion... | reason |
|
||||
|
||||
- Bold changed words
|
||||
- Group by category for Full reviews
|
||||
- End with a 2-3 sentence overall impression
|
||||
|
||||
### 4. After review
|
||||
If the review looks good, suggest: "Want me to save a snapshot?" (calls \`git_snapshot\`).
|
||||
|
||||
## Rules
|
||||
- Do not rewrite unless asked — suggest only
|
||||
`;
|
||||
}
|
||||
|
||||
function skillBrainstorm(ctx: TemplateContext): string {
|
||||
const { title, arcFolder, memoriesFolder } = ctx;
|
||||
const audienceStr = ctx.audience || 'the target audience';
|
||||
return `# Skill: /brainstorm
|
||||
|
||||
Brainstorm story ideas, character moments, or plot solutions for "${title}".
|
||||
|
||||
## Trigger
|
||||
User says \`/brainstorm\`, "I'm stuck", "help me think of ideas", or "Am I stuck?".
|
||||
|
||||
## Clarify first
|
||||
- Focus: plot beat / character moment / scene idea / chapter opening-closing?
|
||||
- Which chapter or story point?
|
||||
- Any constraints to respect?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools to gather context:
|
||||
- \`retrieve_context(query, language)\` — find thematic parallels and related moments across the book
|
||||
- \`get_notes(category, name)\` — look up character profiles, world rules, or equipment details
|
||||
- \`get_chapter(chapterNumber, language)\` — read a specific chapter for reference
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read \`${memoriesFolder}/global.md\` and the relevant arc file from \`${arcFolder}/\`.
|
||||
2. If character-focused, use \`get_notes(category: "Characters")\` for character profiles.
|
||||
3. Use \`retrieve_context\` to find related moments or themes already in the book.
|
||||
4. Generate 3-5 concrete ideas that fit the arc and feel true to the characters.
|
||||
|
||||
## Output format
|
||||
|
||||
**Option A — [short title]**
|
||||
[3-5 sentence description]
|
||||
|
||||
...
|
||||
|
||||
End with a brief note on which options feel most aligned with the arc.
|
||||
|
||||
## Rules
|
||||
- Respect established world rules and character voices
|
||||
- Keep ideas appropriate for ${audienceStr}
|
||||
`;
|
||||
}
|
||||
|
||||
function skillMemory(ctx: TemplateContext): string {
|
||||
return `# Skill: /memory
|
||||
|
||||
Update project memory files with decisions from the current session.
|
||||
|
||||
## Trigger
|
||||
User says \`/memory\`, "save this to memory", "update memories", or at session end.
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`memory_list\` — discover which memory files exist and their line counts
|
||||
- \`memory_append(file, title, content)\` — append a dated session entry; the tool stamps the date automatically
|
||||
- \`memory_compact(file, compacted_content)\` — overwrite a file with a summary; backs up the original to \`archive/\` automatically
|
||||
- \`git_snapshot(message)\` — after updating memories, offer to save a snapshot
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify what to save
|
||||
List the decisions, insights, or facts from the session worth preserving.
|
||||
|
||||
### 2. Check existing files
|
||||
Use \`memory_list\` to see which memory files exist and how large they are.
|
||||
|
||||
### 3. Append the entry
|
||||
Use \`memory_append\` to write to the right file:
|
||||
- \`global.md\` — cross-chapter decisions (character names, world rules, style choices)
|
||||
- \`chXX.md\` — chapter-specific decisions (e.g. \`ch10.md\`)
|
||||
|
||||
Arguments:
|
||||
- \`file\`: just the filename, e.g. \`global.md\` or \`ch10.md\`
|
||||
- \`title\`: short topic label, e.g. \`"Daeven introduction — character decisions"\`
|
||||
- \`content\`: the decisions to record, one per line
|
||||
|
||||
The tool stamps the current date. Do not add a date to the content.
|
||||
|
||||
### 4. Compact if needed
|
||||
If \`memory_list\` shows a file exceeding ~150 lines, offer to compact it:
|
||||
- Summarise the existing content into a concise replacement
|
||||
- Call \`memory_compact(file, compacted_content)\` — original is backed up automatically
|
||||
|
||||
### 5. Snapshot
|
||||
Offer to save a snapshot with \`git_snapshot\`.
|
||||
|
||||
## Rules
|
||||
- Always use \`memory_append\` — never use the Edit tool to write to memory files
|
||||
- Do not add dates to content — the tool stamps them automatically
|
||||
- Compaction is always opt-in
|
||||
`;
|
||||
}
|
||||
|
||||
function skillTranslate(ctx: TemplateContext): string {
|
||||
return `# Skill: /translate
|
||||
|
||||
Translate a chapter or passage into the target language.
|
||||
|
||||
## Trigger
|
||||
User says \`/translate\`, "translate chapter X", or "help me with the translation".
|
||||
|
||||
## Clarify first
|
||||
- Which chapter number and target language?
|
||||
- Full translation or spot-check an existing translation?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_chapter(chapterNumber, language)\` — read a chapter in any language (source or existing translation)
|
||||
- \`get_translation(language)\` — list all rules for a language key (e.g. \`"nl"\` or \`"en-gb"\`)
|
||||
- \`get_translation(language, word)\` — look up a specific term; forgiving: case-insensitive, handles plurals and inflected forms
|
||||
- \`search(query, language)\` — verify how a term was rendered in other translated chapters
|
||||
- \`add_translation(langKey, from, to)\` — save a new term pair to \`.bindery/translations.json\` when the user confirms a translation choice
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load the translation table
|
||||
Call \`get_translation(language)\` to load all known term mappings for the target language before translating anything.
|
||||
|
||||
### 2. Load the chapter
|
||||
Use \`get_chapter(chapterNumber, sourceLanguage)\` to read the source chapter.
|
||||
For spot-check mode, also call \`get_chapter(chapterNumber, targetLanguage)\` to read the existing translation.
|
||||
|
||||
### 3. Translate or review
|
||||
**Full translation** — translate paragraph by paragraph, applying all terms from the glossary. Output the full result in a fenced \`\`\`markdown block for easy pasting.
|
||||
|
||||
**Spot-check** — compare source and translation side-by-side. Use a feedback table:
|
||||
|
||||
| Location | Source | Current translation | Suggestion | Reason |
|
||||
|---|---|---|---|---|
|
||||
|
||||
### 4. Save confirmed terms
|
||||
When the user confirms a new or corrected term translation, call \`add_translation\` to persist it so future exports apply it automatically.
|
||||
|
||||
## Rules
|
||||
- Always load the translation table first — never invent translations for world-specific terms
|
||||
- Flag uncertain terms rather than guessing
|
||||
`;
|
||||
}
|
||||
|
||||
function skillStatus(ctx: TemplateContext): string {
|
||||
const { notesFolder, arcFolder, memoriesFolder } = ctx;
|
||||
return `# Skill: /status
|
||||
|
||||
Snapshot of the book's progress: what's done, in progress, and coming up.
|
||||
|
||||
## Trigger
|
||||
User says \`/status\`, "what's the book status", or "where are we".
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_overview(language)\` — list all acts and chapters with titles
|
||||
- \`get_text(identifier)\` — read COWORK.md, \`${notesFolder}/Chapter_Status.md\`, and \`${memoriesFolder}/global.md\`
|
||||
- \`memory_list\` — discover which chapter memory files exist (\`chXX.md\`)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use \`memory_list\` to check available memory files. Use \`get_text\` to read COWORK.md (current focus), \`${notesFolder}/Chapter_Status.md\`, and \`${memoriesFolder}/global.md\`.
|
||||
2. Use \`get_overview\` for the full chapter listing.
|
||||
3. Check \`${arcFolder}/\` for what's planned vs written (Overall.md + the relevant act file).
|
||||
4. Output: overall count / done / in-progress / coming up (next 2-3 chapters) / open questions.
|
||||
|
||||
## Output
|
||||
Keep it scannable — bold headers, short lines. This is a working tool, not a narrative summary.
|
||||
`;
|
||||
}
|
||||
|
||||
function skillContinuity(ctx: TemplateContext): string {
|
||||
const { memoriesFolder } = ctx;
|
||||
return `# Skill: /continuity
|
||||
|
||||
Cross-check a chapter for consistency errors.
|
||||
|
||||
## Trigger
|
||||
User says \`/continuity\`, "check continuity", or "check chapter X for errors".
|
||||
|
||||
## Clarify first
|
||||
- Chapter number?
|
||||
- Focus: All / Characters / World rules / Timeline?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_chapter(chapterNumber, language)\` — read the chapter (and previous chapter for timeline checks)
|
||||
- \`get_notes(category, name)\` — look up character profiles or world rules
|
||||
- \`retrieve_context(query, language)\` — find earlier mentions of a character detail or event
|
||||
- \`search(query, language)\` — exact-match search for names, places, or specific terms
|
||||
- \`memory_list\` — check whether a chapter-specific memory file exists (\`chXX.md\`)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use \`get_chapter\` to read the chapter.
|
||||
2. Use \`get_text\` to read \`${memoriesFolder}/global.md\`. Use \`memory_list\` to check if a chapter-specific memory file (\`chXX.md\`) exists; if so, read it with \`get_text\` too. Use \`get_notes(category: "Characters")\` for character profiles.
|
||||
3. For world rules: use \`get_notes(category: "World")\`.
|
||||
4. For timeline: also use \`get_chapter\` to read the previous chapter.
|
||||
5. Use \`retrieve_context\` or \`search\` to verify specific details against earlier chapters.
|
||||
|
||||
## Output format
|
||||
|
||||
| Type | Location | Issue | Reference |
|
||||
|---|---|---|---|
|
||||
| Character | Line X | Description contradicts... | global.md |
|
||||
|
||||
End with a one-line overall assessment. If no issues found, say so clearly.
|
||||
|
||||
## Rules
|
||||
- Flag issues only — do not suggest rewrites
|
||||
- Phrase uncertain items as questions, not errors
|
||||
`;
|
||||
}
|
||||
|
||||
function skillReadAloud(ctx: TemplateContext): string {
|
||||
const audienceStr = ctx.audience || 'the target audience';
|
||||
return `# Skill: /read-aloud
|
||||
|
||||
Test how a chapter sounds when read aloud to ${audienceStr}.
|
||||
|
||||
## Trigger
|
||||
User says \`/read-aloud\`, "reading test", or "how does this sound".
|
||||
|
||||
## Clarify first
|
||||
- Whole chapter or specific passage?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_chapter(chapterNumber, language)\` — read the full chapter
|
||||
- \`get_text(identifier, startLine, endLine)\` — read a specific passage by line range
|
||||
|
||||
## What to check
|
||||
- Sentences over ~30 words
|
||||
- Sequences of 3+ short sentences (staccato)
|
||||
- Vocabulary too complex for ${audienceStr}
|
||||
- Said-bookisms in dialogue ("she exclaimed breathlessly" → prefer "said" or action beat)
|
||||
- Paragraphs over 8 lines without a break
|
||||
- Accidental word repetition within 2-3 sentences
|
||||
|
||||
## Output format
|
||||
|
||||
| Type | Location | Flagged text | Note |
|
||||
|---|---|---|---|
|
||||
| Long sentence | Para 3 | "..." (34 words) | Consider splitting |
|
||||
|
||||
Brief overall impression (2-3 sentences) after the table.
|
||||
|
||||
## Rules
|
||||
- Focus on how it sounds when spoken — not a content review
|
||||
- Suggestions are gentle ("consider", not "must change")
|
||||
`;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
buildIndex, loadIndex, indexPath, search, rerank,
|
||||
type SearchResult,
|
||||
} from './search.js';
|
||||
import { setupAiFiles, ALL_SKILLS, type AiTarget, type SkillTemplate } from './aisetup.js';
|
||||
|
||||
// ─── Shared helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -45,7 +46,17 @@ export function toolHealth(root: string): string {
|
|||
const lines: string[] = [`root: ${root}`];
|
||||
|
||||
const settingsPath = path.join(root, '.bindery', 'settings.json');
|
||||
lines.push(`settings.json: ${fs.existsSync(settingsPath) ? 'present' : 'missing'}`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
lines.push('settings.json: present');
|
||||
} else {
|
||||
lines.push('settings.json: missing — run init_workspace to set up this book');
|
||||
}
|
||||
|
||||
const memDir = path.join(root, '.bindery', 'memories');
|
||||
const memFiles = fs.existsSync(memDir)
|
||||
? fs.readdirSync(memDir).filter(f => f.endsWith('.md')).length
|
||||
: -1;
|
||||
lines.push(`memories: ${memFiles >= 0 ? `present (${memFiles} file${memFiles === 1 ? '' : 's'})` : 'not created yet'}`);
|
||||
|
||||
const idxPath = indexPath(root);
|
||||
if (fs.existsSync(idxPath)) {
|
||||
|
|
@ -531,6 +542,72 @@ export function toolGitSnapshot(root: string, args: GitSnapshotArgs): string {
|
|||
return `Snapshot saved: "${msg}" (${fileCount} file${fileCount === 1 ? '' : 's'})`;
|
||||
}
|
||||
|
||||
// ─── get_translation ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface GetTranslationArgs {
|
||||
language: string;
|
||||
word?: string;
|
||||
}
|
||||
|
||||
export function toolGetTranslation(root: string, args: GetTranslationArgs): string {
|
||||
const filePath = path.join(root, '.bindery', 'translations.json');
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return 'No translations.json found. Run "Bindery: Initialise Workspace" or "Bindery: Add Translation" first.';
|
||||
}
|
||||
|
||||
let translations: TranslationsFile;
|
||||
try { translations = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as TranslationsFile; }
|
||||
catch { return 'Error: failed to parse .bindery/translations.json'; }
|
||||
|
||||
// Resolve the requested language key — case-insensitive, accept code or label
|
||||
const langLower = args.language.toLowerCase();
|
||||
const matchedKey = Object.keys(translations).find(
|
||||
k => k.toLowerCase() === langLower ||
|
||||
translations[k].label?.toLowerCase() === langLower ||
|
||||
translations[k].sourceLanguage?.toLowerCase() === langLower
|
||||
);
|
||||
|
||||
if (!matchedKey) {
|
||||
const available = Object.entries(translations)
|
||||
.map(([k, e]) => `${k}${e.label ? ` (${e.label})` : ''}`)
|
||||
.join(', ');
|
||||
return `No translation entry found for "${args.language}". Available: ${available || 'none'}`;
|
||||
}
|
||||
|
||||
const entry = translations[matchedKey];
|
||||
const rules = entry.rules ?? [];
|
||||
|
||||
if (!args.word) {
|
||||
// Dump all rules for this language
|
||||
if (rules.length === 0) { return `No rules defined for "${matchedKey}" yet.`; }
|
||||
const header = `${matchedKey}${entry.label ? ` — ${entry.label}` : ''} (${entry.type}, ${rules.length} rules):`;
|
||||
return [header, ...rules.map(r => ` ${r.from} → ${r.to}`)].join('\n');
|
||||
}
|
||||
|
||||
// Word lookup — forgiving: case-insensitive + stem variants
|
||||
const stems = wordStems(args.word.toLowerCase());
|
||||
const matches = rules.filter(r => stems.some(s => r.from.toLowerCase() === s));
|
||||
|
||||
if (matches.length === 0) {
|
||||
return `"${args.word}" not found in ${matchedKey} translations.`;
|
||||
}
|
||||
return matches.map(r => `${r.from} → ${r.to} [${matchedKey}]`).join('\n');
|
||||
}
|
||||
|
||||
/** Generate stem variants for forgiving word lookup. */
|
||||
function wordStems(word: string): string[] {
|
||||
const variants = new Set<string>([word]);
|
||||
// strip common suffixes to reach a base form
|
||||
if (word.endsWith('ies')) { variants.add(word.slice(0, -3) + 'y'); }
|
||||
if (word.endsWith('es')) { variants.add(word.slice(0, -2)); }
|
||||
if (word.endsWith('s')) { variants.add(word.slice(0, -1)); }
|
||||
if (word.endsWith('ed')) { variants.add(word.slice(0, -2)); variants.add(word.slice(0, -1)); }
|
||||
if (word.endsWith('ing')) { variants.add(word.slice(0, -3)); variants.add(word.slice(0, -3) + 'e'); }
|
||||
// also try adding -s so a bare stem matches plurals stored in the file
|
||||
variants.add(word + 's');
|
||||
return Array.from(variants);
|
||||
}
|
||||
|
||||
// ─── add_translation ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface AddTranslationArgs {
|
||||
|
|
@ -656,6 +733,203 @@ function formatReviewFiles(files: DiffFile[]): string {
|
|||
return parts.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
// ─── init_workspace ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface InitWorkspaceArgs {
|
||||
bookTitle?: string;
|
||||
author?: string;
|
||||
storyFolder?: string;
|
||||
genre?: string;
|
||||
description?: string;
|
||||
targetAudience?: string;
|
||||
}
|
||||
|
||||
export function toolInitWorkspace(root: string, args: InitWorkspaceArgs): string {
|
||||
const settingsPath = path.join(root, '.bindery', 'settings.json');
|
||||
const translationsPath = path.join(root, '.bindery', 'translations.json');
|
||||
|
||||
// Load existing settings to preserve any keys not being updated
|
||||
let existing: Record<string, unknown> = {};
|
||||
const isNew = !fs.existsSync(settingsPath);
|
||||
if (!isNew) {
|
||||
try { existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as Record<string, unknown>; }
|
||||
catch { /* corrupt — treat as new */ }
|
||||
}
|
||||
|
||||
const storyFolderName = args.storyFolder ?? (existing['storyFolder'] as string | undefined) ?? 'Story';
|
||||
const bookTitle = args.bookTitle ?? (existing['bookTitle'] as string | undefined) ?? path.basename(root);
|
||||
|
||||
// Detect language folders from the story directory
|
||||
const storyPath = path.join(root, storyFolderName);
|
||||
const detectedLangs: Array<{ code: string; folderName: string; chapterWord: string; actPrefix: string; prologueLabel: string; epilogueLabel: string }> = [];
|
||||
if (fs.existsSync(storyPath)) {
|
||||
for (const entry of fs.readdirSync(storyPath, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && /^[A-Z]{2,3}$/i.test(entry.name)) {
|
||||
const code = entry.name.toUpperCase();
|
||||
detectedLangs.push({ code, folderName: entry.name, chapterWord: 'Chapter', actPrefix: 'Act', prologueLabel: 'Prologue', epilogueLabel: 'Epilogue' });
|
||||
}
|
||||
}
|
||||
}
|
||||
const languages = detectedLangs.length > 0
|
||||
? detectedLangs
|
||||
: [{ code: 'EN', folderName: 'EN', chapterWord: 'Chapter', actPrefix: 'Act', prologueLabel: 'Prologue', epilogueLabel: 'Epilogue' }];
|
||||
|
||||
const slug = bookTitle.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/, '') || 'Book';
|
||||
const settings: Record<string, unknown> = {
|
||||
...existing,
|
||||
bookTitle,
|
||||
...(args.author ? { author: args.author } : {}),
|
||||
...(args.genre ? { genre: args.genre } : {}),
|
||||
...(args.description ? { description: args.description } : {}),
|
||||
...(args.targetAudience ? { targetAudience: args.targetAudience }: {}),
|
||||
storyFolder: storyFolderName,
|
||||
mergedOutputDir: (existing['mergedOutputDir'] as string | undefined) ?? 'Merged',
|
||||
mergeFilePrefix: (existing['mergeFilePrefix'] as string | undefined) ?? slug,
|
||||
formatOnSave: (existing['formatOnSave'] as boolean | undefined) ?? false,
|
||||
languages,
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.join(root, '.bindery'), { recursive: true });
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
||||
const created: string[] = ['.bindery/settings.json'];
|
||||
|
||||
// Create translations.json only if it does not already exist
|
||||
if (!fs.existsSync(translationsPath)) {
|
||||
const translations = {
|
||||
'en-gb': { label: 'British English', type: 'substitution', sourceLanguage: 'en', rules: [], ignoredWords: [] },
|
||||
};
|
||||
fs.writeFileSync(translationsPath, JSON.stringify(translations, null, 2) + '\n', 'utf-8');
|
||||
created.push('.bindery/translations.json');
|
||||
}
|
||||
|
||||
const action = isNew ? 'Initialised' : 'Updated';
|
||||
const langNote = languages.map(l => l.code).join(', ');
|
||||
const hint = isNew
|
||||
? '\n\nTip: AI instruction files (CLAUDE.md, skills, copilot-instructions.md) are not yet set up. Run setup_ai_files to generate them, or use "Bindery: Set Up AI Files" in VS Code.'
|
||||
: '';
|
||||
return `${action}: ${created.join(', ')}. Book: "${bookTitle}", story folder: ${storyFolderName}/, languages: ${langNote}.${hint}`;
|
||||
}
|
||||
|
||||
// ─── setup_ai_files ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface SetupAiFilesArgs {
|
||||
targets?: string[]; // 'claude' | 'copilot' | 'cursor' | 'agents'
|
||||
skills?: string[]; // skill names; omit for all
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string {
|
||||
const validTargets: AiTarget[] = ['claude', 'copilot', 'cursor', 'agents'];
|
||||
const validSkills = new Set(ALL_SKILLS);
|
||||
|
||||
const targets: AiTarget[] = (args.targets ?? validTargets)
|
||||
.filter((t): t is AiTarget => validTargets.includes(t as AiTarget));
|
||||
|
||||
const skills: SkillTemplate[] = args.skills
|
||||
? args.skills.filter((s): s is SkillTemplate => validSkills.has(s as SkillTemplate))
|
||||
: ALL_SKILLS;
|
||||
|
||||
if (targets.length === 0) {
|
||||
return `No valid targets specified. Valid targets: ${validTargets.join(', ')}`;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = setupAiFiles({ root, targets, skills, overwrite: args.overwrite ?? false });
|
||||
} catch (e) {
|
||||
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
if (result.created.length > 0) {
|
||||
lines.push(`Created (${result.created.length}):\n${result.created.map(f => ` ${f}`).join('\n')}`);
|
||||
}
|
||||
if (result.skipped.length > 0) {
|
||||
lines.push(`Skipped — already exist (pass overwrite: true to replace) (${result.skipped.length}):\n${result.skipped.map(f => ` ${f}`).join('\n')}`);
|
||||
}
|
||||
if (lines.length === 0) { return 'Nothing to do.'; }
|
||||
return lines.join('\n\n');
|
||||
}
|
||||
|
||||
// ─── memory_list ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function toolMemoryList(root: string): string {
|
||||
const memDir = path.join(root, '.bindery', 'memories');
|
||||
if (!fs.existsSync(memDir)) { return 'No memory files found yet.'; }
|
||||
|
||||
const files = fs.readdirSync(memDir, { withFileTypes: true })
|
||||
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (files.length === 0) { return 'No memory files found yet.'; }
|
||||
|
||||
return files.map(e => {
|
||||
const lineCount = fs.readFileSync(path.join(memDir, e.name), 'utf-8').split(/\r?\n/).length;
|
||||
return `${e.name} (${lineCount} lines)`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ─── memory_append ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface MemoryAppendArgs {
|
||||
file: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function toolMemoryAppend(root: string, args: MemoryAppendArgs): string {
|
||||
const memDir = path.join(root, '.bindery', 'memories');
|
||||
fs.mkdirSync(memDir, { recursive: true });
|
||||
|
||||
const filePath = path.join(memDir, args.file);
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const header = `## Session ${date} — ${args.title}`;
|
||||
const addition = `\n${header}\n${args.content}`;
|
||||
|
||||
fs.appendFileSync(filePath, addition, 'utf-8');
|
||||
|
||||
const newTotal = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/).length;
|
||||
const addedLines = addition.split(/\r?\n/).length;
|
||||
|
||||
return `Appended to ${args.file}: ${addedLines} lines added, ${newTotal} total lines.`;
|
||||
}
|
||||
|
||||
// ─── memory_compact ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface MemoryCompactArgs {
|
||||
file: string;
|
||||
compacted_content: string;
|
||||
}
|
||||
|
||||
export function toolMemoryCompact(root: string, args: MemoryCompactArgs): string {
|
||||
const memDir = path.join(root, '.bindery', 'memories');
|
||||
const filePath = path.join(memDir, args.file);
|
||||
|
||||
const oldLineCount = fs.existsSync(filePath)
|
||||
? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/).length
|
||||
: 0;
|
||||
|
||||
const archiveDir = path.join(memDir, 'archive');
|
||||
fs.mkdirSync(archiveDir, { recursive: true });
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const basename = path.basename(args.file, '.md');
|
||||
const backupName = `${basename}_${date}.md`;
|
||||
const backupPath = path.join(archiveDir, backupName);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.copyFileSync(filePath, backupPath);
|
||||
}
|
||||
|
||||
fs.mkdirSync(memDir, { recursive: true });
|
||||
fs.writeFileSync(filePath, args.compacted_content, 'utf-8');
|
||||
|
||||
const newLineCount = args.compacted_content.split(/\r?\n/).length;
|
||||
const relBackup = path.join('.bindery', 'memories', 'archive', backupName);
|
||||
|
||||
return `Compacted ${args.file}: backup → ${relBackup}, old lines: ${oldLineCount}, new lines: ${newLineCount}.`;
|
||||
}
|
||||
|
||||
// ─── Shared formatter ─────────────────────────────────────────────────────────
|
||||
|
||||
function formatResult(r: SearchResult, idx: number): string {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
# Bindery MCP — Desktop Extension (.mcpb)
|
||||
|
||||
Book authoring tools for Claude Desktop: chapter navigation, full-text search,
|
||||
translation management, typography formatting, and version snapshots. Works with
|
||||
any Markdown book project structured with the Bindery VS Code extension.
|
||||
translation management, session memory, typography formatting, and version snapshots.
|
||||
Works with any Markdown book project structured with the Bindery VS Code extension.
|
||||
|
||||
## Features
|
||||
|
||||
- **Chapter navigation** — jump to any chapter by number and language
|
||||
- **Full-text search** — BM25 ranked search across all story and notes files
|
||||
- **Context retrieval** — "where did X happen" queries with ranked passages
|
||||
- **Translation management** — add and update dialect substitution rules
|
||||
- **Translation management** — list, look up, add, and update translation and dialect substitution rules
|
||||
- **Session memory** — append, list, and compact persistent session notes in `Notes/Memories/`
|
||||
- **Typography formatting** — curly quotes, em-dashes, ellipses
|
||||
- **Version snapshots** — git-based save points after writing sessions
|
||||
- **Review diffs** — structured git diff of uncommitted changes
|
||||
|
||||
## Installation
|
||||
## Manual Installation
|
||||
|
||||
To install manually without using published Claude Connectors
|
||||
|
||||
1. Download the `.mcpb` file from the latest release
|
||||
2. Open Claude Desktop → **Settings** → **Extensions**
|
||||
|
|
@ -61,6 +64,82 @@ during future exports.
|
|||
Claude calls `get_review_text` to show the diff, reviews it, then calls
|
||||
`git_snapshot` to commit the changes with a descriptive message.
|
||||
|
||||
### Look up a translation rule
|
||||
|
||||
> "How is 'flux' translated in the Dutch version?"
|
||||
|
||||
Claude calls `get_translation` with `language: "nl"` and `word: "flux"`. The
|
||||
lookup is forgiving — it matches case-insensitively and checks plural and
|
||||
inflected forms automatically. If no rule exists yet, Claude can call
|
||||
`add_translation` to create one.
|
||||
|
||||
### List all known substitution rules for a language
|
||||
|
||||
> "Show me all the British English substitution rules"
|
||||
|
||||
Claude calls `get_translation` with `language: "en-gb"` (omitting `word`) to
|
||||
dump every `from → to` rule in the `en-gb` entry of `.bindery/translations.json`.
|
||||
Useful before a translation or export session to see what's already configured.
|
||||
|
||||
### Save session decisions to memory
|
||||
|
||||
> "Save today's character decisions to memory"
|
||||
|
||||
Claude calls `memory_list` to check which files already exist and their sizes,
|
||||
then calls `memory_append` with `file: "global.md"`, a short title, and the
|
||||
decisions to record. The tool stamps the current date automatically — no manual
|
||||
date formatting needed.
|
||||
|
||||
### Compact a memory file that has grown too large
|
||||
|
||||
> "The global memory file is getting long — please compact it"
|
||||
|
||||
Claude reads the current content, summarises it, then calls `memory_compact`
|
||||
with the compacted text. The original is automatically backed up to
|
||||
`Notes/Memories/archive/global_YYYY-MM-DD.md` before the file is overwritten.
|
||||
|
||||
### Spot-check a chapter translation
|
||||
|
||||
> "Compare chapter 10 in EN and NL and flag any translation issues"
|
||||
|
||||
Claude calls `get_chapter` twice — once for EN, once for NL — then calls
|
||||
`get_translation` with the target language to load the known term table.
|
||||
Discrepancies are presented in a side-by-side table. Any confirmed corrections
|
||||
are saved back with `add_translation`.
|
||||
|
||||
### Check continuity across chapters
|
||||
|
||||
> "Check chapter 8 for consistency errors — character descriptions and world rules"
|
||||
|
||||
Claude calls `get_chapter` to read the chapter, `get_notes` to load character
|
||||
profiles and world rules, then uses `retrieve_context` and `search` to verify
|
||||
specific details against earlier chapters. Results are presented in a table with
|
||||
issue type, location, and the reference that contradicts it.
|
||||
|
||||
## Tools reference
|
||||
|
||||
| Tool | What it does |
|
||||
|---|---|
|
||||
| `list_books` | List all configured book names |
|
||||
| `identify_book` | Match a working directory to a book name |
|
||||
| `health` | Server status: settings, index, embedding backend |
|
||||
| `index_build` | Build or rebuild the full-text search index |
|
||||
| `index_status` | Show index chunk count and build time |
|
||||
| `get_text` | Read any file by relative path, with optional line range |
|
||||
| `get_chapter` | Full chapter content by number and language |
|
||||
| `get_overview` | Chapter structure — acts, chapters, titles |
|
||||
| `get_notes` | Notes/ and Details_*.md files, filterable by category or name |
|
||||
| `search` | BM25 full-text search with ranked snippets |
|
||||
| `retrieve_context` | Semantic passage retrieval for "where did X happen" queries |
|
||||
| `format` | Apply typography formatting to a file or folder |
|
||||
| `get_review_text` | Structured git diff with optional auto-staging |
|
||||
| `git_snapshot` | Git commit of story, notes, and arc changes |
|
||||
| `get_translation` | List all rules for a language, or look up a specific word (forgiving) |
|
||||
| `add_translation` | Add or update a rule in `.bindery/translations.json` |
|
||||
| `memory_list` | List `Notes/Memories/` files with line counts |
|
||||
| `memory_append` | Append a dated session entry to a memory file |
|
||||
| `memory_compact` | Overwrite a memory file with a summary (backs up original) |
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
Bindery MCP runs entirely on your local machine. No data is sent to external
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
{ "name": "list_books", "description": "List all books configured in the MCP server. Call this first to discover available book names." },
|
||||
{ "name": "identify_book", "description": "Identify which book matches a working directory. Pass your cwd and the server matches by folder name or .bindery/settings.json." },
|
||||
{ "name": "health", "description": "Check server status: active book, settings, search index, and embedding backend." },
|
||||
{ "name": "init_workspace", "description": "Create or update .bindery/settings.json and translations.json. All arguments optional — smart defaults used if omitted. Detects language folders automatically. Safe to run on an existing workspace." },
|
||||
{ "name": "setup_ai_files", "description": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md) and Claude skill templates from settings. Run init_workspace first. Skips existing files unless overwrite: true." },
|
||||
{ "name": "index_build", "description": "Build or rebuild the full-text search index for a book. Run once after cloning, then as needed." },
|
||||
{ "name": "index_status", "description": "Show current index metadata: chunk count and build time." },
|
||||
{ "name": "retrieve_context", "description": "Find the most relevant passages for a query. Use for 'where did X happen' or 'what did character Y say about Z'." },
|
||||
|
|
@ -56,6 +58,10 @@
|
|||
{ "name": "format", "description": "Apply typography formatting (curly quotes, em-dashes, ellipses) to a file or folder." },
|
||||
{ "name": "get_review_text", "description": "Structured git diff of uncommitted changes. Filter by language (EN, NL, ALL). Set autoStage to stage reviewed files." },
|
||||
{ "name": "git_snapshot", "description": "Save a snapshot (git commit) of all changes in story, notes, and arc folders. Use after writing sessions or reviews." },
|
||||
{ "name": "add_translation", "description": "Add or update a substitution rule in .bindery/translations.json for dialect conversion (e.g. US→UK spelling)." }
|
||||
{ "name": "add_translation", "description": "Add or update a substitution rule in .bindery/translations.json for translation or dialect conversion (e.g. US→UK spelling)." },
|
||||
{ "name": "get_translation", "description": "Look up translation/substitution rules from .bindery/translations.json. List all rules for a language or do a forgiving word lookup (case-insensitive, handles plurals and inflected forms)." },
|
||||
{ "name": "memory_list", "description": "List all session memory files in .bindery/memories/ with their line counts." },
|
||||
{ "name": "memory_append", "description": "Append a dated session entry to a memory file in .bindery/memories/. Supply a short title and content; the tool stamps the date." },
|
||||
{ "name": "memory_compact", "description": "Overwrite a memory file with a compacted summary. The original is backed up to .bindery/memories/archive/ first." }
|
||||
]
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
"dialect"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onStartupFinished",
|
||||
"onLanguage:markdown"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
|
|
@ -345,6 +346,85 @@
|
|||
},
|
||||
"required": ["langKey", "from", "to"]
|
||||
}
|
||||
},
|
||||
{ "name": "bindery_get_translation",
|
||||
"tags": ["bindery"],
|
||||
"displayName": "Bindery: Get Translation",
|
||||
"modelDescription": "Look up translation/substitution rules in .bindery/translations.json. Without a word, lists all rules for the language. With a word, does a forgiving case-insensitive lookup including plural and inflected forms.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"language": { "type": "string", "description": "Language key, label, or code (e.g. 'nl', 'en-gb', 'British English')" },
|
||||
"word": { "type": "string", "description": "Word or term to look up (omit to list all rules for the language)" }
|
||||
},
|
||||
"required": ["language"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bindery_setup_ai_files",
|
||||
"tags": ["bindery"],
|
||||
"displayName": "Bindery: Setup AI Files",
|
||||
"modelDescription": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md) and Claude skill templates from .bindery/settings.json. Run init_workspace first. Skips existing files unless overwrite is true.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targets": { "type": "array", "items": { "type": "string" }, "description": "Files to generate: claude, copilot, cursor, agents. Default: all." },
|
||||
"skills": { "type": "array", "items": { "type": "string" }, "description": "Claude skills to include: review, brainstorm, memory, translate, status, continuity, read_aloud. Default: all." },
|
||||
"overwrite": { "type": "boolean", "description": "Overwrite existing files? Default false." }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bindery_init_workspace",
|
||||
"tags": ["bindery"],
|
||||
"displayName": "Bindery: Init Workspace",
|
||||
"modelDescription": "Create or update .bindery/settings.json and translations.json. All arguments optional — smart defaults used for any omitted values. Detects language folders automatically. Safe to run on an existing workspace.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bookTitle": { "type": "string", "description": "Book title (defaults to folder name)" },
|
||||
"author": { "type": "string" },
|
||||
"storyFolder": { "type": "string", "description": "Story folder name relative to root (default: Story)" },
|
||||
"genre": { "type": "string" },
|
||||
"description": { "type": "string", "description": "One-line description for AI instruction files" },
|
||||
"targetAudience": { "type": "string", "description": "Target audience, e.g. 12+ or adults" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "name": "bindery_memory_list",
|
||||
"tags": ["bindery"],
|
||||
"displayName": "Bindery: Memory List",
|
||||
"modelDescription": "List all session memory files in .bindery/memories/ with their line counts.",
|
||||
"inputSchema": { "type": "object", "properties": {} }
|
||||
},
|
||||
{
|
||||
"name": "bindery_memory_append",
|
||||
"tags": ["bindery"],
|
||||
"displayName": "Bindery: Memory Append",
|
||||
"modelDescription": "Append a dated session entry to a memory file in .bindery/memories/. The tool stamps the date; supply a short title and content.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string", "description": "Filename within .bindery/memories/, e.g. global.md or ch10.md" },
|
||||
"title": { "type": "string", "description": "Short session title describing the topic" },
|
||||
"content": { "type": "string", "description": "Text to record under this session entry" }
|
||||
},
|
||||
"required": ["file", "title", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bindery_memory_compact",
|
||||
"tags": ["bindery"],
|
||||
"displayName": "Bindery: Memory Compact",
|
||||
"modelDescription": "Overwrite a memory file with a compacted summary. The original is backed up to .bindery/memories/archive/ first.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string", "description": "Filename within .bindery/memories/, e.g. global.md" },
|
||||
"compacted_content": { "type": "string", "description": "Full replacement content (model-supplied summary)" }
|
||||
},
|
||||
"required": ["file", "compacted_content"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@
|
|||
* copilot → .github/copilot-instructions.md
|
||||
* cursor → .cursor/rules
|
||||
* agents → AGENTS.md (OpenAI Agents, Aider, Codex, etc.)
|
||||
*
|
||||
* Templates live in ai-setup-templates.ts, which is a copy of
|
||||
* mcp-ts/src/templates.ts — the single source of truth.
|
||||
* The copy is kept in sync by the CI workflow and locally via:
|
||||
* cp mcp-ts/src/templates.ts vscode-ext/src/ai-setup-templates.ts
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import type { WorkspaceSettings } from './workspace';
|
||||
import type { LanguageConfig } from './merge';
|
||||
import { renderTemplate } from './ai-setup-templates';
|
||||
|
||||
// ─── Public types ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -47,6 +53,13 @@ export const ALL_SKILLS: SkillTemplate[] = [
|
|||
'review', 'brainstorm', 'memory', 'translate', 'status', 'continuity', 'read_aloud',
|
||||
];
|
||||
|
||||
/**
|
||||
* Bump this number whenever the generated skill/instruction templates change
|
||||
* significantly enough that existing users should regenerate their AI files.
|
||||
* Written to .bindery/ai-version.json after each successful setupAiFiles() run.
|
||||
*/
|
||||
export const AI_SETUP_VERSION = 4;
|
||||
|
||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
|
||||
|
|
@ -58,44 +71,66 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
|
|||
for (const target of targets) {
|
||||
switch (target) {
|
||||
case 'claude':
|
||||
writeFile(root, 'CLAUDE.md', claudeMd(ctx), overwrite, result);
|
||||
writeFile(root, 'CLAUDE.md', renderTemplate('claude', ctx), overwrite, result);
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join('.claude', 'skills', skill);
|
||||
writeFile(root, path.join(skillDir, 'SKILL.md'), skillMd(skill, ctx), overwrite, result);
|
||||
writeFile(root, path.join(skillDir, 'SKILL.md'), renderTemplate(skill, ctx), overwrite, result);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'copilot':
|
||||
writeFile(root, path.join('.github', 'copilot-instructions.md'), copilotMd(ctx), overwrite, result);
|
||||
writeFile(root, path.join('.github', 'copilot-instructions.md'), renderTemplate('copilot', ctx), overwrite, result);
|
||||
break;
|
||||
|
||||
case 'cursor':
|
||||
writeFile(root, path.join('.cursor', 'rules'), cursorRules(ctx), overwrite, result);
|
||||
writeFile(root, path.join('.cursor', 'rules'), renderTemplate('cursor', ctx), overwrite, result);
|
||||
break;
|
||||
|
||||
case 'agents':
|
||||
writeFile(root, 'AGENTS.md', agentsMd(ctx), overwrite, result);
|
||||
writeFile(root, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stampAiVersion(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Template context ─────────────────────────────────────────────────────────
|
||||
// ─── Version stamp ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Read the ai-version.json version number, or 0 if absent / unreadable. */
|
||||
export function readAiSetupVersion(root: string): number {
|
||||
const p = path.join(root, '.bindery', 'ai-version.json');
|
||||
if (!fs.existsSync(p)) { return 0; }
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as { version?: unknown };
|
||||
return typeof raw.version === 'number' ? raw.version : 0;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
function stampAiVersion(root: string): void {
|
||||
const dir = path.join(root, '.bindery');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'ai-version.json'),
|
||||
JSON.stringify({ version: AI_SETUP_VERSION }, null, 2) + '\n',
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
interface TemplateContext {
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
genre: string;
|
||||
audience: string;
|
||||
storyFolder: string;
|
||||
notesFolder: string;
|
||||
arcFolder: string;
|
||||
languages: LanguageConfig[];
|
||||
langList: string; // e.g. "EN (source), NL (translation)"
|
||||
hasMultiLang: boolean;
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
genre: string;
|
||||
audience: string;
|
||||
storyFolder: string;
|
||||
notesFolder: string;
|
||||
arcFolder: string;
|
||||
memoriesFolder: string;
|
||||
languages: LanguageConfig[];
|
||||
langList: string;
|
||||
hasMultiLang: boolean;
|
||||
}
|
||||
|
||||
function buildContext(s: WorkspaceSettings): TemplateContext {
|
||||
|
|
@ -113,7 +148,7 @@ function buildContext(s: WorkspaceSettings): TemplateContext {
|
|||
? languages.map((l, i) => i === 0 ? `${l.code} (source)` : `${l.code} (translation)`).join(', ')
|
||||
: 'EN (source)';
|
||||
|
||||
return { title, author, description, genre, audience, storyFolder, notesFolder, arcFolder, languages, langList, hasMultiLang: languages.length > 1 };
|
||||
return { title, author, description, genre, audience, storyFolder, notesFolder, arcFolder, languages, langList, hasMultiLang: languages.length > 1, memoriesFolder: '.bindery/memories' };
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -135,451 +170,3 @@ function writeFile(
|
|||
result.created.push(relPath);
|
||||
}
|
||||
|
||||
function audienceNote(ctx: TemplateContext): string {
|
||||
return ctx.audience ? `Target audience: ${ctx.audience}.` : '';
|
||||
}
|
||||
|
||||
function languageSection(ctx: TemplateContext): string {
|
||||
if (!ctx.hasMultiLang) { return ''; }
|
||||
return `\nLanguages: ${ctx.langList}.\n`;
|
||||
}
|
||||
|
||||
// ─── CLAUDE.md ────────────────────────────────────────────────────────────────
|
||||
|
||||
function claudeMd(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder } = ctx;
|
||||
const lines: string[] = [
|
||||
`# Claude — ${title}`,
|
||||
'',
|
||||
'## Project',
|
||||
];
|
||||
if (genre) { lines.push(`Genre: ${genre}.`); }
|
||||
if (description) { lines.push(description); }
|
||||
if (ctx.audience){ lines.push(audienceNote(ctx)); }
|
||||
if (author) { lines.push(`Author: ${author}.`); }
|
||||
lines.push(languageSection(ctx));
|
||||
|
||||
lines.push(
|
||||
'## Start of session',
|
||||
`1. Read COWORK.md (if present) for current focus and context.`,
|
||||
`2. Read ${notesFolder}/Memories/global.md for cross-chapter decisions.`,
|
||||
`3. If working on a specific chapter, read ${notesFolder}/Memories/chXX.md if it exists.`,
|
||||
'',
|
||||
'## Repo layout',
|
||||
'```',
|
||||
`${arcFolder}/ ← story arc files (Overall.md, Act_I_*.md, Act_II_*.md, Act_III_*.md)`,
|
||||
`${notesFolder}/ ← story bible (characters, world, translation table, memories)`,
|
||||
`${storyFolder}/`,
|
||||
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters (one .md per chapter)`),
|
||||
'```',
|
||||
'',
|
||||
'## Writing rules',
|
||||
'- Never rewrite paragraphs unless explicitly asked. Suggest edits only.',
|
||||
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not prose.',
|
||||
'- Quotation marks and dashes in chapter files are managed by the Bindery extension. Do not flag these as formatting errors.',
|
||||
);
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Content is aimed at ${ctx.audience}. Keep language accessible and themes age-appropriate.`);
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'## Key reference files',
|
||||
'| File | Contains |',
|
||||
'|---|---|',
|
||||
`| \`${arcFolder}/Overall.md\` | Full story arc |`,
|
||||
`| \`${arcFolder}/Act_I_*.md\`, \`Act_II_*.md\`, \`Act_III_*.md\` | Per-act arc details |`,
|
||||
`| \`${notesFolder}/Details_Characters.md\` | Character profiles |`,
|
||||
`| \`${notesFolder}/Details_World_and_Magic.md\` | World rules and magic system |`,
|
||||
`| \`${notesFolder}/Details_Translation_notes.md\` | Term translations / glossary |`,
|
||||
`| \`${notesFolder}/Memories/global.md\` | Cross-session decisions |`,
|
||||
'',
|
||||
'## Available skills',
|
||||
'Use these slash commands to trigger structured workflows:',
|
||||
'| Command | Purpose |',
|
||||
'|---|---|',
|
||||
'| `/review` | Review a chapter for language, arc consistency, and age-appropriateness |',
|
||||
'| `/brainstorm` | Generate plot/character/scene ideas |',
|
||||
'| `/memory` | Update memory files and compact if needed |',
|
||||
'| `/translate` | Assist with chapter translation |',
|
||||
'| `/status` | Book progress snapshot |',
|
||||
'| `/continuity` | Check a chapter for consistency errors |',
|
||||
'| `/read-aloud` | Test how a passage reads when spoken |',
|
||||
);
|
||||
|
||||
return lines.filter(l => l !== '\n').join('\n') + '\n';
|
||||
}
|
||||
|
||||
// ─── .github/copilot-instructions.md ─────────────────────────────────────────
|
||||
|
||||
function copilotMd(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder } = ctx;
|
||||
const lines: string[] = [`# GitHub Copilot — ${title}`, ''];
|
||||
if (genre || description || ctx.audience) {
|
||||
lines.push('## Project');
|
||||
if (genre) { lines.push(`${genre} novel.`); }
|
||||
if (description) { lines.push(description); }
|
||||
if (ctx.audience){ lines.push(audienceNote(ctx)); }
|
||||
if (author) { lines.push(`Author: ${author}.`); }
|
||||
lines.push(languageSection(ctx), '');
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'## Repo layout',
|
||||
'```',
|
||||
`${arcFolder}/ ← story arc files`,
|
||||
`${notesFolder}/ ← story bible, translation table, memories`,
|
||||
`${storyFolder}/`,
|
||||
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters`),
|
||||
'```',
|
||||
'',
|
||||
'## Writing guidelines',
|
||||
'- HTML comments `<!-- -->` in chapter files are writer notes — treat as context only.',
|
||||
'- Quotation marks and dashes are managed by the Bindery VS Code extension. Do not normalise them.',
|
||||
'- Check `Notes/Details_Translation_notes.md` before using or translating world-specific terms.',
|
||||
);
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Content targets ${ctx.audience}. Keep vocabulary accessible and themes appropriate.`);
|
||||
}
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
// ─── .cursor/rules ────────────────────────────────────────────────────────────
|
||||
|
||||
function cursorRules(ctx: TemplateContext): string {
|
||||
const { title, storyFolder, notesFolder, arcFolder } = ctx;
|
||||
const lines: string[] = [
|
||||
`# Cursor rules — ${title}`,
|
||||
'',
|
||||
`Story folder: \`${storyFolder}/\``,
|
||||
`Notes folder: \`${notesFolder}/\``,
|
||||
`Arc folder: \`${arcFolder}/\` (Overall.md, Act_I_*.md, Act_II_*.md, Act_III_*.md)`,
|
||||
'',
|
||||
'## Context files to read',
|
||||
`- \`${notesFolder}/Memories/global.md\` — cross-chapter decisions (read at start of session)`,
|
||||
`- \`${arcFolder}/Overall.md\` — full story arc`,
|
||||
`- \`${notesFolder}/Details_Characters.md\` — character profiles`,
|
||||
`- \`${notesFolder}/Details_World_and_Magic.md\` — world rules`,
|
||||
`- \`${notesFolder}/Details_Translation_notes.md\` — term translations`,
|
||||
'',
|
||||
'## Rules',
|
||||
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not story content.',
|
||||
'- Do not normalise quotation marks or dashes — these are managed by the Bindery extension.',
|
||||
'- Do not rewrite prose unless explicitly asked. Suggest edits only.',
|
||||
];
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Target audience is ${ctx.audience}. Flag content that is too complex or inappropriate.`);
|
||||
}
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
// ─── AGENTS.md ────────────────────────────────────────────────────────────────
|
||||
|
||||
function agentsMd(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder } = ctx;
|
||||
const lines: string[] = [`# Agent Instructions — ${title}`, ''];
|
||||
lines.push('## Project overview');
|
||||
if (genre) { lines.push(`${genre} novel.`); }
|
||||
if (description) { lines.push(description); }
|
||||
if (ctx.audience){ lines.push(audienceNote(ctx)); }
|
||||
if (author) { lines.push(`Author: ${author}.`); }
|
||||
lines.push(languageSection(ctx), '');
|
||||
|
||||
lines.push(
|
||||
'## Start of session',
|
||||
`1. Read \`${notesFolder}/Memories/global.md\` for cross-chapter context.`,
|
||||
`2. If working on a specific chapter, read \`${notesFolder}/Memories/chXX.md\` if it exists.`,
|
||||
`3. Check \`${notesFolder}/Details_Translation_notes.md\` before using or translating world-specific terms.`,
|
||||
'',
|
||||
'## Story files',
|
||||
`- Chapter files are \`.md\` files in \`${storyFolder}/\`, organised in act subfolders.`,
|
||||
'- HTML comments `<!-- -->` are writer notes — treat as context only, not prose.',
|
||||
'- Quotation marks and em-dashes are managed by the Bindery extension. Do not normalise them.',
|
||||
'',
|
||||
'## Writing guidelines',
|
||||
'- Do not rewrite paragraphs unless explicitly asked. Suggest edits only.',
|
||||
);
|
||||
if (ctx.audience) {
|
||||
lines.push(`- Audience is ${ctx.audience}. Keep vocabulary clear and themes age-appropriate.`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'## Key reference files',
|
||||
'| File | Contains |',
|
||||
'|---|---|',
|
||||
`| \`${arcFolder}/Overall.md\` | Full story arc |`,
|
||||
`| \`${arcFolder}/Act_I_*.md\`, \`Act_II_*.md\`, \`Act_III_*.md\` | Per-act arc details |`,
|
||||
`| \`${notesFolder}/Details_Characters.md\` | Character profiles |`,
|
||||
`| \`${notesFolder}/Details_World_and_Magic.md\` | World rules |`,
|
||||
`| \`${notesFolder}/Details_Translation_notes.md\` | EN ↔ translation term table |`,
|
||||
`| \`${notesFolder}/Memories/global.md\` | Cross-session decisions |`,
|
||||
);
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
// ─── Skill templates ──────────────────────────────────────────────────────────
|
||||
|
||||
function skillMd(skill: SkillTemplate, ctx: TemplateContext): string {
|
||||
const { title, storyFolder, notesFolder, arcFolder, audience } = ctx;
|
||||
const audienceStr = audience ? audience : 'the target audience';
|
||||
|
||||
switch (skill) {
|
||||
case 'review': return `# Skill: /review
|
||||
|
||||
Review a chapter of "${title}" and give structured feedback.
|
||||
|
||||
## Trigger
|
||||
User says \`/review\`, "review chapter X", "quick review", or "review my changes".
|
||||
|
||||
## Clarify first
|
||||
- Which chapter number?
|
||||
- Type: **Full** (language + arc + age-appropriateness) or **Quick** (language and typos only)?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools to gather context:
|
||||
- \`get_review_text(language)\` — get the git diff of uncommitted changes (for "review my changes")
|
||||
- \`get_chapter(chapterNumber, language)\` — read the full chapter text
|
||||
- \`get_notes(category, name)\` — look up character profiles (\`category: "Characters"\`) or world rules
|
||||
- \`retrieve_context(query, language)\` — find related passages across the book
|
||||
- \`git_snapshot(message)\` — after a successful review, suggest saving a snapshot
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load context
|
||||
- Read \`${notesFolder}/Memories/global.md\`
|
||||
- Use \`get_chapter\` to load the chapter
|
||||
- For a Full review, read the relevant arc file: \`${arcFolder}/Act_I_[X].md\`, \`Act_II_[X].md\`, or \`Act_III_[X].md\`
|
||||
- For "review my changes", use \`get_review_text\` to get the diff
|
||||
|
||||
### 2. Perform the review
|
||||
|
||||
**Quick** — language and typos only.
|
||||
|
||||
**Full** — adds:
|
||||
- Arc consistency with the arc file
|
||||
- Age-appropriateness for ${audienceStr}
|
||||
- Character consistency (use \`get_notes(category: "Characters")\`)
|
||||
|
||||
### 3. Output format
|
||||
|
||||
| Location | Before | Suggested | Reason |
|
||||
|---|---|---|---|
|
||||
| Line X | ...original... | ...suggestion... | reason |
|
||||
|
||||
- Bold changed words
|
||||
- Group by category for Full reviews
|
||||
- End with a 2-3 sentence overall impression
|
||||
|
||||
### 4. After review
|
||||
If the review looks good, suggest: "Want me to save a snapshot?" (calls \`git_snapshot\`).
|
||||
|
||||
## Rules
|
||||
- Do not rewrite unless asked — suggest only
|
||||
- Respond in English always
|
||||
`;
|
||||
|
||||
case 'brainstorm': return `# Skill: /brainstorm
|
||||
|
||||
Brainstorm story ideas, character moments, or plot solutions for "${title}".
|
||||
|
||||
## Trigger
|
||||
User says \`/brainstorm\`, "I'm stuck", "help me think of ideas", or "Am I stuck?".
|
||||
|
||||
## Clarify first
|
||||
- Focus: plot beat / character moment / scene idea / chapter opening-closing?
|
||||
- Which chapter or story point?
|
||||
- Any constraints to respect?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools to gather context:
|
||||
- \`retrieve_context(query, language)\` — find thematic parallels and related moments across the book
|
||||
- \`get_notes(category, name)\` — look up character profiles, world rules, or equipment details
|
||||
- \`get_chapter(chapterNumber, language)\` — read a specific chapter for reference
|
||||
|
||||
## Steps
|
||||
|
||||
1. Read \`${notesFolder}/Memories/global.md\` and the relevant arc file from \`${arcFolder}/\`.
|
||||
2. If character-focused, use \`get_notes(category: "Characters")\` for character profiles.
|
||||
3. Use \`retrieve_context\` to find related moments or themes already in the book.
|
||||
4. Generate 3–5 concrete ideas that fit the arc and feel true to the characters.
|
||||
|
||||
## Output format
|
||||
|
||||
**Option A — [short title]**
|
||||
[3-5 sentence description]
|
||||
|
||||
...
|
||||
|
||||
End with a brief note on which options feel most aligned with the arc.
|
||||
|
||||
## Rules
|
||||
- Respect established world rules and character voices
|
||||
- Keep ideas appropriate for ${audienceStr}
|
||||
- Respond in English always
|
||||
`;
|
||||
|
||||
case 'memory': return `# Skill: /memory
|
||||
|
||||
Update project memory files with decisions from the current session.
|
||||
|
||||
## Trigger
|
||||
User says \`/memory\`, "save this to memory", "update memories", or at session end.
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_text(identifier)\` — read current memory files before appending (e.g. \`${notesFolder}/Memories/global.md\`)
|
||||
- \`git_snapshot(message)\` — after updating memories, offer to save a snapshot
|
||||
|
||||
## Steps
|
||||
|
||||
1. Identify decisions/insights from the session.
|
||||
2. Use \`get_text\` to read the current content of the target memory file.
|
||||
3. Write to \`${notesFolder}/Memories/global.md\` (cross-chapter) or \`${notesFolder}/Memories/chXX.md\` (chapter-specific).
|
||||
4. Append only — never edit existing entries.
|
||||
5. Format: \`**[YYYY-MM-DD]:** - [decision]\`
|
||||
6. If a file exceeds ~150 lines, offer to compact the oldest 50% into a summary block.
|
||||
7. Offer to save a snapshot with \`git_snapshot\`.
|
||||
|
||||
## Rules
|
||||
- Append only
|
||||
- Date every entry
|
||||
- Compaction is always opt-in
|
||||
- Respond in English always
|
||||
`;
|
||||
|
||||
case 'translate': return `# Skill: /translate
|
||||
|
||||
Translate a chapter or passage into the target language.
|
||||
|
||||
## Trigger
|
||||
User says \`/translate\`, "translate chapter X", or "help me with the translation".
|
||||
|
||||
## Clarify first
|
||||
- Which chapter or passage?
|
||||
- Full translation or spot-check an existing translation?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_chapter(chapterNumber, language)\` — read the source chapter
|
||||
- \`get_notes(category: "Translation")\` — load the translation table (\`Details_Translation_notes.md\`)
|
||||
- \`get_text(identifier)\` — read any specific file (e.g. an existing translation to spot-check)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use \`get_notes(category: "Translation")\` to load world-specific term translations.
|
||||
2. Use \`get_chapter\` to read the source chapter.
|
||||
3. Translate paragraph by paragraph, applying all terms from the translation table.
|
||||
4. Output the translation in a fenced \`\`\`markdown code block for easy pasting.
|
||||
|
||||
For spot-check mode, use a feedback table instead of a full translation.
|
||||
|
||||
## Rules
|
||||
- Always consult the translation table first — never invent translations for world-specific terms
|
||||
- Flag uncertain terms rather than guessing
|
||||
- Respond in English in explanations
|
||||
`;
|
||||
|
||||
case 'status': return `# Skill: /status
|
||||
|
||||
Snapshot of the book's progress: what's done, in progress, and coming up.
|
||||
|
||||
## Trigger
|
||||
User says \`/status\`, "what's the book status", or "where are we".
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_overview(language)\` — list all acts and chapters with titles
|
||||
- \`get_text(identifier)\` — read COWORK.md, \`${notesFolder}/Chapter_Status.md\`, and \`${notesFolder}/Memories/global.md\`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use \`get_text\` to read COWORK.md (current focus), \`${notesFolder}/Chapter_Status.md\`, and \`${notesFolder}/Memories/global.md\`.
|
||||
2. Use \`get_overview\` for the full chapter listing.
|
||||
3. Check \`${arcFolder}/\` for what's planned vs written (Overall.md + the relevant act file).
|
||||
4. Output: overall count / done / in-progress / coming up (next 2-3 chapters) / open questions.
|
||||
|
||||
## Output
|
||||
Keep it scannable — bold headers, short lines. This is a working tool, not a narrative summary.
|
||||
`;
|
||||
|
||||
case 'continuity': return `# Skill: /continuity
|
||||
|
||||
Cross-check a chapter for consistency errors.
|
||||
|
||||
## Trigger
|
||||
User says \`/continuity\`, "check continuity", or "check chapter X for errors".
|
||||
|
||||
## Clarify first
|
||||
- Chapter number?
|
||||
- Focus: All / Characters / World rules / Timeline?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_chapter(chapterNumber, language)\` — read the chapter (and previous chapter for timeline checks)
|
||||
- \`get_notes(category, name)\` — look up character profiles or world rules
|
||||
- \`retrieve_context(query, language)\` — find earlier mentions of a character detail or event
|
||||
- \`search(query, language)\` — exact-match search for names, places, or specific terms
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use \`get_chapter\` to read the chapter.
|
||||
2. Load \`${notesFolder}/Memories/global.md\` and use \`get_notes(category: "Characters")\` for character profiles.
|
||||
3. For world rules: use \`get_notes(category: "World")\`.
|
||||
4. For timeline: also use \`get_chapter\` to read the previous chapter.
|
||||
5. Use \`retrieve_context\` or \`search\` to verify specific details against earlier chapters.
|
||||
|
||||
## Output format
|
||||
|
||||
| Type | Location | Issue | Reference |
|
||||
|---|---|---|---|
|
||||
| Character | Line X | Description contradicts... | global.md |
|
||||
|
||||
End with a one-line overall assessment. If no issues found, say so clearly.
|
||||
|
||||
## Rules
|
||||
- Flag issues only — do not suggest rewrites
|
||||
- Phrase uncertain items as questions, not errors
|
||||
- Respond in English always
|
||||
`;
|
||||
|
||||
case 'read_aloud': return `# Skill: /read-aloud
|
||||
|
||||
Test how a chapter sounds when read aloud to ${audienceStr}.
|
||||
|
||||
## Trigger
|
||||
User says \`/read-aloud\`, "reading test", or "how does this sound".
|
||||
|
||||
## Clarify first
|
||||
- Whole chapter or specific passage?
|
||||
|
||||
## Tools
|
||||
Use these Bindery MCP tools:
|
||||
- \`get_chapter(chapterNumber, language)\` — read the full chapter
|
||||
- \`get_text(identifier, startLine, endLine)\` — read a specific passage by line range
|
||||
|
||||
## What to check
|
||||
- Sentences over ~30 words
|
||||
- Sequences of 3+ short sentences (staccato)
|
||||
- Vocabulary too complex for ${audienceStr}
|
||||
- Said-bookisms in dialogue ("she exclaimed breathlessly" → prefer "said" or action beat)
|
||||
- Paragraphs over 8 lines without a break
|
||||
- Accidental word repetition within 2-3 sentences
|
||||
|
||||
## Output format
|
||||
|
||||
| Type | Location | Flagged text | Note |
|
||||
|---|---|---|---|
|
||||
| Long sentence | Para 3 | "..." (34 words) | Consider splitting |
|
||||
|
||||
Brief overall impression (2-3 sentences) after the table.
|
||||
|
||||
## Rules
|
||||
- Focus on how it sounds when spoken — not a content review
|
||||
- Suggestions are gentle ("consider", not "must change")
|
||||
- Respond in English always
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
type WorkspaceSettings, type TranslationsFile,
|
||||
} from './workspace';
|
||||
import {
|
||||
setupAiFiles, ALL_SKILLS,
|
||||
setupAiFiles, ALL_SKILLS, AI_SETUP_VERSION, readAiSetupVersion,
|
||||
type AiTarget, type SkillTemplate,
|
||||
} from './ai-setup';
|
||||
import { registerLmTools, registerMcpCommand } from './mcp';
|
||||
|
|
@ -987,6 +987,23 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
// LM tools (Copilot Chat)
|
||||
registerLmTools(context);
|
||||
|
||||
// AI setup version check — prompt if generated files are out of date
|
||||
const root = getWorkspaceRoot();
|
||||
if (root && fs.existsSync(getSettingsPath(root))) {
|
||||
const installedVersion = readAiSetupVersion(root);
|
||||
if (installedVersion < AI_SETUP_VERSION) {
|
||||
vscode.window.showInformationMessage(
|
||||
'Bindery: AI assistant files may be out of date (skill templates were updated).',
|
||||
'Update now',
|
||||
'Dismiss'
|
||||
).then(action => {
|
||||
if (action === 'Update now') {
|
||||
vscode.commands.executeCommand('bindery.setupAI');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar — shown when a markdown file is active
|
||||
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
statusBar.text = '$(book) Bindery';
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ interface FormatInput { filePath?: string; dryRun?: boolean; noRecurse?: boo
|
|||
interface GetReviewTextInput { language?: string; contextLines?: number; autoStage?: boolean }
|
||||
interface GitSnapshotInput { message?: string }
|
||||
interface AddTranslationInput { langKey: string; from: string; to: string }
|
||||
interface GetTranslationInput { language: string; word?: string }
|
||||
interface InitWorkspaceInput { bookTitle?: string; author?: string; storyFolder?: string; genre?: string; description?: string; targetAudience?: string }
|
||||
interface SetupAiFilesInput { targets?: string[]; skills?: string[]; overwrite?: boolean }
|
||||
interface MemoryAppendInput { file: string; title: string; content: string }
|
||||
interface MemoryCompactInput { file: string; compacted_content: string }
|
||||
|
||||
interface McpTools {
|
||||
toolHealth: (root: string) => string;
|
||||
|
|
@ -39,6 +44,12 @@ interface McpTools {
|
|||
toolGetReviewText: (root: string, args: GetReviewTextInput) => string;
|
||||
toolGitSnapshot: (root: string, args: GitSnapshotInput) => string;
|
||||
toolAddTranslation: (root: string, args: AddTranslationInput) => string;
|
||||
toolGetTranslation: (root: string, args: GetTranslationInput) => string;
|
||||
toolInitWorkspace: (root: string, args: InitWorkspaceInput) => string;
|
||||
toolSetupAiFiles: (root: string, args: SetupAiFilesInput) => string;
|
||||
toolMemoryList: (root: string) => string;
|
||||
toolMemoryAppend: (root: string, args: MemoryAppendInput) => string;
|
||||
toolMemoryCompact: (root: string, args: MemoryCompactInput) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,6 +135,30 @@ export function registerLmTools(context: vscode.ExtensionContext): void {
|
|||
vscode.lm.registerTool<AddTranslationInput>('bindery_add_translation', {
|
||||
invoke: async (opts, _token) => ok(t.toolAddTranslation(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetTranslationInput>('bindery_get_translation', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetTranslation(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<InitWorkspaceInput>('bindery_init_workspace', {
|
||||
invoke: async (opts, _token) => ok(t.toolInitWorkspace(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<SetupAiFilesInput>('bindery_setup_ai_files', {
|
||||
invoke: async (opts, _token) => ok(t.toolSetupAiFiles(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool('bindery_memory_list', {
|
||||
invoke: async (_opts, _token) => ok(t.toolMemoryList(requireRoot())),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<MemoryAppendInput>('bindery_memory_append', {
|
||||
invoke: async (opts, _token) => ok(t.toolMemoryAppend(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<MemoryCompactInput>('bindery_memory_compact', {
|
||||
invoke: async (opts, _token) => ok(t.toolMemoryCompact(requireRoot(), opts.input)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue