import { z } from 'zod'; import { existsSync } from 'node:fs'; import { writeFile, unlink } from 'node:fs/promises'; import { join } from 'node:path'; import type { ToolDef } from '../tools/types.js'; import { ensureMemoryScaffold, getMemoryRoot } from '../memory/paths.js'; import { writeEntry, readTopicFiles } from '../memory/store.js'; const ManageMemoryInput = z.object({ topic: z.enum(['project', 'user', 'reference']).describe('Memory topic category'), title: z.string().min(1).max(200).describe('Entry title (used as identifier for update/delete)'), content: z.string().optional().describe('Memory content body (required for create/update)'), tags: z.array(z.string()).optional().describe('Optional tags for search'), action: z.enum(['create', 'update', 'delete']).describe('Action to perform'), }); type InputT = z.infer; function titleToFilename(title: string): string { return ( title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, '') + '.md' ); } /** * Try to update the CoreTier SQLite database in addition to the file store. * This is best-effort — CoreTier is optional (file store is primary). */ async function syncCoreTier( _root: string, _topic: string, _title: string, _content: string, _tags: string[], ): Promise { // CoreTier SQLite backend is not available in this build — file store only. } export const manageMemoryTool: ToolDef = { name: 'manage_memory', description: 'Create, update, or delete memory entries in .boocode/memory/ for cross-session recall. ' + 'Use to persist project conventions, user preferences, and architectural decisions. ' + 'Actions: create (write new entry), update (modify existing entry), delete (remove entry).', inputSchema: ManageMemoryInput, jsonSchema: { type: 'function', function: { name: 'manage_memory', description: 'Manage memory entries — create, update, or delete', parameters: { type: 'object', properties: { topic: { type: 'string', enum: ['project', 'user', 'reference'], description: 'Memory topic category', }, title: { type: 'string', description: 'Entry title (identifier for update/delete)' }, content: { type: 'string', description: 'Memory content body (required for create/update)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for search', }, action: { type: 'string', enum: ['create', 'update', 'delete'], description: 'Action to perform', }, }, required: ['topic', 'title', 'action'], }, }, }, async execute(input: InputT, projectRoot: string): Promise { const root = getMemoryRoot(projectRoot); await ensureMemoryScaffold(root); const filename = titleToFilename(input.title); if (input.action === 'create') { if (!input.content) { return { error: 'Content is required for create action.' }; } await writeEntry(root, input.topic, input.title, input.content, input.tags ?? []); await syncCoreTier(root, input.topic, input.title, input.content, input.tags ?? []); return { result: `Memory entry "${input.title}" created in .boocode/memory/${input.topic}/`, }; } if (input.action === 'update') { if (!input.content) { return { error: 'Content is required for update action.' }; } // Resolve target file path — try computed filename first, then heading match let targetPath = join(root, input.topic, filename); if (!existsSync(targetPath)) { const files = await readTopicFiles(root, input.topic); const matched = [...files.keys()].find((name) => { const content = files.get(name); return content?.trimStart().startsWith(`## ${input.topic}: ${input.title}`); }); if (matched) { targetPath = join(root, input.topic, matched); } else { return { error: `Memory entry "${input.title}" not found in .boocode/memory/${input.topic}/`, }; } } const tagLine = (input.tags ?? []).length > 0 ? `> tags: ${(input.tags ?? []).join(', ')}\n\n` : '\n'; const entry = `## ${input.topic}: ${input.title}\n${tagLine}${input.content}\n`; await writeFile(targetPath, entry, 'utf8'); await syncCoreTier(root, input.topic, input.title, input.content, input.tags ?? []); return { result: `Memory entry "${input.title}" updated in .boocode/memory/${input.topic}/`, }; } if (input.action === 'delete') { // Resolve target file path let targetPath = join(root, input.topic, filename); if (!existsSync(targetPath)) { const files = await readTopicFiles(root, input.topic); const matched = [...files.keys()].find((name) => { const content = files.get(name); return content?.trimStart().startsWith(`## ${input.topic}: ${input.title}`); }); if (matched) { targetPath = join(root, input.topic, matched); } else { return { error: `Memory entry "${input.title}" not found in .boocode/memory/${input.topic}/`, }; } } await unlink(targetPath); return { result: `Memory entry "${input.title}" deleted from .boocode/memory/${input.topic}/`, }; } return { error: `Unknown action: ${input.action}` }; }, };