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:
Erik van der Boom 2026-05-06 12:08:58 +02:00
parent 2081fe438e
commit 87906e8e42
12 changed files with 3002 additions and 108 deletions

View file

@ -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: {

View file

@ -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); }
});

View file

@ -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;

View file

@ -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,
};

View file

@ -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'
)
: [];

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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();
}
);
}

View file

@ -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));
}
}