@omnidev-ai/core 0.1.0 → 0.3.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 +3 -2
- package/src/capability/commands.test.ts +6 -10
- package/src/capability/commands.ts +3 -1
- package/src/capability/docs.test.ts +39 -46
- package/src/capability/docs.ts +3 -1
- package/src/capability/loader.test.ts +10 -157
- package/src/capability/loader.ts +8 -69
- package/src/capability/registry.test.ts +9 -27
- package/src/capability/rules.test.ts +25 -35
- package/src/capability/rules.ts +3 -1
- package/src/capability/skills.test.ts +6 -10
- package/src/capability/skills.ts +3 -1
- package/src/capability/sources.test.ts +142 -41
- package/src/capability/sources.ts +377 -345
- package/src/capability/subagents.test.ts +7 -11
- package/src/capability/subagents.ts +3 -1
- package/src/capability/wrapping-integration.test.ts +412 -0
- package/src/config/capabilities.ts +0 -28
- package/src/config/env.test.ts +4 -20
- package/src/config/loader.test.ts +4 -88
- package/src/config/loader.ts +88 -18
- package/src/config/parser.test.ts +0 -25
- package/src/config/profiles.test.ts +5 -42
- package/src/config/provider.test.ts +5 -18
- package/src/index.ts +1 -3
- package/src/mcp-json/manager.test.ts +77 -182
- package/src/mcp-json/manager.ts +22 -34
- package/src/state/active-profile.test.ts +4 -18
- package/src/state/index.ts +1 -0
- package/src/state/manifest.test.ts +25 -162
- package/src/state/manifest.ts +4 -31
- package/src/state/providers.test.ts +125 -0
- package/src/state/providers.ts +69 -0
- package/src/sync.ts +128 -53
- package/src/templates/claude.ts +9 -74
- package/src/test-utils/helpers.test.ts +18 -0
- package/src/test-utils/helpers.ts +98 -1
- package/src/test-utils/index.ts +4 -0
- package/src/types/capability-export.ts +0 -77
- package/src/types/index.ts +66 -22
- package/src/gitignore/manager.test.ts +0 -219
- package/src/gitignore/manager.ts +0 -167
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnidev-ai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"directory": "packages/core"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
|
-
".": "./src/index.ts"
|
|
12
|
+
".": "./src/index.ts",
|
|
13
|
+
"./test-utils": "./src/test-utils/index.ts"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"src",
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdirSync,
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
4
5
|
import { loadCommands } from "./commands";
|
|
5
6
|
|
|
6
7
|
describe("loadCommands", () => {
|
|
7
|
-
const testDir =
|
|
8
|
-
|
|
8
|
+
const testDir = setupTestDir("capability-commands-test-");
|
|
9
|
+
let capabilityPath: string;
|
|
9
10
|
|
|
10
11
|
beforeEach(() => {
|
|
12
|
+
capabilityPath = join(testDir.path, "test-capability");
|
|
11
13
|
mkdirSync(capabilityPath, { recursive: true });
|
|
12
14
|
});
|
|
13
15
|
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
if (testDir) {
|
|
16
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
16
|
test("returns empty array when commands directory does not exist", async () => {
|
|
21
17
|
const commands = await loadCommands(capabilityPath, "test-cap");
|
|
22
18
|
expect(commands).toEqual([]);
|
|
@@ -24,7 +24,9 @@ export async function loadCommands(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const commands: Command[] = [];
|
|
27
|
-
const entries = readdirSync(commandsDir, { withFileTypes: true })
|
|
27
|
+
const entries = readdirSync(commandsDir, { withFileTypes: true }).sort((a, b) =>
|
|
28
|
+
a.name.localeCompare(b.name),
|
|
29
|
+
);
|
|
28
30
|
|
|
29
31
|
for (const entry of entries) {
|
|
30
32
|
if (entry.isDirectory()) {
|
|
@@ -1,31 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdirSync,
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
4
5
|
import { loadDocs } from "./docs";
|
|
5
6
|
|
|
6
7
|
describe("loadDocs", () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
testDir = join(process.cwd(), "test-capability-docs");
|
|
11
|
-
mkdirSync(testDir, { recursive: true });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
if (testDir) {
|
|
16
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
});
|
|
8
|
+
const testDir = setupTestDir("capability-docs-test-");
|
|
19
9
|
|
|
20
10
|
test("returns empty array when no docs exist", async () => {
|
|
21
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
11
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
22
12
|
expect(docs).toEqual([]);
|
|
23
13
|
});
|
|
24
14
|
|
|
25
15
|
test("loads only definition.md when docs directory does not exist", async () => {
|
|
26
|
-
writeFileSync(
|
|
16
|
+
writeFileSync(
|
|
17
|
+
join(testDir.path, "definition.md"),
|
|
18
|
+
"# Test Capability\n\nThis is the definition.",
|
|
19
|
+
);
|
|
27
20
|
|
|
28
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
21
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
29
22
|
expect(docs).toHaveLength(1);
|
|
30
23
|
expect(docs[0]?.name).toBe("definition");
|
|
31
24
|
expect(docs[0]?.content).toBe("# Test Capability\n\nThis is the definition.");
|
|
@@ -33,11 +26,11 @@ describe("loadDocs", () => {
|
|
|
33
26
|
});
|
|
34
27
|
|
|
35
28
|
test("loads only docs from docs directory when definition.md does not exist", async () => {
|
|
36
|
-
const docsDir = join(testDir, "docs");
|
|
29
|
+
const docsDir = join(testDir.path, "docs");
|
|
37
30
|
mkdirSync(docsDir);
|
|
38
31
|
writeFileSync(join(docsDir, "guide.md"), "# Guide\n\nGuide content.");
|
|
39
32
|
|
|
40
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
33
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
41
34
|
expect(docs).toHaveLength(1);
|
|
42
35
|
expect(docs[0]?.name).toBe("guide");
|
|
43
36
|
expect(docs[0]?.content).toBe("# Guide\n\nGuide content.");
|
|
@@ -45,14 +38,14 @@ describe("loadDocs", () => {
|
|
|
45
38
|
});
|
|
46
39
|
|
|
47
40
|
test("loads both definition.md and docs from docs directory", async () => {
|
|
48
|
-
writeFileSync(join(testDir, "definition.md"), "# Definition");
|
|
41
|
+
writeFileSync(join(testDir.path, "definition.md"), "# Definition");
|
|
49
42
|
|
|
50
|
-
const docsDir = join(testDir, "docs");
|
|
43
|
+
const docsDir = join(testDir.path, "docs");
|
|
51
44
|
mkdirSync(docsDir);
|
|
52
45
|
writeFileSync(join(docsDir, "guide.md"), "# Guide");
|
|
53
46
|
writeFileSync(join(docsDir, "examples.md"), "# Examples");
|
|
54
47
|
|
|
55
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
48
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
56
49
|
expect(docs).toHaveLength(3);
|
|
57
50
|
|
|
58
51
|
const names = docs.map((d) => d.name).sort();
|
|
@@ -60,37 +53,37 @@ describe("loadDocs", () => {
|
|
|
60
53
|
});
|
|
61
54
|
|
|
62
55
|
test("trims whitespace from doc content", async () => {
|
|
63
|
-
writeFileSync(join(testDir, "definition.md"), "\n\n # Definition\n\nContent.\n\n ");
|
|
56
|
+
writeFileSync(join(testDir.path, "definition.md"), "\n\n # Definition\n\nContent.\n\n ");
|
|
64
57
|
|
|
65
|
-
const docsDir = join(testDir, "docs");
|
|
58
|
+
const docsDir = join(testDir.path, "docs");
|
|
66
59
|
mkdirSync(docsDir);
|
|
67
60
|
writeFileSync(join(docsDir, "guide.md"), " \n# Guide\n\nGuide content.\n ");
|
|
68
61
|
|
|
69
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
62
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
70
63
|
expect(docs).toHaveLength(2);
|
|
71
64
|
expect(docs[0]?.content).toBe("# Definition\n\nContent.");
|
|
72
65
|
expect(docs[1]?.content).toBe("# Guide\n\nGuide content.");
|
|
73
66
|
});
|
|
74
67
|
|
|
75
68
|
test("ignores non-markdown files in docs directory", async () => {
|
|
76
|
-
const docsDir = join(testDir, "docs");
|
|
69
|
+
const docsDir = join(testDir.path, "docs");
|
|
77
70
|
mkdirSync(docsDir);
|
|
78
71
|
writeFileSync(join(docsDir, "guide.md"), "# Guide");
|
|
79
72
|
writeFileSync(join(docsDir, "readme.txt"), "Not markdown");
|
|
80
73
|
writeFileSync(join(docsDir, "config.json"), "{}");
|
|
81
74
|
|
|
82
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
75
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
83
76
|
expect(docs).toHaveLength(1);
|
|
84
77
|
expect(docs[0]?.name).toBe("guide");
|
|
85
78
|
});
|
|
86
79
|
|
|
87
80
|
test("ignores directories in docs directory", async () => {
|
|
88
|
-
const docsDir = join(testDir, "docs");
|
|
81
|
+
const docsDir = join(testDir.path, "docs");
|
|
89
82
|
mkdirSync(docsDir);
|
|
90
83
|
mkdirSync(join(docsDir, "subdir"));
|
|
91
84
|
writeFileSync(join(docsDir, "guide.md"), "# Guide");
|
|
92
85
|
|
|
93
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
86
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
94
87
|
expect(docs).toHaveLength(1);
|
|
95
88
|
expect(docs[0]?.name).toBe("guide");
|
|
96
89
|
});
|
|
@@ -115,46 +108,46 @@ const code = "example";
|
|
|
115
108
|
|----------|----------|
|
|
116
109
|
| Cell 1 | Cell 2 |`;
|
|
117
110
|
|
|
118
|
-
writeFileSync(join(testDir, "definition.md"), complexMarkdown);
|
|
111
|
+
writeFileSync(join(testDir.path, "definition.md"), complexMarkdown);
|
|
119
112
|
|
|
120
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
113
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
121
114
|
expect(docs).toHaveLength(1);
|
|
122
115
|
expect(docs[0]?.content).toBe(complexMarkdown);
|
|
123
116
|
});
|
|
124
117
|
|
|
125
118
|
test("handles empty doc files", async () => {
|
|
126
|
-
writeFileSync(join(testDir, "definition.md"), "");
|
|
119
|
+
writeFileSync(join(testDir.path, "definition.md"), "");
|
|
127
120
|
|
|
128
|
-
const docsDir = join(testDir, "docs");
|
|
121
|
+
const docsDir = join(testDir.path, "docs");
|
|
129
122
|
mkdirSync(docsDir);
|
|
130
123
|
writeFileSync(join(docsDir, "guide.md"), "");
|
|
131
124
|
|
|
132
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
125
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
133
126
|
expect(docs).toHaveLength(2);
|
|
134
127
|
expect(docs[0]?.content).toBe("");
|
|
135
128
|
expect(docs[1]?.content).toBe("");
|
|
136
129
|
});
|
|
137
130
|
|
|
138
131
|
test("handles docs with only whitespace", async () => {
|
|
139
|
-
writeFileSync(join(testDir, "definition.md"), " \n\n ");
|
|
132
|
+
writeFileSync(join(testDir.path, "definition.md"), " \n\n ");
|
|
140
133
|
|
|
141
|
-
const docsDir = join(testDir, "docs");
|
|
134
|
+
const docsDir = join(testDir.path, "docs");
|
|
142
135
|
mkdirSync(docsDir);
|
|
143
136
|
writeFileSync(join(docsDir, "guide.md"), "\n\n\n");
|
|
144
137
|
|
|
145
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
138
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
146
139
|
expect(docs).toHaveLength(2);
|
|
147
140
|
expect(docs[0]?.content).toBe("");
|
|
148
141
|
expect(docs[1]?.content).toBe("");
|
|
149
142
|
});
|
|
150
143
|
|
|
151
144
|
test("handles doc names with hyphens and underscores", async () => {
|
|
152
|
-
const docsDir = join(testDir, "docs");
|
|
145
|
+
const docsDir = join(testDir.path, "docs");
|
|
153
146
|
mkdirSync(docsDir);
|
|
154
147
|
writeFileSync(join(docsDir, "api-reference.md"), "Content");
|
|
155
148
|
writeFileSync(join(docsDir, "getting_started.md"), "Content");
|
|
156
149
|
|
|
157
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
150
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
158
151
|
expect(docs).toHaveLength(2);
|
|
159
152
|
|
|
160
153
|
const names = docs.map((d) => d.name).sort();
|
|
@@ -162,15 +155,15 @@ const code = "example";
|
|
|
162
155
|
});
|
|
163
156
|
|
|
164
157
|
test("loads multiple docs in consistent order", async () => {
|
|
165
|
-
writeFileSync(join(testDir, "definition.md"), "Definition");
|
|
158
|
+
writeFileSync(join(testDir.path, "definition.md"), "Definition");
|
|
166
159
|
|
|
167
|
-
const docsDir = join(testDir, "docs");
|
|
160
|
+
const docsDir = join(testDir.path, "docs");
|
|
168
161
|
mkdirSync(docsDir);
|
|
169
162
|
writeFileSync(join(docsDir, "aaa.md"), "AAA");
|
|
170
163
|
writeFileSync(join(docsDir, "zzz.md"), "ZZZ");
|
|
171
164
|
writeFileSync(join(docsDir, "mmm.md"), "MMM");
|
|
172
165
|
|
|
173
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
166
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
174
167
|
expect(docs).toHaveLength(4);
|
|
175
168
|
|
|
176
169
|
// definition.md should be first, then docs in filesystem order
|
|
@@ -183,16 +176,16 @@ const code = "example";
|
|
|
183
176
|
});
|
|
184
177
|
|
|
185
178
|
test("returns empty array when docs directory is empty", async () => {
|
|
186
|
-
mkdirSync(join(testDir, "docs"));
|
|
187
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
179
|
+
mkdirSync(join(testDir.path, "docs"));
|
|
180
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
188
181
|
expect(docs).toEqual([]);
|
|
189
182
|
});
|
|
190
183
|
|
|
191
184
|
test("handles very long doc content", async () => {
|
|
192
185
|
const longContent = `# Long Document\n\n${"Content line.\n".repeat(1000)}`;
|
|
193
|
-
writeFileSync(join(testDir, "definition.md"), longContent);
|
|
186
|
+
writeFileSync(join(testDir.path, "definition.md"), longContent);
|
|
194
187
|
|
|
195
|
-
const docs = await loadDocs(testDir, "test-cap");
|
|
188
|
+
const docs = await loadDocs(testDir.path, "test-cap");
|
|
196
189
|
expect(docs).toHaveLength(1);
|
|
197
190
|
expect(docs[0]?.content).toBe(longContent.trim());
|
|
198
191
|
});
|
package/src/capability/docs.ts
CHANGED
|
@@ -26,7 +26,9 @@ export async function loadDocs(capabilityPath: string, capabilityId: string): Pr
|
|
|
26
26
|
// Load docs/*.md
|
|
27
27
|
const docsDir = join(capabilityPath, "docs");
|
|
28
28
|
if (existsSync(docsDir)) {
|
|
29
|
-
const entries = readdirSync(docsDir, { withFileTypes: true })
|
|
29
|
+
const entries = readdirSync(docsDir, { withFileTypes: true }).sort((a, b) =>
|
|
30
|
+
a.name.localeCompare(b.name),
|
|
31
|
+
);
|
|
30
32
|
|
|
31
33
|
for (const entry of entries) {
|
|
32
34
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -1,35 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
5
5
|
import { discoverCapabilities, loadCapability, loadCapabilityConfig } from "./loader";
|
|
6
6
|
|
|
7
7
|
describe("discoverCapabilities", () => {
|
|
8
|
-
|
|
8
|
+
const testDir = setupTestDir("test-capabilities-", { chdir: true });
|
|
9
9
|
let capabilitiesDir: string;
|
|
10
|
-
let originalCwd: string;
|
|
11
10
|
|
|
12
11
|
beforeEach(() => {
|
|
13
|
-
// Save current working directory
|
|
14
|
-
originalCwd = process.cwd();
|
|
15
|
-
|
|
16
12
|
// Create test directory in os temp dir
|
|
17
|
-
|
|
18
|
-
capabilitiesDir = join(testDir, "omni", "capabilities");
|
|
13
|
+
capabilitiesDir = join(testDir.path, "omni", "capabilities");
|
|
19
14
|
mkdirSync(capabilitiesDir, { recursive: true });
|
|
20
|
-
|
|
21
|
-
// Change to test directory
|
|
22
|
-
process.chdir(testDir);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
// Restore working directory
|
|
27
|
-
process.chdir(originalCwd);
|
|
28
|
-
|
|
29
|
-
// Cleanup
|
|
30
|
-
if (existsSync(testDir)) {
|
|
31
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
32
|
-
}
|
|
33
15
|
});
|
|
34
16
|
|
|
35
17
|
test("returns empty array when capabilities directory does not exist", async () => {
|
|
@@ -143,31 +125,13 @@ describe("discoverCapabilities", () => {
|
|
|
143
125
|
});
|
|
144
126
|
|
|
145
127
|
describe("loadCapabilityConfig", () => {
|
|
146
|
-
|
|
128
|
+
const testDir = setupTestDir("test-capability-config-", { chdir: true });
|
|
147
129
|
let capabilitiesDir: string;
|
|
148
|
-
let originalCwd: string;
|
|
149
130
|
|
|
150
131
|
beforeEach(() => {
|
|
151
|
-
// Save current working directory
|
|
152
|
-
originalCwd = process.cwd();
|
|
153
|
-
|
|
154
132
|
// Create test directory in os temp dir
|
|
155
|
-
|
|
156
|
-
capabilitiesDir = join(testDir, ".omni", "capabilities");
|
|
133
|
+
capabilitiesDir = join(testDir.path, ".omni", "capabilities");
|
|
157
134
|
mkdirSync(capabilitiesDir, { recursive: true });
|
|
158
|
-
|
|
159
|
-
// Change to test directory
|
|
160
|
-
process.chdir(testDir);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
afterEach(() => {
|
|
164
|
-
// Restore working directory
|
|
165
|
-
process.chdir(originalCwd);
|
|
166
|
-
|
|
167
|
-
// Cleanup
|
|
168
|
-
if (existsSync(testDir)) {
|
|
169
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
170
|
-
}
|
|
171
135
|
});
|
|
172
136
|
|
|
173
137
|
test("loads valid capability config with all required fields", async () => {
|
|
@@ -238,78 +202,6 @@ secret = true`,
|
|
|
238
202
|
expect(config.env?.[0]?.secret).toBe(true);
|
|
239
203
|
});
|
|
240
204
|
|
|
241
|
-
test("loads capability config with optional mcp field", async () => {
|
|
242
|
-
const capPath = join(".omni", "capabilities", "with-mcp");
|
|
243
|
-
mkdirSync(capPath, { recursive: true });
|
|
244
|
-
writeFileSync(
|
|
245
|
-
join(capPath, "capability.toml"),
|
|
246
|
-
`[capability]
|
|
247
|
-
id = "with-mcp"
|
|
248
|
-
name = "With MCP"
|
|
249
|
-
version = "1.0.0"
|
|
250
|
-
description = "Has MCP tools"
|
|
251
|
-
|
|
252
|
-
[mcp]
|
|
253
|
-
tools = ["test_tool"]`,
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
const config = await loadCapabilityConfig(capPath);
|
|
257
|
-
|
|
258
|
-
expect(config.capability.id).toBe("with-mcp");
|
|
259
|
-
expect(config.mcp?.tools).toEqual(["test_tool"]);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("throws error for reserved capability name (fs)", async () => {
|
|
263
|
-
const capPath = join(".omni", "capabilities", "fs");
|
|
264
|
-
mkdirSync(capPath, { recursive: true });
|
|
265
|
-
writeFileSync(
|
|
266
|
-
join(capPath, "capability.toml"),
|
|
267
|
-
`[capability]
|
|
268
|
-
id = "fs"
|
|
269
|
-
name = "File System"
|
|
270
|
-
version = "1.0.0"
|
|
271
|
-
description = "Reserved name"`,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
expect(async () => await loadCapabilityConfig(capPath)).toThrow(
|
|
275
|
-
'Capability name "fs" is reserved. Choose a different name.',
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("throws error for reserved capability name (react)", async () => {
|
|
280
|
-
const capPath = join(".omni", "capabilities", "react-cap");
|
|
281
|
-
mkdirSync(capPath, { recursive: true });
|
|
282
|
-
writeFileSync(
|
|
283
|
-
join(capPath, "capability.toml"),
|
|
284
|
-
`[capability]
|
|
285
|
-
id = "react"
|
|
286
|
-
name = "React"
|
|
287
|
-
version = "1.0.0"
|
|
288
|
-
description = "Reserved name"`,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
expect(async () => await loadCapabilityConfig(capPath)).toThrow(
|
|
292
|
-
'Capability name "react" is reserved. Choose a different name.',
|
|
293
|
-
);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test("throws error for reserved capability name (typescript)", async () => {
|
|
297
|
-
const capPath = join(".omni", "capabilities", "ts-cap");
|
|
298
|
-
mkdirSync(capPath, { recursive: true });
|
|
299
|
-
writeFileSync(
|
|
300
|
-
join(capPath, "capability.toml"),
|
|
301
|
-
`[capability]
|
|
302
|
-
id = "typescript"
|
|
303
|
-
name = "TypeScript"
|
|
304
|
-
version = "1.0.0"
|
|
305
|
-
description = "Reserved name"`,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
expect(async () => await loadCapabilityConfig(capPath)).toThrow(
|
|
309
|
-
'Capability name "typescript" is reserved. Choose a different name.',
|
|
310
|
-
);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
205
|
test("throws error when capability.toml is missing", async () => {
|
|
314
206
|
const capPath = join(".omni", "capabilities", "missing-config");
|
|
315
207
|
mkdirSync(capPath, { recursive: true });
|
|
@@ -345,24 +237,6 @@ id = "bad-toml"
|
|
|
345
237
|
expect(async () => await loadCapabilityConfig(capPath)).toThrow();
|
|
346
238
|
});
|
|
347
239
|
|
|
348
|
-
test("allows non-reserved capability names", async () => {
|
|
349
|
-
const capPath = join(".omni", "capabilities", "my-custom-capability");
|
|
350
|
-
mkdirSync(capPath, { recursive: true });
|
|
351
|
-
writeFileSync(
|
|
352
|
-
join(capPath, "capability.toml"),
|
|
353
|
-
`[capability]
|
|
354
|
-
id = "my-custom-capability"
|
|
355
|
-
name = "My Custom Capability"
|
|
356
|
-
version = "2.1.0"
|
|
357
|
-
description = "A custom capability"`,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
const config = await loadCapabilityConfig(capPath);
|
|
361
|
-
|
|
362
|
-
expect(config.capability.id).toBe("my-custom-capability");
|
|
363
|
-
expect(config.capability.name).toBe("My Custom Capability");
|
|
364
|
-
});
|
|
365
|
-
|
|
366
240
|
test("handles capability config with all optional fields defined", async () => {
|
|
367
241
|
const capPath = join(".omni", "capabilities", "complete-cap");
|
|
368
242
|
mkdirSync(capPath, { recursive: true });
|
|
@@ -388,9 +262,7 @@ key = "VAR2"
|
|
|
388
262
|
description = "Variable 2"
|
|
389
263
|
required = false
|
|
390
264
|
secret = true
|
|
391
|
-
|
|
392
|
-
[mcp]
|
|
393
|
-
tools = ["tool1", "tool2"]`,
|
|
265
|
+
`,
|
|
394
266
|
);
|
|
395
267
|
|
|
396
268
|
const config = await loadCapabilityConfig(capPath);
|
|
@@ -398,36 +270,17 @@ tools = ["tool1", "tool2"]`,
|
|
|
398
270
|
expect(config.capability.id).toBe("complete-cap");
|
|
399
271
|
expect(config.exports?.functions).toEqual(["fn1", "fn2"]);
|
|
400
272
|
expect(config.env).toHaveLength(2);
|
|
401
|
-
expect(config.mcp?.tools).toEqual(["tool1", "tool2"]);
|
|
402
273
|
});
|
|
403
274
|
});
|
|
404
275
|
|
|
405
276
|
describe("loadCapability", () => {
|
|
406
|
-
|
|
277
|
+
const testDir = setupTestDir("test-load-capability-", { chdir: true });
|
|
407
278
|
let capabilitiesDir: string;
|
|
408
|
-
let originalCwd: string;
|
|
409
279
|
|
|
410
280
|
beforeEach(() => {
|
|
411
|
-
// Save current working directory
|
|
412
|
-
originalCwd = process.cwd();
|
|
413
|
-
|
|
414
281
|
// Create test directory in os temp dir
|
|
415
|
-
|
|
416
|
-
capabilitiesDir = join(testDir, "omni", "capabilities");
|
|
282
|
+
capabilitiesDir = join(testDir.path, "omni", "capabilities");
|
|
417
283
|
mkdirSync(capabilitiesDir, { recursive: true });
|
|
418
|
-
|
|
419
|
-
// Change to test directory
|
|
420
|
-
process.chdir(testDir);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
afterEach(() => {
|
|
424
|
-
// Restore working directory
|
|
425
|
-
process.chdir(originalCwd);
|
|
426
|
-
|
|
427
|
-
// Cleanup
|
|
428
|
-
if (existsSync(testDir)) {
|
|
429
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
430
|
-
}
|
|
431
284
|
});
|
|
432
285
|
|
|
433
286
|
test("loads capability with minimal config (no optional fields)", async () => {
|
package/src/capability/loader.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { validateEnv } from "../config/env";
|
|
4
4
|
import { parseCapabilityConfig } from "../config/parser";
|
|
@@ -24,78 +24,24 @@ import { loadSkills } from "./skills";
|
|
|
24
24
|
import { loadSubagents } from "./subagents";
|
|
25
25
|
|
|
26
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
27
|
|
|
67
28
|
/**
|
|
68
29
|
* Discovers capabilities by scanning the .omni/capabilities directory.
|
|
69
30
|
* A directory is considered a capability if it contains a capability.toml file.
|
|
70
|
-
* Follows symlinks to support linked capability directories.
|
|
71
31
|
*
|
|
72
32
|
* @returns Array of capability directory paths
|
|
73
33
|
*/
|
|
74
34
|
export async function discoverCapabilities(): Promise<string[]> {
|
|
75
35
|
const capabilities: string[] = [];
|
|
76
36
|
|
|
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
37
|
if (existsSync(CAPABILITIES_DIR)) {
|
|
94
|
-
const entries = readdirSync(CAPABILITIES_DIR, { withFileTypes: true })
|
|
38
|
+
const entries = readdirSync(CAPABILITIES_DIR, { withFileTypes: true }).sort((a, b) =>
|
|
39
|
+
a.name.localeCompare(b.name),
|
|
40
|
+
);
|
|
95
41
|
|
|
96
42
|
for (const entry of entries) {
|
|
97
|
-
|
|
98
|
-
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
const entryPath = join(CAPABILITIES_DIR, entry.name);
|
|
99
45
|
const configPath = join(entryPath, "capability.toml");
|
|
100
46
|
if (existsSync(configPath)) {
|
|
101
47
|
capabilities.push(entryPath);
|
|
@@ -109,24 +55,17 @@ export async function discoverCapabilities(): Promise<string[]> {
|
|
|
109
55
|
|
|
110
56
|
/**
|
|
111
57
|
* Loads and parses a capability configuration file.
|
|
112
|
-
* Validates required fields
|
|
58
|
+
* Validates required fields.
|
|
113
59
|
*
|
|
114
60
|
* @param capabilityPath - Path to the capability directory
|
|
115
61
|
* @returns Parsed capability configuration
|
|
116
|
-
* @throws Error if the config is invalid
|
|
62
|
+
* @throws Error if the config is invalid
|
|
117
63
|
*/
|
|
118
64
|
export async function loadCapabilityConfig(capabilityPath: string): Promise<CapabilityConfig> {
|
|
119
65
|
const configPath = join(capabilityPath, "capability.toml");
|
|
120
66
|
const content = await Bun.file(configPath).text();
|
|
121
67
|
const config = parseCapabilityConfig(content);
|
|
122
68
|
|
|
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
69
|
return config;
|
|
131
70
|
}
|
|
132
71
|
|