UNPKG

a24z-memory

Version:

Standalone a24z Memory MCP server

1,360 lines (1,323 loc) 62.1 kB
#!/usr/bin/env node // src/mcp-cli.ts import * as fs from "node:fs"; import * as path2 from "node:path"; import * as os from "node:os"; // src/mcp/server/McpServer.ts import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; // src/mcp/tools/base-tool.ts import { z } from "zod"; // src/mcp/utils/zod-to-json-schema.ts function zodToJsonSchema(schema) { const def = schema._def; const typeName = def?.typeName || ""; const description = def?.description; let result; switch (typeName) { case "ZodObject": { const shape = def.shape(); const properties = {}; const required = []; for (const key of Object.keys(shape)) { const child = shape[key]; const isOptional = child._def?.typeName === "ZodOptional" || child._def?.typeName === "ZodDefault"; properties[key] = zodToJsonSchema(child); if (!isOptional) required.push(key); } result = { type: "object", properties, required }; break; } case "ZodString": result = { type: "string" }; break; case "ZodNumber": result = { type: "number" }; break; case "ZodBoolean": result = { type: "boolean" }; break; case "ZodArray": result = { type: "array", items: zodToJsonSchema(def.type) }; break; case "ZodEnum": result = { type: "string", enum: def.values }; break; case "ZodUnion": result = { anyOf: def.options.map((o) => zodToJsonSchema(o)) }; break; case "ZodOptional": result = zodToJsonSchema(def.innerType); if (!result.description && description) { result.description = description; } break; case "ZodDefault": result = zodToJsonSchema(def.innerType); if (def.defaultValue !== void 0) { result.default = def.defaultValue(); } if (!result.description && description) { result.description = description; } break; default: result = {}; } if (description && result) { result.description = description; } return result; } // src/mcp/tools/base-tool.ts var BaseTool = class { // Zod requires any for type parameters get inputSchema() { return zodToJsonSchema(this.schema); } async handler(params) { try { const validatedParams = this.schema.parse(params); return await this.execute(validatedParams); } catch (error) { console.error(`[${this.name}] DEBUG: Error occurred:`, error); if (error instanceof z.ZodError) { const errorMessages = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", "); return { content: [ { type: "text", text: `\u274C **Validation Error** The provided parameters don't match the expected format: ${errorMessages} \u{1F4A1} **Tip:** Check the parameter types and ensure all required fields are provided. Use the tool description to see the correct format.` } ], isError: true }; } const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `\u274C **Error in ${this.name}** ${errorMessage} \u{1F4A1} **Debug Info:** - Tool: ${this.name} - Working Directory: ${process.cwd()} - Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()} If this error persists, please check: 1. File/directory paths are absolute and exist 2. Git repository is properly initialized 3. Required permissions are available` } ], isError: true }; } } }; // src/mcp/tools/CreateRepositoryAnchoredNoteTool.ts import { z as z2 } from "zod"; import { MemoryPalace } from "@a24z/core-library"; var CreateRepositoryAnchoredNoteTool = class extends BaseTool { name = "create_repository_note"; description = "Document tribal knowledge, architectural decisions, implementation patterns, and important lessons learned. This tool creates searchable notes that help future developers understand context and avoid repeating mistakes. Notes are stored locally in your repository and can be retrieved using the get_notes tool."; fs; constructor(fs2) { super(); this.fs = fs2; } schema = z2.object({ note: z2.string().describe( "The tribal knowledge content in Markdown format. Use code blocks with ``` for code snippets, **bold** for emphasis, and [file.ts](path/to/file.ts) for file references" ), directoryPath: z2.string().describe( "The absolute path to the git repository root directory (the directory containing .git). This determines which repository the note belongs to. Must be an absolute path starting with / and must point to a valid git repository root." ), anchors: z2.array(z2.string()).min(1, "At least one anchor path is required.").describe( "File or directory paths that this note relates to. These paths enable cross-referencing and help surface relevant notes when working in different parts of the codebase. Include all paths that should trigger this note to appear, such as the files or directories the note is about." ), tags: z2.array(z2.string()).min(1, "At least one tag is required.").describe( "Required semantic tags for categorization. Use get_repository_tags tool to see available tags. New tags will be created automatically if they don't exist." ), metadata: z2.record(z2.any()).optional().describe( "Additional structured data about the note. Can include custom fields like author, related PRs, issue numbers, or any other contextual information that might be useful for future reference." ), codebaseViewId: z2.string().optional().describe( "Optional CodebaseView ID to associate this note with. If not provided, a session view will be auto-created based on your file anchors." ) }); async execute(input) { const parsed = this.schema.parse(input); if (!this.fs.isAbsolute(parsed.directoryPath)) { throw new Error( `\u274C directoryPath must be an absolute path starting with '/'. Received relative path: "${parsed.directoryPath}". \u{1F4A1} Tip: Use absolute paths like /Users/username/projects/my-repo or /home/user/project. You can get the current working directory and build the absolute path from there.` ); } if (!this.fs.exists(parsed.directoryPath)) { throw new Error( `\u274C directoryPath does not exist: "${parsed.directoryPath}". \u{1F4A1} Tip: Make sure the path exists and you have read access to it. Check your current working directory and build the correct absolute path.` ); } const palace = new MemoryPalace(parsed.directoryPath, this.fs); const repositoryRoot = MemoryPalace.validateRepositoryPath(this.fs, parsed.directoryPath); const config = palace.getConfiguration(); const tagEnforcement = config.tags?.enforceAllowedTags || false; const existingTagDescriptions = palace.getTagDescriptions(); const existingTags = Object.keys(existingTagDescriptions); const newTags = parsed.tags.filter((tag) => !existingTags.includes(tag)); if (tagEnforcement && newTags.length > 0) { const allowedTags = existingTags; const tagList = allowedTags.length > 0 ? allowedTags.map((tag) => { const desc = existingTagDescriptions[tag]; return desc ? `\u2022 **${tag}**: ${desc.split("\n")[0].substring(0, 50)}...` : `\u2022 **${tag}**`; }).join("\n") : "No tags with descriptions exist yet."; throw new Error( `\u274C Tag creation is not allowed when tag enforcement is enabled. The following tags do not exist: ${newTags.join(", ")} **Available tags with descriptions:** ${tagList} \u{1F4A1} To use new tags, either: 1. Use one of the existing tags above 2. Ask an administrator to create the tag with a proper description 3. Disable tag enforcement in the repository configuration` ); } const autoCreatedTags = []; if (!tagEnforcement && newTags.length > 0) { for (const tag of newTags) { try { palace.saveTagDescription(tag, ""); autoCreatedTags.push(tag); } catch (error) { console.error(`Failed to auto-create tag description for "${tag}":`, error); } } } let view; let actualCodebaseViewId; if (parsed.codebaseViewId) { view = palace.getView(parsed.codebaseViewId); if (!view) { const availableViews = palace.listViews(); const viewsList = availableViews.length > 0 ? availableViews.map((v) => `\u2022 ${v.id}: ${v.name}`).join("\n") : "No views found. Please create a view first."; throw new Error( `\u274C CodebaseView with ID "${parsed.codebaseViewId}" not found. **Available views:** ${viewsList} \u{1F4A1} Tip: Use an existing view ID from the list above, or create a new view first.` ); } actualCodebaseViewId = parsed.codebaseViewId; } else { const defaultViewId = "default-explorer-log"; actualCodebaseViewId = defaultViewId; } const savedWithPath = palace.saveNote({ note: parsed.note, anchors: parsed.anchors, tags: parsed.tags, codebaseViewId: actualCodebaseViewId, metadata: { ...parsed.metadata || {}, toolVersion: "2.0.0", createdBy: "create_repository_note_tool" } }); const saved = savedWithPath.note; if (!parsed.codebaseViewId) { view = palace.getView(actualCodebaseViewId); if (!view) { view = this.createCatchallView(repositoryRoot, actualCodebaseViewId, saved.anchors); palace.saveView(view); this.generateOverviewFile(repositoryRoot, view); } else { this.updateCatchallViewWithTimeCell( repositoryRoot, actualCodebaseViewId, saved.anchors, palace ); view = palace.getView(actualCodebaseViewId); this.generateOverviewFile(repositoryRoot, view); } } else { view = palace.getView(actualCodebaseViewId); } let response = `\u2705 **Note saved successfully!** \u{1F194} **Note ID:** ${saved.id} \u{1F4C1} **Repository:** ${repositoryRoot} \u{1F4CA} **View:** ${view.name} (${actualCodebaseViewId}) \u{1F3F7}\uFE0F **Tags:** ${parsed.tags.join(", ")} `; if (!parsed.codebaseViewId) { response += `\u{1F504} **Default View:** Using time-based catchall view `; } return { content: [{ type: "text", text: response }] }; } /** * Create a catchall view with basic time-based configuration */ createCatchallView(repositoryPath, viewId, initialAnchors) { const now = /* @__PURE__ */ new Date(); const today = now.toISOString().split("T")[0]; const hour = now.getHours().toString().padStart(2, "0"); const cellName = `${today}-${hour}`; return { id: viewId, version: "1.0.0", name: "Default Exploration Log", description: "Time-based catchall view that grows with each note creation", category: "reference", // Exploration logs are reference material displayOrder: 0, // Will be auto-assigned when saved timestamp: (/* @__PURE__ */ new Date()).toISOString(), cells: { [cellName]: { files: initialAnchors, coordinates: [0, 0], priority: 5 } }, overviewPath: `docs/${viewId}.md`, metadata: { generationType: "user" } }; } /** * Add or update a time-based cell in the catchall view with note anchors */ updateCatchallViewWithTimeCell(repositoryPath, viewId, anchors, palace) { const view = palace.getView(viewId); if (!view) return; const now = /* @__PURE__ */ new Date(); const today = now.toISOString().split("T")[0]; const hour = now.getHours().toString().padStart(2, "0"); const cellName = `${today}-${hour}`; if (!view.cells[cellName]) { const existingCells = Object.values(view.cells); const maxRow = Math.max(0, ...existingCells.map((cell) => cell.coordinates[0])); const cellsInMaxRow = existingCells.filter((cell) => cell.coordinates[0] === maxRow); const maxColInRow = cellsInMaxRow.length > 0 ? Math.max(...cellsInMaxRow.map((cell) => cell.coordinates[1])) : -1; const nextCoordinates = maxColInRow >= 23 ? [maxRow + 1, 0] : [maxRow, maxColInRow + 1]; view.cells[cellName] = { files: anchors, coordinates: nextCoordinates, priority: 5 }; } else { const existingFiles = new Set(view.cells[cellName].files); const newAnchors = anchors.filter((anchor) => !existingFiles.has(anchor)); if (newAnchors.length > 0) { view.cells[cellName].files = [...view.cells[cellName].files, ...newAnchors]; } } palace.saveView(view); } /** * Generate or update the markdown overview file for a codebase view */ generateOverviewFile(repositoryPath, view) { const docsDir = this.fs.join(repositoryPath, "docs"); this.fs.createDir(docsDir); const content = this.generateOverviewContent(view); const overviewFilePath = this.fs.join(repositoryPath, view.overviewPath); this.fs.writeFile(overviewFilePath, content); } /** * Generate the markdown content for a codebase view overview */ generateOverviewContent(view) { let content = `# ${view.name} `; content += `${view.description} `; content += `**View ID:** \`${view.id}\` `; content += `**Generated:** ${new Date(view.timestamp || /* @__PURE__ */ new Date()).toLocaleString()} `; content += `**Type:** ${view.metadata?.generationType || "user"} `; const cellEntries = Object.entries(view.cells); if (cellEntries.length > 0) { content += `## Time-based Cells `; cellEntries.sort(([a], [b]) => a.localeCompare(b)); for (const [cellName, cell] of cellEntries) { content += `### ${cellName} `; if (cell.files && cell.files.length > 0) { content += `**Files:** `; for (const file of cell.files) { content += `- \`${file}\` `; } content += "\n"; } content += `**Coordinates:** [${cell.coordinates[0]}, ${cell.coordinates[1]}] `; content += `**Priority:** ${cell.priority} `; } } else { content += `## Cells *No cells defined yet.* `; } content += `--- `; content += `*This overview is automatically generated and updated when notes are added to this view.* `; return content; } }; // src/mcp/tools/GetAnchoredNotesTool.ts import { z as z3 } from "zod"; import { MemoryPalace as MemoryPalace2 } from "@a24z/core-library"; var GetAnchoredNotesTool = class extends BaseTool { name = "get_notes"; description = "Retrieve raw notes from the repository. Returns the actual note content, metadata, and anchors. Use this when you want to see the exact notes stored, browse through knowledge, or need the raw data for further processing."; fs; constructor(fs2) { super(); this.fs = fs2; } schema = z3.object({ path: z3.string().describe( "The absolute path to a file or directory to get notes for. Notes from this path and parent directories will be included. Must be an absolute path starting with /" ), includeParentNotes: z3.boolean().optional().default(true).describe( "Whether to include notes from parent directories. Set to false to only get notes directly anchored to the specified path" ), filterTags: z3.array(z3.string()).optional().describe( "Filter results to only notes containing at least one of these tags. Leave empty to include all tags" ), filterReviewed: z3.enum(["reviewed", "unreviewed", "all"]).optional().default("all").describe( 'Filter by review status. "reviewed" = only reviewed notes, "unreviewed" = only unreviewed notes, "all" = include both' ), includeStale: z3.boolean().optional().default(true).describe( "Whether to include notes with stale anchors (files that no longer exist). Set to false to exclude stale notes" ), sortBy: z3.enum(["timestamp", "reviewed", "type", "relevance"]).optional().default("timestamp").describe( 'How to sort the results. "timestamp" = newest first, "reviewed" = reviewed notes first, "type" = grouped by type, "relevance" = by path proximity' ), limit: z3.number().optional().default(50).describe( "Maximum number of notes to return. Use a smaller number for preview, larger for comprehensive retrieval" ), offset: z3.number().optional().default(0).describe( "Number of notes to skip for pagination. Use with limit to paginate through large result sets" ), includeMetadata: z3.boolean().optional().default(true).describe( "Whether to include full metadata for each note. Set to false for a more compact response" ) }); async execute(input) { const parsed = this.schema.parse(input); if (!this.fs.isAbsolute(parsed.path)) { return { content: [ { type: "text", text: `\u274C Error: Path must be absolute. Received: "${parsed.path}"` } ] }; } let repoRoot; try { repoRoot = this.fs.normalizeRepositoryPath(parsed.path); } catch { return { content: [ { type: "text", text: `\u274C Error: Could not find repository for path "${parsed.path}". Make sure the path is within a git repository.` } ] }; } const validatedRepoPath = MemoryPalace2.validateRepositoryPath(this.fs, repoRoot); const memoryPalace = new MemoryPalace2(validatedRepoPath, this.fs); const validatedRelativePath = MemoryPalace2.validateRelativePath( validatedRepoPath, parsed.path, this.fs ); let allNotesWithPath = memoryPalace.getNotesForPath( validatedRelativePath, parsed.includeParentNotes ); let allNotes = allNotesWithPath.map((noteWithPath) => noteWithPath.note); let staleNoteMap = /* @__PURE__ */ new Map(); if (!parsed.includeStale) { const staleNotes = memoryPalace.getStaleNotes(); for (const staleNote of staleNotes) { staleNoteMap.set(staleNote.note.id, staleNote.staleAnchors); } allNotes = allNotes.filter((note) => { const staleAnchors = staleNoteMap.get(note.id); return !staleAnchors || staleAnchors.length < note.anchors.length; }); } else { const staleNotes = memoryPalace.getStaleNotes(); for (const staleNote of staleNotes) { if (staleNote.staleAnchors.length > 0) { staleNoteMap.set(staleNote.note.id, staleNote.staleAnchors); } } } const availableTags = /* @__PURE__ */ new Set(); let totalReviewedCount = 0; let totalUnreviewedCount = 0; for (const note of allNotes) { note.tags.forEach((tag) => availableTags.add(tag)); if (note.reviewed) { totalReviewedCount++; } else { totalUnreviewedCount++; } } let filteredNotes = [...allNotes]; if (parsed.filterTags && parsed.filterTags.length > 0) { filteredNotes = filteredNotes.filter( (note) => parsed.filterTags.some((tag) => note.tags.includes(tag)) ); } if (parsed.filterReviewed !== "all") { filteredNotes = filteredNotes.filter( (note) => parsed.filterReviewed === "reviewed" ? note.reviewed : !note.reviewed ); } filteredNotes.sort((a, b) => { switch (parsed.sortBy) { case "timestamp": return b.timestamp - a.timestamp; // Newest first case "reviewed": if (a.reviewed === b.reviewed) { return b.timestamp - a.timestamp; } return a.reviewed ? -1 : 1; case "type": return b.timestamp - a.timestamp; case "relevance": { const aIsExact = a.anchors.some( (anchor) => this.fs.join(repoRoot, anchor) === parsed.path ); const bIsExact = b.anchors.some( (anchor) => this.fs.join(repoRoot, anchor) === parsed.path ); if (aIsExact !== bIsExact) { return aIsExact ? -1 : 1; } return b.timestamp - a.timestamp; } default: return b.timestamp - a.timestamp; } }); const total = filteredNotes.length; const start = parsed.offset; const end = Math.min(start + parsed.limit, total); const paginatedNotes = filteredNotes.slice(start, end); const formattedNotes = paginatedNotes.map((note) => { const formatted = { id: note.id, note: note.note, anchors: note.anchors, tags: note.tags, timestamp: note.timestamp, reviewed: note.reviewed || false }; if (parsed.includeMetadata) { formatted.metadata = note.metadata; } const staleAnchors = staleNoteMap.get(note.id); if (staleAnchors && staleAnchors.length > 0) { formatted.staleAnchors = staleAnchors; } return formatted; }); const response = { notes: formattedNotes, pagination: { total, returned: formattedNotes.length, offset: parsed.offset, limit: parsed.limit, hasMore: end < total }, filters: { applied: { tags: parsed.filterTags, reviewStatus: parsed.filterReviewed, includeStale: parsed.includeStale, includeParentNotes: parsed.includeParentNotes }, available: { tags: Array.from(availableTags).sort(), reviewedCount: totalReviewedCount, unreviewedCount: totalUnreviewedCount } } }; return { content: [ { type: "text", text: JSON.stringify(response, null, 2) } ] }; } }; // src/mcp/tools/GetRepositoryTagsTool.ts import { z as z4 } from "zod"; import { MemoryPalace as MemoryPalace3 } from "@a24z/core-library"; import { ALEXANDRIA_DIRS } from "@a24z/core-library"; var GetRepositoryTagsTool = class extends BaseTool { name = "get_repository_tags"; description = "Get available tags for categorizing notes in a repository path, including repository-specific guidance"; fs; constructor(fs2) { super(); this.fs = fs2; } schema = z4.object({ path: z4.string().describe( "The file or directory path to get tags for. Can be any path within the repository - the tool will find the repository root and analyze notes." ), includeUsedTags: z4.boolean().optional().default(true).describe( "Include tags that have been used in existing notes for this repository. Helps maintain consistency with established tagging patterns." ), includeGuidance: z4.boolean().optional().default(true).describe( `Include repository-specific note guidance. Shows either custom guidance from ${ALEXANDRIA_DIRS.PRIMARY}/note-guidance.md or falls back to default guidance.` ) }); async execute(input) { let repositoryRoot; try { repositoryRoot = this.fs.normalizeRepositoryPath(input.path); } catch { throw new Error("Not a git repository (or any parent up to mount point)"); } const memoryPalace = new MemoryPalace3(repositoryRoot, this.fs); const result = { success: true, path: input.path }; const tagDescriptions = memoryPalace.getTagDescriptions(); const config = memoryPalace.getConfiguration(); const enforced = config.tags?.enforceAllowedTags || false; const allowedTags = Object.keys(tagDescriptions); if (enforced && allowedTags.length > 0) { const allowedTagsWithDescriptions = allowedTags.map((tagName) => ({ name: tagName, description: tagDescriptions[tagName] })); result.tagRestrictions = { enforced: true, allowedTags: allowedTagsWithDescriptions, message: "This repository enforces tag restrictions. Only the allowed tags listed above can be used for notes." }; } else { result.tagRestrictions = { enforced: false, message: "This repository does not enforce tag restrictions. Any tags can be used." }; } if (input.includeUsedTags !== false) { const usedTags = memoryPalace.getUsedTags(); result.usedTags = usedTags.map((tagName) => ({ name: tagName, description: tagDescriptions[tagName] })); } if (input.includeGuidance !== false) { const guidance = memoryPalace.getGuidance(); if (guidance) { result.repositoryGuidance = guidance; } else { result.guidanceNote = `No repository-specific guidance found. Consider creating a note-guidance.md file in your ${ALEXANDRIA_DIRS.PRIMARY} directory to help team members understand what types of notes are most valuable for this project.`; } } return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } }; // src/mcp/tools/GetRepositoryGuidanceTool.ts import { z as z5 } from "zod"; import { MemoryPalace as MemoryPalace4 } from "@a24z/core-library"; var GetRepositoryGuidanceTool = class extends BaseTool { name = "get_repository_guidance"; description = "Get comprehensive repository configuration including note guidance, tag restrictions, and tag descriptions"; fs; constructor(fs2) { super(); this.fs = fs2; } schema = z5.object({ path: z5.string().describe( "The file or directory path to get guidance for. Can be any path within the repository - the tool will find the repository root and provide comprehensive configuration." ) }); async execute(input) { if (!this.fs.exists(input.path)) { throw new Error(`Path does not exist: ${input.path}`); } let repoRoot; try { repoRoot = this.fs.normalizeRepositoryPath(input.path); } catch { throw new Error(`Not a git repository: ${input.path}. This tool requires a git repository.`); } const memoryPalace = new MemoryPalace4(repoRoot, this.fs); const fullContent = memoryPalace.getFullGuidance(); const result = { content: [ { type: "text", text: JSON.stringify(fullContent, null, 2) } ] }; return result; } }; // src/mcp/tools/GetAnchoredNoteByIdTool.ts import { z as z6 } from "zod"; import { MemoryPalace as MemoryPalace5 } from "@a24z/core-library"; import { NodeFileSystemAdapter } from "@a24z/core-library"; var GetAnchoredNoteByIdTool = class extends BaseTool { constructor(fs2 = new NodeFileSystemAdapter()) { super(); this.fs = fs2; } name = "get_repository_note"; description = "Get a repository note by its unique ID"; schema = z6.object({ noteId: z6.string().describe('The unique ID of the note to retrieve (e.g., "note-1734567890123-abc123def")'), directoryPath: z6.string().describe( "The absolute path to the git repository root directory or any path within it. The tool will find the repository root automatically." ) }); async execute(input) { const parsed = this.schema.parse(input); if (!this.fs.isAbsolute(parsed.directoryPath)) { throw new Error( `directoryPath must be an absolute path. Received relative path: "${parsed.directoryPath}". Please provide the full absolute path (e.g., /Users/username/projects/my-repo).` ); } if (!await this.fs.exists(parsed.directoryPath)) { throw new Error( `directoryPath does not exist: "${parsed.directoryPath}". Please provide a valid absolute path to an existing directory.` ); } let gitRoot; try { gitRoot = this.fs.normalizeRepositoryPath(parsed.directoryPath); } catch { throw new Error( `directoryPath is not within a git repository: "${parsed.directoryPath}". Please provide a path within a git repository.` ); } const memoryPalace = new MemoryPalace5(gitRoot, this.fs); const note = memoryPalace.getNoteById(parsed.noteId); if (!note) { throw new Error( `Note with ID "${parsed.noteId}" not found in repository "${gitRoot}". Please verify the note ID is correct.` ); } const output = [ `# Note ID: ${note.id}`, "", `**Tags:** ${note.tags.join(", ")}`, `**Created:** ${new Date(note.timestamp).toISOString()}`, "", "## Anchors", ...note.anchors.map((anchor) => `- ${anchor}`), "", "## Content", note.note ]; if (note.metadata && Object.keys(note.metadata).length > 0) { output.push("", "## Metadata"); for (const [key, value] of Object.entries(note.metadata)) { output.push(`- **${key}:** ${JSON.stringify(value)}`); } } return { content: [ { type: "text", text: output.join("\n") } ] }; } }; // src/mcp/tools/DeleteAnchoredNoteTool.ts import { z as z7 } from "zod"; import { MemoryPalace as MemoryPalace6 } from "@a24z/core-library"; var DeleteAnchoredNoteTool = class extends BaseTool { name = "delete_repository_note"; description = "Delete a repository note by its ID"; fs; constructor(fs2) { super(); this.fs = fs2; } schema = z7.object({ noteId: z7.string().describe('The unique ID of the note to delete (e.g., "note-1734567890123-abc123def")'), directoryPath: z7.string().describe( "The absolute path to the git repository root directory or any path within it. The tool will find the repository root automatically." ) }); async execute(input) { const parsed = this.schema.parse(input); if (!this.fs.isAbsolute(parsed.directoryPath)) { throw new Error( `directoryPath must be an absolute path. Received relative path: "${parsed.directoryPath}". Please provide the full absolute path (e.g., /Users/username/projects/my-repo).` ); } if (!this.fs.exists(parsed.directoryPath)) { throw new Error( `directoryPath does not exist: "${parsed.directoryPath}". Please provide a valid absolute path to an existing directory.` ); } let gitRoot; try { gitRoot = this.fs.normalizeRepositoryPath(parsed.directoryPath); } catch { throw new Error( `directoryPath is not within a git repository: "${parsed.directoryPath}". Please provide a path within a git repository.` ); } const memoryPalace = new MemoryPalace6(gitRoot, this.fs); const note = memoryPalace.getNoteById(parsed.noteId); if (!note) { throw new Error( `Note with ID "${parsed.noteId}" not found in repository "${gitRoot}". Please verify the note ID is correct.` ); } const deleted = memoryPalace.deleteNoteById(parsed.noteId); if (deleted) { return { content: [ { type: "text", text: `Successfully deleted note ${parsed.noteId} Deleted note preview: | Tags: ${note.tags.join(", ")} ${note.note.substring(0, 200)}${note.note.length > 200 ? "..." : ""}` } ] }; } else { throw new Error( `Failed to delete note ${parsed.noteId}. The note may have already been deleted.` ); } } }; // src/mcp/tools/GetStaleAnchoredNotesTool.ts import { z as z8 } from "zod"; import { MemoryPalace as MemoryPalace7 } from "@a24z/core-library"; var GetStaleAnchoredNotesSchema = z8.object({ directoryPath: z8.string().describe( "The absolute path to the git repository root directory or any path within it. The tool will find the repository root automatically." ), includeContent: z8.boolean().default(true).describe( "Whether to include the full note content in the response. Set to false for a more compact output." ), includeValidAnchors: z8.boolean().default(false).describe( "Whether to include the list of valid anchors (files that still exist) for each note." ) }); var GetStaleAnchoredNotesTool = class extends BaseTool { name = "get_stale_notes"; description = "Get all notes that have stale anchors (references to files that no longer exist) in a repository"; schema = GetStaleAnchoredNotesSchema; fs; constructor(fs2) { super(); this.fs = fs2; } async execute(input) { const parsed = this.schema.parse(input); const { directoryPath, includeContent, includeValidAnchors } = parsed; if (!this.fs.exists(directoryPath)) { throw new Error(`Path does not exist: ${directoryPath}`); } let repoRoot; try { repoRoot = this.fs.normalizeRepositoryPath(directoryPath); } catch { throw new Error( `Not a git repository: ${directoryPath}. This tool requires a git repository.` ); } const memoryPalace = new MemoryPalace7(repoRoot, this.fs); const staleNotes = memoryPalace.getStaleNotes(); if (staleNotes.length === 0) { return { content: [ { type: "text", text: `No notes with stale anchors found in repository: ${repoRoot}` } ] }; } const formattedNotes = staleNotes.map( (staleNote) => { const formatted = { noteId: staleNote.note.id, tags: staleNote.note.tags, staleAnchors: staleNote.staleAnchors, timestamp: staleNote.note.timestamp }; if (includeValidAnchors) { formatted.validAnchors = staleNote.validAnchors; } if (includeContent) { formatted.content = staleNote.note.note; } if (staleNote.note.metadata && Object.keys(staleNote.note.metadata).length > 0) { formatted.metadata = staleNote.note.metadata; } return formatted; } ); formattedNotes.sort((a, b) => b.timestamp - a.timestamp); const totalStaleAnchors = staleNotes.reduce( (sum, note) => sum + note.staleAnchors.length, 0 ); const totalValidAnchors = staleNotes.reduce( (sum, note) => sum + note.validAnchors.length, 0 ); const response = { repository: repoRoot, totalStaleNotes: staleNotes.length, totalStaleAnchors, totalValidAnchors, notes: formattedNotes, recommendation: "Consider updating or removing these notes as their referenced files no longer exist. Use delete_repository_note to remove them or update their anchors to point to existing files." }; return { content: [ { type: "text", text: JSON.stringify(response, null, 2) } ] }; } }; // src/mcp/tools/GetTagUsageTool.ts import { z as z9 } from "zod"; import { MemoryPalace as MemoryPalace8 } from "@a24z/core-library"; import { NodeFileSystemAdapter as NodeFileSystemAdapter2 } from "@a24z/core-library"; var GetTagUsageSchema = z9.object({ directoryPath: z9.string().describe( "The absolute path to the git repository root directory or any path within it. The tool will find the repository root automatically." ), filterTags: z9.array(z9.string()).optional().describe( "Optional list of specific tags to get usage for. If not provided, all tags will be analyzed." ), includeNoteIds: z9.boolean().default(false).describe("Whether to include the list of note IDs that use each tag."), includeDescriptions: z9.boolean().default(true).describe("Whether to include tag descriptions in the output.") }); var GetTagUsageTool = class extends BaseTool { constructor(fs2 = new NodeFileSystemAdapter2()) { super(); this.fs = fs2; } name = "get_tag_usage"; description = "Get comprehensive usage statistics for tags in a repository, showing which tags are used, how often, and whether they have descriptions"; schema = GetTagUsageSchema; async execute(input) { const parsed = this.schema.parse(input); const { directoryPath, filterTags, includeNoteIds, includeDescriptions } = parsed; let repoRoot; try { repoRoot = this.fs.normalizeRepositoryPath(directoryPath); } catch { throw new Error( `Not a git repository: ${directoryPath}. This tool requires a git repository.` ); } const memoryPalace = new MemoryPalace8(repoRoot, this.fs); const tagDescriptions = memoryPalace.getTagDescriptions(); const allNotes = memoryPalace.getNotes(true); const tagUsageMap = /* @__PURE__ */ new Map(); for (const noteWithPath of allNotes) { const note = noteWithPath.note; for (const tag of note.tags || []) { if (!tagUsageMap.has(tag)) { tagUsageMap.set(tag, /* @__PURE__ */ new Set()); } tagUsageMap.get(tag).add(note.id); } } const allTags = /* @__PURE__ */ new Set([...Object.keys(tagDescriptions), ...tagUsageMap.keys()]); const tagsToAnalyze = filterTags ? filterTags.filter((tag) => allTags.has(tag)) : Array.from(allTags); const tagUsageInfo = tagsToAnalyze.map((tag) => { const noteIds = tagUsageMap.get(tag) || /* @__PURE__ */ new Set(); const info = { tag, usageCount: noteIds.size, hasDescription: tag in tagDescriptions }; if (includeDescriptions && tag in tagDescriptions) { info.description = tagDescriptions[tag]; } if (includeNoteIds && noteIds.size > 0) { info.noteIds = Array.from(noteIds); } return info; }).sort((a, b) => { if (a.usageCount !== b.usageCount) { return b.usageCount - a.usageCount; } return a.tag.localeCompare(b.tag); }); const totalTags = tagUsageInfo.length; const usedTags = tagUsageInfo.filter((t) => t.usageCount > 0).length; const unusedTags = tagUsageInfo.filter((t) => t.usageCount === 0).length; const tagsWithDescriptions = tagUsageInfo.filter((t) => t.hasDescription).length; const tagsWithoutDescriptions = tagUsageInfo.filter( (t) => !t.hasDescription && t.usageCount > 0 ).length; const response = { repository: repoRoot, statistics: { totalTags, usedTags, unusedTags, tagsWithDescriptions, tagsWithoutDescriptions }, tags: tagUsageInfo, recommendations: [] }; if (unusedTags > 0) { response.recommendations.push( `Found ${unusedTags} unused tag(s) that could be deleted to keep the repository clean.` ); } if (tagsWithoutDescriptions > 0) { response.recommendations.push( `Found ${tagsWithoutDescriptions} tag(s) being used without descriptions. Consider adding descriptions for better documentation.` ); } return { content: [ { type: "text", text: JSON.stringify(response, null, 2) } ] }; } }; // src/mcp/tools/DeleteTagTool.ts import { z as z10 } from "zod"; import { MemoryPalace as MemoryPalace9 } from "@a24z/core-library"; import { NodeFileSystemAdapter as NodeFileSystemAdapter3 } from "@a24z/core-library"; var DeleteTagSchema = z10.object({ directoryPath: z10.string().describe( "The absolute path to the git repository root directory or any path within it. The tool will find the repository root automatically." ), tag: z10.string().describe("The tag name to delete from the repository."), confirmDeletion: z10.boolean().optional().default(false).describe( "Must be set to true to confirm the deletion. This is a safety measure to prevent accidental deletions." ) }); var DeleteTagTool = class extends BaseTool { constructor(fs2 = new NodeFileSystemAdapter3()) { super(); this.fs = fs2; } name = "delete_tag"; description = "Delete a tag from the repository, removing it from all notes and deleting its description"; schema = DeleteTagSchema; async execute(input) { const parsed = this.schema.parse(input); const { directoryPath, tag, confirmDeletion } = parsed; if (!confirmDeletion) { return { content: [ { type: "text", text: JSON.stringify( { error: "Deletion not confirmed", message: "Set confirmDeletion to true to delete this tag.", tagToDelete: tag, warning: "This will remove the tag from all notes and delete its description." }, null, 2 ) } ] }; } let repoRoot; try { repoRoot = this.fs.normalizeRepositoryPath(directoryPath); } catch { throw new Error( `Not a git repository: ${directoryPath}. This tool requires a git repository.` ); } const memoryPalace = new MemoryPalace9(repoRoot, this.fs); const tagDescriptions = memoryPalace.getTagDescriptions(); const hadDescription = tag in tagDescriptions; const descriptionContent = hadDescription ? tagDescriptions[tag] : null; const notesModified = memoryPalace.removeTagFromNotes(tag); let descriptionDeleted = false; if (hadDescription) { descriptionDeleted = memoryPalace.deleteTagDescription(tag); } const response = { repository: repoRoot, tag, results: { notesModified, descriptionDeleted, hadDescription }, summary: "" }; if (notesModified === 0 && !hadDescription) { response.summary = `Tag '${tag}' was not found in any notes and had no description. Nothing was deleted.`; } else { const actions = []; if (notesModified > 0) { actions.push(`removed from ${notesModified} note(s)`); } if (descriptionDeleted) { actions.push("description file deleted"); } response.summary = `Tag '${tag}' successfully ${actions.join(" and ")}.`; } if (descriptionContent) { const responseWithDesc = response; responseWithDesc.deletedDescription = descriptionContent; } return { content: [ { type: "text", text: JSON.stringify(response, null, 2) } ] }; } }; // src/mcp/tools/ReplaceTagTool.ts import { z as z11 } from "zod"; import { MemoryPalace as MemoryPalace10 } from "@a24z/core-library"; import { NodeFileSystemAdapter as NodeFileSystemAdapter4 } from "@a24z/core-library"; import path from "path"; import { existsSync } from "fs"; var ReplaceTagSchema = z11.object({ directoryPath: z11.string().describe( "The absolute path to the git repository root directory or any path within it. The tool will find the repository root automatically." ), oldTag: z11.string().describe("The tag name to replace in all notes."), newTag: z11.string().describe("The new tag name to replace the old tag with."), confirmReplacement: z11.boolean().optional().default(false).describe( "Must be set to true to confirm the replacement. This is a safety measure to prevent accidental replacements." ), transferDescription: z11.boolean().optional().default(true).describe( "Whether to transfer the description from the old tag to the new tag. If the new tag already has a description, it will not be overwritten unless this is set to false." ) }); var ReplaceTagTool = class extends BaseTool { name = "replace_tag"; description = "Replace a tag with another tag across all notes in the repository. This tool updates all notes that have the old tag to use the new tag instead."; schema = ReplaceTagSchema; // Allow injection of a custom filesystem adapter for testing fsAdapter; constructor(fsAdapter) { super(); this.fsAdapter = fsAdapter; } async execute(input) { const parsed = this.schema.parse(input); const { directoryPath, oldTag, newTag, confirmReplacement, transferDescription } = parsed; if (!confirmReplacement) { return { content: [ { type: "text", text: JSON.stringify( { error: "Replacement not confirmed", message: "Set confirmReplacement to true to replace this tag.", oldTag, newTag, warning: `This will replace '${oldTag}' with '${newTag}' in all notes.` }, null, 2 ) } ] }; } if (oldTag === newTag) { return { content: [ { type: "text", text: JSON.stringify( { error: "Invalid replacement", message: "The old tag and new tag must be different.", oldTag, newTag }, null, 2 ) } ] }; } const nodeFs = this.fsAdapter || new NodeFileSystemAdapter4(); let repoRoot; if (this.fsAdapter) { repoRoot = directoryPath; if (!nodeFs.exists(repoRoot)) { throw new Error(`Path does not exist: ${repoRoot}`); } if (!nodeFs.exists(nodeFs.join(repoRoot, ".git"))) { throw new Error(`Not a git repository: ${repoRoot}. This tool requires a git repository.`); } } else { const normalizedPath = path.resolve(directoryPath); if (!existsSync(normalizedPath)) { throw new Error(`Path does not exist: ${normalizedPath}`); } try { repoRoot = nodeFs.normalizeRepositoryPath(normalizedPath); } catch { throw new Error( `Not a git repository: ${normalizedPath}. This tool requires a git repository.` ); } } const memoryPalace = new MemoryPalace10(repoRoot, nodeFs); const tagDescriptions = memoryPalace.getTagDescriptions(); const oldTagHasDescription = oldTag in tagDescriptions; const oldTagDescription = oldTagHasDescription ? tagDescriptions[oldTag] : null; const newTagHasDescription = newTag in tagDescriptions; const newTagDescription = newTagHasDescription ? tagDescriptions[newTag] : null; const notesModified = memoryPalace.replaceTagInNotes(oldTag, newTag); let descriptionTransferred = false; let oldDescriptionDeleted = false; let descriptionAction = "none"; if (transferDescription && oldTagHasDescription) { if (!newTagHasDescription) { memoryPalace.saveTagDescription(newTag, oldTagDescription); memoryPalace.deleteTagDescription(oldTag); descriptionTransferred = true; oldDescriptionDeleted = true; descriptionAction = "transferred"; } else { memoryPalace.deleteTagDescription(oldTag); oldDescriptionDeleted = true; descriptionAction = "kept_existing"; } } else if (oldTagHasDescription) { memoryPalace.deleteTagDescription(oldTag); oldDescriptionDeleted = true; descriptionAction = "deleted"; } const response = { repository: repoRoot, oldTag, newTag, results: { notesModified, oldTagHadDescription: oldTagHasDescription, newTagHadDescription: newTagHasDescription, descriptionTransferred, oldDescriptionDeleted, descriptionAction }, summary: "", descriptions: {} }; if (oldTagDescription) { response.descriptions.oldTagDescription = oldTagDescription; } if (newTagDescription) { response.descriptions.existingNewTagDescription = newTagDescription; } if (descriptionTransferred) { response.descriptions.transferredDescription = oldTagDescription; } if (notesModified === 0) { response.summary = `No notes found with tag '${oldTag}'. Nothing was replaced.`; } else { const actions = [`replaced '${oldTag}' with '${newTag}' in ${notesModified} note(s)`]; if (descriptionAction === "transferred") { actions.push(`transferred description from '${oldTag}' to '${newTag}'`); } else if (descriptionAction === "kept_existing") { actions.push(`kept existing description for '${newTag}'`); } else if (descriptionAction === "deleted") { actions.push(`deleted description for '${oldTag}'`); } response.summary = `Successfully ${actions.join(" and ")}.`; } return { content: [ { type: "text", text: JSON.stringify(response, null, 2) } ] }; } }; // src/mcp/tools/GetAnchoredNoteCoverageTool.ts import { z as z12 } from "zod"; import { MemoryPalace as MemoryPalace11 } from "@a24z/core-library"; var inputSchema = z12.object({ path: z12.string().describe( "The absolute path to a git repository or any path within it to analyze coverage for" ), outputFormat: z12.enum(["markdown", "json", "summary"]).optional().default("markdown").describe("The format for the coverage report output"), includeDirectories: z12.boolean().optional().default(true).describe("Whether to include directory coverage in the analysis"), includeUncoveredFiles: z12.boolean().optional().default(false).describe("Whether to include the full list of uncovered files in the response"), maxStaleAnchoredNotes: z12.number().optional().default(10).describe("Maximum number of stale notes to include in the report"), fileTypeFilter: z12.array(z12.string()).optional().describe('Filter coverage analysis to specific file extensions (e.g., ["ts", "js", "py"])'), excludeDirectoryAnchors: z12.boolean().optional().default(false).describe( "Whether to exclude notes that are directly anchored to directories from coverage calculation" ) }); var GetAnchoredNoteCoverageTool = class extends BaseTool { name = "get_note_coverage"; description = "Analyze note coverage for a repository, showing which files have documentation and coverage statistics"; schema = inputSchema; fs; constructor(fs2) { super(); this.fs = fs2; } async execute(in