Skip to content

Commit ad52374

Browse files
committed
feat: add oh-my-pi (OMP) support
Add ToolCommandAdapter for oh-my-pi terminal AI coding agent. Commands generate to .omp/commands/opsx-<id>.md with description frontmatter and hyphen-based command references. Skills generate to .omp/skills/ via transformToHyphenCommands. - New adapter: src/core/command-generation/adapters/oh-my-pi.ts - Register in CommandAdapterRegistry and index - Add to AI_TOOLS with skillsDir: '.omp' - Add to hyphen command transformer whitelist in init.ts and update.ts - Full test coverage following OMP patterns
1 parent 053d8a5 commit ad52374

8 files changed

Lines changed: 102 additions & 5 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-07

src/core/command-generation/adapters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { iflowAdapter } from './iflow.js';
2323
export { junieAdapter } from './junie.js';
2424
export { kilocodeAdapter } from './kilocode.js';
2525
export { kiroAdapter } from './kiro.js';
26+
export { ohMyPiAdapter } from './oh-my-pi.js';
2627
export { opencodeAdapter } from './opencode.js';
2728
export { piAdapter } from './pi.js';
2829
export { qoderAdapter } from './qoder.js';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* oh-my-pi (OMP) Command Adapter
3+
*
4+
* Formats commands for oh-my-pi following its slash command specification.
5+
* OMP loads slash commands from .omp/commands/*.md with YAML frontmatter.
6+
* The filename (minus .md) becomes the slash command name.
7+
*/
8+
9+
import path from 'path';
10+
import type { CommandContent, ToolCommandAdapter } from '../types.js';
11+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
12+
13+
/**
14+
* oh-my-pi adapter for command generation.
15+
* File path: .omp/commands/opsx-<id>.md
16+
* Frontmatter: description
17+
*
18+
* OMP uses the filename (minus .md) as the slash command name, so
19+
* opsx-propose.md → /opsx-propose. Command references in the body
20+
* are transformed from /opsx: to /opsx- for consistency.
21+
*/
22+
export const ohMyPiAdapter: ToolCommandAdapter = {
23+
toolId: 'oh-my-pi',
24+
25+
getFilePath(commandId: string): string {
26+
return path.join('.omp', 'commands', `opsx-${commandId}.md`);
27+
},
28+
29+
formatFile(content: CommandContent): string {
30+
// Transform /opsx: references to /opsx- for filename-based command naming
31+
const transformedBody = transformToHyphenCommands(content.body);
32+
33+
return `---
34+
description: ${content.description}
35+
---
36+
37+
${transformedBody}
38+
`;
39+
},
40+
};

src/core/command-generation/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { iflowAdapter } from './adapters/iflow.js';
2525
import { junieAdapter } from './adapters/junie.js';
2626
import { kilocodeAdapter } from './adapters/kilocode.js';
2727
import { kiroAdapter } from './adapters/kiro.js';
28+
import { ohMyPiAdapter } from './adapters/oh-my-pi.js';
2829
import { opencodeAdapter } from './adapters/opencode.js';
2930
import { piAdapter } from './adapters/pi.js';
3031
import { qoderAdapter } from './adapters/qoder.js';
@@ -60,6 +61,7 @@ export class CommandAdapterRegistry {
6061
CommandAdapterRegistry.register(junieAdapter);
6162
CommandAdapterRegistry.register(kilocodeAdapter);
6263
CommandAdapterRegistry.register(kiroAdapter);
64+
CommandAdapterRegistry.register(ohMyPiAdapter);
6365
CommandAdapterRegistry.register(opencodeAdapter);
6466
CommandAdapterRegistry.register(piAdapter);
6567
CommandAdapterRegistry.register(qoderAdapter);

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const AI_TOOLS: AIToolOption[] = [
4040
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
4141
{ name: 'Kimi CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' },
4242
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
43+
{ name: 'oh-my-pi', value: 'oh-my-pi', available: true, successLabel: 'oh-my-pi', skillsDir: '.omp' },
4344
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
4445
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
4546
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },

src/core/init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export class InitCommand {
315315
if (!a.configured && b.configured) return 1;
316316
if (a.detected && !b.detected) return -1;
317317
if (!a.detected && b.detected) return 1;
318-
return 0;
318+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
319319
});
320320

321321
const configuredNames = validTools
@@ -538,7 +538,7 @@ export class InitCommand {
538538

539539
// Generate SKILL.md content with YAML frontmatter including generatedBy
540540
// Use hyphen-based command references for tools where filename = command name
541-
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
541+
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
542542
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
543543

544544
// Write the skill file

src/core/update.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export class UpdateCommand {
197197
const skillFile = path.join(skillDir, 'SKILL.md');
198198

199199
// Use hyphen-based command references for OpenCode
200-
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
200+
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
201201
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
202202
await FileSystemUtils.writeFile(skillFile, skillContent);
203203
}
@@ -691,7 +691,7 @@ export class UpdateCommand {
691691
const skillFile = path.join(skillDir, 'SKILL.md');
692692

693693
// Use hyphen-based command references for OpenCode
694-
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
694+
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
695695
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
696696
await FileSystemUtils.writeFile(skillFile, skillContent);
697697
}

test/core/command-generation/adapters.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { geminiAdapter } from '../../../src/core/command-generation/adapters/gem
1818
import { githubCopilotAdapter } from '../../../src/core/command-generation/adapters/github-copilot.js';
1919
import { iflowAdapter } from '../../../src/core/command-generation/adapters/iflow.js';
2020
import { kilocodeAdapter } from '../../../src/core/command-generation/adapters/kilocode.js';
21+
import { ohMyPiAdapter } from '../../../src/core/command-generation/adapters/oh-my-pi.js';
2122
import { opencodeAdapter } from '../../../src/core/command-generation/adapters/opencode.js';
2223
import { piAdapter } from '../../../src/core/command-generation/adapters/pi.js';
2324
import { qoderAdapter } from '../../../src/core/command-generation/adapters/qoder.js';
@@ -653,6 +654,56 @@ describe('command-generation/adapters', () => {
653654
expect(output).toContain('description: "Line 1\\nLine 2"');
654655
});
655656
});
657+
describe('ohMyPiAdapter', () => {
658+
it('should have correct toolId', () => {
659+
expect(ohMyPiAdapter.toolId).toBe('oh-my-pi');
660+
});
661+
662+
it('should generate correct file path', () => {
663+
const filePath = ohMyPiAdapter.getFilePath('explore');
664+
expect(filePath).toBe(path.join('.omp', 'commands', 'opsx-explore.md'));
665+
});
666+
667+
it('should generate correct file paths for different commands', () => {
668+
expect(ohMyPiAdapter.getFilePath('new')).toBe(path.join('.omp', 'commands', 'opsx-new.md'));
669+
expect(ohMyPiAdapter.getFilePath('bulk-archive')).toBe(path.join('.omp', 'commands', 'opsx-bulk-archive.md'));
670+
});
671+
672+
it('should format file with description frontmatter', () => {
673+
const output = ohMyPiAdapter.formatFile(sampleContent);
674+
expect(output).toContain('---\n');
675+
expect(output).toContain('description: Enter explore mode for thinking');
676+
expect(output).toContain('---\n\n');
677+
expect(output).toContain('This is the command body.');
678+
});
679+
680+
it('should transform command references from colon to hyphen format', () => {
681+
const contentWithRefs: CommandContent = {
682+
...sampleContent,
683+
body: 'Run /opsx:apply to implement. Then /opsx:archive when done.',
684+
};
685+
686+
const output = ohMyPiAdapter.formatFile(contentWithRefs);
687+
expect(output).toContain('/opsx-apply');
688+
expect(output).toContain('/opsx-archive');
689+
expect(output).not.toContain('/opsx:apply');
690+
});
691+
692+
it('should handle multiple command references in body', () => {
693+
const contentWithMultipleCommands: CommandContent = {
694+
...sampleContent,
695+
body: `/opsx:explore for ideas
696+
/opsx:new to create
697+
/opsx:continue to proceed
698+
/opsx:apply to implement`,
699+
};
700+
const output = ohMyPiAdapter.formatFile(contentWithMultipleCommands);
701+
expect(output).toContain('/opsx-explore');
702+
expect(output).toContain('/opsx-new');
703+
expect(output).toContain('/opsx-continue');
704+
expect(output).toContain('/opsx-apply');
705+
});
706+
});
656707

657708
describe('roocodeAdapter', () => {
658709
it('should have correct toolId', () => {
@@ -697,7 +748,7 @@ describe('command-generation/adapters', () => {
697748
amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, clineAdapter,
698749
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
699750
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
700-
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
751+
iflowAdapter, kilocodeAdapter, ohMyPiAdapter, opencodeAdapter, piAdapter, qoderAdapter,
701752
qwenAdapter, roocodeAdapter
702753
];
703754
for (const adapter of adapters) {

0 commit comments

Comments
 (0)