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:
2025-10-17 00:16:14 -04:00
parent e6cdd6d90a
commit 4e399e00f8
7 changed files with 489 additions and 26 deletions

View File

@@ -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}` }],

View File

@@ -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}` }]

View File

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