@omnidev-ai/core 0.7.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
  }
@@ -729,10 +1379,6 @@ function generateConfigToml(config) {
729
1379
  lines.push("# 3. Run: omnidev sync");
730
1380
  lines.push("# 4. Switch profiles: omnidev profile use <name>");
731
1381
  lines.push("");
732
- if (config.project) {
733
- lines.push(`project = "${config.project}"`);
734
- lines.push("");
735
- }
736
1382
  if (config.providers?.enabled && config.providers.enabled.length > 0) {
737
1383
  lines.push("# AI providers to enable (claude, codex, or both)");
738
1384
  lines.push("[providers]");
@@ -788,6 +1434,28 @@ function generateConfigToml(config) {
788
1434
  }
789
1435
  lines.push("");
790
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("# =============================================================================");
791
1459
  lines.push("# MCP Servers");
792
1460
  lines.push("# =============================================================================");
793
1461
  lines.push("# Define MCP servers that automatically become capabilities.");
@@ -877,12 +1545,12 @@ function generateConfigToml(config) {
877
1545
  }
878
1546
 
879
1547
  // src/state/active-profile.ts
880
- import { existsSync as existsSync9, mkdirSync } from "node:fs";
1548
+ import { existsSync as existsSync11, mkdirSync } from "node:fs";
881
1549
  import { readFile as readFile9, unlink, writeFile as writeFile3 } from "node:fs/promises";
882
1550
  var STATE_DIR = ".omni/state";
883
1551
  var ACTIVE_PROFILE_PATH = `${STATE_DIR}/active-profile`;
884
1552
  async function readActiveProfileState() {
885
- if (!existsSync9(ACTIVE_PROFILE_PATH)) {
1553
+ if (!existsSync11(ACTIVE_PROFILE_PATH)) {
886
1554
  return null;
887
1555
  }
888
1556
  try {
@@ -898,7 +1566,7 @@ async function writeActiveProfileState(profileName) {
898
1566
  await writeFile3(ACTIVE_PROFILE_PATH, profileName, "utf-8");
899
1567
  }
900
1568
  async function clearActiveProfileState() {
901
- if (existsSync9(ACTIVE_PROFILE_PATH)) {
1569
+ if (existsSync11(ACTIVE_PROFILE_PATH)) {
902
1570
  await unlink(ACTIVE_PROFILE_PATH);
903
1571
  }
904
1572
  }
@@ -919,7 +1587,24 @@ function resolveEnabledCapabilities(config, profileName) {
919
1587
  const profile = profileName ? config.profiles?.[profileName] : config.profiles?.[config.active_profile ?? "default"];
920
1588
  const profileCapabilities = profile?.capabilities ?? [];
921
1589
  const alwaysEnabled = config.always_enabled_capabilities ?? [];
922
- 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])];
923
1608
  }
924
1609
  async function loadProfileConfig(profileName) {
925
1610
  const config = await loadConfig();
@@ -966,6 +1651,93 @@ async function disableCapability(capabilityId) {
966
1651
  await writeConfig(config);
967
1652
  }
968
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
+
969
1741
  // src/capability/registry.ts
970
1742
  async function buildCapabilityRegistry() {
971
1743
  const env = await loadEnvironment();
@@ -984,21 +1756,32 @@ async function buildCapabilityRegistry() {
984
1756
  console.warn(` ${errorMessage}`);
985
1757
  }
986
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
+ };
987
1768
  return {
988
1769
  capabilities,
989
1770
  getCapability: (id) => capabilities.get(id),
990
1771
  getAllCapabilities: () => [...capabilities.values()],
991
1772
  getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
992
1773
  getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
993
- getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs)
1774
+ getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
1775
+ getAllCapabilityHooks,
1776
+ getMergedHooks: () => mergeHooksConfigs(getAllCapabilityHooks())
994
1777
  };
995
1778
  }
996
1779
  // src/capability/sources.ts
997
- import { existsSync as existsSync10 } from "node:fs";
1780
+ import { existsSync as existsSync12 } from "node:fs";
998
1781
  import { spawn } from "node:child_process";
999
- import { cp, mkdir, readdir, readFile as readFile10, rm, stat, writeFile as writeFile4 } from "node:fs/promises";
1000
- import { join as join7 } from "node:path";
1001
- 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";
1002
1785
  var OMNI_LOCAL = ".omni";
1003
1786
  var SKILL_DIRS = ["skills", "skill"];
1004
1787
  var AGENT_DIRS = ["agents", "agent", "subagents", "subagent"];
@@ -1009,7 +1792,7 @@ var SKILL_FILES = ["SKILL.md", "skill.md", "Skill.md"];
1009
1792
  var AGENT_FILES = ["AGENT.md", "agent.md", "Agent.md", "SUBAGENT.md", "subagent.md"];
1010
1793
  var COMMAND_FILES = ["COMMAND.md", "command.md", "Command.md"];
1011
1794
  async function spawnCapture(command, args, options) {
1012
- return await new Promise((resolve, reject) => {
1795
+ return await new Promise((resolve2, reject) => {
1013
1796
  const child = spawn(command, args, {
1014
1797
  cwd: options?.cwd,
1015
1798
  stdio: ["ignore", "pipe", "pipe"]
@@ -1026,7 +1809,7 @@ async function spawnCapture(command, args, options) {
1026
1809
  });
1027
1810
  child.on("error", (error) => reject(error));
1028
1811
  child.on("close", (exitCode) => {
1029
- resolve({ exitCode: exitCode ?? 0, stdout, stderr });
1812
+ resolve2({ exitCode: exitCode ?? 0, stdout, stderr });
1030
1813
  });
1031
1814
  });
1032
1815
  }
@@ -1055,19 +1838,19 @@ function sourceToGitUrl(source) {
1055
1838
  return source;
1056
1839
  }
1057
1840
  function getSourceCapabilityPath(id) {
1058
- return join7(OMNI_LOCAL, "capabilities", id);
1841
+ return join8(OMNI_LOCAL, "capabilities", id);
1059
1842
  }
1060
1843
  function getLockFilePath() {
1061
1844
  return "omni.lock.toml";
1062
1845
  }
1063
1846
  async function loadLockFile() {
1064
1847
  const lockPath = getLockFilePath();
1065
- if (!existsSync10(lockPath)) {
1848
+ if (!existsSync12(lockPath)) {
1066
1849
  return { capabilities: {} };
1067
1850
  }
1068
1851
  try {
1069
1852
  const content = await readFile10(lockPath, "utf-8");
1070
- const parsed = parseToml(content);
1853
+ const parsed = parseToml2(content);
1071
1854
  const capabilities = parsed["capabilities"];
1072
1855
  return {
1073
1856
  capabilities: capabilities || {}
@@ -1096,7 +1879,7 @@ function stringifyLockFile(lockFile) {
1096
1879
  }
1097
1880
  async function saveLockFile(lockFile) {
1098
1881
  const lockPath = getLockFilePath();
1099
- await mkdir(join7(OMNI_LOCAL, "capabilities"), { recursive: true });
1882
+ await mkdir(join8(OMNI_LOCAL, "capabilities"), { recursive: true });
1100
1883
  const header = `# Auto-generated by OmniDev - DO NOT EDIT
1101
1884
  # Records installed capability versions for reproducibility
1102
1885
  # Last updated: ${new Date().toISOString()}
@@ -1118,7 +1901,7 @@ function shortCommit(commit) {
1118
1901
  return commit.substring(0, 7);
1119
1902
  }
1120
1903
  async function cloneRepo(gitUrl, targetPath, ref) {
1121
- await mkdir(join7(targetPath, ".."), { recursive: true });
1904
+ await mkdir(join8(targetPath, ".."), { recursive: true });
1122
1905
  const args = ["clone", "--depth", "1"];
1123
1906
  if (ref) {
1124
1907
  args.push("--branch", ref);
@@ -1155,16 +1938,16 @@ async function fetchRepo(repoPath, ref) {
1155
1938
  return true;
1156
1939
  }
1157
1940
  function hasCapabilityToml(dirPath) {
1158
- return existsSync10(join7(dirPath, "capability.toml"));
1941
+ return existsSync12(join8(dirPath, "capability.toml"));
1159
1942
  }
1160
1943
  async function shouldWrapDirectory(dirPath) {
1161
- if (existsSync10(join7(dirPath, ".claude-plugin", "plugin.json"))) {
1944
+ if (existsSync12(join8(dirPath, ".claude-plugin", "plugin.json"))) {
1162
1945
  return true;
1163
1946
  }
1164
1947
  const allDirs = [...SKILL_DIRS, ...AGENT_DIRS, ...COMMAND_DIRS, ...RULE_DIRS, ...DOC_DIRS];
1165
1948
  for (const dirName of allDirs) {
1166
- const checkPath = join7(dirPath, dirName);
1167
- if (existsSync10(checkPath)) {
1949
+ const checkPath = join8(dirPath, dirName);
1950
+ if (existsSync12(checkPath)) {
1168
1951
  const stats = await stat(checkPath);
1169
1952
  if (stats.isDirectory()) {
1170
1953
  return true;
@@ -1175,8 +1958,8 @@ async function shouldWrapDirectory(dirPath) {
1175
1958
  }
1176
1959
  async function findMatchingDirs(basePath, names) {
1177
1960
  for (const name of names) {
1178
- const dirPath = join7(basePath, name);
1179
- if (existsSync10(dirPath)) {
1961
+ const dirPath = join8(basePath, name);
1962
+ if (existsSync12(dirPath)) {
1180
1963
  const stats = await stat(dirPath);
1181
1964
  if (stats.isDirectory()) {
1182
1965
  return dirPath;
@@ -1187,15 +1970,15 @@ async function findMatchingDirs(basePath, names) {
1187
1970
  }
1188
1971
  async function findContentItems(dirPath, filePatterns) {
1189
1972
  const items = [];
1190
- if (!existsSync10(dirPath)) {
1973
+ if (!existsSync12(dirPath)) {
1191
1974
  return items;
1192
1975
  }
1193
1976
  const entries = (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1194
1977
  for (const entry of entries) {
1195
- const entryPath = join7(dirPath, entry.name);
1978
+ const entryPath = join8(dirPath, entry.name);
1196
1979
  if (entry.isDirectory()) {
1197
1980
  for (const pattern of filePatterns) {
1198
- if (existsSync10(join7(entryPath, pattern))) {
1981
+ if (existsSync12(join8(entryPath, pattern))) {
1199
1982
  items.push({
1200
1983
  name: entry.name,
1201
1984
  path: entryPath,
@@ -1216,8 +1999,8 @@ async function findContentItems(dirPath, filePatterns) {
1216
1999
  return items;
1217
2000
  }
1218
2001
  async function parsePluginJson(dirPath) {
1219
- const pluginJsonPath = join7(dirPath, ".claude-plugin", "plugin.json");
1220
- if (!existsSync10(pluginJsonPath)) {
2002
+ const pluginJsonPath = join8(dirPath, ".claude-plugin", "plugin.json");
2003
+ if (!existsSync12(pluginJsonPath)) {
1221
2004
  return null;
1222
2005
  }
1223
2006
  try {
@@ -1241,8 +2024,8 @@ async function parsePluginJson(dirPath) {
1241
2024
  }
1242
2025
  }
1243
2026
  async function readReadmeDescription(dirPath) {
1244
- const readmePath = join7(dirPath, "README.md");
1245
- if (!existsSync10(readmePath)) {
2027
+ const readmePath = join8(dirPath, "README.md");
2028
+ if (!existsSync12(readmePath)) {
1246
2029
  return null;
1247
2030
  }
1248
2031
  try {
@@ -1274,6 +2057,29 @@ async function readReadmeDescription(dirPath) {
1274
2057
  return null;
1275
2058
  }
1276
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
+ }
1277
2083
  async function discoverContent(repoPath) {
1278
2084
  const result = {
1279
2085
  skills: [],
@@ -1351,7 +2157,7 @@ repository = "${repoUrl}"
1351
2157
  wrapped = true
1352
2158
  commit = "${commit}"
1353
2159
  `;
1354
- await writeFile4(join7(repoPath, "capability.toml"), tomlContent, "utf-8");
2160
+ await writeFile4(join8(repoPath, "capability.toml"), tomlContent, "utf-8");
1355
2161
  }
1356
2162
  async function fetchGitCapabilitySource(id, config, options) {
1357
2163
  const gitUrl = sourceToGitUrl(config.source);
@@ -1360,8 +2166,8 @@ async function fetchGitCapabilitySource(id, config, options) {
1360
2166
  let commit;
1361
2167
  let repoPath;
1362
2168
  if (config.path) {
1363
- const tempPath = join7(OMNI_LOCAL, "_temp", `${id}-repo`);
1364
- if (existsSync10(join7(tempPath, ".git"))) {
2169
+ const tempPath = join8(OMNI_LOCAL, "_temp", `${id}-repo`);
2170
+ if (existsSync12(join8(tempPath, ".git"))) {
1365
2171
  if (!options?.silent) {
1366
2172
  console.log(` Checking ${id}...`);
1367
2173
  }
@@ -1371,23 +2177,23 @@ async function fetchGitCapabilitySource(id, config, options) {
1371
2177
  if (!options?.silent) {
1372
2178
  console.log(` Cloning ${id} from ${config.source}...`);
1373
2179
  }
1374
- await mkdir(join7(tempPath, ".."), { recursive: true });
2180
+ await mkdir(join8(tempPath, ".."), { recursive: true });
1375
2181
  await cloneRepo(gitUrl, tempPath, config.ref);
1376
2182
  commit = await getRepoCommit(tempPath);
1377
2183
  updated = true;
1378
2184
  }
1379
- const sourcePath = join7(tempPath, config.path);
1380
- if (!existsSync10(sourcePath)) {
2185
+ const sourcePath = join8(tempPath, config.path);
2186
+ if (!existsSync12(sourcePath)) {
1381
2187
  throw new Error(`Path not found in repository: ${config.path}`);
1382
2188
  }
1383
- if (existsSync10(targetPath)) {
2189
+ if (existsSync12(targetPath)) {
1384
2190
  await rm(targetPath, { recursive: true });
1385
2191
  }
1386
- await mkdir(join7(targetPath, ".."), { recursive: true });
2192
+ await mkdir(join8(targetPath, ".."), { recursive: true });
1387
2193
  await cp(sourcePath, targetPath, { recursive: true });
1388
2194
  repoPath = targetPath;
1389
2195
  } else {
1390
- if (existsSync10(join7(targetPath, ".git"))) {
2196
+ if (existsSync12(join8(targetPath, ".git"))) {
1391
2197
  if (!options?.silent) {
1392
2198
  console.log(` Checking ${id}...`);
1393
2199
  }
@@ -1408,6 +2214,7 @@ async function fetchGitCapabilitySource(id, config, options) {
1408
2214
  needsWrap = await shouldWrapDirectory(repoPath);
1409
2215
  }
1410
2216
  if (needsWrap) {
2217
+ await normalizeFolderNames(repoPath);
1411
2218
  const content = await discoverContent(repoPath);
1412
2219
  await generateCapabilityToml(id, repoPath, config.source, commit, content);
1413
2220
  if (!options?.silent) {
@@ -1424,8 +2231,8 @@ async function fetchGitCapabilitySource(id, config, options) {
1424
2231
  }
1425
2232
  }
1426
2233
  let version = shortCommit(commit);
1427
- const pkgJsonPath = join7(repoPath, "package.json");
1428
- if (existsSync10(pkgJsonPath)) {
2234
+ const pkgJsonPath = join8(repoPath, "package.json");
2235
+ if (existsSync12(pkgJsonPath)) {
1429
2236
  try {
1430
2237
  const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
1431
2238
  if (pkgJson.version) {
@@ -1510,17 +2317,17 @@ generated_from_omni_toml = true
1510
2317
  }
1511
2318
  async function generateMcpCapabilityToml(id, mcpConfig, targetPath) {
1512
2319
  const tomlContent = generateMcpCapabilityTomlContent(id, mcpConfig);
1513
- await writeFile4(join7(targetPath, "capability.toml"), tomlContent, "utf-8");
2320
+ await writeFile4(join8(targetPath, "capability.toml"), tomlContent, "utf-8");
1514
2321
  }
1515
2322
  async function isGeneratedMcpCapability(capabilityDir) {
1516
- const tomlPath = join7(capabilityDir, "capability.toml");
1517
- if (!existsSync10(tomlPath)) {
2323
+ const tomlPath = join8(capabilityDir, "capability.toml");
2324
+ if (!existsSync12(tomlPath)) {
1518
2325
  console.warn("no capability.toml found in", capabilityDir);
1519
2326
  return false;
1520
2327
  }
1521
2328
  try {
1522
2329
  const content = await readFile10(tomlPath, "utf-8");
1523
- const parsed = parseToml(content);
2330
+ const parsed = parseToml2(content);
1524
2331
  const capability = parsed["capability"];
1525
2332
  const metadata = capability?.["metadata"];
1526
2333
  return metadata?.["generated_from_omni_toml"] === true;
@@ -1529,14 +2336,14 @@ async function isGeneratedMcpCapability(capabilityDir) {
1529
2336
  }
1530
2337
  }
1531
2338
  async function cleanupStaleMcpCapabilities(currentMcpIds) {
1532
- const capabilitiesDir = join7(OMNI_LOCAL, "capabilities");
1533
- if (!existsSync10(capabilitiesDir)) {
2339
+ const capabilitiesDir = join8(OMNI_LOCAL, "capabilities");
2340
+ if (!existsSync12(capabilitiesDir)) {
1534
2341
  return;
1535
2342
  }
1536
2343
  const entries = await readdir(capabilitiesDir, { withFileTypes: true });
1537
2344
  for (const entry of entries) {
1538
2345
  if (entry.isDirectory()) {
1539
- const capDir = join7(capabilitiesDir, entry.name);
2346
+ const capDir = join8(capabilitiesDir, entry.name);
1540
2347
  const isGenerated = await isGeneratedMcpCapability(capDir);
1541
2348
  if (isGenerated && !currentMcpIds.has(entry.name)) {
1542
2349
  await rm(capDir, { recursive: true });
@@ -1549,10 +2356,10 @@ async function generateMcpCapabilities(config) {
1549
2356
  await cleanupStaleMcpCapabilities(new Set);
1550
2357
  return;
1551
2358
  }
1552
- const mcpCapabilitiesDir = join7(OMNI_LOCAL, "capabilities");
2359
+ const mcpCapabilitiesDir = join8(OMNI_LOCAL, "capabilities");
1553
2360
  const currentMcpIds = new Set;
1554
2361
  for (const [id, mcpConfig] of Object.entries(config.mcps)) {
1555
- const targetPath = join7(mcpCapabilitiesDir, id);
2362
+ const targetPath = join8(mcpCapabilitiesDir, id);
1556
2363
  currentMcpIds.add(id);
1557
2364
  await mkdir(targetPath, { recursive: true });
1558
2365
  await generateMcpCapabilityToml(id, mcpConfig, targetPath);
@@ -1626,7 +2433,7 @@ async function checkForUpdates(config) {
1626
2433
  const targetPath = getSourceCapabilityPath(id);
1627
2434
  const existing = lockFile.capabilities[id];
1628
2435
  const gitConfig = sourceConfig;
1629
- if (!existsSync10(join7(targetPath, ".git"))) {
2436
+ if (!existsSync12(join8(targetPath, ".git"))) {
1630
2437
  updates.push({
1631
2438
  id,
1632
2439
  source: gitConfig.source,
@@ -1662,12 +2469,12 @@ async function checkForUpdates(config) {
1662
2469
  return updates;
1663
2470
  }
1664
2471
  // src/config/provider.ts
1665
- import { existsSync as existsSync11 } from "node:fs";
2472
+ import { existsSync as existsSync13 } from "node:fs";
1666
2473
  import { readFile as readFile11, writeFile as writeFile5 } from "node:fs/promises";
1667
2474
  import { parse as parse2 } from "smol-toml";
1668
2475
  var PROVIDER_CONFIG_PATH = ".omni/provider.toml";
1669
2476
  async function loadProviderConfig() {
1670
- if (!existsSync11(PROVIDER_CONFIG_PATH)) {
2477
+ if (!existsSync13(PROVIDER_CONFIG_PATH)) {
1671
2478
  return { provider: "claude" };
1672
2479
  }
1673
2480
  const content = await readFile11(PROVIDER_CONFIG_PATH, "utf-8");
@@ -1711,16 +2518,222 @@ function parseProviderFlag(flag) {
1711
2518
  }
1712
2519
  throw new Error(`Invalid provider: ${flag}. Must be 'claude', 'codex', or 'both'.`);
1713
2520
  }
1714
- // src/mcp-json/manager.ts
1715
- import { existsSync as existsSync12 } from "node:fs";
2521
+ // src/config/toml-patcher.ts
2522
+ import { existsSync as existsSync14 } from "node:fs";
1716
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";
1717
2730
  var MCP_JSON_PATH = ".mcp.json";
1718
2731
  async function readMcpJson() {
1719
- if (!existsSync12(MCP_JSON_PATH)) {
2732
+ if (!existsSync15(MCP_JSON_PATH)) {
1720
2733
  return { mcpServers: {} };
1721
2734
  }
1722
2735
  try {
1723
- const content = await readFile12(MCP_JSON_PATH, "utf-8");
2736
+ const content = await readFile13(MCP_JSON_PATH, "utf-8");
1724
2737
  const parsed = JSON.parse(content);
1725
2738
  return {
1726
2739
  mcpServers: parsed.mcpServers || {}
@@ -1729,8 +2742,8 @@ async function readMcpJson() {
1729
2742
  return { mcpServers: {} };
1730
2743
  }
1731
2744
  }
1732
- async function writeMcpJson(config) {
1733
- 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)}
1734
2747
  `, "utf-8");
1735
2748
  }
1736
2749
  function buildMcpServerConfig(mcp) {
@@ -1739,41 +2752,41 @@ function buildMcpServerConfig(mcp) {
1739
2752
  if (!mcp.url) {
1740
2753
  throw new Error("HTTP transport requires a URL");
1741
2754
  }
1742
- const config2 = {
2755
+ const config3 = {
1743
2756
  type: "http",
1744
2757
  url: mcp.url
1745
2758
  };
1746
2759
  if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1747
- config2.headers = mcp.headers;
2760
+ config3.headers = mcp.headers;
1748
2761
  }
1749
- return config2;
2762
+ return config3;
1750
2763
  }
1751
2764
  if (transport === "sse") {
1752
2765
  if (!mcp.url) {
1753
2766
  throw new Error("SSE transport requires a URL");
1754
2767
  }
1755
- const config2 = {
2768
+ const config3 = {
1756
2769
  type: "sse",
1757
2770
  url: mcp.url
1758
2771
  };
1759
2772
  if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1760
- config2.headers = mcp.headers;
2773
+ config3.headers = mcp.headers;
1761
2774
  }
1762
- return config2;
2775
+ return config3;
1763
2776
  }
1764
2777
  if (!mcp.command) {
1765
2778
  throw new Error("stdio transport requires a command");
1766
2779
  }
1767
- const config = {
2780
+ const config2 = {
1768
2781
  command: mcp.command
1769
2782
  };
1770
2783
  if (mcp.args) {
1771
- config.args = mcp.args;
2784
+ config2.args = mcp.args;
1772
2785
  }
1773
2786
  if (mcp.env) {
1774
- config.env = mcp.env;
2787
+ config2.env = mcp.env;
1775
2788
  }
1776
- return config;
2789
+ return config2;
1777
2790
  }
1778
2791
  async function syncMcpJson(capabilities2, previousManifest, options = {}) {
1779
2792
  const mcpJson = await readMcpJson();
@@ -1799,24 +2812,24 @@ async function syncMcpJson(capabilities2, previousManifest, options = {}) {
1799
2812
  }
1800
2813
  }
1801
2814
  // src/state/manifest.ts
1802
- import { existsSync as existsSync13, mkdirSync as mkdirSync2, rmSync } from "node:fs";
1803
- 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";
1804
2817
  var MANIFEST_PATH = ".omni/state/manifest.json";
1805
2818
  var CURRENT_VERSION = 1;
1806
2819
  async function loadManifest() {
1807
- if (!existsSync13(MANIFEST_PATH)) {
2820
+ if (!existsSync16(MANIFEST_PATH)) {
1808
2821
  return {
1809
2822
  version: CURRENT_VERSION,
1810
2823
  syncedAt: new Date().toISOString(),
1811
2824
  capabilities: {}
1812
2825
  };
1813
2826
  }
1814
- const content = await readFile13(MANIFEST_PATH, "utf-8");
2827
+ const content = await readFile14(MANIFEST_PATH, "utf-8");
1815
2828
  return JSON.parse(content);
1816
2829
  }
1817
2830
  async function saveManifest(manifest) {
1818
2831
  mkdirSync2(".omni/state", { recursive: true });
1819
- await writeFile7(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
2832
+ await writeFile8(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
1820
2833
  `, "utf-8");
1821
2834
  }
1822
2835
  function buildManifestFromCapabilities(capabilities2) {
@@ -1851,14 +2864,14 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
1851
2864
  }
1852
2865
  for (const skillName of resources.skills) {
1853
2866
  const skillDir = `.claude/skills/${skillName}`;
1854
- if (existsSync13(skillDir)) {
2867
+ if (existsSync16(skillDir)) {
1855
2868
  rmSync(skillDir, { recursive: true });
1856
2869
  result.deletedSkills.push(skillName);
1857
2870
  }
1858
2871
  }
1859
2872
  for (const ruleName of resources.rules) {
1860
2873
  const rulePath = `.cursor/rules/omnidev-${ruleName}.mdc`;
1861
- if (existsSync13(rulePath)) {
2874
+ if (existsSync16(rulePath)) {
1862
2875
  rmSync(rulePath);
1863
2876
  result.deletedRules.push(ruleName);
1864
2877
  }
@@ -1867,17 +2880,17 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
1867
2880
  return result;
1868
2881
  }
1869
2882
  // src/state/providers.ts
1870
- import { existsSync as existsSync14, mkdirSync as mkdirSync3 } from "node:fs";
1871
- 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";
1872
2885
  var STATE_DIR2 = ".omni/state";
1873
2886
  var PROVIDERS_PATH = `${STATE_DIR2}/providers.json`;
1874
2887
  var DEFAULT_PROVIDERS = ["claude-code"];
1875
2888
  async function readEnabledProviders() {
1876
- if (!existsSync14(PROVIDERS_PATH)) {
2889
+ if (!existsSync17(PROVIDERS_PATH)) {
1877
2890
  return DEFAULT_PROVIDERS;
1878
2891
  }
1879
2892
  try {
1880
- const content = await readFile14(PROVIDERS_PATH, "utf-8");
2893
+ const content = await readFile15(PROVIDERS_PATH, "utf-8");
1881
2894
  const state = JSON.parse(content);
1882
2895
  return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
1883
2896
  } catch {
@@ -1887,7 +2900,7 @@ async function readEnabledProviders() {
1887
2900
  async function writeEnabledProviders(providers) {
1888
2901
  mkdirSync3(STATE_DIR2, { recursive: true });
1889
2902
  const state = { enabled: providers };
1890
- await writeFile8(PROVIDERS_PATH, `${JSON.stringify(state, null, 2)}
2903
+ await writeFile9(PROVIDERS_PATH, `${JSON.stringify(state, null, 2)}
1891
2904
  `, "utf-8");
1892
2905
  }
1893
2906
  async function enableProvider(providerId) {
@@ -1909,18 +2922,18 @@ async function isProviderEnabled(providerId) {
1909
2922
  import { spawn as spawn2 } from "node:child_process";
1910
2923
  import { mkdirSync as mkdirSync4 } from "node:fs";
1911
2924
  async function installCapabilityDependencies(silent) {
1912
- const { existsSync: existsSync15, readdirSync: readdirSync7 } = await import("node:fs");
1913
- 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");
1914
2927
  const capabilitiesDir = ".omni/capabilities";
1915
- if (!existsSync15(capabilitiesDir)) {
2928
+ if (!existsSync18(capabilitiesDir)) {
1916
2929
  return;
1917
2930
  }
1918
2931
  const entries = readdirSync7(capabilitiesDir, { withFileTypes: true });
1919
2932
  async function commandExists(cmd) {
1920
- return await new Promise((resolve) => {
2933
+ return await new Promise((resolve2) => {
1921
2934
  const proc = spawn2(cmd, ["--version"], { stdio: "ignore" });
1922
- proc.on("error", () => resolve(false));
1923
- proc.on("close", (code) => resolve(code === 0));
2935
+ proc.on("error", () => resolve2(false));
2936
+ proc.on("close", (code) => resolve2(code === 0));
1924
2937
  });
1925
2938
  }
1926
2939
  const hasBun = await commandExists("bun");
@@ -1932,16 +2945,16 @@ async function installCapabilityDependencies(silent) {
1932
2945
  if (!entry.isDirectory()) {
1933
2946
  continue;
1934
2947
  }
1935
- const capabilityPath = join8(capabilitiesDir, entry.name);
1936
- const packageJsonPath = join8(capabilityPath, "package.json");
1937
- if (!existsSync15(packageJsonPath)) {
2948
+ const capabilityPath = join9(capabilitiesDir, entry.name);
2949
+ const packageJsonPath = join9(capabilityPath, "package.json");
2950
+ if (!existsSync18(packageJsonPath)) {
1938
2951
  continue;
1939
2952
  }
1940
2953
  if (!silent) {
1941
2954
  console.log(`Installing dependencies for ${capabilityPath}...`);
1942
2955
  }
1943
- await new Promise((resolve, reject) => {
1944
- 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"));
1945
2958
  const cmd = hasBun ? "bun" : "npm";
1946
2959
  const args = hasBun ? ["install"] : useNpmCi ? ["ci"] : ["install"];
1947
2960
  const proc = spawn2(cmd, args, {
@@ -1950,7 +2963,7 @@ async function installCapabilityDependencies(silent) {
1950
2963
  });
1951
2964
  proc.on("close", (code) => {
1952
2965
  if (code === 0) {
1953
- resolve();
2966
+ resolve2();
1954
2967
  } else {
1955
2968
  reject(new Error(`Failed to install dependencies for ${capabilityPath}`));
1956
2969
  }
@@ -1963,8 +2976,8 @@ async function installCapabilityDependencies(silent) {
1963
2976
  }
1964
2977
  async function buildSyncBundle(options) {
1965
2978
  const silent = options?.silent ?? false;
1966
- const config = await loadConfig();
1967
- await fetchAllCapabilitySources(config, { silent });
2979
+ const config2 = await loadConfig();
2980
+ await fetchAllCapabilitySources(config2, { silent });
1968
2981
  await installCapabilityDependencies(silent);
1969
2982
  const registry = await buildCapabilityRegistry();
1970
2983
  const capabilities2 = registry.getAllCapabilities();
@@ -1973,6 +2986,7 @@ async function buildSyncBundle(options) {
1973
2986
  const docs = registry.getAllDocs();
1974
2987
  const commands = capabilities2.flatMap((c) => c.commands);
1975
2988
  const subagents = capabilities2.flatMap((c) => c.subagents);
2989
+ const mergedHooks = registry.getMergedHooks();
1976
2990
  const instructionsContent = generateInstructionsContent(rules, docs);
1977
2991
  const bundle = {
1978
2992
  capabilities: capabilities2,
@@ -1984,6 +2998,9 @@ async function buildSyncBundle(options) {
1984
2998
  instructionsPath: ".omni/instructions.md",
1985
2999
  instructionsContent
1986
3000
  };
3001
+ if (hasAnyHooks(mergedHooks)) {
3002
+ bundle.hooks = mergedHooks;
3003
+ }
1987
3004
  return { bundle };
1988
3005
  }
1989
3006
  async function syncAgentConfiguration(options) {
@@ -2032,10 +3049,10 @@ async function syncAgentConfiguration(options) {
2032
3049
  const newManifest = buildManifestFromCapabilities(capabilities2);
2033
3050
  await saveManifest(newManifest);
2034
3051
  if (adapters.length > 0) {
2035
- const config = await loadConfig();
3052
+ const config2 = await loadConfig();
2036
3053
  const ctx = {
2037
3054
  projectRoot: process.cwd(),
2038
- config
3055
+ config: config2
2039
3056
  };
2040
3057
  for (const adapter of adapters) {
2041
3058
  try {
@@ -2157,12 +3174,33 @@ No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable
2157
3174
  <!-- END OMNIDEV GENERATED CONTENT -->
2158
3175
  `;
2159
3176
  }
3177
+ // src/templates/omni.ts
3178
+ function generateOmniMdTemplate() {
3179
+ return `# Project Instructions
3180
+
3181
+ <!-- This file is your project's instruction manifest for AI agents. -->
3182
+ <!-- It will be combined with capability-generated content during sync. -->
3183
+
3184
+ ## Project Description
3185
+
3186
+ <!-- TODO: Add 2-3 sentences describing your project -->
3187
+ [Describe what this project does and its main purpose]
3188
+
3189
+ ## Conventions
3190
+
3191
+ <!-- Add your project conventions, coding standards, and guidelines here -->
3192
+
3193
+ ## Architecture
3194
+
3195
+ <!-- Describe your project's architecture and key components -->
3196
+ `;
3197
+ }
2160
3198
  // src/types/index.ts
2161
- function getActiveProviders(config) {
2162
- if (config.providers)
2163
- return config.providers;
2164
- if (config.provider)
2165
- return [config.provider];
3199
+ function getActiveProviders(config2) {
3200
+ if (config2.providers)
3201
+ return config2.providers;
3202
+ if (config2.provider)
3203
+ return [config2.provider];
2166
3204
  return ["claude"];
2167
3205
  }
2168
3206
  // src/debug.ts
@@ -2193,7 +3231,12 @@ export {
2193
3231
  writeConfig,
2194
3232
  writeActiveProfileState,
2195
3233
  version,
3234
+ validateHooksConfig,
3235
+ validateHook,
2196
3236
  validateEnv,
3237
+ transformToOmnidev,
3238
+ transformToClaude,
3239
+ transformHooksConfig,
2197
3240
  syncMcpJson,
2198
3241
  syncAgentConfiguration,
2199
3242
  sourceToGitUrl,
@@ -2205,10 +3248,15 @@ export {
2205
3248
  readMcpJson,
2206
3249
  readEnabledProviders,
2207
3250
  readActiveProfileState,
3251
+ patchAddToProfile,
3252
+ patchAddMcp,
3253
+ patchAddCapabilitySource,
2208
3254
  parseSourceConfig,
2209
3255
  parseProviderFlag,
2210
3256
  parseOmniConfig,
2211
3257
  parseCapabilityConfig,
3258
+ mergeHooksConfigs,
3259
+ mergeAndDeduplicateHooks,
2212
3260
  loadSubagents,
2213
3261
  loadSkills,
2214
3262
  loadRules,
@@ -2216,25 +3264,41 @@ export {
2216
3264
  loadProfileConfig,
2217
3265
  loadManifest,
2218
3266
  loadLockFile,
3267
+ loadHooksFromCapability,
2219
3268
  loadEnvironment,
2220
3269
  loadDocs,
2221
3270
  loadConfig,
2222
3271
  loadCommands,
3272
+ loadCapabilityHooks,
2223
3273
  loadCapabilityConfig,
2224
3274
  loadCapability,
2225
3275
  loadBaseConfig,
3276
+ isValidMatcherPattern,
2226
3277
  isSecretEnvVar,
2227
3278
  isProviderEnabled,
3279
+ isPromptHookEvent,
3280
+ isMatcherEvent,
3281
+ isHookType,
3282
+ isHookPrompt,
3283
+ isHookEvent,
3284
+ isHookCommand,
2228
3285
  installCapabilityDependencies,
3286
+ hasHooks,
3287
+ hasAnyHooks,
2229
3288
  getVersion,
2230
3289
  getSourceCapabilityPath,
2231
3290
  getLockFilePath,
3291
+ getHooksDirectory,
3292
+ getHooksConfigPath,
3293
+ getEventsWithHooks,
2232
3294
  getEnabledCapabilities,
2233
3295
  getActiveProviders,
2234
3296
  getActiveProfile,
3297
+ generateOmniMdTemplate,
2235
3298
  generateInstructionsTemplate,
2236
3299
  generateClaudeTemplate,
2237
3300
  generateAgentsTemplate,
3301
+ findDuplicateCommands,
2238
3302
  fetchCapabilitySource,
2239
3303
  fetchAllCapabilitySources,
2240
3304
  enableProvider,
@@ -2243,6 +3307,11 @@ export {
2243
3307
  disableProvider,
2244
3308
  disableCapability,
2245
3309
  debug,
3310
+ createEmptyValidationResult,
3311
+ createEmptyHooksConfig,
3312
+ countHooks,
3313
+ containsOmnidevVariables,
3314
+ containsClaudeVariables,
2246
3315
  clearActiveProfileState,
2247
3316
  cleanupStaleResources,
2248
3317
  checkForUpdates,
@@ -2250,5 +3319,18 @@ export {
2250
3319
  buildRouteMap,
2251
3320
  buildManifestFromCapabilities,
2252
3321
  buildCommand,
2253
- 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
2254
3336
  };