From d6af3188fcf32c5c82538989f8c3cee6b683fc3f Mon Sep 17 00:00:00 2001 From: tsubus Date: Thu, 7 May 2026 17:52:19 +0200 Subject: [PATCH] feat: add Oh My Pi (omp) support Add ToolCommandAdapter for Oh My Pi terminal AI coding agent. Commands generate to .omp/commands/opsx-.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 --- src/core/command-generation/adapters/index.ts | 1 + .../command-generation/adapters/oh-my-pi.ts | 40 ++++++++++++++ src/core/command-generation/registry.ts | 2 + src/core/config.ts | 1 + src/core/init.ts | 4 +- src/core/update.ts | 4 +- test/core/command-generation/adapters.test.ts | 53 ++++++++++++++++++- 7 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/core/command-generation/adapters/oh-my-pi.ts diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 00fc75d5d..d42a57457 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -23,6 +23,7 @@ export { iflowAdapter } from './iflow.js'; export { junieAdapter } from './junie.js'; export { kilocodeAdapter } from './kilocode.js'; export { kiroAdapter } from './kiro.js'; +export { ohMyPiAdapter } from './oh-my-pi.js'; export { opencodeAdapter } from './opencode.js'; export { piAdapter } from './pi.js'; export { qoderAdapter } from './qoder.js'; diff --git a/src/core/command-generation/adapters/oh-my-pi.ts b/src/core/command-generation/adapters/oh-my-pi.ts new file mode 100644 index 000000000..cffec29ac --- /dev/null +++ b/src/core/command-generation/adapters/oh-my-pi.ts @@ -0,0 +1,40 @@ +/** + * Oh My Pi (OMP) Command Adapter + * + * Formats commands for Oh My Pi following its slash command specification. + * OMP loads slash commands from .omp/commands/*.md with YAML frontmatter. + * The filename (minus .md) becomes the slash command name. + */ + +import path from 'path'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; +import { transformToHyphenCommands } from '../../../utils/command-references.js'; + +/** + * Oh My Pi adapter for command generation. + * File path: .omp/commands/opsx-.md + * Frontmatter: description + * + * OMP uses the filename (minus .md) as the slash command name, so + * opsx-propose.md → /opsx-propose. Command references in the body + * are transformed from /opsx: to /opsx- for consistency. + */ +export const ohMyPiAdapter: ToolCommandAdapter = { + toolId: 'oh-my-pi', + + getFilePath(commandId: string): string { + return path.join('.omp', 'commands', `opsx-${commandId}.md`); + }, + + formatFile(content: CommandContent): string { + // Transform /opsx: references to /opsx- for filename-based command naming + const transformedBody = transformToHyphenCommands(content.body); + + return `--- +description: ${content.description} +--- + +${transformedBody} +`; + }, +}; diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index 3b726d707..5a17b2f39 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -25,6 +25,7 @@ import { iflowAdapter } from './adapters/iflow.js'; import { junieAdapter } from './adapters/junie.js'; import { kilocodeAdapter } from './adapters/kilocode.js'; import { kiroAdapter } from './adapters/kiro.js'; +import { ohMyPiAdapter } from './adapters/oh-my-pi.js'; import { opencodeAdapter } from './adapters/opencode.js'; import { piAdapter } from './adapters/pi.js'; import { qoderAdapter } from './adapters/qoder.js'; @@ -60,6 +61,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(junieAdapter); CommandAdapterRegistry.register(kilocodeAdapter); CommandAdapterRegistry.register(kiroAdapter); + CommandAdapterRegistry.register(ohMyPiAdapter); CommandAdapterRegistry.register(opencodeAdapter); CommandAdapterRegistry.register(piAdapter); CommandAdapterRegistry.register(qoderAdapter); diff --git a/src/core/config.ts b/src/core/config.ts index 68f1abd33..2f2ae22b0 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -40,6 +40,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' }, { name: 'Kimi CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' }, { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' }, + { name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' }, { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' }, { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, diff --git a/src/core/init.ts b/src/core/init.ts index aa38408f2..d14958069 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -315,7 +315,7 @@ export class InitCommand { if (!a.configured && b.configured) return 1; if (a.detected && !b.detected) return -1; if (!a.detected && b.detected) return 1; - return 0; + return a.name.localeCompare(b.name); }); const configuredNames = validTools @@ -538,7 +538,7 @@ export class InitCommand { // Generate SKILL.md content with YAML frontmatter including generatedBy // Use hyphen-based command references for tools where filename = command name - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); // Write the skill file diff --git a/src/core/update.ts b/src/core/update.ts index e1582cd5b..5c123babe 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -197,7 +197,7 @@ export class UpdateCommand { const skillFile = path.join(skillDir, 'SKILL.md'); // Use hyphen-based command references for OpenCode - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } @@ -691,7 +691,7 @@ export class UpdateCommand { const skillFile = path.join(skillDir, 'SKILL.md'); // Use hyphen-based command references for OpenCode - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index b91dc024f..6f617ebdc 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -18,6 +18,7 @@ import { geminiAdapter } from '../../../src/core/command-generation/adapters/gem import { githubCopilotAdapter } from '../../../src/core/command-generation/adapters/github-copilot.js'; import { iflowAdapter } from '../../../src/core/command-generation/adapters/iflow.js'; import { kilocodeAdapter } from '../../../src/core/command-generation/adapters/kilocode.js'; +import { ohMyPiAdapter } from '../../../src/core/command-generation/adapters/oh-my-pi.js'; import { opencodeAdapter } from '../../../src/core/command-generation/adapters/opencode.js'; import { piAdapter } from '../../../src/core/command-generation/adapters/pi.js'; import { qoderAdapter } from '../../../src/core/command-generation/adapters/qoder.js'; @@ -653,6 +654,56 @@ describe('command-generation/adapters', () => { expect(output).toContain('description: "Line 1\\nLine 2"'); }); }); + describe('ohMyPiAdapter', () => { + it('should have correct toolId', () => { + expect(ohMyPiAdapter.toolId).toBe('oh-my-pi'); + }); + + it('should generate correct file path', () => { + const filePath = ohMyPiAdapter.getFilePath('explore'); + expect(filePath).toBe(path.join('.omp', 'commands', 'opsx-explore.md')); + }); + + it('should generate correct file paths for different commands', () => { + expect(ohMyPiAdapter.getFilePath('new')).toBe(path.join('.omp', 'commands', 'opsx-new.md')); + expect(ohMyPiAdapter.getFilePath('bulk-archive')).toBe(path.join('.omp', 'commands', 'opsx-bulk-archive.md')); + }); + + it('should format file with description frontmatter', () => { + const output = ohMyPiAdapter.formatFile(sampleContent); + expect(output).toContain('---\n'); + expect(output).toContain('description: Enter explore mode for thinking'); + expect(output).toContain('---\n\n'); + expect(output).toContain('This is the command body.'); + }); + + it('should transform command references from colon to hyphen format', () => { + const contentWithRefs: CommandContent = { + ...sampleContent, + body: 'Run /opsx:apply to implement. Then /opsx:archive when done.', + }; + + const output = ohMyPiAdapter.formatFile(contentWithRefs); + expect(output).toContain('/opsx-apply'); + expect(output).toContain('/opsx-archive'); + expect(output).not.toContain('/opsx:apply'); + }); + + it('should handle multiple command references in body', () => { + const contentWithMultipleCommands: CommandContent = { + ...sampleContent, + body: `/opsx:explore for ideas +/opsx:new to create +/opsx:continue to proceed +/opsx:apply to implement`, + }; + const output = ohMyPiAdapter.formatFile(contentWithMultipleCommands); + expect(output).toContain('/opsx-explore'); + expect(output).toContain('/opsx-new'); + expect(output).toContain('/opsx-continue'); + expect(output).toContain('/opsx-apply'); + }); + }); describe('roocodeAdapter', () => { it('should have correct toolId', () => { @@ -697,7 +748,7 @@ describe('command-generation/adapters', () => { amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, clineAdapter, codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter, crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter, - iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter, + iflowAdapter, kilocodeAdapter, ohMyPiAdapter, opencodeAdapter, piAdapter, qoderAdapter, qwenAdapter, roocodeAdapter ]; for (const adapter of adapters) {