mirror of
https://github.com/evdboom/Bindery.git
synced 2026-07-22 06:49:36 +00:00
feat: update package.json to set module type and add eslint-plugin-obsidianmd as devDependency
fix: refactor lineBoundaryWrap function and improve startReviewMarkerCommand logic in extension.ts refactor: simplify invoke functions in mcp.ts by removing async where unnecessary
This commit is contained in:
parent
2081fe438e
commit
87906e8e42
12 changed files with 3002 additions and 108 deletions
|
|
@ -1,11 +1,33 @@
|
|||
import js from '@eslint/js';
|
||||
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import obsidianmd from 'eslint-plugin-obsidianmd';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Scope the full obsidianmd recommended config to the Obsidian plugin only.
|
||||
// We collect rules and plugins from TS-targeting entries but strip languageOptions
|
||||
// so our main block's parserOptions.project (with all workspace tsconfigs) stays in effect.
|
||||
const targetsTs = (files) => {
|
||||
if (!files) return true;
|
||||
const flat = files.flat();
|
||||
return flat.some((f) => typeof f === 'string' && (f.includes('.ts') || f.endsWith('.ts')));
|
||||
};
|
||||
const obsidianMergedRules = Object.assign(
|
||||
{},
|
||||
...obsidianmd.configs.recommended
|
||||
.filter((entry) => targetsTs(entry.files))
|
||||
.map((entry) => entry.rules ?? {}),
|
||||
);
|
||||
const obsidianMergedPlugins = Object.assign(
|
||||
{},
|
||||
...obsidianmd.configs.recommended
|
||||
.filter((entry) => targetsTs(entry.files))
|
||||
.map((entry) => entry.plugins ?? {}),
|
||||
);
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/.vscode/**', '**/coverage/**'],
|
||||
|
|
@ -60,6 +82,35 @@ export default [
|
|||
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||
},
|
||||
},
|
||||
// Obsidian plugin: full recommended rules + generic quality rules
|
||||
{
|
||||
files: ['obsidian-plugin/src/**/*.ts'],
|
||||
plugins: obsidianMergedPlugins,
|
||||
rules: {
|
||||
...obsidianMergedRules,
|
||||
// TypeScript handles undefined identifiers; no-undef causes false positives on imported types
|
||||
'no-undef': 'off',
|
||||
'no-alert': 'error',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/unbound-method': 'error',
|
||||
},
|
||||
},
|
||||
// Other packages: generic rules only (no Obsidian-specific rules)
|
||||
{
|
||||
files: [
|
||||
'mcp-ts/src/**/*.ts',
|
||||
'vscode-ext/src/**/*.ts',
|
||||
'bindery-core/src/**/*.ts',
|
||||
'bindery-merge/src/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'no-alert': 'error',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/unbound-method': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/test/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
|
||||
languageOptions: {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ server.registerTool('list_books', {
|
|||
description: 'List all books registered via --book args in the MCP server config. Call this first to discover available book names.',
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async () => {
|
||||
}, () => {
|
||||
const books = listBooks();
|
||||
if (books.length === 0) {
|
||||
return ok(
|
||||
|
|
@ -92,7 +92,7 @@ server.registerTool('identify_book', {
|
|||
),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ workingDirectory }) => {
|
||||
}, ({ workingDirectory }) => {
|
||||
try {
|
||||
const match = findBookByPath(workingDirectory);
|
||||
if (!match) {
|
||||
|
|
@ -113,7 +113,7 @@ server.registerTool('health', {
|
|||
description: 'Check server status: active book, settings, index, and embedding backend.',
|
||||
inputSchema: { book: bookSchema },
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book }) => {
|
||||
}, ({ book }) => {
|
||||
try { return ok(toolHealth(resolveBook(book).root)); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ server.registerTool('index_status', {
|
|||
description: 'Show current index metadata: chunk count and build time.',
|
||||
inputSchema: { book: bookSchema },
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book }) => {
|
||||
}, ({ book }) => {
|
||||
try { return ok(toolIndexStatus(resolveBook(book).root)); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ server.registerTool('get_text', {
|
|||
endLine: z.number().optional().describe('1-based end line inclusive (optional)'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, identifier, startLine, endLine }) => {
|
||||
}, ({ book, identifier, startLine, endLine }) => {
|
||||
try { return ok(toolGetText(resolveBook(book).root, { identifier, startLine, endLine })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ server.registerTool('get_chapter', {
|
|||
language: z.string().describe('Language code, e.g. EN or NL'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, chapterNumber, language }) => {
|
||||
}, ({ book, chapterNumber, language }) => {
|
||||
try { return ok(toolGetChapter(resolveBook(book).root, { chapterNumber, language })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ server.registerTool('get_book_until', {
|
|||
startChapter: z.number().optional().describe('Starting chapter number (default 1)'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, chapterNumber, language, startChapter }) => {
|
||||
}, ({ book, chapterNumber, language, startChapter }) => {
|
||||
try { return ok(toolGetBookUntil(resolveBook(book).root, { chapterNumber, language, startChapter })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ server.registerTool('get_overview', {
|
|||
act: z.number().optional().describe('Filter to act number 1, 2, or 3 (optional)'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, language, act }) => {
|
||||
}, ({ book, language, act }) => {
|
||||
try { return ok(toolGetOverview(resolveBook(book).root, { language, act })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -198,7 +198,7 @@ server.registerTool('get_notes', {
|
|||
name: z.string().optional().describe('Filter sections containing this name'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, category, name }) => {
|
||||
}, ({ book, category, name }) => {
|
||||
try { return ok(toolGetNotes(resolveBook(book).root, { category, name })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ server.registerTool('format', {
|
|||
noRecurse: z.boolean().optional().describe('Do not recurse into subdirectories (default false)'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, filePath, dryRun, noRecurse }) => {
|
||||
}, ({ book, filePath, dryRun, noRecurse }) => {
|
||||
try { return ok(toolFormat(resolveBook(book).root, { filePath, dryRun, noRecurse })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ server.registerTool('get_review_text', {
|
|||
autoStage: z.boolean().optional().describe('Stage reviewed files and consume review markers after producing the diff (default false)'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, language, contextLines, autoStage }) => {
|
||||
}, ({ book, language, contextLines, autoStage }) => {
|
||||
try { return ok(toolGetReviewText(resolveBook(book).root, { language, contextLines, autoStage })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ server.registerTool('update_workspace', {
|
|||
autoStash: z.boolean().optional().describe('Temporarily stash local changes before pull when needed (default true)'),
|
||||
},
|
||||
annotations: { destructiveHint: true, openWorldHint: true },
|
||||
}, async ({ book, remote, branch, switchBranch, autoStash }) => {
|
||||
}, ({ book, remote, branch, switchBranch, autoStash }) => {
|
||||
try { return ok(toolUpdateWorkspace(resolveBook(book).root, { remote, branch, switchBranch, autoStash })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -282,7 +282,7 @@ server.registerTool('git_snapshot', {
|
|||
rememberPushDefaults: z.boolean().optional().describe('Persist the push preference, remote, and branch under .bindery/settings.json'),
|
||||
},
|
||||
annotations: { destructiveHint: true, openWorldHint: true },
|
||||
}, async ({ book, message, push, remote, branch, rememberPushDefaults }) => {
|
||||
}, ({ book, message, push, remote, branch, rememberPushDefaults }) => {
|
||||
try { return ok(toolGitSnapshot(resolveBook(book).root, { message, push, remote, branch, rememberPushDefaults })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -300,7 +300,7 @@ server.registerTool('get_translation', {
|
|||
type: z.enum(['glossary', 'substitution']).optional().describe('Entry type filter — defaults to "glossary"'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, language, word, type }) => {
|
||||
}, ({ book, language, word, type }) => {
|
||||
try { return ok(toolGetTranslation(resolveBook(book).root, { language, word, type })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ server.registerTool('add_translation', {
|
|||
to: z.string().describe('Target term (e.g. "Kern")'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, targetLangCode, from, to }) => {
|
||||
}, ({ book, targetLangCode, from, to }) => {
|
||||
try { return ok(toolAddTranslation(resolveBook(book).root, { targetLangCode, from, to })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -334,7 +334,7 @@ server.registerTool('add_dialect', {
|
|||
to: z.string().describe('Target word (e.g. "colour")'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, dialectCode, from, to }) => {
|
||||
}, ({ book, dialectCode, from, to }) => {
|
||||
try { return ok(toolAddDialect(resolveBook(book).root, { dialectCode, from, to })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ server.registerTool('get_dialect', {
|
|||
word: z.string().optional().describe('Word to look up (optional — omit to list all rules)'),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book, dialectCode, word }) => {
|
||||
}, ({ book, dialectCode, word }) => {
|
||||
try { return ok(toolGetDialect(resolveBook(book).root, { dialectCode, word })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ server.registerTool('add_language', {
|
|||
createStubs: z.boolean().optional().describe('Mirror source language folder with stub files (default true)'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, code, folderName, chapterWord, actPrefix, prologueLabel, epilogueLabel, createStubs }) => {
|
||||
}, ({ book, code, folderName, chapterWord, actPrefix, prologueLabel, epilogueLabel, createStubs }) => {
|
||||
try { return ok(toolAddLanguage(resolveBook(book).root, { code, folderName, chapterWord, actPrefix, prologueLabel, epilogueLabel, createStubs })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -392,7 +392,7 @@ server.registerTool('init_workspace', {
|
|||
targetAudience: z.string().optional().describe('Target audience, e.g. 12+ or adults'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, bookTitle, author, storyFolder, genre, description, targetAudience }) => {
|
||||
}, ({ book, bookTitle, author, storyFolder, genre, description, targetAudience }) => {
|
||||
try { return ok(toolInitWorkspace(resolveBook(book).root, { bookTitle, author, storyFolder, genre, description, targetAudience })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -406,7 +406,7 @@ server.registerTool('settings_update', {
|
|||
patch: z.record(z.string(), z.any()).describe('Partial settings object to deep-merge into .bindery/settings.json'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, patch }) => {
|
||||
}, ({ book, patch }) => {
|
||||
try { return ok(toolSettingsUpdate(resolveBook(book).root, { patch })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -423,7 +423,7 @@ server.registerTool('setup_ai_files', {
|
|||
overwrite: z.boolean().optional().describe('Overwrite existing files? Default false (skip existing).'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, targets, skills, overwrite }) => {
|
||||
}, ({ book, targets, skills, overwrite }) => {
|
||||
try { return ok(toolSetupAiFiles(resolveBook(book).root, { targets, skills, overwrite })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -432,7 +432,7 @@ server.registerTool('memory_list', {
|
|||
description: 'List all session memory files in .bindery/memories/. Returns each filename and its line count.',
|
||||
inputSchema: { book: bookSchema },
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book }) => {
|
||||
}, ({ book }) => {
|
||||
try { return ok(toolMemoryList(resolveBook(book).root)); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -449,7 +449,7 @@ server.registerTool('memory_append', {
|
|||
content: z.string().describe('Text to record under this session entry'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, file, title, content }) => {
|
||||
}, ({ book, file, title, content }) => {
|
||||
try { return ok(toolMemoryAppend(resolveBook(book).root, { file, title, content })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -465,7 +465,7 @@ server.registerTool('memory_compact', {
|
|||
compacted_content: z.string().describe('Full replacement content (model-supplied summary)'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, file, compacted_content }) => {
|
||||
}, ({ book, file, compacted_content }) => {
|
||||
try { return ok(toolMemoryCompact(resolveBook(book).root, { file, compacted_content })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -477,7 +477,7 @@ server.registerTool('chapter_status_get', {
|
|||
'Returns a clear empty-state message if no status has been recorded yet.',
|
||||
inputSchema: { book: bookSchema },
|
||||
annotations: { readOnlyHint: true },
|
||||
}, async ({ book }) => {
|
||||
}, ({ book }) => {
|
||||
try { return ok(toolChapterStatusGet(resolveBook(book).root)); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
@ -501,7 +501,7 @@ server.registerTool('chapter_status_update', {
|
|||
})).describe('Chapter entries to upsert (existing entries not listed are preserved)'),
|
||||
},
|
||||
annotations: { destructiveHint: true },
|
||||
}, async ({ book, chapters }) => {
|
||||
}, ({ book, chapters }) => {
|
||||
try { return ok(toolChapterStatusUpdate(resolveBook(book).root, { chapters })); } catch (e) { return err(e); }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -520,7 +520,7 @@ async function fetchEmbedding(
|
|||
} catch {
|
||||
if (attempt === maxRetries) { return null; }
|
||||
// Short exponential backoff: 200ms, 400ms, 800ms, ...
|
||||
await new Promise(r => setTimeout(r, 200 * 2 ** attempt));
|
||||
await new Promise(r => activeWindow.setTimeout(r, 200 * 2 ** attempt));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1603,7 +1603,7 @@ function detectWorkspaceLangs(
|
|||
: [{ code: 'EN', folderName: 'EN', chapterWord: 'Chapter', actPrefix: 'Act', prologueLabel: 'Prologue', epilogueLabel: 'Epilogue' }];
|
||||
return base.map(dl => {
|
||||
const el = existingLangs.find(l => (l['code'] as string | undefined)?.toUpperCase() === dl.code);
|
||||
return el ? { ...el, code: dl.code, folderName: dl.folderName } : (dl as unknown as Record<string, unknown>);
|
||||
return el ? { ...el, code: dl.code, folderName: dl.folderName } : (dl);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1650,9 +1650,9 @@ export function toolInitWorkspace(root: string, args: InitWorkspaceArgs): string
|
|||
...(args.description ? { description: args.description } : {}),
|
||||
...(args.targetAudience ? { targetAudience: args.targetAudience }: {}),
|
||||
storyFolder: storyFolderName,
|
||||
mergedOutputDir: (existing['mergedOutputDir'] as string | undefined) ?? 'Merged',
|
||||
mergeFilePrefix: (existing['mergeFilePrefix'] as string | undefined) ?? slug,
|
||||
formatOnSave: (existing['formatOnSave'] as boolean | undefined) ?? false,
|
||||
mergedOutputDir: (existing['mergedOutputDir']) ?? 'Merged',
|
||||
mergeFilePrefix: (existing['mergeFilePrefix']) ?? slug,
|
||||
formatOnSave: (existing['formatOnSave']) ?? false,
|
||||
languages,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export interface AiSetupResult {
|
|||
/**
|
||||
* Generate AI instruction files from vault settings.
|
||||
*/
|
||||
export async function setupAiFiles(
|
||||
export function setupAiFiles(
|
||||
_app: App,
|
||||
bookRoot: string,
|
||||
targets: AiTarget[] = ['claude', 'copilot'],
|
||||
|
|
@ -79,7 +79,7 @@ export async function setupAiFiles(
|
|||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -96,10 +96,12 @@ function buildContext(settings: Record<string, unknown>): TemplateContext {
|
|||
const arcFolder = 'Arc';
|
||||
const memoriesFolder = '.bindery/memories';
|
||||
|
||||
const rawLanguages = settings.languages as any[] | undefined;
|
||||
const rawLanguages = settings.languages;
|
||||
const languages: Array<{ code: string; folderName: string }> = Array.isArray(rawLanguages)
|
||||
? rawLanguages.filter(
|
||||
(l: any) => typeof l?.code === 'string' && typeof l?.folderName === 'string'
|
||||
? (rawLanguages as unknown[]).filter(
|
||||
(l): l is { code: string; folderName: string } =>
|
||||
typeof (l as Record<string, unknown>)?.code === 'string' &&
|
||||
typeof (l as Record<string, unknown>)?.folderName === 'string'
|
||||
)
|
||||
: [];
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { exportBook, 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';
|
||||
import { Notice, Plugin, TFile } from 'obsidian';
|
||||
import { App, Modal, Notice, Plugin, TFile } from 'obsidian';
|
||||
import type { Editor } from 'obsidian';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
|
@ -30,6 +30,60 @@ function isTFile(obj: unknown): obj is TFile {
|
|||
return typeof obj === 'object' && obj !== null && 'extension' in obj && 'path' in obj;
|
||||
}
|
||||
|
||||
class TextPromptModal extends Modal {
|
||||
private readonly titleText: string;
|
||||
private readonly defaultValue: string;
|
||||
private readonly onResolve: (_value: string | null) => void;
|
||||
private inputEl: HTMLInputElement | null = null;
|
||||
private resolved = false;
|
||||
|
||||
constructor(app: App, titleText: string, defaultValue: string, onResolve: (_value: string | null) => void) {
|
||||
super(app);
|
||||
this.titleText = titleText;
|
||||
this.defaultValue = defaultValue;
|
||||
this.onResolve = onResolve;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText(this.titleText);
|
||||
this.contentEl.empty();
|
||||
|
||||
const input = this.contentEl.createEl('input', { type: 'text', value: this.defaultValue });
|
||||
this.inputEl = input;
|
||||
input.addClass('mod-text-input');
|
||||
input.focus();
|
||||
input.select();
|
||||
input.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.resolveAndClose(input.value);
|
||||
}
|
||||
});
|
||||
|
||||
const buttons = this.contentEl.createDiv({ cls: 'bindery-prompt-buttons' });
|
||||
const submitButton = buttons.createEl('button', { text: 'OK', cls: 'mod-cta' });
|
||||
submitButton.addEventListener('click', () => this.resolveAndClose(this.inputEl?.value ?? ''));
|
||||
|
||||
const cancelButton = buttons.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addEventListener('click', () => this.resolveAndClose(null));
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
if (!this.resolved) {
|
||||
this.onResolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveAndClose(value: string | null): void {
|
||||
if (this.resolved) {
|
||||
return;
|
||||
}
|
||||
this.resolved = true;
|
||||
this.onResolve(value);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
export default class BinderyPlugin extends Plugin {
|
||||
settings: BinderySettings = { ...DEFAULT_SETTINGS };
|
||||
|
||||
|
|
@ -51,27 +105,27 @@ export default class BinderyPlugin extends Plugin {
|
|||
m.addSeparator?.();
|
||||
|
||||
m.addItem((item) => {
|
||||
item.setTitle('Bindery: Merge chapters to all formats');
|
||||
item.setTitle('Merge chapters to all formats');
|
||||
item.setIcon?.('book-open');
|
||||
item.onClick(() => { void this.mergeBook(['md', 'docx', 'epub', 'pdf']); });
|
||||
});
|
||||
|
||||
if (forEditor) {
|
||||
m.addItem((item) => {
|
||||
item.setTitle('Bindery: Format document');
|
||||
item.setTitle('Format document');
|
||||
item.setIcon?.('wand');
|
||||
item.onClick(() => { void this.formatActive(); });
|
||||
});
|
||||
}
|
||||
|
||||
m.addItem((item) => {
|
||||
item.setTitle('Bindery: Find probable US to UK words');
|
||||
item.setTitle('Find probable us to uk words');
|
||||
item.setIcon?.('languages');
|
||||
item.onClick(() => { void this.findUsToUkCommand(); });
|
||||
});
|
||||
|
||||
m.addItem((item) => {
|
||||
item.setTitle('Bindery: Generate AI assistant files');
|
||||
item.setTitle('Generate AI assistant files');
|
||||
item.setIcon?.('sparkles');
|
||||
item.onClick(() => { void this.setupAiCommand(); });
|
||||
});
|
||||
|
|
@ -117,19 +171,19 @@ export default class BinderyPlugin extends Plugin {
|
|||
// Command: format active document
|
||||
this.addCommand({
|
||||
id: 'format-document',
|
||||
name: 'Bindery: Format document',
|
||||
name: 'Format document',
|
||||
callback: () => void this.formatActive(),
|
||||
});
|
||||
|
||||
// Review marker commands
|
||||
this.addCommand({
|
||||
id: 'start-review-marker',
|
||||
name: 'Bindery: Insert Review Start Marker (or wrap selection)',
|
||||
name: 'Insert review start marker (or wrap selection)',
|
||||
editorCallback: (editor) => this.insertStartReviewMarker(editor),
|
||||
});
|
||||
this.addCommand({
|
||||
id: 'stop-review-marker',
|
||||
name: 'Bindery: Insert Review Stop Marker',
|
||||
name: 'Insert review stop marker',
|
||||
editorCallback: (editor) => this.insertStopReviewMarker(editor),
|
||||
});
|
||||
|
||||
|
|
@ -138,7 +192,7 @@ export default class BinderyPlugin extends Plugin {
|
|||
const label = fmt.toUpperCase();
|
||||
this.addCommand({
|
||||
id: `export-${fmt}`,
|
||||
name: `Bindery: Export → ${label}`,
|
||||
name: `Export → ${label}`,
|
||||
callback: () => void exportBook(this.app, this.settings, fmt),
|
||||
});
|
||||
}
|
||||
|
|
@ -149,76 +203,75 @@ export default class BinderyPlugin extends Plugin {
|
|||
const label = fmt === 'all' ? 'All Formats' : fmt.toUpperCase();
|
||||
this.addCommand({
|
||||
id: `merge-${fmt}`,
|
||||
name: `Bindery: Merge Chapters → ${label}`,
|
||||
callback: () => void this.mergeBook(outputTypes as any),
|
||||
name: `Merge chapters → ${label}`,
|
||||
callback: () => void this.mergeBook(outputTypes as OutputType[]),
|
||||
});
|
||||
}
|
||||
|
||||
// Init workspace
|
||||
this.addCommand({
|
||||
id: 'init-workspace',
|
||||
name: 'Bindery: Initialize workspace',
|
||||
callback: () => void this.initWorkspace(),
|
||||
name: 'Initialize workspace',
|
||||
callback: () => this.initWorkspace(),
|
||||
});
|
||||
|
||||
// AI setup command
|
||||
this.addCommand({
|
||||
id: 'setup-ai-files',
|
||||
name: 'Bindery: Generate AI Assistant Files',
|
||||
name: 'Generate AI assistant files',
|
||||
callback: () => void this.setupAiCommand(),
|
||||
});
|
||||
|
||||
// Workspace management commands
|
||||
this.addCommand({
|
||||
id: 'add-dialect',
|
||||
name: 'Bindery: Add Dialect Rule',
|
||||
name: 'Add dialect rule',
|
||||
callback: () => void this.addDialectCommand(),
|
||||
});
|
||||
this.addCommand({
|
||||
id: 'add-translation',
|
||||
name: 'Bindery: Add Translation Glossary Entry',
|
||||
name: 'Add translation glossary entry',
|
||||
callback: () => void this.addTranslationCommand(),
|
||||
});
|
||||
this.addCommand({
|
||||
id: 'add-language',
|
||||
name: 'Bindery: Add Language',
|
||||
name: 'Add language',
|
||||
callback: () => void this.addLanguageCommand(),
|
||||
});
|
||||
this.addCommand({
|
||||
id: 'open-translations',
|
||||
name: 'Bindery: Open Translations File',
|
||||
callback: () => void this.openTranslationsCommand(),
|
||||
name: 'Open translations file',
|
||||
callback: () => this.openTranslationsCommand(),
|
||||
});
|
||||
this.addCommand({
|
||||
id: 'find-us-to-uk-words',
|
||||
name: 'Bindery: Find Probable US → UK Words',
|
||||
name: 'Find probable us to uk words',
|
||||
callback: () => void this.findUsToUkCommand(),
|
||||
});
|
||||
|
||||
// Show MCP config snippet — copies JSON to clipboard
|
||||
this.addCommand({
|
||||
id: 'show-mcp-config',
|
||||
name: 'Bindery: Show MCP config snippet',
|
||||
name: 'Show mcp config snippet',
|
||||
callback: () => void this.copyMcpSnippet(),
|
||||
});
|
||||
|
||||
this.addRibbonIcon('book-open', 'Bindery: Merge chapters to all formats', () => {
|
||||
this.addRibbonIcon('book-open', 'Merge chapters to all formats', () => {
|
||||
void this.mergeBook(['md', 'docx', 'epub', 'pdf']);
|
||||
});
|
||||
this.addRibbonIcon('wand', 'Bindery: Format active note', () => {
|
||||
this.addRibbonIcon('wand', 'Format active note', () => {
|
||||
void this.formatActive();
|
||||
});
|
||||
this.addRibbonIcon('languages', 'Bindery: Find probable US to UK words', () => {
|
||||
this.addRibbonIcon('languages', 'Find probable us to uk words', () => {
|
||||
void this.findUsToUkCommand();
|
||||
});
|
||||
|
||||
// Right-click menus in editor and file explorer.
|
||||
const workspaceOn = this.app.workspace?.on;
|
||||
if (workspaceOn) {
|
||||
this.registerEvent(workspaceOn.call(this.app.workspace, 'editor-menu', (menu: unknown) => {
|
||||
if (this.app.workspace) {
|
||||
this.registerEvent(this.app.workspace.on('editor-menu', (menu: unknown) => {
|
||||
this.addContextMenuItems(menu, true);
|
||||
}));
|
||||
this.registerEvent(workspaceOn.call(this.app.workspace, 'file-menu', (menu: unknown) => {
|
||||
this.registerEvent(this.app.workspace.on('file-menu', (menu: unknown) => {
|
||||
this.addContextMenuItems(menu, false);
|
||||
}));
|
||||
}
|
||||
|
|
@ -402,10 +455,9 @@ export default class BinderyPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
private async promptString(prompt: string, defaultValue: string = ''): Promise<string | null> {
|
||||
private async promptString(promptText: string, defaultValue: string = ''): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const result = globalThis.prompt(prompt, defaultValue);
|
||||
resolve(result);
|
||||
new TextPromptModal(this.app, promptText, defaultValue, resolve).open();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -479,7 +531,8 @@ export default class BinderyPlugin extends Plugin {
|
|||
|
||||
private async copyMcpSnippet(): Promise<void> {
|
||||
const snippet = this.showMcpSnippet();
|
||||
const clipboard = globalThis.navigator?.clipboard;
|
||||
const activeWindow = this.app.workspace?.containerEl.ownerDocument.defaultView;
|
||||
const clipboard = activeWindow?.navigator?.clipboard;
|
||||
if (clipboard?.writeText) {
|
||||
await clipboard.writeText(snippet);
|
||||
return;
|
||||
|
|
|
|||
28
obsidian-plugin/src/obsidian.d.ts
vendored
28
obsidian-plugin/src/obsidian.d.ts
vendored
|
|
@ -34,11 +34,23 @@ declare module 'obsidian' {
|
|||
export interface App {
|
||||
vault: Vault;
|
||||
workspace?: {
|
||||
containerEl: HTMLElement;
|
||||
getActiveFile(): TFile | null;
|
||||
on(_event: string, _callback: (..._args: unknown[]) => unknown): EventRef;
|
||||
};
|
||||
}
|
||||
|
||||
export class Modal {
|
||||
app: App;
|
||||
titleEl: HTMLElement;
|
||||
contentEl: HTMLElement;
|
||||
constructor(_app: App);
|
||||
open(): void;
|
||||
close(): void;
|
||||
onOpen(): void;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export class Notice {
|
||||
constructor(_message: string, _timeout?: number);
|
||||
}
|
||||
|
|
@ -61,3 +73,19 @@ declare module 'obsidian' {
|
|||
registerEvent(_eventRef: EventRef): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface CreateElementOptions {
|
||||
text?: string;
|
||||
cls?: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface HTMLElement {
|
||||
empty(): void;
|
||||
addClass(..._classes: string[]): void;
|
||||
createEl<K extends keyof HTMLElementTagNameMap>(_tag: K, _options?: CreateElementOptions): HTMLElementTagNameMap[K];
|
||||
createDiv(_options?: CreateElementOptions): HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ import * as path from 'node:path';
|
|||
import {
|
||||
BINDERY_FOLDER,
|
||||
SETTINGS_FILENAME,
|
||||
type WorkspaceSettings,
|
||||
type LanguageConfig,
|
||||
upsertSubstitutionRule,
|
||||
WorkspaceSettings,
|
||||
} from '@bindery/core';
|
||||
|
||||
export type { WorkspaceSettings, LanguageConfig };
|
||||
export type { WorkspaceSettings, LanguageConfig } from '@bindery/core';
|
||||
|
||||
/**
|
||||
* Read workspace settings from .bindery/settings.json
|
||||
|
|
|
|||
2766
package-lock.json
generated
2766
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "bindery-monorepo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"bindery-core",
|
||||
"bindery-merge",
|
||||
|
|
@ -15,5 +16,8 @@
|
|||
"lint:fix": "npx eslint . --fix",
|
||||
"build:core": "npm run build --workspace=bindery-core",
|
||||
"obsidian:sync-version": "node ./scripts/sync-obsidian-version.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-obsidianmd": "^0.2.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,11 +404,9 @@ function isMarkdownEditor(editor: vscode.TextEditor): boolean {
|
|||
* marker is always on its own line and matches the marker scanner regex.
|
||||
*/
|
||||
function lineBoundaryWrap(doc: vscode.TextDocument, pos: vscode.Position, marker: string): string {
|
||||
const lineText = doc.lineAt(pos.line).text;
|
||||
const atLineStart = pos.character === 0;
|
||||
const atLineEnd = pos.character >= lineText.length;
|
||||
const prefix = atLineStart ? '' : '\n';
|
||||
const suffix = atLineEnd ? '\n' : '\n';
|
||||
const suffix = '\n';
|
||||
return `${prefix}${marker}${suffix}`;
|
||||
}
|
||||
|
||||
|
|
@ -422,16 +420,16 @@ async function startReviewMarkerCommand() {
|
|||
|
||||
const sel = editor.selection;
|
||||
await editor.edit(eb => {
|
||||
if (!sel.isEmpty) {
|
||||
if (sel.isEmpty) {
|
||||
eb.insert(sel.active, lineBoundaryWrap(editor.document, sel.active, REVIEW_START_MARKER));
|
||||
} else {
|
||||
// Wrap selection. Snap markers to line boundaries so they match
|
||||
// the marker scanner regex even if the selection starts/ends
|
||||
// mid-line.
|
||||
const text = editor.document.getText(sel);
|
||||
const startWrap = lineBoundaryWrap(editor.document, sel.start, REVIEW_START_MARKER);
|
||||
const stopWrap = lineBoundaryWrap(editor.document, sel.end, REVIEW_STOP_MARKER);
|
||||
eb.replace(sel, `${startWrap}${text}${stopWrap}`);
|
||||
} else {
|
||||
eb.insert(sel.active, lineBoundaryWrap(editor.document, sel.active, REVIEW_START_MARKER));
|
||||
eb.replace(sel, `${startWrap}${text}${stopWrap}`);
|
||||
}
|
||||
});
|
||||
vscode.window.setStatusBarMessage(
|
||||
|
|
@ -1144,9 +1142,10 @@ async function formatFolderCommand(uri?: vscode.Uri) {
|
|||
}
|
||||
await vscode.window.withProgress(
|
||||
{ location: vscode.ProgressLocation.Notification, title: 'Formatting markdown files…' },
|
||||
async () => {
|
||||
() => {
|
||||
const count = formatDirectoryRecursive(folderPath);
|
||||
vscode.window.showInformationMessage(`Typography: ${count} file(s) updated.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export function registerLmTools(context: vscode.ExtensionContext): void {
|
|||
|
||||
context.subscriptions.push(
|
||||
vscode.lm.registerTool('bindery_health', {
|
||||
invoke: async (_opts, _token) => ok(t.toolHealth(requireRoot())),
|
||||
invoke: (_opts, _token) => ok(t.toolHealth(requireRoot())),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool('bindery_index_build', {
|
||||
|
|
@ -112,27 +112,27 @@ export function registerLmTools(context: vscode.ExtensionContext): void {
|
|||
}),
|
||||
|
||||
vscode.lm.registerTool('bindery_index_status', {
|
||||
invoke: async (_opts, _token) => ok(t.toolIndexStatus(requireRoot())),
|
||||
invoke: (_opts, _token) => ok(t.toolIndexStatus(requireRoot())),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetTextInput>('bindery_get_text', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetText(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetText(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetChapterInput>('bindery_get_chapter', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetChapter(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetChapter(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetBookUntilInput>('bindery_get_book_until', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetBookUntil(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetBookUntil(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetOverviewInput>('bindery_get_overview', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetOverview(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetOverview(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetNotesInput>('bindery_get_notes', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetNotes(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetNotes(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<SearchInput>('bindery_search', {
|
||||
|
|
@ -140,71 +140,71 @@ export function registerLmTools(context: vscode.ExtensionContext): void {
|
|||
}),
|
||||
|
||||
vscode.lm.registerTool<FormatInput>('bindery_format', {
|
||||
invoke: async (opts, _token) => ok(t.toolFormat(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolFormat(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetReviewTextInput>('bindery_get_review_text', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetReviewText(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetReviewText(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<UpdateWorkspaceInput>('bindery_update_workspace', {
|
||||
invoke: async (opts, _token) => ok(t.toolUpdateWorkspace(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolUpdateWorkspace(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GitSnapshotInput>('bindery_git_snapshot', {
|
||||
invoke: async (opts, _token) => ok(t.toolGitSnapshot(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGitSnapshot(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<AddTranslationInput>('bindery_add_translation', {
|
||||
invoke: async (opts, _token) => ok(t.toolAddTranslation(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolAddTranslation(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetTranslationInput>('bindery_get_translation', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetTranslation(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetTranslation(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<AddDialectInput>('bindery_add_dialect', {
|
||||
invoke: async (opts, _token) => ok(t.toolAddDialect(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolAddDialect(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<GetDialectInput>('bindery_get_dialect', {
|
||||
invoke: async (opts, _token) => ok(t.toolGetDialect(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolGetDialect(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<AddLanguageInput>('bindery_add_language', {
|
||||
invoke: async (opts, _token) => ok(t.toolAddLanguage(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolAddLanguage(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<InitWorkspaceInput>('bindery_init_workspace', {
|
||||
invoke: async (opts, _token) => ok(t.toolInitWorkspace(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolInitWorkspace(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<SettingsUpdateInput>('bindery_settings_update', {
|
||||
invoke: async (opts, _token) => ok(t.toolSettingsUpdate(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolSettingsUpdate(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<SetupAiFilesInput>('bindery_setup_ai_files', {
|
||||
invoke: async (opts, _token) => ok(t.toolSetupAiFiles(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolSetupAiFiles(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool('bindery_memory_list', {
|
||||
invoke: async (_opts, _token) => ok(t.toolMemoryList(requireRoot())),
|
||||
invoke: (_opts, _token) => ok(t.toolMemoryList(requireRoot())),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<MemoryAppendInput>('bindery_memory_append', {
|
||||
invoke: async (opts, _token) => ok(t.toolMemoryAppend(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolMemoryAppend(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<MemoryCompactInput>('bindery_memory_compact', {
|
||||
invoke: async (opts, _token) => ok(t.toolMemoryCompact(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolMemoryCompact(requireRoot(), opts.input)),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool('bindery_chapter_status_get', {
|
||||
invoke: async (_opts, _token) => ok(t.toolChapterStatusGet(requireRoot())),
|
||||
invoke: (_opts, _token) => ok(t.toolChapterStatusGet(requireRoot())),
|
||||
}),
|
||||
|
||||
vscode.lm.registerTool<ChapterStatusUpdateInput>('bindery_chapter_status_update', {
|
||||
invoke: async (opts, _token) => ok(t.toolChapterStatusUpdate(requireRoot(), opts.input)),
|
||||
invoke: (opts, _token) => ok(t.toolChapterStatusUpdate(requireRoot(), opts.input)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -274,3 +274,5 @@ export async function registerMcpCommand(context: vscode.ExtensionContext): Prom
|
|||
vscode.window.showTextDocument(await vscode.workspace.openTextDocument(mcpPath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue