/** * notes.ts — SearchNotes / ReadNote / WriteNote tools * * Agents can search, read, and write shared knowledge notes * stored under data/users/{userId}/notes/{folder}/{file}.md. */ import { ToolDef } from '../../llm/openai-compat.js'; import { NotesService } from '../../notes/notes-service.js'; import type { ToolContext, ToolResult } from './core.js'; export interface NotesToolContext { notesService: NotesService; user: Express.User; } export const TOOL_DEFS: Record = { SearchNotes: { type: 'function', function: { name: 'SearchNotes', description: '購読中の knowledge notes を全文検索 (FTS5)。' + '詳細は ReadToolDoc({ name: "SearchNotes" }) で取得可能。', parameters: { type: 'object', properties: { query: { type: 'string', description: '検索クエリ文字列(FTS5 構文)', }, folder: { type: 'string', description: '特定フォルダーのみに絞り込む(省略可)', }, limit: { type: 'integer', description: '最大取得件数(デフォルト 10、最大 100)', }, }, required: ['query'], }, }, }, ReadNote: { type: 'function', function: { name: 'ReadNote', description: '特定 note の全文(frontmatter + 本文)を取得。可視性チェックあり。' + '詳細は ReadToolDoc({ name: "ReadNote" }) で取得可能。', parameters: { type: 'object', properties: { owner_id: { type: 'string', description: 'note の所有者 user ID', }, folder: { type: 'string', description: 'フォルダー名', }, file_name: { type: 'string', description: 'ファイル名(例: foo.md)', }, }, required: ['owner_id', 'folder', 'file_name'], }, }, }, WriteNote: { type: 'function', function: { name: 'WriteNote', description: '自分の notes/{folder}/{file}.md に Markdown note を書き込む(作成 / 更新)。' + '詳細は ReadToolDoc({ name: "WriteNote" }) で取得可能。', parameters: { type: 'object', properties: { folder: { type: 'string', description: 'フォルダー名(英数字・. - _ のみ)', }, file_name: { type: 'string', description: 'ファイル名(.md で終わる)', }, content: { type: 'string', description: 'YAML frontmatter を含む完全な Markdown 内容', }, }, required: ['folder', 'file_name', 'content'], }, }, }, }; /** * Execute a notes tool. * Returns null when the tool name is not handled (allows index.ts to fall through). */ export async function executeNotesTools( name: string, args: Record, ctx: NotesToolContext | ToolContext, ): Promise { // Extract notes-specific context. When called via index.ts the ctx is a full // ToolContext; when called from tests it's a minimal NotesToolContext. const notesCtx = ctx as NotesToolContext & ToolContext; const notesService = notesCtx.notesService; if (!notesService) return null; // Reconstruct a minimal Express.User from ToolContext fields if needed. const user: Express.User = notesCtx.user ?? { id: notesCtx.userId ?? '', role: (notesCtx as ToolContext).notesUserRole ?? 'user', orgIds: (notesCtx as ToolContext).notesUserOrgIds ?? [], email: '', name: null, avatarUrl: null, status: 'active', defaultVisibility: 'private', defaultVisibilityOrgId: null, }; switch (name) { case 'SearchNotes': { const rawQuery = String(args['query'] ?? ''); // Wrap in FTS5 phrase quotes to avoid query syntax injection const safeQuery = '"' + rawQuery.replace(/"/g, '""') + '"'; const rows = notesService.searchNotes({ user, query: safeQuery, folder: args['folder'] ? String(args['folder']) : undefined, limit: args['limit'] ? Math.min(parseInt(String(args['limit']), 10), 100) : 10, consumerSubscribed: true, }); if (rows.length === 0) { return { output: 'No notes found matching the query.', isError: false }; } const lines = rows.map( (r) => `- ${r.owner_id}/${r.folder}/${r.file_name}` + (r.title ? `: ${r.title}` : '') + (r.tags_json && r.tags_json !== '[]' ? ` [tags: ${r.tags_json}]` : '') ); return { output: lines.join('\n'), isError: false }; } case 'ReadNote': { const ownerId = String(args['owner_id'] ?? ''); const folder = String(args['folder'] ?? ''); const fileName = String(args['file_name'] ?? ''); const out = notesService.getCrossUserNote({ user, ownerId, folder, fileName }); if (!out) { return { output: 'Note not found or not accessible.', isError: true }; } return { output: out.content, isError: false }; } case 'WriteNote': { try { const result = notesService.writeNote({ ownerId: user.id, folder: String(args['folder'] ?? ''), fileName: String(args['file_name'] ?? ''), content: String(args['content'] ?? ''), }); return { output: `Wrote ${result.row.owner_id}/${result.row.folder}/${result.row.file_name}`, isError: false, }; } catch (err) { return { output: `WriteNote error: ${(err as Error).message}`, isError: true }; } } default: return null; } } // Re-export as executeTool for ToolModule interface compatibility export const executeTool = executeNotesTools;