@liustack/modlens 3.13.0 → 3.14.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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.14.0 - 2026-08-14
4
+
5
+ - **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.
6
+ - **The CLI survives Electron hosts ([#25](https://github.com/liustack/modlens/issues/25)).** In the packaged dsh desktop app, `process.execPath` is the Electron binary, and commander's Electron auto-detection then mis-slices argv so the script path lands as a stray positional (`too many arguments for 'analyze'`). The CLI now parses argv with explicit node semantics — it is always spawned script-first, whatever binary hosts it — and the plugin's spawns set `ELECTRON_RUN_AS_NODE` for good measure. Thanks to @hi-fangj for tracing it into commander's `_prepareUserArgs`.
7
+
3
8
  ## 3.13.0 - 2026-08-14
4
9
 
5
10
  - **Proxy support actually works now ([#23](https://github.com/liustack/modlens/issues/23)).** 3.12.0's proxy path was broken on arrival, twice over: the bundled copy of undici had its internal `node:http2` references destroyed by bundling (the embedded `ProxyAgent` threw `http2.connect is not a function`), and handing any undici 8 dispatcher to the host's built-in fetch (a different undici major) fails with `UND_ERR_INVALID_ARG` regardless. undici is no longer bundled — it resolves from `node_modules`, shrinking the CLI bundle from 1.17 MB to 131 KB — and the proxied path now uses undici's own fetch so dispatcher and fetch are same-sourced, with the dispatcher closed after the response so its keep-alive pool cannot pin the process open. A new integration test drives the built CLI through a real local HTTP proxy, the exact coverage whose absence let 3.12.0 ship broken (and whose first draft repeated a classic mistake: `spawnSync` freezes the test's own fake servers, so the CLI must be spawned async); both the env-var and explicit-setting forms were also verified against a real LAN proxy. Independent review of the fix then caught the same cross-version boundary hiding in the no-proxy remote-image path — the IP-pinned download `Agent` was still handed to the host's fetch — so that path is now same-sourced too, and the Node floor rises to 22.19 (undici 8's own engine requirement, which externalizing made load-bearing). Thanks to @JooJeen for a diagnosis that had already isolated both layers.
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.13.0");
3740
+ program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.14.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"}}'`
@@ -3904,4 +3904,4 @@ config.command("show").description("Print the effective config (file merged with
3904
3904
  process.exitCode = 1;
3905
3905
  }
3906
3906
  });
3907
- await program.parseAsync();
3907
+ await program.parseAsync(process.argv, { from: "node" });
@@ -57,3 +57,21 @@ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@latest
57
57
  ```
58
58
 
59
59
  This registers a `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.
60
+
61
+ ### Paste-to-path (web profile)
62
+
63
+ Pasting an image into the dsh Web UI under a **text-only model** used to die at
64
+ image admission. The plugin now ships a browser half (loaded automatically by
65
+ dsh's client plugin system) that takes over the paste in exactly that case:
66
+ the image bytes go to the plugin's `/modlens/paste` route on the dsh web
67
+ server (loopback, magic-byte checked, 25 MB cap), land as a private temp file,
68
+ and the composer receives the file path as plain text — the same shape Pi,
69
+ OpenCode, and Claude Code hand their models, and the modlens skill's and
70
+ `read_image` tool's primary trigger. Admission never fires because the message
71
+ carries no image attachment.
72
+
73
+ The takeover is conditional: when the selected model is a `(modlens vision)`
74
+ variant or a known vision model, the native paste flow is left alone (variants
75
+ convert at request time with the thumbnail preserved; vision models read
76
+ images themselves). `pasteToPath: false` in the plugin row turns the whole
77
+ feature off.
package/dsh/client.js ADDED
@@ -0,0 +1,126 @@
1
+ // Browser half of the modlens dsh plugin: paste-to-path.
2
+ //
3
+ // A capture-phase paste listener runs before the composer's own handler.
4
+ // When the clipboard carries image files, the default intake (attachment ->
5
+ // host image admission -> "model does not support images" for text-only
6
+ // models) is suppressed; the bytes go to the plugin's host route
7
+ // (POST /modlens/paste), land as a private temp file, and the returned path
8
+ // is inserted into the composer as plain text. A text-only model then sees
9
+ // exactly what Pi, OpenCode, and Claude Code hand their models: a file path,
10
+ // which is also the modlens skill's and read_image tool's primary trigger.
11
+ //
12
+ // Hand-written in the lazy-CJS bundle protocol (window.__ModuleLoader__.load
13
+ // with a factory returning cordis-plugin exports), so no build step and no
14
+ // imports from dsh client packages — the same zero-dependency stance as the
15
+ // host half.
16
+ window.__ModuleLoader__.load({
17
+ id: '@liustack/modlens',
18
+ factory: () => {
19
+ var module = { exports: {} }
20
+ var exports = module.exports
21
+
22
+ function imageFilesOf(event) {
23
+ var items = event.clipboardData && event.clipboardData.items
24
+ if (!items) return []
25
+ var files = []
26
+ for (var i = 0; i < items.length; i++) {
27
+ var item = items[i]
28
+ if (item.kind !== 'file') continue
29
+ var file = item.getAsFile()
30
+ if (file && /^image\//.test(file.type)) files.push(file)
31
+ }
32
+ return files
33
+ }
34
+
35
+ function insertText(target, text) {
36
+ var el =
37
+ target && (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT')
38
+ ? target
39
+ : document.activeElement
40
+ if (!el || (el.tagName !== 'TEXTAREA' && el.tagName !== 'INPUT')) return
41
+ el.focus()
42
+ // execCommand fires the input event React's controlled textarea needs;
43
+ // the prototype-setter dance is the fallback for engines dropping it.
44
+ var inserted = false
45
+ try {
46
+ inserted = document.execCommand('insertText', false, text)
47
+ } catch {
48
+ inserted = false
49
+ }
50
+ if (!inserted) {
51
+ var proto =
52
+ el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype
53
+ var setter = Object.getOwnPropertyDescriptor(proto, 'value').set
54
+ setter.call(el, el.value + text)
55
+ el.dispatchEvent(new Event('input', { bubbles: true }))
56
+ }
57
+ }
58
+
59
+ function uploadOne(file) {
60
+ return file.arrayBuffer().then((buffer) =>
61
+ fetch('/modlens/paste', { method: 'POST', body: buffer }).then((res) => {
62
+ if (!res.ok) {
63
+ return res
64
+ .json()
65
+ .catch(() => ({}))
66
+ .then((body) => {
67
+ throw new Error(body.error || `paste upload failed (${res.status})`)
68
+ })
69
+ }
70
+ return res.json()
71
+ }),
72
+ )
73
+ }
74
+
75
+ // The takeover is for text-only models: the (modlens vision) variants
76
+ // convert pastes at request time with the thumbnail preserved, and real
77
+ // vision models read images natively — both keep the original paste UX.
78
+ // The model selector button's accessible label is the only client-side
79
+ // source of the current model; when it cannot be found, taking over is
80
+ // the safe default (text-only is the common case this exists for).
81
+ var VISION_HINT = /\(modlens vision\)|deepseek-(vl|ocr)|janus|glm-[\d.]*v\b|vision|image/i
82
+
83
+ function currentModelLabel() {
84
+ var buttons = document.querySelectorAll('button[aria-label]')
85
+ for (var i = 0; i < buttons.length; i++) {
86
+ var label = buttons[i].getAttribute('aria-label') || ''
87
+ if (/选择模型|select model|current model/i.test(label)) return label
88
+ }
89
+ return ''
90
+ }
91
+
92
+ function onPaste(event) {
93
+ var files = imageFilesOf(event)
94
+ if (files.length === 0) return
95
+ if (VISION_HINT.test(currentModelLabel())) return
96
+ // Take the paste before the composer's intake starts an attachment (and
97
+ // with it the host-side image admission a text-only model fails).
98
+ event.preventDefault()
99
+ event.stopImmediatePropagation()
100
+ var target = event.target
101
+ Promise.all(files.map(uploadOne))
102
+ .then((results) => {
103
+ var text = results
104
+ .map((r) => r.path)
105
+ .filter(Boolean)
106
+ .join(' ')
107
+ if (text) insertText(target, `${text} `)
108
+ })
109
+ .catch((error) => {
110
+ console.error(`[modlens] paste-to-path failed: ${error && error.message ? error.message : error}`)
111
+ })
112
+ }
113
+
114
+ function apply(ctx) {
115
+ document.addEventListener('paste', onPaste, true)
116
+ // cordis effect: unregister on plugin disposal (HMR, profile reload).
117
+ if (typeof ctx.effect === 'function') {
118
+ ctx.effect(() => () => document.removeEventListener('paste', onPaste, true), 'modlens: paste-to-path listener')
119
+ }
120
+ }
121
+
122
+ exports.apply = apply
123
+ exports.inject = []
124
+ return module.exports
125
+ },
126
+ })
package/dsh/index.js CHANGED
@@ -43,6 +43,24 @@ export function apply(ctx, config = {}) {
43
43
  if (config.visionProvider !== false) {
44
44
  registerVisionProvider(ctx, config)
45
45
  }
46
+ // Paste-to-path: the browser half (dsh/client.js) intercepts image pastes
47
+ // and POSTs the bytes here; the file lands in a private temp dir and the
48
+ // path text goes into the composer instead of an image attachment. A
49
+ // text-only model then never trips image admission, and the path is the
50
+ // same trigger shape Pi, OpenCode, and Claude Code hand their models.
51
+ // webServer exists only under the web profile, and this cordis has no
52
+ // optional-inject form, so the route rides a scoped ctx.inject: the closure
53
+ // runs when the service appears and never runs where it does not (headless
54
+ // stays untouched, and the plugin itself never waits on it).
55
+ if (config.pasteToPath !== false && typeof ctx.inject === 'function') {
56
+ ctx.inject(['webServer'], (scope) => {
57
+ try {
58
+ registerPasteRoute(scope)
59
+ } catch (error) {
60
+ console.error(`[modlens] paste-to-path route skipped: ${error}`)
61
+ }
62
+ })
63
+ }
46
64
  // Registered as a raw JSON-Schema tool definition (no dsh package imports:
47
65
  // the developer-preview registry accepts these and out-of-tree resolution
48
66
  // of @deepseek-ai/dsh-tools is not yet reliable), so this plugin owns its
@@ -134,6 +152,68 @@ export function apply(ctx, config = {}) {
134
152
  }
135
153
  }
136
154
 
155
+ // Image magic bytes for the paste route: refuse anything that is not a real
156
+ // image before a byte touches disk. Mirrors the CLI's sniffing table.
157
+ const PASTE_SNIFFS = [
158
+ { ext: '.png', test: (b) => b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 },
159
+ { ext: '.jpg', test: (b) => b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
160
+ { ext: '.gif', test: (b) => b.length >= 6 && b.toString('ascii', 0, 3) === 'GIF' },
161
+ { ext: '.webp', test: (b) => b.length >= 12 && b.toString('ascii', 0, 4) === 'RIFF' && b.toString('ascii', 8, 12) === 'WEBP' },
162
+ { ext: '.heic', test: (b) => b.length >= 12 && b.toString('ascii', 4, 8) === 'ftyp' },
163
+ ]
164
+ const PASTE_MAX_BYTES = 25 * 1024 * 1024
165
+
166
+ /**
167
+ * POST /modlens/paste: image bytes in, `{ path }` out. Bound to the dsh web
168
+ * server, which listens on loopback by default; the file is private (0600)
169
+ * in a fresh unpredictable temp dir, magic-byte checked and size-capped.
170
+ */
171
+ function registerPasteRoute(ctx) {
172
+ ctx.webServer.register({
173
+ name: 'modlens-paste',
174
+ kind: 'exact',
175
+ path: '/modlens/paste',
176
+ handler: async (req, res) => {
177
+ if (req.method !== 'POST') {
178
+ res.writeHead(405).end()
179
+ return
180
+ }
181
+ try {
182
+ const chunks = []
183
+ let total = 0
184
+ for await (const chunk of req) {
185
+ total += chunk.length
186
+ if (total > PASTE_MAX_BYTES) {
187
+ res.writeHead(413, { 'content-type': 'application/json' })
188
+ res.end(JSON.stringify({ error: `image over the ${PASTE_MAX_BYTES}-byte limit` }))
189
+ req.destroy()
190
+ return
191
+ }
192
+ chunks.push(chunk)
193
+ }
194
+ const buffer = Buffer.concat(chunks)
195
+ const sniff = PASTE_SNIFFS.find((s) => s.test(buffer))
196
+ if (!sniff) {
197
+ res.writeHead(400, { 'content-type': 'application/json' })
198
+ res.end(JSON.stringify({ error: 'not a recognized image (png/jpeg/gif/webp/heic)' }))
199
+ return
200
+ }
201
+ const { mkdtemp, writeFile } = await import('node:fs/promises')
202
+ const { tmpdir } = await import('node:os')
203
+ const { join } = await import('node:path')
204
+ const dir = await mkdtemp(join(tmpdir(), 'modlens-dsh-paste-'))
205
+ const file = join(dir, `paste${sniff.ext}`)
206
+ await writeFile(file, buffer, { mode: 0o600 })
207
+ res.writeHead(200, { 'content-type': 'application/json' })
208
+ res.end(JSON.stringify({ path: file }))
209
+ } catch (error) {
210
+ res.writeHead(500, { 'content-type': 'application/json' })
211
+ res.end(JSON.stringify({ error: String(error && error.message ? error.message : error) }))
212
+ }
213
+ },
214
+ })
215
+ }
216
+
137
217
  /**
138
218
  * Phase 3: the paste unlock. dsh's image admission asks the selected
139
219
  * provider's adapter for inputModalities, and the DeepSeek adapter hardcodes
@@ -444,7 +524,13 @@ async function readImageBlock(ctx, block, signal) {
444
524
 
445
525
  function run(command, args, signal) {
446
526
  return new Promise((resolve, reject) => {
447
- const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], signal })
527
+ const child = spawn(command, args, {
528
+ stdio: ['ignore', 'pipe', 'pipe'],
529
+ signal,
530
+ // In the packaged desktop app process.execPath is the Electron binary;
531
+ // this makes it behave as plain node for the spawned CLI (issue #25).
532
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
533
+ })
448
534
  let stdout = ''
449
535
  let stderr = ''
450
536
  child.stdout.on('data', (chunk) => {
package/package.json CHANGED
@@ -1,80 +1,86 @@
1
1
  {
2
- "name": "@liustack/modlens",
3
- "version": "3.13.0",
4
- "description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
5
- "type": "module",
6
- "bin": {
7
- "modlens": "./dist/main.js"
8
- },
9
- "scripts": {
10
- "dev": "vite build --watch",
11
- "build": "vite build",
12
- "typecheck": "tsc --noEmit",
13
- "test": "vitest run",
14
- "coverage": "vitest run --coverage",
15
- "lint": "biome check src scripts dsh",
16
- "format": "biome check --write src scripts",
17
- "eval": "node evals/run.mjs",
18
- "release": "node scripts/release.mjs",
19
- "prepublishOnly": "pnpm build",
20
- "docs:list": "node scripts/docs-list.js"
21
- },
22
- "files": [
23
- "dist",
24
- "docs",
25
- "skills/modlens/SKILL.md",
26
- "skills/modlens/scripts",
27
- "skills/modlens/references",
28
- "CHANGELOG.md",
29
- "SECURITY.md",
30
- "dsh",
31
- "cordis.patch.yml"
32
- ],
33
- "keywords": [
34
- "cli",
35
- "vision",
36
- "ocr",
37
- "antigravity",
38
- "agent-skill",
39
- "claude-code",
40
- "skill",
41
- "image-to-text",
42
- "multimodal",
43
- "modlens"
44
- ],
45
- "author": "Leon Liu",
46
- "license": "MIT",
47
- "repository": {
48
- "type": "git",
49
- "url": "git+https://github.com/liustack/modlens.git"
50
- },
51
- "bugs": {
52
- "url": "https://github.com/liustack/modlens/issues"
53
- },
54
- "homepage": "https://github.com/liustack/modlens#readme",
55
- "engines": {
56
- "node": ">=22.19"
57
- },
58
- "dependencies": {
59
- "commander": "^13.1.0",
60
- "undici": "^8.10.0"
61
- },
62
- "devDependencies": {
63
- "@biomejs/biome": "^2.5.7",
64
- "@types/node": "^22.19.7",
65
- "@vitest/coverage-v8": "^3.2.7",
66
- "typescript": "^5.9.3",
67
- "vite": "^6.4.1",
68
- "vitest": "^3.2.7"
69
- },
70
- "exports": {
71
- ".": "./dsh/index.js",
72
- "./dsh": "./dsh/index.js",
73
- "./package.json": "./package.json"
74
- },
75
- "dsh": {
76
- "bundle": {
77
- "patch": "./cordis.patch.yml"
2
+ "name": "@liustack/modlens",
3
+ "version": "3.14.0",
4
+ "description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "modlens": "./dist/main.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "vite build --watch",
11
+ "build": "vite build",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "vitest run",
14
+ "coverage": "vitest run --coverage",
15
+ "lint": "biome check src scripts dsh",
16
+ "format": "biome check --write src scripts",
17
+ "eval": "node evals/run.mjs",
18
+ "release": "node scripts/release.mjs",
19
+ "prepublishOnly": "pnpm build",
20
+ "docs:list": "node scripts/docs-list.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "docs",
25
+ "skills/modlens/SKILL.md",
26
+ "skills/modlens/scripts",
27
+ "skills/modlens/references",
28
+ "CHANGELOG.md",
29
+ "SECURITY.md",
30
+ "dsh",
31
+ "cordis.patch.yml"
32
+ ],
33
+ "keywords": [
34
+ "cli",
35
+ "vision",
36
+ "ocr",
37
+ "antigravity",
38
+ "agent-skill",
39
+ "claude-code",
40
+ "skill",
41
+ "image-to-text",
42
+ "multimodal",
43
+ "modlens"
44
+ ],
45
+ "author": "Leon Liu",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/liustack/modlens.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/liustack/modlens/issues"
53
+ },
54
+ "homepage": "https://github.com/liustack/modlens#readme",
55
+ "engines": {
56
+ "node": ">=22.19"
57
+ },
58
+ "dependencies": {
59
+ "commander": "^13.1.0",
60
+ "undici": "^8.10.0"
61
+ },
62
+ "devDependencies": {
63
+ "@biomejs/biome": "^2.5.7",
64
+ "@types/node": "^22.19.7",
65
+ "@vitest/coverage-v8": "^3.2.7",
66
+ "typescript": "^5.9.3",
67
+ "vite": "^6.4.1",
68
+ "vitest": "^3.2.7"
69
+ },
70
+ "exports": {
71
+ ".": "./dsh/index.js",
72
+ "./dsh": "./dsh/index.js",
73
+ "./package.json": "./package.json",
74
+ "./client": "./dsh/client.js"
75
+ },
76
+ "dsh": {
77
+ "bundle": {
78
+ "patch": "./cordis.patch.yml"
79
+ },
80
+ "client": {
81
+ "inject": [],
82
+ "platform": "web",
83
+ "immediately": true
84
+ }
78
85
  }
79
- }
80
86
  }
@@ -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.13.0):
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.14.0):
24
24
 
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>`.
25
+ 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.14.0: `modlens <args>`.
26
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.14.0 modlens <args>`.
27
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.14.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.
@@ -8,7 +8,7 @@ shell syntax.
8
8
 
9
9
  ## Pinned version
10
10
 
11
- - Pinned CLI version: 3.13.0
11
+ - Pinned CLI version: 3.14.0
12
12
  - npm package: `@liustack/modlens`
13
13
  - CLI binary name: `modlens`
14
14
 
@@ -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.13.0'
27
+ $Pinned = '3.14.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.13.0"
25
+ PINNED="3.14.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"