@omnidev-ai/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +331 -1
- package/dist/index.js +1200 -136
- package/package.json +1 -1
- package/src/capability/loader.ts +9 -0
- package/src/capability/registry.ts +19 -1
- package/src/capability/sources.ts +36 -1
- package/src/config/capabilities.ts +1 -1
- package/src/config/{loader.ts → config.ts} +25 -0
- package/src/config/index.ts +2 -1
- package/src/config/profiles.ts +23 -3
- package/src/config/toml-patcher.ts +309 -0
- package/src/hooks/constants.ts +100 -0
- package/src/hooks/index.ts +99 -0
- package/src/hooks/loader.ts +189 -0
- package/src/hooks/merger.ts +157 -0
- package/src/hooks/types.ts +212 -0
- package/src/hooks/validation.ts +516 -0
- package/src/hooks/variables.ts +151 -0
- package/src/index.ts +3 -0
- package/src/sync.ts +10 -1
- package/src/types/index.ts +23 -0
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
|
|
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
|
|
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
|
|
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
|
|
859
|
+
import { basename as basename2, join as join4 } from "node:path";
|
|
214
860
|
async function loadRules(capabilityPath, capabilityId) {
|
|
215
|
-
const rulesDir =
|
|
216
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
958
|
+
import { join as join5 } from "node:path";
|
|
313
959
|
async function loadSkills(capabilityPath, capabilityId) {
|
|
314
|
-
const skillsDir =
|
|
315
|
-
if (!
|
|
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 =
|
|
323
|
-
if (
|
|
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
|
|
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
|
|
999
|
+
import { join as join6 } from "node:path";
|
|
354
1000
|
async function loadSubagents(capabilityPath, capabilityId) {
|
|
355
|
-
const subagentsDir =
|
|
356
|
-
if (!
|
|
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 =
|
|
364
|
-
if (
|
|
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 (
|
|
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 =
|
|
422
|
-
const configPath =
|
|
423
|
-
if (
|
|
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 =
|
|
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 =
|
|
439
|
-
if (!
|
|
1084
|
+
const indexPath = join7(capabilityPath, "index.ts");
|
|
1085
|
+
if (!existsSync9(indexPath)) {
|
|
440
1086
|
return {};
|
|
441
1087
|
}
|
|
442
1088
|
try {
|
|
443
|
-
const absolutePath =
|
|
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 =
|
|
459
|
-
if (!
|
|
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/
|
|
675
|
-
import { existsSync as
|
|
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 (
|
|
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 (
|
|
1354
|
+
if (existsSync10(LOCAL_CONFIG)) {
|
|
705
1355
|
const content = await readFile8(LOCAL_CONFIG, "utf-8");
|
|
706
1356
|
localConfig = parseOmniConfig(content);
|
|
707
1357
|
}
|
|
@@ -784,6 +1434,28 @@ function generateConfigToml(config) {
|
|
|
784
1434
|
}
|
|
785
1435
|
lines.push("");
|
|
786
1436
|
lines.push("# =============================================================================");
|
|
1437
|
+
lines.push("# Capability Groups");
|
|
1438
|
+
lines.push("# =============================================================================");
|
|
1439
|
+
lines.push("# Bundle multiple capabilities under a single name for cleaner profiles.");
|
|
1440
|
+
lines.push('# Reference groups in profiles with the "group:" prefix.');
|
|
1441
|
+
lines.push("#");
|
|
1442
|
+
const groups = config.capabilities?.groups;
|
|
1443
|
+
if (groups && Object.keys(groups).length > 0) {
|
|
1444
|
+
lines.push("[capabilities.groups]");
|
|
1445
|
+
for (const [name, caps] of Object.entries(groups)) {
|
|
1446
|
+
const capsStr = caps.map((c) => `"${c}"`).join(", ");
|
|
1447
|
+
lines.push(`${name} = [${capsStr}]`);
|
|
1448
|
+
}
|
|
1449
|
+
} else {
|
|
1450
|
+
lines.push("# [capabilities.groups]");
|
|
1451
|
+
lines.push('# expo = ["expo-app-design", "expo-deployment", "upgrading-expo"]');
|
|
1452
|
+
lines.push('# backend = ["cloudflare", "database-tools"]');
|
|
1453
|
+
lines.push("#");
|
|
1454
|
+
lines.push("# [profiles.mobile]");
|
|
1455
|
+
lines.push('# capabilities = ["group:expo", "react-native-tools"]');
|
|
1456
|
+
}
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
lines.push("# =============================================================================");
|
|
787
1459
|
lines.push("# MCP Servers");
|
|
788
1460
|
lines.push("# =============================================================================");
|
|
789
1461
|
lines.push("# Define MCP servers that automatically become capabilities.");
|
|
@@ -873,12 +1545,12 @@ function generateConfigToml(config) {
|
|
|
873
1545
|
}
|
|
874
1546
|
|
|
875
1547
|
// src/state/active-profile.ts
|
|
876
|
-
import { existsSync as
|
|
1548
|
+
import { existsSync as existsSync11, mkdirSync } from "node:fs";
|
|
877
1549
|
import { readFile as readFile9, unlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
878
1550
|
var STATE_DIR = ".omni/state";
|
|
879
1551
|
var ACTIVE_PROFILE_PATH = `${STATE_DIR}/active-profile`;
|
|
880
1552
|
async function readActiveProfileState() {
|
|
881
|
-
if (!
|
|
1553
|
+
if (!existsSync11(ACTIVE_PROFILE_PATH)) {
|
|
882
1554
|
return null;
|
|
883
1555
|
}
|
|
884
1556
|
try {
|
|
@@ -894,7 +1566,7 @@ async function writeActiveProfileState(profileName) {
|
|
|
894
1566
|
await writeFile3(ACTIVE_PROFILE_PATH, profileName, "utf-8");
|
|
895
1567
|
}
|
|
896
1568
|
async function clearActiveProfileState() {
|
|
897
|
-
if (
|
|
1569
|
+
if (existsSync11(ACTIVE_PROFILE_PATH)) {
|
|
898
1570
|
await unlink(ACTIVE_PROFILE_PATH);
|
|
899
1571
|
}
|
|
900
1572
|
}
|
|
@@ -915,7 +1587,24 @@ function resolveEnabledCapabilities(config, profileName) {
|
|
|
915
1587
|
const profile = profileName ? config.profiles?.[profileName] : config.profiles?.[config.active_profile ?? "default"];
|
|
916
1588
|
const profileCapabilities = profile?.capabilities ?? [];
|
|
917
1589
|
const alwaysEnabled = config.always_enabled_capabilities ?? [];
|
|
918
|
-
|
|
1590
|
+
const groups = config.capabilities?.groups ?? {};
|
|
1591
|
+
const expandCapabilities = (caps) => {
|
|
1592
|
+
return caps.flatMap((cap) => {
|
|
1593
|
+
if (cap.startsWith("group:")) {
|
|
1594
|
+
const groupName = cap.slice(6);
|
|
1595
|
+
const groupCaps = groups[groupName];
|
|
1596
|
+
if (!groupCaps) {
|
|
1597
|
+
console.warn(`Unknown capability group: ${groupName}`);
|
|
1598
|
+
return [];
|
|
1599
|
+
}
|
|
1600
|
+
return groupCaps;
|
|
1601
|
+
}
|
|
1602
|
+
return cap;
|
|
1603
|
+
});
|
|
1604
|
+
};
|
|
1605
|
+
const expandedAlways = expandCapabilities(alwaysEnabled);
|
|
1606
|
+
const expandedProfile = expandCapabilities(profileCapabilities);
|
|
1607
|
+
return [...new Set([...expandedAlways, ...expandedProfile])];
|
|
919
1608
|
}
|
|
920
1609
|
async function loadProfileConfig(profileName) {
|
|
921
1610
|
const config = await loadConfig();
|
|
@@ -962,6 +1651,93 @@ async function disableCapability(capabilityId) {
|
|
|
962
1651
|
await writeConfig(config);
|
|
963
1652
|
}
|
|
964
1653
|
|
|
1654
|
+
// src/hooks/merger.ts
|
|
1655
|
+
function mergeHooksConfigs(capabilityHooks) {
|
|
1656
|
+
const result = {};
|
|
1657
|
+
for (const event of HOOK_EVENTS) {
|
|
1658
|
+
const allMatchers = [];
|
|
1659
|
+
for (const capHooks of capabilityHooks) {
|
|
1660
|
+
const matchers = capHooks.config[event];
|
|
1661
|
+
if (matchers && matchers.length > 0) {
|
|
1662
|
+
allMatchers.push(...matchers);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (allMatchers.length > 0) {
|
|
1666
|
+
result[event] = allMatchers;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return result;
|
|
1670
|
+
}
|
|
1671
|
+
function mergeAndDeduplicateHooks(capabilityHooks, options) {
|
|
1672
|
+
const merged = mergeHooksConfigs(capabilityHooks);
|
|
1673
|
+
if (!options?.deduplicateCommands) {
|
|
1674
|
+
return merged;
|
|
1675
|
+
}
|
|
1676
|
+
const result = {};
|
|
1677
|
+
for (const event of HOOK_EVENTS) {
|
|
1678
|
+
const matchers = merged[event];
|
|
1679
|
+
if (!matchers || matchers.length === 0) {
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
const seenCommands = new Set;
|
|
1683
|
+
const deduplicatedMatchers = [];
|
|
1684
|
+
for (const matcher of matchers) {
|
|
1685
|
+
const deduplicatedHooks = matcher.hooks.filter((hook) => {
|
|
1686
|
+
if (hook.type !== "command") {
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
const key = hook.command;
|
|
1690
|
+
if (seenCommands.has(key)) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
seenCommands.add(key);
|
|
1694
|
+
return true;
|
|
1695
|
+
});
|
|
1696
|
+
if (deduplicatedHooks.length > 0) {
|
|
1697
|
+
deduplicatedMatchers.push({
|
|
1698
|
+
...matcher,
|
|
1699
|
+
hooks: deduplicatedHooks
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (deduplicatedMatchers.length > 0) {
|
|
1704
|
+
result[event] = deduplicatedMatchers;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return result;
|
|
1708
|
+
}
|
|
1709
|
+
function hasAnyHooks(config) {
|
|
1710
|
+
for (const event of HOOK_EVENTS) {
|
|
1711
|
+
const matchers = config[event];
|
|
1712
|
+
if (matchers && matchers.length > 0) {
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
function countHooks(config) {
|
|
1719
|
+
let count = 0;
|
|
1720
|
+
for (const event of HOOK_EVENTS) {
|
|
1721
|
+
const matchers = config[event];
|
|
1722
|
+
if (matchers) {
|
|
1723
|
+
for (const matcher of matchers) {
|
|
1724
|
+
count += matcher.hooks.length;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return count;
|
|
1729
|
+
}
|
|
1730
|
+
function getEventsWithHooks(config) {
|
|
1731
|
+
const events = [];
|
|
1732
|
+
for (const event of HOOK_EVENTS) {
|
|
1733
|
+
const matchers = config[event];
|
|
1734
|
+
if (matchers && matchers.length > 0) {
|
|
1735
|
+
events.push(event);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return events;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
965
1741
|
// src/capability/registry.ts
|
|
966
1742
|
async function buildCapabilityRegistry() {
|
|
967
1743
|
const env = await loadEnvironment();
|
|
@@ -980,21 +1756,32 @@ async function buildCapabilityRegistry() {
|
|
|
980
1756
|
console.warn(` ${errorMessage}`);
|
|
981
1757
|
}
|
|
982
1758
|
}
|
|
1759
|
+
const getAllCapabilityHooks = () => {
|
|
1760
|
+
const hooks = [];
|
|
1761
|
+
for (const cap of capabilities.values()) {
|
|
1762
|
+
if (cap.hooks) {
|
|
1763
|
+
hooks.push(cap.hooks);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return hooks;
|
|
1767
|
+
};
|
|
983
1768
|
return {
|
|
984
1769
|
capabilities,
|
|
985
1770
|
getCapability: (id) => capabilities.get(id),
|
|
986
1771
|
getAllCapabilities: () => [...capabilities.values()],
|
|
987
1772
|
getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
|
|
988
1773
|
getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
|
|
989
|
-
getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs)
|
|
1774
|
+
getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
|
|
1775
|
+
getAllCapabilityHooks,
|
|
1776
|
+
getMergedHooks: () => mergeHooksConfigs(getAllCapabilityHooks())
|
|
990
1777
|
};
|
|
991
1778
|
}
|
|
992
1779
|
// src/capability/sources.ts
|
|
993
|
-
import { existsSync as
|
|
1780
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
994
1781
|
import { spawn } from "node:child_process";
|
|
995
|
-
import { cp, mkdir, readdir, readFile as readFile10, rm, stat, writeFile as writeFile4 } from "node:fs/promises";
|
|
996
|
-
import { join as
|
|
997
|
-
import { parse as
|
|
1782
|
+
import { cp, mkdir, readdir, readFile as readFile10, rename, rm, stat, writeFile as writeFile4 } from "node:fs/promises";
|
|
1783
|
+
import { join as join8 } from "node:path";
|
|
1784
|
+
import { parse as parseToml2 } from "smol-toml";
|
|
998
1785
|
var OMNI_LOCAL = ".omni";
|
|
999
1786
|
var SKILL_DIRS = ["skills", "skill"];
|
|
1000
1787
|
var AGENT_DIRS = ["agents", "agent", "subagents", "subagent"];
|
|
@@ -1005,7 +1792,7 @@ var SKILL_FILES = ["SKILL.md", "skill.md", "Skill.md"];
|
|
|
1005
1792
|
var AGENT_FILES = ["AGENT.md", "agent.md", "Agent.md", "SUBAGENT.md", "subagent.md"];
|
|
1006
1793
|
var COMMAND_FILES = ["COMMAND.md", "command.md", "Command.md"];
|
|
1007
1794
|
async function spawnCapture(command, args, options) {
|
|
1008
|
-
return await new Promise((
|
|
1795
|
+
return await new Promise((resolve2, reject) => {
|
|
1009
1796
|
const child = spawn(command, args, {
|
|
1010
1797
|
cwd: options?.cwd,
|
|
1011
1798
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1022,7 +1809,7 @@ async function spawnCapture(command, args, options) {
|
|
|
1022
1809
|
});
|
|
1023
1810
|
child.on("error", (error) => reject(error));
|
|
1024
1811
|
child.on("close", (exitCode) => {
|
|
1025
|
-
|
|
1812
|
+
resolve2({ exitCode: exitCode ?? 0, stdout, stderr });
|
|
1026
1813
|
});
|
|
1027
1814
|
});
|
|
1028
1815
|
}
|
|
@@ -1051,19 +1838,19 @@ function sourceToGitUrl(source) {
|
|
|
1051
1838
|
return source;
|
|
1052
1839
|
}
|
|
1053
1840
|
function getSourceCapabilityPath(id) {
|
|
1054
|
-
return
|
|
1841
|
+
return join8(OMNI_LOCAL, "capabilities", id);
|
|
1055
1842
|
}
|
|
1056
1843
|
function getLockFilePath() {
|
|
1057
1844
|
return "omni.lock.toml";
|
|
1058
1845
|
}
|
|
1059
1846
|
async function loadLockFile() {
|
|
1060
1847
|
const lockPath = getLockFilePath();
|
|
1061
|
-
if (!
|
|
1848
|
+
if (!existsSync12(lockPath)) {
|
|
1062
1849
|
return { capabilities: {} };
|
|
1063
1850
|
}
|
|
1064
1851
|
try {
|
|
1065
1852
|
const content = await readFile10(lockPath, "utf-8");
|
|
1066
|
-
const parsed =
|
|
1853
|
+
const parsed = parseToml2(content);
|
|
1067
1854
|
const capabilities = parsed["capabilities"];
|
|
1068
1855
|
return {
|
|
1069
1856
|
capabilities: capabilities || {}
|
|
@@ -1092,7 +1879,7 @@ function stringifyLockFile(lockFile) {
|
|
|
1092
1879
|
}
|
|
1093
1880
|
async function saveLockFile(lockFile) {
|
|
1094
1881
|
const lockPath = getLockFilePath();
|
|
1095
|
-
await mkdir(
|
|
1882
|
+
await mkdir(join8(OMNI_LOCAL, "capabilities"), { recursive: true });
|
|
1096
1883
|
const header = `# Auto-generated by OmniDev - DO NOT EDIT
|
|
1097
1884
|
# Records installed capability versions for reproducibility
|
|
1098
1885
|
# Last updated: ${new Date().toISOString()}
|
|
@@ -1114,7 +1901,7 @@ function shortCommit(commit) {
|
|
|
1114
1901
|
return commit.substring(0, 7);
|
|
1115
1902
|
}
|
|
1116
1903
|
async function cloneRepo(gitUrl, targetPath, ref) {
|
|
1117
|
-
await mkdir(
|
|
1904
|
+
await mkdir(join8(targetPath, ".."), { recursive: true });
|
|
1118
1905
|
const args = ["clone", "--depth", "1"];
|
|
1119
1906
|
if (ref) {
|
|
1120
1907
|
args.push("--branch", ref);
|
|
@@ -1151,16 +1938,16 @@ async function fetchRepo(repoPath, ref) {
|
|
|
1151
1938
|
return true;
|
|
1152
1939
|
}
|
|
1153
1940
|
function hasCapabilityToml(dirPath) {
|
|
1154
|
-
return
|
|
1941
|
+
return existsSync12(join8(dirPath, "capability.toml"));
|
|
1155
1942
|
}
|
|
1156
1943
|
async function shouldWrapDirectory(dirPath) {
|
|
1157
|
-
if (
|
|
1944
|
+
if (existsSync12(join8(dirPath, ".claude-plugin", "plugin.json"))) {
|
|
1158
1945
|
return true;
|
|
1159
1946
|
}
|
|
1160
1947
|
const allDirs = [...SKILL_DIRS, ...AGENT_DIRS, ...COMMAND_DIRS, ...RULE_DIRS, ...DOC_DIRS];
|
|
1161
1948
|
for (const dirName of allDirs) {
|
|
1162
|
-
const checkPath =
|
|
1163
|
-
if (
|
|
1949
|
+
const checkPath = join8(dirPath, dirName);
|
|
1950
|
+
if (existsSync12(checkPath)) {
|
|
1164
1951
|
const stats = await stat(checkPath);
|
|
1165
1952
|
if (stats.isDirectory()) {
|
|
1166
1953
|
return true;
|
|
@@ -1171,8 +1958,8 @@ async function shouldWrapDirectory(dirPath) {
|
|
|
1171
1958
|
}
|
|
1172
1959
|
async function findMatchingDirs(basePath, names) {
|
|
1173
1960
|
for (const name of names) {
|
|
1174
|
-
const dirPath =
|
|
1175
|
-
if (
|
|
1961
|
+
const dirPath = join8(basePath, name);
|
|
1962
|
+
if (existsSync12(dirPath)) {
|
|
1176
1963
|
const stats = await stat(dirPath);
|
|
1177
1964
|
if (stats.isDirectory()) {
|
|
1178
1965
|
return dirPath;
|
|
@@ -1183,15 +1970,15 @@ async function findMatchingDirs(basePath, names) {
|
|
|
1183
1970
|
}
|
|
1184
1971
|
async function findContentItems(dirPath, filePatterns) {
|
|
1185
1972
|
const items = [];
|
|
1186
|
-
if (!
|
|
1973
|
+
if (!existsSync12(dirPath)) {
|
|
1187
1974
|
return items;
|
|
1188
1975
|
}
|
|
1189
1976
|
const entries = (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1190
1977
|
for (const entry of entries) {
|
|
1191
|
-
const entryPath =
|
|
1978
|
+
const entryPath = join8(dirPath, entry.name);
|
|
1192
1979
|
if (entry.isDirectory()) {
|
|
1193
1980
|
for (const pattern of filePatterns) {
|
|
1194
|
-
if (
|
|
1981
|
+
if (existsSync12(join8(entryPath, pattern))) {
|
|
1195
1982
|
items.push({
|
|
1196
1983
|
name: entry.name,
|
|
1197
1984
|
path: entryPath,
|
|
@@ -1212,8 +1999,8 @@ async function findContentItems(dirPath, filePatterns) {
|
|
|
1212
1999
|
return items;
|
|
1213
2000
|
}
|
|
1214
2001
|
async function parsePluginJson(dirPath) {
|
|
1215
|
-
const pluginJsonPath =
|
|
1216
|
-
if (!
|
|
2002
|
+
const pluginJsonPath = join8(dirPath, ".claude-plugin", "plugin.json");
|
|
2003
|
+
if (!existsSync12(pluginJsonPath)) {
|
|
1217
2004
|
return null;
|
|
1218
2005
|
}
|
|
1219
2006
|
try {
|
|
@@ -1237,8 +2024,8 @@ async function parsePluginJson(dirPath) {
|
|
|
1237
2024
|
}
|
|
1238
2025
|
}
|
|
1239
2026
|
async function readReadmeDescription(dirPath) {
|
|
1240
|
-
const readmePath =
|
|
1241
|
-
if (!
|
|
2027
|
+
const readmePath = join8(dirPath, "README.md");
|
|
2028
|
+
if (!existsSync12(readmePath)) {
|
|
1242
2029
|
return null;
|
|
1243
2030
|
}
|
|
1244
2031
|
try {
|
|
@@ -1270,6 +2057,29 @@ async function readReadmeDescription(dirPath) {
|
|
|
1270
2057
|
return null;
|
|
1271
2058
|
}
|
|
1272
2059
|
}
|
|
2060
|
+
async function normalizeFolderNames(repoPath) {
|
|
2061
|
+
const renameMappings = [
|
|
2062
|
+
{ from: "skill", to: "skills" },
|
|
2063
|
+
{ from: "command", to: "commands" },
|
|
2064
|
+
{ from: "rule", to: "rules" },
|
|
2065
|
+
{ from: "agent", to: "agents" },
|
|
2066
|
+
{ from: "subagent", to: "subagents" }
|
|
2067
|
+
];
|
|
2068
|
+
for (const { from, to } of renameMappings) {
|
|
2069
|
+
const fromPath = join8(repoPath, from);
|
|
2070
|
+
const toPath = join8(repoPath, to);
|
|
2071
|
+
if (existsSync12(fromPath) && !existsSync12(toPath)) {
|
|
2072
|
+
try {
|
|
2073
|
+
const stats = await stat(fromPath);
|
|
2074
|
+
if (stats.isDirectory()) {
|
|
2075
|
+
await rename(fromPath, toPath);
|
|
2076
|
+
}
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
console.warn(`Failed to rename ${from} to ${to}:`, error);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
1273
2083
|
async function discoverContent(repoPath) {
|
|
1274
2084
|
const result = {
|
|
1275
2085
|
skills: [],
|
|
@@ -1347,7 +2157,7 @@ repository = "${repoUrl}"
|
|
|
1347
2157
|
wrapped = true
|
|
1348
2158
|
commit = "${commit}"
|
|
1349
2159
|
`;
|
|
1350
|
-
await writeFile4(
|
|
2160
|
+
await writeFile4(join8(repoPath, "capability.toml"), tomlContent, "utf-8");
|
|
1351
2161
|
}
|
|
1352
2162
|
async function fetchGitCapabilitySource(id, config, options) {
|
|
1353
2163
|
const gitUrl = sourceToGitUrl(config.source);
|
|
@@ -1356,8 +2166,8 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
1356
2166
|
let commit;
|
|
1357
2167
|
let repoPath;
|
|
1358
2168
|
if (config.path) {
|
|
1359
|
-
const tempPath =
|
|
1360
|
-
if (
|
|
2169
|
+
const tempPath = join8(OMNI_LOCAL, "_temp", `${id}-repo`);
|
|
2170
|
+
if (existsSync12(join8(tempPath, ".git"))) {
|
|
1361
2171
|
if (!options?.silent) {
|
|
1362
2172
|
console.log(` Checking ${id}...`);
|
|
1363
2173
|
}
|
|
@@ -1367,23 +2177,23 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
1367
2177
|
if (!options?.silent) {
|
|
1368
2178
|
console.log(` Cloning ${id} from ${config.source}...`);
|
|
1369
2179
|
}
|
|
1370
|
-
await mkdir(
|
|
2180
|
+
await mkdir(join8(tempPath, ".."), { recursive: true });
|
|
1371
2181
|
await cloneRepo(gitUrl, tempPath, config.ref);
|
|
1372
2182
|
commit = await getRepoCommit(tempPath);
|
|
1373
2183
|
updated = true;
|
|
1374
2184
|
}
|
|
1375
|
-
const sourcePath =
|
|
1376
|
-
if (!
|
|
2185
|
+
const sourcePath = join8(tempPath, config.path);
|
|
2186
|
+
if (!existsSync12(sourcePath)) {
|
|
1377
2187
|
throw new Error(`Path not found in repository: ${config.path}`);
|
|
1378
2188
|
}
|
|
1379
|
-
if (
|
|
2189
|
+
if (existsSync12(targetPath)) {
|
|
1380
2190
|
await rm(targetPath, { recursive: true });
|
|
1381
2191
|
}
|
|
1382
|
-
await mkdir(
|
|
2192
|
+
await mkdir(join8(targetPath, ".."), { recursive: true });
|
|
1383
2193
|
await cp(sourcePath, targetPath, { recursive: true });
|
|
1384
2194
|
repoPath = targetPath;
|
|
1385
2195
|
} else {
|
|
1386
|
-
if (
|
|
2196
|
+
if (existsSync12(join8(targetPath, ".git"))) {
|
|
1387
2197
|
if (!options?.silent) {
|
|
1388
2198
|
console.log(` Checking ${id}...`);
|
|
1389
2199
|
}
|
|
@@ -1404,6 +2214,7 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
1404
2214
|
needsWrap = await shouldWrapDirectory(repoPath);
|
|
1405
2215
|
}
|
|
1406
2216
|
if (needsWrap) {
|
|
2217
|
+
await normalizeFolderNames(repoPath);
|
|
1407
2218
|
const content = await discoverContent(repoPath);
|
|
1408
2219
|
await generateCapabilityToml(id, repoPath, config.source, commit, content);
|
|
1409
2220
|
if (!options?.silent) {
|
|
@@ -1420,8 +2231,8 @@ async function fetchGitCapabilitySource(id, config, options) {
|
|
|
1420
2231
|
}
|
|
1421
2232
|
}
|
|
1422
2233
|
let version = shortCommit(commit);
|
|
1423
|
-
const pkgJsonPath =
|
|
1424
|
-
if (
|
|
2234
|
+
const pkgJsonPath = join8(repoPath, "package.json");
|
|
2235
|
+
if (existsSync12(pkgJsonPath)) {
|
|
1425
2236
|
try {
|
|
1426
2237
|
const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
|
|
1427
2238
|
if (pkgJson.version) {
|
|
@@ -1506,17 +2317,17 @@ generated_from_omni_toml = true
|
|
|
1506
2317
|
}
|
|
1507
2318
|
async function generateMcpCapabilityToml(id, mcpConfig, targetPath) {
|
|
1508
2319
|
const tomlContent = generateMcpCapabilityTomlContent(id, mcpConfig);
|
|
1509
|
-
await writeFile4(
|
|
2320
|
+
await writeFile4(join8(targetPath, "capability.toml"), tomlContent, "utf-8");
|
|
1510
2321
|
}
|
|
1511
2322
|
async function isGeneratedMcpCapability(capabilityDir) {
|
|
1512
|
-
const tomlPath =
|
|
1513
|
-
if (!
|
|
2323
|
+
const tomlPath = join8(capabilityDir, "capability.toml");
|
|
2324
|
+
if (!existsSync12(tomlPath)) {
|
|
1514
2325
|
console.warn("no capability.toml found in", capabilityDir);
|
|
1515
2326
|
return false;
|
|
1516
2327
|
}
|
|
1517
2328
|
try {
|
|
1518
2329
|
const content = await readFile10(tomlPath, "utf-8");
|
|
1519
|
-
const parsed =
|
|
2330
|
+
const parsed = parseToml2(content);
|
|
1520
2331
|
const capability = parsed["capability"];
|
|
1521
2332
|
const metadata = capability?.["metadata"];
|
|
1522
2333
|
return metadata?.["generated_from_omni_toml"] === true;
|
|
@@ -1525,14 +2336,14 @@ async function isGeneratedMcpCapability(capabilityDir) {
|
|
|
1525
2336
|
}
|
|
1526
2337
|
}
|
|
1527
2338
|
async function cleanupStaleMcpCapabilities(currentMcpIds) {
|
|
1528
|
-
const capabilitiesDir =
|
|
1529
|
-
if (!
|
|
2339
|
+
const capabilitiesDir = join8(OMNI_LOCAL, "capabilities");
|
|
2340
|
+
if (!existsSync12(capabilitiesDir)) {
|
|
1530
2341
|
return;
|
|
1531
2342
|
}
|
|
1532
2343
|
const entries = await readdir(capabilitiesDir, { withFileTypes: true });
|
|
1533
2344
|
for (const entry of entries) {
|
|
1534
2345
|
if (entry.isDirectory()) {
|
|
1535
|
-
const capDir =
|
|
2346
|
+
const capDir = join8(capabilitiesDir, entry.name);
|
|
1536
2347
|
const isGenerated = await isGeneratedMcpCapability(capDir);
|
|
1537
2348
|
if (isGenerated && !currentMcpIds.has(entry.name)) {
|
|
1538
2349
|
await rm(capDir, { recursive: true });
|
|
@@ -1545,10 +2356,10 @@ async function generateMcpCapabilities(config) {
|
|
|
1545
2356
|
await cleanupStaleMcpCapabilities(new Set);
|
|
1546
2357
|
return;
|
|
1547
2358
|
}
|
|
1548
|
-
const mcpCapabilitiesDir =
|
|
2359
|
+
const mcpCapabilitiesDir = join8(OMNI_LOCAL, "capabilities");
|
|
1549
2360
|
const currentMcpIds = new Set;
|
|
1550
2361
|
for (const [id, mcpConfig] of Object.entries(config.mcps)) {
|
|
1551
|
-
const targetPath =
|
|
2362
|
+
const targetPath = join8(mcpCapabilitiesDir, id);
|
|
1552
2363
|
currentMcpIds.add(id);
|
|
1553
2364
|
await mkdir(targetPath, { recursive: true });
|
|
1554
2365
|
await generateMcpCapabilityToml(id, mcpConfig, targetPath);
|
|
@@ -1622,7 +2433,7 @@ async function checkForUpdates(config) {
|
|
|
1622
2433
|
const targetPath = getSourceCapabilityPath(id);
|
|
1623
2434
|
const existing = lockFile.capabilities[id];
|
|
1624
2435
|
const gitConfig = sourceConfig;
|
|
1625
|
-
if (!
|
|
2436
|
+
if (!existsSync12(join8(targetPath, ".git"))) {
|
|
1626
2437
|
updates.push({
|
|
1627
2438
|
id,
|
|
1628
2439
|
source: gitConfig.source,
|
|
@@ -1658,12 +2469,12 @@ async function checkForUpdates(config) {
|
|
|
1658
2469
|
return updates;
|
|
1659
2470
|
}
|
|
1660
2471
|
// src/config/provider.ts
|
|
1661
|
-
import { existsSync as
|
|
2472
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
1662
2473
|
import { readFile as readFile11, writeFile as writeFile5 } from "node:fs/promises";
|
|
1663
2474
|
import { parse as parse2 } from "smol-toml";
|
|
1664
2475
|
var PROVIDER_CONFIG_PATH = ".omni/provider.toml";
|
|
1665
2476
|
async function loadProviderConfig() {
|
|
1666
|
-
if (!
|
|
2477
|
+
if (!existsSync13(PROVIDER_CONFIG_PATH)) {
|
|
1667
2478
|
return { provider: "claude" };
|
|
1668
2479
|
}
|
|
1669
2480
|
const content = await readFile11(PROVIDER_CONFIG_PATH, "utf-8");
|
|
@@ -1707,16 +2518,222 @@ function parseProviderFlag(flag) {
|
|
|
1707
2518
|
}
|
|
1708
2519
|
throw new Error(`Invalid provider: ${flag}. Must be 'claude', 'codex', or 'both'.`);
|
|
1709
2520
|
}
|
|
1710
|
-
// src/
|
|
1711
|
-
import { existsSync as
|
|
2521
|
+
// src/config/toml-patcher.ts
|
|
2522
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
1712
2523
|
import { readFile as readFile12, writeFile as writeFile6 } from "node:fs/promises";
|
|
2524
|
+
var CONFIG_PATH2 = "omni.toml";
|
|
2525
|
+
async function readConfigFile() {
|
|
2526
|
+
if (!existsSync14(CONFIG_PATH2)) {
|
|
2527
|
+
return "";
|
|
2528
|
+
}
|
|
2529
|
+
return readFile12(CONFIG_PATH2, "utf-8");
|
|
2530
|
+
}
|
|
2531
|
+
async function writeConfigFile(content) {
|
|
2532
|
+
await writeFile6(CONFIG_PATH2, content, "utf-8");
|
|
2533
|
+
}
|
|
2534
|
+
function findSection(lines, sectionPattern) {
|
|
2535
|
+
return lines.findIndex((line) => sectionPattern.test(line.trim()));
|
|
2536
|
+
}
|
|
2537
|
+
function findSectionEnd(lines, startIndex) {
|
|
2538
|
+
for (let i = startIndex + 1;i < lines.length; i++) {
|
|
2539
|
+
const line = lines[i];
|
|
2540
|
+
if (line === undefined)
|
|
2541
|
+
continue;
|
|
2542
|
+
const trimmed = line.trim();
|
|
2543
|
+
if (/^\[(?!\[)/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
2544
|
+
return i;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return lines.length;
|
|
2548
|
+
}
|
|
2549
|
+
function formatCapabilitySource(name, source) {
|
|
2550
|
+
if (typeof source === "string") {
|
|
2551
|
+
return `${name} = "${source}"`;
|
|
2552
|
+
}
|
|
2553
|
+
if (source.path) {
|
|
2554
|
+
return `${name} = { source = "${source.source}", path = "${source.path}" }`;
|
|
2555
|
+
}
|
|
2556
|
+
return `${name} = "${source.source}"`;
|
|
2557
|
+
}
|
|
2558
|
+
async function patchAddCapabilitySource(name, source) {
|
|
2559
|
+
let content = await readConfigFile();
|
|
2560
|
+
const lines = content.split(`
|
|
2561
|
+
`);
|
|
2562
|
+
const sectionIndex = findSection(lines, /^\[capabilities\.sources\]$/);
|
|
2563
|
+
const newEntry = formatCapabilitySource(name, source);
|
|
2564
|
+
if (sectionIndex !== -1) {
|
|
2565
|
+
const sectionEnd = findSectionEnd(lines, sectionIndex);
|
|
2566
|
+
let insertIndex = sectionEnd;
|
|
2567
|
+
for (let i = sectionEnd - 1;i > sectionIndex; i--) {
|
|
2568
|
+
const line = lines[i];
|
|
2569
|
+
if (line === undefined)
|
|
2570
|
+
continue;
|
|
2571
|
+
const trimmed = line.trim();
|
|
2572
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
2573
|
+
insertIndex = i + 1;
|
|
2574
|
+
break;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
if (insertIndex === sectionEnd && sectionIndex + 1 < lines.length) {
|
|
2578
|
+
insertIndex = sectionIndex + 1;
|
|
2579
|
+
}
|
|
2580
|
+
lines.splice(insertIndex, 0, newEntry);
|
|
2581
|
+
} else {
|
|
2582
|
+
const capabilitiesIndex = findSection(lines, /^\[capabilities\]$/);
|
|
2583
|
+
if (capabilitiesIndex !== -1) {
|
|
2584
|
+
const capEnd = findSectionEnd(lines, capabilitiesIndex);
|
|
2585
|
+
lines.splice(capEnd, 0, "", "[capabilities.sources]", newEntry);
|
|
2586
|
+
} else {
|
|
2587
|
+
const mcpsIndex = findSection(lines, /^\[mcps/);
|
|
2588
|
+
if (mcpsIndex !== -1) {
|
|
2589
|
+
lines.splice(mcpsIndex, 0, "[capabilities.sources]", newEntry, "");
|
|
2590
|
+
} else {
|
|
2591
|
+
lines.push("", "[capabilities.sources]", newEntry);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
content = lines.join(`
|
|
2596
|
+
`);
|
|
2597
|
+
await writeConfigFile(content);
|
|
2598
|
+
}
|
|
2599
|
+
function formatMcpConfig(name, config) {
|
|
2600
|
+
const lines = [];
|
|
2601
|
+
lines.push(`[mcps.${name}]`);
|
|
2602
|
+
if (config.transport && config.transport !== "stdio") {
|
|
2603
|
+
lines.push(`transport = "${config.transport}"`);
|
|
2604
|
+
}
|
|
2605
|
+
if (config.command) {
|
|
2606
|
+
lines.push(`command = "${config.command}"`);
|
|
2607
|
+
}
|
|
2608
|
+
if (config.args && config.args.length > 0) {
|
|
2609
|
+
const argsStr = config.args.map((a) => `"${a}"`).join(", ");
|
|
2610
|
+
lines.push(`args = [${argsStr}]`);
|
|
2611
|
+
}
|
|
2612
|
+
if (config.cwd) {
|
|
2613
|
+
lines.push(`cwd = "${config.cwd}"`);
|
|
2614
|
+
}
|
|
2615
|
+
if (config.url) {
|
|
2616
|
+
lines.push(`url = "${config.url}"`);
|
|
2617
|
+
}
|
|
2618
|
+
if (config.env && Object.keys(config.env).length > 0) {
|
|
2619
|
+
lines.push(`[mcps.${name}.env]`);
|
|
2620
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
2621
|
+
lines.push(`${key} = "${value}"`);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
2625
|
+
lines.push(`[mcps.${name}.headers]`);
|
|
2626
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
2627
|
+
lines.push(`${key} = "${value}"`);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return lines;
|
|
2631
|
+
}
|
|
2632
|
+
async function patchAddMcp(name, config) {
|
|
2633
|
+
let content = await readConfigFile();
|
|
2634
|
+
const lines = content.split(`
|
|
2635
|
+
`);
|
|
2636
|
+
const mcpLines = formatMcpConfig(name, config);
|
|
2637
|
+
const existingMcpIndex = findSection(lines, /^\[mcps\./);
|
|
2638
|
+
if (existingMcpIndex !== -1) {
|
|
2639
|
+
let lastMcpEnd = existingMcpIndex;
|
|
2640
|
+
for (let i = existingMcpIndex;i < lines.length; i++) {
|
|
2641
|
+
const line = lines[i];
|
|
2642
|
+
if (line === undefined)
|
|
2643
|
+
continue;
|
|
2644
|
+
const trimmed = line.trim();
|
|
2645
|
+
if (/^\[mcps\./.test(trimmed)) {
|
|
2646
|
+
lastMcpEnd = findSectionEnd(lines, i);
|
|
2647
|
+
} else if (/^\[(?!mcps\.)/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
lines.splice(lastMcpEnd, 0, "", ...mcpLines);
|
|
2652
|
+
} else {
|
|
2653
|
+
const profilesIndex = findSection(lines, /^\[profiles\./);
|
|
2654
|
+
if (profilesIndex !== -1) {
|
|
2655
|
+
lines.splice(profilesIndex, 0, ...mcpLines, "");
|
|
2656
|
+
} else {
|
|
2657
|
+
lines.push("", ...mcpLines);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
content = lines.join(`
|
|
2661
|
+
`);
|
|
2662
|
+
await writeConfigFile(content);
|
|
2663
|
+
}
|
|
2664
|
+
async function patchAddToProfile(profileName, capabilityName) {
|
|
2665
|
+
let content = await readConfigFile();
|
|
2666
|
+
const lines = content.split(`
|
|
2667
|
+
`);
|
|
2668
|
+
const profilePattern = new RegExp(`^\\[profiles\\.${escapeRegExp(profileName)}\\]$`);
|
|
2669
|
+
const profileIndex = findSection(lines, profilePattern);
|
|
2670
|
+
if (profileIndex !== -1) {
|
|
2671
|
+
const profileEnd = findSectionEnd(lines, profileIndex);
|
|
2672
|
+
let capabilitiesLineIndex = -1;
|
|
2673
|
+
for (let i = profileIndex + 1;i < profileEnd; i++) {
|
|
2674
|
+
const line = lines[i];
|
|
2675
|
+
if (line === undefined)
|
|
2676
|
+
continue;
|
|
2677
|
+
const trimmed = line.trim();
|
|
2678
|
+
if (trimmed.startsWith("capabilities")) {
|
|
2679
|
+
capabilitiesLineIndex = i;
|
|
2680
|
+
break;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (capabilitiesLineIndex !== -1) {
|
|
2684
|
+
const line = lines[capabilitiesLineIndex];
|
|
2685
|
+
if (line !== undefined) {
|
|
2686
|
+
const match = line.match(/capabilities\s*=\s*\[(.*)\]/);
|
|
2687
|
+
if (match && match[1] !== undefined) {
|
|
2688
|
+
const existingCaps = match[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2689
|
+
const quotedCap = `"${capabilityName}"`;
|
|
2690
|
+
if (!existingCaps.includes(quotedCap)) {
|
|
2691
|
+
existingCaps.push(quotedCap);
|
|
2692
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
2693
|
+
lines[capabilitiesLineIndex] = `${indent}capabilities = [${existingCaps.join(", ")}]`;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
} else {
|
|
2698
|
+
lines.splice(profileIndex + 1, 0, `capabilities = ["${capabilityName}"]`);
|
|
2699
|
+
}
|
|
2700
|
+
} else {
|
|
2701
|
+
const anyProfileIndex = findSection(lines, /^\[profiles\./);
|
|
2702
|
+
if (anyProfileIndex !== -1) {
|
|
2703
|
+
let lastProfileEnd = anyProfileIndex;
|
|
2704
|
+
for (let i = anyProfileIndex;i < lines.length; i++) {
|
|
2705
|
+
const line = lines[i];
|
|
2706
|
+
if (line === undefined)
|
|
2707
|
+
continue;
|
|
2708
|
+
const trimmed = line.trim();
|
|
2709
|
+
if (/^\[profiles\./.test(trimmed)) {
|
|
2710
|
+
lastProfileEnd = findSectionEnd(lines, i);
|
|
2711
|
+
} else if (/^\[(?!profiles\.)/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
lines.splice(lastProfileEnd, 0, "", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
|
|
2716
|
+
} else {
|
|
2717
|
+
lines.push("", `[profiles.${profileName}]`, `capabilities = ["${capabilityName}"]`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
content = lines.join(`
|
|
2721
|
+
`);
|
|
2722
|
+
await writeConfigFile(content);
|
|
2723
|
+
}
|
|
2724
|
+
function escapeRegExp(str) {
|
|
2725
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2726
|
+
}
|
|
2727
|
+
// src/mcp-json/manager.ts
|
|
2728
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
2729
|
+
import { readFile as readFile13, writeFile as writeFile7 } from "node:fs/promises";
|
|
1713
2730
|
var MCP_JSON_PATH = ".mcp.json";
|
|
1714
2731
|
async function readMcpJson() {
|
|
1715
|
-
if (!
|
|
2732
|
+
if (!existsSync15(MCP_JSON_PATH)) {
|
|
1716
2733
|
return { mcpServers: {} };
|
|
1717
2734
|
}
|
|
1718
2735
|
try {
|
|
1719
|
-
const content = await
|
|
2736
|
+
const content = await readFile13(MCP_JSON_PATH, "utf-8");
|
|
1720
2737
|
const parsed = JSON.parse(content);
|
|
1721
2738
|
return {
|
|
1722
2739
|
mcpServers: parsed.mcpServers || {}
|
|
@@ -1725,8 +2742,8 @@ async function readMcpJson() {
|
|
|
1725
2742
|
return { mcpServers: {} };
|
|
1726
2743
|
}
|
|
1727
2744
|
}
|
|
1728
|
-
async function writeMcpJson(
|
|
1729
|
-
await
|
|
2745
|
+
async function writeMcpJson(config2) {
|
|
2746
|
+
await writeFile7(MCP_JSON_PATH, `${JSON.stringify(config2, null, 2)}
|
|
1730
2747
|
`, "utf-8");
|
|
1731
2748
|
}
|
|
1732
2749
|
function buildMcpServerConfig(mcp) {
|
|
@@ -1735,41 +2752,41 @@ function buildMcpServerConfig(mcp) {
|
|
|
1735
2752
|
if (!mcp.url) {
|
|
1736
2753
|
throw new Error("HTTP transport requires a URL");
|
|
1737
2754
|
}
|
|
1738
|
-
const
|
|
2755
|
+
const config3 = {
|
|
1739
2756
|
type: "http",
|
|
1740
2757
|
url: mcp.url
|
|
1741
2758
|
};
|
|
1742
2759
|
if (mcp.headers && Object.keys(mcp.headers).length > 0) {
|
|
1743
|
-
|
|
2760
|
+
config3.headers = mcp.headers;
|
|
1744
2761
|
}
|
|
1745
|
-
return
|
|
2762
|
+
return config3;
|
|
1746
2763
|
}
|
|
1747
2764
|
if (transport === "sse") {
|
|
1748
2765
|
if (!mcp.url) {
|
|
1749
2766
|
throw new Error("SSE transport requires a URL");
|
|
1750
2767
|
}
|
|
1751
|
-
const
|
|
2768
|
+
const config3 = {
|
|
1752
2769
|
type: "sse",
|
|
1753
2770
|
url: mcp.url
|
|
1754
2771
|
};
|
|
1755
2772
|
if (mcp.headers && Object.keys(mcp.headers).length > 0) {
|
|
1756
|
-
|
|
2773
|
+
config3.headers = mcp.headers;
|
|
1757
2774
|
}
|
|
1758
|
-
return
|
|
2775
|
+
return config3;
|
|
1759
2776
|
}
|
|
1760
2777
|
if (!mcp.command) {
|
|
1761
2778
|
throw new Error("stdio transport requires a command");
|
|
1762
2779
|
}
|
|
1763
|
-
const
|
|
2780
|
+
const config2 = {
|
|
1764
2781
|
command: mcp.command
|
|
1765
2782
|
};
|
|
1766
2783
|
if (mcp.args) {
|
|
1767
|
-
|
|
2784
|
+
config2.args = mcp.args;
|
|
1768
2785
|
}
|
|
1769
2786
|
if (mcp.env) {
|
|
1770
|
-
|
|
2787
|
+
config2.env = mcp.env;
|
|
1771
2788
|
}
|
|
1772
|
-
return
|
|
2789
|
+
return config2;
|
|
1773
2790
|
}
|
|
1774
2791
|
async function syncMcpJson(capabilities2, previousManifest, options = {}) {
|
|
1775
2792
|
const mcpJson = await readMcpJson();
|
|
@@ -1795,24 +2812,24 @@ async function syncMcpJson(capabilities2, previousManifest, options = {}) {
|
|
|
1795
2812
|
}
|
|
1796
2813
|
}
|
|
1797
2814
|
// src/state/manifest.ts
|
|
1798
|
-
import { existsSync as
|
|
1799
|
-
import { readFile as
|
|
2815
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync2, rmSync } from "node:fs";
|
|
2816
|
+
import { readFile as readFile14, writeFile as writeFile8 } from "node:fs/promises";
|
|
1800
2817
|
var MANIFEST_PATH = ".omni/state/manifest.json";
|
|
1801
2818
|
var CURRENT_VERSION = 1;
|
|
1802
2819
|
async function loadManifest() {
|
|
1803
|
-
if (!
|
|
2820
|
+
if (!existsSync16(MANIFEST_PATH)) {
|
|
1804
2821
|
return {
|
|
1805
2822
|
version: CURRENT_VERSION,
|
|
1806
2823
|
syncedAt: new Date().toISOString(),
|
|
1807
2824
|
capabilities: {}
|
|
1808
2825
|
};
|
|
1809
2826
|
}
|
|
1810
|
-
const content = await
|
|
2827
|
+
const content = await readFile14(MANIFEST_PATH, "utf-8");
|
|
1811
2828
|
return JSON.parse(content);
|
|
1812
2829
|
}
|
|
1813
2830
|
async function saveManifest(manifest) {
|
|
1814
2831
|
mkdirSync2(".omni/state", { recursive: true });
|
|
1815
|
-
await
|
|
2832
|
+
await writeFile8(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
|
|
1816
2833
|
`, "utf-8");
|
|
1817
2834
|
}
|
|
1818
2835
|
function buildManifestFromCapabilities(capabilities2) {
|
|
@@ -1847,14 +2864,14 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
|
|
|
1847
2864
|
}
|
|
1848
2865
|
for (const skillName of resources.skills) {
|
|
1849
2866
|
const skillDir = `.claude/skills/${skillName}`;
|
|
1850
|
-
if (
|
|
2867
|
+
if (existsSync16(skillDir)) {
|
|
1851
2868
|
rmSync(skillDir, { recursive: true });
|
|
1852
2869
|
result.deletedSkills.push(skillName);
|
|
1853
2870
|
}
|
|
1854
2871
|
}
|
|
1855
2872
|
for (const ruleName of resources.rules) {
|
|
1856
2873
|
const rulePath = `.cursor/rules/omnidev-${ruleName}.mdc`;
|
|
1857
|
-
if (
|
|
2874
|
+
if (existsSync16(rulePath)) {
|
|
1858
2875
|
rmSync(rulePath);
|
|
1859
2876
|
result.deletedRules.push(ruleName);
|
|
1860
2877
|
}
|
|
@@ -1863,17 +2880,17 @@ async function cleanupStaleResources(previousManifest, currentCapabilityIds) {
|
|
|
1863
2880
|
return result;
|
|
1864
2881
|
}
|
|
1865
2882
|
// src/state/providers.ts
|
|
1866
|
-
import { existsSync as
|
|
1867
|
-
import { readFile as
|
|
2883
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync3 } from "node:fs";
|
|
2884
|
+
import { readFile as readFile15, writeFile as writeFile9 } from "node:fs/promises";
|
|
1868
2885
|
var STATE_DIR2 = ".omni/state";
|
|
1869
2886
|
var PROVIDERS_PATH = `${STATE_DIR2}/providers.json`;
|
|
1870
2887
|
var DEFAULT_PROVIDERS = ["claude-code"];
|
|
1871
2888
|
async function readEnabledProviders() {
|
|
1872
|
-
if (!
|
|
2889
|
+
if (!existsSync17(PROVIDERS_PATH)) {
|
|
1873
2890
|
return DEFAULT_PROVIDERS;
|
|
1874
2891
|
}
|
|
1875
2892
|
try {
|
|
1876
|
-
const content = await
|
|
2893
|
+
const content = await readFile15(PROVIDERS_PATH, "utf-8");
|
|
1877
2894
|
const state = JSON.parse(content);
|
|
1878
2895
|
return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
|
|
1879
2896
|
} catch {
|
|
@@ -1883,7 +2900,7 @@ async function readEnabledProviders() {
|
|
|
1883
2900
|
async function writeEnabledProviders(providers) {
|
|
1884
2901
|
mkdirSync3(STATE_DIR2, { recursive: true });
|
|
1885
2902
|
const state = { enabled: providers };
|
|
1886
|
-
await
|
|
2903
|
+
await writeFile9(PROVIDERS_PATH, `${JSON.stringify(state, null, 2)}
|
|
1887
2904
|
`, "utf-8");
|
|
1888
2905
|
}
|
|
1889
2906
|
async function enableProvider(providerId) {
|
|
@@ -1905,18 +2922,18 @@ async function isProviderEnabled(providerId) {
|
|
|
1905
2922
|
import { spawn as spawn2 } from "node:child_process";
|
|
1906
2923
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
1907
2924
|
async function installCapabilityDependencies(silent) {
|
|
1908
|
-
const { existsSync:
|
|
1909
|
-
const { join:
|
|
2925
|
+
const { existsSync: existsSync18, readdirSync: readdirSync7 } = await import("node:fs");
|
|
2926
|
+
const { join: join9 } = await import("node:path");
|
|
1910
2927
|
const capabilitiesDir = ".omni/capabilities";
|
|
1911
|
-
if (!
|
|
2928
|
+
if (!existsSync18(capabilitiesDir)) {
|
|
1912
2929
|
return;
|
|
1913
2930
|
}
|
|
1914
2931
|
const entries = readdirSync7(capabilitiesDir, { withFileTypes: true });
|
|
1915
2932
|
async function commandExists(cmd) {
|
|
1916
|
-
return await new Promise((
|
|
2933
|
+
return await new Promise((resolve2) => {
|
|
1917
2934
|
const proc = spawn2(cmd, ["--version"], { stdio: "ignore" });
|
|
1918
|
-
proc.on("error", () =>
|
|
1919
|
-
proc.on("close", (code) =>
|
|
2935
|
+
proc.on("error", () => resolve2(false));
|
|
2936
|
+
proc.on("close", (code) => resolve2(code === 0));
|
|
1920
2937
|
});
|
|
1921
2938
|
}
|
|
1922
2939
|
const hasBun = await commandExists("bun");
|
|
@@ -1928,16 +2945,16 @@ async function installCapabilityDependencies(silent) {
|
|
|
1928
2945
|
if (!entry.isDirectory()) {
|
|
1929
2946
|
continue;
|
|
1930
2947
|
}
|
|
1931
|
-
const capabilityPath =
|
|
1932
|
-
const packageJsonPath =
|
|
1933
|
-
if (!
|
|
2948
|
+
const capabilityPath = join9(capabilitiesDir, entry.name);
|
|
2949
|
+
const packageJsonPath = join9(capabilityPath, "package.json");
|
|
2950
|
+
if (!existsSync18(packageJsonPath)) {
|
|
1934
2951
|
continue;
|
|
1935
2952
|
}
|
|
1936
2953
|
if (!silent) {
|
|
1937
2954
|
console.log(`Installing dependencies for ${capabilityPath}...`);
|
|
1938
2955
|
}
|
|
1939
|
-
await new Promise((
|
|
1940
|
-
const useNpmCi = hasNpm &&
|
|
2956
|
+
await new Promise((resolve2, reject) => {
|
|
2957
|
+
const useNpmCi = hasNpm && existsSync18(join9(capabilityPath, "package-lock.json"));
|
|
1941
2958
|
const cmd = hasBun ? "bun" : "npm";
|
|
1942
2959
|
const args = hasBun ? ["install"] : useNpmCi ? ["ci"] : ["install"];
|
|
1943
2960
|
const proc = spawn2(cmd, args, {
|
|
@@ -1946,7 +2963,7 @@ async function installCapabilityDependencies(silent) {
|
|
|
1946
2963
|
});
|
|
1947
2964
|
proc.on("close", (code) => {
|
|
1948
2965
|
if (code === 0) {
|
|
1949
|
-
|
|
2966
|
+
resolve2();
|
|
1950
2967
|
} else {
|
|
1951
2968
|
reject(new Error(`Failed to install dependencies for ${capabilityPath}`));
|
|
1952
2969
|
}
|
|
@@ -1959,8 +2976,8 @@ async function installCapabilityDependencies(silent) {
|
|
|
1959
2976
|
}
|
|
1960
2977
|
async function buildSyncBundle(options) {
|
|
1961
2978
|
const silent = options?.silent ?? false;
|
|
1962
|
-
const
|
|
1963
|
-
await fetchAllCapabilitySources(
|
|
2979
|
+
const config2 = await loadConfig();
|
|
2980
|
+
await fetchAllCapabilitySources(config2, { silent });
|
|
1964
2981
|
await installCapabilityDependencies(silent);
|
|
1965
2982
|
const registry = await buildCapabilityRegistry();
|
|
1966
2983
|
const capabilities2 = registry.getAllCapabilities();
|
|
@@ -1969,6 +2986,7 @@ async function buildSyncBundle(options) {
|
|
|
1969
2986
|
const docs = registry.getAllDocs();
|
|
1970
2987
|
const commands = capabilities2.flatMap((c) => c.commands);
|
|
1971
2988
|
const subagents = capabilities2.flatMap((c) => c.subagents);
|
|
2989
|
+
const mergedHooks = registry.getMergedHooks();
|
|
1972
2990
|
const instructionsContent = generateInstructionsContent(rules, docs);
|
|
1973
2991
|
const bundle = {
|
|
1974
2992
|
capabilities: capabilities2,
|
|
@@ -1980,6 +2998,9 @@ async function buildSyncBundle(options) {
|
|
|
1980
2998
|
instructionsPath: ".omni/instructions.md",
|
|
1981
2999
|
instructionsContent
|
|
1982
3000
|
};
|
|
3001
|
+
if (hasAnyHooks(mergedHooks)) {
|
|
3002
|
+
bundle.hooks = mergedHooks;
|
|
3003
|
+
}
|
|
1983
3004
|
return { bundle };
|
|
1984
3005
|
}
|
|
1985
3006
|
async function syncAgentConfiguration(options) {
|
|
@@ -2028,10 +3049,10 @@ async function syncAgentConfiguration(options) {
|
|
|
2028
3049
|
const newManifest = buildManifestFromCapabilities(capabilities2);
|
|
2029
3050
|
await saveManifest(newManifest);
|
|
2030
3051
|
if (adapters.length > 0) {
|
|
2031
|
-
const
|
|
3052
|
+
const config2 = await loadConfig();
|
|
2032
3053
|
const ctx = {
|
|
2033
3054
|
projectRoot: process.cwd(),
|
|
2034
|
-
config
|
|
3055
|
+
config: config2
|
|
2035
3056
|
};
|
|
2036
3057
|
for (const adapter of adapters) {
|
|
2037
3058
|
try {
|
|
@@ -2175,11 +3196,11 @@ function generateOmniMdTemplate() {
|
|
|
2175
3196
|
`;
|
|
2176
3197
|
}
|
|
2177
3198
|
// src/types/index.ts
|
|
2178
|
-
function getActiveProviders(
|
|
2179
|
-
if (
|
|
2180
|
-
return
|
|
2181
|
-
if (
|
|
2182
|
-
return [
|
|
3199
|
+
function getActiveProviders(config2) {
|
|
3200
|
+
if (config2.providers)
|
|
3201
|
+
return config2.providers;
|
|
3202
|
+
if (config2.provider)
|
|
3203
|
+
return [config2.provider];
|
|
2183
3204
|
return ["claude"];
|
|
2184
3205
|
}
|
|
2185
3206
|
// src/debug.ts
|
|
@@ -2210,7 +3231,12 @@ export {
|
|
|
2210
3231
|
writeConfig,
|
|
2211
3232
|
writeActiveProfileState,
|
|
2212
3233
|
version,
|
|
3234
|
+
validateHooksConfig,
|
|
3235
|
+
validateHook,
|
|
2213
3236
|
validateEnv,
|
|
3237
|
+
transformToOmnidev,
|
|
3238
|
+
transformToClaude,
|
|
3239
|
+
transformHooksConfig,
|
|
2214
3240
|
syncMcpJson,
|
|
2215
3241
|
syncAgentConfiguration,
|
|
2216
3242
|
sourceToGitUrl,
|
|
@@ -2222,10 +3248,15 @@ export {
|
|
|
2222
3248
|
readMcpJson,
|
|
2223
3249
|
readEnabledProviders,
|
|
2224
3250
|
readActiveProfileState,
|
|
3251
|
+
patchAddToProfile,
|
|
3252
|
+
patchAddMcp,
|
|
3253
|
+
patchAddCapabilitySource,
|
|
2225
3254
|
parseSourceConfig,
|
|
2226
3255
|
parseProviderFlag,
|
|
2227
3256
|
parseOmniConfig,
|
|
2228
3257
|
parseCapabilityConfig,
|
|
3258
|
+
mergeHooksConfigs,
|
|
3259
|
+
mergeAndDeduplicateHooks,
|
|
2229
3260
|
loadSubagents,
|
|
2230
3261
|
loadSkills,
|
|
2231
3262
|
loadRules,
|
|
@@ -2233,19 +3264,33 @@ export {
|
|
|
2233
3264
|
loadProfileConfig,
|
|
2234
3265
|
loadManifest,
|
|
2235
3266
|
loadLockFile,
|
|
3267
|
+
loadHooksFromCapability,
|
|
2236
3268
|
loadEnvironment,
|
|
2237
3269
|
loadDocs,
|
|
2238
3270
|
loadConfig,
|
|
2239
3271
|
loadCommands,
|
|
3272
|
+
loadCapabilityHooks,
|
|
2240
3273
|
loadCapabilityConfig,
|
|
2241
3274
|
loadCapability,
|
|
2242
3275
|
loadBaseConfig,
|
|
3276
|
+
isValidMatcherPattern,
|
|
2243
3277
|
isSecretEnvVar,
|
|
2244
3278
|
isProviderEnabled,
|
|
3279
|
+
isPromptHookEvent,
|
|
3280
|
+
isMatcherEvent,
|
|
3281
|
+
isHookType,
|
|
3282
|
+
isHookPrompt,
|
|
3283
|
+
isHookEvent,
|
|
3284
|
+
isHookCommand,
|
|
2245
3285
|
installCapabilityDependencies,
|
|
3286
|
+
hasHooks,
|
|
3287
|
+
hasAnyHooks,
|
|
2246
3288
|
getVersion,
|
|
2247
3289
|
getSourceCapabilityPath,
|
|
2248
3290
|
getLockFilePath,
|
|
3291
|
+
getHooksDirectory,
|
|
3292
|
+
getHooksConfigPath,
|
|
3293
|
+
getEventsWithHooks,
|
|
2249
3294
|
getEnabledCapabilities,
|
|
2250
3295
|
getActiveProviders,
|
|
2251
3296
|
getActiveProfile,
|
|
@@ -2253,6 +3298,7 @@ export {
|
|
|
2253
3298
|
generateInstructionsTemplate,
|
|
2254
3299
|
generateClaudeTemplate,
|
|
2255
3300
|
generateAgentsTemplate,
|
|
3301
|
+
findDuplicateCommands,
|
|
2256
3302
|
fetchCapabilitySource,
|
|
2257
3303
|
fetchAllCapabilitySources,
|
|
2258
3304
|
enableProvider,
|
|
@@ -2261,6 +3307,11 @@ export {
|
|
|
2261
3307
|
disableProvider,
|
|
2262
3308
|
disableCapability,
|
|
2263
3309
|
debug,
|
|
3310
|
+
createEmptyValidationResult,
|
|
3311
|
+
createEmptyHooksConfig,
|
|
3312
|
+
countHooks,
|
|
3313
|
+
containsOmnidevVariables,
|
|
3314
|
+
containsClaudeVariables,
|
|
2264
3315
|
clearActiveProfileState,
|
|
2265
3316
|
cleanupStaleResources,
|
|
2266
3317
|
checkForUpdates,
|
|
@@ -2268,5 +3319,18 @@ export {
|
|
|
2268
3319
|
buildRouteMap,
|
|
2269
3320
|
buildManifestFromCapabilities,
|
|
2270
3321
|
buildCommand,
|
|
2271
|
-
buildCapabilityRegistry
|
|
3322
|
+
buildCapabilityRegistry,
|
|
3323
|
+
VARIABLE_MAPPINGS,
|
|
3324
|
+
SESSION_START_MATCHERS,
|
|
3325
|
+
PROMPT_HOOK_EVENTS,
|
|
3326
|
+
PRE_COMPACT_MATCHERS,
|
|
3327
|
+
NOTIFICATION_MATCHERS,
|
|
3328
|
+
MATCHER_EVENTS,
|
|
3329
|
+
HOOK_TYPES,
|
|
3330
|
+
HOOK_EVENTS,
|
|
3331
|
+
HOOKS_DIRECTORY,
|
|
3332
|
+
HOOKS_CONFIG_FILENAME,
|
|
3333
|
+
DEFAULT_PROMPT_TIMEOUT,
|
|
3334
|
+
DEFAULT_COMMAND_TIMEOUT,
|
|
3335
|
+
COMMON_TOOL_MATCHERS
|
|
2272
3336
|
};
|