Files
maestro/src/engine/tools/notes.ts
T

191 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, ToolDef> = {
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<string, unknown>,
ctx: NotesToolContext | ToolContext,
): Promise<ToolResult | null> {
// 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;