mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
Compare commits
2 Commits
main
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b21069bda | ||
|
|
11bd1ab57e |
@@ -1,9 +1,7 @@
|
||||
// ========================================
|
||||
// Hook Quick Templates Component
|
||||
// ========================================
|
||||
// Frontend component for displaying and installing hook templates
|
||||
// Templates are defined in backend: ccw/src/core/hooks/hook-templates.ts
|
||||
// All templates use `ccw hook template exec <id> --stdin` to avoid Windows quote issues
|
||||
// Predefined hook templates for quick installation
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -34,10 +32,10 @@ import type { HookTriggerType } from './HookCard';
|
||||
/**
|
||||
* Template category type
|
||||
*/
|
||||
export type TemplateCategory = 'notification' | 'indexing' | 'automation' | 'utility' | 'protection';
|
||||
export type TemplateCategory = 'notification' | 'indexing' | 'automation' | 'utility';
|
||||
|
||||
/**
|
||||
* Hook template definition (frontend view of backend templates)
|
||||
* Hook template definition
|
||||
*/
|
||||
export interface HookTemplate {
|
||||
id: string;
|
||||
@@ -45,6 +43,8 @@ export interface HookTemplate {
|
||||
description: string;
|
||||
category: TemplateCategory;
|
||||
trigger: HookTriggerType;
|
||||
command: string;
|
||||
args?: string[];
|
||||
matcher?: string;
|
||||
}
|
||||
|
||||
@@ -63,22 +63,24 @@ export interface HookQuickTemplatesProps {
|
||||
}
|
||||
|
||||
// ========== Hook Templates ==========
|
||||
// NOTE: Templates are defined in backend (ccw/src/core/hooks/hook-templates.ts)
|
||||
// This is a copy for frontend display purposes.
|
||||
// All templates use `ccw hook template exec <id> --stdin` format.
|
||||
// NOTE: Hook input is received via stdin (not environment variable)
|
||||
// Use: const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');
|
||||
|
||||
/**
|
||||
* Predefined hook templates for quick installation
|
||||
* These mirror the backend templates in ccw/src/core/hooks/hook-templates.ts
|
||||
*/
|
||||
export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
// ============ Notification ============
|
||||
{
|
||||
id: 'session-start-notify',
|
||||
name: 'Session Start Notify',
|
||||
description: 'Notify dashboard when a new workflow session is created',
|
||||
category: 'notification',
|
||||
trigger: 'SessionStart',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_CREATED",timestamp:Date.now(),project:process.env.CLAUDE_PROJECT_DIR||process.cwd()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'session-state-watch',
|
||||
@@ -87,24 +89,26 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(/workflow-session\\.json$|session-metadata\\.json$/.test(file)){try{const content=fs.readFileSync(file,"utf8");const data=JSON.parse(content);const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_STATE_CHANGED",file:file,sessionId:data.session_id||"",status:data.status||"unknown",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})}catch(e){}}'
|
||||
]
|
||||
},
|
||||
// --- Notification ---
|
||||
{
|
||||
id: 'stop-notify',
|
||||
name: 'Stop Notify',
|
||||
description: 'Notify dashboard when Claude finishes responding',
|
||||
category: 'notification',
|
||||
trigger: 'Stop',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");const payload=JSON.stringify({type:"TASK_COMPLETED",timestamp:Date.now(),project:process.env.CLAUDE_PROJECT_DIR||process.cwd()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'memory-sync-dashboard',
|
||||
name: 'Memory Sync Dashboard',
|
||||
description: 'Sync memory V2 status to dashboard on changes',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'mcp__ccw-tools__core_memory',
|
||||
},
|
||||
|
||||
// ============ Automation ============
|
||||
// --- Automation ---
|
||||
{
|
||||
id: 'auto-format-on-write',
|
||||
name: 'Auto Format on Write',
|
||||
@@ -112,6 +116,11 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");cp.spawnSync("npx",["prettier","--write",file],{stdio:"inherit",shell:true})}'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'auto-lint-on-write',
|
||||
@@ -120,6 +129,24 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");cp.spawnSync("npx",["eslint","--fix",file],{stdio:"inherit",shell:true})}'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'block-sensitive-files',
|
||||
name: 'Block Sensitive Files',
|
||||
description: 'Block modifications to sensitive files (.env, secrets, credentials)',
|
||||
category: 'automation',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(/\\.env|secret|credential|\\.key$/.test(file)){process.stderr.write("Blocked: modifying sensitive file "+file);process.exit(2)}'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'git-auto-stage',
|
||||
@@ -127,67 +154,13 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
description: 'Auto stage all modified files when Claude finishes responding',
|
||||
category: 'automation',
|
||||
trigger: 'Stop',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");cp.spawnSync("git",["add","-u"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
},
|
||||
|
||||
// ============ Protection ============
|
||||
{
|
||||
id: 'block-sensitive-files',
|
||||
name: 'Block Sensitive Files',
|
||||
description: 'Block modifications to sensitive files (.env, secrets, credentials)',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
},
|
||||
{
|
||||
id: 'danger-bash-confirm',
|
||||
name: 'Danger Bash Confirm',
|
||||
description: 'Require confirmation for dangerous bash commands',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
},
|
||||
{
|
||||
id: 'danger-file-protection',
|
||||
name: 'Danger File Protection',
|
||||
description: 'Block modifications to protected files',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
},
|
||||
{
|
||||
id: 'danger-git-destructive',
|
||||
name: 'Danger Git Destructive',
|
||||
description: 'Require confirmation for destructive git operations',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
},
|
||||
{
|
||||
id: 'danger-network-confirm',
|
||||
name: 'Danger Network Confirm',
|
||||
description: 'Require confirmation for network operations',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash|WebFetch',
|
||||
},
|
||||
{
|
||||
id: 'danger-system-paths',
|
||||
name: 'Danger System Paths',
|
||||
description: 'Block modifications to system paths',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit|Bash',
|
||||
},
|
||||
{
|
||||
id: 'danger-permission-change',
|
||||
name: 'Danger Permission Change',
|
||||
description: 'Require confirmation for permission changes',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
},
|
||||
|
||||
// ============ Indexing ============
|
||||
// --- Indexing ---
|
||||
{
|
||||
id: 'post-edit-index',
|
||||
name: 'Post Edit Index',
|
||||
@@ -195,6 +168,11 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
category: 'indexing',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");const payload=JSON.stringify({type:"FILE_MODIFIED",file:file,project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})}'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'session-end-summary',
|
||||
@@ -202,22 +180,74 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
description: 'Send session summary to dashboard on session end',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_SUMMARY",transcript:p.transcript_path||"",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
},
|
||||
|
||||
// ============ Utility ============
|
||||
{
|
||||
id: 'project-state-inject',
|
||||
name: 'Project State Inject',
|
||||
description: 'Inject project guidelines and recent dev history at session start',
|
||||
category: 'indexing',
|
||||
trigger: 'SessionStart',
|
||||
command: 'ccw',
|
||||
args: ['hook', 'project-state', '--stdin']
|
||||
},
|
||||
// --- Memory V2 ---
|
||||
{
|
||||
id: 'memory-v2-extract',
|
||||
name: 'Memory V2 Extract',
|
||||
description: 'Trigger Phase 1 extraction when session ends (after idle period)',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
command: 'ccw',
|
||||
args: ['core-memory', 'extract', '--max-sessions', '10']
|
||||
},
|
||||
{
|
||||
id: 'memory-v2-auto-consolidate',
|
||||
name: 'Memory V2 Auto Consolidate',
|
||||
description: 'Trigger Phase 2 consolidation after extraction jobs complete',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");const r=cp.spawnSync("ccw",["core-memory","extract","--json"],{encoding:"utf8",shell:true});try{const d=JSON.parse(r.stdout);if(d&&d.total_stage1>=5){cp.spawnSync("ccw",["core-memory","consolidate"],{stdio:"inherit",shell:true})}}catch(e){}'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'memory-sync-dashboard',
|
||||
name: 'Memory Sync Dashboard',
|
||||
description: 'Sync memory V2 status to dashboard on changes',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'mcp__ccw-tools__core_memory',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const cp=require("child_process");const payload=JSON.stringify({type:"MEMORY_V2_STATUS_UPDATED",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
},
|
||||
// --- Memory Operations ---
|
||||
{
|
||||
id: 'memory-auto-compress',
|
||||
name: 'Auto Memory Compress',
|
||||
description: 'Automatically compress memory when entries exceed threshold',
|
||||
category: 'utility',
|
||||
category: 'automation',
|
||||
trigger: 'Stop',
|
||||
command: 'ccw',
|
||||
args: ['memory', 'consolidate', '--threshold', '50']
|
||||
},
|
||||
{
|
||||
id: 'memory-preview-extract',
|
||||
name: 'Memory Preview & Extract',
|
||||
description: 'Preview extraction queue and extract eligible sessions',
|
||||
category: 'utility',
|
||||
category: 'automation',
|
||||
trigger: 'SessionStart',
|
||||
command: 'ccw',
|
||||
args: ['memory', 'preview', '--include-native']
|
||||
},
|
||||
{
|
||||
id: 'memory-status-check',
|
||||
@@ -225,21 +255,9 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
description: 'Check memory extraction and consolidation status',
|
||||
category: 'utility',
|
||||
trigger: 'SessionStart',
|
||||
},
|
||||
{
|
||||
id: 'memory-v2-extract',
|
||||
name: 'Memory V2 Extract',
|
||||
description: 'Trigger Phase 1 extraction when session ends',
|
||||
category: 'utility',
|
||||
trigger: 'Stop',
|
||||
},
|
||||
{
|
||||
id: 'memory-v2-auto-consolidate',
|
||||
name: 'Memory V2 Auto Consolidate',
|
||||
description: 'Trigger Phase 2 consolidation after extraction jobs complete',
|
||||
category: 'utility',
|
||||
trigger: 'Stop',
|
||||
},
|
||||
command: 'ccw',
|
||||
args: ['memory', 'status']
|
||||
}
|
||||
] as const;
|
||||
|
||||
// ========== Category Icons ==========
|
||||
@@ -248,8 +266,7 @@ const CATEGORY_ICONS: Record<TemplateCategory, { icon: typeof Bell; color: strin
|
||||
notification: { icon: Bell, color: 'text-blue-500', bg: 'bg-blue-500/10' },
|
||||
indexing: { icon: Database, color: 'text-purple-500', bg: 'bg-purple-500/10' },
|
||||
automation: { icon: Wrench, color: 'text-orange-500', bg: 'bg-orange-500/10' },
|
||||
utility: { icon: Settings, color: 'text-gray-500', bg: 'bg-gray-500/10' },
|
||||
protection: { icon: Shield, color: 'text-red-500', bg: 'bg-red-500/10' },
|
||||
utility: { icon: Settings, color: 'text-gray-500', bg: 'bg-gray-500/10' }
|
||||
};
|
||||
|
||||
// ========== Template Icons ==========
|
||||
@@ -274,8 +291,7 @@ function getCategoryName(category: TemplateCategory, formatMessage: ReturnType<t
|
||||
notification: formatMessage({ id: 'cliHooks.templates.categories.notification' }),
|
||||
indexing: formatMessage({ id: 'cliHooks.templates.categories.indexing' }),
|
||||
automation: formatMessage({ id: 'cliHooks.templates.categories.automation' }),
|
||||
utility: formatMessage({ id: 'cliHooks.templates.categories.utility' }),
|
||||
protection: formatMessage({ id: 'cliHooks.templates.categories.protection' }),
|
||||
utility: formatMessage({ id: 'cliHooks.templates.categories.utility' })
|
||||
};
|
||||
return names[category];
|
||||
}
|
||||
@@ -305,7 +321,7 @@ export function HookQuickTemplates({
|
||||
}, []);
|
||||
|
||||
// Define category order
|
||||
const categoryOrder: TemplateCategory[] = ['notification', 'indexing', 'automation', 'protection', 'utility'];
|
||||
const categoryOrder: TemplateCategory[] = ['notification', 'indexing', 'automation'];
|
||||
|
||||
const handleInstall = async (templateId: string) => {
|
||||
await onInstallTemplate(templateId);
|
||||
|
||||
@@ -74,21 +74,81 @@ interface SkillContextConfig {
|
||||
skillConfigs: Array<{ skill: string; keywords: string }>;
|
||||
}
|
||||
|
||||
// ========== Hook Templates ==========
|
||||
// Templates are now defined in backend: ccw/src/core/hooks/hook-templates.ts
|
||||
// All templates use `ccw hook template exec <id> --stdin` format
|
||||
// This avoids Windows Git Bash quote handling issues
|
||||
// ========== Hook Templates (from old hook-manager.js) ==========
|
||||
|
||||
// Template IDs that map to backend templates
|
||||
const TEMPLATE_IDS = {
|
||||
'memory-update-queue': 'memory-auto-compress',
|
||||
'danger-bash-confirm': 'danger-bash-confirm',
|
||||
'danger-file-protection': 'danger-file-protection',
|
||||
'danger-git-destructive': 'danger-git-destructive',
|
||||
'danger-network-confirm': 'danger-network-confirm',
|
||||
'danger-system-paths': 'danger-system-paths',
|
||||
'danger-permission-change': 'danger-permission-change',
|
||||
} as const;
|
||||
interface HookTemplate {
|
||||
event: string;
|
||||
matcher: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// NOTE: Hook input is received via stdin (not environment variable)
|
||||
// Node.js: const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');
|
||||
// Bash: INPUT=$(cat)
|
||||
const HOOK_TEMPLATES: Record<string, HookTemplate> = {
|
||||
'memory-update-queue': {
|
||||
event: 'Stop',
|
||||
matcher: '',
|
||||
command: 'node',
|
||||
args: ['-e', "require('child_process').spawnSync(process.platform==='win32'?'cmd':'ccw',process.platform==='win32'?['/c','ccw','tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'gemini'})]:['tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'gemini'})],{stdio:'inherit'})"],
|
||||
},
|
||||
'skill-context-keyword': {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'node',
|
||||
args: ['-e', "const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({prompt:p.prompt||''})],{stdio:'inherit'})"],
|
||||
},
|
||||
'skill-context-auto': {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'node',
|
||||
args: ['-e', "const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({mode:'auto',prompt:p.prompt||''})],{stdio:'inherit'})"],
|
||||
},
|
||||
'danger-bash-confirm': {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); DANGEROUS_PATTERNS="rm -rf|rmdir|del /|format |shutdown|reboot|kill -9|pkill|mkfs|dd if=|chmod 777|chown -R|>/dev/|wget.*\\|.*sh|curl.*\\|.*bash"; if echo "$CMD" | grep -qiE "$DANGEROUS_PATTERNS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"Potentially dangerous command detected: requires user confirmation\\"}}" && exit 0; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-file-protection': {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); PROTECTED=".env|.git/|package-lock.json|yarn.lock|.credentials|secrets|id_rsa|.pem$|.key$"; if echo "$FILE" | grep -qiE "$PROTECTED"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"Protected file cannot be modified: $FILE\\"}}" >&2 && exit 2; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-git-destructive': {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); GIT_DANGEROUS="git push.*--force|git push.*-f|git reset --hard|git clean -fd|git checkout.*--force|git branch -D|git rebase.*-f"; if echo "$CMD" | grep -qiE "$GIT_DANGEROUS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"Destructive git operation detected: $CMD\\"}}" && exit 0; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-network-confirm': {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Bash|WebFetch',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty"); if [ "$TOOL" = "WebFetch" ]; then URL=$(echo "$INPUT" | jq -r ".tool_input.url // empty"); echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"Network request to: $URL\\"}}" && exit 0; fi; CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); NET_CMDS="curl|wget|nc |netcat|ssh |scp |rsync|ftp "; if echo "$CMD" | grep -qiE "^($NET_CMDS)"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"Network command requires confirmation: $CMD\\"}}" && exit 0; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-system-paths': {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Write|Edit|Bash',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty"); if [ "$TOOL" = "Bash" ]; then CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); SYS_PATHS="/etc/|/usr/|/bin/|/sbin/|/boot/|/sys/|/proc/|C:\\\\Windows|C:\\\\Program Files"; if echo "$CMD" | grep -qiE "$SYS_PATHS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"System path operation requires confirmation\\"}}" && exit 0; fi; else FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); SYS_PATHS="/etc/|/usr/|/bin/|/sbin/|C:\\\\Windows|C:\\\\Program Files"; if echo "$FILE" | grep -qiE "$SYS_PATHS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"Cannot modify system file: $FILE\\"}}" >&2 && exit 2; fi; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-permission-change': {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); PERM_CMDS="chmod|chown|chgrp|setfacl|icacls|takeown|cacls"; if echo "$CMD" | grep -qiE "^($PERM_CMDS)"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"Permission change requires confirmation: $CMD\\"}}" && exit 0; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
};
|
||||
|
||||
// Danger protection option definitions
|
||||
const DANGER_OPTIONS = [
|
||||
@@ -100,6 +160,72 @@ const DANGER_OPTIONS = [
|
||||
{ id: 'permission-change', templateId: 'danger-permission-change', labelKey: 'cliHooks.wizards.dangerProtection.options.permissionChange', descKey: 'cliHooks.wizards.dangerProtection.options.permissionChangeDesc' },
|
||||
] as const;
|
||||
|
||||
// ========== convertToClaudeCodeFormat (ported from old hook-manager.js) ==========
|
||||
|
||||
function convertToClaudeCodeFormat(hookData: {
|
||||
command: string;
|
||||
args: string[];
|
||||
matcher?: string;
|
||||
timeout?: number;
|
||||
}): Record<string, unknown> {
|
||||
let commandStr = hookData.command || '';
|
||||
|
||||
if (hookData.args && Array.isArray(hookData.args)) {
|
||||
if (commandStr === 'bash' && hookData.args.length >= 2 && hookData.args[0] === '-c') {
|
||||
const script = hookData.args[1];
|
||||
const escapedScript = script.replace(/'/g, "'\\''");
|
||||
commandStr = `bash -c '${escapedScript}'`;
|
||||
if (hookData.args.length > 2) {
|
||||
const additionalArgs = hookData.args.slice(2).map(arg =>
|
||||
arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")
|
||||
? `"${arg.replace(/"/g, '\\"')}"`
|
||||
: arg
|
||||
);
|
||||
commandStr += ' ' + additionalArgs.join(' ');
|
||||
}
|
||||
} else if (commandStr === 'node' && hookData.args.length >= 2 && hookData.args[0] === '-e') {
|
||||
const script = hookData.args[1];
|
||||
const isWindows = typeof navigator !== 'undefined' && navigator.userAgent.includes('Win');
|
||||
if (isWindows) {
|
||||
const escapedScript = script.replace(/"/g, '\\"');
|
||||
commandStr = `node -e "${escapedScript}"`;
|
||||
} else {
|
||||
const escapedScript = script.replace(/'/g, "'\\''");
|
||||
commandStr = `node -e '${escapedScript}'`;
|
||||
}
|
||||
if (hookData.args.length > 2) {
|
||||
const additionalArgs = hookData.args.slice(2).map(arg =>
|
||||
arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")
|
||||
? `"${arg.replace(/"/g, '\\"')}"`
|
||||
: arg
|
||||
);
|
||||
commandStr += ' ' + additionalArgs.join(' ');
|
||||
}
|
||||
} else {
|
||||
const quotedArgs = hookData.args.map(arg =>
|
||||
arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")
|
||||
? `"${arg.replace(/"/g, '\\"')}"`
|
||||
: arg
|
||||
);
|
||||
commandStr = `${commandStr} ${quotedArgs.join(' ')}`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const converted: Record<string, unknown> = {
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: commandStr,
|
||||
...(hookData.timeout ? { timeout: Math.ceil(hookData.timeout / 1000) } : {}),
|
||||
}],
|
||||
};
|
||||
|
||||
if (hookData.matcher) {
|
||||
converted.matcher = hookData.matcher;
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
// ========== Wizard Definitions ==========
|
||||
|
||||
const WIZARD_METADATA = {
|
||||
@@ -200,65 +326,59 @@ export function HookWizard({
|
||||
try {
|
||||
switch (wizardType) {
|
||||
case 'memory-update': {
|
||||
// Use backend template API to install memory template
|
||||
const response = await fetch('/api/hooks/templates/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
templateId: 'memory-auto-compress',
|
||||
scope,
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to install template');
|
||||
}
|
||||
const selectedTool = memoryConfig.tool;
|
||||
const template = HOOK_TEMPLATES['memory-update-queue'];
|
||||
const hookData = {
|
||||
command: template.command,
|
||||
args: ['-e', `require('child_process').spawnSync(process.platform==='win32'?'cmd':'ccw',process.platform==='win32'?['/c','ccw','tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})]:['tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})],{stdio:'inherit'})`],
|
||||
};
|
||||
const converted = convertToClaudeCodeFormat(hookData);
|
||||
await saveHook(scope, template.event, converted);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'danger-protection': {
|
||||
// Install each selected protection template via backend API
|
||||
for (const optionId of dangerConfig.selectedOptions) {
|
||||
const option = DANGER_OPTIONS.find(o => o.id === optionId);
|
||||
if (!option) continue;
|
||||
const templateId = TEMPLATE_IDS[option.templateId as keyof typeof TEMPLATE_IDS] || option.templateId;
|
||||
|
||||
const response = await fetch('/api/hooks/templates/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
scope,
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
console.warn(`Failed to install template ${templateId}:`, result.error);
|
||||
}
|
||||
const template = HOOK_TEMPLATES[option.templateId];
|
||||
if (!template) continue;
|
||||
const hookData = {
|
||||
command: template.command,
|
||||
args: [...template.args],
|
||||
matcher: template.matcher,
|
||||
timeout: template.timeout,
|
||||
};
|
||||
const converted = convertToClaudeCodeFormat(hookData);
|
||||
await saveHook(scope, template.event, converted);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'skill-context': {
|
||||
// Use ccw hook command directly for skill context
|
||||
const hookData = skillConfig.mode === 'auto'
|
||||
? {
|
||||
_templateId: 'skill-context-auto',
|
||||
matcher: '',
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: 'ccw hook keyword --stdin',
|
||||
}],
|
||||
}
|
||||
: {
|
||||
_templateId: 'skill-context-keyword',
|
||||
matcher: '',
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: 'ccw hook keyword --stdin',
|
||||
}],
|
||||
};
|
||||
await saveHook(scope, 'UserPromptSubmit', hookData);
|
||||
if (skillConfig.mode === 'auto') {
|
||||
const template = HOOK_TEMPLATES['skill-context-auto'];
|
||||
const hookData = {
|
||||
command: template.command,
|
||||
args: [...template.args],
|
||||
};
|
||||
const converted = convertToClaudeCodeFormat(hookData);
|
||||
await saveHook(scope, template.event, converted);
|
||||
} else {
|
||||
const validConfigs = skillConfig.skillConfigs.filter(c => c.skill && c.keywords);
|
||||
if (validConfigs.length === 0) break;
|
||||
const configJson = validConfigs.map(c => ({
|
||||
skill: c.skill,
|
||||
keywords: c.keywords.split(',').map(k => k.trim()).filter(k => k),
|
||||
}));
|
||||
const paramsStr = JSON.stringify({ configs: configJson });
|
||||
const hookData = {
|
||||
command: 'node',
|
||||
args: ['-e', `const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify(Object.assign(${paramsStr},{prompt:p.user_prompt||''}))],{stdio:'inherit'})`],
|
||||
};
|
||||
const converted = convertToClaudeCodeFormat(hookData);
|
||||
await saveHook(scope, 'UserPromptSubmit', converted);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -796,22 +916,34 @@ export function HookWizard({
|
||||
const getPreviewCommand = (): string => {
|
||||
switch (wizardType) {
|
||||
case 'memory-update': {
|
||||
return `ccw hook template exec memory-auto-compress --stdin`;
|
||||
const selectedTool = memoryConfig.tool;
|
||||
return `node -e "require('child_process').spawnSync(process.platform==='win32'?'cmd':'ccw',process.platform==='win32'?['/c','ccw','tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})]:['tool','exec','memory_queue',JSON.stringify({action:'add',path:process.env.CLAUDE_PROJECT_DIR,tool:'${selectedTool}'})],{stdio:'inherit'})"`;
|
||||
}
|
||||
case 'danger-protection': {
|
||||
const templates = dangerConfig.selectedOptions
|
||||
.map(id => DANGER_OPTIONS.find(o => o.id === id))
|
||||
.filter(Boolean)
|
||||
.map(opt => {
|
||||
const templateId = TEMPLATE_IDS[opt!.templateId as keyof typeof TEMPLATE_IDS] || opt!.templateId;
|
||||
return `ccw hook template exec ${templateId} --stdin`;
|
||||
});
|
||||
const tpl = HOOK_TEMPLATES[opt!.templateId];
|
||||
return tpl ? `[${tpl.event}/${tpl.matcher || '*'}] ${tpl.command} ${tpl.args[0]} ...` : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
return templates.length > 0
|
||||
? templates.join('\n')
|
||||
: '# No protections selected';
|
||||
}
|
||||
case 'skill-context': {
|
||||
return `ccw hook keyword --stdin`;
|
||||
if (skillConfig.mode === 'auto') {
|
||||
return `node -e "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({mode:'auto',prompt:p.user_prompt||''})],{stdio:'inherit'})"`;
|
||||
}
|
||||
const validConfigs = skillConfig.skillConfigs.filter(c => c.skill && c.keywords);
|
||||
if (validConfigs.length === 0) return '# No SKILL configurations yet';
|
||||
const configJson = validConfigs.map(c => ({
|
||||
skill: c.skill,
|
||||
keywords: c.keywords.split(',').map(k => k.trim()).filter(k => k),
|
||||
}));
|
||||
const paramsStr = JSON.stringify({ configs: configJson });
|
||||
return `node -e "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify(Object.assign(${paramsStr},{prompt:p.user_prompt||''}))],{stdio:'inherit'})"`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { HookCard, HookFormDialog, HookQuickTemplates, HookWizard, type HookCardData, type HookFormData, type HookTriggerType, HOOK_TEMPLATES, type WizardType } from '@/components/hook';
|
||||
import { useHooks, useToggleHook } from '@/hooks';
|
||||
import { installHookTemplate } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
@@ -267,25 +268,12 @@ export function HookManagerPage() {
|
||||
|
||||
setInstallingTemplateId(templateId);
|
||||
try {
|
||||
// Use backend API to install template
|
||||
const response = await fetch('/api/hooks/templates/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
scope: 'project',
|
||||
}),
|
||||
await installHookTemplate(template.trigger, {
|
||||
id: template.id,
|
||||
command: template.command,
|
||||
args: template.args ? [...template.args] : undefined,
|
||||
matcher: template.matcher,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to install template: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Unknown error');
|
||||
}
|
||||
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
console.error('Failed to install template:', error);
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
/**
|
||||
* Hook Command - CLI endpoint for Claude Code hooks
|
||||
* Provides simplified interface for hook operations, replacing complex bash/curl commands
|
||||
*
|
||||
* Subcommands:
|
||||
* parse-status - Parse CCW status.json
|
||||
* session-context - Progressive session context loading
|
||||
* session-end - Trigger background memory tasks
|
||||
* stop - Handle Stop hook events
|
||||
* keyword - Detect mode keywords
|
||||
* pre-compact - Handle PreCompact events
|
||||
* notify - Send notification to dashboard
|
||||
* project-state - Output project state summary
|
||||
* template - Manage and execute hook templates
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
@@ -48,7 +37,6 @@ interface HookData {
|
||||
|
||||
/**
|
||||
* Read JSON data from stdin (for Claude Code hooks)
|
||||
* Returns the raw string data
|
||||
*/
|
||||
async function readStdin(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -727,164 +715,6 @@ async function notifyAction(options: HookOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Template action - manage and execute hook templates
|
||||
*
|
||||
* Subcommands:
|
||||
* list - List all available templates
|
||||
* install <id> - Install a template to settings.json
|
||||
* exec <id> - Execute a template (for hooks)
|
||||
*/
|
||||
async function templateAction(subcommand: string, args: string[], options: HookOptions): Promise<void> {
|
||||
const { stdin } = options;
|
||||
|
||||
// Dynamic import to avoid circular dependencies
|
||||
const {
|
||||
getTemplate,
|
||||
getAllTemplates,
|
||||
listTemplatesByCategory,
|
||||
executeTemplate,
|
||||
installTemplateToSettings,
|
||||
} = await import('../core/hooks/hook-templates.js');
|
||||
type HookInputData = import('../core/hooks/hook-templates.js').HookInputData;
|
||||
|
||||
switch (subcommand) {
|
||||
case 'list': {
|
||||
const byCategory = listTemplatesByCategory();
|
||||
const categoryNames: Record<string, string> = {
|
||||
notification: 'Notification',
|
||||
indexing: 'Indexing',
|
||||
automation: 'Automation',
|
||||
utility: 'Utility',
|
||||
protection: 'Protection',
|
||||
};
|
||||
|
||||
if (stdin) {
|
||||
// JSON output for programmatic use
|
||||
process.stdout.write(JSON.stringify(byCategory, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(chalk.green('Hook Templates'));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
for (const [category, templates] of Object.entries(byCategory)) {
|
||||
if (templates.length === 0) continue;
|
||||
console.log(chalk.cyan(`\n${categoryNames[category] || category}:`));
|
||||
for (const t of templates) {
|
||||
console.log(` ${chalk.yellow(t.id)}`);
|
||||
console.log(` ${chalk.gray(t.description)}`);
|
||||
console.log(` Trigger: ${t.trigger}${t.matcher ? ` (${t.matcher})` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.gray('\n─'.repeat(50)));
|
||||
console.log(chalk.gray('Usage: ccw hook template install <id> [--scope project|global]'));
|
||||
console.log(chalk.gray(' ccw hook template exec <id> --stdin'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
case 'install': {
|
||||
const templateId = args[0];
|
||||
if (!templateId) {
|
||||
console.error(chalk.red('Error: template ID required'));
|
||||
console.error(chalk.gray('Usage: ccw hook template install <id> [--scope project|global]'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scope = args.includes('--global') ? 'global' : 'project';
|
||||
const result = installTemplateToSettings(templateId, scope);
|
||||
|
||||
if (stdin) {
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
process.exit(result.success ? 0 : 1);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
console.log(chalk.green(result.message));
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(chalk.red(result.message));
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'exec': {
|
||||
const templateId = args[0];
|
||||
if (!templateId) {
|
||||
if (stdin) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red('Error: template ID required'));
|
||||
console.error(chalk.gray('Usage: ccw hook template exec <id> --stdin'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const template = getTemplate(templateId);
|
||||
if (!template) {
|
||||
if (stdin) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Template not found: ${templateId}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read hook data from stdin
|
||||
let hookData: HookInputData = {};
|
||||
if (stdin) {
|
||||
try {
|
||||
const stdinData = await readStdin();
|
||||
if (stdinData) {
|
||||
hookData = JSON.parse(stdinData) as HookInputData;
|
||||
}
|
||||
} catch {
|
||||
// Continue with empty data
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await executeTemplate(templateId, hookData);
|
||||
|
||||
// Handle JSON output for hook decisions
|
||||
if (output.jsonOutput) {
|
||||
process.stdout.write(JSON.stringify(output.jsonOutput));
|
||||
process.exit(output.exitCode || 0);
|
||||
}
|
||||
|
||||
// Handle stderr
|
||||
if (output.stderr) {
|
||||
process.stderr.write(output.stderr);
|
||||
}
|
||||
|
||||
// Handle stdout
|
||||
if (output.stdout) {
|
||||
process.stdout.write(output.stdout);
|
||||
}
|
||||
|
||||
process.exit(output.exitCode || 0);
|
||||
} catch (error) {
|
||||
if (stdin) {
|
||||
// Silent failure for hooks
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (stdin) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Unknown template subcommand: ${subcommand}`));
|
||||
console.error(chalk.gray('Usage: ccw hook template <list|install|exec> [args]'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Project state action - reads project-tech.json and specs
|
||||
* and outputs a concise summary for session context injection.
|
||||
@@ -1028,7 +858,6 @@ ${chalk.bold('SUBCOMMANDS')}
|
||||
pre-compact Handle PreCompact hook events (checkpoint creation)
|
||||
notify Send notification to ccw view dashboard
|
||||
project-state Output project guidelines and recent dev history summary
|
||||
template Manage and execute hook templates (list, install, exec)
|
||||
|
||||
${chalk.bold('OPTIONS')}
|
||||
--stdin Read input from stdin (for Claude Code hooks)
|
||||
@@ -1037,11 +866,6 @@ ${chalk.bold('OPTIONS')}
|
||||
--session-id Session ID (alternative to stdin)
|
||||
--prompt Current prompt text (alternative to stdin)
|
||||
|
||||
${chalk.bold('TEMPLATE SUBCOMMANDS')}
|
||||
template list List all available hook templates
|
||||
template install <id> [--global] Install a template to settings.json
|
||||
template exec <id> --stdin Execute a template (for Claude Code hooks)
|
||||
|
||||
${chalk.bold('EXAMPLES')}
|
||||
${chalk.gray('# Parse CCW status file:')}
|
||||
ccw hook parse-status --path .workflow/.ccw/ccw-123/status.json
|
||||
@@ -1070,15 +894,6 @@ ${chalk.bold('EXAMPLES')}
|
||||
${chalk.gray('# Project state summary (hook, reads cwd from stdin):')}
|
||||
ccw hook project-state --stdin
|
||||
|
||||
${chalk.gray('# List available templates:')}
|
||||
ccw hook template list
|
||||
|
||||
${chalk.gray('# Install a template:')}
|
||||
ccw hook template install block-sensitive-files
|
||||
|
||||
${chalk.gray('# Execute a template (for hooks):')}
|
||||
ccw hook template exec block-sensitive-files --stdin
|
||||
|
||||
${chalk.bold('HOOK CONFIGURATION')}
|
||||
${chalk.gray('Add to .claude/settings.json for Stop hook:')}
|
||||
{
|
||||
@@ -1093,16 +908,14 @@ ${chalk.bold('HOOK CONFIGURATION')}
|
||||
}
|
||||
}
|
||||
|
||||
${chalk.gray('Add to .claude/settings.json using templates (recommended):')}
|
||||
${chalk.gray('Add to .claude/settings.json for status tracking:')}
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"_templateId": "block-sensitive-files",
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "ccw hook template exec block-sensitive-files --stdin"
|
||||
}]
|
||||
"PostToolUse": [{
|
||||
"trigger": "PostToolUse",
|
||||
"matcher": "Write",
|
||||
"command": "bash",
|
||||
"args": ["-c", "INPUT=$(cat); FILE_PATH=$(echo \\"$INPUT\\" | jq -r \\".tool_input.file_path // empty\\"); [ -n \\"$FILE_PATH\\" ] && ccw hook parse-status --path \\"$FILE_PATH\\""]
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1117,8 +930,6 @@ export async function hookCommand(
|
||||
args: string | string[],
|
||||
options: HookOptions
|
||||
): Promise<void> {
|
||||
const argsArray = Array.isArray(args) ? args : [args];
|
||||
|
||||
switch (subcommand) {
|
||||
case 'parse-status':
|
||||
await parseStatusAction(options);
|
||||
@@ -1146,10 +957,6 @@ export async function hookCommand(
|
||||
case 'project-state':
|
||||
await projectStateAction(options);
|
||||
break;
|
||||
case 'template':
|
||||
// template has its own subcommands: list, install, exec
|
||||
await templateAction(argsArray[0] || 'list', argsArray.slice(1), options);
|
||||
break;
|
||||
case 'help':
|
||||
case undefined:
|
||||
showHelp();
|
||||
|
||||
@@ -1,778 +0,0 @@
|
||||
/**
|
||||
* Hook Templates - Backend Template Definitions
|
||||
*
|
||||
* All hook templates are defined here and executed via `ccw hook template exec <id> --stdin`.
|
||||
* This avoids Windows Git Bash quote handling issues when inline scripts are used.
|
||||
*
|
||||
* Usage:
|
||||
* ccw hook template list - List available templates
|
||||
* ccw hook template install <id> [--scope project|global] - Install template to settings.json
|
||||
* ccw hook template exec <id> --stdin - Execute template logic (for hooks)
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join, resolve, basename } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type HookTriggerType =
|
||||
| 'SessionStart'
|
||||
| 'UserPromptSubmit'
|
||||
| 'PreToolUse'
|
||||
| 'PostToolUse'
|
||||
| 'Notification'
|
||||
| 'Stop'
|
||||
| 'PreCompact';
|
||||
|
||||
export type TemplateCategory = 'notification' | 'indexing' | 'automation' | 'utility' | 'protection';
|
||||
|
||||
export interface HookTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: TemplateCategory;
|
||||
trigger: HookTriggerType;
|
||||
matcher?: string;
|
||||
timeout?: number;
|
||||
/** Execute function - receives parsed stdin data */
|
||||
execute: (data: HookInputData) => HookOutput | Promise<HookOutput>;
|
||||
}
|
||||
|
||||
export interface HookInputData {
|
||||
session_id?: string;
|
||||
cwd?: string;
|
||||
prompt?: string;
|
||||
user_prompt?: string;
|
||||
tool_name?: string;
|
||||
tool_input?: Record<string, unknown>;
|
||||
stop_reason?: string;
|
||||
stopReason?: string;
|
||||
end_turn_reason?: string;
|
||||
endTurnReason?: string;
|
||||
user_requested?: boolean;
|
||||
userRequested?: boolean;
|
||||
active_mode?: string;
|
||||
activeMode?: string;
|
||||
active_workflow?: boolean;
|
||||
activeWorkflow?: boolean;
|
||||
transcript_path?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface HookOutput {
|
||||
/** Exit code: 0 = success, 2 = block */
|
||||
exitCode?: 0 | 2;
|
||||
/** stdout content (for system message injection) */
|
||||
stdout?: string;
|
||||
/** stderr content (for error messages) */
|
||||
stderr?: string;
|
||||
/** JSON output for hook decision */
|
||||
jsonOutput?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Send notification to dashboard via HTTP (using native fetch)
|
||||
*/
|
||||
function notifyDashboard(type: string, payload: Record<string, unknown>): void {
|
||||
const data = {
|
||||
type,
|
||||
...payload,
|
||||
project: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Use native fetch (Node.js 18+) to avoid shell command injection
|
||||
fetch('http://localhost:3456/api/hook', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
}).catch(() => {
|
||||
// Silently ignore errors - dashboard may not be running
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file matches sensitive patterns
|
||||
*/
|
||||
function isSensitiveFile(filePath: string): boolean {
|
||||
return /\.env|secret|credential|\.key$|\.pem$|id_rsa|\.credentials/i.test(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command matches dangerous patterns
|
||||
*/
|
||||
function isDangerousCommand(cmd: string): boolean {
|
||||
const patterns = [
|
||||
/rm\s+-rf/i,
|
||||
/rmdir/i,
|
||||
/del\s+\//i,
|
||||
/format\s+/i,
|
||||
/shutdown/i,
|
||||
/reboot/i,
|
||||
/kill\s+-9/i,
|
||||
/pkill/i,
|
||||
/mkfs/i,
|
||||
/dd\s+if=/i,
|
||||
/chmod\s+777/i,
|
||||
/chown\s+-R/i,
|
||||
/>\s*\/dev\//i,
|
||||
/wget.*\|.*sh/i,
|
||||
/curl.*\|.*bash/i,
|
||||
];
|
||||
return patterns.some(p => p.test(cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command is a dangerous git operation
|
||||
*/
|
||||
function isDangerousGitCommand(cmd: string): boolean {
|
||||
const patterns = [
|
||||
/git\s+push.*--force/i,
|
||||
/git\s+push.*-f/i,
|
||||
/git\s+reset\s+--hard/i,
|
||||
/git\s+clean\s+-fd/i,
|
||||
/git\s+checkout.*--force/i,
|
||||
/git\s+branch\s+-D/i,
|
||||
/git\s+rebase.*-f/i,
|
||||
];
|
||||
return patterns.some(p => p.test(cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extract string value from unknown input
|
||||
*/
|
||||
function getStringInput(value: unknown): string {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file path to prevent command injection
|
||||
* Returns null if path is invalid, otherwise returns the sanitized path
|
||||
*/
|
||||
function validateFilePath(filePath: string): string | null {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for dangerous characters that could be used for command injection
|
||||
const dangerousPatterns = /[;&|`$(){}[\]<>!\\]/;
|
||||
if (dangerousPatterns.test(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if (filePath.includes('..')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for null bytes
|
||||
if (filePath.includes('\0')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe spawnSync wrapper that avoids shell: true to prevent command injection
|
||||
* On Windows, this uses .cmd extension for npm/npx commands
|
||||
*/
|
||||
function safeSpawnSync(command: string, args: string[]): { stdout: string; stderr: string; status: number | null } {
|
||||
// Use spawnSync without shell to avoid command injection
|
||||
// Note: On Windows, npx/npm/git may need .cmd extension
|
||||
const isWindows = process.platform === 'win32';
|
||||
const execCommand = isWindows && !command.endsWith('.cmd') && ['npx', 'npm', 'git', 'ccw'].includes(command)
|
||||
? `${command}.cmd`
|
||||
: command;
|
||||
|
||||
return spawnSync(execCommand, args, {
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
shell: false,
|
||||
encoding: 'utf8',
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe spawnSync with inherited stdio (for tools that need interactive output)
|
||||
*/
|
||||
function safeSpawnSyncInherit(command: string, args: string[]): void {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const execCommand = isWindows && !command.endsWith('.cmd') && ['npx', 'npm', 'git', 'ccw'].includes(command)
|
||||
? `${command}.cmd`
|
||||
: command;
|
||||
|
||||
spawnSync(execCommand, args, {
|
||||
stdio: 'inherit',
|
||||
shell: false,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is in protected system paths
|
||||
*/
|
||||
function isSystemPath(path: string): boolean {
|
||||
const sysPatterns = [
|
||||
/\/etc\//i,
|
||||
/\/usr\//i,
|
||||
/\/bin\//i,
|
||||
/\/sbin\//i,
|
||||
/\/boot\//i,
|
||||
/\/sys\//i,
|
||||
/\/proc\//i,
|
||||
/C:\\Windows/i,
|
||||
/C:\\Program Files/i,
|
||||
];
|
||||
return sysPatterns.some(p => p.test(path));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hook Templates
|
||||
// ============================================================================
|
||||
|
||||
export const HOOK_TEMPLATES: HookTemplate[] = [
|
||||
// ============ Notification Templates ============
|
||||
{
|
||||
id: 'session-start-notify',
|
||||
name: 'Session Start Notify',
|
||||
description: 'Notify dashboard when a new workflow session is created',
|
||||
category: 'notification',
|
||||
trigger: 'SessionStart',
|
||||
execute: () => {
|
||||
notifyDashboard('SESSION_CREATED', {});
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'session-state-watch',
|
||||
name: 'Session State Watch',
|
||||
description: 'Watch for session metadata file changes (workflow-session.json)',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
execute: (data) => {
|
||||
const file = getStringInput(data.tool_input?.file_path);
|
||||
if (file && /workflow-session\.json$|session-metadata\.json$/.test(file)) {
|
||||
try {
|
||||
if (existsSync(file)) {
|
||||
const content = readFileSync(file, 'utf8');
|
||||
const sessionData = JSON.parse(content);
|
||||
notifyDashboard('SESSION_STATE_CHANGED', {
|
||||
file,
|
||||
sessionId: sessionData.session_id || '',
|
||||
status: sessionData.status || 'unknown',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'stop-notify',
|
||||
name: 'Stop Notify',
|
||||
description: 'Notify dashboard when Claude finishes responding',
|
||||
category: 'notification',
|
||||
trigger: 'Stop',
|
||||
execute: () => {
|
||||
notifyDashboard('TASK_COMPLETED', {});
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'memory-sync-dashboard',
|
||||
name: 'Memory Sync Dashboard',
|
||||
description: 'Sync memory V2 status to dashboard on changes',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'mcp__ccw-tools__core_memory',
|
||||
execute: () => {
|
||||
notifyDashboard('MEMORY_V2_STATUS_UPDATED', {});
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
// ============ Automation Templates ============
|
||||
{
|
||||
id: 'auto-format-on-write',
|
||||
name: 'Auto Format on Write',
|
||||
description: 'Auto-format files after Claude writes or edits them',
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
execute: (data) => {
|
||||
const rawFile = getStringInput(data.tool_input?.file_path);
|
||||
const file = validateFilePath(rawFile);
|
||||
if (file) {
|
||||
safeSpawnSyncInherit('npx', ['prettier', '--write', file]);
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'auto-lint-on-write',
|
||||
name: 'Auto Lint on Write',
|
||||
description: 'Auto-lint files after Claude writes or edits them',
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
execute: (data) => {
|
||||
const rawFile = getStringInput(data.tool_input?.file_path);
|
||||
const file = validateFilePath(rawFile);
|
||||
if (file) {
|
||||
safeSpawnSyncInherit('npx', ['eslint', '--fix', file]);
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'git-auto-stage',
|
||||
name: 'Git Auto Stage',
|
||||
description: 'Auto stage all modified files when Claude finishes responding',
|
||||
category: 'automation',
|
||||
trigger: 'Stop',
|
||||
execute: () => {
|
||||
safeSpawnSyncInherit('git', ['add', '-u']);
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
// ============ Protection Templates ============
|
||||
{
|
||||
id: 'block-sensitive-files',
|
||||
name: 'Block Sensitive Files',
|
||||
description: 'Block modifications to sensitive files (.env, secrets, credentials)',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
execute: (data) => {
|
||||
const file = getStringInput(data.tool_input?.file_path);
|
||||
if (file && isSensitiveFile(file)) {
|
||||
return {
|
||||
exitCode: 2,
|
||||
stderr: `Blocked: modifying sensitive file ${file}`,
|
||||
};
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'danger-bash-confirm',
|
||||
name: 'Danger Bash Confirm',
|
||||
description: 'Require confirmation for dangerous bash commands',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
execute: (data) => {
|
||||
const cmd = (data.tool_input?.command as string) || '';
|
||||
if (isDangerousCommand(cmd)) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'ask',
|
||||
permissionDecisionReason: `Potentially dangerous command detected: requires user confirmation`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'danger-file-protection',
|
||||
name: 'Danger File Protection',
|
||||
description: 'Block modifications to protected files',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
execute: (data) => {
|
||||
const file = getStringInput(data.tool_input?.file_path);
|
||||
const protectedPatterns = /\.env|\.git\/|package-lock\.json|yarn\.lock|\.credentials|secrets|id_rsa|\.pem$|\.key$/i;
|
||||
if (file && protectedPatterns.test(file)) {
|
||||
return {
|
||||
exitCode: 2,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'deny',
|
||||
permissionDecisionReason: `Protected file cannot be modified: ${file}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'danger-git-destructive',
|
||||
name: 'Danger Git Destructive',
|
||||
description: 'Require confirmation for destructive git operations',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
execute: (data) => {
|
||||
const cmd = (data.tool_input?.command as string) || '';
|
||||
if (isDangerousGitCommand(cmd)) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'ask',
|
||||
permissionDecisionReason: `Destructive git operation detected: ${cmd}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'danger-network-confirm',
|
||||
name: 'Danger Network Confirm',
|
||||
description: 'Require confirmation for network operations',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash|WebFetch',
|
||||
execute: (data) => {
|
||||
const tool = data.tool_name || '';
|
||||
|
||||
if (tool === 'WebFetch') {
|
||||
const url = (data.tool_input?.url as string) || '';
|
||||
return {
|
||||
exitCode: 0,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'ask',
|
||||
permissionDecisionReason: `Network request to: ${url}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const cmd = (data.tool_input?.command as string) || '';
|
||||
const netCmds = /^(curl|wget|nc |netcat|ssh |scp |rsync|ftp )/i;
|
||||
if (netCmds.test(cmd)) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'ask',
|
||||
permissionDecisionReason: `Network command requires confirmation: ${cmd}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'danger-system-paths',
|
||||
name: 'Danger System Paths',
|
||||
description: 'Block modifications to system paths',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Write|Edit|Bash',
|
||||
execute: (data) => {
|
||||
const tool = data.tool_name || '';
|
||||
|
||||
if (tool === 'Bash') {
|
||||
const cmd = (data.tool_input?.command as string) || '';
|
||||
if (isSystemPath(cmd)) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'ask',
|
||||
permissionDecisionReason: `System path operation requires confirmation`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const file = getStringInput(data.tool_input?.file_path);
|
||||
if (file && isSystemPath(file)) {
|
||||
return {
|
||||
exitCode: 2,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'deny',
|
||||
permissionDecisionReason: `Cannot modify system file: ${file}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'danger-permission-change',
|
||||
name: 'Danger Permission Change',
|
||||
description: 'Require confirmation for permission changes',
|
||||
category: 'protection',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: 'Bash',
|
||||
execute: (data) => {
|
||||
const cmd = (data.tool_input?.command as string) || '';
|
||||
const permCmds = /^(chmod|chown|chgrp|setfacl|icacls|takeown|cacls)/i;
|
||||
if (permCmds.test(cmd)) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
jsonOutput: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'ask',
|
||||
permissionDecisionReason: `Permission change requires confirmation: ${cmd}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
// ============ Indexing Templates ============
|
||||
{
|
||||
id: 'post-edit-index',
|
||||
name: 'Post Edit Index',
|
||||
description: 'Notify indexing service when files are modified',
|
||||
category: 'indexing',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
execute: (data) => {
|
||||
const file = getStringInput(data.tool_input?.file_path);
|
||||
if (file) {
|
||||
notifyDashboard('FILE_MODIFIED', { file });
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'session-end-summary',
|
||||
name: 'Session End Summary',
|
||||
description: 'Send session summary to dashboard on session end',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
execute: (data) => {
|
||||
notifyDashboard('SESSION_SUMMARY', {
|
||||
transcript: data.transcript_path || '',
|
||||
});
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
// ============ Utility Templates ============
|
||||
{
|
||||
id: 'memory-auto-compress',
|
||||
name: 'Auto Memory Compress',
|
||||
description: 'Automatically compress memory when entries exceed threshold',
|
||||
category: 'utility',
|
||||
trigger: 'Stop',
|
||||
execute: () => {
|
||||
safeSpawnSyncInherit('ccw', ['memory', 'consolidate', '--threshold', '50']);
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'memory-preview-extract',
|
||||
name: 'Memory Preview & Extract',
|
||||
description: 'Preview extraction queue and extract eligible sessions',
|
||||
category: 'utility',
|
||||
trigger: 'SessionStart',
|
||||
execute: () => {
|
||||
safeSpawnSyncInherit('ccw', ['memory', 'preview', '--include-native']);
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'memory-status-check',
|
||||
name: 'Memory Status Check',
|
||||
description: 'Check memory extraction and consolidation status',
|
||||
category: 'utility',
|
||||
trigger: 'SessionStart',
|
||||
execute: () => {
|
||||
safeSpawnSyncInherit('ccw', ['memory', 'status']);
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'memory-v2-extract',
|
||||
name: 'Memory V2 Extract',
|
||||
description: 'Trigger Phase 1 extraction when session ends',
|
||||
category: 'utility',
|
||||
trigger: 'Stop',
|
||||
execute: () => {
|
||||
safeSpawnSyncInherit('ccw', ['core-memory', 'extract', '--max-sessions', '10']);
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'memory-v2-auto-consolidate',
|
||||
name: 'Memory V2 Auto Consolidate',
|
||||
description: 'Trigger Phase 2 consolidation after extraction jobs complete',
|
||||
category: 'utility',
|
||||
trigger: 'Stop',
|
||||
execute: () => {
|
||||
const result = safeSpawnSync('ccw', ['core-memory', 'extract', '--json']);
|
||||
try {
|
||||
const d = JSON.parse(result.stdout);
|
||||
if (d && d.total_stage1 >= 5) {
|
||||
safeSpawnSyncInherit('ccw', ['core-memory', 'consolidate']);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Template Registry
|
||||
// ============================================================================
|
||||
|
||||
const templateMap = new Map<string, HookTemplate>();
|
||||
HOOK_TEMPLATES.forEach(t => templateMap.set(t.id, t));
|
||||
|
||||
/**
|
||||
* Get template by ID
|
||||
*/
|
||||
export function getTemplate(id: string): HookTemplate | undefined {
|
||||
return templateMap.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all templates grouped by category
|
||||
*/
|
||||
export function listTemplatesByCategory(): Record<TemplateCategory, HookTemplate[]> {
|
||||
const result: Record<TemplateCategory, HookTemplate[]> = {
|
||||
notification: [],
|
||||
indexing: [],
|
||||
automation: [],
|
||||
utility: [],
|
||||
protection: [],
|
||||
};
|
||||
HOOK_TEMPLATES.forEach(t => {
|
||||
result[t.category].push(t);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all templates
|
||||
*/
|
||||
export function getAllTemplates(): HookTemplate[] {
|
||||
return [...HOOK_TEMPLATES];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a template by ID
|
||||
*/
|
||||
export async function executeTemplate(id: string, data: HookInputData): Promise<HookOutput> {
|
||||
const template = templateMap.get(id);
|
||||
if (!template) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
stderr: `Template not found: ${id}`,
|
||||
};
|
||||
}
|
||||
return template.execute(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings.json hook configuration for a template
|
||||
*/
|
||||
export function generateHookConfig(template: HookTemplate): Record<string, unknown> {
|
||||
const config: Record<string, unknown> = {
|
||||
_templateId: template.id,
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: `ccw hook template exec ${template.id} --stdin`,
|
||||
...(template.timeout ? { timeout: template.timeout } : {}),
|
||||
}],
|
||||
};
|
||||
|
||||
if (template.matcher) {
|
||||
config.matcher = template.matcher;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a template to settings.json
|
||||
*/
|
||||
export function installTemplateToSettings(
|
||||
templateId: string,
|
||||
scope: 'project' | 'global' = 'project'
|
||||
): { success: boolean; message: string } {
|
||||
const template = templateMap.get(templateId);
|
||||
if (!template) {
|
||||
return { success: false, message: `Template not found: ${templateId}` };
|
||||
}
|
||||
|
||||
const settingsPath = scope === 'global'
|
||||
? join(homedir(), '.claude', 'settings.json')
|
||||
: join(process.cwd(), '.claude', 'settings.json');
|
||||
|
||||
let settings: Record<string, unknown> = {};
|
||||
|
||||
if (existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
||||
} catch {
|
||||
return { success: false, message: `Failed to parse ${settingsPath}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize hooks structure
|
||||
if (!settings.hooks) {
|
||||
settings.hooks = {};
|
||||
}
|
||||
const hooks = settings.hooks as Record<string, unknown[]>;
|
||||
if (!hooks[template.trigger]) {
|
||||
hooks[template.trigger] = [];
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
const triggerHooks = hooks[template.trigger] as Array<Record<string, unknown>>;
|
||||
const alreadyInstalled = triggerHooks.some((h) =>
|
||||
h._templateId === templateId
|
||||
);
|
||||
|
||||
if (alreadyInstalled) {
|
||||
return { success: true, message: `Template ${templateId} already installed` };
|
||||
}
|
||||
|
||||
// Add the hook
|
||||
triggerHooks.push(generateHookConfig(template));
|
||||
|
||||
// Write back
|
||||
try {
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
return { success: true, message: `Template ${templateId} installed to ${settingsPath}` };
|
||||
} catch (e) {
|
||||
return { success: false, message: `Failed to write settings: ${(e as Error).message}` };
|
||||
}
|
||||
}
|
||||
@@ -708,78 +708,6 @@ export async function handleHooksRoutes(ctx: HooksRouteContext): Promise<boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get hook templates list
|
||||
if (pathname === '/api/hooks/templates' && req.method === 'GET') {
|
||||
(async () => {
|
||||
try {
|
||||
const { getAllTemplates, listTemplatesByCategory } = await import('../hooks/hook-templates.js');
|
||||
const category = url.searchParams.get('category');
|
||||
|
||||
if (category) {
|
||||
const byCategory = listTemplatesByCategory();
|
||||
const templates = byCategory[category as keyof typeof byCategory] || [];
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, templates }));
|
||||
} else {
|
||||
const templates = getAllTemplates();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, templates }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Hooks] Failed to get templates:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: (error as Error).message }));
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Install hook template
|
||||
if (pathname === '/api/hooks/templates/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
if (typeof body !== 'object' || body === null) {
|
||||
return { error: 'Invalid request body', status: 400 };
|
||||
}
|
||||
|
||||
const { templateId, scope = 'project', projectPath } = body as {
|
||||
templateId?: unknown;
|
||||
scope?: unknown;
|
||||
projectPath?: unknown;
|
||||
};
|
||||
|
||||
if (typeof templateId !== 'string') {
|
||||
return { error: 'templateId is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const { installTemplateToSettings } = await import('../hooks/hook-templates.js');
|
||||
const resolvedProjectPath = typeof projectPath === 'string' && projectPath.trim().length > 0
|
||||
? projectPath
|
||||
: initialPath;
|
||||
|
||||
// Override process.cwd() for project-scoped installation
|
||||
const originalCwd = process.cwd;
|
||||
if (scope === 'project') {
|
||||
process.cwd = () => resolvedProjectPath;
|
||||
}
|
||||
|
||||
const result = installTemplateToSettings(
|
||||
templateId,
|
||||
(scope === 'global' ? 'global' : 'project') as 'global' | 'project'
|
||||
);
|
||||
|
||||
// Restore original cwd
|
||||
process.cwd = originalCwd;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[Hooks] Failed to install template:', error);
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Migrate Hook Templates Script
|
||||
*
|
||||
* This script helps migrate hook templates from inline bash/node commands
|
||||
* to the new `ccw hook template exec` approach, which avoids Windows Git Bash
|
||||
* quote handling issues.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/migrate-hook-templates.ts [--dry-run] [--settings path]
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
interface OldHookEntry {
|
||||
matcher?: string;
|
||||
command?: string;
|
||||
_templateId?: string;
|
||||
hooks?: Array<{
|
||||
type?: string;
|
||||
command?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
hooks?: Record<string, OldHookEntry[]>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Command patterns that indicate old-style inline scripts
|
||||
const OLD_PATTERNS = [
|
||||
/bash\s+-c.*jq/,
|
||||
/node\s+-e.*child_process/,
|
||||
/node\s+-e.*spawnSync/,
|
||||
/command.*node -e ".*\{.*\}.*"/,
|
||||
];
|
||||
|
||||
// Mapping from old patterns to new template IDs
|
||||
const MIGRATION_MAP: Record<string, string> = {
|
||||
'danger-bash-confirm': 'danger-bash-confirm',
|
||||
'danger-file-protection': 'danger-file-protection',
|
||||
'danger-git-destructive': 'danger-git-destructive',
|
||||
'danger-network-confirm': 'danger-network-confirm',
|
||||
'danger-system-paths': 'danger-system-paths',
|
||||
'danger-permission-change': 'danger-permission-change',
|
||||
'memory-update-queue': 'memory-auto-compress',
|
||||
'memory-v2-extract': 'memory-v2-extract',
|
||||
'session-start-notify': 'session-start-notify',
|
||||
'stop-notify': 'stop-notify',
|
||||
'session-state-watch': 'session-state-watch',
|
||||
'auto-format-on-write': 'auto-format-on-write',
|
||||
'auto-lint-on-write': 'auto-lint-on-write',
|
||||
'block-sensitive-files': 'block-sensitive-files',
|
||||
'git-auto-stage': 'git-auto-stage',
|
||||
'post-edit-index': 'post-edit-index',
|
||||
'memory-preview-extract': 'memory-preview-extract',
|
||||
'memory-status-check': 'memory-status-check',
|
||||
};
|
||||
|
||||
function detectTemplateFromCommand(command: string): string | null {
|
||||
for (const [pattern, templateId] of Object.entries(MIGRATION_MAP)) {
|
||||
if (command.includes(pattern)) {
|
||||
return templateId;
|
||||
}
|
||||
}
|
||||
|
||||
if (command.includes('jq -r') && command.includes('DANGEROUS_PATTERNS')) {
|
||||
return 'danger-bash-confirm';
|
||||
}
|
||||
|
||||
if (command.includes('localhost:3456/api/hook')) {
|
||||
if (command.includes('SESSION_CREATED')) return 'session-start-notify';
|
||||
if (command.includes('TASK_COMPLETED')) return 'stop-notify';
|
||||
if (command.includes('FILE_MODIFIED')) return 'post-edit-index';
|
||||
if (command.includes('SESSION_STATE_CHANGED')) return 'session-state-watch';
|
||||
}
|
||||
|
||||
if (command.includes('prettier --write')) {
|
||||
return 'auto-format-on-write';
|
||||
}
|
||||
|
||||
if (command.includes('eslint --fix')) {
|
||||
return 'auto-lint-on-write';
|
||||
}
|
||||
|
||||
if (command.includes('git add -u')) {
|
||||
return 'git-auto-stage';
|
||||
}
|
||||
|
||||
if (command.includes('.env') && command.includes('credential')) {
|
||||
return 'block-sensitive-files';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isOldStyleHook(entry: OldHookEntry): boolean {
|
||||
const command = entry.command || entry.hooks?.[0]?.command || '';
|
||||
return OLD_PATTERNS.some(pattern => pattern.test(command));
|
||||
}
|
||||
|
||||
function migrateHookEntry(entry: OldHookEntry, trigger: string): OldHookEntry {
|
||||
const command = entry.command || entry.hooks?.[0]?.command || '';
|
||||
const templateId = detectTemplateFromCommand(command);
|
||||
|
||||
if (!templateId) {
|
||||
console.log(` ⚠️ Could not auto-detect template for: ${command.substring(0, 50)}...`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
console.log(` ✓ Migrating to template: ${templateId}`);
|
||||
|
||||
return {
|
||||
_templateId: templateId,
|
||||
matcher: entry.matcher,
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: `ccw hook template exec ${templateId} --stdin`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function migrateSettings(settings: Settings, dryRun: boolean): Settings {
|
||||
const migrated = { ...settings };
|
||||
|
||||
if (!migrated.hooks) {
|
||||
return migrated;
|
||||
}
|
||||
|
||||
console.log('\n📋 Analyzing hooks...');
|
||||
|
||||
for (const [trigger, entries] of Object.entries(migrated.hooks)) {
|
||||
if (!Array.isArray(entries)) continue;
|
||||
|
||||
console.log(`\n${trigger}:`);
|
||||
const newEntries: OldHookEntry[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (isOldStyleHook(entry)) {
|
||||
console.log(` Found old-style hook with matcher: ${entry.matcher || '*'}`);
|
||||
const migratedEntry = migrateHookEntry(entry, trigger);
|
||||
newEntries.push(migratedEntry);
|
||||
} else {
|
||||
if (entry.hooks?.[0]?.command?.includes('ccw hook template')) {
|
||||
console.log(` ✓ Already using template: ${(entry as OldHookEntry & { _templateId?: string })._templateId || 'unknown'}`);
|
||||
}
|
||||
newEntries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
migrated.hooks[trigger] = newEntries;
|
||||
}
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
|
||||
let settingsPath: string;
|
||||
const settingsIndex = args.indexOf('--settings');
|
||||
if (settingsIndex >= 0 && args[settingsIndex + 1]) {
|
||||
settingsPath = args[settingsIndex + 1];
|
||||
} else {
|
||||
settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
||||
}
|
||||
|
||||
console.log('🔧 Hook Template Migration Script');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Settings file: ${settingsPath}`);
|
||||
console.log(`Mode: ${dryRun ? 'DRY RUN (no changes)' : 'LIVE (will modify)'}`);
|
||||
|
||||
if (!existsSync(settingsPath)) {
|
||||
console.error(`\n❌ Settings file not found: ${settingsPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let settings: Settings;
|
||||
try {
|
||||
const content = readFileSync(settingsPath, 'utf8');
|
||||
settings = JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.error(`\n❌ Failed to parse settings: ${(e as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const migrated = migrateSettings(settings, dryRun);
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n📄 Migrated settings (dry run):');
|
||||
console.log(JSON.stringify(migrated, null, 2));
|
||||
} else {
|
||||
const backupPath = `${settingsPath}.backup-${Date.now()}`;
|
||||
writeFileSync(backupPath, JSON.stringify(settings, null, 2));
|
||||
console.log(`\n💾 Backup saved to: ${backupPath}`);
|
||||
|
||||
writeFileSync(settingsPath, JSON.stringify(migrated, null, 2));
|
||||
console.log(`\n✅ Settings migrated successfully!`);
|
||||
}
|
||||
|
||||
console.log('\n📌 Next steps:');
|
||||
console.log(' 1. Review the migrated settings');
|
||||
console.log(' 2. Test your hooks to ensure they work correctly');
|
||||
console.log(' 3. Run "ccw hook template list" to see all available templates');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
| 命令 | 功能 | 难度 |
|
||||
| --- | --- | --- |
|
||||
| [`/workflow:lite-lite-lite`](./workflow.md#lite-lite-lite) | 超轻量级多工具分析和直接执行 | Intermediate |
|
||||
| [`/workflow-lite-planex`](./workflow.md#lite-plan) | 轻量级交互式规划工作流 | Intermediate |
|
||||
| [`/workflow:lite-fix`](./workflow.md#lite-fix) | 轻量级 Bug 诊断和修复 | Intermediate |
|
||||
| [`/workflow-plan`](./workflow.md#plan) | 5 阶段规划工作流 | Intermediate |
|
||||
|
||||
@@ -898,31 +898,10 @@ textarea:focus-visible {
|
||||
* "Intelligent Responsive Content Width" section.
|
||||
*/
|
||||
@media (min-width: 1024px) {
|
||||
/* Remove padding from VPDoc to maximize content width */
|
||||
.VPDoc.has-aside {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Remove padding from container to maximize content width */
|
||||
.VPDoc.has-aside .container {
|
||||
justify-content: flex-start !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Keep .aside at fixed width */
|
||||
.VPDoc.has-aside .container .aside {
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
/* Expand .content to fill available space and remove padding */
|
||||
/* Expand .content to fill available space */
|
||||
.VPDoc.has-aside .container .content {
|
||||
flex: 1 1 0 !important;
|
||||
min-width: 0 !important;
|
||||
flex-grow: 1 !important;
|
||||
max-width: none !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Use multiple selectors to increase specificity and override scoped styles */
|
||||
@@ -931,6 +910,9 @@ textarea:focus-visible {
|
||||
.content-container {
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
flex-grow: 1 !important;
|
||||
flex-basis: 100% !important;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
| Command | Function | Difficulty |
|
||||
|---------|----------|------------|
|
||||
| [`/workflow:lite-lite-lite`](./workflow.md#lite-lite-lite) | Ultra-lightweight multi-tool analysis and direct execution | Intermediate |
|
||||
| [`/workflow-lite-planex`](./workflow.md#lite-plan) | Lightweight interactive planning workflow | Intermediate |
|
||||
| [`/workflow:lite-fix`](./workflow.md#lite-fix) | Lightweight bug diagnosis and fix | Intermediate |
|
||||
| [`/workflow-plan`](./workflow.md#plan) | 5-phase planning workflow | Intermediate |
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
| Command | Function | Syntax |
|
||||
|---------|----------|--------|
|
||||
| [`lite-lite-lite`](#lite-lite-lite) | Ultra-lightweight multi-tool analysis and direct execution | `/workflow:lite-lite-lite [-y] <task>` |
|
||||
| [`lite-plan`](#lite-plan) | Lightweight interactive planning workflow | `/workflow-lite-planex [-y] [-e] "task"` |
|
||||
| [`lite-fix`](#lite-fix) | Lightweight bug diagnosis and fix | `/workflow:lite-fix [-y] [--hotfix] "bug description"` |
|
||||
|
||||
@@ -65,6 +66,29 @@
|
||||
|
||||
## Command Details
|
||||
|
||||
### lite-lite-lite
|
||||
|
||||
**Function**: Ultra-lightweight multi-tool analysis and direct execution. Simple tasks have no artifacts, complex tasks automatically create planning documents in `.workflow/.scratchpad/`.
|
||||
|
||||
**Syntax**:
|
||||
```bash
|
||||
/workflow:lite-lite-lite [-y|--yes] <task description>
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Ultra-simple quick tasks
|
||||
- Code modifications not needing planning documents
|
||||
- Automatic tool selection
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Ultra-simple task
|
||||
/workflow:lite-lite-lite "fix header styles"
|
||||
|
||||
# Auto mode
|
||||
/workflow:lite-lite-lite -y "update README links"
|
||||
```
|
||||
|
||||
### lite-plan
|
||||
|
||||
**Function**: Lightweight interactive planning and execution workflow (Phase 1: plan, Phase 2: execute), supporting in-memory planning, code exploration, and automatic execution after confirmation.
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
| 命令 | 功能 | 难度 |
|
||||
| --- | --- | --- |
|
||||
| [`/workflow:lite-lite-lite`](./workflow.md#lite-lite-lite) | 超轻量级多工具分析和直接执行 | Intermediate |
|
||||
| [`/workflow-lite-planex`](./workflow.md#lite-plan) | 轻量级交互式规划工作流 | Intermediate |
|
||||
| [`/workflow:lite-fix`](./workflow.md#lite-fix) | 轻量级 Bug 诊断和修复 | Intermediate |
|
||||
| [`/workflow-plan`](./workflow.md#plan) | 5 阶段规划工作流 | Intermediate |
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
| 命令 | 功能 | 语法 |
|
||||
| --- | --- | --- |
|
||||
| [`lite-lite-lite`](#lite-lite-lite) | 超轻量级多工具分析和直接执行 | `/workflow:lite-lite-lite [-y] <任务>` |
|
||||
| [`lite-plan`](#lite-plan) | 轻量级交互式规划工作流 | `/workflow-lite-planex [-y] [-e] "任务"` |
|
||||
| [`lite-fix`](#lite-fix) | 轻量级 Bug 诊断和修复 | `/workflow:lite-fix [-y] [--hotfix] "Bug 描述"` |
|
||||
|
||||
@@ -65,6 +66,29 @@
|
||||
|
||||
## 命令详解
|
||||
|
||||
### lite-lite-lite
|
||||
|
||||
**功能**: 超轻量级多工具分析和直接执行。简单任务无工件,复杂任务自动在 `.workflow/.scratchpad/` 创建规划文档。
|
||||
|
||||
**语法**:
|
||||
```bash
|
||||
/workflow:lite-lite-lite [-y|--yes] <任务描述>
|
||||
```
|
||||
|
||||
**使用场景**:
|
||||
- 超简单快速任务
|
||||
- 不需要规划文档的代码修改
|
||||
- 自动工具选择
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
# 超简单任务
|
||||
/workflow:lite-lite-lite "修复 header 样式"
|
||||
|
||||
# 自动模式
|
||||
/workflow:lite-lite-lite -y "更新 README 链接"
|
||||
```
|
||||
|
||||
### lite-plan
|
||||
|
||||
**功能**: 轻量级交互式规划和执行工作流(Phase 1: 规划,Phase 2: 执行),支持内存规划、代码探索和确认后自动执行。
|
||||
|
||||
Reference in New Issue
Block a user