@omnidev-ai/core 0.8.0 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnidev-ai/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { validateEnv } from "../config/env";
5
5
  import { parseCapabilityConfig } from "../config/parser";
6
+ import { loadCapabilityHooks } from "../hooks/loader.js";
6
7
  import type {
7
8
  CapabilityConfig,
8
9
  Command,
@@ -405,6 +406,9 @@ export async function loadCapability(
405
406
  ? (exportsAny.gitignore as string[])
406
407
  : undefined;
407
408
 
409
+ // Load hooks from hooks/hooks.toml if present
410
+ const hooks = loadCapabilityHooks(id, capabilityPath);
411
+
408
412
  // Build result object with explicit handling for optional typeDefinitions
409
413
  const result: LoadedCapability = {
410
414
  id,
@@ -428,5 +432,10 @@ export async function loadCapability(
428
432
  result.gitignore = gitignore;
429
433
  }
430
434
 
435
+ // Only add hooks if found
436
+ if (hooks !== null) {
437
+ result.hooks = hooks;
438
+ }
439
+
431
440
  return result;
432
441
  }
@@ -1,6 +1,7 @@
1
1
  import { getEnabledCapabilities } from "../config/capabilities";
2
2
  import { loadEnvironment } from "../config/env";
3
- import type { Doc, LoadedCapability, Rule, Skill } from "../types";
3
+ import { mergeHooksConfigs } from "../hooks/merger.js";
4
+ import type { HooksConfig, CapabilityHooks, Doc, LoadedCapability, Rule, Skill } from "../types";
4
5
  import { discoverCapabilities, loadCapability } from "./loader";
5
6
 
6
7
  /**
@@ -13,6 +14,10 @@ export interface CapabilityRegistry {
13
14
  getAllSkills(): Skill[];
14
15
  getAllRules(): Rule[];
15
16
  getAllDocs(): Doc[];
17
+ /** Get all capability hooks metadata */
18
+ getAllCapabilityHooks(): CapabilityHooks[];
19
+ /** Get merged hooks from all capabilities */
20
+ getMergedHooks(): HooksConfig;
16
21
  }
17
22
 
18
23
  /**
@@ -44,6 +49,17 @@ export async function buildCapabilityRegistry(): Promise<CapabilityRegistry> {
44
49
  }
45
50
  }
46
51
 
52
+ // Helper to get all capability hooks
53
+ const getAllCapabilityHooks = (): CapabilityHooks[] => {
54
+ const hooks: CapabilityHooks[] = [];
55
+ for (const cap of capabilities.values()) {
56
+ if (cap.hooks) {
57
+ hooks.push(cap.hooks);
58
+ }
59
+ }
60
+ return hooks;
61
+ };
62
+
47
63
  return {
48
64
  capabilities,
49
65
  getCapability: (id: string) => capabilities.get(id),
@@ -51,5 +67,7 @@ export async function buildCapabilityRegistry(): Promise<CapabilityRegistry> {
51
67
  getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
52
68
  getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
53
69
  getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
70
+ getAllCapabilityHooks,
71
+ getMergedHooks: () => mergeHooksConfigs(getAllCapabilityHooks()),
54
72
  };
55
73
  }
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { existsSync } from "node:fs";
12
12
  import { spawn } from "node:child_process";
13
- import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
13
+ import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
14
14
  import { join } from "node:path";
15
15
  import { parse as parseToml } from "smol-toml";
16
16
  import type {
@@ -500,6 +500,38 @@ export interface DiscoveredContent {
500
500
  docsDir: string | null;
501
501
  }
502
502
 
503
+ /**
504
+ * Rename singular folder names to plural for consistency
505
+ * skill -> skills, command -> commands, rule -> rules, agent -> agents
506
+ */
507
+ export async function normalizeFolderNames(repoPath: string): Promise<void> {
508
+ const renameMappings = [
509
+ { from: "skill", to: "skills" },
510
+ { from: "command", to: "commands" },
511
+ { from: "rule", to: "rules" },
512
+ { from: "agent", to: "agents" },
513
+ { from: "subagent", to: "subagents" },
514
+ ];
515
+
516
+ for (const { from, to } of renameMappings) {
517
+ const fromPath = join(repoPath, from);
518
+ const toPath = join(repoPath, to);
519
+
520
+ // Only rename if singular exists and plural doesn't
521
+ if (existsSync(fromPath) && !existsSync(toPath)) {
522
+ try {
523
+ const stats = await stat(fromPath);
524
+ if (stats.isDirectory()) {
525
+ await rename(fromPath, toPath);
526
+ }
527
+ } catch (error) {
528
+ // Ignore rename errors (might be permissions, etc.)
529
+ console.warn(`Failed to rename ${from} to ${to}:`, error);
530
+ }
531
+ }
532
+ }
533
+ }
534
+
503
535
  async function discoverContent(repoPath: string): Promise<DiscoveredContent> {
504
536
  const result: DiscoveredContent = {
505
537
  skills: [],
@@ -694,6 +726,9 @@ async function fetchGitCapabilitySource(
694
726
  }
695
727
 
696
728
  if (needsWrap) {
729
+ // Normalize folder names (singular -> plural)
730
+ await normalizeFolderNames(repoPath);
731
+
697
732
  // Discover content and generate capability.toml
698
733
  const content = await discoverContent(repoPath);
699
734
  await generateCapabilityToml(id, repoPath, config.source, commit, content);
@@ -1,4 +1,4 @@
1
- import { loadBaseConfig, loadConfig, writeConfig } from "./loader.js";
1
+ import { loadBaseConfig, loadConfig, writeConfig } from "./config.js";
2
2
  import { getActiveProfile, resolveEnabledCapabilities } from "./profiles.js";
3
3
 
4
4
  /**
@@ -173,6 +173,31 @@ function generateConfigToml(config: OmniConfig): string {
173
173
  }
174
174
  lines.push("");
175
175
 
176
+ // Capability groups
177
+ lines.push("# =============================================================================");
178
+ lines.push("# Capability Groups");
179
+ lines.push("# =============================================================================");
180
+ lines.push("# Bundle multiple capabilities under a single name for cleaner profiles.");
181
+ lines.push('# Reference groups in profiles with the "group:" prefix.');
182
+ lines.push("#");
183
+
184
+ const groups = config.capabilities?.groups;
185
+ if (groups && Object.keys(groups).length > 0) {
186
+ lines.push("[capabilities.groups]");
187
+ for (const [name, caps] of Object.entries(groups)) {
188
+ const capsStr = caps.map((c) => `"${c}"`).join(", ");
189
+ lines.push(`${name} = [${capsStr}]`);
190
+ }
191
+ } else {
192
+ lines.push("# [capabilities.groups]");
193
+ lines.push('# expo = ["expo-app-design", "expo-deployment", "upgrading-expo"]');
194
+ lines.push('# backend = ["cloudflare", "database-tools"]');
195
+ lines.push("#");
196
+ lines.push("# [profiles.mobile]");
197
+ lines.push('# capabilities = ["group:expo", "react-native-tools"]');
198
+ }
199
+ lines.push("");
200
+
176
201
  // MCP servers
177
202
  lines.push("# =============================================================================");
178
203
  lines.push("# MCP Servers");
@@ -1,6 +1,7 @@
1
1
  export * from "./capabilities";
2
2
  export * from "./env";
3
- export * from "./loader";
3
+ export * from "./config";
4
4
  export * from "./parser";
5
5
  export * from "./profiles";
6
6
  export * from "./provider";
7
+ export * from "./toml-patcher";
@@ -1,6 +1,6 @@
1
1
  import { readActiveProfileState, writeActiveProfileState } from "../state/active-profile.js";
2
2
  import type { OmniConfig, ProfileConfig } from "../types/index.js";
3
- import { loadConfig, writeConfig } from "./loader.js";
3
+ import { loadConfig, writeConfig } from "./config.js";
4
4
 
5
5
  /**
6
6
  * Gets the name of the currently active profile.
@@ -45,9 +45,29 @@ export function resolveEnabledCapabilities(
45
45
 
46
46
  const profileCapabilities = profile?.capabilities ?? [];
47
47
  const alwaysEnabled = config.always_enabled_capabilities ?? [];
48
+ const groups = config.capabilities?.groups ?? {};
48
49
 
49
- // Merge always-enabled capabilities with profile capabilities (no duplicates)
50
- return [...new Set([...alwaysEnabled, ...profileCapabilities])];
50
+ // Expand group references (group:name -> constituent capabilities)
51
+ const expandCapabilities = (caps: string[]): string[] => {
52
+ return caps.flatMap((cap) => {
53
+ if (cap.startsWith("group:")) {
54
+ const groupName = cap.slice(6);
55
+ const groupCaps = groups[groupName];
56
+ if (!groupCaps) {
57
+ console.warn(`Unknown capability group: ${groupName}`);
58
+ return [];
59
+ }
60
+ return groupCaps;
61
+ }
62
+ return cap;
63
+ });
64
+ };
65
+
66
+ const expandedAlways = expandCapabilities(alwaysEnabled);
67
+ const expandedProfile = expandCapabilities(profileCapabilities);
68
+
69
+ // Merge always-enabled capabilities with profile capabilities (deduplicated)
70
+ return [...new Set([...expandedAlways, ...expandedProfile])];
51
71
  }
52
72
 
53
73
  /**
@@ -0,0 +1,309 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import type { CapabilitySourceConfig, McpConfig } from "../types/index.js";
4
+
5
+ const CONFIG_PATH = "omni.toml";
6
+
7
+ /**
8
+ * Read the raw TOML file content
9
+ */
10
+ async function readConfigFile(): Promise<string> {
11
+ if (!existsSync(CONFIG_PATH)) {
12
+ return "";
13
+ }
14
+ return readFile(CONFIG_PATH, "utf-8");
15
+ }
16
+
17
+ /**
18
+ * Write content to the config file
19
+ */
20
+ async function writeConfigFile(content: string): Promise<void> {
21
+ await writeFile(CONFIG_PATH, content, "utf-8");
22
+ }
23
+
24
+ /**
25
+ * Find the line index where a TOML section starts
26
+ * @returns The line index or -1 if not found
27
+ */
28
+ function findSection(lines: string[], sectionPattern: RegExp): number {
29
+ return lines.findIndex((line) => sectionPattern.test(line.trim()));
30
+ }
31
+
32
+ /**
33
+ * Find the end of a TOML section (next section start or end of file)
34
+ */
35
+ function findSectionEnd(lines: string[], startIndex: number): number {
36
+ // Look for the next section header after the start
37
+ for (let i = startIndex + 1; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ if (line === undefined) continue;
40
+ const trimmed = line.trim();
41
+ // Check if this is a new section (but not a subsection of current)
42
+ if (/^\[(?!\[)/.test(trimmed) && !trimmed.startsWith("#")) {
43
+ return i;
44
+ }
45
+ }
46
+ return lines.length;
47
+ }
48
+
49
+ /**
50
+ * Format a capability source for TOML
51
+ */
52
+ function formatCapabilitySource(name: string, source: CapabilitySourceConfig): string {
53
+ if (typeof source === "string") {
54
+ return `${name} = "${source}"`;
55
+ }
56
+ if (source.path) {
57
+ return `${name} = { source = "${source.source}", path = "${source.path}" }`;
58
+ }
59
+ return `${name} = "${source.source}"`;
60
+ }
61
+
62
+ /**
63
+ * Add a capability source to [capabilities.sources]
64
+ */
65
+ export async function patchAddCapabilitySource(
66
+ name: string,
67
+ source: CapabilitySourceConfig,
68
+ ): Promise<void> {
69
+ let content = await readConfigFile();
70
+ const lines = content.split("\n");
71
+
72
+ // Find [capabilities.sources] section
73
+ const sectionIndex = findSection(lines, /^\[capabilities\.sources\]$/);
74
+
75
+ const newEntry = formatCapabilitySource(name, source);
76
+
77
+ if (sectionIndex !== -1) {
78
+ // Section exists, add entry after section header
79
+ const sectionEnd = findSectionEnd(lines, sectionIndex);
80
+ // Insert before the end of section (or before blank lines at end)
81
+ let insertIndex = sectionEnd;
82
+ // Find a good insertion point (after last non-blank, non-comment line in section)
83
+ for (let i = sectionEnd - 1; i > sectionIndex; i--) {
84
+ const line = lines[i];
85
+ if (line === undefined) continue;
86
+ const trimmed = line.trim();
87
+ if (trimmed && !trimmed.startsWith("#")) {
88
+ insertIndex = i + 1;
89
+ break;
90
+ }
91
+ }
92
+ // If we only found the header, insert right after it
93
+ if (insertIndex === sectionEnd && sectionIndex + 1 < lines.length) {
94
+ insertIndex = sectionIndex + 1;
95
+ }
96
+ lines.splice(insertIndex, 0, newEntry);
97
+ } else {
98
+ // Section doesn't exist, need to create it
99
+ // Try to find [capabilities] section first
100
+ const capabilitiesIndex = findSection(lines, /^\[capabilities\]$/);
101
+
102
+ if (capabilitiesIndex !== -1) {
103
+ // Insert after [capabilities] section
104
+ const capEnd = findSectionEnd(lines, capabilitiesIndex);
105
+ lines.splice(capEnd, 0, "", "[capabilities.sources]", newEntry);
106
+ } else {
107
+ // No capabilities section at all, add at end
108
+ // Find a good place - after mcps or at end
109
+ const mcpsIndex = findSection(lines, /^\[mcps/);
110
+ if (mcpsIndex !== -1) {
111
+ // Insert before mcps
112
+ lines.splice(mcpsIndex, 0, "[capabilities.sources]", newEntry, "");
113
+ } else {
114
+ // Just append at end
115
+ lines.push("", "[capabilities.sources]", newEntry);
116
+ }
117
+ }
118
+ }
119
+
120
+ content = lines.join("\n");
121
+ await writeConfigFile(content);
122
+ }
123
+
124
+ /**
125
+ * Format an MCP config as TOML lines
126
+ */
127
+ function formatMcpConfig(name: string, config: McpConfig): string[] {
128
+ const lines: string[] = [];
129
+ lines.push(`[mcps.${name}]`);
130
+
131
+ // Transport (only if not stdio, since stdio is default)
132
+ if (config.transport && config.transport !== "stdio") {
133
+ lines.push(`transport = "${config.transport}"`);
134
+ }
135
+
136
+ // For stdio transport
137
+ if (config.command) {
138
+ lines.push(`command = "${config.command}"`);
139
+ }
140
+ if (config.args && config.args.length > 0) {
141
+ const argsStr = config.args.map((a) => `"${a}"`).join(", ");
142
+ lines.push(`args = [${argsStr}]`);
143
+ }
144
+ if (config.cwd) {
145
+ lines.push(`cwd = "${config.cwd}"`);
146
+ }
147
+
148
+ // For http/sse transport
149
+ if (config.url) {
150
+ lines.push(`url = "${config.url}"`);
151
+ }
152
+
153
+ // Environment variables
154
+ if (config.env && Object.keys(config.env).length > 0) {
155
+ lines.push(`[mcps.${name}.env]`);
156
+ for (const [key, value] of Object.entries(config.env)) {
157
+ lines.push(`${key} = "${value}"`);
158
+ }
159
+ }
160
+
161
+ // Headers
162
+ if (config.headers && Object.keys(config.headers).length > 0) {
163
+ lines.push(`[mcps.${name}.headers]`);
164
+ for (const [key, value] of Object.entries(config.headers)) {
165
+ lines.push(`${key} = "${value}"`);
166
+ }
167
+ }
168
+
169
+ return lines;
170
+ }
171
+
172
+ /**
173
+ * Add an MCP server configuration
174
+ */
175
+ export async function patchAddMcp(name: string, config: McpConfig): Promise<void> {
176
+ let content = await readConfigFile();
177
+ const lines = content.split("\n");
178
+
179
+ const mcpLines = formatMcpConfig(name, config);
180
+
181
+ // Find any existing [mcps.*] section to add near it
182
+ const existingMcpIndex = findSection(lines, /^\[mcps\./);
183
+
184
+ if (existingMcpIndex !== -1) {
185
+ // Find the end of all mcp sections
186
+ let lastMcpEnd = existingMcpIndex;
187
+ for (let i = existingMcpIndex; i < lines.length; i++) {
188
+ const line = lines[i];
189
+ if (line === undefined) continue;
190
+ const trimmed = line.trim();
191
+ if (/^\[mcps\./.test(trimmed)) {
192
+ lastMcpEnd = findSectionEnd(lines, i);
193
+ } else if (/^\[(?!mcps\.)/.test(trimmed) && !trimmed.startsWith("#")) {
194
+ break;
195
+ }
196
+ }
197
+ // Insert after the last mcp section
198
+ lines.splice(lastMcpEnd, 0, "", ...mcpLines);
199
+ } else {
200
+ // No mcp sections exist, add after profiles or at end
201
+ const profilesIndex = findSection(lines, /^\[profiles\./);
202
+ if (profilesIndex !== -1) {
203
+ // Insert before profiles
204
+ lines.splice(profilesIndex, 0, ...mcpLines, "");
205
+ } else {
206
+ // Just append at end
207
+ lines.push("", ...mcpLines);
208
+ }
209
+ }
210
+
211
+ content = lines.join("\n");
212
+ await writeConfigFile(content);
213
+ }
214
+
215
+ /**
216
+ * Add a capability to a profile's capabilities array
217
+ */
218
+ export async function patchAddToProfile(
219
+ profileName: string,
220
+ capabilityName: string,
221
+ ): Promise<void> {
222
+ let content = await readConfigFile();
223
+ const lines = content.split("\n");
224
+
225
+ // Find the profile section
226
+ const profilePattern = new RegExp(`^\\[profiles\\.${escapeRegExp(profileName)}\\]$`);
227
+ const profileIndex = findSection(lines, profilePattern);
228
+
229
+ if (profileIndex !== -1) {
230
+ // Profile exists, find and modify capabilities line
231
+ const profileEnd = findSectionEnd(lines, profileIndex);
232
+
233
+ let capabilitiesLineIndex = -1;
234
+ for (let i = profileIndex + 1; i < profileEnd; i++) {
235
+ const line = lines[i];
236
+ if (line === undefined) continue;
237
+ const trimmed = line.trim();
238
+ if (trimmed.startsWith("capabilities")) {
239
+ capabilitiesLineIndex = i;
240
+ break;
241
+ }
242
+ }
243
+
244
+ if (capabilitiesLineIndex !== -1) {
245
+ // Parse and modify the existing capabilities array
246
+ const line = lines[capabilitiesLineIndex];
247
+ if (line !== undefined) {
248
+ const match = line.match(/capabilities\s*=\s*\[(.*)\]/);
249
+ if (match && match[1] !== undefined) {
250
+ const existingCaps = match[1]
251
+ .split(",")
252
+ .map((s) => s.trim())
253
+ .filter((s) => s.length > 0);
254
+
255
+ // Check if capability already exists
256
+ const quotedCap = `"${capabilityName}"`;
257
+ if (!existingCaps.includes(quotedCap)) {
258
+ existingCaps.push(quotedCap);
259
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
260
+ lines[capabilitiesLineIndex] = `${indent}capabilities = [${existingCaps.join(", ")}]`;
261
+ }
262
+ }
263
+ }
264
+ } else {
265
+ // No capabilities line, add one after profile header
266
+ lines.splice(profileIndex + 1, 0, `capabilities = ["${capabilityName}"]`);
267
+ }
268
+ } else {
269
+ // Profile doesn't exist, create it
270
+ // Find where to insert (after other profiles or at end)
271
+ const anyProfileIndex = findSection(lines, /^\[profiles\./);
272
+
273
+ if (anyProfileIndex !== -1) {
274
+ // Find end of all profiles sections
275
+ let lastProfileEnd = anyProfileIndex;
276
+ for (let i = anyProfileIndex; i < lines.length; i++) {
277
+ const line = lines[i];
278
+ if (line === undefined) continue;
279
+ const trimmed = line.trim();
280
+ if (/^\[profiles\./.test(trimmed)) {
281
+ lastProfileEnd = findSectionEnd(lines, i);
282
+ } else if (/^\[(?!profiles\.)/.test(trimmed) && !trimmed.startsWith("#")) {
283
+ break;
284
+ }
285
+ }
286
+ // Insert after the last profile
287
+ lines.splice(
288
+ lastProfileEnd,
289
+ 0,
290
+ "",
291
+ `[profiles.${profileName}]`,
292
+ `capabilities = ["${capabilityName}"]`,
293
+ );
294
+ } else {
295
+ // No profiles at all, add at end
296
+ lines.push("", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
297
+ }
298
+ }
299
+
300
+ content = lines.join("\n");
301
+ await writeConfigFile(content);
302
+ }
303
+
304
+ /**
305
+ * Escape special regex characters in a string
306
+ */
307
+ function escapeRegExp(str: string): string {
308
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
309
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Hook system constants
3
+ *
4
+ * All constants are defined as readonly arrays/objects to enable
5
+ * TypeScript literal type inference.
6
+ */
7
+
8
+ /** All supported hook events */
9
+ export const HOOK_EVENTS = [
10
+ "PreToolUse",
11
+ "PostToolUse",
12
+ "PermissionRequest",
13
+ "UserPromptSubmit",
14
+ "Stop",
15
+ "SubagentStop",
16
+ "Notification",
17
+ "SessionStart",
18
+ "SessionEnd",
19
+ "PreCompact",
20
+ ] as const;
21
+
22
+ /** Events that support matchers (regex patterns to filter tool names) */
23
+ export const MATCHER_EVENTS = [
24
+ "PreToolUse",
25
+ "PostToolUse",
26
+ "PermissionRequest",
27
+ "Notification",
28
+ "SessionStart",
29
+ "PreCompact",
30
+ ] as const;
31
+
32
+ /** Events that support prompt-type hooks (LLM evaluation) */
33
+ export const PROMPT_HOOK_EVENTS = [
34
+ "Stop",
35
+ "SubagentStop",
36
+ "UserPromptSubmit",
37
+ "PreToolUse",
38
+ "PermissionRequest",
39
+ ] as const;
40
+
41
+ /** Hook execution types */
42
+ export const HOOK_TYPES = ["command", "prompt"] as const;
43
+
44
+ /**
45
+ * Common tool matchers (for validation hints, not exhaustive)
46
+ * These are the tools available in Claude Code that hooks commonly target.
47
+ */
48
+ export const COMMON_TOOL_MATCHERS = [
49
+ "Bash",
50
+ "Read",
51
+ "Write",
52
+ "Edit",
53
+ "Glob",
54
+ "Grep",
55
+ "Task",
56
+ "WebFetch",
57
+ "WebSearch",
58
+ "NotebookEdit",
59
+ "LSP",
60
+ "TodoWrite",
61
+ "AskUserQuestion",
62
+ ] as const;
63
+
64
+ /** Notification type matchers */
65
+ export const NOTIFICATION_MATCHERS = [
66
+ "permission_prompt",
67
+ "idle_prompt",
68
+ "auth_success",
69
+ "elicitation_dialog",
70
+ ] as const;
71
+
72
+ /** SessionStart source matchers */
73
+ export const SESSION_START_MATCHERS = ["startup", "resume", "clear", "compact"] as const;
74
+
75
+ /** PreCompact trigger matchers */
76
+ export const PRE_COMPACT_MATCHERS = ["manual", "auto"] as const;
77
+
78
+ /** Default timeout for command hooks (in seconds) */
79
+ export const DEFAULT_COMMAND_TIMEOUT = 60;
80
+
81
+ /** Default timeout for prompt hooks (in seconds) */
82
+ export const DEFAULT_PROMPT_TIMEOUT = 30;
83
+
84
+ /**
85
+ * Environment variable mappings between OmniDev and Claude Code
86
+ *
87
+ * When capabilities define hooks, they use OMNIDEV_ prefixed variables.
88
+ * When writing to .claude/settings.json, these are transformed to CLAUDE_ variables.
89
+ * When importing external capabilities, CLAUDE_ variables are transformed to OMNIDEV_.
90
+ */
91
+ export const VARIABLE_MAPPINGS = {
92
+ OMNIDEV_CAPABILITY_ROOT: "CLAUDE_PLUGIN_ROOT",
93
+ OMNIDEV_PROJECT_DIR: "CLAUDE_PROJECT_DIR",
94
+ } as const;
95
+
96
+ /** The hooks configuration filename within a capability */
97
+ export const HOOKS_CONFIG_FILENAME = "hooks.toml";
98
+
99
+ /** The hooks directory name within a capability */
100
+ export const HOOKS_DIRECTORY = "hooks";