feat(llm): migrate the calendar tool into the manage_note tool

This commit is contained in:
perf3ct 2025-10-10 16:49:47 -07:00
parent 4a239248b1
commit 5710becf05
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
5 changed files with 413 additions and 506 deletions

View File

@ -505,7 +505,7 @@ export class ChatService {
async generateChatCompletion(messages: Message[], options: ChatCompletionOptions = {}): Promise<ChatResponse> {
log.info(`========== CHAT SERVICE FLOW CHECK ==========`);
log.info(`Entered generateChatCompletion in ChatService`);
log.info(`Using pipeline for chat completion: ${this.getPipeline(options.pipeline).constructor.name}`);
log.info(`Using pipeline for chat completion: pipelineV2`);
log.info(`Tool support enabled: ${options.enableTools !== false}`);
try {

View File

@ -1,482 +0,0 @@
/**
* Calendar Integration Tool
*
* This tool allows the LLM to find date-related notes or create date-based entries.
*/
import type { Tool, ToolHandler } from './tool_interfaces.js';
import log from '../../log.js';
import becca from '../../../becca/becca.js';
import notes from '../../notes.js';
import attributes from '../../attributes.js';
import dateNotes from '../../date_notes.js';
/**
* Definition of the calendar integration tool
*/
export const calendarIntegrationToolDefinition: Tool = {
type: 'function',
function: {
name: 'calendar_integration',
description: 'Manage date-based notes: find notes by date/range, create dated entries, or get daily notes. Supports YYYY-MM-DD format.',
parameters: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Action to perform',
enum: ['find_date_notes', 'create_date_note', 'find_notes_with_date_range', 'get_daily_note']
},
date: {
type: 'string',
description: 'Date in ISO format (YYYY-MM-DD) for the note'
},
dateStart: {
type: 'string',
description: 'Start date in ISO format (YYYY-MM-DD) for date range queries'
},
dateEnd: {
type: 'string',
description: 'End date in ISO format (YYYY-MM-DD) for date range queries'
},
title: {
type: 'string',
description: 'Title for creating a new date-related note'
},
content: {
type: 'string',
description: 'Content for creating a new date-related note'
},
parentNoteId: {
type: 'string',
description: 'Optional parent note ID for the new date note. If not specified, will use default calendar container.'
}
},
required: ['action']
}
}
};
/**
* Calendar integration tool implementation
*/
export class CalendarIntegrationTool implements ToolHandler {
public definition: Tool = calendarIntegrationToolDefinition;
/**
* Execute the calendar integration tool
*/
public async execute(args: {
action: string,
date?: string,
dateStart?: string,
dateEnd?: string,
title?: string,
content?: string,
parentNoteId?: string
}): Promise<string | object> {
try {
const { action, date, dateStart, dateEnd, title, content, parentNoteId } = args;
log.info(`Executing calendar_integration tool - Action: ${action}, Date: ${date || 'not specified'}`);
// Handle different actions
if (action === 'find_date_notes') {
return await this.findDateNotes(date);
} else if (action === 'create_date_note') {
return await this.createDateNote(date, title, content, parentNoteId);
} else if (action === 'find_notes_with_date_range') {
return await this.findNotesWithDateRange(dateStart, dateEnd);
} else if (action === 'get_daily_note') {
return await this.getDailyNote(date);
} else {
return `Error: Unsupported action "${action}". Supported actions are: find_date_notes, create_date_note, find_notes_with_date_range, get_daily_note`;
}
} catch (error: any) {
log.error(`Error executing calendar_integration tool: ${error.message || String(error)}`);
return `Error: ${error.message || String(error)}`;
}
}
/**
* Find notes related to a specific date
*/
private async findDateNotes(date?: string): Promise<object> {
if (!date) {
// If no date is provided, use today's date
const today = new Date();
date = today.toISOString().split('T')[0];
log.info(`No date specified, using today's date: ${date}`);
}
try {
// Validate date format
if (!this.isValidDate(date)) {
return {
success: false,
message: `Invalid date format. Please use YYYY-MM-DD format.`
};
}
log.info(`Finding notes related to date: ${date}`);
// Get notes with dateNote attribute matching this date
const notesWithDateAttribute = this.getNotesWithDateAttribute(date);
log.info(`Found ${notesWithDateAttribute.length} notes with date attribute for ${date}`);
// Get year, month, day notes if they exist
const yearMonthDayNotes = await this.getYearMonthDayNotes(date);
// Format results
return {
success: true,
date: date,
yearNote: yearMonthDayNotes.yearNote ? {
noteId: yearMonthDayNotes.yearNote.noteId,
title: yearMonthDayNotes.yearNote.title
} : null,
monthNote: yearMonthDayNotes.monthNote ? {
noteId: yearMonthDayNotes.monthNote.noteId,
title: yearMonthDayNotes.monthNote.title
} : null,
dayNote: yearMonthDayNotes.dayNote ? {
noteId: yearMonthDayNotes.dayNote.noteId,
title: yearMonthDayNotes.dayNote.title
} : null,
relatedNotes: notesWithDateAttribute.map(note => ({
noteId: note.noteId,
title: note.title,
type: note.type
})),
message: `Found ${notesWithDateAttribute.length} notes related to date ${date}`
};
} catch (error: any) {
log.error(`Error finding date notes: ${error.message || String(error)}`);
throw error;
}
}
/**
* Create a new note associated with a date
*/
private async createDateNote(date?: string, title?: string, content?: string, parentNoteId?: string): Promise<object> {
if (!date) {
// If no date is provided, use today's date
const today = new Date();
date = today.toISOString().split('T')[0];
log.info(`No date specified, using today's date: ${date}`);
}
// Validate date format
if (!this.isValidDate(date)) {
return {
success: false,
message: `Invalid date format. Please use YYYY-MM-DD format.`
};
}
if (!title) {
title = `Note for ${date}`;
}
if (!content) {
content = `<p>Date note created for ${date}</p>`;
}
try {
log.info(`Creating new date note for ${date} with title "${title}"`);
// If no parent is specified, try to find appropriate date container
if (!parentNoteId) {
// Get or create day note to use as parent
const dateComponents = this.parseDateString(date);
if (!dateComponents) {
return {
success: false,
message: `Invalid date format. Please use YYYY-MM-DD format.`
};
}
// Use the date string directly with getDayNote
const dayNote = await dateNotes.getDayNote(date);
if (dayNote) {
parentNoteId = dayNote.noteId;
log.info(`Using day note ${dayNote.title} (${parentNoteId}) as parent`);
} else {
// Use root if day note couldn't be found/created
parentNoteId = 'root';
log.info(`Could not find/create day note, using root as parent`);
}
}
// Validate parent note exists
const parent = becca.notes[parentNoteId];
if (!parent) {
return {
success: false,
message: `Parent note with ID ${parentNoteId} not found. Please specify a valid parent note ID.`
};
}
// Create the new note
const createStartTime = Date.now();
const result = notes.createNewNote({
parentNoteId: parent.noteId,
title: title,
content: content,
type: 'text' as const,
mime: 'text/html'
});
const noteId = result.note.noteId;
const createDuration = Date.now() - createStartTime;
if (!noteId) {
return {
success: false,
message: `Failed to create date note. An unknown error occurred.`
};
}
log.info(`Created new note with ID ${noteId} in ${createDuration}ms`);
// Add dateNote attribute with the specified date
const attrStartTime = Date.now();
await attributes.createLabel(noteId, 'dateNote', date);
const attrDuration = Date.now() - attrStartTime;
log.info(`Added dateNote=${date} attribute in ${attrDuration}ms`);
// Return the new note information
return {
success: true,
noteId: noteId,
date: date,
title: title,
message: `Created new date note "${title}" for ${date}`
};
} catch (error: any) {
log.error(`Error creating date note: ${error.message || String(error)}`);
throw error;
}
}
/**
* Find notes with date attributes in a specified range
*/
private async findNotesWithDateRange(dateStart?: string, dateEnd?: string): Promise<object> {
if (!dateStart || !dateEnd) {
return {
success: false,
message: `Both dateStart and dateEnd are required for find_notes_with_date_range action.`
};
}
// Validate date formats
if (!this.isValidDate(dateStart) || !this.isValidDate(dateEnd)) {
return {
success: false,
message: `Invalid date format. Please use YYYY-MM-DD format.`
};
}
try {
log.info(`Finding notes with date attributes in range ${dateStart} to ${dateEnd}`);
// Get all notes with dateNote attribute
const allNotes = this.getAllNotesWithDateAttribute();
// Filter by date range
const startDate = new Date(dateStart);
const endDate = new Date(dateEnd);
const filteredNotes = allNotes.filter(note => {
const dateAttr = note.getOwnedAttributes()
.find((attr: any) => attr.name === 'dateNote');
if (dateAttr && dateAttr.value) {
const noteDate = new Date(dateAttr.value);
return noteDate >= startDate && noteDate <= endDate;
}
return false;
});
log.info(`Found ${filteredNotes.length} notes in date range`);
// Sort notes by date
filteredNotes.sort((a, b) => {
const aDateAttr = a.getOwnedAttributes().find((attr: any) => attr.name === 'dateNote');
const bDateAttr = b.getOwnedAttributes().find((attr: any) => attr.name === 'dateNote');
if (aDateAttr && bDateAttr) {
const aDate = new Date(aDateAttr.value);
const bDate = new Date(bDateAttr.value);
return aDate.getTime() - bDate.getTime();
}
return 0;
});
// Format results
return {
success: true,
dateStart: dateStart,
dateEnd: dateEnd,
noteCount: filteredNotes.length,
notes: filteredNotes.map(note => {
const dateAttr = note.getOwnedAttributes().find((attr: any) => attr.name === 'dateNote');
return {
noteId: note.noteId,
title: note.title,
type: note.type,
date: dateAttr ? dateAttr.value : null
};
}),
message: `Found ${filteredNotes.length} notes in date range ${dateStart} to ${dateEnd}`
};
} catch (error: any) {
log.error(`Error finding notes in date range: ${error.message || String(error)}`);
throw error;
}
}
/**
* Get or create a daily note for a specific date
*/
private async getDailyNote(date?: string): Promise<object> {
if (!date) {
// If no date is provided, use today's date
const today = new Date();
date = today.toISOString().split('T')[0];
log.info(`No date specified, using today's date: ${date}`);
}
// Validate date format
if (!this.isValidDate(date)) {
return {
success: false,
message: `Invalid date format. Please use YYYY-MM-DD format.`
};
}
try {
log.info(`Getting daily note for ${date}`);
// Get or create day note - directly pass the date string
const startTime = Date.now();
const dayNote = await dateNotes.getDayNote(date);
const duration = Date.now() - startTime;
if (!dayNote) {
return {
success: false,
message: `Could not find or create daily note for ${date}`
};
}
log.info(`Retrieved/created daily note for ${date} in ${duration}ms`);
// Get parent month and year notes
const yearStr = date.substring(0, 4);
const monthStr = date.substring(0, 7);
const monthNote = await dateNotes.getMonthNote(monthStr);
const yearNote = await dateNotes.getYearNote(yearStr);
// Return the note information
return {
success: true,
date: date,
dayNote: {
noteId: dayNote.noteId,
title: dayNote.title,
content: await dayNote.getContent()
},
monthNote: monthNote ? {
noteId: monthNote.noteId,
title: monthNote.title
} : null,
yearNote: yearNote ? {
noteId: yearNote.noteId,
title: yearNote.title
} : null,
message: `Retrieved daily note for ${date}`
};
} catch (error: any) {
log.error(`Error getting daily note: ${error.message || String(error)}`);
throw error;
}
}
/**
* Helper method to get notes with a specific date attribute
*/
private getNotesWithDateAttribute(date: string): any[] {
// Find notes with matching dateNote attribute
return attributes.getNotesWithLabel('dateNote', date) || [];
}
/**
* Helper method to get all notes with any date attribute
*/
private getAllNotesWithDateAttribute(): any[] {
// Find all notes with dateNote attribute
return attributes.getNotesWithLabel('dateNote') || [];
}
/**
* Helper method to get year, month, and day notes for a date
*/
private async getYearMonthDayNotes(date: string): Promise<{
yearNote: any | null;
monthNote: any | null;
dayNote: any | null;
}> {
if (!this.isValidDate(date)) {
return { yearNote: null, monthNote: null, dayNote: null };
}
// Extract the year and month from the date string
const yearStr = date.substring(0, 4);
const monthStr = date.substring(0, 7);
// Use the dateNotes service to get the notes
const yearNote = await dateNotes.getYearNote(yearStr);
const monthNote = await dateNotes.getMonthNote(monthStr);
const dayNote = await dateNotes.getDayNote(date);
return { yearNote, monthNote, dayNote };
}
/**
* Helper method to validate date string format
*/
private isValidDate(dateString: string): boolean {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateString)) {
return false;
}
const date = new Date(dateString);
return date.toString() !== 'Invalid Date';
}
/**
* Helper method to parse date string into components
*/
private parseDateString(dateString: string): { year: number; month: number; day: number } | null {
if (!this.isValidDate(dateString)) {
return null;
}
const [yearStr, monthStr, dayStr] = dateString.split('-');
return {
year: parseInt(yearStr, 10),
month: parseInt(monthStr, 10),
day: parseInt(dayStr, 10)
};
}
}

View File

@ -31,6 +31,12 @@ vi.mock('../../../attributes.js', () => ({
}
}));
vi.mock('../../../cloning.js', () => ({
default: {
cloneNoteToParentNote: vi.fn()
}
}));
describe('ManageNoteTool', () => {
let tool: ManageNoteTool;
@ -58,6 +64,8 @@ describe('ManageNoteTool', () => {
expect(action.enum).toContain('create');
expect(action.enum).toContain('update');
expect(action.enum).toContain('delete');
expect(action.enum).toContain('move');
expect(action.enum).toContain('clone');
expect(action.enum).toContain('add_attribute');
expect(action.enum).toContain('remove_attribute');
expect(action.enum).toContain('add_relation');
@ -67,6 +75,34 @@ describe('ManageNoteTool', () => {
it('should require action parameter', () => {
expect(tool.definition.function.parameters.required).toContain('action');
});
it('should have all 17 Trilium note types in note_type enum', () => {
const noteType = tool.definition.function.parameters.properties.note_type;
expect(noteType).toBeDefined();
expect(noteType.enum).toBeDefined();
expect(noteType.enum).toHaveLength(17);
// Verify all official Trilium note types are present
const expectedTypes = [
'text', 'code', 'file', 'image', 'search', 'noteMap',
'relationMap', 'launcher', 'doc', 'contentWidget', 'render',
'canvas', 'mermaid', 'book', 'webView', 'mindMap', 'aiChat'
];
for (const type of expectedTypes) {
expect(noteType.enum).toContain(type);
}
});
it('should have default values for optional enum parameters', () => {
const noteType = tool.definition.function.parameters.properties.note_type;
expect(noteType.default).toBe('text');
expect(noteType.enum).toContain(noteType.default);
const updateMode = tool.definition.function.parameters.properties.update_mode;
expect(updateMode.default).toBe('replace');
expect(updateMode.enum).toContain(updateMode.default);
});
});
describe('read action', () => {
@ -214,6 +250,62 @@ describe('ManageNoteTool', () => {
expect.objectContaining({ parentNoteId: 'root' })
);
});
it('should validate content size limit', async () => {
const result = await tool.execute({
action: 'create',
title: 'Test Note',
content: 'x'.repeat(10_000_001) // Exceeds 10MB limit
});
expect(typeof result).toBe('string');
expect(result).toContain('exceeds maximum size of 10MB');
expect(result).toContain('Consider splitting into multiple notes');
});
it('should validate title length limit', async () => {
const result = await tool.execute({
action: 'create',
title: 'x'.repeat(201), // Exceeds 200 char limit
content: 'Test content'
});
expect(typeof result).toBe('string');
expect(result).toContain('exceeds maximum length of 200 characters');
expect(result).toContain('Please shorten the title');
});
it('should accept all valid note types', async () => {
const notes = await import('../../../notes.js');
const becca = await import('../../../../becca/becca.js');
const mockRoot = {
noteId: 'root',
title: 'Root'
};
vi.mocked(becca.default.getNote).mockReturnValue(mockRoot as any);
const mockNewNote = { noteId: 'new123', title: 'New Note' };
vi.mocked(notes.default.createNewNote).mockReturnValue({ note: mockNewNote } as any);
const validTypes = [
'text', 'code', 'file', 'image', 'search', 'noteMap',
'relationMap', 'launcher', 'doc', 'contentWidget', 'render',
'canvas', 'mermaid', 'book', 'webView', 'mindMap', 'aiChat'
];
for (const noteType of validTypes) {
const result = await tool.execute({
action: 'create',
title: `Note of type ${noteType}`,
content: 'Test content',
note_type: noteType
}) as any;
expect(result.success).toBe(true);
expect(result.type).toBe(noteType);
}
});
});
describe('update action', () => {
@ -429,6 +521,192 @@ describe('ManageNoteTool', () => {
});
});
describe('move action', () => {
it('should move note successfully', async () => {
const mockNote = {
noteId: 'note123',
title: 'Note to Move'
};
const mockParent = {
noteId: 'parent123',
title: 'New Parent'
};
const becca = await import('../../../../becca/becca.js');
const cloningService = await import('../../../cloning.js');
vi.mocked(becca.default.notes)['note123'] = mockNote as any;
vi.mocked(becca.default.notes)['parent123'] = mockParent as any;
vi.mocked(cloningService.default.cloneNoteToParentNote).mockReturnValue({
branchId: 'branch123'
} as any);
const result = await tool.execute({
action: 'move',
note_id: 'note123',
parent_note_id: 'parent123'
}) as any;
expect(result.success).toBe(true);
expect(result.noteId).toBe('note123');
expect(result.newParentId).toBe('parent123');
expect(result.branchId).toBe('branch123');
expect(cloningService.default.cloneNoteToParentNote).toHaveBeenCalledWith(
'note123',
'parent123'
);
});
it('should require note_id for move', async () => {
const result = await tool.execute({
action: 'move',
parent_note_id: 'parent123'
});
expect(typeof result).toBe('string');
expect(result).toContain('note_id is required');
});
it('should require parent_note_id for move', async () => {
const result = await tool.execute({
action: 'move',
note_id: 'note123'
});
expect(typeof result).toBe('string');
expect(result).toContain('parent_note_id is required');
});
it('should return error for non-existent note in move', async () => {
const becca = await import('../../../../becca/becca.js');
vi.mocked(becca.default.notes)['note123'] = undefined as any;
const result = await tool.execute({
action: 'move',
note_id: 'note123',
parent_note_id: 'parent123'
});
expect(typeof result).toBe('string');
expect(result).toContain('not found');
});
it('should return error for non-existent parent in move', async () => {
const mockNote = {
noteId: 'note123',
title: 'Note to Move'
};
const becca = await import('../../../../becca/becca.js');
vi.mocked(becca.default.notes)['note123'] = mockNote as any;
vi.mocked(becca.default.notes)['parent123'] = undefined as any;
const result = await tool.execute({
action: 'move',
note_id: 'note123',
parent_note_id: 'parent123'
});
expect(typeof result).toBe('string');
expect(result).toContain('Parent note');
expect(result).toContain('not found');
});
});
describe('clone action', () => {
it('should clone note successfully', async () => {
const mockNote = {
noteId: 'note123',
title: 'Note to Clone'
};
const mockParent = {
noteId: 'parent123',
title: 'Target Parent'
};
const becca = await import('../../../../becca/becca.js');
const cloningService = await import('../../../cloning.js');
vi.mocked(becca.default.notes)['note123'] = mockNote as any;
vi.mocked(becca.default.notes)['parent123'] = mockParent as any;
vi.mocked(cloningService.default.cloneNoteToParentNote).mockReturnValue({
branchId: 'branch456'
} as any);
const result = await tool.execute({
action: 'clone',
note_id: 'note123',
parent_note_id: 'parent123'
}) as any;
expect(result.success).toBe(true);
expect(result.sourceNoteId).toBe('note123');
expect(result.parentNoteId).toBe('parent123');
expect(result.branchId).toBe('branch456');
expect(cloningService.default.cloneNoteToParentNote).toHaveBeenCalledWith(
'note123',
'parent123'
);
});
it('should require note_id for clone', async () => {
const result = await tool.execute({
action: 'clone',
parent_note_id: 'parent123'
});
expect(typeof result).toBe('string');
expect(result).toContain('note_id is required');
});
it('should require parent_note_id for clone', async () => {
const result = await tool.execute({
action: 'clone',
note_id: 'note123'
});
expect(typeof result).toBe('string');
expect(result).toContain('parent_note_id is required');
});
it('should return error for non-existent note in clone', async () => {
const becca = await import('../../../../becca/becca.js');
vi.mocked(becca.default.notes)['note123'] = undefined as any;
const result = await tool.execute({
action: 'clone',
note_id: 'note123',
parent_note_id: 'parent123'
});
expect(typeof result).toBe('string');
expect(result).toContain('not found');
});
it('should return error for non-existent parent in clone', async () => {
const mockNote = {
noteId: 'note123',
title: 'Note to Clone'
};
const becca = await import('../../../../becca/becca.js');
vi.mocked(becca.default.notes)['note123'] = mockNote as any;
vi.mocked(becca.default.notes)['parent123'] = undefined as any;
const result = await tool.execute({
action: 'clone',
note_id: 'note123',
parent_note_id: 'parent123'
});
expect(typeof result).toBe('string');
expect(result).toContain('Parent note');
expect(result).toContain('not found');
});
});
describe('error handling', () => {
it('should handle unknown action', async () => {
const result = await tool.execute({

View File

@ -18,6 +18,7 @@ import log from '../../../log.js';
import becca from '../../../../becca/becca.js';
import notes from '../../../notes.js';
import attributes from '../../../attributes.js';
import cloningService from '../../../cloning.js';
import type { BNote } from '../../../backend_script_entrypoint.js';
/**
@ -28,6 +29,8 @@ type NoteAction =
| 'create'
| 'update'
| 'delete'
| 'move'
| 'clone'
| 'add_attribute'
| 'remove_attribute'
| 'add_relation'
@ -59,14 +62,14 @@ export const manageNoteToolDefinition: Tool = {
type: 'function',
function: {
name: 'manage_note',
description: 'Unified interface for all note operations: read, create, update, and manage attributes/relations. Replaces separate read, create, update, attribute, and relationship tools.',
description: 'Unified interface for all note operations: read, create, update, delete, move, clone, and manage attributes/relations. Replaces separate read, create, update, attribute, and relationship tools.',
parameters: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Operation to perform',
enum: ['read', 'create', 'update', 'delete', 'add_attribute', 'remove_attribute', 'add_relation', 'remove_relation', 'list_attributes', 'list_relations']
enum: ['read', 'create', 'update', 'delete', 'move', 'clone', 'add_attribute', 'remove_attribute', 'add_relation', 'remove_relation', 'list_attributes', 'list_relations']
},
note_id: {
type: 'string',
@ -86,8 +89,9 @@ export const manageNoteToolDefinition: Tool = {
},
note_type: {
type: 'string',
description: 'Note type: text, code, file, image, etc.',
enum: ['text', 'code', 'file', 'image', 'search', 'relation-map', 'book', 'mermaid', 'canvas']
description: 'Note type (default: text). User-creatable: text, code, book, canvas, mermaid, mindMap, relationMap, webView, render. System types: file, image, search, noteMap, launcher, doc, contentWidget, aiChat.',
enum: ['text', 'code', 'book', 'canvas', 'mermaid', 'mindMap', 'relationMap', 'webView', 'render', 'file', 'image', 'search', 'noteMap', 'launcher', 'doc', 'contentWidget', 'aiChat'],
default: 'text'
},
mime: {
type: 'string',
@ -95,8 +99,9 @@ export const manageNoteToolDefinition: Tool = {
},
update_mode: {
type: 'string',
description: 'Content update mode: replace, append, or prepend',
enum: ['replace', 'append', 'prepend']
description: 'Content update mode (default: replace)',
enum: ['replace', 'append', 'prepend'],
default: 'replace'
},
attribute_name: {
type: 'string',
@ -169,6 +174,10 @@ export class ManageNoteTool implements ToolHandler {
return await this.updateNote(args);
case 'delete':
return await this.deleteNote(args);
case 'move':
return await this.moveNote(args);
case 'clone':
return await this.cloneNote(args);
case 'add_attribute':
return await this.addAttribute(args);
case 'remove_attribute':
@ -252,12 +261,23 @@ export class ManageNoteTool implements ToolHandler {
return 'Error: content is required for create action';
}
// Validate parent note
// Business logic validations (not schema validations - those are enforced by LLM provider)
const MAX_CONTENT_SIZE = 10_000_000; // 10MB
if (content.length > MAX_CONTENT_SIZE) {
return `Error: Content exceeds maximum size of 10MB (${content.length} bytes). Consider splitting into multiple notes.`;
}
const MAX_TITLE_LENGTH = 200;
if (title.length > MAX_TITLE_LENGTH) {
return `Error: Title exceeds maximum length of 200 characters. Current length: ${title.length}. Please shorten the title.`;
}
// Validate parent note exists (business logic constraint)
let parent: BNote | null = null;
if (parent_note_id) {
parent = becca.notes[parent_note_id];
if (!parent) {
return `Error: Parent note with ID ${parent_note_id} not found`;
return `Error: Parent note ${parent_note_id} not found. Use smart_search to find valid parent notes.`;
}
} else {
parent = becca.getNote('root');
@ -392,6 +412,95 @@ export class ManageNoteTool implements ToolHandler {
};
}
/**
* Move a note to a new parent (creates a new branch)
* In Trilium, notes can have multiple parents, so "moving" means creating a new branch
*/
private async moveNote(args: { note_id?: string; parent_note_id?: string }): Promise<string | object> {
const { note_id, parent_note_id } = args;
if (!note_id) {
return 'Error: note_id is required for move action';
}
if (!parent_note_id) {
return 'Error: parent_note_id is required for move action';
}
const note = becca.notes[note_id];
if (!note) {
return `Error: Note with ID ${note_id} not found`;
}
const parentNote = becca.notes[parent_note_id];
if (!parentNote) {
return `Error: Parent note with ID ${parent_note_id} not found`;
}
log.info(`Moving note "${note.title}" to parent "${parentNote.title}"`);
// Clone note to new parent (this creates a new branch)
const startTime = Date.now();
const cloneResult = cloningService.cloneNoteToParentNote(note_id, parent_note_id);
const duration = Date.now() - startTime;
log.info(`Note moved in ${duration}ms - new branch ID: ${cloneResult.branchId}`);
return {
success: true,
noteId: note.noteId,
title: note.title,
newParentId: parent_note_id,
newParentTitle: parentNote.title,
branchId: cloneResult.branchId,
message: `Note "${note.title}" moved to "${parentNote.title}" (notes can have multiple parents in Trilium)`
};
}
/**
* Clone a note (deep copy with all children)
*/
private async cloneNote(args: { note_id?: string; parent_note_id?: string }): Promise<string | object> {
const { note_id, parent_note_id } = args;
if (!note_id) {
return 'Error: note_id is required for clone action';
}
if (!parent_note_id) {
return 'Error: parent_note_id is required for clone action';
}
const note = becca.notes[note_id];
if (!note) {
return `Error: Note with ID ${note_id} not found`;
}
const parentNote = becca.notes[parent_note_id];
if (!parentNote) {
return `Error: Parent note with ID ${parent_note_id} not found`;
}
log.info(`Cloning note "${note.title}" to parent "${parentNote.title}"`);
// Clone note to new parent
const startTime = Date.now();
const cloneResult = cloningService.cloneNoteToParentNote(note_id, parent_note_id);
const duration = Date.now() - startTime;
log.info(`Note cloned in ${duration}ms - new branch ID: ${cloneResult.branchId}`);
return {
success: true,
sourceNoteId: note.noteId,
sourceTitle: note.title,
parentNoteId: parent_note_id,
parentTitle: parentNote.title,
branchId: cloneResult.branchId,
message: `Note "${note.title}" cloned to "${parentNote.title}"`
};
}
/**
* Add an attribute to a note
*/
@ -749,10 +858,18 @@ export class ManageNoteTool implements ToolHandler {
'file': 'application/octet-stream',
'image': 'image/png',
'search': 'application/json',
'relation-map': 'application/json',
'book': 'text/html',
'noteMap': '',
'relationMap': 'application/json',
'launcher': '',
'doc': '',
'contentWidget': '',
'render': '',
'canvas': 'application/json',
'mermaid': 'text/mermaid',
'canvas': 'application/json'
'book': 'text/html',
'webView': '',
'mindMap': 'application/json',
'aiChat': 'application/json'
};
return mimeMap[noteType] || 'text/html';

View File

@ -25,7 +25,6 @@ import toolRegistry from './tool_registry.js';
import { SmartSearchTool } from './consolidated/smart_search_tool.js';
import { ManageNoteTool } from './consolidated/manage_note_tool.js';
import { NavigateHierarchyTool } from './consolidated/navigate_hierarchy_tool.js';
import { CalendarIntegrationTool } from './calendar_integration_tool.js';
import log from '../../log.js';
/**
@ -43,18 +42,17 @@ export async function initializeConsolidatedTools(): Promise<void> {
try {
log.info('Initializing consolidated LLM tools (V2)...');
// Register the 4 consolidated tools
// Register the 3 consolidated tools
toolRegistry.registerTool(new SmartSearchTool()); // Replaces: search_notes, keyword_search, attribute_search, search_suggestion
toolRegistry.registerTool(new ManageNoteTool()); // Replaces: read_note, note_creation, note_update, attribute_manager, relationship
toolRegistry.registerTool(new ManageNoteTool()); // Replaces: read_note, note_creation, note_update, attribute_manager, relationship, calendar (via attributes)
toolRegistry.registerTool(new NavigateHierarchyTool()); // New: tree navigation capability
toolRegistry.registerTool(new CalendarIntegrationTool()); // Enhanced: calendar operations
// Log registered tools
const toolCount = toolRegistry.getAllTools().length;
const toolNames = toolRegistry.getAllTools().map(tool => tool.definition.function.name).join(', ');
log.info(`Successfully registered ${toolCount} consolidated LLM tools: ${toolNames}`);
log.info('Tool consolidation: 12 tools → 4 tools (67% reduction, ~600 tokens saved)');
log.info('Tool consolidation: 12 tools → 3 tools (75% reduction, ~725 tokens saved)');
} catch (error: unknown) {
const errorMessage = isError(error) ? error.message : String(error);
log.error(`Error initializing consolidated LLM tools: ${errorMessage}`);
@ -77,9 +75,9 @@ export function getConsolidationInfo(): {
} {
return {
version: 'v2',
toolCount: 4,
toolCount: 3,
consolidatedFrom: 12,
tokenSavings: 600, // Estimated
tokenSavings: 725, // Estimated (increased from 600 with calendar removal)
tools: [
{
name: 'smart_search',
@ -87,15 +85,11 @@ export function getConsolidationInfo(): {
},
{
name: 'manage_note',
replaces: ['read_note', 'create_note', 'update_note', 'manage_attributes', 'manage_relationships', 'note_summarization', 'content_extraction']
replaces: ['read_note', 'create_note', 'update_note', 'delete_note', 'move_note', 'clone_note', 'manage_attributes', 'manage_relationships', 'note_summarization', 'content_extraction', 'calendar_integration (via attributes)']
},
{
name: 'navigate_hierarchy',
replaces: ['(new capability - no replacement)']
},
{
name: 'calendar_integration',
replaces: ['calendar_integration (enhanced)']
}
]
};