@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.
- package/package.json +31 -0
- package/src/capability/AGENTS.md +58 -0
- package/src/capability/commands.test.ts +414 -0
- package/src/capability/commands.ts +70 -0
- package/src/capability/docs.test.ts +199 -0
- package/src/capability/docs.ts +46 -0
- package/src/capability/index.ts +20 -0
- package/src/capability/loader.test.ts +815 -0
- package/src/capability/loader.ts +492 -0
- package/src/capability/registry.test.ts +473 -0
- package/src/capability/registry.ts +55 -0
- package/src/capability/rules.test.ts +145 -0
- package/src/capability/rules.ts +133 -0
- package/src/capability/skills.test.ts +316 -0
- package/src/capability/skills.ts +56 -0
- package/src/capability/sources.test.ts +338 -0
- package/src/capability/sources.ts +966 -0
- package/src/capability/subagents.test.ts +478 -0
- package/src/capability/subagents.ts +103 -0
- package/src/capability/yaml-parser.ts +81 -0
- package/src/config/AGENTS.md +46 -0
- package/src/config/capabilities.ts +82 -0
- package/src/config/env.test.ts +286 -0
- package/src/config/env.ts +96 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +282 -0
- package/src/config/loader.ts +137 -0
- package/src/config/parser.test.ts +281 -0
- package/src/config/parser.ts +55 -0
- package/src/config/profiles.test.ts +259 -0
- package/src/config/profiles.ts +75 -0
- package/src/config/provider.test.ts +79 -0
- package/src/config/provider.ts +55 -0
- package/src/debug.ts +20 -0
- package/src/gitignore/manager.test.ts +219 -0
- package/src/gitignore/manager.ts +167 -0
- package/src/index.test.ts +26 -0
- package/src/index.ts +39 -0
- package/src/mcp-json/index.ts +1 -0
- package/src/mcp-json/manager.test.ts +415 -0
- package/src/mcp-json/manager.ts +118 -0
- package/src/state/active-profile.test.ts +131 -0
- package/src/state/active-profile.ts +41 -0
- package/src/state/index.ts +2 -0
- package/src/state/manifest.test.ts +548 -0
- package/src/state/manifest.ts +164 -0
- package/src/sync.ts +213 -0
- package/src/templates/agents.test.ts +23 -0
- package/src/templates/agents.ts +14 -0
- package/src/templates/claude.test.ts +48 -0
- package/src/templates/claude.ts +122 -0
- package/src/test-utils/helpers.test.ts +196 -0
- package/src/test-utils/helpers.ts +187 -0
- package/src/test-utils/index.ts +30 -0
- package/src/test-utils/mocks.test.ts +83 -0
- package/src/test-utils/mocks.ts +101 -0
- package/src/types/capability-export.ts +234 -0
- package/src/types/index.test.ts +28 -0
- 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
|
+
}
|