# modlens

> An official plugin that

## Metadata

- Author: [@liustack](https://github.com/liustack)
- Repo: <https://github.com/liustack/modlens.git>
- GitHub: [liustack/modlens](https://github.com/liustack/modlens)
- Stars: 2,758
- Language: TypeScript
- License: [MIT](https://spdx.org/licenses/MIT.html)
- Homepage: <https://liustack.dev>
- Topics: `agent-skills`, `claude-code`, `claude-skills`, `codex`, `cordis`, `deepseek`, `dsh`, `dsh-plugin`, `glm`, `harness`, `harness-engineering`, `hermes-agent`, `image-to-text`, `multimodal`, `ocr`, `openclaw`, `pi-agent`, `text-only-llm`, `vision`, `vision-transformer`
- Forks: 76
- Open Issues: 6
- Last push: 2026-08-17T15:39:40.000Z
- Added: 2026-08-14T00:00:00.000Z

## Install

```bash
dsh plugin --profile web add github:liustack/modlens
```

## Wiki

## One-Line Positioning
modlens is the official visual plugin for DeepSeek Harness, giving native non-image-supporting DeepSeek/GLM text models a pair of eyes: after installation, you can directly paste screenshots in dsh, drop an image path, or add a dragged image, and the model can "see" the content and answer based on it.

## Core Capabilities
- Register `modlens_read_image` tool for text models, pass a local path or http(s) URL to convert to structured JSON evidence (full transcription, layout blocks, entity relationships, uncertain items)
- Automatically wrap all providers routing DeepSeek/GLM pure text models, each route generating a set of model variants with "(modlens vision)" suffix; both vendors' built-in vision models are automatically excluded
- Browser-side intercepts pasted screenshots, first POST to local route, then insert the image as a path into the input box (same "path trigger" interaction as OpenCode, Pi)
- Built-in six vision engines (Gemini API, Anthropic API, any OpenAI compatible endpoint, Antigravity CLI, Claude Code CLI, Kimi Code CLI), when unspecified composed into a failover chain
- `recover-paste` subcommand retrieves pasted image bytes from Claude Code / Pi / OpenCode local session storage, dropping them into a 0700 private directory
- `doctor` subcommand performs pure local health checks: Node version, engine readiness, current selection, host, guard determination, no quota consumption, no network requests

## Technical Implementation
- **Language**: TypeScript (Node.js ESM), a small amount of hand-written CJS for browser side (`dsh/client.js`)
- **Key dependencies**: `commander` (CLI routing), `undici` (remote image download + DNS pinning), `@biomejs/biome` (lint, devDep only); host side only uses Node built-in modules
- **Architecture pattern**: Dual-form package — the same npm package is both CLI (`bin: modlens` → `dist/main.js`) and dsh cordis plugin (`dsh.bundle.patch` → `cordis.patch.yml` injects `@liustack/modlens` into dsh cordis registry, exported from `dsh/index.js` as `apply(ctx, config)`). DSH browser side has another zero-dependency lazy-CJS client (`dsh/client.js`) intercepting pastes
- **Entry points**: CLI entry `src/main.ts`; DSH plugin entry `dsh/index.js`; browser client `dsh/client.js`

## Use Cases
Daily use of dsh Web/Desktop App to chat with DeepSeek or GLM text models, frequently needing to paste screenshots or drop image paths to ask questions — after installation no commands to memorize, just paste normally in chat. Also suitable for developers wanting to squeeze a single text model but not wanting to switch models, and not wanting to write their own OCR/vision scripts.

## Prerequisites & Compatibility
| Dependency | Minimum Version | Description |
|---|---|---|
| Node.js | >=22.19 | package.json `engines.node` declaration; OpenCode paste recovery further depends on `node:sqlite` (unflagged from Node 22.13+) |
| dsh | No specific version declared | Injected via `dsh.bundle.patch` (cordis.patch.yml); install via `dsh plugin --profile web add github:liustack/modlens`; package.json declares no peerDependencies |
| Platform | macOS / Windows / Linux | `src/util/winExec.ts` handles Windows .cmd shim; `src/auto/discover.ts` cross-platform probes PATH for harness CLI |
| Native modules | None | No node-gyp dependencies; remote image download uses `undici`'s built-in Agent; OpenCode recovery uses Node built-in `node:sqlite` |
| External accounts | At least one vision source | Defaults to Antigravity CLI (key-free); can also configure Gemini / Anthropic API key or any OpenAI compatible endpoint; can borrow this machine's other harness CLI login |

## Installation
```bash
dsh plugin --profile web add github:liustack/modlens
```

## Configuration
Config file at `~/.modlens/config.json` (0600 permission, keys automatically masked when rendered via `modlens config show`). Commonly writable keys:

| Config | Type | Description | Default |
|---|---|---|---|
| `provider` | string | Vision engine preference name (when unspecified, all ready engines form failover chain) | `antigravity-cli` (Antigravity CLI) |
| `providers.<name>.apiKey` | string | API key for that engine; `<name>` ∈ `gemini-api` / `openai` / `anthropic`. `modlens config set <name>.apiKey` without value triggers hidden input | none |
| `providers.<name>.baseUrl` | string | Custom API endpoint; `openai` must be explicitly configured (no default to avoid misrouting to OpenAI official) | engine's own default |
| `providers.<name>.model` | string | Model name used by default for that engine | engine's own default |
| `providers.<name>.proxy` | string | Dedicated proxy URL for that engine, falls back to top-level `proxy`, then to `HTTPS_PROXY` / `HTTP_PROXY` | none |
| `providers.<name>.extraBody` | object | JSON merged into API request body (commonly used to disable thinking, e.g. `{"thinking":{"type":"disabled"}}`); reserved fields: `contents` / `messages` / `model` / schema etc. cannot be overridden | none |
| `providers.<name>.structuredOutput` | boolean | Let OpenAI compatible endpoints self-enforce output with `response_format: json_schema`; default false (some gateways will 400) | `false` |
| `proxy` | string | Fallback proxy URL for all API providers; remote image download (SSRF-guarded) unaffected | none |
| `guards.denyModels` | string array | glob pattern list; if current model matches, refuse to start vision engine (make way for native vision models) | `[]` |
| `guards.allowModels` | string array | Non-empty enters "whitelist mode": only matched models will invoke engine, others rejected | `[]` |
| `guards.denyWhenUnknown` | boolean | Whether to deny reading when current model cannot be identified (default allow, to avoid mistakenly blocking text models) | `false` |
| `reuse.claude` / `codex` / `opencode` / `pi` / `grok` | boolean | Whether to allow borrowing that harness' login on this machine for reading; `claude` defaults to true (backward compatible) | `claude=true` / others unasked |
| `MODLENS_MODEL` / `MODLENS_HARNESS` / `MODLENS_DSH_CLI` | env var | Directly tell modlens current model / host / CLI path, overriding detection | none |

## FAQ

**Q: Who is modlens for? Why do regular DeepSeek/GLM models need it?**

A: The main chat models like DeepSeek-V4 and GLM under the default dsh host are pure text and cannot directly read pasted images. modlens converts these images into structured text evidence through an external vision engine, then feeds them back to the text model, making "screenshot + question" a complete conversation.

**Q: Is it usable after installation by default, or do I need to configure an API key myself?**

A: Defaults to Antigravity CLI (key-free, browser login once), zero config needed. For faster results, configure a free Gemini key typically completes in 5-10 seconds; other OpenAI compatible endpoints (DashScope, SiliconFlow, OpenRouter, self-hosted vLLM/Ollama, etc.) can also connect directly.

**Q: What changes will I see after installation? Which model do I need to select?**

A: The model selector will show additional entries with "(modlens vision)" suffix, like DeepSeek-V4-Flash (modlens vision), DeepSeek-V4-Pro (modlens vision). Switch to these entries and paste images, thumbnails directly visible, experience close to Codex App. Not switching also works, image goes as path into input box.

**Q: Which engine processes an image? Which account's quota gets deducted?**

A: When unspecified, all configured engines form a failover chain: API-class fast lanes attempt first, agent CLI as fallback; each result's `meta.attempts` lists attempt sequence, `meta.warnings` indicates which harness login was reused and whose quota was deducted, never silent quota deduction.

**Q: What does modlens recover-paste do?**

A: Claude Code, Pi, OpenCode these terminals by default store pasted images directly in session storage without falling to ordinary temp files. recover-paste reads those pasted bytes from these harness' local storage and saves them to a secure directory (0700). OpenCode recovery requires Node 22.13+ providing node:sqlite.

**Q: How do I troubleshoot when errors occur?**

A: Run `modlens doctor`, it reports Node version, which providers are ready, which will be selected, which harness was detected, all local checks, no quota consumption, no network requests. Error messages usually include fix commands, e.g., when key is missing it tells you directly `modlens config set <provider>.apiKey`.

**Q: What remains after uninstallation?**

A: Delete this plugin package to uninstall. What stays on machine is only `~/.modlens/config.json` (your explicitly configured keys, endpoints, reuse authorizations) and the recovery directory under `~/.modlens/`; not placed into dsh hooks, also doesn't touch any harness configuration, host itself can seamlessly revert to original state.

## Getting Started Difficulty
Entry level — one dsh installation command plus a default engine (Antigravity CLI key-free browser login) can get it running; wanting "advanced configuration" (proxy, custom OpenAI endpoint, borrowing other harness credentials) requires reading the configuration manual.

## Known Issues & Limitations
- Kimi Code CLI lacks server-side enforcement like `--json-schema`, can only rely on prompt templates + fault-tolerant parsing; and because kimi scans the shared skill directory, there's possibility of being recursively triggered by modlens itself (mitigated via `MODLENS_INSIDE_KIMI_CLI` and `--skills-dir` pointing to empty directory, but whether model takes that path is model's own decision, occasionally hard to reproduce)
- `openai` engine doesn't enable `response_format: json_schema` by default because some compatible gateways don't recognize this field and 400; when server-side enforcement needed explicitly `modlens config set openai.structuredOutput true`
- Remote image download has 25 MB limit (`src/imageInput.ts` `MAX_REMOTE_IMAGE_BYTES`), covers a dense screenshot or high-res photo, but large volume originals will be rejected
- Paste file supports png/jpeg/gif/webp/heic/heif (others like bmp / svg / raw not accepted); validated through file magic bytes, doesn't trust extension names
- Kimi / Codex / OpenCode / Claude CLI routes can only read local path files; remote URLs must use API-class engines (gemini-api / openai / anthropic)
- Antigravity CLI's free quota is a weekly shared bucket (desktop App / CLI / SDK combined), concurrent sub-agents will deplete quickly (issue docs explicitly state, need to wait for reset or switch to `gemini-api`)

---

This document is auto-generated by [deepseek-plugin.org](https://deepseek-plugin.org). HTML page: [modlens](https://deepseek-plugin.org/plugins/liustack/modlens)
Wiki generated by AI (model: `MiniMax-M2.5`)
