@liustack/modlens 3.13.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 +9 -0
- package/README.md +44 -0
- package/README.zh-CN.md +56 -12
- package/dist/main.js +2 -2
- package/docs/cli.md +2 -0
- package/docs/cli.zh-CN.md +98 -0
- package/docs/harness-setup.md +20 -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/client.js +126 -0
- package/dsh/index.js +241 -55
- package/package.json +83 -77
- 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/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,21 +152,93 @@ 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
|
|
140
220
|
* text-only, so pastes are refused before any plugin hook runs. This wrapper
|
|
141
221
|
* registers a NEW provider whose model metadata declares image input and
|
|
142
222
|
* whose stream() is a one-line delegation back to the real route. Pick the
|
|
143
|
-
* wrapped model in the model selector, paste, and the
|
|
223
|
+
* wrapped model in the model selector, paste, and the request-time rewrite
|
|
144
224
|
* turns the image into evidence text before the delegated request goes out;
|
|
145
225
|
* the upstream serializer's own image rejection stays as the fail-closed
|
|
146
226
|
* backstop. Guarded feature-detection: if the llm registration surface moved
|
|
147
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.
|
|
148
240
|
*/
|
|
149
241
|
function registerVisionProvider(ctx, config) {
|
|
150
|
-
const upstream = config.upstream || 'deepseek-official'
|
|
151
|
-
const providerId = config.providerId || 'deepseek-modlens'
|
|
152
242
|
// Wrap only the text-only members of these families. Their own vision
|
|
153
243
|
// models (present or future: deepseek-vl/ocr/janus, glm-4.5v, glm-5v-...)
|
|
154
244
|
// need no bridge and are excluded by name and by declared modality.
|
|
@@ -164,58 +254,148 @@ function registerVisionProvider(ctx, config) {
|
|
|
164
254
|
if (typeof ctx.llm?.registerAdapter !== 'function' || typeof ctx.llm?.stream !== 'function') {
|
|
165
255
|
return
|
|
166
256
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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()
|
|
213
398
|
})
|
|
214
|
-
} catch (error) {
|
|
215
|
-
// DUPLICATE_ADAPTER or a preview-era surface change: degrade to the
|
|
216
|
-
// read_image-only plugin, but say so in the harness log instead of
|
|
217
|
-
// vanishing (a swallowed TypeError here once hid a missing base method).
|
|
218
|
-
console.error(`[modlens] vision provider registration skipped: ${error}`)
|
|
219
399
|
}
|
|
220
400
|
}
|
|
221
401
|
|
|
@@ -444,7 +624,13 @@ async function readImageBlock(ctx, block, signal) {
|
|
|
444
624
|
|
|
445
625
|
function run(command, args, signal) {
|
|
446
626
|
return new Promise((resolve, reject) => {
|
|
447
|
-
const child = spawn(command, args, {
|
|
627
|
+
const child = spawn(command, args, {
|
|
628
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
629
|
+
signal,
|
|
630
|
+
// In the packaged desktop app process.execPath is the Electron binary;
|
|
631
|
+
// this makes it behave as plain node for the spawned CLI (issue #25).
|
|
632
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
|
633
|
+
})
|
|
448
634
|
let stdout = ''
|
|
449
635
|
let stderr = ''
|
|
450
636
|
child.stdout.on('data', (chunk) => {
|
package/package.json
CHANGED
|
@@ -1,80 +1,86 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
|
|
2
|
+
"name": "@liustack/modlens",
|
|
3
|
+
"version": "3.15.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
|
}
|
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.
|