DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Ocr Review — DSH Plugin for DeepSeek Harness
← Plugins
O

@jayyuen66/dsh-ocr-review

Ocr Review

dsh workspace code review plugin: wraps open-code-review (ocr CLI) as 5 model tools + settings card (selects provider/model from the host settings service's provider list and applies them to the OCR configuration).

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:JayYuen666/dsh-ocr-review#c2d11a124d6cf3108cd41e0808cedfa2952a3b47
READMECompatibilityVersions

Compatibility and provenance

Ocr Review is published as @jayyuen66/dsh-ocr-review and currently resolves to version 0.1.5. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
10/15/2026

Versions

0.1.5stable
10/15/2026
0.1.4stable
10/12/2026
0.1.3stable
10/2/2026
Show 4 more versionsCollapse versions
0.1.5-rcprerelease
10/15/2026
0.1.4-rc.1prerelease
10/11/2026
0.1.4-rcprerelease
10/9/2026
0.1.2stable
10/1/2026

Related plugins

Loading related plugins…

Latest
0.1.5
DSH
*
HMR
Process restart
Tree shaking
Declares side effects by file
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
10/15/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in

Related plugins

More verified plugins in developer-tools.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Sdk Minimal@deepseek-ai/dsh-sdk-minimalThe standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessionsSdk App@deepseek-ai/dsh-sdk-appThe dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-baseExperimental Inspector@deepseek-ai/dsh-experimental-inspectorExperimental cross-realm CDP hub for Host debugging and Client Runtime inspection

README

@jayyuen66/dsh-ocr-review

English · 中文

What it does

  • Wraps the external ocr (open-code-review) CLI into 5 model tools plus 1 web settings card for dsh; the host validates tool arguments not at all, so every shape and type gate lives in this package.
  • The host half (host.ts) declares 6 .volatile() settings fields — the namespace is implicit: the host projects the form from the profile entry id ocr-review and the plugin calls no settings.register.
  • It also registers 5 tools, one systemPrompt routing section (ocr-review-routing, order 1555) and 5 /_dsh/ocr-review/* endpoints.
  • The client half (src/client-entry.ts → client.js) is the settings card, taking provider/model from the llm-pi-ai namespace of the settings service, plus three read-only contributions about the external CLI: a badge and a section on the plugin's detail page and the guidance shown right after the bundle is enabled.
  • It writes provider/model into the external ocr CLI's own ~/.opencodereview/config.json — as an api_key_cmd command, never as a plaintext key.
  • Review results come back as a structured summary whose fields split into two notions: what OCR found (status, aggregation, topComments, warnings) and whether the run actually covered the selected files (coverage, budgetExceeded, toolCalls). Under --audience agent the CLI drops its progress lines to io.Discard, so the second notion is only reachable through those fields.

Prerequisites (external CLI)

  • This package imports no OCR library, it only spawns processes: ocr review / ocr scan / ocr delegate preview / ocr delegate rule / ocr session / ocr llm test.

  • Commands are built against the contract verified line-by-line on the 1.13.0 source (header comments of lib/cli.ts and lib/parse.ts). That version is declared once, in ocrCliVersion in package.json, and mirrored by EXPECTED_OCR_CLI_VERSION in lib/cli.ts; test/publish-manifest.test.ts asserts the two are the same string and test/readme-parity.test.ts asserts this paragraph says it too, so "the README tells you to install A while the code verifies against B" cannot be self-consistent on both sides.

  • Multi-value --exclude/--path are merged into one comma-separated flag, and --format only accepts json/text/sarif.

  • You install the CLI yourself. This package deliberately ships no optionalDependencies: pulling @alibaba-group/open-code-review in as a dependency changed the user's global toolchain as a side effect of installing a plugin, and — worse — its absolute path resolved ahead of whatever the user had on PATH, so upgrading the CLI in the terminal never reached the plugin and the version on the card never matched the one they were running.

  • Command resolution order, therefore: the first ocr executable found on PATH (its absolute path becomes the command word), then a copy somebody placed in this package's own dependency tree as a fallback, and only if neither exists the bare ocr (which is what the tools refuse to spawn, see below). The card, the tool preflight and the actual command word all read this one judgement, so "which copy is used" cannot be told three different ways. On Windows the bundled fallback is skipped outright (bin/ocr.js is a shebang script that pwsh/cmd cannot execute) and the command word stays the bare ocr, because an absolute path with backslashes is not reliably quotable in cmd.

  • Install it with either command; the npm one carries the verified version, the brew one cannot:

    brew install open-code-review                                   # formula tracks the newest release
    npm i -g @alibaba-group/open-code-review@1.13.0                 # pinned to the verified version
    

    homebrew/core carries a single stable formula with no versioned variant (open-code-review@x.y does not exist), so brew cannot pin: what you get is whatever is newest. When that differs from the version above, the tools say so (see "External CLI status and version"). ocrInstallHint lets a deployment lead with one of the two when nothing is installed yet; a CLI that is already installed is always described by its own source.

  • With neither path available the tools refuse before spawning and give the install commands above with the version filled in; if a candidate existed when the verdict was taken but the child still cannot exec it, the shell answers 127 and the tool reports "ocr command not found" pointing at the card's External CLI row.

  • Platforms: macOS and Linux are the targets. Process reaping (the reaper guard around ocr_review / ocr_scan) depends on a POSIX toolchain - bash traps and job control plus the process-group semantics of ps / pgrep / kill - and the HOME= assignment prefix behind a custom ocrConfigPath is POSIX-only too. None of that exists on Windows: the package runs the bare command there (guarded by process.platform), skips the bundled launcher (a bare ocr on PATH is required), and writes the api_key_cmd with double-quote escaping because ocr runs credential commands through cmd.exe on Windows. The cost is that a hard host exit can leave an orphaned OCR process on Windows.

  • The provider list and the key status come from the official host channels ctx.settings.describe() (the value.providers of the llm-pi-ai entry) and ctx.credentials.

  • When either is missing the card shows the degradation reason while tools and settings keep working.

  • The host must be >= 0.2.1-alpha.3. That floor sits in engines.dsh, which is documentation only — no loader or installer reads it. What actually gates installation is the exact spelling of the four peer pins: the host compares every peer range against its own runtime version (prereleases participate) and refuses with incompatible-version when one does not match. On a machine still running dsh 0.2.1-alpha.2, upgrade dsh first, or record an exemption for this package in the profile's compatibility.json (dsh plugin --profile <profile> allow-version <name@version> --dsh-version <runtime version> --accept-risk). This bundle imports no symbol from the @deepseek-ai/dsh package itself (every service face comes from the @deepseek-ai/dsh-* sub-packages), so it is not declared as a dependency of any kind.

External CLI status and version

One judgement, resolveOcrAvailability in lib/cli.ts, answers "is there an ocr here, which copy, and how was it installed": the first executable found on PATH (with PATHEXT extensions on Windows), otherwise a copy inside this package's own dependency tree. "How it was installed" is read off the file the symlink in bin actually points at (realpathSync): anything under a node_modules is an npm-style global install, anything under Cellar/Homebrew is a formula, and neither of those is reported as unrecognised rather than guessed — the brew-vs-npm order matters because a formula-installed Node puts its own global npm packages under the same homebrew prefix. It is pure filesystem — statSync(...).isFile(), never a spawn — and it answers candidate, not runnable: a file that exists but cannot execute (no executable bit, or the bundled entry present while its platform binary is missing) passes the probe and is reported by the real spawn instead, which is why the "command not found" wording is kept as a separate fallback.

SurfaceWhat it does with that answer
The five toolsThe probe runs before anything spawns. With no candidate on either path the model gets "neither path found, install it like this" — with the verified version inside the command — instead of an exit-127 reverse-derivation. Re-read per call, never cached per process: installing the CLI takes effect on the next call without restarting dsh
Settings cardA status line outside the provider panel (whether this machine has ocr and whether the host can list providers are two different facts, and a machine missing the CLI must still see the reason): which copy, which install method, which version this plugin was verified against, which version was last measured, plus the install commands when nothing is there. One Detect version button
Detail pageA badge beside the title and a section under the page's own content, both rendering null for a subject that is not this package and while the read has not come back. The badge switches to "version differs" once a measurement disagrees with the verified version
After enablingThe activation guidance slot says the install recipe, version and all, only when the answer is none — the outermost position a third-party bundle may render in
  • The plugin list card's tag row is not plugin-extensible: those tags are the host's own verdicts on the entry (running / off / problem). The two places above are as far out as a bundle can reach, and they are client-half surfaces, so a disabled plugin cannot warn that its CLI is missing — disabling it removes the thing that would have said so.
  • All three client surfaces read the same ocrBinary field of GET providers, and none of them computes availability, the install channel or the install command locally: three separate judgements is how a status display drifts, and a command written out in two places is how the card ends up teaching a version the plugin does not verify against.
  • Version is not free: reading it spawns ocr version. It runs when you press Detect version, and once at apply when startupVerify is on (the same switch that governs the apply-time ocr llm test, because both are "may the plugin start a process at load"). A page render never spawns: GET providers only reports the version measured so far, or null when nothing has been measured yet — the card says "verified against 1.13.0" and stays quiet about your machine until a measurement exists.
  • A version disagreement is shown and said: the receipt line names both numbers (measured vs verified), the badge flips, and ocr_review / ocr_scan append a note to the summary's hint so the model is told this result did not come from the verified version. It is never presented as a hard failure — a newer CLI usually works — and this package runs no install or upgrade itself: the command it shows depends on which install method that copy came from (brew upgrade open-code-review for a formula, npm i -g @alibaba-group/open-code-review@<verified> for a global package), with the brew caveat spelled out because a formula cannot pin.
  • The receipt shows the CLI's own output underneath the version line, with terminal control sequences stripped (upstream's launcher hard-codes the colour codes around its "new version available" nudge, and that nudge is printed by the JS wrapper, so --color never cannot reach it) — the line you are meant to select and copy is npm i -g …, and copying it must not drag escape bytes into your terminal. One completed round trip re-reads the payload, so the status line and the version you just got describe the same moment (install the CLI while the page is open and the row no longer contradicts the receipt). This package deliberately provides no update action and no clipboard API (a non-secure context would fail silently, which looks like the button doing nothing).

Installation

dsh plugin --profile web add @jayyuen66/dsh-ocr-review
  • Upgrading an installed copy is remove-then-add. While the profile runs HMR (the shipped profiles do), add on a package the profile already depends on refuses before pnpm runs - application: 'failed' with error.code: 'already-installed', no profile file touched - and so does the Web page's install box. Run dsh plugin --profile web remove @jayyuen66/dsh-ocr-review first, then add the target version; the removal stops the old bundle's plugins and drops its module mappings, so the new version starts without a restart. Without HMR the same add runs pnpm and reports restart-required.
  • The packages are on the public npm registry, so installation needs no credentials.
  • The published files are only host.js, client.js, cordis.patch.yml, scripts and THIRD_PARTY_NOTICES.md (third-party licenses of the bundled runtime dependencies; prepack rebuilds both bundles), source repository: see repository.url in package.json.
  • No @deepseek-ai/* goes in dependencies, and that is not a style preference. Consumers of this package are dsh plugins installed into a profile (nodeLinker: hoisted, autoInstallPeers: false), so any host package listed under dependencies is written into that profile as a real directory, coexisting with the copy the host process already loaded. @deepseek-ai/dsh-tools keys its TOOL_RUNTIME_SCHEDULER with a plain Symbol (not Symbol.for), so symbol identity is per module instance: the host's dsh-agent-loop reads a ToolRuntime mounted from the duplicate using its own copy's symbol, gets undefined, and every native tool call throws Cannot read properties of undefined (reading 'prepare') — the whole profile loses its tools. This package's build, typecheck and tests all stay green right up to that point, so test/publish-manifest.test.ts pins the invariant in the manifest instead.
  • The four host packages this bundle imports as values are required (non-optional) peerDependencies: @deepseek-ai/schemastery (the host's fork of schemastery — only it implements .volatile() resolution), @deepseek-ai/dsh-credentials, @deepseek-ai/dsh-home-paths and @deepseek-ai/dsh-atomic-write. The host provides them at runtime, and a profile installs no peers, so no duplicate appears. externalizes , , so keeps the bare specifiers unchanged.

Installing and upgrading the external CLI

This package spawns ocr; it does not install it. Pick whichever fits your machine:

brew install open-code-review                        # the formula tracks the newest release
npm i -g @alibaba-group/open-code-review@1.13.0      # pinned to the version this plugin verifies against
  • brew cannot pin a version. homebrew/core carries one stable formula for open-code-review and no versioned variant (open-code-review@1.12 does not exist), so brew install / brew upgrade always yields whatever is newest upstream. If the plugin's verified version is what you need, install it with npm; the card says the same thing wherever it shows a brew command.
  • The plugin never runs either command. The version probe sets OCR_NO_UPDATE=1 so that reading state cannot trigger upstream's own global install, and ocrNoAutoUpdate extends that to every command (see above).
  • Upgrading an existing install is described by that install's own source: brew upgrade open-code-review for a formula, npm i -g @alibaba-group/open-code-review@<version> for a global package. Where the path says neither (~/bin/ocr copied by hand, npx, a prefix we do not recognise), the deployment's ocrInstallHint decides which of the two to lead with.
  • Upgrading from a release that shipped the CLI (optionalDependencies, every release up to and including 0.1.5-rc): nothing needs doing, because PATH now wins over the bundled copy, so the stale one stops shadowing your own. If you want it gone from the profile's tree, remove it there (pnpm remove @alibaba-group/open-code-review under <dsh data dir>/profiles/<profile>/, or reinstall the profile); the allowBuilds: {'@alibaba-group/open-code-review': true} entry that release needed in that profile's pnpm-workspace.yaml can be deleted too.
  • ERR_PNPM_IGNORED_BUILDS no longer comes from this package. Earlier releases tripped it because upstream's wrapper carries a postinstall (scripts/install.js) and pnpm 10+ will not run a dependency's build script unless allowlisted. With no dependency to run, that error can only be coming from some other package; the host's own approval flow (the web plugin page, or install_bundle's approvedBuilds) still covers those cases, and the host validates only the pending package names — you have to approve explicitly either way.

Enabling it in dsh

  • The in-package cordis.patch.yml declares - id: ocr-review / name: "@jayyuen66/dsh-ocr-review" and is pointed at by dsh.bundle.patch in package.json.
  • dsh plugin --profile web add/remove registers and removes it, then restart dsh.
  • The card needs a web profile (dsh.client.platform: web, immediately: true).
  • The five /_dsh/ocr-review/* endpoints hang off an inject(["webServer"]) child fiber: with no webServer (a TUI host) the child never activates, so the endpoints simply do not exist while tools and settings stay available.
    • On the real host webServer arrives about a second after this entry, which is why it has to be a dependency rather than a one-off ctx.get in apply.
  • Deployment defaults can go on the profile's config: line (cordis validates and fills defaults through the exported Config schema); precedence: runtime settings value > line config > built-in default.

Tools exposed to the model

ToolArguments and constraints
ocr_reviewrepo (absolute path, default = current session workspace), scope workspace/commit/branch (branch needs both from+to and is mutually exclusive with commit), effort, background, exclude (at most 50 entries), provider/model/resume (resume only with commit/branch — workspace reviews cannot be resumed), wait
Numeric group (arguments of ocr_review)concurrency/timeoutMinutes/maxTools/maxTokens/maxTokensBudget (mapped to --concurrency --timeout --max-tools --max-tokens --max-tokens-budget; non-integers or values below the floor are rejected; note that --max-tools values 1-49 are raised to 50 by OCR — its help text says min 50)
ocr_scanrepo, path (at most 100 entries), exclude (50), batch none/by-language/by-directory, provider/model (native shared flags of scan_cmd, matching review), the same numeric group, wait; no git diff required
ocr_delegate_preview / ocr_delegate_ruleThe former: repo, scope/commit/from/to, exclude 50, background. The latter: repo, paths required, 1–200 entries, positional arguments after --, each escaped. Zero LLM cost on the OCR side, they return only the file list and the rule groups
ocr_sessionrepo, action list/show/comments (whitelist, anything else errors), id, limit (clamped to 1–100, default 10); show/comments without an id falls back to list
Background and reaperwait: false (background start, no host deadline, no time limit by default, no --output, poll results with ocr_session) and the host reaper guard apply only to these two minute-scale commands, review and scan - that subprocess is also an official job (see the job-surface section below); that change left the tool's JSON untouched (a later output-contract rework switched it to the direct canonical object, see below)

Settings

FieldValues, defaults and use
effortlow/medium/high, built-in default medium; used when ocr_review is called without an explicit effort
language中文/English, default 中文; written as the top-level language key of the OCR config when a selection is applied
autoVerifydefault true; when true, a successful /select also runs ocr llm test --color never (capped at 90 seconds)
maxCommentsdefault 12, the number of comments kept in the summary; 0 = no truncation, non-integers and negatives fall back to 12
timeoutMinutesdefault 0 = request the host cap (the host clamps to min(request, shell.maxTimeoutMs)), >0 is a number of minutes
ocrConfigPathwhere the external ocr CLI config lives; left empty it derives os.homedir()/.opencodereview/config.json, ~ and ~/ prefixes are accepted, relative paths error out. A custom value must follow the <X>/.opencodereview/config.json layout: OCR has no file-level path override (it only ever looks at <HOME>/.opencodereview/config.json, config_cmd.go:92-99), so the plugin makes it effective by injecting HOME=<X> into the ocr subprocess (config and sessions are redirected together, and api_key_cmd carries the host-resolved data directory as get-cred's explicit first-tier locator); any path outside that layout is rejected outright at the setting layer rather than silently mismatching (the HOME injection is a POSIX assignment prefix; Windows has no such channel, so custom paths only take effect on macOS/Linux)

Three deployment-level tuning keys are deliberately not on the settings card — they are plain (non-volatile) fields of the exported Config, settable only on the profile's config: line and taking effect on restart:

KeyDefaultUse
llmTestTimeoutMs90000Wall clock for ocr llm test (shared by autoVerify and the card's "test connection"); raise it on slow machines
ocrBackgroundMaxMs0Backstop window for background reviews (jobs.wait to the deadline, then kill); 0 = none, so a run ends only when the host exits, the session stops, the plugin unloads or the job is killed
stdoutMaxBytes400000Executor stdout buffer cap, shared by foreground and background runs

A fourth non-volatile field is a deployment decision rather than a tuning value, for the same reason (it changes what a run may do to the machine, so it does not belong on a card a user can flip mid-review):

KeyDefaultUse
ocrNoAutoUpdatefalseWhen true, every ocr command this package spawns carries OCR_NO_UPDATE=1, which stops upstream's wrapper from running npm i -g @alibaba-group/open-code-review@<latest> in the middle of a review. false keeps upstream behaviour (18-minute cooldown, pid lock). The version probe sets the variable regardless of this switch

The fifth is about which install command a user is shown, which the same reasoning keeps off the card (a deployment that standardises on brew or on npm should not have each machine discover it separately):

KeyDefaultUse
ocrInstallHintbothWhen nothing is installed, which command the tools, the card and the activation guidance lead with: both (show both), brew, npm. It is a fallback, not an override: once a candidate exists its own install source decides the wording, because telling a formula user to run npm i -g would leave a second copy behind while PATH still resolves to the old one. The npm command always carries the verified version; the brew one never can

Credentials and config files

AspectFact
Credential form in the configWhat lands in the config is a command, not a secret: api_key_cmd = node '<path of scripts/get-cred.mjs in this package>' '<REF>' '<dsh data dir>' (POSIX, single-quote escaped) or node "<path>" "<REF>" "<dsh data dir>" (Windows, double-quote escaped - ocr runs credential commands through cmd.exe there); the third part is the host's current resolveDshHome() result, so get-cred's fallback layers stay correct when HOME is redirected or the dsh home is set outside the env, and the REF name must match ^[A-Z0-9_]+$ or the command is refused
Precedence mirroringget-cred reads its three layers in the same order as the official credentials-local: refs.<REF> in $DSH_HOME/.credentials.yaml first, then the inherited process environment, then $DSH_HOME/.env. Getting the order wrong fails silently: the host treats the managed file as the effective credential, so a script that answers with the environment copy first makes OCR authenticate against a different key than the one the card reports while both sides show success. dsh 0.2.1-alpha.3 moved the environment layer below the managed file, and this package's script plus test/get-cred.test.ts moved with it
Reachability gate/select and /migrate refuse a key whose credential source OCR cannot reach at runtime: dsh strips variables named KEY/PASSWORD/SECRET/TOKEN or prefixed DSH_ from spawned shells (scrubbedParentEnv), and get-cred deliberately does not read the reviewed repository's .env. Keys that only live in the dsh process environment or the workspace .env therefore resolve on the host side yet fail at OCR runtime - previously a silent "select succeeded → OCR 401". Sources file ($DSH_HOME/.credentials.yaml) and user-env ($DSH_HOME/.env) pass; env passes only when the ref name matches none of the scrubbed patterns; project-env never passes. The refusal happens before any write, with the fix named in the error text (store the key in the managed credentials file or $DSH_HOME/.env)
How writes happenAtomic replacements via the official @deepseek-ai/dsh-atomic-write: a random-suffixed .<hex>.tmp sibling created with , then rename, with the new inode carrying 0600 (parent directories 0700). The backup is copied by this package the rename and chmod-ed to 0600 (the official helper has no backup semantics). The whole read→decide→write span is serialized by the official , so a short-lived appears in that directory during a write and is removed in ; a lock left by a crashed holder is taken over via a pid liveness probe. : contention beyond the official default 2s wait makes the write throw , and the endpoint answers 400 with that English text verbatim — it comes from the official package and bypasses , unlike every other user-visible error here. Before this swap the read-modify-write shared one event-loop tick, so this failure mode did not exist; mapping it would need an injectable wait budget plus a timeout test, so this package documents it instead of silently widening the surface. An existing config that is invalid JSON or whose top level is not an object aborts the write instead of "treating it as empty and overwriting"

Public endpoints

RouteMethodInputSuccess payload
/_dsh/ocr-review/providersGET—{providers (each with a hasKey boolean), current, getCredReady, source, csrf, endpointVerify, ocrBinary}, never containing a key. ocrBinary is the filesystem-only availability verdict plus the version context the card renders: {source: "path" | "bundled" | "none", path?, channel?: "brew" | "npm" | "unknown", expectedVersion, measuredVersion: string | null, installHint} — measuredVersion is whatever the last ocr version produced (apply-time self-check or a pressed Detect), null when nothing has run yet, and reading this route never spawns. installHint is that install sentence, localized and composed host-side so the card cannot drift from the version the plugin verifies against. endpointVerify is the apply-time ocr llm test result or null when it has not run
/_dsh/ocr-review/selectPOSTbody {provider, model}, both must hit the list returned by describe(){ok, applied, llmTest} (llmTest is null when autoVerify is off)
/_dsh/ocr-review/migratePOST—{ok, migrated, skippedUnknown}
/_dsh/ocr-review/testPOST—{ok, output} (output is redacted first, then cut to 8000 characters)
/_dsh/ocr-review/probePOST—{ok, version, expectedVersion, consistent, output, reason, changeText} — spawns ocr version with a 15s wall clock and OCR_NO_UPDATE=1, reads the version out of its output and writes it into this apply's slot so the next GET providers and the next ocr_review/ocr_scan receipt describe the same measurement. version is null unless the command succeeded and printed a parseable version; consistent is a three-state (true / false / null — null means no version was read, where claiming a mismatch would be claiming something with no evidence); reason names the failure arm (, non-zero exit, timeout, or exit 0 with nothing parseable, which is what an unreleased build looks like); carries the install/upgrade sentence only when there is a command to give — a measured version that differs, or no candidate at all — and is empty on a timeout with the CLI still in place; is redacted and cut the same way as , with terminal control sequences stripped, so an upstream upgrade notice stays copyable

These five routes live and die with the plugin's injected child fiber. The official webServer.register hands back a disposer the caller must invoke, and it throws on a duplicate path outright (installed dsh-host-webserver/lib/index.js:177-184), while the route table belongs to the host-provided webServer instance and does not die when this plugin's fiber does. This package attaches all five disposers to the effect of the inject(["webServer"]) child fiber, so a hot reload (old fiber disposes, then a new apply runs) cannot collide on the path and kill the whole settings card, and disabling or uninstalling the plugin makes these endpoints unreachable again.

The background job surface (official ctx.jobs)

The external ocr subprocess started by wait: false is also registered as an unowned job in the host's ctx.jobs (kind: "ocr-review", id ocr-review-<16 hex>, handed out by the registry from a truncated randomUUID rather than a counter; being unowned also means the official job/registered / job/settled session lifecycle records are not written, since those need an owner). What changed is who can see and stop it, not what the model receives:

AspectFact
Model observation surfaceThree more tools: the official job_list shows the review, job_output reads its stdout/stderr (the registry pulls from proc.observed on its own cadence - non-consuming, so it never steals bytes from the readOutput() cursor), and job_kill stops it. Results still come from the ocr_session record; the payload content is unchanged since the job-surface swap, and a later output-contract rework returns the canonical object directly (no more stringify-ing the receipt into a { text } wrapper)
Broken executor readerWhen the executor's reader throws, the official pump only writes one line to the host log and afterwards reads that channel as empty (measured) ⇒ this package writes one "this section is incomplete" note into the ring, exactly once, and stops knocking on the broken reader
Controller lifetimeAttached only for the instant of start: the registry refuses start when no controller serves the owner, and on the web surface the host's own tool-jobs row is disabled at the host plane (each preset realm mounts its own), so an unowned job needs a controller of our own. We attach it and detach it immediately (measured: detaching restores the refusal, and read/kill/settlement never need it again) - a resident token would pry that gate open for a composition that deliberately has no job tools
Three reap pathsThe optional backstop window, a plugin unload, and a session dispose all call jobs.kill(id, undefined, reason) instead of killing the process around it, so the record moves to stopping and the reason lands in detail (measured: bypassing it left the row stuck at running forever). They may overlap - killing a stopping job still returns requested and proc.kill() is idempotent. Reasons are model-facing, so they come from the bilingual catalog
Nothing there may throwAn error raised inside a host event handler is rethrown by Node via process.nextTick ⇒ uncaught ⇒ the whole host process exits (reproduced locally: Error: unknown job ... followed by EXIT=1). The official package does not cover for producers either - killJob calls job.cancel() bare (dsh-jobs-local's killJob, and only the teardown arm wraps each call in try/catch), and a reloaded registry throws for an unfamiliar id (the throw in the same file). So two layers each catch once: swallows the "process is already gone" family through , catches the registry arm and falls back to killing our own process. One test per layer - deleting either layer turns it red

Data and privacy

AspectFact
Process boundaryThe reviewed repository's paths and diff content go into the ocr child process, and OCR sends them to the LLM endpoint it has in its own config.json; that egress is the review feature itself and does not pass through the dsh host. Plaintext keys never appear in tool results or in /_dsh/*, and the value the host gets from resolve() is used only to confirm "it resolves right now" and is discarded on the spot
Redaction of raw outputRaw OCR stdout/stderr (failure details, llm test, parse-failure echoes, session passthrough) all go through redactKeyMaterial() before leaving the host: bearer and assignment forms are redacted strictly, while the standalone-key form additionally demands random-string features so that file paths handed to the model are not erased as well
Places writtenThis package writes to only two places: the external ocr CLI's config.json (plus its random-suffixed .tmp, the .bak backup, and the short-lived config.json.lock held only for the duration of one read-modify-write) and ocr-review-*/ocr-scan-* in the system temp directory (removed right after reading). Plaintext that predates a migration may still sit in the .bak produced by that same write (mode 0600); removing it for good means deleting that backup yourself. Writes outside those two places can still happen inside the CLI's own wrapper: unless ocrNoAutoUpdate is on, an ocr invocation may trigger upstream's global npm i -g of a newer release. This package never runs an installer itself, and the version probe always suppresses that path

FAQ

SymptomVerdict
Exit 0 taken for a successful reviewExit 0 is not a successful review: any status outside success/complete/partial/completed_with_warnings/completed_with_errors/skipped is reported as failure ({"error": …} is of that kind; partial is one of the manifest terminal states — some files failed but coverage remains, and the comments still stand), and a parse failure echoes the first 500 redacted UTF-16 code units of the raw output (an emoji takes 2)
Whether the summary covered everythingTwo different notions, neither replaces the other: droppedCount and invalidCommentCount only say whether this summary listed every comment OCR produced (raise maxComments, or set 0, and re-check). Whether the run covered everything is in coverage (selected is the denominator, completed/reused/failed/waived partition it, failedItems names the files with their classification and reason) plus budgetExceeded (the token budget stopped it early — status still only says partial); toolCalls reports how many of the model's tool calls failed. stdout has its own 400000-byte cap, which is why review results always go through the --output temp file
Background processes when dsh exitsWhen dsh exits in any way (including kill -9 followed by launchd adoption), the OCR process tree of review/scan is taken down by the reaper guard inside the command with TERM→1s→KILL; cancelling the tool call terminates the corresponding background process too

Development

  • npm run check is the single gate to run before publishing: typecheck (both tsconfigs) + oxlint + rebuild both bundles + npm test (vitest with coverage) + oxfmt --check.
  • npm test runs with --coverage under a 100% global threshold (lines/statements/branches/functions) — any line or branch not pinned by a test turns the suite red by design.
  • npm run build / npm run build:client produce the published host.js / client.js. After editing host.ts, lib/* or src/* they must be rebuilt (test/build-host.test.ts pins bundle freshness against the sources, so a stale bundle fails the suite). Never hand-edit the bundles.
  • The plugin source is TypeScript loaded directly by the host's cordis loader (Node ≥ 22.18 type stripping); the bundles exist for the published npm surface.

License

MIT — see LICENSE.

  • Upstream self-update: the ocr npm wrapper runs scripts/update.js on its own during invocations (18-minute cooldown, pid lock), and that script executes npm i -g @alibaba-group/open-code-review@<latest>. The ocrNoAutoUpdate deployment switch sets OCR_NO_UPDATE=1 on every command this package spawns, so a review never changes global packages under you; it defaults to off, i.e. upstream behaviour. The version probe always sets it, switch or not — probing is a read and must not install anything. The launcher prints its "new version available" nudge to stderr before checking that variable, so the nudge still arrives in the receipt text.
  • build-host.mjs
    dependencies
    peerDependencies
    and
    optionalDependencies
    host.js
  • Those four are repeated in devDependencies — that is what lets this repository's own typecheck, build and tests resolve them. Every other @deepseek-ai/* is type-only (erased at build, no specifier left in host.js) and stays in devDependencies alone.
  • What is left in dependencies is genuinely this package's own runtime code: @jayyuen66/dsh-plugin-shared and js-yaml (the last one used only by scripts/get-cred.mjs).
  • wx
    .bak
    before
    withFileLock
    config.json.lock
    finally
    Known inconsistency
    atomic-write: timed out waiting for the writer lock at …
    lib/messages.ts
    POST migrateReplaces the plaintext api_key of custom provider entries with api_key_cmd and deletes the plaintext; entries whose key source is unreachable at OCR runtime (same gate as /select) or whose apiKeyEnv is not found in settings are left untouched and listed in skippedUnknown; nothing is written when nothing was migrated
    scripts/get-cred.mjsSpawned by the external ocr CLI itself and the only place in this package still reading host credential files. The data directory is resolved as explicit third argument > $DSH_HOME > ~/.dsh, and the lookup order is the one named under Precedence mirroring above (managed document > inherited environment > .env) - note the inherited layer is usually empty for credential refs, because dsh scrubs KEY/PASSWORD/SECRET/TOKEN- and DSH_*-named variables out of spawned shells. The project .env of the reviewed repository is deliberately not read; when all three layers miss it exits non-zero and names every location it checked
    command not found
    changeText
    output
    /test
    Front guard of the four write endpoints——The first statement of each handler is guardTrust (Host authority -> the sec-fetch-site allowlist -> a verbatim origin compare; failing it gives 403 with {ok:false,error:"untrusted host authority" | "cross-origin request rejected"}); 405 for non-POST carrying Allow: POST and {ok:false,error:"POST only"}; 403 for a missing or wrong x-ocr-csrf header; 413 above the 8KB body cap; 400 for an aborted stream. The token is regenerated on every plugin apply and the old one stops working
    expect
    cancel
    killProc
    stop
    The optional backstopWe still give the external CLI no host timeoutMs (a long review would be cut off), but "no end at all" gained a victim with this adoption - the 10-slot unowned bucket is shared host-wide (measured: running and stopping both count, overflow refuses new starts), so one hung CLI parks a slot forever. The backstop uses the registry's own wait(id, ms) + kill, not a host timer and not a bare setTimeout
    Unowned exposureUnowned means visible to every session - any caller can list/read/kill it. Known and accepted for this wave (owned jobs would need the dsh-agent registry, measured unavailable); rolling the adoption back is the only way to close it
    Settled-record windowThe registry never recycles settled records on its own, and it outlives this plugin - so before each registration this package prunes the oldest settled ocr-review records down to 9 and only then starts, so the roster never holds more than 10 (each ring is capped at 256 KiB). At unload both jobs are done: live ones are killed through the registry, settled ones are removed - disabling this plugin means there is no "next apply" left to prune the window. Live and stopping records are never pruned: the registry answers overflow by refusing start (measured: the unowned bucket holds 10), not by killing somebody's review
    No registry, no backgroundWhen the host has not loaded dsh-jobs-local, wait: false throws an error naming the missing piece instead of starting a subprocess nobody can observe. Foreground runs (wait: true) are unaffected