@liustack/modlens 3.14.0 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +44 -0
- package/README.zh-CN.md +56 -12
- package/dist/main.js +1 -1
- package/docs/cli.md +2 -0
- package/docs/cli.zh-CN.md +98 -0
- package/docs/harness-setup.md +2 -0
- package/docs/harness-setup.zh-CN.md +67 -0
- package/docs/output-schema.md +2 -0
- package/docs/output-schema.zh-CN.md +74 -0
- package/docs/security.md +2 -0
- package/docs/security.zh-CN.md +43 -0
- package/docs/troubleshooting.md +2 -0
- package/docs/troubleshooting.zh-CN.md +210 -0
- package/dsh/index.js +154 -54
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- package/skills/modlens/references/configure.md +2 -0
- package/skills/modlens/references/configure.zh-CN.md +177 -0
- package/skills/modlens/references/runtime.md +1 -1
- package/skills/modlens/scripts/run.ps1 +1 -1
- package/skills/modlens/scripts/run.sh +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.15.0 - 2026-08-14
|
|
4
|
+
|
|
5
|
+
- **dsh: the vision wrap discovers every eligible provider route ([#29](https://github.com/liustack/modlens/issues/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.
|
|
6
|
+
|
|
3
7
|
## 3.14.0 - 2026-08-14
|
|
4
8
|
|
|
5
9
|
- **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.
|
package/README.md
CHANGED
|
@@ -63,6 +63,50 @@ The install also inventories vision reachable through your other local harness C
|
|
|
63
63
|
|
|
64
64
|
Once installed, just chat. Paste an image or drop a path, ask anything, and the skill triggers on its own: the image goes to a vision engine and the answer comes back grounded in what it read.
|
|
65
65
|
|
|
66
|
+
## Vision engines: five built-in providers, four reusable CLIs, one failover chain
|
|
67
|
+
|
|
68
|
+
ModLens does not depend on any single vision service. Nine sources of vision in total: five built-in providers, any one of which is enough, plus four local agent CLIs whose logins can be reused. The built-ins:
|
|
69
|
+
|
|
70
|
+
| Provider | What it needs | Speed per read | Good for |
|
|
71
|
+
| :-- | :-- | :-- | :-- |
|
|
72
|
+
| `gemini-api` | a free Gemini API key ([3 minutes, no card](https://aistudio.google.com)) | 5-10s | the recommended default |
|
|
73
|
+
| `openai` | any OpenAI-compatible endpoint (key + baseUrl + model) | 5-10s | qwen-vl, GLM, self-hosted gateways |
|
|
74
|
+
| `anthropic` | an Anthropic API key | 5-10s | machines already holding one |
|
|
75
|
+
| `antigravity-cli` | the free `agy` CLI, one browser sign-in, no key | 15-45s | zero-signup starts |
|
|
76
|
+
| `claude-cli` | a signed-in Claude Code | 20-45s | riding your existing Claude subscription |
|
|
77
|
+
|
|
78
|
+
Without a pinned provider, every configured engine forms one failover chain: the fast API providers try first, the agent CLIs back them up, the first good result wins, and `meta.attempts` records every attempt so a fallback is never silent.
|
|
79
|
+
|
|
80
|
+
### `openai` is a universal socket, not just OpenAI
|
|
81
|
+
|
|
82
|
+
Any endpoint speaking the OpenAI chat-completions protocol with image input plugs straight in — that covers most of the vision-model world:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
modlens config set openai.baseUrl https://dashscope.aliyuncs.com/compatible-mode/v1 # qwen-vl
|
|
86
|
+
modlens config set openai.apiKey <key>
|
|
87
|
+
modlens config set openai.model qwen3-vl-plus
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The same three keys work for GLM's open platform, SiliconFlow, OpenRouter, a self-hosted vLLM/Ollama, or any gateway of your own. If your favorite vision model has an OpenAI-compatible API, ModLens can drive it.
|
|
91
|
+
|
|
92
|
+
### Reusing what your machine already has
|
|
93
|
+
|
|
94
|
+
Two more sources of vision need zero new keys, each behind one explicit consent recorded in config:
|
|
95
|
+
|
|
96
|
+
- **The harness you are talking in right now.** Running inside Claude Code with a subscription signed in? `claude-cli` reads images through it out of the box. The install flow asks the same question for whichever harness you install into.
|
|
97
|
+
- **Every other agent CLI on the machine.** `modlens doctor` discovers them, you grant per harness, and they join the same failover chain with no priority over your own keys. Every reused read is labeled in `meta.warnings` with whose quota it spent, so nothing is ever silently billed:
|
|
98
|
+
|
|
99
|
+
| Reused CLI | What it needs | Grant with | Rides as |
|
|
100
|
+
| :-- | :-- | :-- | :-- |
|
|
101
|
+
| Codex | a signed-in Codex CLI with a vision model | `config set reuse.codex true` | agent lane, 15-45s |
|
|
102
|
+
| OpenCode | a vision model configured in OpenCode | `config set reuse.opencode true` | agent lane, 15-45s |
|
|
103
|
+
| Pi | model credentials held by Pi | `config set reuse.pi true` | an API key upgrades to the 5-10s inline lane, OAuth drives Pi itself |
|
|
104
|
+
| Grok | a signed-in Grok CLI (SuperGrok) | `config set reuse.grok true` | agent lane, 15-45s |
|
|
105
|
+
|
|
106
|
+
### Picking and routing
|
|
107
|
+
|
|
108
|
+
Two knobs: `modlens config set provider <name>` states a preference (the chain still backs it up), `-p <name>` pins exactly one with no fallback. Machines behind a proxy set `HTTPS_PROXY` or `modlens config set proxy <url>` and the API providers route through it. Details: the [CLI manual](docs/cli.md) for defaults and flags, [Configuration](skills/modlens/references/configure.md) for every key, and [Security](docs/security.md) for who fetches what on remote URLs.
|
|
109
|
+
|
|
66
110
|
## See it work
|
|
67
111
|
|
|
68
112
|
Unedited runs, all driving a text-only DeepSeek-V4-Flash.
|
package/README.zh-CN.md
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="./README.md">English</a> ·
|
|
13
|
-
<a href="docs/troubleshooting.md">故障排查</a> ·
|
|
14
|
-
<a href="skills/modlens/references/configure.md">配置</a> ·
|
|
15
|
-
<a href="docs/output-schema.md">输出契约</a> ·
|
|
16
|
-
<a href="docs/security.md">安全</a> ·
|
|
13
|
+
<a href="docs/troubleshooting.zh-CN.md">故障排查</a> ·
|
|
14
|
+
<a href="skills/modlens/references/configure.zh-CN.md">配置</a> ·
|
|
15
|
+
<a href="docs/output-schema.zh-CN.md">输出契约</a> ·
|
|
16
|
+
<a href="docs/security.zh-CN.md">安全</a> ·
|
|
17
17
|
<a href="https://github.com/liustack/modsearch">ModSearch(联网)</a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
@@ -34,7 +34,7 @@ DeepSeek 和 GLM 没有视觉能力,无法进行图片识别。ModLens 借助
|
|
|
34
34
|
|
|
35
35
|
## 亮点
|
|
36
36
|
|
|
37
|
-
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@latest`,dsh 背后的纯文本 DeepSeek 模型即可通过原生 `read_image` 工具读图。如果 dsh 提示 `declares no dsh.bundle`,是 pnpm 的发布冷静期把版本压旧了,一行配置可解,见[故障排查](docs/troubleshooting.md#dsh
|
|
37
|
+
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@latest`,dsh 背后的纯文本 DeepSeek 模型即可通过原生 `read_image` 工具读图。如果 dsh 提示 `declares no dsh.bundle`,是 pnpm 的发布冷静期把版本压旧了,一行配置可解,见[故障排查](docs/troubleshooting.zh-CN.md#dsh-提示-declares-no-dshbundle--installed-as-a-plain-dependency)。要粘贴识图,把模型选择器切到插件新增的两个条目之一:**`DeepSeek-V4-Flash (modlens vision)`** 或 **`DeepSeek-V4-Pro (modlens vision)`**,贴图放行、发请求时转成证据(你的消息保留原生缩略图)、仍由原 DeepSeek 路由回答。包装只覆盖 DeepSeek 与 GLM 的文本模型,两家自己的视觉型号自动排除。
|
|
38
38
|
|
|
39
39
|
**直接粘贴图片识别** 无需先保存成文件再提供路径。
|
|
40
40
|
|
|
@@ -65,12 +65,56 @@ agy # 浏览器完成
|
|
|
65
65
|
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@latest
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
装完即有 `read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.md)。
|
|
68
|
+
装完即有 `read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
|
|
69
69
|
|
|
70
70
|
## 用法
|
|
71
71
|
|
|
72
72
|
装好之后不需要记任何命令。正常聊天,粘贴图片或给出图片路径,提问即可,skill 自动触发:图片交给视觉引擎,答案基于读到的内容返回。
|
|
73
73
|
|
|
74
|
+
## 视觉引擎:五个内置 provider,四家可复用 CLI,一条故障转移链
|
|
75
|
+
|
|
76
|
+
ModLens 不绑定任何单一视觉服务。视觉来源一共九个:五个内置 provider(配好任意一个就能用),加四家本机 agent CLI 的登录可以复用。先看内置的:
|
|
77
|
+
|
|
78
|
+
| Provider | 需要什么 | 单次识别耗时 | 适合谁 |
|
|
79
|
+
| :-- | :-- | :-- | :-- |
|
|
80
|
+
| `gemini-api` | 免费 Gemini key([三分钟领取,无需信用卡](https://aistudio.google.com)) | 5-10 秒 | 推荐默认 |
|
|
81
|
+
| `openai` | 任意 OpenAI 兼容端点(key + baseUrl + model) | 5-10 秒 | qwen-vl、GLM、自建网关 |
|
|
82
|
+
| `anthropic` | Anthropic API key | 5-10 秒 | 手上已有 key 的机器 |
|
|
83
|
+
| `antigravity-cli` | 免费的 `agy` CLI,浏览器登录一次,无需 key | 15-45 秒 | 完全免注册起步 |
|
|
84
|
+
| `claude-cli` | 已登录的 Claude Code | 20-45 秒 | 复用现有 Claude 订阅 |
|
|
85
|
+
|
|
86
|
+
不钉死 provider 时,所有配好的引擎组成一条故障转移链:API 快车道先试,agent CLI 兜底,第一个可用结果胜出,`meta.attempts` 记录每次尝试,回退永远不是无声的。
|
|
87
|
+
|
|
88
|
+
### `openai` 是万能接口,不只是 OpenAI
|
|
89
|
+
|
|
90
|
+
任何讲 OpenAI chat-completions 协议、支持图片输入的端点都能直接插上,这基本覆盖了视觉模型的大半个世界:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
modlens config set openai.baseUrl https://dashscope.aliyuncs.com/compatible-mode/v1 # qwen-vl
|
|
94
|
+
modlens config set openai.apiKey <key>
|
|
95
|
+
modlens config set openai.model qwen3-vl-plus
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
同样三个键,换成 GLM 开放平台、SiliconFlow、OpenRouter、自建 vLLM/Ollama 或你自己的网关都一样。你常用的视觉模型只要有 OpenAI 兼容 API,ModLens 就能驱动它。
|
|
99
|
+
|
|
100
|
+
### 复用你机器上已有的东西
|
|
101
|
+
|
|
102
|
+
还有两处现成的视觉能力,一个新 key 都不用配,每家都在你明确同意后才启用:
|
|
103
|
+
|
|
104
|
+
- **你正在对话的这个 harness 本身。**在登录了订阅的 Claude Code 里用?`claude-cli` 开箱即可借它读图。装进哪个 harness,安装流程就会问哪个 harness 的授权。
|
|
105
|
+
- **机器上其他的 agent CLI。**`modlens doctor` 会逐个发现,你按家授权,它们与你自己的 key 平级入链,不插队。每次复用都在 `meta.warnings` 里标明花的是谁的额度,绝不无声扣费:
|
|
106
|
+
|
|
107
|
+
| 复用来源 | 需要什么 | 授权命令 | 走哪条道 |
|
|
108
|
+
| :-- | :-- | :-- | :-- |
|
|
109
|
+
| Codex | 已登录且有视觉模型的 Codex CLI | `config set reuse.codex true` | agent 通道,15-45 秒 |
|
|
110
|
+
| OpenCode | OpenCode 里配好的视觉模型 | `config set reuse.opencode true` | agent 通道,15-45 秒 |
|
|
111
|
+
| Pi | Pi 持有的模型凭据 | `config set reuse.pi true` | API key 直接升级到 5-10 秒的快车道,OAuth 驱动 Pi 本体 |
|
|
112
|
+
| Grok | 已登录的 Grok CLI(SuperGrok) | `config set reuse.grok true` | agent 通道,15-45 秒 |
|
|
113
|
+
|
|
114
|
+
### 选择与路由
|
|
115
|
+
|
|
116
|
+
两个旋钮:`modlens config set provider <name>` 表达偏好(链继续兜底),`-p <name>` 钉死单个不回退。代理环境设 `HTTPS_PROXY` 或 `modlens config set proxy <url>`,API provider 自动走代理。细节见 [CLI 手册](docs/cli.zh-CN.md)(默认模型与参数)、[配置手册](skills/modlens/references/configure.zh-CN.md)(全部配置键)、[安全说明](docs/security.zh-CN.md)(远程 URL 由谁抓取)。
|
|
117
|
+
|
|
74
118
|
## 实测
|
|
75
119
|
|
|
76
120
|
以下均为原样记录,驱动的都是纯文本的 DeepSeek-V4-Flash。
|
|
@@ -100,12 +144,12 @@ Codex 桌面 App 中识别一张推文截图。作者、配文、照片内容(
|
|
|
100
144
|
| 文档 | 适用场景 |
|
|
101
145
|
| :------------------------------------------------- | :----------------------------------------- |
|
|
102
146
|
| [安装手册](INSTALL.md) | 一步步安装 skill(为 agent 编写) |
|
|
103
|
-
| [CLI 手册](docs/cli.md) | skill 所驱动的 CLI:参数、配置与体检 |
|
|
104
|
-
| [故障排查](docs/troubleshooting.md) | 命令报错,查成因和解法 |
|
|
105
|
-
| [配置手册](skills/modlens/references/configure.md) | 配置 key、切换 provider、排查配置 |
|
|
106
|
-
| [输出契约](docs/output-schema.md) | 解析 JSON 或构建下游工具 |
|
|
107
|
-
| [宿主接入](docs/harness-setup.md) | 在 Codex、Claude Code、Pi、OpenCode 中配置 |
|
|
108
|
-
| [安全说明](docs/security.md) | 恢复文件的权限、图片内容作为不可信输入 |
|
|
147
|
+
| [CLI 手册](docs/cli.zh-CN.md) | skill 所驱动的 CLI:参数、配置与体检 |
|
|
148
|
+
| [故障排查](docs/troubleshooting.zh-CN.md) | 命令报错,查成因和解法 |
|
|
149
|
+
| [配置手册](skills/modlens/references/configure.zh-CN.md) | 配置 key、切换 provider、排查配置 |
|
|
150
|
+
| [输出契约](docs/output-schema.zh-CN.md) | 解析 JSON 或构建下游工具 |
|
|
151
|
+
| [宿主接入](docs/harness-setup.zh-CN.md) | 在 Codex、Claude Code、Pi、OpenCode 中配置 |
|
|
152
|
+
| [安全说明](docs/security.zh-CN.md) | 恢复文件的权限、图片内容作为不可信输入 |
|
|
109
153
|
| [更新日志](CHANGELOG.md) | 查询版本变更 |
|
|
110
154
|
|
|
111
155
|
## 参与方式
|
package/dist/main.js
CHANGED
|
@@ -3737,7 +3737,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
3737
3737
|
}
|
|
3738
3738
|
return Number.parseInt(raw, 10);
|
|
3739
3739
|
}
|
|
3740
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.
|
|
3740
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.15.0");
|
|
3741
3741
|
program.command("analyze", { isDefault: true }).description("Analyze an image into structured JSON evidence (default command)").requiredOption("-i, --input <path|url>", "Input image path or https URL").option("-o, --output <path>", "Write result JSON to a file").option("-m, --model <name>", "Provider model name").option("-p, --provider <name>", `Vision provider (${listProviders().join(", ")})`).option("--prompt <text>", "Extra focus for this image").option("--timeout <ms>", "Provider timeout in milliseconds", "180000").option("--provider-bin <path>", "Provider binary path (default: agy)").option("--workdir <path>", "Working directory for the provider").option(
|
|
3742
3742
|
"--extra-body <json>",
|
|
3743
3743
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
package/docs/cli.md
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: 'CLI 手册:参数、故障转移链、guard 与 doctor 子命令、配置键'
|
|
3
|
+
read_when:
|
|
4
|
+
- 不经 skill 直接手动运行 CLI
|
|
5
|
+
- 查某个参数、默认模型或子命令
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ModLens CLI 手册
|
|
9
|
+
|
|
10
|
+
[English](cli.md) | 中文
|
|
11
|
+
|
|
12
|
+
skill 通过它的启动器驱动这个 CLI。本页讲的是直接手动运行。
|
|
13
|
+
|
|
14
|
+
## 直接使用
|
|
15
|
+
|
|
16
|
+
装好 skill 后不需要敲命令:粘贴图片或给出路径,随便提问,它会自动触发。手动运行:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
modlens -i screenshot.png # local image
|
|
20
|
+
modlens -i https://example.com/chart.png # remote image
|
|
21
|
+
modlens -i chart.png --prompt "focus on axes" # extra focus
|
|
22
|
+
modlens recover-paste # pull a pasted image into a file
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
输出是固定的 JSON 结构:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"image": "/path/to/screenshot.png",
|
|
30
|
+
"provider": "gemini-api",
|
|
31
|
+
"result": {
|
|
32
|
+
"summary": "A workflow diagram with four nodes connected by labeled arrows.",
|
|
33
|
+
"ocr": { "full_text": "/shaping\nBEFORE YOU BUILD\n...", "lines": [] },
|
|
34
|
+
"layout": { "regions": [{ "reading_order": 1, "type": "title", "text": "/shaping" }] },
|
|
35
|
+
"semantics": { "scene": "workflow diagram", "entities": [], "relations": [] },
|
|
36
|
+
"visual": { "dominant_colors": ["white", "black"], "style": "flat", "notes": [] },
|
|
37
|
+
"uncertainty": []
|
|
38
|
+
},
|
|
39
|
+
"meta": {
|
|
40
|
+
"generatedAt": "2026-08-06T12:00:00.000Z",
|
|
41
|
+
"model": "gemini-3.6-flash",
|
|
42
|
+
"conversationId": null,
|
|
43
|
+
"durationSeconds": 6.4,
|
|
44
|
+
"usage": { "promptTokenCount": 1234, "candidatesTokenCount": 567 },
|
|
45
|
+
"attempts": [{ "provider": "gemini-api", "ok": true, "durationSeconds": 6.4 }],
|
|
46
|
+
"warnings": []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`meta` 记录结果是怎么产生的:什么时间(`generatedAt`)、用了哪个 `model`、provider 有会话时的 `conversationId`、实际耗时 `durationSeconds`,以及 provider 上报的原始 `usage`(结构因 provider 而异,没有时为 `null`)。`attempts` 按顺序列出故障转移链尝试过的每个 provider 和失败原因。`warnings` 携带路由通知(故障转移、被忽略的 extraBody、自动模式下这次识别花了谁的额度)。
|
|
52
|
+
|
|
53
|
+
## 参数
|
|
54
|
+
|
|
55
|
+
`modlens analyze`(默认命令):
|
|
56
|
+
|
|
57
|
+
| 参数 | 含义 | 默认值 |
|
|
58
|
+
| :-- | :-- | :-- |
|
|
59
|
+
| `-i, --input <path\|url>` | 要分析的图片(必填) | |
|
|
60
|
+
| `-p, --provider <name>` | 钉死单个 provider,不回退 | 故障转移链(见下) |
|
|
61
|
+
| `-m, --model <name>` | provider 的模型 | 按 provider 而定(见下) |
|
|
62
|
+
| `-o, --output <path>` | 同时把 JSON 写入文件 | |
|
|
63
|
+
| `--prompt <text>` | 额外关注点 | |
|
|
64
|
+
| `--timeout <ms>` | provider 超时 | `180000` |
|
|
65
|
+
| `--provider-bin <path>` | provider 可执行文件路径 | `agy` / `claude` |
|
|
66
|
+
| `--workdir <path>` | provider 的工作目录 | 每次运行新建的隔离目录 |
|
|
67
|
+
| `--extra-body <json>` | 合并进 API 请求体的 JSON,如 `'{"thinking":{"type":"disabled"}}'` | 配置里该 provider 的 `extraBody` |
|
|
68
|
+
|
|
69
|
+
`--extra-body` 是厂商专属开关的通道,最常见的用途是关掉 thinking。它作用于三个 API provider,并在该次运行中替换配置里的 `extraBody`。各厂商的具体写法和它拒绝改动的字段见[配置手册](../skills/modlens/references/configure.zh-CN.md)。
|
|
70
|
+
|
|
71
|
+
`-m` 的默认模型取决于 provider:
|
|
72
|
+
|
|
73
|
+
| Provider | 默认模型 |
|
|
74
|
+
| :-- | :-- |
|
|
75
|
+
| `antigravity-cli`(默认) | `gemini-3.6-flash-low` |
|
|
76
|
+
| `gemini-api` | `gemini-3.6-flash` |
|
|
77
|
+
| `anthropic` | `claude-haiku-4-5-20251001` |
|
|
78
|
+
| `claude-cli` | `haiku` |
|
|
79
|
+
| `openai` | 无,必须传 `-m` |
|
|
80
|
+
|
|
81
|
+
`modlens recover-paste`:
|
|
82
|
+
|
|
83
|
+
| 参数 | 含义 | 默认值 |
|
|
84
|
+
| :-- | :-- | :-- |
|
|
85
|
+
| `--count <n>` | 恢复最近几张粘贴的图片 | `1` |
|
|
86
|
+
| `--out-dir <path>` | 恢复出的图片写到哪里 | 每次运行新建的私有 `<tmpdir>/modlens-paste-*` |
|
|
87
|
+
| `--session <id>` | 用会话 id 精确定位 | 自动检测 |
|
|
88
|
+
| `--transcript <path>` | 显式指定 transcript 的 `.jsonl` 或 `.db`(覆盖 `--session`) | |
|
|
89
|
+
| `--harness <name>` | 强制指定存储范围:`claude-code`、`pi`、`opencode`、`none` | 自动检测 |
|
|
90
|
+
| `--cwd <path>` | 粘贴图片时所在的项目目录 | 当前目录 |
|
|
91
|
+
|
|
92
|
+
共五个 provider:`antigravity-cli`(免 key)、`gemini-api`(最快的免费通道)、`openai`(任意 OpenAI 兼容的多模态端点)、`anthropic`,以及 `claude-cli`(复用你现有的 Claude 订阅)。不带 `-p` 时,一次运行会依次尝试每个已配好的 provider:API 快车道(inline API provider,不启动 agent、直接调 API 的引擎)先试(5-10 秒),agent 类兜底,第一个可用结果胜出,其余尝试记录在 `meta.attempts` 里。通过 `reuse.<harness>` 授权的 harness 会把复用来的引擎补进相同的区段(pi 的凭据算快车道,agent CLI 排在后面),不会插到你自己引擎的前面。细节和 `guards` 的 deny/allow 名单见[配置手册](../skills/modlens/references/configure.zh-CN.md)。
|
|
93
|
+
|
|
94
|
+
其他子命令:
|
|
95
|
+
|
|
96
|
+
- `modlens guard [--model <id>]`:判断当前激活的模型到底该不该运行引擎。退出码 0 表示放行,1 表示拒绝,判定结果以 JSON 输出。
|
|
97
|
+
- `modlens config <init|set|show>`:可用的键有 `provider`、`proxy`(API provider 的 HTTP/HTTPS 代理,也认 `HTTPS_PROXY`/`HTTP_PROXY`)、`reuse.<claude|codex|opencode|pi|grok>`、`guards.<denyModels|allowModels|denyWhenUnknown>`,以及 `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`。
|
|
98
|
+
- `modlens doctor`:报告 Node 与 node:sqlite、各 provider 的就绪状态、本机的故障转移链、检测到的 harness、guard 规则和一次现场判定,以及 Reuse 一节里按 harness 的授权决定与发现的视觉能力。不花任何额度,`--json` 输出机器可读报告。
|
package/docs/harness-setup.md
CHANGED
|
@@ -8,6 +8,8 @@ read_when:
|
|
|
8
8
|
|
|
9
9
|
# Harness setup
|
|
10
10
|
|
|
11
|
+
English | [中文](harness-setup.zh-CN.md)
|
|
12
|
+
|
|
11
13
|
Where a pasted image ends up differs per harness, and modlens takes a different route in each. `recover-paste` detects which harness it runs inside (process ancestry, then environment fingerprints) and reads only that harness's storage.
|
|
12
14
|
|
|
13
15
|
## Codex
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: '宿主接入:图片在 Codex、Claude Code、Pi、OpenCode 中如何抵达模型'
|
|
3
|
+
read_when:
|
|
4
|
+
- 在某个具体的编码 agent 里安装配置 modlens
|
|
5
|
+
- 粘贴的图片没有抵达模型
|
|
6
|
+
- 了解 recover-paste 在各 harness 里分别做什么
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 宿主接入
|
|
10
|
+
|
|
11
|
+
[English](harness-setup.md) | 中文
|
|
12
|
+
|
|
13
|
+
粘贴的图片最终落在哪里,每个 harness 都不一样,modlens 在每个 harness 里走的路线也不同。`recover-paste` 会检测自己运行在哪个 harness 里(先看进程祖先,再看环境变量指纹),只读取该 harness 的存储。
|
|
14
|
+
|
|
15
|
+
## Codex
|
|
16
|
+
|
|
17
|
+
粘贴的图片会落成真实的临时文件,消息里带着形如 `<image name=[Image #1] path="/tmp/xxxx.png">` 的标签。skill 直接从标签里读出路径。`recover-paste` 检测到 Codex 后会拒绝执行,并把你指回这个标签。
|
|
18
|
+
|
|
19
|
+
纯文本模型有一个坑:一旦 `models.json` 声明了 `input_modalities: ["text"]`,Codex TUI 会直接拦下 Ctrl+V 粘贴。改为把文件拖进终端、手动输入路径,或使用 `codex exec -i image.png "..."`。
|
|
20
|
+
|
|
21
|
+
## Claude Code、Pi、OpenCode
|
|
22
|
+
|
|
23
|
+
这三家都不像 Codex 那样递给模型一个可用的临时文件路径(较新的 Claude Code 版本确实会把粘贴写进自己的 `~/.claude/image-cache/`,但只在终端入口以路径行的形式注入),不过三者都会在网关剥离图片之前,把用户消息完整存在本地:
|
|
24
|
+
|
|
25
|
+
| Harness | 存储位置 | 说明 |
|
|
26
|
+
| :-- | :-- | :-- |
|
|
27
|
+
| Claude Code | `~/.claude/projects/<slug>/<session>.jsonl` | 图片以 base64 存储。注入的 `CLAUDE_CODE_SESSION_ID` 可精确定位当前 session |
|
|
28
|
+
| Pi | `~/.pi/agent/sessions/--<encoded-cwd>--/*.jsonl` | 结构与 Claude Code 相同 |
|
|
29
|
+
| OpenCode | `~/.local/share/opencode/opencode.db` | SQLite,图片以 data URL 存储(通过 `node:sqlite` 读取) |
|
|
30
|
+
|
|
31
|
+
在 Claude Code 里通过 `ANTHROPIC_BASE_URL` 接入纯文本模型时,粘贴的图片要么变成一个不带路径的 `[Unsupported Image]` 占位符(宽松的网关),要么直接让请求报错([#62009](https://github.com/anthropics/claude-code/issues/62009))。图片字节并没有丢,`recover-paste` 取回的就是它。
|
|
32
|
+
|
|
33
|
+
## skill 的存放位置
|
|
34
|
+
|
|
35
|
+
| Harness | skill 读取位置 |
|
|
36
|
+
| :-- | :-- |
|
|
37
|
+
| Claude Code | `~/.claude/skills/` |
|
|
38
|
+
| Codex | `~/.codex/skills/` |
|
|
39
|
+
| Pi、OpenCode | `~/.agents/skills/` |
|
|
40
|
+
|
|
41
|
+
这些位置都支持符号链接,把 skill 目录链接一次,每个 agent 用的就都是最新版本。
|
|
42
|
+
|
|
43
|
+
## 平台支持
|
|
44
|
+
|
|
45
|
+
macOS 和 Linux 完整支持,并在 CI 上以 Node 22 和 24 验证。
|
|
46
|
+
|
|
47
|
+
Windows 跑同一套 CI 矩阵。那里没有 `ps`,检测会跳过进程祖先这一步,退回到上面的环境变量指纹,所以一个什么指纹都不设的 harness 会被判为未检出(用 `--harness` 或 `MODLENS_HARNESS` 强制指定)。OpenCode 的粘贴恢复在 Windows 上有覆盖,包括 [#11](https://github.com/liustack/modlens/issues/11) 里的路径分隔符归一化:opencode 记录的 `session.directory` 用正斜杠,而那里的 `path.resolve` 返回反斜杠,匹配前两边都会归一化。JSONL 存储(Claude Code、Pi)以 `os.homedir()` 和各 harness 自己的磁盘 slug 为键,在 POSIX 上验证。外部引擎(Antigravity CLI、Claude CLI)只在有 Windows 版本的平台上运行。
|
|
48
|
+
|
|
49
|
+
## 网关配置
|
|
50
|
+
|
|
51
|
+
OpenCode 接 DeepSeek:执行 `opencode auth login`,选择 DeepSeek 并粘贴 key(会存进 `~/.local/share/opencode/auth.json`),然后在 `~/.config/opencode/opencode.jsonc` 里把默认模型设为 `deepseek/deepseek-v4-flash`。Pi 从 `~/.pi/agent/auth.json` 读取它的 key。
|
|
52
|
+
|
|
53
|
+
## DeepSeek Harness(dsh)
|
|
54
|
+
|
|
55
|
+
dsh 与其他 harness 不同:modlens 以原生工具的形式接入,而不是靠提示词触发的 skill。本包自身就是一个 dsh bundle,一条命令即可装进某个 profile:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@latest
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
这会注册一个 `read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
|
|
62
|
+
|
|
63
|
+
### 粘贴转路径(paste-to-path,web profile)
|
|
64
|
+
|
|
65
|
+
过去在 dsh Web UI 里,**纯文本模型**下粘贴图片会死在图片准入检查这一步。插件现在带了一个浏览器端半边(由 dsh 的客户端插件系统自动加载),恰好在这种情况下接管粘贴:图片字节发到插件在 dsh web 服务器上的 `/modlens/paste` 路由(仅回环地址,校验 magic byte,上限 25 MB),落成一个私有临时文件,输入框收到的则是纯文本的文件路径。这与 Pi、OpenCode、Claude Code 递给模型的形态一致,也正是 modlens skill 和 `read_image` 工具的首要触发条件。消息里不带图片附件,准入检查根本不会触发。
|
|
66
|
+
|
|
67
|
+
接管是有条件的:当选中的模型是 `(modlens vision)` 变体或已知的视觉模型时,原生粘贴流程不受影响(变体在发请求时转换且保留缩略图,视觉模型则自己读图)。在插件配置行里设 `pasteToPath: false` 可整体关掉这个功能。
|
package/docs/output-schema.md
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: '输出契约:每次识别返回的 JSON 结构、result 字段与 meta'
|
|
3
|
+
read_when:
|
|
4
|
+
- 解析 modlens 输出或在它之上构建工具
|
|
5
|
+
- 查 meta.attempts 和 meta.warnings 的含义
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ModLens 输出契约(v2)
|
|
9
|
+
|
|
10
|
+
[English](output-schema.md) | 中文
|
|
11
|
+
|
|
12
|
+
CLI 向 stdout 打印一个 JSON 对象:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"image": "/abs/path/or/url",
|
|
17
|
+
"provider": "antigravity-cli",
|
|
18
|
+
"result": { "...": "see below" },
|
|
19
|
+
"meta": {
|
|
20
|
+
"generatedAt": "2026-08-01T12:00:00.000Z",
|
|
21
|
+
"model": "gemini-3.6-flash-low",
|
|
22
|
+
"conversationId": "string|null",
|
|
23
|
+
"durationSeconds": 25.4,
|
|
24
|
+
"usage": {},
|
|
25
|
+
"attempts": [{ "provider": "antigravity-cli", "ok": true, "durationSeconds": 25.4 }],
|
|
26
|
+
"warnings": []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`meta.attempts` 按顺序列出这次运行中故障转移链尝试过的每个 provider,失败时附带 `error` 字符串。`meta.warnings` 携带路由通知:故障转移、被忽略的 `extraBody`,以及自动模式下这次识别花了谁的额度。
|
|
32
|
+
|
|
33
|
+
只要 provider 支持,`result` 就由 JSON schema 强制约束(agent CLI 走 `--json-schema`,API provider 走 response-schema 字段或预填模板),CLI 返回前还会自己校验一遍结构,所以结构损坏的结果会触发故障转移,不会到你手上:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"summary": "string",
|
|
38
|
+
"ocr": {
|
|
39
|
+
"full_text": "string",
|
|
40
|
+
"lines": [
|
|
41
|
+
{ "text": "string", "language": "string (optional)" }
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"layout": {
|
|
45
|
+
"regions": [
|
|
46
|
+
{
|
|
47
|
+
"type": "title|subtitle|paragraph|list|table|chart|form|code|image|icon|other",
|
|
48
|
+
"reading_order": 1,
|
|
49
|
+
"text": "string"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"semantics": {
|
|
54
|
+
"scene": "string",
|
|
55
|
+
"intent": "string (optional)",
|
|
56
|
+
"entities": [
|
|
57
|
+
{ "name": "string", "type": "string", "evidence": "string (optional)" }
|
|
58
|
+
],
|
|
59
|
+
"relations": [
|
|
60
|
+
{ "subject": "string", "predicate": "string", "object": "string" }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"visual": {
|
|
64
|
+
"dominant_colors": ["string"],
|
|
65
|
+
"style": "string",
|
|
66
|
+
"notes": ["string"]
|
|
67
|
+
},
|
|
68
|
+
"uncertainty": ["string"]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
必填字段:`summary`、`ocr`、`layout`、`semantics`、`visual`、`uncertainty`,也就是每一个顶层字段,`visual` 也不例外。(早期文档把 `visual` 写成可选,但强制执行的 schema 一直要求它,请以 schema 为准。)
|
|
73
|
+
|
|
74
|
+
相对 v1 的变化:删掉了像素级 `bbox` 坐标和数值型 `confidence` 分数。视觉模型会凭空编造这两样,v2 不再假装提供。`layout.regions[].type` 新增了 `code`。
|
package/docs/security.md
CHANGED
|
@@ -8,6 +8,8 @@ read_when:
|
|
|
8
8
|
|
|
9
9
|
# Security
|
|
10
10
|
|
|
11
|
+
English | [中文](security.zh-CN.md)
|
|
12
|
+
|
|
11
13
|
## Recovered images are private
|
|
12
14
|
|
|
13
15
|
Images pulled out of session storage are written 0600 into a 0700 directory. By default that directory is a fresh, unpredictable `<tmpdir>/modlens-paste-*` minted per run, so nobody on a shared machine can pre-create a known path (`recursive` mkdir leaves an existing directory's mode alone) and read the bytes. A pasted screenshot can hold anything. An explicit `--out-dir` is honoured but refused when unsafe: it must be a real directory, not a symlink, owned by you, with no group or world access.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: '安全:modlens 会运行什么、恢复出的图片如何受保护、图片内容属于不可信输入'
|
|
3
|
+
read_when:
|
|
4
|
+
- 审查这个工具会在你的机器上做什么
|
|
5
|
+
- 在共享机器上恢复粘贴的图片
|
|
6
|
+
- 判断视觉引擎的报告可信到什么程度
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 安全
|
|
10
|
+
|
|
11
|
+
[English](security.md) | 中文
|
|
12
|
+
|
|
13
|
+
## 恢复出的图片是私有的
|
|
14
|
+
|
|
15
|
+
从会话存储里取出的图片以 0600 权限写入 0700 目录。默认这个目录是每次运行新建的、不可预测的 `<tmpdir>/modlens-paste-*`,共享机器上没人能预先创建一个已知路径来读取字节(`recursive` 的 mkdir 不会改动已存在目录的权限模式)。一张粘贴的截图里可能有任何东西。显式传入的 `--out-dir` 会被接受,但不安全时会被拒绝:它必须是真实目录,不是符号链接,归你所有,组和其他用户无任何权限。
|
|
16
|
+
|
|
17
|
+
恢复同样限定在单个项目内:检查的是 transcript 里记录的工作目录,不只是目录名,因为目录 slug 会撞车(`/tmp/a.b` 和 `/tmp/a-b` 生成同一个 slug)。相邻项目的图片绝不会被交出去。
|
|
18
|
+
|
|
19
|
+
## 传给引擎的权限
|
|
20
|
+
|
|
21
|
+
ModLens 调用 `agy` 时带上 `--dangerously-skip-permissions`,因为某些环境下 prompt 模式不带它就会失败。prompt 把 agent 限制为只读交给它的那一张图片,并指示它把图片内容严格当作数据。
|
|
22
|
+
|
|
23
|
+
`claude-cli` provider 只带 `--allowedTools Read` 运行,因此它能读本地文件,别的什么都做不了。
|
|
24
|
+
|
|
25
|
+
两个子进程 provider 还都运行在一个用完即弃的目录里,每次调用新建,结束后删除。本地图片时,目录里只有那张图片的一份私有副本,别无他物,而且是真实拷贝,绝不用硬链接,provider 往自己的临时路径写东西也碰不到原文件。远程图片时目录是空的,agent 把文件下载进去。没有这层隔离,图片里的文字就可能引导一个权限宽泛的 agent 去读原图旁边的文件,或读调用者恰好所在的项目。传 `--workdir` 即放弃这层隔离,在你指定的位置运行。
|
|
26
|
+
|
|
27
|
+
这是在缩小暴露面,不是操作系统沙箱:agent 仍然能读绝对路径、访问网络、启动进程。把它当作更收紧的默认值,而不是安全边界。不信任的图片优先用 `-p gemini-api`,它自己下载字节,不运行任何本地 agent。远程 URL 本来就优先走内联这一侧:远程 URL 的故障转移链先试内联的 API provider,agent 排在最后。远程 URL 实际由谁抓取因 provider 而异,而只有本地下载能被本地防护覆盖:
|
|
28
|
+
|
|
29
|
+
| Provider | 远程 URL 由谁抓取 | 本地防护 |
|
|
30
|
+
| :-- | :-- | :-- |
|
|
31
|
+
| `gemini-api` | modlens 自己下载,字节内联发送 | 私有地址防护、文件头魔数(magic bytes)检查、25 MB 上限 |
|
|
32
|
+
| `openai`、`anthropic` | URL 传给厂商,由厂商抓取 | 本地无,适用厂商自己的抓取策略 |
|
|
33
|
+
| `antigravity-cli`、agent CLI | agent 自行抓取 | 本地无 |
|
|
34
|
+
|
|
35
|
+
所以私有地址防护、文件头魔数检查和大小上限保护的恰好是 modlens 亲自下载的路径:每一次本地文件读取,加上 gemini-api 的远程抓取。显式 `-p` 钉死单个 provider,覆盖整条链。
|
|
36
|
+
|
|
37
|
+
## 图片内容是不可信输入
|
|
38
|
+
|
|
39
|
+
图片里的文字是不可信的,和网页一样。一张截图可能带着写给任何读取者的指令。prompt 里明确说了这一点,但那是缓解措施,不是保证:只分析你愿意打开的图片,图片来自别处时优先用沙箱化的工作目录。
|
|
40
|
+
|
|
41
|
+
## 证据,而非编造
|
|
42
|
+
|
|
43
|
+
引擎读不出来的内容会进入 `uncertainty`,而不是被脑补出来。v2 彻底去掉了像素坐标和置信度分数,因为这两个字段正是模型编造得最像真的。
|
package/docs/troubleshooting.md
CHANGED
|
@@ -8,6 +8,8 @@ read_when:
|
|
|
8
8
|
|
|
9
9
|
# Troubleshooting
|
|
10
10
|
|
|
11
|
+
English | [中文](troubleshooting.zh-CN.md)
|
|
12
|
+
|
|
11
13
|
Start with `modlens doctor`: it checks your Node version, which providers are ready, which one will be selected and why, and the detected harness, all without spending quota or making a network request. It catches most setup problems before you read any further.
|
|
12
14
|
|
|
13
15
|
Every message below is one modlens actually prints. Search this file for the words you saw.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: '故障排查:modlens 可能打印的每一条报错、成因与解法'
|
|
3
|
+
read_when:
|
|
4
|
+
- 运行失败了,报错信息看不明白
|
|
5
|
+
- recover-paste 什么都没找到,或找到了错的图片
|
|
6
|
+
- 判断一次失败属于配置问题、额度问题还是 bug
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 故障排查
|
|
10
|
+
|
|
11
|
+
[English](troubleshooting.md) | 中文
|
|
12
|
+
|
|
13
|
+
先跑 `modlens doctor`:它会检查你的 Node 版本、哪些 provider 已就绪、将选中哪一个及其原因,以及检测到的 harness,全程不消耗额度,也不发网络请求。大多数配置问题在你继续往下读之前就能被它查出来。
|
|
14
|
+
|
|
15
|
+
下面每条消息都是 modlens 实际会打印的。拿你看到的字眼在本文里搜索即可。
|
|
16
|
+
|
|
17
|
+
## Antigravity CLI 读不到已保存的登录令牌
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Antigravity CLI cannot read its stored login token.
|
|
21
|
+
|
|
22
|
+
On Linux this usually means the OS keyring is locked, which is normal for headless
|
|
23
|
+
sessions (agents, cron, systemd, SSH without a desktop login) ...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
agy 把令牌存在操作系统钥匙串里。钥匙串被锁定时,agy 会把自己报告为未登录,并尝试浏览器登录,而没有显示器时这个流程无法完成。三条出路:
|
|
27
|
+
|
|
28
|
+
- 解锁钥匙串,或在桌面会话里运行 modlens。
|
|
29
|
+
- 用 `agy` 重新登录。
|
|
30
|
+
- 换一个不需要交互式登录的 provider:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
modlens config set gemini-api.apiKey <key> # free key: https://aistudio.google.com
|
|
34
|
+
modlens config set provider gemini-api
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 额度用尽
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Individual quota reached. ... Resets in 94h19m9s.
|
|
41
|
+
|
|
42
|
+
agy's free tier is one weekly bucket shared by the desktop app, the CLI, and the SDK ...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
等重置,或换到 `gemini-api`,它有自己独立的预算。并行的 subagent 会飞快耗干这个共享额度池,用得猛的一天就能把它用完。
|
|
46
|
+
|
|
47
|
+
## 找不到 provider CLI
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Provider CLI not found: agy. Install it and sign in first.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
二进制不在 PATH 上,或者 `--provider-bin` 指错了地方。
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Working directory does not exist: /some/path
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
成因不同,但操作系统返回的是同一个底层错误码:`--workdir` 指向了一个不存在的目录。二进制本身没问题。
|
|
60
|
+
|
|
61
|
+
## recover-paste 什么都没找到
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
No pasted images found in any session storage for this directory (looked in: ...)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
按可能性从高到低:
|
|
68
|
+
|
|
69
|
+
- **你在错误的目录里。**恢复只限于对话所在的项目。传 `--cwd /path/to/project`。
|
|
70
|
+
- **根本没有粘贴过。**拖进来的文件和手打的路径本来就是真实文件,没有什么可恢复的:直接用那个路径。
|
|
71
|
+
- **某个配置问题挡住了一个 harness。**被挡的原因会出现在同一条消息的 `Blocked:` 之后,例如 OpenCode 需要 Node 22.13+ 才能用 `node:sqlite`。
|
|
72
|
+
|
|
73
|
+
## recover-paste 返回了另一个项目的图片
|
|
74
|
+
|
|
75
|
+
这种情况现在不应该再出现了,真出现就是值得上报的 bug。恢复检查的是 transcript 里记录的工作目录,不只是目录名,因为目录 slug 会撞车(`/tmp/a.b` 和 `/tmp/a-b` 生成同一个 slug)。提 issue 时带上输出里的 `harness` 和 `transcript` 字段。
|
|
76
|
+
|
|
77
|
+
## 项目对了,图片恢复错了
|
|
78
|
+
|
|
79
|
+
输出按从旧到新排列,所以**最后**一条才是最近一次粘贴。harness 存了文件名时条目会带 `filename`:用户提到名字时按它来匹配。`--count 3` 能多给几个候选。
|
|
80
|
+
|
|
81
|
+
## recover-paste:覆盖检测结果与输出位置
|
|
82
|
+
|
|
83
|
+
`recover-paste` 会自动检测自己运行在哪个 harness 里(先看进程祖先,再看环境特征),并且只读那个 harness 的存储。两个旋钮可以覆盖它:
|
|
84
|
+
|
|
85
|
+
- **`MODLENS_HARNESS`** 不用命令行参数就能强制指定存储范围:`claude-code`、`pi`、`opencode`、`codex`,或 `none`(扫描所有存储,不限范围)。检测最先读它,所以它优先于进程祖先和环境特征。`--harness` 对单次运行做同样的事。
|
|
86
|
+
- **`--out-dir`** 决定恢复出的图片落在哪。默认每次运行都新建一个不可预测的 `<tmpdir>/modlens-paste-*` 目录(0700,内含 0600 文件),没人能预先创建一个共享路径来截获字节。系统临时目录不合适时可以指到别处。显式传入的 `--out-dir` 若已存在,必须是真实目录(不是符号链接)、归你所有、组和其他用户无任何权限,否则会被拒绝。Windows 上会跳过所有权和权限检查,因为该平台没有 POSIX 权限位(见下方 Windows 一节)。符号链接检查仍然生效。
|
|
87
|
+
|
|
88
|
+
## 这是一个 Codex 会话
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
This is a Codex session: pasted images already exist as temp files, and each image
|
|
92
|
+
tag in the message carries its path.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
一切符合设计。Codex 会把粘贴的图片写到磁盘,并把路径放进消息里,所以直接从 tag 里取路径来读,不需要恢复任何东西。
|
|
96
|
+
|
|
97
|
+
## openai provider 的结果被拒绝
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
OpenAI-compatible API returned JSON that does not match the vision schema
|
|
101
|
+
(missing: ocr, ocr.full_text, ...)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
那个端点返回了残缺的结果。只有 agy、gemini-api、anthropic 和 claude-cli 在服务端强制执行 schema,较弱的网关可能只产出半个结果。重试一次,然后换 provider:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
modlens -i <image> -p gemini-api
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## guard 给出了 deny,或一次读取被拒绝
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Invocation guard denied this read: active model "gemini-3.1-pro" matches guards.denyModels pattern "gemini-3*". A model with native vision should read the image itself. To override, unset MODLENS_MODEL or edit guards in /Users/you/.modlens/config.json.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
这是配置在按预期工作:配置文件里的 `guards.denyModels` 列出了自带视觉的模型,当前模型匹配到了其中一条,引擎因此拒绝为一张该模型自己就能读的图片花掉一次 provider 调用。`modlens doctor` 有一个 Guard 小节,展示规则、检测到的模型、来自哪个信号(`MODLENS_MODEL` 环境变量、会话存储或 `--model` 自报),以及判定结果。
|
|
117
|
+
|
|
118
|
+
如果检测错了,`MODLENS_MODEL=<actual-model> modlens guard` 覆盖一切,`MODLENS_MODEL=none` 把模型标为未知(判定随 `denyWhenUnknown` 走,默认 allow)。彻底关掉 guard:`modlens config set guards.denyModels ''`。
|
|
119
|
+
|
|
120
|
+
一个已知盲区:存储检测读的是这个项目记录的最新一条 assistant 轮次,所以同一个项目目录里同时跑着不同模型的两个会话可能互相遮蔽(Claude Code 和 Codex 通过注入的会话 id 锁定确切会话,Pi 和 OpenCode 做不到)。中招时用 `MODLENS_MODEL` 覆盖。
|
|
121
|
+
|
|
122
|
+
注意上面那种硬拒绝只在显式的 `MODLENS_MODEL` 值真正匹配到 `denyModels` 时才触发。存储检测和 `denyWhenUnknown` 策略从不阻断 `analyze`,它们只通过 `modlens guard` 发声,而 guard 的 deny 是给 agent 的建议,不是上了锁的门。
|
|
123
|
+
|
|
124
|
+
## dsh 提示 `declares no dsh.bundle — installed as a plain dependency`
|
|
125
|
+
|
|
126
|
+
dsh profile 装到的是旧版 modlens。`dsh.bundle` 声明从 3.9.0 起才存在,而 pnpm v11 的发布冷静期机制(`minimumReleaseAge`,隔离刚发布的版本,pnpm 11.21 上实测窗口为 10 天)在所有较新版本都在窗口内时,会静默回退到更旧的版本。那个旧版本没有 bundle 声明,dsh 于是正确地把它当作普通依赖,一个工具都不会出现。
|
|
127
|
+
|
|
128
|
+
解法:把版本写死。pnpm 只在解析版本范围时应用冷静期,显式版本或 dist-tag 会跳过它([pnpm#9989](https://github.com/pnpm/pnpm/issues/9989),pnpm 11.21 上实测),安装命令带 `@latest` 就是这个原因:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@latest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
dsh 的 reconcile 会注意到新版本上的 bundle 声明并激活它,随后重启 dsh。用 `npx -y @deepseek-ai/dsh plugin --profile <name> list` 验证,显示的版本应该是 3.9.0 或更新。
|
|
135
|
+
|
|
136
|
+
如果将来的 pnpm 关掉了这条跳过通道,更持久的替代方案是在 `~/.dsh/profiles/<name>/pnpm-workspace.yaml` 里加一条一次性排除(写裸包名,不写 `name@version`,这样以后的新版本也能沿用):
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
minimumReleaseAgeExclude:
|
|
140
|
+
- '@liustack/modlens'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
然后执行 `npx -y @deepseek-ai/dsh plugin --profile <name> update @liustack/modlens`。两种方式的代价都摆在明面上:显式 `@latest`(或这条排除)让 modlens 退出 pnpm 的供应链冷静期,新版本会立即装上。
|
|
144
|
+
|
|
145
|
+
## fetch failed 或连接失败
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Could not connect to generativelanguage.googleapis.com (UND_ERR_CONNECT_TIMEOUT). The request never reached the network. ...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
API 请求根本没离开这台机器。在要靠代理才能上网的网络里这是预期表现:Node 的 fetch 默认无视代理环境变量。你明确要求走代理后 modlens 才会遵循,两种写法任选:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
HTTPS_PROXY=http://127.0.0.1:7890 modlens -i shot.png -p gemini-api # env (NO_PROXY honored too)
|
|
155
|
+
modlens config set proxy http://127.0.0.1:7890 # persistent, all API providers
|
|
156
|
+
modlens config set openai.proxy http://127.0.0.1:7890 # one provider only
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
代理只作用于 API provider 的请求。远程图片的下载路径有意保持直连并钉死 IP:它的 SSRF 防护校验的正是实际连接的那个地址,加了代理这些防护就失明了。在必须走代理的机器上,优先用本地文件,或让故障转移链把远程 URL 交给会在上游自行抓取的 provider。
|
|
160
|
+
|
|
161
|
+
## 配置文件问题
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
Cannot read /Users/you/.modlens/config.json: EACCES ... Fix the file or its permissions.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
文件存在但读不了。文件缺失是正常的,所以这是真问题,不能无视。
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
Failed to parse ... Fix or delete the file.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
JSON 无效。`modlens config init --force` 会写入一份干净的配置,旧内容会丢失。
|
|
174
|
+
|
|
175
|
+
## 超时
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
antigravity-cli provider timed out after 210000 ms.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
带 `--timeout 300000` 重试一次。信息密集的图片在 agy 上花 15-40 秒属于正常,`-m gemini-3.1-pro-high` 还会更慢。无视 SIGTERM 的引擎会被升级为 SIGKILL,所以超时无论如何都会迅速返回。
|
|
182
|
+
|
|
183
|
+
## 推理模型上每次读取都很慢
|
|
184
|
+
|
|
185
|
+
默认思考的模型会在开始转录之前先把预算花在思考上,而视觉读取并不需要思考。没有统一的 `--no-thinking` 参数,因为每家厂商给这个开关起的名字都不一样,所以直接传厂商自己的字段:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}'
|
|
189
|
+
modlens -i shot.png --extra-body '{"reasoning_effort":"low"}' # one run only
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
各家厂商的具体写法、哪些模型完全关不掉,以及怎么确认字段真的生效,见[配置手册](../skills/modlens/references/configure.zh-CN.md#关闭思考)。
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
extraBody cannot override "messages" for the openai provider
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
这个字段承载着图片、prompt 和 schema 强制逻辑。把它删掉,只保留厂商的开关字段。网关返回 400 并点名你设置的某个字段,说明那个端点用的是另一种写法。在 `antigravity-cli` 或 `claude-cli` 上运行时,`meta.warnings` 会说明该值被忽略了,因为 CLI provider 没有请求体。
|
|
199
|
+
|
|
200
|
+
## Windows
|
|
201
|
+
|
|
202
|
+
ModLens 可以在 Windows 上运行。三个值得了解的平台差异:
|
|
203
|
+
|
|
204
|
+
- **没有 POSIX 权限检查。**Windows 文件没有所有者、组、其他用户的权限位(读出来是 `0o666`/`0o777`,实际访问由 ACL 控制),所以 `doctor` 不评判配置文件的权限模式,`recover-paste --out-dir` 也不会因所有权或组和其他用户的权限而拒绝目录。`--out-dir` 的符号链接检查仍然生效。
|
|
205
|
+
- **Harness 检测依赖环境特征。**没有 `ps` 可以读进程树,检测只能依靠各 harness 设置的环境变量。猜错时用 `--harness <name>` 或 `MODLENS_HARNESS` 强制指定。
|
|
206
|
+
- **粘贴恢复。**OpenCode 的恢复在 Windows 上已覆盖(issue #11)。Claude Code 和 Pi 的 JSONL 路径依赖 `os.homedir()` 和各 harness 在那里的磁盘 slug。恢复扑空时,用 `--transcript` 直接指向文件,或把图片拖进终端。
|
|
207
|
+
|
|
208
|
+
## 还是没解决
|
|
209
|
+
|
|
210
|
+
提 issue 时附上完整命令和完整报错:https://github.com/liustack/modlens/issues
|
package/dsh/index.js
CHANGED
|
@@ -220,15 +220,25 @@ function registerPasteRoute(ctx) {
|
|
|
220
220
|
* text-only, so pastes are refused before any plugin hook runs. This wrapper
|
|
221
221
|
* registers a NEW provider whose model metadata declares image input and
|
|
222
222
|
* whose stream() is a one-line delegation back to the real route. Pick the
|
|
223
|
-
* wrapped model in the model selector, paste, and the
|
|
223
|
+
* wrapped model in the model selector, paste, and the request-time rewrite
|
|
224
224
|
* turns the image into evidence text before the delegated request goes out;
|
|
225
225
|
* the upstream serializer's own image rejection stays as the fail-closed
|
|
226
226
|
* backstop. Guarded feature-detection: if the llm registration surface moved
|
|
227
227
|
* (developer preview), the plugin quietly stays a read_image-only tool.
|
|
228
|
+
*
|
|
229
|
+
* Two modes (issue #29, design contributed by @zlycode01):
|
|
230
|
+
* - `config.upstream` set: wrap exactly that one route, legacy behavior.
|
|
231
|
+
* - unset: auto-discovery — every registered provider route carrying
|
|
232
|
+
* wrappable text-only family models gets its own `modlens-<provider>`
|
|
233
|
+
* wrapper, so a machine with several subscription packages (opencode-go,
|
|
234
|
+
* zai, ...) wraps them all instead of hand-picking one. A `discover` array
|
|
235
|
+
* of provider ids narrows the set. Routes that register late (llm-pi-ai
|
|
236
|
+
* mounts its routes after settings load) are picked up by re-sweeping on
|
|
237
|
+
* the registry's own `llm/adapters-updated` notification, no polling. The
|
|
238
|
+
* deepseek-official wrap keeps its historical `deepseek-modlens` id, so a
|
|
239
|
+
* selector remembering that provider survives the upgrade.
|
|
228
240
|
*/
|
|
229
241
|
function registerVisionProvider(ctx, config) {
|
|
230
|
-
const upstream = config.upstream || 'deepseek-official'
|
|
231
|
-
const providerId = config.providerId || 'deepseek-modlens'
|
|
232
242
|
// Wrap only the text-only members of these families. Their own vision
|
|
233
243
|
// models (present or future: deepseek-vl/ocr/janus, glm-4.5v, glm-5v-...)
|
|
234
244
|
// need no bridge and are excluded by name and by declared modality.
|
|
@@ -244,58 +254,148 @@ function registerVisionProvider(ctx, config) {
|
|
|
244
254
|
if (typeof ctx.llm?.registerAdapter !== 'function' || typeof ctx.llm?.stream !== 'function') {
|
|
245
255
|
return
|
|
246
256
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
257
|
+
|
|
258
|
+
const registerWrapper = (upstream, providerId, displayName) => {
|
|
259
|
+
const withVision = (info) => ({
|
|
260
|
+
...info,
|
|
261
|
+
provider: providerId,
|
|
262
|
+
inputModalities: ['text', 'image'],
|
|
263
|
+
})
|
|
264
|
+
try {
|
|
265
|
+
ctx.llm.registerAdapter([providerId], {
|
|
266
|
+
// Duck-typing LlmAdapter: providerInfo/providerRetryPolicy are
|
|
267
|
+
// base-class defaults a plain object must supply itself (their
|
|
268
|
+
// absence is exactly the silent registration failure this catch
|
|
269
|
+
// used to swallow).
|
|
270
|
+
providerInfo(provider) {
|
|
271
|
+
return { id: provider, name: displayName }
|
|
272
|
+
},
|
|
273
|
+
providerRetryPolicy() {
|
|
274
|
+
return undefined
|
|
275
|
+
},
|
|
276
|
+
async listModels(_provider, signal) {
|
|
277
|
+
try {
|
|
278
|
+
const models = await ctx.llm.listModels(upstream, signal)
|
|
279
|
+
return models.filter(shouldWrap).map((model) => ({
|
|
280
|
+
...withVision(model),
|
|
281
|
+
name: `${model.name ?? model.id} (modlens vision)`,
|
|
282
|
+
}))
|
|
283
|
+
} catch {
|
|
284
|
+
return []
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
async resolveModel(_provider, model, signal) {
|
|
288
|
+
const info = await ctx.llm.resolveModelInfo(upstream, model, signal)
|
|
289
|
+
if (!shouldWrap(info)) {
|
|
290
|
+
throw new Error(`model "${model}" is outside the modlens vision wrap scope`)
|
|
291
|
+
}
|
|
292
|
+
return { ...withVision(info), id: model }
|
|
293
|
+
},
|
|
294
|
+
stream(options) {
|
|
295
|
+
// Convert at request time, not at log time: the durable session
|
|
296
|
+
// log keeps the real image blocks (so the UI shows the paste
|
|
297
|
+
// natively), and only the wire messages carry evidence text.
|
|
298
|
+
// Cached per attachment, since the same history rides every step.
|
|
299
|
+
const self = this
|
|
300
|
+
return (async function* () {
|
|
301
|
+
const messages = await convertImagesToEvidence(ctx, options.messages, options.signal, self)
|
|
302
|
+
yield* ctx.llm.stream({ ...options, provider: upstream, messages })
|
|
303
|
+
})()
|
|
304
|
+
},
|
|
305
|
+
evidenceCache: new Map(),
|
|
306
|
+
})
|
|
307
|
+
return true
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// A duplicate means a concurrent or earlier registration already won:
|
|
310
|
+
// that is success for the claim, not a reason to retry forever.
|
|
311
|
+
if (/already|duplicate/i.test(String(error))) {
|
|
312
|
+
console.error(`[modlens] vision provider ${providerId} already registered, keeping the existing one`)
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
// A preview-era surface change: degrade to the read_image-only plugin,
|
|
316
|
+
// but say so in the harness log instead of vanishing (a swallowed
|
|
317
|
+
// TypeError here once hid a missing base method).
|
|
318
|
+
console.error(`[modlens] vision provider registration skipped (${providerId}): ${error}`)
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (config.upstream) {
|
|
324
|
+
registerWrapper(
|
|
325
|
+
config.upstream,
|
|
326
|
+
config.providerId || 'deepseek-modlens',
|
|
327
|
+
'DeepSeek (modlens vision)',
|
|
328
|
+
)
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Auto-discovery. `wrapped` guards duplicates across sweeps and the
|
|
333
|
+
// self-nesting case (our own wrappers appear in listProviders too). Two
|
|
334
|
+
// re-entrancy rules matter because registerAdapter itself broadcasts
|
|
335
|
+
// llm/adapters-updated, so every successful wrap re-triggers a sweep:
|
|
336
|
+
// an id is claimed in `wrapped` BEFORE any await (a concurrent sweep must
|
|
337
|
+
// skip it while this one is still probing), and sweeps are serialized on
|
|
338
|
+
// one promise chain so two can never interleave their probes at all.
|
|
339
|
+
const discover = Array.isArray(config.discover) ? new Set(config.discover) : null
|
|
340
|
+
const wrapped = new Set(['deepseek-modlens'])
|
|
341
|
+
const sweepOnce = async () => {
|
|
342
|
+
try {
|
|
343
|
+
await sweepBody()
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// A sweep failure must never become an unhandled rejection inside the
|
|
346
|
+
// host process; the next topology notification simply tries again.
|
|
347
|
+
console.error(`[modlens] vision provider discovery sweep failed: ${error}`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const sweepBody = async () => {
|
|
351
|
+
if (typeof ctx.llm.listProviders !== 'function') {
|
|
352
|
+
// Older registry surface: fall back to the single legacy wrap once.
|
|
353
|
+
if (!wrapped.has('__legacy_fallback__')) {
|
|
354
|
+
wrapped.add('__legacy_fallback__')
|
|
355
|
+
registerWrapper('deepseek-official', 'deepseek-modlens', 'DeepSeek (modlens vision)')
|
|
356
|
+
}
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
for (const info of ctx.llm.listProviders()) {
|
|
360
|
+
const id = info?.id
|
|
361
|
+
if (!id || wrapped.has(id) || String(id).startsWith('modlens-')) continue
|
|
362
|
+
if (discover && !discover.has(id)) continue
|
|
363
|
+
// Claim before the await: the probe may suspend, and the sweep a
|
|
364
|
+
// registration triggers must not probe the same id concurrently.
|
|
365
|
+
wrapped.add(id)
|
|
366
|
+
let models = []
|
|
367
|
+
try {
|
|
368
|
+
models = await ctx.llm.listModels(id)
|
|
369
|
+
} catch {
|
|
370
|
+
// Unreachable route today; release the claim so a later topology
|
|
371
|
+
// change retries it.
|
|
372
|
+
wrapped.delete(id)
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
if (!models.some(shouldWrap)) {
|
|
376
|
+
// No eligible models yet: release, the route may gain some later.
|
|
377
|
+
wrapped.delete(id)
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
const providerId = id === 'deepseek-official' ? 'deepseek-modlens' : `modlens-${id}`
|
|
381
|
+
const base = info.name ?? id
|
|
382
|
+
if (!registerWrapper(id, providerId, `${base} (modlens vision)`)) {
|
|
383
|
+
wrapped.delete(id)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Serialize: a sweep triggered mid-sweep runs after, never interleaved.
|
|
388
|
+
// The first sweep is invoked directly so its synchronous prefix (the
|
|
389
|
+
// legacy fallback, the pre-await claims) completes during apply().
|
|
390
|
+
let sweeping = sweepOnce()
|
|
391
|
+
const sweep = () => {
|
|
392
|
+
sweeping = sweeping.then(sweepOnce, sweepOnce)
|
|
393
|
+
return sweeping
|
|
394
|
+
}
|
|
395
|
+
if (typeof ctx.on === 'function') {
|
|
396
|
+
ctx.on('llm/adapters-updated', () => {
|
|
397
|
+
void sweep()
|
|
293
398
|
})
|
|
294
|
-
} catch (error) {
|
|
295
|
-
// DUPLICATE_ADAPTER or a preview-era surface change: degrade to the
|
|
296
|
-
// read_image-only plugin, but say so in the harness log instead of
|
|
297
|
-
// vanishing (a swallowed TypeError here once hid a missing base method).
|
|
298
|
-
console.error(`[modlens] vision provider registration skipped: ${error}`)
|
|
299
399
|
}
|
|
300
400
|
}
|
|
301
401
|
|
package/package.json
CHANGED
package/skills/modlens/SKILL.md
CHANGED
|
@@ -20,11 +20,11 @@ powershell -ExecutionPolicy Bypass -File <skill-dir>\scripts\run.ps1 <args>
|
|
|
20
20
|
|
|
21
21
|
It resolves a working runtime (PATH `modlens`, then `npx`, then `bunx`) and forwards your arguments unchanged. Exit 78 means no runtime: relay the `nextSteps` from its stderr JSON instead of retrying.
|
|
22
22
|
|
|
23
|
-
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.
|
|
23
|
+
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.15.0):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.15.0: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.15.0 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.15.0 <args>`.
|
|
28
28
|
4. Otherwise tell the user no JavaScript runtime was found and that installing Node 22.19+ (https://nodejs.org) or Bun (https://bun.sh) is the next step. Do not claim modlens itself failed.
|
|
29
29
|
|
|
30
30
|
`references/runtime.md` documents the pin and the diagnostic fields.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# 配置 ModLens
|
|
2
|
+
|
|
3
|
+
[English](configure.md) | 中文
|
|
4
|
+
|
|
5
|
+
用户询问如何安装、配置或切换 ModLens provider 时读这份文档。优先替用户把命令跑掉,而不是解释给他听。
|
|
6
|
+
|
|
7
|
+
## 配置放在哪
|
|
8
|
+
|
|
9
|
+
`~/.modlens/config.json`,由 CLI 管理。优先级:CLI 参数 > 环境变量 > 配置文件 > 内置默认值。零配置时的默认 provider 是 `antigravity-cli`。
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
modlens config init # 写入一份起步配置(已存在则拒绝,--force 重写)
|
|
13
|
+
modlens config show # 生效的配置文件,API key 打码显示
|
|
14
|
+
modlens config set provider <name> # 更改默认 provider
|
|
15
|
+
modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、model、extraBody
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`config set` 写文件时权限为 0600。
|
|
19
|
+
|
|
20
|
+
## 配置文件的完整形状
|
|
21
|
+
|
|
22
|
+
所有内容都在四个顶层键之下,全部可选。下面的示例一次性展示了所有支持的键和字段(真实文件只需要写你用到的部分)。文件不存在就全用默认值。provider 的设置放在 `providers.<name>` 下面,不在顶层,手工编辑最常犯的就是这个错。
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"provider": "gemini-api",
|
|
27
|
+
"proxy": "http://127.0.0.1:7890",
|
|
28
|
+
"reuse": { "claude": true, "codex": true, "opencode": false, "pi": true, "grok": true },
|
|
29
|
+
"guards": {
|
|
30
|
+
"allowModels": ["deepseek-v4-*", "glm-5.*", "minimax-m2.5*", "qwen3-coder*"],
|
|
31
|
+
"denyModels": ["glm-*v*", "deepseek-vl*"],
|
|
32
|
+
"denyWhenUnknown": false
|
|
33
|
+
},
|
|
34
|
+
"providers": {
|
|
35
|
+
"antigravity-cli": { "model": "gemini-3.6-flash-low" },
|
|
36
|
+
"gemini-api": {
|
|
37
|
+
"apiKey": "AIza...",
|
|
38
|
+
"baseUrl": "https://generativelanguage.googleapis.com",
|
|
39
|
+
"model": "gemini-3.6-flash"
|
|
40
|
+
},
|
|
41
|
+
"openai": {
|
|
42
|
+
"apiKey": "sk-...",
|
|
43
|
+
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
44
|
+
"model": "qwen3.6-27b",
|
|
45
|
+
"extraBody": { "thinking": { "type": "disabled" } }
|
|
46
|
+
},
|
|
47
|
+
"anthropic": {
|
|
48
|
+
"apiKey": "sk-ant-...",
|
|
49
|
+
"baseUrl": "https://api.anthropic.com",
|
|
50
|
+
"model": "claude-haiku-4-5-20251001"
|
|
51
|
+
},
|
|
52
|
+
"claude-cli": { "model": "haiku" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
字段含义:
|
|
58
|
+
|
|
59
|
+
- `provider`:不传 `-p` 时由哪个 provider 执行。标准名和别名都行(`agy`/`antigravity` 对应 `antigravity-cli`,`gemini` 对应 `gemini-api`,`openai-compat` 对应 `openai`,`claude` 对应 `anthropic`,`claude-code` 对应 `claude-cli`)。留空或缺失表示 `antigravity-cli`。
|
|
60
|
+
- `providers.<name>.<field>`:共四个字段,`apiKey`、`baseUrl`、`model`、`extraBody`。每个 provider 条目都可选,条目里的每个字段也都可选。别名键同样会被读取(存在 `gemini` 下的设置在解析到 `gemini-api` 时也能找到),冲突时标准键胜出。
|
|
61
|
+
- `providers.<name>.extraBody`:一个 JSON 对象,合并进 API provider(`gemini-api`、`openai`、`anthropic`)的请求体,用来传厂商有而 modlens 没有对应参数的开关。最常见的用途是关掉思考,见下文小节。嵌套对象逐键合并,所以加一个开关不会动到该块里的其他内容。承载图片、提示词和 schema 约束的字段会被拒绝,报错会点名该字段。两个 CLI provider 不发请求体,所以在 `antigravity-cli` 或 `claude-cli` 上运行时它会被忽略,并在 `meta.warnings` 里说明。
|
|
62
|
+
- `guards`:调用 guard,给在同一个客户端里既跑纯文本模型又跑视觉模型的人用。两个列表都放 glob 模式(支持 `*` 和 `?`,不区分大小写,同时匹配模型名和 `provider/model`),用 `modlens config set guards.denyModels '["gemini-3*"]'` 或 `guards.allowModels` 设置(JSON 数组或逗号分隔的列表都行,传空则清除)。两种写法表达同一个意图,选列表更短的那种:
|
|
63
|
+
- 只用 `denyModels`:除了列出的视觉模型,其余全部运行引擎。适合你接入的模型大多是纯文本的情况。
|
|
64
|
+
- `allowModels` 非空(白名单模式):只有列出的模型运行引擎,其他所有已识别的模型一律拒绝。适合 2026 年的实际格局,纯文本模型才是那份短名单。deny 模式仍然优先于 allow 匹配,所以宽泛的 allow 可以把视觉变体剔出去,正如上面的示例:`glm-5.*` 放行文本系列,`glm-*v*` 抓住 `glm-5v-turbo`。allow 模式要锚定得紧一些(写 `deepseek-v4-*` 而不是 `deepseek*`),这样厂商下一代多模态型号会自动掉出名单,等你检查过再上场。
|
|
65
|
+
- 按真正抵达模型的内容来列名单,而不是按它本来能看到什么:多模态模型如果躲在一个剥离图片的网关后面,照样需要 modlens,而你的会话记录里存的是网关上报的模型名。`modlens doctor` 的 Guard 一节会显示规则和一条实时判定,方便核对结果。
|
|
66
|
+
- `denyWhenUnknown`(默认 `false`)决定在两种模式下,当没有任何信号能识别当前模型时怎么办:`false` 放行,`true` 拒绝。当前模型的检测来源从强到弱依次是:`MODLENS_MODEL` 环境变量(`none` 表示「按未知处理」)、harness 的会话存储、`--model` 自报。
|
|
67
|
+
- 以下绑定上,环境变量会覆盖配置文件:`GEMINI_API_KEY`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL`。除此之外,modlens 还读取 `MODLENS_HARNESS`(粘贴恢复和 guard 的作用范围)、`MODLENS_MODEL`(guard 覆盖,见 `guards`),以及各 harness 自己注入的指纹,它们把 guard 的存储查询钉在当前 session 上:`CLAUDE_CODE_SESSION_ID`、`CODEX_THREAD_ID`,加上 harness 检测依赖的存在性标记(`CLAUDECODE`、`PI_CODING_AGENT`、`CODEX_SANDBOX`)。
|
|
68
|
+
- `reuse.<claude|codex|opencode|pi|grok>`:按 harness 记录的授权,决定能否花费本机其他登录态,由引导对话(`references/onboard.md`)写入。`true` 允许读图时复用该 harness(pi 的凭据加入 inline 区且所有 guard 照常生效,已登录的 Codex、OpenCode 的视觉模型或直接驱动的 pi 加入 agent 区,排在 `claude-cli` 之前),`false` 记下一次拒绝,用户不会被再次询问,缺失表示从未问过,什么都不会运行。`claude` 缺失视为已授权:`claude-cli` 作为内置 provider 早于这套模型存在,`reuse.claude false` 会把它移出链条(`-p claude-cli` 仍可钉死)。复用来的引擎不比用户自己的优先:分区只按速度档次排序。每个复用得来的答案都会在 `meta.warnings` 里加一行,说明花的是谁的额度,`modlens doctor` 的 Reuse 一节会显示每个 harness 的决定和探测发现的结果(探测结果在 `~/.modlens/auto-cache.json` 里缓存 6 小时,doctor 每次都重新探测)。用 `modlens config set reuse.codex true` 设置(传空恢复为从未问过)。
|
|
69
|
+
- 未知的顶层键和未知的 provider 名会被忽略而不是报错,所以敲错字会无声失败:手工编辑后跑一下 `modlens doctor`,它会显示哪些文件值和环境变量真正生效。
|
|
70
|
+
|
|
71
|
+
手工编辑没问题(保持文件是合法 JSON,权限 0600)。`modlens config set` 做的是同一件事,只是多了护栏。
|
|
72
|
+
|
|
73
|
+
## 各 provider 配置步骤
|
|
74
|
+
|
|
75
|
+
### antigravity-cli(默认,免费,无需 key)
|
|
76
|
+
|
|
77
|
+
需要装好 Antigravity CLI 并完成登录:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
curl -fsSL https://antigravity.google/cli/install.sh | bash
|
|
81
|
+
agy # 用户需自己在浏览器完成登录,然后退出
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
任何免费 Google 账号都行,不需要 Google AI Pro。登录无法自动化,请让用户自己跑一次 `agy`。
|
|
85
|
+
|
|
86
|
+
### gemini-api(免费 key,最快的免费通道,5-10 秒)
|
|
87
|
+
|
|
88
|
+
1. 用户到 https://aistudio.google.com 创建一个 key(约三分钟,无需信用卡,免费额度不过期)。
|
|
89
|
+
2. 两种方式任选其一保存:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
modlens config set gemini-api.apiKey <key>
|
|
93
|
+
# 或走环境变量:export GEMINI_API_KEY=<key>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
默认模型 `gemini-3.6-flash` 在免费档就有视觉能力(约每分钟 10-15 次请求,每天 1500 次)。免费档的数据可能被 Google 用于改进产品,用户要处理敏感图片时请提醒这一点。
|
|
97
|
+
|
|
98
|
+
### openai(任意 OpenAI 兼容的多模态端点)
|
|
99
|
+
|
|
100
|
+
需要三个值。以 DashScope 的 qwen 为例:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
modlens config set openai.baseUrl https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
104
|
+
modlens config set openai.apiKey <sk-key>
|
|
105
|
+
modlens config set openai.model qwen3.6-27b
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
官方 OpenAI 的写法:baseUrl 用 `https://api.openai.com/v1`,配一个具备视觉能力的模型。对应的环境变量:`OPENAI_BASE_URL`、`OPENAI_API_KEY`。模型必须是多模态的,纯文本模型会失败或产生幻觉。这条路线没有服务端 schema 约束,偶发的结构错误会以明确报错的形式暴露出来,重试或换 provider 即可。
|
|
109
|
+
|
|
110
|
+
### anthropic(Claude API key)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
modlens config set anthropic.apiKey <sk-ant-key>
|
|
114
|
+
# 或:export ANTHROPIC_API_KEY=<key>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
默认模型是 Claude Haiku(`claude-haiku-4-5-20251001`)。schema 通过强制工具调用来约束。
|
|
118
|
+
|
|
119
|
+
**`ANTHROPIC_BASE_URL` 陷阱。**modlens 把 `ANTHROPIC_BASE_URL` 绑定到 `anthropic.baseUrl`,所以这个变量指向哪它就继承哪。如果用户在 shell 里设过它,用来把 Claude Code 路由到某个纯文本网关(在 Claude Code 界面下跑非 Claude 模型的常见做法),那么 `-p anthropic` 也会把视觉请求无声地发到那个网关,要么失败,要么返回的结果像没看过图,而且没有任何端点被换掉的提示。anthropic 的视觉表现异常时,先 `echo $ANTHROPIC_BASE_URL` 查一下。解法:给 modlens 调用临时取消这个变量,或用 `modlens config set anthropic.baseUrl https://api.anthropic.com` 钉死真实端点,或改用 `-p gemini-api`。
|
|
120
|
+
|
|
121
|
+
### claude-cli(Claude Code 登录态,无需 key)
|
|
122
|
+
|
|
123
|
+
借用已有的 `claude` 登录态,花的是用户的 Claude 订阅额度,不产生单独的 API 账单。需要装好并登录 Claude Code(用 `claude --version` 检查)。运行时只带 `--allowedTools Read`。只支持本地图片文件,远程 URL 请改用 gemini-api。默认模型别名 `haiku`。
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
modlens config set provider claude-cli # 用户愿意的话把它设为默认
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 关闭思考
|
|
130
|
+
|
|
131
|
+
推理模型答题前要先花掉思考预算。从图片里读文字用不上这些,所以在默认思考的模型上,一次识别白白变得又慢又贵。每家厂商给这个开关起的名字都不一样,也没有通用写法,所以 modlens 只负责把你放进 `extraBody` 的内容原样发出去,名字怎么写去查厂商自己的文档。
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}' # 持久保存
|
|
135
|
+
modlens -i shot.png --extra-body '{"thinking":{"type":"disabled"}}' # 仅本次运行
|
|
136
|
+
modlens config set openai.extraBody '' # 清除
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`--extra-body` 在该次运行中整体替换已存储的对象,而不是合并进去。
|
|
140
|
+
|
|
141
|
+
已知写法,截至 2026 年 8 月:
|
|
142
|
+
|
|
143
|
+
| 端点 | 要发送的字段 |
|
|
144
|
+
| :-- | :-- |
|
|
145
|
+
| MiMo 官方 API(`api.xiaomimimo.com/v1`) | `{"thinking":{"type":"disabled"}}` |
|
|
146
|
+
| MiMo Responses 格式路由 | `{"reasoning":{"effort":"none"}}` |
|
|
147
|
+
| Qwen、GLM、MiMo 等自建在 vLLM 或 SGLang 上 | `{"chat_template_kwargs":{"enable_thinking":false}}` |
|
|
148
|
+
| 接受 effort 档位的 OpenAI 风格网关 | `{"reasoning_effort":"low"}` |
|
|
149
|
+
| `gemini-api`,Gemini 3 系列 | `{"generationConfig":{"thinkingConfig":{"thinkingLevel":"LOW"}}}` |
|
|
150
|
+
| `gemini-api`,Gemini 2.5 Flash 与 Flash Lite | `{"generationConfig":{"thinkingConfig":{"thinkingBudget":0}}}` |
|
|
151
|
+
| `anthropic` | 什么都不用做,不主动要求就不思考 |
|
|
152
|
+
|
|
153
|
+
三个会咬人的地方:
|
|
154
|
+
|
|
155
|
+
- 不是每个模型都能关。Gemini 3 Pro 和 Gemini 2.5 Pro 没有关闭开关,只能调低档位。有些模型完全无视 effort 字段,照样思考。
|
|
156
|
+
- 严格的云(Groq 和 Cerebras 都在内)遇到不认识的字段会直接返回 400。以前能跑的请求现在报 400 并点名你的字段,说明那个网关要的是另一种写法,不是这一种。
|
|
157
|
+
- 另一些则会接受未知字段然后悄悄忽略,所以要验证它是否生效,别想当然。把 `meta.durationSeconds` 和 `meta.usage` 里的 token 数与不带 `extraBody` 的一次运行对比,两者都没变,就是字段没起作用。
|
|
158
|
+
- 较弱的模型可能得靠思考才能填满 schema。在同一张流程图上实测:`gemini-3.6-flash` 在 `thinkingLevel: LOW` 下从 12 秒缩到 5.7 秒,区块和转录内容不变,但 DashScope 上的 `qwen3.6-27b` 设了 `enable_thinking: false` 后开始漏掉版面区块必填的 `type`,modlens 会拒绝这种结果而不是当作证据放行。刚关掉思考就出现结构错误,说明这就是代价,给那个模型把思考打开,或换到有服务端 schema 约束的路线。
|
|
159
|
+
|
|
160
|
+
## 替用户选 provider
|
|
161
|
+
|
|
162
|
+
- 想零配置且免费:`antigravity-cli`(需要 agy 登录,每张图 15-40 秒,密集或困难的图可试 `-m gemini-3.1-pro-high`)。
|
|
163
|
+
- 想又快又免费:`gemini-api`(三分钟领 key,5-10 秒)。
|
|
164
|
+
- 已经在给 Claude 付费:`claude-cli`(无需额外 key,agent 循环 20-45 秒)或 `anthropic`(API 计费)。
|
|
165
|
+
- 有偏好的多模态端点(qwen、GLM 等):`openai`。
|
|
166
|
+
|
|
167
|
+
每个配好的 provider 也互为后备:一次运行按固定顺序尝试它们(5-10 秒的 inline API provider 先上,然后是 agent 类,对远程 URL 来说这个顺序同时也是一道安全边界),遇到报错、超时或违反 schema 的结果就故障转移。`config set provider <name>` 把某个 provider 提到它所在允许分区的最前面,`-p <name>` 钉死唯一一个,不做回退。`doctor` 会打印这些故障转移链,结果里的 `meta.attempts` 显示一次运行实际试了什么。
|
|
168
|
+
|
|
169
|
+
## 故障排查
|
|
170
|
+
|
|
171
|
+
- 报错点名了缺失的环境变量或某条 `config set` 命令:照着运行即可。
|
|
172
|
+
- `Provider CLI not found: agy`:安装 Antigravity CLI 或换 provider。
|
|
173
|
+
- `Claude CLI reported ...` 或结果为空:检查 `claude` 的登录状态。
|
|
174
|
+
- openai 路线报 `does not match the vision schema`:重试一次,仍不行就换 gemini-api 或 anthropic。
|
|
175
|
+
- `extraBody cannot override "<field>"`:该字段承载图片、提示词或 schema。把它从对象里去掉,留下厂商开关即可。
|
|
176
|
+
- 400 报错点名了你在 `extraBody` 里设的字段:那个网关不认识它。其他写法见上文关闭思考一节。
|
|
177
|
+
- `config init` 拒绝执行:文件已存在。先用 `modlens config show` 查看,只有用户同意覆盖时才加 `--force`。
|
|
@@ -24,7 +24,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
24
24
|
# package.json version, and the release script rewrites it on every bump.
|
|
25
25
|
$Package = '@liustack/modlens'
|
|
26
26
|
$Bin = 'modlens'
|
|
27
|
-
$Pinned = '3.
|
|
27
|
+
$Pinned = '3.15.0'
|
|
28
28
|
# -------------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
@@ -22,7 +22,7 @@ set -eu
|
|
|
22
22
|
# package.json version, and the release script rewrites it on every bump.
|
|
23
23
|
PKG="@liustack/modlens"
|
|
24
24
|
BIN="modlens"
|
|
25
|
-
PINNED="3.
|
|
25
|
+
PINNED="3.15.0"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|