@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
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@omnidev-ai/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Nikola-Milovic/omnidev.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "exports": {
12
+ ".": "./src/index.ts"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org"
21
+ },
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "echo 'Build not needed for Bun runtime'"
25
+ },
26
+ "dependencies": {
27
+ "@stricli/core": "^1.2.5",
28
+ "smol-toml": "^1.6.0"
29
+ },
30
+ "devDependencies": {}
31
+ }
@@ -0,0 +1,58 @@
1
+ # PROJECT KNOWLEDGE BASE
2
+
3
+ **Generated:** 2026-01-12
4
+ **Commit:** (not specified)
5
+ **Branch:** (not specified)
6
+
7
+ ## OVERVIEW
8
+ Core capability loading system: discovers capabilities from .omni/capabilities/ & capabilities/, validates TOML configs, loads TypeScript exports (skills/rules/docs/commands/subagents), and builds runtime registry.
9
+
10
+ ## WHERE TO LOOK
11
+ | Task | Location | Notes |
12
+ |------|----------|-------|
13
+ | Capability discovery | loader.ts | Scans .omni/capabilities/ & capabilities/ for capability.toml |
14
+ | Load TOML config | loader.ts | Validates required fields, checks reserved names |
15
+ | Dynamic imports | loader.ts | Imports index.ts exports, handles missing deps gracefully |
16
+ | Registry build | registry.ts | Filters by enabled caps, aggregates all content types |
17
+ | Skills loading | skills.ts | Parses SKILL.md with YAML frontmatter |
18
+ | Rules loading | rules.ts | Loads *.md from rules/ directory |
19
+ | Docs loading | docs.ts | Loads definition.md + docs/*.md |
20
+ | Commands loading | commands.ts | Parses COMMAND.md with YAML frontmatter |
21
+ | Subagents loading | subagents.ts | Parses SUBAGENT.md, supports tools/skills/models |
22
+ | Remote sources | sources.ts | Git clone/fetch, wrap external repos, lock file mgmt |
23
+
24
+ ## CONVENTIONS
25
+
26
+ **Capability Structure:**
27
+ - Must have capability.toml in root
28
+ - Optional: index.ts (exports: skills/rules/docs/commands/subagents/gitignore)
29
+ - Optional directories: skills/, rules/, docs/, commands/, subagents/
30
+ - Optional types.d.ts (for LLM type hints)
31
+
32
+ **YAML Frontmatter Format:**
33
+ - Skills/Commands: name, description (required)
34
+ - Subagents: name, description + optional tools/disallowedTools/model/permissionMode/skills
35
+ - Supports kebab-case keys (converted to camelCase)
36
+
37
+ **Content Loading Priority:**
38
+ - Programmatic exports (index.ts) take precedence over file-based content
39
+ - Loader converts both old and new export formats automatically
40
+
41
+ **Reserved Names:**
42
+ - Node builtins (fs, path, http, crypto, os, etc.)
43
+ - Common libs (react, vue, lodash, axios, express, typescript)
44
+ - Prevents import conflicts with capability modules
45
+
46
+ **Remote Capability Sources:**
47
+ - Shorthand: "github:user/repo#ref"
48
+ - Lock file: .omni/capabilities.lock.toml (tracks versions/commits)
49
+ - Wrap mode: discovers skills/agents/commands in repo without capability.toml
50
+ - Version: from package.json if available, else short commit hash
51
+
52
+ ## ANTI-PATTERNS (THIS MODULE)
53
+
54
+ - **NEVER** use reserved capability names (fs, path, react, typescript, etc.)
55
+ - **NEVER** commit capabilities.lock.toml modifications - auto-generated
56
+ - **NEVER** skip YAML frontmatter validation - name/description required
57
+ - **NEVER** modify generated capability.toml in wrapped repos
58
+ - **NEVER** assume index.ts exists - loader returns empty object if missing
@@ -0,0 +1,414 @@
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 { loadCommands } from "./commands";
5
+
6
+ describe("loadCommands", () => {
7
+ const testDir = join(process.cwd(), "test-commands-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 commands directory does not exist", async () => {
21
+ const commands = await loadCommands(capabilityPath, "test-cap");
22
+ expect(commands).toEqual([]);
23
+ });
24
+
25
+ test("returns empty array when commands directory is empty", async () => {
26
+ mkdirSync(join(capabilityPath, "commands"), { recursive: true });
27
+ const commands = await loadCommands(capabilityPath, "test-cap");
28
+ expect(commands).toEqual([]);
29
+ });
30
+
31
+ test("loads single command with valid frontmatter and prompt", async () => {
32
+ const commandDir = join(capabilityPath, "commands", "test-command");
33
+ mkdirSync(commandDir, { recursive: true });
34
+
35
+ const commandContent = `---
36
+ name: test-command
37
+ description: A test command
38
+ ---
39
+
40
+ Test command prompt with $ARGUMENTS.`;
41
+
42
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
43
+
44
+ const commands = await loadCommands(capabilityPath, "test-cap");
45
+
46
+ expect(commands).toHaveLength(1);
47
+ expect(commands[0]).toEqual({
48
+ name: "test-command",
49
+ description: "A test command",
50
+ prompt: "Test command prompt with $ARGUMENTS.",
51
+ capabilityId: "test-cap",
52
+ });
53
+ });
54
+
55
+ test("loads command with allowedTools field", async () => {
56
+ const commandDir = join(capabilityPath, "commands", "tools-command");
57
+ mkdirSync(commandDir, { recursive: true });
58
+
59
+ const commandContent = `---
60
+ name: tools-command
61
+ description: Command with allowed tools
62
+ allowedTools: Bash(git add:*), Bash(git commit:*)
63
+ ---
64
+
65
+ Command prompt here.`;
66
+
67
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
68
+
69
+ const commands = await loadCommands(capabilityPath, "test-cap");
70
+
71
+ expect(commands).toHaveLength(1);
72
+ expect(commands[0]?.allowedTools).toBe("Bash(git add:*), Bash(git commit:*)");
73
+ });
74
+
75
+ test("loads command with allowed-tools field (kebab-case)", async () => {
76
+ const commandDir = join(capabilityPath, "commands", "kebab-tools");
77
+ mkdirSync(commandDir, { recursive: true });
78
+
79
+ const commandContent = `---
80
+ name: kebab-tools
81
+ description: Command with kebab-case allowed-tools
82
+ allowed-tools: Bash(npm test:*)
83
+ ---
84
+
85
+ Command prompt here.`;
86
+
87
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
88
+
89
+ const commands = await loadCommands(capabilityPath, "test-cap");
90
+
91
+ expect(commands).toHaveLength(1);
92
+ expect(commands[0]?.allowedTools).toBe("Bash(npm test:*)");
93
+ });
94
+
95
+ test("loads multiple commands from different directories", async () => {
96
+ const command1Dir = join(capabilityPath, "commands", "command-1");
97
+ const command2Dir = join(capabilityPath, "commands", "command-2");
98
+ mkdirSync(command1Dir, { recursive: true });
99
+ mkdirSync(command2Dir, { recursive: true });
100
+
101
+ writeFileSync(
102
+ join(command1Dir, "COMMAND.md"),
103
+ `---
104
+ name: command-1
105
+ description: First command
106
+ ---
107
+
108
+ First command prompt.`,
109
+ );
110
+
111
+ writeFileSync(
112
+ join(command2Dir, "COMMAND.md"),
113
+ `---
114
+ name: command-2
115
+ description: Second command
116
+ ---
117
+
118
+ Second command prompt.`,
119
+ );
120
+
121
+ const commands = await loadCommands(capabilityPath, "test-cap");
122
+
123
+ expect(commands).toHaveLength(2);
124
+ expect(commands[0]?.name).toBe("command-1");
125
+ expect(commands[1]?.name).toBe("command-2");
126
+ });
127
+
128
+ test("skips command directories without COMMAND.md file", async () => {
129
+ const validDir = join(capabilityPath, "commands", "valid-command");
130
+ const invalidDir = join(capabilityPath, "commands", "no-file");
131
+ mkdirSync(validDir, { recursive: true });
132
+ mkdirSync(invalidDir, { recursive: true });
133
+
134
+ writeFileSync(
135
+ join(validDir, "COMMAND.md"),
136
+ `---
137
+ name: valid-command
138
+ description: Valid command
139
+ ---
140
+
141
+ Valid prompt.`,
142
+ );
143
+
144
+ // No COMMAND.md in invalidDir
145
+
146
+ const commands = await loadCommands(capabilityPath, "test-cap");
147
+
148
+ expect(commands).toHaveLength(1);
149
+ expect(commands[0]?.name).toBe("valid-command");
150
+ });
151
+
152
+ test("handles YAML frontmatter with quoted values", async () => {
153
+ const commandDir = join(capabilityPath, "commands", "quoted-command");
154
+ mkdirSync(commandDir, { recursive: true });
155
+
156
+ const commandContent = `---
157
+ name: "quoted-command"
158
+ description: "A command with quoted values"
159
+ ---
160
+
161
+ Command prompt here.`;
162
+
163
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
164
+
165
+ const commands = await loadCommands(capabilityPath, "test-cap");
166
+
167
+ expect(commands).toHaveLength(1);
168
+ expect(commands[0]?.name).toBe("quoted-command");
169
+ expect(commands[0]?.description).toBe("A command with quoted values");
170
+ });
171
+
172
+ test("trims whitespace from prompt", async () => {
173
+ const commandDir = join(capabilityPath, "commands", "whitespace-command");
174
+ mkdirSync(commandDir, { recursive: true });
175
+
176
+ const commandContent = `---
177
+ name: whitespace-command
178
+ description: Test whitespace trimming
179
+ ---
180
+
181
+
182
+ Command prompt with leading/trailing whitespace.
183
+
184
+ `;
185
+
186
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
187
+
188
+ const commands = await loadCommands(capabilityPath, "test-cap");
189
+
190
+ expect(commands).toHaveLength(1);
191
+ expect(commands[0]?.prompt).toBe("Command prompt with leading/trailing whitespace.");
192
+ });
193
+
194
+ test("throws error when COMMAND.md has no frontmatter", async () => {
195
+ const commandDir = join(capabilityPath, "commands", "no-frontmatter");
196
+ mkdirSync(commandDir, { recursive: true });
197
+
198
+ const commandContent = `# Just Instructions
199
+
200
+ No frontmatter here.`;
201
+
202
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
203
+
204
+ await expect(loadCommands(capabilityPath, "test-cap")).rejects.toThrow(
205
+ /Invalid COMMAND\.md format.*missing YAML frontmatter/,
206
+ );
207
+ });
208
+
209
+ test("throws error when COMMAND.md is missing name field", async () => {
210
+ const commandDir = join(capabilityPath, "commands", "missing-name");
211
+ mkdirSync(commandDir, { recursive: true });
212
+
213
+ const commandContent = `---
214
+ description: Missing name field
215
+ ---
216
+
217
+ Command prompt here.`;
218
+
219
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
220
+
221
+ await expect(loadCommands(capabilityPath, "test-cap")).rejects.toThrow(
222
+ /name and description required/,
223
+ );
224
+ });
225
+
226
+ test("throws error when COMMAND.md is missing description field", async () => {
227
+ const commandDir = join(capabilityPath, "commands", "missing-description");
228
+ mkdirSync(commandDir, { recursive: true });
229
+
230
+ const commandContent = `---
231
+ name: missing-description
232
+ ---
233
+
234
+ Command prompt here.`;
235
+
236
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
237
+
238
+ await expect(loadCommands(capabilityPath, "test-cap")).rejects.toThrow(
239
+ /name and description required/,
240
+ );
241
+ });
242
+
243
+ test("handles empty prompt after frontmatter", async () => {
244
+ const commandDir = join(capabilityPath, "commands", "empty-prompt");
245
+ mkdirSync(commandDir, { recursive: true });
246
+
247
+ const commandContent = `---
248
+ name: empty-prompt
249
+ description: Command with no prompt
250
+ ---
251
+ `;
252
+
253
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
254
+
255
+ const commands = await loadCommands(capabilityPath, "test-cap");
256
+
257
+ expect(commands).toHaveLength(1);
258
+ expect(commands[0]?.prompt).toBe("");
259
+ });
260
+
261
+ test("preserves markdown formatting in prompt", async () => {
262
+ const commandDir = join(capabilityPath, "commands", "markdown-command");
263
+ mkdirSync(commandDir, { recursive: true });
264
+
265
+ const commandContent = `---
266
+ name: markdown-command
267
+ description: Command with markdown
268
+ ---
269
+
270
+ # Header
271
+
272
+ - List item 1
273
+ - List item 2
274
+
275
+ **Bold text** and *italic text*.
276
+
277
+ \`\`\`bash
278
+ git add .
279
+ git commit -m "message"
280
+ \`\`\`
281
+
282
+ Use $ARGUMENTS and $1, $2 for arguments.
283
+ Execute bash commands with !\`git status\`.
284
+ Reference files with @src/file.js.`;
285
+
286
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
287
+
288
+ const commands = await loadCommands(capabilityPath, "test-cap");
289
+
290
+ expect(commands).toHaveLength(1);
291
+ expect(commands[0]?.prompt).toContain("# Header");
292
+ expect(commands[0]?.prompt).toContain("- List item 1");
293
+ expect(commands[0]?.prompt).toContain("**Bold text**");
294
+ expect(commands[0]?.prompt).toContain("```bash");
295
+ expect(commands[0]?.prompt).toContain("$ARGUMENTS");
296
+ expect(commands[0]?.prompt).toContain("$1, $2");
297
+ expect(commands[0]?.prompt).toContain("!`git status`");
298
+ expect(commands[0]?.prompt).toContain("@src/file.js");
299
+ });
300
+
301
+ test("ignores non-directory entries in commands folder", async () => {
302
+ const commandsDir = join(capabilityPath, "commands");
303
+ const validDir = join(commandsDir, "valid-command");
304
+ mkdirSync(validDir, { recursive: true });
305
+
306
+ writeFileSync(
307
+ join(validDir, "COMMAND.md"),
308
+ `---
309
+ name: valid-command
310
+ description: Valid command
311
+ ---
312
+
313
+ Valid prompt.`,
314
+ );
315
+
316
+ // Create a file directly in commands/ directory (should be ignored)
317
+ writeFileSync(join(commandsDir, "README.md"), "This should be ignored");
318
+
319
+ const commands = await loadCommands(capabilityPath, "test-cap");
320
+
321
+ expect(commands).toHaveLength(1);
322
+ expect(commands[0]?.name).toBe("valid-command");
323
+ });
324
+
325
+ test("associates commands with correct capability ID", async () => {
326
+ const commandDir = join(capabilityPath, "commands", "test-command");
327
+ mkdirSync(commandDir, { recursive: true });
328
+
329
+ writeFileSync(
330
+ join(commandDir, "COMMAND.md"),
331
+ `---
332
+ name: test-command
333
+ description: Test command
334
+ ---
335
+
336
+ Command prompt.`,
337
+ );
338
+
339
+ const commands = await loadCommands(capabilityPath, "my-capability");
340
+
341
+ expect(commands).toHaveLength(1);
342
+ expect(commands[0]?.capabilityId).toBe("my-capability");
343
+ });
344
+
345
+ test("handles commands with argument placeholders", async () => {
346
+ const commandDir = join(capabilityPath, "commands", "arg-command");
347
+ mkdirSync(commandDir, { recursive: true });
348
+
349
+ const commandContent = `---
350
+ name: arg-command
351
+ description: Command with argument placeholders
352
+ ---
353
+
354
+ Fix issue #$ARGUMENTS.
355
+
356
+ Review PR #$1 with priority $2 and assign to $3.`;
357
+
358
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
359
+
360
+ const commands = await loadCommands(capabilityPath, "test-cap");
361
+
362
+ expect(commands).toHaveLength(1);
363
+ expect(commands[0]?.prompt).toContain("$ARGUMENTS");
364
+ expect(commands[0]?.prompt).toContain("$1");
365
+ expect(commands[0]?.prompt).toContain("$2");
366
+ expect(commands[0]?.prompt).toContain("$3");
367
+ });
368
+
369
+ test("handles commands with bash execution syntax", async () => {
370
+ const commandDir = join(capabilityPath, "commands", "bash-command");
371
+ mkdirSync(commandDir, { recursive: true });
372
+
373
+ const commandContent = `---
374
+ name: bash-command
375
+ description: Command with bash execution
376
+ ---
377
+
378
+ Current status: !\`git status\`
379
+ Current branch: !\`git branch --show-current\`
380
+ Recent commits: !\`git log --oneline -10\``;
381
+
382
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
383
+
384
+ const commands = await loadCommands(capabilityPath, "test-cap");
385
+
386
+ expect(commands).toHaveLength(1);
387
+ expect(commands[0]?.prompt).toContain("!`git status`");
388
+ expect(commands[0]?.prompt).toContain("!`git branch --show-current`");
389
+ expect(commands[0]?.prompt).toContain("!`git log --oneline -10`");
390
+ });
391
+
392
+ test("handles commands with file references", async () => {
393
+ const commandDir = join(capabilityPath, "commands", "file-command");
394
+ mkdirSync(commandDir, { recursive: true });
395
+
396
+ const commandContent = `---
397
+ name: file-command
398
+ description: Command with file references
399
+ ---
400
+
401
+ Review the implementation in @src/utils/helpers.js.
402
+
403
+ Compare @src/old-version.js with @src/new-version.js.`;
404
+
405
+ writeFileSync(join(commandDir, "COMMAND.md"), commandContent);
406
+
407
+ const commands = await loadCommands(capabilityPath, "test-cap");
408
+
409
+ expect(commands).toHaveLength(1);
410
+ expect(commands[0]?.prompt).toContain("@src/utils/helpers.js");
411
+ expect(commands[0]?.prompt).toContain("@src/old-version.js");
412
+ expect(commands[0]?.prompt).toContain("@src/new-version.js");
413
+ });
414
+ });
@@ -0,0 +1,70 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Command } from "../types";
4
+ import { parseFrontmatterWithMarkdown } from "./yaml-parser";
5
+
6
+ interface CommandFrontmatter {
7
+ name: string;
8
+ description: string;
9
+ allowedTools?: string;
10
+ }
11
+
12
+ /**
13
+ * Load commands from a commands/ directory of a capability.
14
+ * Each command is a COMMAND.md file in its own subdirectory.
15
+ */
16
+ export async function loadCommands(
17
+ capabilityPath: string,
18
+ capabilityId: string,
19
+ ): Promise<Command[]> {
20
+ const commandsDir = join(capabilityPath, "commands");
21
+
22
+ if (!existsSync(commandsDir)) {
23
+ return [];
24
+ }
25
+
26
+ const commands: Command[] = [];
27
+ const entries = readdirSync(commandsDir, { withFileTypes: true });
28
+
29
+ for (const entry of entries) {
30
+ if (entry.isDirectory()) {
31
+ const commandPath = join(commandsDir, entry.name, "COMMAND.md");
32
+ if (existsSync(commandPath)) {
33
+ const command = await parseCommandFile(commandPath, capabilityId);
34
+ commands.push(command);
35
+ }
36
+ }
37
+ }
38
+
39
+ return commands;
40
+ }
41
+
42
+ async function parseCommandFile(filePath: string, capabilityId: string): Promise<Command> {
43
+ const content = await Bun.file(filePath).text();
44
+ const parsed = parseFrontmatterWithMarkdown<CommandFrontmatter>(content);
45
+
46
+ if (!parsed) {
47
+ throw new Error(`Invalid COMMAND.md format at ${filePath}: missing YAML frontmatter`);
48
+ }
49
+
50
+ const frontmatter = parsed.frontmatter;
51
+ const prompt = parsed.markdown;
52
+
53
+ if (!frontmatter.name || !frontmatter.description) {
54
+ throw new Error(`Invalid COMMAND.md at ${filePath}: name and description required`);
55
+ }
56
+
57
+ const result: Command = {
58
+ name: frontmatter.name,
59
+ description: frontmatter.description,
60
+ prompt: prompt.trim(),
61
+ capabilityId,
62
+ };
63
+
64
+ // Add optional fields if present
65
+ if (frontmatter.allowedTools) {
66
+ result.allowedTools = frontmatter.allowedTools;
67
+ }
68
+
69
+ return result;
70
+ }