Skip to content

Commit 91e4792

Browse files
catlog22claude
andcommitted
feat(ccw): 添加 ccw tool exec 工具系统
新增工具: - edit_file: AI辅助文件编辑 (update/line模式) - get_modules_by_depth: 项目结构分析 - update_module_claude: CLAUDE.md文档生成 - generate_module_docs: 模块文档生成 - detect_changed_modules: Git变更检测 - classify_folders: 文件夹分类 - discover_design_files: 设计文件发现 - convert_tokens_to_css: 设计token转CSS - ui_generate_preview: UI预览生成 - ui_instantiate_prototypes: 原型实例化 使用方式: ccw tool list # 列出所有工具 ccw tool exec <name> '{}' # 执行工具 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 813bfa8 commit 91e4792

18 files changed

+3329
-70
lines changed

ccw/src/cli.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { installCommand } from './commands/install.js';
66
import { uninstallCommand } from './commands/uninstall.js';
77
import { upgradeCommand } from './commands/upgrade.js';
88
import { listCommand } from './commands/list.js';
9+
import { toolCommand } from './commands/tool.js';
910
import { readFileSync, existsSync } from 'fs';
1011
import { fileURLToPath } from 'url';
1112
import { dirname, join } from 'path';
@@ -105,5 +106,11 @@ export function run(argv) {
105106
.description('List all installed Claude Code Workflow instances')
106107
.action(listCommand);
107108

109+
// Tool command
110+
program
111+
.command('tool [subcommand] [args] [json]')
112+
.description('Execute CCW tools')
113+
.action((subcommand, args, json) => toolCommand(subcommand, args, { json }));
114+
108115
program.parse(argv);
109116
}

ccw/src/commands/tool.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* Tool Command - Execute and manage CCW tools
3+
*/
4+
5+
import chalk from 'chalk';
6+
import { listTools, executeTool, getTool, getAllToolSchemas } from '../tools/index.js';
7+
8+
/**
9+
* List all available tools
10+
*/
11+
async function listAction() {
12+
const tools = listTools();
13+
14+
if (tools.length === 0) {
15+
console.log(chalk.yellow('No tools registered'));
16+
return;
17+
}
18+
19+
console.log(chalk.bold.cyan('\nAvailable Tools:\n'));
20+
21+
for (const tool of tools) {
22+
console.log(chalk.bold.white(` ${tool.name}`));
23+
console.log(chalk.gray(` ${tool.description}`));
24+
25+
if (tool.parameters?.properties) {
26+
const props = tool.parameters.properties;
27+
const required = tool.parameters.required || [];
28+
29+
console.log(chalk.gray(' Parameters:'));
30+
for (const [name, schema] of Object.entries(props)) {
31+
const req = required.includes(name) ? chalk.red('*') : '';
32+
const defaultVal = schema.default !== undefined ? chalk.gray(` (default: ${schema.default})`) : '';
33+
console.log(chalk.gray(` - ${name}${req}: ${schema.description}${defaultVal}`));
34+
}
35+
}
36+
console.log();
37+
}
38+
}
39+
40+
/**
41+
* Show tool schema in MCP-compatible JSON format
42+
*/
43+
async function schemaAction(options) {
44+
const { name } = options;
45+
46+
if (name) {
47+
const tool = getTool(name);
48+
if (!tool) {
49+
console.error(chalk.red(`Tool not found: ${name}`));
50+
process.exit(1);
51+
}
52+
53+
const schema = {
54+
name: tool.name,
55+
description: tool.description,
56+
inputSchema: {
57+
type: 'object',
58+
properties: tool.parameters?.properties || {},
59+
required: tool.parameters?.required || []
60+
}
61+
};
62+
console.log(JSON.stringify(schema, null, 2));
63+
} else {
64+
const schemas = getAllToolSchemas();
65+
console.log(JSON.stringify({ tools: schemas }, null, 2));
66+
}
67+
}
68+
69+
/**
70+
* Read from stdin if available
71+
*/
72+
async function readStdin() {
73+
// Check if stdin is a TTY (interactive terminal)
74+
if (process.stdin.isTTY) {
75+
return null;
76+
}
77+
78+
return new Promise((resolve, reject) => {
79+
let data = '';
80+
81+
process.stdin.setEncoding('utf8');
82+
83+
process.stdin.on('readable', () => {
84+
let chunk;
85+
while ((chunk = process.stdin.read()) !== null) {
86+
data += chunk;
87+
}
88+
});
89+
90+
process.stdin.on('end', () => {
91+
resolve(data.trim() || null);
92+
});
93+
94+
process.stdin.on('error', (err) => {
95+
reject(err);
96+
});
97+
});
98+
}
99+
100+
/**
101+
* Execute a tool with given parameters
102+
*/
103+
async function execAction(toolName, jsonInput, options) {
104+
if (!toolName) {
105+
console.error(chalk.red('Tool name is required'));
106+
console.error(chalk.gray('Usage: ccw tool exec <tool-name> \'{"param": "value"}\''));
107+
process.exit(1);
108+
}
109+
110+
const tool = getTool(toolName);
111+
if (!tool) {
112+
console.error(chalk.red(`Tool not found: ${toolName}`));
113+
console.error(chalk.gray('Use "ccw tool list" to see available tools'));
114+
process.exit(1);
115+
}
116+
117+
// Parse JSON input (default format)
118+
let params = {};
119+
120+
if (jsonInput) {
121+
try {
122+
params = JSON.parse(jsonInput);
123+
} catch (error) {
124+
console.error(chalk.red(`Invalid JSON: ${error.message}`));
125+
process.exit(1);
126+
}
127+
}
128+
129+
// Check for stdin input (for piped commands)
130+
const stdinData = await readStdin();
131+
if (stdinData) {
132+
// If tool has an 'input' parameter, use it
133+
// Otherwise, try to parse stdin as JSON and merge with params
134+
if (tool.parameters?.properties?.input) {
135+
params.input = stdinData;
136+
} else {
137+
try {
138+
const stdinJson = JSON.parse(stdinData);
139+
params = { ...stdinJson, ...params };
140+
} catch {
141+
// If not JSON, store as 'input' anyway
142+
params.input = stdinData;
143+
}
144+
}
145+
}
146+
147+
// Execute tool
148+
const result = await executeTool(toolName, params);
149+
150+
// Always output JSON
151+
console.log(JSON.stringify(result, null, 2));
152+
}
153+
154+
/**
155+
* Tool command entry point
156+
*/
157+
export async function toolCommand(subcommand, args, options) {
158+
// Handle subcommands
159+
switch (subcommand) {
160+
case 'list':
161+
await listAction();
162+
break;
163+
case 'schema':
164+
await schemaAction({ name: args });
165+
break;
166+
case 'exec':
167+
await execAction(args, options.json, options);
168+
break;
169+
default:
170+
console.log(chalk.bold.cyan('\nCCW Tool System\n'));
171+
console.log('Subcommands:');
172+
console.log(chalk.gray(' list List all available tools'));
173+
console.log(chalk.gray(' schema [name] Show tool schema (JSON)'));
174+
console.log(chalk.gray(' exec <name> Execute a tool'));
175+
console.log();
176+
console.log('Examples:');
177+
console.log(chalk.gray(' ccw tool list'));
178+
console.log(chalk.gray(' ccw tool schema edit_file'));
179+
console.log(chalk.gray(' ccw tool exec edit_file \'{"path":"file.txt","oldText":"old","newText":"new"}\''));
180+
}
181+
}

ccw/src/core/lite-scanner.js

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,36 @@ function scanLiteDir(dir, type) {
5454
}
5555

5656
/**
57-
* Load plan.json from session directory
57+
* Load plan.json or fix-plan.json from session directory
5858
* @param {string} sessionPath - Session directory path
5959
* @returns {Object|null} - Plan data or null
6060
*/
6161
function loadPlanJson(sessionPath) {
62+
// Try fix-plan.json first (for lite-fix), then plan.json (for lite-plan)
63+
const fixPlanPath = join(sessionPath, 'fix-plan.json');
6264
const planPath = join(sessionPath, 'plan.json');
63-
if (!existsSync(planPath)) return null;
6465

65-
try {
66-
const content = readFileSync(planPath, 'utf8');
67-
return JSON.parse(content);
68-
} catch {
69-
return null;
66+
// Try fix-plan.json first
67+
if (existsSync(fixPlanPath)) {
68+
try {
69+
const content = readFileSync(fixPlanPath, 'utf8');
70+
return JSON.parse(content);
71+
} catch {
72+
// Continue to try plan.json
73+
}
7074
}
75+
76+
// Fallback to plan.json
77+
if (existsSync(planPath)) {
78+
try {
79+
const content = readFileSync(planPath, 'utf8');
80+
return JSON.parse(content);
81+
} catch {
82+
return null;
83+
}
84+
}
85+
86+
return null;
7187
}
7288

7389
/**
@@ -91,6 +107,7 @@ function loadTaskJsons(sessionPath) {
91107
f.startsWith('IMPL-') ||
92108
f.startsWith('TASK-') ||
93109
f.startsWith('task-') ||
110+
f.startsWith('diagnosis-') ||
94111
/^T\d+\.json$/i.test(f)
95112
))
96113
.map(f => {
@@ -109,12 +126,18 @@ function loadTaskJsons(sessionPath) {
109126
}
110127
}
111128

112-
// Method 2: Check plan.json for embedded tasks array
129+
// Method 2: Check plan.json or fix-plan.json for embedded tasks array
113130
if (tasks.length === 0) {
131+
// Try fix-plan.json first (for lite-fix), then plan.json (for lite-plan)
132+
const fixPlanPath = join(sessionPath, 'fix-plan.json');
114133
const planPath = join(sessionPath, 'plan.json');
115-
if (existsSync(planPath)) {
134+
135+
const planFile = existsSync(fixPlanPath) ? fixPlanPath :
136+
existsSync(planPath) ? planPath : null;
137+
138+
if (planFile) {
116139
try {
117-
const plan = JSON.parse(readFileSync(planPath, 'utf8'));
140+
const plan = JSON.parse(readFileSync(planFile, 'utf8'));
118141
if (Array.isArray(plan.tasks)) {
119142
tasks = plan.tasks.map(t => normalizeTask(t));
120143
}
@@ -124,13 +147,14 @@ function loadTaskJsons(sessionPath) {
124147
}
125148
}
126149

127-
// Method 3: Check for task-*.json files in session root
150+
// Method 3: Check for task-*.json and diagnosis-*.json files in session root
128151
if (tasks.length === 0) {
129152
try {
130153
const rootTasks = readdirSync(sessionPath)
131154
.filter(f => f.endsWith('.json') && (
132155
f.startsWith('task-') ||
133156
f.startsWith('TASK-') ||
157+
f.startsWith('diagnosis-') ||
134158
/^T\d+\.json$/i.test(f)
135159
))
136160
.map(f => {

0 commit comments

Comments
 (0)