diff --git a/.github/release-notes-template.md b/.github/release-notes-template.md new file mode 100644 index 0000000..8d27503 --- /dev/null +++ b/.github/release-notes-template.md @@ -0,0 +1,24 @@ +# Bindery {{VERSION}} + +## Highlights + +- Standardized release packaging across all supported surfaces. +- See attached assets below for install-ready downloads. + +## Release Assets + +- bindery-{{VERSION}}.mcpb (Claude Desktop / Cowork MCP package) +- bindery-mcp-server-{{VERSION}}.zip (Standalone MCP package for clients that support command + args + env configuration, such as ChatGPT Work and LM Studio) +- bindery-{{VERSION}}.vsix (VS Code extension) +- bindery-obsidian-bundled-{{VERSION}}.zip (Obsidian plugin bundle) +- obsidian-plugin/dist/main.js and obsidian-plugin/dist/manifest.json (raw Obsidian artifacts for obsidian automated scanning) + +## Installation + +- VS Code extension and MCP setup overview: https://github.com/evdboom/Bindery/blob/main/README.md +- MCP package details and manual install: https://github.com/evdboom/Bindery/blob/main/mcpb/README.md +- Standalone MCP zip quick install: included README.md inside bindery-mcp-server-{{VERSION}}.zip + +## Notes + +- Release notes below this section are auto-generated from merged pull requests and commits. \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ed094d..0ae4cf7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,9 +25,9 @@ jobs: fi echo "release_version=$VERSION" >> "$GITHUB_OUTPUT" - # ── Job 1: Build .mcpb for Claude Desktop ─────────────────────────────────── + # ── Job 1: Build .mcpb for Claude Desktop + standalone MCP zip ───────────── build-mcpb: - name: Build .mcpb + name: Build MCP Packages needs: validate-release-version runs-on: ubuntu-latest permissions: @@ -76,15 +76,38 @@ jobs: mv "$f" "bindery-${VERSION}.mcpb" 2>/dev/null || true done + - name: Package standalone MCP server zip + run: | + VERSION="$RELEASE_VERSION" + mkdir -p mcpb/standalone-mcp/server + cp mcpb/server/index.js mcpb/standalone-mcp/server/index.js + cp mcpb/standalone-mcp-README.md mcpb/standalone-mcp/README.md + cd mcpb/standalone-mcp + zip "../../bindery-mcp-server-${VERSION}.zip" README.md server/index.js + + - name: Render standardized release notes + run: | + VERSION="$RELEASE_VERSION" + sed "s/{{VERSION}}/${VERSION}/g" .github/release-notes-template.md > .github/release-notes.generated.md + - name: Attest .mcpb build provenance uses: actions/attest-build-provenance@v4 with: subject-path: mcpb/*.mcpb + - name: Attest standalone MCP zip provenance + uses: actions/attest-build-provenance@v4 + with: + subject-path: bindery-mcp-server-*.zip + - name: Upload .mcpb to release uses: softprops/action-gh-release@v3 with: - files: mcpb/*.mcpb + files: | + mcpb/*.mcpb + bindery-mcp-server-*.zip + body_path: .github/release-notes.generated.md + generate_release_notes: true fail_on_unmatched_files: true # ── Job 2: Package VS Code extension + attach VSIX to release ─────────────── diff --git a/README.md b/README.md index 043e413..3693219 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,10 @@ Then in Obsidian: Settings → Community plugins (if not restricted) → Install ### [mcpb/](mcpb/) — Claude Desktop Extension -Packages the MCP server as a `.mcpb` file for one-click installation in Claude Desktop or Cowork. +Release assets now include two MCP package formats: + +- `bindery-.mcpb` for Claude Desktop / Cowork +- `bindery-mcp-server-.zip` as a standalone MCP package for clients that support command + args + env setup (for example ChatGPT Work or LM Studio) **Download the latest release** from [Releases](../../releases) — no build step needed. @@ -110,6 +113,21 @@ Packages the MCP server as a `.mcpb` file for one-click installation in Claude D - **Note:** full embedding can be a heavy operation, depending on your hardware, when running a local Ollama instance. 6. Tools are now available — the agent calls `list_books` to discover book names +### Standalone MCP Clients (ChatGPT Work, LM Studio, etc.) + +1. Download `bindery-mcp-server-*.zip` from the [latest release](../../releases/latest) +2. Unzip it to a stable folder +3. In your MCP client, add an MCP server with: + - eg. in ChatGPT Work -> **Settings** -> **Plug-ins** -> **MCP's** tab + - Command: `node` + - Args: absolute path to `server/index.js` from the unzipped folder +4. Set environment variables: + - `BINDERY_BOOKS` (required): semicolon-separated `Name=path` pairs + - `BINDERY_OLLAMA_URL` (optional): Ollama endpoint for semantic features + - `BINDERY_ENABLE_SEMANTIC_INDEX` (optional): `true` to enable full semantic index builds + - `BINDERY_DEFAULT_SEARCH_MODE` (optional): `lexical`, `semantic_rerank`, or `full_semantic` +5. Save and reconnect the MCP server + ## Architecture Overview The following flow shows how Bindery's writing environments connect to agents, MCP, and skills at a high level. diff --git a/bindery-core/src/templates/bindery-readme.ts b/bindery-core/src/templates/bindery-readme.ts index 355f659..410ff08 100644 --- a/bindery-core/src/templates/bindery-readme.ts +++ b/bindery-core/src/templates/bindery-readme.ts @@ -2,7 +2,7 @@ import type { TemplateContext, TemplateMeta } from './context'; export const meta: TemplateMeta = { file: '.bindery/README.md', - version: 9, + version: 10, label: 'bindery capabilities', zip: null, }; @@ -121,7 +121,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, skill zips, 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, Claude 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}/\`. diff --git a/bindery-core/src/templates/claude.ts b/bindery-core/src/templates/claude.ts index df71a51..66ee2b3 100644 --- a/bindery-core/src/templates/claude.ts +++ b/bindery-core/src/templates/claude.ts @@ -2,7 +2,7 @@ import { audienceNote, languageSection, type TemplateContext, type TemplateMeta export const meta: TemplateMeta = { file: 'CLAUDE.md', - version: 15, + version: 17, label: 'project instructions', zip: null, }; @@ -23,7 +23,7 @@ export function render(ctx: TemplateContext): string { '## 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.', + '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', @@ -82,7 +82,7 @@ export function render(ctx: TemplateContext): string { '| `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, rebuild Claude skill zip files, and return a change manifest |', + '| `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 |', diff --git a/bindery-core/src/templates/context.ts b/bindery-core/src/templates/context.ts index e178f51..c2b69cd 100644 --- a/bindery-core/src/templates/context.ts +++ b/bindery-core/src/templates/context.ts @@ -24,7 +24,7 @@ export interface TemplateContext { hasMultiLang: boolean; } -/** Per-template metadata. `zip` is non-null only for skills (which ship as zips). */ +/** Per-template metadata used by AI file version reporting. */ export interface TemplateMeta { /** Output path relative to the workspace root. Used as the FILE_VERSION_INFO key. */ file: string; @@ -32,7 +32,7 @@ export interface TemplateMeta { version: number; /** Short, human-readable label used by health reporting. */ label: string; - /** Companion zip path (skills only) or null. */ + /** Legacy companion zip path or null. */ zip: string | null; } diff --git a/bindery-core/src/templates/skills/brainstorm.ts b/bindery-core/src/templates/skills/brainstorm.ts index 1d5181e..30ad47d 100644 --- a/bindery-core/src/templates/skills/brainstorm.ts +++ b/bindery-core/src/templates/skills/brainstorm.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/brainstorm/SKILL.md', version: 12, label: 'brainstorm skill', - zip: '.claude/skills/brainstorm.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/character-setup.ts b/bindery-core/src/templates/skills/character-setup.ts index c7a635e..3659998 100644 --- a/bindery-core/src/templates/skills/character-setup.ts +++ b/bindery-core/src/templates/skills/character-setup.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/character-setup/SKILL.md', version: 1, label: 'character-setup skill', - zip: '.claude/skills/character-setup.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/continuity.ts b/bindery-core/src/templates/skills/continuity.ts index 36a1afb..e2684d2 100644 --- a/bindery-core/src/templates/skills/continuity.ts +++ b/bindery-core/src/templates/skills/continuity.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/continuity/SKILL.md', version: 14, label: 'continuity skill', - zip: '.claude/skills/continuity.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/memory.ts b/bindery-core/src/templates/skills/memory.ts index eaa962d..c6ddf3a 100644 --- a/bindery-core/src/templates/skills/memory.ts +++ b/bindery-core/src/templates/skills/memory.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/memory/SKILL.md', version: 14, label: 'memory skill', - zip: '.claude/skills/memory.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/plan-beats.ts b/bindery-core/src/templates/skills/plan-beats.ts index 3dfaf4c..8eb71b0 100644 --- a/bindery-core/src/templates/skills/plan-beats.ts +++ b/bindery-core/src/templates/skills/plan-beats.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/plan-beats/SKILL.md', version: 3, label: 'plan-beats skill', - zip: '.claude/skills/plan-beats.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/proof-read.ts b/bindery-core/src/templates/skills/proof-read.ts index 0f0df9e..b9ddb1c 100644 --- a/bindery-core/src/templates/skills/proof-read.ts +++ b/bindery-core/src/templates/skills/proof-read.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/proof-read/SKILL.md', version: 6, label: 'proof-read skill', - zip: '.claude/skills/proof-read.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/read-aloud.ts b/bindery-core/src/templates/skills/read-aloud.ts index f7c7250..837e640 100644 --- a/bindery-core/src/templates/skills/read-aloud.ts +++ b/bindery-core/src/templates/skills/read-aloud.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/read-aloud/SKILL.md', version: 11, label: 'read-aloud skill', - zip: '.claude/skills/read-aloud.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/read-in.ts b/bindery-core/src/templates/skills/read-in.ts index ab6a9d1..8fc77a8 100644 --- a/bindery-core/src/templates/skills/read-in.ts +++ b/bindery-core/src/templates/skills/read-in.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/read-in/SKILL.md', version: 16, label: 'read-in skill', - zip: '.claude/skills/read-in.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/review.ts b/bindery-core/src/templates/skills/review.ts index d2301ec..41f2574 100644 --- a/bindery-core/src/templates/skills/review.ts +++ b/bindery-core/src/templates/skills/review.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/review/SKILL.md', version: 14, label: 'review skill', - zip: '.claude/skills/review.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/status.ts b/bindery-core/src/templates/skills/status.ts index 6e0d5e0..c3b98cb 100644 --- a/bindery-core/src/templates/skills/status.ts +++ b/bindery-core/src/templates/skills/status.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/status/SKILL.md', version: 14, label: 'status skill', - zip: '.claude/skills/status.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/translate.ts b/bindery-core/src/templates/skills/translate.ts index a250c2d..fbe8e37 100644 --- a/bindery-core/src/templates/skills/translate.ts +++ b/bindery-core/src/templates/skills/translate.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/translate/SKILL.md', version: 10, label: 'translate skill', - zip: '.claude/skills/translate.zip', + zip: null, }; const CONTENT = [ diff --git a/bindery-core/src/templates/skills/translation-review.ts b/bindery-core/src/templates/skills/translation-review.ts index a07961e..167b113 100644 --- a/bindery-core/src/templates/skills/translation-review.ts +++ b/bindery-core/src/templates/skills/translation-review.ts @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { file: '.claude/skills/translation-review/SKILL.md', version: 4, label: 'translation-review skill', - zip: '.claude/skills/translation-review.zip', + zip: null, }; const CONTENT = [ diff --git a/mcp-ts/src/aisetup.ts b/mcp-ts/src/aisetup.ts index 1d2b052..fffb55a 100644 --- a/mcp-ts/src/aisetup.ts +++ b/mcp-ts/src/aisetup.ts @@ -12,7 +12,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { zipSync, strToU8 } from 'fflate'; import { renderTemplate, FILE_VERSION_INFO, @@ -161,7 +160,6 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult { const skillMd = path.join('.claude', 'skills', skill, 'SKILL.md'); writeFile(root, skillMd, renderTemplate(skill, ctx), overwrite, versionFile, result); } - rebuildSkillZips(root, skills, result); break; case 'copilot': writeFile(root, path.join('.github', 'copilot-instructions.md'), renderTemplate('copilot', ctx), overwrite, versionFile, result); @@ -279,60 +277,6 @@ function stampAiVersionFile(root: string, versionFile: AiVersionFile): void { ); } -function rebuildSkillZips(root: string, skills: SkillTemplate[], result: AiSetupResult): void { - for (const skill of skills) { - const skillDir = path.join(root, '.claude', 'skills', skill); - const skillMd = path.join(skillDir, 'SKILL.md'); - if (!fs.existsSync(skillMd)) { continue; } - - const zipRel = `.claude/skills/${skill}.zip`; - const zipAbs = path.join(root, zipRel); - - const zipExists = fs.existsSync(zipAbs); - const upToDate = zipExists - && fs.statSync(zipAbs).mtimeMs >= fs.statSync(skillMd).mtimeMs; - - if (upToDate) { - result.skillZipManifest.skipped.push(zipRel); - continue; - } - - if (!zipSkillFolder(root, skill, zipAbs)) { - result.skillZipManifest.failed.push(zipRel); - continue; - } - - if (zipExists) { - result.skillZipManifest.rebuilt.push(zipRel); - } else { - result.skillZipManifest.created.push(zipRel); - } - } -} - function toKey(relPath: string): string { return relPath.replaceAll('\\', '/'); } - -function zipSkillFolder(root: string, skill: SkillTemplate, zipAbs: string): boolean { - const skillMd = path.join(root, '.claude', 'skills', skill, 'SKILL.md'); - if (!fs.existsSync(skillMd)) { return false; } - - try { - const skillContent = fs.readFileSync(skillMd, 'utf-8'); - const zipBytes = zipSync({ - [`${skill}/SKILL.md`]: strToU8(skillContent), - }); - - fs.mkdirSync(path.dirname(zipAbs), { recursive: true }); - const copyPath = zipAbs + '.tmp'; - fs.writeFileSync(copyPath, Buffer.from(zipBytes)); - if (fs.existsSync(zipAbs)) { - fs.unlinkSync(zipAbs); - } - fs.renameSync(copyPath, zipAbs); - return true; - } catch { - return false; - } -} diff --git a/mcp-ts/src/index.ts b/mcp-ts/src/index.ts index 1e59254..6512441 100644 --- a/mcp-ts/src/index.ts +++ b/mcp-ts/src/index.ts @@ -616,7 +616,7 @@ server.registerTool('setup_ai_files', { title: 'Setup AI Files', description: 'Generate AI assistant instruction files (CLAUDE.md, .github/copilot-instructions.md, ' + - '.cursor/rules, AGENTS.md), Claude skill templates, skill zips, and the generated .bindery/README.md capability reference from .bindery/settings.json. ' + + '.cursor/rules, AGENTS.md), Claude skill templates, and the generated .bindery/README.md capability reference from .bindery/settings.json. ' + 'Run init_workspace first. Safe to run multiple times — skips existing files unless overwrite is true.', inputSchema: { book: bookSchema, diff --git a/mcp-ts/src/tools.ts b/mcp-ts/src/tools.ts index e4a3571..072fa7b 100644 --- a/mcp-ts/src/tools.ts +++ b/mcp-ts/src/tools.ts @@ -518,7 +518,7 @@ export function toolHealth(root: string): string { ai_version_outdated: aiVersionsOutdated.length > 0, ai_versions_outdated: aiVersionsOutdated, message: aiVersionsOutdated.length > 0 - ? 'AI instruction files are out of date. Run setup_ai_files, then re-upload any listed skill zip files in Claude Desktop.' + ? 'AI instruction files are out of date. Run setup_ai_files to regenerate them. If you use Claude Desktop skills, re-upload updated SKILL.md files.' : 'AI instruction files are up to date.', }; @@ -2621,10 +2621,7 @@ export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string { } } catch { /* non-fatal */ } - const zipToUpload = [ - ...result.skillZipManifest.created, - ...result.skillZipManifest.rebuilt, - ]; + const skillFilesToReupload = result.regenerated.filter(file => /^\.claude\/skills\/[^/]+\/SKILL\.md$/.test(file)); const response = { regenerated_files: result.regenerated, @@ -2634,12 +2631,15 @@ export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string { rebuilt: result.skillZipManifest.rebuilt, skipped: result.skillZipManifest.skipped, failed: result.skillZipManifest.failed, - reupload_required: zipToUpload, + reupload_required: [], + }, + skill_files: { + reupload_required: skillFilesToReupload, }, ai_versions: result.versionStamp, - message: - 'If you are using Claude Desktop as AI assistant, re-upload these skill zips via Customize -> Skills: ' + - (zipToUpload.length > 0 ? zipToUpload.join(', ') : 'none'), + message: skillFilesToReupload.length > 0 + ? 'AI instruction files were generated. If you use Claude Desktop skills, re-upload these SKILL.md files in Claude Desktop: ' + skillFilesToReupload.join(', ') + : 'AI instruction files were generated. If you use Claude Desktop skills, upload the generated SKILL.md files in Claude Desktop', }; return JSON.stringify(response, null, 2); diff --git a/mcp-ts/test/aisetup.test.ts b/mcp-ts/test/aisetup.test.ts index 68f42b5..2add7ba 100644 --- a/mcp-ts/test/aisetup.test.ts +++ b/mcp-ts/test/aisetup.test.ts @@ -21,8 +21,6 @@ async function loadAiSetup() { return import('../src/aisetup'); } -vi.unmock('fflate'); - afterEach(() => { vi.restoreAllMocks(); vi.resetModules(); @@ -31,8 +29,8 @@ afterEach(() => { } }); -describe('setupAiFiles zip generation', () => { - it('builds the translation-review skill zip when requested', async () => { +describe('setupAiFiles skill generation', () => { + it('generates requested skill markdown files for claude target', async () => { const { setupAiFiles } = await loadAiSetup(); const root = makeRoot(); @@ -49,11 +47,12 @@ describe('setupAiFiles zip generation', () => { overwrite: true, }); - expect(result.skillZipManifest.created).toContain('.claude/skills/translation-review.zip'); expect(result.regenerated).toContain('.claude/skills/translation-review/SKILL.md'); + expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review.zip'))).toBe(false); }); - it('builds skill zips in-process with normalized entry paths', async () => { + it('returns an empty skill zip manifest (zips are no longer generated)', async () => { const { setupAiFiles } = await loadAiSetup(); const root = makeRoot(); @@ -70,45 +69,33 @@ describe('setupAiFiles zip generation', () => { overwrite: true, }); - expect(result.skillZipManifest.created).toContain('.claude/skills/read-aloud.zip'); - expect(result.skillZipManifest.failed).toEqual([]); - - const zipPath = path.join(root, '.claude', 'skills', 'read-aloud.zip'); - expect(fs.existsSync(zipPath)).toBe(true); - - const zipText = fs.readFileSync(zipPath, 'latin1'); - expect(zipText).toContain('read-aloud/SKILL.md'); - expect(zipText).not.toContain('read-aloud\\SKILL.md'); - }); - - it('reports a failed skill zip when archive generation throws', async () => { - vi.doMock('fflate', async () => { - const actual = await vi.importActual('fflate'); - return { - ...actual, - zipSync: () => { - throw new Error('zip failed'); - }, - }; - }); - 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.failed).toContain('.claude/skills/read-aloud.zip'); expect(result.skillZipManifest.created).toEqual([]); - expect(fs.existsSync(path.join(root, '.claude', 'skills', 'read-aloud.zip'))).toBe(false); + 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(); + + 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'); + + write(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'legacy zip bytes'); + + const result = 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'); }); }); diff --git a/mcp-ts/test/tools.test.ts b/mcp-ts/test/tools.test.ts index e3f83a1..b95cfd0 100644 --- a/mcp-ts/test/tools.test.ts +++ b/mcp-ts/test/tools.test.ts @@ -343,6 +343,7 @@ describe('mcp tools', () => { 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 }; }; @@ -353,10 +354,12 @@ describe('mcp tools', () => { 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'); }); - it('builds skill zips with normalized forward-slash entry paths', () => { + it('does not build skill zip artifacts', () => { const root = makeRoot(); write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({ bookTitle: 'Test Book', @@ -364,14 +367,19 @@ describe('mcp tools', () => { languages: [{ code: 'EN', folderName: 'EN' }], }, null, 2) + '\n'); - toolSetupAiFiles(root, { targets: ['claude'], skills: ['read-aloud'], overwrite: true }); + 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(true); - - const zipText = fs.readFileSync(zipPath).toString('latin1'); - expect(zipText).toContain('read-aloud/SKILL.md'); - expect(zipText).not.toContain(String.raw`read-aloud\SKILL.md`); + 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']); }); it('health skips claude files when aiTargets excludes claude', () => { diff --git a/mcpb/README.md b/mcpb/README.md index cc00cd7..dc31f0c 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -1,4 +1,4 @@ -# Bindery MCP — Desktop Extension (.mcpb) +# Bindery MCP — Desktop Packages Book authoring tools for Claude Desktop: chapter navigation, full-text search, translation management, session memory, typography formatting, and version snapshots. @@ -20,7 +20,7 @@ Works with any Markdown book project structured with the Bindery VS Code extensi - **Version snapshots** — git-based save points after writing sessions - **Review diffs** — structured git diff of uncommitted changes -## Manual Installation +## Manual Installation (Claude Desktop) To install manually without using published Claude Connectors @@ -29,6 +29,21 @@ To install manually without using published Claude Connectors 3. Click **Install from file** (or drag-drop the `.mcpb` file) 4. Fill in the **Books** field (see Configuration below) +## Manual Installation (Standalone MCP clients) + +1. Download the `bindery-mcp-server-*.zip` file from the latest release +2. Unzip it to a stable folder +3. In your MCP client, add a new MCP server. + - eg. in ChatGPT Work -> **Settings** -> **Plug-ins** -> **MCP's** tab +4. With: + - Command: `node` + - Args: absolute path to `server/index.js` from the unzipped folder +5. Add environment variables: + - `BINDERY_BOOKS` (required): semicolon-separated `Name=path` pairs + - `BINDERY_OLLAMA_URL` (optional): Ollama endpoint for semantic features + - `BINDERY_ENABLE_SEMANTIC_INDEX` (optional): `true` to enable full semantic index builds + - `BINDERY_DEFAULT_SEARCH_MODE` (optional): `lexical`, `semantic_rerank`, or `full_semantic` + ## Configuration | Setting | Required | Description | @@ -165,7 +180,7 @@ table with issue type, location, and the reference that contradicts it. | `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`, generated `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold | -| `setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, skill zips, and refresh generated `.bindery/README.md` | +| `setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, and refresh generated `.bindery/README.md` | | `index_build` | Build or rebuild the lexical index and, when enabled, the semantic embedding index | | `index_status` | Show lexical and semantic index status, build times, and stale hints | | `get_text` | Read any file by relative path, with optional line range | @@ -230,6 +245,8 @@ mcpb pack This produces `bindery-mcp-.mcpb`. +For standalone MCP clients, release CI also produces `bindery-mcp-server-.zip` containing `server/index.js` and setup instructions. + ## Files - `manifest.json` — Extension metadata, tool declarations, user config schema diff --git a/mcpb/manifest.json b/mcpb/manifest.json index 1398c6b..7ef5821 100644 --- a/mcpb/manifest.json +++ b/mcpb/manifest.json @@ -62,7 +62,7 @@ { "name": "identify_book", "description": "Identify which book matches a working directory. Pass your cwd and the server matches by folder name or .bindery/settings.json." }, { "name": "health", "description": "Check server status: active book, settings, search index, and embedding backend." }, { "name": "init_workspace", "description": "Create or update .bindery/settings.json, translations.json, .bindery/README.md, and the opinionated Arc, Notes, Characters, SESSION, PREFERENCES, memory, and chapter-status scaffold. All arguments optional — smart defaults used if omitted. Detects language folders automatically. Safe to run on an existing workspace." }, - { "name": "setup_ai_files", "description": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, skill zips, and the generated .bindery/README.md capability reference from settings. Run init_workspace first. Skips existing files unless overwrite: true." }, + { "name": "setup_ai_files", "description": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, and the generated .bindery/README.md capability reference from settings. Run init_workspace first. Skips existing files unless overwrite: true." }, { "name": "index_build", "description": "Build or rebuild the lexical search index and, when enabled, the semantic embedding index for a book." }, { "name": "index_status", "description": "Show lexical and semantic index metadata, including stale-status hints." }, { "name": "search", "description": "Search using lexical BM25, semantic reranking, or full semantic mode. Semantic modes fall back to lexical with a warning if Ollama or the semantic index is unavailable." }, diff --git a/mcpb/standalone-mcp-README.md b/mcpb/standalone-mcp-README.md new file mode 100644 index 0000000..3811ea4 --- /dev/null +++ b/mcpb/standalone-mcp-README.md @@ -0,0 +1,38 @@ +# Bindery Standalone MCP Server Package + +This package contains: + +- `server/index.js` (the bundled Bindery MCP server) +- `README.md` (this file) + +## Install in any MCP client + +1. Unzip this archive to a stable folder on your machine. +2. In your MCP client, add a new MCP server. + - eg. in ChatGPT Work -> **Settings** -> **Plug-ins** -> **MCP's** tab +3. Use the following values: + - Command: `node` + - Args: absolute path to `server/index.js` in the folder you unzipped +4. Configure environment variables (at minimum `BINDERY_BOOKS`). + +## Environment variables + +- `BINDERY_BOOKS` (required) + - Semicolon-separated `Name=path` pairs. + - Example: `MyBook=C:\Users\Me\MyBook;MyNovel=D:\Writing\MyNovel` +- `BINDERY_OLLAMA_URL` (optional) + - Ollama endpoint for semantic reranking/full semantic search. + - Example: `http://localhost:11434` +- `BINDERY_ENABLE_SEMANTIC_INDEX` (optional) + - `true` to enable full semantic index builds via `index_build`. + - Default: `false` +- `BINDERY_DEFAULT_SEARCH_MODE` (optional) + - One of `lexical`, `semantic_rerank`, `full_semantic`. + - Default: `lexical` + +## Notes + +- Keep the unzipped folder in place after configuring the MCP server. +- If Node is not on PATH, use the absolute path to the Node executable as Command. +- If Node is not installed at all you can get it from https://nodejs.org/ +- This package is suitable for any MCP client that supports executable command + arguments + environment variables (for example ChatGPT Work or LM Studio). diff --git a/obsidian-plugin/test/ai-setup.test.ts b/obsidian-plugin/test/ai-setup.test.ts index 789649b..0b2d095 100644 --- a/obsidian-plugin/test/ai-setup.test.ts +++ b/obsidian-plugin/test/ai-setup.test.ts @@ -126,7 +126,7 @@ describe('AI Setup', () => { expect(original).not.toBe(updated); }); - it('should create skill zips for requested skills', async () => { + it('should create skill markdown files for requested skills', async () => { const settingsPath = path.join(tempRoot, '.bindery', 'settings.json'); fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); fs.writeFileSync(settingsPath, JSON.stringify({ diff --git a/vscode-ext/README.md b/vscode-ext/README.md index 1177fb3..8a8b9c3 100644 --- a/vscode-ext/README.md +++ b/vscode-ext/README.md @@ -66,7 +66,7 @@ Bindery includes a bundled MCP server that makes your book's chapters, arc files |------|-------------| | `bindery_health` | Check workspace status: settings, search index, and embedding backend | | `bindery_init_workspace` | Create or update `.bindery/settings.json`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold | -| `bindery_setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, etc.), Claude skills, skill zips, and refresh generated `.bindery/README.md` | +| `bindery_setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, etc.), Claude skills, and refresh generated `.bindery/README.md` | | `bindery_settings_update` | Merge a partial patch into `.bindery/settings.json` | | `bindery_index_build` | Build or rebuild the search index (lexical + optional semantic) | | `bindery_index_status` | Show index metadata and stale-status hints | @@ -138,7 +138,7 @@ All commands are available from the Command Palette (`Ctrl+Shift+P`) under the * | Command | Description | |---------|-------------| | `Initialize Workspace` | Create `.bindery/settings.json`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold | -| `Setup AI Assistant Files` | Generate CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md, Claude skills, skill zips, and refresh generated `.bindery/README.md` | +| `Setup AI Assistant Files` | Generate CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md, Claude skills, and refresh generated `.bindery/README.md` | | `Register MCP Server` | Write `.vscode/mcp.json` for Claude / Codex MCP discovery | | `Format Typography` | Apply typography formatting to the active markdown file (`Ctrl+K Ctrl+B`) | | `Format All Markdown in Folder` | Apply typography to all `.md` files in a folder | diff --git a/vscode-ext/package.json b/vscode-ext/package.json index 2615147..e51d26b 100644 --- a/vscode-ext/package.json +++ b/vscode-ext/package.json @@ -1057,7 +1057,7 @@ "displayName": "Bindery: Setup AI Files", "toolReferenceName": "binderySetupAiFiles", "canBeReferencedInPrompt": true, - "modelDescription": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, skill zips, and the generated .bindery/README.md capability reference from .bindery/settings.json. Run init_workspace first. Skips existing files unless overwrite is true.", + "modelDescription": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, and the generated .bindery/README.md capability reference from .bindery/settings.json. Run init_workspace first. Skips existing files unless overwrite is true.", "inputSchema": { "type": "object", "properties": { diff --git a/vscode-ext/src/extension.ts b/vscode-ext/src/extension.ts index 8ddf0e3..aad4852 100644 --- a/vscode-ext/src/extension.ts +++ b/vscode-ext/src/extension.ts @@ -1163,12 +1163,12 @@ async function setupAiCommand(context?: vscode.ExtensionContext) { const parsed = JSON.parse(raw) as { regenerated_files?: string[]; skipped_files?: string[]; - skill_zips?: { reupload_required?: string[] }; + skill_files?: { reupload_required?: string[] }; }; const regenerated = parsed.regenerated_files ?? []; const skipped = parsed.skipped_files ?? []; - const reupload = parsed.skill_zips?.reupload_required ?? []; + const reuploadSkillFiles = parsed.skill_files?.reupload_required ?? []; const summary: string[] = []; if (regenerated.length > 0) { @@ -1179,8 +1179,8 @@ async function setupAiCommand(context?: vscode.ExtensionContext) { } const base = summary.length > 0 ? summary.join(' | ') : 'No files changed.'; - const suffix = reupload.length > 0 - ? ` If you use Claude Desktop: open Customize > Skills and re-upload ${reupload.join(', ')}.` + const suffix = reuploadSkillFiles.length > 0 + ? ` If you use Claude Desktop skills, re-upload these SKILL.md files: ${reuploadSkillFiles.join(', ')}.` : ''; vscode.window.showInformationMessage(base + suffix); } catch {