@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/dist/index.js CHANGED
@@ -118,9 +118,9 @@ async function loadDocs(capabilityPath, capabilityId) {
118
118
  return docs;
119
119
  }
120
120
  // src/capability/loader.ts
121
- import { existsSync as existsSync7, readdirSync as readdirSync6 } from "node:fs";
121
+ import { existsSync as existsSync9, readdirSync as readdirSync6 } from "node:fs";
122
122
  import { readFile as readFile7 } from "node:fs/promises";
123
- import { join as join6 } from "node:path";
123
+ import { join as join7 } from "node:path";
124
124
 
125
125
  // src/config/env.ts
126
126
  import { existsSync as existsSync3 } from "node:fs";
@@ -207,20 +207,666 @@ function parseCapabilityConfig(tomlContent) {
207
207
  }
208
208
  }
209
209
 
210
+ // src/hooks/loader.ts
211
+ import { existsSync as existsSync5, readFileSync } from "node:fs";
212
+ import { join as join3 } from "node:path";
213
+ import { parse as parseToml } from "smol-toml";
214
+
215
+ // src/hooks/constants.ts
216
+ var HOOK_EVENTS = [
217
+ "PreToolUse",
218
+ "PostToolUse",
219
+ "PermissionRequest",
220
+ "UserPromptSubmit",
221
+ "Stop",
222
+ "SubagentStop",
223
+ "Notification",
224
+ "SessionStart",
225
+ "SessionEnd",
226
+ "PreCompact"
227
+ ];
228
+ var MATCHER_EVENTS = [
229
+ "PreToolUse",
230
+ "PostToolUse",
231
+ "PermissionRequest",
232
+ "Notification",
233
+ "SessionStart",
234
+ "PreCompact"
235
+ ];
236
+ var PROMPT_HOOK_EVENTS = [
237
+ "Stop",
238
+ "SubagentStop",
239
+ "UserPromptSubmit",
240
+ "PreToolUse",
241
+ "PermissionRequest"
242
+ ];
243
+ var HOOK_TYPES = ["command", "prompt"];
244
+ var COMMON_TOOL_MATCHERS = [
245
+ "Bash",
246
+ "Read",
247
+ "Write",
248
+ "Edit",
249
+ "Glob",
250
+ "Grep",
251
+ "Task",
252
+ "WebFetch",
253
+ "WebSearch",
254
+ "NotebookEdit",
255
+ "LSP",
256
+ "TodoWrite",
257
+ "AskUserQuestion"
258
+ ];
259
+ var NOTIFICATION_MATCHERS = [
260
+ "permission_prompt",
261
+ "idle_prompt",
262
+ "auth_success",
263
+ "elicitation_dialog"
264
+ ];
265
+ var SESSION_START_MATCHERS = ["startup", "resume", "clear", "compact"];
266
+ var PRE_COMPACT_MATCHERS = ["manual", "auto"];
267
+ var DEFAULT_COMMAND_TIMEOUT = 60;
268
+ var DEFAULT_PROMPT_TIMEOUT = 30;
269
+ var VARIABLE_MAPPINGS = {
270
+ OMNIDEV_CAPABILITY_ROOT: "CLAUDE_PLUGIN_ROOT",
271
+ OMNIDEV_PROJECT_DIR: "CLAUDE_PROJECT_DIR"
272
+ };
273
+ var HOOKS_CONFIG_FILENAME = "hooks.toml";
274
+ var HOOKS_DIRECTORY = "hooks";
275
+
276
+ // src/hooks/validation.ts
277
+ import { existsSync as existsSync4, statSync } from "node:fs";
278
+ import { resolve } from "node:path";
279
+
280
+ // src/hooks/types.ts
281
+ function isHookCommand(hook) {
282
+ return hook.type === "command";
283
+ }
284
+ function isHookPrompt(hook) {
285
+ return hook.type === "prompt";
286
+ }
287
+ function isMatcherEvent(event) {
288
+ return MATCHER_EVENTS.includes(event);
289
+ }
290
+ function isPromptHookEvent(event) {
291
+ return PROMPT_HOOK_EVENTS.includes(event);
292
+ }
293
+ function isHookEvent(event) {
294
+ return HOOK_EVENTS.includes(event);
295
+ }
296
+ function isHookType(type) {
297
+ return HOOK_TYPES.includes(type);
298
+ }
299
+
300
+ // src/hooks/validation.ts
301
+ function validateHooksConfig(config, options) {
302
+ const errors = [];
303
+ const warnings = [];
304
+ const opts = { checkScripts: false, ...options };
305
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
306
+ errors.push({
307
+ severity: "error",
308
+ code: "HOOKS_INVALID_TOML",
309
+ message: "Hooks configuration must be an object"
310
+ });
311
+ return { valid: false, errors, warnings };
312
+ }
313
+ const configObj = config;
314
+ for (const key of Object.keys(configObj)) {
315
+ if (key === "description") {
316
+ continue;
317
+ }
318
+ if (!isHookEvent(key)) {
319
+ errors.push({
320
+ severity: "error",
321
+ code: "HOOKS_UNKNOWN_EVENT",
322
+ message: `Unknown hook event: "${key}"`,
323
+ suggestion: `Valid events are: ${HOOK_EVENTS.join(", ")}`
324
+ });
325
+ continue;
326
+ }
327
+ const event = key;
328
+ const matchers = configObj[key];
329
+ if (!Array.isArray(matchers)) {
330
+ errors.push({
331
+ severity: "error",
332
+ code: "HOOKS_INVALID_TOML",
333
+ event,
334
+ message: `${event} must be an array of matchers`
335
+ });
336
+ continue;
337
+ }
338
+ matchers.forEach((matcher, matcherIndex) => {
339
+ const matcherIssues = validateMatcher(matcher, event, matcherIndex, opts);
340
+ for (const issue of matcherIssues) {
341
+ if (issue.severity === "error") {
342
+ errors.push(issue);
343
+ } else {
344
+ warnings.push(issue);
345
+ }
346
+ }
347
+ });
348
+ }
349
+ return {
350
+ valid: errors.length === 0,
351
+ errors,
352
+ warnings
353
+ };
354
+ }
355
+ function validateMatcher(matcher, event, matcherIndex, options) {
356
+ const issues = [];
357
+ if (typeof matcher !== "object" || matcher === null || Array.isArray(matcher)) {
358
+ issues.push({
359
+ severity: "error",
360
+ code: "HOOKS_INVALID_TOML",
361
+ event,
362
+ matcherIndex,
363
+ message: `Matcher at index ${matcherIndex} must be an object`
364
+ });
365
+ return issues;
366
+ }
367
+ const matcherObj = matcher;
368
+ const matcherPattern = matcherObj["matcher"];
369
+ if (matcherPattern !== undefined) {
370
+ if (typeof matcherPattern !== "string") {
371
+ issues.push({
372
+ severity: "error",
373
+ code: "HOOKS_INVALID_MATCHER",
374
+ event,
375
+ matcherIndex,
376
+ message: "Matcher pattern must be a string"
377
+ });
378
+ } else {
379
+ if (!isMatcherEvent(event) && matcherPattern !== "" && matcherPattern !== "*") {
380
+ issues.push({
381
+ severity: "warning",
382
+ code: "HOOKS_INVALID_MATCHER",
383
+ event,
384
+ matcherIndex,
385
+ message: `Matcher pattern on ${event} will be ignored (this event doesn't support matchers)`
386
+ });
387
+ }
388
+ const patternIssue = validateMatcherPattern(matcherPattern, event, matcherIndex);
389
+ if (patternIssue) {
390
+ issues.push(patternIssue);
391
+ }
392
+ }
393
+ }
394
+ if (!("hooks" in matcherObj)) {
395
+ issues.push({
396
+ severity: "error",
397
+ code: "HOOKS_INVALID_HOOKS_ARRAY",
398
+ event,
399
+ matcherIndex,
400
+ message: "Matcher must have a 'hooks' array"
401
+ });
402
+ return issues;
403
+ }
404
+ const hooksArray = matcherObj["hooks"];
405
+ if (!Array.isArray(hooksArray)) {
406
+ issues.push({
407
+ severity: "error",
408
+ code: "HOOKS_INVALID_HOOKS_ARRAY",
409
+ event,
410
+ matcherIndex,
411
+ message: "'hooks' must be an array"
412
+ });
413
+ return issues;
414
+ }
415
+ if (hooksArray.length === 0) {
416
+ issues.push({
417
+ severity: "warning",
418
+ code: "HOOKS_EMPTY_ARRAY",
419
+ event,
420
+ matcherIndex,
421
+ message: "Empty hooks array"
422
+ });
423
+ }
424
+ hooksArray.forEach((hook, hookIndex) => {
425
+ const hookIssues = validateHook(hook, event, { matcherIndex, hookIndex }, options);
426
+ issues.push(...hookIssues);
427
+ });
428
+ return issues;
429
+ }
430
+ function validateHook(hook, event, context, options) {
431
+ const issues = [];
432
+ const { matcherIndex, hookIndex } = context;
433
+ if (typeof hook !== "object" || hook === null || Array.isArray(hook)) {
434
+ issues.push({
435
+ severity: "error",
436
+ code: "HOOKS_INVALID_TOML",
437
+ event,
438
+ matcherIndex,
439
+ hookIndex,
440
+ message: "Hook must be an object"
441
+ });
442
+ return issues;
443
+ }
444
+ const hookObj = hook;
445
+ if (!("type" in hookObj)) {
446
+ issues.push({
447
+ severity: "error",
448
+ code: "HOOKS_INVALID_TYPE",
449
+ event,
450
+ matcherIndex,
451
+ hookIndex,
452
+ message: "Hook must have a 'type' field"
453
+ });
454
+ return issues;
455
+ }
456
+ const hookType = hookObj["type"];
457
+ if (typeof hookType !== "string" || !isHookType(hookType)) {
458
+ issues.push({
459
+ severity: "error",
460
+ code: "HOOKS_INVALID_TYPE",
461
+ event,
462
+ matcherIndex,
463
+ hookIndex,
464
+ message: `Invalid hook type: "${String(hookType)}". Must be "command" or "prompt"`
465
+ });
466
+ return issues;
467
+ }
468
+ if (hookType === "prompt" && !isPromptHookEvent(event)) {
469
+ issues.push({
470
+ severity: "error",
471
+ code: "HOOKS_PROMPT_NOT_ALLOWED",
472
+ event,
473
+ matcherIndex,
474
+ hookIndex,
475
+ message: `Prompt-type hooks are not allowed for ${event}`,
476
+ suggestion: `Prompt hooks are only allowed for: Stop, SubagentStop, UserPromptSubmit, PreToolUse, PermissionRequest`
477
+ });
478
+ }
479
+ if (hookType === "command") {
480
+ const command = hookObj["command"];
481
+ if (typeof command !== "string") {
482
+ issues.push({
483
+ severity: "error",
484
+ code: "HOOKS_MISSING_COMMAND",
485
+ event,
486
+ matcherIndex,
487
+ hookIndex,
488
+ message: "Command hook must have a 'command' string field"
489
+ });
490
+ } else {
491
+ const claudeVarMatch = command.match(/\$\{?CLAUDE_[A-Z_]+\}?/);
492
+ if (claudeVarMatch) {
493
+ const matchedVar = claudeVarMatch[0];
494
+ if (matchedVar) {
495
+ const omnidevVar = Object.entries(VARIABLE_MAPPINGS).find(([, claude]) => matchedVar.includes(claude))?.[0];
496
+ const issue = {
497
+ severity: "warning",
498
+ code: "HOOKS_CLAUDE_VARIABLE",
499
+ event,
500
+ matcherIndex,
501
+ hookIndex,
502
+ message: `Using Claude variable "${matchedVar}" instead of OmniDev variable`
503
+ };
504
+ if (omnidevVar) {
505
+ issue.suggestion = `Use \${${omnidevVar}} instead`;
506
+ }
507
+ issues.push(issue);
508
+ }
509
+ }
510
+ if (options?.checkScripts && options.basePath) {
511
+ const scriptIssues = validateScriptInCommand(command, options.basePath, event, matcherIndex, hookIndex);
512
+ issues.push(...scriptIssues);
513
+ }
514
+ }
515
+ }
516
+ if (hookType === "prompt") {
517
+ const prompt = hookObj["prompt"];
518
+ if (typeof prompt !== "string") {
519
+ issues.push({
520
+ severity: "error",
521
+ code: "HOOKS_MISSING_PROMPT",
522
+ event,
523
+ matcherIndex,
524
+ hookIndex,
525
+ message: "Prompt hook must have a 'prompt' string field"
526
+ });
527
+ }
528
+ }
529
+ if ("timeout" in hookObj) {
530
+ const timeout = hookObj["timeout"];
531
+ if (typeof timeout !== "number") {
532
+ issues.push({
533
+ severity: "error",
534
+ code: "HOOKS_INVALID_TIMEOUT",
535
+ event,
536
+ matcherIndex,
537
+ hookIndex,
538
+ message: "Timeout must be a number"
539
+ });
540
+ } else if (timeout <= 0) {
541
+ issues.push({
542
+ severity: "error",
543
+ code: "HOOKS_INVALID_TIMEOUT",
544
+ event,
545
+ matcherIndex,
546
+ hookIndex,
547
+ message: "Timeout must be a positive number"
548
+ });
549
+ }
550
+ }
551
+ return issues;
552
+ }
553
+ function validateMatcherPattern(pattern, event, matcherIndex) {
554
+ if (pattern === "" || pattern === "*") {
555
+ return null;
556
+ }
557
+ try {
558
+ new RegExp(pattern);
559
+ return null;
560
+ } catch {
561
+ return {
562
+ severity: "error",
563
+ code: "HOOKS_INVALID_MATCHER",
564
+ event,
565
+ matcherIndex,
566
+ message: `Invalid regex pattern: "${pattern}"`
567
+ };
568
+ }
569
+ }
570
+ function isValidMatcherPattern(pattern) {
571
+ if (pattern === "" || pattern === "*") {
572
+ return true;
573
+ }
574
+ try {
575
+ new RegExp(pattern);
576
+ return true;
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+ function validateScriptInCommand(command, basePath, event, matcherIndex, hookIndex) {
582
+ const issues = [];
583
+ const scriptPatterns = [
584
+ /"\$\{?OMNIDEV_CAPABILITY_ROOT\}?\/([^"]+)"/g,
585
+ /(?:^|\s)\.\/([^\s;|&]+)/g
586
+ ];
587
+ for (const pattern of scriptPatterns) {
588
+ let match = pattern.exec(command);
589
+ while (match !== null) {
590
+ const relativePath = match[1];
591
+ if (relativePath) {
592
+ const fullPath = resolve(basePath, relativePath);
593
+ if (!existsSync4(fullPath)) {
594
+ issues.push({
595
+ severity: "error",
596
+ code: "HOOKS_SCRIPT_NOT_FOUND",
597
+ event,
598
+ matcherIndex,
599
+ hookIndex,
600
+ path: fullPath,
601
+ message: `Script file not found: ${relativePath}`
602
+ });
603
+ } else {
604
+ try {
605
+ const stats = statSync(fullPath);
606
+ const isExecutable = !!(stats.mode & 73);
607
+ if (!isExecutable) {
608
+ issues.push({
609
+ severity: "warning",
610
+ code: "HOOKS_SCRIPT_NOT_EXECUTABLE",
611
+ event,
612
+ matcherIndex,
613
+ hookIndex,
614
+ path: fullPath,
615
+ message: `Script file is not executable: ${relativePath}`,
616
+ suggestion: `Run: chmod +x ${relativePath}`
617
+ });
618
+ }
619
+ } catch {}
620
+ }
621
+ }
622
+ match = pattern.exec(command);
623
+ }
624
+ }
625
+ return issues;
626
+ }
627
+ function findDuplicateCommands(config) {
628
+ const issues = [];
629
+ const seenCommands = new Map;
630
+ for (const eventName of HOOK_EVENTS) {
631
+ const matchers = config[eventName];
632
+ if (!matchers)
633
+ continue;
634
+ matchers.forEach((matcher, matcherIndex) => {
635
+ matcher.hooks.forEach((hook, hookIndex) => {
636
+ if (hook.type === "command") {
637
+ const command = hook.command;
638
+ const existing = seenCommands.get(command);
639
+ if (existing) {
640
+ issues.push({
641
+ severity: "warning",
642
+ code: "HOOKS_DUPLICATE_COMMAND",
643
+ event: eventName,
644
+ matcherIndex,
645
+ hookIndex,
646
+ message: `Duplicate command found (also at ${existing.event}[${existing.matcherIndex}].hooks[${existing.hookIndex}])`
647
+ });
648
+ } else {
649
+ seenCommands.set(command, { event: eventName, matcherIndex, hookIndex });
650
+ }
651
+ }
652
+ });
653
+ });
654
+ }
655
+ return issues;
656
+ }
657
+ function createEmptyHooksConfig() {
658
+ return {};
659
+ }
660
+ function createEmptyValidationResult() {
661
+ return {
662
+ valid: true,
663
+ errors: [],
664
+ warnings: []
665
+ };
666
+ }
667
+
668
+ // src/hooks/variables.ts
669
+ var REVERSE_MAPPINGS = Object.fromEntries(Object.entries(VARIABLE_MAPPINGS).map(([omni, claude]) => [claude, omni]));
670
+ function transformToOmnidev(content) {
671
+ let result = content;
672
+ for (const [claude, omni] of Object.entries(REVERSE_MAPPINGS)) {
673
+ result = result.replace(new RegExp(`\\$\\{${claude}\\}`, "g"), `\${${omni}}`);
674
+ result = result.replace(new RegExp(`\\$${claude}(?![A-Za-z0-9_])`, "g"), `$${omni}`);
675
+ }
676
+ return result;
677
+ }
678
+ function transformToClaude(content) {
679
+ let result = content;
680
+ for (const [omni, claude] of Object.entries(VARIABLE_MAPPINGS)) {
681
+ result = result.replace(new RegExp(`\\$\\{${omni}\\}`, "g"), `\${${claude}}`);
682
+ result = result.replace(new RegExp(`\\$${omni}(?![A-Za-z0-9_])`, "g"), `$${claude}`);
683
+ }
684
+ return result;
685
+ }
686
+ function transformHook(hook, direction) {
687
+ const transform = direction === "toOmnidev" ? transformToOmnidev : transformToClaude;
688
+ if (hook.type === "command") {
689
+ return {
690
+ ...hook,
691
+ command: transform(hook.command)
692
+ };
693
+ }
694
+ if (hook.type === "prompt") {
695
+ return {
696
+ ...hook,
697
+ prompt: transform(hook.prompt)
698
+ };
699
+ }
700
+ return hook;
701
+ }
702
+ function transformMatcher(matcher, direction) {
703
+ return {
704
+ ...matcher,
705
+ hooks: matcher.hooks.map((hook) => transformHook(hook, direction))
706
+ };
707
+ }
708
+ function transformHooksConfig(config, direction) {
709
+ const result = {};
710
+ if (config.description !== undefined) {
711
+ result.description = config.description;
712
+ }
713
+ const events = [
714
+ "PreToolUse",
715
+ "PostToolUse",
716
+ "PermissionRequest",
717
+ "UserPromptSubmit",
718
+ "Stop",
719
+ "SubagentStop",
720
+ "Notification",
721
+ "SessionStart",
722
+ "SessionEnd",
723
+ "PreCompact"
724
+ ];
725
+ for (const event of events) {
726
+ const matchers = config[event];
727
+ if (matchers) {
728
+ result[event] = matchers.map((m) => transformMatcher(m, direction));
729
+ }
730
+ }
731
+ return result;
732
+ }
733
+ function containsClaudeVariables(content) {
734
+ for (const claude of Object.values(VARIABLE_MAPPINGS)) {
735
+ if (content.includes(`\${${claude}}`) || content.includes(`$${claude}`)) {
736
+ return true;
737
+ }
738
+ }
739
+ return false;
740
+ }
741
+ function containsOmnidevVariables(content) {
742
+ for (const omni of Object.keys(VARIABLE_MAPPINGS)) {
743
+ if (content.includes(`\${${omni}}`) || content.includes(`$${omni}`)) {
744
+ return true;
745
+ }
746
+ }
747
+ return false;
748
+ }
749
+
750
+ // src/hooks/loader.ts
751
+ function loadHooksFromCapability(capabilityPath, options) {
752
+ const opts = {
753
+ transformVariables: true,
754
+ validate: true,
755
+ checkScripts: false,
756
+ ...options
757
+ };
758
+ const hooksDir = join3(capabilityPath, HOOKS_DIRECTORY);
759
+ const configPath = join3(hooksDir, HOOKS_CONFIG_FILENAME);
760
+ if (!existsSync5(configPath)) {
761
+ return {
762
+ config: createEmptyHooksConfig(),
763
+ validation: createEmptyValidationResult(),
764
+ found: false
765
+ };
766
+ }
767
+ let rawContent;
768
+ try {
769
+ rawContent = readFileSync(configPath, "utf-8");
770
+ } catch (error) {
771
+ return {
772
+ config: createEmptyHooksConfig(),
773
+ validation: {
774
+ valid: false,
775
+ errors: [
776
+ {
777
+ severity: "error",
778
+ code: "HOOKS_INVALID_TOML",
779
+ message: `Failed to read hooks config: ${error instanceof Error ? error.message : String(error)}`,
780
+ path: configPath
781
+ }
782
+ ],
783
+ warnings: []
784
+ },
785
+ found: true,
786
+ configPath,
787
+ loadError: `Failed to read: ${error instanceof Error ? error.message : String(error)}`
788
+ };
789
+ }
790
+ let content = rawContent;
791
+ if (opts.transformVariables && containsClaudeVariables(rawContent)) {
792
+ content = transformToOmnidev(rawContent);
793
+ }
794
+ let parsed;
795
+ try {
796
+ parsed = parseToml(content);
797
+ } catch (error) {
798
+ return {
799
+ config: createEmptyHooksConfig(),
800
+ validation: {
801
+ valid: false,
802
+ errors: [
803
+ {
804
+ severity: "error",
805
+ code: "HOOKS_INVALID_TOML",
806
+ message: `Invalid TOML syntax: ${error instanceof Error ? error.message : String(error)}`,
807
+ path: configPath
808
+ }
809
+ ],
810
+ warnings: []
811
+ },
812
+ found: true,
813
+ configPath,
814
+ loadError: `Invalid TOML: ${error instanceof Error ? error.message : String(error)}`
815
+ };
816
+ }
817
+ let validation;
818
+ if (opts.validate) {
819
+ validation = validateHooksConfig(parsed, {
820
+ basePath: hooksDir,
821
+ checkScripts: opts.checkScripts ?? false
822
+ });
823
+ } else {
824
+ validation = createEmptyValidationResult();
825
+ }
826
+ return {
827
+ config: validation.valid ? parsed : createEmptyHooksConfig(),
828
+ validation,
829
+ found: true,
830
+ configPath
831
+ };
832
+ }
833
+ function loadCapabilityHooks(capabilityName, capabilityPath, options) {
834
+ const result = loadHooksFromCapability(capabilityPath, options);
835
+ if (!result.found) {
836
+ return null;
837
+ }
838
+ return {
839
+ capabilityName,
840
+ capabilityPath,
841
+ config: result.config,
842
+ validation: result.validation
843
+ };
844
+ }
845
+ function hasHooks(capabilityPath) {
846
+ const configPath = join3(capabilityPath, HOOKS_DIRECTORY, HOOKS_CONFIG_FILENAME);
847
+ return existsSync5(configPath);
848
+ }
849
+ function getHooksDirectory(capabilityPath) {
850
+ return join3(capabilityPath, HOOKS_DIRECTORY);
851
+ }
852
+ function getHooksConfigPath(capabilityPath) {
853
+ return join3(capabilityPath, HOOKS_DIRECTORY, HOOKS_CONFIG_FILENAME);
854
+ }
855
+
210
856
  // src/capability/rules.ts
211
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
857
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "node:fs";
212
858
  import { readFile as readFile4, writeFile } from "node:fs/promises";
213
- import { basename as basename2, join as join3 } from "node:path";
859
+ import { basename as basename2, join as join4 } from "node:path";
214
860
  async function loadRules(capabilityPath, capabilityId) {
215
- const rulesDir = join3(capabilityPath, "rules");
216
- if (!existsSync4(rulesDir)) {
861
+ const rulesDir = join4(capabilityPath, "rules");
862
+ if (!existsSync6(rulesDir)) {
217
863
  return [];
218
864
  }
219
865
  const rules = [];
220
866
  const entries = readdirSync3(rulesDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
221
867
  for (const entry of entries) {
222
868
  if (entry.isFile() && entry.name.endsWith(".md")) {
223
- const rulePath = join3(rulesDir, entry.name);
869
+ const rulePath = join4(rulesDir, entry.name);
224
870
  const content = await readFile4(rulePath, "utf-8");
225
871
  rules.push({
226
872
  name: basename2(entry.name, ".md"),
@@ -235,7 +881,7 @@ async function writeRules(rules, docs = []) {
235
881
  const instructionsPath = ".omni/instructions.md";
236
882
  const rulesContent = generateRulesContent(rules, docs);
237
883
  let content;
238
- if (existsSync4(instructionsPath)) {
884
+ if (existsSync6(instructionsPath)) {
239
885
  content = await readFile4(instructionsPath, "utf-8");
240
886
  } else {
241
887
  content = `# OmniDev Instructions
@@ -307,20 +953,20 @@ ${rule.content}
307
953
  }
308
954
 
309
955
  // src/capability/skills.ts
310
- import { existsSync as existsSync5, readdirSync as readdirSync4 } from "node:fs";
956
+ import { existsSync as existsSync7, readdirSync as readdirSync4 } from "node:fs";
311
957
  import { readFile as readFile5 } from "node:fs/promises";
312
- import { join as join4 } from "node:path";
958
+ import { join as join5 } from "node:path";
313
959
  async function loadSkills(capabilityPath, capabilityId) {
314
- const skillsDir = join4(capabilityPath, "skills");
315
- if (!existsSync5(skillsDir)) {
960
+ const skillsDir = join5(capabilityPath, "skills");
961
+ if (!existsSync7(skillsDir)) {
316
962
  return [];
317
963
  }
318
964
  const skills = [];
319
965
  const entries = readdirSync4(skillsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
320
966
  for (const entry of entries) {
321
967
  if (entry.isDirectory()) {
322
- const skillPath = join4(skillsDir, entry.name, "SKILL.md");
323
- if (existsSync5(skillPath)) {
968
+ const skillPath = join5(skillsDir, entry.name, "SKILL.md");
969
+ if (existsSync7(skillPath)) {
324
970
  const skill = await parseSkillFile(skillPath, capabilityId);
325
971
  skills.push(skill);
326
972
  }
@@ -348,20 +994,20 @@ async function parseSkillFile(filePath, capabilityId) {
348
994
  }
349
995
 
350
996
  // src/capability/subagents.ts
351
- import { existsSync as existsSync6, readdirSync as readdirSync5 } from "node:fs";
997
+ import { existsSync as existsSync8, readdirSync as readdirSync5 } from "node:fs";
352
998
  import { readFile as readFile6 } from "node:fs/promises";
353
- import { join as join5 } from "node:path";
999
+ import { join as join6 } from "node:path";
354
1000
  async function loadSubagents(capabilityPath, capabilityId) {
355
- const subagentsDir = join5(capabilityPath, "subagents");
356
- if (!existsSync6(subagentsDir)) {
1001
+ const subagentsDir = join6(capabilityPath, "subagents");
1002
+ if (!existsSync8(subagentsDir)) {
357
1003
  return [];
358
1004
  }
359
1005
  const subagents = [];
360
1006
  const entries = readdirSync5(subagentsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
361
1007
  for (const entry of entries) {
362
1008
  if (entry.isDirectory()) {
363
- const subagentPath = join5(subagentsDir, entry.name, "SUBAGENT.md");
364
- if (existsSync6(subagentPath)) {
1009
+ const subagentPath = join6(subagentsDir, entry.name, "SUBAGENT.md");
1010
+ if (existsSync8(subagentPath)) {
365
1011
  const subagent = await parseSubagentFile(subagentPath, capabilityId);
366
1012
  subagents.push(subagent);
367
1013
  }
@@ -414,13 +1060,13 @@ function parseCommaSeparatedList(value) {
414
1060
  var CAPABILITIES_DIR = ".omni/capabilities";
415
1061
  async function discoverCapabilities() {
416
1062
  const capabilities = [];
417
- if (existsSync7(CAPABILITIES_DIR)) {
1063
+ if (existsSync9(CAPABILITIES_DIR)) {
418
1064
  const entries = readdirSync6(CAPABILITIES_DIR, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
419
1065
  for (const entry of entries) {
420
1066
  if (entry.isDirectory()) {
421
- const entryPath = join6(CAPABILITIES_DIR, entry.name);
422
- const configPath = join6(entryPath, "capability.toml");
423
- if (existsSync7(configPath)) {
1067
+ const entryPath = join7(CAPABILITIES_DIR, entry.name);
1068
+ const configPath = join7(entryPath, "capability.toml");
1069
+ if (existsSync9(configPath)) {
424
1070
  capabilities.push(entryPath);
425
1071
  }
426
1072
  }
@@ -429,18 +1075,18 @@ async function discoverCapabilities() {
429
1075
  return capabilities;
430
1076
  }
431
1077
  async function loadCapabilityConfig(capabilityPath) {
432
- const configPath = join6(capabilityPath, "capability.toml");
1078
+ const configPath = join7(capabilityPath, "capability.toml");
433
1079
  const content = await readFile7(configPath, "utf-8");
434
1080
  const config = parseCapabilityConfig(content);
435
1081
  return config;
436
1082
  }
437
1083
  async function importCapabilityExports(capabilityPath) {
438
- const indexPath = join6(capabilityPath, "index.ts");
439
- if (!existsSync7(indexPath)) {
1084
+ const indexPath = join7(capabilityPath, "index.ts");
1085
+ if (!existsSync9(indexPath)) {
440
1086
  return {};
441
1087
  }
442
1088
  try {
443
- const absolutePath = join6(process.cwd(), indexPath);
1089
+ const absolutePath = join7(process.cwd(), indexPath);
444
1090
  const module = await import(absolutePath);
445
1091
  return module;
446
1092
  } catch (error) {
@@ -455,8 +1101,8 @@ If this is a project-specific capability, install dependencies or remove it from
455
1101
  }
456
1102
  }
457
1103
  async function loadTypeDefinitions(capabilityPath) {
458
- const typesPath = join6(capabilityPath, "types.d.ts");
459
- if (!existsSync7(typesPath)) {
1104
+ const typesPath = join7(capabilityPath, "types.d.ts");
1105
+ if (!existsSync9(typesPath)) {
460
1106
  return;
461
1107
  }
462
1108
  return readFile7(typesPath, "utf-8");
@@ -652,6 +1298,7 @@ async function loadCapability(capabilityPath, env) {
652
1298
  const typeDefinitionsFromExports = "typeDefinitions" in exports && typeof exportsAny.typeDefinitions === "string" ? exportsAny.typeDefinitions : undefined;
653
1299
  const typeDefinitions = typeDefinitionsFromExports !== undefined ? typeDefinitionsFromExports : await loadTypeDefinitions(capabilityPath);
654
1300
  const gitignore = "gitignore" in exports && Array.isArray(exportsAny.gitignore) ? exportsAny.gitignore : undefined;
1301
+ const hooks = loadCapabilityHooks(id, capabilityPath);
655
1302
  const result = {
656
1303
  id,
657
1304
  path: capabilityPath,
@@ -669,10 +1316,13 @@ async function loadCapability(capabilityPath, env) {
669
1316
  if (gitignore !== undefined) {
670
1317
  result.gitignore = gitignore;
671
1318
  }
1319
+ if (hooks !== null) {
1320
+ result.hooks = hooks;
1321
+ }
672
1322
  return result;
673
1323
  }
674
- // src/config/loader.ts
675
- import { existsSync as existsSync8 } from "node:fs";
1324
+ // src/config/config.ts
1325
+ import { existsSync as existsSync10 } from "node:fs";
676
1326
  import { readFile as readFile8, writeFile as writeFile2 } from "node:fs/promises";
677
1327
  var CONFIG_PATH = "omni.toml";
678
1328
  var LOCAL_CONFIG = "omni.local.toml";
@@ -692,7 +1342,7 @@ function mergeConfigs(base, override) {
692
1342
  return merged;
693
1343
  }
694
1344
  async function loadBaseConfig() {
695
- if (existsSync8(CONFIG_PATH)) {
1345
+ if (existsSync10(CONFIG_PATH)) {
696
1346
  const content = await readFile8(CONFIG_PATH, "utf-8");
697
1347
  return parseOmniConfig(content);
698
1348
  }
@@ -701,7 +1351,7 @@ async function loadBaseConfig() {
701
1351
  async function loadConfig() {
702
1352
  const baseConfig = await loadBaseConfig();
703
1353
  let localConfig = {};
704
- if (existsSync8(LOCAL_CONFIG)) {
1354
+ if (existsSync10(LOCAL_CONFIG)) {
705
1355
  const content = await readFile8(LOCAL_CONFIG, "utf-8");
706
1356
  localConfig = parseOmniConfig(content);
707
1357
  }
@@ -784,6 +1434,28 @@ function generateConfigToml(config) {
784
1434
  }
785
1435
  lines.push("");
786
1436
  lines.push("# =============================================================================");
1437
+ lines.push("# Capability Groups");
1438
+ lines.push("# =============================================================================");
1439
+ lines.push("# Bundle multiple capabilities under a single name for cleaner profiles.");
1440
+ lines.push('# Reference groups in profiles with the "group:" prefix.');
1441
+ lines.push("#");
1442
+ const groups = config.capabilities?.groups;
1443
+ if (groups && Object.keys(groups).length > 0) {
1444
+ lines.push("[capabilities.groups]");
1445
+ for (const [name, caps] of Object.entries(groups)) {
1446
+ const capsStr = caps.map((c) => `"${c}"`).join(", ");
1447
+ lines.push(`${name} = [${capsStr}]`);
1448
+ }
1449
+ } else {
1450
+ lines.push("# [capabilities.groups]");
1451
+ lines.push('# expo = ["expo-app-design", "expo-deployment", "upgrading-expo"]');
1452
+ lines.push('# backend = ["cloudflare", "database-tools"]');
1453
+ lines.push("#");
1454
+ lines.push("# [profiles.mobile]");
1455
+ lines.push('# capabilities = ["group:expo", "react-native-tools"]');
1456
+ }
1457
+ lines.push("");
1458
+ lines.push("# =============================================================================");
787
1459
  lines.push("# MCP Servers");
788
1460
  lines.push("# =============================================================================");
789
1461
  lines.push("# Define MCP servers that automatically become capabilities.");
@@ -873,12 +1545,12 @@ function generateConfigToml(config) {
873
1545
  }
874
1546
 
875
1547
  // src/state/active-profile.ts
876
- import { existsSync as existsSync9, mkdirSync } from "node:fs";
1548
+ import { existsSync as existsSync11, mkdirSync } from "node:fs";
877
1549
  import { readFile as readFile9, unlink, writeFile as writeFile3 } from "node:fs/promises";
878
1550
  var STATE_DIR = ".omni/state";
879
1551
  var ACTIVE_PROFILE_PATH = `${STATE_DIR}/active-profile`;
880
1552
  async function readActiveProfileState() {
881
- if (!existsSync9(ACTIVE_PROFILE_PATH)) {
1553
+ if (!existsSync11(ACTIVE_PROFILE_PATH)) {
882
1554
  return null;
883
1555
  }
884
1556
  try {
@@ -894,7 +1566,7 @@ async function writeActiveProfileState(profileName) {
894
1566
  await writeFile3(ACTIVE_PROFILE_PATH, profileName, "utf-8");
895
1567
  }
896
1568
  async function clearActiveProfileState() {
897
- if (existsSync9(ACTIVE_PROFILE_PATH)) {
1569
+ if (existsSync11(ACTIVE_PROFILE_PATH)) {
898
1570
  await unlink(ACTIVE_PROFILE_PATH);
899
1571
  }
900
1572
  }
@@ -915,7 +1587,24 @@ function resolveEnabledCapabilities(config, profileName) {
915
1587
  const profile = profileName ? config.profiles?.[profileName] : config.profiles?.[config.active_profile ?? "default"];
916
1588
  const profileCapabilities = profile?.capabilities ?? [];
917
1589
  const alwaysEnabled = config.always_enabled_capabilities ?? [];
918
- return [...new Set([...alwaysEnabled, ...profileCapabilities])];
1590
+ const groups = config.capabilities?.groups ?? {};
1591
+ const expandCapabilities = (caps) => {
1592
+ return caps.flatMap((cap) => {
1593
+ if (cap.startsWith("group:")) {
1594
+ const groupName = cap.slice(6);
1595
+ const groupCaps = groups[groupName];
1596
+ if (!groupCaps) {
1597
+ console.warn(`Unknown capability group: ${groupName}`);
1598
+ return [];
1599
+ }
1600
+ return groupCaps;
1601
+ }
1602
+ return cap;
1603
+ });
1604
+ };
1605
+ const expandedAlways = expandCapabilities(alwaysEnabled);
1606
+ const expandedProfile = expandCapabilities(profileCapabilities);
1607
+ return [...new Set([...expandedAlways, ...expandedProfile])];
919
1608
  }
920
1609
  async function loadProfileConfig(profileName) {
921
1610
  const config = await loadConfig();
@@ -962,6 +1651,93 @@ async function disableCapability(capabilityId) {
962
1651
  await writeConfig(config);
963
1652
  }
964
1653
 
1654
+ // src/hooks/merger.ts
1655
+ function mergeHooksConfigs(capabilityHooks) {
1656
+ const result = {};
1657
+ for (const event of HOOK_EVENTS) {
1658
+ const allMatchers = [];
1659
+ for (const capHooks of capabilityHooks) {
1660
+ const matchers = capHooks.config[event];
1661
+ if (matchers && matchers.length > 0) {
1662
+ allMatchers.push(...matchers);
1663
+ }
1664
+ }
1665
+ if (allMatchers.length > 0) {
1666
+ result[event] = allMatchers;
1667
+ }
1668
+ }
1669
+ return result;
1670
+ }
1671
+ function mergeAndDeduplicateHooks(capabilityHooks, options) {
1672
+ const merged = mergeHooksConfigs(capabilityHooks);
1673
+ if (!options?.deduplicateCommands) {
1674
+ return merged;
1675
+ }
1676
+ const result = {};
1677
+ for (const event of HOOK_EVENTS) {
1678
+ const matchers = merged[event];
1679
+ if (!matchers || matchers.length === 0) {
1680
+ continue;
1681
+ }
1682
+ const seenCommands = new Set;
1683
+ const deduplicatedMatchers = [];
1684
+ for (const matcher of matchers) {
1685
+ const deduplicatedHooks = matcher.hooks.filter((hook) => {
1686
+ if (hook.type !== "command") {
1687
+ return true;
1688
+ }
1689
+ const key = hook.command;
1690
+ if (seenCommands.has(key)) {
1691
+ return false;
1692
+ }
1693
+ seenCommands.add(key);
1694
+ return true;
1695
+ });
1696
+ if (deduplicatedHooks.length > 0) {
1697
+ deduplicatedMatchers.push({
1698
+ ...matcher,
1699
+ hooks: deduplicatedHooks
1700
+ });
1701
+ }
1702
+ }
1703
+ if (deduplicatedMatchers.length > 0) {
1704
+ result[event] = deduplicatedMatchers;
1705
+ }
1706
+ }
1707
+ return result;
1708
+ }
1709
+ function hasAnyHooks(config) {
1710
+ for (const event of HOOK_EVENTS) {
1711
+ const matchers = config[event];
1712
+ if (matchers && matchers.length > 0) {
1713
+ return true;
1714
+ }
1715
+ }
1716
+ return false;
1717
+ }
1718
+ function countHooks(config) {
1719
+ let count = 0;
1720
+ for (const event of HOOK_EVENTS) {
1721
+ const matchers = config[event];
1722
+ if (matchers) {
1723
+ for (const matcher of matchers) {
1724
+ count += matcher.hooks.length;
1725
+ }
1726
+ }
1727
+ }
1728
+ return count;
1729
+ }
1730
+ function getEventsWithHooks(config) {
1731
+ const events = [];
1732
+ for (const event of HOOK_EVENTS) {
1733
+ const matchers = config[event];
1734
+ if (matchers && matchers.length > 0) {
1735
+ events.push(event);
1736
+ }
1737
+ }
1738
+ return events;
1739
+ }
1740
+
965
1741
  // src/capability/registry.ts
966
1742
  async function buildCapabilityRegistry() {
967
1743
  const env = await loadEnvironment();
@@ -980,21 +1756,32 @@ async function buildCapabilityRegistry() {
980
1756
  console.warn(` ${errorMessage}`);
981
1757
  }
982
1758
  }
1759
+ const getAllCapabilityHooks = () => {
1760
+ const hooks = [];
1761
+ for (const cap of capabilities.values()) {
1762
+ if (cap.hooks) {
1763
+ hooks.push(cap.hooks);
1764
+ }
1765
+ }
1766
+ return hooks;
1767
+ };
983
1768
  return {
984
1769
  capabilities,
985
1770
  getCapability: (id) => capabilities.get(id),
986
1771
  getAllCapabilities: () => [...capabilities.values()],
987
1772
  getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
988
1773
  getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
989
- getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs)
1774
+ getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
1775
+ getAllCapabilityHooks,
1776
+ getMergedHooks: () => mergeHooksConfigs(getAllCapabilityHooks())
990
1777
  };
991
1778
  }
992
1779
  // src/capability/sources.ts
993
- import { existsSync as existsSync10 } from "node:fs";
1780
+ import { existsSync as existsSync12 } from "node:fs";
994
1781
  import { spawn } from "node:child_process";
995
- import { cp, mkdir, readdir, readFile as readFile10, rm, stat, writeFile as writeFile4 } from "node:fs/promises";
996
- import { join as join7 } from "node:path";
997
- import { parse as parseToml } from "smol-toml";
1782
+ import { cp, mkdir, readdir, readFile as readFile10, rename, rm, stat, writeFile as writeFile4 } from "node:fs/promises";
1783
+ import { join as join8 } from "node:path";
1784
+ import { parse as parseToml2 } from "smol-toml";
998
1785
  var OMNI_LOCAL = ".omni";
999
1786
  var SKILL_DIRS = ["skills", "skill"];
1000
1787
  var AGENT_DIRS = ["agents", "agent", "subagents", "subagent"];
@@ -1005,7 +1792,7 @@ var SKILL_FILES = ["SKILL.md", "skill.md", "Skill.md"];
1005
1792
  var AGENT_FILES = ["AGENT.md", "agent.md", "Agent.md", "SUBAGENT.md", "subagent.md"];
1006
1793
  var COMMAND_FILES = ["COMMAND.md", "command.md", "Command.md"];
1007
1794
  async function spawnCapture(command, args, options) {
1008
- return await new Promise((resolve, reject) => {
1795
+ return await new Promise((resolve2, reject) => {
1009
1796
  const child = spawn(command, args, {
1010
1797
  cwd: options?.cwd,
1011
1798
  stdio: ["ignore", "pipe", "pipe"]
@@ -1022,7 +1809,7 @@ async function spawnCapture(command, args, options) {
1022
1809
  });
1023
1810
  child.on("error", (error) => reject(error));
1024
1811
  child.on("close", (exitCode) => {
1025
- resolve({ exitCode: exitCode ?? 0, stdout, stderr });
1812
+ resolve2({ exitCode: exitCode ?? 0, stdout, stderr });
1026
1813
  });
1027
1814
  });
1028
1815
  }
@@ -1051,19 +1838,19 @@ function sourceToGitUrl(source) {
1051
1838
  return source;
1052
1839
  }
1053
1840
  function getSourceCapabilityPath(id) {
1054
- return join7(OMNI_LOCAL, "capabilities", id);
1841
+ return join8(OMNI_LOCAL, "capabilities", id);
1055
1842
  }
1056
1843
  function getLockFilePath() {
1057
1844
  return "omni.lock.toml";
1058
1845
  }
1059
1846
  async function loadLockFile() {
1060
1847
  const lockPath = getLockFilePath();
1061
- if (!existsSync10(lockPath)) {
1848
+ if (!existsSync12(lockPath)) {
1062
1849
  return { capabilities: {} };
1063
1850
  }
1064
1851
  try {
1065
1852
  const content = await readFile10(lockPath, "utf-8");
1066
- const parsed = parseToml(content);
1853
+ const parsed = parseToml2(content);
1067
1854
  const capabilities = parsed["capabilities"];
1068
1855
  return {
1069
1856
  capabilities: capabilities || {}
@@ -1092,7 +1879,7 @@ function stringifyLockFile(lockFile) {
1092
1879
  }
1093
1880
  async function saveLockFile(lockFile) {
1094
1881
  const lockPath = getLockFilePath();
1095
- await mkdir(join7(OMNI_LOCAL, "capabilities"), { recursive: true });
1882
+ await mkdir(join8(OMNI_LOCAL, "capabilities"), { recursive: true });
1096
1883
  const header = `# Auto-generated by OmniDev - DO NOT EDIT
1097
1884
  # Records installed capability versions for reproducibility
1098
1885
  # Last updated: ${new Date().toISOString()}
@@ -1114,7 +1901,7 @@ function shortCommit(commit) {
1114
1901
  return commit.substring(0, 7);
1115
1902
  }
1116
1903
  async function cloneRepo(gitUrl, targetPath, ref) {
1117
- await mkdir(join7(targetPath, ".."), { recursive: true });
1904
+ await mkdir(join8(targetPath, ".."), { recursive: true });
1118
1905
  const args = ["clone", "--depth", "1"];
1119
1906
  if (ref) {
1120
1907
  args.push("--branch", ref);
@@ -1151,16 +1938,16 @@ async function fetchRepo(repoPath, ref) {
1151
1938
  return true;
1152
1939
  }
1153
1940
  function hasCapabilityToml(dirPath) {
1154
- return existsSync10(join7(dirPath, "capability.toml"));
1941
+ return existsSync12(join8(dirPath, "capability.toml"));
1155
1942
  }
1156
1943
  async function shouldWrapDirectory(dirPath) {
1157
- if (existsSync10(join7(dirPath, ".claude-plugin", "plugin.json"))) {
1944
+ if (existsSync12(join8(dirPath, ".claude-plugin", "plugin.json"))) {
1158
1945
  return true;
1159
1946
  }
1160
1947
  const allDirs = [...SKILL_DIRS, ...AGENT_DIRS, ...COMMAND_DIRS, ...RULE_DIRS, ...DOC_DIRS];
1161
1948
  for (const dirName of allDirs) {
1162
- const checkPath = join7(dirPath, dirName);
1163
- if (existsSync10(checkPath)) {
1949
+ const checkPath = join8(dirPath, dirName);
1950
+ if (existsSync12(checkPath)) {
1164
1951
  const stats = await stat(checkPath);
1165
1952
  if (stats.isDirectory()) {
1166
1953
  return true;
@@ -1171,8 +1958,8 @@ async function shouldWrapDirectory(dirPath) {
1171
1958
  }
1172
1959
  async function findMatchingDirs(basePath, names) {
1173
1960
  for (const name of names) {
1174
- const dirPath = join7(basePath, name);
1175
- if (existsSync10(dirPath)) {
1961
+ const dirPath = join8(basePath, name);
1962
+ if (existsSync12(dirPath)) {
1176
1963
  const stats = await stat(dirPath);
1177
1964
  if (stats.isDirectory()) {
1178
1965
  return dirPath;
@@ -1183,15 +1970,15 @@ async function findMatchingDirs(basePath, names) {
1183
1970
  }
1184
1971
  async function findContentItems(dirPath, filePatterns) {
1185
1972
  const items = [];
1186
- if (!existsSync10(dirPath)) {
1973
+ if (!existsSync12(dirPath)) {
1187
1974
  return items;
1188
1975
  }
1189
1976
  const entries = (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1190
1977
  for (const entry of entries) {
1191
- const entryPath = join7(dirPath, entry.name);
1978
+ const entryPath = join8(dirPath, entry.name);
1192
1979
  if (entry.isDirectory()) {
1193
1980
  for (const pattern of filePatterns) {
1194
- if (existsSync10(join7(entryPath, pattern))) {
1981
+ if (existsSync12(join8(entryPath, pattern))) {
1195
1982
  items.push({
1196
1983
  name: entry.name,
1197
1984
  path: entryPath,
@@ -1212,8 +1999,8 @@ async function findContentItems(dirPath, filePatterns) {
1212
1999
  return items;
1213
2000
  }
1214
2001
  async function parsePluginJson(dirPath) {
1215
- const pluginJsonPath = join7(dirPath, ".claude-plugin", "plugin.json");
1216
- if (!existsSync10(pluginJsonPath)) {
2002
+ const pluginJsonPath = join8(dirPath, ".claude-plugin", "plugin.json");
2003
+ if (!existsSync12(pluginJsonPath)) {
1217
2004
  return null;
1218
2005
  }
1219
2006
  try {
@@ -1237,8 +2024,8 @@ async function parsePluginJson(dirPath) {
1237
2024
  }
1238
2025
  }
1239
2026
  async function readReadmeDescription(dirPath) {
1240
- const readmePath = join7(dirPath, "README.md");
1241
- if (!existsSync10(readmePath)) {
2027
+ const readmePath = join8(dirPath, "README.md");
2028
+ if (!existsSync12(readmePath)) {
1242
2029
  return null;
1243
2030
  }
1244
2031
  try {
@@ -1270,6 +2057,29 @@ async function readReadmeDescription(dirPath) {
1270
2057
  return null;
1271
2058
  }
1272
2059
  }
2060
+ async function normalizeFolderNames(repoPath) {
2061
+ const renameMappings = [
2062
+ { from: "skill", to: "skills" },
2063
+ { from: "command", to: "commands" },
2064
+ { from: "rule", to: "rules" },
2065
+ { from: "agent", to: "agents" },
2066
+ { from: "subagent", to: "subagents" }
2067
+ ];
2068
+ for (const { from, to } of renameMappings) {
2069
+ const fromPath = join8(repoPath, from);
2070
+ const toPath = join8(repoPath, to);
2071
+ if (existsSync12(fromPath) && !existsSync12(toPath)) {
2072
+ try {
2073
+ const stats = await stat(fromPath);
2074
+ if (stats.isDirectory()) {
2075
+ await rename(fromPath, toPath);
2076
+ }
2077
+ } catch (error) {
2078
+ console.warn(`Failed to rename ${from} to ${to}:`, error);
2079
+ }
2080
+ }
2081
+ }
2082
+ }
1273
2083
  async function discoverContent(repoPath) {
1274
2084
  const result = {
1275
2085
  skills: [],
@@ -1347,7 +2157,7 @@ repository = "${repoUrl}"
1347
2157
  wrapped = true
1348
2158
  commit = "${commit}"
1349
2159
  `;
1350
- await writeFile4(join7(repoPath, "capability.toml"), tomlContent, "utf-8");
2160
+ await writeFile4(join8(repoPath, "capability.toml"), tomlContent, "utf-8");
1351
2161
  }
1352
2162
  async function fetchGitCapabilitySource(id, config, options) {
1353
2163
  const gitUrl = sourceToGitUrl(config.source);
@@ -1356,8 +2166,8 @@ async function fetchGitCapabilitySource(id, config, options) {
1356
2166
  let commit;
1357
2167
  let repoPath;
1358
2168
  if (config.path) {
1359
- const tempPath = join7(OMNI_LOCAL, "_temp", `${id}-repo`);
1360
- if (existsSync10(join7(tempPath, ".git"))) {
2169
+ const tempPath = join8(OMNI_LOCAL, "_temp", `${id}-repo`);
2170
+ if (existsSync12(join8(tempPath, ".git"))) {
1361
2171
  if (!options?.silent) {
1362
2172
  console.log(` Checking ${id}...`);
1363
2173
  }
@@ -1367,23 +2177,23 @@ async function fetchGitCapabilitySource(id, config, options) {
1367
2177
  if (!options?.silent) {
1368
2178
  console.log(` Cloning ${id} from ${config.source}...`);
1369
2179
  }
1370
- await mkdir(join7(tempPath, ".."), { recursive: true });
2180
+ await mkdir(join8(tempPath, ".."), { recursive: true });
1371
2181
  await cloneRepo(gitUrl, tempPath, config.ref);
1372
2182
  commit = await getRepoCommit(tempPath);
1373
2183
  updated = true;
1374
2184
  }
1375
- const sourcePath = join7(tempPath, config.path);
1376
- if (!existsSync10(sourcePath)) {
2185
+ const sourcePath = join8(tempPath, config.path);
2186
+ if (!existsSync12(sourcePath)) {
1377
2187
  throw new Error(`Path not found in repository: ${config.path}`);
1378
2188
  }
1379
- if (existsSync10(targetPath)) {
2189
+ if (existsSync12(targetPath)) {
1380
2190
  await rm(targetPath, { recursive: true });
1381
2191
  }
1382
- await mkdir(join7(targetPath, ".."), { recursive: true });
2192
+ await mkdir(join8(targetPath, ".."), { recursive: true });
1383
2193
  await cp(sourcePath, targetPath, { recursive: true });
1384
2194
  repoPath = targetPath;
1385
2195
  } else {
1386
- if (existsSync10(join7(targetPath, ".git"))) {
2196
+ if (existsSync12(join8(targetPath, ".git"))) {
1387
2197
  if (!options?.silent) {
1388
2198
  console.log(` Checking ${id}...`);
1389
2199
  }
@@ -1404,6 +2214,7 @@ async function fetchGitCapabilitySource(id, config, options) {
1404
2214
  needsWrap = await shouldWrapDirectory(repoPath);
1405
2215
  }
1406
2216
  if (needsWrap) {
2217
+ await normalizeFolderNames(repoPath);
1407
2218
  const content = await discoverContent(repoPath);
1408
2219
  await generateCapabilityToml(id, repoPath, config.source, commit, content);
1409
2220
  if (!options?.silent) {
@@ -1420,8 +2231,8 @@ async function fetchGitCapabilitySource(id, config, options) {
1420
2231
  }
1421
2232
  }
1422
2233
  let version = shortCommit(commit);
1423
- const pkgJsonPath = join7(repoPath, "package.json");
1424
- if (existsSync10(pkgJsonPath)) {
2234
+ const pkgJsonPath = join8(repoPath, "package.json");
2235
+ if (existsSync12(pkgJsonPath)) {
1425
2236
  try {
1426
2237
  const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
1427
2238
  if (pkgJson.version) {
@@ -1506,17 +2317,17 @@ generated_from_omni_toml = true
1506
2317
  }
1507
2318
  async function generateMcpCapabilityToml(id, mcpConfig, targetPath) {
1508
2319
  const tomlContent = generateMcpCapabilityTomlContent(id, mcpConfig);
1509
- await writeFile4(join7(targetPath, "capability.toml"), tomlContent, "utf-8");
2320
+ await writeFile4(join8(targetPath, "capability.toml"), tomlContent, "utf-8");
1510
2321
  }
1511
2322
  async function isGeneratedMcpCapability(capabilityDir) {
1512
- const tomlPath = join7(capabilityDir, "capability.toml");
1513
- if (!existsSync10(tomlPath)) {
2323
+ const tomlPath = join8(capabilityDir, "capability.toml");
2324
+ if (!existsSync12(tomlPath)) {
1514
2325
  console.warn("no capability.toml found in", capabilityDir);
1515
2326
  return false;
1516
2327
  }
1517
2328
  try {
1518
2329
  const content = await readFile10(tomlPath, "utf-8");
1519
- const parsed = parseToml(content);
2330
+ const parsed = parseToml2(content);
1520
2331
  const capability = parsed["capability"];
1521
2332
  const metadata = capability?.["metadata"];
1522
2333
  return metadata?.["generated_from_omni_toml"] === true;
@@ -1525,14 +2336,14 @@ async function isGeneratedMcpCapability(capabilityDir) {
1525
2336
  }
1526
2337
  }
1527
2338
  async function cleanupStaleMcpCapabilities(currentMcpIds) {
1528
- const capabilitiesDir = join7(OMNI_LOCAL, "capabilities");
1529
- if (!existsSync10(capabilitiesDir)) {
2339
+ const capabilitiesDir = join8(OMNI_LOCAL, "capabilities");
2340
+ if (!existsSync12(capabilitiesDir)) {
1530
2341
  return;
1531
2342
  }
1532
2343
  const entries = await readdir(capabilitiesDir, { withFileTypes: true });
1533
2344
  for (const entry of entries) {
1534
2345
  if (entry.isDirectory()) {
1535
- const capDir = join7(capabilitiesDir, entry.name);
2346
+ const capDir = join8(capabilitiesDir, entry.name);
1536
2347
  const isGenerated = await isGeneratedMcpCapability(capDir);
1537
2348
  if (isGenerated && !currentMcpIds.has(entry.name)) {
1538
2349
  await rm(capDir, { recursive: true });
@@ -1545,10 +2356,10 @@ async function generateMcpCapabilities(config) {
1545
2356
  await cleanupStaleMcpCapabilities(new Set);
1546
2357
  return;
1547
2358
  }
1548
- const mcpCapabilitiesDir = join7(OMNI_LOCAL, "capabilities");
2359
+ const mcpCapabilitiesDir = join8(OMNI_LOCAL, "capabilities");
1549
2360
  const currentMcpIds = new Set;
1550
2361
  for (const [id, mcpConfig] of Object.entries(config.mcps)) {
1551
- const targetPath = join7(mcpCapabilitiesDir, id);
2362
+ const targetPath = join8(mcpCapabilitiesDir, id);
1552
2363
  currentMcpIds.add(id);
1553
2364
  await mkdir(targetPath, { recursive: true });
1554
2365
  await generateMcpCapabilityToml(id, mcpConfig, targetPath);
@@ -1622,7 +2433,7 @@ async function checkForUpdates(config) {
1622
2433
  const targetPath = getSourceCapabilityPath(id);
1623
2434
  const existing = lockFile.capabilities[id];
1624
2435
  const gitConfig = sourceConfig;
1625
- if (!existsSync10(join7(targetPath, ".git"))) {
2436
+ if (!existsSync12(join8(targetPath, ".git"))) {
1626
2437
  updates.push({
1627
2438
  id,
1628
2439
  source: gitConfig.source,
@@ -1658,12 +2469,12 @@ async function checkForUpdates(config) {
1658
2469
  return updates;
1659
2470
  }
1660
2471
  // src/config/provider.ts
1661
- import { existsSync as existsSync11 } from "node:fs";
2472
+ import { existsSync as existsSync13 } from "node:fs";
1662
2473
  import { readFile as readFile11, writeFile as writeFile5 } from "node:fs/promises";
1663
2474
  import { parse as parse2 } from "smol-toml";
1664
2475
  var PROVIDER_CONFIG_PATH = ".omni/provider.toml";
1665
2476
  async function loadProviderConfig() {
1666
- if (!existsSync11(PROVIDER_CONFIG_PATH)) {
2477
+ if (!existsSync13(PROVIDER_CONFIG_PATH)) {
1667
2478
  return { provider: "claude" };
1668
2479
  }
1669
2480
  const content = await readFile11(PROVIDER_CONFIG_PATH, "utf-8");
@@ -1707,16 +2518,222 @@ function parseProviderFlag(flag) {
1707
2518
  }
1708
2519
  throw new Error(`Invalid provider: ${flag}. Must be 'claude', 'codex', or 'both'.`);
1709
2520
  }
1710
- // src/mcp-json/manager.ts
1711
- import { existsSync as existsSync12 } from "node:fs";
2521
+ // src/config/toml-patcher.ts
2522
+ import { existsSync as existsSync14 } from "node:fs";
1712
2523
  import { readFile as readFile12, writeFile as writeFile6 } from "node:fs/promises";
2524
+ var CONFIG_PATH2 = "omni.toml";
2525
+ async function readConfigFile() {
2526
+ if (!existsSync14(CONFIG_PATH2)) {
2527
+ return "";
2528
+ }
2529
+ return readFile12(CONFIG_PATH2, "utf-8");
2530
+ }
2531
+ async function writeConfigFile(content) {
2532
+ await writeFile6(CONFIG_PATH2, content, "utf-8");
2533
+ }
2534
+ function findSection(lines, sectionPattern) {
2535
+ return lines.findIndex((line) => sectionPattern.test(line.trim()));
2536
+ }
2537
+ function findSectionEnd(lines, startIndex) {
2538
+ for (let i = startIndex + 1;i < lines.length; i++) {
2539
+ const line = lines[i];
2540
+ if (line === undefined)
2541
+ continue;
2542
+ const trimmed = line.trim();
2543
+ if (/^\[(?!\[)/.test(trimmed) && !trimmed.startsWith("#")) {
2544
+ return i;
2545
+ }
2546
+ }
2547
+ return lines.length;
2548
+ }
2549
+ function formatCapabilitySource(name, source) {
2550
+ if (typeof source === "string") {
2551
+ return `${name} = "${source}"`;
2552
+ }
2553
+ if (source.path) {
2554
+ return `${name} = { source = "${source.source}", path = "${source.path}" }`;
2555
+ }
2556
+ return `${name} = "${source.source}"`;
2557
+ }
2558
+ async function patchAddCapabilitySource(name, source) {
2559
+ let content = await readConfigFile();
2560
+ const lines = content.split(`
2561
+ `);
2562
+ const sectionIndex = findSection(lines, /^\[capabilities\.sources\]$/);
2563
+ const newEntry = formatCapabilitySource(name, source);
2564
+ if (sectionIndex !== -1) {
2565
+ const sectionEnd = findSectionEnd(lines, sectionIndex);
2566
+ let insertIndex = sectionEnd;
2567
+ for (let i = sectionEnd - 1;i > sectionIndex; i--) {
2568
+ const line = lines[i];
2569
+ if (line === undefined)
2570
+ continue;
2571
+ const trimmed = line.trim();
2572
+ if (trimmed && !trimmed.startsWith("#")) {
2573
+ insertIndex = i + 1;
2574
+ break;
2575
+ }
2576
+ }
2577
+ if (insertIndex === sectionEnd && sectionIndex + 1 < lines.length) {
2578
+ insertIndex = sectionIndex + 1;
2579
+ }
2580
+ lines.splice(insertIndex, 0, newEntry);
2581
+ } else {
2582
+ const capabilitiesIndex = findSection(lines, /^\[capabilities\]$/);
2583
+ if (capabilitiesIndex !== -1) {
2584
+ const capEnd = findSectionEnd(lines, capabilitiesIndex);
2585
+ lines.splice(capEnd, 0, "", "[capabilities.sources]", newEntry);
2586
+ } else {
2587
+ const mcpsIndex = findSection(lines, /^\[mcps/);
2588
+ if (mcpsIndex !== -1) {
2589
+ lines.splice(mcpsIndex, 0, "[capabilities.sources]", newEntry, "");
2590
+ } else {
2591
+ lines.push("", "[capabilities.sources]", newEntry);
2592
+ }
2593
+ }
2594
+ }
2595
+ content = lines.join(`
2596
+ `);
2597
+ await writeConfigFile(content);
2598
+ }
2599
+ function formatMcpConfig(name, config) {
2600
+ const lines = [];
2601
+ lines.push(`[mcps.${name}]`);
2602
+ if (config.transport && config.transport !== "stdio") {
2603
+ lines.push(`transport = "${config.transport}"`);
2604
+ }
2605
+ if (config.command) {
2606
+ lines.push(`command = "${config.command}"`);
2607
+ }
2608
+ if (config.args && config.args.length > 0) {
2609
+ const argsStr = config.args.map((a) => `"${a}"`).join(", ");
2610
+ lines.push(`args = [${argsStr}]`);
2611
+ }
2612
+ if (config.cwd) {
2613
+ lines.push(`cwd = "${config.cwd}"`);
2614
+ }
2615
+ if (config.url) {
2616
+ lines.push(`url = "${config.url}"`);
2617
+ }
2618
+ if (config.env && Object.keys(config.env).length > 0) {
2619
+ lines.push(`[mcps.${name}.env]`);
2620
+ for (const [key, value] of Object.entries(config.env)) {
2621
+ lines.push(`${key} = "${value}"`);
2622
+ }
2623
+ }
2624
+ if (config.headers && Object.keys(config.headers).length > 0) {
2625
+ lines.push(`[mcps.${name}.headers]`);
2626
+ for (const [key, value] of Object.entries(config.headers)) {
2627
+ lines.push(`${key} = "${value}"`);
2628
+ }
2629
+ }
2630
+ return lines;
2631
+ }
2632
+ async function patchAddMcp(name, config) {
2633
+ let content = await readConfigFile();
2634
+ const lines = content.split(`
2635
+ `);
2636
+ const mcpLines = formatMcpConfig(name, config);
2637
+ const existingMcpIndex = findSection(lines, /^\[mcps\./);
2638
+ if (existingMcpIndex !== -1) {
2639
+ let lastMcpEnd = existingMcpIndex;
2640
+ for (let i = existingMcpIndex;i < lines.length; i++) {
2641
+ const line = lines[i];
2642
+ if (line === undefined)
2643
+ continue;
2644
+ const trimmed = line.trim();
2645
+ if (/^\[mcps\./.test(trimmed)) {
2646
+ lastMcpEnd = findSectionEnd(lines, i);
2647
+ } else if (/^\[(?!mcps\.)/.test(trimmed) && !trimmed.startsWith("#")) {
2648
+ break;
2649
+ }
2650
+ }
2651
+ lines.splice(lastMcpEnd, 0, "", ...mcpLines);
2652
+ } else {
2653
+ const profilesIndex = findSection(lines, /^\[profiles\./);
2654
+ if (profilesIndex !== -1) {
2655
+ lines.splice(profilesIndex, 0, ...mcpLines, "");
2656
+ } else {
2657
+ lines.push("", ...mcpLines);
2658
+ }
2659
+ }
2660
+ content = lines.join(`
2661
+ `);
2662
+ await writeConfigFile(content);
2663
+ }
2664
+ async function patchAddToProfile(profileName, capabilityName) {
2665
+ let content = await readConfigFile();
2666
+ const lines = content.split(`
2667
+ `);
2668
+ const profilePattern = new RegExp(`^\\[profiles\\.${escapeRegExp(profileName)}\\]$`);
2669
+ const profileIndex = findSection(lines, profilePattern);
2670
+ if (profileIndex !== -1) {
2671
+ const profileEnd = findSectionEnd(lines, profileIndex);
2672
+ let capabilitiesLineIndex = -1;
2673
+ for (let i = profileIndex + 1;i < profileEnd; i++) {
2674
+ const line = lines[i];
2675
+ if (line === undefined)
2676
+ continue;
2677
+ const trimmed = line.trim();
2678
+ if (trimmed.startsWith("capabilities")) {
2679
+ capabilitiesLineIndex = i;
2680
+ break;
2681
+ }
2682
+ }
2683
+ if (capabilitiesLineIndex !== -1) {
2684
+ const line = lines[capabilitiesLineIndex];
2685
+ if (line !== undefined) {
2686
+ const match = line.match(/capabilities\s*=\s*\[(.*)\]/);
2687
+ if (match && match[1] !== undefined) {
2688
+ const existingCaps = match[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2689
+ const quotedCap = `"${capabilityName}"`;
2690
+ if (!existingCaps.includes(quotedCap)) {
2691
+ existingCaps.push(quotedCap);
2692
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
2693
+ lines[capabilitiesLineIndex] = `${indent}capabilities = [${existingCaps.join(", ")}]`;
2694
+ }
2695
+ }
2696
+ }
2697
+ } else {
2698
+ lines.splice(profileIndex + 1, 0, `capabilities = ["${capabilityName}"]`);
2699
+ }
2700
+ } else {
2701
+ const anyProfileIndex = findSection(lines, /^\[profiles\./);
2702
+ if (anyProfileIndex !== -1) {
2703
+ let lastProfileEnd = anyProfileIndex;
2704
+ for (let i = anyProfileIndex;i < lines.length; i++) {
2705
+ const line = lines[i];
2706
+ if (line === undefined)
2707
+ continue;
2708
+ const trimmed = line.trim();
2709
+ if (/^\[profiles\./.test(trimmed)) {
2710
+ lastProfileEnd = findSectionEnd(lines, i);
2711
+ } else if (/^\[(?!profiles\.)/.test(trimmed) && !trimmed.startsWith("#")) {
2712
+ break;
2713
+ }
2714
+ }
2715
+ lines.splice(lastProfileEnd, 0, "", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
2716
+ } else {
2717
+ lines.push("", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
2718
+ }
2719
+ }
2720
+ content = lines.join(`
2721
+ `);
2722
+ await writeConfigFile(content);
2723
+ }
2724
+ function escapeRegExp(str) {
2725
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2726
+ }
2727
+ // src/mcp-json/manager.ts
2728
+ import { existsSync as existsSync15 } from "node:fs";
2729
+ import { readFile as readFile13, writeFile as writeFile7 } from "node:fs/promises";
1713
2730
  var MCP_JSON_PATH = ".mcp.json";
1714
2731
  async function readMcpJson() {
1715
- if (!existsSync12(MCP_JSON_PATH)) {
2732
+ if (!existsSync15(MCP_JSON_PATH)) {
1716
2733
  return { mcpServers: {} };
1717
2734
  }
1718
2735
  try {
1719
- const content = await readFile12(MCP_JSON_PATH, "utf-8");
2736
+ const content = await readFile13(MCP_JSON_PATH, "utf-8");
1720
2737
  const parsed = JSON.parse(content);
1721
2738
  return {
1722
2739
  mcpServers: parsed.mcpServers || {}
@@ -1725,8 +2742,8 @@ async function readMcpJson() {
1725
2742
  return { mcpServers: {} };
1726
2743
  }
1727
2744
  }
1728
- async function writeMcpJson(config) {
1729
- await writeFile6(MCP_JSON_PATH, `${JSON.stringify(config, null, 2)}
2745
+ async function writeMcpJson(config2) {
2746
+ await writeFile7(MCP_JSON_PATH, `${JSON.stringify(config2, null, 2)}
1730
2747
  `, "utf-8");
1731
2748
  }
1732
2749
  function buildMcpServerConfig(mcp) {
@@ -1735,41 +2752,41 @@ function buildMcpServerConfig(mcp) {
1735
2752
  if (!mcp.url) {
1736
2753
  throw new Error("HTTP transport requires a URL");
1737
2754
  }
1738
- const config2 = {
2755
+ const config3 = {
1739
2756
  type: "http",
1740
2757
  url: mcp.url
1741
2758
  };
1742
2759
  if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1743
- config2.headers = mcp.headers;
2760
+ config3.headers = mcp.headers;
1744
2761
  }
1745
- return config2;
2762
+ return config3;
1746
2763
  }
1747
2764
  if (transport === "sse") {
1748
2765
  if (!mcp.url) {
1749
2766
  throw new Error("SSE transport requires a URL");
1750
2767
  }
1751
- const config2 = {
2768
+ const config3 = {
1752
2769
  type: "sse",
1753
2770
  url: mcp.url
1754
2771
  };
1755
2772
  if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1756
- config2.headers = mcp.headers;
2773
+ config3.headers = mcp.headers;
1757
2774
  }
1758
- return config2;
2775
+ return config3;
1759
2776
  }
1760
2777
  if (!mcp.command) {
1761
2778
  throw new Error("stdio transport requires a command");
1762
2779
  }
1763
- const config = {
2780
+ const config2 = {
1764
2781
  command: mcp.command
1765
2782
  };
1766
2783
  if (mcp.args) {
1767
- config.args = mcp.args;
2784
+ config2.args = mcp.args;
1768
2785
  }
1769
2786
  if (mcp.env) {
1770
- config.env = mcp.env;
2787
+ config2.env = mcp.env;
1771
2788
  }
1772
- return config;
2789
+ return config2;
1773
2790
  }
1774
2791
  async function syncMcpJson(capabilities2, previousManifest, options = {}) {
1775
2792
  const mcpJson = await readMcpJson();
@@ -1795,24 +2812,24 @@ async function syncMcpJson(capabilities2, previousManifest, options = {}) {
1795
2812
  }
1796
2813
  }
1797
2814
  // src/state/manifest.ts
1798
- import { existsSync as existsSync13, mkdirSync as mkdirSync2, rmSync } from "node:fs";
1799
- import { readFile as readFile13, writeFile as writeFile7 } from "node:fs/promises";
2815
+ import { existsSync as existsSync16, mkdirSync as mkdirSync2, rmSync } from "node:fs";
2816
+ import { readFile as readFile14, writeFile as writeFile8 } from "node:fs/promises";
1800
2817
  var MANIFEST_PATH = ".omni/state/manifest.json";
1801
2818
  var CURRENT_VERSION = 1;
1802
2819
  async function loadManifest() {
1803
- if (!existsSync13(MANIFEST_PATH)) {
2820
+ if (!existsSync16(MANIFEST_PATH)) {
1804
2821
  return {
1805
2822
  version: CURRENT_VERSION,
1806
2823
  syncedAt: new Date().toISOString(),
1807
2824
  capabilities: {}
1808
2825
  };
1809
2826
  }
1810
- const content = await readFile13(MANIFEST_PATH, "utf-8");
2827
+ const content = await readFile14(MANIFEST_PATH, "utf-8");
1811
2828
  return JSON.parse(content);
1812
2829
  }
1813
2830
  async function saveManifest(manifest) {
1814
2831
  mkdirSync2(".omni/state", { recursive: true });
1815
- await writeFile7(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
2832
+ await writeFile8(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
1816
2833
  `, "utf-8");
1817
2834
  }
1818
2835
  function buildManifestFromCapabilities(capabilities2) {
@@ -1847,14 +2864,14 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
1847
2864
  }
1848
2865
  for (const skillName of resources.skills) {
1849
2866
  const skillDir = `.claude/skills/${skillName}`;
1850
- if (existsSync13(skillDir)) {
2867
+ if (existsSync16(skillDir)) {
1851
2868
  rmSync(skillDir, { recursive: true });
1852
2869
  result.deletedSkills.push(skillName);
1853
2870
  }
1854
2871
  }
1855
2872
  for (const ruleName of resources.rules) {
1856
2873
  const rulePath = `.cursor/rules/omnidev-${ruleName}.mdc`;
1857
- if (existsSync13(rulePath)) {
2874
+ if (existsSync16(rulePath)) {
1858
2875
  rmSync(rulePath);
1859
2876
  result.deletedRules.push(ruleName);
1860
2877
  }
@@ -1863,17 +2880,17 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
1863
2880
  return result;
1864
2881
  }
1865
2882
  // src/state/providers.ts
1866
- import { existsSync as existsSync14, mkdirSync as mkdirSync3 } from "node:fs";
1867
- import { readFile as readFile14, writeFile as writeFile8 } from "node:fs/promises";
2883
+ import { existsSync as existsSync17, mkdirSync as mkdirSync3 } from "node:fs";
2884
+ import { readFile as readFile15, writeFile as writeFile9 } from "node:fs/promises";
1868
2885
  var STATE_DIR2 = ".omni/state";
1869
2886
  var PROVIDERS_PATH = `${STATE_DIR2}/providers.json`;
1870
2887
  var DEFAULT_PROVIDERS = ["claude-code"];
1871
2888
  async function readEnabledProviders() {
1872
- if (!existsSync14(PROVIDERS_PATH)) {
2889
+ if (!existsSync17(PROVIDERS_PATH)) {
1873
2890
  return DEFAULT_PROVIDERS;
1874
2891
  }
1875
2892
  try {
1876
- const content = await readFile14(PROVIDERS_PATH, "utf-8");
2893
+ const content = await readFile15(PROVIDERS_PATH, "utf-8");
1877
2894
  const state = JSON.parse(content);
1878
2895
  return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
1879
2896
  } catch {
@@ -1883,7 +2900,7 @@ async function readEnabledProviders() {
1883
2900
  async function writeEnabledProviders(providers) {
1884
2901
  mkdirSync3(STATE_DIR2, { recursive: true });
1885
2902
  const state = { enabled: providers };
1886
- await writeFile8(PROVIDERS_PATH, `${JSON.stringify(state, null, 2)}
2903
+ await writeFile9(PROVIDERS_PATH, `${JSON.stringify(state, null, 2)}
1887
2904
  `, "utf-8");
1888
2905
  }
1889
2906
  async function enableProvider(providerId) {
@@ -1905,18 +2922,18 @@ async function isProviderEnabled(providerId) {
1905
2922
  import { spawn as spawn2 } from "node:child_process";
1906
2923
  import { mkdirSync as mkdirSync4 } from "node:fs";
1907
2924
  async function installCapabilityDependencies(silent) {
1908
- const { existsSync: existsSync15, readdirSync: readdirSync7 } = await import("node:fs");
1909
- const { join: join8 } = await import("node:path");
2925
+ const { existsSync: existsSync18, readdirSync: readdirSync7 } = await import("node:fs");
2926
+ const { join: join9 } = await import("node:path");
1910
2927
  const capabilitiesDir = ".omni/capabilities";
1911
- if (!existsSync15(capabilitiesDir)) {
2928
+ if (!existsSync18(capabilitiesDir)) {
1912
2929
  return;
1913
2930
  }
1914
2931
  const entries = readdirSync7(capabilitiesDir, { withFileTypes: true });
1915
2932
  async function commandExists(cmd) {
1916
- return await new Promise((resolve) => {
2933
+ return await new Promise((resolve2) => {
1917
2934
  const proc = spawn2(cmd, ["--version"], { stdio: "ignore" });
1918
- proc.on("error", () => resolve(false));
1919
- proc.on("close", (code) => resolve(code === 0));
2935
+ proc.on("error", () => resolve2(false));
2936
+ proc.on("close", (code) => resolve2(code === 0));
1920
2937
  });
1921
2938
  }
1922
2939
  const hasBun = await commandExists("bun");
@@ -1928,16 +2945,16 @@ async function installCapabilityDependencies(silent) {
1928
2945
  if (!entry.isDirectory()) {
1929
2946
  continue;
1930
2947
  }
1931
- const capabilityPath = join8(capabilitiesDir, entry.name);
1932
- const packageJsonPath = join8(capabilityPath, "package.json");
1933
- if (!existsSync15(packageJsonPath)) {
2948
+ const capabilityPath = join9(capabilitiesDir, entry.name);
2949
+ const packageJsonPath = join9(capabilityPath, "package.json");
2950
+ if (!existsSync18(packageJsonPath)) {
1934
2951
  continue;
1935
2952
  }
1936
2953
  if (!silent) {
1937
2954
  console.log(`Installing dependencies for ${capabilityPath}...`);
1938
2955
  }
1939
- await new Promise((resolve, reject) => {
1940
- const useNpmCi = hasNpm && existsSync15(join8(capabilityPath, "package-lock.json"));
2956
+ await new Promise((resolve2, reject) => {
2957
+ const useNpmCi = hasNpm && existsSync18(join9(capabilityPath, "package-lock.json"));
1941
2958
  const cmd = hasBun ? "bun" : "npm";
1942
2959
  const args = hasBun ? ["install"] : useNpmCi ? ["ci"] : ["install"];
1943
2960
  const proc = spawn2(cmd, args, {
@@ -1946,7 +2963,7 @@ async function installCapabilityDependencies(silent) {
1946
2963
  });
1947
2964
  proc.on("close", (code) => {
1948
2965
  if (code === 0) {
1949
- resolve();
2966
+ resolve2();
1950
2967
  } else {
1951
2968
  reject(new Error(`Failed to install dependencies for ${capabilityPath}`));
1952
2969
  }
@@ -1959,8 +2976,8 @@ async function installCapabilityDependencies(silent) {
1959
2976
  }
1960
2977
  async function buildSyncBundle(options) {
1961
2978
  const silent = options?.silent ?? false;
1962
- const config = await loadConfig();
1963
- await fetchAllCapabilitySources(config, { silent });
2979
+ const config2 = await loadConfig();
2980
+ await fetchAllCapabilitySources(config2, { silent });
1964
2981
  await installCapabilityDependencies(silent);
1965
2982
  const registry = await buildCapabilityRegistry();
1966
2983
  const capabilities2 = registry.getAllCapabilities();
@@ -1969,6 +2986,7 @@ async function buildSyncBundle(options) {
1969
2986
  const docs = registry.getAllDocs();
1970
2987
  const commands = capabilities2.flatMap((c) => c.commands);
1971
2988
  const subagents = capabilities2.flatMap((c) => c.subagents);
2989
+ const mergedHooks = registry.getMergedHooks();
1972
2990
  const instructionsContent = generateInstructionsContent(rules, docs);
1973
2991
  const bundle = {
1974
2992
  capabilities: capabilities2,
@@ -1980,6 +2998,9 @@ async function buildSyncBundle(options) {
1980
2998
  instructionsPath: ".omni/instructions.md",
1981
2999
  instructionsContent
1982
3000
  };
3001
+ if (hasAnyHooks(mergedHooks)) {
3002
+ bundle.hooks = mergedHooks;
3003
+ }
1983
3004
  return { bundle };
1984
3005
  }
1985
3006
  async function syncAgentConfiguration(options) {
@@ -2028,10 +3049,10 @@ async function syncAgentConfiguration(options) {
2028
3049
  const newManifest = buildManifestFromCapabilities(capabilities2);
2029
3050
  await saveManifest(newManifest);
2030
3051
  if (adapters.length > 0) {
2031
- const config = await loadConfig();
3052
+ const config2 = await loadConfig();
2032
3053
  const ctx = {
2033
3054
  projectRoot: process.cwd(),
2034
- config
3055
+ config: config2
2035
3056
  };
2036
3057
  for (const adapter of adapters) {
2037
3058
  try {
@@ -2175,11 +3196,11 @@ function generateOmniMdTemplate() {
2175
3196
  `;
2176
3197
  }
2177
3198
  // src/types/index.ts
2178
- function getActiveProviders(config) {
2179
- if (config.providers)
2180
- return config.providers;
2181
- if (config.provider)
2182
- return [config.provider];
3199
+ function getActiveProviders(config2) {
3200
+ if (config2.providers)
3201
+ return config2.providers;
3202
+ if (config2.provider)
3203
+ return [config2.provider];
2183
3204
  return ["claude"];
2184
3205
  }
2185
3206
  // src/debug.ts
@@ -2210,7 +3231,12 @@ export {
2210
3231
  writeConfig,
2211
3232
  writeActiveProfileState,
2212
3233
  version,
3234
+ validateHooksConfig,
3235
+ validateHook,
2213
3236
  validateEnv,
3237
+ transformToOmnidev,
3238
+ transformToClaude,
3239
+ transformHooksConfig,
2214
3240
  syncMcpJson,
2215
3241
  syncAgentConfiguration,
2216
3242
  sourceToGitUrl,
@@ -2222,10 +3248,15 @@ export {
2222
3248
  readMcpJson,
2223
3249
  readEnabledProviders,
2224
3250
  readActiveProfileState,
3251
+ patchAddToProfile,
3252
+ patchAddMcp,
3253
+ patchAddCapabilitySource,
2225
3254
  parseSourceConfig,
2226
3255
  parseProviderFlag,
2227
3256
  parseOmniConfig,
2228
3257
  parseCapabilityConfig,
3258
+ mergeHooksConfigs,
3259
+ mergeAndDeduplicateHooks,
2229
3260
  loadSubagents,
2230
3261
  loadSkills,
2231
3262
  loadRules,
@@ -2233,19 +3264,33 @@ export {
2233
3264
  loadProfileConfig,
2234
3265
  loadManifest,
2235
3266
  loadLockFile,
3267
+ loadHooksFromCapability,
2236
3268
  loadEnvironment,
2237
3269
  loadDocs,
2238
3270
  loadConfig,
2239
3271
  loadCommands,
3272
+ loadCapabilityHooks,
2240
3273
  loadCapabilityConfig,
2241
3274
  loadCapability,
2242
3275
  loadBaseConfig,
3276
+ isValidMatcherPattern,
2243
3277
  isSecretEnvVar,
2244
3278
  isProviderEnabled,
3279
+ isPromptHookEvent,
3280
+ isMatcherEvent,
3281
+ isHookType,
3282
+ isHookPrompt,
3283
+ isHookEvent,
3284
+ isHookCommand,
2245
3285
  installCapabilityDependencies,
3286
+ hasHooks,
3287
+ hasAnyHooks,
2246
3288
  getVersion,
2247
3289
  getSourceCapabilityPath,
2248
3290
  getLockFilePath,
3291
+ getHooksDirectory,
3292
+ getHooksConfigPath,
3293
+ getEventsWithHooks,
2249
3294
  getEnabledCapabilities,
2250
3295
  getActiveProviders,
2251
3296
  getActiveProfile,
@@ -2253,6 +3298,7 @@ export {
2253
3298
  generateInstructionsTemplate,
2254
3299
  generateClaudeTemplate,
2255
3300
  generateAgentsTemplate,
3301
+ findDuplicateCommands,
2256
3302
  fetchCapabilitySource,
2257
3303
  fetchAllCapabilitySources,
2258
3304
  enableProvider,
@@ -2261,6 +3307,11 @@ export {
2261
3307
  disableProvider,
2262
3308
  disableCapability,
2263
3309
  debug,
3310
+ createEmptyValidationResult,
3311
+ createEmptyHooksConfig,
3312
+ countHooks,
3313
+ containsOmnidevVariables,
3314
+ containsClaudeVariables,
2264
3315
  clearActiveProfileState,
2265
3316
  cleanupStaleResources,
2266
3317
  checkForUpdates,
@@ -2268,5 +3319,18 @@ export {
2268
3319
  buildRouteMap,
2269
3320
  buildManifestFromCapabilities,
2270
3321
  buildCommand,
2271
- buildCapabilityRegistry
3322
+ buildCapabilityRegistry,
3323
+ VARIABLE_MAPPINGS,
3324
+ SESSION_START_MATCHERS,
3325
+ PROMPT_HOOK_EVENTS,
3326
+ PRE_COMPACT_MATCHERS,
3327
+ NOTIFICATION_MATCHERS,
3328
+ MATCHER_EVENTS,
3329
+ HOOK_TYPES,
3330
+ HOOK_EVENTS,
3331
+ HOOKS_DIRECTORY,
3332
+ HOOKS_CONFIG_FILENAME,
3333
+ DEFAULT_PROMPT_TIMEOUT,
3334
+ DEFAULT_COMMAND_TIMEOUT,
3335
+ COMMON_TOOL_MATCHERS
2272
3336
  };