/** * Tool Registry - MCP-like tool system for CCW * Provides tool discovery, validation, and execution */ import http from 'http'; import type { ToolSchema, ToolResult } from '../types/tool.js'; // Import TypeScript migrated tools (schema + handler) import * as editFileMod from './edit-file.js'; import * as writeFileMod from './write-file.js'; import * as getModulesByDepthMod from './get-modules-by-depth.js'; import * as classifyFoldersMod from './classify-folders.js'; import * as detectChangedModulesMod from './detect-changed-modules.js'; import * as discoverDesignFilesMod from './discover-design-files.js'; import * as generateModuleDocsMod from './generate-module-docs.js'; import * as convertTokensToCssMod from './convert-tokens-to-css.js'; import * as sessionManagerMod from './session-manager.js'; import * as cliExecutorMod from './cli-executor.js'; import * as smartSearchMod from './smart-search.js'; import { executeInitWithProgress } from './smart-search.js'; // codex_lens removed - functionality integrated into smart_search import * as readFileMod from './read-file.js'; import * as coreMemoryMod from './core-memory.js'; import * as contextCacheMod from './context-cache.js'; import * as skillContextLoaderMod from './skill-context-loader.js'; import type { ProgressInfo } from './codex-lens.js'; // Import legacy JS tools import { uiGeneratePreviewTool } from './ui-generate-preview.js'; import { uiInstantiatePrototypesTool } from './ui-instantiate-prototypes.js'; import { updateModuleClaudeTool } from './update-module-claude.js'; import { memoryQueueTool } from './memory-update-queue.js'; interface LegacyTool { name: string; description: string; parameters: { type: string; properties: Record; required?: string[]; }; execute: (params: Record) => Promise; } // Tool registry const tools = new Map(); // Dashboard notification settings const DASHBOARD_PORT = process.env.CCW_PORT || 3456; /** * Notify dashboard of tool execution events (fire and forget) */ function notifyDashboard(data: Record): void { const payload = JSON.stringify({ type: 'tool_execution', ...data, timestamp: new Date().toISOString() }); const req = http.request({ hostname: 'localhost', port: Number(DASHBOARD_PORT), path: '/api/hook', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } }); // Fire and forget - log errors only in debug mode req.on('error', (err) => { if (process.env.DEBUG) console.error('[Dashboard] Tool notification failed:', err.message); }); req.write(payload); req.end(); } /** * Convert new-style tool (schema + handler) to legacy format */ function toLegacyTool(mod: { schema: ToolSchema; handler: (params: Record) => Promise>; }): LegacyTool { return { name: mod.schema.name, description: mod.schema.description, parameters: { type: 'object', properties: mod.schema.inputSchema?.properties || {}, required: mod.schema.inputSchema?.required || [] }, execute: async (params: Record) => { const result = await mod.handler(params); if (!result.success) { throw new Error(result.error); } return result.result; } }; } /** * Register a tool in the registry */ function registerTool(tool: LegacyTool): void { if (!tool.name || !tool.execute) { throw new Error('Tool must have name and execute function'); } tools.set(tool.name, tool); } /** * Get all registered tools */ export function listTools(): Array> { return Array.from(tools.values()).map(tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters })); } /** * Get a specific tool by name */ export function getTool(name: string): LegacyTool | null { return tools.get(name) || null; } /** * Validate parameters against tool schema */ function validateParams(tool: LegacyTool, params: Record): { valid: boolean; errors: string[]; } { const errors: string[] = []; const schema = tool.parameters; if (!schema || !schema.properties) { return { valid: true, errors: [] }; } // Check required parameters const required = schema.required || []; for (const req of required) { if (params[req] === undefined || params[req] === null) { errors.push(`Missing required parameter: ${req}`); } } // Type validation for (const [key, value] of Object.entries(params)) { const propSchema = schema.properties[key] as { type?: string }; if (!propSchema) { continue; // Allow extra params } if (propSchema.type === 'string' && typeof value !== 'string') { errors.push(`Parameter '${key}' must be a string`); } if (propSchema.type === 'boolean' && typeof value !== 'boolean') { errors.push(`Parameter '${key}' must be a boolean`); } if (propSchema.type === 'number' && typeof value !== 'number') { errors.push(`Parameter '${key}' must be a number`); } } return { valid: errors.length === 0, errors }; } /** * Execute a tool with given parameters */ export async function executeTool(name: string, params: Record = {}): Promise<{ success: boolean; result?: unknown; error?: string; }> { const tool = tools.get(name); if (!tool) { return { success: false, error: `Tool not found: ${name}` }; } // Validate parameters const validation = validateParams(tool, params); if (!validation.valid) { return { success: false, error: `Parameter validation failed: ${validation.errors.join(', ')}` }; } // Notify dashboard - execution started notifyDashboard({ toolName: name, status: 'started', params: sanitizeParams(params) }); // Execute tool try { const result = await tool.execute(params); // Notify dashboard - execution completed notifyDashboard({ toolName: name, status: 'completed', result: sanitizeResult(result) }); return { success: true, result }; } catch (error) { // Notify dashboard - execution failed notifyDashboard({ toolName: name, status: 'failed', error: (error as Error).message || 'Tool execution failed' }); return { success: false, error: (error as Error).message || 'Tool execution failed' }; } } /** * Sanitize params for notification (truncate large values) */ function sanitizeParams(params: Record): Record { const sanitized: Record = {}; for (const [key, value] of Object.entries(params)) { if (typeof value === 'string' && value.length > 200) { sanitized[key] = value.substring(0, 200) + '...'; } else if (typeof value === 'object' && value !== null) { sanitized[key] = '[Object]'; } else { sanitized[key] = value; } } return sanitized; } /** * Sanitize result for notification (truncate large values) */ function sanitizeResult(result: unknown): unknown { if (result === null || result === undefined) return result; const str = JSON.stringify(result); if (str.length > 500) { return { _truncated: true, preview: str.substring(0, 500) + '...' }; } return result; } /** * Execute a tool with progress callback (for init actions) */ export async function executeToolWithProgress( name: string, params: Record = {}, onProgress?: (progress: ProgressInfo) => void ): Promise<{ success: boolean; result?: unknown; error?: string; }> { // For smart_search init, use special progress-aware execution if (name === 'smart_search' && params.action === 'init') { try { // Notify dashboard - execution started notifyDashboard({ toolName: name, status: 'started', params: sanitizeParams(params) }); const result = await executeInitWithProgress(params, onProgress); // Notify dashboard - execution completed notifyDashboard({ toolName: name, status: 'completed', result: sanitizeResult(result) }); return { success: result.success, result, error: result.error }; } catch (error) { notifyDashboard({ toolName: name, status: 'failed', error: (error as Error).message || 'Tool execution failed' }); return { success: false, error: (error as Error).message || 'Tool execution failed' }; } } // Fall back to regular execution for other tools return executeTool(name, params); } /** * Get tool schema in MCP-compatible format */ export function getToolSchema(name: string): ToolSchema | null { const tool = tools.get(name); if (!tool) return null; return { name: tool.name, description: tool.description, inputSchema: { type: 'object', properties: tool.parameters?.properties || {}, required: tool.parameters?.required || [] } }; } /** * Get all tool schemas in MCP-compatible format */ export function getAllToolSchemas(): ToolSchema[] { return Array.from(tools.keys()).map(name => getToolSchema(name)).filter((s): s is ToolSchema => s !== null); } // Register TypeScript migrated tools registerTool(toLegacyTool(editFileMod)); registerTool(toLegacyTool(writeFileMod)); registerTool(toLegacyTool(getModulesByDepthMod)); registerTool(toLegacyTool(classifyFoldersMod)); registerTool(toLegacyTool(detectChangedModulesMod)); registerTool(toLegacyTool(discoverDesignFilesMod)); registerTool(toLegacyTool(generateModuleDocsMod)); registerTool(toLegacyTool(convertTokensToCssMod)); registerTool(toLegacyTool(sessionManagerMod)); registerTool(toLegacyTool(cliExecutorMod)); registerTool(toLegacyTool(smartSearchMod)); // codex_lens removed - functionality integrated into smart_search registerTool(toLegacyTool(readFileMod)); registerTool(toLegacyTool(coreMemoryMod)); registerTool(toLegacyTool(contextCacheMod)); registerTool(toLegacyTool(skillContextLoaderMod)); // Register legacy JS tools registerTool(uiGeneratePreviewTool); registerTool(uiInstantiatePrototypesTool); registerTool(updateModuleClaudeTool); registerTool(memoryQueueTool); // Export for external tool registration export { registerTool }; // Export ToolSchema type export type { ToolSchema };