refactor: update descriptions for bindery_git_snapshot command in README and package.json

This commit is contained in:
Erik van der Boom 2026-05-12 23:43:22 +02:00
parent 9b5aaf7a68
commit a7049e1900
16 changed files with 682 additions and 531 deletions

View file

@ -42,7 +42,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server that exposes
- **BM25 full-text search** — fast lexical search across all chapters and notes via [MiniSearch](https://lucaong.github.io/minisearch/)
- **Optional semantic search** — set `BINDERY_OLLAMA_URL` for semantic reranking, or enable a full semantic index for precomputed embedding search
- **Version tracking**`get_review_text` returns a structured git diff **plus** any regions wrapped in `<!-- Bindery: Review start --> ... <!-- Bindery: Review stop -->` markers (so committed work-in-progress can still be reviewed). `git_snapshot` saves progress as a git commit scoped to story/notes/arc folders. Git is auto-initialized during workspace setup if available
- **Version tracking**`get_review_text` returns a structured git diff **plus** any regions wrapped in `<!-- Bindery: Review start --> ... <!-- Bindery: Review stop -->` markers (so committed work-in-progress can still be reviewed). `git_snapshot` saves progress as a git commit. Git is auto-initialized during workspace setup if available
- **Translation & dialect management** — glossary entries and dialect substitution rules in `.bindery/translations.json`, queryable and updatable by agents
- **Session memory** — persistent `.bindery/memories/` files for cross-session decisions, with append, list, and compact operations
- **Chapter status tracking** — per-chapter progress tracker (`draft`, `in-progress`, `done`, `needs-review`)

View file

@ -2,7 +2,7 @@ import type { TemplateContext, TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: '.bindery/README.md',
version: 5,
version: 6,
label: 'bindery capabilities',
zip: null,
};
@ -77,7 +77,7 @@ tagged **(writes)** modify files or git state.
| \`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. |
| \`git_snapshot\` (writes) | Commit changes in bindery workspace, 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. |

View file

@ -2,7 +2,7 @@ import { audienceNote, languageSection, type TemplateContext, type TemplateMeta
export const meta: TemplateMeta = {
file: 'CLAUDE.md',
version: 11,
version: 12,
label: 'project instructions',
zip: null,
};
@ -86,7 +86,7 @@ export function render(ctx: TemplateContext): string {
'| `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 |',
'| `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 |',

View file

@ -12,7 +12,8 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node"],
"declaration": true
"declaration": true,
"composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]

View file

@ -12,7 +12,8 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node"],
"declaration": true
"declaration": true,
"composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]

View file

@ -270,7 +270,7 @@ server.registerTool('update_workspace', {
server.registerTool('git_snapshot', {
title: 'Git Snapshot',
description:
'Save a snapshot (git commit) of all changes in story, notes, and arc folders. ' +
'Save a snapshot (git commit) of all changes in the bindery workspace. ' +
'Provides an optional commit message, can optionally push to a remote branch, and can remember push defaults in settings.json. ' +
'Use this to create save points after writing sessions or successful reviews.',
inputSchema: {

View file

@ -1157,12 +1157,6 @@ function consumeReviewMarkers(root: string, relFiles: string[]): void {
}
}
/** Content folders that git operations should scope to. */
function contentFolders(root: string): string[] {
const story = storyFolder(root);
return [story, 'Notes', 'Arc'].filter(d => fs.existsSync(path.join(root, d)));
}
// ─── update_workspace ────────────────────────────────────────────────────────
export interface UpdateWorkspaceArgs {
@ -1233,13 +1227,11 @@ export interface GitSnapshotArgs {
}
export function toolGitSnapshot(root: string, args: GitSnapshotArgs): string {
const dirs = contentFolders(root);
if (dirs.length === 0) { return 'No content folders found to snapshot.'; }
// Stage content folders
// Stage everything tracked or untracked in the repo
try {
const result = spawnSync('git', ['add', ...dirs], { cwd: root, encoding: 'utf-8' });
const result = spawnSync('git', ['add', '.'], { cwd: root, encoding: 'utf-8' });
if (result.error) { throw result.error; }
if (result.status !== 0) { throw new Error(result.stderr || 'git add failed'); }
} catch {
return 'Failed to stage files. Is this a git repository?';
}
@ -1254,7 +1246,7 @@ export function toolGitSnapshot(root: string, args: GitSnapshotArgs): string {
return 'Failed to check staged files.';
}
if (!staged.trim()) { return 'Nothing to snapshot — no changes in content folders.'; }
if (!staged.trim()) { return 'Nothing to snapshot — no changes to commit.'; }
const fileCount = staged.trim().split('\n').length;
const msg = args.message ?? `Snapshot ${new Date().toISOString().slice(0, 16).replaceAll('T', ' ')}`;

View file

@ -165,21 +165,15 @@ describe('toolGetReviewText', () => {
// ─── toolGitSnapshot ──────────────────────────────────────────────────────────
describe('toolGitSnapshot', () => {
it('returns "Nothing to snapshot" for a non-git dir with Story folder', () => {
it('returns "Nothing to snapshot" for a non-git dir', () => {
const root = makeRoot();
fs.mkdirSync(path.join(root, 'Story'), { recursive: true });
const result = toolGitSnapshot(root, {});
expect(result).toBe('Nothing to snapshot — no changes in content folders.');
expect(result).toBe('Failed to stage files. Is this a git repository?');
});
it('returns "No content folders found" when no Story/Notes/Arc exist', () => {
const root = makeRoot();
const result = toolGitSnapshot(root, {});
expect(result).toBe('No content folders found to snapshot.');
});
it('returns "Nothing to snapshot" when content is committed and has no changes', () => {
it('returns "Nothing to snapshot" when everything is already committed', () => {
const root = makeGitRepo();
write(path.join(root, 'Story', 'EN', 'Chapter 1.md'), '# Ch1\nContent.\n');
@ -187,7 +181,7 @@ describe('toolGitSnapshot', () => {
spawnSync('git', ['commit', '-m', 'Add chapter'], { cwd: root });
const result = toolGitSnapshot(root, {});
expect(result).toBe('Nothing to snapshot — no changes in content folders.');
expect(result).toBe('Nothing to snapshot — no changes to commit.');
});
it('commits a new content file and returns snapshot message', () => {
@ -283,6 +277,31 @@ describe('toolGitSnapshot', () => {
expect(result).toContain('Could not save snapshot push defaults: .bindery/settings.json was not found.');
});
it('commits .bindery files (memories, translations) along with story content', () => {
const root = makeGitRepo();
write(path.join(root, 'Story', 'EN', 'Chapter 1.md'), '# Ch1\nContent.\n');
write(path.join(root, '.bindery', 'memories', 'global.md'), '## Session\nSome notes.\n');
const result = toolGitSnapshot(root, {});
expect(result).toContain('Snapshot saved:');
expect(result).toContain('2 files');
});
it('snapshots .bindery changes even without story folder changes', () => {
const root = makeGitRepo();
write(path.join(root, 'Story', 'EN', 'Chapter 1.md'), '# Ch1\nContent.\n');
spawnSync('git', ['add', '.'], { cwd: root });
spawnSync('git', ['commit', '-m', 'Initial'], { cwd: root });
// Only memory changed
write(path.join(root, '.bindery', 'memories', 'global.md'), '## Session\nNew note.\n');
const result = toolGitSnapshot(root, {});
expect(result).toContain('Snapshot saved:');
expect(result).toContain('1 file');
});
});
describe('toolUpdateWorkspace', () => {

View file

@ -146,7 +146,7 @@ table with issue type, location, and the reference that contradicts it.
| `format` | Apply typography formatting to a file or folder |
| `get_review_text` | Git diff of uncommitted changes plus any `<!-- Bindery: Review start/stop -->` marker regions; optional auto-staging consumes the 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 |
| `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) |
| `get_dialect` | List dialect substitution rules, or look up a specific word |

View file

@ -74,7 +74,7 @@
{ "name": "format", "description": "Apply typography formatting (curly quotes, em-dashes, ellipses) to a file or folder." },
{ "name": "get_review_text", "description": "Structured review payload: git diff of uncommitted changes plus any regions wrapped in <!-- Bindery: Review start/stop --> markers (works even on committed work). Filter by language (EN, NL, ALL). autoStage stages reviewed files and consumes review markers." },
{ "name": "update_workspace", "description": "Fetch and pull the current git branch, report the current branch versus the remote default branch, and optionally switch branches before pulling." },
{ "name": "git_snapshot", "description": "Save a snapshot (git commit) of all changes in story, notes, and arc folders. Can also push and remember push defaults." },
{ "name": "git_snapshot", "description": "Save a snapshot (git commit) of all changes in the bindery workspace." },
{ "name": "add_translation", "description": "Add or update a cross-language glossary entry in .bindery/translations.json for agent reference (source → target term). For dialect substitution rules use add_dialect." },
{ "name": "get_translation", "description": "Look up cross-language glossary entries from .bindery/translations.json. List all entries for a language or do a forgiving word lookup. For dialect substitution rules use get_dialect." },
{ "name": "add_dialect", "description": "Add or update a dialect substitution rule in .bindery/translations.json (e.g. US→UK spelling: color→colour). Applied automatically during export." },

View file

@ -9,11 +9,10 @@
* inside the Obsidian app itself.
*/
import { BINDERY_FOLDER, SETTINGS_FILENAME, upsertGlossaryRule, getDefaultLanguage, type LanguageConfig } from '@bindery/core';
import type { OutputType } from '@bindery/merge';
import { mergeBook } from './merge';
import { BINDERY_FOLDER, SETTINGS_FILENAME, upsertGlossaryRule, getDefaultLanguage, applyTypography, readWorkspaceSettings, type LanguageConfig } from '@bindery/core';
import { mergeBook, type OutputType } from './merge';
import { formatFile } from './formatter';
import { exportBook, resolveBookRoot } from './exporter';
import { resolveBookRoot } from './exporter';
import { setupAiFiles, ALL_SKILLS } from './ai-setup';
import { readSettings, addDialectRule, addLanguage, findProbableUsWords } from './workspace';
import { BinderySettingsTab, DEFAULT_SETTINGS, type BinderySettings } from './settings-tab';
@ -175,6 +174,13 @@ export default class BinderyPlugin extends Plugin {
callback: () => void this.formatActive(),
});
// Command: format all markdown files in a folder
this.addCommand({
id: 'format-folder',
name: 'Format folder (all .md files)',
callback: () => void this.formatFolder(),
});
// Review marker commands
this.addCommand({
id: 'start-review-marker',
@ -187,17 +193,7 @@ export default class BinderyPlugin extends Plugin {
editorCallback: (editor) => this.insertStopReviewMarker(editor),
});
// Export commands
for (const fmt of ['md', 'docx', 'epub', 'pdf'] as const) {
const label = fmt.toUpperCase();
this.addCommand({
id: `export-${fmt}`,
name: `Export → ${label}`,
callback: () => void exportBook(this.app, this.settings, fmt),
});
}
// Merge commands (NEW: discover chapters and merge them)
// Merge commands
for (const fmt of ['md', 'docx', 'epub', 'pdf', 'all'] as const) {
const outputTypes = fmt === 'all' ? ['md', 'docx', 'epub', 'pdf'] : [fmt];
const label = fmt === 'all' ? 'All Formats' : fmt.toUpperCase();
@ -302,6 +298,46 @@ export default class BinderyPlugin extends Plugin {
}
}
private async formatFolder(): Promise<void> {
let vaultPath: string;
let bookRoot: string;
try {
vaultPath = this.getVaultBasePath();
bookRoot = resolveBookRoot(vaultPath, this.settings.bookRoot);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.notify(`Format folder failed: ${message}`);
return;
}
const wsSettings = readWorkspaceSettings(bookRoot);
const storyFolder = wsSettings?.storyFolder ?? 'Story';
const targetDir = path.join(bookRoot, storyFolder);
if (!fs.existsSync(targetDir)) {
this.notify(`Story folder not found: ${storyFolder}`);
return;
}
const count = this.formatDirectoryRecursive(targetDir);
this.notify(`Typography: ${count} file(s) updated.`);
}
private formatDirectoryRecursive(dirPath: string): number {
let count = 0;
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
count += this.formatDirectoryRecursive(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
const content = fs.readFileSync(fullPath, 'utf-8');
const formatted = applyTypography(content);
if (content !== formatted) {
fs.writeFileSync(fullPath, formatted, 'utf-8');
count++;
}
}
}
return count;
}
private async formatActive(): Promise<void> {
const file = this.app.workspace?.getActiveFile?.();
if (file?.extension !== 'md') {

View file

@ -12,6 +12,8 @@ import {
type MergeResult,
type OutputType,
} from '@bindery/merge';
export type { OutputType };
import {
readWorkspaceSettings,
getDefaultLanguage,

View file

@ -3,9 +3,9 @@
"module": "commonjs",
"target": "ES2022",
"outDir": "out",
"rootDir": "src",
"lib": ["ES2022", "DOM"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@ -14,5 +14,9 @@
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"references": [
{ "path": "../bindery-core" },
{ "path": "../bindery-merge" }
]
}

1056
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -79,7 +79,7 @@ Bindery includes a bundled MCP server that makes your book's chapters, notes, se
| `bindery_format` | Apply typography formatting to a file or folder |
| `bindery_get_review_text` | Git diff of uncommitted changes **plus** any `<!-- Bindery: Review start/stop -->` regions (works on committed work too) |
| `bindery_update_workspace` | Fetch and pull the current branch, with branch/default-branch reporting |
| `bindery_git_snapshot` | Save a snapshot (git commit) of story/notes/arc changes, with optional push |
| `bindery_git_snapshot` | Commit changes in bindery workspace, with optional push |
| `bindery_get_translation` | Look up cross-language glossary entries |
| `bindery_add_translation` | Add or update a glossary entry |
| `bindery_get_dialect` | Look up dialect substitution rules |

View file

@ -559,7 +559,7 @@
"displayName": "Bindery: Git Snapshot",
"toolReferenceName": "binderyGitSnapshot",
"canBeReferencedInPrompt": true,
"modelDescription": "Save a snapshot (git commit) of all changes in story, notes, and arc folders, with optional push and saved push defaults.",
"modelDescription": "Save a snapshot (git commit) of all changes in the bindery workspace, with optional push and saved push defaults.",
"inputSchema": {
"type": "object",
"properties": {