feat: add bindery-core shared TypeScript package

Extracts pure logic from vscode-ext and mcp-ts into a shared package:

- formatting.ts: updateTypography() + applyTypography() alias
- settings.ts: WorkspaceSettings, LanguageConfig, DialectConfig types, path helpers, readers, and accessors
- translations.ts: TranslationsFile types, UkReplacement, all translation read/write/mutate helpers
- templates.ts + src/templates/: all AI instruction file templates (canonical source of truth)
- index.ts: barrel export of all modules
- test/formatting.test.ts: 25 tests ported from vscode-ext format-on-save tests

Build: tsc (commonjs, ES2022, strict)
Tests: vitest (25/25 passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: evdboom <18037882+evdboom@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-27 23:01:58 +00:00 committed by GitHub
parent 42a98777b8
commit 9dd9820646
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 5687 additions and 0 deletions

19
bindery-core/out/formatting.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
/**
* Typography formatting for markdown files.
*
* Converts straight quotes to curly quotes, `...` to ellipsis,
* and `--` to em-dash while preserving content inside HTML comments.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
*/
/**
* Apply typographic formatting to text.
*
* - `...` `` (ellipsis)
* - `--` `` (em-dash, but not `---` which is markdown HR)
* - `"text"` `\u201Ctext\u201D` (curly double quotes)
* - `'text'` `\u2018text\u2019` (curly single quotes / apostrophes)
*/
export declare function updateTypography(text: string): string;
/** Alias for `updateTypography` — used by obsidian-plugin and other consumers. */
export declare const applyTypography: typeof updateTypography;

View file

@ -0,0 +1,83 @@
"use strict";
/**
* Typography formatting for markdown files.
*
* Converts straight quotes to curly quotes, `...` to ellipsis,
* and `--` to em-dash while preserving content inside HTML comments.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyTypography = void 0;
exports.updateTypography = updateTypography;
// ─── Typographic Characters ─────────────────────────────────────────────────
const OPEN_DOUBLE = '\u{201C}'; // "
const CLOSE_DOUBLE = '\u{201D}'; // "
const OPEN_SINGLE = '\u{2018}'; // '
const CLOSE_SINGLE = '\u{2019}'; // ' (also used for apostrophes)
const ELLIPSIS = '\u{2026}'; // …
const EM_DASH = '\u{2014}'; // —
// ─── Cached Regex Patterns ──────────────────────────────────────────────────
/** Matches HTML comments: <!-- ... --> */
const COMMENT_RE = /<!--[\s\S]*?-->/g;
/** Matches opening double quote context: after whitespace, line start, or brackets */
const OPEN_DOUBLE_RE = /(^|[\s([{—–-])"/gm;
/** Matches opening single quote context: after whitespace, line start, or brackets */
const OPEN_SINGLE_RE = /(^|[\s([{—–-])'/gm;
/** Matches a closing double quote after an em-dash at end-of-word or line */
const CLOSE_DOUBLE_AFTER_EM_DASH_RE = /—"([\s)\].,;:!?]|$)/gm;
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Apply typographic formatting to text.
*
* - `...` `` (ellipsis)
* - `--` `` (em-dash, but not `---` which is markdown HR)
* - `"text"` `\u201Ctext\u201D` (curly double quotes)
* - `'text'` `\u2018text\u2019` (curly single quotes / apostrophes)
*/
function updateTypography(text) {
let result = text;
// Step 1: Convert ... to ellipsis (must happen before quote processing)
result = result.replaceAll(/\.\.\./g, ELLIPSIS);
// Step 2: Protect HTML comments from em-dash conversion
const protectedComments = [];
result = result.replaceAll(COMMENT_RE, (match) => {
const placeholder = `\x00COMMENT${protectedComments.length}\x00`;
protectedComments.push(match);
return placeholder;
});
// Step 3: Convert -- to em-dash (but preserve --- for markdown HR)
const protectedTriple = '\x00TRIPLE\x00';
result = result.replaceAll(/---/g, protectedTriple);
result = result.replaceAll(/--/g, EM_DASH);
result = result.replaceAll(new RegExp(escapeRegex(protectedTriple), 'g'), '---');
// Step 4: Restore HTML comments
for (let i = 0; i < protectedComments.length; i++) {
result = result.replaceAll(`\x00COMMENT${i}\x00`, protectedComments[i]);
}
// Step 4b: Fix closing quotes after em-dash introduced from --
result = result.replaceAll(CLOSE_DOUBLE_AFTER_EM_DASH_RE, (_match, after) => {
return `${EM_DASH}${CLOSE_DOUBLE}${after}`;
});
// Step 5: Convert double quotes
// Opening: after whitespace, start of line, or opening brackets
result = result.replaceAll(OPEN_DOUBLE_RE, (_match, before) => {
return `${before}${OPEN_DOUBLE}`;
});
// Closing: all remaining straight double quotes
result = result.replaceAll(/"/g, CLOSE_DOUBLE);
// Step 6: Convert single quotes
// Opening: after whitespace, start of line, or opening brackets
result = result.replaceAll(OPEN_SINGLE_RE, (_match, before) => {
return `${before}${OPEN_SINGLE}`;
});
// Closing/apostrophe: all remaining straight single quotes
result = result.replaceAll(/'/g, CLOSE_SINGLE);
return result;
}
/** Alias for `updateTypography` — used by obsidian-plugin and other consumers. */
exports.applyTypography = updateTypography;
function escapeRegex(str) {
return str.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
//# sourceMappingURL=formatting.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"formatting.js","sourceRoot":"","sources":["../src/formatting.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAmCH,4CA+CC;AAhFD,+EAA+E;AAE/E,MAAM,WAAW,GAAG,UAAU,CAAC,CAAG,IAAI;AACtC,MAAM,YAAY,GAAG,UAAU,CAAC,CAAE,IAAI;AACtC,MAAM,WAAW,GAAG,UAAU,CAAC,CAAG,IAAI;AACtC,MAAM,YAAY,GAAG,UAAU,CAAC,CAAE,gCAAgC;AAClE,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAM,IAAI;AACtC,MAAM,OAAO,GAAG,UAAU,CAAC,CAAO,IAAI;AAEtC,+EAA+E;AAE/E,0CAA0C;AAC1C,MAAM,UAAU,GAAG,kBAAkB,CAAC;AAEtC,sFAAsF;AACtF,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAE3C,sFAAsF;AACtF,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAE3C,6EAA6E;AAC7E,MAAM,6BAA6B,GAAG,uBAAuB,CAAC;AAE9D,+EAA+E;AAE/E;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,IAAY;IACzC,IAAI,MAAM,GAAG,IAAI,CAAC;IAElB,wEAAwE;IACxE,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhD,wDAAwD;IACxD,MAAM,iBAAiB,GAAa,EAAE,CAAC;IACvC,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC7C,MAAM,WAAW,GAAG,cAAc,iBAAiB,CAAC,MAAM,MAAM,CAAC;QACjE,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,WAAW,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,mEAAmE;IACnE,MAAM,eAAe,GAAG,gBAAgB,CAAC;IACzC,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACpD,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAEjF,gCAAgC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,iBAAiB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChD,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,+DAA+D;IAC/D,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,6BAA6B,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACxE,OAAO,GAAG,OAAO,GAAG,YAAY,GAAG,KAAK,EAAE,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,gCAAgC;IAChC,gEAAgE;IAChE,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;QAC1D,OAAO,GAAG,MAAM,GAAG,WAAW,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IACH,gDAAgD;IAChD,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAE/C,gCAAgC;IAChC,gEAAgE;IAChE,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;QAC1D,OAAO,GAAG,MAAM,GAAG,WAAW,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IACH,2DAA2D;IAC3D,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAE/C,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,kFAAkF;AACrE,QAAA,eAAe,GAAG,gBAAgB,CAAC;AAEhD,SAAS,WAAW,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,UAAU,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACzD,CAAC"}

9
bindery-core/out/index.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/**
* Bindery Core barrel export.
*
* Re-exports all public APIs from the shared modules.
*/
export * from './formatting';
export * from './settings';
export * from './translations';
export * from './templates';

26
bindery-core/out/index.js Normal file
View file

@ -0,0 +1,26 @@
"use strict";
/**
* Bindery Core barrel export.
*
* Re-exports all public APIs from the shared modules.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./formatting"), exports);
__exportStar(require("./settings"), exports);
__exportStar(require("./translations"), exports);
__exportStar(require("./templates"), exports);
//# sourceMappingURL=index.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;;;;;;;;;;;;;;AAEH,+CAA6B;AAC7B,6CAA2B;AAC3B,iDAA+B;AAC/B,8CAA4B"}

80
bindery-core/out/settings.d.ts vendored Normal file
View file

@ -0,0 +1,80 @@
/**
* Bindery workspace settings types and helpers.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
* Zero dependency on VS Code or Obsidian APIs.
*/
export interface LanguageConfig {
code: string;
folderName: string;
/** Optional per-language export title from settings.json languages[]. */
bookTitle?: string;
chapterWord: string;
actPrefix: string;
prologueLabel: string;
epilogueLabel: string;
/** True for the primary language the book is written in. */
isDefault?: boolean;
/** Dialect exports derived from this language (e.g. en-gb from EN). No story folder of their own. */
dialects?: DialectConfig[];
}
/** A dialect derived from a parent language — same story folder, word substitutions applied at export. */
export interface DialectConfig {
/** Dialect code, used as the key in translations.json (e.g. 'en-gb'). */
code: string;
/** Human-readable label, e.g. 'British English'. */
label?: string;
}
/**
* .bindery/settings.json
*
* bookTitle may be a plain string or a per-language map:
* "bookTitle": "The Hollow Road"
* "bookTitle": { "en": "The Hollow Road", "nl": "De Holle Weg" }
*/
export interface WorkspaceSettings {
bookTitle?: string | Record<string, string>;
author?: string;
/** Short description or tagline used when generating AI assistant files. */
description?: string;
/** Genre of the book (e.g. "sci-fi/fantasy", "mystery", "contemporary fiction"). */
genre?: string;
/** Target audience, e.g. "12+" or "adults" or "8-10". Used to calibrate AI review feedback. */
targetAudience?: string;
/** AI targets previously chosen when running Set Up AI Files (claude, copilot, cursor, agents). */
aiTargets?: string[];
/** Claude skills previously chosen when running Set Up AI Files. */
aiSkills?: string[];
storyFolder?: string;
mergedOutputDir?: string;
mergeFilePrefix?: string;
formatOnSave?: boolean;
languages?: LanguageConfig[];
git?: {
snapshot?: {
pushDefault?: boolean;
remote?: string;
branch?: string;
};
};
}
export declare const BINDERY_FOLDER = ".bindery";
export declare const SETTINGS_FILENAME = "settings.json";
export declare const TRANSLATIONS_FILENAME = "translations.json";
export declare function getBinderyFolder(root: string): string;
export declare function getSettingsPath(root: string): string;
export declare function getTranslationsPath(root: string): string;
export declare function readWorkspaceSettings(root: string): WorkspaceSettings | null;
/**
* Resolve the book title for a given language code.
* Falls back to the English title if no language-specific title is found.
*/
export declare function getBookTitleForLang(settings: WorkspaceSettings | null, langCode: string): string | undefined;
/**
* Return the language marked isDefault, or the first language in the list.
*/
export declare function getDefaultLanguage(settings: WorkspaceSettings | null): LanguageConfig | undefined;
/**
* Return dialects[] for the language matching langCode, or [].
*/
export declare function getDialectsForLanguage(settings: WorkspaceSettings | null, langCode: string): DialectConfig[];

View file

@ -0,0 +1,113 @@
"use strict";
/**
* Bindery workspace settings types and helpers.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
* Zero dependency on VS Code or Obsidian APIs.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TRANSLATIONS_FILENAME = exports.SETTINGS_FILENAME = exports.BINDERY_FOLDER = void 0;
exports.getBinderyFolder = getBinderyFolder;
exports.getSettingsPath = getSettingsPath;
exports.getTranslationsPath = getTranslationsPath;
exports.readWorkspaceSettings = readWorkspaceSettings;
exports.getBookTitleForLang = getBookTitleForLang;
exports.getDefaultLanguage = getDefaultLanguage;
exports.getDialectsForLanguage = getDialectsForLanguage;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
// ─── Constants ───────────────────────────────────────────────────────────────
exports.BINDERY_FOLDER = '.bindery';
exports.SETTINGS_FILENAME = 'settings.json';
exports.TRANSLATIONS_FILENAME = 'translations.json';
// ─── Path helpers ─────────────────────────────────────────────────────────────
function getBinderyFolder(root) {
return path.join(root, exports.BINDERY_FOLDER);
}
function getSettingsPath(root) {
return path.join(root, exports.BINDERY_FOLDER, exports.SETTINGS_FILENAME);
}
function getTranslationsPath(root) {
return path.join(root, exports.BINDERY_FOLDER, exports.TRANSLATIONS_FILENAME);
}
// ─── Readers ─────────────────────────────────────────────────────────────────
function readWorkspaceSettings(root) {
const p = getSettingsPath(root);
if (!fs.existsSync(p)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(p, 'utf-8'));
}
catch {
return null;
}
}
// ─── Accessors ────────────────────────────────────────────────────────────────
/**
* Resolve the book title for a given language code.
* Falls back to the English title if no language-specific title is found.
*/
function getBookTitleForLang(settings, langCode) {
if (!settings?.bookTitle) {
return undefined;
}
if (typeof settings.bookTitle === 'string') {
return settings.bookTitle || undefined;
}
const code = langCode.toLowerCase();
return settings.bookTitle[code]
?? settings.bookTitle['en']
?? undefined;
}
/**
* Return the language marked isDefault, or the first language in the list.
*/
function getDefaultLanguage(settings) {
const langs = settings?.languages;
if (!langs || langs.length === 0) {
return undefined;
}
return langs.find(l => l.isDefault) ?? langs[0];
}
/**
* Return dialects[] for the language matching langCode, or [].
*/
function getDialectsForLanguage(settings, langCode) {
const lang = settings?.languages?.find(l => l.code.toUpperCase() === langCode.toUpperCase());
return lang?.dialects ?? [];
}
//# sourceMappingURL=settings.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EH,4CAEC;AAED,0CAEC;AAED,kDAEC;AAID,sDAQC;AAQD,kDAYC;AAKD,gDAMC;AAKD,wDAQC;AA1ID,4CAAgC;AAChC,gDAAkC;AA+DlC,gFAAgF;AAEnE,QAAA,cAAc,GAAU,UAAU,CAAC;AACnC,QAAA,iBAAiB,GAAO,eAAe,CAAC;AACxC,QAAA,qBAAqB,GAAG,mBAAmB,CAAC;AAEzD,iFAAiF;AAEjF,SAAgB,gBAAgB,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAc,CAAC,CAAC;AAC3C,CAAC;AAED,SAAgB,eAAe,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAc,EAAE,yBAAiB,CAAC,CAAC;AAC9D,CAAC;AAED,SAAgB,mBAAmB,CAAC,IAAY;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAc,EAAE,6BAAqB,CAAC,CAAC;AAClE,CAAC;AAED,gFAAgF;AAEhF,SAAgB,qBAAqB,CAAC,IAAY;IAC9C,MAAM,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IACvC,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAsB,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC;AAED,iFAAiF;AAEjF;;;GAGG;AACH,SAAgB,mBAAmB,CAC/B,QAAkC,EAClC,QAAiB;IAEjB,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC;QAAC,OAAO,SAAS,CAAC;IAAC,CAAC;IAC/C,IAAI,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;QACzC,OAAO,QAAQ,CAAC,SAAS,IAAI,SAAS,CAAC;IAC3C,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACpC,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC;WACxB,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC;WACxB,SAAS,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB,CAC9B,QAAkC;IAElC,MAAM,KAAK,GAAG,QAAQ,EAAE,SAAS,CAAC;IAClC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAAC,OAAO,SAAS,CAAC;IAAC,CAAC;IACvD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,SAAgB,sBAAsB,CAClC,QAAkC,EAClC,QAAgB;IAEhB,MAAM,IAAI,GAAG,QAAQ,EAAE,SAAS,EAAE,IAAI,CAClC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,QAAQ,CAAC,WAAW,EAAE,CACvD,CAAC;IACF,OAAO,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAChC,CAAC"}

30
bindery-core/out/templates.d.ts vendored Normal file
View file

@ -0,0 +1,30 @@
/**
* Bindery AI instruction file templates thin aggregator.
*
* Each template lives in its own file under `./templates/`. Each module
* exports `{ meta, render }` so the version stays glued to the content.
* This file just collects them and exposes the public API:
*
* - `TemplateContext` re-exported from ./templates/context
* - `FILE_VERSION_INFO` built from each module's `meta`
* - `renderTemplate(name, ctx)` dispatches to the right module
*
* SINGLE SOURCE OF TRUTH this is the canonical copy in bindery-core.
* Do not hand-edit copies in mcp-ts/src/ or vscode-ext/src/.
*/
import type { TemplateContext } from './templates/context';
export type { TemplateContext } from './templates/context';
export declare const FILE_VERSION_INFO: Record<string, {
version: number;
label: string;
zip: string | null;
}>;
/**
* Render a named template with the given context.
*
* Top-level file templates: 'claude', 'copilot', 'cursor', 'agents', 'bindery-readme'
* Skill templates: 'review', 'brainstorm', 'memory', 'translate',
* 'translation-review', 'status', 'continuity', 'read-aloud',
* 'read-in', 'proof-read'
*/
export declare function renderTemplate(name: string, ctx: TemplateContext): string;

View file

@ -0,0 +1,107 @@
"use strict";
/**
* Bindery AI instruction file templates thin aggregator.
*
* Each template lives in its own file under `./templates/`. Each module
* exports `{ meta, render }` so the version stays glued to the content.
* This file just collects them and exposes the public API:
*
* - `TemplateContext` re-exported from ./templates/context
* - `FILE_VERSION_INFO` built from each module's `meta`
* - `renderTemplate(name, ctx)` dispatches to the right module
*
* SINGLE SOURCE OF TRUTH this is the canonical copy in bindery-core.
* Do not hand-edit copies in mcp-ts/src/ or vscode-ext/src/.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FILE_VERSION_INFO = void 0;
exports.renderTemplate = renderTemplate;
const claude = __importStar(require("./templates/claude"));
const copilot = __importStar(require("./templates/copilot"));
const cursor = __importStar(require("./templates/cursor"));
const agents = __importStar(require("./templates/agents"));
const binderyReadme = __importStar(require("./templates/bindery-readme"));
const review = __importStar(require("./templates/skills/review"));
const brainstorm = __importStar(require("./templates/skills/brainstorm"));
const memory = __importStar(require("./templates/skills/memory"));
const translate = __importStar(require("./templates/skills/translate"));
const translationReview = __importStar(require("./templates/skills/translation-review"));
const status = __importStar(require("./templates/skills/status"));
const continuity = __importStar(require("./templates/skills/continuity"));
const readAloud = __importStar(require("./templates/skills/read-aloud"));
const readIn = __importStar(require("./templates/skills/read-in"));
const proofRead = __importStar(require("./templates/skills/proof-read"));
const TEMPLATES = {
'claude': claude,
'copilot': copilot,
'cursor': cursor,
'agents': agents,
'bindery-readme': binderyReadme,
'review': review,
'brainstorm': brainstorm,
'memory': memory,
'translate': translate,
'translation-review': translationReview,
'status': status,
'continuity': continuity,
'read-aloud': readAloud,
'read-in': readIn,
'proof-read': proofRead,
};
// ─── File version metadata ────────────────────────────────────────────────────
// Bump per-file version inside the matching module's `meta` when content
// changes significantly so users with outdated content are prompted.
exports.FILE_VERSION_INFO = Object.fromEntries(Object.values(TEMPLATES).map(t => [
t.meta.file,
{ version: t.meta.version, label: t.meta.label, zip: t.meta.zip },
]));
// ─── Entry point ──────────────────────────────────────────────────────────────
/**
* Render a named template with the given context.
*
* Top-level file templates: 'claude', 'copilot', 'cursor', 'agents', 'bindery-readme'
* Skill templates: 'review', 'brainstorm', 'memory', 'translate',
* 'translation-review', 'status', 'continuity', 'read-aloud',
* 'read-in', 'proof-read'
*/
function renderTemplate(name, ctx) {
const t = TEMPLATES[name];
if (!t) {
return `Unknown template: ${name}`;
}
return t.render(ctx);
}
//# sourceMappingURL=templates.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEH,wCAIC;AArED,2DAAsD;AACtD,6DAAuD;AACvD,2DAAsD;AACtD,2DAAsD;AACtD,0EAA8D;AAC9D,kEAA6D;AAC7D,0EAAiE;AACjE,kEAA6D;AAC7D,wEAAgE;AAChE,yFAA2E;AAC3E,kEAA6D;AAC7D,0EAAiE;AACjE,yEAAiE;AACjE,mEAA8D;AAC9D,yEAAiE;AAWjE,MAAM,SAAS,GAAmC;IAC9C,QAAQ,EAAe,MAAM;IAC7B,SAAS,EAAc,OAAO;IAC9B,QAAQ,EAAe,MAAM;IAC7B,QAAQ,EAAe,MAAM;IAC7B,gBAAgB,EAAO,aAAa;IACpC,QAAQ,EAAe,MAAM;IAC7B,YAAY,EAAW,UAAU;IACjC,QAAQ,EAAe,MAAM;IAC7B,WAAW,EAAY,SAAS;IAChC,oBAAoB,EAAG,iBAAiB;IACxC,QAAQ,EAAe,MAAM;IAC7B,YAAY,EAAW,UAAU;IACjC,YAAY,EAAW,SAAS;IAChC,SAAS,EAAc,MAAM;IAC7B,YAAY,EAAW,SAAS;CACnC,CAAC;AAEF,iFAAiF;AACjF,yEAAyE;AACzE,qEAAqE;AAExD,QAAA,iBAAiB,GAC1B,MAAM,CAAC,WAAW,CACd,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,CAAC,CAAC,IAAI,CAAC,IAAI;IACX,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;CACpE,CAAC,CACL,CAAC;AAEN,iFAAiF;AAEjF;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,IAAY,EAAE,GAAoB;IAC7D,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,CAAC,EAAE,CAAC;QAAC,OAAO,qBAAqB,IAAI,EAAE,CAAC;IAAC,CAAC;IAC/C,OAAO,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC"}

View file

@ -0,0 +1,3 @@
import { type TemplateContext, type TemplateMeta } from './context';
export declare const meta: TemplateMeta;
export declare function render(ctx: TemplateContext): string;

View file

@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
const context_1 = require("./context");
exports.meta = {
file: 'AGENTS.md',
version: 8,
label: 'agents instructions',
zip: null,
};
function render(ctx) {
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, memoriesFolder } = ctx;
const lines = [`# Agent Instructions — ${title}`, ''];
lines.push('## Project overview');
if (genre) {
lines.push(`${genre} novel.`);
}
if (description) {
lines.push(description);
}
if (ctx.audience) {
lines.push((0, context_1.audienceNote)(ctx));
}
if (author) {
lines.push(`Author: ${author}.`);
}
lines.push((0, context_1.languageSection)(ctx), '', '## 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 `.claude/skills/` for shared slash workflows before improvising a bespoke process.', '', '## Story files', `- Chapter files are \`.md\` files in \`${storyFolder}/\`, organized 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 normalize them.', '', '## Shared skill workflows', '- Shared workflows live in `.claude/skills/` and can be used by agents beyond Claude when the runtime supports workspace skills.', '- Prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, and `/proof-read` when the user is asking for one of those structured tasks.', '', '## 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}/\` | Story arc files for overall and per-act structure and beats |`, `| \`${notesFolder}/\` | Story notes, like character profiles and world rules |`, `| \`${memoriesFolder}/global.md\` | Cross-session decisions |`);
return lines.join('\n') + '\n';
}
//# sourceMappingURL=agents.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"agents.js","sourceRoot":"","sources":["../../src/templates/agents.ts"],"names":[],"mappings":";;;AASA,wBAyCC;AAlDD,uCAAmG;AAEtF,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,WAAW;IACpB,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,qBAAqB;IAC9B,GAAG,EAAM,IAAI;CAChB,CAAC;AAEF,SAAgB,MAAM,CAAC,GAAoB;IACvC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,GAAG,CAAC;IACvG,MAAM,KAAK,GAAa,CAAC,0BAA0B,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAClC,IAAI,KAAK,EAAQ,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;IAAC,CAAC;IACnD,IAAI,WAAW,EAAE,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAAC,CAAC;IAC7C,IAAI,GAAG,CAAC,QAAQ,EAAC,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,IAAA,sBAAY,EAAC,GAAG,CAAC,CAAC,CAAC;IAAC,CAAC;IACnD,IAAI,MAAM,EAAO,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,WAAW,MAAM,GAAG,CAAC,CAAC;IAAC,CAAC;IACtD,KAAK,CAAC,IAAI,CACN,IAAA,yBAAe,EAAC,GAAG,CAAC,EACpB,EAAE,EACF,qBAAqB,EACrB,aAAa,cAAc,yCAAyC,EACpE,+CAA+C,cAAc,0BAA0B,EACvF,6FAA6F,EAC7F,EAAE,EACF,gBAAgB,EAChB,0CAA0C,WAAW,mCAAmC,EACxF,iFAAiF,EACjF,8FAA8F,EAC9F,EAAE,EACF,2BAA2B,EAC3B,kIAAkI,EAClI,uMAAuM,EACvM,EAAE,EACF,uBAAuB,EACvB,0EAA0E,CAC7E,CAAC;IACF,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,GAAG,CAAC,QAAQ,qDAAqD,CAAC,CAAC;IACnG,CAAC;IACD,KAAK,CAAC,IAAI,CACN,EAAE,EACF,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,OAAO,SAAS,qEAAqE,EACrF,OAAO,WAAW,8DAA8D,EAChF,OAAO,cAAc,0CAA0C,CAClE,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACnC,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from './context';
export declare const meta: TemplateMeta;
export declare function render(ctx: TemplateContext): string;

View file

@ -0,0 +1,138 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.bindery/README.md',
version: 4,
label: 'bindery capabilities',
zip: null,
};
function render(ctx) {
const { title } = ctx;
return `# Bindery — capabilities for this workspace
This file is **the single source agents should consult to answer "What can Bindery do?"**
It is generated by Bindery itself (\`bindery init\` and \`setup_ai_files\`). Edits will be
overwritten on the next setup.
For richer narrative docs (install, contributing, release notes), see the project on
GitHub: <https://github.com/evdboom/Bindery> (README at
<https://github.com/evdboom/Bindery/blob/main/README.md>). Agents with internet access
may fetch it for context; otherwise, treat this file as the complete answer.
> Workspace: **${title}**
## What Bindery is
Bindery is a markdown book authoring toolkit with three surfaces:
1. **VS Code extension** commands, format-on-save, export to DOCX/EPUB/PDF.
2. **MCP server** programmatic tools for AI assistants (Claude, Copilot, Codex, Cursor).
3. **Skill workflows** opinionated slash-commands like \`/review\`, \`/translate\`, \`/memory\`.
All three operate on the same workspace state: \`.bindery/settings.json\`,
\`.bindery/translations.json\`, the story folder, \`Notes/\`, \`Arc/\`, and
\`.bindery/memories/\`.
## VS Code commands (Command Palette "Bindery: …")
| Command | What it does |
|---|---|
| \`Bindery: Initialize Workspace\` | Create \`.bindery/settings.json\`, \`.bindery/translations.json\`, and this README. |
| \`Bindery: Setup AI Assistant Files\` | Generate CLAUDE.md / copilot-instructions.md / cursor rules / AGENTS.md and refresh this capabilities doc. |
| \`Bindery: Format Typography\` / \`Format All Markdown in Folder\` | Curly quotes, em-dashes, ellipses, etc. |
| \`Bindery: Merge Chapters → Markdown / DOCX / EPUB / PDF / All Formats\` | Build a deliverable from chapter files. |
| \`Bindery: Find Probable US→UK Words\` | Surface probable US spellings in EN source. |
| \`Bindery: Add Dialect Rule\` / \`Add Translation (Glossary)\` / \`Add Language\` | Maintain dialect substitutions, glossary entries, and language scaffolding. |
| \`Bindery: Open translations.json\` | Open the per-language rules file. |
| \`Bindery: Insert Review Start Marker (or wrap selection)\` | Insert \`<!-- Bindery: Review start -->\`, or wrap the current selection in matched start/stop markers. |
| \`Bindery: Insert Review Stop Marker\` | Insert \`<!-- Bindery: Review stop -->\` at the cursor. |
| \`Bindery: Register MCP Server\` | Write \`.vscode/mcp.json\` so Claude / Codex pick the bundled server up. |
### Default keybindings (markdown editors only)
| Shortcut | Command |
|---|---|
| \`Ctrl+K Ctrl+B\` | Format Typography |
| \`Ctrl+K Ctrl+,\` | Insert Review Start Marker (or wrap selection) |
| \`Ctrl+K Ctrl+.\` | Insert Review Stop Marker |
These only fire while a markdown file is focused (\`editorTextFocus && resourceLangId == markdown\`). Rebind in **File → Preferences → Keyboard Shortcuts** if they collide with another extension. On macOS, swap \`Ctrl\` for \`Cmd\`.
## MCP tools
Use these from any MCP-aware assistant. Tools tagged **(reads)** are safe; tools
tagged **(writes)** modify files or git state.
| Tool | What it does |
|---|---|
| \`health\` (reads) | Workspace status: settings, index, AI-file versions, embedding backend. |
| \`identify_book\` (reads) | Confirm the active book root and registry entry. |
| \`init_workspace\` (writes) | Create \`.bindery/settings.json\` with title/author/story-folder defaults. |
| \`update_workspace\` (writes) | \`git fetch\` + pull, optional branch switch, optional auto-stash. |
| \`settings_update\` (writes) | Patch \`.bindery/settings.json\` from an agent. |
| \`setup_ai_files\` (writes) | (Re)generate AI instruction files + refresh this capabilities doc. |
| \`get_text\` / \`get_chapter\` / \`get_book_until\` / \`get_overview\` / \`get_notes\` (reads) | Read source files, chapters, ranges, and notes. |
| \`search\` (reads) | BM25 / semantic search across the corpus (Ollama optional). |
| \`index_build\` / \`index_status\` (writes / reads) | Build or inspect the lexical/semantic index. |
| \`format\` (writes) | Apply typography to a file or folder (\`dryRun\` supported). |
| \`get_review_text\` (writes) | Returns the **git diff of uncommitted changes** *plus* any regions wrapped in \`<!-- Bindery: Review start --> ... <!-- Bindery: Review stop -->\` markers. Marker regions surface even after a commit, so committed work-in-progress can still be reviewed. \`autoStage: true\` stages files **and** consumes (removes) the marker lines. |
| \`git_snapshot\` (writes) | Commit changes in story / notes / arc folders, optional push. |
| \`get_translation\` / \`add_translation\` (reads / writes) | Glossary lookup and upsert per target language. |
| \`get_dialect\` / \`add_dialect\` (reads / writes) | Dialect substitution lookup and upsert (e.g. \`en-gb\`). |
| \`add_language\` (writes) | Scaffold a new language under the story folder. |
| \`memory_list\` / \`memory_append\` / \`memory_compact\` (reads / writes) | Manage \`.bindery/memories/\` files. |
| \`chapter_status_get\` / \`chapter_status_update\` (reads / writes) | Per-chapter progress tracker in \`.bindery/chapter-status.json\`. |
## Review markers
Bindery review markers let an author tag arbitrary regions for the next review
pass even after those edits have been committed. Use them when you commit
work-in-progress and continue on another machine.
\`\`\`
<!-- Bindery: Review start -->
...lines you want reviewed...
<!-- Bindery: Review stop -->
\`\`\`
- The stop marker is **optional** an unclosed start runs to end of file.
- Multiple regions per file are supported.
- Insert markers via the VS Code commands above, or just type them.
- \`get_review_text(autoStage: true)\` returns marker regions in addition to the
git diff and **removes** the marker lines as part of staging, so the next
review pass starts clean.
## Skill workflows (Claude / Copilot Chat / etc.)
Generated under \`.claude/skills/<name>/SKILL.md\`. Trigger them with a slash
command or by paraphrasing the description.
| Skill | Trigger phrases |
|---|---|
| \`/review\` | "review chapter X", "review my changes", "quick review" |
| \`/translation-review\` | "review my translation", "what do you think" (translation focus) |
| \`/translate\` | "translate chapter X", "spot-check the NL of chapter X" |
| \`/brainstorm\` | "I'm stuck", "help me think of ideas" |
| \`/memory\` | "save what we decided", end-of-session memory updates |
| \`/status\` | "where are we", "what's the book status" |
| \`/continuity\` | "check chapter X for errors", "continuity check" |
| \`/read-aloud\` | "read this aloud", reading-aloud test |
| \`/read-in\` | start-of-session context loading |
| \`/proof-read\` | "proofread chapter X", "get reader feedback" |
## How to answer "What can Bindery do?"
When an agent is asked what Bindery can do for this project, it should:
1. Read **this file** (\`.bindery/README.md\`) — it is intentionally complete.
2. Cross-check live state with \`health\` if the user asks about the index,
AI-file freshness, or the embedding backend.
3. For narrative / install / contribution guidance, point the user to
<https://github.com/evdboom/Bindery> (or fetch the README from
<https://github.com/evdboom/Bindery/blob/main/README.md> if internet access
is available) not for capability listings.
`;
}
//# sourceMappingURL=bindery-readme.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"bindery-readme.js","sourceRoot":"","sources":["../../src/templates/bindery-readme.ts"],"names":[],"mappings":";;;AASA,wBA8HC;AArIY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,oBAAoB;IAC7B,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,sBAAsB;IAC/B,GAAG,EAAM,IAAI;CAChB,CAAC;AAEF,SAAgB,MAAM,CAAC,GAAoB;IACvC,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;IACtB,OAAO;;;;;;;;;;;iBAWM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgHrB,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import { type TemplateContext, type TemplateMeta } from './context';
export declare const meta: TemplateMeta;
export declare function render(ctx: TemplateContext): string;

View file

@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
const context_1 = require("./context");
exports.meta = {
file: 'CLAUDE.md',
version: 11,
label: 'project instructions',
zip: null,
};
function render(ctx) {
const { title, author, description, genre, storyFolder, notesFolder, arcFolder } = ctx;
const lines = [
`# Claude — ${title}`,
'',
'## Project',
];
if (genre) {
lines.push(`Genre: ${genre}.`);
}
if (description) {
lines.push(description);
}
if (ctx.audience) {
lines.push((0, context_1.audienceNote)(ctx));
}
if (author) {
lines.push(`Author: ${author}.`);
}
lines.push((0, context_1.languageSection)(ctx), '## Start of session', '1. Use /read-in at the start of a session to load context and get your bearings.', '2. Run `health` from the Bindery MCP and check `ai_versions_outdated`.', '3. If `ai_versions_outdated` has entries, run `setup_ai_files` and present the returned `skill_zips.reupload_required` list to the user for Claude Desktop.', '4. If the skill or MCP server is not available, read at least COWORK.md (if present) for current focus and context.', '', '## Memory system', '1. When concluding a discussion, or after you give a meaningful, preservation-worthy response: use /memory to store it.', '2. Also when the user asks or otherwise indicates the end of a session: use /memory to save decisions.', '', '## Repo layout', '```', `${arcFolder}/ ← story arc files`, `${notesFolder}/ ← story notes (characters, world)`, `${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('', '## 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 |', '| `/translation-review` | Review a hand-crafted translation against the source |', '| `/status` | Book progress snapshot |', '| `/continuity` | Check a chapter for consistency errors |', '| `/read-aloud` | Test how a passage reads when spoken |', '| `/read-in` | Load context and get your bearings at the start of a session |', '| `/proof-read` | Read the book as multiple proofreaders and present the findings |', '', '## MCP server (bindery-mcp)', '', 'All tools require a `book` argument. Use `list_books` to discover available names.', 'Prefer these tools over Read/Bash when they apply.', '', '| 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 |', '| `init_workspace` | Create or update `.bindery/settings.json` and `translations.json` |', '| `setup_ai_files` | Regenerate AI instruction files, rebuild Claude skill zip files, and return a change manifest |', '| `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_book_until` | Fetch chapters from 1..N (or start..N) in one call, concatenated in reading order |', '| `get_overview` | Chapter structure — acts, chapters, titles |', '| `get_notes` | Notes/ files, filterable by category or name |', '| `search` | BM25 full-text search with ranked snippets, optional semantic ranking |', '| `format` | Apply typography formatting to a file or folder |', '| `get_review_text` | Structured git diff with review-marker regions and optional auto-staging that consumes markers |', '| `update_workspace` | Fetch and pull the current branch, with branch/default-branch reporting |', '| `git_snapshot` | Git commit of story, notes, and arc changes, with optional push |', '| `get_translation` | List glossary entries for a language, or look up a specific term (forgiving) |', '| `add_translation` | Add or update a cross-language glossary entry (agent reference, not auto-applied) |', '| `get_dialect` | List dialect substitution rules, or look up a specific word |', '| `add_dialect` | Add or update a dialect substitution rule (auto-applied at export, e.g. US→UK) |', '| `add_language` | Add a language to settings.json and scaffold its story folder with stubs |', '| `settings_update` | Merge a partial patch into settings.json without replacing unrelated keys |', '| `memory_list` | List `.bindery/memories/` files with line counts |', '| `memory_append` | Append a dated session entry to a file in `.bindery/memories/` |', '| `memory_compact` | Overwrite a file in `.bindery/memories/` with a summary (backs up original to `.bindery/memories/archive/`) |', '| `chapter_status_get` | Read the chapter progress tracker — entries grouped by status |', '| `chapter_status_update` | Upsert chapter progress entries (send only changed chapters) |');
return lines.filter(l => l !== '\n').join('\n') + '\n';
}
//# sourceMappingURL=claude.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/templates/claude.ts"],"names":[],"mappings":";;;AASA,wBA6FC;AAtGD,uCAAmG;AAEtF,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,WAAW;IACpB,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,sBAAsB;IAC/B,GAAG,EAAM,IAAI;CAChB,CAAC;AAEF,SAAgB,MAAM,CAAC,GAAoB;IACvC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC;IACvF,MAAM,KAAK,GAAa;QACpB,cAAc,KAAK,EAAE;QACrB,EAAE;QACF,YAAY;KACf,CAAC;IACF,IAAI,KAAK,EAAQ,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC;IAAC,CAAC;IACpD,IAAI,WAAW,EAAE,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAAC,CAAC;IAC7C,IAAI,GAAG,CAAC,QAAQ,EAAC,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,IAAA,sBAAY,EAAC,GAAG,CAAC,CAAC,CAAC;IAAC,CAAC;IACnD,IAAI,MAAM,EAAO,CAAC;QAAC,KAAK,CAAC,IAAI,CAAC,WAAW,MAAM,GAAG,CAAC,CAAC;IAAC,CAAC;IACtD,KAAK,CAAC,IAAI,CACN,IAAA,yBAAe,EAAC,GAAG,CAAC,EACpB,qBAAqB,EACrB,kFAAkF,EAClF,wEAAwE,EACxE,6JAA6J,EAC7J,qHAAqH,EACrH,EAAE,EACF,kBAAkB,EAClB,yHAAyH,EACzH,wGAAwG,EACxG,EAAE,EACF,gBAAgB,EAChB,KAAK,EACL,GAAG,SAAS,sBAAsB,EAClC,GAAG,WAAW,sCAAsC,EACpD,GAAG,WAAW,GAAG,EACjB,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,QAAQ,CAAC,CAAC,IAAI,iCAAiC,CAAC,EAC3F,KAAK,EACL,EAAE,EACF,kBAAkB,EAClB,yEAAyE,EACzE,4FAA4F,EAC5F,6HAA6H,CAChI,CAAC;IACF,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,yBAAyB,GAAG,CAAC,QAAQ,wDAAwD,CAAC,CAAC;IAC9G,CAAC;IACD,KAAK,CAAC,IAAI,CACN,EAAE,EACF,qBAAqB,EACrB,2DAA2D,EAC3D,uBAAuB,EACvB,WAAW,EACX,yFAAyF,EACzF,yDAAyD,EACzD,2DAA2D,EAC3D,oDAAoD,EACpD,kFAAkF,EAClF,wCAAwC,EACxC,4DAA4D,EAC5D,0DAA0D,EAC1D,+EAA+E,EAC/E,qFAAqF,EACrF,EAAE,EACF,6BAA6B,EAC7B,EAAE,EACF,oFAAoF,EACpF,oDAAoD,EACpD,EAAE,EACF,yBAAyB,EACzB,WAAW,EACX,mDAAmD,EACnD,gEAAgE,EAChE,kEAAkE,EAClE,0FAA0F,EAC1F,sHAAsH,EACtH,iEAAiE,EACjE,4DAA4D,EAC5D,2EAA2E,EAC3E,iEAAiE,EACjE,0GAA0G,EAC1G,iEAAiE,EACjE,gEAAgE,EAChE,sFAAsF,EACtF,gEAAgE,EAChE,wHAAwH,EACxH,kGAAkG,EAClG,sFAAsF,EACtF,sGAAsG,EACtG,2GAA2G,EAC3G,iFAAiF,EACjF,oGAAoG,EACpG,+FAA+F,EAC/F,mGAAmG,EACnG,sEAAsE,EACtE,sFAAsF,EACtF,oIAAoI,EACpI,0FAA0F,EAC1F,4FAA4F,CAC/F,CAAC;IACF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAC3D,CAAC"}

36
bindery-core/out/templates/context.d.ts vendored Normal file
View file

@ -0,0 +1,36 @@
/**
* Shared types + tiny helpers for every template module.
*
* Each template file under `templates/` exports `{ meta, render }` and
* imports `TemplateContext` (and helpers when needed) from this module.
*/
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;
}
/** Per-template metadata. `zip` is non-null only for skills (which ship as zips). */
export interface TemplateMeta {
/** Output path relative to the workspace root. Used as the FILE_VERSION_INFO key. */
file: string;
/** Bump when content changes significantly so users are prompted to refresh. */
version: number;
/** Short, human-readable label used by health reporting. */
label: string;
/** Companion zip path (skills only) or null. */
zip: string | null;
}
export declare function audienceNote(ctx: TemplateContext): string;
export declare function languageSection(ctx: TemplateContext): string;

View file

@ -0,0 +1,20 @@
"use strict";
/**
* Shared types + tiny helpers for every template module.
*
* Each template file under `templates/` exports `{ meta, render }` and
* imports `TemplateContext` (and helpers when needed) from this module.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.audienceNote = audienceNote;
exports.languageSection = languageSection;
function audienceNote(ctx) {
return ctx.audience ? `Target audience: ${ctx.audience}.` : '';
}
function languageSection(ctx) {
if (!ctx.hasMultiLang) {
return '';
}
return `\nLanguages: ${ctx.langList}.\n`;
}
//# sourceMappingURL=context.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/templates/context.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AA6BH,oCAEC;AAED,0CAGC;AAPD,SAAgB,YAAY,CAAC,GAAoB;IAC7C,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,oBAAoB,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,SAAgB,eAAe,CAAC,GAAoB;IAChD,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;IACrC,OAAO,gBAAgB,GAAG,CAAC,QAAQ,KAAK,CAAC;AAC7C,CAAC"}

View file

@ -0,0 +1,3 @@
import { type TemplateContext, type TemplateMeta } from './context';
export declare const meta: TemplateMeta;
export declare function render(ctx: TemplateContext): string;

View file

@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
const context_1 = require("./context");
exports.meta = {
file: '.github/copilot-instructions.md',
version: 8,
label: 'copilot instructions',
zip: null,
};
function render(ctx) {
const { title, author, description, genre, storyFolder, notesFolder, arcFolder } = ctx;
const lines = [`# 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((0, context_1.audienceNote)(ctx));
}
if (author) {
lines.push(`Author: ${author}.`);
}
lines.push((0, context_1.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`), '```', '', '## Shared skill workflows', '- Workspace skill files live in `.claude/skills/` and may also be picked up by agents beyond Claude.', '- Prefer those shared slash workflows when available: `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, `/proof-read`.', '', '## 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 normalize them.');
if (ctx.audience) {
lines.push(`- Content targets ${ctx.audience}. Keep vocabulary accessible and themes appropriate.`);
}
return lines.join('\n') + '\n';
}
//# sourceMappingURL=copilot.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"copilot.js","sourceRoot":"","sources":["../../src/templates/copilot.ts"],"names":[],"mappings":";;;AASA,wBAgCC;AAzCD,uCAAmG;AAEtF,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,iCAAiC;IAC1C,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,sBAAsB;IAC/B,GAAG,EAAM,IAAI;CAChB,CAAC;AAEF,SAAgB,MAAM,CAAC,GAAoB;IACvC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC;IACvF,MAAM,KAAK,GAAa,CAAC,sBAAsB,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5D,IAAI,KAAK,IAAI,WAAW,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACzB,IAAI,KAAK,EAAQ,CAAC;YAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;QAAC,CAAC;QACnD,IAAI,WAAW,EAAE,CAAC;YAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAAC,CAAC;QAC7C,IAAI,GAAG,CAAC,QAAQ,EAAC,CAAC;YAAC,KAAK,CAAC,IAAI,CAAC,IAAA,sBAAY,EAAC,GAAG,CAAC,CAAC,CAAC;QAAC,CAAC;QACnD,IAAI,MAAM,EAAO,CAAC;YAAC,KAAK,CAAC,IAAI,CAAC,WAAW,MAAM,GAAG,CAAC,CAAC;QAAC,CAAC;QACtD,KAAK,CAAC,IAAI,CAAC,IAAA,yBAAe,EAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,KAAK,CAAC,IAAI,CACN,gBAAgB,EAChB,KAAK,EACL,GAAG,SAAS,sBAAsB,EAClC,GAAG,WAAW,+CAA+C,EAC7D,GAAG,WAAW,GAAG,EACjB,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,QAAQ,CAAC,CAAC,IAAI,WAAW,CAAC,EACrE,KAAK,EACL,EAAE,EACF,2BAA2B,EAC3B,sGAAsG,EACtG,sLAAsL,EACtL,EAAE,EACF,uBAAuB,EACvB,uFAAuF,EACvF,mGAAmG,CACtG,CAAC;IACF,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,qBAAqB,GAAG,CAAC,QAAQ,sDAAsD,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACnC,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from './context';
export declare const meta: TemplateMeta;
export declare function render(ctx: TemplateContext): string;

View file

@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.cursor/rules',
version: 8,
label: 'cursor rules',
zip: null,
};
function render(ctx) {
const { title, storyFolder, notesFolder, arcFolder, memoriesFolder } = ctx;
const lines = [
`# 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}/\` — story arc files for overall and per-act structure and beats`,
`- \`${notesFolder}/\` — story notes, like character profiles and world rules`,
'- Shared workflows live in `.claude/skills/`; if your runtime exposes them, prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, and `/proof-read` for those tasks.',
'',
'## Rules',
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not story content.',
'- Do not normalize 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';
}
//# sourceMappingURL=cursor.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/templates/cursor.ts"],"names":[],"mappings":";;;AASA,wBAwBC;AA/BY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,eAAe;IACxB,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,cAAc;IACvB,GAAG,EAAM,IAAI;CAChB,CAAC;AAEF,SAAgB,MAAM,CAAC,GAAoB;IACvC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,GAAG,CAAC;IAC3E,MAAM,KAAK,GAAa;QACpB,oBAAoB,KAAK,EAAE;QAC3B,EAAE;QACF,mBAAmB,WAAW,KAAK;QACnC,mBAAmB,WAAW,KAAK;QACnC,iBAAiB,SAAS,yDAAyD;QACnF,EAAE;QACF,0BAA0B;QAC1B,OAAO,cAAc,mEAAmE;QACxF,OAAO,SAAS,mEAAmE;QACnF,OAAO,WAAW,4DAA4D;QAC9E,uOAAuO;QACvO,EAAE;QACF,UAAU;QACV,oGAAoG;QACpG,4FAA4F;QAC5F,qEAAqE;KACxE,CAAC;IACF,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,wBAAwB,GAAG,CAAC,QAAQ,sDAAsD,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACnC,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/brainstorm/SKILL.md',
version: 11,
label: 'brainstorm skill',
zip: '.claude/skills/brainstorm.zip',
};
function render(_ctx) {
return `---
name: brainstorm
description: Bindery workspace - Brainstorm story ideas, plot beats, character moments, or scene concepts. Use for /brainstorm, "I'm stuck", "help me think of ideas", or "Am I stuck?".
---
# Skill: /brainstorm
Brainstorm story ideas, character moments, or plot solutions.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/brainstorm\`, "I'm stuck", "help me think of ideas", or "Am I stuck?".
## Clarify first
- Scope: plot beat | character moment | scene idea | chapter open/close
- Chapter/story point: specify one
- Constraints: list any
## Tools
Use these Bindery MCP tools to gather context:
- \`search(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 ".bindery/settings.json" with \`get_text\` to pick up the current book's genre, target audience, and story structure.
2. Read \`.bindery/memories/global.md\` and the relevant arc file from \`Arc/\`.
3. If chapter specific, read \`.bindery/memories/chXX.md\` if it exists.
4. If character-focused, use \`get_notes(category: "Characters")\` for character profiles.
5. Use \`search\` to find related moments or themes already in the book.
6. 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 the book's configured target audience
`;
}
//# sourceMappingURL=brainstorm.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"brainstorm.js","sourceRoot":"","sources":["../../../src/templates/skills/brainstorm.ts"],"names":[],"mappings":";;;AASA,wBAgDC;AAvDY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,oCAAoC;IAC7C,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,kBAAkB;IAC3B,GAAG,EAAM,+BAA+B;CAC3C,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/continuity/SKILL.md',
version: 12,
label: 'continuity skill',
zip: '.claude/skills/continuity.zip',
};
function render(_ctx) {
return `---
name: continuity
description: Bindery workspace - Cross-check a chapter for consistency errors in characters, world rules, or timeline. Use for /continuity, "check continuity", or "check chapter X for errors".
---
# Skill: /continuity
Cross-check a chapter for consistency errors.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## 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 a specific chapter
- \`get_book_until(chapterNumber, language, startChapter?)\` — load prior chapters in one call for timeline/continuity context
- \`get_notes(category, name)\` — look up character profiles or world rules
- \`search(query, language)\` — find earlier mentions of a character detail or event
- \`memory_list\` — check whether a chapter-specific memory file exists (\`chXX.md\`)
## Steps
1. Use \`get_text(".bindery/settings.json")\` to pick up the current book's structure and conventions.
2. Use \`get_chapter\` to read the chapter.
3. Use \`get_text\` to read \`.bindery/memories/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.
4. For world rules: use \`get_notes(category: "World")\`.
5. For timeline and continuity drift checks: use \`get_book_until\` up to the focus chapter. If unavailable, fall back to \`get_chapter\` for nearby prior chapters.
6. Use \`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
`;
}
//# sourceMappingURL=continuity.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"continuity.js","sourceRoot":"","sources":["../../../src/templates/skills/continuity.ts"],"names":[],"mappings":";;;AASA,wBAgDC;AAvDY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,oCAAoC;IAC7C,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,kBAAkB;IAC3B,GAAG,EAAM,+BAA+B;CAC3C,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/memory/SKILL.md',
version: 11,
label: 'memory skill',
zip: '.claude/skills/memory.zip',
};
function render(_ctx) {
return `---
name: memory
description: Bindery workspace - Save session decisions to persistent memory files using Bindery MCP tools. Use for /memory, "save this to memory", "update memories", or at end of session.
---
# Skill: /memory
Update project memory files with decisions from the current session.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/memory\`, "save this to memory", "update memories", at meaningful points, 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
### 0. Cross-check assistant memory (if available)
If the runtime has local/session memory, review entries from this session.
Promote repo-worthy entries into Step 3 content.
Promote:
- Story/craft decisions
- Character or world rules
- Structural decisions needed in future sessions
- Anything that must survive across devices
Keep local only:
- Workflow/tool preferences
- Assistant behavior feedback
- Setup/environment notes
- Session-local context
If no local/session memory exists, skip this step.
### 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. \`"Elder 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:
- Summarize 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
`;
}
//# sourceMappingURL=memory.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../../src/templates/skills/memory.ts"],"names":[],"mappings":";;;AASA,wBAyEC;AAhFY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,gCAAgC;IACzC,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,cAAc;IACvB,GAAG,EAAM,2BAA2B;CACvC,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,249 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/proof-read/SKILL.md',
version: 5,
label: 'proof-read skill',
zip: '.claude/skills/proof-read.zip',
};
function render(_ctx) {
return `---
name: proof-read
description: Bindery workspace - Multi-perspective proofreading using isolated reader and author personas. Each persona runs as a scoped subagent with no arc, notes, or memory context only the reading-text payload for the read-so-far experience (chapters 1..N). Use for /proof-read, "proofread chapter X", "get reader feedback", "how does this land with readers", "simulate reader reactions", or "peer review".
---
# Skill: /proof-read
Simulates a panel of readers reviewing a chapter as genuine first-time readers no arc knowledge, no notes, no memory of prior sessions. Each persona runs as an isolated subagent that only sees the reading-text payload so far (chapters 1..N) and their assigned role.
The value is in the isolation. A reader doesn't know what the arc says should happen, what a character's backstory is, or what the chapter was *trying* to do. That's exactly the feedback you can't give yourself, and can't get from an agent that has been working on the book with you.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/proof-read\`, "proofread chapter X", "get reader feedback", "how does this land with readers", "simulate reader reactions", or "peer review".
## Steps
### Step 0: Load project context
Before asking the user anything, read the project settings:
\`\`\`
get_text(".bindery/settings.json")
\`\`\`
Extract:
- \`targetAudience\` — used to calibrate reader personas (age, reading level)
- \`genre\` — used to construct the genre-fan persona and to generate author suggestions if needed
- \`proof_read.authors\` — the stored author panel for this project (may be absent)
If \`settings.json\` has no \`proof_read\` section yet, that's expected on first run — handle it in the author setup step below.
### Step 1: Author panel setup
**If \`proof_read.authors\` is set:**
Present the stored authors and confirm:
> "I have [Author A], [Author B], and [Author C] saved for this project. Shall I use them, or would you like to change the panel?"
If the user wants to change: follow the "no authors stored" flow below, then update settings.
**If \`proof_read.authors\` is not set (first run):**
Ask:
> "No author panel configured yet for this project. Would you like suggestions based on the genre, or do you have specific writers in mind?"
- If **suggestions**: generate 4-5 relevant author names based on the book's genre, audience, and tone (see Author Suggestions below). Present them with a one-line description each. Let the user pick 23.
- If **own names**: accept the user's list as-is.
Once the panel is confirmed, store it back to settings:
\`\`\`
settings_update({ patch: { proof_read: { authors: [ { name: "...", known_for: "...", reads_for: "..." } ] } } })
\`\`\`
The \`reads_for\` field is a short phrase describing what this author's lens brings — e.g. "pacing of reveals, handling of danger for the age group". Generate it at storage time so it's available for subagent prompts without needing a web lookup later.
### Step 2: Gather remaining parameters
Ask:
1. Which chapter to focus on or the whole book?
2. Quick run (2 readers + 1 author) or full run (all 4 readers + full author panel)?
If the user invoked \`/proof-read 7\` or similar, the focus chapter is known — no need to ask.
### Step 3: Fetch the reading context
A real reader arrives at chapter N having read everything before it. Subagents receive the full text from chapter 1 up to and including the focus chapter not a summary, not just the target chapter in isolation.
**Why not a summary of prior chapters?** Any summary written by an agent who has worked on the book will carry arc knowledge framing, foreshadowing, loaded context. It biases the subagent in ways a real reader wouldn't be. Full text preserves the isolation.
**Why not have subagents call MCP themselves?** Subagents with MCP access could accidentally pull notes, arc files, or overviews. Using a pre-written staging file and passing only that payload to subagents reduces that risk and is the best available way in this workflow to keep them focused on reader-visible text.
Use \`get_book_until(chapterNumber: n, language)\` to fetch all prior chapters in one call. If unavailable, loop \`get_chapter(1)\` through \`get_chapter(n)\` in the main agent. For a **whole-book** run, fetch all chapters.
Once the text is retrieved, **write it to a staging file**:
\`.bindery/proof-read-payload.md\`
If the file already exists from a previous run, overwrite it.
Modern context windows handle full books comfortably a 20-chapter 12+ novel is roughly 60-80k words, well within range.
### Step 4: Spawn all subagents in a single turn
Launch all persona subagents in parallel. Each receives:
- Their persona description (constructed from project context see Reader Personas and Author Personas below)
- The path to the staging file written in Step 3
- The review task (see Review Task Template) which instructs them to read the staging file as their **only** file access
- An explicit reminder that they have no prior knowledge of this book beyond what they read from that file
### Step 5: Aggregate
Once all subagents return, aggregate across the full panel:
1. **Consensus positives** moments or elements praised by a multitude of readers. These are your strongest material.
2. **Consensus issues** problems flagged by a multitude of readers. Highest priority to address.
3. **Notable divergences** where one reader type loved something another didn't. Not automatically a problem, but a useful creative signal (e.g. a core reader engaged by a worldbuilding passage that lost the reluctant reader).
4. **Author notes** surface separately. These are craft-level observations, not reader reactions, and shouldn't be averaged against them.
Present individual reactions first (summarised), then the aggregated view. Close with a short prioritised action list.
---
## Reader Personas
Reader personas are constructed from the project's \`targetAudience\` and \`genre\` settings — do not hardcode ages or genre references. Use the actual values from settings.
The four reader roles stay stable, but R1 and R3 should be chosen relative to the book's genre rather than treated as fixed labels:
**R1 Core Reader**
A reader at the target age who actively seeks out this kind of book. If the project is fantasy, this is a fantasy reader; if it is realistic contemporary fiction, this is a realistic-fiction reader. They know what this corner does well, enjoy its native pleasures, and notice quickly when the execution is strong or weak.
**R2 Curious Reader**
A reader at the target age who reads regularly but not primarily in this genre. Open and engaged, but reacts as an outsider to genre conventions.
**R3 Opposite-Corner Reader**
A reader at the target age whose tastes pull away from the book's home genre. Their job is to test whether the text still works for someone who does not naturally prize this genre's default strengths. For fantasy, this might be a realism-first reader who cares most about emotional plausibility and character grounding. For realistic fiction, it should be a reader from a different corner, such as mystery, thriller, romance, horror, or speculative fiction, who wants a stronger external hook or a different kind of momentum.
**R4 Reluctant Reader**
A reader at the target age who reads when they have to. Will notice immediately if something drags or confuses. Short patience for exposition. Will find genuine excitement if it's there — but won't invent it.
When building the subagent prompt, fill in the actual age range and genre from settings. Choose R3 as the deliberate contrast to the project's genre, not always as "the realist". For example, if \`targetAudience\` is "12+" and \`genre\` is "sci-fi/fantasy crossover", R1 becomes: *"You are 12-13 years old. You read a lot and you love sci-fi and fantasy..."* If the genre is realistic contemporary fiction, R3 should instead come from a different reading corner, such as mystery, thriller, or speculative fiction.
---
## Author Personas
Author personas come from \`proof_read.authors\` in settings. Each entry has \`name\`, \`known_for\`, and \`reads_for\`. Use these fields directly in the subagent prompt — no need to reconstruct them.
### Author Suggestions
When the user asks for suggestions, generate a shortlist of 4-5 authors whose work overlaps meaningfully with the book's genre, tone, and target audience. Good criteria:
- Writes for approximately the same age group
- Works in the same genre or a closely adjacent one
- Has a distinctive craft lens that adds something different from the others (e.g. one known for worldbuilding, one for pacing, one for character voice)
- Ideally at least one who writes in a "neighbouring" genre (e.g. for a fantasy book, a post-apocalyptic author) to get an outside-genre craft read
Present each suggestion with: name, one well-known title, and what their lens would add to the review.
---
## Review Task Template
For **reader personas**:
> You are [PERSONA DESCRIPTION built from project settings].
>
> You are reading [TARGET CHAPTER OR BOOK] from a [GENRE] novel aimed at [TARGET AUDIENCE] readers. You have no prior knowledge of this book no plot summaries, no character guides, no notes. You are reading this cold, exactly as you would if you'd just picked it up.
> [CHAPTER NOTE: if the focus is a single chapter, say, "you read up to and including chapter N, focus your feedback on chapter N"]
>
> The text is in the file at: \`[STAGING FILE PATH]\`
> Read that file using the \`read_file\` tool. **That is the only file you may access.** Do not call any other tool, MCP server, or external resource.
>
> Give your honest reaction as this reader. Cover:
> 1. Your overall impression (1-2 sentences)
> 2. Moments that worked where you were engaged, what you enjoyed
> 3. Moments that didn't land confusion, slow patches, anything that pulled you out
> 4. Characters: did they feel real? Did you care what happened to them?
> 5. Specific lines or passages worth flagging (positive or negative) quote them
> 6. Would you keep reading? Why or why not?
>
> Be specific. Quote the text when it helps. Do not summarise the plot react to it.
For **author personas**:
> You are reading this book as [AUTHOR NAME], author of [KNOWN_FOR], giving peer feedback to a fellow writer. The book is aimed at [TARGET AUDIENCE] readers. You have no prior knowledge of the manuscript beyond this text.
> [CHAPTER NOTE: if the focus is a single chapter, say, "you read up to and including chapter N, focus your feedback on chapter N"]
>
> Your particular focus: [READS_FOR].
>
> The manuscript is in the file at: \`[STAGING FILE PATH]\`
> Read that file using the \`read_file\` tool. **That is the only file you may access.** Do not call any other tool, MCP server, or external resource.
>
> Give craft-level feedback: what's working and why, what isn't and how you'd think about fixing it. Voice, pacing, structure, dialogue, the handling of tension. Quote the text when useful. Be honest this is peer review, not encouragement.
---
## Output Format
\`\`\`
## Proof-read: [Book title if available] / Chapter [N] [Chapter title if available]
### Reader reactions
**R1 Core reader**
[2-3 sentence summary. Key quote if strong.]
**R2 Curious reader**
...
**R3 Opposite-corner reader**
...
**R4 Reluctant reader**
...
### Author peer review
**[Author name]** ([known_for, short])
[Craft observations, 3-4 sentences]
...
### What landed (consensus 3+ readers)
- [Specific moment or element] flagged by [names]
- ...
### What needs attention (consensus 3+ readers)
- [Issue] flagged by [names]
- ...
### Divergences worth noting
- [Element] resonated with core readers but lost the opposite-corner / reluctant reader
- ...
### Suggested actions
1. [Highest priority]
2. ...
\`\`\`
---
## Quick Run
For a faster pass: **R1** (core reader), **R4** (reluctant reader), and the first stored author. Two reader extremes plus a craft read widest spread with fewest subagents.
---
## Notes for the agent
- **Never** give subagents MCP access. The calling agent should write the reading text to \`.bindery/proof-read-payload.md\` and have subagents work only from that staged file. This reduces the risk of them pulling arc files, notes, or overviews, but treat it as a best-effort workflow unless access restrictions are enforced by the runtime.
- **Staging file:** overwrite it fresh each run so stale text from a previous session never bleeds in.
- **Multiple chapters:** Run each chapter as a separate parallel batch. Aggregate per chapter first, then offer a cross-chapter summary if the user asks.
- **Cost awareness:** Full run is 7 subagent calls per chapter (4 readers + 3 authors). Mention this if the user hasn't specified quick vs. full, especially for longer chapters.
- **Divergences are data, not problems.** A passage that splits readers along genre-familiarity lines might be exactly right for this book. Surface it, let the author decide.
- **Author panel changes:** If the user swaps authors mid-session, update \`proof_read.authors\` in settings before running so the change persists.`;
}
//# sourceMappingURL=proof-read.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"proof-read.js","sourceRoot":"","sources":["../../../src/templates/skills/proof-read.ts"],"names":[],"mappings":";;;AASA,wBA6OC;AApPY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,oCAAoC;IAC7C,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,kBAAkB;IAC3B,GAAG,EAAM,+BAA+B;CAC3C,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oJA2OyI,CAAC;AACrJ,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/read-aloud/SKILL.md',
version: 10,
label: 'read-aloud skill',
zip: '.claude/skills/read-aloud.zip',
};
function render(_ctx) {
return `---
name: read-aloud
description: Bindery workspace - Test how a chapter or passage sounds when read aloud flags long sentences, staccato rhythm, complex vocabulary, and said-bookisms. Use for /read-aloud, "reading test", or "how does this sound".
---
# Skill: /read-aloud
Test how a chapter sounds when read aloud.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/read-aloud\`, "reading test", or "how does this sound".
## Clarify first
- Whole chapter or specific passage?
## Runtime context
Before reviewing, read ".bindery/settings.json" with \`get_text\` to pick up the current book's target audience and genre.
## 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 the book's configured target audience
- 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")
`;
}
//# sourceMappingURL=read-aloud.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"read-aloud.js","sourceRoot":"","sources":["../../../src/templates/skills/read-aloud.ts"],"names":[],"mappings":";;;AASA,wBA8CC;AArDY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,oCAAoC;IAC7C,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,kBAAkB;IAC3B,GAAG,EAAM,+BAA+B;CAC3C,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/read-in/SKILL.md',
version: 12,
label: 'read-in skill',
zip: '.claude/skills/read-in.zip',
};
function render(_ctx) {
return `---
name: read-in
description: Bindery workspace - Load project context at the start of a session memory, progress tracker, and chapter notes. Use for /read-in, "get your bearings", "what were we doing", or at the start of any working session.
---
# Skill: /read-in
Load context and get your bearings before starting work.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/read-in\`, "get your bearings", "what were we working on", or at the start of a session.
## Tools
Use these Bindery MCP tools:
- \`update_workspace\` — fetch and pull the workspace before loading context; also reports current branch versus the remote default branch
- \`memory_list\` — discover which memory files exist (\`global.md\`, \`chXX.md\` files)
- \`get_text(identifier)\` — read COWORK.md and memory files
- \`chapter_status_get(book)\` — read the structured progress tracker
- \`get_overview(language)\` — list all acts and chapters (only if tracker is empty or sparse)
- \`get_notes(category, name)\` — look up key character or world notes if relevant to current focus
- \`search(query, language)\` — find relevant passages across the book based on current focus or open questions
- \`get_chapter(chapterNumber, language)\` — read a chapter if that's the current focus
## Steps
### 0. Sync repository
Call \`update_workspace\` before loading any context.
- If the update fails (for example: no remote, merge issue, or upstream problem), flag it to the user and stop do not proceed with stale context.
- If the tool reports that the current branch differs from the remote default branch, mention that briefly so the user can decide whether to switch.
- If the tool reports that the workspace is already up to date, say nothing unless the branch status matters.
### 1. Check for current focus
Use \`get_text("COWORK.md")\` to read the current focus file (ignore if missing).
### 2. Load global memory
Use \`get_text(".bindery/settings.json")\` first to pick up the current book's structure and conventions.
Then use \`memory_list\` to discover available memory files, and \`get_text(".bindery/memories/global.md")\` to load cross-chapter decisions.
### 3. Read the progress tracker
Use \`chapter_status_get\` to read current chapter progress. If it is empty or has fewer than 3 entries, also call \`get_overview\` for the full chapter listing.
### 4. Determine working chapter
If COWORK.md names a chapter, use that.
Otherwise if the tracker has a single \`in-progress\` chapter, use that.
Otherwise **ask the user**: "Which chapter do you want to work on?"
### 5. Load chapter memory
- Once the chapter is known (e.g. chapter 10), check \`memory_list\` output for a matching file (\`ch10.md\`). If it exists, read it with \`get_text(".bindery/memories/ch10.md")\`.
- Also read the full chapter text with \`get_chapter\` to have it fresh in context, and to check for any discrepancies with the memory file.
### 6. Story / Arc focus
Depending on the focus and open questions, use \`get_notes\` or \`search\` to load any additional relevant context.
### 7. Summarize
Output a short orientation (3-6 lines):
- Which chapter / scene we're in
- Status from the tracker (draft / in-progress / needs-review)
- Key open decisions from global memory relevant to this chapter
- Any chapter-specific notes from the chapter memory file
- End with a phrase like: "Ready — what would you like to work on?"
## Rules
- Do not load *all* chapter memories only the one being worked on
- Keep the summary brief; this is orientation, not a full status report
- Do not suggest work or ask multiple questions one question at most (which chapter?)
`;
}
//# sourceMappingURL=read-in.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"read-in.js","sourceRoot":"","sources":["../../../src/templates/skills/read-in.ts"],"names":[],"mappings":";;;AASA,wBAqEC;AA5EY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,iCAAiC;IAC1C,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,eAAe;IACxB,GAAG,EAAM,4BAA4B;CACxC,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/review/SKILL.md',
version: 13,
label: 'review skill',
zip: '.claude/skills/review.zip',
};
function render(_ctx) {
return `---
name: review
description: Bindery workspace - Review a chapter for language, arc consistency, and age-appropriateness. Use for /review, "review chapter X", "quick review", or "review my changes".
---
# Skill: /review
Review a chapter and give structured feedback.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/review\`, "review chapter X", "quick review", or "review my changes".
## Clarify first
- Changes, chapter, translation, or overall feedback?
- Type: **Full** (language + arc + age-appropriateness) or **Quick** (language and typos only)?
## Tools
Use these Bindery MCP tools to gather context:
- \`get_review_text(autoStage: true, contextLines: 3)\` — returns the git diff of uncommitted changes **plus** any regions wrapped in \`<!-- Bindery: Review start -->\` / \`<!-- Bindery: Review stop -->\` markers (works even on committed work). \`autoStage: true\` stages reviewed files **and** removes the marker lines from disk so the next call only shows new changes. Pass more contextLines when join points to existing prose need checking
- \`get_chapter(chapterNumber, language)\` — read the full chapter text
- \`get_notes(category, name)\` — look up character profiles (\`category: "Characters"\`) or world rules
- \`search(query, language)\` — find related passages across the book
- \`git_snapshot(message)\` — after a successful review, suggest saving a snapshot
## Steps
### 1. Load settings and context
Start by reading ".bindery/settings.json" with \
\`get_text(".bindery/settings.json")\` to pick up the current book's target audience, genre, and story structure.
Load the right context, pick any or all as needed:
- Read \`.bindery/memories/global.md\`
- Read \`.bindery/memories/chXX.md\` if it exists for chapter-specific context
- Use \`get_chapter\` to load the chapter
- For a Full review, read the relevant arc file from \`Arc/\`.
- For "review my changes", use \`get_review_text\` to get the diff
- If the diff includes translated chapter files, flag that and offer \`/translation-review\` for source-vs-translation feedback
### 2. Perform the review
**Quick** language and typos only.
**Full** adds:
- Arc consistency with the arc file
- Age-appropriateness for the book's configured target audience
- 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
`;
}
//# sourceMappingURL=review.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../../src/templates/skills/review.ts"],"names":[],"mappings":";;;AASA,wBAkEC;AAzEY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,gCAAgC;IACzC,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,cAAc;IACvB,GAAG,EAAM,2BAA2B;CACvC,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgEV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/status/SKILL.md',
version: 10,
label: 'status skill',
zip: '.claude/skills/status.zip',
};
function render(_ctx) {
return `---
name: status
description: Bindery workspace - Give a book progress snapshot chapters done, in progress, and coming up. Use for /status, "what's the book status", or "where are we".
---
# Skill: /status
Snapshot of the book's progress: what's done, in progress, and coming up.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/status\`, "what's the book status", or "where are we".
## Tools
Use these Bindery MCP tools:
- \`chapter_status_get(book)\` — read the structured progress tracker from \`.bindery/chapter-status.json\`
- \`chapter_status_update(book, chapters)\` — upsert chapter progress entries (send only changed chapters)
- \`get_overview(language)\` — list all acts and chapters with titles
- \`get_text(identifier)\` — read COWORK.md, settings.json, and memory files
- \`memory_list\` — discover which chapter memory files exist (\`chXX.md\`)
## Steps
1. Use \`get_text(".bindery/settings.json")\` to pick up the current book's structure and conventions.
2. Use \`chapter_status_get\` to read the current tracker. Use \`memory_list\` to check available memory files.
3. Use \`get_text\` to read COWORK.md (current focus), \`.bindery/memories/global.md\`, and for in-progress chapters \`.bindery/memories/chXX.md\`.
4. Use \`get_overview\` for the full chapter listing if the tracker is empty or incomplete.
5. Check \`Arc/\` for what's planned vs written (Overall.md + the relevant act file).
6. Output: overall count / done / in-progress / coming up (next 2-3 chapters) / open questions.
7. If the tracker is out of date or missing entries, update it with \`chapter_status_update\` (upsert only the changed chapters).
## Output
Keep it scannable bold headers, short lines. This is a working tool, not a narrative summary.
`;
}
//# sourceMappingURL=status.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/templates/skills/status.ts"],"names":[],"mappings":";;;AASA,wBAoCC;AA3CY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,gCAAgC;IACzC,OAAO,EAAE,EAAE;IACX,KAAK,EAAI,cAAc;IACvB,GAAG,EAAM,2BAA2B;CACvC,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/translate/SKILL.md',
version: 9,
label: 'translate skill',
zip: '.claude/skills/translate.zip',
};
function render(_ctx) {
return `---
name: translate
description: Bindery workspace - Translate a chapter or spot-check an existing translation using the Bindery translation table. Use for /translate, "translate chapter X", or "help me with the translation".
---
# Skill: /translate
Translate a chapter or passage into the target language.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## 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? Default to spot-check if a chapter file already exists for the target language.
## Tools
Use these Bindery MCP tools:
- \`get_chapter(chapterNumber, language)\` — read a chapter in any language (source or existing translation)
- \`get_translation(targetLanguage)\` — list glossary entries for a target language (e.g. \`"nl"\`)
- \`get_translation(targetLanguage, word)\` — look up a specific term; forgiving: case-insensitive, handles plurals and inflected forms
- \`search(query, targetLanguage)\` — verify how a term was rendered in other translated chapters
- \`add_translation(targetLanguage, from, to)\` — save a new glossary term pair when the user confirms a translation choice
## Steps
### 1. Load the translation table
Call \`get_translation(targetLanguage)\` to load all known glossary 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 as a glossary entry. For spelling variant rules (dialect substitutions applied at export), use \`add_dialect\` instead.
## Rules
- Always load the translation table first never invent translations for world-specific terms
- Flag uncertain terms rather than guessing
`;
}
//# sourceMappingURL=translate.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../../../src/templates/skills/translate.ts"],"names":[],"mappings":";;;AASA,wBAmDC;AA1DY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,mCAAmC;IAC5C,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,iBAAiB;IAC1B,GAAG,EAAM,8BAA8B;CAC1C,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDV,CAAC;AACF,CAAC"}

View file

@ -0,0 +1,3 @@
import type { TemplateContext, TemplateMeta } from '../context';
export declare const meta: TemplateMeta;
export declare function render(_ctx: TemplateContext): string;

View file

@ -0,0 +1,89 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.meta = void 0;
exports.render = render;
exports.meta = {
file: '.claude/skills/translation-review/SKILL.md',
version: 2,
label: 'translation-review skill',
zip: '.claude/skills/translation-review.zip',
};
function render(_ctx) {
return `---
name: translation-review
description: Bindery workspace - Review a hand-crafted translation against the source language for fidelity, naturalness, and glossary consistency. Use for /translation-review, "review my translation", or "what do you think" when translation is the current focus.
---
# Skill: /translation-review
Review a hand-crafted translation against the source.
Use this when the user has written or updated the target-language text and wants structured feedback.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/translation-review\`, "review my translation", or "what do you think" when translation is the active focus.
## Not this skill
- Generating translation text from scratch -> use \`/translate\`
- Reviewing source-language writing quality -> use \`/review\`
## Tools
Use these Bindery MCP tools:
- \`get_review_text(autoStage: true, contextLines: 3)\` — git diff of uncommitted changes **plus** any regions wrapped in \`<!-- Bindery: Review start -->\` / \`<!-- Bindery: Review stop -->\` markers. Marker regions surface even after the author committed and continued elsewhere. \`autoStage: true\` stages files **and** consumes (removes) the marker lines so the next pass starts clean.
- \`get_text(identifier, startLine?, endLine?)\` — fetch matching source lines or focused ranges
- \`get_translation(targetLanguage)\` — load glossary terms for the target language before reviewing
- \`get_chapter(chapterNumber, language)\` — full chapter source/target pair for full spot-check mode
- \`search(query, targetLanguage)\` — verify how a term was used in previously translated chapters before flagging it
- \`add_translation(targetLanguage, from, to)\` — persist a confirmed glossary correction
## Mode 1 - Scoped diff review (primary)
### Steps
1. Call \`get_review_text\`. The response has two sections: a \`# Git diff\` block and a \`# Review markers\` block (one or both may be empty).
2. If both sections are empty, report that nothing new has been translated yet.
3. Identify changed files and determine source/target language from available context: session file (for example COWORK.md), recent conversation, or ask the user if ambiguous.
4. If the target-language file changed (or has marker regions), capture the changed/marked target line range.
5. **Line parity matching** attempt to fetch the corresponding source lines:
- First, assume line parity: call \`get_text(sourceFile, startLine, endLine)\` for the same range as the target.
- **If the content is a complete mismatch** (opening words differ significantly), the translation work may have added or removed lines. Search a window: fetch \`get_text(sourceFile, startLine - 5, endLine + 5)\` and scan for the target text within that range.
- **If still not found**, ask the user: "I couldn't locate these source lines. Can you point me to the starting line number in the source file for this translation?"
6. Load glossary entries via \`get_translation(targetLanguage)\`.
7. Use \`search(query, targetLanguage)\` when a term may have an established translation elsewhere in the book.
8. Compare source vs target and produce feedback using the table below.
9. If source-language lines also changed, flag that and suggest \`/review\` for source-quality feedback.
## Mode 2 - Full chapter spot-check
Use this when the user asks for a full chapter comparison.
1. Determine source language, target language, and chapter number.
2. Load glossary with \`get_translation(targetLanguage)\`.
3. Use \`search(query, targetLanguage)\` as needed to verify recurring terminology in earlier translated chapters.
4. Load chapters with \`get_chapter(chapterNumber, sourceLanguage)\` and \`get_chapter(chapterNumber, targetLanguage)\`.
5. Compare paragraph by paragraph and report findings with the same table.
## Output format
| Before (target) | After (target) | Reason |
|---|---|---|
| Keep context short; bold only the changed words | Suggested wording | Fidelity, naturalness, glossary, or terminology consistency |
Also list glossary mismatches and untranslated world-specific terms explicitly.
## Cross-skill handoff
- If changed lines are only source-language files, suggest switching to \`/review\`.
- If both source and target changed, run translation-review findings first, then prompt whether to run \`/review\` for source edits too.
## Rules
- Load glossary before reviewing and flag mismatches explicitly
- Suggest edits only; do not rewrite entire passages unless asked
- Bold only changed words in Before/After rows
- Mark uncertain calls as questions for user confirmation
- When the user confirms a corrected term, call \`add_translation\` before moving on
- Respond in the session language (usually source language)
`;
}
//# sourceMappingURL=translation-review.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"translation-review.js","sourceRoot":"","sources":["../../../src/templates/skills/translation-review.ts"],"names":[],"mappings":";;;AASA,wBA6EC;AApFY,QAAA,IAAI,GAAiB;IAC9B,IAAI,EAAK,4CAA4C;IACrD,OAAO,EAAE,CAAC;IACV,KAAK,EAAI,0BAA0B;IACnC,GAAG,EAAM,uCAAuC;CACnD,CAAC;AAEF,SAAgB,MAAM,CAAC,IAAqB;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2EV,CAAC;AACF,CAAC"}

69
bindery-core/out/translations.d.ts vendored Normal file
View file

@ -0,0 +1,69 @@
/**
* Bindery translation types and helpers.
*
* Manages .bindery/translations.json substitution rules and glossaries
* per language pair.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
* Zero dependency on VS Code or Obsidian APIs.
*/
/** Type of a translation entry — determines how the extension uses its rules. */
export type TranslationType = 'substitution' | 'glossary';
/** A single from→to rule inside a translation entry. */
export interface TranslationRule {
from: string;
to: string;
}
/**
* One entry in translations.json, keyed by a language code (e.g. "en-gb", "nl").
*
* substitution applied automatically during export (word-by-word replace).
* glossary reference only; used for consistency checking, not auto-applied.
*/
export interface TranslationEntry {
label?: string;
type: TranslationType;
sourceLanguage?: string;
rules?: TranslationRule[];
ignoredWords?: string[];
}
/** The full .bindery/translations.json file. */
export type TranslationsFile = Record<string, TranslationEntry>;
/** A US→UK word substitution pair. */
export interface UkReplacement {
us: string;
uk: string;
}
export declare function readTranslations(root: string): TranslationsFile | null;
export declare function writeTranslations(root: string, data: TranslationsFile): void;
/**
* Get substitution rules from translations.json for the given language key.
* Returns UkReplacement[] compatible with merge.ts (field names us/uk).
* Only entries with type === 'substitution' are returned.
*/
export declare function getSubstitutionRules(translations: TranslationsFile | null, langKey: string): UkReplacement[];
/**
* Get the ignored-words set for a given language key.
*/
export declare function getIgnoredWords(translations: TranslationsFile | null, langKey: string): Set<string>;
/**
* Get glossary rules for a language key (type === 'glossary' entries).
*/
export declare function getGlossaryRules(translations: TranslationsFile | null, langKey: string): TranslationRule[];
/**
* Add or update a substitution rule in .bindery/translations.json.
* Creates the file and entry if they do not yet exist.
*/
export declare function upsertSubstitutionRule(root: string, langKey: string, rule: TranslationRule): void;
/**
* Add words to the ignoredWords list in .bindery/translations.json.
* Returns the count of newly added words (duplicates are skipped).
*/
export declare function addIgnoredWords(root: string, langKey: string, words: string[]): number;
/**
* Add or update a glossary rule in .bindery/translations.json.
* Glossary entries are for cross-language reference (e.g. ENNL world terms).
* They are not auto-applied at export; agents use them for consistency checking.
* Creates the file and entry if they do not yet exist.
*/
export declare function upsertGlossaryRule(root: string, langKey: string, langLabel: string, sourceLang: string, rule: TranslationRule): void;

View file

@ -0,0 +1,241 @@
"use strict";
/**
* Bindery translation types and helpers.
*
* Manages .bindery/translations.json substitution rules and glossaries
* per language pair.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
* Zero dependency on VS Code or Obsidian APIs.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.readTranslations = readTranslations;
exports.writeTranslations = writeTranslations;
exports.getSubstitutionRules = getSubstitutionRules;
exports.getIgnoredWords = getIgnoredWords;
exports.getGlossaryRules = getGlossaryRules;
exports.upsertSubstitutionRule = upsertSubstitutionRule;
exports.addIgnoredWords = addIgnoredWords;
exports.upsertGlossaryRule = upsertGlossaryRule;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const settings_1 = require("./settings");
// ─── Readers ─────────────────────────────────────────────────────────────────
function readTranslations(root) {
const p = (0, settings_1.getTranslationsPath)(root);
if (!fs.existsSync(p)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(p, 'utf-8'));
}
catch {
return null;
}
}
// ─── Writers ─────────────────────────────────────────────────────────────────
function writeTranslations(root, data) {
const p = (0, settings_1.getTranslationsPath)(root);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}
// ─── Accessors ────────────────────────────────────────────────────────────────
/**
* Get substitution rules from translations.json for the given language key.
* Returns UkReplacement[] compatible with merge.ts (field names us/uk).
* Only entries with type === 'substitution' are returned.
*/
function getSubstitutionRules(translations, langKey) {
if (!translations) {
return [];
}
const entry = resolveEntry(translations, langKey);
if (entry?.type !== 'substitution') {
return [];
}
return (entry.rules ?? [])
.filter(r => r.from?.trim() && r.to?.trim())
.map(r => ({ us: r.from.trim().toLowerCase(), uk: r.to.trim() }));
}
/**
* Get the ignored-words set for a given language key.
*/
function getIgnoredWords(translations, langKey) {
if (!translations) {
return new Set();
}
const entry = resolveEntry(translations, langKey);
const result = new Set();
for (const word of entry?.ignoredWords ?? []) {
const w = word.trim().toLowerCase();
if (w) {
result.add(w);
}
}
return result;
}
/**
* Get glossary rules for a language key (type === 'glossary' entries).
*/
function getGlossaryRules(translations, langKey) {
if (!translations) {
return [];
}
const entry = resolveEntry(translations, langKey);
if (!entry) {
return [];
}
return (entry.rules ?? []).filter(r => r.from?.trim() && r.to?.trim());
}
// ─── Mutators ─────────────────────────────────────────────────────────────────
/**
* Add or update a substitution rule in .bindery/translations.json.
* Creates the file and entry if they do not yet exist.
*/
function upsertSubstitutionRule(root, langKey, rule) {
const translations = readTranslations(root) ?? {};
if (!translations[langKey]) {
translations[langKey] = {
type: 'substitution',
sourceLanguage: 'en',
rules: [],
ignoredWords: [],
};
}
const entry = translations[langKey];
if (entry.type !== 'substitution') {
throw new Error(`Entry '${langKey}' has type '${entry.type}', expected 'substitution'.`);
}
const rules = entry.rules ?? [];
const idx = rules.findIndex(r => r.from.toLowerCase() === rule.from.toLowerCase());
if (idx >= 0) {
rules[idx] = rule;
}
else {
rules.push(rule);
rules.sort((a, b) => a.from.localeCompare(b.from));
}
entry.rules = rules;
writeTranslations(root, translations);
}
/**
* Add words to the ignoredWords list in .bindery/translations.json.
* Returns the count of newly added words (duplicates are skipped).
*/
function addIgnoredWords(root, langKey, words) {
const translations = readTranslations(root) ?? {};
if (!translations[langKey]) {
translations[langKey] = {
type: 'substitution',
sourceLanguage: 'en',
rules: [],
ignoredWords: [],
};
}
const entry = translations[langKey];
const existing = new Set((entry.ignoredWords ?? []).map(w => w.toLowerCase()));
let added = 0;
for (const word of words) {
const w = word.trim().toLowerCase();
if (w && !existing.has(w)) {
existing.add(w);
added++;
}
}
entry.ignoredWords = Array.from(existing).sort((a, b) => a.localeCompare(b));
writeTranslations(root, translations);
return added;
}
/**
* Add or update a glossary rule in .bindery/translations.json.
* Glossary entries are for cross-language reference (e.g. ENNL world terms).
* They are not auto-applied at export; agents use them for consistency checking.
* Creates the file and entry if they do not yet exist.
*/
function upsertGlossaryRule(root, langKey, langLabel, sourceLang, rule) {
const translations = readTranslations(root) ?? {};
if (!translations[langKey]) {
translations[langKey] = {
label: langLabel,
type: 'glossary',
sourceLanguage: sourceLang,
rules: [],
};
}
const entry = translations[langKey];
// If entry exists but was previously substitution, keep it — don't downgrade
const rules = entry.rules ?? [];
const idx = rules.findIndex(r => r.from.toLowerCase() === rule.from.toLowerCase());
if (idx >= 0) {
rules[idx] = rule;
}
else {
rules.push(rule);
rules.sort((a, b) => a.from.localeCompare(b.from));
}
entry.rules = rules;
writeTranslations(root, translations);
}
// ─── Internal helpers ─────────────────────────────────────────────────────────
function normaliseKey(key) {
return key.trim().toLowerCase();
}
function isUkLike(key) {
const k = normaliseKey(key);
return k === 'uk' || k === 'en-gb' || k === 'en-uk';
}
/**
* Look up a translation entry by language key.
* Falls back to 'en-gb' for UK-like codes.
*/
function resolveEntry(translations, langKey) {
const target = normaliseKey(langKey);
for (const [k, v] of Object.entries(translations)) {
if (normaliseKey(k) === target) {
return v;
}
}
// For UK-like codes, also accept an 'en-gb' entry
if (isUkLike(target)) {
for (const [k, v] of Object.entries(translations)) {
if (normaliseKey(k) === 'en-gb') {
return v;
}
}
}
return undefined;
}
//# sourceMappingURL=translations.js.map

File diff suppressed because one or more lines are too long

1539
bindery-core/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

23
bindery-core/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "@bindery/core",
"version": "0.1.0",
"main": "out/index.js",
"types": "out/index.d.ts",
"private": true,
"scripts": {
"build": "tsc",
"compile": "tsc",
"test": "vitest run",
"test:ci": "vitest run --reporter=verbose --reporter=json --outputFile=test-results.json",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@vitest/coverage-v8": "^4.1.5",
"typescript": "6.0.3",
"vitest": "^4.1.5"
},
"engines": {
"node": ">=18.0.0"
}
}

View file

@ -0,0 +1,97 @@
/**
* Typography formatting for markdown files.
*
* Converts straight quotes to curly quotes, `...` to ellipsis,
* and `--` to em-dash while preserving content inside HTML comments.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
*/
// ─── Typographic Characters ─────────────────────────────────────────────────
const OPEN_DOUBLE = '\u{201C}'; // "
const CLOSE_DOUBLE = '\u{201D}'; // "
const OPEN_SINGLE = '\u{2018}'; // '
const CLOSE_SINGLE = '\u{2019}'; // ' (also used for apostrophes)
const ELLIPSIS = '\u{2026}'; // …
const EM_DASH = '\u{2014}'; // —
// ─── Cached Regex Patterns ──────────────────────────────────────────────────
/** Matches HTML comments: <!-- ... --> */
const COMMENT_RE = /<!--[\s\S]*?-->/g;
/** Matches opening double quote context: after whitespace, line start, or brackets */
const OPEN_DOUBLE_RE = /(^|[\s([{—–-])"/gm;
/** Matches opening single quote context: after whitespace, line start, or brackets */
const OPEN_SINGLE_RE = /(^|[\s([{—–-])'/gm;
/** Matches a closing double quote after an em-dash at end-of-word or line */
const CLOSE_DOUBLE_AFTER_EM_DASH_RE = /—"([\s)\].,;:!?]|$)/gm;
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Apply typographic formatting to text.
*
* - `...` `` (ellipsis)
* - `--` `` (em-dash, but not `---` which is markdown HR)
* - `"text"` `\u201Ctext\u201D` (curly double quotes)
* - `'text'` `\u2018text\u2019` (curly single quotes / apostrophes)
*/
export function updateTypography(text: string): string {
let result = text;
// Step 1: Convert ... to ellipsis (must happen before quote processing)
result = result.replaceAll(/\.\.\./g, ELLIPSIS);
// Step 2: Protect HTML comments from em-dash conversion
const protectedComments: string[] = [];
result = result.replaceAll(COMMENT_RE, (match) => {
const placeholder = `\x00COMMENT${protectedComments.length}\x00`;
protectedComments.push(match);
return placeholder;
});
// Step 3: Convert -- to em-dash (but preserve --- for markdown HR)
const protectedTriple = '\x00TRIPLE\x00';
result = result.replaceAll(/---/g, protectedTriple);
result = result.replaceAll(/--/g, EM_DASH);
result = result.replaceAll(new RegExp(escapeRegex(protectedTriple), 'g'), '---');
// Step 4: Restore HTML comments
for (let i = 0; i < protectedComments.length; i++) {
result = result.replaceAll(`\x00COMMENT${i}\x00`, protectedComments[i]);
}
// Step 4b: Fix closing quotes after em-dash introduced from --
result = result.replaceAll(CLOSE_DOUBLE_AFTER_EM_DASH_RE, (_match, after) => {
return `${EM_DASH}${CLOSE_DOUBLE}${after}`;
});
// Step 5: Convert double quotes
// Opening: after whitespace, start of line, or opening brackets
result = result.replaceAll(OPEN_DOUBLE_RE, (_match, before) => {
return `${before}${OPEN_DOUBLE}`;
});
// Closing: all remaining straight double quotes
result = result.replaceAll(/"/g, CLOSE_DOUBLE);
// Step 6: Convert single quotes
// Opening: after whitespace, start of line, or opening brackets
result = result.replaceAll(OPEN_SINGLE_RE, (_match, before) => {
return `${before}${OPEN_SINGLE}`;
});
// Closing/apostrophe: all remaining straight single quotes
result = result.replaceAll(/'/g, CLOSE_SINGLE);
return result;
}
/** Alias for `updateTypography` — used by obsidian-plugin and other consumers. */
export const applyTypography = updateTypography;
function escapeRegex(str: string): string {
return str.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

10
bindery-core/src/index.ts Normal file
View file

@ -0,0 +1,10 @@
/**
* Bindery Core barrel export.
*
* Re-exports all public APIs from the shared modules.
*/
export * from './formatting';
export * from './settings';
export * from './translations';
export * from './templates';

View file

@ -0,0 +1,146 @@
/**
* Bindery workspace settings types and helpers.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
* Zero dependency on VS Code or Obsidian APIs.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
// ─── Language / Dialect Types ──────────────────────────────────────────────
export interface LanguageConfig {
code: string;
folderName: string;
/** Optional per-language export title from settings.json languages[]. */
bookTitle?: string;
chapterWord: string;
actPrefix: string;
prologueLabel: string;
epilogueLabel: string;
/** True for the primary language the book is written in. */
isDefault?: boolean;
/** Dialect exports derived from this language (e.g. en-gb from EN). No story folder of their own. */
dialects?: DialectConfig[];
}
/** A dialect derived from a parent language — same story folder, word substitutions applied at export. */
export interface DialectConfig {
/** Dialect code, used as the key in translations.json (e.g. 'en-gb'). */
code: string;
/** Human-readable label, e.g. 'British English'. */
label?: string;
}
// ─── Settings Schema ──────────────────────────────────────────────────────
/**
* .bindery/settings.json
*
* bookTitle may be a plain string or a per-language map:
* "bookTitle": "The Hollow Road"
* "bookTitle": { "en": "The Hollow Road", "nl": "De Holle Weg" }
*/
export interface WorkspaceSettings {
bookTitle?: string | Record<string, string>;
author?: string;
/** Short description or tagline used when generating AI assistant files. */
description?: string;
/** Genre of the book (e.g. "sci-fi/fantasy", "mystery", "contemporary fiction"). */
genre?: string;
/** Target audience, e.g. "12+" or "adults" or "8-10". Used to calibrate AI review feedback. */
targetAudience?: string;
/** AI targets previously chosen when running Set Up AI Files (claude, copilot, cursor, agents). */
aiTargets?: string[];
/** Claude skills previously chosen when running Set Up AI Files. */
aiSkills?: string[];
storyFolder?: string;
mergedOutputDir?: string;
mergeFilePrefix?: string;
formatOnSave?: boolean;
languages?: LanguageConfig[];
git?: {
snapshot?: {
pushDefault?: boolean;
remote?: string;
branch?: string;
};
};
}
// ─── Constants ───────────────────────────────────────────────────────────────
export const BINDERY_FOLDER = '.bindery';
export const SETTINGS_FILENAME = 'settings.json';
export const TRANSLATIONS_FILENAME = 'translations.json';
// ─── Path helpers ─────────────────────────────────────────────────────────────
export function getBinderyFolder(root: string): string {
return path.join(root, BINDERY_FOLDER);
}
export function getSettingsPath(root: string): string {
return path.join(root, BINDERY_FOLDER, SETTINGS_FILENAME);
}
export function getTranslationsPath(root: string): string {
return path.join(root, BINDERY_FOLDER, TRANSLATIONS_FILENAME);
}
// ─── Readers ─────────────────────────────────────────────────────────────────
export function readWorkspaceSettings(root: string): WorkspaceSettings | null {
const p = getSettingsPath(root);
if (!fs.existsSync(p)) { return null; }
try {
return JSON.parse(fs.readFileSync(p, 'utf-8')) as WorkspaceSettings;
} catch {
return null;
}
}
// ─── Accessors ────────────────────────────────────────────────────────────────
/**
* Resolve the book title for a given language code.
* Falls back to the English title if no language-specific title is found.
*/
export function getBookTitleForLang(
settings: WorkspaceSettings | null,
langCode: string
): string | undefined {
if (!settings?.bookTitle) { return undefined; }
if (typeof settings.bookTitle === 'string') {
return settings.bookTitle || undefined;
}
const code = langCode.toLowerCase();
return settings.bookTitle[code]
?? settings.bookTitle['en']
?? undefined;
}
/**
* Return the language marked isDefault, or the first language in the list.
*/
export function getDefaultLanguage(
settings: WorkspaceSettings | null
): LanguageConfig | undefined {
const langs = settings?.languages;
if (!langs || langs.length === 0) { return undefined; }
return langs.find(l => l.isDefault) ?? langs[0];
}
/**
* Return dialects[] for the language matching langCode, or [].
*/
export function getDialectsForLanguage(
settings: WorkspaceSettings | null,
langCode: string
): DialectConfig[] {
const lang = settings?.languages?.find(
l => l.code.toUpperCase() === langCode.toUpperCase()
);
return lang?.dialects ?? [];
}

View file

@ -0,0 +1,85 @@
/**
* Bindery AI instruction file templates thin aggregator.
*
* Each template lives in its own file under `./templates/`. Each module
* exports `{ meta, render }` so the version stays glued to the content.
* This file just collects them and exposes the public API:
*
* - `TemplateContext` re-exported from ./templates/context
* - `FILE_VERSION_INFO` built from each module's `meta`
* - `renderTemplate(name, ctx)` dispatches to the right module
*
* SINGLE SOURCE OF TRUTH this is the canonical copy in bindery-core.
* Do not hand-edit copies in mcp-ts/src/ or vscode-ext/src/.
*/
import * as claude from './templates/claude';
import * as copilot from './templates/copilot';
import * as cursor from './templates/cursor';
import * as agents from './templates/agents';
import * as binderyReadme from './templates/bindery-readme';
import * as review from './templates/skills/review';
import * as brainstorm from './templates/skills/brainstorm';
import * as memory from './templates/skills/memory';
import * as translate from './templates/skills/translate';
import * as translationReview from './templates/skills/translation-review';
import * as status from './templates/skills/status';
import * as continuity from './templates/skills/continuity';
import * as readAloud from './templates/skills/read-aloud';
import * as readIn from './templates/skills/read-in';
import * as proofRead from './templates/skills/proof-read';
import type { TemplateContext, TemplateMeta } from './templates/context';
export type { TemplateContext } from './templates/context';
interface TemplateModule {
meta: TemplateMeta;
render: (ctx: TemplateContext) => string;
}
const TEMPLATES: Record<string, TemplateModule> = {
'claude': claude,
'copilot': copilot,
'cursor': cursor,
'agents': agents,
'bindery-readme': binderyReadme,
'review': review,
'brainstorm': brainstorm,
'memory': memory,
'translate': translate,
'translation-review': translationReview,
'status': status,
'continuity': continuity,
'read-aloud': readAloud,
'read-in': readIn,
'proof-read': proofRead,
};
// ─── File version metadata ────────────────────────────────────────────────────
// Bump per-file version inside the matching module's `meta` when content
// changes significantly so users with outdated content are prompted.
export const FILE_VERSION_INFO: Record<string, { version: number; label: string; zip: string | null }> =
Object.fromEntries(
Object.values(TEMPLATES).map(t => [
t.meta.file,
{ version: t.meta.version, label: t.meta.label, zip: t.meta.zip },
]),
);
// ─── Entry point ──────────────────────────────────────────────────────────────
/**
* Render a named template with the given context.
*
* Top-level file templates: 'claude', 'copilot', 'cursor', 'agents', 'bindery-readme'
* Skill templates: 'review', 'brainstorm', 'memory', 'translate',
* 'translation-review', 'status', 'continuity', 'read-aloud',
* 'read-in', 'proof-read'
*/
export function renderTemplate(name: string, ctx: TemplateContext): string {
const t = TEMPLATES[name];
if (!t) { return `Unknown template: ${name}`; }
return t.render(ctx);
}

View file

@ -0,0 +1,51 @@
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: 'AGENTS.md',
version: 8,
label: 'agents instructions',
zip: null,
};
export function render(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),
'',
'## 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 `.claude/skills/` for shared slash workflows before improvising a bespoke process.',
'',
'## Story files',
`- Chapter files are \`.md\` files in \`${storyFolder}/\`, organized 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 normalize them.',
'',
'## Shared skill workflows',
'- Shared workflows live in `.claude/skills/` and can be used by agents beyond Claude when the runtime supports workspace skills.',
'- Prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, and `/proof-read` when the user is asking for one of those structured tasks.',
'',
'## 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}/\` | Story arc files for overall and per-act structure and beats |`,
`| \`${notesFolder}/\` | Story notes, like character profiles and world rules |`,
`| \`${memoriesFolder}/global.md\` | Cross-session decisions |`,
);
return lines.join('\n') + '\n';
}

View file

@ -0,0 +1,136 @@
import type { TemplateContext, TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: '.bindery/README.md',
version: 4,
label: 'bindery capabilities',
zip: null,
};
export function render(ctx: TemplateContext): string {
const { title } = ctx;
return `# Bindery — capabilities for this workspace
This file is **the single source agents should consult to answer "What can Bindery do?"**
It is generated by Bindery itself (\`bindery init\` and \`setup_ai_files\`). Edits will be
overwritten on the next setup.
For richer narrative docs (install, contributing, release notes), see the project on
GitHub: <https://github.com/evdboom/Bindery> (README at
<https://github.com/evdboom/Bindery/blob/main/README.md>). Agents with internet access
may fetch it for context; otherwise, treat this file as the complete answer.
> Workspace: **${title}**
## What Bindery is
Bindery is a markdown book authoring toolkit with three surfaces:
1. **VS Code extension** commands, format-on-save, export to DOCX/EPUB/PDF.
2. **MCP server** programmatic tools for AI assistants (Claude, Copilot, Codex, Cursor).
3. **Skill workflows** opinionated slash-commands like \`/review\`, \`/translate\`, \`/memory\`.
All three operate on the same workspace state: \`.bindery/settings.json\`,
\`.bindery/translations.json\`, the story folder, \`Notes/\`, \`Arc/\`, and
\`.bindery/memories/\`.
## VS Code commands (Command Palette "Bindery: …")
| Command | What it does |
|---|---|
| \`Bindery: Initialize Workspace\` | Create \`.bindery/settings.json\`, \`.bindery/translations.json\`, and this README. |
| \`Bindery: Setup AI Assistant Files\` | Generate CLAUDE.md / copilot-instructions.md / cursor rules / AGENTS.md and refresh this capabilities doc. |
| \`Bindery: Format Typography\` / \`Format All Markdown in Folder\` | Curly quotes, em-dashes, ellipses, etc. |
| \`Bindery: Merge Chapters → Markdown / DOCX / EPUB / PDF / All Formats\` | Build a deliverable from chapter files. |
| \`Bindery: Find Probable US→UK Words\` | Surface probable US spellings in EN source. |
| \`Bindery: Add Dialect Rule\` / \`Add Translation (Glossary)\` / \`Add Language\` | Maintain dialect substitutions, glossary entries, and language scaffolding. |
| \`Bindery: Open translations.json\` | Open the per-language rules file. |
| \`Bindery: Insert Review Start Marker (or wrap selection)\` | Insert \`<!-- Bindery: Review start -->\`, or wrap the current selection in matched start/stop markers. |
| \`Bindery: Insert Review Stop Marker\` | Insert \`<!-- Bindery: Review stop -->\` at the cursor. |
| \`Bindery: Register MCP Server\` | Write \`.vscode/mcp.json\` so Claude / Codex pick the bundled server up. |
### Default keybindings (markdown editors only)
| Shortcut | Command |
|---|---|
| \`Ctrl+K Ctrl+B\` | Format Typography |
| \`Ctrl+K Ctrl+,\` | Insert Review Start Marker (or wrap selection) |
| \`Ctrl+K Ctrl+.\` | Insert Review Stop Marker |
These only fire while a markdown file is focused (\`editorTextFocus && resourceLangId == markdown\`). Rebind in **File → Preferences → Keyboard Shortcuts** if they collide with another extension. On macOS, swap \`Ctrl\` for \`Cmd\`.
## MCP tools
Use these from any MCP-aware assistant. Tools tagged **(reads)** are safe; tools
tagged **(writes)** modify files or git state.
| Tool | What it does |
|---|---|
| \`health\` (reads) | Workspace status: settings, index, AI-file versions, embedding backend. |
| \`identify_book\` (reads) | Confirm the active book root and registry entry. |
| \`init_workspace\` (writes) | Create \`.bindery/settings.json\` with title/author/story-folder defaults. |
| \`update_workspace\` (writes) | \`git fetch\` + pull, optional branch switch, optional auto-stash. |
| \`settings_update\` (writes) | Patch \`.bindery/settings.json\` from an agent. |
| \`setup_ai_files\` (writes) | (Re)generate AI instruction files + refresh this capabilities doc. |
| \`get_text\` / \`get_chapter\` / \`get_book_until\` / \`get_overview\` / \`get_notes\` (reads) | Read source files, chapters, ranges, and notes. |
| \`search\` (reads) | BM25 / semantic search across the corpus (Ollama optional). |
| \`index_build\` / \`index_status\` (writes / reads) | Build or inspect the lexical/semantic index. |
| \`format\` (writes) | Apply typography to a file or folder (\`dryRun\` supported). |
| \`get_review_text\` (writes) | Returns the **git diff of uncommitted changes** *plus* any regions wrapped in \`<!-- Bindery: Review start --> ... <!-- Bindery: Review stop -->\` markers. Marker regions surface even after a commit, so committed work-in-progress can still be reviewed. \`autoStage: true\` stages files **and** consumes (removes) the marker lines. |
| \`git_snapshot\` (writes) | Commit changes in story / notes / arc folders, optional push. |
| \`get_translation\` / \`add_translation\` (reads / writes) | Glossary lookup and upsert per target language. |
| \`get_dialect\` / \`add_dialect\` (reads / writes) | Dialect substitution lookup and upsert (e.g. \`en-gb\`). |
| \`add_language\` (writes) | Scaffold a new language under the story folder. |
| \`memory_list\` / \`memory_append\` / \`memory_compact\` (reads / writes) | Manage \`.bindery/memories/\` files. |
| \`chapter_status_get\` / \`chapter_status_update\` (reads / writes) | Per-chapter progress tracker in \`.bindery/chapter-status.json\`. |
## Review markers
Bindery review markers let an author tag arbitrary regions for the next review
pass even after those edits have been committed. Use them when you commit
work-in-progress and continue on another machine.
\`\`\`
<!-- Bindery: Review start -->
...lines you want reviewed...
<!-- Bindery: Review stop -->
\`\`\`
- The stop marker is **optional** an unclosed start runs to end of file.
- Multiple regions per file are supported.
- Insert markers via the VS Code commands above, or just type them.
- \`get_review_text(autoStage: true)\` returns marker regions in addition to the
git diff and **removes** the marker lines as part of staging, so the next
review pass starts clean.
## Skill workflows (Claude / Copilot Chat / etc.)
Generated under \`.claude/skills/<name>/SKILL.md\`. Trigger them with a slash
command or by paraphrasing the description.
| Skill | Trigger phrases |
|---|---|
| \`/review\` | "review chapter X", "review my changes", "quick review" |
| \`/translation-review\` | "review my translation", "what do you think" (translation focus) |
| \`/translate\` | "translate chapter X", "spot-check the NL of chapter X" |
| \`/brainstorm\` | "I'm stuck", "help me think of ideas" |
| \`/memory\` | "save what we decided", end-of-session memory updates |
| \`/status\` | "where are we", "what's the book status" |
| \`/continuity\` | "check chapter X for errors", "continuity check" |
| \`/read-aloud\` | "read this aloud", reading-aloud test |
| \`/read-in\` | start-of-session context loading |
| \`/proof-read\` | "proofread chapter X", "get reader feedback" |
## How to answer "What can Bindery do?"
When an agent is asked what Bindery can do for this project, it should:
1. Read **this file** (\`.bindery/README.md\`) — it is intentionally complete.
2. Cross-check live state with \`health\` if the user asks about the index,
AI-file freshness, or the embedding backend.
3. For narrative / install / contribution guidance, point the user to
<https://github.com/evdboom/Bindery> (or fetch the README from
<https://github.com/evdboom/Bindery/blob/main/README.md> if internet access
is available) not for capability listings.
`;
}

View file

@ -0,0 +1,103 @@
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: 'CLAUDE.md',
version: 11,
label: 'project instructions',
zip: null,
};
export function render(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),
'## Start of session',
'1. Use /read-in at the start of a session to load context and get your bearings.',
'2. Run `health` from the Bindery MCP and check `ai_versions_outdated`.',
'3. If `ai_versions_outdated` has entries, run `setup_ai_files` and present the returned `skill_zips.reupload_required` list to the user for Claude Desktop.',
'4. If the skill or MCP server is not available, read at least COWORK.md (if present) for current focus and context.',
'',
'## Memory system',
'1. When concluding a discussion, or after you give a meaningful, preservation-worthy response: use /memory to store it.',
'2. Also when the user asks or otherwise indicates the end of a session: use /memory to save decisions.',
'',
'## Repo layout',
'```',
`${arcFolder}/ ← story arc files`,
`${notesFolder}/ ← story notes (characters, world)`,
`${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(
'',
'## 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 |',
'| `/translation-review` | Review a hand-crafted translation against the source |',
'| `/status` | Book progress snapshot |',
'| `/continuity` | Check a chapter for consistency errors |',
'| `/read-aloud` | Test how a passage reads when spoken |',
'| `/read-in` | Load context and get your bearings at the start of a session |',
'| `/proof-read` | Read the book as multiple proofreaders and present the findings |',
'',
'## MCP server (bindery-mcp)',
'',
'All tools require a `book` argument. Use `list_books` to discover available names.',
'Prefer these tools over Read/Bash when they apply.',
'',
'| 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 |',
'| `init_workspace` | Create or update `.bindery/settings.json` and `translations.json` |',
'| `setup_ai_files` | Regenerate AI instruction files, rebuild Claude skill zip files, and return a change manifest |',
'| `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_book_until` | Fetch chapters from 1..N (or start..N) in one call, concatenated in reading order |',
'| `get_overview` | Chapter structure — acts, chapters, titles |',
'| `get_notes` | Notes/ files, filterable by category or name |',
'| `search` | BM25 full-text search with ranked snippets, optional semantic ranking |',
'| `format` | Apply typography formatting to a file or folder |',
'| `get_review_text` | Structured git diff with review-marker regions and optional auto-staging that consumes markers |',
'| `update_workspace` | Fetch and pull the current branch, with branch/default-branch reporting |',
'| `git_snapshot` | Git commit of story, notes, and arc changes, with optional push |',
'| `get_translation` | List glossary entries for a language, or look up a specific term (forgiving) |',
'| `add_translation` | Add or update a cross-language glossary entry (agent reference, not auto-applied) |',
'| `get_dialect` | List dialect substitution rules, or look up a specific word |',
'| `add_dialect` | Add or update a dialect substitution rule (auto-applied at export, e.g. US→UK) |',
'| `add_language` | Add a language to settings.json and scaffold its story folder with stubs |',
'| `settings_update` | Merge a partial patch into settings.json without replacing unrelated keys |',
'| `memory_list` | List `.bindery/memories/` files with line counts |',
'| `memory_append` | Append a dated session entry to a file in `.bindery/memories/` |',
'| `memory_compact` | Overwrite a file in `.bindery/memories/` with a summary (backs up original to `.bindery/memories/archive/`) |',
'| `chapter_status_get` | Read the chapter progress tracker — entries grouped by status |',
'| `chapter_status_update` | Upsert chapter progress entries (send only changed chapters) |',
);
return lines.filter(l => l !== '\n').join('\n') + '\n';
}

View file

@ -0,0 +1,42 @@
/**
* Shared types + tiny helpers for every template module.
*
* Each template file under `templates/` exports `{ meta, render }` and
* imports `TemplateContext` (and helpers when needed) from this module.
*/
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;
}
/** Per-template metadata. `zip` is non-null only for skills (which ship as zips). */
export interface TemplateMeta {
/** Output path relative to the workspace root. Used as the FILE_VERSION_INFO key. */
file: string;
/** Bump when content changes significantly so users are prompted to refresh. */
version: number;
/** Short, human-readable label used by health reporting. */
label: string;
/** Companion zip path (skills only) or null. */
zip: string | null;
}
export function audienceNote(ctx: TemplateContext): string {
return ctx.audience ? `Target audience: ${ctx.audience}.` : '';
}
export function languageSection(ctx: TemplateContext): string {
if (!ctx.hasMultiLang) { return ''; }
return `\nLanguages: ${ctx.langList}.\n`;
}

View file

@ -0,0 +1,42 @@
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: '.github/copilot-instructions.md',
version: 8,
label: 'copilot instructions',
zip: null,
};
export function render(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`),
'```',
'',
'## Shared skill workflows',
'- Workspace skill files live in `.claude/skills/` and may also be picked up by agents beyond Claude.',
'- Prefer those shared slash workflows when available: `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, `/proof-read`.',
'',
'## 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 normalize them.',
);
if (ctx.audience) {
lines.push(`- Content targets ${ctx.audience}. Keep vocabulary accessible and themes appropriate.`);
}
return lines.join('\n') + '\n';
}

View file

@ -0,0 +1,34 @@
import type { TemplateContext, TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: '.cursor/rules',
version: 8,
label: 'cursor rules',
zip: null,
};
export function render(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}/\` — story arc files for overall and per-act structure and beats`,
`- \`${notesFolder}/\` — story notes, like character profiles and world rules`,
'- Shared workflows live in `.claude/skills/`; if your runtime exposes them, prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, and `/proof-read` for those tasks.',
'',
'## Rules',
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not story content.',
'- Do not normalize 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';
}

View file

@ -0,0 +1,58 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/brainstorm/SKILL.md',
version: 11,
label: 'brainstorm skill',
zip: '.claude/skills/brainstorm.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: brainstorm
description: Bindery workspace - Brainstorm story ideas, plot beats, character moments, or scene concepts. Use for /brainstorm, "I'm stuck", "help me think of ideas", or "Am I stuck?".
---
# Skill: /brainstorm
Brainstorm story ideas, character moments, or plot solutions.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/brainstorm\`, "I'm stuck", "help me think of ideas", or "Am I stuck?".
## Clarify first
- Scope: plot beat | character moment | scene idea | chapter open/close
- Chapter/story point: specify one
- Constraints: list any
## Tools
Use these Bindery MCP tools to gather context:
- \`search(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 ".bindery/settings.json" with \`get_text\` to pick up the current book's genre, target audience, and story structure.
2. Read \`.bindery/memories/global.md\` and the relevant arc file from \`Arc/\`.
3. If chapter specific, read \`.bindery/memories/chXX.md\` if it exists.
4. If character-focused, use \`get_notes(category: "Characters")\` for character profiles.
5. Use \`search\` to find related moments or themes already in the book.
6. 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 the book's configured target audience
`;
}

View file

@ -0,0 +1,58 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/continuity/SKILL.md',
version: 12,
label: 'continuity skill',
zip: '.claude/skills/continuity.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: continuity
description: Bindery workspace - Cross-check a chapter for consistency errors in characters, world rules, or timeline. Use for /continuity, "check continuity", or "check chapter X for errors".
---
# Skill: /continuity
Cross-check a chapter for consistency errors.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## 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 a specific chapter
- \`get_book_until(chapterNumber, language, startChapter?)\` — load prior chapters in one call for timeline/continuity context
- \`get_notes(category, name)\` — look up character profiles or world rules
- \`search(query, language)\` — find earlier mentions of a character detail or event
- \`memory_list\` — check whether a chapter-specific memory file exists (\`chXX.md\`)
## Steps
1. Use \`get_text(".bindery/settings.json")\` to pick up the current book's structure and conventions.
2. Use \`get_chapter\` to read the chapter.
3. Use \`get_text\` to read \`.bindery/memories/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.
4. For world rules: use \`get_notes(category: "World")\`.
5. For timeline and continuity drift checks: use \`get_book_until\` up to the focus chapter. If unavailable, fall back to \`get_chapter\` for nearby prior chapters.
6. Use \`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
`;
}

View file

@ -0,0 +1,83 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/memory/SKILL.md',
version: 11,
label: 'memory skill',
zip: '.claude/skills/memory.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: memory
description: Bindery workspace - Save session decisions to persistent memory files using Bindery MCP tools. Use for /memory, "save this to memory", "update memories", or at end of session.
---
# Skill: /memory
Update project memory files with decisions from the current session.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/memory\`, "save this to memory", "update memories", at meaningful points, 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
### 0. Cross-check assistant memory (if available)
If the runtime has local/session memory, review entries from this session.
Promote repo-worthy entries into Step 3 content.
Promote:
- Story/craft decisions
- Character or world rules
- Structural decisions needed in future sessions
- Anything that must survive across devices
Keep local only:
- Workflow/tool preferences
- Assistant behavior feedback
- Setup/environment notes
- Session-local context
If no local/session memory exists, skip this step.
### 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. \`"Elder 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:
- Summarize 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
`;
}

View file

@ -0,0 +1,247 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/proof-read/SKILL.md',
version: 5,
label: 'proof-read skill',
zip: '.claude/skills/proof-read.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: proof-read
description: Bindery workspace - Multi-perspective proofreading using isolated reader and author personas. Each persona runs as a scoped subagent with no arc, notes, or memory context only the reading-text payload for the read-so-far experience (chapters 1..N). Use for /proof-read, "proofread chapter X", "get reader feedback", "how does this land with readers", "simulate reader reactions", or "peer review".
---
# Skill: /proof-read
Simulates a panel of readers reviewing a chapter as genuine first-time readers no arc knowledge, no notes, no memory of prior sessions. Each persona runs as an isolated subagent that only sees the reading-text payload so far (chapters 1..N) and their assigned role.
The value is in the isolation. A reader doesn't know what the arc says should happen, what a character's backstory is, or what the chapter was *trying* to do. That's exactly the feedback you can't give yourself, and can't get from an agent that has been working on the book with you.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/proof-read\`, "proofread chapter X", "get reader feedback", "how does this land with readers", "simulate reader reactions", or "peer review".
## Steps
### Step 0: Load project context
Before asking the user anything, read the project settings:
\`\`\`
get_text(".bindery/settings.json")
\`\`\`
Extract:
- \`targetAudience\` — used to calibrate reader personas (age, reading level)
- \`genre\` — used to construct the genre-fan persona and to generate author suggestions if needed
- \`proof_read.authors\` — the stored author panel for this project (may be absent)
If \`settings.json\` has no \`proof_read\` section yet, that's expected on first run — handle it in the author setup step below.
### Step 1: Author panel setup
**If \`proof_read.authors\` is set:**
Present the stored authors and confirm:
> "I have [Author A], [Author B], and [Author C] saved for this project. Shall I use them, or would you like to change the panel?"
If the user wants to change: follow the "no authors stored" flow below, then update settings.
**If \`proof_read.authors\` is not set (first run):**
Ask:
> "No author panel configured yet for this project. Would you like suggestions based on the genre, or do you have specific writers in mind?"
- If **suggestions**: generate 4-5 relevant author names based on the book's genre, audience, and tone (see Author Suggestions below). Present them with a one-line description each. Let the user pick 23.
- If **own names**: accept the user's list as-is.
Once the panel is confirmed, store it back to settings:
\`\`\`
settings_update({ patch: { proof_read: { authors: [ { name: "...", known_for: "...", reads_for: "..." } ] } } })
\`\`\`
The \`reads_for\` field is a short phrase describing what this author's lens brings — e.g. "pacing of reveals, handling of danger for the age group". Generate it at storage time so it's available for subagent prompts without needing a web lookup later.
### Step 2: Gather remaining parameters
Ask:
1. Which chapter to focus on or the whole book?
2. Quick run (2 readers + 1 author) or full run (all 4 readers + full author panel)?
If the user invoked \`/proof-read 7\` or similar, the focus chapter is known — no need to ask.
### Step 3: Fetch the reading context
A real reader arrives at chapter N having read everything before it. Subagents receive the full text from chapter 1 up to and including the focus chapter not a summary, not just the target chapter in isolation.
**Why not a summary of prior chapters?** Any summary written by an agent who has worked on the book will carry arc knowledge framing, foreshadowing, loaded context. It biases the subagent in ways a real reader wouldn't be. Full text preserves the isolation.
**Why not have subagents call MCP themselves?** Subagents with MCP access could accidentally pull notes, arc files, or overviews. Using a pre-written staging file and passing only that payload to subagents reduces that risk and is the best available way in this workflow to keep them focused on reader-visible text.
Use \`get_book_until(chapterNumber: n, language)\` to fetch all prior chapters in one call. If unavailable, loop \`get_chapter(1)\` through \`get_chapter(n)\` in the main agent. For a **whole-book** run, fetch all chapters.
Once the text is retrieved, **write it to a staging file**:
\`.bindery/proof-read-payload.md\`
If the file already exists from a previous run, overwrite it.
Modern context windows handle full books comfortably a 20-chapter 12+ novel is roughly 60-80k words, well within range.
### Step 4: Spawn all subagents in a single turn
Launch all persona subagents in parallel. Each receives:
- Their persona description (constructed from project context see Reader Personas and Author Personas below)
- The path to the staging file written in Step 3
- The review task (see Review Task Template) which instructs them to read the staging file as their **only** file access
- An explicit reminder that they have no prior knowledge of this book beyond what they read from that file
### Step 5: Aggregate
Once all subagents return, aggregate across the full panel:
1. **Consensus positives** moments or elements praised by a multitude of readers. These are your strongest material.
2. **Consensus issues** problems flagged by a multitude of readers. Highest priority to address.
3. **Notable divergences** where one reader type loved something another didn't. Not automatically a problem, but a useful creative signal (e.g. a core reader engaged by a worldbuilding passage that lost the reluctant reader).
4. **Author notes** surface separately. These are craft-level observations, not reader reactions, and shouldn't be averaged against them.
Present individual reactions first (summarised), then the aggregated view. Close with a short prioritised action list.
---
## Reader Personas
Reader personas are constructed from the project's \`targetAudience\` and \`genre\` settings — do not hardcode ages or genre references. Use the actual values from settings.
The four reader roles stay stable, but R1 and R3 should be chosen relative to the book's genre rather than treated as fixed labels:
**R1 Core Reader**
A reader at the target age who actively seeks out this kind of book. If the project is fantasy, this is a fantasy reader; if it is realistic contemporary fiction, this is a realistic-fiction reader. They know what this corner does well, enjoy its native pleasures, and notice quickly when the execution is strong or weak.
**R2 Curious Reader**
A reader at the target age who reads regularly but not primarily in this genre. Open and engaged, but reacts as an outsider to genre conventions.
**R3 Opposite-Corner Reader**
A reader at the target age whose tastes pull away from the book's home genre. Their job is to test whether the text still works for someone who does not naturally prize this genre's default strengths. For fantasy, this might be a realism-first reader who cares most about emotional plausibility and character grounding. For realistic fiction, it should be a reader from a different corner, such as mystery, thriller, romance, horror, or speculative fiction, who wants a stronger external hook or a different kind of momentum.
**R4 Reluctant Reader**
A reader at the target age who reads when they have to. Will notice immediately if something drags or confuses. Short patience for exposition. Will find genuine excitement if it's there — but won't invent it.
When building the subagent prompt, fill in the actual age range and genre from settings. Choose R3 as the deliberate contrast to the project's genre, not always as "the realist". For example, if \`targetAudience\` is "12+" and \`genre\` is "sci-fi/fantasy crossover", R1 becomes: *"You are 12-13 years old. You read a lot and you love sci-fi and fantasy..."* If the genre is realistic contemporary fiction, R3 should instead come from a different reading corner, such as mystery, thriller, or speculative fiction.
---
## Author Personas
Author personas come from \`proof_read.authors\` in settings. Each entry has \`name\`, \`known_for\`, and \`reads_for\`. Use these fields directly in the subagent prompt — no need to reconstruct them.
### Author Suggestions
When the user asks for suggestions, generate a shortlist of 4-5 authors whose work overlaps meaningfully with the book's genre, tone, and target audience. Good criteria:
- Writes for approximately the same age group
- Works in the same genre or a closely adjacent one
- Has a distinctive craft lens that adds something different from the others (e.g. one known for worldbuilding, one for pacing, one for character voice)
- Ideally at least one who writes in a "neighbouring" genre (e.g. for a fantasy book, a post-apocalyptic author) to get an outside-genre craft read
Present each suggestion with: name, one well-known title, and what their lens would add to the review.
---
## Review Task Template
For **reader personas**:
> You are [PERSONA DESCRIPTION built from project settings].
>
> You are reading [TARGET CHAPTER OR BOOK] from a [GENRE] novel aimed at [TARGET AUDIENCE] readers. You have no prior knowledge of this book no plot summaries, no character guides, no notes. You are reading this cold, exactly as you would if you'd just picked it up.
> [CHAPTER NOTE: if the focus is a single chapter, say, "you read up to and including chapter N, focus your feedback on chapter N"]
>
> The text is in the file at: \`[STAGING FILE PATH]\`
> Read that file using the \`read_file\` tool. **That is the only file you may access.** Do not call any other tool, MCP server, or external resource.
>
> Give your honest reaction as this reader. Cover:
> 1. Your overall impression (1-2 sentences)
> 2. Moments that worked where you were engaged, what you enjoyed
> 3. Moments that didn't land confusion, slow patches, anything that pulled you out
> 4. Characters: did they feel real? Did you care what happened to them?
> 5. Specific lines or passages worth flagging (positive or negative) quote them
> 6. Would you keep reading? Why or why not?
>
> Be specific. Quote the text when it helps. Do not summarise the plot react to it.
For **author personas**:
> You are reading this book as [AUTHOR NAME], author of [KNOWN_FOR], giving peer feedback to a fellow writer. The book is aimed at [TARGET AUDIENCE] readers. You have no prior knowledge of the manuscript beyond this text.
> [CHAPTER NOTE: if the focus is a single chapter, say, "you read up to and including chapter N, focus your feedback on chapter N"]
>
> Your particular focus: [READS_FOR].
>
> The manuscript is in the file at: \`[STAGING FILE PATH]\`
> Read that file using the \`read_file\` tool. **That is the only file you may access.** Do not call any other tool, MCP server, or external resource.
>
> Give craft-level feedback: what's working and why, what isn't and how you'd think about fixing it. Voice, pacing, structure, dialogue, the handling of tension. Quote the text when useful. Be honest this is peer review, not encouragement.
---
## Output Format
\`\`\`
## Proof-read: [Book title if available] / Chapter [N] [Chapter title if available]
### Reader reactions
**R1 Core reader**
[2-3 sentence summary. Key quote if strong.]
**R2 Curious reader**
...
**R3 Opposite-corner reader**
...
**R4 Reluctant reader**
...
### Author peer review
**[Author name]** ([known_for, short])
[Craft observations, 3-4 sentences]
...
### What landed (consensus 3+ readers)
- [Specific moment or element] flagged by [names]
- ...
### What needs attention (consensus 3+ readers)
- [Issue] flagged by [names]
- ...
### Divergences worth noting
- [Element] resonated with core readers but lost the opposite-corner / reluctant reader
- ...
### Suggested actions
1. [Highest priority]
2. ...
\`\`\`
---
## Quick Run
For a faster pass: **R1** (core reader), **R4** (reluctant reader), and the first stored author. Two reader extremes plus a craft read widest spread with fewest subagents.
---
## Notes for the agent
- **Never** give subagents MCP access. The calling agent should write the reading text to \`.bindery/proof-read-payload.md\` and have subagents work only from that staged file. This reduces the risk of them pulling arc files, notes, or overviews, but treat it as a best-effort workflow unless access restrictions are enforced by the runtime.
- **Staging file:** overwrite it fresh each run so stale text from a previous session never bleeds in.
- **Multiple chapters:** Run each chapter as a separate parallel batch. Aggregate per chapter first, then offer a cross-chapter summary if the user asks.
- **Cost awareness:** Full run is 7 subagent calls per chapter (4 readers + 3 authors). Mention this if the user hasn't specified quick vs. full, especially for longer chapters.
- **Divergences are data, not problems.** A passage that splits readers along genre-familiarity lines might be exactly right for this book. Surface it, let the author decide.
- **Author panel changes:** If the user swaps authors mid-session, update \`proof_read.authors\` in settings before running so the change persists.`;
}

View file

@ -0,0 +1,56 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/read-aloud/SKILL.md',
version: 10,
label: 'read-aloud skill',
zip: '.claude/skills/read-aloud.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: read-aloud
description: Bindery workspace - Test how a chapter or passage sounds when read aloud flags long sentences, staccato rhythm, complex vocabulary, and said-bookisms. Use for /read-aloud, "reading test", or "how does this sound".
---
# Skill: /read-aloud
Test how a chapter sounds when read aloud.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/read-aloud\`, "reading test", or "how does this sound".
## Clarify first
- Whole chapter or specific passage?
## Runtime context
Before reviewing, read ".bindery/settings.json" with \`get_text\` to pick up the current book's target audience and genre.
## 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 the book's configured target audience
- 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")
`;
}

View file

@ -0,0 +1,79 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/read-in/SKILL.md',
version: 12,
label: 'read-in skill',
zip: '.claude/skills/read-in.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: read-in
description: Bindery workspace - Load project context at the start of a session memory, progress tracker, and chapter notes. Use for /read-in, "get your bearings", "what were we doing", or at the start of any working session.
---
# Skill: /read-in
Load context and get your bearings before starting work.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/read-in\`, "get your bearings", "what were we working on", or at the start of a session.
## Tools
Use these Bindery MCP tools:
- \`update_workspace\` — fetch and pull the workspace before loading context; also reports current branch versus the remote default branch
- \`memory_list\` — discover which memory files exist (\`global.md\`, \`chXX.md\` files)
- \`get_text(identifier)\` — read COWORK.md and memory files
- \`chapter_status_get(book)\` — read the structured progress tracker
- \`get_overview(language)\` — list all acts and chapters (only if tracker is empty or sparse)
- \`get_notes(category, name)\` — look up key character or world notes if relevant to current focus
- \`search(query, language)\` — find relevant passages across the book based on current focus or open questions
- \`get_chapter(chapterNumber, language)\` — read a chapter if that's the current focus
## Steps
### 0. Sync repository
Call \`update_workspace\` before loading any context.
- If the update fails (for example: no remote, merge issue, or upstream problem), flag it to the user and stop do not proceed with stale context.
- If the tool reports that the current branch differs from the remote default branch, mention that briefly so the user can decide whether to switch.
- If the tool reports that the workspace is already up to date, say nothing unless the branch status matters.
### 1. Check for current focus
Use \`get_text("COWORK.md")\` to read the current focus file (ignore if missing).
### 2. Load global memory
Use \`get_text(".bindery/settings.json")\` first to pick up the current book's structure and conventions.
Then use \`memory_list\` to discover available memory files, and \`get_text(".bindery/memories/global.md")\` to load cross-chapter decisions.
### 3. Read the progress tracker
Use \`chapter_status_get\` to read current chapter progress. If it is empty or has fewer than 3 entries, also call \`get_overview\` for the full chapter listing.
### 4. Determine working chapter
If COWORK.md names a chapter, use that.
Otherwise if the tracker has a single \`in-progress\` chapter, use that.
Otherwise **ask the user**: "Which chapter do you want to work on?"
### 5. Load chapter memory
- Once the chapter is known (e.g. chapter 10), check \`memory_list\` output for a matching file (\`ch10.md\`). If it exists, read it with \`get_text(".bindery/memories/ch10.md")\`.
- Also read the full chapter text with \`get_chapter\` to have it fresh in context, and to check for any discrepancies with the memory file.
### 6. Story / Arc focus
Depending on the focus and open questions, use \`get_notes\` or \`search\` to load any additional relevant context.
### 7. Summarize
Output a short orientation (3-6 lines):
- Which chapter / scene we're in
- Status from the tracker (draft / in-progress / needs-review)
- Key open decisions from global memory relevant to this chapter
- Any chapter-specific notes from the chapter memory file
- End with a phrase like: "Ready — what would you like to work on?"
## Rules
- Do not load *all* chapter memories only the one being worked on
- Keep the summary brief; this is orientation, not a full status report
- Do not suggest work or ask multiple questions one question at most (which chapter?)
`;
}

View file

@ -0,0 +1,76 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/review/SKILL.md',
version: 13,
label: 'review skill',
zip: '.claude/skills/review.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: review
description: Bindery workspace - Review a chapter for language, arc consistency, and age-appropriateness. Use for /review, "review chapter X", "quick review", or "review my changes".
---
# Skill: /review
Review a chapter and give structured feedback.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/review\`, "review chapter X", "quick review", or "review my changes".
## Clarify first
- Changes, chapter, translation, or overall feedback?
- Type: **Full** (language + arc + age-appropriateness) or **Quick** (language and typos only)?
## Tools
Use these Bindery MCP tools to gather context:
- \`get_review_text(autoStage: true, contextLines: 3)\` — returns the git diff of uncommitted changes **plus** any regions wrapped in \`<!-- Bindery: Review start -->\` / \`<!-- Bindery: Review stop -->\` markers (works even on committed work). \`autoStage: true\` stages reviewed files **and** removes the marker lines from disk so the next call only shows new changes. Pass more contextLines when join points to existing prose need checking
- \`get_chapter(chapterNumber, language)\` — read the full chapter text
- \`get_notes(category, name)\` — look up character profiles (\`category: "Characters"\`) or world rules
- \`search(query, language)\` — find related passages across the book
- \`git_snapshot(message)\` — after a successful review, suggest saving a snapshot
## Steps
### 1. Load settings and context
Start by reading ".bindery/settings.json" with \
\`get_text(".bindery/settings.json")\` to pick up the current book's target audience, genre, and story structure.
Load the right context, pick any or all as needed:
- Read \`.bindery/memories/global.md\`
- Read \`.bindery/memories/chXX.md\` if it exists for chapter-specific context
- Use \`get_chapter\` to load the chapter
- For a Full review, read the relevant arc file from \`Arc/\`.
- For "review my changes", use \`get_review_text\` to get the diff
- If the diff includes translated chapter files, flag that and offer \`/translation-review\` for source-vs-translation feedback
### 2. Perform the review
**Quick** language and typos only.
**Full** adds:
- Arc consistency with the arc file
- Age-appropriateness for the book's configured target audience
- 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
`;
}

View file

@ -0,0 +1,46 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/status/SKILL.md',
version: 10,
label: 'status skill',
zip: '.claude/skills/status.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: status
description: Bindery workspace - Give a book progress snapshot chapters done, in progress, and coming up. Use for /status, "what's the book status", or "where are we".
---
# Skill: /status
Snapshot of the book's progress: what's done, in progress, and coming up.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/status\`, "what's the book status", or "where are we".
## Tools
Use these Bindery MCP tools:
- \`chapter_status_get(book)\` — read the structured progress tracker from \`.bindery/chapter-status.json\`
- \`chapter_status_update(book, chapters)\` — upsert chapter progress entries (send only changed chapters)
- \`get_overview(language)\` — list all acts and chapters with titles
- \`get_text(identifier)\` — read COWORK.md, settings.json, and memory files
- \`memory_list\` — discover which chapter memory files exist (\`chXX.md\`)
## Steps
1. Use \`get_text(".bindery/settings.json")\` to pick up the current book's structure and conventions.
2. Use \`chapter_status_get\` to read the current tracker. Use \`memory_list\` to check available memory files.
3. Use \`get_text\` to read COWORK.md (current focus), \`.bindery/memories/global.md\`, and for in-progress chapters \`.bindery/memories/chXX.md\`.
4. Use \`get_overview\` for the full chapter listing if the tracker is empty or incomplete.
5. Check \`Arc/\` for what's planned vs written (Overall.md + the relevant act file).
6. Output: overall count / done / in-progress / coming up (next 2-3 chapters) / open questions.
7. If the tracker is out of date or missing entries, update it with \`chapter_status_update\` (upsert only the changed chapters).
## Output
Keep it scannable bold headers, short lines. This is a working tool, not a narrative summary.
`;
}

View file

@ -0,0 +1,61 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/translate/SKILL.md',
version: 9,
label: 'translate skill',
zip: '.claude/skills/translate.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: translate
description: Bindery workspace - Translate a chapter or spot-check an existing translation using the Bindery translation table. Use for /translate, "translate chapter X", or "help me with the translation".
---
# Skill: /translate
Translate a chapter or passage into the target language.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## 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? Default to spot-check if a chapter file already exists for the target language.
## Tools
Use these Bindery MCP tools:
- \`get_chapter(chapterNumber, language)\` — read a chapter in any language (source or existing translation)
- \`get_translation(targetLanguage)\` — list glossary entries for a target language (e.g. \`"nl"\`)
- \`get_translation(targetLanguage, word)\` — look up a specific term; forgiving: case-insensitive, handles plurals and inflected forms
- \`search(query, targetLanguage)\` — verify how a term was rendered in other translated chapters
- \`add_translation(targetLanguage, from, to)\` — save a new glossary term pair when the user confirms a translation choice
## Steps
### 1. Load the translation table
Call \`get_translation(targetLanguage)\` to load all known glossary 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 as a glossary entry. For spelling variant rules (dialect substitutions applied at export), use \`add_dialect\` instead.
## Rules
- Always load the translation table first never invent translations for world-specific terms
- Flag uncertain terms rather than guessing
`;
}

View file

@ -0,0 +1,87 @@
import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/translation-review/SKILL.md',
version: 2,
label: 'translation-review skill',
zip: '.claude/skills/translation-review.zip',
};
export function render(_ctx: TemplateContext): string {
return `---
name: translation-review
description: Bindery workspace - Review a hand-crafted translation against the source language for fidelity, naturalness, and glossary consistency. Use for /translation-review, "review my translation", or "what do you think" when translation is the current focus.
---
# Skill: /translation-review
Review a hand-crafted translation against the source.
Use this when the user has written or updated the target-language text and wants structured feedback.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/translation-review\`, "review my translation", or "what do you think" when translation is the active focus.
## Not this skill
- Generating translation text from scratch -> use \`/translate\`
- Reviewing source-language writing quality -> use \`/review\`
## Tools
Use these Bindery MCP tools:
- \`get_review_text(autoStage: true, contextLines: 3)\` — git diff of uncommitted changes **plus** any regions wrapped in \`<!-- Bindery: Review start -->\` / \`<!-- Bindery: Review stop -->\` markers. Marker regions surface even after the author committed and continued elsewhere. \`autoStage: true\` stages files **and** consumes (removes) the marker lines so the next pass starts clean.
- \`get_text(identifier, startLine?, endLine?)\` — fetch matching source lines or focused ranges
- \`get_translation(targetLanguage)\` — load glossary terms for the target language before reviewing
- \`get_chapter(chapterNumber, language)\` — full chapter source/target pair for full spot-check mode
- \`search(query, targetLanguage)\` — verify how a term was used in previously translated chapters before flagging it
- \`add_translation(targetLanguage, from, to)\` — persist a confirmed glossary correction
## Mode 1 - Scoped diff review (primary)
### Steps
1. Call \`get_review_text\`. The response has two sections: a \`# Git diff\` block and a \`# Review markers\` block (one or both may be empty).
2. If both sections are empty, report that nothing new has been translated yet.
3. Identify changed files and determine source/target language from available context: session file (for example COWORK.md), recent conversation, or ask the user if ambiguous.
4. If the target-language file changed (or has marker regions), capture the changed/marked target line range.
5. **Line parity matching** attempt to fetch the corresponding source lines:
- First, assume line parity: call \`get_text(sourceFile, startLine, endLine)\` for the same range as the target.
- **If the content is a complete mismatch** (opening words differ significantly), the translation work may have added or removed lines. Search a window: fetch \`get_text(sourceFile, startLine - 5, endLine + 5)\` and scan for the target text within that range.
- **If still not found**, ask the user: "I couldn't locate these source lines. Can you point me to the starting line number in the source file for this translation?"
6. Load glossary entries via \`get_translation(targetLanguage)\`.
7. Use \`search(query, targetLanguage)\` when a term may have an established translation elsewhere in the book.
8. Compare source vs target and produce feedback using the table below.
9. If source-language lines also changed, flag that and suggest \`/review\` for source-quality feedback.
## Mode 2 - Full chapter spot-check
Use this when the user asks for a full chapter comparison.
1. Determine source language, target language, and chapter number.
2. Load glossary with \`get_translation(targetLanguage)\`.
3. Use \`search(query, targetLanguage)\` as needed to verify recurring terminology in earlier translated chapters.
4. Load chapters with \`get_chapter(chapterNumber, sourceLanguage)\` and \`get_chapter(chapterNumber, targetLanguage)\`.
5. Compare paragraph by paragraph and report findings with the same table.
## Output format
| Before (target) | After (target) | Reason |
|---|---|---|
| Keep context short; bold only the changed words | Suggested wording | Fidelity, naturalness, glossary, or terminology consistency |
Also list glossary mismatches and untranslated world-specific terms explicitly.
## Cross-skill handoff
- If changed lines are only source-language files, suggest switching to \`/review\`.
- If both source and target changed, run translation-review findings first, then prompt whether to run \`/review\` for source edits too.
## Rules
- Load glossary before reviewing and flag mismatches explicitly
- Suggest edits only; do not rewrite entire passages unless asked
- Bold only changed words in Before/After rows
- Mark uncertain calls as questions for user confirmation
- When the user confirms a corrected term, call \`add_translation\` before moving on
- Respond in the session language (usually source language)
`;
}

View file

@ -0,0 +1,253 @@
/**
* Bindery translation types and helpers.
*
* Manages .bindery/translations.json substitution rules and glossaries
* per language pair.
*
* Shared across vscode-ext, obsidian-plugin, and mcp-ts.
* Zero dependency on VS Code or Obsidian APIs.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { getTranslationsPath } from './settings';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Type of a translation entry — determines how the extension uses its rules. */
export type TranslationType = 'substitution' | 'glossary';
/** A single from→to rule inside a translation entry. */
export interface TranslationRule {
from: string;
to: string;
}
/**
* One entry in translations.json, keyed by a language code (e.g. "en-gb", "nl").
*
* substitution applied automatically during export (word-by-word replace).
* glossary reference only; used for consistency checking, not auto-applied.
*/
export interface TranslationEntry {
label?: string;
type: TranslationType;
sourceLanguage?: string;
rules?: TranslationRule[];
ignoredWords?: string[];
}
/** The full .bindery/translations.json file. */
export type TranslationsFile = Record<string, TranslationEntry>;
/** A US→UK word substitution pair. */
export interface UkReplacement {
us: string;
uk: string;
}
// ─── Readers ─────────────────────────────────────────────────────────────────
export function readTranslations(root: string): TranslationsFile | null {
const p = getTranslationsPath(root);
if (!fs.existsSync(p)) { return null; }
try {
return JSON.parse(fs.readFileSync(p, 'utf-8')) as TranslationsFile;
} catch {
return null;
}
}
// ─── Writers ─────────────────────────────────────────────────────────────────
export function writeTranslations(root: string, data: TranslationsFile): void {
const p = getTranslationsPath(root);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}
// ─── Accessors ────────────────────────────────────────────────────────────────
/**
* Get substitution rules from translations.json for the given language key.
* Returns UkReplacement[] compatible with merge.ts (field names us/uk).
* Only entries with type === 'substitution' are returned.
*/
export function getSubstitutionRules(
translations: TranslationsFile | null,
langKey: string
): UkReplacement[] {
if (!translations) { return []; }
const entry = resolveEntry(translations, langKey);
if (entry?.type !== 'substitution') { return []; }
return (entry.rules ?? [])
.filter(r => r.from?.trim() && r.to?.trim())
.map(r => ({ us: r.from.trim().toLowerCase(), uk: r.to.trim() }));
}
/**
* Get the ignored-words set for a given language key.
*/
export function getIgnoredWords(
translations: TranslationsFile | null,
langKey: string
): Set<string> {
if (!translations) { return new Set(); }
const entry = resolveEntry(translations, langKey);
const result = new Set<string>();
for (const word of entry?.ignoredWords ?? []) {
const w = word.trim().toLowerCase();
if (w) { result.add(w); }
}
return result;
}
/**
* Get glossary rules for a language key (type === 'glossary' entries).
*/
export function getGlossaryRules(
translations: TranslationsFile | null,
langKey: string
): TranslationRule[] {
if (!translations) { return []; }
const entry = resolveEntry(translations, langKey);
if (!entry) { return []; }
return (entry.rules ?? []).filter(r => r.from?.trim() && r.to?.trim());
}
// ─── Mutators ─────────────────────────────────────────────────────────────────
/**
* Add or update a substitution rule in .bindery/translations.json.
* Creates the file and entry if they do not yet exist.
*/
export function upsertSubstitutionRule(
root: string,
langKey: string,
rule: TranslationRule
): void {
const translations = readTranslations(root) ?? {};
if (!translations[langKey]) {
translations[langKey] = {
type: 'substitution',
sourceLanguage: 'en',
rules: [],
ignoredWords: [],
};
}
const entry = translations[langKey];
if (entry.type !== 'substitution') {
throw new Error(`Entry '${langKey}' has type '${entry.type}', expected 'substitution'.`);
}
const rules = entry.rules ?? [];
const idx = rules.findIndex(r => r.from.toLowerCase() === rule.from.toLowerCase());
if (idx >= 0) {
rules[idx] = rule;
} else {
rules.push(rule);
rules.sort((a, b) => a.from.localeCompare(b.from));
}
entry.rules = rules;
writeTranslations(root, translations);
}
/**
* Add words to the ignoredWords list in .bindery/translations.json.
* Returns the count of newly added words (duplicates are skipped).
*/
export function addIgnoredWords(
root: string,
langKey: string,
words: string[]
): number {
const translations = readTranslations(root) ?? {};
if (!translations[langKey]) {
translations[langKey] = {
type: 'substitution',
sourceLanguage: 'en',
rules: [],
ignoredWords: [],
};
}
const entry = translations[langKey];
const existing = new Set((entry.ignoredWords ?? []).map(w => w.toLowerCase()));
let added = 0;
for (const word of words) {
const w = word.trim().toLowerCase();
if (w && !existing.has(w)) {
existing.add(w);
added++;
}
}
entry.ignoredWords = Array.from(existing).sort((a, b) => a.localeCompare(b));
writeTranslations(root, translations);
return added;
}
/**
* Add or update a glossary rule in .bindery/translations.json.
* Glossary entries are for cross-language reference (e.g. ENNL world terms).
* They are not auto-applied at export; agents use them for consistency checking.
* Creates the file and entry if they do not yet exist.
*/
export function upsertGlossaryRule(
root: string,
langKey: string,
langLabel: string,
sourceLang: string,
rule: TranslationRule
): void {
const translations = readTranslations(root) ?? {};
if (!translations[langKey]) {
translations[langKey] = {
label: langLabel,
type: 'glossary',
sourceLanguage: sourceLang,
rules: [],
};
}
const entry = translations[langKey];
// If entry exists but was previously substitution, keep it — don't downgrade
const rules = entry.rules ?? [];
const idx = rules.findIndex(r => r.from.toLowerCase() === rule.from.toLowerCase());
if (idx >= 0) {
rules[idx] = rule;
} else {
rules.push(rule);
rules.sort((a, b) => a.from.localeCompare(b.from));
}
entry.rules = rules;
writeTranslations(root, translations);
}
// ─── Internal helpers ─────────────────────────────────────────────────────────
function normaliseKey(key: string): string {
return key.trim().toLowerCase();
}
function isUkLike(key: string): boolean {
const k = normaliseKey(key);
return k === 'uk' || k === 'en-gb' || k === 'en-uk';
}
/**
* Look up a translation entry by language key.
* Falls back to 'en-gb' for UK-like codes.
*/
function resolveEntry(
translations: TranslationsFile,
langKey: string
): TranslationEntry | undefined {
const target = normaliseKey(langKey);
for (const [k, v] of Object.entries(translations)) {
if (normaliseKey(k) === target) { return v; }
}
// For UK-like codes, also accept an 'en-gb' entry
if (isUkLike(target)) {
for (const [k, v] of Object.entries(translations)) {
if (normaliseKey(k) === 'en-gb') { return v; }
}
}
return undefined;
}

View file

@ -0,0 +1,183 @@
/**
* Unit tests for formatting.ts updateTypography() / applyTypography() pure functions.
*
* No VS Code extension host, no commands, no file I/O.
*/
import { describe, expect, it } from 'vitest';
import { updateTypography, applyTypography } from '../src/formatting';
describe('updateTypography()', () => {
// ─── Edge cases ───────────────────────────────────────────────────────────
it('empty string returns empty string', () => {
expect(updateTypography('')).toBe('');
});
it('preserves newlines in multi-line input', () => {
const input = 'line one\nline two\nline three';
const result = updateTypography(input);
expect(result.split('\n')).toHaveLength(3);
expect(result).toContain('\n');
});
// ─── Ellipsis ─────────────────────────────────────────────────────────────
it('converts triple dots to ellipsis character', () => {
expect(updateTypography('And then...')).toBe('And then\u2026');
});
// ─── Em-dash ──────────────────────────────────────────────────────────────
it('converts double-dash to em-dash', () => {
expect(updateTypography('It was--perfect.')).toBe('It was\u2014perfect.');
});
it('does not convert triple-dash (markdown HR)', () => {
const input = '---\nSome text\n---';
const result = updateTypography(input);
expect(result).toContain('---');
expect(result).not.toContain('\u2014\u2014\u2014');
});
// ─── Double quotes ────────────────────────────────────────────────────────
it('converts straight double quotes to curly quotes', () => {
expect(updateTypography('"Hello"')).toBe('\u201CHello\u201D');
});
it('already-correct curly double quotes are not double-processed', () => {
const input = '\u201CHello,\u201D she said.';
expect(updateTypography(input)).toBe(input);
});
// ─── Single quotes / apostrophes ──────────────────────────────────────────
it("converts apostrophe: it's → it\u2019s", () => {
expect(updateTypography("it's")).toBe('it\u2019s');
});
// ─── HTML comment protection ──────────────────────────────────────────────
it('preserves double-dash inside HTML comments (no em-dash conversion)', () => {
const input = '<!-- keep -- this --> "outside"';
const result = updateTypography(input);
// Double-dash inside the comment must survive intact
expect(result).toContain('<!-- keep -- this -->');
// Content outside the comment is still formatted
expect(result).toContain('\u201Coutside\u201D');
});
it('does not alter HTML comment content while formatting surrounding text', () => {
const input = 'before <!-- comment -- here --> after--end';
const result = updateTypography(input);
expect(result).toContain('<!-- comment -- here -->');
expect(result).toContain('\u2014end');
});
// ─── Fenced code blocks ───────────────────────────────────────────────────
it('backtick code fence delimiters are not corrupted by any transform', () => {
// Triple backticks are not matched by ellipsis (/\.\.\./) or em-dash (/--/) regexes
const input = '```typescript\nconst x = 1;\n```';
const result = updateTypography(input);
expect(result).toContain('```typescript');
expect(result).toContain('```');
});
// ─── Mixed content ────────────────────────────────────────────────────────
it('applies all transforms together on mixed content', () => {
const input = '"Wait..." she said--stepping back.';
const result = updateTypography(input);
expect(result).toContain('\u201C'); // opening curly double quote
expect(result).toContain('\u201D'); // closing curly double quote
expect(result).toContain('\u2026'); // ellipsis
expect(result).toContain('\u2014'); // em-dash
expect(result).not.toContain('"');
expect(result).not.toContain('...');
expect(result).not.toContain('--');
});
it('already-formatted content is returned unchanged (idempotent)', () => {
const input = '\u201CHello,\u201D she said\u2014stepping back\u2026';
expect(updateTypography(input)).toBe(input);
});
// ─── Opening quotes after various opening characters ──────────────────────
it('opens double quote after opening parenthesis', () => {
expect(updateTypography('("Hello")')).toContain('\u201CHello\u201D');
});
it('opens double quote after opening bracket', () => {
expect(updateTypography('["Hello"]')).toContain('\u201CHello\u201D');
});
it('opens double quote after em-dash', () => {
const result = updateTypography('\u2014"Hello"');
expect(result).toContain('\u201CHello\u201D');
});
it('opens single quote after parenthesis', () => {
const result = updateTypography("('test')");
expect(result).toContain('\u2018test\u2019');
});
// ─── Closing double quote after em-dash ───────────────────────────────────
it('corrects closing quote after em-dash at end of sentence', () => {
// Input: "word--" should become "word—" (em-dash + closing quote)
const input = 'He said "fine--"';
const result = updateTypography(input);
expect(result).toContain('\u2014\u201D');
});
it('corrects closing quote after em-dash followed by punctuation', () => {
const input = 'He said "fine--" and left.';
const result = updateTypography(input);
expect(result).toContain('\u2014\u201D');
});
// ─── Multiple HTML comments ───────────────────────────────────────────────
it('handles multiple HTML comments in one document', () => {
const input = '<!-- first -- comment -->\nText--here.\n<!-- second -- comment -->';
const result = updateTypography(input);
expect(result).toContain('<!-- first -- comment -->');
expect(result).toContain('<!-- second -- comment -->');
expect(result).toContain('Text\u2014here.');
});
// ─── Multi-line quotes ────────────────────────────────────────────────────
it('handles quote at start of a line', () => {
const result = updateTypography('"Start of line."');
expect(result).toBe('\u201CStart of line.\u201D');
});
// ─── No-op for content without formattable characters ─────────────────────
it('returns plain text unchanged when no formatting is needed', () => {
const input = 'Plain text without anything special.';
expect(updateTypography(input)).toBe(input);
});
});
describe('applyTypography()', () => {
it('is an alias for updateTypography — same output', () => {
const input = '"Hello..." she said--stepping back.';
expect(applyTypography(input)).toBe(updateTypography(input));
});
it('empty string returns empty string', () => {
expect(applyTypography('')).toBe('');
});
it('applies all transforms', () => {
const result = applyTypography('"Test..." value--end.');
expect(result).toContain('\u201C');
expect(result).toContain('\u2026');
expect(result).toContain('\u2014');
});
});

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"outDir": "out",
"lib": ["ES2022"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node"],
"declaration": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'text-summary', 'lcov', 'html'],
reportsDirectory: './coverage',
include: ['src/**/*.ts'],
thresholds: {
statements: 80,
branches: 65,
functions: 90,
lines: 80,
},
},
},
});