@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,133 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import type { Doc, Rule } from "../types";
4
+
5
+ /**
6
+ * Load rules from a capability's rules/ directory
7
+ * @param capabilityPath Path to the capability directory
8
+ * @param capabilityId ID of the capability
9
+ * @returns Array of Rule objects
10
+ */
11
+ export async function loadRules(capabilityPath: string, capabilityId: string): Promise<Rule[]> {
12
+ const rulesDir = join(capabilityPath, "rules");
13
+
14
+ if (!existsSync(rulesDir)) {
15
+ return [];
16
+ }
17
+
18
+ const rules: Rule[] = [];
19
+ const entries = readdirSync(rulesDir, { withFileTypes: true });
20
+
21
+ for (const entry of entries) {
22
+ if (entry.isFile() && entry.name.endsWith(".md")) {
23
+ const rulePath = join(rulesDir, entry.name);
24
+ const content = await Bun.file(rulePath).text();
25
+
26
+ rules.push({
27
+ name: basename(entry.name, ".md"),
28
+ content: content.trim(),
29
+ capabilityId,
30
+ });
31
+ }
32
+ }
33
+
34
+ return rules;
35
+ }
36
+
37
+ /**
38
+ * Write aggregated rules and docs to .omni/instructions.md
39
+ * Updates the generated section between markers while preserving user content
40
+ * @param rules Array of rules from all enabled capabilities
41
+ * @param docs Array of docs from all enabled capabilities
42
+ */
43
+ export async function writeRules(rules: Rule[], docs: Doc[] = []): Promise<void> {
44
+ const instructionsPath = ".omni/instructions.md";
45
+
46
+ // Generate content from rules and docs
47
+ const rulesContent = generateRulesContent(rules, docs);
48
+
49
+ // Read existing content or create new file
50
+ let content: string;
51
+ if (existsSync(instructionsPath)) {
52
+ content = await Bun.file(instructionsPath).text();
53
+ } else {
54
+ // Create new file with basic template
55
+ content = `# OmniDev Instructions
56
+
57
+ ## Project Description
58
+ <!-- TODO: Add 2-3 sentences describing your project -->
59
+ [Describe what this project does and its main purpose]
60
+
61
+ <!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->
62
+ <!-- END OMNIDEV GENERATED CONTENT -->
63
+ `;
64
+ }
65
+
66
+ // Replace content between markers
67
+ const beginMarker = "<!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->";
68
+ const endMarker = "<!-- END OMNIDEV GENERATED CONTENT -->";
69
+
70
+ const beginIndex = content.indexOf(beginMarker);
71
+ const endIndex = content.indexOf(endMarker);
72
+
73
+ if (beginIndex === -1 || endIndex === -1) {
74
+ // Markers not found, append to end
75
+ content += `\n\n${beginMarker}\n${rulesContent}\n${endMarker}\n`;
76
+ } else {
77
+ // Replace content between markers
78
+ content =
79
+ content.substring(0, beginIndex + beginMarker.length) +
80
+ "\n" +
81
+ rulesContent +
82
+ "\n" +
83
+ content.substring(endIndex);
84
+ }
85
+
86
+ await Bun.write(instructionsPath, content);
87
+ }
88
+
89
+ function generateRulesContent(rules: Rule[], docs: Doc[] = []): string {
90
+ if (rules.length === 0 && docs.length === 0) {
91
+ return `<!-- This section is automatically updated when capabilities change -->
92
+
93
+ ## Capabilities
94
+
95
+ No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable capabilities.`;
96
+ }
97
+
98
+ let content = `<!-- This section is automatically updated when capabilities change -->
99
+
100
+ ## Capabilities
101
+
102
+ `;
103
+
104
+ // Add documentation section if there are docs
105
+ if (docs.length > 0) {
106
+ content += `### Documentation
107
+
108
+ `;
109
+ for (const doc of docs) {
110
+ content += `#### ${doc.name} (from ${doc.capabilityId})
111
+
112
+ ${doc.content}
113
+
114
+ `;
115
+ }
116
+ }
117
+
118
+ // Add rules section if there are rules
119
+ if (rules.length > 0) {
120
+ content += `### Rules
121
+
122
+ `;
123
+ for (const rule of rules) {
124
+ content += `#### ${rule.name} (from ${rule.capabilityId})
125
+
126
+ ${rule.content}
127
+
128
+ `;
129
+ }
130
+ }
131
+
132
+ return content.trim();
133
+ }
@@ -0,0 +1,316 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadSkills } from "./skills";
5
+
6
+ describe("loadSkills", () => {
7
+ const testDir = join(process.cwd(), "test-skills-temp");
8
+ const capabilityPath = join(testDir, "test-capability");
9
+
10
+ beforeEach(() => {
11
+ mkdirSync(capabilityPath, { recursive: true });
12
+ });
13
+
14
+ afterEach(() => {
15
+ if (testDir) {
16
+ rmSync(testDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ test("returns empty array when skills directory does not exist", async () => {
21
+ const skills = await loadSkills(capabilityPath, "test-cap");
22
+ expect(skills).toEqual([]);
23
+ });
24
+
25
+ test("returns empty array when skills directory is empty", async () => {
26
+ mkdirSync(join(capabilityPath, "skills"), { recursive: true });
27
+ const skills = await loadSkills(capabilityPath, "test-cap");
28
+ expect(skills).toEqual([]);
29
+ });
30
+
31
+ test("loads single skill with valid frontmatter and instructions", async () => {
32
+ const skillsDir = join(capabilityPath, "skills", "test-skill");
33
+ mkdirSync(skillsDir, { recursive: true });
34
+
35
+ const skillContent = `---
36
+ name: test-skill
37
+ description: A test skill
38
+ ---
39
+
40
+ # Instructions
41
+
42
+ This is a test skill.`;
43
+
44
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
45
+
46
+ const skills = await loadSkills(capabilityPath, "test-cap");
47
+
48
+ expect(skills).toHaveLength(1);
49
+ expect(skills[0]).toEqual({
50
+ name: "test-skill",
51
+ description: "A test skill",
52
+ instructions: "# Instructions\n\nThis is a test skill.",
53
+ capabilityId: "test-cap",
54
+ });
55
+ });
56
+
57
+ test("loads multiple skills from different directories", async () => {
58
+ const skill1Dir = join(capabilityPath, "skills", "skill-1");
59
+ const skill2Dir = join(capabilityPath, "skills", "skill-2");
60
+ mkdirSync(skill1Dir, { recursive: true });
61
+ mkdirSync(skill2Dir, { recursive: true });
62
+
63
+ writeFileSync(
64
+ join(skill1Dir, "SKILL.md"),
65
+ `---
66
+ name: skill-1
67
+ description: First skill
68
+ ---
69
+
70
+ Instructions for skill 1.`,
71
+ );
72
+
73
+ writeFileSync(
74
+ join(skill2Dir, "SKILL.md"),
75
+ `---
76
+ name: skill-2
77
+ description: Second skill
78
+ ---
79
+
80
+ Instructions for skill 2.`,
81
+ );
82
+
83
+ const skills = await loadSkills(capabilityPath, "test-cap");
84
+
85
+ expect(skills).toHaveLength(2);
86
+ expect(skills[0]?.name).toBe("skill-1");
87
+ expect(skills[1]?.name).toBe("skill-2");
88
+ });
89
+
90
+ test("skips skill directories without SKILL.md file", async () => {
91
+ const validSkillDir = join(capabilityPath, "skills", "valid-skill");
92
+ const invalidSkillDir = join(capabilityPath, "skills", "no-skill-file");
93
+ mkdirSync(validSkillDir, { recursive: true });
94
+ mkdirSync(invalidSkillDir, { recursive: true });
95
+
96
+ writeFileSync(
97
+ join(validSkillDir, "SKILL.md"),
98
+ `---
99
+ name: valid-skill
100
+ description: Valid skill
101
+ ---
102
+
103
+ Valid instructions.`,
104
+ );
105
+
106
+ // No SKILL.md in invalidSkillDir
107
+
108
+ const skills = await loadSkills(capabilityPath, "test-cap");
109
+
110
+ expect(skills).toHaveLength(1);
111
+ expect(skills[0]?.name).toBe("valid-skill");
112
+ });
113
+
114
+ test("handles YAML frontmatter with quoted values", async () => {
115
+ const skillsDir = join(capabilityPath, "skills", "quoted-skill");
116
+ mkdirSync(skillsDir, { recursive: true });
117
+
118
+ const skillContent = `---
119
+ name: "quoted-skill"
120
+ description: "A skill with quoted values"
121
+ ---
122
+
123
+ Instructions here.`;
124
+
125
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
126
+
127
+ const skills = await loadSkills(capabilityPath, "test-cap");
128
+
129
+ expect(skills).toHaveLength(1);
130
+ expect(skills[0]?.name).toBe("quoted-skill");
131
+ expect(skills[0]?.description).toBe("A skill with quoted values");
132
+ });
133
+
134
+ test("handles YAML frontmatter with colons in values", async () => {
135
+ const skillsDir = join(capabilityPath, "skills", "colon-skill");
136
+ mkdirSync(skillsDir, { recursive: true });
137
+
138
+ const skillContent = `---
139
+ name: colon-skill
140
+ description: A skill with a colon: in the description
141
+ ---
142
+
143
+ Instructions here.`;
144
+
145
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
146
+
147
+ const skills = await loadSkills(capabilityPath, "test-cap");
148
+
149
+ expect(skills).toHaveLength(1);
150
+ expect(skills[0]?.description).toBe("A skill with a colon: in the description");
151
+ });
152
+
153
+ test("trims whitespace from instructions", async () => {
154
+ const skillsDir = join(capabilityPath, "skills", "whitespace-skill");
155
+ mkdirSync(skillsDir, { recursive: true });
156
+
157
+ const skillContent = `---
158
+ name: whitespace-skill
159
+ description: Test whitespace trimming
160
+ ---
161
+
162
+
163
+ Instructions with leading/trailing whitespace.
164
+
165
+ `;
166
+
167
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
168
+
169
+ const skills = await loadSkills(capabilityPath, "test-cap");
170
+
171
+ expect(skills).toHaveLength(1);
172
+ expect(skills[0]?.instructions).toBe("Instructions with leading/trailing whitespace.");
173
+ });
174
+
175
+ test("throws error when SKILL.md has no frontmatter", async () => {
176
+ const skillsDir = join(capabilityPath, "skills", "no-frontmatter");
177
+ mkdirSync(skillsDir, { recursive: true });
178
+
179
+ const skillContent = `# Just Instructions
180
+
181
+ No frontmatter here.`;
182
+
183
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
184
+
185
+ await expect(loadSkills(capabilityPath, "test-cap")).rejects.toThrow(
186
+ /Invalid SKILL\.md format.*missing YAML frontmatter/,
187
+ );
188
+ });
189
+
190
+ test("throws error when SKILL.md is missing name field", async () => {
191
+ const skillsDir = join(capabilityPath, "skills", "missing-name");
192
+ mkdirSync(skillsDir, { recursive: true });
193
+
194
+ const skillContent = `---
195
+ description: Missing name field
196
+ ---
197
+
198
+ Instructions here.`;
199
+
200
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
201
+
202
+ await expect(loadSkills(capabilityPath, "test-cap")).rejects.toThrow(
203
+ /name and description required/,
204
+ );
205
+ });
206
+
207
+ test("throws error when SKILL.md is missing description field", async () => {
208
+ const skillsDir = join(capabilityPath, "skills", "missing-description");
209
+ mkdirSync(skillsDir, { recursive: true });
210
+
211
+ const skillContent = `---
212
+ name: missing-description
213
+ ---
214
+
215
+ Instructions here.`;
216
+
217
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
218
+
219
+ await expect(loadSkills(capabilityPath, "test-cap")).rejects.toThrow(
220
+ /name and description required/,
221
+ );
222
+ });
223
+
224
+ test("handles empty instructions after frontmatter", async () => {
225
+ const skillsDir = join(capabilityPath, "skills", "empty-instructions");
226
+ mkdirSync(skillsDir, { recursive: true });
227
+
228
+ const skillContent = `---
229
+ name: empty-instructions
230
+ description: Skill with no instructions
231
+ ---
232
+ `;
233
+
234
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
235
+
236
+ const skills = await loadSkills(capabilityPath, "test-cap");
237
+
238
+ expect(skills).toHaveLength(1);
239
+ expect(skills[0]?.instructions).toBe("");
240
+ });
241
+
242
+ test("preserves markdown formatting in instructions", async () => {
243
+ const skillsDir = join(capabilityPath, "skills", "markdown-skill");
244
+ mkdirSync(skillsDir, { recursive: true });
245
+
246
+ const skillContent = `---
247
+ name: markdown-skill
248
+ description: Skill with markdown
249
+ ---
250
+
251
+ # Header
252
+
253
+ - List item 1
254
+ - List item 2
255
+
256
+ **Bold text** and *italic text*.
257
+
258
+ \`\`\`typescript
259
+ const code = "example";
260
+ \`\`\``;
261
+
262
+ writeFileSync(join(skillsDir, "SKILL.md"), skillContent);
263
+
264
+ const skills = await loadSkills(capabilityPath, "test-cap");
265
+
266
+ expect(skills).toHaveLength(1);
267
+ expect(skills[0]?.instructions).toContain("# Header");
268
+ expect(skills[0]?.instructions).toContain("- List item 1");
269
+ expect(skills[0]?.instructions).toContain("**Bold text**");
270
+ expect(skills[0]?.instructions).toContain("```typescript");
271
+ });
272
+
273
+ test("ignores non-directory entries in skills folder", async () => {
274
+ const skillsDir = join(capabilityPath, "skills");
275
+ const validSkillDir = join(skillsDir, "valid-skill");
276
+ mkdirSync(validSkillDir, { recursive: true });
277
+
278
+ writeFileSync(
279
+ join(validSkillDir, "SKILL.md"),
280
+ `---
281
+ name: valid-skill
282
+ description: Valid skill
283
+ ---
284
+
285
+ Valid instructions.`,
286
+ );
287
+
288
+ // Create a file directly in skills/ directory (should be ignored)
289
+ writeFileSync(join(skillsDir, "README.md"), "This should be ignored");
290
+
291
+ const skills = await loadSkills(capabilityPath, "test-cap");
292
+
293
+ expect(skills).toHaveLength(1);
294
+ expect(skills[0]?.name).toBe("valid-skill");
295
+ });
296
+
297
+ test("associates skills with correct capability ID", async () => {
298
+ const skillsDir = join(capabilityPath, "skills", "test-skill");
299
+ mkdirSync(skillsDir, { recursive: true });
300
+
301
+ writeFileSync(
302
+ join(skillsDir, "SKILL.md"),
303
+ `---
304
+ name: test-skill
305
+ description: Test skill
306
+ ---
307
+
308
+ Instructions.`,
309
+ );
310
+
311
+ const skills = await loadSkills(capabilityPath, "my-capability");
312
+
313
+ expect(skills).toHaveLength(1);
314
+ expect(skills[0]?.capabilityId).toBe("my-capability");
315
+ });
316
+ });
@@ -0,0 +1,56 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Skill } from "../types";
4
+ import { parseFrontmatterWithMarkdown } from "./yaml-parser";
5
+
6
+ interface SkillFrontmatter {
7
+ name: string;
8
+ description: string;
9
+ }
10
+
11
+ export async function loadSkills(capabilityPath: string, capabilityId: string): Promise<Skill[]> {
12
+ const skillsDir = join(capabilityPath, "skills");
13
+
14
+ if (!existsSync(skillsDir)) {
15
+ return [];
16
+ }
17
+
18
+ const skills: Skill[] = [];
19
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
20
+
21
+ for (const entry of entries) {
22
+ if (entry.isDirectory()) {
23
+ const skillPath = join(skillsDir, entry.name, "SKILL.md");
24
+ if (existsSync(skillPath)) {
25
+ const skill = await parseSkillFile(skillPath, capabilityId);
26
+ skills.push(skill);
27
+ }
28
+ }
29
+ }
30
+
31
+ return skills;
32
+ }
33
+
34
+ async function parseSkillFile(filePath: string, capabilityId: string): Promise<Skill> {
35
+ const content = await Bun.file(filePath).text();
36
+
37
+ const parsed = parseFrontmatterWithMarkdown<SkillFrontmatter>(content);
38
+
39
+ if (!parsed) {
40
+ throw new Error(`Invalid SKILL.md format at ${filePath}: missing YAML frontmatter`);
41
+ }
42
+
43
+ const frontmatter = parsed.frontmatter;
44
+ const instructions = parsed.markdown;
45
+
46
+ if (!frontmatter.name || !frontmatter.description) {
47
+ throw new Error(`Invalid SKILL.md at ${filePath}: name and description required`);
48
+ }
49
+
50
+ return {
51
+ name: frontmatter.name,
52
+ description: frontmatter.description,
53
+ instructions: instructions.trim(),
54
+ capabilityId,
55
+ };
56
+ }