@liustack/modlens 3.12.0 → 3.13.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 +10 -0
- package/dist/main.js +72 -27037
- package/dsh/index.js +69 -26
- package/package.json +2 -2
- package/skills/modlens/SKILL.md +5 -5
- package/skills/modlens/references/runtime.md +7 -6
- package/skills/modlens/scripts/run.ps1 +4 -4
- package/skills/modlens/scripts/run.sh +4 -4
package/dsh/index.js
CHANGED
|
@@ -47,8 +47,16 @@ export function apply(ctx, config = {}) {
|
|
|
47
47
|
// the developer-preview registry accepts these and out-of-tree resolution
|
|
48
48
|
// of @deepseek-ai/dsh-tools is not yet reliable), so this plugin owns its
|
|
49
49
|
// own argument validation inside execute.
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
//
|
|
51
|
+
// The name can collide: hosts with a durable attachment store mount their
|
|
52
|
+
// own native read_image (dsh-tool-fs), and a duplicate registration throws,
|
|
53
|
+
// which used to fail the whole plugin fiber (issue #21). The collision
|
|
54
|
+
// falls back to a prefixed name — valuable exactly there, since the native
|
|
55
|
+
// tool is gated on the model declaring image input and vanishes for
|
|
56
|
+
// text-only models — and any other registration error degrades loudly
|
|
57
|
+
// instead of taking the vision wrapper down with it.
|
|
58
|
+
const readImageTool = (toolName) => ({
|
|
59
|
+
name: toolName,
|
|
52
60
|
description:
|
|
53
61
|
'Read an image through the modlens vision bridge. Use whenever a message references an image the current model cannot see: a local file path or an http(s) URL to a screenshot, photo, chart, diagram, or document scan. Returns structured evidence with every word transcribed (ocr.full_text), layout regions in reading order, semantics, and an uncertainty list; quote the evidence instead of guessing. Requires a configured modlens engine (run `npx @liustack/modlens doctor` in a terminal to check).',
|
|
54
62
|
parameters: {
|
|
@@ -106,6 +114,24 @@ export function apply(ctx, config = {}) {
|
|
|
106
114
|
return parsed.result
|
|
107
115
|
},
|
|
108
116
|
})
|
|
117
|
+
const preferred = config.toolName || 'read_image'
|
|
118
|
+
try {
|
|
119
|
+
ctx.tools.register(readImageTool(preferred))
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const fallback = 'modlens_read_image'
|
|
122
|
+
if (preferred !== fallback && /already|duplicate/i.test(String(error))) {
|
|
123
|
+
try {
|
|
124
|
+
ctx.tools.register(readImageTool(fallback))
|
|
125
|
+
console.error(
|
|
126
|
+
`[modlens] tool name "${preferred}" is taken by the host; registered as "${fallback}" instead`,
|
|
127
|
+
)
|
|
128
|
+
} catch (retryError) {
|
|
129
|
+
console.error(`[modlens] read_image registration skipped: ${retryError}`)
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.error(`[modlens] read_image registration skipped: ${error}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
/**
|
|
@@ -271,21 +297,47 @@ function abortableWait(promise, signal) {
|
|
|
271
297
|
})
|
|
272
298
|
}
|
|
273
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Image blocks hide at two depths: top-level message content (pastes), and
|
|
302
|
+
* inside tool-result content (dsh's own read_image tool nests one there).
|
|
303
|
+
* The upstream adapter's rejection check recurses (issue #24), so the
|
|
304
|
+
* conversion must recurse the same way or a nested image wedges the session
|
|
305
|
+
* permanently — the durable log keeps the real block, and every later turn
|
|
306
|
+
* re-fails on it.
|
|
307
|
+
*/
|
|
308
|
+
function contentHasImage(blocks) {
|
|
309
|
+
return (
|
|
310
|
+
Array.isArray(blocks) &&
|
|
311
|
+
blocks.some(
|
|
312
|
+
(b) => b?.type === 'image' || (b?.type === 'tool-result' && contentHasImage(b.content)),
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function convertBlocks(blocks, convertOne) {
|
|
318
|
+
const out = []
|
|
319
|
+
for (const block of blocks) {
|
|
320
|
+
if (block?.type === 'image') {
|
|
321
|
+
out.push(await convertOne(block))
|
|
322
|
+
} else if (block?.type === 'tool-result' && contentHasImage(block.content)) {
|
|
323
|
+
out.push({ ...block, content: await convertBlocks(block.content, convertOne) })
|
|
324
|
+
} else {
|
|
325
|
+
out.push(block)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return out
|
|
329
|
+
}
|
|
330
|
+
|
|
274
331
|
async function convertImagesToEvidence(ctx, messages, signal, adapter) {
|
|
275
332
|
const out = []
|
|
276
333
|
for (const message of messages) {
|
|
277
|
-
if (!
|
|
334
|
+
if (!contentHasImage(message.content)) {
|
|
278
335
|
out.push(message)
|
|
279
336
|
continue
|
|
280
337
|
}
|
|
281
|
-
const content =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
content.push(block)
|
|
285
|
-
continue
|
|
286
|
-
}
|
|
287
|
-
content.push(await abortableWait(cachedEvidence(ctx, adapter, block), signal))
|
|
288
|
-
}
|
|
338
|
+
const content = await convertBlocks(message.content, (block) =>
|
|
339
|
+
abortableWait(cachedEvidence(ctx, adapter, block), signal),
|
|
340
|
+
)
|
|
289
341
|
out.push({ ...message, content })
|
|
290
342
|
}
|
|
291
343
|
return out
|
|
@@ -305,28 +357,19 @@ function registerAutoRead(ctx) {
|
|
|
305
357
|
if (decision.kind !== 'enter') {
|
|
306
358
|
return decision
|
|
307
359
|
}
|
|
308
|
-
|
|
309
|
-
(message) =>
|
|
310
|
-
Array.isArray(message.content) &&
|
|
311
|
-
message.content.some((block) => block?.type === 'image'),
|
|
312
|
-
)
|
|
313
|
-
if (!hasImage) {
|
|
360
|
+
if (!decision.messages.some((message) => contentHasImage(message.content))) {
|
|
314
361
|
return decision
|
|
315
362
|
}
|
|
316
363
|
const messages = []
|
|
317
364
|
for (const message of decision.messages) {
|
|
318
|
-
if (!
|
|
365
|
+
if (!contentHasImage(message.content)) {
|
|
319
366
|
messages.push(message)
|
|
320
367
|
continue
|
|
321
368
|
}
|
|
322
|
-
const content =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
continue
|
|
327
|
-
}
|
|
328
|
-
content.push((await readImageBlock(ctx, block, payload.signal)).block)
|
|
329
|
-
}
|
|
369
|
+
const content = await convertBlocks(
|
|
370
|
+
message.content,
|
|
371
|
+
async (block) => (await readImageBlock(ctx, block, payload.signal)).block,
|
|
372
|
+
)
|
|
330
373
|
messages.push({ ...message, content })
|
|
331
374
|
}
|
|
332
375
|
return { kind: 'enter', messages }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liustack/modlens",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"homepage": "https://github.com/liustack/modlens#readme",
|
|
55
55
|
"engines": {
|
|
56
|
-
"node": ">=22.
|
|
56
|
+
"node": ">=22.19"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"commander": "^13.1.0",
|
package/skills/modlens/SKILL.md
CHANGED
|
@@ -20,12 +20,12 @@ 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.13.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.
|
|
28
|
-
4. Otherwise tell the user no JavaScript runtime was found and that installing Node 22.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.13.0: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.13.0 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.13.0 <args>`.
|
|
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.
|
|
31
31
|
|
|
@@ -8,7 +8,7 @@ shell syntax.
|
|
|
8
8
|
|
|
9
9
|
## Pinned version
|
|
10
10
|
|
|
11
|
-
- Pinned CLI version: 3.
|
|
11
|
+
- Pinned CLI version: 3.13.0
|
|
12
12
|
- npm package: `@liustack/modlens`
|
|
13
13
|
- CLI binary name: `modlens`
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ launcher/reference copies ever drift from `package.json`.
|
|
|
22
22
|
Each call resolves a way to run the CLI, in this order:
|
|
23
23
|
|
|
24
24
|
1. **A compatible `modlens` already on `PATH`** — run it directly, by name.
|
|
25
|
-
2. **`npx` present, and `node` meets the CLI's 22.
|
|
25
|
+
2. **`npx` present, and `node` meets the CLI's 22.19 floor** — `npx --yes --package @liustack/modlens@<pinned> modlens <args>`. An npx sitting on an older node is skipped: it would select a path known to fail at run time.
|
|
26
26
|
3. **`bunx` present** — `bunx --bun @liustack/modlens@<pinned> <args>`.
|
|
27
27
|
4. **A native artifact** — reserved for phase B. None is published yet, so this
|
|
28
28
|
branch reports `nativeArtifact.available: false` and moves on.
|
|
@@ -70,13 +70,13 @@ would have set.
|
|
|
70
70
|
- `checked.pathCli` — `{ present, path, version, compatible }` for a `modlens`
|
|
71
71
|
on `PATH`, with `compatible` applying the rule above.
|
|
72
72
|
- `checked.npx` — `{ present, path, nodeMeetsFloor }`; `nodeMeetsFloor` is whether
|
|
73
|
-
the local node satisfies the CLI's 22.
|
|
73
|
+
the local node satisfies the CLI's 22.19 floor, required for the npx path.
|
|
74
74
|
- `checked.bunx` — `{ present, path }`.
|
|
75
75
|
- `checked.node` — `{ present, version }`.
|
|
76
76
|
- `nativeArtifact` — `{ available, note }`; `available` is `false` in phase A.
|
|
77
77
|
- `selected` — the resolved path: `path`, `npx`, `bunx`, or `none`.
|
|
78
78
|
- `nextSteps` — when `selected` is `none`, one or two plain-language actions for
|
|
79
|
-
the user (install Node 22.
|
|
79
|
+
the user (install Node 22.19+, or Bun); empty otherwise.
|
|
80
80
|
- `cliDoctor` — when a CLI is resolvable, the CLI's own `doctor --json` report
|
|
81
81
|
(provider, config, and harness diagnosis) is nested here; `null` otherwise.
|
|
82
82
|
|
|
@@ -87,8 +87,9 @@ first time (that is how those runners work); after that it is served from the
|
|
|
87
87
|
local cache.
|
|
88
88
|
|
|
89
89
|
One capability note for the bunx path: Bun cannot load `node:sqlite`, which
|
|
90
|
-
OpenCode paste recovery needs, so on a machine
|
|
91
|
-
bunx, `recover-paste` for OpenCode requires
|
|
90
|
+
OpenCode paste recovery needs (unflagged in Node since 22.13), so on a machine
|
|
91
|
+
where the launcher resolved to bunx, `recover-paste` for OpenCode requires a
|
|
92
|
+
real Node install — 22.19+, since that is the floor this launcher accepts.
|
|
92
93
|
|
|
93
94
|
## Delivery form: local CLI, long term
|
|
94
95
|
|
|
@@ -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.13.0'
|
|
28
28
|
# -------------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
@@ -72,7 +72,7 @@ function Test-Compatible {
|
|
|
72
72
|
# The npx path runs the CLI on this machine's node, so npx is only usable when
|
|
73
73
|
# node itself meets the CLI's floor. An old node with a working npx used to be
|
|
74
74
|
# selected anyway, a path known to fail at run time.
|
|
75
|
-
$NodeFloor = '22.
|
|
75
|
+
$NodeFloor = '22.19.0'
|
|
76
76
|
function Test-NodeMeetsFloor {
|
|
77
77
|
if (-not (Get-Command node -ErrorAction SilentlyContinue)) { return $false }
|
|
78
78
|
try { $nv = ((& node --version 2>$null) -replace '^v', '') } catch { return $false }
|
|
@@ -165,7 +165,7 @@ function Build-DiagnosisJson {
|
|
|
165
165
|
$steps = @()
|
|
166
166
|
if ($script:Selected -eq 'none') {
|
|
167
167
|
$major = $Pinned.Split('.')[0]
|
|
168
|
-
$first = "Install Node 22.
|
|
168
|
+
$first = "Install Node 22.19+ from https://nodejs.org so npx can run $Package@$Pinned, then re-run this launcher."
|
|
169
169
|
if ($script:NpxPresent -and (-not $script:NodeFloorOk)) {
|
|
170
170
|
$first = "npx is present but node $(if ($script:NodeVer) { $script:NodeVer } else { 'missing' }) is below the $NodeFloor floor this CLI needs. Upgrade Node at https://nodejs.org, then re-run this launcher."
|
|
171
171
|
}
|
|
@@ -215,7 +215,7 @@ function Write-DiagnosisText {
|
|
|
215
215
|
Write-Output ''
|
|
216
216
|
Write-Output ("No runtime can launch {0} here. {1}" -f $Bin, $NativeNote)
|
|
217
217
|
Write-Output 'Next steps:'
|
|
218
|
-
Write-Output ' - Install Node 22.
|
|
218
|
+
Write-Output ' - Install Node 22.19+ from https://nodejs.org, then re-run this launcher.'
|
|
219
219
|
Write-Output (" - Or install Bun from https://bun.sh, or put a compatible {0} on PATH." -f $Bin)
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -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.13.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"
|
|
@@ -70,7 +70,7 @@ cli_version() {
|
|
|
70
70
|
# The npx path runs the CLI on this machine's node, so npx is only usable when
|
|
71
71
|
# node itself meets the CLI's floor. An old node with a working npx used to be
|
|
72
72
|
# selected anyway, a path known to fail at run time.
|
|
73
|
-
NODE_FLOOR="22.
|
|
73
|
+
NODE_FLOOR="22.19.0"
|
|
74
74
|
node_meets_floor() {
|
|
75
75
|
command -v node >/dev/null 2>&1 || return 1
|
|
76
76
|
_nv="$(node --version 2>/dev/null | sed 's/^v//')"
|
|
@@ -189,7 +189,7 @@ compute_next_steps() {
|
|
|
189
189
|
if [ "$G_NPX_PRESENT" = 1 ] && [ "$G_NODE_FLOOR_OK" = 0 ]; then
|
|
190
190
|
_s1="npx is present but node ${G_NODE_VER:-missing} is below the $NODE_FLOOR floor this CLI needs. Upgrade Node at https://nodejs.org, then re-run this launcher."
|
|
191
191
|
else
|
|
192
|
-
_s1="Install Node 22.
|
|
192
|
+
_s1="Install Node 22.19+ from https://nodejs.org so npx can run $PKG@$PINNED, then re-run this launcher."
|
|
193
193
|
fi
|
|
194
194
|
_s2="No JavaScript runtime? Install Bun from https://bun.sh to use bunx, or put a compatible $BIN (major ${PINNED%%.*}, at or above $PINNED) on PATH."
|
|
195
195
|
G_NEXTSTEPS="$(printf '"%s", "%s"' "$(json_escape "$_s1")" "$(json_escape "$_s2")")"
|
|
@@ -255,7 +255,7 @@ emit_text() {
|
|
|
255
255
|
if [ "$G_SEL" = "none" ]; then
|
|
256
256
|
printf '\nNo runtime can launch %s here. %s\n' "$BIN" "$NATIVE_NOTE"
|
|
257
257
|
printf 'Next steps:\n'
|
|
258
|
-
printf ' - Install Node 22.
|
|
258
|
+
printf ' - Install Node 22.19+ from https://nodejs.org, then re-run this launcher.\n'
|
|
259
259
|
printf ' - Or install Bun from https://bun.sh, or put a compatible %s on PATH.\n' "$BIN"
|
|
260
260
|
fi
|
|
261
261
|
}
|