mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat(hooks): add 7 hook templates with full install pipeline and extended trigger types
Extend HookTriggerType from 5 to 12 official events (Notification, SubagentStart, SubagentStop, PreCompact, SessionEnd, PostToolUseFailure, PermissionRequest). Add templates: stop-notify, auto-format-on-write, auto-lint-on-write, block-sensitive-files, git-auto-stage, post-edit-index, session-end-summary across notification/automation/indexing categories. Fix install pipeline to use correct nested settings.json format with _templateId metadata for precise detection. Redesign templates UI as responsive card grid with per-template icons.
This commit is contained in:
@@ -3420,21 +3420,100 @@ export interface Hook {
|
||||
command?: string;
|
||||
trigger: string;
|
||||
matcher?: string;
|
||||
scope?: 'global' | 'project';
|
||||
index?: number;
|
||||
templateId?: string;
|
||||
}
|
||||
|
||||
export interface HooksResponse {
|
||||
hooks: Hook[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw hook entry as stored in settings.json
|
||||
* Format: { matcher?: string, hooks: [{ type: "command", command: "..." }] }
|
||||
*/
|
||||
interface RawHookEntry {
|
||||
matcher?: string;
|
||||
_templateId?: string;
|
||||
hooks?: Array<{
|
||||
type?: string;
|
||||
command?: string;
|
||||
prompt?: string;
|
||||
timeout?: number;
|
||||
async?: boolean;
|
||||
}>;
|
||||
// Legacy flat format support
|
||||
command?: string;
|
||||
args?: string[];
|
||||
script?: string;
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw hooks config from backend into flat Hook array
|
||||
*/
|
||||
function parseHooksConfig(
|
||||
data: {
|
||||
global?: { path?: string; hooks?: Record<string, RawHookEntry[]> };
|
||||
project?: { path?: string | null; hooks?: Record<string, RawHookEntry[]> };
|
||||
}
|
||||
): Hook[] {
|
||||
const result: Hook[] = [];
|
||||
|
||||
for (const scope of ['project', 'global'] as const) {
|
||||
const scopeData = data[scope];
|
||||
if (!scopeData?.hooks || typeof scopeData.hooks !== 'object') continue;
|
||||
|
||||
for (const [event, entries] of Object.entries(scopeData.hooks)) {
|
||||
if (!Array.isArray(entries)) continue;
|
||||
|
||||
entries.forEach((entry, index) => {
|
||||
// Extract command from nested hooks array (official format)
|
||||
let command = '';
|
||||
if (entry.hooks && Array.isArray(entry.hooks) && entry.hooks.length > 0) {
|
||||
command = entry.hooks.map(h => h.command || h.prompt || '').filter(Boolean).join(' && ');
|
||||
}
|
||||
// Legacy flat format fallback
|
||||
if (!command && entry.command) {
|
||||
command = entry.args
|
||||
? `${entry.command} ${entry.args.join(' ')}`
|
||||
: entry.command;
|
||||
}
|
||||
if (!command && entry.script) {
|
||||
command = entry.script;
|
||||
}
|
||||
|
||||
const name = `${scope}-${event}-${index}`;
|
||||
|
||||
result.push({
|
||||
name,
|
||||
description: entry.description,
|
||||
enabled: entry.enabled !== false,
|
||||
command,
|
||||
trigger: event,
|
||||
matcher: entry.matcher,
|
||||
scope,
|
||||
index,
|
||||
templateId: entry._templateId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all hooks for a specific workspace
|
||||
* @param projectPath - Optional project path to filter data by workspace
|
||||
*/
|
||||
export async function fetchHooks(projectPath?: string): Promise<HooksResponse> {
|
||||
const url = projectPath ? `/api/hooks?path=${encodeURIComponent(projectPath)}` : '/api/hooks';
|
||||
const data = await fetchApi<{ hooks?: Hook[] }>(url);
|
||||
const data = await fetchApi<Record<string, unknown>>(url);
|
||||
return {
|
||||
hooks: data.hooks ?? [],
|
||||
hooks: parseHooksConfig(data as Parameters<typeof parseHooksConfig>[0]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3515,12 +3594,35 @@ export async function deleteHook(hookName: string): Promise<void> {
|
||||
|
||||
/**
|
||||
* Install a hook from predefined template
|
||||
* Converts template data to Claude Code's settings.json format:
|
||||
* { _templateId, matcher?, hooks: [{ type: "command", command: "full command string" }] }
|
||||
*/
|
||||
export async function installHookTemplate(templateId: string): Promise<Hook> {
|
||||
return fetchApi<Hook>('/api/hooks/install-template', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ templateId }),
|
||||
});
|
||||
export async function installHookTemplate(
|
||||
trigger: string,
|
||||
templateData: { id: string; command: string; args?: string[]; matcher?: string }
|
||||
): Promise<{ success: boolean }> {
|
||||
// Build full command string from command + args
|
||||
const fullCommand = templateData.args
|
||||
? `${templateData.command} ${templateData.args.map(a => a.includes(' ') ? `'${a}'` : a).join(' ')}`
|
||||
: templateData.command;
|
||||
|
||||
// Build hookData in Claude Code's official nested format
|
||||
// _templateId is ignored by Claude Code but used for installed detection
|
||||
const hookData: Record<string, unknown> = {
|
||||
_templateId: templateData.id,
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: fullCommand,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (templateData.matcher) {
|
||||
hookData.matcher = templateData.matcher;
|
||||
}
|
||||
|
||||
return saveHook('project', trigger, hookData);
|
||||
}
|
||||
|
||||
// ========== Rules API ==========
|
||||
|
||||
Reference in New Issue
Block a user