mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-22 19:18:47 +08:00
- 新增 path-validator.ts:参考 MCP filesystem 服务器设计的集中式路径验证器
- 支持 CCW_PROJECT_ROOT 和 CCW_ALLOWED_DIRS 环境变量配置
- 多层路径验证:绝对路径解析 → 沙箱检查 → 符号链接验证
- 向后兼容:未设置环境变量时回退到 process.cwd()
- 更新所有 MCP 工具使用集中式路径验证:
- write-file.ts: 使用 validatePath()
- edit-file.ts: 使用 validatePath({ mustExist: true })
- read-file.ts: 使用 validatePath() + getProjectRoot()
- smart-search.ts: 使用 getProjectRoot()
- core-memory.ts: 使用 getProjectRoot()
- MCP 服务器启动时输出项目根目录和允许目录信息
- MCP 管理界面增强:
- CCW Tools 安装卡片新增路径设置 UI
- 支持 CCW_PROJECT_ROOT 和 CCW_ALLOWED_DIRS 配置
- 添加"使用当前项目"快捷按钮
- 支持 Claude 和 Codex 两种模式
- 添加中英文国际化翻译
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
223 lines
6.3 KiB
TypeScript
223 lines
6.3 KiB
TypeScript
/**
|
|
* Write File Tool - Create or overwrite files
|
|
*
|
|
* Features:
|
|
* - Create new files or overwrite existing
|
|
* - Auto-create parent directories
|
|
* - Support for text content with proper encoding
|
|
* - Optional backup before overwrite
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, statSync } from 'fs';
|
|
import { resolve, isAbsolute, dirname, basename } from 'path';
|
|
import { validatePath } from '../utils/path-validator.js';
|
|
|
|
// Define Zod schema for validation
|
|
const ParamsSchema = z.object({
|
|
path: z.string().min(1, 'Path is required'),
|
|
content: z.string(),
|
|
createDirectories: z.boolean().default(true),
|
|
backup: z.boolean().default(false),
|
|
encoding: z.enum(['utf8', 'utf-8', 'ascii', 'latin1', 'binary', 'hex', 'base64']).default('utf8'),
|
|
});
|
|
|
|
type Params = z.infer<typeof ParamsSchema>;
|
|
|
|
// Compact result for output
|
|
interface WriteResult {
|
|
path: string;
|
|
bytes: number;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* Ensure parent directory exists
|
|
* @param filePath - Path to file
|
|
*/
|
|
function ensureDir(filePath: string): void {
|
|
const dir = dirname(filePath);
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create backup of existing file
|
|
* @param filePath - Path to file
|
|
* @returns Backup path or null if no backup created
|
|
*/
|
|
function createBackup(filePath: string): string | null {
|
|
if (!existsSync(filePath)) {
|
|
return null;
|
|
}
|
|
|
|
const dir = dirname(filePath);
|
|
const name = basename(filePath);
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const backupPath = resolve(dir, `.${name}.${timestamp}.bak`);
|
|
|
|
try {
|
|
const content = readFileSync(filePath);
|
|
writeFileSync(backupPath, content);
|
|
return backupPath;
|
|
} catch (error) {
|
|
throw new Error(`Failed to create backup: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify file write operation completed successfully
|
|
* @param filePath - Path to written file
|
|
* @param expectedBytes - Expected file size in bytes
|
|
* @param encoding - File encoding used
|
|
* @returns Error message if verification fails, null if successful
|
|
*/
|
|
function verifyFileWrite(filePath: string, expectedBytes: number, encoding: BufferEncoding): string | null {
|
|
// Check 1: File exists
|
|
if (!existsSync(filePath)) {
|
|
return `File verification failed: file does not exist at ${filePath}`;
|
|
}
|
|
|
|
try {
|
|
// Check 2: File size matches expected bytes
|
|
const stats = statSync(filePath);
|
|
if (stats.size !== expectedBytes) {
|
|
return `File verification failed: size mismatch (expected ${expectedBytes}B, actual ${stats.size}B)`;
|
|
}
|
|
|
|
// Check 3: File is readable (for long JSON files)
|
|
const readContent = readFileSync(filePath, { encoding });
|
|
const actualBytes = Buffer.byteLength(readContent, encoding);
|
|
if (actualBytes !== expectedBytes) {
|
|
return `File verification failed: content size mismatch after read (expected ${expectedBytes}B, read ${actualBytes}B)`;
|
|
}
|
|
|
|
return null; // Verification passed
|
|
} catch (error) {
|
|
return `File verification failed: ${(error as Error).message}`;
|
|
}
|
|
}
|
|
|
|
// Tool schema for MCP
|
|
export const schema: ToolSchema = {
|
|
name: 'write_file',
|
|
description: `Write content to file. Auto-creates parent directories.
|
|
|
|
Usage: write_file(path="file.js", content="code here")
|
|
Options: backup=true (backup before overwrite), createDirectories=false (disable auto-creation), encoding="utf8"`,
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'Path to the file to create or overwrite',
|
|
},
|
|
content: {
|
|
type: 'string',
|
|
description: 'Content to write to the file',
|
|
},
|
|
createDirectories: {
|
|
type: 'boolean',
|
|
description: 'Create parent directories if they do not exist (default: true)',
|
|
default: true,
|
|
},
|
|
backup: {
|
|
type: 'boolean',
|
|
description: 'Create backup of existing file before overwriting (default: false)',
|
|
default: false,
|
|
},
|
|
encoding: {
|
|
type: 'string',
|
|
description: 'File encoding (default: utf8)',
|
|
default: 'utf8',
|
|
enum: ['utf8', 'utf-8', 'ascii', 'latin1', 'binary', 'hex', 'base64'],
|
|
},
|
|
},
|
|
required: ['path', 'content'],
|
|
},
|
|
};
|
|
|
|
// Handler function
|
|
export async function handler(params: Record<string, unknown>): Promise<ToolResult<WriteResult>> {
|
|
const parsed = ParamsSchema.safeParse(params);
|
|
if (!parsed.success) {
|
|
return { success: false, error: `Invalid params: ${parsed.error.message}` };
|
|
}
|
|
|
|
const {
|
|
path: filePath,
|
|
content,
|
|
createDirectories,
|
|
backup,
|
|
encoding,
|
|
} = parsed.data;
|
|
|
|
// Validate and resolve path
|
|
const resolvedPath = await validatePath(filePath);
|
|
const fileExists = existsSync(resolvedPath);
|
|
|
|
// Create parent directories if needed
|
|
if (createDirectories) {
|
|
ensureDir(resolvedPath);
|
|
} else if (!existsSync(dirname(resolvedPath))) {
|
|
return {
|
|
success: false,
|
|
error: `Parent directory does not exist: ${dirname(resolvedPath)}`,
|
|
};
|
|
}
|
|
|
|
// Create backup if requested and file exists
|
|
let backupPath: string | null = null;
|
|
if (backup && fileExists) {
|
|
try {
|
|
backupPath = createBackup(resolvedPath);
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: (error as Error).message,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Write file
|
|
try {
|
|
writeFileSync(resolvedPath, content, { encoding });
|
|
const bytes = Buffer.byteLength(content, encoding);
|
|
|
|
// Verify write operation completed successfully
|
|
const verificationError = verifyFileWrite(resolvedPath, bytes, encoding as BufferEncoding);
|
|
if (verificationError) {
|
|
return {
|
|
success: false,
|
|
error: verificationError,
|
|
};
|
|
}
|
|
|
|
// Build compact message
|
|
let message: string;
|
|
if (fileExists) {
|
|
message = backupPath
|
|
? `Overwrote (${bytes}B, backup: ${basename(backupPath)}) - verified`
|
|
: `Overwrote (${bytes}B) - verified`;
|
|
} else {
|
|
message = `Created (${bytes}B) - verified`;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
result: {
|
|
path: resolvedPath,
|
|
bytes,
|
|
message,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to write file: ${(error as Error).message}`,
|
|
};
|
|
}
|
|
}
|