@liustack/modlens 3.11.0 → 3.12.1

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.12.1 - 2026-08-14
4
+
5
+ - **claude-cli reads the envelope's `structured_output` first ([#22](https://github.com/liustack/modlens/issues/22)).** Newer claude CLI builds ship the schema-parsed object beside the `result` string, and the parser only hard-parsed the string, so an unescaped newline in the OCR text failed the whole read while the good object sat unread — intermittently, since it depended on what the model emitted. The parse order is now `structured_output`, then fence-tolerant extraction of the result string, then the error, matching the antigravity provider. Thanks to @lin-nanxing for the precise diagnosis, down to the code lines.
6
+ - **A `read_image` name collision no longer kills the whole dsh plugin ([#21](https://github.com/liustack/modlens/issues/21)).** Hosts with a durable attachment store mount dsh's own native `read_image` (from `dsh-tool-fs`), the duplicate registration threw, and the whole plugin fiber failed — vision wrapper included. The registration now falls back to `modlens_read_image` on a name collision (valuable exactly there: the native tool is gated on the model declaring image input and vanishes for text-only models, so the renamed bridge is the only image path left), the name is configurable via the plugin row's `toolName`, and any other registration error degrades loudly instead of taking the plugin down. Thanks to @abyss-stars for the root-cause analysis and the interim patch.
7
+
8
+ ## 3.12.0 - 2026-08-14
9
+
10
+ - **The API providers work behind a proxy ([#20](https://github.com/liustack/modlens/issues/20)).** Node's fetch ignores `HTTP_PROXY`/`HTTPS_PROXY` entirely, so machines that reach the internet through a proxy could not use `gemini-api` at all, and the failure surfaced as a bare `fetch failed`. The three inline API providers now honor the standard environment variables (`NO_PROXY` included, via undici's `EnvHttpProxyAgent`), with an explicit setting as the escape hatch: `modlens config set proxy <url>` for all API providers, `<provider>.proxy` to scope it to one. A connect-level failure now names the unreachable host and points at both knobs instead of saying `fetch failed`. Scope is deliberate and documented: the proxy applies to API requests only, while the remote-image download path keeps its direct, IP-pinned connection, because its SSRF guards validate the exact address being contacted and a proxy would blind them. Thanks to @soloyu for a report that arrived with the diagnosis, the fix direction, and the security boundary already thought through.
11
+
3
12
  ## 3.11.0 - 2026-08-14
4
13
 
5
14
  - **A full-project audit, all ten findings fixed, then re-reviewed until clean.** An independent deep review of the whole repository (P0: none) surfaced ten conditional-but-real defects. Every fix went back through further independent review rounds, which caught real bugs in the first fixes themselves (case and Unicode boundaries, a cancellation regression); the final round accepted with no blocking findings. Each item below carries a regression test — the suite grew by 32 cases.
package/dist/main.js CHANGED
@@ -27414,6 +27414,51 @@ async function readCapped(response2, url) {
27414
27414
  }
27415
27415
  return Buffer.concat(chunks);
27416
27416
  }
27417
+ function apiProxyDispatcher(explicitProxy, env) {
27418
+ const proxy = explicitProxy?.trim();
27419
+ if (proxy) {
27420
+ return new undiciExports.ProxyAgent(proxy);
27421
+ }
27422
+ if (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy) {
27423
+ return new undiciExports.EnvHttpProxyAgent();
27424
+ }
27425
+ return void 0;
27426
+ }
27427
+ const CONNECT_CODES = /* @__PURE__ */ new Set([
27428
+ "UND_ERR_CONNECT_TIMEOUT",
27429
+ "ECONNREFUSED",
27430
+ "ECONNRESET",
27431
+ "ENOTFOUND",
27432
+ "EHOSTUNREACH",
27433
+ "ENETUNREACH",
27434
+ "ETIMEDOUT"
27435
+ ]);
27436
+ function connectFailureHint(error, url) {
27437
+ const cause = error instanceof Error ? error.cause : void 0;
27438
+ if (!cause?.code || !CONNECT_CODES.has(cause.code)) {
27439
+ return null;
27440
+ }
27441
+ let host;
27442
+ try {
27443
+ host = new URL(url).host;
27444
+ } catch {
27445
+ return null;
27446
+ }
27447
+ return `Could not connect to ${host} (${cause.code}). The request never reached the network. If this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY, or run: modlens config set proxy <url>`;
27448
+ }
27449
+ async function apiFetch(url, init, proxy, env = process.env) {
27450
+ const dispatcher2 = apiProxyDispatcher(proxy, env);
27451
+ try {
27452
+ return await fetch(
27453
+ url,
27454
+ // `dispatcher` is a Node/undici extension to fetch's options.
27455
+ dispatcher2 ? { ...init, dispatcher: dispatcher2 } : init
27456
+ );
27457
+ } catch (error) {
27458
+ const hint = connectFailureHint(error, url);
27459
+ throw hint ? new Error(hint, { cause: error }) : error;
27460
+ }
27461
+ }
27417
27462
  const JSON_TEMPLATE_INSTRUCTION = `Respond with ONE JSON object only, no markdown fences, no commentary. Fill this exact structure with your findings from the image (do not repeat this template literally, replace every value):
27418
27463
  {"summary":"one paragraph describing the image","ocr":{"full_text":"all visible text","lines":[{"text":"one line","language":"en"}]},"layout":{"regions":[{"type":"title|subtitle|paragraph|list|table|chart|form|code|image|icon|other","reading_order":1,"text":"region text"}]},"semantics":{"scene":"what kind of scene","intent":"what the image is for","entities":[{"name":"entity","type":"kind","evidence":"where seen"}],"relations":[{"subject":"a","predicate":"relates to","object":"b"}]},"visual":{"dominant_colors":["color"],"style":"visual style","notes":["notable visual detail"]},"uncertainty":["anything unreadable or ambiguous"]}`;
27419
27464
  function buildVisionPrompt(options) {
@@ -27735,43 +27780,47 @@ async function executeAnthropicApi(options) {
27735
27780
 
27736
27781
  Report your findings by calling the ${TOOL_NAME} tool.`;
27737
27782
  const startedAt = Date.now();
27738
- const response2 = await fetch(`${baseUrl}/v1/messages`, {
27739
- method: "POST",
27740
- headers: {
27741
- "x-api-key": apiKey,
27742
- "anthropic-version": "2023-06-01",
27743
- "Content-Type": "application/json"
27783
+ const response2 = await apiFetch(
27784
+ `${baseUrl}/v1/messages`,
27785
+ {
27786
+ method: "POST",
27787
+ headers: {
27788
+ "x-api-key": apiKey,
27789
+ "anthropic-version": "2023-06-01",
27790
+ "Content-Type": "application/json"
27791
+ },
27792
+ body: JSON.stringify(
27793
+ mergeExtraBody(
27794
+ {
27795
+ model,
27796
+ max_tokens: 4096,
27797
+ tools: [
27798
+ {
27799
+ name: TOOL_NAME,
27800
+ description: "Report the structured visual evidence extracted from the image.",
27801
+ input_schema: VISION_RESULT_SCHEMA
27802
+ }
27803
+ ],
27804
+ tool_choice: { type: "tool", name: TOOL_NAME },
27805
+ messages: [
27806
+ {
27807
+ role: "user",
27808
+ content: [
27809
+ { type: "image", source: imageSource },
27810
+ { type: "text", text: prompt }
27811
+ ]
27812
+ }
27813
+ ]
27814
+ },
27815
+ options.settings?.extraBody,
27816
+ ["model", "messages", "tools", "tool_choice", "stream"],
27817
+ "anthropic"
27818
+ )
27819
+ ),
27820
+ signal: AbortSignal.timeout(options.timeoutMs)
27744
27821
  },
27745
- body: JSON.stringify(
27746
- mergeExtraBody(
27747
- {
27748
- model,
27749
- max_tokens: 4096,
27750
- tools: [
27751
- {
27752
- name: TOOL_NAME,
27753
- description: "Report the structured visual evidence extracted from the image.",
27754
- input_schema: VISION_RESULT_SCHEMA
27755
- }
27756
- ],
27757
- tool_choice: { type: "tool", name: TOOL_NAME },
27758
- messages: [
27759
- {
27760
- role: "user",
27761
- content: [
27762
- { type: "image", source: imageSource },
27763
- { type: "text", text: prompt }
27764
- ]
27765
- }
27766
- ]
27767
- },
27768
- options.settings?.extraBody,
27769
- ["model", "messages", "tools", "tool_choice", "stream"],
27770
- "anthropic"
27771
- )
27772
- ),
27773
- signal: AbortSignal.timeout(options.timeoutMs)
27774
- });
27822
+ options.settings?.proxy
27823
+ );
27775
27824
  if (!response2.ok) {
27776
27825
  const body2 = await response2.text();
27777
27826
  throw new Error(
@@ -27982,14 +28031,12 @@ function parseClaudeCliOutput(stdout) {
27982
28031
  `Claude CLI reported ${envelope.subtype ?? "an error"}: ${truncate(envelope.result ?? "")}`
27983
28032
  );
27984
28033
  }
27985
- if (typeof envelope.result !== "string" || !envelope.result.trim()) {
28034
+ if (envelope.structured_output === void 0 && (typeof envelope.result !== "string" || !envelope.result.trim())) {
27986
28035
  throw new Error("Claude CLI output contains no result. Check login state (run: claude).");
27987
28036
  }
27988
- let result;
27989
- try {
27990
- result = JSON.parse(envelope.result);
27991
- } catch {
27992
- throw new Error(`Claude CLI returned non-JSON result: ${truncate(envelope.result)}`);
28037
+ const result = envelope.structured_output ?? (typeof envelope.result === "string" ? extractJson(envelope.result) : null);
28038
+ if (result === null || result === void 0) {
28039
+ throw new Error(`Claude CLI returned non-JSON result: ${truncate(envelope.result ?? "")}`);
27993
28040
  }
27994
28041
  return {
27995
28042
  result,
@@ -28032,39 +28079,48 @@ async function executeGeminiApi(options) {
28032
28079
  extraPrompt: options.extraPrompt
28033
28080
  });
28034
28081
  const startedAt = Date.now();
28035
- const response2 = await fetch(`${baseUrl}/v1beta/models/${model}:generateContent`, {
28036
- method: "POST",
28037
- headers: {
28038
- "x-goog-api-key": apiKey,
28039
- "Content-Type": "application/json"
28040
- },
28041
- body: JSON.stringify(
28042
- mergeExtraBody(
28043
- {
28044
- contents: [
28045
- {
28046
- parts: [
28047
- { inline_data: { mime_type: image.mimeType, data: image.data } },
28048
- { text: prompt }
28049
- ]
28082
+ const response2 = await apiFetch(
28083
+ `${baseUrl}/v1beta/models/${model}:generateContent`,
28084
+ {
28085
+ method: "POST",
28086
+ headers: {
28087
+ "x-goog-api-key": apiKey,
28088
+ "Content-Type": "application/json"
28089
+ },
28090
+ body: JSON.stringify(
28091
+ mergeExtraBody(
28092
+ {
28093
+ contents: [
28094
+ {
28095
+ parts: [
28096
+ {
28097
+ inline_data: {
28098
+ mime_type: image.mimeType,
28099
+ data: image.data
28100
+ }
28101
+ },
28102
+ { text: prompt }
28103
+ ]
28104
+ }
28105
+ ],
28106
+ generationConfig: {
28107
+ responseMimeType: "application/json",
28108
+ responseJsonSchema: VISION_RESULT_SCHEMA
28050
28109
  }
28110
+ },
28111
+ options.settings?.extraBody,
28112
+ [
28113
+ "contents",
28114
+ "generationConfig.responseMimeType",
28115
+ "generationConfig.responseJsonSchema"
28051
28116
  ],
28052
- generationConfig: {
28053
- responseMimeType: "application/json",
28054
- responseJsonSchema: VISION_RESULT_SCHEMA
28055
- }
28056
- },
28057
- options.settings?.extraBody,
28058
- [
28059
- "contents",
28060
- "generationConfig.responseMimeType",
28061
- "generationConfig.responseJsonSchema"
28062
- ],
28063
- "gemini-api"
28064
- )
28065
- ),
28066
- signal: AbortSignal.timeout(options.timeoutMs)
28067
- });
28117
+ "gemini-api"
28118
+ )
28119
+ ),
28120
+ signal: AbortSignal.timeout(options.timeoutMs)
28121
+ },
28122
+ options.settings?.proxy
28123
+ );
28068
28124
  if (!response2.ok) {
28069
28125
  const body2 = await response2.text();
28070
28126
  throw new Error(
@@ -28114,33 +28170,37 @@ async function executeOpenaiCompat(options) {
28114
28170
 
28115
28171
  ${JSON_TEMPLATE_INSTRUCTION}`;
28116
28172
  const startedAt = Date.now();
28117
- const response2 = await fetch(`${baseUrl}/chat/completions`, {
28118
- method: "POST",
28119
- headers: {
28120
- Authorization: `Bearer ${apiKey}`,
28121
- "Content-Type": "application/json"
28173
+ const response2 = await apiFetch(
28174
+ `${baseUrl}/chat/completions`,
28175
+ {
28176
+ method: "POST",
28177
+ headers: {
28178
+ Authorization: `Bearer ${apiKey}`,
28179
+ "Content-Type": "application/json"
28180
+ },
28181
+ body: JSON.stringify(
28182
+ mergeExtraBody(
28183
+ {
28184
+ model,
28185
+ messages: [
28186
+ {
28187
+ role: "user",
28188
+ content: [
28189
+ { type: "image_url", image_url: { url: imageUrl } },
28190
+ { type: "text", text: prompt }
28191
+ ]
28192
+ }
28193
+ ]
28194
+ },
28195
+ options.settings?.extraBody,
28196
+ ["model", "messages", "stream"],
28197
+ "openai"
28198
+ )
28199
+ ),
28200
+ signal: AbortSignal.timeout(options.timeoutMs)
28122
28201
  },
28123
- body: JSON.stringify(
28124
- mergeExtraBody(
28125
- {
28126
- model,
28127
- messages: [
28128
- {
28129
- role: "user",
28130
- content: [
28131
- { type: "image_url", image_url: { url: imageUrl } },
28132
- { type: "text", text: prompt }
28133
- ]
28134
- }
28135
- ]
28136
- },
28137
- options.settings?.extraBody,
28138
- ["model", "messages", "stream"],
28139
- "openai"
28140
- )
28141
- ),
28142
- signal: AbortSignal.timeout(options.timeoutMs)
28143
- });
28202
+ options.settings?.proxy
28203
+ );
28144
28204
  if (!response2.ok) {
28145
28205
  const body2 = await response2.text();
28146
28206
  throw new Error(
@@ -28210,7 +28270,7 @@ function providerAliases() {
28210
28270
  function listProviders() {
28211
28271
  return [...new Set(Object.values(PROVIDERS).map((provider) => provider.name))];
28212
28272
  }
28213
- const STRING_FIELDS = ["apiKey", "baseUrl", "model"];
28273
+ const STRING_FIELDS = ["apiKey", "baseUrl", "model", "proxy"];
28214
28274
  const REUSE_HARNESSES = ["claude", "codex", "opencode", "pi", "grok"];
28215
28275
  const CONFIG_DIR = path.join(os.homedir(), ".modlens");
28216
28276
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
@@ -28251,6 +28311,9 @@ function resolveProviderSettings(providerName, config2, env = process.env) {
28251
28311
  };
28252
28312
  const bindings = ENV_BINDINGS[providerName] ?? {};
28253
28313
  const settings = { ...fromFile };
28314
+ if (!settings.proxy && config2.proxy?.trim()) {
28315
+ settings.proxy = config2.proxy.trim();
28316
+ }
28254
28317
  for (const [field, envName] of Object.entries(bindings)) {
28255
28318
  const value = env[envName]?.trim();
28256
28319
  if (value) {
@@ -28263,6 +28326,12 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
28263
28326
  const config2 = loadConfigFile(configPath);
28264
28327
  if (dottedKey === "provider") {
28265
28328
  config2.provider = value;
28329
+ } else if (dottedKey === "proxy") {
28330
+ if (value.trim() === "") {
28331
+ delete config2.proxy;
28332
+ } else {
28333
+ config2.proxy = value.trim();
28334
+ }
28266
28335
  } else if (dottedKey.startsWith("reuse.")) {
28267
28336
  const harness = dottedKey.slice("reuse.".length);
28268
28337
  if (!REUSE_HARNESSES.includes(harness)) {
@@ -28307,7 +28376,7 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
28307
28376
  }
28308
28377
  } else if (!STRING_FIELDS.includes(field)) {
28309
28378
  throw new Error(
28310
- `Unknown config field: ${field}. Use apiKey, baseUrl, model, or extraBody.`
28379
+ `Unknown config field: ${field}. Use apiKey, baseUrl, model, proxy, or extraBody.`
28311
28380
  );
28312
28381
  } else {
28313
28382
  config2.providers ??= {};
@@ -28409,6 +28478,11 @@ function renderEffectiveConfig(config2, env = process.env) {
28409
28478
  if (config2.provider?.trim()) {
28410
28479
  effective.provider = config2.provider.trim();
28411
28480
  }
28481
+ if (config2.proxy?.trim()) {
28482
+ effective.proxy = `${config2.proxy.trim()} (file)`;
28483
+ } else if (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy) {
28484
+ effective.proxy = `${env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy} (env)`;
28485
+ }
28412
28486
  if (config2.guards) {
28413
28487
  const guards = {};
28414
28488
  if (config2.guards.denyModels !== void 0) {
@@ -30626,7 +30700,7 @@ function parsePositiveInt(raw, flag) {
30626
30700
  }
30627
30701
  return Number.parseInt(raw, 10);
30628
30702
  }
30629
- program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.11.0");
30703
+ program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.12.1");
30630
30704
  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(
30631
30705
  "--extra-body <json>",
30632
30706
  `JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
package/docs/cli.md CHANGED
@@ -95,6 +95,6 @@ Five providers: `antigravity-cli` (no key), `gemini-api` (fastest free route), `
95
95
  Other subcommands:
96
96
 
97
97
  - `modlens guard [--model <id>]`: should the engine run for the active model at all? Exit 0 allow, 1 deny, verdict as JSON.
98
- - `modlens config <init|set|show>`: keys are `provider`, `reuse.<claude|codex|opencode|pi|grok>`, `guards.<denyModels|allowModels|denyWhenUnknown>`, and `<provider>.<apiKey|baseUrl|model|extraBody>`.
98
+ - `modlens config <init|set|show>`: keys are `provider`, `proxy` (HTTP/HTTPS proxy for the API providers, `HTTPS_PROXY`/`HTTP_PROXY` also honored), `reuse.<claude|codex|opencode|pi|grok>`, `guards.<denyModels|allowModels|denyWhenUnknown>`, and `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`.
99
99
  - `modlens doctor`: Node and node:sqlite, provider readiness, the failover chains for this machine, the detected harness, the guard's rules with a live verdict, and the Reuse section with per-harness grant decisions and discovered vision. Spends no quota; `--json` for a machine-readable report.
100
100
 
@@ -154,6 +154,29 @@ The trade-off is honest either way: an explicit `@latest` (or the exclusion)
154
154
  opts modlens out of pnpm's supply-chain cooling-off window, so new releases
155
155
  install immediately.
156
156
 
157
+ ## fetch failed, or could not connect
158
+
159
+ ```
160
+ Could not connect to generativelanguage.googleapis.com (UND_ERR_CONNECT_TIMEOUT). The request never reached the network. ...
161
+ ```
162
+
163
+ The API request never left the machine. On networks that reach the internet
164
+ through a proxy this is expected: Node's fetch ignores the proxy environment
165
+ variables by default. modlens honors them once you ask it to route that way,
166
+ in either form:
167
+
168
+ ```bash
169
+ HTTPS_PROXY=http://127.0.0.1:7890 modlens -i shot.png -p gemini-api # env (NO_PROXY honored too)
170
+ modlens config set proxy http://127.0.0.1:7890 # persistent, all API providers
171
+ modlens config set openai.proxy http://127.0.0.1:7890 # one provider only
172
+ ```
173
+
174
+ The proxy applies to API provider requests only. The remote-image download
175
+ path keeps its direct, IP-pinned connection on purpose: its SSRF guards
176
+ validate the exact address being contacted, and a proxy would blind them. On
177
+ a proxied machine, prefer local files or let the failover chain hand remote
178
+ URLs to a provider that fetches them upstream.
179
+
157
180
  ## Config file problems
158
181
 
159
182
  ```
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
- ctx.tools.register({
51
- name: 'read_image',
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liustack/modlens",
3
- "version": "3.11.0",
3
+ "version": "3.12.1",
4
4
  "description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.11.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.12.1):
24
24
 
25
- 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.11.0: `modlens <args>`.
26
- 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.11.0 modlens <args>`.
27
- 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.11.0 <args>`.
25
+ 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.12.1: `modlens <args>`.
26
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.12.1 modlens <args>`.
27
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.12.1 <args>`.
28
28
  4. Otherwise tell the user no JavaScript runtime was found and that installing Node 22.13+ (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.
@@ -22,6 +22,7 @@ Everything lives under four top-level keys, all optional. This example shows eve
22
22
  ```json
23
23
  {
24
24
  "provider": "gemini-api",
25
+ "proxy": "http://127.0.0.1:7890",
25
26
  "reuse": { "claude": true, "codex": true, "opencode": false, "pi": true, "grok": true },
26
27
  "guards": {
27
28
  "allowModels": ["deepseek-v4-*", "glm-5.*", "minimax-m2.5*", "qwen3-coder*"],
@@ -8,7 +8,7 @@ shell syntax.
8
8
 
9
9
  ## Pinned version
10
10
 
11
- - Pinned CLI version: 3.11.0
11
+ - Pinned CLI version: 3.12.1
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.11.0'
27
+ $Pinned = '3.12.1'
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.11.0"
25
+ PINNED="3.12.1"
26
26
  # -------------------------------------------------------------------------------
27
27
 
28
28
  NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"