@omnidev-ai/core 0.4.0 → 0.5.1
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/dist/index.d.ts +600 -664
- package/dist/index.js +1841 -1915
- package/dist/shared/chunk-1dqs11h6.js +20 -0
- package/dist/test-utils/index.d.ts +97 -101
- package/dist/test-utils/index.js +203 -234
- package/package.json +5 -3
- package/src/capability/AGENTS.md +58 -0
- package/src/capability/commands.ts +72 -0
- package/src/capability/docs.ts +48 -0
- package/src/capability/index.ts +20 -0
- package/src/capability/loader.ts +431 -0
- package/src/capability/registry.ts +55 -0
- package/src/capability/rules.ts +135 -0
- package/src/capability/skills.ts +58 -0
- package/src/capability/sources.ts +998 -0
- package/src/capability/subagents.ts +105 -0
- package/src/capability/yaml-parser.ts +81 -0
- package/src/config/AGENTS.md +46 -0
- package/src/config/capabilities.ts +54 -0
- package/src/config/env.ts +96 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.ts +207 -0
- package/src/config/parser.ts +55 -0
- package/src/config/profiles.ts +75 -0
- package/src/config/provider.ts +55 -0
- package/src/debug.ts +20 -0
- package/src/index.ts +37 -0
- package/src/mcp-json/index.ts +1 -0
- package/src/mcp-json/manager.ts +106 -0
- package/src/state/active-profile.ts +41 -0
- package/src/state/index.ts +3 -0
- package/src/state/manifest.ts +137 -0
- package/src/state/providers.ts +69 -0
- package/src/sync.ts +288 -0
- package/src/templates/agents.ts +14 -0
- package/src/templates/claude.ts +57 -0
- package/src/test-utils/helpers.ts +289 -0
- package/src/test-utils/index.ts +34 -0
- package/src/test-utils/mocks.ts +101 -0
- package/src/types/capability-export.ts +157 -0
- package/src/types/index.ts +314 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { loadCommands } from "./commands";
|
|
2
|
+
export { loadDocs } from "./docs";
|
|
3
|
+
export { discoverCapabilities, loadCapability, loadCapabilityConfig } from "./loader";
|
|
4
|
+
export type { CapabilityRegistry } from "./registry";
|
|
5
|
+
export { buildCapabilityRegistry } from "./registry";
|
|
6
|
+
export { loadRules, writeRules } from "./rules";
|
|
7
|
+
export { loadSkills } from "./skills";
|
|
8
|
+
export {
|
|
9
|
+
fetchAllCapabilitySources,
|
|
10
|
+
fetchCapabilitySource,
|
|
11
|
+
checkForUpdates,
|
|
12
|
+
loadLockFile,
|
|
13
|
+
saveLockFile,
|
|
14
|
+
parseSourceConfig,
|
|
15
|
+
sourceToGitUrl,
|
|
16
|
+
getSourceCapabilityPath,
|
|
17
|
+
getLockFilePath,
|
|
18
|
+
} from "./sources";
|
|
19
|
+
export type { FetchResult, SourceUpdateInfo, DiscoveredContent } from "./sources";
|
|
20
|
+
export { loadSubagents } from "./subagents";
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { validateEnv } from "../config/env";
|
|
4
|
+
import { parseCapabilityConfig } from "../config/parser";
|
|
5
|
+
import type {
|
|
6
|
+
CapabilityConfig,
|
|
7
|
+
Command,
|
|
8
|
+
Doc,
|
|
9
|
+
LoadedCapability,
|
|
10
|
+
Rule,
|
|
11
|
+
Skill,
|
|
12
|
+
Subagent,
|
|
13
|
+
} from "../types";
|
|
14
|
+
import type {
|
|
15
|
+
CommandExport,
|
|
16
|
+
DocExport,
|
|
17
|
+
SkillExport,
|
|
18
|
+
SubagentExport,
|
|
19
|
+
} from "../types/capability-export";
|
|
20
|
+
import { loadCommands } from "./commands";
|
|
21
|
+
import { loadDocs } from "./docs";
|
|
22
|
+
import { loadRules } from "./rules";
|
|
23
|
+
import { loadSkills } from "./skills";
|
|
24
|
+
import { loadSubagents } from "./subagents";
|
|
25
|
+
|
|
26
|
+
const CAPABILITIES_DIR = ".omni/capabilities";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Discovers capabilities by scanning the .omni/capabilities directory.
|
|
30
|
+
* A directory is considered a capability if it contains a capability.toml file.
|
|
31
|
+
*
|
|
32
|
+
* @returns Array of capability directory paths
|
|
33
|
+
*/
|
|
34
|
+
export async function discoverCapabilities(): Promise<string[]> {
|
|
35
|
+
const capabilities: string[] = [];
|
|
36
|
+
|
|
37
|
+
if (existsSync(CAPABILITIES_DIR)) {
|
|
38
|
+
const entries = readdirSync(CAPABILITIES_DIR, { withFileTypes: true }).sort((a, b) =>
|
|
39
|
+
a.name.localeCompare(b.name),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
const entryPath = join(CAPABILITIES_DIR, entry.name);
|
|
45
|
+
const configPath = join(entryPath, "capability.toml");
|
|
46
|
+
if (existsSync(configPath)) {
|
|
47
|
+
capabilities.push(entryPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return capabilities;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Loads and parses a capability configuration file.
|
|
58
|
+
* Validates required fields.
|
|
59
|
+
*
|
|
60
|
+
* @param capabilityPath - Path to the capability directory
|
|
61
|
+
* @returns Parsed capability configuration
|
|
62
|
+
* @throws Error if the config is invalid
|
|
63
|
+
*/
|
|
64
|
+
export async function loadCapabilityConfig(capabilityPath: string): Promise<CapabilityConfig> {
|
|
65
|
+
const configPath = join(capabilityPath, "capability.toml");
|
|
66
|
+
const content = await Bun.file(configPath).text();
|
|
67
|
+
const config = parseCapabilityConfig(content);
|
|
68
|
+
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Dynamically imports capability exports from index.ts.
|
|
74
|
+
* Returns an empty object if index.ts doesn't exist.
|
|
75
|
+
*
|
|
76
|
+
* @param capabilityPath - Path to the capability directory
|
|
77
|
+
* @returns Exported module or empty object
|
|
78
|
+
* @throws Error if import fails
|
|
79
|
+
*/
|
|
80
|
+
async function importCapabilityExports(capabilityPath: string): Promise<Record<string, unknown>> {
|
|
81
|
+
const indexPath = join(capabilityPath, "index.ts");
|
|
82
|
+
|
|
83
|
+
if (!existsSync(indexPath)) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const absolutePath = join(process.cwd(), indexPath);
|
|
89
|
+
const module = await import(absolutePath);
|
|
90
|
+
return module;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Check if it's a module resolution error
|
|
93
|
+
const errorMessage = String(error);
|
|
94
|
+
if (errorMessage.includes("Cannot find module")) {
|
|
95
|
+
const match = errorMessage.match(/Cannot find module '([^']+)'/);
|
|
96
|
+
const missingModule = match ? match[1] : "unknown";
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Missing dependency '${missingModule}' for capability at ${capabilityPath}.\n` +
|
|
99
|
+
`If this is a project-specific capability, install dependencies or remove it from .omni/capabilities/`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Failed to import capability at ${capabilityPath}: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Loads type definitions from types.d.ts if it exists.
|
|
108
|
+
*
|
|
109
|
+
* @param capabilityPath - Path to the capability directory
|
|
110
|
+
* @returns Type definitions as string or undefined
|
|
111
|
+
*/
|
|
112
|
+
async function loadTypeDefinitions(capabilityPath: string): Promise<string | undefined> {
|
|
113
|
+
const typesPath = join(capabilityPath, "types.d.ts");
|
|
114
|
+
|
|
115
|
+
if (!existsSync(typesPath)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Bun.file(typesPath).text();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert programmatic skill exports to Skill objects
|
|
124
|
+
* Expects SkillExport format with skillMd (markdown with YAML frontmatter)
|
|
125
|
+
*/
|
|
126
|
+
function convertSkillExports(skillExports: unknown[], capabilityId: string): Skill[] {
|
|
127
|
+
return skillExports.map((skillExport) => {
|
|
128
|
+
const exportObj = skillExport as SkillExport;
|
|
129
|
+
const lines = exportObj.skillMd.split("\n");
|
|
130
|
+
let name = "unnamed";
|
|
131
|
+
let description = "";
|
|
132
|
+
let instructions = exportObj.skillMd;
|
|
133
|
+
|
|
134
|
+
// Simple YAML frontmatter parser
|
|
135
|
+
if (lines[0]?.trim() === "---") {
|
|
136
|
+
const endIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
137
|
+
if (endIndex > 0) {
|
|
138
|
+
const frontmatter = lines.slice(1, endIndex);
|
|
139
|
+
instructions = lines
|
|
140
|
+
.slice(endIndex + 1)
|
|
141
|
+
.join("\n")
|
|
142
|
+
.trim();
|
|
143
|
+
|
|
144
|
+
for (const line of frontmatter) {
|
|
145
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
146
|
+
if (match?.[1] && match[2]) {
|
|
147
|
+
const key = match[1];
|
|
148
|
+
const value = match[2];
|
|
149
|
+
if (key === "name") {
|
|
150
|
+
name = value.replace(/^["']|["']$/g, "");
|
|
151
|
+
} else if (key === "description") {
|
|
152
|
+
description = value.replace(/^["']|["']$/g, "");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name,
|
|
161
|
+
description,
|
|
162
|
+
instructions,
|
|
163
|
+
capabilityId,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Convert programmatic rule exports to Rule objects
|
|
170
|
+
* Expects array of string content (markdown)
|
|
171
|
+
*/
|
|
172
|
+
function convertRuleExports(ruleExports: unknown[], capabilityId: string): Rule[] {
|
|
173
|
+
return ruleExports.map((ruleExport, index) => {
|
|
174
|
+
return {
|
|
175
|
+
name: `rule-${index + 1}`,
|
|
176
|
+
content: String(ruleExport).trim(),
|
|
177
|
+
capabilityId,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Convert programmatic doc exports to Doc objects
|
|
184
|
+
* Expects DocExport format with title and content
|
|
185
|
+
*/
|
|
186
|
+
function convertDocExports(docExports: unknown[], capabilityId: string): Doc[] {
|
|
187
|
+
return docExports.map((docExport) => {
|
|
188
|
+
const exportObj = docExport as DocExport;
|
|
189
|
+
return {
|
|
190
|
+
name: exportObj.title,
|
|
191
|
+
content: exportObj.content.trim(),
|
|
192
|
+
capabilityId,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert programmatic subagent exports to Subagent objects
|
|
199
|
+
* Parses SubagentExport markdown with YAML frontmatter
|
|
200
|
+
*/
|
|
201
|
+
function convertSubagentExports(subagentExports: unknown[], capabilityId: string): Subagent[] {
|
|
202
|
+
return subagentExports.map((subagentExport) => {
|
|
203
|
+
const exportObj = subagentExport as SubagentExport;
|
|
204
|
+
const lines = exportObj.subagentMd.split("\n");
|
|
205
|
+
let name = "unnamed";
|
|
206
|
+
let description = "";
|
|
207
|
+
let systemPrompt = exportObj.subagentMd;
|
|
208
|
+
let tools: string[] | undefined;
|
|
209
|
+
let disallowedTools: string[] | undefined;
|
|
210
|
+
let model: string | undefined;
|
|
211
|
+
let permissionMode: string | undefined;
|
|
212
|
+
let skills: string[] | undefined;
|
|
213
|
+
|
|
214
|
+
// Simple YAML frontmatter parser
|
|
215
|
+
if (lines[0]?.trim() === "---") {
|
|
216
|
+
const endIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
217
|
+
if (endIndex > 0) {
|
|
218
|
+
const frontmatter = lines.slice(1, endIndex);
|
|
219
|
+
systemPrompt = lines
|
|
220
|
+
.slice(endIndex + 1)
|
|
221
|
+
.join("\n")
|
|
222
|
+
.trim();
|
|
223
|
+
|
|
224
|
+
for (const line of frontmatter) {
|
|
225
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
226
|
+
if (match?.[1] && match[2]) {
|
|
227
|
+
const key = match[1];
|
|
228
|
+
const value = match[2].replace(/^["']|["']$/g, "");
|
|
229
|
+
switch (key) {
|
|
230
|
+
case "name":
|
|
231
|
+
name = value;
|
|
232
|
+
break;
|
|
233
|
+
case "description":
|
|
234
|
+
description = value;
|
|
235
|
+
break;
|
|
236
|
+
case "tools":
|
|
237
|
+
tools = value.split(",").map((t) => t.trim());
|
|
238
|
+
break;
|
|
239
|
+
case "disallowedTools":
|
|
240
|
+
disallowedTools = value.split(",").map((t) => t.trim());
|
|
241
|
+
break;
|
|
242
|
+
case "model":
|
|
243
|
+
model = value;
|
|
244
|
+
break;
|
|
245
|
+
case "permissionMode":
|
|
246
|
+
permissionMode = value;
|
|
247
|
+
break;
|
|
248
|
+
case "skills":
|
|
249
|
+
skills = value.split(",").map((s) => s.trim());
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const result: Subagent = {
|
|
258
|
+
name,
|
|
259
|
+
description,
|
|
260
|
+
systemPrompt,
|
|
261
|
+
capabilityId,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (tools) result.tools = tools;
|
|
265
|
+
if (disallowedTools) result.disallowedTools = disallowedTools;
|
|
266
|
+
if (model) {
|
|
267
|
+
result.model = model as NonNullable<Subagent["model"]>;
|
|
268
|
+
}
|
|
269
|
+
if (permissionMode) {
|
|
270
|
+
result.permissionMode = permissionMode as NonNullable<Subagent["permissionMode"]>;
|
|
271
|
+
}
|
|
272
|
+
if (skills) result.skills = skills;
|
|
273
|
+
|
|
274
|
+
return result;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Convert programmatic command exports to Command objects
|
|
280
|
+
* Parses CommandExport markdown with YAML frontmatter
|
|
281
|
+
*/
|
|
282
|
+
function convertCommandExports(commandExports: unknown[], capabilityId: string): Command[] {
|
|
283
|
+
return commandExports.map((commandExport) => {
|
|
284
|
+
const exportObj = commandExport as CommandExport;
|
|
285
|
+
const lines = exportObj.commandMd.split("\n");
|
|
286
|
+
let name = "unnamed";
|
|
287
|
+
let description = "";
|
|
288
|
+
let prompt = exportObj.commandMd;
|
|
289
|
+
let allowedTools: string | undefined;
|
|
290
|
+
|
|
291
|
+
// Simple YAML frontmatter parser
|
|
292
|
+
if (lines[0]?.trim() === "---") {
|
|
293
|
+
const endIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
294
|
+
if (endIndex > 0) {
|
|
295
|
+
const frontmatter = lines.slice(1, endIndex);
|
|
296
|
+
prompt = lines
|
|
297
|
+
.slice(endIndex + 1)
|
|
298
|
+
.join("\n")
|
|
299
|
+
.trim();
|
|
300
|
+
|
|
301
|
+
for (const line of frontmatter) {
|
|
302
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
303
|
+
if (match?.[1] && match[2]) {
|
|
304
|
+
const key = match[1];
|
|
305
|
+
const value = match[2].replace(/^["']|["']$/g, "");
|
|
306
|
+
switch (key) {
|
|
307
|
+
case "name":
|
|
308
|
+
name = value;
|
|
309
|
+
break;
|
|
310
|
+
case "description":
|
|
311
|
+
description = value;
|
|
312
|
+
break;
|
|
313
|
+
case "allowedTools":
|
|
314
|
+
case "allowed-tools":
|
|
315
|
+
allowedTools = value;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const result: Command = {
|
|
324
|
+
name,
|
|
325
|
+
description,
|
|
326
|
+
prompt,
|
|
327
|
+
capabilityId,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (allowedTools) {
|
|
331
|
+
result.allowedTools = allowedTools;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Loads a complete capability including config, skills, rules, docs, and exports.
|
|
340
|
+
* Validates environment requirements before loading.
|
|
341
|
+
*
|
|
342
|
+
* @param capabilityPath - Path to the capability directory
|
|
343
|
+
* @param env - Environment variables to validate against
|
|
344
|
+
* @returns Fully loaded capability
|
|
345
|
+
* @throws Error if validation fails or loading errors occur
|
|
346
|
+
*/
|
|
347
|
+
export async function loadCapability(
|
|
348
|
+
capabilityPath: string,
|
|
349
|
+
env: Record<string, string>,
|
|
350
|
+
): Promise<LoadedCapability> {
|
|
351
|
+
const config = await loadCapabilityConfig(capabilityPath);
|
|
352
|
+
const id = config.capability.id;
|
|
353
|
+
|
|
354
|
+
// Validate environment
|
|
355
|
+
if (config.env) {
|
|
356
|
+
validateEnv(config.env, env, id);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Load content - programmatic takes precedence
|
|
360
|
+
const exports = await importCapabilityExports(capabilityPath);
|
|
361
|
+
|
|
362
|
+
// Check if exports contains programmatic skills/rules/docs
|
|
363
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic module exports need runtime type checking
|
|
364
|
+
const exportsAny = exports as any;
|
|
365
|
+
|
|
366
|
+
const skills =
|
|
367
|
+
"skills" in exports && Array.isArray(exportsAny.skills)
|
|
368
|
+
? convertSkillExports(exportsAny.skills, id)
|
|
369
|
+
: await loadSkills(capabilityPath, id);
|
|
370
|
+
|
|
371
|
+
const rules =
|
|
372
|
+
"rules" in exports && Array.isArray(exportsAny.rules)
|
|
373
|
+
? convertRuleExports(exportsAny.rules, id)
|
|
374
|
+
: await loadRules(capabilityPath, id);
|
|
375
|
+
|
|
376
|
+
const docs =
|
|
377
|
+
"docs" in exports && Array.isArray(exportsAny.docs)
|
|
378
|
+
? convertDocExports(exportsAny.docs, id)
|
|
379
|
+
: await loadDocs(capabilityPath, id);
|
|
380
|
+
|
|
381
|
+
const subagents =
|
|
382
|
+
"subagents" in exports && Array.isArray(exportsAny.subagents)
|
|
383
|
+
? convertSubagentExports(exportsAny.subagents, id)
|
|
384
|
+
: await loadSubagents(capabilityPath, id);
|
|
385
|
+
|
|
386
|
+
const commands =
|
|
387
|
+
"commands" in exports && Array.isArray(exportsAny.commands)
|
|
388
|
+
? convertCommandExports(exportsAny.commands, id)
|
|
389
|
+
: await loadCommands(capabilityPath, id);
|
|
390
|
+
|
|
391
|
+
const typeDefinitionsFromExports =
|
|
392
|
+
"typeDefinitions" in exports && typeof exportsAny.typeDefinitions === "string"
|
|
393
|
+
? (exportsAny.typeDefinitions as string)
|
|
394
|
+
: undefined;
|
|
395
|
+
|
|
396
|
+
const typeDefinitions =
|
|
397
|
+
typeDefinitionsFromExports !== undefined
|
|
398
|
+
? typeDefinitionsFromExports
|
|
399
|
+
: await loadTypeDefinitions(capabilityPath);
|
|
400
|
+
|
|
401
|
+
// Extract gitignore patterns from exports
|
|
402
|
+
const gitignore =
|
|
403
|
+
"gitignore" in exports && Array.isArray(exportsAny.gitignore)
|
|
404
|
+
? (exportsAny.gitignore as string[])
|
|
405
|
+
: undefined;
|
|
406
|
+
|
|
407
|
+
// Build result object with explicit handling for optional typeDefinitions
|
|
408
|
+
const result: LoadedCapability = {
|
|
409
|
+
id,
|
|
410
|
+
path: capabilityPath,
|
|
411
|
+
config,
|
|
412
|
+
skills,
|
|
413
|
+
rules,
|
|
414
|
+
docs,
|
|
415
|
+
subagents,
|
|
416
|
+
commands,
|
|
417
|
+
exports,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Only add typeDefinitions if it exists
|
|
421
|
+
if (typeDefinitions !== undefined) {
|
|
422
|
+
result.typeDefinitions = typeDefinitions;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Only add gitignore if it exists
|
|
426
|
+
if (gitignore !== undefined) {
|
|
427
|
+
result.gitignore = gitignore;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getEnabledCapabilities } from "../config/capabilities";
|
|
2
|
+
import { loadEnvironment } from "../config/env";
|
|
3
|
+
import type { Doc, LoadedCapability, Rule, Skill } from "../types";
|
|
4
|
+
import { discoverCapabilities, loadCapability } from "./loader";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registry of loaded capabilities with helper functions.
|
|
8
|
+
*/
|
|
9
|
+
export interface CapabilityRegistry {
|
|
10
|
+
capabilities: Map<string, LoadedCapability>;
|
|
11
|
+
getCapability(id: string): LoadedCapability | undefined;
|
|
12
|
+
getAllCapabilities(): LoadedCapability[];
|
|
13
|
+
getAllSkills(): Skill[];
|
|
14
|
+
getAllRules(): Rule[];
|
|
15
|
+
getAllDocs(): Doc[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds a capability registry by discovering, loading, and filtering capabilities.
|
|
20
|
+
* Only enabled capabilities (based on active profile) are included.
|
|
21
|
+
*
|
|
22
|
+
* @returns Capability registry with helper functions
|
|
23
|
+
*/
|
|
24
|
+
export async function buildCapabilityRegistry(): Promise<CapabilityRegistry> {
|
|
25
|
+
const env = await loadEnvironment();
|
|
26
|
+
const enabledIds = await getEnabledCapabilities();
|
|
27
|
+
|
|
28
|
+
const capabilityPaths = await discoverCapabilities();
|
|
29
|
+
const capabilities = new Map<string, LoadedCapability>();
|
|
30
|
+
|
|
31
|
+
for (const path of capabilityPaths) {
|
|
32
|
+
try {
|
|
33
|
+
const cap = await loadCapability(path, env);
|
|
34
|
+
|
|
35
|
+
// Only add if enabled
|
|
36
|
+
if (enabledIds.includes(cap.id)) {
|
|
37
|
+
capabilities.set(cap.id, cap);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Extract just the error message without stack trace for cleaner output
|
|
41
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
42
|
+
console.warn(`Warning: Skipping capability at ${path}`);
|
|
43
|
+
console.warn(` ${errorMessage}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
capabilities,
|
|
49
|
+
getCapability: (id: string) => capabilities.get(id),
|
|
50
|
+
getAllCapabilities: () => [...capabilities.values()],
|
|
51
|
+
getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
|
|
52
|
+
getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
|
|
53
|
+
getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import type { Doc, Rule } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load rules from a capability's rules/ directory
|
|
7
|
+
* @param capabilityPath Path to the capability directory
|
|
8
|
+
* @param capabilityId ID of the capability
|
|
9
|
+
* @returns Array of Rule objects
|
|
10
|
+
*/
|
|
11
|
+
export async function loadRules(capabilityPath: string, capabilityId: string): Promise<Rule[]> {
|
|
12
|
+
const rulesDir = join(capabilityPath, "rules");
|
|
13
|
+
|
|
14
|
+
if (!existsSync(rulesDir)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rules: Rule[] = [];
|
|
19
|
+
const entries = readdirSync(rulesDir, { withFileTypes: true }).sort((a, b) =>
|
|
20
|
+
a.name.localeCompare(b.name),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
25
|
+
const rulePath = join(rulesDir, entry.name);
|
|
26
|
+
const content = await Bun.file(rulePath).text();
|
|
27
|
+
|
|
28
|
+
rules.push({
|
|
29
|
+
name: basename(entry.name, ".md"),
|
|
30
|
+
content: content.trim(),
|
|
31
|
+
capabilityId,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return rules;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write aggregated rules and docs to .omni/instructions.md
|
|
41
|
+
* Updates the generated section between markers while preserving user content
|
|
42
|
+
* @param rules Array of rules from all enabled capabilities
|
|
43
|
+
* @param docs Array of docs from all enabled capabilities
|
|
44
|
+
*/
|
|
45
|
+
export async function writeRules(rules: Rule[], docs: Doc[] = []): Promise<void> {
|
|
46
|
+
const instructionsPath = ".omni/instructions.md";
|
|
47
|
+
|
|
48
|
+
// Generate content from rules and docs
|
|
49
|
+
const rulesContent = generateRulesContent(rules, docs);
|
|
50
|
+
|
|
51
|
+
// Read existing content or create new file
|
|
52
|
+
let content: string;
|
|
53
|
+
if (existsSync(instructionsPath)) {
|
|
54
|
+
content = await Bun.file(instructionsPath).text();
|
|
55
|
+
} else {
|
|
56
|
+
// Create new file with basic template
|
|
57
|
+
content = `# OmniDev Instructions
|
|
58
|
+
|
|
59
|
+
## Project Description
|
|
60
|
+
<!-- TODO: Add 2-3 sentences describing your project -->
|
|
61
|
+
[Describe what this project does and its main purpose]
|
|
62
|
+
|
|
63
|
+
<!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->
|
|
64
|
+
<!-- END OMNIDEV GENERATED CONTENT -->
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Replace content between markers
|
|
69
|
+
const beginMarker = "<!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->";
|
|
70
|
+
const endMarker = "<!-- END OMNIDEV GENERATED CONTENT -->";
|
|
71
|
+
|
|
72
|
+
const beginIndex = content.indexOf(beginMarker);
|
|
73
|
+
const endIndex = content.indexOf(endMarker);
|
|
74
|
+
|
|
75
|
+
if (beginIndex === -1 || endIndex === -1) {
|
|
76
|
+
// Markers not found, append to end
|
|
77
|
+
content += `\n\n${beginMarker}\n${rulesContent}\n${endMarker}\n`;
|
|
78
|
+
} else {
|
|
79
|
+
// Replace content between markers
|
|
80
|
+
content =
|
|
81
|
+
content.substring(0, beginIndex + beginMarker.length) +
|
|
82
|
+
"\n" +
|
|
83
|
+
rulesContent +
|
|
84
|
+
"\n" +
|
|
85
|
+
content.substring(endIndex);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await Bun.write(instructionsPath, content);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function generateRulesContent(rules: Rule[], docs: Doc[] = []): string {
|
|
92
|
+
if (rules.length === 0 && docs.length === 0) {
|
|
93
|
+
return `<!-- This section is automatically updated when capabilities change -->
|
|
94
|
+
|
|
95
|
+
## Capabilities
|
|
96
|
+
|
|
97
|
+
No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable capabilities.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let content = `<!-- This section is automatically updated when capabilities change -->
|
|
101
|
+
|
|
102
|
+
## Capabilities
|
|
103
|
+
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
// Add documentation section if there are docs
|
|
107
|
+
if (docs.length > 0) {
|
|
108
|
+
content += `### Documentation
|
|
109
|
+
|
|
110
|
+
`;
|
|
111
|
+
for (const doc of docs) {
|
|
112
|
+
content += `#### ${doc.name} (from ${doc.capabilityId})
|
|
113
|
+
|
|
114
|
+
${doc.content}
|
|
115
|
+
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add rules section if there are rules
|
|
121
|
+
if (rules.length > 0) {
|
|
122
|
+
content += `### Rules
|
|
123
|
+
|
|
124
|
+
`;
|
|
125
|
+
for (const rule of rules) {
|
|
126
|
+
content += `#### ${rule.name} (from ${rule.capabilityId})
|
|
127
|
+
|
|
128
|
+
${rule.content}
|
|
129
|
+
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return content.trim();
|
|
135
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Skill } from "../types";
|
|
4
|
+
import { parseFrontmatterWithMarkdown } from "./yaml-parser";
|
|
5
|
+
|
|
6
|
+
interface SkillFrontmatter {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function loadSkills(capabilityPath: string, capabilityId: string): Promise<Skill[]> {
|
|
12
|
+
const skillsDir = join(capabilityPath, "skills");
|
|
13
|
+
|
|
14
|
+
if (!existsSync(skillsDir)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const skills: Skill[] = [];
|
|
19
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true }).sort((a, b) =>
|
|
20
|
+
a.name.localeCompare(b.name),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
const skillPath = join(skillsDir, entry.name, "SKILL.md");
|
|
26
|
+
if (existsSync(skillPath)) {
|
|
27
|
+
const skill = await parseSkillFile(skillPath, capabilityId);
|
|
28
|
+
skills.push(skill);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return skills;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function parseSkillFile(filePath: string, capabilityId: string): Promise<Skill> {
|
|
37
|
+
const content = await Bun.file(filePath).text();
|
|
38
|
+
|
|
39
|
+
const parsed = parseFrontmatterWithMarkdown<SkillFrontmatter>(content);
|
|
40
|
+
|
|
41
|
+
if (!parsed) {
|
|
42
|
+
throw new Error(`Invalid SKILL.md format at ${filePath}: missing YAML frontmatter`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const frontmatter = parsed.frontmatter;
|
|
46
|
+
const instructions = parsed.markdown;
|
|
47
|
+
|
|
48
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
49
|
+
throw new Error(`Invalid SKILL.md at ${filePath}: name and description required`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: frontmatter.name,
|
|
54
|
+
description: frontmatter.description,
|
|
55
|
+
instructions: instructions.trim(),
|
|
56
|
+
capabilityId,
|
|
57
|
+
};
|
|
58
|
+
}
|