Enhance skill generator documentation and templates

- Updated Phase 1 and Phase 2 documentation to include next phase links and data flow details.
- Expanded Phase 5 documentation to include comprehensive validation and README generation steps, along with validation report structure.
- Added purpose and usage context sections to various action and script templates (e.g., autonomous-action, llm-action, script-bash).
- Improved commands management by simplifying the command scanning logic and enabling/disabling commands through renaming files.
- Enhanced dashboard command manager to format group names and display nested groups with appropriate icons and colors.
- Updated LiteLLM executor to allow model overrides during execution.
- Added action reference guide and template reference sections to the skill-tuning SKILL.md for better navigation and understanding.
This commit is contained in:
catlog22
2026-01-28 20:34:03 +08:00
parent 29274ee943
commit 3998d24e32
46 changed files with 1559 additions and 7731 deletions

View File

@@ -281,7 +281,8 @@ export function addEndpoint(
addClaudeApiEndpoint(homedir(), {
id: endpoint.id,
name: endpoint.id, // Use endpoint ID as tool name for CLI access
enabled: endpoint.enabled !== false
enabled: endpoint.enabled !== false,
model: endpoint.model // Sync model as primaryModel/secondaryModel
});
console.log(`[LiteLLM Config] Synced endpoint ${endpoint.id} to cli-tools.json (api-endpoint)`);
} catch (syncError) {
@@ -320,13 +321,14 @@ export function updateEndpoint(
saveConfig(baseDir, config);
// Sync enabled status to cli-tools.json
// Sync enabled status and model to cli-tools.json
const updatedEndpoint = config.endpoints[endpointIndex];
try {
addClaudeApiEndpoint(homedir(), {
id: updatedEndpoint.id,
name: updatedEndpoint.id,
enabled: updatedEndpoint.enabled !== false
enabled: updatedEndpoint.enabled !== false,
model: updatedEndpoint.model // Sync model as primaryModel/secondaryModel
});
console.log(`[LiteLLM Config] Synced endpoint ${updatedEndpoint.id} update to cli-tools.json`);
} catch (syncError) {

View File

@@ -7,7 +7,7 @@
* - POST /api/commands/:name/toggle - Enable/disable single command
* - POST /api/commands/group/:groupName/toggle - Batch toggle commands by group
*/
import { existsSync, readdirSync, readFileSync, mkdirSync, cpSync, rmSync, renameSync, statSync } from 'fs';
import { existsSync, readdirSync, readFileSync, mkdirSync, renameSync } from 'fs';
import { join, relative, dirname, basename } from 'path';
import { homedir } from 'os';
import { validatePath as validateAllowedPath } from '../../utils/path-validator.js';
@@ -78,15 +78,6 @@ function getCommandsDir(location: CommandLocation, projectPath: string): string
return join(homedir(), '.claude', 'commands');
}
/**
* Get disabled commands directory path
*/
function getDisabledCommandsDir(location: CommandLocation, projectPath: string): string {
if (location === 'project') {
return join(projectPath, '.claude', 'commands', '_disabled');
}
return join(homedir(), '.claude', 'commands', '_disabled');
}
/**
* Parse YAML frontmatter from command file
@@ -221,7 +212,6 @@ function scanCommandsRecursive(
baseDir: string,
currentDir: string,
location: CommandLocation,
enabled: boolean,
projectPath: string
): CommandInfo[] {
const results: CommandInfo[] = [];
@@ -235,37 +225,46 @@ function scanCommandsRecursive(
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
const relativePath = relative(baseDir, fullPath);
let relativePath = relative(baseDir, fullPath);
if (entry.isDirectory()) {
// Skip _disabled directory when scanning enabled commands
if (entry.name === '_disabled') continue;
// Recursively scan subdirectories
results.push(...scanCommandsRecursive(baseDir, fullPath, location, enabled, projectPath));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
try {
const content = readFileSync(fullPath, 'utf8');
const metadata = parseCommandFrontmatter(content);
const commandName = metadata.name || basename(entry.name, '.md');
results.push(...scanCommandsRecursive(baseDir, fullPath, location, projectPath));
} else if (entry.isFile()) {
// Check for .md or .md.disabled files
const isEnabled = entry.name.endsWith('.md') && !entry.name.endsWith('.md.disabled');
const isDisabled = entry.name.endsWith('.md.disabled');
// Get group from external config (not from frontmatter)
const group = getCommandGroup(commandName, relativePath, location, projectPath);
if (isEnabled || isDisabled) {
try {
const content = readFileSync(fullPath, 'utf8');
const metadata = parseCommandFrontmatter(content);
results.push({
name: commandName,
description: metadata.description,
group,
enabled,
location,
path: fullPath,
relativePath,
argumentHint: metadata.argumentHint,
allowedTools: metadata.allowedTools
});
} catch (err) {
// Skip files that fail to read
console.error(`[Commands] Failed to read ${fullPath}:`, err);
// For disabled files, remove .disabled from relativePath for consistency
if (isDisabled) {
relativePath = relativePath.replace(/\.disabled$/, '');
}
const commandName = metadata.name || basename(relativePath, '.md');
// Get group from external config (not from frontmatter)
const group = getCommandGroup(commandName, relativePath, location, projectPath);
results.push({
name: commandName,
description: metadata.description,
group,
enabled: isEnabled,
location,
path: fullPath,
relativePath,
argumentHint: metadata.argumentHint,
allowedTools: metadata.allowedTools
});
} catch (err) {
// Skip files that fail to read
console.error(`[Commands] Failed to read ${fullPath}:`, err);
}
}
}
}
@@ -289,33 +288,13 @@ function getCommandsConfig(projectPath: string): CommandsConfig {
const groupSet = new Set<string>();
try {
// Scan project commands
// Scan project commands (includes both .md and .md.disabled)
const projectDir = getCommandsDir('project', projectPath);
const projectDisabledDir = getDisabledCommandsDir('project', projectPath);
result.projectCommands = scanCommandsRecursive(projectDir, projectDir, 'project', projectPath);
// Enabled project commands
const enabledProject = scanCommandsRecursive(projectDir, projectDir, 'project', true, projectPath);
result.projectCommands.push(...enabledProject);
// Disabled project commands
if (existsSync(projectDisabledDir)) {
const disabledProject = scanCommandsRecursive(projectDisabledDir, projectDisabledDir, 'project', false, projectPath);
result.projectCommands.push(...disabledProject);
}
// Scan user commands
// Scan user commands (includes both .md and .md.disabled)
const userDir = getCommandsDir('user', projectPath);
const userDisabledDir = getDisabledCommandsDir('user', projectPath);
// Enabled user commands
const enabledUser = scanCommandsRecursive(userDir, userDir, 'user', true, projectPath);
result.userCommands.push(...enabledUser);
// Disabled user commands
if (existsSync(userDisabledDir)) {
const disabledUser = scanCommandsRecursive(userDisabledDir, userDisabledDir, 'user', false, projectPath);
result.userCommands.push(...disabledUser);
}
result.userCommands = scanCommandsRecursive(userDir, userDir, 'user', projectPath);
// Collect all groups
for (const cmd of [...result.projectCommands, ...result.userCommands]) {
@@ -330,40 +309,6 @@ function getCommandsConfig(projectPath: string): CommandsConfig {
return result;
}
/**
* Move directory with fallback to copy-delete and rollback on failure
*/
function moveDirectory(source: string, target: string): void {
try {
// Ensure target parent directory exists
const targetParent = dirname(target);
if (!existsSync(targetParent)) {
mkdirSync(targetParent, { recursive: true });
}
// Try atomic rename first
renameSync(source, target);
} catch (error: unknown) {
const err = error as NodeJS.ErrnoException;
// If rename fails (cross-filesystem, permission issues), fallback to copy-delete
if (err.code === 'EXDEV' || err.code === 'EPERM' || err.code === 'EBUSY') {
cpSync(source, target, { recursive: true, force: true });
try {
rmSync(source, { recursive: true, force: true });
} catch (rmError) {
// Rollback: remove the copied target to avoid duplicates
try {
rmSync(target, { recursive: true, force: true });
} catch {
// Ignore rollback errors
}
throw new Error(`Failed to remove source after copy: ${(rmError as Error).message}`);
}
} else {
throw error;
}
}
}
/**
* Find command by name in commands list
@@ -416,47 +361,48 @@ async function toggleCommand(
}
const commandsDir = getCommandsDir(location, projectPath);
const disabledDir = getDisabledCommandsDir(location, projectPath);
// relativePath already includes .md extension (e.g., 'workflow/plan.md')
const commandPath = join(commandsDir, command.relativePath);
const disabledPath = commandPath + '.disabled';
if (command.enabled) {
// Disable: move from commands to _disabled
const targetPath = join(disabledDir, command.relativePath);
// Check if target already exists
if (existsSync(targetPath)) {
return { success: false, message: 'Command already exists in disabled directory', status: 409 };
// Disable: rename .md to .md.disabled
if (!existsSync(commandPath)) {
return { success: false, message: 'Command file not found', status: 404 };
}
if (existsSync(disabledPath)) {
return { success: false, message: 'Command already disabled', status: 409 };
}
moveDirectory(command.path, targetPath);
return {
success: true,
message: 'Command disabled',
commandName: command.name,
location
renameSync(commandPath, disabledPath);
return {
success: true,
message: 'Command disabled',
commandName: command.name,
location
};
} else {
// Enable: move from _disabled back to commands
// Calculate target path in enabled directory
const targetPath = join(commandsDir, command.relativePath);
// Check if target already exists
if (existsSync(targetPath)) {
return { success: false, message: 'Command already exists in commands directory', status: 409 };
// Enable: rename .md.disabled back to .md
if (!existsSync(disabledPath)) {
return { success: false, message: 'Disabled command not found', status: 404 };
}
if (existsSync(commandPath)) {
return { success: false, message: 'Command already enabled', status: 409 };
}
moveDirectory(command.path, targetPath);
return {
success: true,
message: 'Command enabled',
commandName: command.name,
location
renameSync(disabledPath, commandPath);
return {
success: true,
message: 'Command enabled',
commandName: command.name,
location
};
}
} catch (error) {
return {
success: false,
message: (error as Error).message,
status: 500
return {
success: false,
message: (error as Error).message,
status: 500
};
}
}

View File

@@ -1631,6 +1631,12 @@ const i18n = {
'commands.group.task': 'Task',
'commands.group.issue': 'Issue',
'commands.group.other': 'Other',
'commands.group.review': 'Review',
'commands.group.execute': 'Execute',
'commands.group.plan': 'Plan',
'commands.group.test': 'Test',
'commands.group.debug': 'Debug',
'commands.group.tools': 'Tools',
'commands.enableAll': 'Enable All',
'commands.disableAll': 'Disable All',
'commands.enableGroupConfirm': 'Enable all commands in "{group}" group?',
@@ -4307,6 +4313,12 @@ const i18n = {
'commands.group.task': '任务',
'commands.group.issue': '问题',
'commands.group.other': '其他',
'commands.group.review': '审查',
'commands.group.execute': '执行',
'commands.group.plan': '规划',
'commands.group.test': '测试',
'commands.group.debug': '调试',
'commands.group.tools': '工具',
'commands.enableAll': '全部启用',
'commands.disableAll': '全部禁用',
'commands.enableGroupConfirm': '启用 "{group}" 分组中的所有命令?',

View File

@@ -205,6 +205,50 @@ function renderCommandsView() {
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Format group name for display (e.g., 'workflow/review' -> 'Workflow > Review')
function formatGroupName(groupName) {
if (!groupName.includes('/')) {
return t('commands.group.' + groupName) || groupName;
}
// Split path and translate each part
const parts = groupName.split('/');
const translatedParts = parts.map(part => t('commands.group.' + part) || part);
return translatedParts.join(' ');
}
// Get icon for a group (use top-level parent's icon for nested groups)
function getGroupIcon(groupName) {
const groupIcons = {
cli: 'terminal',
workflow: 'git-branch',
memory: 'brain',
task: 'clipboard-list',
issue: 'alert-circle',
other: 'folder'
};
// For nested groups, use the top-level parent's icon
const topLevel = groupName.split('/')[0];
return groupIcons[topLevel] || 'folder';
}
// Get color for a group (use top-level parent's color for nested groups)
function getGroupColor(groupName) {
const groupColors = {
cli: 'text-primary bg-primary/10',
workflow: 'text-success bg-success/10',
memory: 'text-indigo bg-indigo/10',
task: 'text-warning bg-warning/10',
issue: 'text-destructive bg-destructive/10',
other: 'text-muted-foreground bg-muted'
};
// For nested groups, use the top-level parent's color
const topLevel = groupName.split('/')[0];
return groupColors[topLevel] || 'text-muted-foreground bg-muted';
}
function renderAccordionGroup(groupName, commands) {
// Default to expanded for new/custom groups
if (expandedGroups[groupName] === undefined) expandedGroups[groupName] = true;
@@ -217,31 +261,14 @@ function renderAccordionGroup(groupName, commands) {
? commands
: enabledCommands;
// Group icons
const groupIcons = {
cli: 'terminal',
workflow: 'workflow',
memory: 'brain',
task: 'clipboard-list',
issue: 'alert-circle',
other: 'folder'
};
// Group colors
const groupColors = {
cli: 'text-primary bg-primary/10',
workflow: 'text-success bg-success/10',
memory: 'text-indigo bg-indigo/10',
task: 'text-warning bg-warning/10',
issue: 'text-destructive bg-destructive/10',
other: 'text-muted-foreground bg-muted'
};
const icon = groupIcons[groupName] || 'folder';
const colorClass = groupColors[groupName] || 'text-muted-foreground bg-muted';
const icon = getGroupIcon(groupName);
const colorClass = getGroupColor(groupName);
const displayName = formatGroupName(groupName);
const indentLevel = (groupName.match(/\//g) || []).length;
const indentStyle = indentLevel > 0 ? `style="margin-left: ${indentLevel * 20}px;"` : '';
return `
<div class="accordion-group mb-4">
<div class="accordion-group mb-4" ${indentStyle}>
<!-- Group Header -->
<div class="accordion-header flex items-center justify-between px-4 py-3 bg-card border border-border rounded-lg hover:bg-hover transition-colors">
<div class="flex items-center gap-3 flex-1 cursor-pointer" onclick="toggleAccordionGroup('${groupName}')">
@@ -250,7 +277,7 @@ function renderAccordionGroup(groupName, commands) {
<i data-lucide="${icon}" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-base font-semibold text-foreground capitalize">${t('commands.group.' + groupName) || groupName}</h3>
<h3 class="text-base font-semibold text-foreground">${displayName}</h3>
<p class="text-xs text-muted-foreground">${enabledCommands.length}/${commands.length} ${t('commands.enabled') || 'enabled'}</p>
</div>
</div>

View File

@@ -802,13 +802,15 @@ export function getNativeResume(projectDir: string): boolean {
*/
export function addClaudeApiEndpoint(
projectDir: string,
endpoint: { id: string; name: string; enabled: boolean }
endpoint: { id: string; name: string; enabled: boolean; model?: string }
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
// Add as a tool with type: 'api-endpoint'
config.tools[endpoint.name] = {
enabled: endpoint.enabled,
primaryModel: endpoint.model, // Use endpoint.model as primaryModel (can be overridden via --model)
secondaryModel: endpoint.model, // Same as primary for fallback
tags: [],
type: 'api-endpoint',
id: endpoint.id // Store endpoint ID for settings lookup

View File

@@ -557,6 +557,11 @@ async function executeCliTool(
// id field is the LiteLLM endpoint ID (e.g., "g25")
const litellmEndpointId = toolConfig.id || toolName;
// Use configured primary model if no explicit model provided
// This allows --model parameter to override the tool's primaryModel
// Use undefined if primaryModel is empty string (endpoint.model will be used as fallback)
const apiEndpointEffectiveModel = model || (toolConfig.primaryModel || undefined);
// Find LiteLLM endpoint configuration
const litellmEndpoint = findEndpointById(workingDir, litellmEndpointId);
if (litellmEndpoint) {
@@ -568,13 +573,14 @@ async function executeCliTool(
});
}
// Execute via LiteLLM
// Execute via LiteLLM with model override
const result = await executeLiteLLMEndpoint({
prompt,
endpointId: litellmEndpointId,
baseDir: workingDir,
cwd: cd || workingDir,
includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
model: apiEndpointEffectiveModel, // Pass effective model (--model or primaryModel)
onOutput: onOutput || undefined,
});
@@ -587,7 +593,7 @@ async function executeCliTool(
id: customId || `${Date.now()}-litellm`,
timestamp: new Date(startTime).toISOString(),
tool: toolName,
model: litellmEndpoint.model,
model: result.model, // Use effective model from result (reflects any override)
mode,
prompt,
status: result.success ? 'success' : 'error',

View File

@@ -19,6 +19,7 @@ export interface LiteLLMExecutionOptions {
cwd?: string; // Working directory for file resolution
includeDirs?: string[]; // Additional directories for @patterns
enableCache?: boolean; // Override endpoint cache setting
model?: string; // Override model for this execution (if not specified, uses endpoint.model)
onOutput?: (unit: CliOutputUnit) => void;
/** Number of retries after the initial attempt (default: 0) */
maxRetries?: number;
@@ -56,7 +57,7 @@ export function extractPatterns(prompt: string): string[] {
export async function executeLiteLLMEndpoint(
options: LiteLLMExecutionOptions
): Promise<LiteLLMExecutionResult> {
const { prompt, endpointId, baseDir, cwd, includeDirs, enableCache, onOutput } = options;
const { prompt, endpointId, baseDir, cwd, includeDirs, enableCache, model: modelOverride, onOutput } = options;
// 1. Find endpoint configuration
const endpoint = findEndpointById(baseDir, endpointId);
@@ -96,7 +97,10 @@ export async function executeLiteLLMEndpoint(
};
}
// 3. Process context cache if enabled
// 3. Determine effective model: use override if provided, otherwise use endpoint.model
const effectiveModel = modelOverride || endpoint.model;
// 4. Process context cache if enabled
let finalPrompt = prompt;
let cacheUsed = false;
let cachedFiles: string[] = [];
@@ -168,12 +172,12 @@ export async function executeLiteLLMEndpoint(
}
}
// 4. Call LiteLLM
// 5. Call LiteLLM
try {
if (onOutput) {
onOutput({
type: 'stderr',
content: `[LiteLLM: Calling ${provider.type}/${endpoint.model}]\n`,
content: `[LiteLLM: Calling ${provider.type}/${effectiveModel}]\n`,
timestamp: new Date().toISOString()
});
}
@@ -206,14 +210,14 @@ export async function executeLiteLLMEndpoint(
delete process.env['CCW_LITELLM_EXTRA_HEADERS'];
}
// Use litellm-client to call chat
// Use litellm-client to call chat with effective model
const response = await callWithRetries(
() => client.chat(finalPrompt, endpoint.model),
() => client.chat(finalPrompt, effectiveModel),
{
maxRetries: options.maxRetries ?? 0,
baseDelayMs: options.retryBaseDelayMs ?? 1000,
onOutput,
rateLimitKey: `${provider.type}:${endpoint.model}`,
rateLimitKey: `${provider.type}:${effectiveModel}`,
},
);
@@ -228,7 +232,7 @@ export async function executeLiteLLMEndpoint(
return {
success: true,
output: response,
model: endpoint.model,
model: effectiveModel,
provider: provider.type,
cacheUsed,
cachedFiles,
@@ -246,7 +250,7 @@ export async function executeLiteLLMEndpoint(
return {
success: false,
output: '',
model: endpoint.model,
model: effectiveModel,
provider: provider.type,
cacheUsed,
error: errorMsg,