@omnidev-ai/cli 0.9.0 → 0.10.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 (2) hide show
  1. package/dist/index.js +413 -179
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -24,149 +24,213 @@ import { run } from "@stricli/core";
24
24
  // src/lib/dynamic-app.ts
25
25
  import { existsSync as existsSync8 } from "node:fs";
26
26
  import { createRequire as createRequire2 } from "node:module";
27
- import { join as join5 } from "node:path";
27
+ import { join as join9 } from "node:path";
28
28
  import { buildApplication, buildRouteMap as buildRouteMap5 } from "@stricli/core";
29
29
 
30
30
  // src/commands/add.ts
31
- import { existsSync as existsSync4 } from "node:fs";
31
+ import { existsSync as existsSync3 } from "node:fs";
32
+ import { basename, resolve } from "node:path";
32
33
 
33
- // ../adapters/src/claude-code/index.ts
34
- import { existsSync, mkdirSync } from "node:fs";
35
- import { readFile, writeFile } from "node:fs/promises";
34
+ // ../adapters/src/writers/cursor-rules.ts
35
+ import { mkdir, writeFile } from "node:fs/promises";
36
36
  import { join } from "node:path";
37
- import {
38
- transformHooksConfig
39
- } from "@omnidev-ai/core";
40
- var claudeCodeAdapter = {
41
- id: "claude-code",
42
- displayName: "Claude Code",
43
- async init(_ctx) {
37
+ var CursorRulesWriter = {
38
+ id: "cursor-rules",
39
+ async write(bundle, ctx) {
40
+ const rulesDir = join(ctx.projectRoot, ctx.outputPath);
41
+ await mkdir(rulesDir, { recursive: true });
42
+ const filesWritten = [];
43
+ for (const rule of bundle.rules) {
44
+ const rulePath = join(rulesDir, `omnidev-${rule.name}.mdc`);
45
+ await writeFile(rulePath, rule.content, "utf-8");
46
+ filesWritten.push(join(ctx.outputPath, `omnidev-${rule.name}.mdc`));
47
+ }
44
48
  return {
45
- filesCreated: [],
46
- message: "Claude Code adapter initialized"
49
+ filesWritten
47
50
  };
48
- },
49
- async sync(bundle, ctx) {
51
+ }
52
+ };
53
+ // ../adapters/src/writers/hooks.ts
54
+ import { existsSync } from "node:fs";
55
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "node:fs/promises";
56
+ import { dirname, join as join2 } from "node:path";
57
+ import { transformHooksConfig } from "@omnidev-ai/core";
58
+ var HooksWriter = {
59
+ id: "hooks",
60
+ async write(bundle, ctx) {
61
+ if (!bundle.hooks) {
62
+ return { filesWritten: [] };
63
+ }
64
+ const settingsPath = join2(ctx.projectRoot, ctx.outputPath);
65
+ const parentDir = dirname(settingsPath);
66
+ await mkdir2(parentDir, { recursive: true });
67
+ const claudeHooks = transformHooksConfig(bundle.hooks, "toClaude");
68
+ let existingSettings = {};
69
+ if (existsSync(settingsPath)) {
70
+ try {
71
+ const content = await readFile(settingsPath, "utf-8");
72
+ existingSettings = JSON.parse(content);
73
+ } catch {
74
+ existingSettings = {};
75
+ }
76
+ }
77
+ const newSettings = {
78
+ ...existingSettings,
79
+ hooks: claudeHooks
80
+ };
81
+ await writeFile2(settingsPath, `${JSON.stringify(newSettings, null, 2)}
82
+ `, "utf-8");
83
+ return {
84
+ filesWritten: [ctx.outputPath]
85
+ };
86
+ }
87
+ };
88
+ // ../adapters/src/writers/instructions-md.ts
89
+ import { existsSync as existsSync2 } from "node:fs";
90
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
91
+ import { dirname as dirname2, join as join3 } from "node:path";
92
+ var InstructionsMdWriter = {
93
+ id: "instructions-md",
94
+ async write(bundle, ctx) {
95
+ const outputFullPath = join3(ctx.projectRoot, ctx.outputPath);
96
+ const parentDir = dirname2(outputFullPath);
97
+ if (parentDir !== ctx.projectRoot) {
98
+ await mkdir3(parentDir, { recursive: true });
99
+ }
100
+ const omniMdPath = join3(ctx.projectRoot, "OMNI.md");
101
+ let omniMdContent = "";
102
+ if (existsSync2(omniMdPath)) {
103
+ omniMdContent = await readFile2(omniMdPath, "utf-8");
104
+ }
105
+ let content = omniMdContent;
106
+ content += `
107
+
108
+ ## OmniDev
109
+
110
+ ${bundle.instructionsContent}
111
+ `;
112
+ await writeFile3(outputFullPath, content, "utf-8");
113
+ return {
114
+ filesWritten: [ctx.outputPath]
115
+ };
116
+ }
117
+ };
118
+ // ../adapters/src/writers/skills.ts
119
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
120
+ import { join as join4 } from "node:path";
121
+ var SkillsWriter = {
122
+ id: "skills",
123
+ async write(bundle, ctx) {
124
+ const skillsDir = join4(ctx.projectRoot, ctx.outputPath);
125
+ await mkdir4(skillsDir, { recursive: true });
50
126
  const filesWritten = [];
51
- const filesDeleted = [];
52
- const claudeMdPath = join(ctx.projectRoot, "CLAUDE.md");
53
- const claudeMdContent = await generateClaudeMdContent(ctx.projectRoot);
54
- await writeFile(claudeMdPath, claudeMdContent, "utf-8");
55
- filesWritten.push("CLAUDE.md");
56
- const claudeDir = join(ctx.projectRoot, ".claude");
57
- mkdirSync(claudeDir, { recursive: true });
58
- const skillsDir = join(claudeDir, "skills");
59
- mkdirSync(skillsDir, { recursive: true });
60
127
  for (const skill of bundle.skills) {
61
- const skillDir = join(skillsDir, skill.name);
62
- mkdirSync(skillDir, { recursive: true });
63
- const skillPath = join(skillDir, "SKILL.md");
128
+ const skillDir = join4(skillsDir, skill.name);
129
+ await mkdir4(skillDir, { recursive: true });
130
+ const skillPath = join4(skillDir, "SKILL.md");
64
131
  const content = `---
65
132
  name: ${skill.name}
66
133
  description: "${skill.description}"
67
134
  ---
68
135
 
69
136
  ${skill.instructions}`;
70
- await writeFile(skillPath, content, "utf-8");
71
- filesWritten.push(`.claude/skills/${skill.name}/SKILL.md`);
72
- }
73
- if (bundle.hooks) {
74
- const settingsPath = join(claudeDir, "settings.json");
75
- const hooksWritten = await writeHooksToSettings(settingsPath, bundle.hooks);
76
- if (hooksWritten) {
77
- filesWritten.push(".claude/settings.json");
78
- }
137
+ await writeFile4(skillPath, content, "utf-8");
138
+ filesWritten.push(join4(ctx.outputPath, skill.name, "SKILL.md"));
79
139
  }
80
140
  return {
81
- filesWritten,
82
- filesDeleted
141
+ filesWritten
83
142
  };
84
143
  }
85
144
  };
86
- async function generateClaudeMdContent(projectRoot) {
87
- const omniMdPath = join(projectRoot, "OMNI.md");
88
- let omniMdContent = "";
89
- if (existsSync(omniMdPath)) {
90
- omniMdContent = await readFile(omniMdPath, "utf-8");
91
- }
92
- let content = omniMdContent;
93
- content += `
94
-
95
- ## OmniDev
96
-
97
- @import .omni/instructions.md
98
- `;
99
- return content;
100
- }
101
- async function writeHooksToSettings(settingsPath, hooks) {
102
- const claudeHooks = transformHooksConfig(hooks, "toClaude");
103
- let existingSettings = {};
104
- if (existsSync(settingsPath)) {
105
- try {
106
- const content = await readFile(settingsPath, "utf-8");
107
- existingSettings = JSON.parse(content);
108
- } catch {
109
- existingSettings = {};
145
+ // ../adapters/src/writers/executor.ts
146
+ async function executeWriters(writerConfigs, bundle, projectRoot) {
147
+ const seen = new Set;
148
+ const uniqueConfigs = [];
149
+ let deduplicatedCount = 0;
150
+ for (const config of writerConfigs) {
151
+ const key = `${config.writer.id}:${config.outputPath}`;
152
+ if (seen.has(key)) {
153
+ deduplicatedCount++;
154
+ continue;
110
155
  }
156
+ seen.add(key);
157
+ uniqueConfigs.push(config);
158
+ }
159
+ const allFilesWritten = [];
160
+ for (const config of uniqueConfigs) {
161
+ const result = await config.writer.write(bundle, {
162
+ outputPath: config.outputPath,
163
+ projectRoot
164
+ });
165
+ allFilesWritten.push(...result.filesWritten);
111
166
  }
112
- const newSettings = {
113
- ...existingSettings,
114
- hooks: claudeHooks
167
+ return {
168
+ filesWritten: allFilesWritten,
169
+ deduplicatedCount
115
170
  };
116
- await writeFile(settingsPath, `${JSON.stringify(newSettings, null, 2)}
117
- `, "utf-8");
118
- return true;
119
171
  }
172
+ // ../adapters/src/claude-code/index.ts
173
+ var claudeCodeAdapter = {
174
+ id: "claude-code",
175
+ displayName: "Claude Code",
176
+ writers: [
177
+ { writer: InstructionsMdWriter, outputPath: "CLAUDE.md" },
178
+ { writer: SkillsWriter, outputPath: ".claude/skills/" },
179
+ { writer: HooksWriter, outputPath: ".claude/settings.json" }
180
+ ],
181
+ async init(_ctx) {
182
+ return {
183
+ filesCreated: [],
184
+ message: "Claude Code adapter initialized"
185
+ };
186
+ },
187
+ async sync(bundle, ctx) {
188
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
189
+ return {
190
+ filesWritten: result.filesWritten,
191
+ filesDeleted: []
192
+ };
193
+ }
194
+ };
120
195
  // ../adapters/src/codex/index.ts
121
- import { existsSync as existsSync2 } from "node:fs";
122
- import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
123
- import { join as join2 } from "node:path";
196
+ import { mkdirSync } from "node:fs";
197
+ import { join as join5 } from "node:path";
124
198
  var codexAdapter = {
125
199
  id: "codex",
126
200
  displayName: "Codex",
127
- async init(_ctx) {
201
+ writers: [
202
+ { writer: InstructionsMdWriter, outputPath: "AGENTS.md" },
203
+ { writer: SkillsWriter, outputPath: ".codex/skills/" }
204
+ ],
205
+ async init(ctx) {
206
+ const codexDir = join5(ctx.projectRoot, ".codex");
207
+ mkdirSync(codexDir, { recursive: true });
128
208
  return {
129
- filesCreated: [],
209
+ filesCreated: [".codex/"],
130
210
  message: "Codex adapter initialized"
131
211
  };
132
212
  },
133
- async sync(_bundle, ctx) {
134
- const filesWritten = [];
135
- const filesDeleted = [];
136
- const agentsMdPath = join2(ctx.projectRoot, "AGENTS.md");
137
- const agentsMdContent = await generateAgentsMdContent(ctx.projectRoot);
138
- await writeFile2(agentsMdPath, agentsMdContent, "utf-8");
139
- filesWritten.push("AGENTS.md");
213
+ async sync(bundle, ctx) {
214
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
140
215
  return {
141
- filesWritten,
142
- filesDeleted
216
+ filesWritten: result.filesWritten,
217
+ filesDeleted: []
143
218
  };
144
219
  }
145
220
  };
146
- async function generateAgentsMdContent(projectRoot) {
147
- const omniMdPath = join2(projectRoot, "OMNI.md");
148
- let omniMdContent = "";
149
- if (existsSync2(omniMdPath)) {
150
- omniMdContent = await readFile2(omniMdPath, "utf-8");
151
- }
152
- let content = omniMdContent;
153
- content += `
154
-
155
- ## OmniDev
156
-
157
- @import .omni/instructions.md
158
- `;
159
- return content;
160
- }
161
221
  // ../adapters/src/cursor/index.ts
162
222
  import { mkdirSync as mkdirSync2 } from "node:fs";
163
- import { writeFile as writeFile3 } from "node:fs/promises";
164
- import { join as join3 } from "node:path";
223
+ import { join as join6 } from "node:path";
165
224
  var cursorAdapter = {
166
225
  id: "cursor",
167
226
  displayName: "Cursor",
227
+ writers: [
228
+ { writer: InstructionsMdWriter, outputPath: "CLAUDE.md" },
229
+ { writer: SkillsWriter, outputPath: ".claude/skills/" },
230
+ { writer: CursorRulesWriter, outputPath: ".cursor/rules/" }
231
+ ],
168
232
  async init(ctx) {
169
- const rulesDir = join3(ctx.projectRoot, ".cursor", "rules");
233
+ const rulesDir = join6(ctx.projectRoot, ".cursor", "rules");
170
234
  mkdirSync2(rulesDir, { recursive: true });
171
235
  return {
172
236
  filesCreated: [".cursor/rules/"],
@@ -174,66 +238,39 @@ var cursorAdapter = {
174
238
  };
175
239
  },
176
240
  async sync(bundle, ctx) {
177
- const filesWritten = [];
178
- const filesDeleted = [];
179
- const rulesDir = join3(ctx.projectRoot, ".cursor", "rules");
180
- mkdirSync2(rulesDir, { recursive: true });
181
- for (const rule of bundle.rules) {
182
- const rulePath = join3(rulesDir, `omnidev-${rule.name}.mdc`);
183
- await writeFile3(rulePath, rule.content, "utf-8");
184
- filesWritten.push(`.cursor/rules/omnidev-${rule.name}.mdc`);
185
- }
241
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
186
242
  return {
187
- filesWritten,
188
- filesDeleted
243
+ filesWritten: result.filesWritten,
244
+ filesDeleted: []
189
245
  };
190
246
  }
191
247
  };
192
248
  // ../adapters/src/opencode/index.ts
193
- import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
194
- import { readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
195
- import { join as join4 } from "node:path";
249
+ import { mkdirSync as mkdirSync3 } from "node:fs";
250
+ import { join as join7 } from "node:path";
196
251
  var opencodeAdapter = {
197
252
  id: "opencode",
198
253
  displayName: "OpenCode",
254
+ writers: [
255
+ { writer: InstructionsMdWriter, outputPath: "AGENTS.md" },
256
+ { writer: SkillsWriter, outputPath: ".opencode/skills/" }
257
+ ],
199
258
  async init(ctx) {
200
- const opencodeDir = join4(ctx.projectRoot, ".opencode");
259
+ const opencodeDir = join7(ctx.projectRoot, ".opencode");
201
260
  mkdirSync3(opencodeDir, { recursive: true });
202
261
  return {
203
262
  filesCreated: [".opencode/"],
204
263
  message: "OpenCode adapter initialized"
205
264
  };
206
265
  },
207
- async sync(_bundle, ctx) {
208
- const filesWritten = [];
209
- const filesDeleted = [];
210
- const opencodeDir = join4(ctx.projectRoot, ".opencode");
211
- mkdirSync3(opencodeDir, { recursive: true });
212
- const instructionsPath = join4(opencodeDir, "instructions.md");
213
- const instructionsContent = await generateOpencodeInstructionsContent(ctx.projectRoot);
214
- await writeFile4(instructionsPath, instructionsContent, "utf-8");
215
- filesWritten.push(".opencode/instructions.md");
266
+ async sync(bundle, ctx) {
267
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
216
268
  return {
217
- filesWritten,
218
- filesDeleted
269
+ filesWritten: result.filesWritten,
270
+ filesDeleted: []
219
271
  };
220
272
  }
221
273
  };
222
- async function generateOpencodeInstructionsContent(projectRoot) {
223
- const omniMdPath = join4(projectRoot, "OMNI.md");
224
- let omniMdContent = "";
225
- if (existsSync3(omniMdPath)) {
226
- omniMdContent = await readFile3(omniMdPath, "utf-8");
227
- }
228
- let content = omniMdContent;
229
- content += `
230
-
231
- ## OmniDev
232
-
233
- @import ../.omni/instructions.md
234
- `;
235
- return content;
236
- }
237
274
  // ../adapters/src/registry.ts
238
275
  import { readEnabledProviders } from "@omnidev-ai/core";
239
276
  var builtInAdapters = [
@@ -257,37 +294,90 @@ import {
257
294
  patchAddCapabilitySource,
258
295
  patchAddMcp,
259
296
  patchAddToProfile,
260
- syncAgentConfiguration
297
+ syncAgentConfiguration,
298
+ readCapabilityIdFromPath
261
299
  } from "@omnidev-ai/core";
262
300
  import { buildCommand, buildRouteMap } from "@stricli/core";
301
+ async function inferCapabilityId(source, sourceType) {
302
+ if (sourceType === "local") {
303
+ const localPath = source.startsWith("file://") ? source.slice(7) : source;
304
+ const resolvedPath = resolve(localPath);
305
+ const id = await readCapabilityIdFromPath(resolvedPath);
306
+ if (id) {
307
+ return id;
308
+ }
309
+ return basename(resolvedPath);
310
+ }
311
+ const parts = source.replace("github:", "").split("/");
312
+ if (parts.length >= 2) {
313
+ return parts[parts.length - 1] ?? parts[1] ?? "capability";
314
+ }
315
+ return "capability";
316
+ }
263
317
  async function runAddCap(flags, name) {
264
318
  try {
265
- if (!existsSync4("omni.toml")) {
319
+ if (!existsSync3("omni.toml")) {
266
320
  console.log("✗ No config file found");
267
321
  console.log(" Run: omnidev init");
268
322
  process.exit(1);
269
323
  }
270
- if (!flags.github.includes("/")) {
271
- console.error("✗ Invalid GitHub repository format");
272
- console.log(" Expected format: user/repo");
273
- console.log(" Example: omnidev add cap my-cap --github expo/skills");
324
+ if (!flags.github && !flags.local) {
325
+ console.error("✗ No source specified");
326
+ console.log(" Use --github or --local to specify the capability source");
327
+ console.log(" Example: omnidev add cap --github expo/skills");
328
+ console.log(" Example: omnidev add cap --local ./capabilities/my-cap");
329
+ process.exit(1);
330
+ }
331
+ if (flags.github && flags.local) {
332
+ console.error("✗ Cannot specify both --github and --local");
333
+ console.log(" Use only one source flag");
274
334
  process.exit(1);
275
335
  }
336
+ let source;
337
+ let sourceType;
338
+ if (flags.local) {
339
+ sourceType = "local";
340
+ const localPath = flags.local.startsWith("file://") ? flags.local.slice(7) : flags.local;
341
+ source = `file://${localPath}`;
342
+ if (!existsSync3(localPath)) {
343
+ console.error(`✗ Local path not found: ${localPath}`);
344
+ process.exit(1);
345
+ }
346
+ } else if (flags.github) {
347
+ sourceType = "github";
348
+ if (!flags.github.includes("/")) {
349
+ console.error("✗ Invalid GitHub repository format");
350
+ console.log(" Expected format: user/repo");
351
+ console.log(" Example: omnidev add cap --github expo/skills");
352
+ process.exit(1);
353
+ }
354
+ source = `github:${flags.github}`;
355
+ } else {
356
+ throw new Error("Unreachable: no source specified");
357
+ }
358
+ let capabilityId = name;
359
+ if (!capabilityId) {
360
+ const sourceValue = sourceType === "local" ? flags.local : flags.github;
361
+ if (!sourceValue) {
362
+ throw new Error("Unreachable: cannot infer capability ID");
363
+ }
364
+ capabilityId = await inferCapabilityId(sourceValue, sourceType);
365
+ console.log(` Inferred capability ID: ${capabilityId}`);
366
+ }
276
367
  const config = await loadBaseConfig();
277
368
  const activeProfile = await getActiveProfile() ?? config.active_profile ?? "default";
278
- if (config.capabilities?.sources?.[name]) {
279
- console.error(`✗ Capability source "${name}" already exists`);
369
+ if (config.capabilities?.sources?.[capabilityId]) {
370
+ console.error(`✗ Capability source "${capabilityId}" already exists`);
280
371
  console.log(" Use a different name or remove the existing source first");
281
372
  process.exit(1);
282
373
  }
283
- const source = `github:${flags.github}`;
284
- if (flags.path) {
285
- await patchAddCapabilitySource(name, { source, path: flags.path });
374
+ if (flags.path && sourceType === "github") {
375
+ await patchAddCapabilitySource(capabilityId, { source, path: flags.path });
286
376
  } else {
287
- await patchAddCapabilitySource(name, source);
377
+ await patchAddCapabilitySource(capabilityId, source);
288
378
  }
289
- await patchAddToProfile(activeProfile, name);
290
- console.log(`✓ Added capability source: ${name}`);
379
+ await patchAddToProfile(activeProfile, capabilityId);
380
+ console.log(`✓ Added capability source: ${capabilityId}`);
291
381
  console.log(` Source: ${source}`);
292
382
  if (flags.path) {
293
383
  console.log(` Path: ${flags.path}`);
@@ -304,7 +394,7 @@ async function runAddCap(flags, name) {
304
394
  }
305
395
  async function runAddMcp(flags, name) {
306
396
  try {
307
- if (!existsSync4("omni.toml")) {
397
+ if (!existsSync3("omni.toml")) {
308
398
  console.log("✗ No config file found");
309
399
  console.log(" Run: omnidev init");
310
400
  process.exit(1);
@@ -416,23 +506,46 @@ function parseArgs(argsString) {
416
506
  return args;
417
507
  }
418
508
  async function runAddCapWrapper(flags, name) {
419
- await runAddCap({ github: flags.github, path: flags.path }, name);
509
+ await runAddCap({ github: flags.github, local: flags.local, path: flags.path }, name);
420
510
  }
421
511
  var addCapCommand = buildCommand({
422
512
  docs: {
423
- brief: "Add a capability source from GitHub",
424
- fullDescription: "Add a capability source from a GitHub repository. The capability will be auto-enabled in the active profile."
513
+ brief: "Add a capability source from GitHub or local path",
514
+ fullDescription: `Add a capability source from a GitHub repository or local path. The capability will be auto-enabled in the active profile.
515
+
516
+ GitHub source:
517
+ omnidev add cap [name] --github user/repo [--path subdir]
518
+
519
+ Local source:
520
+ omnidev add cap [name] --local ./path/to/capability
521
+
522
+ If the capability name is omitted, it will be inferred from:
523
+ - For local sources: the ID in capability.toml or directory name
524
+ - For GitHub sources: the repository name or last path segment
525
+
526
+ Examples:
527
+ omnidev add cap my-cap --github expo/skills
528
+ omnidev add cap --github expo/skills # Infers name as "skills"
529
+ omnidev add cap --local ./capabilities/my-cap # Infers name from capability.toml
530
+ omnidev add cap custom-name --local ./capabilities/my-cap`
425
531
  },
426
532
  parameters: {
427
533
  flags: {
428
534
  github: {
429
535
  kind: "parsed",
430
536
  brief: "GitHub repository in user/repo format",
431
- parse: String
537
+ parse: String,
538
+ optional: true
539
+ },
540
+ local: {
541
+ kind: "parsed",
542
+ brief: "Local path to capability directory",
543
+ parse: String,
544
+ optional: true
432
545
  },
433
546
  path: {
434
547
  kind: "parsed",
435
- brief: "Subdirectory within the repo containing the capability",
548
+ brief: "Subdirectory within the repo containing the capability (GitHub only)",
436
549
  parse: String,
437
550
  optional: true
438
551
  }
@@ -441,10 +554,16 @@ var addCapCommand = buildCommand({
441
554
  kind: "tuple",
442
555
  parameters: [
443
556
  {
444
- brief: "Capability name",
445
- parse: String
557
+ brief: "Capability name (optional, will be inferred if omitted)",
558
+ parse: String,
559
+ optional: true
446
560
  }
447
561
  ]
562
+ },
563
+ aliases: {
564
+ g: "github",
565
+ l: "local",
566
+ p: "path"
448
567
  }
449
568
  },
450
569
  func: runAddCapWrapper
@@ -550,15 +669,31 @@ var addRoutes = buildRouteMap({
550
669
  });
551
670
 
552
671
  // src/commands/capability.ts
672
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
673
+ import { writeFile as writeFile5 } from "node:fs/promises";
674
+ import { join as join8 } from "node:path";
675
+ import { input } from "@inquirer/prompts";
553
676
  import {
554
677
  disableCapability,
555
678
  discoverCapabilities,
556
679
  enableCapability,
680
+ generateCapabilityToml,
681
+ generateHooksTemplate,
682
+ generateHookScript,
683
+ generateRuleTemplate,
684
+ generateSkillTemplate,
557
685
  getEnabledCapabilities,
558
686
  loadCapabilityConfig,
559
687
  syncAgentConfiguration as syncAgentConfiguration2
560
688
  } from "@omnidev-ai/core";
561
689
  import { buildCommand as buildCommand2, buildRouteMap as buildRouteMap2 } from "@stricli/core";
690
+
691
+ // src/prompts/capability.ts
692
+ function isValidCapabilityId(id) {
693
+ return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(id);
694
+ }
695
+
696
+ // src/commands/capability.ts
562
697
  async function runCapabilityList() {
563
698
  try {
564
699
  const enabledIds = await getEnabledCapabilities();
@@ -627,6 +762,105 @@ async function runCapabilityDisable(_flags, name) {
627
762
  process.exit(1);
628
763
  }
629
764
  }
765
+ function toTitleCase(kebabCase) {
766
+ return kebabCase.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
767
+ }
768
+ async function runCapabilityNew(flags, capabilityId) {
769
+ try {
770
+ if (!existsSync4(".omni")) {
771
+ console.error("✗ OmniDev is not initialized in this directory.");
772
+ console.log("");
773
+ console.log(" Run: omnidev init");
774
+ process.exit(1);
775
+ }
776
+ if (!isValidCapabilityId(capabilityId)) {
777
+ console.error(`✗ Invalid capability ID: '${capabilityId}'`);
778
+ console.log("");
779
+ console.log(" ID must be lowercase, start with a letter, and use kebab-case");
780
+ console.log(" Example: my-capability, tasks, api-client");
781
+ process.exit(1);
782
+ }
783
+ const id = capabilityId;
784
+ let capabilityDir;
785
+ if (flags.path) {
786
+ capabilityDir = flags.path;
787
+ } else {
788
+ const defaultPath = `capabilities/${id}`;
789
+ capabilityDir = await input({
790
+ message: "Output path:",
791
+ default: defaultPath
792
+ });
793
+ }
794
+ if (existsSync4(capabilityDir)) {
795
+ console.error(`✗ Directory already exists at ${capabilityDir}`);
796
+ process.exit(1);
797
+ }
798
+ const name = toTitleCase(id);
799
+ mkdirSync4(capabilityDir, { recursive: true });
800
+ const capabilityToml = generateCapabilityToml({ id, name });
801
+ await writeFile5(join8(capabilityDir, "capability.toml"), capabilityToml, "utf-8");
802
+ const skillDir = join8(capabilityDir, "skills", "getting-started");
803
+ mkdirSync4(skillDir, { recursive: true });
804
+ await writeFile5(join8(skillDir, "SKILL.md"), generateSkillTemplate("getting-started"), "utf-8");
805
+ const rulesDir = join8(capabilityDir, "rules");
806
+ mkdirSync4(rulesDir, { recursive: true });
807
+ await writeFile5(join8(rulesDir, "coding-standards.md"), generateRuleTemplate("coding-standards"), "utf-8");
808
+ const hooksDir = join8(capabilityDir, "hooks");
809
+ mkdirSync4(hooksDir, { recursive: true });
810
+ await writeFile5(join8(hooksDir, "hooks.toml"), generateHooksTemplate(), "utf-8");
811
+ await writeFile5(join8(hooksDir, "example-hook.sh"), generateHookScript(), "utf-8");
812
+ console.log(`✓ Created capability: ${name}`);
813
+ console.log(` Location: ${capabilityDir}`);
814
+ console.log("");
815
+ console.log(" Files created:");
816
+ console.log(" - capability.toml");
817
+ console.log(" - skills/getting-started/SKILL.md");
818
+ console.log(" - rules/coding-standards.md");
819
+ console.log(" - hooks/hooks.toml");
820
+ console.log(" - hooks/example-hook.sh");
821
+ console.log("");
822
+ console.log("\uD83D\uDCA1 To add this capability as a local source, run:");
823
+ console.log(` omnidev add cap --local ./${capabilityDir}`);
824
+ } catch (error) {
825
+ console.error("Error creating capability:", error);
826
+ process.exit(1);
827
+ }
828
+ }
829
+ var newCommand = buildCommand2({
830
+ docs: {
831
+ brief: "Create a new capability with templates",
832
+ fullDescription: `Create a new capability with templates at a specified path.
833
+
834
+ By default, creates the capability at capabilities/<id>. You can specify a custom path using the --path flag or interactively.
835
+
836
+ Examples:
837
+ omnidev capability new my-cap # Prompts for path, defaults to capabilities/my-cap
838
+ omnidev capability new my-cap --path ./caps/my # Uses ./caps/my directly`
839
+ },
840
+ parameters: {
841
+ flags: {
842
+ path: {
843
+ kind: "parsed",
844
+ brief: "Output path for the capability (skips interactive prompt)",
845
+ parse: String,
846
+ optional: true
847
+ }
848
+ },
849
+ positional: {
850
+ kind: "tuple",
851
+ parameters: [
852
+ {
853
+ brief: "Capability ID (kebab-case)",
854
+ parse: String
855
+ }
856
+ ]
857
+ },
858
+ aliases: {
859
+ p: "path"
860
+ }
861
+ },
862
+ func: runCapabilityNew
863
+ });
630
864
  var listCommand = buildCommand2({
631
865
  docs: {
632
866
  brief: "List all discovered capabilities"
@@ -674,6 +908,7 @@ var disableCommand = buildCommand2({
674
908
  });
675
909
  var capabilityRoutes = buildRouteMap2({
676
910
  routes: {
911
+ new: newCommand,
677
912
  list: listCommand,
678
913
  enable: enableCommand,
679
914
  disable: disableCommand
@@ -686,7 +921,7 @@ var capabilityRoutes = buildRouteMap2({
686
921
  // src/commands/doctor.ts
687
922
  import { existsSync as existsSync5 } from "node:fs";
688
923
  import { execFile } from "node:child_process";
689
- import { readFile as readFile4 } from "node:fs/promises";
924
+ import { readFile as readFile3 } from "node:fs/promises";
690
925
  import { promisify } from "node:util";
691
926
  import { buildCommand as buildCommand3 } from "@stricli/core";
692
927
  var doctorCommand = buildCommand3({
@@ -826,7 +1061,7 @@ async function checkRootGitignore() {
826
1061
  fix: "Run: omnidev init"
827
1062
  };
828
1063
  }
829
- const content = await readFile4(gitignorePath, "utf-8");
1064
+ const content = await readFile3(gitignorePath, "utf-8");
830
1065
  const lines = content.split(`
831
1066
  `).map((line) => line.trim());
832
1067
  const hasOmniDir = lines.includes(".omni/");
@@ -868,8 +1103,8 @@ async function checkCapabilitiesDir() {
868
1103
 
869
1104
  // src/commands/init.ts
870
1105
  import { exec } from "node:child_process";
871
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
872
- import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1106
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
1107
+ import { readFile as readFile4, writeFile as writeFile6 } from "node:fs/promises";
873
1108
  import { promisify as promisify2 } from "node:util";
874
1109
  import {
875
1110
  generateOmniMdTemplate,
@@ -918,9 +1153,9 @@ async function promptForGitignoreProviderFiles(selectedProviders) {
918
1153
  var execAsync = promisify2(exec);
919
1154
  async function runInit(_flags, providerArg) {
920
1155
  console.log("Initializing OmniDev...");
921
- mkdirSync4(".omni", { recursive: true });
922
- mkdirSync4(".omni/capabilities", { recursive: true });
923
- mkdirSync4(".omni/state", { recursive: true });
1156
+ mkdirSync5(".omni", { recursive: true });
1157
+ mkdirSync5(".omni/capabilities", { recursive: true });
1158
+ mkdirSync5(".omni/state", { recursive: true });
924
1159
  await updateRootGitignore();
925
1160
  let providerIds;
926
1161
  const isInteractive = !providerArg;
@@ -963,7 +1198,7 @@ async function runInit(_flags, providerArg) {
963
1198
  await setActiveProfile("default");
964
1199
  }
965
1200
  if (!existsSync6("OMNI.md")) {
966
- await writeFile5("OMNI.md", generateOmniMdTemplate(), "utf-8");
1201
+ await writeFile6("OMNI.md", generateOmniMdTemplate(), "utf-8");
967
1202
  }
968
1203
  const config = await loadConfig();
969
1204
  const ctx = {
@@ -1037,7 +1272,7 @@ async function addToGitignore(entriesToAdd, sectionHeader) {
1037
1272
  const gitignorePath = ".gitignore";
1038
1273
  let content = "";
1039
1274
  if (existsSync6(gitignorePath)) {
1040
- content = await readFile5(gitignorePath, "utf-8");
1275
+ content = await readFile4(gitignorePath, "utf-8");
1041
1276
  }
1042
1277
  const lines = content.split(`
1043
1278
  `);
@@ -1052,7 +1287,7 @@ async function addToGitignore(entriesToAdd, sectionHeader) {
1052
1287
  ${missingEntries.join(`
1053
1288
  `)}
1054
1289
  `;
1055
- await writeFile5(gitignorePath, content + section, "utf-8");
1290
+ await writeFile6(gitignorePath, content + section, "utf-8");
1056
1291
  }
1057
1292
  async function getTrackedProviderFiles(files) {
1058
1293
  const tracked = [];
@@ -1341,9 +1576,8 @@ async function runSync() {
1341
1576
  console.log(" • Capability registry");
1342
1577
  console.log(" • Capability sync hooks");
1343
1578
  console.log(" • .omni/.gitignore");
1344
- console.log(" • .omni/instructions.md");
1345
1579
  if (adapters.length > 0) {
1346
- console.log(" • Provider-specific files");
1580
+ console.log(" • Provider-specific files (instructions embedded)");
1347
1581
  }
1348
1582
  } catch (error) {
1349
1583
  console.error("");
@@ -1445,10 +1679,10 @@ async function loadCapabilityCommands() {
1445
1679
  return commands;
1446
1680
  }
1447
1681
  async function loadCapabilityExport(capability) {
1448
- const capabilityPath = join5(process.cwd(), capability.path);
1449
- const indexPath = join5(capabilityPath, "index.ts");
1682
+ const capabilityPath = join9(process.cwd(), capability.path);
1683
+ const indexPath = join9(capabilityPath, "index.ts");
1450
1684
  if (!existsSync8(indexPath)) {
1451
- const jsIndexPath = join5(capabilityPath, "index.js");
1685
+ const jsIndexPath = join9(capabilityPath, "index.js");
1452
1686
  if (!existsSync8(jsIndexPath)) {
1453
1687
  return null;
1454
1688
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnidev-ai/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@inquirer/prompts": "^8.1.0",
31
- "@omnidev-ai/core": "0.9.0",
31
+ "@omnidev-ai/core": "0.10.0",
32
32
  "@stricli/core": "^1.2.5"
33
33
  },
34
34
  "devDependencies": {