@omnidev-ai/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Hook configuration merging
3
+ *
4
+ * Merges hooks from multiple capabilities into a single configuration.
5
+ * Used when generating provider settings from all active capabilities.
6
+ */
7
+
8
+ import { HOOK_EVENTS } from "./constants.js";
9
+ import type { HooksConfig, HookMatcher, CapabilityHooks, HookEvent } from "./types.js";
10
+
11
+ /**
12
+ * Merge hooks from multiple capabilities into a single HooksConfig.
13
+ *
14
+ * Strategy:
15
+ * - All matchers from all capabilities are collected
16
+ * - Hooks are not deduplicated to preserve execution order and capability-specific behavior
17
+ * - Description field is omitted in merged config (it's per-capability metadata)
18
+ */
19
+ export function mergeHooksConfigs(capabilityHooks: CapabilityHooks[]): HooksConfig {
20
+ const result: HooksConfig = {};
21
+
22
+ for (const event of HOOK_EVENTS) {
23
+ const allMatchers: HookMatcher[] = [];
24
+
25
+ for (const capHooks of capabilityHooks) {
26
+ const matchers = capHooks.config[event];
27
+ if (matchers && matchers.length > 0) {
28
+ allMatchers.push(...matchers);
29
+ }
30
+ }
31
+
32
+ if (allMatchers.length > 0) {
33
+ result[event] = allMatchers;
34
+ }
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ /**
41
+ * Options for deduplication
42
+ */
43
+ export interface DeduplicateOptions {
44
+ /** Deduplicate identical commands across matchers (default: false) */
45
+ deduplicateCommands?: boolean;
46
+ }
47
+
48
+ /**
49
+ * Merge and deduplicate hooks from multiple capability hooks.
50
+ *
51
+ * When deduplicateCommands is true:
52
+ * - Commands that are exactly identical are kept only once per event
53
+ * - The first occurrence is kept, subsequent duplicates removed
54
+ *
55
+ * This is useful when multiple capabilities might register the same hook
56
+ * (e.g., a common formatting hook).
57
+ */
58
+ export function mergeAndDeduplicateHooks(
59
+ capabilityHooks: CapabilityHooks[],
60
+ options?: DeduplicateOptions,
61
+ ): HooksConfig {
62
+ const merged = mergeHooksConfigs(capabilityHooks);
63
+
64
+ if (!options?.deduplicateCommands) {
65
+ return merged;
66
+ }
67
+
68
+ // Deduplicate commands within each event
69
+ const result: HooksConfig = {};
70
+
71
+ for (const event of HOOK_EVENTS) {
72
+ const matchers = merged[event];
73
+ if (!matchers || matchers.length === 0) {
74
+ continue;
75
+ }
76
+
77
+ const seenCommands = new Set<string>();
78
+ const deduplicatedMatchers: HookMatcher[] = [];
79
+
80
+ for (const matcher of matchers) {
81
+ const deduplicatedHooks = matcher.hooks.filter((hook) => {
82
+ if (hook.type !== "command") {
83
+ // Prompt hooks are not deduplicated
84
+ return true;
85
+ }
86
+
87
+ const key = hook.command;
88
+ if (seenCommands.has(key)) {
89
+ return false;
90
+ }
91
+ seenCommands.add(key);
92
+ return true;
93
+ });
94
+
95
+ // Only include matcher if it has hooks remaining
96
+ if (deduplicatedHooks.length > 0) {
97
+ deduplicatedMatchers.push({
98
+ ...matcher,
99
+ hooks: deduplicatedHooks,
100
+ });
101
+ }
102
+ }
103
+
104
+ if (deduplicatedMatchers.length > 0) {
105
+ result[event] = deduplicatedMatchers;
106
+ }
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Check if a merged config has any hooks defined
114
+ */
115
+ export function hasAnyHooks(config: HooksConfig): boolean {
116
+ for (const event of HOOK_EVENTS) {
117
+ const matchers = config[event];
118
+ if (matchers && matchers.length > 0) {
119
+ return true;
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ /**
126
+ * Count total number of hook definitions across all events
127
+ */
128
+ export function countHooks(config: HooksConfig): number {
129
+ let count = 0;
130
+
131
+ for (const event of HOOK_EVENTS) {
132
+ const matchers = config[event];
133
+ if (matchers) {
134
+ for (const matcher of matchers) {
135
+ count += matcher.hooks.length;
136
+ }
137
+ }
138
+ }
139
+
140
+ return count;
141
+ }
142
+
143
+ /**
144
+ * Get all events that have hooks defined
145
+ */
146
+ export function getEventsWithHooks(config: HooksConfig): HookEvent[] {
147
+ const events: HookEvent[] = [];
148
+
149
+ for (const event of HOOK_EVENTS) {
150
+ const matchers = config[event];
151
+ if (matchers && matchers.length > 0) {
152
+ events.push(event);
153
+ }
154
+ }
155
+
156
+ return events;
157
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Hook system types
3
+ *
4
+ * Provides TypeScript types for the hooks configuration system,
5
+ * including validation result types and type guards.
6
+ */
7
+
8
+ import {
9
+ HOOK_EVENTS,
10
+ HOOK_TYPES,
11
+ MATCHER_EVENTS,
12
+ PROMPT_HOOK_EVENTS,
13
+ type NOTIFICATION_MATCHERS,
14
+ type SESSION_START_MATCHERS,
15
+ type PRE_COMPACT_MATCHERS,
16
+ type VARIABLE_MAPPINGS,
17
+ } from "./constants.js";
18
+
19
+ // ============ Core Type Derivations ============
20
+
21
+ /** All supported hook event names */
22
+ export type HookEvent = (typeof HOOK_EVENTS)[number];
23
+
24
+ /** Hook execution types: command or prompt */
25
+ export type HookType = (typeof HOOK_TYPES)[number];
26
+
27
+ /** Events that support matcher patterns */
28
+ export type MatcherEvent = (typeof MATCHER_EVENTS)[number];
29
+
30
+ /** Events that support prompt-type hooks */
31
+ export type PromptHookEvent = (typeof PROMPT_HOOK_EVENTS)[number];
32
+
33
+ // Matcher types for specific events
34
+ export type NotificationMatcher = (typeof NOTIFICATION_MATCHERS)[number];
35
+ export type SessionStartMatcher = (typeof SESSION_START_MATCHERS)[number];
36
+ export type PreCompactMatcher = (typeof PRE_COMPACT_MATCHERS)[number];
37
+
38
+ // Variable types
39
+ export type OmnidevVariable = keyof typeof VARIABLE_MAPPINGS;
40
+ export type ClaudeVariable = (typeof VARIABLE_MAPPINGS)[OmnidevVariable];
41
+
42
+ // ============ Hook Definitions ============
43
+
44
+ /** Command-type hook that executes a shell command */
45
+ export interface HookCommand {
46
+ type: "command";
47
+ /** Shell command to execute */
48
+ command: string;
49
+ /** Timeout in seconds (default: 60) */
50
+ timeout?: number;
51
+ }
52
+
53
+ /** Prompt-type hook that uses LLM evaluation */
54
+ export interface HookPrompt {
55
+ type: "prompt";
56
+ /** Prompt text to send to LLM (use $ARGUMENTS for input) */
57
+ prompt: string;
58
+ /** Timeout in seconds (default: 30) */
59
+ timeout?: number;
60
+ }
61
+
62
+ /** Union of all hook types */
63
+ export type Hook = HookCommand | HookPrompt;
64
+
65
+ /** Hook matcher entry - groups hooks by matcher pattern */
66
+ export interface HookMatcher {
67
+ /**
68
+ * Regex pattern to match tool/event names
69
+ * - Use "*" or "" to match all
70
+ * - Supports regex: "Edit|Write", "Bash.*"
71
+ */
72
+ matcher?: string;
73
+ /** Array of hooks to execute when pattern matches */
74
+ hooks: Hook[];
75
+ }
76
+
77
+ // ============ Configuration Structure ============
78
+
79
+ /** Full hooks configuration from hooks.toml */
80
+ export interface HooksConfig {
81
+ /** Optional description of the hooks */
82
+ description?: string;
83
+
84
+ // Events with matchers
85
+ PreToolUse?: HookMatcher[];
86
+ PostToolUse?: HookMatcher[];
87
+ PermissionRequest?: HookMatcher[];
88
+ Notification?: HookMatcher[];
89
+ SessionStart?: HookMatcher[];
90
+ PreCompact?: HookMatcher[];
91
+
92
+ // Events without matchers (matcher field ignored if present)
93
+ UserPromptSubmit?: HookMatcher[];
94
+ Stop?: HookMatcher[];
95
+ SubagentStop?: HookMatcher[];
96
+ SessionEnd?: HookMatcher[];
97
+ }
98
+
99
+ // ============ Validation Types ============
100
+
101
+ export type ValidationSeverity = "error" | "warning";
102
+
103
+ /** Validation error codes for consistent reporting */
104
+ export type HookValidationCode =
105
+ | "HOOKS_INVALID_TOML"
106
+ | "HOOKS_UNKNOWN_EVENT"
107
+ | "HOOKS_INVALID_TYPE"
108
+ | "HOOKS_PROMPT_NOT_ALLOWED"
109
+ | "HOOKS_MISSING_COMMAND"
110
+ | "HOOKS_MISSING_PROMPT"
111
+ | "HOOKS_INVALID_TIMEOUT"
112
+ | "HOOKS_INVALID_MATCHER"
113
+ | "HOOKS_SCRIPT_NOT_FOUND"
114
+ | "HOOKS_SCRIPT_NOT_EXECUTABLE"
115
+ | "HOOKS_CLAUDE_VARIABLE"
116
+ | "HOOKS_EMPTY_ARRAY"
117
+ | "HOOKS_DUPLICATE_COMMAND"
118
+ | "HOOKS_INVALID_HOOKS_ARRAY";
119
+
120
+ export interface HookValidationIssue {
121
+ severity: ValidationSeverity;
122
+ /** Validation error code */
123
+ code: HookValidationCode;
124
+ /** Which event the issue is in */
125
+ event?: HookEvent;
126
+ /** Which matcher index (0-based) */
127
+ matcherIndex?: number;
128
+ /** Which hook index within the matcher (0-based) */
129
+ hookIndex?: number;
130
+ /** Human-readable error message */
131
+ message: string;
132
+ /** File path if applicable (for script validation) */
133
+ path?: string;
134
+ /** Suggestion for fixing the issue */
135
+ suggestion?: string;
136
+ }
137
+
138
+ export interface HookValidationResult {
139
+ valid: boolean;
140
+ errors: HookValidationIssue[];
141
+ warnings: HookValidationIssue[];
142
+ }
143
+
144
+ // ============ Capability Integration ============
145
+
146
+ // TODO: Add programmatic export support for hooks in CapabilityExport interface
147
+ // This would allow capabilities to define hooks in index.ts alongside other exports
148
+
149
+ /** Hooks metadata attached to a capability */
150
+ export interface CapabilityHooks {
151
+ /** Source capability name */
152
+ capabilityName: string;
153
+ /** Source capability path */
154
+ capabilityPath: string;
155
+ /** The hooks configuration */
156
+ config: HooksConfig;
157
+ /** Validation result */
158
+ validation: HookValidationResult;
159
+ }
160
+
161
+ // ============ Doctor Types ============
162
+
163
+ export type DoctorCheckStatus = "pass" | "fail" | "warn";
164
+
165
+ export interface HooksDoctorCheck {
166
+ name: string;
167
+ status: DoctorCheckStatus;
168
+ message: string;
169
+ details?: string[];
170
+ }
171
+
172
+ export interface HooksDoctorResult {
173
+ checks: HooksDoctorCheck[];
174
+ summary: {
175
+ total: number;
176
+ passed: number;
177
+ failed: number;
178
+ warnings: number;
179
+ };
180
+ }
181
+
182
+ // ============ Type Guards ============
183
+
184
+ /** Check if a hook is a command hook */
185
+ export function isHookCommand(hook: Hook): hook is HookCommand {
186
+ return hook.type === "command";
187
+ }
188
+
189
+ /** Check if a hook is a prompt hook */
190
+ export function isHookPrompt(hook: Hook): hook is HookPrompt {
191
+ return hook.type === "prompt";
192
+ }
193
+
194
+ /** Check if an event supports matchers */
195
+ export function isMatcherEvent(event: string): event is MatcherEvent {
196
+ return (MATCHER_EVENTS as readonly string[]).includes(event);
197
+ }
198
+
199
+ /** Check if an event supports prompt-type hooks */
200
+ export function isPromptHookEvent(event: string): event is PromptHookEvent {
201
+ return (PROMPT_HOOK_EVENTS as readonly string[]).includes(event);
202
+ }
203
+
204
+ /** Check if a string is a valid hook event */
205
+ export function isHookEvent(event: string): event is HookEvent {
206
+ return (HOOK_EVENTS as readonly string[]).includes(event);
207
+ }
208
+
209
+ /** Check if a string is a valid hook type */
210
+ export function isHookType(type: string): type is HookType {
211
+ return (HOOK_TYPES as readonly string[]).includes(type);
212
+ }