Skip to content

feat(opencode): support jsonc configs with comment preservation#41

Merged
RealZST merged 3 commits intomainfrom
feat/opencode-jsonc-support
May 7, 2026
Merged

feat(opencode): support jsonc configs with comment preservation#41
RealZST merged 3 commits intomainfrom
feat/opencode-jsonc-support

Conversation

@RealZST
Copy link
Copy Markdown
Owner

@RealZST RealZST commented May 7, 2026

Summary

  • HK silently emptied OpenCode users' MCP list whenever their opencode.json[c] had a // comment or trailing comma β€” serde_json::from_str rejects what OpenCode itself happily parses (its loader runs both extensions through jsonc-parser). Now OpencodeAdapter::parse_json matches upstream, and mcp_config_path[_for] smart-picks an existing .jsonc over .json (matches OpenCode's load-order precedence).
  • Disable / Enable / Delete / cross-agent install of an OpenCode MCP entry no longer destroys user comments and formatting. Each McpFormat::Opencode dispatch routes to a dedicated function backed by a new locked_modify_jsonc helper that round-trips the file through jsonc-parser's CST API; everything outside the touched entry stays verbatim.
  • Other JSON-format agents (Claude .mcp.json, Cursor, Antigravity, Copilot, Gemini) keep their strict serde_json path β€” accidentally accepting jsonc syntax in their configs would surface entries the agent itself can't load.

Design notes

  • Format-driven dispatch, not filename sniffing: McpFormat::Opencode goes through *_opencode functions, regardless of .json vs .jsonc extension. json_top_key's Opencode arm is hardened to unreachable!() so any future regression that re-routes through the generic JSON path fails loudly.
  • Scanner gains mcp_config_path_for(&scope) instead of joining project_mcp_config_relpath() directly. Default trait impl is no-op for adapters with a single canonical filename β€” only OpenCode overrides to probe disk state, so jsonc-only projects are now discoverable.
  • A comment line directly above a removed entry is left in place by design β€” HK never edits user comment text, only its own data entries. The orphan is the user's content to keep or clean up.
  • New dep: jsonc-parser 0.32 with serde + serde_json + cst features. Same library OpenCode uses (TS twin); also Deno's pinned dep. Active, 1.5M dl/90d.

Test plan

  • cargo test -p hk-core --lib β€” 376 passed (12 new)
  • cargo clippy -p hk-core --lib --tests -- -D warnings clean
  • cargo build --workspace β€” hk-core / hk-web / hk-desktop / hk-cli all pass
  • Manual UI smoke test on /private/tmp/hk-opencode-test/opencode.jsonc (with // comments + trailing comma):
    • HK reads all 3 MCP entries (was 0 before fix)
    • Disable preserves all comments and sibling entries
    • Enable preserves all comments and sibling entries
    • Delete preserves all comments and sibling entries
  • Cross-agent deploy into OpenCode at project scope: not exercised manually (frontend projectScopeBlocked gate); covered by unit test test_deploy_mcp_server_opencode_preserves_comments and shares CST code path with restore/enable

Out of scope (explicit)

  • Read-merge of co-existing .json and .jsonc (HK picks .jsonc when both present, matching OpenCode's load-order winner; merging both into a single read is roadmap'd as a P2 follow-up).
  • Comment-attached-to-entry tracking on remove: HK leaves orphans intact; programmatic distinction between "section header" and "entry note" comments is not attempted.

πŸ€– Generated with Claude Code

RealZST and others added 3 commits May 7, 2026 23:40
OpenCode's loader runs every config file through jsonc-parser regardless
of extension (packages/opencode/src/config/config.ts uses
ConfigParse.jsonc for both opencode.json and opencode.jsonc), so a single
`//` comment or trailing comma in the user's opencode.json silently
emptied HK's MCP list under serde_json::from_str. Match upstream's
permissive parsing in `OpencodeAdapter::parse_json`.

Path resolution also gains a `.jsonc` preference: `mcp_config_path` and
the new `mcp_config_path_for` override return an existing `.jsonc` over
`.json` (matching OpenCode's load-order precedence β€” `.jsonc` wins on
conflict). Falls back to `.json` when neither exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch project-scope MCP discovery from a manual
`project_mcp_config_relpath().join()` to the trait's scope-aware
`mcp_config_path_for(&scope)` accessor. The default trait impl is
behaviorally equivalent for adapters that use a single canonical
filename (Claude `.mcp.json`, Cursor `.cursor/mcp.json`, etc.), so this
is a no-op for them. It gives OpenCode the override hook needed to
prefer an existing opencode.jsonc over opencode.json β€” without this
change, `is_file()` on the hardcoded `.json` path would silently miss
jsonc-only projects.

Also collapses the now-redundant nested `if` to satisfy clippy's
collapsible_if lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Disable / Enable / Delete / cross-agent install of an OpenCode MCP
entry now flow through jsonc-parser's CST API instead of
serde_json::Value round-trip. The CST holds comments, whitespace, and
trailing commas as first-class nodes, so `to_string()` after a targeted
mutation regenerates the file with everything outside the touched entry
kept verbatim.

Architecture: each McpFormat::Opencode dispatch routes to a dedicated
function (`deploy_mcp_server_opencode`, `remove_mcp_server_opencode`,
`restore_mcp_server_opencode`, `read_mcp_server_config_opencode`) sharing
a new `locked_modify_jsonc` helper that mirrors `locked_modify_json`'s
advisory-lock-and-rewrite semantics. Other JSON-format agents (Claude,
Cursor, Antigravity, Copilot, Gemini) keep the strict serde_json path
unchanged β€” accidentally accepting jsonc syntax in their configs would
surface entries the agent itself can't load.

By design, a comment line directly above a removed entry is left in
place β€” HK never edits user comment text, only its own data entries.
The orphan comment is the user's content to keep or clean up.

`json_top_key`'s Opencode arm is hardened to `unreachable!()` (matching
Toml's pattern) so any future regression that re-routes Opencode through
the generic JSON path fails loudly instead of silently producing strict
JSON output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@RealZST RealZST merged commit 87b5703 into main May 7, 2026
3 checks passed
@RealZST RealZST deleted the feat/opencode-jsonc-support branch May 7, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant