Files
Claude-Code-Workflow/ccw/src/tools/index.ts

385 lines
11 KiB
TypeScript

/**
* 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 codexLensLspMod from './codex-lens-lsp.js';
import * as vscodeLspMod from './vscode-lsp.js';
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<string, unknown>;
required?: string[];
};
execute: (params: Record<string, unknown>) => Promise<unknown>;
}
// Tool registry
const tools = new Map<string, LegacyTool>();
// Dashboard notification settings
const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
/**
* Notify dashboard of tool execution events (fire and forget)
*/
function notifyDashboard(data: Record<string, unknown>): 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<string, unknown>) => Promise<ToolResult<unknown>>;
}): 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<string, unknown>) => {
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<Omit<LegacyTool, 'execute'>> {
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<string, unknown>): {
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<string, unknown> = {}): 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<string, unknown>): Record<string, unknown> {
const sanitized: Record<string, unknown> = {};
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<string, unknown> = {},
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(codexLensLspMod));
registerTool(toLegacyTool(vscodeLspMod));
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 };
// Export CommandRegistry for direct import
export { CommandRegistry, createCommandRegistry, getAllCommandsSync, getCommandSync } from './command-registry.js';
export type { CommandMetadata, CommandSummary } from './command-registry.js';