@liustack/modlens 3.0.0 → 3.1.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.
@@ -38,6 +38,12 @@ Running a text-only model behind `ANTHROPIC_BASE_URL` in Claude Code, a pasted i
38
38
 
39
39
  Symlinks work in all of them, so linking the skill folder once keeps every agent on the latest version.
40
40
 
41
+ ## Platform support
42
+
43
+ macOS and Linux are fully supported and verified in CI on Node 22 and 24.
44
+
45
+ Windows runs the same CI matrix. Detection there skips the process-ancestry pass, since there is no `ps`, and falls back to the environment fingerprints above, so a harness that sets none of them reads as undetected (force it with `--harness` or `MODLENS_HARNESS`). OpenCode paste recovery is covered on Windows, including the path-separator normalization from [#11](https://github.com/liustack/modlens/issues/11): opencode records `session.directory` with forward slashes while `path.resolve` returns backslashes there, and both sides are normalized before matching. The JSONL stores (Claude Code, Pi) key off `os.homedir()` and each harness's own on-disk slug, and are exercised on POSIX. External engines (Antigravity CLI, the Claude CLI) run only where they ship a Windows build.
46
+
41
47
  ## Gateway setups
42
48
 
43
49
  OpenCode with DeepSeek: `opencode auth login`, pick DeepSeek and paste the key (it lands in `~/.local/share/opencode/auth.json`), then set the default model in `~/.config/opencode/opencode.jsonc` to `deepseek/deepseek-v4-flash`. Pi reads its key from `~/.pi/agent/auth.json`.
package/docs/security.md CHANGED
@@ -20,7 +20,9 @@ ModLens invokes `agy` with `--dangerously-skip-permissions` because prompt mode
20
20
 
21
21
  The `claude-cli` provider runs with `--allowedTools Read` only, so it can read local files and nothing else.
22
22
 
23
- Both subprocess providers also run in a throwaway directory containing only the one image, created fresh per call and removed afterward. Text inside an image is untrusted, so an injection could otherwise steer a broadly-permissioned agent into reading files that sit next to the original. A directory of one removes that reach. Passing `--workdir` opts out and runs where you point it.
23
+ Both subprocess providers also run in a throwaway directory created fresh per call and removed afterward. For a local image it holds a private copy of that one image and nothing else, and it is a real copy, never a hardlink, so a provider writing to its temp path cannot touch the original. For a remote image the directory is empty and the agent downloads into it. Without this, text inside an image could steer a broadly-permissioned agent into reading files next to the original, or whatever project the caller happened to be in. Passing `--workdir` opts out and runs where you point it.
24
+
25
+ This is exposure reduction, not an OS sandbox: the agent can still read absolute paths, reach the network, and spawn processes. Treat it as a narrower default, not a security boundary. For images you do not trust, prefer an inline API provider (`-p gemini-api`), which hands the bytes to an HTTP endpoint and runs no local agent.
24
26
 
25
27
  ## Image content is untrusted input
26
28
 
@@ -81,7 +81,7 @@ The output lists images oldest to newest, so the **last** entry is the most rece
81
81
  `recover-paste` auto-detects which harness it runs inside (process ancestry first, then environment fingerprints) and reads only that harness's storage. Two knobs override it:
82
82
 
83
83
  - **`MODLENS_HARNESS`** forces the storage scope without a flag: `claude-code`, `pi`, `opencode`, `codex`, or `none` (scan every store, no scoping). Detection reads it first, so it wins over ancestry and env fingerprints. `--harness` does the same for a single run.
84
- - **`--out-dir`** sets where recovered images land. By default each run mints a fresh, unpredictable `<tmpdir>/modlens-paste-*` directory (0700, holding 0600 files), so nobody can pre-create a shared path to intercept the bytes. Point it elsewhere when the system temp dir is not where you want them. An explicit `--out-dir` that already exists is rejected unless it is a real directory (not a symlink), owned by you, with no group or world access.
84
+ - **`--out-dir`** sets where recovered images land. By default each run mints a fresh, unpredictable `<tmpdir>/modlens-paste-*` directory (0700, holding 0600 files), so nobody can pre-create a shared path to intercept the bytes. Point it elsewhere when the system temp dir is not where you want them. An explicit `--out-dir` that already exists is rejected unless it is a real directory (not a symlink), owned by you, with no group or world access. On Windows those ownership and permission checks are skipped, since the platform has no POSIX bits (see the Windows section below). The symlink guard still applies.
85
85
 
86
86
  ## This is a Codex session
87
87
 
@@ -127,6 +127,14 @@ antigravity-cli provider timed out after 210000 ms.
127
127
 
128
128
  Retry once with `--timeout 300000`. Dense images on agy legitimately take 15-40 seconds, and `-m gemini-3.1-pro-high` is slower still. Engines that ignore SIGTERM are escalated to SIGKILL, so a timeout returns promptly regardless.
129
129
 
130
+ ## Windows
131
+
132
+ ModLens runs on Windows. Three platform differences are worth knowing:
133
+
134
+ - **No POSIX permission checks.** Windows files carry no owner, group, or world bits (they read back as `0o666`/`0o777`, with access governed by ACLs), so `doctor` does not judge the config file's mode and `recover-paste --out-dir` does not reject a directory on ownership or group/world access. The symlink guard on `--out-dir` still applies.
135
+ - **Harness detection uses environment fingerprints.** There is no `ps` to read the process tree, so detection relies on the environment variables each harness sets. If a run guesses wrong, force it with `--harness <name>` or `MODLENS_HARNESS`.
136
+ - **Paste recovery.** OpenCode recovery is covered on Windows (issue #11). The Claude Code and Pi JSONL paths depend on `os.homedir()` and each harness's on-disk slug there. If recovery comes up empty, pass `--transcript` at the file, or drag the image into the terminal.
137
+
130
138
  ## Still stuck
131
139
 
132
140
  Include the exact command and the full error in an issue: https://github.com/liustack/modlens/issues
package/package.json CHANGED
@@ -1,15 +1,29 @@
1
1
  {
2
2
  "name": "@liustack/modlens",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "modlens": "./dist/main.js"
8
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",
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
+ },
9
22
  "files": [
10
23
  "dist",
11
24
  "docs",
12
25
  "skills/modlens/SKILL.md",
26
+ "skills/modlens/scripts",
13
27
  "skills/modlens/references",
14
28
  "CHANGELOG.md",
15
29
  "SECURITY.md"
@@ -40,7 +54,8 @@
40
54
  "node": ">=22.13"
41
55
  },
42
56
  "dependencies": {
43
- "commander": "^13.1.0"
57
+ "commander": "^13.1.0",
58
+ "undici": "^8.10.0"
44
59
  },
45
60
  "devDependencies": {
46
61
  "@biomejs/biome": "^2.5.7",
@@ -49,17 +64,5 @@
49
64
  "typescript": "^5.9.3",
50
65
  "vite": "^6.4.1",
51
66
  "vitest": "^3.2.7"
52
- },
53
- "scripts": {
54
- "dev": "vite build --watch",
55
- "build": "vite build",
56
- "typecheck": "tsc --noEmit",
57
- "test": "vitest run",
58
- "coverage": "vitest run --coverage",
59
- "lint": "biome check src scripts",
60
- "format": "biome check --write src scripts",
61
- "eval": "node evals/run.mjs",
62
- "release": "node scripts/release.mjs",
63
- "docs:list": "node scripts/docs-list.js"
64
67
  }
65
68
  }
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: modlens
3
3
  description: "Plug-in vision for text-only models. Use whenever the user shares an image (local path, screenshot, photo, chart, document scan, or image URL) and the active model cannot see images or has no vision tool. Runs the modlens CLI to convert the image into structured JSON evidence: every word transcribed, layout regions, semantics, visual clues. Also use when the user asks how to install, configure, or switch modlens providers (Gemini API key, OpenAI-compatible endpoints, Claude API or Claude Code CLI)."
4
- allowed-tools:
5
- - Bash
4
+ compatibility: Requires network access and one of node 22+/npx, bun/bunx, or a preinstalled modlens binary on PATH.
5
+ allowed-tools: Bash
6
6
  ---
7
7
 
8
8
  # ModLens — Vision Bridge Skill
@@ -21,13 +21,28 @@ Do not use this skill for:
21
21
 
22
22
  ## Prerequisites
23
23
 
24
+ Run every modlens command through the launcher bundled with this skill.
25
+ Replace `<skill-dir>` with the directory this SKILL.md lives in:
26
+
24
27
  ```bash
25
- modlens --version
28
+ bash <skill-dir>/scripts/run.sh -i <image-path-or-url> # macOS / Linux
29
+ powershell -ExecutionPolicy Bypass -File <skill-dir>\scripts\run.ps1 -i <image> # Windows
26
30
  ```
27
31
 
28
- If `modlens` is missing, run it via `npx @liustack/modlens` instead.
32
+ The launcher finds a working way to run modlens and forwards your arguments to it unchanged. It tries, in order: a compatible `modlens` already on `PATH`, then `npx`, then `bunx`. If none of those exists it prints a JSON diagnosis to stderr and exits 78, with a `nextSteps` list for the user. Relay those steps instead of retrying. To see the full diagnosis, run `bash <skill-dir>/scripts/run.sh doctor --json` (on a machine that can launch the CLI it also chains modlens's own provider/config `doctor`).
33
+
34
+ ### If you cannot run the launcher script
35
+
36
+ Some harnesses forbid running scripts. Reason through the same order by hand and run the first line that works (the pinned version is 3.1.1):
29
37
 
30
- ModLens supports five vision providers. Check what is configured:
38
+ 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.1.1: `modlens <args>`.
39
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.1.1 modlens <args>`.
40
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.1.1 <args>`.
41
+ 4. Otherwise none of these runtimes is here. 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.
42
+
43
+ `references/runtime.md` documents the version pin, the compatibility rule, and the diagnostic fields.
44
+
45
+ ModLens supports five vision providers. Check what is configured (through the launcher, as above):
31
46
 
32
47
  ```bash
33
48
  modlens config show
@@ -43,12 +58,12 @@ modlens config show
43
58
 
44
59
  ## Command
45
60
 
61
+ In the examples below, `modlens` means the command run through the launcher above (`bash <skill-dir>/scripts/run.sh ...`, or the PowerShell form on Windows).
62
+
46
63
  ```bash
47
64
  modlens -i <image-path-or-url>
48
65
  # pick a provider explicitly
49
66
  modlens -i <image> -p gemini-api
50
- # or without a global install
51
- npx @liustack/modlens -i <image-path-or-url>
52
67
  ```
53
68
 
54
69
  Optional flags:
@@ -69,9 +84,10 @@ Harnesses rarely hand you a clean path. First identify which harness you are in,
69
84
 
70
85
  **Claude Code, Pi, or OpenCode** (no path tag anywhere; the image reads as `[Unsupported Image]`, a bare `[Image #1]`, or an attachment you simply cannot see):
71
86
 
72
- - None of these harnesses writes pasted images to a regular temp file, but all of them persist user messages locally before any gateway strips them: Claude Code and Pi in session JSONL files (`~/.claude/projects/`, `~/.pi/agent/sessions/`), OpenCode in a SQLite database (`~/.local/share/opencode/opencode.db`, read via node:sqlite, needs Node 22.5+). Run `modlens recover-paste` from the project directory the conversation is happening in (add `--count <n>` for several images). It detects which harness it is running inside (process ancestry, then env fingerprints) and reads ONLY that harness's storage, so another tool's old sessions cannot leak in. In Claude Code it also targets your exact session automatically via the injected CLAUDE_CODE_SESSION_ID; `--session <id>` (e.g. from the ${CLAUDE_SESSION_ID} substitution) is only needed to override.
87
+ - None of these harnesses writes pasted images to a regular temp file, but all of them persist user messages locally before any gateway strips them: Claude Code and Pi in session JSONL files (`~/.claude/projects/`, `~/.pi/agent/sessions/`), OpenCode in a SQLite database (`~/.local/share/opencode/opencode.db`, read via node:sqlite, needs Node 22.5+; Bun cannot load node:sqlite, so if the launcher resolved to bunx, OpenCode recovery needs a real Node install). Run `modlens recover-paste` from the project directory the conversation is happening in (add `--count <n>` for several images). It detects which harness it is running inside (process ancestry, then env fingerprints) and reads ONLY that harness's storage, so another tool's old sessions cannot leak in. In Claude Code it also targets your exact session automatically via the injected CLAUDE_CODE_SESSION_ID; `--session <id>` (e.g. from the ${CLAUDE_SESSION_ID} substitution) is only needed to override.
73
88
  - The output is JSON with real file paths, ordered oldest to newest, so the LAST path is the user's most recent paste. Analyze that one first. Entries carry `filename` (the original attachment name) when the harness stored one; if the user's message or an error mentions a filename, match on it.
74
89
  - Run every command yourself: `recover-paste`, then `modlens -i <path>` on the recovered file, then answer from the JSON. Never ask the user to run modlens or to relay paths.
90
+ - When the analysis is done, delete the recovered files: they are private copies of the user's pasted images sitting in the temp dir, and nothing cleans them up until the OS does. Remove the recovery output directory (each entry's `path` sits inside it), unless the user asked to keep the files.
75
91
  - The output's `detected` field names the harness scope that was applied. If it is absent, detection failed and every store was scanned by newest-image timestamp: before describing anything, check that `harness` and `filename` match what you expect, force the scope with `--harness <claude-code|pi|opencode>` if they do not, and when in doubt ask the user for the file instead of describing the wrong image.
76
92
  - Recovery is scoped to this project: the harness's own record of its working directory is checked, not just the directory name, so images from a neighbouring project are never handed over. Recovered files are private to the user (0600).
77
93
  - If recovery fails (session storage is each harness's internals and may change), ask the user to drag the image file into the terminal or type its path.
@@ -101,7 +117,7 @@ Structure is enforced by schema on antigravity-cli and claude-cli (`--json-schem
101
117
 
102
118
  ## Failure Handling
103
119
 
104
- Every error this CLI prints is catalogued with its cause and fix in the project's `docs/troubleshooting.md`. Read the message first: most of them already name the fix.
120
+ Every error this CLI prints names its cause, and most already name the fix, so read the message first.
105
121
 
106
122
  - `Provider CLI not found`: Antigravity CLI is not installed. Install it, or switch provider: `-p gemini-api`.
107
123
  - Missing key errors name the exact env var and `config set` command to run. Relay that to the user.
@@ -0,0 +1,77 @@
1
+ # ModLens CLI manual
2
+
3
+ The skill drives this CLI through its launcher. This page is for running it directly.
4
+
5
+ ## Direct usage
6
+
7
+ With the skill installed you do not type commands: paste an image or drop a path, ask anything, and it fires on its own. By hand:
8
+
9
+ ```bash
10
+ modlens -i screenshot.png # local image
11
+ modlens -i https://example.com/chart.png # remote image
12
+ modlens -i chart.png --prompt "focus on axes" # extra focus
13
+ modlens recover-paste # pull a pasted image into a file
14
+ ```
15
+
16
+ Output is a fixed JSON shape:
17
+
18
+ ```json
19
+ {
20
+ "image": "/path/to/screenshot.png",
21
+ "provider": "gemini-api",
22
+ "result": {
23
+ "summary": "A workflow diagram with four nodes connected by labeled arrows.",
24
+ "ocr": { "full_text": "/shaping\nBEFORE YOU BUILD\n...", "lines": [] },
25
+ "layout": { "regions": [{ "reading_order": 1, "type": "title", "text": "/shaping" }] },
26
+ "uncertainty": []
27
+ },
28
+ "meta": {
29
+ "generatedAt": "2026-08-06T12:00:00.000Z",
30
+ "model": "gemini-3.6-flash",
31
+ "conversationId": null,
32
+ "durationSeconds": 6.4,
33
+ "usage": { "promptTokenCount": 1234, "candidatesTokenCount": 567 }
34
+ }
35
+ }
36
+ ```
37
+
38
+ `meta` records how the result was produced: when (`generatedAt`), which `model`, the provider's `conversationId` when it has one, wall-clock `durationSeconds`, and the raw `usage` the provider reported (shape varies by provider, `null` when none).
39
+
40
+ ## Flags
41
+
42
+ `modlens analyze` (the default command):
43
+
44
+ | Flag | Meaning | Default |
45
+ | :-- | :-- | :-- |
46
+ | `-i, --input <path\|url>` | Image to analyze (required) | |
47
+ | `-p, --provider <name>` | Vision provider | `antigravity-cli` |
48
+ | `-m, --model <name>` | Provider model | per provider (below) |
49
+ | `-o, --output <path>` | Also write JSON to a file | |
50
+ | `--prompt <text>` | Extra focus | |
51
+ | `--timeout <ms>` | Provider timeout | `180000` |
52
+ | `--provider-bin <path>` | Provider binary path | `agy` / `claude` |
53
+ | `--workdir <path>` | Working directory for the provider | a fresh isolated directory per run |
54
+
55
+ The default `-m` model depends on the provider:
56
+
57
+ | Provider | Default model |
58
+ | :-- | :-- |
59
+ | `antigravity-cli` (default) | `gemini-3.6-flash-low` |
60
+ | `gemini-api` | `gemini-3.6-flash` |
61
+ | `anthropic` | `claude-haiku-4-5-20251001` |
62
+ | `claude-cli` | `haiku` |
63
+ | `openai` | none, `-m` is required |
64
+
65
+ `modlens recover-paste`:
66
+
67
+ | Flag | Meaning | Default |
68
+ | :-- | :-- | :-- |
69
+ | `--count <n>` | How many recent pasted images to recover | `1` |
70
+ | `--out-dir <path>` | Where to write recovered images | a fresh private `<tmpdir>/modlens-paste-*` per run |
71
+ | `--session <id>` | Session id for exact targeting | auto-detect |
72
+ | `--transcript <path>` | Explicit transcript `.jsonl` or `.db` (overrides `--session`) | |
73
+ | `--harness <name>` | Force storage scope: `claude-code`, `pi`, `opencode`, `none` | auto-detect |
74
+ | `--cwd <path>` | Project directory the image was pasted in | current directory |
75
+
76
+ Five providers: `antigravity-cli` (default, no key), `gemini-api` (fastest free route), `openai` (any OpenAI-compatible multimodal endpoint), `anthropic`, and `claude-cli` (uses your existing Claude subscription). Two more subcommands: `modlens config <init|set|show>`, and `modlens doctor` (checks Node, provider readiness, which provider will be selected and why, and the detected harness, without spending quota or touching the network. Add `--json` for a machine-readable report).
77
+
@@ -15,6 +15,40 @@ modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model
15
15
 
16
16
  `config set` writes the file with 0600 permissions.
17
17
 
18
+ ## The file's exact shape
19
+
20
+ Everything lives under two top-level keys, both optional. A missing file means all defaults. Provider settings sit under `providers.<name>`, not at the top level, which is the mistake hand-editors make most.
21
+
22
+ ```json
23
+ {
24
+ "provider": "gemini-api",
25
+ "providers": {
26
+ "antigravity-cli": { "model": "gemini-3.6-flash-low" },
27
+ "gemini-api": {
28
+ "apiKey": "AIza...",
29
+ "baseUrl": "https://generativelanguage.googleapis.com",
30
+ "model": "gemini-3.6-flash"
31
+ },
32
+ "openai": {
33
+ "apiKey": "sk-...",
34
+ "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
35
+ "model": "qwen3.6-27b"
36
+ },
37
+ "anthropic": { "apiKey": "sk-ant-..." },
38
+ "claude-cli": { "model": "haiku" }
39
+ }
40
+ }
41
+ ```
42
+
43
+ Field semantics:
44
+
45
+ - `provider`: which provider runs when `-p` is not given. Canonical names or aliases both work (`agy`/`antigravity` for `antigravity-cli`, `gemini` for `gemini-api`, `openai-compat` for `openai`, `claude` for `anthropic`, `claude-code` for `claude-cli`). Empty or absent means `antigravity-cli`.
46
+ - `providers.<name>.<field>`: only three fields exist, `apiKey`, `baseUrl`, `model`. Every provider entry is optional, and every field inside it is optional. Alias keys are read too (settings saved under `gemini` are found when `gemini-api` resolves), with the canonical key winning on conflict.
47
+ - Environment variables override the file for these bindings: `GEMINI_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`. Nothing else is read from the environment except `MODLENS_HARNESS` (paste-recovery scope, unrelated to this file).
48
+ - Unknown top-level keys and unknown provider names are ignored rather than rejected, so a typo fails quiet: run `modlens doctor` after hand-editing, it shows which file and env values are actually in effect.
49
+
50
+ Hand-editing is fine (keep the file valid JSON and its permissions 0600). `modlens config set` does the same thing with guardrails.
51
+
18
52
  ## Provider setup recipes
19
53
 
20
54
  ### antigravity-cli (default, free, no key)
@@ -0,0 +1,105 @@
1
+ # modlens runtime reference
2
+
3
+ How the skill launches the `modlens` CLI, what version it pins, and how it
4
+ diagnoses a machine where nothing can run. The launchers `scripts/run.sh`
5
+ (macOS / Linux) and `scripts/run.ps1` (Windows) implement everything below and
6
+ must stay byte-for-byte identical apart from their version constants and their
7
+ shell syntax.
8
+
9
+ ## Pinned version
10
+
11
+ - Pinned CLI version: 3.1.1
12
+ - npm package: `@liustack/modlens`
13
+ - CLI binary name: `modlens`
14
+
15
+ The pinned version line above and the constants inside both launchers are
16
+ stamped by `scripts/release.mjs` at release time from `package.json`. Do not
17
+ edit them by hand. `scripts/stamp.test.mjs` fails the build if the three
18
+ launcher/reference copies ever drift from `package.json`.
19
+
20
+ ## Resolution order
21
+
22
+ Each call resolves a way to run the CLI, in this order:
23
+
24
+ 1. **A compatible `modlens` already on `PATH`** — run it directly, by name.
25
+ 2. **`npx` present, and `node` meets the CLI's 22.13 floor** — `npx --yes --package @liustack/modlens@<pinned> modlens <args>`. An npx sitting on an older node is skipped: it would select a path known to fail at run time.
26
+ 3. **`bunx` present** — `bunx --bun @liustack/modlens@<pinned> <args>`.
27
+ 4. **A native artifact** — reserved for phase B. None is published yet, so this
28
+ branch reports `nativeArtifact.available: false` and moves on.
29
+ 5. **Nothing usable** — print a structured diagnosis and exit `78` (`EX_CONFIG`).
30
+
31
+ The launcher forwards stdin, stdout, stderr, and the exit code unchanged, so the
32
+ CLI's JSON output contract is identical however it was launched.
33
+
34
+ ## Compatibility rule
35
+
36
+ A `modlens` found on `PATH` counts as compatible only when it is **the same
37
+ major version as the pinned version and not older than it**. Same major keeps a
38
+ user who already installed a matching CLI from being forced through an `npx`
39
+ re-download (the "no regression" requirement in the design). Not-older refuses a
40
+ stale global build that predates the version this skill was written against; in
41
+ that case the launcher skips `PATH` and uses the pinned `npx` / `bunx` version
42
+ instead.
43
+
44
+ ## Cache and permissions (phase B, not active yet)
45
+
46
+ Phase A ships no native artifact. The `npx` and `bunx` paths fetch the pinned
47
+ npm package on first use and cache it (that is how those runners work); nothing
48
+ else is ever downloaded. When native artifacts land in phase B, the
49
+ launchers will cache them per user, keyed by version, and launch them by
50
+ absolute path:
51
+
52
+ - macOS: `~/Library/Caches/liustack/modlens/<version>/`
53
+ - Linux: `${XDG_CACHE_HOME:-$HOME/.cache}/liustack/modlens/<version>/`
54
+ - Windows: `%LOCALAPPDATA%\liustack\modlens\<version>\`
55
+
56
+ With these constraints: no `sudo` or admin rights, no system directories, no
57
+ `PATH` edits, download to a temp file and verify SHA-256 before an atomic move,
58
+ and keep no unverified executable on failure. Any download uses `curl` (on
59
+ Windows, `curl.exe` written in full), which does not stamp quarantine or
60
+ Mark-of-the-Web, and the launcher never removes a security marker a browser
61
+ would have set.
62
+
63
+ ## Diagnostic fields
64
+
65
+ `run.sh doctor --json` (and `run.ps1 doctor --json`) print this shape:
66
+
67
+ - `tool`, `package`, `pinnedVersion` — what this skill targets.
68
+ - `os`, `arch` — normalized host identity (`darwin` / `linux` / `windows`,
69
+ `arm64` / `x64`).
70
+ - `checked.pathCli` — `{ present, path, version, compatible }` for a `modlens`
71
+ on `PATH`, with `compatible` applying the rule above.
72
+ - `checked.npx` — `{ present, path, nodeMeetsFloor }`; `nodeMeetsFloor` is whether
73
+ the local node satisfies the CLI's 22.13 floor, required for the npx path.
74
+ - `checked.bunx` — `{ present, path }`.
75
+ - `checked.node` — `{ present, version }`.
76
+ - `nativeArtifact` — `{ available, note }`; `available` is `false` in phase A.
77
+ - `selected` — the resolved path: `path`, `npx`, `bunx`, or `none`.
78
+ - `nextSteps` — when `selected` is `none`, one or two plain-language actions for
79
+ the user (install Node 22.13+, or Bun); empty otherwise.
80
+ - `cliDoctor` — when a CLI is resolvable, the CLI's own `doctor --json` report
81
+ (provider, config, and harness diagnosis) is nested here; `null` otherwise.
82
+
83
+ `doctor` spends no quota. The launcher's own diagnosis is offline: it inspects
84
+ the local environment and makes no network request of its own. Chaining the
85
+ CLI's `doctor` through the npx or bunx path can download the pinned package the
86
+ first time (that is how those runners work); after that it is served from the
87
+ local cache.
88
+
89
+ One capability note for the bunx path: Bun cannot load `node:sqlite`, which
90
+ OpenCode paste recovery needs, so on a machine where the launcher resolved to
91
+ bunx, `recover-paste` for OpenCode requires installing Node 22.13+ instead.
92
+
93
+ ## Delivery form: local CLI, long term
94
+
95
+ modlens stays a local CLI on purpose, and section 10 of the distribution design
96
+ (move capabilities to a remote MCP when they need no local execution) does
97
+ **not** apply to it. The reasons are the product itself: the vision-provider key
98
+ is held on the user's machine, the quota billed is the user's own, and there is
99
+ no central service in the middle. modlens also reads local files directly. Its
100
+ `recover-paste` pulls pasted images out of the harness's own session storage on
101
+ disk, which a remote service structurally cannot reach. A hosted MCP would move
102
+ the key and the quota off the user's machine and still could not see those local
103
+ files, which is the opposite of what this tool is for. Phase D may retire native
104
+ artifacts for some future tool, but modlens keeps its local-CLI form for as long
105
+ as those properties hold.