feat: Phase 7 - Waypoint Support
- Add get_folder_waypoint tool to extract waypoint blocks from folder notes - Add is_folder_note tool to detect folder notes by basename or waypoint markers - Implement waypoint edit protection in update_note to prevent corruption - Create waypoint-utils.ts with helper functions for waypoint operations - Add FolderWaypointResult and FolderNoteResult types - Update ROADMAP.md and CHANGELOG.md with Phase 7 completion - All manual tests passing
This commit is contained in:
@@ -256,6 +256,34 @@ export class ToolRegistry {
|
||||
},
|
||||
required: ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_folder_waypoint",
|
||||
description: "Get Waypoint block from a folder note. Waypoint blocks (%% Begin Waypoint %% ... %% End Waypoint %%) are auto-generated by the Waypoint plugin to create folder indexes. Returns structured JSON with waypoint presence, line range, extracted wikilinks, and raw content. Use this to inspect folder note navigation structures without parsing the entire file.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Vault-relative path to the folder note (e.g., 'projects/projects.md' or 'daily/daily.md'). Paths are case-sensitive on macOS/Linux. Do not use leading or trailing slashes."
|
||||
}
|
||||
},
|
||||
required: ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "is_folder_note",
|
||||
description: "Check if a note is a folder note. A folder note is identified by either having the same basename as its parent folder OR containing Waypoint markers. Returns structured JSON with boolean result, detection reason (basename_match, waypoint_marker, both, or none), and folder path. Use this to identify navigation/index notes in your vault structure.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Vault-relative path to the note to check (e.g., 'projects/projects.md'). Paths are case-sensitive on macOS/Linux. Do not use leading or trailing slashes."
|
||||
}
|
||||
},
|
||||
required: ["path"]
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -311,6 +339,10 @@ export class ToolRegistry {
|
||||
includeCompressed: args.includeCompressed,
|
||||
includePreview: args.includePreview
|
||||
});
|
||||
case "get_folder_waypoint":
|
||||
return await this.vaultTools.getFolderWaypoint(args.path);
|
||||
case "is_folder_note":
|
||||
return await this.vaultTools.isFolderNote(args.path);
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CallToolResult, ParsedNote, ExcalidrawMetadata } from '../types/mcp-typ
|
||||
import { PathUtils } from '../utils/path-utils';
|
||||
import { ErrorMessages } from '../utils/error-messages';
|
||||
import { FrontmatterUtils } from '../utils/frontmatter-utils';
|
||||
import { WaypointUtils } from '../utils/waypoint-utils';
|
||||
|
||||
export class NoteTools {
|
||||
constructor(private app: App) {}
|
||||
@@ -230,6 +231,28 @@ export class NoteTools {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for waypoint edit protection
|
||||
const currentContent = await this.app.vault.read(file);
|
||||
const waypointCheck = WaypointUtils.wouldAffectWaypoint(currentContent, content);
|
||||
|
||||
if (waypointCheck.affected) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Cannot update note: This would modify a Waypoint block.\n\n` +
|
||||
`Waypoint blocks (%% Begin Waypoint %% ... %% End Waypoint %%) are auto-generated ` +
|
||||
`by the Waypoint plugin and should not be manually edited.\n\n` +
|
||||
`Waypoint location: lines ${waypointCheck.waypointRange?.start}-${waypointCheck.waypointRange?.end}\n\n` +
|
||||
`Troubleshooting tips:\n` +
|
||||
`• Use get_folder_waypoint() to view the current waypoint content\n` +
|
||||
`• Edit content outside the waypoint block\n` +
|
||||
`• Let the Waypoint plugin regenerate the block automatically\n` +
|
||||
`• If you need to force this edit, the waypoint will need to be regenerated`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
await this.app.vault.modify(file, content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note updated successfully: ${file.path}` }]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { App, TFile, TFolder } from 'obsidian';
|
||||
import { CallToolResult, FileMetadata, DirectoryMetadata, VaultInfo, SearchResult, SearchMatch, StatResult, ExistsResult, ListResult, FileMetadataWithFrontmatter, FrontmatterSummary, WaypointSearchResult } from '../types/mcp-types';
|
||||
import { CallToolResult, FileMetadata, DirectoryMetadata, VaultInfo, SearchResult, SearchMatch, StatResult, ExistsResult, ListResult, FileMetadataWithFrontmatter, FrontmatterSummary, WaypointSearchResult, FolderWaypointResult, FolderNoteResult } from '../types/mcp-types';
|
||||
import { PathUtils } from '../utils/path-utils';
|
||||
import { ErrorMessages } from '../utils/error-messages';
|
||||
import { GlobUtils } from '../utils/glob-utils';
|
||||
import { SearchUtils } from '../utils/search-utils';
|
||||
import { WaypointUtils } from '../utils/waypoint-utils';
|
||||
|
||||
export class VaultTools {
|
||||
constructor(private app: App) {}
|
||||
@@ -596,4 +597,96 @@ export class VaultTools {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getFolderWaypoint(path: string): Promise<CallToolResult> {
|
||||
try {
|
||||
// Normalize and validate path
|
||||
const normalizedPath = PathUtils.normalizePath(path);
|
||||
|
||||
// Resolve file
|
||||
const file = PathUtils.resolveFile(this.app, normalizedPath);
|
||||
if (!file) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: ErrorMessages.fileNotFound(normalizedPath)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
// Extract waypoint block
|
||||
const waypointBlock = WaypointUtils.extractWaypointBlock(content);
|
||||
|
||||
const result: FolderWaypointResult = {
|
||||
path: file.path,
|
||||
hasWaypoint: waypointBlock.hasWaypoint,
|
||||
waypointRange: waypointBlock.waypointRange,
|
||||
links: waypointBlock.links,
|
||||
rawContent: waypointBlock.rawContent
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Get folder waypoint error: ${(error as Error).message}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async isFolderNote(path: string): Promise<CallToolResult> {
|
||||
try {
|
||||
// Normalize and validate path
|
||||
const normalizedPath = PathUtils.normalizePath(path);
|
||||
|
||||
// Resolve file
|
||||
const file = PathUtils.resolveFile(this.app, normalizedPath);
|
||||
if (!file) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: ErrorMessages.fileNotFound(normalizedPath)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a folder note
|
||||
const folderNoteInfo = await WaypointUtils.isFolderNote(this.app, file);
|
||||
|
||||
const result: FolderNoteResult = {
|
||||
path: file.path,
|
||||
isFolderNote: folderNoteInfo.isFolderNote,
|
||||
reason: folderNoteInfo.reason,
|
||||
folderPath: folderNoteInfo.folderPath
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Is folder note error: ${(error as Error).message}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user