@omnidev-ai/cli 0.8.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 +419 -177
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -24,118 +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
- var claudeCodeAdapter = {
38
- id: "claude-code",
39
- displayName: "Claude Code",
40
- 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
+ }
41
48
  return {
42
- filesCreated: [],
43
- message: "Claude Code adapter initialized"
49
+ filesWritten
44
50
  };
45
- },
46
- 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 });
47
126
  const filesWritten = [];
48
- const filesDeleted = [];
49
- const claudeMdPath = join(ctx.projectRoot, "CLAUDE.md");
50
- const claudeMdContent = await generateClaudeMdContent(ctx.projectRoot);
51
- await writeFile(claudeMdPath, claudeMdContent, "utf-8");
52
- filesWritten.push("CLAUDE.md");
53
- const skillsDir = join(ctx.projectRoot, ".claude", "skills");
54
- mkdirSync(skillsDir, { recursive: true });
55
127
  for (const skill of bundle.skills) {
56
- const skillDir = join(skillsDir, skill.name);
57
- mkdirSync(skillDir, { recursive: true });
58
- 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");
59
131
  const content = `---
60
132
  name: ${skill.name}
61
133
  description: "${skill.description}"
62
134
  ---
63
135
 
64
136
  ${skill.instructions}`;
65
- await writeFile(skillPath, content, "utf-8");
66
- filesWritten.push(`.claude/skills/${skill.name}/SKILL.md`);
137
+ await writeFile4(skillPath, content, "utf-8");
138
+ filesWritten.push(join4(ctx.outputPath, skill.name, "SKILL.md"));
67
139
  }
68
140
  return {
69
- filesWritten,
70
- filesDeleted
141
+ filesWritten
71
142
  };
72
143
  }
73
144
  };
74
- async function generateClaudeMdContent(projectRoot) {
75
- const omniMdPath = join(projectRoot, "OMNI.md");
76
- let omniMdContent = "";
77
- if (existsSync(omniMdPath)) {
78
- omniMdContent = await readFile(omniMdPath, "utf-8");
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;
155
+ }
156
+ seen.add(key);
157
+ uniqueConfigs.push(config);
79
158
  }
80
- let content = omniMdContent;
81
- content += `
82
-
83
- ## OmniDev
84
-
85
- @import .omni/instructions.md
86
- `;
87
- return content;
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);
166
+ }
167
+ return {
168
+ filesWritten: allFilesWritten,
169
+ deduplicatedCount
170
+ };
88
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
+ };
89
195
  // ../adapters/src/codex/index.ts
90
- import { existsSync as existsSync2 } from "node:fs";
91
- import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
92
- import { join as join2 } from "node:path";
196
+ import { mkdirSync } from "node:fs";
197
+ import { join as join5 } from "node:path";
93
198
  var codexAdapter = {
94
199
  id: "codex",
95
200
  displayName: "Codex",
96
- 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 });
97
208
  return {
98
- filesCreated: [],
209
+ filesCreated: [".codex/"],
99
210
  message: "Codex adapter initialized"
100
211
  };
101
212
  },
102
- async sync(_bundle, ctx) {
103
- const filesWritten = [];
104
- const filesDeleted = [];
105
- const agentsMdPath = join2(ctx.projectRoot, "AGENTS.md");
106
- const agentsMdContent = await generateAgentsMdContent(ctx.projectRoot);
107
- await writeFile2(agentsMdPath, agentsMdContent, "utf-8");
108
- filesWritten.push("AGENTS.md");
213
+ async sync(bundle, ctx) {
214
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
109
215
  return {
110
- filesWritten,
111
- filesDeleted
216
+ filesWritten: result.filesWritten,
217
+ filesDeleted: []
112
218
  };
113
219
  }
114
220
  };
115
- async function generateAgentsMdContent(projectRoot) {
116
- const omniMdPath = join2(projectRoot, "OMNI.md");
117
- let omniMdContent = "";
118
- if (existsSync2(omniMdPath)) {
119
- omniMdContent = await readFile2(omniMdPath, "utf-8");
120
- }
121
- let content = omniMdContent;
122
- content += `
123
-
124
- ## OmniDev
125
-
126
- @import .omni/instructions.md
127
- `;
128
- return content;
129
- }
130
221
  // ../adapters/src/cursor/index.ts
131
222
  import { mkdirSync as mkdirSync2 } from "node:fs";
132
- import { writeFile as writeFile3 } from "node:fs/promises";
133
- import { join as join3 } from "node:path";
223
+ import { join as join6 } from "node:path";
134
224
  var cursorAdapter = {
135
225
  id: "cursor",
136
226
  displayName: "Cursor",
227
+ writers: [
228
+ { writer: InstructionsMdWriter, outputPath: "CLAUDE.md" },
229
+ { writer: SkillsWriter, outputPath: ".claude/skills/" },
230
+ { writer: CursorRulesWriter, outputPath: ".cursor/rules/" }
231
+ ],
137
232
  async init(ctx) {
138
- const rulesDir = join3(ctx.projectRoot, ".cursor", "rules");
233
+ const rulesDir = join6(ctx.projectRoot, ".cursor", "rules");
139
234
  mkdirSync2(rulesDir, { recursive: true });
140
235
  return {
141
236
  filesCreated: [".cursor/rules/"],
@@ -143,66 +238,39 @@ var cursorAdapter = {
143
238
  };
144
239
  },
145
240
  async sync(bundle, ctx) {
146
- const filesWritten = [];
147
- const filesDeleted = [];
148
- const rulesDir = join3(ctx.projectRoot, ".cursor", "rules");
149
- mkdirSync2(rulesDir, { recursive: true });
150
- for (const rule of bundle.rules) {
151
- const rulePath = join3(rulesDir, `omnidev-${rule.name}.mdc`);
152
- await writeFile3(rulePath, rule.content, "utf-8");
153
- filesWritten.push(`.cursor/rules/omnidev-${rule.name}.mdc`);
154
- }
241
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
155
242
  return {
156
- filesWritten,
157
- filesDeleted
243
+ filesWritten: result.filesWritten,
244
+ filesDeleted: []
158
245
  };
159
246
  }
160
247
  };
161
248
  // ../adapters/src/opencode/index.ts
162
- import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
163
- import { readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
164
- import { join as join4 } from "node:path";
249
+ import { mkdirSync as mkdirSync3 } from "node:fs";
250
+ import { join as join7 } from "node:path";
165
251
  var opencodeAdapter = {
166
252
  id: "opencode",
167
253
  displayName: "OpenCode",
254
+ writers: [
255
+ { writer: InstructionsMdWriter, outputPath: "AGENTS.md" },
256
+ { writer: SkillsWriter, outputPath: ".opencode/skills/" }
257
+ ],
168
258
  async init(ctx) {
169
- const opencodeDir = join4(ctx.projectRoot, ".opencode");
259
+ const opencodeDir = join7(ctx.projectRoot, ".opencode");
170
260
  mkdirSync3(opencodeDir, { recursive: true });
171
261
  return {
172
262
  filesCreated: [".opencode/"],
173
263
  message: "OpenCode adapter initialized"
174
264
  };
175
265
  },
176
- async sync(_bundle, ctx) {
177
- const filesWritten = [];
178
- const filesDeleted = [];
179
- const opencodeDir = join4(ctx.projectRoot, ".opencode");
180
- mkdirSync3(opencodeDir, { recursive: true });
181
- const instructionsPath = join4(opencodeDir, "instructions.md");
182
- const instructionsContent = await generateOpencodeInstructionsContent(ctx.projectRoot);
183
- await writeFile4(instructionsPath, instructionsContent, "utf-8");
184
- filesWritten.push(".opencode/instructions.md");
266
+ async sync(bundle, ctx) {
267
+ const result = await executeWriters(this.writers, bundle, ctx.projectRoot);
185
268
  return {
186
- filesWritten,
187
- filesDeleted
269
+ filesWritten: result.filesWritten,
270
+ filesDeleted: []
188
271
  };
189
272
  }
190
273
  };
191
- async function generateOpencodeInstructionsContent(projectRoot) {
192
- const omniMdPath = join4(projectRoot, "OMNI.md");
193
- let omniMdContent = "";
194
- if (existsSync3(omniMdPath)) {
195
- omniMdContent = await readFile3(omniMdPath, "utf-8");
196
- }
197
- let content = omniMdContent;
198
- content += `
199
-
200
- ## OmniDev
201
-
202
- @import ../.omni/instructions.md
203
- `;
204
- return content;
205
- }
206
274
  // ../adapters/src/registry.ts
207
275
  import { readEnabledProviders } from "@omnidev-ai/core";
208
276
  var builtInAdapters = [
@@ -223,59 +291,93 @@ async function getEnabledAdapters() {
223
291
  import {
224
292
  getActiveProfile,
225
293
  loadBaseConfig,
294
+ patchAddCapabilitySource,
295
+ patchAddMcp,
296
+ patchAddToProfile,
226
297
  syncAgentConfiguration,
227
- writeConfig
298
+ readCapabilityIdFromPath
228
299
  } from "@omnidev-ai/core";
229
300
  import { buildCommand, buildRouteMap } from "@stricli/core";
230
- function addToActiveProfile(config, activeProfile, capabilityName) {
231
- if (!config.profiles) {
232
- config.profiles = {};
233
- }
234
- if (!config.profiles[activeProfile]) {
235
- config.profiles[activeProfile] = { capabilities: [] };
236
- }
237
- if (!config.profiles[activeProfile].capabilities) {
238
- config.profiles[activeProfile].capabilities = [];
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);
239
310
  }
240
- if (!config.profiles[activeProfile].capabilities.includes(capabilityName)) {
241
- config.profiles[activeProfile].capabilities.push(capabilityName);
311
+ const parts = source.replace("github:", "").split("/");
312
+ if (parts.length >= 2) {
313
+ return parts[parts.length - 1] ?? parts[1] ?? "capability";
242
314
  }
315
+ return "capability";
243
316
  }
244
317
  async function runAddCap(flags, name) {
245
318
  try {
246
- if (!existsSync4("omni.toml")) {
319
+ if (!existsSync3("omni.toml")) {
247
320
  console.log("✗ No config file found");
248
321
  console.log(" Run: omnidev init");
249
322
  process.exit(1);
250
323
  }
251
- if (!flags.github.includes("/")) {
252
- console.error("✗ Invalid GitHub repository format");
253
- console.log(" Expected format: user/repo");
254
- 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");
255
329
  process.exit(1);
256
330
  }
257
- const config = await loadBaseConfig();
258
- const activeProfile = await getActiveProfile() ?? config.active_profile ?? "default";
259
- if (!config.capabilities) {
260
- config.capabilities = {};
331
+ if (flags.github && flags.local) {
332
+ console.error("✗ Cannot specify both --github and --local");
333
+ console.log(" Use only one source flag");
334
+ process.exit(1);
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");
261
357
  }
262
- if (!config.capabilities.sources) {
263
- config.capabilities.sources = {};
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}`);
264
366
  }
265
- if (config.capabilities.sources[name]) {
266
- console.error(`✗ Capability source "${name}" already exists`);
367
+ const config = await loadBaseConfig();
368
+ const activeProfile = await getActiveProfile() ?? config.active_profile ?? "default";
369
+ if (config.capabilities?.sources?.[capabilityId]) {
370
+ console.error(`✗ Capability source "${capabilityId}" already exists`);
267
371
  console.log(" Use a different name or remove the existing source first");
268
372
  process.exit(1);
269
373
  }
270
- const source = `github:${flags.github}`;
271
- if (flags.path) {
272
- config.capabilities.sources[name] = { source, path: flags.path };
374
+ if (flags.path && sourceType === "github") {
375
+ await patchAddCapabilitySource(capabilityId, { source, path: flags.path });
273
376
  } else {
274
- config.capabilities.sources[name] = source;
377
+ await patchAddCapabilitySource(capabilityId, source);
275
378
  }
276
- addToActiveProfile(config, activeProfile, name);
277
- await writeConfig(config);
278
- console.log(`✓ Added capability source: ${name}`);
379
+ await patchAddToProfile(activeProfile, capabilityId);
380
+ console.log(`✓ Added capability source: ${capabilityId}`);
279
381
  console.log(` Source: ${source}`);
280
382
  if (flags.path) {
281
383
  console.log(` Path: ${flags.path}`);
@@ -292,17 +394,14 @@ async function runAddCap(flags, name) {
292
394
  }
293
395
  async function runAddMcp(flags, name) {
294
396
  try {
295
- if (!existsSync4("omni.toml")) {
397
+ if (!existsSync3("omni.toml")) {
296
398
  console.log("✗ No config file found");
297
399
  console.log(" Run: omnidev init");
298
400
  process.exit(1);
299
401
  }
300
402
  const config = await loadBaseConfig();
301
403
  const activeProfile = await getActiveProfile() ?? config.active_profile ?? "default";
302
- if (!config.mcps) {
303
- config.mcps = {};
304
- }
305
- if (config.mcps[name]) {
404
+ if (config.mcps?.[name]) {
306
405
  console.error(`✗ MCP "${name}" already exists`);
307
406
  console.log(" Use a different name or remove the existing MCP first");
308
407
  process.exit(1);
@@ -356,9 +455,8 @@ async function runAddMcp(flags, name) {
356
455
  }
357
456
  }
358
457
  }
359
- config.mcps[name] = mcpConfig;
360
- addToActiveProfile(config, activeProfile, name);
361
- await writeConfig(config);
458
+ await patchAddMcp(name, mcpConfig);
459
+ await patchAddToProfile(activeProfile, name);
362
460
  console.log(`✓ Added MCP: ${name}`);
363
461
  console.log(` Transport: ${transport}`);
364
462
  if (mcpConfig.url) {
@@ -408,23 +506,46 @@ function parseArgs(argsString) {
408
506
  return args;
409
507
  }
410
508
  async function runAddCapWrapper(flags, name) {
411
- await runAddCap({ github: flags.github, path: flags.path }, name);
509
+ await runAddCap({ github: flags.github, local: flags.local, path: flags.path }, name);
412
510
  }
413
511
  var addCapCommand = buildCommand({
414
512
  docs: {
415
- brief: "Add a capability source from GitHub",
416
- 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`
417
531
  },
418
532
  parameters: {
419
533
  flags: {
420
534
  github: {
421
535
  kind: "parsed",
422
536
  brief: "GitHub repository in user/repo format",
423
- 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
424
545
  },
425
546
  path: {
426
547
  kind: "parsed",
427
- brief: "Subdirectory within the repo containing the capability",
548
+ brief: "Subdirectory within the repo containing the capability (GitHub only)",
428
549
  parse: String,
429
550
  optional: true
430
551
  }
@@ -433,10 +554,16 @@ var addCapCommand = buildCommand({
433
554
  kind: "tuple",
434
555
  parameters: [
435
556
  {
436
- brief: "Capability name",
437
- parse: String
557
+ brief: "Capability name (optional, will be inferred if omitted)",
558
+ parse: String,
559
+ optional: true
438
560
  }
439
561
  ]
562
+ },
563
+ aliases: {
564
+ g: "github",
565
+ l: "local",
566
+ p: "path"
440
567
  }
441
568
  },
442
569
  func: runAddCapWrapper
@@ -542,15 +669,31 @@ var addRoutes = buildRouteMap({
542
669
  });
543
670
 
544
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";
545
676
  import {
546
677
  disableCapability,
547
678
  discoverCapabilities,
548
679
  enableCapability,
680
+ generateCapabilityToml,
681
+ generateHooksTemplate,
682
+ generateHookScript,
683
+ generateRuleTemplate,
684
+ generateSkillTemplate,
549
685
  getEnabledCapabilities,
550
686
  loadCapabilityConfig,
551
687
  syncAgentConfiguration as syncAgentConfiguration2
552
688
  } from "@omnidev-ai/core";
553
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
554
697
  async function runCapabilityList() {
555
698
  try {
556
699
  const enabledIds = await getEnabledCapabilities();
@@ -619,6 +762,105 @@ async function runCapabilityDisable(_flags, name) {
619
762
  process.exit(1);
620
763
  }
621
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
+ });
622
864
  var listCommand = buildCommand2({
623
865
  docs: {
624
866
  brief: "List all discovered capabilities"
@@ -666,6 +908,7 @@ var disableCommand = buildCommand2({
666
908
  });
667
909
  var capabilityRoutes = buildRouteMap2({
668
910
  routes: {
911
+ new: newCommand,
669
912
  list: listCommand,
670
913
  enable: enableCommand,
671
914
  disable: disableCommand
@@ -678,7 +921,7 @@ var capabilityRoutes = buildRouteMap2({
678
921
  // src/commands/doctor.ts
679
922
  import { existsSync as existsSync5 } from "node:fs";
680
923
  import { execFile } from "node:child_process";
681
- import { readFile as readFile4 } from "node:fs/promises";
924
+ import { readFile as readFile3 } from "node:fs/promises";
682
925
  import { promisify } from "node:util";
683
926
  import { buildCommand as buildCommand3 } from "@stricli/core";
684
927
  var doctorCommand = buildCommand3({
@@ -818,7 +1061,7 @@ async function checkRootGitignore() {
818
1061
  fix: "Run: omnidev init"
819
1062
  };
820
1063
  }
821
- const content = await readFile4(gitignorePath, "utf-8");
1064
+ const content = await readFile3(gitignorePath, "utf-8");
822
1065
  const lines = content.split(`
823
1066
  `).map((line) => line.trim());
824
1067
  const hasOmniDir = lines.includes(".omni/");
@@ -860,15 +1103,15 @@ async function checkCapabilitiesDir() {
860
1103
 
861
1104
  // src/commands/init.ts
862
1105
  import { exec } from "node:child_process";
863
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
864
- 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";
865
1108
  import { promisify as promisify2 } from "node:util";
866
1109
  import {
867
1110
  generateOmniMdTemplate,
868
1111
  loadConfig,
869
1112
  setActiveProfile,
870
1113
  syncAgentConfiguration as syncAgentConfiguration3,
871
- writeConfig as writeConfig2,
1114
+ writeConfig,
872
1115
  writeEnabledProviders
873
1116
  } from "@omnidev-ai/core";
874
1117
  import { buildCommand as buildCommand4 } from "@stricli/core";
@@ -910,9 +1153,9 @@ async function promptForGitignoreProviderFiles(selectedProviders) {
910
1153
  var execAsync = promisify2(exec);
911
1154
  async function runInit(_flags, providerArg) {
912
1155
  console.log("Initializing OmniDev...");
913
- mkdirSync4(".omni", { recursive: true });
914
- mkdirSync4(".omni/capabilities", { recursive: true });
915
- mkdirSync4(".omni/state", { recursive: true });
1156
+ mkdirSync5(".omni", { recursive: true });
1157
+ mkdirSync5(".omni/capabilities", { recursive: true });
1158
+ mkdirSync5(".omni/state", { recursive: true });
916
1159
  await updateRootGitignore();
917
1160
  let providerIds;
918
1161
  const isInteractive = !providerArg;
@@ -939,7 +1182,7 @@ async function runInit(_flags, providerArg) {
939
1182
  }
940
1183
  await writeEnabledProviders(providerIds);
941
1184
  if (!existsSync6("omni.toml")) {
942
- await writeConfig2({
1185
+ await writeConfig({
943
1186
  profiles: {
944
1187
  default: {
945
1188
  capabilities: []
@@ -955,7 +1198,7 @@ async function runInit(_flags, providerArg) {
955
1198
  await setActiveProfile("default");
956
1199
  }
957
1200
  if (!existsSync6("OMNI.md")) {
958
- await writeFile5("OMNI.md", generateOmniMdTemplate(), "utf-8");
1201
+ await writeFile6("OMNI.md", generateOmniMdTemplate(), "utf-8");
959
1202
  }
960
1203
  const config = await loadConfig();
961
1204
  const ctx = {
@@ -1029,7 +1272,7 @@ async function addToGitignore(entriesToAdd, sectionHeader) {
1029
1272
  const gitignorePath = ".gitignore";
1030
1273
  let content = "";
1031
1274
  if (existsSync6(gitignorePath)) {
1032
- content = await readFile5(gitignorePath, "utf-8");
1275
+ content = await readFile4(gitignorePath, "utf-8");
1033
1276
  }
1034
1277
  const lines = content.split(`
1035
1278
  `);
@@ -1044,7 +1287,7 @@ async function addToGitignore(entriesToAdd, sectionHeader) {
1044
1287
  ${missingEntries.join(`
1045
1288
  `)}
1046
1289
  `;
1047
- await writeFile5(gitignorePath, content + section, "utf-8");
1290
+ await writeFile6(gitignorePath, content + section, "utf-8");
1048
1291
  }
1049
1292
  async function getTrackedProviderFiles(files) {
1050
1293
  const tracked = [];
@@ -1333,9 +1576,8 @@ async function runSync() {
1333
1576
  console.log(" • Capability registry");
1334
1577
  console.log(" • Capability sync hooks");
1335
1578
  console.log(" • .omni/.gitignore");
1336
- console.log(" • .omni/instructions.md");
1337
1579
  if (adapters.length > 0) {
1338
- console.log(" • Provider-specific files");
1580
+ console.log(" • Provider-specific files (instructions embedded)");
1339
1581
  }
1340
1582
  } catch (error) {
1341
1583
  console.error("");
@@ -1437,10 +1679,10 @@ async function loadCapabilityCommands() {
1437
1679
  return commands;
1438
1680
  }
1439
1681
  async function loadCapabilityExport(capability) {
1440
- const capabilityPath = join5(process.cwd(), capability.path);
1441
- const indexPath = join5(capabilityPath, "index.ts");
1682
+ const capabilityPath = join9(process.cwd(), capability.path);
1683
+ const indexPath = join9(capabilityPath, "index.ts");
1442
1684
  if (!existsSync8(indexPath)) {
1443
- const jsIndexPath = join5(capabilityPath, "index.js");
1685
+ const jsIndexPath = join9(capabilityPath, "index.js");
1444
1686
  if (!existsSync8(jsIndexPath)) {
1445
1687
  return null;
1446
1688
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnidev-ai/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@inquirer/prompts": "^8.1.0",
31
- "@omnidev-ai/core": "0.8.0",
31
+ "@omnidev-ai/core": "0.10.0",
32
32
  "@stricli/core": "^1.2.5"
33
33
  },
34
34
  "devDependencies": {
35
- "@omnidev-ai/adapters": "0.0.1",
35
+ "@omnidev-ai/adapters": "0.0.11",
36
36
  "bunup": "^0.16.20"
37
37
  }
38
38
  }