feat: add automatic word count and link validation to write operations

Add automatic word count and link validation to create_note, update_note,
and update_sections operations to provide immediate feedback on note content
quality and link integrity.

Features:
- Word counting excludes frontmatter and Obsidian comments, includes all
  other content (code blocks, inline code, headings, lists, etc.)
- Link validation checks wikilinks, heading links, and embeds
- Results categorized as: valid links, broken notes (note doesn't exist),
  and broken headings (note exists but heading missing)
- Detailed broken link info includes line number and context snippet
- Human-readable summary (e.g., "15 links: 12 valid, 2 broken notes, 1 broken heading")
- Opt-out capability via validateLinks parameter (default: true) for
  performance-critical operations

Implementation:
- New ContentUtils.countWords() for word counting logic
- Enhanced LinkUtils.validateLinks() for comprehensive link validation
- Updated create_note, update_note, update_sections to return wordCount
  and linkValidation fields
- Updated MCP tool descriptions to document new features and parameters
- update_note now returns structured JSON instead of simple success message

Response format changes:
- create_note: added wordCount and linkValidation fields
- update_note: changed to structured response with wordCount and linkValidation
- update_sections: added wordCount and linkValidation fields

Breaking changes:
- update_note response format changed from simple message to structured JSON
This commit is contained in:
2025-10-30 09:40:57 -04:00
parent c574a237ce
commit f0808c0346
10 changed files with 679 additions and 21 deletions

View File

@@ -16,12 +16,15 @@ import { ErrorMessages } from '../utils/error-messages';
import { FrontmatterUtils } from '../utils/frontmatter-utils';
import { WaypointUtils } from '../utils/waypoint-utils';
import { VersionUtils } from '../utils/version-utils';
import { IVaultAdapter, IFileManagerAdapter } from '../adapters/interfaces';
import { ContentUtils } from '../utils/content-utils';
import { LinkUtils } from '../utils/link-utils';
import { IVaultAdapter, IFileManagerAdapter, IMetadataCacheAdapter } from '../adapters/interfaces';
export class NoteTools {
constructor(
private vault: IVaultAdapter,
private fileManager: IFileManagerAdapter,
private metadata: IMetadataCacheAdapter,
private app: App // Keep temporarily for methods not yet migrated
) {}
@@ -119,10 +122,11 @@ export class NoteTools {
}
async createNote(
path: string,
content: string,
path: string,
content: string,
createParents: boolean = false,
onConflict: ConflictStrategy = 'error'
onConflict: ConflictStrategy = 'error',
validateLinks: boolean = true
): Promise<CallToolResult> {
// Validate path
if (!path || path.trim() === '') {
@@ -213,7 +217,7 @@ export class NoteTools {
// Proceed with file creation
try {
const file = await this.vault.create(finalPath, content);
const result: CreateNoteResult = {
success: true,
path: file.path,
@@ -223,6 +227,19 @@ export class NoteTools {
originalPath: originalPath
};
// Add word count
result.wordCount = ContentUtils.countWords(content);
// Add link validation if requested
if (validateLinks) {
result.linkValidation = await LinkUtils.validateLinks(
this.vault,
this.metadata,
content,
file.path
);
}
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
@@ -271,7 +288,7 @@ export class NoteTools {
}
}
async updateNote(path: string, content: string): Promise<CallToolResult> {
async updateNote(path: string, content: string, validateLinks: boolean = true): Promise<CallToolResult> {
// Validate path
if (!path || path.trim() === '') {
return {
@@ -329,8 +346,30 @@ export class NoteTools {
}
await this.vault.modify(file, content);
// Build response with word count and link validation
const result: any = {
success: true,
path: file.path,
versionId: VersionUtils.generateVersionId(file),
modified: file.stat.mtime
};
// Add word count
result.wordCount = ContentUtils.countWords(content);
// Add link validation if requested
if (validateLinks) {
result.linkValidation = await LinkUtils.validateLinks(
this.vault,
this.metadata,
content,
file.path
);
}
return {
content: [{ type: "text", text: `Note updated successfully: ${file.path}` }]
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
} catch (error) {
return {
@@ -813,7 +852,8 @@ export class NoteTools {
async updateSections(
path: string,
edits: SectionEdit[],
ifMatch?: string
ifMatch?: string,
validateLinks: boolean = true
): Promise<CallToolResult> {
// Validate path
if (!path || path.trim() === '') {
@@ -917,6 +957,19 @@ export class NoteTools {
sectionsUpdated: edits.length
};
// Add word count
result.wordCount = ContentUtils.countWords(newContent);
// Add link validation if requested
if (validateLinks) {
result.linkValidation = await LinkUtils.validateLinks(
this.vault,
this.metadata,
newContent,
file.path
);
}
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};