Skip to content

Releases: liustack/modlens

v3.15.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 09:34
  • dsh: the vision wrap discovers every eligible provider route (#29). The wrapper used to bind one hardcoded upstream, so a machine with several subscription packages (opencode-go, zai, ...) had to hand-pick a single route and lose the rest. With upstream unset the plugin now sweeps the llm registry and registers a modlens-<provider> wrapper for every route carrying wrappable text-only DeepSeek/GLM models, re-sweeping on the registry's own llm/adapters-updated notification so late-registering routes (llm-pi-ai mounts after settings load) are picked up without polling. A discover array narrows the set, families filters as before, setting upstream keeps exact legacy single-route behavior, wrappers never wrap themselves, and the deepseek-official wrap keeps its historical deepseek-modlens id so an upgraded machine's model selection survives. Design and behavior spec contributed by @zlycode01, who also published a reference implementation in a public fork; this is an independent upstream implementation of that spec, with the polling schedule replaced by the registry notification.

v3.14.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 07:42
  • dsh: pasting into a text-only model now just works — the paste becomes a file path. The plugin grows a browser half (a hand-written bundle in dsh's client plugin protocol, zero dependencies, loaded automatically under the web profile). A capture-phase listener takes over image pastes before the composer's own intake: the bytes go to the plugin's /modlens/paste route on the dsh web server (loopback-bound, magic-byte checked, 25 MB cap, private 0600 temp file), and the composer receives the file path as plain text — the exact shape Pi, OpenCode, and Claude Code hand their models, and the modlens skill's primary trigger. Image admission never fires because the message carries no image attachment; verified end to end with a native text-only DeepSeek-V4-Flash, whose visible reasoning quoted the modlens skill rule and went for the path. The takeover is conditional: (modlens vision) variants and known vision models keep the native paste flow (thumbnails and all), and pasteToPath: false turns the feature off. The host route rides a scoped ctx.inject(['webServer'], ...), so headless profiles never see any of it.
  • The CLI survives Electron hosts (#25). In the packaged dsh desktop app, process.execPath is the Electron binary, and commander's Electron auto-detection then mis-slices argv so the script path lands as a stray positional (too many arguments for 'analyze'). The CLI now parses argv with explicit node semantics — it is always spawned script-first, whatever binary hosts it — and the plugin's spawns set ELECTRON_RUN_AS_NODE for good measure. Thanks to @hi-fangj for tracing it into commander's _prepareUserArgs.

v3.13.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 07:20
  • Proxy support actually works now (#23). 3.12.0's proxy path was broken on arrival, twice over: the bundled copy of undici had its internal node:http2 references destroyed by bundling (the embedded ProxyAgent threw http2.connect is not a function), and handing any undici 8 dispatcher to the host's built-in fetch (a different undici major) fails with UND_ERR_INVALID_ARG regardless. undici is no longer bundled — it resolves from node_modules, shrinking the CLI bundle from 1.17 MB to 131 KB — and the proxied path now uses undici's own fetch so dispatcher and fetch are same-sourced, with the dispatcher closed after the response so its keep-alive pool cannot pin the process open. A new integration test drives the built CLI through a real local HTTP proxy, the exact coverage whose absence let 3.12.0 ship broken (and whose first draft repeated a classic mistake: spawnSync freezes the test's own fake servers, so the CLI must be spawned async); both the env-var and explicit-setting forms were also verified against a real LAN proxy. Independent review of the fix then caught the same cross-version boundary hiding in the no-proxy remote-image path — the IP-pinned download Agent was still handed to the host's fetch — so that path is now same-sourced too, and the Node floor rises to 22.19 (undici 8's own engine requirement, which externalizing made load-bearing). Thanks to @JooJeen for a diagnosis that had already isolated both layers.
  • dsh: images nested inside tool-result content no longer wedge the session (#24). dsh's own native read_image (from dsh-tool-fs, reachable under the (modlens vision) variants since they declare image input) returns its image block nested inside tool-result content. Both conversion paths only scanned top-level message content, while the upstream DeepSeek adapter's rejection check recurses — so one nested image block in the history failed every later turn with UNSUPPORTED_CONTENT, permanently, since the durable log by design keeps the real blocks. Both paths now recurse through tool-result content exactly as the adapter does, converting nested images to evidence text on the wire while the log keeps its native blocks. Thanks to @alex16lai for the precise trace.

v3.12.1

Choose a tag to compare

@github-actions github-actions released this 14 Aug 04:40
  • claude-cli reads the envelope's structured_output first (#22). Newer claude CLI builds ship the schema-parsed object beside the result string, and the parser only hard-parsed the string, so an unescaped newline in the OCR text failed the whole read while the good object sat unread — intermittently, since it depended on what the model emitted. The parse order is now structured_output, then fence-tolerant extraction of the result string, then the error, matching the antigravity provider. Thanks to @lin-nanxing for the precise diagnosis, down to the code lines.
  • A read_image name collision no longer kills the whole dsh plugin (#21). Hosts with a durable attachment store mount dsh's own native read_image (from dsh-tool-fs), the duplicate registration threw, and the whole plugin fiber failed — vision wrapper included. The registration now falls back to modlens_read_image on a name collision (valuable exactly there: the native tool is gated on the model declaring image input and vanishes for text-only models, so the renamed bridge is the only image path left), the name is configurable via the plugin row's toolName, and any other registration error degrades loudly instead of taking the plugin down. Thanks to @abyss-stars for the root-cause analysis and the interim patch.

v3.12.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 03:53
  • The API providers work behind a proxy (#20). Node's fetch ignores HTTP_PROXY/HTTPS_PROXY entirely, so machines that reach the internet through a proxy could not use gemini-api at all, and the failure surfaced as a bare fetch failed. The three inline API providers now honor the standard environment variables (NO_PROXY included, via undici's EnvHttpProxyAgent), with an explicit setting as the escape hatch: modlens config set proxy <url> for all API providers, <provider>.proxy to scope it to one. A connect-level failure now names the unreachable host and points at both knobs instead of saying fetch failed. Scope is deliberate and documented: the proxy applies to API requests only, while the remote-image download path keeps its direct, IP-pinned connection, because its SSRF guards validate the exact address being contacted and a proxy would blind them. Thanks to @soloyu for a report that arrived with the diagnosis, the fix direction, and the security boundary already thought through.

v3.11.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 19:35
  • A full-project audit, all ten findings fixed, then re-reviewed until clean. An independent deep review of the whole repository (P0: none) surfaced ten conditional-but-real defects. Every fix went back through further independent review rounds, which caught real bugs in the first fixes themselves (case and Unicode boundaries, a cancellation regression); the final round accepted with no blocking findings. Each item below carries a regression test — the suite grew by 32 cases.
    • Cross-project recovery, two ways in. The OpenCode directory filter's ancestor branch built a LIKE pattern from the database's own session.directory, so _/% inside another project's path acted as wildcards; the comparison now uses exact SUBSTR prefixes in both directions, measured in Unicode code points (SQLite counts characters, JS counts UTF-16 units, so a path with an emoji shifted the boundary), case-sensitive on POSIX and LOWER-folded on Windows (SQLite's LIKE was ASCII case-insensitive, crossing projects on case-sensitive filesystems), with filesystem roots normalized so / and E:/ match as ancestors. And a Claude/Pi transcript recording no cwd at all was accepted on slug evidence alone, though slugs collide (/tmp/a.b and /tmp/a-b); ownerless transcripts are now skipped by scanning and session lookup, while an explicit --transcript still reads anything you point it at.
    • Image-type spoofing. When magic-byte sniffing failed, the extension or content-type used to win, so an .png URL serving HTML was encoded and uploaded as an image. Every allowed type must now prove itself from its file header — heic/heif included, via ftyp-box sniffing — so there is no extension or content-type fallback left at all.
    • Credential redaction, everywhere errors quote foreign text. Subprocess stderr, the three API providers' error bodies, and discovery-probe failures now pass through a shared redactor (known keys replaced exactly, common token shapes as the second net) before reaching terminals, meta.attempts, model contexts, or the discovery cache. Remote-URL errors quote origin and path only, so signed query tokens (S3 presigns) never travel, and the shapes are tuned to leave prose like "token limit_exceeded" alone.
    • Windows PATH probing. findOnPath now walks PATHEXT, so a real agy.exe/claude.cmd install no longer reads as "not on PATH" (doctor and the failover chain both trusted that verdict).
    • dsh evidence cache. Failed reads are no longer memoized forever (a fixed config gets a fresh chance without restarting dsh), concurrent steps join one in-flight read instead of double-spending the engine, the cache is capped LRU-style, and cancellation is per-waiter: aborting one step stops its own wait immediately while the shared read completes into the cache, instead of killing every concurrent joiner. Pasted HEIC/HEIF now maps to its real extension, and an unknown media type refuses instead of dressing up as PNG.
    • Docs told the truth about less than the code did. The security page now states per provider who fetches a remote URL (only gemini-api downloads locally behind the SSRF guards, magic-byte check, and size cap; openai/anthropic hand the URL to the vendor). The output contract now lists visual as required, matching the schema that always enforced it, with a docs-contract test pinning the two together. Doctor renders CLI providers as [ok?] installed / sign-in not verified offline instead of a flat ok, adds a machine-readable status field (ready/installed/missing) so JSON consumers stop reading bare-binary as ready, and the install flow treats the first real read as the auth check. The skill's trigger list drops .bmp (never supported by the CLI) and gains .heif.
    • Hardening around the edges. The release script and workflow now run lint, refuse version downgrades, and verify the tag matches package.json; CLI numeric flags reject trailing garbage (--count 3x); error exits use process.exitCode so piped output survives.
  • dsh install can silently land on 3.5.0 (#18). pnpm v11's release-age quarantine falls back to an old version when every recent one is inside the window (10 days on pnpm 11.21, measured), and versions before 3.9.0 carry no dsh.bundle declaration, so the plugin installs as a plain dependency and none of the tools appear. There is no CLI or env override for the gate (pnpm#11224 is still open), but an explicit version or dist-tag skips it (pnpm#9989, verified against pnpm 11.21: a bare add resolved 2.8.0 while @latest resolved 3.10.0 under the same default gate). The install command everywhere now carries @latest, so new installs land current with zero user-side setup; troubleshooting documents the mechanism plus the durable bare-name minimumReleaseAgeExclude fallback for whenever pnpm closes that skip. Thanks to @stonogic086-1 for the precise diagnosis.

v3.10.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 17:42
  • Pasting into the dsh Web UI now works end to end, thumbnails included. The plugin registers a wrapper provider whose model list carries two entries, DeepSeek-V4-Flash (modlens vision) and DeepSeek-V4-Pro (modlens vision); select one and pastes are admitted, because dsh's image admission asks the selected provider for input modalities and the wrapper declares image input. Conversion happens at request time on the wire messages only: the durable session log and the UI keep the native image blocks (your paste keeps its thumbnail), each pasted attachment is read once through the modlens CLI and cached for every later step, and the wrapped request is answered by the same DeepSeek route it always used. The wrap covers DeepSeek and GLM text models only; their own vision models (deepseek-vl/ocr, janus, glm v-series) are excluded by name pattern and by declared modality, so a model that can already see is never wrapped. The agent/pre-step auto-read from 3.9.0 becomes opt-in (autoRead: true) since request-time conversion owns the paste path and preserves the native UX. Registration also stopped failing silently: the adapter now supplies the base-class methods dsh's duck-typing expects (providerInfo, providerRetryPolicy), whose absence was exactly the invisible failure, and a registration error is logged to the harness console instead of swallowed.
  • Paste auto-read read a field the attachment store never had (#17). dsh's attachments.readImage returns { ref, data }, but the 3.9.1 plugin read stored.bytes and stored.mediaType, so every pasted image degraded to a Buffer.from(undefined) TypeError block. The plugin now reads the real shape (data, with the media type on ref), and a missing byte field fails with a named error pointing at the dsh attachment shape instead of a bare TypeError, so the next developer-preview drift stays legible. Thanks to @nico2656-ui for the rc.6 type archaeology and a verified patch.
  • The README leads its demo gallery with the real dsh paste session (recovered from the machine that ran it), states the wrap scope, and lists both variant names verbatim under a rather more visible first-plugin banner.

v3.9.1

Choose a tag to compare

@github-actions github-actions released this 13 Aug 14:53
  • dsh follow-ups that missed the 3.9.0 tag during a GitHub outage: the plugin row references the bare package name via a root export, so the dsh plugin list shows modlens instead of modlens/dsh; install lines use npx -y @deepseek-ai/dsh (the developer preview has no global binary); and both READMEs state the paste status honestly, since the dsh DeepSeek adapter declares text-only input and Web-UI image admission runs before any plugin hook, read_image is the working path today and paste auto-read stays wired for when images can enter.

v3.9.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 14:52
  • The first plug-in vision plugin for DeepSeek Harness (dsh). The npm package is now also a dsh bundle: dsh plugin --profile <name> add @liustack/modlens is the whole install. It registers a native read_image tool (schema in every model request, so there is no trigger heuristic at all) that spawns the modlens CLI shipped in the same package, declares the vision schema as its canonical output contract, and renders evidence text for the model. Phase 2 rides agent/pre-step: images pasted or dropped into the dsh Web UI are read automatically and enter the step as modlens evidence blocks, with failed reads degrading to an explanatory note instead of rejecting the step (autoRead: false in the plugin row turns this off). The plugin imports no dsh packages (raw JSON-Schema tool registration, node builtins only), which is also the smallest possible surface against developer-preview churn. Verified end to end on a real dsh headless profile: the DeepSeek model called read_image and quoted the exact transcription back.
  • Grok Build joins as the fifth reusable harness. reuse.grok grants the local Grok CLI login as an engine: discovery reads ~/.grok (OAuth evidence in auth.json, model ids from models_cache.json judged by the builtin vision table), and the route drives headless grok -p with --json-schema (which accepts this project's schema unmodified; the structuredOutput field carries the conforming answer) and --allow Read, following the claude-cli template since headless grok has no image-attach flag. Verified live: an exact OCR read through a real SuperGrok login. The agent region order becomes antigravity, codex, opencode, grok, pi-cli, claude-cli.

v3.8.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 13:48
  • Reuse: per-harness grants for the logins already on your machine. A signed-in Codex, an OpenCode vision model, or credentials held by pi can now serve as vision engines, each behind one explicit yes recorded as reuse.<claude|codex|opencode|pi> (true grants, false means never ask again, absent means never asked; claude absent counts as granted since claude-cli predates the model, and reuse.claude false now removes it from the chain). pi credentials with an exactly-supported api shape (openai-completions, anthropic-messages) and a printable API key run through the existing inline providers with every guard intact; OAuth logins and other shapes (Responses, Vertex) drive pi itself (pi -p with the @file attachment), and codex (codex exec -i) and opencode (opencode run -f) join as agent routes. Reused engines get no priority: one chain, two regions, speed-class placement only, and every reused answer adds a meta.warnings line naming whose quota it spent. Keys are fetched at call time, live only in memory, and no subprocess output ever travels into an error message.
  • The failover chain leads inline everywhere. The local chain becomes gemini-api, openai, anthropic, then antigravity-cli, then claude-cli: a configured key answers in 5-10 seconds while an agent loop takes 15-45, so with both set up the old agent-first order made every read slower than it had to be. A config set provider preference still moves any provider to the front for local images; for remote URLs inline-first stays a security boundary even against a preferred agent, since only the inline download path runs the private-address guards, the magic-byte check, and the size cap.
  • Onboarding is one consent conversation, symmetric across harnesses. INSTALL.md's engine step now reads doctor's Reuse section and asks one question per never-asked harness, the harness you are installing into included, so a Codex-only or Pi-only machine gets the same zero-config offer a Claude Code machine always had. The same flow lives in the skill as references/onboard.md for machines where the skill arrived without a config. Refusals are recorded and never re-asked; config show renders the tri-state so the flow can tell.
  • doctor tells the whole truth. The Auto section becomes Reuse (per-harness decision plus what discovery found), the failover chains display includes reused routes labeled (reused) so a machine living entirely on granted logins no longer reads as having no engine, and a fully failed chain distinguishes never-asked reusable vision from a grant that stopped working. A stock codex install without a config.toml counts as vision-capable, and a cache timestamp that fails to parse now expires instead of living forever.
  • SKILL.md went progressive. 59 lines instead of 164: the trigger contract, the launcher, and the loop stay; machine state is read from doctor and config show at run time, and the heavy flows load on demand from references (find-image.md for the per-harness paste-path branches, onboard.md for first-run setup, configure.md for every key). cli.md and output-schema.md moved to docs/: they are manuals for humans, not run-time knowledge.
  • The whole reuse surface passed an independent code acceptance review (run through a reused Codex, fittingly): nine findings fixed, from credential redaction and exact api-shape mapping to preference preservation and the remote security boundary, each with a regression test. 279 tests.