mirror of
https://github.com/evdboom/Bindery.git
synced 2026-07-22 06:49:36 +00:00
Merge pull request #128 from evdboom/ai-instructions-overhaul
Ai instructions overhaul
This commit is contained in:
commit
a731a5e5ce
30 changed files with 339 additions and 375 deletions
2
.github/release-notes-template.md
vendored
2
.github/release-notes-template.md
vendored
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Highlights
|
||||
|
||||
- Standardized release packaging across all supported surfaces.
|
||||
- Uniformed template generation
|
||||
- See attached assets below for install-ready downloads.
|
||||
|
||||
## Release Assets
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ const TEMPLATES: Record<string, TemplateModule> = {
|
|||
// 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 }> =
|
||||
export const FILE_VERSION_INFO: Record<string, { version: number; label: string; }> =
|
||||
Object.fromEntries(
|
||||
Object.values(TEMPLATES).map(t => [
|
||||
t.meta.file,
|
||||
{ version: t.meta.version, label: t.meta.label, zip: t.meta.zip },
|
||||
{ version: t.meta.version, label: t.meta.label },
|
||||
]),
|
||||
);
|
||||
|
||||
|
|
|
|||
23
bindery-core/src/templates/agentRender.ts
Normal file
23
bindery-core/src/templates/agentRender.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { TemplateContext, AgentTemplate } from './context';
|
||||
import { pushCapabilitiesSource, pushProjectSection, pushSessionStart, pushMemorySystem, pushRepoLayout, pushWritingRules } from './building-blocks';
|
||||
|
||||
export type TopLevelTarget = 'claude' | 'copilot' | 'cursor' | 'agents';
|
||||
|
||||
export function renderAgentTemplate(ctx: TemplateContext, agent: AgentTemplate): string {
|
||||
const lines: string[] = [
|
||||
`# ${agent.name} — ${ctx.title}`,
|
||||
'',
|
||||
];
|
||||
pushProjectSection(lines, ctx);
|
||||
lines.push('');
|
||||
pushSessionStart(lines, ctx, agent);
|
||||
lines.push('');
|
||||
pushMemorySystem(lines, agent);
|
||||
lines.push('');
|
||||
pushRepoLayout(lines, ctx);
|
||||
lines.push('');
|
||||
pushWritingRules(lines, ctx);
|
||||
lines.push('');
|
||||
pushCapabilitiesSource(lines);
|
||||
return lines.filter(l => l !== '\n').join('\n') + '\n';
|
||||
};
|
||||
|
|
@ -1,59 +1,12 @@
|
|||
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
|
||||
import { type TemplateContext, type TemplateMeta } from './context';
|
||||
import { renderAgentTemplate } from './agentRender';
|
||||
|
||||
export const meta: TemplateMeta = {
|
||||
file: 'AGENTS.md',
|
||||
version: 11,
|
||||
label: 'agents instructions',
|
||||
zip: null,
|
||||
version: 13,
|
||||
label: 'agent instructions',
|
||||
};
|
||||
|
||||
export function render(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity, 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 \`${sessionFile}\` for current focus and handoff notes (via \`session_focus_get\`), and \`${preferencesFile}\` for the author's durable working preferences.`,
|
||||
`2. Read \`${memoriesFolder}/global.md\` for cross-chapter context.`,
|
||||
`3. If working on a specific chapter, read \`${memoriesFolder}/chXX.md\` if it exists.`,
|
||||
'4. 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.`,
|
||||
`- Arc files live in \`${arcFolder}/\`; default planning granularity is \`${arcGranularity}\`. Treat these as story architecture.`,
|
||||
`- Character profiles live in \`${charactersFolder}/\`; use one profile per character plus the index.`,
|
||||
'- 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`, `/proof-read`, `/plan-beats`, and `/character-setup` when the user is asking for one of those structured tasks.',
|
||||
'- Use `arc_*` tools for story structure, `character_*` tools for cast profiles, `note_*` tools for story notes, `memory_*` tools for session decisions, `chapter_status_*` tools for progress, and `session_focus_*` tools for current working state. PREFERENCES.md is user-owned — propose changes rather than writing it.',
|
||||
'- Send rough, unsorted, or pasted material to `Notes/Inbox.md`, then triage with `inbox_process` (propose destinations) and `inbox_resolve` (clear routed items) — do not dump it into memory.',
|
||||
'',
|
||||
'## 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 |`,
|
||||
`| \`${charactersFolder}/\` | Character index and one profile per character |`,
|
||||
`| \`${notesFolder}/\` | Story notes, like world rules, scene ideas, inbox, and research |`,
|
||||
`| \`${sessionFile}\` | Ephemeral working state: current focus, next actions, open questions, handoff (via \`session_focus_*\`) |`,
|
||||
`| \`${preferencesFile}\` | Durable working preferences; user-owned, never tool-written |`,
|
||||
`| \`${memoriesFolder}/global.md\` | Cross-session decisions |`,
|
||||
);
|
||||
return lines.join('\n') + '\n';
|
||||
return renderAgentTemplate(ctx, { hasSkills: true, requiresSkillUpload: false, name: 'Agent Instructions' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.bindery/README.md',
|
||||
version: 10,
|
||||
label: 'bindery capabilities',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
export function render(ctx: TemplateContext): string {
|
||||
|
|
@ -27,7 +26,7 @@ may fetch it for context; otherwise, treat this file as the complete answer.
|
|||
Bindery is a markdown book authoring toolkit with three surfaces:
|
||||
|
||||
1. **VS Code / Obsidian extension** — commands, format-on-save, export to DOCX/EPUB/PDF.
|
||||
2. **MCP server** — programmatic tools for AI assistants (Claude, Copilot, Codex, Cursor).
|
||||
2. **MCP server** — programmatic tools for AI assistants (Claude, Copilot, ChatGPT, Cursor).
|
||||
3. **Skill workflows** — opinionated slash-commands like \`/review\`, \`/translate\`, \`/memory\`.
|
||||
|
||||
All three operate on the same workspace state: \`.bindery/settings.json\`,
|
||||
|
|
@ -121,7 +120,7 @@ tagged **(writes)** modify files or git state.
|
|||
### Tool Workflow Shortcuts
|
||||
|
||||
- Use \`init_workspace\` first for a new book. It creates the settings, translation file, generated capability README, and the default authoring scaffold.
|
||||
- Use \`setup_ai_files\` after init or when \`health\` reports outdated AI files. It refreshes CLAUDE.md, Copilot instructions, Cursor rules, AGENTS.md, Claude skills, and this capability README.
|
||||
- Use \`setup_ai_files\` after init or when \`health\` reports outdated AI files. It refreshes CLAUDE.md, Copilot instructions, Cursor rules, AGENTS.md, generated skills, and this capability README.
|
||||
- Use \`arc_*\` for story architecture under \`${arcFolder}/\`.
|
||||
- Use \`character_*\` for cast profiles under \`${charactersFolder}/\`.
|
||||
- Use \`note_*\` for canonical story notes under \`${notesFolder}/\`.
|
||||
|
|
@ -156,7 +155,7 @@ work-in-progress and continue on another machine.
|
|||
|
||||
## Skill workflows (Claude / Copilot Chat / etc.)
|
||||
|
||||
Generated under \`.claude/skills/<name>/SKILL.md\`. Trigger them with a slash
|
||||
Generated under \`.claude/skills/<name>/SKILL.md\` and/or \`.agents/skills/<name>/SKILL.md\` depending on selected targets. Trigger them with a slash
|
||||
command or by paraphrasing the description.
|
||||
|
||||
| Skill | Trigger phrases |
|
||||
|
|
|
|||
97
bindery-core/src/templates/building-blocks.ts
Normal file
97
bindery-core/src/templates/building-blocks.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { type TemplateContext, type AgentTemplate } from './context';
|
||||
|
||||
/**
|
||||
* Shared project-intent section used by top-level AI instruction templates.
|
||||
*/
|
||||
export function pushProjectSection(lines: string[], ctx: TemplateContext): void {
|
||||
lines.push('## Project');
|
||||
if (ctx.genre) {lines.push(`Genre: ${ctx.genre}`); }
|
||||
if (ctx.description) { lines.push(ctx.description); }
|
||||
if (ctx.audience) { lines.push(`Target audience: ${ctx.audience}.`); }
|
||||
if (ctx.author) { lines.push(`Author: ${ctx.author}.`); }
|
||||
if (ctx.hasMultiLang) { lines.push(`Languages: ${ctx.langList}`); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical capabilities pointer shared by all top-level AI instruction files.
|
||||
*/
|
||||
export function pushCapabilitiesSource(lines: string[]): void {
|
||||
lines.push(
|
||||
'## Capabilities source',
|
||||
'- Use `.bindery/README.md` if you require the canonical source for Bindery capabilities and workflows.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared session-start instructions used by top-level AI instruction templates.
|
||||
*/
|
||||
export function pushSessionStart(lines: string[], ctx: TemplateContext, agent: AgentTemplate): void {
|
||||
lines.push(
|
||||
'## Start of session',
|
||||
'1. Run `health` from the Bindery MCP and check `ai_versions_outdated`.',
|
||||
'2. If `ai_versions_outdated` has entries, run `setup_ai_files`.' + (agent.requiresSkillUpload ? ' If `skill_files.reupload_required` has entries, ask the user to re-upload those SKILL.md files to use them.' : ''),
|
||||
);
|
||||
if (agent.hasSkills) {
|
||||
lines.push(
|
||||
'3. Use /read-in at the start of a session to load context and get your bearings.',
|
||||
`4. If the skill is not available, read at least ${ctx.sessionFile} (if present, use \`session_focus_get\`) for current focus and handoff context, and ${ctx.preferencesFile} for the author's durable working preferences.`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`3. Read ${ctx.sessionFile} (if present) for current focus and handoff context, and ${ctx.preferencesFile} for the author's durable working preferences.`,
|
||||
`4. If ${ctx.sessionFile} mentions a chapter, \`get_chapter(chapterNumber)\` to read that chapter and \`memory_list\` to check for any chapter-specific memory files.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared memory-system instructions used by top-level AI instruction templates.
|
||||
*/
|
||||
export function pushMemorySystem(lines: string[], agent: AgentTemplate): void {
|
||||
lines.push('## Memory system');
|
||||
|
||||
if (agent.hasSkills) {
|
||||
lines.push(
|
||||
`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.`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`1. When concluding a discussion, or after you give a meaningful, preservation-worthy response: use \`memory_append\` to store it.`,
|
||||
`2. Also when the user asks or otherwise indicates the end of a session: use \`memory_append\` to save decisions.`,
|
||||
`3. If a memory file grows too large, use \`memory_compact\` to condense it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function pushRepoLayout(lines: string[], ctx: TemplateContext): void {
|
||||
lines.push(
|
||||
'## Repository layout',
|
||||
'```',
|
||||
`${ctx.arcFolder}/ ← story architecture (${ctx.arcGranularity}-level arc planning by default)`,
|
||||
' index.md ← arc map',
|
||||
' Overall.md ← whole-book arc',
|
||||
' Acts/ ← act-level arc files',
|
||||
`${ctx.notesFolder}/ ← story notes`,
|
||||
`${ctx.charactersFolder}/ ← character index and one profile per character`,
|
||||
`${ctx.sessionFile} ← ephemeral working state (current focus / next actions / open questions / handoff) via session_focus_*`,
|
||||
`${ctx.preferencesFile} ← durable working preferences ("do it like this for me"); user-owned, never tool-written`,
|
||||
`${ctx.storyFolder}/`,
|
||||
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters (one .md per chapter)`),
|
||||
'```',
|
||||
);
|
||||
}
|
||||
|
||||
export function pushWritingRules(lines: string[], ctx: TemplateContext): void {
|
||||
lines.push(
|
||||
'## Writing rules',
|
||||
`- If these rules conflict with the author's preferences in ${ctx.preferencesFile}, follow the preferences.`,
|
||||
'- Prefer usage of the Bindery MCP tools over direct file access.',
|
||||
'- 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.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +1,12 @@
|
|||
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
|
||||
import { type TemplateContext, type TemplateMeta } from './context';
|
||||
import { renderAgentTemplate } from './agentRender';
|
||||
|
||||
export const meta: TemplateMeta = {
|
||||
file: 'CLAUDE.md',
|
||||
version: 17,
|
||||
version: 18,
|
||||
label: 'project instructions',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
export function render(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity } = 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`; if `skill_files.reupload_required` has entries, ask the user to re-upload those SKILL.md files in Claude Desktop Skills (no zip needed).',
|
||||
`4. If the skill or MCP server is not available, read at least ${sessionFile} (if present) for current focus and handoff context, and ${preferencesFile} for the author's durable working preferences.`,
|
||||
'',
|
||||
'## 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 architecture (${arcGranularity}-level arc planning by default)`,
|
||||
` index.md ← arc map`,
|
||||
` Overall.md ← whole-book arc`,
|
||||
` Acts/ ← act-level arc files`,
|
||||
`${notesFolder}/ ← story notes`,
|
||||
`${charactersFolder}/ ← character index and one profile per character`,
|
||||
`${sessionFile} ← ephemeral working state (current focus / next actions / open questions / handoff) via session_focus_*`,
|
||||
`${preferencesFile} ← durable working preferences ("do it like this for me"); user-owned, never tool-written`,
|
||||
`${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` | Guided story ideation when the author is stuck |',
|
||||
'| `/plan-beats` | Create, expand, or refine chapter and scene beatmaps |',
|
||||
'| `/character-setup` | Create or refine structured character profiles |',
|
||||
'| `/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`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold |',
|
||||
'| `setup_ai_files` | Regenerate AI instruction 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 |',
|
||||
'| `note_list` / `note_get` | List or read canonical story notes under Notes/ |',
|
||||
'| `note_create` / `note_append` | Create or append canonical story notes under Notes/ |',
|
||||
'| `character_list` / `character_get` | List or read structured character profiles |',
|
||||
'| `character_create` / `character_update` | Create or update structured character profiles and the cast index |',
|
||||
'| `arc_list` / `arc_get` | List or read structured arc files |',
|
||||
'| `arc_create` / `arc_update` | Create or update structured arc files and the arc index |',
|
||||
'| `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` | Commit changes in bindery workspace, 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) |',
|
||||
`| \`session_focus_get\` | Read current working state from ${sessionFile} (optionally one section) |`,
|
||||
`| \`session_focus_update\` | Update neutral ${sessionFile} sections (Current Focus, Next Actions, Open Questions, Handoff Notes); leaves ${preferencesFile} untouched |`,
|
||||
`| \`inbox_process\` | Enumerate ${notesFolder}/Inbox.md items with stable numbers and propose destinations (read-only) |`,
|
||||
`| \`inbox_resolve\` | Remove already-routed inbox items by number after confirmation |`,
|
||||
);
|
||||
return lines.filter(l => l !== '\n').join('\n') + '\n';
|
||||
return renderAgentTemplate(ctx, { hasSkills: true, requiresSkillUpload: true, name: 'Claude' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,15 +32,10 @@ export interface TemplateMeta {
|
|||
version: number;
|
||||
/** Short, human-readable label used by health reporting. */
|
||||
label: string;
|
||||
/** Legacy companion zip path 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`;
|
||||
}
|
||||
export interface AgentTemplate {
|
||||
hasSkills: boolean;
|
||||
requiresSkillUpload: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
|
@ -1,48 +1,12 @@
|
|||
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
|
||||
import { type TemplateContext, type TemplateMeta } from './context';
|
||||
import { renderAgentTemplate } from './agentRender';
|
||||
|
||||
export const meta: TemplateMeta = {
|
||||
file: '.github/copilot-instructions.md',
|
||||
version: 11,
|
||||
version: 12,
|
||||
label: 'copilot instructions',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
export function render(ctx: TemplateContext): string {
|
||||
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity } = 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 architecture (${arcGranularity}-level arc planning by default)`,
|
||||
` index.md / Overall.md / Acts/`,
|
||||
`${notesFolder}/ ← story notes`,
|
||||
`${charactersFolder}/ ← character index and one profile per character`,
|
||||
`${sessionFile} ← ephemeral working state (current focus / handoff) via session_focus_*`,
|
||||
`${preferencesFile} ← durable working preferences; user-owned, never tool-written`,
|
||||
`${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`, `/plan-beats`, `/character-setup`.',
|
||||
'- Treat arc files as story architecture, not generic notes. Use `arc_*` tools for structure, `character_*` tools for durable cast facts, `note_*` tools for story notes, `memory_*` tools for cross-session decisions, and `session_focus_*` tools for current working state. Durable preferences are user-owned in PREFERENCES.md — propose changes rather than writing it.',
|
||||
'- Send rough, unsorted, or pasted material to `Notes/Inbox.md`, then triage it with `inbox_process` (propose destinations) and `inbox_resolve` (clear routed items) — do not dump it into memory.',
|
||||
'',
|
||||
'## 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';
|
||||
return renderAgentTemplate(ctx, { hasSkills: true, requiresSkillUpload: false, name: 'GitHub Copilot' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,12 @@
|
|||
import type { TemplateContext, TemplateMeta } from './context';
|
||||
import { renderAgentTemplate } from './agentRender';
|
||||
|
||||
export const meta: TemplateMeta = {
|
||||
file: '.cursor/rules',
|
||||
version: 11,
|
||||
version: 12,
|
||||
label: 'cursor rules',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
export function render(ctx: TemplateContext): string {
|
||||
const { title, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity, memoriesFolder } = ctx;
|
||||
const lines: string[] = [
|
||||
`# Cursor rules — ${title}`,
|
||||
'',
|
||||
`Story folder: \`${storyFolder}/\``,
|
||||
`Notes folder: \`${notesFolder}/\``,
|
||||
`Arc folder: \`${arcFolder}/\` (index.md, Overall.md, Acts/; default granularity: ${arcGranularity})`,
|
||||
`Characters folder: \`${charactersFolder}/\``,
|
||||
`Session file: \`${sessionFile}\` (ephemeral working state)`,
|
||||
`Preferences file: \`${preferencesFile}\` (durable, user-owned)`,
|
||||
'',
|
||||
'## Context files to read',
|
||||
`- \`${sessionFile}\` — ephemeral working state (current focus, next actions, open questions, handoff) via \`session_focus_*\``,
|
||||
`- \`${preferencesFile}\` — durable working preferences ("do it like this for me"); user-owned, never tool-written`,
|
||||
`- \`${memoriesFolder}/global.md\` — cross-chapter decisions (read at start of session)`,
|
||||
`- \`${arcFolder}/\` — story architecture, structure, pacing, and beats`,
|
||||
`- \`${charactersFolder}/\` — character index and one profile per character`,
|
||||
`- \`${notesFolder}/\` — story notes, like world rules, scene ideas, inbox, and research`,
|
||||
'- Shared workflows live in `.claude/skills/`; if your runtime exposes them, prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, `/proof-read`, `/plan-beats`, and `/character-setup` for those tasks.',
|
||||
'- Use `arc_*` tools for story structure, `character_*` tools for cast profiles, `note_*` tools for story notes, `memory_*` tools for session decisions, `chapter_status_*` tools for progress, and `session_focus_*` tools for current working state.',
|
||||
'- Send rough, unsorted, or pasted material to `Notes/Inbox.md`, then triage with `inbox_process` and `inbox_resolve` — do not dump it into memory.',
|
||||
'',
|
||||
'## 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';
|
||||
return renderAgentTemplate(ctx, { hasSkills: false, requiresSkillUpload: false, name: 'Cursor' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/brainstorm/SKILL.md',
|
||||
version: 12,
|
||||
label: 'brainstorm skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/character-setup/SKILL.md',
|
||||
version: 1,
|
||||
label: 'character-setup skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/continuity/SKILL.md',
|
||||
version: 14,
|
||||
label: 'continuity skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/memory/SKILL.md',
|
||||
version: 14,
|
||||
label: 'memory skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/plan-beats/SKILL.md',
|
||||
version: 3,
|
||||
label: 'plan-beats skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/proof-read/SKILL.md',
|
||||
version: 6,
|
||||
label: 'proof-read skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/read-aloud/SKILL.md',
|
||||
version: 11,
|
||||
label: 'read-aloud skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/read-in/SKILL.md',
|
||||
version: 16,
|
||||
label: 'read-in skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/review/SKILL.md',
|
||||
version: 14,
|
||||
label: 'review skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/status/SKILL.md',
|
||||
version: 14,
|
||||
label: 'status skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
|
|||
file: '.claude/skills/translate/SKILL.md',
|
||||
version: 10,
|
||||
label: 'translate skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import type { TemplateContext, TemplateMeta } from '../context';
|
|||
|
||||
export const meta: TemplateMeta = {
|
||||
file: '.claude/skills/translation-review/SKILL.md',
|
||||
version: 4,
|
||||
version: 5,
|
||||
label: 'translation-review skill',
|
||||
zip: null,
|
||||
};
|
||||
|
||||
const CONTENT = [
|
||||
|
|
@ -73,9 +72,9 @@ const CONTENT = [
|
|||
"",
|
||||
"## Output format",
|
||||
"",
|
||||
"| Category | Before (target) | After (target) | Reason |",
|
||||
"|---|---|---|---|",
|
||||
"| Glossary | Keep context short; bold only the changed words | Suggested wording | Term differs from stored glossary |",
|
||||
"| Category | Location | Before (target) | After (target) | Reason |",
|
||||
"|---|---|---|---|---|",
|
||||
"| Glossary | Line 5 | Keep context short; bold only the changed words | Suggested wording | Term differs from stored glossary |",
|
||||
"",
|
||||
"**Category values:** Fidelity · Naturalness · Glossary · Register · Comment",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -64,10 +64,9 @@ describe('renderTemplate — claude', () => {
|
|||
expect(result).toContain('## Project');
|
||||
expect(result).toContain('## Start of session');
|
||||
expect(result).toContain('## Memory system');
|
||||
expect(result).toContain('## Repo layout');
|
||||
expect(result).toContain('## Repository layout');
|
||||
expect(result).toContain('## Writing rules');
|
||||
expect(result).toContain('## Available skills');
|
||||
expect(result).toContain('## MCP server (bindery-mcp)');
|
||||
expect(result).toContain('## Capabilities source');
|
||||
});
|
||||
|
||||
it('includes genre, description, audience, and author', () => {
|
||||
|
|
@ -113,8 +112,12 @@ describe('renderTemplate — copilot', () => {
|
|||
it('contains required sections', () => {
|
||||
const result = renderTemplate('copilot', makeCtx());
|
||||
expect(result).toContain('# GitHub Copilot — Test Book');
|
||||
expect(result).toContain('## Repo layout');
|
||||
expect(result).toContain('## Writing guidelines');
|
||||
expect(result).toContain('## Project');
|
||||
expect(result).toContain('## Start of session');
|
||||
expect(result).toContain('## Memory system');
|
||||
expect(result).toContain('## Repository layout');
|
||||
expect(result).toContain('## Writing rules');
|
||||
expect(result).toContain('## Capabilities source');
|
||||
});
|
||||
|
||||
it('includes genre and description when set', () => {
|
||||
|
|
@ -123,9 +126,12 @@ describe('renderTemplate — copilot', () => {
|
|||
expect(result).toContain('A tale of adventure.');
|
||||
});
|
||||
|
||||
it('omits project section entirely when all optional fields are empty', () => {
|
||||
it('keeps project section even when optional project fields are empty', () => {
|
||||
const result = renderTemplate('copilot', makeMinimalCtx());
|
||||
expect(result).not.toContain('## Project');
|
||||
expect(result).toContain('## Project');
|
||||
expect(result).not.toContain('Genre:');
|
||||
expect(result).not.toContain('Author:');
|
||||
expect(result).not.toContain('Target audience:');
|
||||
});
|
||||
|
||||
it('includes audience writing guideline when audience is set', () => {
|
||||
|
|
@ -133,13 +139,10 @@ describe('renderTemplate — copilot', () => {
|
|||
expect(result).toContain('middle-grade');
|
||||
});
|
||||
|
||||
it('mentions shared workspace skills from the .claude folder', () => {
|
||||
it('uses read-in startup guidance and no explicit skill catalog', () => {
|
||||
const result = renderTemplate('copilot', makeCtx());
|
||||
expect(result).toContain('.claude/skills/');
|
||||
expect(result).toContain('/translation-review');
|
||||
expect(result).toContain('/read-in');
|
||||
expect(result).toContain('/plan-beats');
|
||||
expect(result).toContain('/character-setup');
|
||||
expect(result).not.toContain('.claude/skills/');
|
||||
});
|
||||
|
||||
it('ends with a newline', () => {
|
||||
|
|
@ -150,14 +153,19 @@ describe('renderTemplate — copilot', () => {
|
|||
describe('renderTemplate — cursor', () => {
|
||||
it('contains required sections', () => {
|
||||
const result = renderTemplate('cursor', makeCtx());
|
||||
expect(result).toContain('# Cursor rules — Test Book');
|
||||
expect(result).toContain('## Context files to read');
|
||||
expect(result).toContain('## Rules');
|
||||
expect(result).toContain('# Cursor — Test Book');
|
||||
expect(result).toContain('## Project');
|
||||
expect(result).toContain('## Start of session');
|
||||
expect(result).toContain('## Memory system');
|
||||
expect(result).toContain('## Repository layout');
|
||||
expect(result).toContain('## Writing rules');
|
||||
expect(result).toContain('## Capabilities source');
|
||||
});
|
||||
|
||||
it('references memory and arc folder paths', () => {
|
||||
it('references arc folder and memory tools', () => {
|
||||
const result = renderTemplate('cursor', makeCtx());
|
||||
expect(result).toContain('.bindery/memories');
|
||||
expect(result).toContain('memory_append');
|
||||
expect(result).toContain('memory_compact');
|
||||
expect(result).toContain('Arc/');
|
||||
});
|
||||
|
||||
|
|
@ -166,13 +174,11 @@ describe('renderTemplate — cursor', () => {
|
|||
expect(result).toContain('middle-grade');
|
||||
});
|
||||
|
||||
it('mentions shared workspace skills from the .claude folder', () => {
|
||||
it('uses non-skill startup guidance for runtimes without skill support', () => {
|
||||
const result = renderTemplate('cursor', makeCtx());
|
||||
expect(result).toContain('.claude/skills/');
|
||||
expect(result).toContain('/translation-review');
|
||||
expect(result).toContain('/proof-read');
|
||||
expect(result).toContain('/plan-beats');
|
||||
expect(result).toContain('/character-setup');
|
||||
expect(result).toContain('Read SESSION.md');
|
||||
expect(result).toContain('get_chapter(chapterNumber)');
|
||||
expect(result).not.toContain('/read-in');
|
||||
});
|
||||
|
||||
it('ends with a newline', () => {
|
||||
|
|
@ -184,11 +190,12 @@ describe('renderTemplate — agents', () => {
|
|||
it('contains required sections', () => {
|
||||
const result = renderTemplate('agents', makeCtx());
|
||||
expect(result).toContain('# Agent Instructions — Test Book');
|
||||
expect(result).toContain('## Project overview');
|
||||
expect(result).toContain('## Project');
|
||||
expect(result).toContain('## Start of session');
|
||||
expect(result).toContain('## Story files');
|
||||
expect(result).toContain('## Writing guidelines');
|
||||
expect(result).toContain('## Key reference files');
|
||||
expect(result).toContain('## Memory system');
|
||||
expect(result).toContain('## Repository layout');
|
||||
expect(result).toContain('## Writing rules');
|
||||
expect(result).toContain('## Capabilities source');
|
||||
});
|
||||
|
||||
it('includes genre, description, audience, and author', () => {
|
||||
|
|
@ -199,19 +206,21 @@ describe('renderTemplate — agents', () => {
|
|||
expect(result).toContain('Jane Doe');
|
||||
});
|
||||
|
||||
it('references memories and arc folders', () => {
|
||||
it('references repository structure and arc folders', () => {
|
||||
const result = renderTemplate('agents', makeCtx());
|
||||
expect(result).toContain('.bindery/memories');
|
||||
expect(result).toContain('Repository layout');
|
||||
expect(result).toContain('Arc/');
|
||||
});
|
||||
|
||||
it('mentions shared workspace skills from the .claude folder', () => {
|
||||
it('uses read-in startup guidance and no explicit shared-skill inventory section', () => {
|
||||
const result = renderTemplate('agents', makeCtx());
|
||||
expect(result).toContain('.claude/skills/');
|
||||
expect(result).toContain('/translation-review');
|
||||
expect(result).toContain('/review');
|
||||
expect(result).toContain('/plan-beats');
|
||||
expect(result).toContain('/character-setup');
|
||||
expect(result).toContain('/read-in');
|
||||
expect(result).not.toContain('## Shared skill workflows');
|
||||
});
|
||||
|
||||
it('uses skill-first startup guidance with read-in', () => {
|
||||
const result = renderTemplate('agents', makeCtx());
|
||||
expect(result).toContain('Use /read-in at the start of a session');
|
||||
});
|
||||
|
||||
it('ends with a newline', () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
* AI instruction file generation for Bindery (MCP server).
|
||||
*
|
||||
* Generates CLAUDE.md, .github/copilot-instructions.md, .cursor/rules,
|
||||
* AGENTS.md, and .claude/skills/<skill>/SKILL.md from the book's
|
||||
* AGENTS.md, and skills under .claude/skills/<skill>/SKILL.md and
|
||||
* .agents/skills/<skill>/SKILL.md from the book's
|
||||
* .bindery/settings.json.
|
||||
*
|
||||
* Template content is maintained in bindery-core/src/templates/*.ts
|
||||
|
|
@ -58,25 +59,21 @@ export interface AiSetupOptions {
|
|||
export interface AiSetupResult {
|
||||
regenerated: string[];
|
||||
skipped: string[];
|
||||
skillZipManifest: {
|
||||
rebuilt: string[];
|
||||
created: string[];
|
||||
skipped: string[];
|
||||
failed: string[];
|
||||
};
|
||||
versionStamp: AiVersionFile;
|
||||
}
|
||||
|
||||
interface AiVersionEntry {
|
||||
version: number;
|
||||
label: string;
|
||||
zip: string | null;
|
||||
}
|
||||
|
||||
export interface AiVersionFile {
|
||||
versions: Record<string, AiVersionEntry>;
|
||||
}
|
||||
|
||||
const SKILL_ROOTS = ['.claude', '.agents'] as const;
|
||||
const SKILL_PATH_RE = /^\.(claude|agents)\/skills\/([^/]+)\/SKILL\.md$/;
|
||||
|
||||
// ─── Settings types ───────────────────────────────────────────────────────────
|
||||
|
||||
interface Settings extends Omit<WorkspaceSettings, 'languages'> {
|
||||
|
|
@ -142,7 +139,6 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
|
|||
const result: AiSetupResult = {
|
||||
regenerated: [],
|
||||
skipped: [],
|
||||
skillZipManifest: { rebuilt: [], created: [], skipped: [], failed: [] },
|
||||
versionStamp: versionFile,
|
||||
};
|
||||
|
||||
|
|
@ -169,6 +165,10 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
|
|||
break;
|
||||
case 'agents':
|
||||
writeFile(root, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, versionFile, result);
|
||||
for (const skill of skills) {
|
||||
const skillMd = path.join('.agents', 'skills', skill, 'SKILL.md');
|
||||
writeFile(root, skillMd, renderTemplate(skill, ctx), overwrite, versionFile, result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +200,10 @@ export function readAiVersionFile(root: string): AiVersionFile {
|
|||
export function expectedAiVersionEntries(): Record<string, AiVersionEntry> {
|
||||
const out: Record<string, AiVersionEntry> = {};
|
||||
for (const [file, info] of Object.entries(FILE_VERSION_INFO)) {
|
||||
out[file] = { version: info.version, label: info.label, zip: info.zip };
|
||||
const entry = { version: info.version, label: info.label };
|
||||
for (const variant of versionPathVariants(file)) {
|
||||
out[variant] = entry;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -222,7 +225,6 @@ export function writeBinderyCapabilitiesReadme(root: string): void {
|
|||
const versionFile = readAiVersionFile(root);
|
||||
const result: AiSetupResult = {
|
||||
regenerated: [], skipped: [],
|
||||
skillZipManifest: { rebuilt: [], created: [], skipped: [], failed: [] },
|
||||
versionStamp: versionFile,
|
||||
};
|
||||
writeFile(root, path.join('.bindery', 'README.md'), renderTemplate('bindery-readme', ctx), true, versionFile, result);
|
||||
|
|
@ -241,7 +243,7 @@ function writeFile(
|
|||
): void {
|
||||
const full = path.join(root, relPath);
|
||||
const key = toKey(relPath);
|
||||
const expected = FILE_VERSION_INFO[key];
|
||||
const expected = expectedVersionInfoForKey(key);
|
||||
const existingVersion = versionFile.versions[key]?.version ?? 0;
|
||||
const isUpToDate = expected ? existingVersion >= expected.version : true;
|
||||
|
||||
|
|
@ -258,15 +260,29 @@ function writeFile(
|
|||
}
|
||||
|
||||
function stampVersionEntry(versionFile: AiVersionFile, relPath: string): void {
|
||||
const expected = FILE_VERSION_INFO[relPath];
|
||||
const expected = expectedVersionInfoForKey(relPath);
|
||||
if (!expected) { return; }
|
||||
versionFile.versions[relPath] = {
|
||||
version: expected.version,
|
||||
label: expected.label,
|
||||
zip: expected.zip,
|
||||
label: expected.label
|
||||
};
|
||||
}
|
||||
|
||||
function expectedVersionInfoForKey(key: string): { version: number; label: string } | undefined {
|
||||
for (const variant of versionPathVariants(key)) {
|
||||
const expected = FILE_VERSION_INFO[variant];
|
||||
if (expected) { return expected; }
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function versionPathVariants(relPath: string): string[] {
|
||||
const skillMatch = SKILL_PATH_RE.exec(relPath);
|
||||
if (!skillMatch) { return [relPath]; }
|
||||
const skillName = skillMatch[2];
|
||||
return SKILL_ROOTS.map(root => `${root}/skills/${skillName}/SKILL.md`);
|
||||
}
|
||||
|
||||
function stampAiVersionFile(root: string, versionFile: AiVersionFile): void {
|
||||
const dir = path.join(root, '.bindery');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ function getSemanticStatus(root: string): SemanticStatus {
|
|||
return { status, staleReasons: stale.isStale ? stale.reasons : [] };
|
||||
}
|
||||
|
||||
type OutdatedEntry = { file: string; label: string; zip: string | null; expected: number; found: number };
|
||||
type OutdatedEntry = { file: string; label: string; expected: number; found: number };
|
||||
|
||||
function getOutdatedAiFiles(root: string, enabledTargets: Set<string>): OutdatedEntry[] {
|
||||
const installed = readAiVersionFile(root);
|
||||
|
|
@ -457,7 +457,7 @@ function getOutdatedAiFiles(root: string, enabledTargets: Set<string>): Outdated
|
|||
if (!fs.existsSync(path.join(root, file))) { continue; }
|
||||
const found = installed.versions[file]?.version ?? 0;
|
||||
if (found < exp.version) {
|
||||
outdated.push({ file, label: exp.label, zip: exp.zip, expected: exp.version, found });
|
||||
outdated.push({ file, label: exp.label, expected: exp.version, found });
|
||||
}
|
||||
}
|
||||
return outdated;
|
||||
|
|
@ -2626,13 +2626,6 @@ export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string {
|
|||
const response = {
|
||||
regenerated_files: result.regenerated,
|
||||
skipped_files: result.skipped,
|
||||
skill_zips: {
|
||||
created: result.skillZipManifest.created,
|
||||
rebuilt: result.skillZipManifest.rebuilt,
|
||||
skipped: result.skillZipManifest.skipped,
|
||||
failed: result.skillZipManifest.failed,
|
||||
reupload_required: [],
|
||||
},
|
||||
skill_files: {
|
||||
reupload_required: skillFilesToReupload,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ async function loadAiSetup() {
|
|||
return import('../src/aisetup');
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
vi.resetModules();
|
||||
return import('../src/templates');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
|
|
@ -52,29 +57,6 @@ describe('setupAiFiles skill generation', () => {
|
|||
expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review.zip'))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns an empty skill zip manifest (zips are no longer generated)', async () => {
|
||||
const { setupAiFiles } = await loadAiSetup();
|
||||
|
||||
const root = makeRoot();
|
||||
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
|
||||
bookTitle: 'Test Book',
|
||||
storyFolder: 'Story',
|
||||
languages: [{ code: 'EN', folderName: 'EN' }],
|
||||
}, null, 2) + '\n');
|
||||
|
||||
const result = setupAiFiles({
|
||||
root,
|
||||
targets: ['claude'],
|
||||
skills: ['read-aloud'],
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
expect(result.skillZipManifest.created).toEqual([]);
|
||||
expect(result.skillZipManifest.rebuilt).toEqual([]);
|
||||
expect(result.skillZipManifest.skipped).toEqual([]);
|
||||
expect(result.skillZipManifest.failed).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not overwrite legacy zip files that already exist', async () => {
|
||||
const { setupAiFiles } = await loadAiSetup();
|
||||
|
||||
|
|
@ -87,15 +69,86 @@ describe('setupAiFiles skill generation', () => {
|
|||
|
||||
write(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'legacy zip bytes');
|
||||
|
||||
const result = setupAiFiles({
|
||||
setupAiFiles({
|
||||
root,
|
||||
targets: ['claude'],
|
||||
skills: ['read-aloud'],
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
expect(result.skillZipManifest.failed).toEqual([]);
|
||||
expect(result.skillZipManifest.created).toEqual([]);
|
||||
|
||||
expect(fs.readFileSync(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'utf-8')).toBe('legacy zip bytes');
|
||||
});
|
||||
|
||||
it('generates requested skill markdown files for agents target under .agents/skills', async () => {
|
||||
const { setupAiFiles } = await loadAiSetup();
|
||||
|
||||
const root = makeRoot();
|
||||
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
|
||||
bookTitle: 'Test Book',
|
||||
storyFolder: 'Story',
|
||||
languages: [{ code: 'EN', folderName: 'EN' }],
|
||||
}, null, 2) + '\n');
|
||||
|
||||
const result = setupAiFiles({
|
||||
root,
|
||||
targets: ['agents'],
|
||||
skills: ['read-in'],
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
expect(result.regenerated).toContain('.agents/skills/read-in/SKILL.md');
|
||||
expect(fs.existsSync(path.join(root, '.agents', 'skills', 'read-in', 'SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
it('includes .agents skill files in expected AI version entries', async () => {
|
||||
const { expectedAiVersionEntries } = await loadAiSetup();
|
||||
const { FILE_VERSION_INFO } = await loadTemplates();
|
||||
|
||||
const expected = expectedAiVersionEntries();
|
||||
const claudeKey = '.claude/skills/read-in/SKILL.md';
|
||||
const agentsKey = '.agents/skills/read-in/SKILL.md';
|
||||
|
||||
expect(expected[agentsKey]).toEqual(expected[claudeKey]);
|
||||
expect(expected[agentsKey]).toEqual({
|
||||
version: FILE_VERSION_INFO[claudeKey].version,
|
||||
label: FILE_VERSION_INFO[claudeKey].label,
|
||||
});
|
||||
});
|
||||
|
||||
it('regenerates .agents skill file when stamped version is stale and overwrite is false', async () => {
|
||||
const { setupAiFiles, readAiVersionFile } = await loadAiSetup();
|
||||
|
||||
const root = makeRoot();
|
||||
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
|
||||
bookTitle: 'Test Book',
|
||||
storyFolder: 'Story',
|
||||
languages: [{ code: 'EN', folderName: 'EN' }],
|
||||
}, null, 2) + '\n');
|
||||
|
||||
setupAiFiles({
|
||||
root,
|
||||
targets: ['agents'],
|
||||
skills: ['read-in'],
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
const versionPath = path.join(root, '.bindery', 'ai-version.json');
|
||||
const raw = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
|
||||
versions: Record<string, { version: number; label: string }>;
|
||||
};
|
||||
raw.versions['.agents/skills/read-in/SKILL.md'].version = 0;
|
||||
fs.writeFileSync(versionPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
|
||||
|
||||
const second = setupAiFiles({
|
||||
root,
|
||||
targets: ['agents'],
|
||||
skills: ['read-in'],
|
||||
overwrite: false,
|
||||
});
|
||||
|
||||
expect(second.regenerated).toContain('.agents/skills/read-in/SKILL.md');
|
||||
|
||||
const stamped = readAiVersionFile(root);
|
||||
expect(stamped.versions['.agents/skills/read-in/SKILL.md'].version).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -342,18 +342,12 @@ describe('mcp tools', () => {
|
|||
const parsed = JSON.parse(raw) as {
|
||||
regenerated_files?: string[];
|
||||
skipped_files?: string[];
|
||||
skill_zips?: { created?: string[]; rebuilt?: string[]; skipped?: string[]; failed?: string[]; reupload_required?: string[] };
|
||||
skill_files?: { reupload_required?: string[] };
|
||||
ai_versions?: { versions?: Record<string, { version: number; label: string; zip: string | null }> };
|
||||
ai_versions?: { versions?: Record<string, { version: number; label: string; }> };
|
||||
};
|
||||
|
||||
expect(Array.isArray(parsed.regenerated_files)).toBe(true);
|
||||
expect(Array.isArray(parsed.skipped_files)).toBe(true);
|
||||
expect(Array.isArray(parsed.skill_zips?.created)).toBe(true);
|
||||
expect(Array.isArray(parsed.skill_zips?.rebuilt)).toBe(true);
|
||||
expect(Array.isArray(parsed.skill_zips?.skipped)).toBe(true);
|
||||
expect(Array.isArray(parsed.skill_zips?.failed)).toBe(true);
|
||||
expect(Array.isArray(parsed.skill_zips?.reupload_required)).toBe(true);
|
||||
expect(Array.isArray(parsed.skill_files?.reupload_required)).toBe(true);
|
||||
expect(parsed.skill_files?.reupload_required).toContain('.claude/skills/review/SKILL.md');
|
||||
expect(parsed.ai_versions?.versions?.['.claude/skills/review/SKILL.md']?.label).toBe('review skill');
|
||||
|
|
@ -369,16 +363,11 @@ describe('mcp tools', () => {
|
|||
|
||||
const raw = toolSetupAiFiles(root, { targets: ['claude'], skills: ['read-aloud'], overwrite: true });
|
||||
const parsed = JSON.parse(raw) as {
|
||||
skill_zips?: { created?: string[]; rebuilt?: string[]; failed?: string[]; reupload_required?: string[] };
|
||||
skill_files?: { reupload_required?: string[] };
|
||||
};
|
||||
|
||||
const zipPath = path.join(root, '.claude', 'skills', 'read-aloud.zip');
|
||||
expect(fs.existsSync(zipPath)).toBe(false);
|
||||
expect(parsed.skill_zips?.created).toEqual([]);
|
||||
expect(parsed.skill_zips?.rebuilt).toEqual([]);
|
||||
expect(parsed.skill_zips?.failed).toEqual([]);
|
||||
expect(parsed.skill_zips?.reupload_required).toEqual([]);
|
||||
expect(parsed.skill_files?.reupload_required).toEqual(['.claude/skills/read-aloud/SKILL.md']);
|
||||
});
|
||||
|
||||
|
|
@ -401,7 +390,7 @@ describe('mcp tools', () => {
|
|||
// Downgrade the review skill version to simulate outdated
|
||||
const versionPath = path.join(root, '.bindery', 'ai-version.json');
|
||||
const versionFile = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
|
||||
versions: Record<string, { version: number; label: string; zip: string | null }>;
|
||||
versions: Record<string, { version: number; label: string; }>;
|
||||
};
|
||||
versionFile.versions['.claude/skills/review/SKILL.md'].version = 0;
|
||||
fs.writeFileSync(versionPath, JSON.stringify(versionFile, null, 2) + '\n', 'utf-8');
|
||||
|
|
@ -436,7 +425,7 @@ describe('mcp tools', () => {
|
|||
// Downgrade the review skill version to simulate outdated
|
||||
const versionPath = path.join(root, '.bindery', 'ai-version.json');
|
||||
const versionFile = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
|
||||
versions: Record<string, { version: number; label: string; zip: string | null }>;
|
||||
versions: Record<string, { version: number; label: string;}>;
|
||||
};
|
||||
versionFile.versions['.claude/skills/review/SKILL.md'].version = 0;
|
||||
fs.writeFileSync(versionPath, JSON.stringify(versionFile, null, 2) + '\n', 'utf-8');
|
||||
|
|
@ -523,7 +512,7 @@ describe('mcp tools', () => {
|
|||
|
||||
const versionPath = path.join(root, '.bindery', 'ai-version.json');
|
||||
const versionFile = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
|
||||
versions: Record<string, { version: number; label: string; zip: string | null }>;
|
||||
versions: Record<string, { version: number; label: string; }>;
|
||||
};
|
||||
versionFile.versions['.claude/skills/review/SKILL.md'].version = 0;
|
||||
fs.writeFileSync(versionPath, JSON.stringify(versionFile, null, 2) + '\n', 'utf-8');
|
||||
|
|
@ -531,7 +520,7 @@ describe('mcp tools', () => {
|
|||
const healthRaw = toolHealth(root);
|
||||
const health = JSON.parse(healthRaw) as {
|
||||
ai_version_outdated?: boolean;
|
||||
ai_versions_outdated?: Array<{ file: string; label: string; zip: string | null }>;
|
||||
ai_versions_outdated?: Array<{ file: string; label: string; }>;
|
||||
};
|
||||
|
||||
expect(health.ai_version_outdated).toBe(true);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Obsidian-specific AI setup wrapper.
|
||||
*
|
||||
* Generates AI instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md)
|
||||
* and Claude skill templates from the book's .bindery/settings.json.
|
||||
* and skill templates from the book's .bindery/settings.json.
|
||||
*
|
||||
* For Obsidian, this is a simpler adaptation since we don't have the VS Code LM API,
|
||||
* so we just generate the files to the filesystem directly.
|
||||
|
|
@ -89,6 +89,10 @@ export function setupAiFiles(
|
|||
break;
|
||||
case 'agents':
|
||||
writeFile(bookRoot, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, result);
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join('.agents', 'skills', skill);
|
||||
writeFile(bookRoot, path.join(skillDir, 'SKILL.md'), renderTemplate(skill, ctx), overwrite, result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,22 @@ describe('AI Setup', () => {
|
|||
expect(fs.existsSync(agentsPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should create skill markdown files under .agents/skills when agents target requested', async () => {
|
||||
const settingsPath = path.join(tempRoot, '.bindery', 'settings.json');
|
||||
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({
|
||||
bookTitle: 'Test Book',
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
await setupAiFiles(mockApp, tempRoot, ['agents'], ['read-in', 'review'], false);
|
||||
|
||||
const readInSkillPath = path.join(tempRoot, '.agents', 'skills', 'read-in', 'SKILL.md');
|
||||
const reviewSkillPath = path.join(tempRoot, '.agents', 'skills', 'review', 'SKILL.md');
|
||||
|
||||
expect(fs.existsSync(readInSkillPath)).toBe(true);
|
||||
expect(fs.existsSync(reviewSkillPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip existing files when overwrite is false', async () => {
|
||||
const settingsPath = path.join(tempRoot, '.bindery', 'settings.json');
|
||||
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* claude → CLAUDE.md + .claude/skills/<skill>/SKILL.md
|
||||
* copilot → .github/copilot-instructions.md
|
||||
* cursor → .cursor/rules
|
||||
* agents → AGENTS.md (OpenAI Agents, Aider, Codex, etc.)
|
||||
* agents → AGENTS.md + .agents/skills/<skill>/SKILL.md (OpenAI Agents, Aider, Codex, etc.)
|
||||
*
|
||||
* Templates and TemplateContext live in @bindery/core (single source of truth).
|
||||
*/
|
||||
|
|
@ -92,6 +92,10 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
|
|||
|
||||
case 'agents':
|
||||
writeFile(root, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, result);
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join('.agents', 'skills', skill);
|
||||
writeFile(root, path.join(skillDir, 'SKILL.md'), renderTemplate(skill, ctx), overwrite, result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue