Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(opencode): support OpenCode 2.x via native tools and commands - #1213

Open
SHJordan wants to merge 3 commits into
alibaba:mainfrom
SHJordan:fix/opencode-2x-tools
Open

fix(opencode): support OpenCode 2.x via native tools and commands#1213
SHJordan wants to merge 3 commits into
alibaba:mainfrom
SHJordan:fix/opencode-2x-tools

Conversation

@SHJordan

@SHJordan SHJordan commented Sep 10, 2026

Copy link
Copy Markdown

The opencode/ README (curl open-code-review.ts into plugins/)
is broken in two independent ways:

  1. Missing dependency. The .ts file imports
    @opencode-ai/plugin, but nothing tells users to add that dependency
    to their config directory. The server log shows:
    failed to load plugin ... Cause([Die(ResolveMessage: Cannot find package '@opencode-ai/plugin' ...)]) (ref err_ae92f004).

  2. V1-only API. The file exports a V1 plugin function, but OpenCode
    2.x requires a default-exported { id, effect | setup } definition.
    Server log: Plugin must export a default definition with an id and an effect or setup function (refs err_01716a41, err_daa504f5).

Changes

  • open-code-review.ts: dual entrypoint per
    https://opencode.ai/v2/docs/build/plugins#support-v1
    export default { id, setup, server }.
    V2 setup registers ocr_review/ocr_health via
    ctx.tool.transform and /ocr-review//ocr-health via
    ctx.command.transform, reusing the exact V1 OCR logic
    (arg validation, ocr review --audience agent --format json,
    30-min overall timeout, 10 MiB output cap, process-group cleanup).
    The V2 API is imported as types only, so OpenCode 1.x never needs
    the @opencode/plugin package at runtime (verified in the built
    output). V2 tool cwd resolves from the session location with
    fallback to the plugin location; V2 has no abort signal, so
    cancellation there relies on the overall timeout (documented).
    V2 numeric inputs require positive integers like V1, and V2
    commands skip user-defined names like the V1 ??= guards.
  • package.json (+ tracked package-lock.json, ~300 locked packages):
    add @opencode/plugin beta devDependency for typechecking the V2
    code; lockfile committed so npm ci works.
  • test/open-code-review.test.mjs: default-export shape test plus a
    V2 stub-context harness (+6 tests).
  • README.md: single download block with per-version dependency
    instructions, corrected 30-minute tool timeout.

Verification (OpenCode v0.0.0-beta-19425 + 1.18.30, ocr v1.11.8)

  • npm run check: typecheck + 29/29 tests pass; clean-room
    npm ci + check also green.
  • Live load on opencode2: dual file loads with no failed to load plugin entries; V2 harness registers both tools/commands and
    ocr_review {preview: true} returned live ocr output through
    session-derived cwd.
  • Live load on opencode 1.18.30 sandbox: plugin loads with no errors,
    exactly one init across sessions (no double registration from
    default.server + named export), both commands registered once.
  • ocr llm test passes, so the health flow has working credentials
    behind it (full ocr_health execute not run here to avoid LLM spend).

Suggested test plan for maintainers

  • 1.x: fresh ~/.config/opencode, follow README 1.x steps, check
    ocr_review/ocr_health appear and /ocr-health succeeds.
  • 2.x: fresh config, follow README 2.x steps, same checks.

AI/LLM disclosure (per AGENTS.md)

This PR was prepared with AI assistance (OpenCode coding agent).
Every line was reviewed by the author, and all behavior claims above
were functionally verified as described (live loads on both versions,
29/29 tests, real ocr runs for preview paths). No AI attribution
trailers in commits.

@CLAassistant

CLAassistant commented Sep 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 4 issue(s) in this PR.

  • ✅ Successfully posted inline: 4 comment(s)

Comment thread plugins/open-code-review/opencode/tools/ocr_health.ts Outdated
Comment thread plugins/open-code-review/opencode/tools/ocr_health.ts Outdated
Comment thread plugins/open-code-review/opencode/tools/ocr_review.ts Outdated
Comment thread plugins/open-code-review/opencode/tools/ocr_review.ts Outdated
The single-file plugin (open-code-review.ts) cannot load on OpenCode 2.x:
the 2.x loader requires a default-exported { id, effect | setup } and has
no custom-tool registration API. It also fails on 1.x for most users
because nothing installs the @opencode-ai/plugin dependency the file
imports.

Ship the same ocr_review / ocr_health features as 2.x-native custom
tools plus /ocr-review /ocr-health commands, and document the
@opencode-ai/plugin dependency step for both versions.
@SHJordan
SHJordan force-pushed the fix/opencode-2x-tools branch from 34330a3 to fe7ccb1 Compare September 10, 2026 20:35
@SHJordan

Copy link
Copy Markdown
Author

Update: addressed the review findings.

Fixed — missing output cap in ocr_health.ts (bug/medium + part of duplication comment). The health tool's runOcr did not enforce maxOutputBytes. Added the same appendChunk guard (10 MiB default) and RunOptions.maxOutputBytes as ocr_review.ts, so all three copies now enforce the limit.

Not applied — clearTimeout(forceKillTimer) in finish() (bug/medium). I tried the suggestion and the repo's own test ocr_review force-kills a child that ignores cancellation fails with it (Process did not exit within 5000ms, reverted, suite green 22/22 again). Reason: on the abort/timeout/output-limit paths the order is terminateChild() (arms the SIGKILL in 3s) then finish() — clearing the timer in finish() disarms the force-kill that was just armed, so a SIGTERM-ignoring child is never reaped. The existing code is correct: close clears the timer once the process exits, and killProcessGroup is guarded by closed, so a fired timer after normal completion is a harmless no-op, not a leak.

Duplication (maintainability/medium x2) — kept self-contained deliberately. Each file under tools/ has its exports registered as individual tools by OpenCode (<filename>_<export>), so a shared helper module would itself be picked up as a (broken) tool definition. Self-contained files are the documented pattern (see https://opencode.ai/docs/custom-tools/); the three copies are byte-identical in the shared sections by construction.

Also fixed the commit author email so the commits link to @SHJordan for the CLA check.

Replace the tools/+commands/ split with the documented dual plugin form:
default-export { ...Plugin.define({ id, setup }), server }. V2 registers
ocr_review/ocr_health via ctx.tool.transform and /ocr-review//ocr-health
via ctx.command.transform, reusing the same OCR logic as V1. Add
@opencode/plugin beta devDependency and a test for the default export.
@SHJordan

Copy link
Copy Markdown
Author

Revision pushed: replaced the separate tools/+commands/ approach with the documented dual V1+V2 entrypoint in the single plugin file (export default { ...Plugin.define({ id, setup }), server }, per https://opencode.ai/v2/docs/build/plugins#support-v1). V2 tools/commands are registered from setup() reusing the exact V1 OCR logic (including the 10 MiB output cap from the earlier review finding), so that finding now holds by construction and the self-containment concern is moot. Verified: npm run check 23/23 green, dual file loads cleanly on opencode2 beta-19425 (no failed to load plugin), and the V2 tool/command path was exercised end-to-end against a scratch repo. Correction to my earlier comment: plugin-registered tools ARE possible on V2 via ctx.tool.transform; one known V2 limitation is documented in the README (no abort signal, timeouts only).

@lizhengfeng101

Copy link
Copy Markdown
Contributor

Approach looks right — I checked the V2 docs' "Support V1" section, and V2 reading id/setup() while ignoring server() means no double registration on 2.x. Pulled the branch: typecheck clean, 23/23 pass. Before merge:

Blocking

  1. package-lock.json wasn't regenerated, so npm ci fails: Missing: @opencode/[email protected] from lock file. Please commit the lockfile — and note in the description that this dep pulls in ~296 packages.

  2. The V2 schema drops V1's .int().positive() — it's just { type: "number" }. Reproducible with a stub context:

overallTimeoutMinutes=0  -> OpenCodeReview timed out after 0 seconds.
overallTimeoutMinutes=-1 -> OpenCodeReview timed out after -60 seconds.

concurrency: -4 and timeoutMinutes: 2.5 reach the CLI unchecked too. { "type": "integer", "minimum": 1 } fixes it.

  1. Please add the AI/LLM disclosure AGENTS.md requires, including tools and models.

Not keen to defer

  1. 1.x isn't verified live, and it's the version that works today. The object form only landed in OpenCode 1.18.29 (repo pins 1.18.5) and V1 loads function exports — so on >=1.18.29 it's unclear whether both default.server and the named export register the tools twice. That's what the assertion you relaxed was guarding. Please test on 1.x and record the result.

  2. ~145 lines of V2 logic sit behind one shape assertion. The stub harness you already wrote would cover point 2 — mine was ~20 lines, so it should be cheap to move into test/.

Smaller notes (dropped period in the V2 command template, redundant as string casts, duplicated README sentence, stale "15-minute" line) I'll post separately.

@lizhengfeng101

Copy link
Copy Markdown
Contributor

Smaller notes, none blocking.

  • V2 command template lost a sentence break. V1 renders context: $ARGUMENTS. If no target...; the split into TEMPLATE + SUFFIX drops the period, and an empty prompt yields a double space:

    ...business context: review my staged changes If no target is specified, ...
    ...business context:  If no target is specified, ...
    

    Start OCR_REVIEW_COMMAND_SUFFIX with ". If no target".

  • as string casts are unnecessary. session.location.directory is already string & Brand<"AbsolutePath"> and session.subpath is already RelativePath | undefined. I removed both casts and typecheck still passes. Keeping them would swallow the error if directory ever becomes optional upstream.

  • resolveSessionCwd has no fallback. V1 had context.worktree || context.directory || worktree; V2 is a single point of failure on ctx.session.get(). Plugin.Context exposes location.directory, so a fallback is cheap.

  • V2 command registration lost V1's ??= guard. V1 preserved a user's own ocr-review command and has a test for it; editor.add is unconditional. Worth confirming what V2 does on a name collision.

  • README: duplicated sentence. "Commit the plugin file if the integration should be shared with the project." appears twice in "Install for one project", the second time glued to the preceding paragraph. Also "copy the same plugin file ... instead" reads as if 2.x needs a different path, but the curl block above already does exactly that — only the deps differ. Same for the globally-install sections: two identical mkdir + curl blocks with the same raw URL will drift.

  • README: the 15-minute timeout is stale. Both versions pass 30 minutes explicitly; runOcr's internal 15-minute default never applies to ocr_review. It's in the bullet list you already touched.

  • README: as above points at the wrong path. The project-scoped step says to add the dep to .opencode/package.json "as above", but above is cd ~/.config/opencode && npm install. The V1 docs' approach is a package.json with a dependencies block, which OpenCode installs at startup — showing that snippet would fix both.

  • Version pinning is inconsistent. package.json pins 0.0.0-beta-19425; the README says @opencode/plugin@beta. The tag moves, so users and CI will drift apart.

  • Test name no longer matches. "module exposes only one OpenCode plugin entry point" now asserts two exports.

…ests

- Import @opencode/plugin as types only and export a plain dual object,
  so OpenCode 1.x never needs the V2 beta package at runtime (verified
  in the built output: only node:*, @opencode-ai/plugin imports remain).
- V2 numeric inputs now require positive integers
  ({ type: integer, minimum: 1 }), matching the V1 zod schema.
- V2 commands keep the V1 sentence break, skip user-defined names like
  the V1 ??= guards, and resolveSessionCwd falls back to the plugin
  location when the session lookup fails.
- README: single download block, corrected 30-minute tool timeout,
  deduped project section.
- Track package-lock.json (drop the local ignore) so npm ci works.
- Move the V2 stub harness into the test suite (+6 tests, 29 passing).
- Verified live on OpenCode 1.18.30 sandbox: plugin loads with no
  errors, single init across sessions, both commands registered once.
@SHJordan

Copy link
Copy Markdown
Author

@Qiyuanqiii @lizhengfeng101 revision pushed addressing everything — walkthrough:

Qiyuanqiii (blocking): V1 hard dependency on the V2 package — fixed structurally, not just docs. The V2 API is now import type only and the default export is a plain object literal (Plugin.define is identity, verified in the published package). Built output imports only node:* + @opencode-ai/plugin, so 1.x needs just the one dep; README reflects exactly that.

Numeric schemas (both): fixed + tested. V2 numerics are now { type: integer, minimum: 1 }; new tests assert the shape for all five fields (the 0/-1 cases from the report can no longer validate).

Attachments in V2 commands: kept text-only deliberately, matching the V1 $ARGUMENTS templates; spreading attachments would break exactOptionalPropertyTypes and risk stale mention offsets after the rewrite. Noted in code.

15-min README line: fixed to the effective 30-minute tool default.

Lockfile: committed (package-lock.json, ~300 packages) with the dir .gitignore updated; clean-room npm ci + check green. Tradeoff noted for maintainers.

AGENTS.md disclosure: added to the PR body (tools used included).

1.x verified live (1.18.30 sandbox): plugin loads clean, exactly one init across sessions, both commands registered once — so no double registration from default.server + named export. @Qiyuanqiii please re-review when you get a chance.

Harness in test/: done (+6 V2 tests, 29/29 green), covering registration, schema shape, command-collision skip, session-cwd resolution + fallback, and template rendering.

Small notes, all applied: sentence break + no double space on empty prompt (tested), as string casts removed, resolveSessionCwd falls back to ctx.location.directory, V2 commands skip existing names via ctx.command.list() (restores the ??= semantics), README deduped to a single download block, misleading "instead" wording gone.

@SHJordan
SHJordan requested a review from Qiyuanqiii September 11, 2026 14:37
@lizhengfeng101

lizhengfeng101 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

@SHJordan Nice fix. One thing worth confirming: @opencode/plugin is only pulled in via import type, which Bun strips at load time — so it shouldn't be needed at runtime on 2.x. Did you verify the plugin loads on 2.x without it installed? If it's type-only, the README's "2.x needs both packages" step is unnecessary and we can drop it (keeping @opencode-ai/plugin, which reviewArgs does need at module load).

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.

3 participants