@liustack/modlens 3.16.1 → 3.16.2

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.16.2 - 2026-08-14
4
+
5
+ - **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.
6
+ - **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.
7
+ - **`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 the dsh plugin and an `@latest` run are both current. 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.
8
+
3
9
  ## 3.16.1 - 2026-08-14
4
10
 
5
11
  - **OpenChamber (OpenCode's desktop UI) is detected, and Windows detection got quieter and sharper ([#30](https://github.com/liustack/modlens/issues/30)).** Three stacked Windows gaps from one runtime-confirmed report. The env-fingerprint fallback never checked the markers opencode servers inject (`OPENCODE`, `OPENCODE_PID`, `OPENCODE_BINARY`), so OpenChamber read as "none detected" and `recover-paste` never auto-ran, while the pasted bytes sat recoverable in the opencode database the whole time; the fingerprint now resolves to `opencode`, placed before Claude Code's so nested setups pick the innermost input box. The `ps` ancestry probe now runs only off Windows: MSYS machines carry a `ps` that exists but rejects `-Ao`, and a failed child's stderr printed into every `doctor` run (the docs always said Windows skips ancestry, now the code agrees). And `findOnPath` tries the PATHEXT extensions before the bare name, so the POSIX `sh` shim npm installs next to `opencode.cmd` no longer shadows the executable into a `spawnSync ENOENT`. Thanks to @IA20201 for a report with the evidence already attached: observed env markers, doctor output, and the exact database row.
package/README.md CHANGED
@@ -159,10 +159,10 @@ ModLens does not accept pull requests. The project is maintained by a single aut
159
159
  This project runs on LIUSTACK Skills: `shaping` before you build, `coding` while you build, `dig` when it breaks, `snapshot` when you hand off. Lighter than Superpowers, and stronger.
160
160
 
161
161
  ```bash
162
- npx -y skills add liustack/liustack -g
162
+ npx -y skills add liustack/vibemaster -g
163
163
  ```
164
164
 
165
- ⭐ If it helps, star [ModLens](https://github.com/liustack/modlens) and [liustack](https://github.com/liustack/liustack). Stars are how the next developer finds them.
165
+ ⭐ If it helps, star [ModLens](https://github.com/liustack/modlens) and [VibeMaster](https://github.com/liustack/vibemaster). Stars are how the next developer finds them.
166
166
 
167
167
  ## Star History
168
168
 
package/dist/main.js CHANGED
@@ -1687,6 +1687,215 @@ function providerChain(kind, config2, env = process.env) {
1687
1687
  }
1688
1688
  return names.filter((name) => providerAvailable(name, config2, env)).map((name) => resolveProvider(name));
1689
1689
  }
1690
+ function tokenizeCmdLine(line) {
1691
+ const pattern = /(?:"[^"]*"|[^\s"])+/g;
1692
+ const args = line.match(pattern) ?? [];
1693
+ if (line.replace(pattern, "").trim() !== "") {
1694
+ return null;
1695
+ }
1696
+ return args.map((raw, index) => {
1697
+ const text = index === 0 ? raw.replace(/^@/, "") : raw;
1698
+ const whole = /^"([^"]*)"$/.exec(text);
1699
+ return whole ? { value: whole[1], quoted: true } : { value: text, quoted: false };
1700
+ });
1701
+ }
1702
+ function expandShimPath(token, shimDir) {
1703
+ const relative = /^%~?dp0%?\\?(.*)$/i.exec(token);
1704
+ if (relative) {
1705
+ return path.win32.join(shimDir, relative[1]);
1706
+ }
1707
+ if (path.win32.isAbsolute(token)) {
1708
+ return token;
1709
+ }
1710
+ return null;
1711
+ }
1712
+ const CMD_SYNTAX = /"|%~?\d/;
1713
+ const CMD_CONTROL = /[\^&|<>()]/;
1714
+ function literalToken(token, shimDir) {
1715
+ const text = token.value;
1716
+ if (CMD_SYNTAX.test(text) || !token.quoted && CMD_CONTROL.test(text)) {
1717
+ return null;
1718
+ }
1719
+ const substituted = text.replace(/%dp0%|%~dp0/gi, `${shimDir}\\`);
1720
+ if (substituted.includes("%")) {
1721
+ return null;
1722
+ }
1723
+ return /^(%dp0%|%~dp0)/i.test(text) ? path.win32.normalize(substituted) : substituted;
1724
+ }
1725
+ function isNodeInterpreter(token) {
1726
+ return /^node(\.exe)?$/i.test(path.win32.basename(token));
1727
+ }
1728
+ function carriesForeignEnv(content) {
1729
+ const setRe = /^\s*@?SET\s+"?([A-Za-z_][A-Za-z0-9_]*)=/gim;
1730
+ let match;
1731
+ while ((match = setRe.exec(content)) !== null) {
1732
+ const name = match[1].toLowerCase();
1733
+ if (name !== "dp0" && name !== "_prog" && name !== "pathext") {
1734
+ return true;
1735
+ }
1736
+ }
1737
+ return false;
1738
+ }
1739
+ function progIsNode(content, shimDir) {
1740
+ const progRe = /^\s*@?SET\s+"?_prog=([^"\r\n]*)"?/gim;
1741
+ const values = [];
1742
+ let match;
1743
+ while ((match = progRe.exec(content)) !== null) {
1744
+ values.push(match[1].trim());
1745
+ }
1746
+ if (values.length === 0) {
1747
+ return { ok: false };
1748
+ }
1749
+ let absolute;
1750
+ for (const value of values) {
1751
+ const expanded = expandShimPath(value, shimDir) ?? value;
1752
+ if (!isNodeInterpreter(expanded)) {
1753
+ return { ok: false };
1754
+ }
1755
+ if (path.win32.isAbsolute(value)) {
1756
+ absolute = value;
1757
+ }
1758
+ }
1759
+ return { ok: true, ...absolute ? { absolute } : {} };
1760
+ }
1761
+ const STRUCTURAL_LINE = [
1762
+ /^\s*$/,
1763
+ /^\s*@?ECHO\s+off\s*$/i,
1764
+ /^\s*@?SETLOCAL\s*$/i,
1765
+ /^\s*@?ENDLOCAL\s*$/i,
1766
+ /^\s*GOTO\s+\S+\s*$/i,
1767
+ /^\s*:\S+\s*$/,
1768
+ /^\s*EXIT\s+\/b\s*$/i,
1769
+ /^\s*CALL\s+:\S+\s*$/i,
1770
+ /^\s*@?SET\s+dp0=%~dp0\s*$/i,
1771
+ /^\s*@?SET\s+"?_prog=[^"\r\n]*"?\s*$/i,
1772
+ /^\s*@?SET\s+PATHEXT=%PATHEXT:[^%]*%\s*$/i,
1773
+ /^\s*@?IF\s+EXIST\s+"[^"]*"\s*\(\s*$/i,
1774
+ /^\s*\)\s*ELSE\s*\(\s*$/i,
1775
+ /^\s*\)\s*$/
1776
+ ];
1777
+ function parseCmdShimTarget(cmdPath, content) {
1778
+ const shimDir = path.win32.dirname(cmdPath);
1779
+ if (carriesForeignEnv(content)) {
1780
+ return null;
1781
+ }
1782
+ const lines = content.split(/\r?\n/);
1783
+ const executionLines = [];
1784
+ for (const line of lines) {
1785
+ if (STRUCTURAL_LINE.some((pattern) => pattern.test(line))) {
1786
+ continue;
1787
+ }
1788
+ if (!line.includes("%*")) {
1789
+ return null;
1790
+ }
1791
+ executionLines.push(line);
1792
+ }
1793
+ if (executionLines.length === 0) {
1794
+ return null;
1795
+ }
1796
+ let agreed = null;
1797
+ for (const line of executionLines) {
1798
+ const parsed = parseExecutionLine(line, content, shimDir);
1799
+ if (!parsed) {
1800
+ return null;
1801
+ }
1802
+ if (agreed && !sameTarget(agreed, parsed)) {
1803
+ return null;
1804
+ }
1805
+ agreed ??= parsed;
1806
+ }
1807
+ return agreed;
1808
+ }
1809
+ function sameTarget(a, b) {
1810
+ return a.nodeExec === b.nodeExec && a.args.length === b.args.length && a.args.every((value, index) => value === b.args[index]);
1811
+ }
1812
+ function parseExecutionLine(line, content, shimDir) {
1813
+ {
1814
+ const tokens = tokenizeCmdLine(line);
1815
+ if (!tokens) {
1816
+ return null;
1817
+ }
1818
+ const forwardIndex = tokens.findIndex((token) => !token.quoted && token.value === "%*");
1819
+ if (forwardIndex < 2 || forwardIndex !== tokens.length - 1) {
1820
+ return null;
1821
+ }
1822
+ const runTokens = tokens.slice(0, forwardIndex);
1823
+ const lastAmp = runTokens.reduce(
1824
+ (found, token, index) => !token.quoted && token.value === "&" ? index : found,
1825
+ -1
1826
+ );
1827
+ let words = lastAmp >= 0 ? runTokens.slice(lastAmp + 1) : runTokens;
1828
+ if (words.length < 2) {
1829
+ return null;
1830
+ }
1831
+ let nodeExec;
1832
+ if (/^-S(\.exe)?$/i.test(path.win32.basename(words[0].value))) {
1833
+ words = words.slice(1);
1834
+ }
1835
+ const interpreter = words[0].value;
1836
+ if (interpreter === "%_prog%") {
1837
+ const prog = progIsNode(content, shimDir);
1838
+ if (!prog.ok) {
1839
+ return null;
1840
+ }
1841
+ nodeExec = prog.absolute;
1842
+ } else {
1843
+ const expanded = expandShimPath(interpreter, shimDir) ?? interpreter;
1844
+ if (!isNodeInterpreter(expanded)) {
1845
+ return null;
1846
+ }
1847
+ if (path.win32.isAbsolute(interpreter)) {
1848
+ nodeExec = interpreter;
1849
+ }
1850
+ }
1851
+ const args = [];
1852
+ let expandable = true;
1853
+ for (const word of words.slice(1)) {
1854
+ const expanded = literalToken(word, shimDir);
1855
+ if (expanded === null) {
1856
+ expandable = false;
1857
+ break;
1858
+ }
1859
+ args.push(expanded);
1860
+ }
1861
+ if (!expandable || args.length === 0) {
1862
+ return null;
1863
+ }
1864
+ return { args, ...nodeExec ? { nodeExec } : {} };
1865
+ }
1866
+ }
1867
+ const REAL_DEPS = {
1868
+ platform: process.platform,
1869
+ readFileSync: (p) => fs.readFileSync(p, "utf-8"),
1870
+ resolveOnPath: findOnPath,
1871
+ execPath: process.execPath
1872
+ };
1873
+ function resolveSpawnPlan(command, args, env = process.env, deps = REAL_DEPS) {
1874
+ if (deps.platform !== "win32") {
1875
+ return { command, args };
1876
+ }
1877
+ let resolved = command;
1878
+ if (!command.includes("/") && !command.includes("\\")) {
1879
+ resolved = deps.resolveOnPath(command, env) ?? command;
1880
+ }
1881
+ if (!/\.(cmd|bat)$/i.test(path.win32.basename(resolved))) {
1882
+ return { command: resolved, args };
1883
+ }
1884
+ let content;
1885
+ try {
1886
+ content = deps.readFileSync(resolved);
1887
+ } catch {
1888
+ return { command: resolved, args };
1889
+ }
1890
+ const target = parseCmdShimTarget(resolved, content);
1891
+ if (!target) {
1892
+ return { command: resolved, args };
1893
+ }
1894
+ return {
1895
+ command: target.nodeExec ?? deps.execPath,
1896
+ args: [...target.args, ...args]
1897
+ };
1898
+ }
1690
1899
  const VISION_MODEL_PATTERNS = [
1691
1900
  "claude-*",
1692
1901
  "gpt-4o*",
@@ -1723,7 +1932,12 @@ function isVisionModel(modelId) {
1723
1932
  const DEFAULT_TTL_MS = 6 * 60 * 60 * 1e3;
1724
1933
  const CLI_TIMEOUT_MS = 1e4;
1725
1934
  function defaultRunCli(bin, args, timeoutMs) {
1726
- return execFileSync(bin, args, { encoding: "utf-8", timeout: timeoutMs, stdio: "pipe" });
1935
+ const plan = resolveSpawnPlan(bin, args);
1936
+ return execFileSync(plan.command, plan.args, {
1937
+ encoding: "utf-8",
1938
+ timeout: timeoutMs,
1939
+ stdio: "pipe"
1940
+ });
1727
1941
  }
1728
1942
  function timed(run) {
1729
1943
  const start = Date.now();
@@ -2295,11 +2509,19 @@ function piRoutes(home, env, targets = DEFAULT_TARGETS) {
2295
2509
  }
2296
2510
  function fetchPiKey(piPath, modelId, provider, timeoutMs) {
2297
2511
  try {
2298
- const key = execFileSync(
2299
- piPath,
2300
- ["auth", "print-api-key", "--model", modelId, "--provider", provider],
2301
- { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs }
2302
- ).trim();
2512
+ const plan = resolveSpawnPlan(piPath, [
2513
+ "auth",
2514
+ "print-api-key",
2515
+ "--model",
2516
+ modelId,
2517
+ "--provider",
2518
+ provider
2519
+ ]);
2520
+ const key = execFileSync(plan.command, plan.args, {
2521
+ encoding: "utf-8",
2522
+ stdio: ["ignore", "pipe", "pipe"],
2523
+ timeout: timeoutMs
2524
+ }).trim();
2303
2525
  if (!key) {
2304
2526
  throw new Error("empty");
2305
2527
  }
@@ -2605,7 +2827,8 @@ function emptyWorkdir() {
2605
2827
  function runCommand(providerName, invocation, timeoutMs, describeFailure) {
2606
2828
  const runStartedAt = Date.now();
2607
2829
  return new Promise((resolve, reject) => {
2608
- const child = spawn(invocation.command, invocation.args, {
2830
+ const plan = resolveSpawnPlan(invocation.command, invocation.args);
2831
+ const child = spawn(plan.command, plan.args, {
2609
2832
  cwd: invocation.cwd,
2610
2833
  stdio: ["ignore", "pipe", "pipe"]
2611
2834
  });
@@ -2681,16 +2904,21 @@ function runCommand(providerName, invocation, timeoutMs, describeFailure) {
2681
2904
  clearTimeout(timer);
2682
2905
  clearTimeout(drainTimer);
2683
2906
  clearTimeout(killTimer);
2684
- if (error.code === "ENOENT") {
2907
+ const code = error.code;
2908
+ if (code === "ENOENT") {
2685
2909
  const missingCwd = !fs.existsSync(invocation.cwd);
2686
2910
  reject(
2687
2911
  new Error(
2688
- missingCwd ? `Working directory does not exist: ${invocation.cwd}` : `Provider CLI not found: ${invocation.command}. Install it and sign in first.`
2912
+ missingCwd ? `Working directory does not exist: ${invocation.cwd}` : `Provider CLI not found: ${invocation.command} (spawn ENOENT). Install it and sign in first.`
2689
2913
  )
2690
2914
  );
2691
2915
  return;
2692
2916
  }
2693
- reject(error);
2917
+ reject(
2918
+ new Error(
2919
+ `${providerName} provider could not start \`${invocation.command}\`: ${error.message}`
2920
+ )
2921
+ );
2694
2922
  });
2695
2923
  child.on("exit", (code) => {
2696
2924
  exitCode = code;
@@ -3351,6 +3579,43 @@ function runGuard(guards, options) {
3351
3579
  }
3352
3580
  return evaluateGuard(guards, detectActiveModel(options));
3353
3581
  }
3582
+ const SKILL_DIRS = [
3583
+ ["claude-code", ".claude/skills"],
3584
+ ["codex", ".codex/skills"],
3585
+ ["pi/opencode", ".agents/skills"],
3586
+ ["dsh", ".dsh/skills"]
3587
+ ];
3588
+ function isOlder(pinned, current) {
3589
+ const parse = (v) => v.split(".").map((part) => Number.parseInt(part, 10) || 0);
3590
+ const [pa, pb, pc] = parse(pinned);
3591
+ const [ca, cb, cc] = parse(current);
3592
+ if (pa !== ca) return pa < ca;
3593
+ if (pb !== cb) return pb < cb;
3594
+ return pc < cc;
3595
+ }
3596
+ function readPinnedVersion(launcher) {
3597
+ return /^PINNED="([^"]+)"/m.exec(launcher)?.[1] ?? null;
3598
+ }
3599
+ function findSkillInstalls(currentVersion, home = os.homedir(), skillName = "modlens") {
3600
+ const installs = [];
3601
+ for (const [harness, relative] of SKILL_DIRS) {
3602
+ const launcher = path.join(home, relative, skillName, "scripts", "run.sh");
3603
+ let text;
3604
+ try {
3605
+ text = fs.readFileSync(launcher, "utf-8");
3606
+ } catch {
3607
+ continue;
3608
+ }
3609
+ const pinned = readPinnedVersion(text);
3610
+ installs.push({
3611
+ harness,
3612
+ path: launcher,
3613
+ pinned,
3614
+ outdated: pinned !== null && isOlder(pinned, currentVersion)
3615
+ });
3616
+ }
3617
+ return installs;
3618
+ }
3354
3619
  const MIN_NODE = "22.19";
3355
3620
  function versionParts(version) {
3356
3621
  const match = /(\d+)\.(\d+)/.exec(version.replace(/^v/, ""));
@@ -3499,6 +3764,7 @@ function buildDoctorReport(input) {
3499
3764
  remote: composeChain("remote", input.config, reuseOptions).map(chainEntryName)
3500
3765
  },
3501
3766
  harness: { detected: harnessDetection.harness, source: harnessDetection.source },
3767
+ skillInstalls: input.version ? findSkillInstalls(input.version, input.home) : [],
3502
3768
  guard: {
3503
3769
  rules: denyPatterns(input.config.guards).length,
3504
3770
  allowRules: allowPatterns(input.config.guards).length,
@@ -3563,6 +3829,18 @@ function renderDoctorReport(report) {
3563
3829
  report.harness.detected ? ` ${report.harness.detected} (via ${report.harness.source})` : ` none detected (${report.harness.source})`
3564
3830
  );
3565
3831
  lines.push("");
3832
+ if (report.skillInstalls.length > 0) {
3833
+ lines.push("Installed skill copies (a copy keeps its install-time version)");
3834
+ for (const install of report.skillInstalls) {
3835
+ const state = install.pinned === null ? "no pin found" : `pins ${install.pinned}`;
3836
+ lines.push(` ${install.harness}: ${state}${install.outdated ? " [outdated]" : ""}`);
3837
+ }
3838
+ if (report.skillInstalls.some((install) => install.outdated)) {
3839
+ lines.push(" Refresh an outdated copy by re-running the install: it overwrites in");
3840
+ lines.push(" place. See https://github.com/liustack/modlens/blob/main/INSTALL.md");
3841
+ }
3842
+ lines.push("");
3843
+ }
3566
3844
  lines.push("Guard (should the vision engine run for the active model?)");
3567
3845
  lines.push(
3568
3846
  ` rules: ${report.guard.rules} deny pattern(s), ${report.guard.allowRules} allow pattern(s)${report.guard.allowRules > 0 ? " (allowlist mode)" : ""}, denyWhenUnknown: ${report.guard.denyWhenUnknown}`
@@ -3794,7 +4072,7 @@ function parsePositiveInt(raw, flag) {
3794
4072
  }
3795
4073
  return Number.parseInt(raw, 10);
3796
4074
  }
3797
- program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.16.1");
4075
+ program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.16.2");
3798
4076
  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(
3799
4077
  "--extra-body <json>",
3800
4078
  `JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
@@ -3901,7 +4179,10 @@ program.command("doctor").description(
3901
4179
  config: loadConfigFile(),
3902
4180
  env: process.env,
3903
4181
  providerFlag: options.provider,
3904
- configPath: CONFIG_PATH
4182
+ configPath: CONFIG_PATH,
4183
+ // Lets doctor name an installed skill copy that is older than
4184
+ // the CLI reporting on it (issue #33).
4185
+ version: "3.16.2"
3905
4186
  });
3906
4187
  const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
3907
4188
  process.stdout.write(`${output}
@@ -47,10 +47,10 @@ Wait for the reset, or move to `gemini-api`, which has its own budget. Parallel
47
47
  ## Provider CLI not found
48
48
 
49
49
  ```
50
- Provider CLI not found: agy. Install it and sign in first.
50
+ Provider CLI not found: agy (spawn ENOENT). Install it and sign in first.
51
51
  ```
52
52
 
53
- The binary is not on PATH, or `--provider-bin` points somewhere wrong.
53
+ The binary is not on PATH, or `--provider-bin` points somewhere wrong. A different spawn-level failure (`... could not start \`claude\`: spawn EACCES`) keeps its real error code so the cause is nameable. On Windows the npm-installed CLIs are `.cmd` shims; modlens resolves them through PATHEXT and runs their real Node entry directly, so neither the bare name (ENOENT) nor the `.cmd` (EINVAL) trips it up.
54
54
 
55
55
  ```
56
56
  Working directory does not exist: /some/path
@@ -47,10 +47,10 @@ agy's free tier is one weekly bucket shared by the desktop app, the CLI, and the
47
47
  ## 找不到 provider CLI
48
48
 
49
49
  ```
50
- Provider CLI not found: agy. Install it and sign in first.
50
+ Provider CLI not found: agy (spawn ENOENT). Install it and sign in first.
51
51
  ```
52
52
 
53
- 二进制不在 PATH 上,或者 `--provider-bin` 指错了地方。
53
+ 二进制不在 PATH 上,或者 `--provider-bin` 指错了地方。其他 spawn 级失败(`... could not start \`claude\`: spawn EACCES`)会保留真实错误码,方便定位。Windows 上 npm 装的 CLI 是 `.cmd` shim,modlens 通过 PATHEXT 解析并直接运行它背后的 Node 入口,所以裸名(ENOENT)和 `.cmd`(EINVAL)都不会卡住它。
54
54
 
55
55
  ```
56
56
  Working directory does not exist: /some/path
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liustack/modlens",
3
- "version": "3.16.1",
3
+ "version": "3.16.2",
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.16.1):
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.2):
24
24
 
25
- 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.16.1: `modlens <args>`.
26
- 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.16.1 modlens <args>`.
27
- 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.16.1 <args>`.
25
+ 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.16.2: `modlens <args>`.
26
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.16.2 modlens <args>`.
27
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.16.2 <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.16.1
11
+ - Pinned CLI version: 3.16.2
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.16.1'
27
+ $Pinned = '3.16.2'
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.1"
25
+ PINNED="3.16.2"
26
26
  # -------------------------------------------------------------------------------
27
27
 
28
28
  NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"