mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
385 lines
11 KiB
TypeScript
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';
|