feat(opencode): sync skills/rules/subagents/MCP into OpenCode config (both scopes) - #306
feat(opencode): sync skills/rules/subagents/MCP into OpenCode config (both scopes)#306daoiqi wants to merge 1 commit into
Conversation
…(both scopes) Add OpenCode as a supported AI coding tool. teamai now syncs all four resource types into OpenCode's native config in both user and project scopes: - Skills: copied to .opencode/skills (project) or ~/.config/opencode/skills (user); OpenCode reads these natively. - Subagents: rendered to .opencode/agents/*.md with OpenCode frontmatter (description + mode:subagent + optional model/tool_extras); name comes from the filename, tools are omitted. - Rules: copied to the rules dir and activated via the opencode.json `instructions` glob (reconciled with key-level surgery); the glob is deactivated when the team's last rule is removed upstream. - MCP: written under the `mcp` top-level key (not mcpServers) with OpenCode's local/remote shapes; stdio + http transports, sse skipped. User scope diverges from project scope (~/.config/opencode/... vs <repo>/.opencode/...), handled by a new `userScope` field on ToolPathsSchema and a scopedToolPaths() choke-point. The install-probe now uses path.dirname so a multi-segment path like .config/opencode/skills resolves to the tool root rather than the near-universal .config. Also fixes a pull orchestration bug: pullAllRules is now always called (even with an empty rule set) so the OpenCode instructions glob is deactivated when the team removes its last rule. Personal rule files are still preserved via the existing tombstone mechanism. Docs (README + usage-guide, both languages) updated with OpenCode.
m0Nst3r873
left a comment
There was a problem hiding this comment.
Thanks for the OpenCode support! A few functional notes below — the first one (empty vs. filtered-empty rule set) is the one worth a closer look. Format/commit-convention items are out of scope for this review.
| // stale local rule files and deactivates the OpenCode instructions glob | ||
| // when the team's last rule is removed. Guarding on items.length > 0 | ||
| // would leak those artifacts on the machine after upstream deletion. | ||
| await rulesHandler.pullAllRules(freshConfig, localConfig, items); |
There was a problem hiding this comment.
pullAllRules is now always called, even with an empty set. But an empty items can mean two different things: (a) the team genuinely has no rules, or (b) the team has rules that were all excluded by tag filtering. Both currently trigger deactivation — clearing the Hermes SOUL managed block (upsertSoulRules('')) and removing the OpenCode glob. Case (b) shouldn't deactivate. Consider distinguishing "no team rules" from "filtered to empty" and only cleaning up in the former.
| if (def.transport === 'stdio') { | ||
| e.type = 'local'; | ||
| // OpenCode folds the executable and its args into one `command` array. | ||
| e.command = [def.command ?? '', ...(def.args ?? [])]; |
There was a problem hiding this comment.
e.command = [def.command ?? '', ...] — when def.command is missing, this silently emits an empty-string command, producing an invalid OpenCode config with no signal. Consider skipping the server (with a recorded change reason) when command is absent for a stdio transport.
| const nonStrings = Array.isArray(data.instructions) | ||
| ? (data.instructions as unknown[]).filter((v) => typeof v !== 'string') | ||
| : []; | ||
| data.instructions = [...next, ...nonStrings]; |
There was a problem hiding this comment.
Preserved non-string instructions entries are re-appended after the string entries ([...next, ...nonStrings]), which reorders the user's original array. Minor, but preserving in-place order would be less surprising for hand-edited configs.
| localConfig: LocalConfig, | ||
| present: boolean, | ||
| ): Promise<void> { | ||
| if (isAgentDisabled(localConfig, 'opencode')) return; |
There was a problem hiding this comment.
When opencode is disabled, activateOpencodeInstructions returns early before removing any glob. A user who previously synced and then disabled opencode will keep a stale glob in their config. Acceptable, but worth a comment noting the residual is intentional.
Summary
Adds OpenCode as a supported AI coding tool.
teamai pullnow syncs all four resource types into OpenCode's native config, in both user and project scopes (part of #303).~/.config/opencode/skills/<repo>/.opencode/skills/instructions: ["rules/*.md"]instructions: [".opencode/rules/*.md"]~/.config/opencode/agents/*.md<repo>/.opencode/agents/*.md~/.config/opencode/opencode.json(mcpkey)<repo>/opencode.json(mcpkey)Details
description+mode: subagent+ optionalmodel/tool_extras); name comes from the filename,toolsomitted.opencode.jsoninstructionsglob using key-level surgery; the glob is deactivated when the team's last rule is removed upstream, while personal rule files on disk are preserved via the existing tombstone mechanism.mcptop-level key (notmcpServers) with OpenCode's local/remote shapes;stdio+httptransports supported,sseskipped.${VAR}secrets resolved to plaintext.~/.config/opencode/...vs<repo>/.opencode/...) handled by a newuserScopefield onToolPathsSchemaand ascopedToolPaths()choke-point. The install-probe now usespath.dirnameso a multi-segment path like.config/opencode/skillsresolves to the tool root rather than the near-universal.config.pullAllRulesis now always called (even with an empty rule set) so the instructions glob is deactivated on the team's last rule removal.Hooks (JS/TS plugin) are intentionally deferred to a follow-up PR.
Test Plan
All executed against a real CLI build (
dist/index.js), not just unit tests:npm run build,npx tsc --noEmit,npx vitest run→ 2091 tests pass (157 files)~/.config/opencode/...;instructions: ["rules/*.md"]<repo>/.opencode/...; MCP in<repo>/opencode.json;instructions: [".opencode/rules/*.md"]description+mode: subagent+model+temperature, noname/toolslocal(stdio → command array + environment) andremote(http → url + headers) shapes;sseskipped;${VAR}resolved to plaintextinstructionskey dropped (glob deactivated), personal rule file preserved on diskinstructions/mcpentries and unrelated top-level keys (e.g.theme) preserved across re-pull (key-level surgery)🤖 Generated with Claude Code