feat: add slides generation tool with multiple slide types
- Add slides.tool.ts with support for title, bullets, chart, stats, table, cards, timeline, quote, comparison, equation, image, summary slide types - Chart types: bar, horizontal-bar, line, donut, radar - Integrate with agent executor and canvas system - Add multilingual support (en/fr) - Various UI improvements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,63 @@ const toolDefinitions = [
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
|
||||
// ═══ TRASH ═══
|
||||
{
|
||||
name: 'trash_note',
|
||||
description: 'Move a note to trash (soft delete). Can be restored later.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { id: { type: 'string', description: 'Note ID' } },
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'restore_note',
|
||||
description: 'Restore a trashed note back to the active notes list.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { id: { type: 'string', description: 'Note ID' } },
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_trash',
|
||||
description: 'List all notes currently in trash.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
|
||||
// ═══ ADVANCED NOTE OPERATIONS ═══
|
||||
{
|
||||
name: 'append_to_note',
|
||||
description: 'Append text to the end of an existing note without replacing its current content.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Note ID' },
|
||||
content: { type: 'string', description: 'Content to append' },
|
||||
},
|
||||
required: ['id', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'find_and_update_note',
|
||||
description: 'Find a note by searching its title/content, then append, prepend, or replace information. Use when you need to update a note found by keyword (e.g. "find the note about bugs and add this info").',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query to find the note' },
|
||||
newContent: { type: 'string', description: 'Content to add to the note' },
|
||||
operation: {
|
||||
type: 'string',
|
||||
enum: ['append', 'prepend', 'replace'],
|
||||
description: 'append: add to end, prepend: add to start, replace: overwrite',
|
||||
default: 'append',
|
||||
},
|
||||
},
|
||||
required: ['query', 'newContent'],
|
||||
},
|
||||
},
|
||||
|
||||
// ═══ LABELS ═══
|
||||
{
|
||||
name: 'create_label',
|
||||
@@ -538,25 +595,32 @@ export function registerTools(server, prisma) {
|
||||
}
|
||||
|
||||
case 'search_notes': {
|
||||
const safeQuery = (args.query || '').replace(/'/g, "''");
|
||||
const userClause = uid ? `AND "userId" = '${uid}'` : '';
|
||||
const notebookClause = args.notebookId ? `AND "notebookId" = '${args.notebookId.replace(/'/g, "''")}'` : '';
|
||||
const query = args.query || ''
|
||||
const params = [query]
|
||||
let paramIdx = 1
|
||||
|
||||
const ftsRows = await prisma.$queryRawUnsafe(`
|
||||
SELECT id, title, content, color, type, "isPinned", "isArchived",
|
||||
"isMarkdown", size, "createdAt", "updatedAt", "notebookId",
|
||||
images, labels, "checkItems", reminder, "isReminderDone"
|
||||
FROM "Note"
|
||||
WHERE "tsv" @@ plainto_tsquery('simple', '${safeQuery}')
|
||||
AND "trashedAt" IS NULL
|
||||
AND "isArchived" = ${args.includeArchived ? 'true' : 'false'}
|
||||
${userClause}
|
||||
${notebookClause}
|
||||
ORDER BY ts_rank("tsv", plainto_tsquery('simple', '${safeQuery}')) DESC
|
||||
LIMIT ${DEFAULT_SEARCH_LIMIT}
|
||||
`);
|
||||
const userClause = uid ? `AND "userId" = $${++paramIdx}` : ''
|
||||
if (uid) params.push(uid)
|
||||
|
||||
return textResult(ftsRows.map(parseNoteLightweight));
|
||||
const notebookClause = args.notebookId ? `AND "notebookId" = $${++paramIdx}` : ''
|
||||
if (args.notebookId) params.push(args.notebookId)
|
||||
|
||||
const ftsRows = await prisma.$queryRawUnsafe(
|
||||
`SELECT id, title, content, color, type, "isPinned", "isArchived",
|
||||
"isMarkdown", size, "createdAt", "updatedAt", "notebookId",
|
||||
images, labels, "checkItems", reminder, "isReminderDone"
|
||||
FROM "Note"
|
||||
WHERE "tsv" @@ plainto_tsquery('simple', $1)
|
||||
AND "trashedAt" IS NULL
|
||||
AND "isArchived" = ${args.includeArchived ? 'true' : 'false'}
|
||||
${userClause}
|
||||
${notebookClause}
|
||||
ORDER BY ts_rank("tsv", plainto_tsquery('simple', $1)) DESC
|
||||
LIMIT ${DEFAULT_SEARCH_LIMIT}`,
|
||||
...params
|
||||
)
|
||||
|
||||
return textResult(ftsRows.map(parseNoteLightweight))
|
||||
}
|
||||
|
||||
case 'move_note': {
|
||||
@@ -950,6 +1014,127 @@ export function registerTools(server, prisma) {
|
||||
return textResult({ count: reminders.length, reminders });
|
||||
}
|
||||
|
||||
// ═══ TRASH ═══
|
||||
case 'trash_note': {
|
||||
const note = await prisma.note.update({
|
||||
where: { id: args.id, ...(uid ? { userId: uid } : {}) },
|
||||
data: { trashedAt: new Date() },
|
||||
});
|
||||
return textResult({ success: true, id: note.id, trashedAt: note.trashedAt });
|
||||
}
|
||||
|
||||
case 'restore_note': {
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: args.id, ...(uid ? { userId: uid } : {}) },
|
||||
});
|
||||
if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
|
||||
if (!note.trashedAt) return textResult({ success: true, message: 'Note was not in trash' });
|
||||
const restored = await prisma.note.update({
|
||||
where: { id: args.id },
|
||||
data: { trashedAt: null },
|
||||
});
|
||||
return textResult({ success: true, id: restored.id });
|
||||
}
|
||||
|
||||
case 'get_trash': {
|
||||
const where = { trashedAt: { not: null }, ...(uid ? { userId: uid } : {}) };
|
||||
const trashed = await prisma.note.findMany({
|
||||
where,
|
||||
select: { id: true, title: true, content: true, color: true, trashedAt: true, notebookId: true },
|
||||
orderBy: { trashedAt: 'desc' },
|
||||
});
|
||||
return textResult(trashed.map(n => ({
|
||||
...n,
|
||||
content: n.content?.slice(0, 100),
|
||||
})));
|
||||
}
|
||||
|
||||
// ═══ ADVANCED NOTE OPERATIONS ═══
|
||||
case 'append_to_note': {
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: args.id, ...(uid ? { userId: uid } : {}), trashedAt: null },
|
||||
select: { content: true },
|
||||
});
|
||||
if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
|
||||
const updated = await prisma.note.update({
|
||||
where: { id: args.id },
|
||||
data: {
|
||||
content: note.content ? `${note.content}\n\n${args.content}` : args.content,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
return textResult({ success: true, id: updated.id });
|
||||
}
|
||||
|
||||
case 'find_and_update_note': {
|
||||
const query = args.query || ''
|
||||
const operation = args.operation || 'append'
|
||||
const params = [query]
|
||||
let paramIdx = 1
|
||||
|
||||
const userClause = uid ? `AND "userId" = $${++paramIdx}` : ''
|
||||
if (uid) params.push(uid)
|
||||
|
||||
const results = await prisma.$queryRawUnsafe(
|
||||
`SELECT id, title, content
|
||||
FROM "Note"
|
||||
WHERE "tsv" @@ plainto_tsquery('simple', $1)
|
||||
AND "trashedAt" IS NULL
|
||||
AND "isArchived" = false
|
||||
${userClause}
|
||||
ORDER BY ts_rank("tsv", plainto_tsquery('simple', $1)) DESC
|
||||
LIMIT 1`,
|
||||
...params
|
||||
)
|
||||
|
||||
let note = results[0] || null
|
||||
|
||||
if (!note) {
|
||||
// Fallback: ILIKE search
|
||||
note = await prisma.note.findFirst({
|
||||
where: {
|
||||
trashedAt: null,
|
||||
isArchived: false,
|
||||
...(uid ? { userId: uid } : {}),
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ content: { contains: query, mode: 'insensitive' } },
|
||||
],
|
||||
},
|
||||
select: { id: true, title: true, content: true },
|
||||
})
|
||||
}
|
||||
|
||||
if (!note) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `No note found matching "${query}"`)
|
||||
}
|
||||
|
||||
let updatedContent
|
||||
switch (operation) {
|
||||
case 'append':
|
||||
updatedContent = note.content ? `${note.content}\n\n${args.newContent}` : args.newContent
|
||||
break
|
||||
case 'prepend':
|
||||
updatedContent = note.content ? `${args.newContent}\n\n${note.content}` : args.newContent
|
||||
break
|
||||
case 'replace':
|
||||
default:
|
||||
updatedContent = args.newContent
|
||||
}
|
||||
|
||||
await prisma.note.update({
|
||||
where: { id: note.id },
|
||||
data: { content: updatedContent, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
return textResult({
|
||||
success: true,
|
||||
noteId: note.id,
|
||||
noteTitle: note.title || 'Untitled',
|
||||
operation,
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user