@liustack/modlens 3.16.3 → 3.16.5
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 +12 -3
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/dist/main.js +2 -2
- package/docs/harness-setup.md +42 -1
- package/docs/harness-setup.zh-CN.md +23 -1
- package/docs/troubleshooting.md +31 -21
- package/docs/troubleshooting.zh-CN.md +13 -7
- package/dsh/index.js +48 -6
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- 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,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.16.5 - 2026-08-15
|
|
4
|
+
|
|
5
|
+
- **dsh: pasting into a plain text-only model works again ([#36](https://github.com/liustack/modlens/issues/36)).** The paste takeover shipped in 3.14.0 has been dead in every default install since 3.16.0 moved the verdict server-side. The verdict refuses when any model matching the selector label declares image input, which is right for a real vision model and wrong for the one case it could not see: this plugin's own `(modlens vision)` wrapper reuses the upstream model id verbatim and declares image input, because that declaration is exactly how the wrapper unlocks admission. So selecting plain `DeepSeek-V4-Pro` matched the real text-only model and the plugin's synthetic twin of it, the twin vetoed, and the paste fell through to dsh's own gate and its `MODEL_DOES_NOT_SUPPORT_IMAGES`. The verdict now skips a twin two ways, both requiring proof rather than a name: the provider ids this instance actually registered, tracked as each wrapper lands, and a model that carries the `(modlens vision)` marker on a provider id minted by the rule this plugin uses, which is how a sibling instance in the same process is recognized. A real vision provider still vetoes even if it borrows the marker, and an id someone else already holds is never trusted as ours. The verdict had no test at all, which is how a regression this total shipped and stayed for six releases; it now has an integration suite driving the real route against a registry shaped like a live install. Thanks to @Taz-dingo for a report that arrived with the conflicting rules already quoted side by side.
|
|
6
|
+
- **The docs cannot print a stale install command any more.** Every file the repo tracks is scanned, and any install of this package that is not pinned to an exact version has to carry `--config.minimumReleaseAge=0` as an argument of that same command. Review found the first version of that check passing five different ways of writing an unpinned install, including a command split across lines and a spec like `3.16.4+local` that only looks pinned.
|
|
7
|
+
|
|
8
|
+
## 3.16.4 - 2026-08-15
|
|
9
|
+
|
|
10
|
+
- **How to update is written down, and the explanation it replaces was wrong.** 3.10.0 claimed that pnpm's release-age gate has a 10-day window and that an explicit version or dist-tag skips it, so every install command in this repo carried `@latest` as the fix. Measured on a machine with no gate configured at all: pnpm 11 turns `minimumReleaseAge` on by default at 24 hours (`24 * 60` in its config reader; `pnpm config get` does not surface that particular default), and asking for `@latest` installed 3.9.1 while 3.16.3 was the published latest. The gate filters the candidate versions before the tag is resolved, so the tag lands on an older one, and a day of held-back wall-clock time can be several releases on a fast week. The pnpm issue behind the original claim describes a bug in pnpm 10.16.1 that was fixed, and says in its own text that `@latest` was subject to the gate. What does work is naming the version, a deliberate request rather than a resolution: pnpm 11 installs it and records that one version as an approved exception, leaving everything else behind the window. Where a stricter policy is configured it refuses instead, and `--config.minimumReleaseAge=0` lifts the gate for one command, for every package that command resolves. `harness-setup` now has an Updating section covering both install shapes, including why `update` cannot cross a major (it stays inside the recorded semver range) and how to check what actually landed.
|
|
11
|
+
|
|
3
12
|
## 3.16.3 - 2026-08-15
|
|
4
13
|
|
|
5
14
|
- **The dsh tool is registered under a name of its own, so the model can actually see it ([#34](https://github.com/liustack/modlens/issues/34)).** A host with a durable attachment store mounts its own `read_image`, and 3.13.0 handled that by catching the duplicate-registration error and renaming. That catch never fired. dsh's tool registry is layered and a scoped tool shadows a global one, so a host `read_image` in the agent-preset scope and ours registered globally are not a duplicate at all: the registration succeeded, nothing was logged, and the model kept resolving the host tool, which refuses a text-only model outright. Nor is the collision cheap to detect, so the tool now takes the name `modlens_read_image` instead of competing for `read_image`, and the model finds it through the schema that reaches it every request regardless of the name. `toolName` still pins whatever a host prefers. Thanks to @ingleav626-art, who instrumented the registry and brought back the layer that made the old fix dead code.
|
|
@@ -9,7 +18,7 @@
|
|
|
9
18
|
|
|
10
19
|
- **The claude-cli provider starts on Windows ([#31](https://github.com/liustack/modlens/issues/31)).** npm installs every JS CLI as a trio, and both ways of reaching it failed: the bare name found none of them (ENOENT, reported as "not installed" next to a `claude --version` that worked fine), and handing spawn the `.cmd` hit Node's post-CVE refusal to run batch files without a shell (EINVAL). Wrapping it in `cmd.exe` turned out to be a trap, since a cmd command line cannot carry a raw newline and our provider arguments are whole multi-line vision prompts, so a wrapped prompt truncates at its first line break and the rest is read as a second command. modlens instead reads the shim, takes the Node entry it points at, and spawns Node on it directly: no shell, no escaping, and the child is the real provider, so a timeout's SIGTERM lands on it rather than an intermediate. The reading is conservative by construction. Every line of the shim must be accounted for, the interpreter is read rather than inferred (cmd-shim will happily generate a python shim for a file named `.js`), and a shim carrying anything this cannot reproduce faithfully, an environment assignment, cmd control syntax, an argument whose quoting it cannot prove, is declined and left to a spawn error that names the provider, the command, and the real error code. Nine rounds of independent review shaped that boundary, each round adding a shape it had to refuse rather than guess at. Thanks to @zhang66633 for a report that arrived with both failure modes already isolated.
|
|
11
20
|
- **The install procedure knows about dsh ([#32](https://github.com/liustack/modlens/issues/32)).** INSTALL.md, the file an agent is pointed at, never mentioned dsh once, so an agent told to install modlens followed the skill procedure to its end and left the user without the `read_image` tool and without the `(modlens vision)` entries they were looking for. It now opens with a dsh branch: install the plugin, skip the skill copy, keep the same engine configuration.
|
|
12
|
-
- **`doctor` says when an installed skill copy has fallen behind ([#33](https://github.com/liustack/modlens/issues/33)).** A skill is installed by copying it, and a copy keeps the version it was stamped with, so one machine ran 3.8.0 for eight releases and hit bugs that were long fixed. doctor now reads the pin out of every installed copy it can find and compares it against the version of the CLI reporting, which differ exactly when it matters, since
|
|
21
|
+
- **`doctor` says when an installed skill copy has fallen behind ([#33](https://github.com/liustack/modlens/issues/33)).** A skill is installed by copying it, and a copy keeps the version it was stamped with, so one machine ran 3.8.0 for eight releases and hit bugs that were long fixed. doctor now reads the pin out of every installed copy it can find and compares it against the version of the CLI reporting, which differ exactly when it matters, since a copy is frozen at install time while the CLI reporting is whatever was just launched. Still offline: two local file reads, no registry call. Thanks to @Ztyss for a report that had already verified the release stamping, leaving the update story as the real gap.
|
|
13
22
|
|
|
14
23
|
## 3.16.1 - 2026-08-14
|
|
15
24
|
|
|
@@ -56,7 +65,7 @@
|
|
|
56
65
|
- *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.
|
|
57
66
|
- *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`.
|
|
58
67
|
- *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.
|
|
59
|
-
- **dsh install can silently land on 3.5.0 ([#18](https://github.com/liustack/modlens/issues/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](https://github.com/pnpm/pnpm/issues/11224) is still open), but an explicit version or dist-tag skips it ([pnpm#9989](https://github.com/pnpm/pnpm/issues/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`,
|
|
68
|
+
- **dsh install can silently land on 3.5.0 ([#18](https://github.com/liustack/modlens/issues/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](https://github.com/pnpm/pnpm/issues/11224) is still open), but an explicit version or dist-tag skips it ([pnpm#9989](https://github.com/pnpm/pnpm/issues/9989), verified against pnpm 11.21: a bare add resolved 2.8.0 while `@latest` resolved 3.10.0 under the same default gate). **(Wrong, corrected in 3.16.4: the window is 24 hours, not 10 days, and `@latest` does not skip the gate.)** The install command everywhere now carries `@latest`, which does not in fact land current either; troubleshooting documents the mechanism plus a durable `minimumReleaseAgeExclude` fallback. Thanks to @stonogic086-1 for the precise diagnosis.
|
|
60
69
|
|
|
61
70
|
## 3.10.0 - 2026-08-14
|
|
62
71
|
|
|
@@ -70,7 +79,7 @@
|
|
|
70
79
|
|
|
71
80
|
## 3.9.0 - 2026-08-13
|
|
72
81
|
|
|
73
|
-
- **The first plug-in vision plugin for DeepSeek Harness (dsh).** The npm package is now also a dsh bundle
|
|
82
|
+
- **The first plug-in vision plugin for DeepSeek Harness (dsh).** The npm package is now also a dsh bundle, so a single `dsh plugin --profile <name> add` is the whole install (the current command, with its version named, is in [INSTALL.md](INSTALL.md)). 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.
|
|
74
83
|
- **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.
|
|
75
84
|
|
|
76
85
|
## 3.8.0 - 2026-08-13
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Issues are welcome any time: [open one](https://github.com/liustack/modlens/issu
|
|
|
34
34
|
|
|
35
35
|
## Highlights
|
|
36
36
|
|
|
37
|
-
**🥇 The first vision plugin for DeepSeek Harness (dsh):** one command, `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@
|
|
37
|
+
**🥇 The first vision plugin for DeepSeek Harness (dsh):** one command, `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.5`, and the text-only DeepSeek model behind dsh reads images through a native `modlens_read_image` tool. Updating is the same command again. The version is named rather than `@latest` on purpose: pnpm 11 holds back releases published in the last 24 hours and resolves the tag against what survives, so `@latest` would install whatever shipped a day ago ([details](docs/harness-setup.md#keeping-it-up-to-date)).
|
|
38
38
|
|
|
39
39
|
Pasting an image works two ways. **① Just paste.** On a text-only model the pasted image lands as a private temp file and its path enters the composer — the same interaction OpenCode and Pi ship — and the `modlens_read_image` tool takes it from there. **② Pick a `(modlens vision)` entry** in the model selector (it remembers your choice, so once is enough), then paste: the thumbnail stays visible in your message, closer to the Codex app feel, and the image is converted to structured evidence at request time, answered by the same underlying route. The plugin auto-discovers every provider route carrying text-only DeepSeek or GLM models and adds a wrapped entry per route (a stock install gets **`DeepSeek-V4-Flash (modlens vision)`** and **`DeepSeek-V4-Pro (modlens vision)`**; extra routes like opencode-go or zai get their own); the two families' own vision models are excluded automatically. Which paste route applies is the host's per-model call: only a model its metadata positively confirms text-only is taken over, anything unconfirmed is left alone, so vision models keep their native paste ([details](docs/harness-setup.md)).
|
|
40
40
|
|
package/README.zh-CN.md
CHANGED
|
@@ -34,7 +34,7 @@ DeepSeek 和 GLM 的主力对话模型是纯文本的,无法进行图片识别
|
|
|
34
34
|
|
|
35
35
|
## 亮点
|
|
36
36
|
|
|
37
|
-
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@
|
|
37
|
+
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.5`,dsh 背后的纯文本 DeepSeek 模型即可通过原生 `modlens_read_image` 工具读图。更新就是再跑一遍同一条命令。这里点名版本号而不用 `@latest` 是有意的:pnpm 11 会扣住最近 24 小时内发布的版本,dist-tag 只在剩下的里面解析,用 `@latest` 装到的会是一天前发布的那个([细节](docs/harness-setup.zh-CN.md#保持更新))。
|
|
38
38
|
|
|
39
39
|
DeepSeek Harness 粘贴识图有两种玩法。
|
|
40
40
|
|
|
@@ -69,7 +69,7 @@ agy # 浏览器完成
|
|
|
69
69
|
**DeepSeek Harness(dsh)用户不走 skill 流程**,本包就是原生 dsh 插件:
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@
|
|
72
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.5
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
装完即有 `modlens_read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
|
package/dist/main.js
CHANGED
|
@@ -4069,7 +4069,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
4069
4069
|
}
|
|
4070
4070
|
return Number.parseInt(raw, 10);
|
|
4071
4071
|
}
|
|
4072
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.16.
|
|
4072
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.16.5");
|
|
4073
4073
|
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(
|
|
4074
4074
|
"--extra-body <json>",
|
|
4075
4075
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
@@ -4179,7 +4179,7 @@ program.command("doctor").description(
|
|
|
4179
4179
|
configPath: CONFIG_PATH,
|
|
4180
4180
|
// Lets doctor name an installed skill copy that is older than
|
|
4181
4181
|
// the CLI reporting on it (issue #33).
|
|
4182
|
-
version: "3.16.
|
|
4182
|
+
version: "3.16.5"
|
|
4183
4183
|
});
|
|
4184
4184
|
const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
|
|
4185
4185
|
process.stdout.write(`${output}
|
package/docs/harness-setup.md
CHANGED
|
@@ -55,11 +55,52 @@ OpenCode with DeepSeek: `opencode auth login`, pick DeepSeek and paste the key (
|
|
|
55
55
|
dsh is different from the other harnesses: modlens plugs in as a native tool, not a prompt-triggered skill. The package itself is a dsh bundle, so one command installs it into a profile:
|
|
56
56
|
|
|
57
57
|
```sh
|
|
58
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.5
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
This registers a `modlens_read_image` tool whose schema reaches the model on every request (no trigger heuristics), runs the modlens CLI shipped inside the same package, and returns the structured evidence as the tool's canonical JSON output. Engines, reuse grants, and guard rules stay in `~/.modlens/config.json`, shared with every other harness. dsh is in developer preview and its plugin surface may change; the plugin keeps its touch small (raw tool registration, the llm adapter surface for the vision variants, the attachment reader, and one agent pre-step hook) and degrades loudly if any of them moves.
|
|
62
62
|
|
|
63
|
+
### Keeping it up to date
|
|
64
|
+
|
|
65
|
+
modlens ships often, and both install shapes freeze at whatever version they
|
|
66
|
+
got. On dsh, re-run the install with the version named:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.5
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`npm view @liustack/modlens version` prints the current one, and this page is
|
|
73
|
+
stamped with it at release time.
|
|
74
|
+
|
|
75
|
+
Two things in that command are deliberate. `add`, not `update`, because
|
|
76
|
+
`update` stays inside the semver range already recorded, and a plain install
|
|
77
|
+
records a caret range, so a profile that once landed on 2.7.1 updates to 2.8.0
|
|
78
|
+
and never crosses into 3.x. And a named version rather than `@latest`, because
|
|
79
|
+
pnpm 11 holds back anything published in the last 24 hours
|
|
80
|
+
(`minimumReleaseAge`, on by default) and resolves the tag against what survives
|
|
81
|
+
that filter: `@latest` lands on an older release instead of skipping the gate.
|
|
82
|
+
What it costs is a day of wall-clock time rather than one version, which on a
|
|
83
|
+
fast-moving week is several releases. A named version is a deliberate request,
|
|
84
|
+
so pnpm installs it, and since 11.1.3 records that one version as an approved
|
|
85
|
+
exception in the profile's `pnpm-workspace.yaml`, leaving everything else
|
|
86
|
+
behind the window.
|
|
87
|
+
|
|
88
|
+
Restart dsh, then confirm what actually landed:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> list
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The [troubleshooting page](troubleshooting.md#dsh-says-declares-no-dshbundle--installed-as-a-plain-dependency)
|
|
95
|
+
covers the stricter case, where you configured `minimumReleaseAge` yourself and
|
|
96
|
+
pnpm refuses rather than approves.
|
|
97
|
+
|
|
98
|
+
On the skill harnesses a skill is a copied folder, and the copy keeps its
|
|
99
|
+
install-time version, so re-run the install to overwrite it in place.
|
|
100
|
+
`modlens doctor` reads the pin out of every copy it can find and flags the ones
|
|
101
|
+
behind the CLI doing the reporting, which makes the drift visible before it
|
|
102
|
+
costs anyone a debugging session.
|
|
103
|
+
|
|
63
104
|
### Paste-to-path (web profile)
|
|
64
105
|
|
|
65
106
|
Pasting an image into the dsh Web UI under a **text-only model** used to die at
|
|
@@ -55,11 +55,33 @@ OpenCode 接 DeepSeek:执行 `opencode auth login`,选择 DeepSeek 并粘贴
|
|
|
55
55
|
dsh 与其他 harness 不同:modlens 以原生工具的形式接入,而不是靠提示词触发的 skill。本包自身就是一个 dsh bundle,一条命令即可装进某个 profile:
|
|
56
56
|
|
|
57
57
|
```sh
|
|
58
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.5
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
这会注册一个 `modlens_read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
|
|
62
62
|
|
|
63
|
+
### 保持更新
|
|
64
|
+
|
|
65
|
+
modlens 发布很频繁,而两种安装形态都会冻结在装进来的那个版本上。dsh 上重跑一遍安装即可,版本号要点名:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.5
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
|
|
72
|
+
|
|
73
|
+
这条命令里有两处是刻意的。用 `add` 而不是 `update`,因为 `update` 只在已记录的 semver 范围内挪动,而普通安装写进去的是 caret 范围,所以一个当初装到 2.7.1 的 profile 只会更新到 2.8.0,永远进不了 3.x。点名版本号而不用 `@latest`,则是因为 pnpm 11 会扣住最近 24 小时内发布的版本(`minimumReleaseAge`,默认开启),dist-tag 只在通过过滤的候选里解析:`@latest` 会落到更旧的版本上,而不是跳过冷静期。代价是一天的自然时间,不是一个版本,发布密集的一周里就是好几个版本。点名版本是一次明确的指定,所以 pnpm 会装上它,11.1.3 起还会把这一个版本作为已批准的例外写进该 profile 的 `pnpm-workspace.yaml`,其余一切仍留在窗口后面。
|
|
74
|
+
|
|
75
|
+
重启 dsh,然后确认实际装到了什么:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> list
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
更严格的情况(你自己配过 `minimumReleaseAge`,pnpm 会拒绝而不是批准)见[故障排查](troubleshooting.zh-CN.md#dsh-提示-declares-no-dshbundle--installed-as-a-plain-dependency)。
|
|
82
|
+
|
|
83
|
+
skill 类 harness 上,skill 是一个拷贝出来的文件夹,拷贝会保留安装时的版本,重跑安装原地覆盖即可。`modlens doctor` 会读出它能找到的每一份拷贝里钉住的版本,并标出落后于当前 CLI 的那些,让版本漂移在坑到人之前就先暴露出来。
|
|
84
|
+
|
|
63
85
|
### 粘贴转路径(paste-to-path,web profile)
|
|
64
86
|
|
|
65
87
|
过去在 dsh Web UI 里,**纯文本模型**下粘贴图片会死在图片准入检查这一步。插件现在带了一个浏览器端半边(由 dsh 的客户端插件系统自动加载),恰好在这种情况下接管粘贴:图片字节发到插件在 dsh web 服务器上的 `/modlens/paste` 路由(仅回环地址,校验 magic byte,上限 25 MB),落成一个私有临时文件,输入框收到的则是纯文本的文件路径。这与 Pi、OpenCode、Claude Code 递给模型的形态一致,也正是 modlens skill 和 `modlens_read_image` 工具的首要触发条件。消息里不带图片附件,准入检查根本不会触发。
|
package/docs/troubleshooting.md
CHANGED
|
@@ -124,37 +124,47 @@ Note that the hard refusal above only fires on an actual `denyModels` match agai
|
|
|
124
124
|
## dsh says "declares no dsh.bundle — installed as a plain dependency"
|
|
125
125
|
|
|
126
126
|
The dsh profile installed an old modlens version. The `dsh.bundle` declaration
|
|
127
|
-
exists since 3.9.0, and pnpm
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
exists since 3.9.0, and pnpm 11 holds back releases published in the last 24
|
|
128
|
+
hours (`minimumReleaseAge`, on by default since 11.0; `pnpm config get` does not
|
|
129
|
+
surface this particular default, so it prints nothing for it). When
|
|
130
|
+
every version carrying the declaration was inside that window, pnpm silently
|
|
131
|
+
resolved to an older one, which has no declaration, so dsh correctly treated it
|
|
132
|
+
as a plain dependency and none of the tools appeared.
|
|
133
|
+
|
|
134
|
+
`@latest` does not avoid this, which earlier versions of this page got wrong.
|
|
135
|
+
The gate filters the candidate versions before the tag is resolved, so the tag
|
|
136
|
+
simply lands on an older one. Name the exact version instead, which pnpm treats
|
|
137
|
+
as a deliberate request rather than a resolution:
|
|
135
138
|
|
|
136
139
|
```sh
|
|
137
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@
|
|
140
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.5
|
|
138
141
|
```
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
143
|
+
`npm view @liustack/modlens version` prints the current one. pnpm 11 installs a named
|
|
144
|
+
version, and since 11.1.3 also records it as an approved exception in the
|
|
145
|
+
profile's `pnpm-workspace.yaml`, leaving every other package and every future
|
|
146
|
+
modlens release behind the window.
|
|
144
147
|
|
|
145
|
-
If
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
If you set `minimumReleaseAge` yourself, pnpm treats the policy as strict and
|
|
149
|
+
refuses instead, naming the version and the cutoff
|
|
150
|
+
(`ERR_PNPM_NO_MATURE_MATCHING_VERSION`). Approve that one version in the same
|
|
151
|
+
file:
|
|
148
152
|
|
|
149
153
|
```yaml
|
|
150
154
|
minimumReleaseAgeExclude:
|
|
151
|
-
- '@liustack/modlens'
|
|
155
|
+
- '@liustack/modlens@3.16.5'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or lift the gate for a single command, which lifts it for everything that
|
|
159
|
+
command resolves, not only modlens:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@latest --config.minimumReleaseAge=0
|
|
152
163
|
```
|
|
153
164
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
install immediately.
|
|
165
|
+
dsh's reconcile notices the bundle declaration on the new version and activates
|
|
166
|
+
it; restart dsh afterwards. Verify with
|
|
167
|
+
`npx -y @deepseek-ai/dsh plugin --profile <name> list`.
|
|
158
168
|
|
|
159
169
|
## dsh: the model cannot see the read_image tool
|
|
160
170
|
|
|
@@ -123,24 +123,30 @@ Invocation guard denied this read: active model "gemini-3.1-pro" matches guards.
|
|
|
123
123
|
|
|
124
124
|
## dsh 提示 `declares no dsh.bundle — installed as a plain dependency`
|
|
125
125
|
|
|
126
|
-
dsh profile 装到的是旧版 modlens。`dsh.bundle` 声明从 3.9.0 起才存在,而 pnpm
|
|
126
|
+
dsh profile 装到的是旧版 modlens。`dsh.bundle` 声明从 3.9.0 起才存在,而 pnpm 11 会扣住最近 24 小时内发布的版本(`minimumReleaseAge`,自 11.0 起默认开启。`pnpm config get` 不展示这一项的内置默认值,所以查它什么都不显示)。当带声明的版本全都落在这个窗口内时,pnpm 会静默回退到更旧的版本,而旧版本没有 bundle 声明,dsh 于是正确地把它当作普通依赖,一个工具都不会出现。
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
`@latest` 绕不开这一层,本页早先的说法是错的。冷静期先把候选版本过滤掉,dist-tag 才在剩下的里面解析,于是它直接落到了更旧的那个上。改成写死精确版本号,pnpm 会把它当作一次明确的指定,而不是一次解析:
|
|
129
129
|
|
|
130
130
|
```sh
|
|
131
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@
|
|
131
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.5
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
`npm view @liustack/modlens version` 可以查到当前版本号。pnpm 11 会装上被点名的版本,11.1.3 起还会把它作为一条已批准的例外写进该 profile 的 `pnpm-workspace.yaml`,其余所有包和 modlens 以后的版本仍然留在窗口后面。
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
如果你自己设过 `minimumReleaseAge`,pnpm 会把这条策略视为严格模式,转而拒绝安装并报出版本与截止时间(`ERR_PNPM_NO_MATURE_MATCHING_VERSION`)。在同一个文件里放行这一个版本:
|
|
137
137
|
|
|
138
138
|
```yaml
|
|
139
139
|
minimumReleaseAgeExclude:
|
|
140
|
-
- '@liustack/modlens'
|
|
140
|
+
- '@liustack/modlens@3.16.5'
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@latest --config.minimumReleaseAge=0
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
dsh 的 reconcile 会注意到新版本上的 bundle 声明并激活它,随后重启 dsh。用 `npx -y @deepseek-ai/dsh plugin --profile <name> list` 验证。
|
|
144
150
|
|
|
145
151
|
## dsh:模型看不到 read_image 工具
|
|
146
152
|
|
package/dsh/index.js
CHANGED
|
@@ -40,8 +40,15 @@ export function apply(ctx, config = {}) {
|
|
|
40
40
|
if (config.autoRead === true) {
|
|
41
41
|
registerAutoRead(ctx)
|
|
42
42
|
}
|
|
43
|
+
// The provider ids this plugin registered itself. The takeover verdict has
|
|
44
|
+
// to skip them: our wrapper models are synthetic twins of upstream ones,
|
|
45
|
+
// carrying the upstream id and declaring image input, so a plain text-only
|
|
46
|
+
// label matches the twin and the twin's declaration vetoes the takeover
|
|
47
|
+
// that label deserved (issue #36). Filled by registerVisionProvider as
|
|
48
|
+
// wrappers land, including the later sweeps, and read by the verdict.
|
|
49
|
+
const ownProviders = new Set()
|
|
43
50
|
if (config.visionProvider !== false) {
|
|
44
|
-
registerVisionProvider(ctx, config)
|
|
51
|
+
registerVisionProvider(ctx, config, ownProviders)
|
|
45
52
|
}
|
|
46
53
|
// Paste-to-path: the browser half (dsh/client.js) intercepts image pastes
|
|
47
54
|
// and POSTs the bytes here; the file lands in a private temp dir and the
|
|
@@ -57,7 +64,7 @@ export function apply(ctx, config = {}) {
|
|
|
57
64
|
try {
|
|
58
65
|
// scope carries webServer; the plugin's own ctx carries llm for the
|
|
59
66
|
// takeover verdicts.
|
|
60
|
-
registerPasteRoute(scope, ctx)
|
|
67
|
+
registerPasteRoute(scope, ctx, ownProviders)
|
|
61
68
|
} catch (error) {
|
|
62
69
|
console.error(`[modlens] paste-to-path route skipped: ${error}`)
|
|
63
70
|
}
|
|
@@ -215,7 +222,14 @@ const PASTE_MAX_BYTES = 25 * 1024 * 1024
|
|
|
215
222
|
* unresolvable answers false: the native path is the safe default, and a
|
|
216
223
|
* text-only model merely keeps its old error message.
|
|
217
224
|
*/
|
|
218
|
-
|
|
225
|
+
// The provider ids registerVisionProvider mints: the legacy deepseek wrap and
|
|
226
|
+
// the `modlens-<upstream>` form auto-discovery uses. A sibling instance of
|
|
227
|
+
// this plugin derives its ids the same way, which is what makes the pair of
|
|
228
|
+
// checks below meaningful. A custom `config.providerId` is outside the
|
|
229
|
+
// convention on purpose and is covered by the registered-id set instead.
|
|
230
|
+
const OWN_PROVIDER_ID = /^(deepseek-modlens$|modlens-)/
|
|
231
|
+
|
|
232
|
+
async function pasteTakeoverVerdict(host, label, ownProviders) {
|
|
219
233
|
if (typeof label !== 'string' || label.trim() === '') return false
|
|
220
234
|
// Our own wrappers convert pastes at request time with the thumbnail
|
|
221
235
|
// preserved; taking their paste over would defeat the better path.
|
|
@@ -229,6 +243,14 @@ async function pasteTakeoverVerdict(host, label) {
|
|
|
229
243
|
for (const info of llm.listProviders()) {
|
|
230
244
|
const providerId = info?.id
|
|
231
245
|
if (!providerId) continue
|
|
246
|
+
// Our own wrapper: every model in it is a synthetic twin of an upstream
|
|
247
|
+
// one, carrying that upstream id and declaring image input because that
|
|
248
|
+
// is how the wrapper unlocks admission. Scanning it means a plain
|
|
249
|
+
// text-only label matches the twin by id and the twin vetoes the
|
|
250
|
+
// takeover the real model deserved (issue #36). Only ids this plugin
|
|
251
|
+
// registered itself are skipped, so a real vision provider, including
|
|
252
|
+
// one that happens to be named like ours, still votes.
|
|
253
|
+
if (ownProviders?.has(providerId)) continue
|
|
232
254
|
let models = []
|
|
233
255
|
try {
|
|
234
256
|
models = await llm.listModels(providerId)
|
|
@@ -236,6 +258,21 @@ async function pasteTakeoverVerdict(host, label) {
|
|
|
236
258
|
return false
|
|
237
259
|
}
|
|
238
260
|
for (const model of models) {
|
|
261
|
+
// A twin from another instance of this plugin, which the set above
|
|
262
|
+
// cannot know about: a second apply() in the same process hits the
|
|
263
|
+
// duplicate branch, does not claim the id, and would otherwise be
|
|
264
|
+
// vetoed by the first instance's wrapper. Both halves are required.
|
|
265
|
+
// The name marker alone proves nothing, since any provider can put that
|
|
266
|
+
// string in a model name and would then slip past a veto it deserves;
|
|
267
|
+
// the id is what makes it ours, because a sibling instance derives its
|
|
268
|
+
// provider id from the same rule this one does.
|
|
269
|
+
if (
|
|
270
|
+
OWN_PROVIDER_ID.test(providerId) &&
|
|
271
|
+
typeof model?.name === 'string' &&
|
|
272
|
+
/\(modlens vision\)/i.test(model.name)
|
|
273
|
+
) {
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
239
276
|
for (const candidate of [model?.name, model?.id]) {
|
|
240
277
|
if (typeof candidate !== 'string' || candidate.length === 0) continue
|
|
241
278
|
if (!lowered.includes(candidate.toLowerCase())) continue
|
|
@@ -272,7 +309,7 @@ const PASTE_VERDICT_CAP = 32
|
|
|
272
309
|
* stands down instead of swallowing pastes into a 404. Bound to the dsh web
|
|
273
310
|
* server, which listens on loopback by default.
|
|
274
311
|
*/
|
|
275
|
-
function registerPasteRoute(ctx, host) {
|
|
312
|
+
function registerPasteRoute(ctx, host, ownProviders) {
|
|
276
313
|
const verdicts = new Map()
|
|
277
314
|
// The cache key is only the selector label, which cannot tell two
|
|
278
315
|
// same-named models on different routes apart. A route mounting mid-TTL
|
|
@@ -310,7 +347,7 @@ function registerPasteRoute(ctx, host) {
|
|
|
310
347
|
let attempts = 0
|
|
311
348
|
for (;;) {
|
|
312
349
|
const startedEpoch = topologyEpoch
|
|
313
|
-
takeover = await pasteTakeoverVerdict(host, label)
|
|
350
|
+
takeover = await pasteTakeoverVerdict(host, label, ownProviders)
|
|
314
351
|
if (topologyEpoch === startedEpoch) {
|
|
315
352
|
verdicts.delete(label)
|
|
316
353
|
verdicts.set(label, { takeover, at: Date.now() })
|
|
@@ -398,7 +435,7 @@ function registerPasteRoute(ctx, host) {
|
|
|
398
435
|
* deepseek-official wrap keeps its historical `deepseek-modlens` id, so a
|
|
399
436
|
* selector remembering that provider survives the upgrade.
|
|
400
437
|
*/
|
|
401
|
-
function registerVisionProvider(ctx, config) {
|
|
438
|
+
function registerVisionProvider(ctx, config, ownProviders) {
|
|
402
439
|
// Wrap only the text-only members of these families. Their own vision
|
|
403
440
|
// models (present or future: deepseek-vl/ocr/janus, glm-4.5v, glm-5v-...)
|
|
404
441
|
// need no bridge and are excluded by name and by declared modality.
|
|
@@ -464,6 +501,11 @@ function registerVisionProvider(ctx, config) {
|
|
|
464
501
|
},
|
|
465
502
|
evidenceCache: new Map(),
|
|
466
503
|
})
|
|
504
|
+
// Trusted as ours only on a registration this call actually made. A
|
|
505
|
+
// duplicate below means someone else holds that id, and skipping a
|
|
506
|
+
// provider we do not own would let a real vision model's paste be
|
|
507
|
+
// taken over, which is the bug the verdict exists to prevent.
|
|
508
|
+
ownProviders?.add(providerId)
|
|
467
509
|
return true
|
|
468
510
|
} catch (error) {
|
|
469
511
|
// A duplicate means a concurrent or earlier registration already won:
|
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.16.
|
|
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.16.5):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.16.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.16.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.16.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.16.5: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.16.5 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.16.5 <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.
|
|
@@ -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.16.
|
|
27
|
+
$Pinned = '3.16.5'
|
|
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.16.
|
|
25
|
+
PINNED="3.16.5"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|