@omnidev-ai/core 0.1.1 → 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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/capability/commands.test.ts +6 -10
  3. package/src/capability/docs.test.ts +39 -46
  4. package/src/capability/docs.ts +3 -1
  5. package/src/capability/loader.test.ts +10 -157
  6. package/src/capability/loader.ts +8 -69
  7. package/src/capability/registry.test.ts +9 -27
  8. package/src/capability/rules.test.ts +25 -35
  9. package/src/capability/rules.ts +3 -1
  10. package/src/capability/skills.test.ts +6 -10
  11. package/src/capability/sources.test.ts +142 -41
  12. package/src/capability/sources.ts +377 -345
  13. package/src/capability/subagents.test.ts +7 -11
  14. package/src/capability/subagents.ts +3 -1
  15. package/src/capability/wrapping-integration.test.ts +412 -0
  16. package/src/config/capabilities.ts +0 -28
  17. package/src/config/env.test.ts +4 -18
  18. package/src/config/loader.test.ts +4 -86
  19. package/src/config/loader.ts +88 -18
  20. package/src/config/parser.test.ts +0 -25
  21. package/src/config/profiles.test.ts +5 -39
  22. package/src/config/provider.test.ts +5 -19
  23. package/src/index.ts +1 -3
  24. package/src/mcp-json/manager.test.ts +77 -182
  25. package/src/mcp-json/manager.ts +22 -34
  26. package/src/state/active-profile.test.ts +4 -18
  27. package/src/state/index.ts +1 -0
  28. package/src/state/manifest.test.ts +25 -162
  29. package/src/state/manifest.ts +4 -31
  30. package/src/state/providers.test.ts +125 -0
  31. package/src/state/providers.ts +69 -0
  32. package/src/sync.ts +128 -53
  33. package/src/templates/claude.ts +9 -74
  34. package/src/test-utils/helpers.test.ts +18 -0
  35. package/src/test-utils/helpers.ts +87 -2
  36. package/src/test-utils/index.ts +3 -0
  37. package/src/types/capability-export.ts +0 -77
  38. package/src/types/index.ts +66 -22
  39. package/src/gitignore/manager.test.ts +0 -216
  40. package/src/gitignore/manager.ts +0 -167
@@ -1,7 +1,5 @@
1
- import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
5
3
  import type { LoadedCapability } from "../types";
6
4
  import {
7
5
  buildManifestFromCapabilities,
@@ -12,20 +10,7 @@ import {
12
10
  } from "./manifest";
13
11
 
14
12
  describe("manifest", () => {
15
- let originalCwd: string;
16
- let tempDir: string;
17
-
18
- beforeEach(() => {
19
- originalCwd = process.cwd();
20
- tempDir = mkdtempSync(join(tmpdir(), "manifest-test-"));
21
- mkdirSync(join(tempDir, ".omni"), { recursive: true });
22
- process.chdir(tempDir);
23
- });
24
-
25
- afterEach(() => {
26
- process.chdir(originalCwd);
27
- rmSync(tempDir, { recursive: true, force: true });
28
- });
13
+ setupTestDir("manifest-test-", { chdir: true, createOmniDir: true });
29
14
 
30
15
  describe("loadManifest", () => {
31
16
  test("returns empty manifest when file does not exist", async () => {
@@ -46,6 +31,7 @@ describe("manifest", () => {
46
31
  rules: ["rule1"],
47
32
  commands: ["cmd1"],
48
33
  subagents: ["agent1"],
34
+ mcps: [],
49
35
  },
50
36
  },
51
37
  };
@@ -69,6 +55,7 @@ describe("manifest", () => {
69
55
  rules: ["r1"],
70
56
  commands: [],
71
57
  subagents: [],
58
+ mcps: [],
72
59
  },
73
60
  },
74
61
  };
@@ -119,6 +106,7 @@ describe("manifest", () => {
119
106
  rules: [],
120
107
  docs: [],
121
108
  subagents: [],
109
+ mcps: [],
122
110
  commands: [],
123
111
  exports: {},
124
112
  },
@@ -132,12 +120,14 @@ describe("manifest", () => {
132
120
  rules: ["rule-a"],
133
121
  commands: ["cmd-a"],
134
122
  subagents: ["agent-a"],
123
+ mcps: [],
135
124
  });
136
125
  expect(manifest.capabilities.cap2).toEqual({
137
126
  skills: [],
138
127
  rules: [],
139
128
  commands: [],
140
129
  subagents: [],
130
+ mcps: [],
141
131
  });
142
132
  });
143
133
 
@@ -148,146 +138,6 @@ describe("manifest", () => {
148
138
  expect(manifest.capabilities).toEqual({});
149
139
  expect(manifest.syncedAt).toBeDefined();
150
140
  });
151
-
152
- test("includes MCP entry when capability has MCP config", () => {
153
- const capabilities: LoadedCapability[] = [
154
- {
155
- id: "context7",
156
- path: "/path/to/context7",
157
- config: {
158
- capability: {
159
- id: "context7",
160
- name: "Context7",
161
- version: "1.0.0",
162
- description: "",
163
- },
164
- mcp: {
165
- command: "npx",
166
- args: ["-y", "@upstash/context7-mcp"],
167
- },
168
- },
169
- skills: [],
170
- rules: [],
171
- docs: [],
172
- subagents: [],
173
- commands: [],
174
- exports: {},
175
- },
176
- ];
177
-
178
- const manifest = buildManifestFromCapabilities(capabilities);
179
-
180
- expect(manifest.capabilities.context7.mcp).toEqual({
181
- serverName: "omni-context7",
182
- command: "npx",
183
- args: ["-y", "@upstash/context7-mcp"],
184
- });
185
- });
186
-
187
- test("includes MCP env when present", () => {
188
- const capabilities: LoadedCapability[] = [
189
- {
190
- id: "my-mcp",
191
- path: "/path/to/my-mcp",
192
- config: {
193
- capability: {
194
- id: "my-mcp",
195
- name: "My MCP",
196
- version: "1.0.0",
197
- description: "",
198
- },
199
- mcp: {
200
- command: "node",
201
- args: ["server.js"],
202
- env: { API_KEY: "secret" },
203
- },
204
- },
205
- skills: [],
206
- rules: [],
207
- docs: [],
208
- subagents: [],
209
- commands: [],
210
- exports: {},
211
- },
212
- ];
213
-
214
- const manifest = buildManifestFromCapabilities(capabilities);
215
-
216
- expect(manifest.capabilities["my-mcp"].mcp).toEqual({
217
- serverName: "omni-my-mcp",
218
- command: "node",
219
- args: ["server.js"],
220
- env: { API_KEY: "secret" },
221
- });
222
- });
223
-
224
- test("does not include MCP entry when capability has no MCP config", () => {
225
- const capabilities: LoadedCapability[] = [
226
- {
227
- id: "tasks",
228
- path: "/path/to/tasks",
229
- config: {
230
- capability: { id: "tasks", name: "Tasks", version: "1.0.0", description: "" },
231
- },
232
- skills: [],
233
- rules: [],
234
- docs: [],
235
- subagents: [],
236
- commands: [],
237
- exports: {},
238
- },
239
- ];
240
-
241
- const manifest = buildManifestFromCapabilities(capabilities);
242
-
243
- expect(manifest.capabilities.tasks.mcp).toBeUndefined();
244
- });
245
-
246
- test("handles mixed capabilities with and without MCP", () => {
247
- const capabilities: LoadedCapability[] = [
248
- {
249
- id: "tasks",
250
- path: "/path/to/tasks",
251
- config: {
252
- capability: { id: "tasks", name: "Tasks", version: "1.0.0", description: "" },
253
- },
254
- skills: [],
255
- rules: [],
256
- docs: [],
257
- subagents: [],
258
- commands: [],
259
- exports: {},
260
- },
261
- {
262
- id: "context7",
263
- path: "/path/to/context7",
264
- config: {
265
- capability: {
266
- id: "context7",
267
- name: "Context7",
268
- version: "1.0.0",
269
- description: "",
270
- },
271
- mcp: {
272
- command: "npx",
273
- args: ["context7-mcp"],
274
- },
275
- },
276
- skills: [],
277
- rules: [],
278
- docs: [],
279
- subagents: [],
280
- commands: [],
281
- exports: {},
282
- },
283
- ];
284
-
285
- const manifest = buildManifestFromCapabilities(capabilities);
286
-
287
- expect(manifest.capabilities.tasks.mcp).toBeUndefined();
288
- expect(manifest.capabilities.context7.mcp).toBeDefined();
289
- expect(manifest.capabilities.context7.mcp?.serverName).toBe("omni-context7");
290
- });
291
141
  });
292
142
 
293
143
  describe("saveManifest and loadManifest round-trip", () => {
@@ -296,8 +146,8 @@ describe("manifest", () => {
296
146
  version: 1,
297
147
  syncedAt: "2025-01-01T00:00:00.000Z",
298
148
  capabilities: {
299
- cap1: { skills: ["s1"], rules: ["r1"], commands: ["c1"], subagents: ["a1"] },
300
- cap2: { skills: [], rules: [], commands: [], subagents: [] },
149
+ cap1: { skills: ["s1"], rules: ["r1"], commands: ["c1"], subagents: ["a1"], mcps: [] },
150
+ cap2: { skills: [], rules: [], commands: [], subagents: [], mcps: [] },
301
151
  },
302
152
  };
303
153
 
@@ -311,12 +161,16 @@ describe("manifest", () => {
311
161
  const manifest1: ResourceManifest = {
312
162
  version: 1,
313
163
  syncedAt: "2025-01-01T00:00:00.000Z",
314
- capabilities: { old: { skills: ["old"], rules: [], commands: [], subagents: [] } },
164
+ capabilities: {
165
+ old: { skills: ["old"], rules: [], commands: [], subagents: [], mcps: [] },
166
+ },
315
167
  };
316
168
  const manifest2: ResourceManifest = {
317
169
  version: 1,
318
170
  syncedAt: "2025-01-02T00:00:00.000Z",
319
- capabilities: { new: { skills: ["new"], rules: [], commands: [], subagents: [] } },
171
+ capabilities: {
172
+ new: { skills: ["new"], rules: [], commands: [], subagents: [], mcps: [] },
173
+ },
320
174
  };
321
175
 
322
176
  await saveManifest(manifest1);
@@ -345,12 +199,14 @@ describe("manifest", () => {
345
199
  rules: ["old-rule"],
346
200
  commands: [],
347
201
  subagents: [],
202
+ mcps: [],
348
203
  },
349
204
  "enabled-cap": {
350
205
  skills: ["keep-skill"],
351
206
  rules: ["keep-rule"],
352
207
  commands: [],
353
208
  subagents: [],
209
+ mcps: [],
354
210
  },
355
211
  },
356
212
  };
@@ -382,6 +238,7 @@ describe("manifest", () => {
382
238
  rules: [],
383
239
  commands: [],
384
240
  subagents: [],
241
+ mcps: [],
385
242
  },
386
243
  },
387
244
  };
@@ -407,6 +264,7 @@ describe("manifest", () => {
407
264
  rules: ["nonexistent-rule"],
408
265
  commands: [],
409
266
  subagents: [],
267
+ mcps: [],
410
268
  },
411
269
  },
412
270
  };
@@ -456,6 +314,7 @@ describe("manifest", () => {
456
314
  rules: ["rule-1", "rule-2"],
457
315
  commands: [],
458
316
  subagents: [],
317
+ mcps: [],
459
318
  },
460
319
  },
461
320
  };
@@ -491,18 +350,21 @@ describe("manifest", () => {
491
350
  rules: ["cap1-rule"],
492
351
  commands: [],
493
352
  subagents: [],
353
+ mcps: [],
494
354
  },
495
355
  cap2: {
496
356
  skills: ["cap2-skill"],
497
357
  rules: ["cap2-rule"],
498
358
  commands: [],
499
359
  subagents: [],
360
+ mcps: [],
500
361
  },
501
362
  cap3: {
502
363
  skills: ["cap3-skill"],
503
364
  rules: [],
504
365
  commands: [],
505
366
  subagents: [],
367
+ mcps: [],
506
368
  },
507
369
  },
508
370
  };
@@ -532,6 +394,7 @@ describe("manifest", () => {
532
394
  rules: ["only-rule"],
533
395
  commands: [],
534
396
  subagents: [],
397
+ mcps: [],
535
398
  },
536
399
  },
537
400
  };
@@ -1,20 +1,6 @@
1
1
  import { existsSync, mkdirSync, rmSync } from "node:fs";
2
2
  import type { LoadedCapability } from "../types";
3
3
 
4
- /**
5
- * MCP entry for a capability
6
- */
7
- export interface McpEntry {
8
- /** Server name in .mcp.json (e.g., "omni-{capabilityId}") */
9
- serverName: string;
10
- /** Command to run the MCP server */
11
- command: string;
12
- /** Arguments for the command */
13
- args?: string[];
14
- /** Environment variables */
15
- env?: Record<string, string>;
16
- }
17
-
18
4
  /**
19
5
  * Resources provided by a single capability
20
6
  */
@@ -23,8 +9,7 @@ export interface CapabilityResources {
23
9
  rules: string[];
24
10
  commands: string[];
25
11
  subagents: string[];
26
- /** MCP configuration if capability has [mcp] section */
27
- mcp?: McpEntry;
12
+ mcps: string[];
28
13
  }
29
14
 
30
15
  /**
@@ -48,6 +33,7 @@ export interface CleanupResult {
48
33
  deletedRules: string[];
49
34
  deletedCommands: string[];
50
35
  deletedSubagents: string[];
36
+ deletedMcps: string[];
51
37
  }
52
38
 
53
39
  const MANIFEST_PATH = ".omni/state/manifest.json";
@@ -94,23 +80,9 @@ export function buildManifestFromCapabilities(capabilities: LoadedCapability[]):
94
80
  rules: cap.rules.map((r) => r.name),
95
81
  commands: cap.commands.map((c) => c.name),
96
82
  subagents: cap.subagents.map((s) => s.name),
83
+ mcps: cap.config.mcp ? [cap.id] : [],
97
84
  };
98
85
 
99
- // Track MCP if capability has one
100
- if (cap.config.mcp) {
101
- const mcpEntry: McpEntry = {
102
- serverName: `omni-${cap.id}`,
103
- command: cap.config.mcp.command,
104
- };
105
- if (cap.config.mcp.args) {
106
- mcpEntry.args = cap.config.mcp.args;
107
- }
108
- if (cap.config.mcp.env) {
109
- mcpEntry.env = cap.config.mcp.env;
110
- }
111
- resources.mcp = mcpEntry;
112
- }
113
-
114
86
  manifest.capabilities[cap.id] = resources;
115
87
  }
116
88
 
@@ -131,6 +103,7 @@ export async function cleanupStaleResources(
131
103
  deletedRules: [],
132
104
  deletedCommands: [],
133
105
  deletedSubagents: [],
106
+ deletedMcps: [],
134
107
  };
135
108
 
136
109
  for (const [capId, resources] of Object.entries(previousManifest.capabilities)) {
@@ -0,0 +1,125 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "../test-utils/index.js";
4
+ import {
5
+ disableProvider,
6
+ enableProvider,
7
+ isProviderEnabled,
8
+ readEnabledProviders,
9
+ writeEnabledProviders,
10
+ } from "./providers.js";
11
+
12
+ describe("providers state", () => {
13
+ let testDir: string;
14
+ let originalCwd: string;
15
+
16
+ beforeEach(() => {
17
+ originalCwd = process.cwd();
18
+ testDir = tmpdir("providers-test-");
19
+ process.chdir(testDir);
20
+ });
21
+
22
+ afterEach(() => {
23
+ process.chdir(originalCwd);
24
+ if (existsSync(testDir)) {
25
+ rmSync(testDir, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ describe("readEnabledProviders", () => {
30
+ test("returns default providers when state file does not exist", async () => {
31
+ const providers = await readEnabledProviders();
32
+ expect(providers).toEqual(["claude-code"]);
33
+ });
34
+
35
+ test("reads providers from state file", async () => {
36
+ await writeEnabledProviders(["cursor", "codex"]);
37
+ const providers = await readEnabledProviders();
38
+ expect(providers).toEqual(["cursor", "codex"]);
39
+ });
40
+
41
+ test("returns default when state file is empty", async () => {
42
+ await writeEnabledProviders([]);
43
+ const providers = await readEnabledProviders();
44
+ expect(providers).toEqual(["claude-code"]);
45
+ });
46
+ });
47
+
48
+ describe("writeEnabledProviders", () => {
49
+ test("creates state directory and writes providers", async () => {
50
+ await writeEnabledProviders(["claude-code", "cursor"]);
51
+ expect(existsSync(".omni/state/providers.json")).toBe(true);
52
+
53
+ const providers = await readEnabledProviders();
54
+ expect(providers).toEqual(["claude-code", "cursor"]);
55
+ });
56
+
57
+ test("overwrites existing providers", async () => {
58
+ await writeEnabledProviders(["claude-code"]);
59
+ await writeEnabledProviders(["cursor", "codex"]);
60
+
61
+ const providers = await readEnabledProviders();
62
+ expect(providers).toEqual(["cursor", "codex"]);
63
+ });
64
+ });
65
+
66
+ describe("enableProvider", () => {
67
+ test("adds provider to enabled list", async () => {
68
+ await writeEnabledProviders(["claude-code"]);
69
+ await enableProvider("cursor");
70
+
71
+ const providers = await readEnabledProviders();
72
+ expect(providers).toContain("claude-code");
73
+ expect(providers).toContain("cursor");
74
+ });
75
+
76
+ test("does not duplicate provider if already enabled", async () => {
77
+ await writeEnabledProviders(["claude-code", "cursor"]);
78
+ await enableProvider("cursor");
79
+
80
+ const providers = await readEnabledProviders();
81
+ expect(providers.filter((p) => p === "cursor").length).toBe(1);
82
+ });
83
+ });
84
+
85
+ describe("disableProvider", () => {
86
+ test("removes provider from enabled list", async () => {
87
+ await writeEnabledProviders(["claude-code", "cursor"]);
88
+ await disableProvider("cursor");
89
+
90
+ const providers = await readEnabledProviders();
91
+ expect(providers).toEqual(["claude-code"]);
92
+ });
93
+
94
+ test("handles disabling non-existent provider gracefully", async () => {
95
+ await writeEnabledProviders(["claude-code"]);
96
+ await disableProvider("cursor");
97
+
98
+ const providers = await readEnabledProviders();
99
+ expect(providers).toEqual(["claude-code"]);
100
+ });
101
+ });
102
+
103
+ describe("isProviderEnabled", () => {
104
+ test("returns true when provider is enabled", async () => {
105
+ await writeEnabledProviders(["claude-code", "cursor"]);
106
+
107
+ expect(await isProviderEnabled("cursor")).toBe(true);
108
+ });
109
+
110
+ test("returns false when provider is not enabled", async () => {
111
+ await writeEnabledProviders(["claude-code"]);
112
+
113
+ expect(await isProviderEnabled("cursor")).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("round-trip", () => {
118
+ test("write then read returns same providers", async () => {
119
+ const original = ["claude-code", "cursor", "codex"];
120
+ await writeEnabledProviders(original);
121
+ const loaded = await readEnabledProviders();
122
+ expect(loaded).toEqual(original);
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import type { ProviderId } from "../types/index.js";
3
+
4
+ const STATE_DIR = ".omni/state";
5
+ const PROVIDERS_PATH = `${STATE_DIR}/providers.json`;
6
+
7
+ export interface ProvidersState {
8
+ enabled: ProviderId[];
9
+ }
10
+
11
+ const DEFAULT_PROVIDERS: ProviderId[] = ["claude-code"];
12
+
13
+ /**
14
+ * Read the enabled providers from local state.
15
+ * Returns default providers if no state file exists.
16
+ */
17
+ export async function readEnabledProviders(): Promise<ProviderId[]> {
18
+ if (!existsSync(PROVIDERS_PATH)) {
19
+ return DEFAULT_PROVIDERS;
20
+ }
21
+
22
+ try {
23
+ const content = await Bun.file(PROVIDERS_PATH).text();
24
+ const state = JSON.parse(content) as ProvidersState;
25
+ return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
26
+ } catch {
27
+ return DEFAULT_PROVIDERS;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Write enabled providers to local state.
33
+ * @param providers - List of provider IDs to enable
34
+ */
35
+ export async function writeEnabledProviders(providers: ProviderId[]): Promise<void> {
36
+ mkdirSync(STATE_DIR, { recursive: true });
37
+ const state: ProvidersState = { enabled: providers };
38
+ await Bun.write(PROVIDERS_PATH, JSON.stringify(state, null, 2));
39
+ }
40
+
41
+ /**
42
+ * Enable a specific provider.
43
+ * @param providerId - The provider to enable
44
+ */
45
+ export async function enableProvider(providerId: ProviderId): Promise<void> {
46
+ const current = await readEnabledProviders();
47
+ if (!current.includes(providerId)) {
48
+ await writeEnabledProviders([...current, providerId]);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Disable a specific provider.
54
+ * @param providerId - The provider to disable
55
+ */
56
+ export async function disableProvider(providerId: ProviderId): Promise<void> {
57
+ const current = await readEnabledProviders();
58
+ const filtered = current.filter((p) => p !== providerId);
59
+ await writeEnabledProviders(filtered);
60
+ }
61
+
62
+ /**
63
+ * Check if a provider is enabled.
64
+ * @param providerId - The provider to check
65
+ */
66
+ export async function isProviderEnabled(providerId: ProviderId): Promise<boolean> {
67
+ const current = await readEnabledProviders();
68
+ return current.includes(providerId);
69
+ }