@omnidev-ai/core 0.1.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.
Files changed (59) hide show
  1. package/package.json +31 -0
  2. package/src/capability/AGENTS.md +58 -0
  3. package/src/capability/commands.test.ts +414 -0
  4. package/src/capability/commands.ts +70 -0
  5. package/src/capability/docs.test.ts +199 -0
  6. package/src/capability/docs.ts +46 -0
  7. package/src/capability/index.ts +20 -0
  8. package/src/capability/loader.test.ts +815 -0
  9. package/src/capability/loader.ts +492 -0
  10. package/src/capability/registry.test.ts +473 -0
  11. package/src/capability/registry.ts +55 -0
  12. package/src/capability/rules.test.ts +145 -0
  13. package/src/capability/rules.ts +133 -0
  14. package/src/capability/skills.test.ts +316 -0
  15. package/src/capability/skills.ts +56 -0
  16. package/src/capability/sources.test.ts +338 -0
  17. package/src/capability/sources.ts +966 -0
  18. package/src/capability/subagents.test.ts +478 -0
  19. package/src/capability/subagents.ts +103 -0
  20. package/src/capability/yaml-parser.ts +81 -0
  21. package/src/config/AGENTS.md +46 -0
  22. package/src/config/capabilities.ts +82 -0
  23. package/src/config/env.test.ts +286 -0
  24. package/src/config/env.ts +96 -0
  25. package/src/config/index.ts +6 -0
  26. package/src/config/loader.test.ts +282 -0
  27. package/src/config/loader.ts +137 -0
  28. package/src/config/parser.test.ts +281 -0
  29. package/src/config/parser.ts +55 -0
  30. package/src/config/profiles.test.ts +259 -0
  31. package/src/config/profiles.ts +75 -0
  32. package/src/config/provider.test.ts +79 -0
  33. package/src/config/provider.ts +55 -0
  34. package/src/debug.ts +20 -0
  35. package/src/gitignore/manager.test.ts +219 -0
  36. package/src/gitignore/manager.ts +167 -0
  37. package/src/index.test.ts +26 -0
  38. package/src/index.ts +39 -0
  39. package/src/mcp-json/index.ts +1 -0
  40. package/src/mcp-json/manager.test.ts +415 -0
  41. package/src/mcp-json/manager.ts +118 -0
  42. package/src/state/active-profile.test.ts +131 -0
  43. package/src/state/active-profile.ts +41 -0
  44. package/src/state/index.ts +2 -0
  45. package/src/state/manifest.test.ts +548 -0
  46. package/src/state/manifest.ts +164 -0
  47. package/src/sync.ts +213 -0
  48. package/src/templates/agents.test.ts +23 -0
  49. package/src/templates/agents.ts +14 -0
  50. package/src/templates/claude.test.ts +48 -0
  51. package/src/templates/claude.ts +122 -0
  52. package/src/test-utils/helpers.test.ts +196 -0
  53. package/src/test-utils/helpers.ts +187 -0
  54. package/src/test-utils/index.ts +30 -0
  55. package/src/test-utils/mocks.test.ts +83 -0
  56. package/src/test-utils/mocks.ts +101 -0
  57. package/src/types/capability-export.ts +234 -0
  58. package/src/types/index.test.ts +28 -0
  59. package/src/types/index.ts +270 -0
@@ -0,0 +1,492 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { validateEnv } from "../config/env";
4
+ import { parseCapabilityConfig } from "../config/parser";
5
+ import type {
6
+ CapabilityConfig,
7
+ Command,
8
+ Doc,
9
+ LoadedCapability,
10
+ Rule,
11
+ Skill,
12
+ Subagent,
13
+ } from "../types";
14
+ import type {
15
+ CommandExport,
16
+ DocExport,
17
+ SkillExport,
18
+ SubagentExport,
19
+ } from "../types/capability-export";
20
+ import { loadCommands } from "./commands";
21
+ import { loadDocs } from "./docs";
22
+ import { loadRules } from "./rules";
23
+ import { loadSkills } from "./skills";
24
+ import { loadSubagents } from "./subagents";
25
+
26
+ const CAPABILITIES_DIR = ".omni/capabilities";
27
+ const BUILTIN_CAPABILITIES_DIR = "capabilities";
28
+
29
+ /**
30
+ * Reserved capability names that cannot be used.
31
+ * These are common package names that might conflict with imports.
32
+ */
33
+ const RESERVED_NAMES = [
34
+ "fs",
35
+ "path",
36
+ "http",
37
+ "https",
38
+ "crypto",
39
+ "os",
40
+ "child_process",
41
+ "stream",
42
+ "buffer",
43
+ "util",
44
+ "events",
45
+ "net",
46
+ "url",
47
+ "querystring",
48
+ "react",
49
+ "vue",
50
+ "lodash",
51
+ "axios",
52
+ "express",
53
+ "typescript",
54
+ ];
55
+
56
+ /**
57
+ * Check if a path is a directory (follows symlinks)
58
+ */
59
+ function isDirectoryOrSymlink(path: string): boolean {
60
+ try {
61
+ return statSync(path).isDirectory();
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Discovers capabilities by scanning the .omni/capabilities directory.
69
+ * A directory is considered a capability if it contains a capability.toml file.
70
+ * Follows symlinks to support linked capability directories.
71
+ *
72
+ * @returns Array of capability directory paths
73
+ */
74
+ export async function discoverCapabilities(): Promise<string[]> {
75
+ const capabilities: string[] = [];
76
+
77
+ // Discover built-in capabilities (from capabilities/ directory)
78
+ if (existsSync(BUILTIN_CAPABILITIES_DIR)) {
79
+ const entries = readdirSync(BUILTIN_CAPABILITIES_DIR, { withFileTypes: true });
80
+
81
+ for (const entry of entries) {
82
+ const entryPath = join(BUILTIN_CAPABILITIES_DIR, entry.name);
83
+ if (entry.isDirectory() || (entry.isSymbolicLink() && isDirectoryOrSymlink(entryPath))) {
84
+ const configPath = join(entryPath, "capability.toml");
85
+ if (existsSync(configPath)) {
86
+ capabilities.push(entryPath);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ // Discover project-specific capabilities (from .omni/capabilities/)
93
+ if (existsSync(CAPABILITIES_DIR)) {
94
+ const entries = readdirSync(CAPABILITIES_DIR, { withFileTypes: true });
95
+
96
+ for (const entry of entries) {
97
+ const entryPath = join(CAPABILITIES_DIR, entry.name);
98
+ if (entry.isDirectory() || (entry.isSymbolicLink() && isDirectoryOrSymlink(entryPath))) {
99
+ const configPath = join(entryPath, "capability.toml");
100
+ if (existsSync(configPath)) {
101
+ capabilities.push(entryPath);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return capabilities;
108
+ }
109
+
110
+ /**
111
+ * Loads and parses a capability configuration file.
112
+ * Validates required fields and checks for reserved names.
113
+ *
114
+ * @param capabilityPath - Path to the capability directory
115
+ * @returns Parsed capability configuration
116
+ * @throws Error if the config is invalid or uses a reserved name
117
+ */
118
+ export async function loadCapabilityConfig(capabilityPath: string): Promise<CapabilityConfig> {
119
+ const configPath = join(capabilityPath, "capability.toml");
120
+ const content = await Bun.file(configPath).text();
121
+ const config = parseCapabilityConfig(content);
122
+
123
+ // Validate name is not reserved
124
+ if (RESERVED_NAMES.includes(config.capability.id)) {
125
+ throw new Error(
126
+ `Capability name "${config.capability.id}" is reserved. Choose a different name.`,
127
+ );
128
+ }
129
+
130
+ return config;
131
+ }
132
+
133
+ /**
134
+ * Dynamically imports capability exports from index.ts.
135
+ * Returns an empty object if index.ts doesn't exist.
136
+ *
137
+ * @param capabilityPath - Path to the capability directory
138
+ * @returns Exported module or empty object
139
+ * @throws Error if import fails
140
+ */
141
+ async function importCapabilityExports(capabilityPath: string): Promise<Record<string, unknown>> {
142
+ const indexPath = join(capabilityPath, "index.ts");
143
+
144
+ if (!existsSync(indexPath)) {
145
+ return {};
146
+ }
147
+
148
+ try {
149
+ const absolutePath = join(process.cwd(), indexPath);
150
+ const module = await import(absolutePath);
151
+ return module;
152
+ } catch (error) {
153
+ // Check if it's a module resolution error
154
+ const errorMessage = String(error);
155
+ if (errorMessage.includes("Cannot find module")) {
156
+ const match = errorMessage.match(/Cannot find module '([^']+)'/);
157
+ const missingModule = match ? match[1] : "unknown";
158
+ throw new Error(
159
+ `Missing dependency '${missingModule}' for capability at ${capabilityPath}.\n` +
160
+ `If this is a project-specific capability, install dependencies or remove it from .omni/capabilities/`,
161
+ );
162
+ }
163
+ throw new Error(`Failed to import capability at ${capabilityPath}: ${error}`);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Loads type definitions from types.d.ts if it exists.
169
+ *
170
+ * @param capabilityPath - Path to the capability directory
171
+ * @returns Type definitions as string or undefined
172
+ */
173
+ async function loadTypeDefinitions(capabilityPath: string): Promise<string | undefined> {
174
+ const typesPath = join(capabilityPath, "types.d.ts");
175
+
176
+ if (!existsSync(typesPath)) {
177
+ return undefined;
178
+ }
179
+
180
+ return Bun.file(typesPath).text();
181
+ }
182
+
183
+ /**
184
+ * Convert programmatic skill exports to Skill objects
185
+ * Expects SkillExport format with skillMd (markdown with YAML frontmatter)
186
+ */
187
+ function convertSkillExports(skillExports: unknown[], capabilityId: string): Skill[] {
188
+ return skillExports.map((skillExport) => {
189
+ const exportObj = skillExport as SkillExport;
190
+ const lines = exportObj.skillMd.split("\n");
191
+ let name = "unnamed";
192
+ let description = "";
193
+ let instructions = exportObj.skillMd;
194
+
195
+ // Simple YAML frontmatter parser
196
+ if (lines[0]?.trim() === "---") {
197
+ const endIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
198
+ if (endIndex > 0) {
199
+ const frontmatter = lines.slice(1, endIndex);
200
+ instructions = lines
201
+ .slice(endIndex + 1)
202
+ .join("\n")
203
+ .trim();
204
+
205
+ for (const line of frontmatter) {
206
+ const match = line.match(/^(\w+):\s*(.+)$/);
207
+ if (match?.[1] && match[2]) {
208
+ const key = match[1];
209
+ const value = match[2];
210
+ if (key === "name") {
211
+ name = value.replace(/^["']|["']$/g, "");
212
+ } else if (key === "description") {
213
+ description = value.replace(/^["']|["']$/g, "");
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ return {
221
+ name,
222
+ description,
223
+ instructions,
224
+ capabilityId,
225
+ };
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Convert programmatic rule exports to Rule objects
231
+ * Expects array of string content (markdown)
232
+ */
233
+ function convertRuleExports(ruleExports: unknown[], capabilityId: string): Rule[] {
234
+ return ruleExports.map((ruleExport, index) => {
235
+ return {
236
+ name: `rule-${index + 1}`,
237
+ content: String(ruleExport).trim(),
238
+ capabilityId,
239
+ };
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Convert programmatic doc exports to Doc objects
245
+ * Expects DocExport format with title and content
246
+ */
247
+ function convertDocExports(docExports: unknown[], capabilityId: string): Doc[] {
248
+ return docExports.map((docExport) => {
249
+ const exportObj = docExport as DocExport;
250
+ return {
251
+ name: exportObj.title,
252
+ content: exportObj.content.trim(),
253
+ capabilityId,
254
+ };
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Convert programmatic subagent exports to Subagent objects
260
+ * Parses SubagentExport markdown with YAML frontmatter
261
+ */
262
+ function convertSubagentExports(subagentExports: unknown[], capabilityId: string): Subagent[] {
263
+ return subagentExports.map((subagentExport) => {
264
+ const exportObj = subagentExport as SubagentExport;
265
+ const lines = exportObj.subagentMd.split("\n");
266
+ let name = "unnamed";
267
+ let description = "";
268
+ let systemPrompt = exportObj.subagentMd;
269
+ let tools: string[] | undefined;
270
+ let disallowedTools: string[] | undefined;
271
+ let model: string | undefined;
272
+ let permissionMode: string | undefined;
273
+ let skills: string[] | undefined;
274
+
275
+ // Simple YAML frontmatter parser
276
+ if (lines[0]?.trim() === "---") {
277
+ const endIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
278
+ if (endIndex > 0) {
279
+ const frontmatter = lines.slice(1, endIndex);
280
+ systemPrompt = lines
281
+ .slice(endIndex + 1)
282
+ .join("\n")
283
+ .trim();
284
+
285
+ for (const line of frontmatter) {
286
+ const match = line.match(/^(\w+):\s*(.+)$/);
287
+ if (match?.[1] && match[2]) {
288
+ const key = match[1];
289
+ const value = match[2].replace(/^["']|["']$/g, "");
290
+ switch (key) {
291
+ case "name":
292
+ name = value;
293
+ break;
294
+ case "description":
295
+ description = value;
296
+ break;
297
+ case "tools":
298
+ tools = value.split(",").map((t) => t.trim());
299
+ break;
300
+ case "disallowedTools":
301
+ disallowedTools = value.split(",").map((t) => t.trim());
302
+ break;
303
+ case "model":
304
+ model = value;
305
+ break;
306
+ case "permissionMode":
307
+ permissionMode = value;
308
+ break;
309
+ case "skills":
310
+ skills = value.split(",").map((s) => s.trim());
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ const result: Subagent = {
319
+ name,
320
+ description,
321
+ systemPrompt,
322
+ capabilityId,
323
+ };
324
+
325
+ if (tools) result.tools = tools;
326
+ if (disallowedTools) result.disallowedTools = disallowedTools;
327
+ if (model) {
328
+ result.model = model as NonNullable<Subagent["model"]>;
329
+ }
330
+ if (permissionMode) {
331
+ result.permissionMode = permissionMode as NonNullable<Subagent["permissionMode"]>;
332
+ }
333
+ if (skills) result.skills = skills;
334
+
335
+ return result;
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Convert programmatic command exports to Command objects
341
+ * Parses CommandExport markdown with YAML frontmatter
342
+ */
343
+ function convertCommandExports(commandExports: unknown[], capabilityId: string): Command[] {
344
+ return commandExports.map((commandExport) => {
345
+ const exportObj = commandExport as CommandExport;
346
+ const lines = exportObj.commandMd.split("\n");
347
+ let name = "unnamed";
348
+ let description = "";
349
+ let prompt = exportObj.commandMd;
350
+ let allowedTools: string | undefined;
351
+
352
+ // Simple YAML frontmatter parser
353
+ if (lines[0]?.trim() === "---") {
354
+ const endIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
355
+ if (endIndex > 0) {
356
+ const frontmatter = lines.slice(1, endIndex);
357
+ prompt = lines
358
+ .slice(endIndex + 1)
359
+ .join("\n")
360
+ .trim();
361
+
362
+ for (const line of frontmatter) {
363
+ const match = line.match(/^(\w+):\s*(.+)$/);
364
+ if (match?.[1] && match[2]) {
365
+ const key = match[1];
366
+ const value = match[2].replace(/^["']|["']$/g, "");
367
+ switch (key) {
368
+ case "name":
369
+ name = value;
370
+ break;
371
+ case "description":
372
+ description = value;
373
+ break;
374
+ case "allowedTools":
375
+ case "allowed-tools":
376
+ allowedTools = value;
377
+ break;
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ const result: Command = {
385
+ name,
386
+ description,
387
+ prompt,
388
+ capabilityId,
389
+ };
390
+
391
+ if (allowedTools) {
392
+ result.allowedTools = allowedTools;
393
+ }
394
+
395
+ return result;
396
+ });
397
+ }
398
+
399
+ /**
400
+ * Loads a complete capability including config, skills, rules, docs, and exports.
401
+ * Validates environment requirements before loading.
402
+ *
403
+ * @param capabilityPath - Path to the capability directory
404
+ * @param env - Environment variables to validate against
405
+ * @returns Fully loaded capability
406
+ * @throws Error if validation fails or loading errors occur
407
+ */
408
+ export async function loadCapability(
409
+ capabilityPath: string,
410
+ env: Record<string, string>,
411
+ ): Promise<LoadedCapability> {
412
+ const config = await loadCapabilityConfig(capabilityPath);
413
+ const id = config.capability.id;
414
+
415
+ // Validate environment
416
+ if (config.env) {
417
+ validateEnv(config.env, env, id);
418
+ }
419
+
420
+ // Load content - programmatic takes precedence
421
+ const exports = await importCapabilityExports(capabilityPath);
422
+
423
+ // Check if exports contains programmatic skills/rules/docs
424
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic module exports need runtime type checking
425
+ const exportsAny = exports as any;
426
+
427
+ const skills =
428
+ "skills" in exports && Array.isArray(exportsAny.skills)
429
+ ? convertSkillExports(exportsAny.skills, id)
430
+ : await loadSkills(capabilityPath, id);
431
+
432
+ const rules =
433
+ "rules" in exports && Array.isArray(exportsAny.rules)
434
+ ? convertRuleExports(exportsAny.rules, id)
435
+ : await loadRules(capabilityPath, id);
436
+
437
+ const docs =
438
+ "docs" in exports && Array.isArray(exportsAny.docs)
439
+ ? convertDocExports(exportsAny.docs, id)
440
+ : await loadDocs(capabilityPath, id);
441
+
442
+ const subagents =
443
+ "subagents" in exports && Array.isArray(exportsAny.subagents)
444
+ ? convertSubagentExports(exportsAny.subagents, id)
445
+ : await loadSubagents(capabilityPath, id);
446
+
447
+ const commands =
448
+ "commands" in exports && Array.isArray(exportsAny.commands)
449
+ ? convertCommandExports(exportsAny.commands, id)
450
+ : await loadCommands(capabilityPath, id);
451
+
452
+ const typeDefinitionsFromExports =
453
+ "typeDefinitions" in exports && typeof exportsAny.typeDefinitions === "string"
454
+ ? (exportsAny.typeDefinitions as string)
455
+ : undefined;
456
+
457
+ const typeDefinitions =
458
+ typeDefinitionsFromExports !== undefined
459
+ ? typeDefinitionsFromExports
460
+ : await loadTypeDefinitions(capabilityPath);
461
+
462
+ // Extract gitignore patterns from exports
463
+ const gitignore =
464
+ "gitignore" in exports && Array.isArray(exportsAny.gitignore)
465
+ ? (exportsAny.gitignore as string[])
466
+ : undefined;
467
+
468
+ // Build result object with explicit handling for optional typeDefinitions
469
+ const result: LoadedCapability = {
470
+ id,
471
+ path: capabilityPath,
472
+ config,
473
+ skills,
474
+ rules,
475
+ docs,
476
+ subagents,
477
+ commands,
478
+ exports,
479
+ };
480
+
481
+ // Only add typeDefinitions if it exists
482
+ if (typeDefinitions !== undefined) {
483
+ result.typeDefinitions = typeDefinitions;
484
+ }
485
+
486
+ // Only add gitignore if it exists
487
+ if (gitignore !== undefined) {
488
+ result.gitignore = gitignore;
489
+ }
490
+
491
+ return result;
492
+ }