import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { DATA_PATH } from './data-path'; type ToolInput = { type: string; description: string; values?: string; optional?: boolean; }; export type ToolDefinition = { name: string; label: string; description: string; language: string; inputs: Record; implementationPath: string; body: string; }; type ToolMeta = { name: string; label: string; description: string; language: string; inputs: Record; }; function parseFrontmatter(content: string): { meta: Partial; body: string } { const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); if (!match) return { meta: {}, body: content }; const yamlBlock = match[1]!; const body = match[2]!; const meta: Record = {}; const lines = yamlBlock.split('\n'); let currentKey: string | null = null; let currentObj: Record | null = null; let currentSubKey: string | null = null; let currentSubObj: Record | null = null; for (const line of lines) { const topMatch = line.match(/^(\w[\w-]*):\s*(.*)$/); if (topMatch && !line.startsWith(' ')) { if (currentSubObj && currentSubKey && currentObj) { currentObj[currentSubKey] = currentSubObj; currentSubObj = null; currentSubKey = null; } if (currentObj && currentKey) { meta[currentKey] = currentObj; currentObj = null; currentKey = null; } const [, key, value] = topMatch; if (!value || value.trim() === '') { currentKey = key!; currentObj = {}; } else { meta[key!] = value.trim(); } continue; } const midMatch = line.match(/^ (\w[\w-]*):\s*(.*)$/); if (midMatch && currentObj !== null) { if (currentSubObj && currentSubKey) { currentObj[currentSubKey] = currentSubObj; currentSubObj = null; currentSubKey = null; } const [, key, value] = midMatch; if (!value || value.trim() === '') { currentSubKey = key!; currentSubObj = {}; } else { currentObj[key!] = value.trim(); } continue; } const deepMatch = line.match(/^ (\w[\w-]*):\s*(.*)$/); if (deepMatch && currentSubObj !== null) { const [, key, value] = deepMatch; currentSubObj[key!] = value!.trim(); continue; } const arrayMatch = line.match(/^ - (.+)$/); if (arrayMatch && currentSubObj !== null) { const key = Object.keys(currentSubObj).at(-1); if (key) { const arr = currentSubObj[key]; if (Array.isArray(arr)) { arr.push(arrayMatch[1]!.trim()); } else { currentSubObj[key] = [arrayMatch[1]!.trim()]; } } } } if (currentSubObj && currentSubKey && currentObj) { currentObj[currentSubKey] = currentSubObj; } if (currentObj && currentKey) { meta[currentKey] = currentObj; } return { meta: meta as Partial, body }; } function discoverToolsInDir(dir: string): ToolDefinition[] { if (!existsSync(dir)) return []; const tools: ToolDefinition[] = []; const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const toolDir = join(dir, entry.name); const toolMdPath = join(toolDir, 'TOOL.md'); if (!existsSync(toolMdPath)) continue; const indexTs = join(toolDir, 'index.ts'); const indexJs = join(toolDir, 'index.js'); const entryFile = existsSync(indexTs) ? indexTs : existsSync(indexJs) ? indexJs : null; if (!entryFile) continue; const content = readFileSync(toolMdPath, 'utf-8'); const { meta, body } = parseFrontmatter(content); if (!meta.name || !meta.description) continue; tools.push({ name: meta.name, label: meta.label ?? meta.name, description: meta.description, language: meta.language ?? 'typescript', inputs: (meta.inputs ?? {}) as Record, implementationPath: entryFile, body, }); } return tools; } export function discoverTools(email: string): ToolDefinition[] { const globalDir = join(DATA_PATH, 'tools'); const userDir = join(DATA_PATH, email, 'tools'); const globalTools = discoverToolsInDir(globalDir); const userTools = discoverToolsInDir(userDir); // User tools override global tools with the same name const seen = new Map(); for (const tool of globalTools) { seen.set(tool.name, tool); } for (const tool of userTools) { seen.set(tool.name, tool); } return Array.from(seen.values()); }