// Hook Manager View // Renders the Claude Code hooks management interface async function renderHookManager() { const container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search for Hook view const statsGrid = document.getElementById('statsGrid'); const searchInput = document.getElementById('searchInput'); if (statsGrid) statsGrid.style.display = 'none'; if (searchInput) searchInput.parentElement.style.display = 'none'; // Always reload hook config and available skills to get latest data await Promise.all([ loadHookConfig(), loadAvailableSkills() ]); const globalHooks = hookConfig.global?.hooks || {}; const projectHooks = hookConfig.project?.hooks || {}; // Count hooks const globalHookCount = countHooks(globalHooks); const projectHookCount = countHooks(projectHooks); container.innerHTML = `

${t('hook.projectHooks')}

${t('hook.projectFile')}
${projectHookCount} ${t('hook.hooksConfigured')}
${projectHookCount === 0 ? `

${t('empty.noHooks')}

${t('empty.createHookHint')}

` : `
${renderHooksByEvent(projectHooks, 'project')}
`}

${t('hook.globalHooks')}

${t('hook.globalFile')}
${globalHookCount} ${t('hook.hooksConfigured')}
${globalHookCount === 0 ? `

${t('empty.noGlobalHooks')}

${t('empty.globalHooksHint')}

` : `
${renderHooksByEvent(globalHooks, 'global')}
`}

${t('hook.wizards')}

${t('hook.guidedSetup')}
${t('hook.wizardsDesc')}
${renderWizardCard('memory-update')} ${renderWizardCard('memory-setup')} ${renderWizardCard('skill-context')}

${t('hook.quickInstall')}

${t('hook.oneClick')}
${renderQuickInstallCard('session-context', t('hook.tpl.sessionContext'), t('hook.tpl.sessionContextDesc'), 'UserPromptSubmit', '')} ${renderQuickInstallCard('codexlens-update', t('hook.tpl.codexlensSync'), t('hook.tpl.codexlensSyncDesc'), 'PostToolUse', 'Write|Edit')} ${renderQuickInstallCard('ccw-notify', t('hook.tpl.ccwDashboardNotify'), t('hook.tpl.ccwDashboardNotifyDesc'), 'PostToolUse', 'Write')} ${renderQuickInstallCard('log-tool', t('hook.tpl.toolLogger'), t('hook.tpl.toolLoggerDesc'), 'PostToolUse', 'All')} ${renderQuickInstallCard('lint-check', t('hook.tpl.autoLint'), t('hook.tpl.autoLintDesc'), 'PostToolUse', 'Write')} ${renderQuickInstallCard('git-add', t('hook.tpl.autoGitStage'), t('hook.tpl.autoGitStageDesc'), 'PostToolUse', 'Write')}

${t('hook.envVarsRef')}

$CLAUDE_FILE_PATHS ${t('hook.filePaths')}
$CLAUDE_TOOL_NAME ${t('hook.toolName')}
$CLAUDE_TOOL_INPUT ${t('hook.toolInput')}
$CLAUDE_SESSION_ID ${t('hook.sessionId')}
$CLAUDE_PROJECT_DIR ${t('hook.projectDir')}
$CLAUDE_WORKING_DIR ${t('hook.workingDir')}
`; // Attach event listeners attachHookEventListeners(); // Initialize Lucide icons if (typeof lucide !== 'undefined') lucide.createIcons(); // Load available SKILLs for skill-context wizard loadAvailableSkills(); } // Load available SKILLs for skill-context wizard async function loadAvailableSkills() { try { const response = await fetch(`/api/skills?path=${encodeURIComponent(projectPath)}`); if (!response.ok) throw new Error('Failed to load skills'); const data = await response.json(); const container = document.getElementById('skill-discovery-skill-context'); if (container && data.skills) { if (data.skills.length === 0) { container.innerHTML = ` ${t('hook.wizard.availableSkills')} ${t('hook.wizard.noSkillsFound').split('.')[0]} `; } else { const skillBadges = data.skills.map(skill => ` ${escapeHtml(skill.name)} `).join(''); container.innerHTML = ` ${t('hook.wizard.availableSkills')}
${skillBadges}
`; } } // Store skills for wizard use window.availableSkills = data.skills || []; } catch (err) { console.error('Failed to load skills:', err); const container = document.getElementById('skill-discovery-skill-context'); if (container) { container.innerHTML = ` ${t('hook.wizard.availableSkills')} ${t('toast.loadFailed', { error: err.message })} `; } } } // Call loadAvailableSkills after rendering hook manager const originalRenderHookManager = typeof renderHookManager === 'function' ? renderHookManager : null; function renderWizardCard(wizardId) { const wizard = WIZARD_TEMPLATES[wizardId]; if (!wizard) return ''; // Get translated wizard name and description const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') : wizardId === 'memory-setup' ? t('hook.wizard.memorySetup') : wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name; const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') : wizardId === 'memory-setup' ? t('hook.wizard.memorySetupDesc') : wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description; // Translate options const getOptionName = (wizardId, optId) => { if (wizardId === 'memory-update') { if (optId === 'on-stop') return t('hook.wizard.onSessionEnd'); if (optId === 'periodic') return t('hook.wizard.periodicUpdate'); if (optId === 'count-based') return t('hook.wizard.countBasedUpdate'); } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTracker'); if (optId === 'file-write') return t('hook.wizard.fileWriteTracker'); if (optId === 'prompts') return t('hook.wizard.promptTracker'); } if (wizardId === 'skill-context') { if (optId === 'keyword') return t('hook.wizard.keywordMatching'); if (optId === 'auto') return t('hook.wizard.autoDetection'); } return wizard.options.find(o => o.id === optId)?.name || ''; }; const getOptionDesc = (wizardId, optId) => { if (wizardId === 'memory-update') { if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc'); if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc'); if (optId === 'count-based') return t('hook.wizard.countBasedUpdateDesc'); } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc'); if (optId === 'file-write') return t('hook.wizard.fileWriteTrackerDesc'); if (optId === 'prompts') return t('hook.wizard.promptTrackerDesc'); } if (wizardId === 'skill-context') { if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc'); if (optId === 'auto') return t('hook.wizard.autoDetectionDesc'); } return wizard.options.find(o => o.id === optId)?.description || ''; }; // Determine what to show in the tools/skills section let toolsSection = ''; if (wizard.requiresSkillDiscovery) { toolsSection = `
${t('hook.wizard.event')} UserPromptSubmit
${t('hook.wizard.availableSkills')} ${t('hook.wizard.loading')}
`; } else if (wizard.multiSelect) { // memory-setup: lightweight tracking, no CLI tools toolsSection = ''; } else { toolsSection = `
${t('hook.wizard.cliTools')} gemini qwen codex
`; } return `

${escapeHtml(wizardName)}

${escapeHtml(wizardDesc)}

${wizard.options.map(opt => `
${escapeHtml(getOptionName(wizardId, opt.id))}: ${escapeHtml(getOptionDesc(wizardId, opt.id))}
`).join('')}
${toolsSection}
`; } function countHooks(hooks) { let count = 0; for (const event of Object.keys(hooks)) { const hookList = hooks[event]; count += Array.isArray(hookList) ? hookList.length : 1; } return count; } function renderHooksByEvent(hooks, scope) { const events = Object.keys(hooks); if (events.length === 0) return ''; return events.map(event => { const hookList = Array.isArray(hooks[event]) ? hooks[event] : [hooks[event]]; return hookList.map((hook, index) => { const matcher = hook.matcher || 'All tools'; // Support both old format (hook.command) and new Claude Code format (hook.hooks[0].command) const command = hook.hooks?.[0]?.command || hook.command || 'N/A'; const args = hook.args || []; const timeout = hook.hooks?.[0]?.timeout || hook.timeout; return `
${getHookEventIconLucide(event)}

${event}

${getHookEventDescription(event)}

matcher ${escapeHtml(matcher)}
command ${escapeHtml(command)}
${args.length > 0 ? `
args ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}
` : ''}
`; }).join(''); }).join(''); } function renderQuickInstallCard(templateId, title, description, event, matcher) { const isInstalled = isHookTemplateInstalled(templateId); const template = HOOK_TEMPLATES[templateId]; const category = template?.category || 'general'; const categoryTranslated = t(`hook.category.${category}`) || category; return `
${isInstalled ? '' : ''}

${escapeHtml(title)}

${escapeHtml(description)}

${event} ${t('hook.wizard.matches')} ${matcher} ${categoryTranslated}
${isInstalled ? ` ` : ` `}
`; } function isHookTemplateInstalled(templateId) { const template = HOOK_TEMPLATES[templateId]; if (!template) return false; // Define unique patterns for each template type (more specific than just command) const uniquePatterns = { 'session-context': 'hook session-context', 'codexlens-update': 'codexlens update', 'ccw-notify': 'api/hook', 'log-tool': 'tool-usage.log', 'lint-check': 'eslint', 'git-add': 'git add', 'memory-file-read': 'memory track --type file --action read', 'memory-file-write': 'memory track --type file --action write', 'memory-prompt-track': 'memory track --type topic', 'skill-context-auto': 'skill-context-auto' }; // Use unique pattern if defined, otherwise fall back to command + args const searchPattern = uniquePatterns[templateId] || (template.command + (template.args ? ' ' + template.args.join(' ') : '')); // Check project hooks const projectHooks = hookConfig.project?.hooks?.[template.event]; if (projectHooks) { const hookList = Array.isArray(projectHooks) ? projectHooks : [projectHooks]; if (hookList.some(h => { // Check both old format (h.command) and new format (h.hooks[0].command) const cmd = h.hooks?.[0]?.command || h.command || ''; return cmd.includes(searchPattern); })) return true; } // Check global hooks const globalHooks = hookConfig.global?.hooks?.[template.event]; if (globalHooks) { const hookList = Array.isArray(globalHooks) ? globalHooks : [globalHooks]; if (hookList.some(h => { const cmd = h.hooks?.[0]?.command || h.command || ''; return cmd.includes(searchPattern); })) return true; } return false; } async function installHookTemplate(templateId, scope) { const template = HOOK_TEMPLATES[templateId]; if (!template) { showRefreshToast('Template not found', 'error'); return; } // Check if already installed if (isHookTemplateInstalled(templateId)) { showRefreshToast('Hook already installed', 'info'); return; } const hookData = { command: template.command, args: template.args }; if (template.matcher) { hookData.matcher = template.matcher; } await saveHook(scope, template.event, hookData); } async function uninstallHookTemplate(templateId) { const template = HOOK_TEMPLATES[templateId]; if (!template) return; // Extract unique identifier from template args for matching // Template args format: ['-c', 'actual command...'] const templateArgs = template.args || []; const templateFullCmd = templateArgs.length > 0 ? templateArgs.join(' ') : ''; // Define unique patterns for each template type const uniquePatterns = { 'session-context': 'hook session-context', 'codexlens-update': 'codexlens update', 'ccw-notify': 'api/hook', 'log-tool': 'tool-usage.log', 'lint-check': 'eslint', 'git-add': 'git add', 'memory-file-read': 'memory track', 'memory-file-write': 'memory track', 'memory-prompt-track': 'memory track' }; const uniquePattern = uniquePatterns[templateId] || template.command; // Helper to check if a hook matches the template const matchesTemplate = (h) => { const hookCmd = h.hooks?.[0]?.command || h.command || ''; return hookCmd.includes(uniquePattern); }; // Find and remove from project hooks const projectHooks = hookConfig.project?.hooks?.[template.event]; if (projectHooks) { const hookList = Array.isArray(projectHooks) ? projectHooks : [projectHooks]; const index = hookList.findIndex(matchesTemplate); if (index !== -1) { await removeHook('project', template.event, index); return; } } // Find and remove from global hooks const globalHooks = hookConfig.global?.hooks?.[template.event]; if (globalHooks) { const hookList = Array.isArray(globalHooks) ? globalHooks : [globalHooks]; const index = hookList.findIndex(matchesTemplate); if (index !== -1) { await removeHook('global', template.event, index); return; } } showRefreshToast('Hook not found', 'error'); } function attachHookEventListeners() { // Edit buttons document.querySelectorAll('.hook-card button[data-action="edit"]').forEach(btn => { btn.addEventListener('click', (e) => { const button = e.currentTarget; const scope = button.dataset.scope; const event = button.dataset.event; const index = parseInt(button.dataset.index); const hooks = scope === 'global' ? hookConfig.global.hooks : hookConfig.project.hooks; const hookList = Array.isArray(hooks[event]) ? hooks[event] : [hooks[event]]; const hook = hookList[index]; if (hook) { // Support both Claude Code format (hooks[0].command) and legacy format (command + args) let command = ''; let args = []; if (hook.hooks && hook.hooks[0]) { // Claude Code format: { hooks: [{ type: "command", command: "bash -c '...'" }] } const fullCommand = hook.hooks[0].command || ''; // Try to split command and args for bash -c commands const bashMatch = fullCommand.match(/^(bash|sh|cmd)\s+(-c)\s+(.+)$/s); if (bashMatch) { command = bashMatch[1]; args = [bashMatch[2], bashMatch[3]]; } else { // For other commands, put the whole thing as command command = fullCommand; args = []; } } else { // Legacy format: { command: "bash", args: ["-c", "..."] } command = hook.command || ''; args = hook.args || []; } openHookCreateModal({ scope: scope, event: event, index: index, matcher: hook.matcher || '', command: command, args: args }); } }); }); // Delete buttons document.querySelectorAll('.hook-card button[data-action="delete"]').forEach(btn => { btn.addEventListener('click', async (e) => { const button = e.currentTarget; const scope = button.dataset.scope; const event = button.dataset.event; const index = parseInt(button.dataset.index); if (confirm(t('hook.deleteConfirm', { event: event }))) { await removeHook(scope, event, index); } }); }); // Install project buttons document.querySelectorAll('button[data-action="install-project"]').forEach(btn => { btn.addEventListener('click', async (e) => { const templateId = e.currentTarget.dataset.template; await installHookTemplate(templateId, 'project'); }); }); // Install global buttons document.querySelectorAll('button[data-action="install-global"]').forEach(btn => { btn.addEventListener('click', async (e) => { const templateId = e.currentTarget.dataset.template; await installHookTemplate(templateId, 'global'); }); }); // Uninstall buttons document.querySelectorAll('button[data-action="uninstall"]').forEach(btn => { btn.addEventListener('click', async (e) => { const templateId = e.currentTarget.dataset.template; await uninstallHookTemplate(templateId); }); }); }