From 675aff26ff9f8a5bcc0abc65e81eb48323f9ac0d Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 13 Dec 2025 17:28:03 +0800 Subject: [PATCH] feat(mcp): add read_file tool and simplify edit/write returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - edit_file: truncate diff to 15 lines, compact result format - write_file: return only path/bytes/message - read_file: new tool with multi-file, directory, regex support - paths: single file, array, or directory - pattern: glob filter (*.ts) - contentPattern: regex content search - maxDepth, maxFiles, includeContent options - Update tool-strategy.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/workflows/tool-strategy.md | 13 + ccw/src/commands/cli.ts | 74 ++++ ccw/src/core/server.ts | 87 +++-- ccw/src/mcp-server/index.ts | 2 +- .../templates/dashboard-css/07-managers.css | 250 +++++++++----- .../dashboard-js/components/cli-history.js | 25 +- .../components/global-notifications.js | 224 +++++++++--- .../dashboard-js/components/notifications.js | 95 ++++- .../components/task-queue-sidebar.js | 6 +- ccw/src/templates/dashboard-js/main.js | 2 +- .../dashboard-js/views/cli-manager.js | 107 +++++- .../templates/dashboard-js/views/history.js | 2 +- ccw/src/tools/cli-executor.ts | 66 ++-- ccw/src/tools/edit-file.ts | 50 ++- ccw/src/tools/index.ts | 2 + ccw/src/tools/read-file.ts | 325 ++++++++++++++++++ ccw/src/tools/write-file.ts | 26 +- 17 files changed, 1108 insertions(+), 248 deletions(-) create mode 100644 ccw/src/tools/read-file.ts diff --git a/.claude/workflows/tool-strategy.md b/.claude/workflows/tool-strategy.md index e283cbca..d707d5ee 100644 --- a/.claude/workflows/tool-strategy.md +++ b/.claude/workflows/tool-strategy.md @@ -37,6 +37,19 @@ mcp__ccw-tools__write_file(path="file.txt", content="code with `backticks` and $ **Options**: `backup`, `createDirectories`, `encoding` +### read_file + +**When to Use**: Read multiple files, directory traversal, content search + +``` +mcp__ccw-tools__read_file(paths="file.ts") # Single file +mcp__ccw-tools__read_file(paths=["a.ts", "b.ts"]) # Multiple files +mcp__ccw-tools__read_file(paths="src/", pattern="*.ts") # Directory + glob +mcp__ccw-tools__read_file(paths="src/", contentPattern="TODO") # Regex search +``` + +**Options**: `pattern`, `contentPattern`, `maxDepth` (3), `includeContent` (true), `maxFiles` (50) + ### codex_lens **When to Use**: Code indexing and semantic search diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index 00e23f5e..ffb1aa5c 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -4,6 +4,7 @@ */ import chalk from 'chalk'; +import http from 'http'; import { cliExecutorTool, getCliToolsStatus, @@ -12,6 +13,38 @@ import { getConversationDetail } from '../tools/cli-executor.js'; +// Dashboard notification settings +const DASHBOARD_PORT = process.env.CCW_PORT || 3456; + +/** + * Notify dashboard of CLI execution events (fire and forget) + */ +function notifyDashboard(data: Record): void { + const payload = JSON.stringify({ + type: 'cli_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] CLI notification failed:', err.message); + }); + req.write(payload); + req.end(); +} + interface CliExecOptions { tool?: string; mode?: string; @@ -88,6 +121,15 @@ async function execAction(prompt: string | undefined, options: CliExecOptions): console.log(); } + // Notify dashboard: execution started + notifyDashboard({ + event: 'started', + tool, + mode, + prompt_preview: prompt.substring(0, 100) + (prompt.length > 100 ? '...' : ''), + custom_id: id || null + }); + // Streaming output handler const onOutput = noStream ? null : (chunk: any) => { process.stdout.write(chunk.data); @@ -130,17 +172,49 @@ async function execAction(prompt: string | undefined, options: CliExecOptions): console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`)); } console.log(chalk.dim(` Continue: ccw cli exec "..." --resume ${result.execution.id}`)); + + // Notify dashboard: execution completed + notifyDashboard({ + event: 'completed', + tool, + mode, + execution_id: result.execution.id, + success: true, + duration_ms: result.execution.duration_ms, + turn_count: result.conversation.turn_count + }); } else { console.log(chalk.red(` ✗ Failed (${result.execution.status})`)); console.log(chalk.gray(` ID: ${result.execution.id}`)); if (result.stderr) { console.error(chalk.red(result.stderr)); } + + // Notify dashboard: execution failed + notifyDashboard({ + event: 'completed', + tool, + mode, + execution_id: result.execution.id, + success: false, + status: result.execution.status, + duration_ms: result.execution.duration_ms + }); + process.exit(1); } } catch (error) { const err = error as Error; console.error(chalk.red(` Error: ${err.message}`)); + + // Notify dashboard: execution error + notifyDashboard({ + event: 'error', + tool, + mode, + error: err.message + }); + process.exit(1); } } diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 10ab89eb..d7dcdb66 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -11,6 +11,7 @@ import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normali import { getCliToolsStatus, getExecutionHistory, getExecutionHistoryAsync, getExecutionDetail, getConversationDetail, deleteExecution, deleteExecutionAsync, batchDeleteExecutionsAsync, executeCliTool } from '../tools/cli-executor.js'; import { getAllManifests } from './manifest.js'; import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js'; +import { generateSmartContext, formatSmartContext } from '../tools/smart-context.js'; import { listTools } from '../tools/index.js'; import type { ServerConfig } from '../types/config.js';interface ServerOptions { port?: number; initialPath?: string; host?: string; open?: boolean;}interface PostResult { error?: string; status?: number; [key: string]: unknown;}type PostHandler = (body: unknown) => Promise; @@ -107,6 +108,7 @@ const MODULE_FILES = [ 'components/_review_tab.js', 'components/task-drawer-core.js', 'components/task-drawer-renderers.js', + 'components/task-queue-sidebar.js', 'components/flowchart.js', 'views/home.js', 'views/project-overview.js', @@ -635,38 +637,25 @@ export async function startServer(options: ServerOptions = {}): Promise { - const { storageBackend: backend } = body as { storageBackend?: string }; - - if (backend && (backend === 'sqlite' || backend === 'json')) { - // Import and set storage backend dynamically - try { - const { setStorageBackend } = await import('../tools/cli-executor.js'); - setStorageBackend(backend as 'sqlite' | 'json'); - return { success: true, storageBackend: backend }; - } catch (err) { - return { success: false, error: (err as Error).message }; - } - } - - return { success: true, message: 'No changes' }; - }); - return; - } - // API: CLI Execution History if (pathname === '/api/cli/history') { const projectPath = url.searchParams.get('path') || initialPath; const limit = parseInt(url.searchParams.get('limit') || '50', 10); const tool = url.searchParams.get('tool') || null; const status = url.searchParams.get('status') || null; + const search = url.searchParams.get('search') || null; const recursive = url.searchParams.get('recursive') !== 'false'; // Default true - const history = getExecutionHistory(projectPath, { limit, tool, status, recursive }); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(history)); + // Use async version to ensure SQLite is initialized + getExecutionHistoryAsync(projectPath, { limit, tool, status, search, recursive }) + .then(history => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(history)); + }) + .catch(err => { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + }); return; } @@ -683,14 +672,21 @@ export async function startServer(options: ServerOptions = {}): Promise { + if (result.success) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, message: 'Execution deleted' })); + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: result.error || 'Delete failed' })); + } + }) + .catch(err => { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + }); return; } @@ -725,12 +721,32 @@ export async function startServer(options: ServerOptions = {}): Promise { - const { tool, prompt, mode, model, dir, includeDirs, timeout } = body; + const { tool, prompt, mode, format, model, dir, includeDirs, timeout, smartContext } = body; if (!tool || !prompt) { return { error: 'tool and prompt are required', status: 400 }; } + // Generate smart context if enabled + let finalPrompt = prompt; + if (smartContext?.enabled) { + try { + const contextResult = await generateSmartContext(prompt, { + enabled: true, + maxFiles: smartContext.maxFiles || 10, + searchMode: 'text' + }, dir || initialPath); + + const contextAppendage = formatSmartContext(contextResult); + if (contextAppendage) { + finalPrompt = prompt + contextAppendage; + } + } catch (err) { + console.warn('[Smart Context] Failed to generate:', err); + // Continue without smart context + } + } + // Start execution const executionId = `${Date.now()}-${tool}`; @@ -749,10 +765,11 @@ export async function startServer(options: ServerOptions = {}): Promise exec.id !== executionId); - renderCliHistory(); + // Reload fresh data from server and re-render + await loadCliHistory(); + + // Render appropriate view based on current view + if (typeof currentView !== 'undefined' && (currentView === 'history' || currentView === 'cli-history')) { + renderCliHistoryView(); + } else { + renderCliHistory(); + } showRefreshToast('Execution deleted', 'success'); } catch (err) { console.error('Failed to delete execution:', err); diff --git a/ccw/src/templates/dashboard-js/components/global-notifications.js b/ccw/src/templates/dashboard-js/components/global-notifications.js index 238d4afd..d5785fa8 100644 --- a/ccw/src/templates/dashboard-js/components/global-notifications.js +++ b/ccw/src/templates/dashboard-js/components/global-notifications.js @@ -2,11 +2,26 @@ // GLOBAL NOTIFICATION SYSTEM - Right Sidebar // ========================================== // Right-side slide-out toolbar for notifications and quick actions +// Supports browser system notifications (cross-platform) + +// Notification settings +let notifSettings = { + systemNotifEnabled: false, + soundEnabled: false +}; /** * Initialize global notification sidebar */ function initGlobalNotifications() { + // Load settings from localStorage + loadNotifSettings(); + + // Request notification permission if enabled + if (notifSettings.systemNotifEnabled) { + requestNotificationPermission(); + } + // Create sidebar if not exists if (!document.getElementById('notifSidebar')) { const sidebarHtml = ` @@ -24,6 +39,19 @@ function initGlobalNotifications() { +
+ +
+
' + - '' + '
' + diff --git a/ccw/src/tools/cli-executor.ts b/ccw/src/tools/cli-executor.ts index faeb630c..e18a24a8 100644 --- a/ccw/src/tools/cli-executor.ts +++ b/ccw/src/tools/cli-executor.ts @@ -40,6 +40,7 @@ const ParamsSchema = z.object({ tool: z.enum(['gemini', 'qwen', 'codex']), prompt: z.string().min(1, 'Prompt is required'), mode: z.enum(['analysis', 'write', 'auto']).default('analysis'), + format: z.enum(['plain', 'yaml', 'json']).default('plain'), // Multi-turn prompt concatenation format model: z.string().optional(), cd: z.string().optional(), includeDirs: z.string().optional(), @@ -50,6 +51,9 @@ const ParamsSchema = z.object({ type Params = z.infer; +// Prompt concatenation format types +type PromptFormat = 'plain' | 'yaml' | 'json'; + interface ToolAvailability { available: boolean; path: string | null; @@ -247,21 +251,6 @@ function ensureHistoryDir(baseDir: string): string { return historyDir; } -/** - * Load history index - */ -function loadHistoryIndex(historyDir: string): HistoryIndex { - const indexPath = join(historyDir, 'index.json'); - if (existsSync(indexPath)) { - try { - return JSON.parse(readFileSync(indexPath, 'utf8')); - } catch { - return { version: 1, total_executions: 0, executions: [] }; - } - } - return { version: 1, total_executions: 0, executions: [] }; -} - /** * Save conversation to SQLite */ @@ -384,29 +373,25 @@ function mergeConversations(conversations: ConversationRecord[]): MergeResult { /** * Build prompt from merged conversations */ -function buildMergedPrompt(mergeResult: MergeResult, newPrompt: string): string { - const parts: string[] = []; +function buildMergedPrompt( + mergeResult: MergeResult, + newPrompt: string, + format: PromptFormat = 'plain' +): string { + const concatenator = createPromptConcatenator({ format }); - parts.push('=== MERGED CONVERSATION HISTORY ==='); - parts.push(`(From ${mergeResult.sourceConversations.length} conversations: ${mergeResult.sourceConversations.map(c => c.id).join(', ')})`); - parts.push(''); + // Set metadata for merged conversations + concatenator.setMetadata( + 'merged_sources', + mergeResult.sourceConversations.map(c => c.id).join(', ') + ); // Add all merged turns with source tracking for (const turn of mergeResult.mergedTurns) { - parts.push(`--- Turn ${turn.turn} [${turn.source_id}] ---`); - parts.push('USER:'); - parts.push(turn.prompt); - parts.push(''); - parts.push('ASSISTANT:'); - parts.push(turn.output.stdout || '[No output recorded]'); - parts.push(''); + concatenator.addFromConversationTurn(turn, turn.source_id); } - parts.push('=== NEW REQUEST ==='); - parts.push(''); - parts.push(newPrompt); - - return parts.join('\n'); + return concatenator.build(newPrompt); } /** @@ -421,7 +406,7 @@ async function executeCliTool( throw new Error(`Invalid params: ${parsed.error.message}`); } - const { tool, prompt, mode, model, cd, includeDirs, timeout, resume, id: customId } = parsed.data; + const { tool, prompt, mode, format, model, cd, includeDirs, timeout, resume, id: customId } = parsed.data; // Determine working directory early (needed for conversation lookup) const workingDir = cd || process.cwd(); @@ -505,11 +490,11 @@ async function executeCliTool( // For append: use existingConversation (from target ID) let finalPrompt = prompt; if (mergeResult && mergeResult.mergedTurns.length > 0) { - finalPrompt = buildMergedPrompt(mergeResult, prompt); + finalPrompt = buildMergedPrompt(mergeResult, prompt, format); } else { const conversationForContext = contextConversation || existingConversation; if (conversationForContext && conversationForContext.turns.length > 0) { - finalPrompt = buildMultiTurnPrompt(conversationForContext, prompt); + finalPrompt = buildMultiTurnPrompt(conversationForContext, prompt, format); } } @@ -845,9 +830,9 @@ function findCliHistoryDirs(baseDir: string, maxDepth: number = 3): string[] { function scanDir(dir: string, depth: number) { if (depth > maxDepth) return; - // Check if this directory has CLI history + // Check if this directory has CLI history (SQLite database) const historyDir = join(dir, '.workflow', '.cli-history'); - if (existsSync(join(historyDir, 'index.json'))) { + if (existsSync(join(historyDir, 'history.db'))) { historyDirs.push(historyDir); } @@ -1047,11 +1032,6 @@ export async function getCliToolsStatus(): Promise; +// Internal type for mode results (content excluded in final output) /** * Resolve file path and read content @@ -485,8 +488,30 @@ Options: dryRun=true (preview diff), replaceAll=true (replace all occurrences)`, }, }; +/** + * Truncate diff to max lines with indicator + */ +function truncateDiff(diff: string, maxLines: number): string { + if (!diff) return ''; + const lines = diff.split('\n'); + if (lines.length <= maxLines) return diff; + return lines.slice(0, maxLines).join('\n') + `\n... (+${lines.length - maxLines} more lines)`; +} + +/** + * Build compact result for output + */ +interface CompactEditResult { + path: string; + modified: boolean; + message: string; + replacements?: number; + diff?: string; + dryRun?: boolean; +} + // Handler function -export async function handler(params: Record): Promise> { +export async function handler(params: Record): Promise> { const parsed = ParamsSchema.safeParse(params); if (!parsed.success) { return { success: false, error: `Invalid params: ${parsed.error.message}` }; @@ -514,9 +539,24 @@ export async function handler(params: Record): Promise; + +interface FileEntry { + path: string; + size: number; + content?: string; + truncated?: boolean; + matches?: string[]; +} + +interface ReadResult { + files: FileEntry[]; + totalFiles: number; + message: string; +} + +// Common binary extensions to skip +const BINARY_EXTENSIONS = new Set([ + '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg', + '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', + '.zip', '.tar', '.gz', '.rar', '.7z', + '.exe', '.dll', '.so', '.dylib', + '.mp3', '.mp4', '.wav', '.avi', '.mov', + '.woff', '.woff2', '.ttf', '.eot', '.otf', + '.pyc', '.class', '.o', '.obj', +]); + +/** + * Check if file is likely binary + */ +function isBinaryFile(filePath: string): boolean { + const ext = extname(filePath).toLowerCase(); + return BINARY_EXTENSIONS.has(ext); +} + +/** + * Convert glob pattern to regex + */ +function globToRegex(pattern: string): RegExp { + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + return new RegExp(`^${escaped}$`, 'i'); +} + +/** + * Check if filename matches glob pattern + */ +function matchesPattern(filename: string, pattern: string): boolean { + const regex = globToRegex(pattern); + return regex.test(filename); +} + +/** + * Recursively collect files from directory + */ +function collectFiles( + dir: string, + pattern: string | undefined, + maxDepth: number, + currentDepth: number = 0 +): string[] { + if (currentDepth > maxDepth) return []; + + const files: string[] = []; + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + // Skip hidden files/dirs and node_modules + if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; + + const fullPath = join(dir, entry.name); + + if (entry.isDirectory()) { + files.push(...collectFiles(fullPath, pattern, maxDepth, currentDepth + 1)); + } else if (entry.isFile()) { + if (!pattern || matchesPattern(entry.name, pattern)) { + files.push(fullPath); + } + } + } + } catch { + // Skip directories we can't read + } + + return files; +} + +/** + * Read file content with truncation + */ +function readFileContent(filePath: string, maxLength: number): { content: string; truncated: boolean } { + if (isBinaryFile(filePath)) { + return { content: '[Binary file]', truncated: false }; + } + + try { + const content = readFileSync(filePath, 'utf8'); + if (content.length > maxLength) { + return { + content: content.substring(0, maxLength) + `\n... (+${content.length - maxLength} chars)`, + truncated: true + }; + } + return { content, truncated: false }; + } catch (error) { + return { content: `[Error: ${(error as Error).message}]`, truncated: false }; + } +} + +/** + * Find regex matches in content + */ +function findMatches(content: string, pattern: string): string[] { + try { + const regex = new RegExp(pattern, 'gm'); + const matches: string[] = []; + let match; + + while ((match = regex.exec(content)) !== null && matches.length < 10) { + // Get line containing match + const lineStart = content.lastIndexOf('\n', match.index) + 1; + const lineEnd = content.indexOf('\n', match.index); + const line = content.substring(lineStart, lineEnd === -1 ? undefined : lineEnd).trim(); + matches.push(line.substring(0, 200)); // Truncate long lines + } + + return matches; + } catch { + return []; + } +} + +// Tool schema for MCP +export const schema: ToolSchema = { + name: 'read_file', + description: `Read files with multi-file, directory, and regex support. + +Usage: + read_file(paths="file.ts") # Single file + read_file(paths=["a.ts", "b.ts"]) # Multiple files + read_file(paths="src/", pattern="*.ts") # Directory with pattern + read_file(paths="src/", contentPattern="TODO") # Search content + +Returns compact file list with optional content.`, + inputSchema: { + type: 'object', + properties: { + paths: { + oneOf: [ + { type: 'string', description: 'Single file or directory path' }, + { type: 'array', items: { type: 'string' }, description: 'Array of file paths' } + ], + description: 'File path(s) or directory to read', + }, + pattern: { + type: 'string', + description: 'Glob pattern to filter files (e.g., "*.ts", "*.{js,ts}")', + }, + contentPattern: { + type: 'string', + description: 'Regex pattern to search within file content', + }, + maxDepth: { + type: 'number', + description: 'Max directory depth to traverse (default: 3)', + default: 3, + }, + includeContent: { + type: 'boolean', + description: 'Include file content in result (default: true)', + default: true, + }, + maxFiles: { + type: 'number', + description: `Max number of files to return (default: ${MAX_FILES})`, + default: MAX_FILES, + }, + }, + required: ['paths'], + }, +}; + +// Handler function +export async function handler(params: Record): Promise> { + const parsed = ParamsSchema.safeParse(params); + if (!parsed.success) { + return { success: false, error: `Invalid params: ${parsed.error.message}` }; + } + + const { + paths, + pattern, + contentPattern, + maxDepth, + includeContent, + maxFiles, + } = parsed.data; + + const cwd = process.cwd(); + + // Normalize paths to array + const inputPaths = Array.isArray(paths) ? paths : [paths]; + + // Collect all files to read + const allFiles: string[] = []; + + for (const inputPath of inputPaths) { + const resolvedPath = isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath); + + if (!existsSync(resolvedPath)) { + continue; // Skip non-existent paths + } + + const stat = statSync(resolvedPath); + + if (stat.isDirectory()) { + // Collect files from directory + const dirFiles = collectFiles(resolvedPath, pattern, maxDepth); + allFiles.push(...dirFiles); + } else if (stat.isFile()) { + // Add single file (check pattern if provided) + if (!pattern || matchesPattern(relative(cwd, resolvedPath), pattern)) { + allFiles.push(resolvedPath); + } + } + } + + // Limit files + const limitedFiles = allFiles.slice(0, maxFiles); + const totalFiles = allFiles.length; + + // Process files + const files: FileEntry[] = []; + let totalContent = 0; + + for (const filePath of limitedFiles) { + if (totalContent >= MAX_TOTAL_CONTENT) break; + + const stat = statSync(filePath); + const entry: FileEntry = { + path: relative(cwd, filePath) || filePath, + size: stat.size, + }; + + if (includeContent) { + const remainingSpace = MAX_TOTAL_CONTENT - totalContent; + const maxLen = Math.min(MAX_CONTENT_LENGTH, remainingSpace); + const { content, truncated } = readFileContent(filePath, maxLen); + + // If contentPattern provided, only include files with matches + if (contentPattern) { + const matches = findMatches(content, contentPattern); + if (matches.length > 0) { + entry.matches = matches; + entry.content = content; + entry.truncated = truncated; + totalContent += content.length; + } else { + continue; // Skip files without matches + } + } else { + entry.content = content; + entry.truncated = truncated; + totalContent += content.length; + } + } + + files.push(entry); + } + + // Build message + let message = `Read ${files.length} file(s)`; + if (totalFiles > maxFiles) { + message += ` (showing ${maxFiles} of ${totalFiles})`; + } + if (contentPattern) { + message += ` matching "${contentPattern}"`; + } + + return { + success: true, + result: { + files, + totalFiles, + message, + }, + }; +} diff --git a/ccw/src/tools/write-file.ts b/ccw/src/tools/write-file.ts index 5c02fdf7..05868587 100644 --- a/ccw/src/tools/write-file.ts +++ b/ccw/src/tools/write-file.ts @@ -24,12 +24,9 @@ const ParamsSchema = z.object({ type Params = z.infer; +// Compact result for output interface WriteResult { - success: boolean; path: string; - created: boolean; - overwritten: boolean; - backupPath: string | null; bytes: number; message: string; } @@ -153,19 +150,24 @@ export async function handler(params: Record): Promise