mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 05:47:26 +02:00
feat(llm): add note mutation tools (rename, delete, move, clone)
Add four new LLM tools for note management: - rename_note: Change the title of an existing note - delete_note: Delete a note with system note protection - move_note: Move a note to a new parent using branch service - clone_note: Clone a note to an additional parent All mutation tools are marked with mutates: true for the tool approval system. Protected and system notes are guarded against modification.
This commit is contained in:
@@ -2363,7 +2363,11 @@
|
||||
"web_search": "Web search",
|
||||
"note_in_parent": "<Note/> in <Parent/>",
|
||||
"get_attachment": "Get attachment",
|
||||
"get_attachment_content": "Read attachment content"
|
||||
"get_attachment_content": "Read attachment content",
|
||||
"rename_note": "Rename note",
|
||||
"delete_note": "Delete note",
|
||||
"move_note": "Move note",
|
||||
"clone_note": "Clone note"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { z } from "zod";
|
||||
|
||||
import becca from "../../../becca/becca.js";
|
||||
import type BNote from "../../../becca/entities/bnote.js";
|
||||
import branchService from "../../branches.js";
|
||||
import cloningService from "../../cloning.js";
|
||||
import { defineTools } from "./tool_registry.js";
|
||||
|
||||
//#region Subtree tool implementation
|
||||
@@ -89,5 +91,83 @@ export const hierarchyTools = defineTools({
|
||||
|
||||
return buildSubtree(note, 0, depth);
|
||||
}
|
||||
},
|
||||
|
||||
move_note: {
|
||||
description: "Move a note to a new parent. The note keeps its content and children but changes its location in the tree. Cannot move system notes.",
|
||||
inputSchema: z.object({
|
||||
noteId: z.string().describe("The ID of the note to move"),
|
||||
newParentNoteId: z.string().describe("The ID of the new parent note")
|
||||
}),
|
||||
mutates: true,
|
||||
execute: ({ noteId, newParentNoteId }) => {
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) {
|
||||
return { error: "Note not found" };
|
||||
}
|
||||
if (note.noteId === "root") {
|
||||
return { error: "Cannot move the root note" };
|
||||
}
|
||||
if (note.isProtected) {
|
||||
return { error: "Note is protected and cannot be moved" };
|
||||
}
|
||||
|
||||
const targetParent = becca.getNote(newParentNoteId);
|
||||
if (!targetParent) {
|
||||
return { error: "Target parent note not found" };
|
||||
}
|
||||
|
||||
// Use the first (primary) parent branch for the move
|
||||
const branches = note.getParentBranches();
|
||||
if (branches.length === 0) {
|
||||
return { error: "Note has no parent branches" };
|
||||
}
|
||||
|
||||
const result = branchService.moveBranchToNote(branches[0], newParentNoteId);
|
||||
if (Array.isArray(result)) {
|
||||
// Validation error: [statusCode, { success: false, message }]
|
||||
const validation = result[1] as { success: boolean; message?: string };
|
||||
return { error: validation.message || "Move validation failed" };
|
||||
}
|
||||
if (!result.success) {
|
||||
return { error: "Failed to move note" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
noteId: note.noteId,
|
||||
title: note.getTitleOrProtected(),
|
||||
newParentNoteId,
|
||||
newParentTitle: targetParent.getTitleOrProtected()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
clone_note: {
|
||||
description: "Clone a note to an additional parent (Trilium supports multiple parents). The note appears in both locations and stays in sync. Use this to organize notes under multiple categories.",
|
||||
inputSchema: z.object({
|
||||
noteId: z.string().describe("The ID of the note to clone"),
|
||||
parentNoteId: z.string().describe("The ID of the new additional parent note"),
|
||||
prefix: z.string().optional().describe("Optional branch prefix (displayed before the note title in the tree)")
|
||||
}),
|
||||
mutates: true,
|
||||
execute: ({ noteId, parentNoteId, prefix }) => {
|
||||
const result = cloningService.cloneNoteToParentNote(noteId, parentNoteId, prefix ?? null);
|
||||
if (!result.success) {
|
||||
return { error: result.message || "Clone failed" };
|
||||
}
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
const parent = becca.getNote(parentNoteId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
noteId,
|
||||
title: note?.getTitleOrProtected() ?? noteId,
|
||||
parentNoteId,
|
||||
parentTitle: parent?.getTitleOrProtected() ?? parentNoteId,
|
||||
branchId: result.branchId
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,9 +9,13 @@ import markdownImport from "../../import/markdown.js";
|
||||
import noteService from "../../notes.js";
|
||||
import SearchContext from "../../search/search_context.js";
|
||||
import searchService from "../../search/services/search.js";
|
||||
import TaskContext from "../../task_context.js";
|
||||
import { TOOL_LIMITS, getContentPreview, getNoteContentForLlm, getNoteMeta, setNoteContentFromLlm } from "./helpers.js";
|
||||
import { defineTools } from "./tool_registry.js";
|
||||
|
||||
/** Note IDs that must not be deleted or moved by the LLM. */
|
||||
const PROTECTED_SYSTEM_NOTES = new Set(["root", "_hidden", "_share", "_lbRoot", "_globalNoteMap"]);
|
||||
|
||||
export const noteTools = defineTools({
|
||||
search_notes: {
|
||||
description: [
|
||||
@@ -231,5 +235,68 @@ export const noteTools = defineTools({
|
||||
return { error: err instanceof Error ? err.message : "Failed to create note" };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
rename_note: {
|
||||
description: "Change the title of an existing note.",
|
||||
inputSchema: z.object({
|
||||
noteId: z.string().describe("The ID of the note to rename"),
|
||||
newTitle: z.string().describe("The new title for the note")
|
||||
}),
|
||||
mutates: true,
|
||||
execute: ({ noteId, newTitle }) => {
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) {
|
||||
return { error: "Note not found" };
|
||||
}
|
||||
if (note.isProtected) {
|
||||
return { error: "Note is protected and cannot be renamed" };
|
||||
}
|
||||
|
||||
const trimmedTitle = newTitle.trim();
|
||||
if (!trimmedTitle) {
|
||||
return { error: "Title cannot be empty" };
|
||||
}
|
||||
|
||||
note.title = trimmedTitle;
|
||||
note.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
noteId: note.noteId,
|
||||
title: note.getTitleOrProtected()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
delete_note: {
|
||||
description: "Delete a note and all its branches (parent links). This is irreversible. The note will be marked as deleted. Cannot delete system notes (root, _hidden, etc.).",
|
||||
inputSchema: z.object({
|
||||
noteId: z.string().describe("The ID of the note to delete")
|
||||
}),
|
||||
mutates: true,
|
||||
execute: ({ noteId }) => {
|
||||
if (PROTECTED_SYSTEM_NOTES.has(noteId)) {
|
||||
return { error: "Cannot delete system notes" };
|
||||
}
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) {
|
||||
return { error: "Note not found" };
|
||||
}
|
||||
if (note.isProtected) {
|
||||
return { error: "Note is protected and cannot be deleted" };
|
||||
}
|
||||
|
||||
const title = note.getTitleOrProtected();
|
||||
const taskContext = new TaskContext("no-progress-reporting", "deleteNotes", null);
|
||||
note.deleteNote(null, taskContext);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
noteId,
|
||||
deletedTitle: title
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user