@omnidev-ai/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
212
- import { readFile as readFile4, writeFile } from "node:fs/promises";
213
- import { basename as basename2, join as join3 } from "node:path";
857
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "node:fs";
858
+ import { readFile as readFile4 } from "node:fs/promises";
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"),
@@ -231,96 +877,22 @@ async function loadRules(capabilityPath, capabilityId) {
231
877
  }
232
878
  return rules;
233
879
  }
234
- async function writeRules(rules, docs = []) {
235
- const instructionsPath = ".omni/instructions.md";
236
- const rulesContent = generateRulesContent(rules, docs);
237
- let content;
238
- if (existsSync4(instructionsPath)) {
239
- content = await readFile4(instructionsPath, "utf-8");
240
- } else {
241
- content = `# OmniDev Instructions
242
-
243
- ## Project Description
244
- <!-- TODO: Add 2-3 sentences describing your project -->
245
- [Describe what this project does and its main purpose]
246
-
247
- <!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->
248
- <!-- END OMNIDEV GENERATED CONTENT -->
249
- `;
250
- }
251
- const beginMarker = "<!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->";
252
- const endMarker = "<!-- END OMNIDEV GENERATED CONTENT -->";
253
- const beginIndex = content.indexOf(beginMarker);
254
- const endIndex = content.indexOf(endMarker);
255
- if (beginIndex === -1 || endIndex === -1) {
256
- content += `
257
-
258
- ${beginMarker}
259
- ${rulesContent}
260
- ${endMarker}
261
- `;
262
- } else {
263
- content = content.substring(0, beginIndex + beginMarker.length) + `
264
- ` + rulesContent + `
265
- ` + content.substring(endIndex);
266
- }
267
- await writeFile(instructionsPath, content, "utf-8");
268
- }
269
- function generateRulesContent(rules, docs = []) {
270
- if (rules.length === 0 && docs.length === 0) {
271
- return `<!-- This section is automatically updated when capabilities change -->
272
-
273
- ## Capabilities
274
-
275
- No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable capabilities.`;
276
- }
277
- let content = `<!-- This section is automatically updated when capabilities change -->
278
-
279
- ## Capabilities
280
-
281
- `;
282
- if (docs.length > 0) {
283
- content += `### Documentation
284
-
285
- `;
286
- for (const doc of docs) {
287
- content += `#### ${doc.name} (from ${doc.capabilityId})
288
-
289
- ${doc.content}
290
-
291
- `;
292
- }
293
- }
294
- if (rules.length > 0) {
295
- content += `### Rules
296
-
297
- `;
298
- for (const rule of rules) {
299
- content += `#### ${rule.name} (from ${rule.capabilityId})
300
-
301
- ${rule.content}
302
-
303
- `;
304
- }
305
- }
306
- return content.trim();
307
- }
308
880
 
309
881
  // src/capability/skills.ts
310
- import { existsSync as existsSync5, readdirSync as readdirSync4 } from "node:fs";
882
+ import { existsSync as existsSync7, readdirSync as readdirSync4 } from "node:fs";
311
883
  import { readFile as readFile5 } from "node:fs/promises";
312
- import { join as join4 } from "node:path";
884
+ import { join as join5 } from "node:path";
313
885
  async function loadSkills(capabilityPath, capabilityId) {
314
- const skillsDir = join4(capabilityPath, "skills");
315
- if (!existsSync5(skillsDir)) {
886
+ const skillsDir = join5(capabilityPath, "skills");
887
+ if (!existsSync7(skillsDir)) {
316
888
  return [];
317
889
  }
318
890
  const skills = [];
319
891
  const entries = readdirSync4(skillsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
320
892
  for (const entry of entries) {
321
893
  if (entry.isDirectory()) {
322
- const skillPath = join4(skillsDir, entry.name, "SKILL.md");
323
- if (existsSync5(skillPath)) {
894
+ const skillPath = join5(skillsDir, entry.name, "SKILL.md");
895
+ if (existsSync7(skillPath)) {
324
896
  const skill = await parseSkillFile(skillPath, capabilityId);
325
897
  skills.push(skill);
326
898
  }
@@ -348,20 +920,20 @@ async function parseSkillFile(filePath, capabilityId) {
348
920
  }
349
921
 
350
922
  // src/capability/subagents.ts
351
- import { existsSync as existsSync6, readdirSync as readdirSync5 } from "node:fs";
923
+ import { existsSync as existsSync8, readdirSync as readdirSync5 } from "node:fs";
352
924
  import { readFile as readFile6 } from "node:fs/promises";
353
- import { join as join5 } from "node:path";
925
+ import { join as join6 } from "node:path";
354
926
  async function loadSubagents(capabilityPath, capabilityId) {
355
- const subagentsDir = join5(capabilityPath, "subagents");
356
- if (!existsSync6(subagentsDir)) {
927
+ const subagentsDir = join6(capabilityPath, "subagents");
928
+ if (!existsSync8(subagentsDir)) {
357
929
  return [];
358
930
  }
359
931
  const subagents = [];
360
932
  const entries = readdirSync5(subagentsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
361
933
  for (const entry of entries) {
362
934
  if (entry.isDirectory()) {
363
- const subagentPath = join5(subagentsDir, entry.name, "SUBAGENT.md");
364
- if (existsSync6(subagentPath)) {
935
+ const subagentPath = join6(subagentsDir, entry.name, "SUBAGENT.md");
936
+ if (existsSync8(subagentPath)) {
365
937
  const subagent = await parseSubagentFile(subagentPath, capabilityId);
366
938
  subagents.push(subagent);
367
939
  }
@@ -414,13 +986,13 @@ function parseCommaSeparatedList(value) {
414
986
  var CAPABILITIES_DIR = ".omni/capabilities";
415
987
  async function discoverCapabilities() {
416
988
  const capabilities = [];
417
- if (existsSync7(CAPABILITIES_DIR)) {
989
+ if (existsSync9(CAPABILITIES_DIR)) {
418
990
  const entries = readdirSync6(CAPABILITIES_DIR, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
419
991
  for (const entry of entries) {
420
992
  if (entry.isDirectory()) {
421
- const entryPath = join6(CAPABILITIES_DIR, entry.name);
422
- const configPath = join6(entryPath, "capability.toml");
423
- if (existsSync7(configPath)) {
993
+ const entryPath = join7(CAPABILITIES_DIR, entry.name);
994
+ const configPath = join7(entryPath, "capability.toml");
995
+ if (existsSync9(configPath)) {
424
996
  capabilities.push(entryPath);
425
997
  }
426
998
  }
@@ -429,18 +1001,18 @@ async function discoverCapabilities() {
429
1001
  return capabilities;
430
1002
  }
431
1003
  async function loadCapabilityConfig(capabilityPath) {
432
- const configPath = join6(capabilityPath, "capability.toml");
1004
+ const configPath = join7(capabilityPath, "capability.toml");
433
1005
  const content = await readFile7(configPath, "utf-8");
434
1006
  const config = parseCapabilityConfig(content);
435
1007
  return config;
436
1008
  }
437
1009
  async function importCapabilityExports(capabilityPath) {
438
- const indexPath = join6(capabilityPath, "index.ts");
439
- if (!existsSync7(indexPath)) {
1010
+ const indexPath = join7(capabilityPath, "index.ts");
1011
+ if (!existsSync9(indexPath)) {
440
1012
  return {};
441
1013
  }
442
1014
  try {
443
- const absolutePath = join6(process.cwd(), indexPath);
1015
+ const absolutePath = join7(process.cwd(), indexPath);
444
1016
  const module = await import(absolutePath);
445
1017
  return module;
446
1018
  } catch (error) {
@@ -455,8 +1027,8 @@ If this is a project-specific capability, install dependencies or remove it from
455
1027
  }
456
1028
  }
457
1029
  async function loadTypeDefinitions(capabilityPath) {
458
- const typesPath = join6(capabilityPath, "types.d.ts");
459
- if (!existsSync7(typesPath)) {
1030
+ const typesPath = join7(capabilityPath, "types.d.ts");
1031
+ if (!existsSync9(typesPath)) {
460
1032
  return;
461
1033
  }
462
1034
  return readFile7(typesPath, "utf-8");
@@ -652,6 +1224,7 @@ async function loadCapability(capabilityPath, env) {
652
1224
  const typeDefinitionsFromExports = "typeDefinitions" in exports && typeof exportsAny.typeDefinitions === "string" ? exportsAny.typeDefinitions : undefined;
653
1225
  const typeDefinitions = typeDefinitionsFromExports !== undefined ? typeDefinitionsFromExports : await loadTypeDefinitions(capabilityPath);
654
1226
  const gitignore = "gitignore" in exports && Array.isArray(exportsAny.gitignore) ? exportsAny.gitignore : undefined;
1227
+ const hooks = loadCapabilityHooks(id, capabilityPath);
655
1228
  const result = {
656
1229
  id,
657
1230
  path: capabilityPath,
@@ -669,11 +1242,14 @@ async function loadCapability(capabilityPath, env) {
669
1242
  if (gitignore !== undefined) {
670
1243
  result.gitignore = gitignore;
671
1244
  }
1245
+ if (hooks !== null) {
1246
+ result.hooks = hooks;
1247
+ }
672
1248
  return result;
673
1249
  }
674
- // src/config/loader.ts
675
- import { existsSync as existsSync8 } from "node:fs";
676
- import { readFile as readFile8, writeFile as writeFile2 } from "node:fs/promises";
1250
+ // src/config/config.ts
1251
+ import { existsSync as existsSync10 } from "node:fs";
1252
+ import { readFile as readFile8, writeFile } from "node:fs/promises";
677
1253
  var CONFIG_PATH = "omni.toml";
678
1254
  var LOCAL_CONFIG = "omni.local.toml";
679
1255
  function mergeConfigs(base, override) {
@@ -692,7 +1268,7 @@ function mergeConfigs(base, override) {
692
1268
  return merged;
693
1269
  }
694
1270
  async function loadBaseConfig() {
695
- if (existsSync8(CONFIG_PATH)) {
1271
+ if (existsSync10(CONFIG_PATH)) {
696
1272
  const content = await readFile8(CONFIG_PATH, "utf-8");
697
1273
  return parseOmniConfig(content);
698
1274
  }
@@ -701,7 +1277,7 @@ async function loadBaseConfig() {
701
1277
  async function loadConfig() {
702
1278
  const baseConfig = await loadBaseConfig();
703
1279
  let localConfig = {};
704
- if (existsSync8(LOCAL_CONFIG)) {
1280
+ if (existsSync10(LOCAL_CONFIG)) {
705
1281
  const content = await readFile8(LOCAL_CONFIG, "utf-8");
706
1282
  localConfig = parseOmniConfig(content);
707
1283
  }
@@ -709,7 +1285,7 @@ async function loadConfig() {
709
1285
  }
710
1286
  async function writeConfig(config) {
711
1287
  const content = generateConfigToml(config);
712
- await writeFile2(CONFIG_PATH, content, "utf-8");
1288
+ await writeFile(CONFIG_PATH, content, "utf-8");
713
1289
  }
714
1290
  function generateConfigToml(config) {
715
1291
  const lines = [];
@@ -764,7 +1340,7 @@ function generateConfigToml(config) {
764
1340
  for (const [name, sourceConfig] of Object.entries(sources)) {
765
1341
  if (typeof sourceConfig === "string") {
766
1342
  lines.push(`${name} = "${sourceConfig}"`);
767
- } else if (sourceConfig.path) {
1343
+ } else if ("path" in sourceConfig && sourceConfig.path) {
768
1344
  lines.push(`${name} = { source = "${sourceConfig.source}", path = "${sourceConfig.path}" }`);
769
1345
  } else {
770
1346
  lines.push(`${name} = "${sourceConfig.source}"`);
@@ -784,6 +1360,28 @@ function generateConfigToml(config) {
784
1360
  }
785
1361
  lines.push("");
786
1362
  lines.push("# =============================================================================");
1363
+ lines.push("# Capability Groups");
1364
+ lines.push("# =============================================================================");
1365
+ lines.push("# Bundle multiple capabilities under a single name for cleaner profiles.");
1366
+ lines.push('# Reference groups in profiles with the "group:" prefix.');
1367
+ lines.push("#");
1368
+ const groups = config.capabilities?.groups;
1369
+ if (groups && Object.keys(groups).length > 0) {
1370
+ lines.push("[capabilities.groups]");
1371
+ for (const [name, caps] of Object.entries(groups)) {
1372
+ const capsStr = caps.map((c) => `"${c}"`).join(", ");
1373
+ lines.push(`${name} = [${capsStr}]`);
1374
+ }
1375
+ } else {
1376
+ lines.push("# [capabilities.groups]");
1377
+ lines.push('# expo = ["expo-app-design", "expo-deployment", "upgrading-expo"]');
1378
+ lines.push('# backend = ["cloudflare", "database-tools"]');
1379
+ lines.push("#");
1380
+ lines.push("# [profiles.mobile]");
1381
+ lines.push('# capabilities = ["group:expo", "react-native-tools"]');
1382
+ }
1383
+ lines.push("");
1384
+ lines.push("# =============================================================================");
787
1385
  lines.push("# MCP Servers");
788
1386
  lines.push("# =============================================================================");
789
1387
  lines.push("# Define MCP servers that automatically become capabilities.");
@@ -873,12 +1471,12 @@ function generateConfigToml(config) {
873
1471
  }
874
1472
 
875
1473
  // src/state/active-profile.ts
876
- import { existsSync as existsSync9, mkdirSync } from "node:fs";
877
- import { readFile as readFile9, unlink, writeFile as writeFile3 } from "node:fs/promises";
1474
+ import { existsSync as existsSync11, mkdirSync } from "node:fs";
1475
+ import { readFile as readFile9, unlink, writeFile as writeFile2 } from "node:fs/promises";
878
1476
  var STATE_DIR = ".omni/state";
879
1477
  var ACTIVE_PROFILE_PATH = `${STATE_DIR}/active-profile`;
880
1478
  async function readActiveProfileState() {
881
- if (!existsSync9(ACTIVE_PROFILE_PATH)) {
1479
+ if (!existsSync11(ACTIVE_PROFILE_PATH)) {
882
1480
  return null;
883
1481
  }
884
1482
  try {
@@ -891,10 +1489,10 @@ async function readActiveProfileState() {
891
1489
  }
892
1490
  async function writeActiveProfileState(profileName) {
893
1491
  mkdirSync(STATE_DIR, { recursive: true });
894
- await writeFile3(ACTIVE_PROFILE_PATH, profileName, "utf-8");
1492
+ await writeFile2(ACTIVE_PROFILE_PATH, profileName, "utf-8");
895
1493
  }
896
1494
  async function clearActiveProfileState() {
897
- if (existsSync9(ACTIVE_PROFILE_PATH)) {
1495
+ if (existsSync11(ACTIVE_PROFILE_PATH)) {
898
1496
  await unlink(ACTIVE_PROFILE_PATH);
899
1497
  }
900
1498
  }
@@ -915,7 +1513,24 @@ function resolveEnabledCapabilities(config, profileName) {
915
1513
  const profile = profileName ? config.profiles?.[profileName] : config.profiles?.[config.active_profile ?? "default"];
916
1514
  const profileCapabilities = profile?.capabilities ?? [];
917
1515
  const alwaysEnabled = config.always_enabled_capabilities ?? [];
918
- return [...new Set([...alwaysEnabled, ...profileCapabilities])];
1516
+ const groups = config.capabilities?.groups ?? {};
1517
+ const expandCapabilities = (caps) => {
1518
+ return caps.flatMap((cap) => {
1519
+ if (cap.startsWith("group:")) {
1520
+ const groupName = cap.slice(6);
1521
+ const groupCaps = groups[groupName];
1522
+ if (!groupCaps) {
1523
+ console.warn(`Unknown capability group: ${groupName}`);
1524
+ return [];
1525
+ }
1526
+ return groupCaps;
1527
+ }
1528
+ return cap;
1529
+ });
1530
+ };
1531
+ const expandedAlways = expandCapabilities(alwaysEnabled);
1532
+ const expandedProfile = expandCapabilities(profileCapabilities);
1533
+ return [...new Set([...expandedAlways, ...expandedProfile])];
919
1534
  }
920
1535
  async function loadProfileConfig(profileName) {
921
1536
  const config = await loadConfig();
@@ -962,6 +1577,93 @@ async function disableCapability(capabilityId) {
962
1577
  await writeConfig(config);
963
1578
  }
964
1579
 
1580
+ // src/hooks/merger.ts
1581
+ function mergeHooksConfigs(capabilityHooks) {
1582
+ const result = {};
1583
+ for (const event of HOOK_EVENTS) {
1584
+ const allMatchers = [];
1585
+ for (const capHooks of capabilityHooks) {
1586
+ const matchers = capHooks.config[event];
1587
+ if (matchers && matchers.length > 0) {
1588
+ allMatchers.push(...matchers);
1589
+ }
1590
+ }
1591
+ if (allMatchers.length > 0) {
1592
+ result[event] = allMatchers;
1593
+ }
1594
+ }
1595
+ return result;
1596
+ }
1597
+ function mergeAndDeduplicateHooks(capabilityHooks, options) {
1598
+ const merged = mergeHooksConfigs(capabilityHooks);
1599
+ if (!options?.deduplicateCommands) {
1600
+ return merged;
1601
+ }
1602
+ const result = {};
1603
+ for (const event of HOOK_EVENTS) {
1604
+ const matchers = merged[event];
1605
+ if (!matchers || matchers.length === 0) {
1606
+ continue;
1607
+ }
1608
+ const seenCommands = new Set;
1609
+ const deduplicatedMatchers = [];
1610
+ for (const matcher of matchers) {
1611
+ const deduplicatedHooks = matcher.hooks.filter((hook) => {
1612
+ if (hook.type !== "command") {
1613
+ return true;
1614
+ }
1615
+ const key = hook.command;
1616
+ if (seenCommands.has(key)) {
1617
+ return false;
1618
+ }
1619
+ seenCommands.add(key);
1620
+ return true;
1621
+ });
1622
+ if (deduplicatedHooks.length > 0) {
1623
+ deduplicatedMatchers.push({
1624
+ ...matcher,
1625
+ hooks: deduplicatedHooks
1626
+ });
1627
+ }
1628
+ }
1629
+ if (deduplicatedMatchers.length > 0) {
1630
+ result[event] = deduplicatedMatchers;
1631
+ }
1632
+ }
1633
+ return result;
1634
+ }
1635
+ function hasAnyHooks(config) {
1636
+ for (const event of HOOK_EVENTS) {
1637
+ const matchers = config[event];
1638
+ if (matchers && matchers.length > 0) {
1639
+ return true;
1640
+ }
1641
+ }
1642
+ return false;
1643
+ }
1644
+ function countHooks(config) {
1645
+ let count = 0;
1646
+ for (const event of HOOK_EVENTS) {
1647
+ const matchers = config[event];
1648
+ if (matchers) {
1649
+ for (const matcher of matchers) {
1650
+ count += matcher.hooks.length;
1651
+ }
1652
+ }
1653
+ }
1654
+ return count;
1655
+ }
1656
+ function getEventsWithHooks(config) {
1657
+ const events = [];
1658
+ for (const event of HOOK_EVENTS) {
1659
+ const matchers = config[event];
1660
+ if (matchers && matchers.length > 0) {
1661
+ events.push(event);
1662
+ }
1663
+ }
1664
+ return events;
1665
+ }
1666
+
965
1667
  // src/capability/registry.ts
966
1668
  async function buildCapabilityRegistry() {
967
1669
  const env = await loadEnvironment();
@@ -980,21 +1682,49 @@ async function buildCapabilityRegistry() {
980
1682
  console.warn(` ${errorMessage}`);
981
1683
  }
982
1684
  }
1685
+ const getAllCapabilityHooks = () => {
1686
+ const hooks = [];
1687
+ for (const cap of capabilities.values()) {
1688
+ if (cap.hooks) {
1689
+ hooks.push(cap.hooks);
1690
+ }
1691
+ }
1692
+ return hooks;
1693
+ };
983
1694
  return {
984
1695
  capabilities,
985
1696
  getCapability: (id) => capabilities.get(id),
986
1697
  getAllCapabilities: () => [...capabilities.values()],
987
1698
  getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
988
1699
  getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
989
- getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs)
1700
+ getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
1701
+ getAllCapabilityHooks,
1702
+ getMergedHooks: () => mergeHooksConfigs(getAllCapabilityHooks())
990
1703
  };
991
1704
  }
992
1705
  // src/capability/sources.ts
993
- import { existsSync as existsSync10 } from "node:fs";
1706
+ import { existsSync as existsSync12 } from "node:fs";
994
1707
  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";
1708
+ import { cp, mkdir, readdir, readFile as readFile10, rename, rm, stat, writeFile as writeFile3 } from "node:fs/promises";
1709
+ import { join as join8 } from "node:path";
1710
+ import { parse as parseToml2 } from "smol-toml";
1711
+
1712
+ // src/types/index.ts
1713
+ function isFileSourceConfig(config) {
1714
+ if (typeof config === "string") {
1715
+ return config.startsWith("file://");
1716
+ }
1717
+ return config.source.startsWith("file://");
1718
+ }
1719
+ function getActiveProviders(config) {
1720
+ if (config.providers)
1721
+ return config.providers;
1722
+ if (config.provider)
1723
+ return [config.provider];
1724
+ return ["claude"];
1725
+ }
1726
+
1727
+ // src/capability/sources.ts
998
1728
  var OMNI_LOCAL = ".omni";
999
1729
  var SKILL_DIRS = ["skills", "skill"];
1000
1730
  var AGENT_DIRS = ["agents", "agent", "subagents", "subagent"];
@@ -1005,7 +1735,7 @@ var SKILL_FILES = ["SKILL.md", "skill.md", "Skill.md"];
1005
1735
  var AGENT_FILES = ["AGENT.md", "agent.md", "Agent.md", "SUBAGENT.md", "subagent.md"];
1006
1736
  var COMMAND_FILES = ["COMMAND.md", "command.md", "Command.md"];
1007
1737
  async function spawnCapture(command, args, options) {
1008
- return await new Promise((resolve, reject) => {
1738
+ return await new Promise((resolve2, reject) => {
1009
1739
  const child = spawn(command, args, {
1010
1740
  cwd: options?.cwd,
1011
1741
  stdio: ["ignore", "pipe", "pipe"]
@@ -1022,12 +1752,43 @@ async function spawnCapture(command, args, options) {
1022
1752
  });
1023
1753
  child.on("error", (error) => reject(error));
1024
1754
  child.on("close", (exitCode) => {
1025
- resolve({ exitCode: exitCode ?? 0, stdout, stderr });
1755
+ resolve2({ exitCode: exitCode ?? 0, stdout, stderr });
1026
1756
  });
1027
1757
  });
1028
1758
  }
1759
+ function isGitSource(source) {
1760
+ return source.startsWith("github:") || source.startsWith("git@") || source.startsWith("https://") || source.startsWith("http://");
1761
+ }
1762
+ function isFileSource(source) {
1763
+ return source.startsWith("file://");
1764
+ }
1765
+ function parseFileSourcePath(source) {
1766
+ if (!source.startsWith("file://")) {
1767
+ throw new Error(`Invalid file source: ${source}`);
1768
+ }
1769
+ return source.slice(7);
1770
+ }
1771
+ async function readCapabilityIdFromPath(capabilityPath) {
1772
+ const tomlPath = join8(capabilityPath, "capability.toml");
1773
+ if (existsSync12(tomlPath)) {
1774
+ try {
1775
+ const content = await readFile10(tomlPath, "utf-8");
1776
+ const parsed = parseToml2(content);
1777
+ const capability = parsed["capability"];
1778
+ if (capability?.["id"] && typeof capability["id"] === "string") {
1779
+ return capability["id"];
1780
+ }
1781
+ } catch {}
1782
+ }
1783
+ const parts = capabilityPath.replace(/\\/g, "/").split("/");
1784
+ const dirName = parts.pop() || parts.pop();
1785
+ return dirName || null;
1786
+ }
1029
1787
  function parseSourceConfig(source) {
1030
1788
  if (typeof source === "string") {
1789
+ if (isFileSource(source)) {
1790
+ return { source };
1791
+ }
1031
1792
  let sourceUrl = source;
1032
1793
  let ref;
1033
1794
  if (source.startsWith("github:") && source.includes("#")) {
@@ -1041,6 +1802,9 @@ function parseSourceConfig(source) {
1041
1802
  }
1042
1803
  return result;
1043
1804
  }
1805
+ if (isFileSourceConfig(source)) {
1806
+ return source;
1807
+ }
1044
1808
  return source;
1045
1809
  }
1046
1810
  function sourceToGitUrl(source) {
@@ -1051,19 +1815,19 @@ function sourceToGitUrl(source) {
1051
1815
  return source;
1052
1816
  }
1053
1817
  function getSourceCapabilityPath(id) {
1054
- return join7(OMNI_LOCAL, "capabilities", id);
1818
+ return join8(OMNI_LOCAL, "capabilities", id);
1055
1819
  }
1056
1820
  function getLockFilePath() {
1057
1821
  return "omni.lock.toml";
1058
1822
  }
1059
1823
  async function loadLockFile() {
1060
1824
  const lockPath = getLockFilePath();
1061
- if (!existsSync10(lockPath)) {
1825
+ if (!existsSync12(lockPath)) {
1062
1826
  return { capabilities: {} };
1063
1827
  }
1064
1828
  try {
1065
1829
  const content = await readFile10(lockPath, "utf-8");
1066
- const parsed = parseToml(content);
1830
+ const parsed = parseToml2(content);
1067
1831
  const capabilities = parsed["capabilities"];
1068
1832
  return {
1069
1833
  capabilities: capabilities || {}
@@ -1092,14 +1856,14 @@ function stringifyLockFile(lockFile) {
1092
1856
  }
1093
1857
  async function saveLockFile(lockFile) {
1094
1858
  const lockPath = getLockFilePath();
1095
- await mkdir(join7(OMNI_LOCAL, "capabilities"), { recursive: true });
1859
+ await mkdir(join8(OMNI_LOCAL, "capabilities"), { recursive: true });
1096
1860
  const header = `# Auto-generated by OmniDev - DO NOT EDIT
1097
1861
  # Records installed capability versions for reproducibility
1098
1862
  # Last updated: ${new Date().toISOString()}
1099
1863
 
1100
1864
  `;
1101
1865
  const content = header + stringifyLockFile(lockFile);
1102
- await writeFile4(lockPath, content, "utf-8");
1866
+ await writeFile3(lockPath, content, "utf-8");
1103
1867
  }
1104
1868
  async function getRepoCommit(repoPath) {
1105
1869
  const { exitCode, stdout, stderr } = await spawnCapture("git", ["rev-parse", "HEAD"], {
@@ -1114,7 +1878,7 @@ function shortCommit(commit) {
1114
1878
  return commit.substring(0, 7);
1115
1879
  }
1116
1880
  async function cloneRepo(gitUrl, targetPath, ref) {
1117
- await mkdir(join7(targetPath, ".."), { recursive: true });
1881
+ await mkdir(join8(targetPath, ".."), { recursive: true });
1118
1882
  const args = ["clone", "--depth", "1"];
1119
1883
  if (ref) {
1120
1884
  args.push("--branch", ref);
@@ -1151,16 +1915,16 @@ async function fetchRepo(repoPath, ref) {
1151
1915
  return true;
1152
1916
  }
1153
1917
  function hasCapabilityToml(dirPath) {
1154
- return existsSync10(join7(dirPath, "capability.toml"));
1918
+ return existsSync12(join8(dirPath, "capability.toml"));
1155
1919
  }
1156
1920
  async function shouldWrapDirectory(dirPath) {
1157
- if (existsSync10(join7(dirPath, ".claude-plugin", "plugin.json"))) {
1921
+ if (existsSync12(join8(dirPath, ".claude-plugin", "plugin.json"))) {
1158
1922
  return true;
1159
1923
  }
1160
1924
  const allDirs = [...SKILL_DIRS, ...AGENT_DIRS, ...COMMAND_DIRS, ...RULE_DIRS, ...DOC_DIRS];
1161
1925
  for (const dirName of allDirs) {
1162
- const checkPath = join7(dirPath, dirName);
1163
- if (existsSync10(checkPath)) {
1926
+ const checkPath = join8(dirPath, dirName);
1927
+ if (existsSync12(checkPath)) {
1164
1928
  const stats = await stat(checkPath);
1165
1929
  if (stats.isDirectory()) {
1166
1930
  return true;
@@ -1171,8 +1935,8 @@ async function shouldWrapDirectory(dirPath) {
1171
1935
  }
1172
1936
  async function findMatchingDirs(basePath, names) {
1173
1937
  for (const name of names) {
1174
- const dirPath = join7(basePath, name);
1175
- if (existsSync10(dirPath)) {
1938
+ const dirPath = join8(basePath, name);
1939
+ if (existsSync12(dirPath)) {
1176
1940
  const stats = await stat(dirPath);
1177
1941
  if (stats.isDirectory()) {
1178
1942
  return dirPath;
@@ -1183,15 +1947,15 @@ async function findMatchingDirs(basePath, names) {
1183
1947
  }
1184
1948
  async function findContentItems(dirPath, filePatterns) {
1185
1949
  const items = [];
1186
- if (!existsSync10(dirPath)) {
1950
+ if (!existsSync12(dirPath)) {
1187
1951
  return items;
1188
1952
  }
1189
1953
  const entries = (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1190
1954
  for (const entry of entries) {
1191
- const entryPath = join7(dirPath, entry.name);
1955
+ const entryPath = join8(dirPath, entry.name);
1192
1956
  if (entry.isDirectory()) {
1193
1957
  for (const pattern of filePatterns) {
1194
- if (existsSync10(join7(entryPath, pattern))) {
1958
+ if (existsSync12(join8(entryPath, pattern))) {
1195
1959
  items.push({
1196
1960
  name: entry.name,
1197
1961
  path: entryPath,
@@ -1212,8 +1976,8 @@ async function findContentItems(dirPath, filePatterns) {
1212
1976
  return items;
1213
1977
  }
1214
1978
  async function parsePluginJson(dirPath) {
1215
- const pluginJsonPath = join7(dirPath, ".claude-plugin", "plugin.json");
1216
- if (!existsSync10(pluginJsonPath)) {
1979
+ const pluginJsonPath = join8(dirPath, ".claude-plugin", "plugin.json");
1980
+ if (!existsSync12(pluginJsonPath)) {
1217
1981
  return null;
1218
1982
  }
1219
1983
  try {
@@ -1237,8 +2001,8 @@ async function parsePluginJson(dirPath) {
1237
2001
  }
1238
2002
  }
1239
2003
  async function readReadmeDescription(dirPath) {
1240
- const readmePath = join7(dirPath, "README.md");
1241
- if (!existsSync10(readmePath)) {
2004
+ const readmePath = join8(dirPath, "README.md");
2005
+ if (!existsSync12(readmePath)) {
1242
2006
  return null;
1243
2007
  }
1244
2008
  try {
@@ -1270,6 +2034,29 @@ async function readReadmeDescription(dirPath) {
1270
2034
  return null;
1271
2035
  }
1272
2036
  }
2037
+ async function normalizeFolderNames(repoPath) {
2038
+ const renameMappings = [
2039
+ { from: "skill", to: "skills" },
2040
+ { from: "command", to: "commands" },
2041
+ { from: "rule", to: "rules" },
2042
+ { from: "agent", to: "agents" },
2043
+ { from: "subagent", to: "subagents" }
2044
+ ];
2045
+ for (const { from, to } of renameMappings) {
2046
+ const fromPath = join8(repoPath, from);
2047
+ const toPath = join8(repoPath, to);
2048
+ if (existsSync12(fromPath) && !existsSync12(toPath)) {
2049
+ try {
2050
+ const stats = await stat(fromPath);
2051
+ if (stats.isDirectory()) {
2052
+ await rename(fromPath, toPath);
2053
+ }
2054
+ } catch (error) {
2055
+ console.warn(`Failed to rename ${from} to ${to}:`, error);
2056
+ }
2057
+ }
2058
+ }
2059
+ }
1273
2060
  async function discoverContent(repoPath) {
1274
2061
  const result = {
1275
2062
  skills: [],
@@ -1347,7 +2134,7 @@ repository = "${repoUrl}"
1347
2134
  wrapped = true
1348
2135
  commit = "${commit}"
1349
2136
  `;
1350
- await writeFile4(join7(repoPath, "capability.toml"), tomlContent, "utf-8");
2137
+ await writeFile3(join8(repoPath, "capability.toml"), tomlContent, "utf-8");
1351
2138
  }
1352
2139
  async function fetchGitCapabilitySource(id, config, options) {
1353
2140
  const gitUrl = sourceToGitUrl(config.source);
@@ -1356,8 +2143,8 @@ async function fetchGitCapabilitySource(id, config, options) {
1356
2143
  let commit;
1357
2144
  let repoPath;
1358
2145
  if (config.path) {
1359
- const tempPath = join7(OMNI_LOCAL, "_temp", `${id}-repo`);
1360
- if (existsSync10(join7(tempPath, ".git"))) {
2146
+ const tempPath = join8(OMNI_LOCAL, "_temp", `${id}-repo`);
2147
+ if (existsSync12(join8(tempPath, ".git"))) {
1361
2148
  if (!options?.silent) {
1362
2149
  console.log(` Checking ${id}...`);
1363
2150
  }
@@ -1367,23 +2154,23 @@ async function fetchGitCapabilitySource(id, config, options) {
1367
2154
  if (!options?.silent) {
1368
2155
  console.log(` Cloning ${id} from ${config.source}...`);
1369
2156
  }
1370
- await mkdir(join7(tempPath, ".."), { recursive: true });
2157
+ await mkdir(join8(tempPath, ".."), { recursive: true });
1371
2158
  await cloneRepo(gitUrl, tempPath, config.ref);
1372
2159
  commit = await getRepoCommit(tempPath);
1373
2160
  updated = true;
1374
2161
  }
1375
- const sourcePath = join7(tempPath, config.path);
1376
- if (!existsSync10(sourcePath)) {
2162
+ const sourcePath = join8(tempPath, config.path);
2163
+ if (!existsSync12(sourcePath)) {
1377
2164
  throw new Error(`Path not found in repository: ${config.path}`);
1378
2165
  }
1379
- if (existsSync10(targetPath)) {
2166
+ if (existsSync12(targetPath)) {
1380
2167
  await rm(targetPath, { recursive: true });
1381
2168
  }
1382
- await mkdir(join7(targetPath, ".."), { recursive: true });
2169
+ await mkdir(join8(targetPath, ".."), { recursive: true });
1383
2170
  await cp(sourcePath, targetPath, { recursive: true });
1384
2171
  repoPath = targetPath;
1385
2172
  } else {
1386
- if (existsSync10(join7(targetPath, ".git"))) {
2173
+ if (existsSync12(join8(targetPath, ".git"))) {
1387
2174
  if (!options?.silent) {
1388
2175
  console.log(` Checking ${id}...`);
1389
2176
  }
@@ -1404,6 +2191,7 @@ async function fetchGitCapabilitySource(id, config, options) {
1404
2191
  needsWrap = await shouldWrapDirectory(repoPath);
1405
2192
  }
1406
2193
  if (needsWrap) {
2194
+ await normalizeFolderNames(repoPath);
1407
2195
  const content = await discoverContent(repoPath);
1408
2196
  await generateCapabilityToml(id, repoPath, config.source, commit, content);
1409
2197
  if (!options?.silent) {
@@ -1420,8 +2208,8 @@ async function fetchGitCapabilitySource(id, config, options) {
1420
2208
  }
1421
2209
  }
1422
2210
  let version = shortCommit(commit);
1423
- const pkgJsonPath = join7(repoPath, "package.json");
1424
- if (existsSync10(pkgJsonPath)) {
2211
+ const pkgJsonPath = join8(repoPath, "package.json");
2212
+ if (existsSync12(pkgJsonPath)) {
1425
2213
  try {
1426
2214
  const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
1427
2215
  if (pkgJson.version) {
@@ -1438,8 +2226,52 @@ async function fetchGitCapabilitySource(id, config, options) {
1438
2226
  wrapped: needsWrap
1439
2227
  };
1440
2228
  }
2229
+ async function fetchFileCapabilitySource(id, config, options) {
2230
+ const sourcePath = parseFileSourcePath(config.source);
2231
+ const targetPath = getSourceCapabilityPath(id);
2232
+ if (!existsSync12(sourcePath)) {
2233
+ throw new Error(`File source not found: ${sourcePath}`);
2234
+ }
2235
+ const sourceStats = await stat(sourcePath);
2236
+ if (!sourceStats.isDirectory()) {
2237
+ throw new Error(`File source must be a directory: ${sourcePath}`);
2238
+ }
2239
+ if (!existsSync12(join8(sourcePath, "capability.toml"))) {
2240
+ throw new Error(`No capability.toml found in: ${sourcePath}`);
2241
+ }
2242
+ if (!options?.silent) {
2243
+ console.log(` Copying ${id} from ${sourcePath}...`);
2244
+ }
2245
+ if (existsSync12(targetPath)) {
2246
+ await rm(targetPath, { recursive: true });
2247
+ }
2248
+ await mkdir(join8(targetPath, ".."), { recursive: true });
2249
+ await cp(sourcePath, targetPath, { recursive: true });
2250
+ let version = "local";
2251
+ const capTomlPath = join8(targetPath, "capability.toml");
2252
+ if (existsSync12(capTomlPath)) {
2253
+ try {
2254
+ const content = await readFile10(capTomlPath, "utf-8");
2255
+ const parsed = parseToml2(content);
2256
+ const capability = parsed["capability"];
2257
+ if (capability?.["version"] && typeof capability["version"] === "string") {
2258
+ version = capability["version"];
2259
+ }
2260
+ } catch {}
2261
+ }
2262
+ return {
2263
+ id,
2264
+ path: targetPath,
2265
+ version,
2266
+ updated: true,
2267
+ wrapped: false
2268
+ };
2269
+ }
1441
2270
  async function fetchCapabilitySource(id, sourceConfig, options) {
1442
2271
  const config = parseSourceConfig(sourceConfig);
2272
+ if (isFileSourceConfig(sourceConfig) || isFileSource(config.source)) {
2273
+ return fetchFileCapabilitySource(id, config, options);
2274
+ }
1443
2275
  return fetchGitCapabilitySource(id, config, options);
1444
2276
  }
1445
2277
  function generateMcpCapabilityTomlContent(id, mcpConfig) {
@@ -1506,17 +2338,17 @@ generated_from_omni_toml = true
1506
2338
  }
1507
2339
  async function generateMcpCapabilityToml(id, mcpConfig, targetPath) {
1508
2340
  const tomlContent = generateMcpCapabilityTomlContent(id, mcpConfig);
1509
- await writeFile4(join7(targetPath, "capability.toml"), tomlContent, "utf-8");
2341
+ await writeFile3(join8(targetPath, "capability.toml"), tomlContent, "utf-8");
1510
2342
  }
1511
2343
  async function isGeneratedMcpCapability(capabilityDir) {
1512
- const tomlPath = join7(capabilityDir, "capability.toml");
1513
- if (!existsSync10(tomlPath)) {
2344
+ const tomlPath = join8(capabilityDir, "capability.toml");
2345
+ if (!existsSync12(tomlPath)) {
1514
2346
  console.warn("no capability.toml found in", capabilityDir);
1515
2347
  return false;
1516
2348
  }
1517
2349
  try {
1518
2350
  const content = await readFile10(tomlPath, "utf-8");
1519
- const parsed = parseToml(content);
2351
+ const parsed = parseToml2(content);
1520
2352
  const capability = parsed["capability"];
1521
2353
  const metadata = capability?.["metadata"];
1522
2354
  return metadata?.["generated_from_omni_toml"] === true;
@@ -1525,14 +2357,14 @@ async function isGeneratedMcpCapability(capabilityDir) {
1525
2357
  }
1526
2358
  }
1527
2359
  async function cleanupStaleMcpCapabilities(currentMcpIds) {
1528
- const capabilitiesDir = join7(OMNI_LOCAL, "capabilities");
1529
- if (!existsSync10(capabilitiesDir)) {
2360
+ const capabilitiesDir = join8(OMNI_LOCAL, "capabilities");
2361
+ if (!existsSync12(capabilitiesDir)) {
1530
2362
  return;
1531
2363
  }
1532
2364
  const entries = await readdir(capabilitiesDir, { withFileTypes: true });
1533
2365
  for (const entry of entries) {
1534
2366
  if (entry.isDirectory()) {
1535
- const capDir = join7(capabilitiesDir, entry.name);
2367
+ const capDir = join8(capabilitiesDir, entry.name);
1536
2368
  const isGenerated = await isGeneratedMcpCapability(capDir);
1537
2369
  if (isGenerated && !currentMcpIds.has(entry.name)) {
1538
2370
  await rm(capDir, { recursive: true });
@@ -1545,10 +2377,10 @@ async function generateMcpCapabilities(config) {
1545
2377
  await cleanupStaleMcpCapabilities(new Set);
1546
2378
  return;
1547
2379
  }
1548
- const mcpCapabilitiesDir = join7(OMNI_LOCAL, "capabilities");
2380
+ const mcpCapabilitiesDir = join8(OMNI_LOCAL, "capabilities");
1549
2381
  const currentMcpIds = new Set;
1550
2382
  for (const [id, mcpConfig] of Object.entries(config.mcps)) {
1551
- const targetPath = join7(mcpCapabilitiesDir, id);
2383
+ const targetPath = join8(mcpCapabilitiesDir, id);
1552
2384
  currentMcpIds.add(id);
1553
2385
  await mkdir(targetPath, { recursive: true });
1554
2386
  await generateMcpCapabilityToml(id, mcpConfig, targetPath);
@@ -1576,12 +2408,14 @@ async function fetchAllCapabilitySources(config, options) {
1576
2408
  version: result.version,
1577
2409
  updated_at: new Date().toISOString()
1578
2410
  };
1579
- const gitConfig = parseSourceConfig(source);
1580
2411
  if (result.commit) {
1581
2412
  lockEntry.commit = result.commit;
1582
2413
  }
1583
- if (gitConfig.ref) {
1584
- lockEntry.ref = gitConfig.ref;
2414
+ if (!isFileSourceConfig(source)) {
2415
+ const gitConfig = parseSourceConfig(source);
2416
+ if (gitConfig.ref) {
2417
+ lockEntry.ref = gitConfig.ref;
2418
+ }
1585
2419
  }
1586
2420
  const existing = lockFile.capabilities[id];
1587
2421
  const hasChanged = !existing || existing.commit !== result.commit;
@@ -1621,8 +2455,18 @@ async function checkForUpdates(config) {
1621
2455
  const sourceConfig = parseSourceConfig(source);
1622
2456
  const targetPath = getSourceCapabilityPath(id);
1623
2457
  const existing = lockFile.capabilities[id];
2458
+ if (isFileSourceConfig(source) || isFileSource(sourceConfig.source)) {
2459
+ updates.push({
2460
+ id,
2461
+ source: sourceConfig.source,
2462
+ currentVersion: existing?.version || "local",
2463
+ latestVersion: "local",
2464
+ hasUpdate: false
2465
+ });
2466
+ continue;
2467
+ }
1624
2468
  const gitConfig = sourceConfig;
1625
- if (!existsSync10(join7(targetPath, ".git"))) {
2469
+ if (!existsSync12(join8(targetPath, ".git"))) {
1626
2470
  updates.push({
1627
2471
  id,
1628
2472
  source: gitConfig.source,
@@ -1658,12 +2502,12 @@ async function checkForUpdates(config) {
1658
2502
  return updates;
1659
2503
  }
1660
2504
  // src/config/provider.ts
1661
- import { existsSync as existsSync11 } from "node:fs";
1662
- import { readFile as readFile11, writeFile as writeFile5 } from "node:fs/promises";
2505
+ import { existsSync as existsSync13 } from "node:fs";
2506
+ import { readFile as readFile11, writeFile as writeFile4 } from "node:fs/promises";
1663
2507
  import { parse as parse2 } from "smol-toml";
1664
2508
  var PROVIDER_CONFIG_PATH = ".omni/provider.toml";
1665
2509
  async function loadProviderConfig() {
1666
- if (!existsSync11(PROVIDER_CONFIG_PATH)) {
2510
+ if (!existsSync13(PROVIDER_CONFIG_PATH)) {
1667
2511
  return { provider: "claude" };
1668
2512
  }
1669
2513
  const content = await readFile11(PROVIDER_CONFIG_PATH, "utf-8");
@@ -1693,7 +2537,7 @@ async function writeProviderConfig(config) {
1693
2537
  lines.push("# Default: Claude");
1694
2538
  lines.push('provider = "claude"');
1695
2539
  }
1696
- await writeFile5(PROVIDER_CONFIG_PATH, `${lines.join(`
2540
+ await writeFile4(PROVIDER_CONFIG_PATH, `${lines.join(`
1697
2541
  `)}
1698
2542
  `, "utf-8");
1699
2543
  }
@@ -1707,16 +2551,222 @@ function parseProviderFlag(flag) {
1707
2551
  }
1708
2552
  throw new Error(`Invalid provider: ${flag}. Must be 'claude', 'codex', or 'both'.`);
1709
2553
  }
2554
+ // src/config/toml-patcher.ts
2555
+ import { existsSync as existsSync14 } from "node:fs";
2556
+ import { readFile as readFile12, writeFile as writeFile5 } from "node:fs/promises";
2557
+ var CONFIG_PATH2 = "omni.toml";
2558
+ async function readConfigFile() {
2559
+ if (!existsSync14(CONFIG_PATH2)) {
2560
+ return "";
2561
+ }
2562
+ return readFile12(CONFIG_PATH2, "utf-8");
2563
+ }
2564
+ async function writeConfigFile(content) {
2565
+ await writeFile5(CONFIG_PATH2, content, "utf-8");
2566
+ }
2567
+ function findSection(lines, sectionPattern) {
2568
+ return lines.findIndex((line) => sectionPattern.test(line.trim()));
2569
+ }
2570
+ function findSectionEnd(lines, startIndex) {
2571
+ for (let i = startIndex + 1;i < lines.length; i++) {
2572
+ const line = lines[i];
2573
+ if (line === undefined)
2574
+ continue;
2575
+ const trimmed = line.trim();
2576
+ if (/^\[(?!\[)/.test(trimmed) && !trimmed.startsWith("#")) {
2577
+ return i;
2578
+ }
2579
+ }
2580
+ return lines.length;
2581
+ }
2582
+ function formatCapabilitySource(name, source) {
2583
+ if (typeof source === "string") {
2584
+ return `${name} = "${source}"`;
2585
+ }
2586
+ if ("path" in source && source.path) {
2587
+ return `${name} = { source = "${source.source}", path = "${source.path}" }`;
2588
+ }
2589
+ return `${name} = "${source.source}"`;
2590
+ }
2591
+ async function patchAddCapabilitySource(name, source) {
2592
+ let content = await readConfigFile();
2593
+ const lines = content.split(`
2594
+ `);
2595
+ const sectionIndex = findSection(lines, /^\[capabilities\.sources\]$/);
2596
+ const newEntry = formatCapabilitySource(name, source);
2597
+ if (sectionIndex !== -1) {
2598
+ const sectionEnd = findSectionEnd(lines, sectionIndex);
2599
+ let insertIndex = sectionEnd;
2600
+ for (let i = sectionEnd - 1;i > sectionIndex; i--) {
2601
+ const line = lines[i];
2602
+ if (line === undefined)
2603
+ continue;
2604
+ const trimmed = line.trim();
2605
+ if (trimmed && !trimmed.startsWith("#")) {
2606
+ insertIndex = i + 1;
2607
+ break;
2608
+ }
2609
+ }
2610
+ if (insertIndex === sectionEnd && sectionIndex + 1 < lines.length) {
2611
+ insertIndex = sectionIndex + 1;
2612
+ }
2613
+ lines.splice(insertIndex, 0, newEntry);
2614
+ } else {
2615
+ const capabilitiesIndex = findSection(lines, /^\[capabilities\]$/);
2616
+ if (capabilitiesIndex !== -1) {
2617
+ const capEnd = findSectionEnd(lines, capabilitiesIndex);
2618
+ lines.splice(capEnd, 0, "", "[capabilities.sources]", newEntry);
2619
+ } else {
2620
+ const mcpsIndex = findSection(lines, /^\[mcps/);
2621
+ if (mcpsIndex !== -1) {
2622
+ lines.splice(mcpsIndex, 0, "[capabilities.sources]", newEntry, "");
2623
+ } else {
2624
+ lines.push("", "[capabilities.sources]", newEntry);
2625
+ }
2626
+ }
2627
+ }
2628
+ content = lines.join(`
2629
+ `);
2630
+ await writeConfigFile(content);
2631
+ }
2632
+ function formatMcpConfig(name, config) {
2633
+ const lines = [];
2634
+ lines.push(`[mcps.${name}]`);
2635
+ if (config.transport && config.transport !== "stdio") {
2636
+ lines.push(`transport = "${config.transport}"`);
2637
+ }
2638
+ if (config.command) {
2639
+ lines.push(`command = "${config.command}"`);
2640
+ }
2641
+ if (config.args && config.args.length > 0) {
2642
+ const argsStr = config.args.map((a) => `"${a}"`).join(", ");
2643
+ lines.push(`args = [${argsStr}]`);
2644
+ }
2645
+ if (config.cwd) {
2646
+ lines.push(`cwd = "${config.cwd}"`);
2647
+ }
2648
+ if (config.url) {
2649
+ lines.push(`url = "${config.url}"`);
2650
+ }
2651
+ if (config.env && Object.keys(config.env).length > 0) {
2652
+ lines.push(`[mcps.${name}.env]`);
2653
+ for (const [key, value] of Object.entries(config.env)) {
2654
+ lines.push(`${key} = "${value}"`);
2655
+ }
2656
+ }
2657
+ if (config.headers && Object.keys(config.headers).length > 0) {
2658
+ lines.push(`[mcps.${name}.headers]`);
2659
+ for (const [key, value] of Object.entries(config.headers)) {
2660
+ lines.push(`${key} = "${value}"`);
2661
+ }
2662
+ }
2663
+ return lines;
2664
+ }
2665
+ async function patchAddMcp(name, config) {
2666
+ let content = await readConfigFile();
2667
+ const lines = content.split(`
2668
+ `);
2669
+ const mcpLines = formatMcpConfig(name, config);
2670
+ const existingMcpIndex = findSection(lines, /^\[mcps\./);
2671
+ if (existingMcpIndex !== -1) {
2672
+ let lastMcpEnd = existingMcpIndex;
2673
+ for (let i = existingMcpIndex;i < lines.length; i++) {
2674
+ const line = lines[i];
2675
+ if (line === undefined)
2676
+ continue;
2677
+ const trimmed = line.trim();
2678
+ if (/^\[mcps\./.test(trimmed)) {
2679
+ lastMcpEnd = findSectionEnd(lines, i);
2680
+ } else if (/^\[(?!mcps\.)/.test(trimmed) && !trimmed.startsWith("#")) {
2681
+ break;
2682
+ }
2683
+ }
2684
+ lines.splice(lastMcpEnd, 0, "", ...mcpLines);
2685
+ } else {
2686
+ const profilesIndex = findSection(lines, /^\[profiles\./);
2687
+ if (profilesIndex !== -1) {
2688
+ lines.splice(profilesIndex, 0, ...mcpLines, "");
2689
+ } else {
2690
+ lines.push("", ...mcpLines);
2691
+ }
2692
+ }
2693
+ content = lines.join(`
2694
+ `);
2695
+ await writeConfigFile(content);
2696
+ }
2697
+ async function patchAddToProfile(profileName, capabilityName) {
2698
+ let content = await readConfigFile();
2699
+ const lines = content.split(`
2700
+ `);
2701
+ const profilePattern = new RegExp(`^\\[profiles\\.${escapeRegExp(profileName)}\\]$`);
2702
+ const profileIndex = findSection(lines, profilePattern);
2703
+ if (profileIndex !== -1) {
2704
+ const profileEnd = findSectionEnd(lines, profileIndex);
2705
+ let capabilitiesLineIndex = -1;
2706
+ for (let i = profileIndex + 1;i < profileEnd; i++) {
2707
+ const line = lines[i];
2708
+ if (line === undefined)
2709
+ continue;
2710
+ const trimmed = line.trim();
2711
+ if (trimmed.startsWith("capabilities")) {
2712
+ capabilitiesLineIndex = i;
2713
+ break;
2714
+ }
2715
+ }
2716
+ if (capabilitiesLineIndex !== -1) {
2717
+ const line = lines[capabilitiesLineIndex];
2718
+ if (line !== undefined) {
2719
+ const match = line.match(/capabilities\s*=\s*\[(.*)\]/);
2720
+ if (match && match[1] !== undefined) {
2721
+ const existingCaps = match[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2722
+ const quotedCap = `"${capabilityName}"`;
2723
+ if (!existingCaps.includes(quotedCap)) {
2724
+ existingCaps.push(quotedCap);
2725
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
2726
+ lines[capabilitiesLineIndex] = `${indent}capabilities = [${existingCaps.join(", ")}]`;
2727
+ }
2728
+ }
2729
+ }
2730
+ } else {
2731
+ lines.splice(profileIndex + 1, 0, `capabilities = ["${capabilityName}"]`);
2732
+ }
2733
+ } else {
2734
+ const anyProfileIndex = findSection(lines, /^\[profiles\./);
2735
+ if (anyProfileIndex !== -1) {
2736
+ let lastProfileEnd = anyProfileIndex;
2737
+ for (let i = anyProfileIndex;i < lines.length; i++) {
2738
+ const line = lines[i];
2739
+ if (line === undefined)
2740
+ continue;
2741
+ const trimmed = line.trim();
2742
+ if (/^\[profiles\./.test(trimmed)) {
2743
+ lastProfileEnd = findSectionEnd(lines, i);
2744
+ } else if (/^\[(?!profiles\.)/.test(trimmed) && !trimmed.startsWith("#")) {
2745
+ break;
2746
+ }
2747
+ }
2748
+ lines.splice(lastProfileEnd, 0, "", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
2749
+ } else {
2750
+ lines.push("", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
2751
+ }
2752
+ }
2753
+ content = lines.join(`
2754
+ `);
2755
+ await writeConfigFile(content);
2756
+ }
2757
+ function escapeRegExp(str) {
2758
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2759
+ }
1710
2760
  // src/mcp-json/manager.ts
1711
- import { existsSync as existsSync12 } from "node:fs";
1712
- import { readFile as readFile12, writeFile as writeFile6 } from "node:fs/promises";
2761
+ import { existsSync as existsSync15 } from "node:fs";
2762
+ import { readFile as readFile13, writeFile as writeFile6 } from "node:fs/promises";
1713
2763
  var MCP_JSON_PATH = ".mcp.json";
1714
2764
  async function readMcpJson() {
1715
- if (!existsSync12(MCP_JSON_PATH)) {
2765
+ if (!existsSync15(MCP_JSON_PATH)) {
1716
2766
  return { mcpServers: {} };
1717
2767
  }
1718
2768
  try {
1719
- const content = await readFile12(MCP_JSON_PATH, "utf-8");
2769
+ const content = await readFile13(MCP_JSON_PATH, "utf-8");
1720
2770
  const parsed = JSON.parse(content);
1721
2771
  return {
1722
2772
  mcpServers: parsed.mcpServers || {}
@@ -1725,8 +2775,8 @@ async function readMcpJson() {
1725
2775
  return { mcpServers: {} };
1726
2776
  }
1727
2777
  }
1728
- async function writeMcpJson(config) {
1729
- await writeFile6(MCP_JSON_PATH, `${JSON.stringify(config, null, 2)}
2778
+ async function writeMcpJson(config2) {
2779
+ await writeFile6(MCP_JSON_PATH, `${JSON.stringify(config2, null, 2)}
1730
2780
  `, "utf-8");
1731
2781
  }
1732
2782
  function buildMcpServerConfig(mcp) {
@@ -1735,41 +2785,41 @@ function buildMcpServerConfig(mcp) {
1735
2785
  if (!mcp.url) {
1736
2786
  throw new Error("HTTP transport requires a URL");
1737
2787
  }
1738
- const config2 = {
2788
+ const config3 = {
1739
2789
  type: "http",
1740
2790
  url: mcp.url
1741
2791
  };
1742
2792
  if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1743
- config2.headers = mcp.headers;
2793
+ config3.headers = mcp.headers;
1744
2794
  }
1745
- return config2;
2795
+ return config3;
1746
2796
  }
1747
2797
  if (transport === "sse") {
1748
2798
  if (!mcp.url) {
1749
2799
  throw new Error("SSE transport requires a URL");
1750
2800
  }
1751
- const config2 = {
2801
+ const config3 = {
1752
2802
  type: "sse",
1753
2803
  url: mcp.url
1754
2804
  };
1755
2805
  if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1756
- config2.headers = mcp.headers;
2806
+ config3.headers = mcp.headers;
1757
2807
  }
1758
- return config2;
2808
+ return config3;
1759
2809
  }
1760
2810
  if (!mcp.command) {
1761
2811
  throw new Error("stdio transport requires a command");
1762
2812
  }
1763
- const config = {
2813
+ const config2 = {
1764
2814
  command: mcp.command
1765
2815
  };
1766
2816
  if (mcp.args) {
1767
- config.args = mcp.args;
2817
+ config2.args = mcp.args;
1768
2818
  }
1769
2819
  if (mcp.env) {
1770
- config.env = mcp.env;
2820
+ config2.env = mcp.env;
1771
2821
  }
1772
- return config;
2822
+ return config2;
1773
2823
  }
1774
2824
  async function syncMcpJson(capabilities2, previousManifest, options = {}) {
1775
2825
  const mcpJson = await readMcpJson();
@@ -1795,19 +2845,19 @@ async function syncMcpJson(capabilities2, previousManifest, options = {}) {
1795
2845
  }
1796
2846
  }
1797
2847
  // 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";
2848
+ import { existsSync as existsSync16, mkdirSync as mkdirSync2, rmSync } from "node:fs";
2849
+ import { readFile as readFile14, writeFile as writeFile7 } from "node:fs/promises";
1800
2850
  var MANIFEST_PATH = ".omni/state/manifest.json";
1801
2851
  var CURRENT_VERSION = 1;
1802
2852
  async function loadManifest() {
1803
- if (!existsSync13(MANIFEST_PATH)) {
2853
+ if (!existsSync16(MANIFEST_PATH)) {
1804
2854
  return {
1805
2855
  version: CURRENT_VERSION,
1806
2856
  syncedAt: new Date().toISOString(),
1807
2857
  capabilities: {}
1808
2858
  };
1809
2859
  }
1810
- const content = await readFile13(MANIFEST_PATH, "utf-8");
2860
+ const content = await readFile14(MANIFEST_PATH, "utf-8");
1811
2861
  return JSON.parse(content);
1812
2862
  }
1813
2863
  async function saveManifest(manifest) {
@@ -1847,14 +2897,14 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
1847
2897
  }
1848
2898
  for (const skillName of resources.skills) {
1849
2899
  const skillDir = `.claude/skills/${skillName}`;
1850
- if (existsSync13(skillDir)) {
2900
+ if (existsSync16(skillDir)) {
1851
2901
  rmSync(skillDir, { recursive: true });
1852
2902
  result.deletedSkills.push(skillName);
1853
2903
  }
1854
2904
  }
1855
2905
  for (const ruleName of resources.rules) {
1856
2906
  const rulePath = `.cursor/rules/omnidev-${ruleName}.mdc`;
1857
- if (existsSync13(rulePath)) {
2907
+ if (existsSync16(rulePath)) {
1858
2908
  rmSync(rulePath);
1859
2909
  result.deletedRules.push(ruleName);
1860
2910
  }
@@ -1863,17 +2913,17 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
1863
2913
  return result;
1864
2914
  }
1865
2915
  // 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";
2916
+ import { existsSync as existsSync17, mkdirSync as mkdirSync3 } from "node:fs";
2917
+ import { readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
1868
2918
  var STATE_DIR2 = ".omni/state";
1869
2919
  var PROVIDERS_PATH = `${STATE_DIR2}/providers.json`;
1870
2920
  var DEFAULT_PROVIDERS = ["claude-code"];
1871
2921
  async function readEnabledProviders() {
1872
- if (!existsSync14(PROVIDERS_PATH)) {
2922
+ if (!existsSync17(PROVIDERS_PATH)) {
1873
2923
  return DEFAULT_PROVIDERS;
1874
2924
  }
1875
2925
  try {
1876
- const content = await readFile14(PROVIDERS_PATH, "utf-8");
2926
+ const content = await readFile15(PROVIDERS_PATH, "utf-8");
1877
2927
  const state = JSON.parse(content);
1878
2928
  return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
1879
2929
  } catch {
@@ -1905,18 +2955,18 @@ async function isProviderEnabled(providerId) {
1905
2955
  import { spawn as spawn2 } from "node:child_process";
1906
2956
  import { mkdirSync as mkdirSync4 } from "node:fs";
1907
2957
  async function installCapabilityDependencies(silent) {
1908
- const { existsSync: existsSync15, readdirSync: readdirSync7 } = await import("node:fs");
1909
- const { join: join8 } = await import("node:path");
2958
+ const { existsSync: existsSync18, readdirSync: readdirSync7 } = await import("node:fs");
2959
+ const { join: join9 } = await import("node:path");
1910
2960
  const capabilitiesDir = ".omni/capabilities";
1911
- if (!existsSync15(capabilitiesDir)) {
2961
+ if (!existsSync18(capabilitiesDir)) {
1912
2962
  return;
1913
2963
  }
1914
2964
  const entries = readdirSync7(capabilitiesDir, { withFileTypes: true });
1915
2965
  async function commandExists(cmd) {
1916
- return await new Promise((resolve) => {
2966
+ return await new Promise((resolve2) => {
1917
2967
  const proc = spawn2(cmd, ["--version"], { stdio: "ignore" });
1918
- proc.on("error", () => resolve(false));
1919
- proc.on("close", (code) => resolve(code === 0));
2968
+ proc.on("error", () => resolve2(false));
2969
+ proc.on("close", (code) => resolve2(code === 0));
1920
2970
  });
1921
2971
  }
1922
2972
  const hasBun = await commandExists("bun");
@@ -1928,16 +2978,16 @@ async function installCapabilityDependencies(silent) {
1928
2978
  if (!entry.isDirectory()) {
1929
2979
  continue;
1930
2980
  }
1931
- const capabilityPath = join8(capabilitiesDir, entry.name);
1932
- const packageJsonPath = join8(capabilityPath, "package.json");
1933
- if (!existsSync15(packageJsonPath)) {
2981
+ const capabilityPath = join9(capabilitiesDir, entry.name);
2982
+ const packageJsonPath = join9(capabilityPath, "package.json");
2983
+ if (!existsSync18(packageJsonPath)) {
1934
2984
  continue;
1935
2985
  }
1936
2986
  if (!silent) {
1937
2987
  console.log(`Installing dependencies for ${capabilityPath}...`);
1938
2988
  }
1939
- await new Promise((resolve, reject) => {
1940
- const useNpmCi = hasNpm && existsSync15(join8(capabilityPath, "package-lock.json"));
2989
+ await new Promise((resolve2, reject) => {
2990
+ const useNpmCi = hasNpm && existsSync18(join9(capabilityPath, "package-lock.json"));
1941
2991
  const cmd = hasBun ? "bun" : "npm";
1942
2992
  const args = hasBun ? ["install"] : useNpmCi ? ["ci"] : ["install"];
1943
2993
  const proc = spawn2(cmd, args, {
@@ -1946,7 +2996,7 @@ async function installCapabilityDependencies(silent) {
1946
2996
  });
1947
2997
  proc.on("close", (code) => {
1948
2998
  if (code === 0) {
1949
- resolve();
2999
+ resolve2();
1950
3000
  } else {
1951
3001
  reject(new Error(`Failed to install dependencies for ${capabilityPath}`));
1952
3002
  }
@@ -1959,8 +3009,8 @@ async function installCapabilityDependencies(silent) {
1959
3009
  }
1960
3010
  async function buildSyncBundle(options) {
1961
3011
  const silent = options?.silent ?? false;
1962
- const config = await loadConfig();
1963
- await fetchAllCapabilitySources(config, { silent });
3012
+ const config2 = await loadConfig();
3013
+ await fetchAllCapabilitySources(config2, { silent });
1964
3014
  await installCapabilityDependencies(silent);
1965
3015
  const registry = await buildCapabilityRegistry();
1966
3016
  const capabilities2 = registry.getAllCapabilities();
@@ -1969,6 +3019,7 @@ async function buildSyncBundle(options) {
1969
3019
  const docs = registry.getAllDocs();
1970
3020
  const commands = capabilities2.flatMap((c) => c.commands);
1971
3021
  const subagents = capabilities2.flatMap((c) => c.subagents);
3022
+ const mergedHooks = registry.getMergedHooks();
1972
3023
  const instructionsContent = generateInstructionsContent(rules, docs);
1973
3024
  const bundle = {
1974
3025
  capabilities: capabilities2,
@@ -1977,9 +3028,11 @@ async function buildSyncBundle(options) {
1977
3028
  docs,
1978
3029
  commands,
1979
3030
  subagents,
1980
- instructionsPath: ".omni/instructions.md",
1981
3031
  instructionsContent
1982
3032
  };
3033
+ if (hasAnyHooks(mergedHooks)) {
3034
+ bundle.hooks = mergedHooks;
3035
+ }
1983
3036
  return { bundle };
1984
3037
  }
1985
3038
  async function syncAgentConfiguration(options) {
@@ -2023,15 +3076,14 @@ async function syncAgentConfiguration(options) {
2023
3076
  }
2024
3077
  }
2025
3078
  mkdirSync4(".omni", { recursive: true });
2026
- await writeRules(bundle.rules, bundle.docs);
2027
3079
  await syncMcpJson(capabilities2, previousManifest, { silent });
2028
3080
  const newManifest = buildManifestFromCapabilities(capabilities2);
2029
3081
  await saveManifest(newManifest);
2030
3082
  if (adapters.length > 0) {
2031
- const config = await loadConfig();
3083
+ const config2 = await loadConfig();
2032
3084
  const ctx = {
2033
3085
  projectRoot: process.cwd(),
2034
- config
3086
+ config: config2
2035
3087
  };
2036
3088
  for (const adapter of adapters) {
2037
3089
  try {
@@ -2046,7 +3098,7 @@ async function syncAgentConfiguration(options) {
2046
3098
  }
2047
3099
  if (!silent) {
2048
3100
  console.log("✓ Synced:");
2049
- console.log(` - .omni/instructions.md (${bundle.docs.length} docs, ${bundle.rules.length} rules)`);
3101
+ console.log(` - ${bundle.docs.length} docs, ${bundle.rules.length} rules`);
2050
3102
  if (adapters.length > 0) {
2051
3103
  console.log(` - Provider adapters: ${adapters.map((a) => a.displayName).join(", ")}`);
2052
3104
  }
@@ -2101,56 +3153,150 @@ function generateAgentsTemplate() {
2101
3153
 
2102
3154
  ## OmniDev
2103
3155
 
2104
- @import .omni/instructions.md
3156
+ <!-- This section is populated during sync with capability rules and docs -->
2105
3157
  `;
2106
3158
  }
2107
- // src/templates/claude.ts
2108
- function generateClaudeTemplate() {
2109
- return `# Project Instructions
3159
+ // src/templates/capability.ts
3160
+ function generateCapabilityToml2(options) {
3161
+ const description = options.description || "TODO: Add a description for your capability";
3162
+ return `[capability]
3163
+ id = "${options.id}"
3164
+ name = "${options.name}"
3165
+ version = "0.1.0"
3166
+ description = "${description}"
2110
3167
 
2111
- <!-- Add your project-specific instructions here -->
3168
+ # Optional author information
3169
+ # [capability.author]
3170
+ # name = "Your Name"
3171
+ # email = "you@example.com"
2112
3172
 
2113
- ## OmniDev
3173
+ # Optional metadata
3174
+ # [capability.metadata]
3175
+ # repository = "https://github.com/user/repo"
3176
+ # license = "MIT"
3177
+ `;
3178
+ }
3179
+ function generateSkillTemplate(skillName) {
3180
+ return `---
3181
+ name: ${skillName}
3182
+ description: TODO: Add a description for this skill
3183
+ ---
3184
+
3185
+ ## What I do
3186
+
3187
+ <!-- Describe what this skill helps the AI agent accomplish -->
3188
+ - TODO: List the main capabilities of this skill
3189
+
3190
+ ## When to use me
2114
3191
 
2115
- @import .omni/instructions.md
3192
+ <!-- Describe scenarios when this skill should be invoked -->
3193
+ Use this skill when you need to:
3194
+ - TODO: Add trigger conditions
3195
+
3196
+ ## Implementation
3197
+
3198
+ <!-- Add detailed instructions for the AI agent -->
3199
+ ### Steps
3200
+
3201
+ 1. TODO: Add implementation steps
3202
+ 2. Validate inputs and outputs
3203
+ 3. Report results to the user
3204
+
3205
+ ## Examples
3206
+
3207
+ <!-- Optional: Add examples of how this skill should be used -->
3208
+ \`\`\`
3209
+ TODO: Add example usage
3210
+ \`\`\`
2116
3211
  `;
2117
3212
  }
2118
- function generateInstructionsTemplate() {
2119
- return `# OmniDev Instructions
3213
+ function generateRuleTemplate(ruleName) {
3214
+ return `# ${formatDisplayName(ruleName)}
2120
3215
 
2121
- ## Project Description
2122
- <!-- TODO: Add 2-3 sentences describing your project -->
2123
- [Describe what this project does and its main purpose]
3216
+ <!-- Rules are guidelines that the AI agent should follow when working in this project -->
3217
+
3218
+ ## Overview
2124
3219
 
2125
- ## How OmniDev Works
3220
+ TODO: Describe what this rule enforces or guides.
2126
3221
 
2127
- OmniDev manages capability content for your project. Capabilities can provide:
3222
+ ## Guidelines
2128
3223
 
2129
- - Skills (for agent workflows)
2130
- - Rules (for guardrails and conventions)
2131
- - Docs (reference material)
2132
- - Commands and subagents (optional)
3224
+ - TODO: Add specific guidelines the AI should follow
3225
+ - Be specific and actionable
3226
+ - Include examples where helpful
2133
3227
 
2134
- Enable capabilities with:
3228
+ ## Examples
3229
+
3230
+ ### Good
2135
3231
 
2136
3232
  \`\`\`
2137
- omnidev capability enable <capability-id>
3233
+ TODO: Add example of correct behavior
2138
3234
  \`\`\`
2139
3235
 
2140
- OmniDev will automatically sync enabled capabilities into your workspace. If you want to force a refresh:
3236
+ ### Bad
2141
3237
 
2142
3238
  \`\`\`
2143
- omnidev sync
3239
+ TODO: Add example of incorrect behavior
2144
3240
  \`\`\`
3241
+ `;
3242
+ }
3243
+ function generateHooksTemplate() {
3244
+ return `# Hook configuration for this capability
3245
+ # See: https://omnidev.dev/docs/advanced/hooks
3246
+
3247
+ # Example: Validate bash commands before execution
3248
+ # [[PreToolUse]]
3249
+ # matcher = "Bash"
3250
+ # [[PreToolUse.hooks]]
3251
+ # type = "command"
3252
+ # command = "\${OMNIDEV_CAPABILITY_ROOT}/hooks/validate-bash.sh"
3253
+ # timeout = 30
3254
+
3255
+ # Example: Run linter after file edits
3256
+ # [[PostToolUse]]
3257
+ # matcher = "Write|Edit"
3258
+ # [[PostToolUse.hooks]]
3259
+ # type = "command"
3260
+ # command = "\${OMNIDEV_CAPABILITY_ROOT}/hooks/run-linter.sh"
3261
+
3262
+ # Example: Load context at session start
3263
+ # [[SessionStart]]
3264
+ # matcher = "startup|resume"
3265
+ # [[SessionStart.hooks]]
3266
+ # type = "command"
3267
+ # command = "\${OMNIDEV_CAPABILITY_ROOT}/hooks/load-context.sh"
3268
+ `;
3269
+ }
3270
+ function generateHookScript() {
3271
+ return `#!/bin/bash
3272
+ # Sample hook script
3273
+ # This script receives JSON input via stdin
3274
+
3275
+ # Read JSON input from stdin
3276
+ INPUT=$(cat)
3277
+
3278
+ # Example: Extract tool information
3279
+ # TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
3280
+ # COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
2145
3281
 
2146
- <!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->
2147
- <!-- This section is automatically updated by 'omnidev agents sync' -->
3282
+ # Add your validation logic here
3283
+ # Exit 0 to allow, exit 2 to block
3284
+
3285
+ exit 0
3286
+ `;
3287
+ }
3288
+ function formatDisplayName(kebabCase) {
3289
+ return kebabCase.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3290
+ }
3291
+ // src/templates/claude.ts
3292
+ function generateClaudeTemplate() {
3293
+ return `# Project Instructions
2148
3294
 
2149
- ## Capabilities
3295
+ <!-- Add your project-specific instructions here -->
2150
3296
 
2151
- No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable capabilities.
3297
+ ## OmniDev
2152
3298
 
2153
- <!-- END OMNIDEV GENERATED CONTENT -->
3299
+ <!-- This section is populated during sync with capability rules and docs -->
2154
3300
  `;
2155
3301
  }
2156
3302
  // src/templates/omni.ts
@@ -2174,14 +3320,6 @@ function generateOmniMdTemplate() {
2174
3320
  <!-- Describe your project's architecture and key components -->
2175
3321
  `;
2176
3322
  }
2177
- // src/types/index.ts
2178
- function getActiveProviders(config) {
2179
- if (config.providers)
2180
- return config.providers;
2181
- if (config.provider)
2182
- return [config.provider];
2183
- return ["claude"];
2184
- }
2185
3323
  // src/debug.ts
2186
3324
  function debug(message, data) {
2187
3325
  if (process.env["OMNIDEV_DEBUG"] !== "1") {
@@ -2203,14 +3341,18 @@ function getVersion() {
2203
3341
  return version;
2204
3342
  }
2205
3343
  export {
2206
- writeRules,
2207
3344
  writeProviderConfig,
2208
3345
  writeMcpJson,
2209
3346
  writeEnabledProviders,
2210
3347
  writeConfig,
2211
3348
  writeActiveProfileState,
2212
3349
  version,
3350
+ validateHooksConfig,
3351
+ validateHook,
2213
3352
  validateEnv,
3353
+ transformToOmnidev,
3354
+ transformToClaude,
3355
+ transformHooksConfig,
2214
3356
  syncMcpJson,
2215
3357
  syncAgentConfiguration,
2216
3358
  sourceToGitUrl,
@@ -2221,11 +3363,18 @@ export {
2221
3363
  resolveEnabledCapabilities,
2222
3364
  readMcpJson,
2223
3365
  readEnabledProviders,
3366
+ readCapabilityIdFromPath,
2224
3367
  readActiveProfileState,
3368
+ patchAddToProfile,
3369
+ patchAddMcp,
3370
+ patchAddCapabilitySource,
2225
3371
  parseSourceConfig,
2226
3372
  parseProviderFlag,
2227
3373
  parseOmniConfig,
3374
+ parseFileSourcePath,
2228
3375
  parseCapabilityConfig,
3376
+ mergeHooksConfigs,
3377
+ mergeAndDeduplicateHooks,
2229
3378
  loadSubagents,
2230
3379
  loadSkills,
2231
3380
  loadRules,
@@ -2233,26 +3382,48 @@ export {
2233
3382
  loadProfileConfig,
2234
3383
  loadManifest,
2235
3384
  loadLockFile,
3385
+ loadHooksFromCapability,
2236
3386
  loadEnvironment,
2237
3387
  loadDocs,
2238
3388
  loadConfig,
2239
3389
  loadCommands,
3390
+ loadCapabilityHooks,
2240
3391
  loadCapabilityConfig,
2241
3392
  loadCapability,
2242
3393
  loadBaseConfig,
3394
+ isValidMatcherPattern,
2243
3395
  isSecretEnvVar,
2244
3396
  isProviderEnabled,
3397
+ isPromptHookEvent,
3398
+ isMatcherEvent,
3399
+ isHookType,
3400
+ isHookPrompt,
3401
+ isHookEvent,
3402
+ isHookCommand,
3403
+ isGitSource,
3404
+ isFileSourceConfig,
3405
+ isFileSource,
2245
3406
  installCapabilityDependencies,
3407
+ hasHooks,
3408
+ hasAnyHooks,
2246
3409
  getVersion,
2247
3410
  getSourceCapabilityPath,
2248
3411
  getLockFilePath,
3412
+ getHooksDirectory,
3413
+ getHooksConfigPath,
3414
+ getEventsWithHooks,
2249
3415
  getEnabledCapabilities,
2250
3416
  getActiveProviders,
2251
3417
  getActiveProfile,
3418
+ generateSkillTemplate,
3419
+ generateRuleTemplate,
2252
3420
  generateOmniMdTemplate,
2253
- generateInstructionsTemplate,
3421
+ generateHooksTemplate,
3422
+ generateHookScript,
2254
3423
  generateClaudeTemplate,
3424
+ generateCapabilityToml2 as generateCapabilityToml,
2255
3425
  generateAgentsTemplate,
3426
+ findDuplicateCommands,
2256
3427
  fetchCapabilitySource,
2257
3428
  fetchAllCapabilitySources,
2258
3429
  enableProvider,
@@ -2261,6 +3432,11 @@ export {
2261
3432
  disableProvider,
2262
3433
  disableCapability,
2263
3434
  debug,
3435
+ createEmptyValidationResult,
3436
+ createEmptyHooksConfig,
3437
+ countHooks,
3438
+ containsOmnidevVariables,
3439
+ containsClaudeVariables,
2264
3440
  clearActiveProfileState,
2265
3441
  cleanupStaleResources,
2266
3442
  checkForUpdates,
@@ -2268,5 +3444,18 @@ export {
2268
3444
  buildRouteMap,
2269
3445
  buildManifestFromCapabilities,
2270
3446
  buildCommand,
2271
- buildCapabilityRegistry
3447
+ buildCapabilityRegistry,
3448
+ VARIABLE_MAPPINGS,
3449
+ SESSION_START_MATCHERS,
3450
+ PROMPT_HOOK_EVENTS,
3451
+ PRE_COMPACT_MATCHERS,
3452
+ NOTIFICATION_MATCHERS,
3453
+ MATCHER_EVENTS,
3454
+ HOOK_TYPES,
3455
+ HOOK_EVENTS,
3456
+ HOOKS_DIRECTORY,
3457
+ HOOKS_CONFIG_FILENAME,
3458
+ DEFAULT_PROMPT_TIMEOUT,
3459
+ DEFAULT_COMMAND_TIMEOUT,
3460
+ COMMON_TOOL_MATCHERS
2272
3461
  };