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