feat: Add OpenCode support#21
Conversation
WalkthroughAdds OpenCode format support across docs, types, index exports, CLI, import/export implementations, and tests. Introduces importOpenCode and exportToOpenCode, extends type unions to include 'opencode', wires OpenCode into importAll/exportAll flows, and adds unit and integration tests for the format. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI
participant Importers as importOpenCode
participant Exporters as exportToOpenCode
participant FS as FileSystem
rect rgba(200,230,255,0.25)
note right of CLI: CLI gains -f opencode handling
User->>CLI: run convert/export with -f opencode
alt Import path
CLI->>Importers: importOpenCode(filePath)
Importers->>FS: read AGENTS.opencode.md / AGENTS.md
Importers-->>CLI: ImportResult(format="opencode", rules, metadata)
else Export path
CLI->>Exporters: exportToOpenCode(rules, outputPath, options)
Exporters->>FS: write AGENTS.opencode.md (headers/sections)
Exporters-->>CLI: success
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
src/exporters.ts (1)
403-423: Code duplication detected.The
exportToOpenCodefunction is identical toexportToClaudeCode(lines 381-401) andexportToCodex(lines 340-360), differing only in name. Consider extracting a shared helper function to reduce duplication and improve maintainability.Example refactor:
+function exportSingleFileWithHeaders( + rules: RuleBlock[], + outputPath: string, + options?: ExportOptions +): void { + const filteredRules = rules.filter(rule => !rule.metadata.private || options?.includePrivate) + const alwaysApplyRules = filteredRules.filter(r => r.metadata.alwaysApply !== false) + const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)) + + const mainContent = alwaysApplyRules + .map(rule => { + const header = rule.metadata.description ? `# ${rule.metadata.description}\n\n` : '' + return header + rule.content + }) + .join('\n\n') + + const fullContent = conditionalSection + ? `${mainContent}\n\n${conditionalSection}` + : mainContent + + ensureDirectoryExists(outputPath) + writeFileSync(outputPath, fullContent, 'utf-8') +} + export function exportToCodex(rules: RuleBlock[], outputPath: string, options?: ExportOptions): void { - // ... existing implementation + exportSingleFileWithHeaders(rules, outputPath, options) } export function exportToClaudeCode(rules: RuleBlock[], outputPath: string, options?: ExportOptions): void { - // ... existing implementation + exportSingleFileWithHeaders(rules, outputPath, options) } export function exportToOpenCode(rules: RuleBlock[], outputPath: string, options?: ExportOptions): void { - // ... existing implementation + exportSingleFileWithHeaders(rules, outputPath, options) }test/opencode.test.ts (2)
105-130: Consider more specific assertion for header absence.The test correctly validates rules without descriptions, but the assertion
expect(exported).not.toContain('#')is brittle—it would fail if the rule content legitimately contained a#character (e.g., in Markdown formatting, comments, or technical content).Consider a more precise assertion:
- // Should not have a header if no description - expect(exported).not.toContain('#') + // Should not have a generated header if no description + expect(exported.split('\n')[0]).not.toMatch(/^#\s/) expect(exported.trim()).toBe('Always use TypeScript')This checks specifically that the first line doesn't start with a Markdown header, allowing
#to appear elsewhere in the content if needed.
132-174: Consider more comprehensive formatting preservation checks.The test validates content preservation but only checks for string presence, not structural integrity. It doesn't verify that Markdown elements (code blocks, lists, blockquotes) remain intact after the roundtrip.
Add assertions to verify formatting is preserved:
expect(exported).toContain('# OpenCode agents and instructions') expect(exported).toContain('Always validate inputs') expect(exported).toContain('Clean code is better than clever code') + // Verify Markdown structures are preserved + expect(exported).toMatch(/```[\s\S]*?```/) // Code blocks + expect(exported).toMatch(/^\d+\.\s/m) // Ordered lists + expect(exported).toMatch(/^>\s/m) // Blockquotes } finally {This ensures formatting elements survive the import/export cycle, not just text content.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
README.md(5 hunks)src/cli.ts(7 hunks)src/exporters.ts(2 hunks)src/importers.ts(2 hunks)src/index.ts(2 hunks)src/types.ts(2 hunks)test/export-functionality.test.ts(2 hunks)test/integration/roundtrip.test.ts(1 hunks)test/opencode.test.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/exporters.ts (1)
src/types.ts (2)
RuleBlock(14-21)ExportOptions(35-41)
test/export-functionality.test.ts (2)
src/exporters.ts (1)
exportToOpenCode(403-423)src/index.ts (1)
exportToOpenCode(37-37)
src/cli.ts (3)
src/exporters.ts (1)
exportToOpenCode(403-423)src/index.ts (2)
exportToOpenCode(37-37)importOpenCode(18-18)src/importers.ts (1)
importOpenCode(661-686)
test/opencode.test.ts (4)
src/importers.ts (1)
importOpenCode(661-686)src/index.ts (3)
importOpenCode(18-18)RuleBlock(48-48)exportToOpenCode(37-37)src/types.ts (1)
RuleBlock(14-21)src/exporters.ts (1)
exportToOpenCode(403-423)
src/importers.ts (2)
src/index.ts (3)
importOpenCode(18-18)ImportResult(49-49)RuleBlock(48-48)src/types.ts (2)
ImportResult(23-28)RuleBlock(14-21)
🔇 Additional comments (17)
src/types.ts (1)
24-24: LGTM!The addition of 'opencode' to the format union types is consistent with the existing pattern and correctly enables type-level support for the new format.
Also applies to: 36-36
test/integration/roundtrip.test.ts (1)
131-131: LGTM!The addition of the 'opencode' format expectation correctly extends the integration test to cover the new format.
src/index.ts (1)
18-18: LGTM!The exports of
importOpenCodeandexportToOpenCodecorrectly extend the public API surface for the new OpenCode format support.Also applies to: 37-37
src/cli.ts (4)
6-6: LGTM!The imports of
importOpenCodeandexportToOpenCodecorrectly integrate the new OpenCode format support into the CLI.
45-45: LGTM!The CLI help text, format lists, and hints have been correctly updated to include OpenCode as a supported format.
Also applies to: 106-106, 218-218, 387-387
197-198: LGTM!The interactive export format menu correctly includes OpenCode with appropriate labeling that shows it uses
AGENTS.md.
424-426: LGTM!The import case for OpenCode correctly handles the conversion flow.
src/importers.ts (2)
157-160: Acknowledge file naming conflict.The comment correctly identifies that both OpenCode and OpenAI Codex use
AGENTS.md, creating ambiguity. However, the current implementation doesn't add an OpenCode import case toimportAll, meaning OpenCode files won't be automatically detected.Additionally, the comment mentions making this configurable, but no configuration mechanism exists yet.
Consider these options:
- Use distinct filenames:
AGENTS.mdfor Codex,AGENTS.opencode.mdfor OpenCode (aligns with exportAll line 583)- Add format detection logic: Analyze file content to distinguish formats
- Make it configurable: Add a user preference for AGENTS.md interpretation
The first option (distinct filenames) is the simplest and most reliable. This would require updating:
- CLI export path for OpenCode (src/cli.ts line 303)
- Documentation (README.md line 29)
- Test format map (test/export-functionality.test.ts line 153)
- Adding import case for
AGENTS.opencode.mdin importAll
661-686: LGTM!The
importOpenCodefunction correctly follows the established pattern for single-file format importers. It properly handles privacy detection and constructs the expected ImportResult structure.README.md (2)
106-106: LGTM!The CLI format flag documentation correctly includes OpenCode in the list of supported formats.
320-320: LGTM!The API reference correctly documents the new
importOpenCodeandexportToOpenCodefunctions, following the established documentation pattern for other format handlers.Also applies to: 337-337
test/opencode.test.ts (6)
1-6: LGTM! Clean test imports.The imports are well-structured and include all necessary dependencies for testing OpenCode format functionality.
9-50: LGTM! Comprehensive import test.The test properly validates import functionality with appropriate assertions for format, metadata, and content preservation. Cleanup is correctly handled.
52-103: LGTM! Well-structured export test.The test properly validates export functionality with multiple rules, verifying header generation, content preservation, and proper rule separation. The setup and assertions align with the expected export behavior.
176-200: LGTM! Proper private rule detection test.The test correctly validates that files matching the private pattern (
.local.md) are imported with theprivatemetadata flag set.
202-241: LGTM! Comprehensive private rule filtering test.The test properly validates both default filtering behavior (excluding private rules) and explicit inclusion via the
includePrivateoption. The assertions correctly verify the expected content in each scenario.
243-278: <!-- [scratchpad]
[task review_file_1/1 review_test_7/7 verify_implementation_1/1]
[observations]
- No standalone
generateConditionalRulesSectionfound; conditional section built in src/exporters.ts.- Line 56 in src/exporters.ts:
sections.push(\When working with files matching \`${scope}\`, also apply:`)`.- Test at line 274 expects
'When working with files matching \src/components/**`'` (prefix only).
[analysis]- Implementation’s string includes the test’s expected substring.
toContaincheck passes as substring before comma matches.
[pending]- None.
[actions]- Conclude that the test and implementation align.
[done]
]Conditional rules section format is correct. The implementation uses “When working with files matching
${scope}, also apply:”, and the test’s substring check covers the expected content.
| case 'opencode': | ||
| exportPath = join(outputDir, 'AGENTS.md') | ||
| if (!isDryRun) exportToOpenCode(rules, exportPath, options) | ||
| exportedPaths.push('AGENTS.md') | ||
| break |
There was a problem hiding this comment.
Critical: File naming inconsistency and format collision.
The CLI exports OpenCode to AGENTS.md (line 303), but:
exportAllexports toAGENTS.opencode.md(src/exporters.ts line 583)- OpenAI Codex format also uses
AGENTS.md(line 283)
This creates two critical issues:
- Inconsistency: Individual exports via CLI produce different filenames than
exportAll - Collision: When exporting both Codex and OpenCode formats, one will overwrite the other
The auto-detect logic (line 379) also can't distinguish between Codex and OpenCode when both use AGENTS.md.
Resolve by choosing a consistent, unique filename. Recommended approach:
case 'opencode':
- exportPath = join(outputDir, 'AGENTS.md')
+ exportPath = join(outputDir, 'AGENTS.opencode.md')
if (!isDryRun) exportToOpenCode(rules, exportPath, options)
- exportedPaths.push('AGENTS.md')
+ exportedPaths.push('AGENTS.opencode.md')
breakThen update the auto-detect logic to handle the new filename:
else if (inputPath.endsWith('AGENTS.md')) format = 'codex'
+else if (inputPath.endsWith('AGENTS.opencode.md')) format = 'opencode'Also update the export formats menu (line 198) and documentation accordingly.
🤖 Prompt for AI Agents
In src/cli.ts around lines 302-306, the CLI currently writes OpenCode exports to
AGENTS.md causing filename inconsistency and collisions with Codex exports and
exportAll; change the OpenCode export filename to a unique, consistent name
(e.g., AGENTS.opencode.md) so it matches exportAll, update the exportedPaths
entry accordingly, then modify the auto-detect logic (around line 379) to detect
the new AGENTS.opencode.md filename separately from AGENTS.md (Codex) and ensure
both formats are distinguished; finally update the export formats menu (around
line 198) and corresponding documentation to list the new filename for OpenCode.
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
README.md(5 hunks)src/exporters.ts(4 hunks)src/importers.ts(2 hunks)test/opencode.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/exporters.ts
- README.md
🧰 Additional context used
🧬 Code graph analysis (2)
src/importers.ts (2)
src/index.ts (3)
importOpenCode(18-18)ImportResult(49-49)RuleBlock(48-48)src/types.ts (2)
ImportResult(23-28)RuleBlock(14-21)
test/opencode.test.ts (3)
src/importers.ts (1)
importOpenCode(671-696)src/types.ts (1)
RuleBlock(14-21)src/exporters.ts (1)
exportToOpenCode(392-394)
🔇 Additional comments (1)
test/opencode.test.ts (1)
1-283: Tests are comprehensive; update file paths after fixing importAll bug.The test suite provides excellent coverage of OpenCode import/export/roundtrip/privacy/conditional scenarios. However, the tests use
AGENTS.mdpaths, which aligns with the current (buggy) implementation but conflicts with the PR objectives.Once you fix the critical bug in
src/importers.tsto use.opencode/rules.mdinstead ofAGENTS.md, update the test file paths accordingly. For example:- const agentsPath = join(tempDir, 'AGENTS.md') + const opencodeDir = join(tempDir, '.opencode') + mkdirSync(opencodeDir, { recursive: true }) + const rulesPath = join(opencodeDir, 'rules.md')Additionally, add an integration test for
importAllto verify OpenCode and Codex imports don't produce duplicates:it('should not create duplicate entries for different formats', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'import-all-test-')) // Create both Codex and OpenCode files writeFileSync(join(tempDir, 'AGENTS.md'), '# Codex rules', 'utf8') const opencodeDir = join(tempDir, '.opencode') mkdirSync(opencodeDir, { recursive: true }) writeFileSync(join(opencodeDir, 'rules.md'), '# OpenCode rules', 'utf8') try { const { results } = await importAll(tempDir) // Should have one entry for each format, not duplicates const codexResults = results.filter(r => r.format === 'codex') const opencodeResults = results.filter(r => r.format === 'opencode') expect(codexResults).toHaveLength(1) expect(opencodeResults).toHaveLength(1) expect(results).toHaveLength(2) } finally { rmSync(tempDir, { recursive: true, force: true }) } })
| // Check for AGENTS.md (OpenCode) | ||
| const opencodeMd = join(repoPath, 'AGENTS.md') | ||
| if (existsSync(opencodeMd)) { | ||
| try { | ||
| results.push(importOpenCode(opencodeMd)) | ||
| } catch (e) { | ||
| errors.push({ file: opencodeMd, error: String(e) }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Critical: Duplicate imports of AGENTS.md causing incorrect results.
Lines 127-135 already import AGENTS.md as format 'codex', and now lines 157-165 import the identical file again as format 'opencode'. When importAll runs, it produces two ImportResult entries for the same file with conflicting formats, breaking downstream consumers that expect one canonical format per file.
According to the PR objectives, OpenCode should use a .opencode/ directory structure with rules.md, not AGENTS.md. This would eliminate the conflict entirely.
Apply this diff to use the correct OpenCode file path:
- // Check for AGENTS.md (OpenCode)
- const opencodeMd = join(repoPath, 'AGENTS.md')
- if (existsSync(opencodeMd)) {
+ // Check for OpenCode rules (.opencode/rules.md)
+ const opencodeRules = join(repoPath, '.opencode', 'rules.md')
+ if (existsSync(opencodeRules)) {
try {
- results.push(importOpenCode(opencodeMd))
+ results.push(importOpenCode(opencodeRules))
} catch (e) {
- errors.push({ file: opencodeMd, error: String(e) })
+ errors.push({ file: opencodeRules, error: String(e) })
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Check for AGENTS.md (OpenCode) | |
| const opencodeMd = join(repoPath, 'AGENTS.md') | |
| if (existsSync(opencodeMd)) { | |
| try { | |
| results.push(importOpenCode(opencodeMd)) | |
| } catch (e) { | |
| errors.push({ file: opencodeMd, error: String(e) }) | |
| } | |
| } | |
| // Check for OpenCode rules (.opencode/rules.md) | |
| const opencodeRules = join(repoPath, '.opencode', 'rules.md') | |
| if (existsSync(opencodeRules)) { | |
| try { | |
| results.push(importOpenCode(opencodeRules)) | |
| } catch (e) { | |
| errors.push({ file: opencodeRules, error: String(e) }) | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/importers.ts around lines 157 to 165, the code incorrectly re-checks
AGENTS.md (causing duplicate imports with different formats); change the check
to target the OpenCode canonical path join(repoPath, '.opencode', 'rules.md')
and call importOpenCode on that path (and push its result) instead of AGENTS.md;
ensure the new path exists check uses existsSync and that errors.push records
that .opencode/rules.md path on failure so the file is only imported once under
the correct OpenCode location and format.
| // Check for AGENTS.md (OpenAI Codex) - Note: This conflicts with OpenCode, | ||
| // so we need to handle this carefully. For now, we'll prioritize OpenAI Codex | ||
| // since it was implemented first, but this could be made configurable. | ||
| // Users can explicitly specify the format using the CLI if needed. | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Remove obsolete comment after fixing the conflict.
This comment acknowledges the AGENTS.md conflict but the code still imports both. Once the critical issue above is resolved by using .opencode/rules.md, remove this comment entirely.
🤖 Prompt for AI Agents
In src/importers.ts around lines 167 to 171, remove the obsolete multi-line
comment about AGENTS.md/OpenAI Codex conflict (the block that begins with "//
Check for AGENTS.md (OpenAI Codex) ...") now that the conflict has been resolved
by using .opencode/rules.md; delete the entire comment block and any stray blank
lines left behind, and verify there are no other references to AGENTS.md in this
file that need updating to .opencode/rules.md.
| export function importOpenCode(filePath: string): ImportResult { | ||
| const content = readFileSync(filePath, 'utf-8') | ||
| const isPrivateFile = isPrivateRule(filePath) | ||
|
|
||
| const metadata: any = { | ||
| id: 'opencode-agents', | ||
| alwaysApply: true, | ||
| description: 'OpenCode agents and instructions' | ||
| } | ||
|
|
||
| if (isPrivateFile) { | ||
| metadata.private = true | ||
| } | ||
|
|
||
| const rules: RuleBlock[] = [{ | ||
| metadata, | ||
| content: content.trim() | ||
| }] | ||
|
|
||
| return { | ||
| format: 'opencode', | ||
| filePath, | ||
| rules, | ||
| raw: content | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Function implementation is correct; integration bug is in importAll.
The importOpenCode function itself follows the established pattern and correctly constructs an ImportResult. The critical issue is in importAll (lines 157-165), which imports AGENTS.md twice—once for Codex and once for OpenCode—creating duplicates.
After fixing the file path in importAll to use .opencode/rules.md, verify that:
- OpenCode rules are imported correctly from the new path
- No duplicate entries appear in
importAllresults - Existing Codex imports remain unaffected
Run this script to confirm no overlap:
🏁 Script executed:
#!/bin/bash
# Description: Verify OpenCode and Codex use distinct file paths
# Search for importOpenCode calls
echo "=== importOpenCode usage ==="
rg -nP 'importOpenCode\(' --type=ts -A2
# Search for importCodex calls
echo -e "\n=== importCodex usage ==="
rg -nP 'importCodex\(' --type=ts -A2
# Verify .opencode directory references
echo -e "\n=== .opencode directory checks ==="
rg -nP '\.opencode' --type=ts -C2Length of output: 1958
🏁 Script executed:
#!/bin/bash
# Extract importAll implementation to verify file paths and potential duplicates
rg -nP 'function importAll\(' -C10 --type=ts src/importers.tsLength of output: 910
🏁 Script executed:
#!/bin/bash
# Extract importAll continuation to inspect file paths for Codex and OpenCode
sed -n '130,260p' src/importers.tsLength of output: 3738
🏁 Script executed:
#!/bin/bash
# Find where agentsMd is defined for importCodex
rg -nP 'const agentsMd' -C3 --type=ts src/importers.ts
# Extract surrounding lines to see context of importCodex's AGENTS.md
sed -n '100,130p' src/importers.tsLength of output: 992
Fix duplicate AGENTS.md imports in importAll
In src/importers.ts’s importAll, both the Codex and OpenCode checks use join(repoPath, 'AGENTS.md'). Change the OpenCode branch to join(repoPath, '.opencode/rules.md') to avoid importing the same file twice.
🤖 Prompt for AI Agents
In src/importers.ts around lines 671 to 696, the OpenCode import branch is
incorrectly using join(repoPath, 'AGENTS.md') which duplicates the Codex import;
change the OpenCode branch to use join(repoPath, '.opencode/rules.md') instead
so OpenCode imports come from .opencode/rules.md. Update any related existence
checks or file reads to reference the new path and ensure the code still guards
against missing files before calling importOpenCode.
|
🎉 This PR is included in version 2.10.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
Added complete OpenCode support to dotagent following established patterns for AI tool integration.
Changes Made
opencodeformat tosrc/types.tsimportOpenCode()function insrc/importers.tsexportToOpenCode()function insrc/exporters.tssrc/cli.tswith OpenCode format detectionsrc/index.tsto export new functionstest/opencode.test.ts(7 tests)Implementation Details
.opencode/directory withrules.mdTest Results
Files Modified
src/types.ts- Added opencode formatsrc/importers.ts- Added importOpenCode() functionsrc/exporters.ts- Added exportToOpenCode() functionsrc/cli.ts- Updated CLI integrationsrc/index.ts- Updated exportsREADME.md- Added to supported formatstest/opencode.test.ts- New test suitetest/export-functionality.test.ts- Added opencode teststest/integration/roundtrip.test.ts- Added opencode roundtrip testsSummary by CodeRabbit
New Features
Documentation
Tests