diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json
index 9605212f6e..6cfdcd069b 100644
--- a/apps/client/src/translations/en/translation.json
+++ b/apps/client/src/translations/en/translation.json
@@ -2363,7 +2363,11 @@
"web_search": "Web search",
"note_in_parent": " in ",
"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"
}
}
}
diff --git a/apps/server/src/services/llm/tools/hierarchy_tools.ts b/apps/server/src/services/llm/tools/hierarchy_tools.ts
index cb75941c5e..2e39f2ed31 100644
--- a/apps/server/src/services/llm/tools/hierarchy_tools.ts
+++ b/apps/server/src/services/llm/tools/hierarchy_tools.ts
@@ -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
+ };
+ }
}
});
diff --git a/apps/server/src/services/llm/tools/note_tools.ts b/apps/server/src/services/llm/tools/note_tools.ts
index 2daa55a1ca..e3d2d12432 100644
--- a/apps/server/src/services/llm/tools/note_tools.ts
+++ b/apps/server/src/services/llm/tools/note_tools.ts
@@ -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
+ };
+ }
}
});