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