diff --git a/docs/cli.md b/docs/cli.md index e9845da4b9..07a5daea63 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -104,7 +104,7 @@ openspec init [path] [options] `--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`). -**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` +**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` > This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths. diff --git a/docs/commands.md b/docs/commands.md index 8587fd062e..6737eb305c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -670,6 +670,7 @@ Different AI tools use slightly different command syntax. Use the format that ma | Cursor | `/opsx-propose`, `/opsx-apply` | | Windsurf | `/opsx-propose`, `/opsx-apply` | | Copilot (IDE) | `/opsx-propose`, `/opsx-apply` | +| Oh My Pi | `/opsx-propose`, `/opsx-apply` | | Kimi CLI | Skill-based invocations such as `/skill:openspec-propose`, `/skill:openspec-apply-change` (no generated `opsx-*` command files) | | Trae | `/opsx-propose`, `/opsx-apply` | diff --git a/docs/how-commands-work.md b/docs/how-commands-work.md index 42d18d4161..29637a4927 100644 --- a/docs/how-commands-work.md +++ b/docs/how-commands-work.md @@ -79,6 +79,7 @@ The intent is identical everywhere. The punctuation differs. Use the form that m | Cursor | `/opsx-propose`, `/opsx-apply` | | Windsurf | `/opsx-propose`, `/opsx-apply` | | GitHub Copilot (IDE) | `/opsx-propose`, `/opsx-apply` | +| Oh My Pi | `/opsx-propose`, `/opsx-apply` | | Kimi CLI | skill-style, e.g. `/skill:openspec-propose` | | Trae | `/opsx-propose`, `/opsx-apply` | diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 7d20d2f17b..fb568832c0 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -45,6 +45,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch | Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-.prompt.md` | | Lingma (`lingma`) | `.lingma/skills/openspec-*/SKILL.md` | `.lingma/commands/opsx/.md` | | Mistral Vibe (`vibe`) | `.vibe/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | +| Oh My Pi (`oh-my-pi`) | `.omp/skills/openspec-*/SKILL.md` | `.omp/commands/opsx-.md` | | OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-.md` | | Pi (`pi`) | `.pi/skills/openspec-*/SKILL.md` | `.pi/prompts/opsx-.md` | | Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/.md` | @@ -75,7 +76,7 @@ openspec init --tools none openspec init --profile core ``` -**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf` +**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` ## Workflow-Dependent Installation diff --git a/openspec/changes/feat-add-omp-tool-support/.openspec.yaml b/openspec/changes/feat-add-omp-tool-support/.openspec.yaml new file mode 100644 index 0000000000..34f9314d22 --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-29 diff --git a/openspec/changes/feat-add-omp-tool-support/design.md b/openspec/changes/feat-add-omp-tool-support/design.md new file mode 100644 index 0000000000..d5de2cad5a --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/design.md @@ -0,0 +1,59 @@ +## Context + +OpenSpec supports AI coding assistants by generating two artifact types per tool: skill files (for agent instruction loading) and command files (for slash-command invocation). Each tool has a `ToolCommandAdapter` that controls the output path and file format. + +Oh My Pi (OMP) is a terminal AI coding agent that uses a `.omp/` project directory. Its command system uses the filename stem as the slash command name (e.g., `opsx-propose.md` → `/opsx-propose`), which requires command body references to be in hyphenated form (`/opsx-propose` rather than `/opsx:propose`). This is the same pattern already used by Pi and OpenCode. + +## Goals / Non-Goals + +**Goals:** +- Add a `ToolCommandAdapter` for Oh My Pi producing `.omp/commands/opsx-.md` with `description` frontmatter. +- Inject `**Provided arguments**: $@` after the `**Input**:` heading in command bodies so user-supplied arguments are visible to the agent when a command is invoked with arguments. +- Register the adapter so `init` and `update` can generate command files and skill files for OMP. +- Apply `transformToHyphenCommands` to OMP skill bodies so `/opsx:` references become `/opsx-` for consistency with the command naming convention. +- Add OMP to `AI_TOOLS` so it appears in tool selection and auto-detection. + +**Non-Goals:** +- Changing the file format used by Pi or OpenCode. +- Adding OMP-specific frontmatter fields beyond `description`. +- Auto-detecting OMP presence (the `.omp/` directory is sufficient as `skillsDir`). + +## Decisions + +### Reuse the existing `transformToHyphenCommands` transformer for skill files + +**Decision**: Add `'oh-my-pi'` to the `tool.value` conditional in `init.ts` and `update.ts` that selects the hyphen transformer. + +**Rationale**: Pi and OpenCode follow the same filename-as-command-name convention and are already handled by this branch. OMP has an identical convention. Extending the same conditional is minimal-diff and keeps the pattern consistent. + +**Alternative considered**: Storing the transformer flag on the `AIToolOption` object (e.g., `useHyphenCommands: true`). This is cleaner long-term but is a larger refactor than this change warrants. It can be done separately if more tools adopt this convention. + +### Use `description`-only frontmatter in command files + +**Decision**: The `formatFile` method outputs only a `description` YAML field in frontmatter. + +**Rationale**: OMP's command format uses filename for the slash command name and `description` for display. No additional frontmatter fields (name, category, tags) are needed, matching the minimalist approach used by Pi. + +### Inject `$@` into command bodies (matching Pi) + +**Decision**: Apply the same `injectArgs` logic as Pi's adapter — append `**Provided arguments**: $@` on the line after the `**Input**:` heading, skipping injection if `$@` or `$ARGUMENTS` is already present. + +**Rationale**: OpenSpec command templates contain an `**Input**:` heading that describes what arguments the command accepts (e.g., `**Input**: The argument after /opsx-propose is the change name…`). Without injecting `$@`, a user running `/opsx-propose my-feature` passes `my-feature` as `$@` but the agent never sees it — the argument is silently discarded. OMP's prompt template spec explicitly supports `$@` and positional forms. Pi faces the same problem and already solves it with identical injection logic. + +**Alternative considered**: Leaving injection out and relying on users to add `$@` manually to the template. Rejected: this would silently break argument passing for all OMP commands and diverge from Pi's established behavior. + +### Tool ID is `'oh-my-pi'`, skills directory is `'.omp'` + +**Decision**: `value: 'oh-my-pi'` in `AI_TOOLS`; `skillsDir: '.omp'`. + +**Rationale**: The tool ID uses the full kebab-case name for human clarity. The `.omp/` directory is the short canonical path users will see on disk. The two are independent and follow the precedent set by `kilocode` (ID) → `.kilocode` (dir). + +## Risks / Trade-offs + +- **`.omp/` directory collision**: If a project uses `.omp/` for another purpose, OMP detection will yield a false positive. → Mitigation: This is consistent with how every other tool is detected; no special handling is warranted. +- **Conditional growth in init.ts / update.ts**: Adding a third value to the `tool.value === 'opencode' || tool.value === 'pi'` checks makes the long-term refactor to a per-tool flag more urgent. → Mitigation: Document in tasks; the refactor is low-risk and can follow separately. +- **Adapter missing `escapeYamlValue`**: If a command description contains special YAML characters, the description frontmatter could be malformed. → Mitigation: `escapeYamlValue` is applied in this implementation (task 1.2), consistent with Pi adapter. + +## Open Questions + +None — implementation is well-defined by the existing Pi/OpenCode/OMP pattern. diff --git a/openspec/changes/feat-add-omp-tool-support/proposal.md b/openspec/changes/feat-add-omp-tool-support/proposal.md new file mode 100644 index 0000000000..6bb5c80781 --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/proposal.md @@ -0,0 +1,34 @@ +## Why + +Oh My Pi (OMP) is a terminal AI coding agent whose users expect OpenSpec workflows to be available as slash commands. Without an adapter, users who have OMP configured in their project cannot generate OMP-native command files or get the correct skill transformations from `openspec init` or `openspec update`. + +## What Changes + +- Add a `ToolCommandAdapter` for Oh My Pi that generates command files at `.omp/commands/opsx-.md` with YAML `description` frontmatter, hyphen-based command references, and `$@` argument injection after the `**Input**:` heading (matching Pi's convention so user-supplied arguments are visible to the agent). +- Register `oh-my-pi` in `AI_TOOLS` with `skillsDir: '.omp'` so detection and skill generation work. +- Register the new adapter in `CommandAdapterRegistry` and `adapters/index.ts`. +- Add Oh My Pi to the `transformToHyphenCommands` whitelist in `init.ts` and `update.ts` so skill files use the correct `/opsx-*` invocation form that matches OMP's filename-based command naming. +- Add test coverage for the new adapter. +- Update `docs/supported-tools.md` with the new tool's directory reference. + +## Capabilities + +### New Capabilities + +- `oh-my-pi-tool`: Command and skill generation support for the Oh My Pi (OMP) AI coding agent, following its `.omp/commands/opsx-.md` format with `description` frontmatter, hyphen-based command references, and `$@` argument injection. + +### Modified Capabilities + +- `cli-init`: Oh My Pi is added to the supported tool list and the hyphen-command transformer whitelist. +- `cli-update`: Oh My Pi is added to the hyphen-command transformer whitelist for skill regeneration. + +## Impact + +- `src/core/command-generation/adapters/oh-my-pi.ts` — new adapter +- `src/core/command-generation/adapters/index.ts` — export new adapter +- `src/core/command-generation/registry.ts` — register adapter +- `src/core/config.ts` — add `oh-my-pi` entry to `AI_TOOLS` +- `src/core/init.ts` — extend hyphen-command transformer conditional +- `src/core/update.ts` — extend hyphen-command transformer conditional (two call sites) +- `test/core/command-generation/adapters.test.ts` — adapter unit tests +- `docs/supported-tools.md` — add Oh My Pi row to directory reference table diff --git a/openspec/changes/feat-add-omp-tool-support/specs/cli-init/spec.md b/openspec/changes/feat-add-omp-tool-support/specs/cli-init/spec.md new file mode 100644 index 0000000000..82fe16cac5 --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/specs/cli-init/spec.md @@ -0,0 +1,15 @@ +## ADDED Requirements + +### Requirement: Oh My Pi tool supported in init +The `openspec init` command SHALL support Oh My Pi as a configurable tool, generating both skill files and command files using Oh My Pi's conventions when selected. + +#### Scenario: Selecting Oh My Pi during init +- **WHEN** a user selects Oh My Pi during `openspec init` +- **THEN** skill files are written to `.omp/skills/openspec-/SKILL.md` for each active command +- **AND** command files are written to `.omp/commands/opsx-.md` for each active command +- **AND** skill file bodies use hyphen-based `/opsx-` command references +- **AND** command file bodies have `**Provided arguments**: $@` injected after any `**Input**:` heading + +#### Scenario: Oh My Pi listed when .omp directory is detected +- **WHEN** the project root contains a `.omp/` directory +- **THEN** Oh My Pi is pre-checked in the tool selection during `openspec init` diff --git a/openspec/changes/feat-add-omp-tool-support/specs/cli-update/spec.md b/openspec/changes/feat-add-omp-tool-support/specs/cli-update/spec.md new file mode 100644 index 0000000000..2457ea6b47 --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/specs/cli-update/spec.md @@ -0,0 +1,13 @@ +## ADDED Requirements + +### Requirement: Oh My Pi tool supported in update +The `openspec update` command SHALL refresh Oh My Pi skill files and command files when Oh My Pi is configured, using Oh My Pi's hyphen-based command reference convention. + +#### Scenario: Updating Oh My Pi skill files +- **WHEN** `openspec update` runs and Oh My Pi is a configured tool +- **THEN** skill files in `.omp/skills/openspec-/SKILL.md` are refreshed with the latest templates +- **AND** skill file bodies use hyphen-based `/opsx-` command references + +#### Scenario: Updating Oh My Pi command files +- **WHEN** `openspec update` runs and Oh My Pi is a configured tool +- **THEN** command files are written to `.omp/commands/opsx-.md` for each workflow in the active profile, creating them if they do not yet exist and overwriting them if they do diff --git a/openspec/changes/feat-add-omp-tool-support/specs/oh-my-pi-tool/spec.md b/openspec/changes/feat-add-omp-tool-support/specs/oh-my-pi-tool/spec.md new file mode 100644 index 0000000000..a7050e7a47 --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/specs/oh-my-pi-tool/spec.md @@ -0,0 +1,48 @@ +## ADDED Requirements + +### Requirement: Oh My Pi command file generation +OpenSpec SHALL generate command files for Oh My Pi in `.omp/commands/opsx-.md`, one per active workflow command. + +Each file SHALL include a YAML frontmatter block with a `description` field. The command body SHALL transform `/opsx:` references to `/opsx-` to match Oh My Pi's filename-based slash command naming (e.g., `opsx-propose.md` → `/opsx-propose`). It SHALL inject `**Provided arguments**: $@` on the line immediately following any `**Input**:` heading, unless `$@` or `$ARGUMENTS` is already present in the body. + +#### Scenario: Command file path follows OMP convention +- **WHEN** OpenSpec generates a command file for Oh My Pi for workflow command `propose` +- **THEN** the file is written to `.omp/commands/opsx-propose.md` + +#### Scenario: Command file format includes description frontmatter +- **WHEN** OpenSpec writes a command file for Oh My Pi +- **THEN** the file begins with a YAML frontmatter block containing only a `description` field +- **AND** the body follows the closing `---` + +#### Scenario: Command body uses hyphen-based references +- **WHEN** OpenSpec writes a command file for Oh My Pi whose body contains `/opsx:apply` or similar colon-style references +- **THEN** those references are transformed to `/opsx-apply` in the output file + +#### Scenario: Command body exposes user arguments via $@ +- **WHEN** OpenSpec writes a command file for Oh My Pi whose body contains a `**Input**:` heading and no existing `$@` or `$ARGUMENTS` reference +- **THEN** `**Provided arguments**: $@` is injected on the line immediately after the `**Input**:` heading +- **AND** when the user invokes `/opsx-propose my-feature`, the agent receives `my-feature` as the value of `$@` + +### Requirement: Oh My Pi skill file generation +OpenSpec SHALL generate skill files for Oh My Pi in `.omp/skills/openspec-/SKILL.md`, one per active workflow command. + +Skill file bodies SHALL have `/opsx:` references transformed to `/opsx-` so that skill invocations refer to the correct hyphen-based slash command names. + +#### Scenario: Skill file path follows OMP convention +- **WHEN** OpenSpec generates a skill file for Oh My Pi for workflow command `explore` +- **THEN** the file is written to `.omp/skills/openspec-explore/SKILL.md` + +#### Scenario: Skill body uses hyphen-based references +- **WHEN** OpenSpec writes a skill file for Oh My Pi whose body contains `/opsx:explore` +- **THEN** the reference is transformed to `/opsx-explore` in the output file + +### Requirement: Oh My Pi tool detection +OpenSpec SHALL detect an Oh My Pi installation when the `.omp/` directory exists at the project root, and SHALL present Oh My Pi as a selectable tool in `openspec init` and `openspec update`. + +#### Scenario: Auto-detection when .omp directory exists +- **WHEN** the project root contains a `.omp/` directory +- **THEN** Oh My Pi is listed as a detected tool during `openspec init` and `openspec update` + +#### Scenario: Oh My Pi appears in the tool selection list +- **WHEN** a user runs `openspec init` interactively +- **THEN** Oh My Pi appears as a selectable option in the tool list diff --git a/openspec/changes/feat-add-omp-tool-support/tasks.md b/openspec/changes/feat-add-omp-tool-support/tasks.md new file mode 100644 index 0000000000..ea394c137e --- /dev/null +++ b/openspec/changes/feat-add-omp-tool-support/tasks.md @@ -0,0 +1,30 @@ +## 1. Adapter + +- [x] 1.1 Create `src/core/command-generation/adapters/oh-my-pi.ts` with `ohMyPiAdapter` (toolId `'oh-my-pi'`, path `.omp/commands/opsx-.md`, description-only frontmatter, `transformToHyphenCommands` on body) +- [x] 1.2 Use `escapeYamlValue` for the `description` frontmatter field (consistent with Pi adapter) +- [x] 1.3 Export `ohMyPiAdapter` from `src/core/command-generation/adapters/index.ts` +- [x] 1.4 Import and register `ohMyPiAdapter` in `src/core/command-generation/registry.ts` +- [x] 1.5 In `formatFile`, inject `**Provided arguments**: $@` on the line after the `**Input**:` heading (skip if `$@` or `$ARGUMENTS` already present) — matching Pi adapter's `injectPiArgs` logic + +## 2. Tool Registration + +- [x] 2.1 Add `{ name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' }` to `AI_TOOLS` in `src/core/config.ts` (alphabetical by name, between Mistral Vibe and OpenCode) + +## 3. Skill Transformer Wiring + +- [x] 3.1 In `src/core/init.ts`, extend the skill transformer conditional to include `tool.value === 'oh-my-pi'` alongside `'opencode'` and `'pi'` (one occurrence, in `generateSkillsAndCommands`) +- [x] 3.2 In `src/core/update.ts`, extend the skill transformer conditional to include `tool.value === 'oh-my-pi'` alongside `'opencode'` and `'pi'` (two occurrences: primary update loop and `upgradeLegacyTools`) + +## 4. Tests + +- [x] 4.1 In `test/core/command-generation/adapters.test.ts`, add unit tests for `ohMyPiAdapter`: verify `toolId`, `getFilePath` output uses `path.join('.omp', 'commands', 'opsx-.md')`, and `formatFile` produces correct description frontmatter and transformed body +- [x] 4.2 Verify all path assertions in the new tests use `path.join()` (not hardcoded slashes) for cross-platform correctness + +## 5. Documentation + +- [x] 5.1 Add Oh My Pi row to the tool directory reference table in `docs/supported-tools.md`: `| Oh My Pi (\`oh-my-pi\`) | \`.omp/skills/openspec-*/SKILL.md\` | \`.omp/commands/opsx-.md\` |` + +## 6. Verification + +- [x] 6.1 Run `pnpm test` and confirm all tests pass, including the new adapter tests +- [x] 6.2 Run `pnpm build` to confirm TypeScript compilation succeeds with the new adapter diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 512a0d4de8..89d0fe5201 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 0000000000..0bbc7fb1a8 --- /dev/null +++ b/src/core/command-generation/adapters/oh-my-pi.ts @@ -0,0 +1,55 @@ +/** + * 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'; +import { escapeYamlValue } from '../yaml.js'; + +const OMP_INPUT_HEADING = /^\*\*Input\*\*:[^\n]*$/m; + +function injectOmpArgs(body: string): string { + if (body.includes('$@') || body.includes('$ARGUMENTS')) { + return body; + } + + return body.replace( + OMP_INPUT_HEADING, + (heading) => `${heading}\n**Provided arguments**: $@` + ); +} + +/** + * 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, and + * $@ is injected after **Input**: headings so user-supplied arguments + * (e.g. /opsx-propose my-feature) are visible to the agent. + */ +export const ohMyPiAdapter: ToolCommandAdapter = { + toolId: 'oh-my-pi', + + getFilePath(commandId: string): string { + return path.join('.omp', 'commands', `opsx-${commandId}.md`); + }, + + formatFile(content: CommandContent): string { + const transformedBody = transformToHyphenCommands(content.body); + + return `--- +description: ${escapeYamlValue(content.description)} +--- + +${injectOmpArgs(transformedBody)} +`; + }, +}; diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index bd41245290..c2773ac410 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'; @@ -61,6 +62,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 3be428b26d..55062273d0 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -42,6 +42,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' }, { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, { name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' }, + { 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 fba6d80733..b6ab31ab77 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -567,8 +567,8 @@ export class InitCommand { const skillFile = path.join(skillDir, 'SKILL.md'); // 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; + // Use hyphen-based command references for tools where filename === command name (oh-my-pi, opencode, pi) + 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 e1582cd5b1..eab233c7d5 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -196,8 +196,8 @@ export class UpdateCommand { const skillDir = path.join(skillsDir, dirName); const skillFile = path.join(skillDir, 'SKILL.md'); - // Use hyphen-based command references for OpenCode - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + // Use hyphen-based command references for tools where filename === command name (oh-my-pi, opencode, pi) + 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); } @@ -690,8 +690,8 @@ export class UpdateCommand { const skillDir = path.join(skillsDir, dirName); const skillFile = path.join(skillDir, 'SKILL.md'); - // Use hyphen-based command references for OpenCode - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + // Use hyphen-based command references for tools where filename === command name (oh-my-pi, opencode, pi) + 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/available-tools.test.ts b/test/core/available-tools.test.ts index 50d7580702..13a3fd7cd1 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -157,11 +157,26 @@ describe('available-tools', () => { const tools = getAvailableTools(testDir); const toolValues = tools.map((t) => t.value); expect(toolValues).toContain('vibe'); - + const vibeTool = tools.find((t) => t.value === 'vibe'); expect(vibeTool).toBeDefined(); expect(vibeTool?.name).toBe('Mistral Vibe'); expect(vibeTool?.skillsDir).toBe('.vibe'); }); + + it('should detect Oh My Pi when .omp directory exists', async () => { + // Oh My Pi uses skillsDir: '.omp' without detectionPaths + // This test ensures path semantics do not drift for Oh My Pi skill detection + await fs.mkdir(path.join(testDir, '.omp'), { recursive: true }); + + const tools = getAvailableTools(testDir); + const toolValues = tools.map((t) => t.value); + expect(toolValues).toContain('oh-my-pi'); + + const ohMyPiTool = tools.find((t) => t.value === 'oh-my-pi'); + expect(ohMyPiTool).toBeDefined(); + expect(ohMyPiTool?.name).toBe('Oh My Pi'); + expect(ohMyPiTool?.skillsDir).toBe('.omp'); + }); }); }); diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index ae3a10a776..6255a4fc7b 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'; @@ -656,6 +657,96 @@ describe('command-generation/adapters', () => { }); }); + 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 escape YAML special characters in description', () => { + const contentWithSpecialChars: CommandContent = { + ...sampleContent, + description: 'Fix: regression in "auth" feature', + }; + const output = ohMyPiAdapter.formatFile(contentWithSpecialChars); + expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"'); + }); + + it('should escape newlines in description', () => { + const contentWithNewline: CommandContent = { + ...sampleContent, + description: 'Line 1\nLine 2', + }; + const output = ohMyPiAdapter.formatFile(contentWithNewline); + expect(output).toContain('description: "Line 1\\nLine 2"'); + }); + + it('should inject $@ after **Input**: heading when not already present', () => { + const contentWithInput: CommandContent = { + ...sampleContent, + body: '**Input**: The argument is the change name.\n\nDo the work.', + }; + const output = ohMyPiAdapter.formatFile(contentWithInput); + expect(output).toContain('**Input**: The argument is the change name.\n**Provided arguments**: $@'); + }); + + it('should inject $@ independently of hyphen transform', () => { + const contentWithInput: CommandContent = { + ...sampleContent, + body: '**Input**: The argument is the change name.\n\nRun /opsx:apply.', + }; + const output = ohMyPiAdapter.formatFile(contentWithInput); + expect(output).toContain('**Provided arguments**: $@'); + expect(output).toContain('/opsx-apply'); + }); + + it('should not inject $@ when $@ is already present in the body', () => { + const contentWithArgs: CommandContent = { + ...sampleContent, + body: '**Input**: Accepts arguments.\n\nUser said: $@', + }; + const output = ohMyPiAdapter.formatFile(contentWithArgs); + expect(output.match(/\$@/g)?.length).toBe(1); + }); + + it('should not inject $@ when $ARGUMENTS is already present in the body', () => { + const contentWithArguments: CommandContent = { + ...sampleContent, + body: '**Input**: Accepts arguments.\n\nUser said: $ARGUMENTS', + }; + const output = ohMyPiAdapter.formatFile(contentWithArguments); + expect(output).not.toContain('$@'); + }); + }); + describe('roocodeAdapter', () => { it('should have correct toolId', () => { expect(roocodeAdapter.toolId).toBe('roocode'); @@ -770,7 +861,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, traeAdapter ]; for (const adapter of adapters) {