mirror of
https://github.com/evdboom/Bindery.git
synced 2026-07-22 06:49:36 +00:00
Merge pull request #124 from evdboom/chatgptwork-support
Chatgptwork support
This commit is contained in:
commit
6367d9563f
30 changed files with 212 additions and 153 deletions
24
.github/release-notes-template.md
vendored
Normal file
24
.github/release-notes-template.md
vendored
Normal file
|
|
@ -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.
|
||||
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
|
|
@ -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 ───────────────
|
||||
|
|
|
|||
20
README.md
20
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-<version>.mcpb` for Claude Desktop / Cowork
|
||||
- `bindery-mcp-server-<version>.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.
|
||||
|
|
|
|||
|
|
@ -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}/\`.
|
||||
|
|
|
|||
|
|
@ -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 |',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<typeof import('fflate')>('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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, { version: number; label: string; zip: string | null }> };
|
||||
};
|
||||
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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-<version>.mcpb`.
|
||||
|
||||
For standalone MCP clients, release CI also produces `bindery-mcp-server-<version>.zip` containing `server/index.js` and setup instructions.
|
||||
|
||||
## Files
|
||||
|
||||
- `manifest.json` — Extension metadata, tool declarations, user config schema
|
||||
|
|
|
|||
|
|
@ -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." },
|
||||
|
|
|
|||
38
mcpb/standalone-mcp-README.md
Normal file
38
mcpb/standalone-mcp-README.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue