@liustack/modlens 2.8.0 → 3.1.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/docs/commit.md ADDED
@@ -0,0 +1,76 @@
1
+ ---
2
+ summary: 'Git Commit Conventions: Conventional Commits format, atomic commits, HEREDOC usage'
3
+ read_when:
4
+ - Preparing to commit code
5
+ - Writing a commit message
6
+ - Need to understand commit conventions
7
+ ---
8
+
9
+ # Git Commit Conventions
10
+
11
+ ## Commit Message Format
12
+
13
+ ```
14
+ <type>[optional scope]: <imperative summary>
15
+
16
+ [optional body]
17
+
18
+ [optional footer(s)]
19
+ ```
20
+
21
+ - Use imperative mood in summary, max 72 characters
22
+ - Do not end with a period
23
+ - Use scope only when it adds clarity (e.g., `auth`, `api`, `ui`)
24
+ - Mark breaking changes with `!` (e.g., `feat!:`) or a `BREAKING CHANGE:` footer
25
+
26
+ ## Available Types
27
+
28
+ | Type | Description |
29
+ |------|-------------|
30
+ | `feat` | New feature |
31
+ | `fix` | Bug fix |
32
+ | `docs` | Documentation update |
33
+ | `style` | Formatting (no logic changes) |
34
+ | `refactor` | Code restructuring (no behavior changes) |
35
+ | `perf` | Performance improvement |
36
+ | `test` | Testing related |
37
+ | `build` | Build system or dependency changes |
38
+ | `ci` | CI/CD changes |
39
+ | `chore` | Maintenance tasks |
40
+ | `revert` | Revert a commit |
41
+
42
+ ## Atomic Commit Principles
43
+
44
+ 1. One commit does one thing
45
+ 2. The repository should remain buildable/runnable after each commit
46
+ 3. Each commit should be independently revertible, affecting only that specific change
47
+ 4. Include related tests in the same commit when behavior changes
48
+ 5. Do not mix refactoring/formatting with behavior changes
49
+
50
+ ## Commit Workflow
51
+
52
+ ```bash
53
+ # 1. Review changes
54
+ git status --short
55
+ git diff --staged --stat
56
+
57
+ # 2. Stage selectively (avoid git add -A)
58
+ git add <specific-files>
59
+
60
+ # 3. Review what will be committed
61
+ git diff --staged
62
+
63
+ # 4. Commit (HEREDOC format)
64
+ git commit -m "$(cat <<'EOF'
65
+ <type>[scope]: <summary>
66
+
67
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
68
+ EOF
69
+ )"
70
+ ```
71
+
72
+ ## Notes
73
+
74
+ - Do not commit sensitive files such as `.env` or credentials
75
+ - After a pre-commit hook failure, fix the issue and create a **new commit** (do not use `--amend`)
76
+ - Do not push automatically; wait for user instructions
@@ -0,0 +1,49 @@
1
+ ---
2
+ summary: 'Harness setup: how images reach the model in Codex, Claude Code, Pi, and OpenCode'
3
+ read_when:
4
+ - Setting modlens up inside a specific coding agent
5
+ - A pasted image is not reaching the model
6
+ - Understanding what recover-paste does per harness
7
+ ---
8
+
9
+ # Harness setup
10
+
11
+ Where a pasted image ends up differs per harness, and modlens takes a different route in each. `recover-paste` detects which harness it runs inside (process ancestry, then environment fingerprints) and reads only that harness's storage.
12
+
13
+ ## Codex
14
+
15
+ Pasted images become real temp files, and the message carries a tag like `<image name=[Image #1] path="/tmp/xxxx.png">`. The skill reads the path out of the tag. `recover-paste` detects Codex and refuses, pointing back at the tag.
16
+
17
+ One catch with text-only models: once `models.json` declares `input_modalities: ["text"]`, the Codex TUI blocks Ctrl+V paste outright. Drag the file into the terminal, type its path, or use `codex exec -i image.png "..."`.
18
+
19
+ ## Claude Code, Pi, OpenCode
20
+
21
+ None of them writes a pasted image to a regular temp file, but all three persist the user message locally before any gateway strips it:
22
+
23
+ | Harness | Storage | Notes |
24
+ | :-- | :-- | :-- |
25
+ | Claude Code | `~/.claude/projects/<slug>/<session>.jsonl` | images as base64. The injected `CLAUDE_CODE_SESSION_ID` targets the exact session |
26
+ | Pi | `~/.pi/agent/sessions/--<encoded-cwd>--/*.jsonl` | same shape as Claude Code |
27
+ | OpenCode | `~/.local/share/opencode/opencode.db` | SQLite, images as data URLs (read via `node:sqlite`) |
28
+
29
+ Running a text-only model behind `ANTHROPIC_BASE_URL` in Claude Code, a pasted image arrives as a pathless `[Unsupported Image]` placeholder (on lenient gateways) or breaks the request outright ([#62009](https://github.com/anthropics/claude-code/issues/62009)). The bytes are not gone, and that is what `recover-paste` retrieves.
30
+
31
+ ## Skill locations
32
+
33
+ | Harness | Reads skills from |
34
+ | :-- | :-- |
35
+ | Claude Code | `~/.claude/skills/` |
36
+ | Codex | `~/.codex/skills/` |
37
+ | Pi, OpenCode | `~/.agents/skills/` |
38
+
39
+ Symlinks work in all of them, so linking the skill folder once keeps every agent on the latest version.
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
+
47
+ ## Gateway setups
48
+
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`.
@@ -0,0 +1,48 @@
1
+ ---
2
+ summary: 'Research Notes: Gemini CLI invocation mechanics + Claude Code skills model'
3
+ read_when:
4
+ - Designing or updating ModLens runtime behavior
5
+ - Updating skill metadata/trigger strategy
6
+ - Verifying compatibility with Claude Code and Gemini CLI
7
+ ---
8
+
9
+ > **HISTORICAL.** Written in the Gemini CLI era, before that free tier was shut
10
+ > down in June 2026 and ModLens moved to Antigravity CLI. Kept for the reasoning
11
+ > behind the skill-trigger design. For how things work now, read the README and
12
+ > `troubleshooting.md`.
13
+
14
+ # Research Notes: Gemini CLI + Claude Code Skills
15
+
16
+ Date: 2026-02-22
17
+
18
+ ## 1) Gemini CLI Findings (v1 implementation impact)
19
+
20
+ - Local environment verified: `gemini --version` is `0.24.0`.
21
+ - CLI help shows `-p/--prompt` is deprecated but still supported; positional prompt is the recommended long-term mode.
22
+ - `--output-format` supports `text | json | stream-json`; `json` is suitable for machine parsing.
23
+ - Gemini CLI supports `@path` references in prompt. For file references, docs specify that `@` can force full content inclusion.
24
+ - Package source inspection confirms images are handled as `inlineData` (base64 + MIME) when explicitly referenced by `@path` and passed through `read_many_files`.
25
+
26
+ Implementation decision:
27
+ - v1 keeps `gemini -p` (matching current project requirement) + `--output-format json`.
28
+ - To avoid workspace-bound path misses, include `--include-directories <image-dir>`.
29
+
30
+ ## 2) Claude Code Skills Findings
31
+
32
+ - Official docs define skills as foldered instruction packs with frontmatter metadata.
33
+ - Triggering relies heavily on `name` and `description` in frontmatter.
34
+ - Supported frontmatter controls include `allowed-tools`, `disable-model-invocation`, `model`, and `context` behaviors.
35
+ - Skills can be project-level and chained by the agent when descriptions match user intent.
36
+
37
+ Implementation decision:
38
+ - `skills/modlens/SKILL.md` uses explicit trigger description for image tasks + non-vision model scenarios.
39
+ - `allowed-tools: Bash` is set so the agent can deterministically call `modlens`.
40
+ - Scope is intentionally limited to image parsing; no modsearch/modfetch logic is included.
41
+
42
+ ## 3) Primary Sources
43
+
44
+ - Gemini CLI repo README: https://github.com/google-gemini/gemini-cli
45
+ - Gemini CLI file management docs (`@` references): https://gemini-cli.xyz/docs/cli/file-management/
46
+ - Claude Code skills docs: https://docs.claude.com/en/docs/claude-code/agent-skills
47
+ - Anthropic skills examples repo: https://github.com/anthropics/skills
48
+ - Claude help center (skills overview): https://support.claude.com/en/articles/11817219-what-are-skills
@@ -0,0 +1,31 @@
1
+ ---
2
+ summary: 'Security: what modlens runs, how recovered images are protected, image content as untrusted input'
3
+ read_when:
4
+ - Reviewing what this tool does on your machine
5
+ - Recovering pastes on a shared machine
6
+ - Deciding how much to trust what a vision engine reports
7
+ ---
8
+
9
+ # Security
10
+
11
+ ## Recovered images are private
12
+
13
+ Images pulled out of session storage are written 0600 into a 0700 directory. By default that directory is a fresh, unpredictable `<tmpdir>/modlens-paste-*` minted per run, so nobody on a shared machine can pre-create a known path (`recursive` mkdir leaves an existing directory's mode alone) and read the bytes. A pasted screenshot can hold anything. An explicit `--out-dir` is honoured but refused when unsafe: it must be a real directory, not a symlink, owned by you, with no group or world access.
14
+
15
+ Recovery is also scoped to one project: the working directory recorded inside the transcript is checked, not just the directory name, because directory slugs collide (`/tmp/a.b` and `/tmp/a-b` produce the same one). A neighbouring project's images are never handed over.
16
+
17
+ ## Permissions passed to engines
18
+
19
+ ModLens invokes `agy` with `--dangerously-skip-permissions` because prompt mode fails in some environments without it. The prompt restricts the agent to reading the one image it was given, and instructs it to treat image content strictly as data.
20
+
21
+ The `claude-cli` provider runs with `--allowedTools Read` only, so it can read local files and nothing else.
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.
24
+
25
+ ## Image content is untrusted input
26
+
27
+ Text inside an image is untrusted, the same as a web page. A screenshot can contain instructions aimed at whatever reads it. The prompt says so explicitly, but that is mitigation, not a guarantee: analyze images you are willing to open, and prefer a sandboxed working directory when they came from elsewhere.
28
+
29
+ ## Evidence, not invention
30
+
31
+ What the engine cannot read goes into `uncertainty` rather than being filled in. v2 dropped pixel coordinates and confidence scores entirely, because those are the two fields models fabricate most convincingly.
@@ -0,0 +1,29 @@
1
+ ---
2
+ summary: 'Testing Guide: Vitest usage, co-located layout, conventions'
3
+ read_when:
4
+ - Running tests
5
+ - Writing tests
6
+ - Troubleshooting test failures
7
+ ---
8
+
9
+ # Testing Guide
10
+
11
+ ## Layout
12
+
13
+ Tests are co-located with sources: every module gets an adjacent `*.test.ts` (for example `src/config.ts` and `src/config.test.ts`, `src/providers/geminiApi.ts` and `src/providers/geminiApi.test.ts`). There is no separate `test/` directory. Vite's lib build only follows the import graph from `src/main.ts`, so test files never enter `dist/`.
14
+
15
+ ## Commands
16
+
17
+ ```bash
18
+ pnpm test # run everything
19
+ pnpm exec vitest run src/config.test.ts # run a single file
20
+ pnpm typecheck # tsc --noEmit, run before tests
21
+ ```
22
+
23
+ ## Conventions
24
+
25
+ - No network in unit tests: stub `fetch` with `vi.stubGlobal('fetch', ...)` and clean up in `afterEach` via `vi.unstubAllGlobals()`.
26
+ - ESM module namespaces cannot be spied on (`vi.spyOn(fs, ...)` throws). Use real temp files via `fs.mkdtempSync(path.join(os.tmpdir(), ...))` and remove them in the test.
27
+ - To fake the home directory (config, transcripts), set `process.env.HOME` and restore it in `finally`; `os.homedir()` follows it on POSIX.
28
+ - Providers with subprocess transports test `buildInvocation`/`parseOutput` as pure functions; API providers test `execute` against a stubbed `fetch`, asserting both the request body and the parsed result.
29
+ - Real provider calls (agy, API keys, Claude login) are end-to-end verification, not unit tests. Keep them out of `pnpm test`.
@@ -0,0 +1,140 @@
1
+ ---
2
+ summary: 'Troubleshooting: every error modlens can print, what causes it, what to do'
3
+ read_when:
4
+ - A run failed and the message is not self-explanatory
5
+ - recover-paste found nothing, or found the wrong image
6
+ - Deciding whether a failure is setup, quota, or a bug
7
+ ---
8
+
9
+ # Troubleshooting
10
+
11
+ Start with `modlens doctor`: it checks your Node version, which providers are ready, which one will be selected and why, and the detected harness, all without spending quota or making a network request. It catches most setup problems before you read any further.
12
+
13
+ Every message below is one modlens actually prints. Search this file for the words you saw.
14
+
15
+ ## Antigravity CLI cannot read its stored login token
16
+
17
+ ```
18
+ Antigravity CLI cannot read its stored login token.
19
+
20
+ On Linux this usually means the OS keyring is locked, which is normal for headless
21
+ sessions (agents, cron, systemd, SSH without a desktop login) ...
22
+ ```
23
+
24
+ agy keeps its token in the OS keyring. When the keyring is locked, agy reports itself as signed out and tries a browser sign-in that cannot finish without a display. Three ways forward:
25
+
26
+ - Unlock the keyring, or run modlens from a desktop session.
27
+ - Sign in again with `agy`.
28
+ - Switch to a provider that needs no interactive login:
29
+
30
+ ```bash
31
+ modlens config set gemini-api.apiKey <key> # free key: https://aistudio.google.com
32
+ modlens config set provider gemini-api
33
+ ```
34
+
35
+ ## Quota exhausted
36
+
37
+ ```
38
+ Individual quota reached. ... Resets in 94h19m9s.
39
+
40
+ agy's free tier is one weekly bucket shared by the desktop app, the CLI, and the SDK ...
41
+ ```
42
+
43
+ Wait for the reset, or move to `gemini-api`, which has its own budget. Parallel subagents drain the shared bucket fast, so a heavy day can end it.
44
+
45
+ ## Provider CLI not found
46
+
47
+ ```
48
+ Provider CLI not found: agy. Install it and sign in first.
49
+ ```
50
+
51
+ The binary is not on PATH, or `--provider-bin` points somewhere wrong.
52
+
53
+ ```
54
+ Working directory does not exist: /some/path
55
+ ```
56
+
57
+ Different cause, same underlying error code from the OS: `--workdir` points at a directory that is not there. The binary is fine.
58
+
59
+ ## recover-paste found nothing
60
+
61
+ ```
62
+ No pasted images found in any session storage for this directory (looked in: ...)
63
+ ```
64
+
65
+ In order of likelihood:
66
+
67
+ - **You are in the wrong directory.** Recovery is scoped to the project the conversation is happening in. Pass `--cwd /path/to/project`.
68
+ - **Nothing was pasted.** Dragged files and typed paths are already real files, so there is nothing to recover: use the path directly.
69
+ - **A setup problem is blocking one harness.** Anything blocking appears after `Blocked:` in the same message, for example OpenCode needing Node 22.13+ for `node:sqlite`.
70
+
71
+ ## recover-paste returned an image from another project
72
+
73
+ This should not happen any more, and if it does it is a bug worth reporting. Recovery checks the working directory recorded inside the transcript, not just the directory name, because directory slugs collide (`/tmp/a.b` and `/tmp/a-b` produce the same one). Include the `harness` and `transcript` fields from the output in the issue.
74
+
75
+ ## Recovered the wrong image from the right project
76
+
77
+ The output lists images oldest to newest, so the **last** entry is the most recent paste. Entries carry `filename` when the harness stored one: match on that when the user mentioned a name. `--count 3` gives you more to choose from.
78
+
79
+ ## recover-paste: overriding detection and output location
80
+
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
+
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. 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
+
86
+ ## This is a Codex session
87
+
88
+ ```
89
+ This is a Codex session: pasted images already exist as temp files, and each image
90
+ tag in the message carries its path.
91
+ ```
92
+
93
+ Working as intended. Codex writes pasted images to disk and puts the path in the message, so read the path out of the tag instead of recovering anything.
94
+
95
+ ## The openai provider rejected a result
96
+
97
+ ```
98
+ OpenAI-compatible API returned JSON that does not match the vision schema
99
+ (missing: ocr, ocr.full_text, ...)
100
+ ```
101
+
102
+ That endpoint returned a partial result. Only agy, gemini-api, anthropic, and claude-cli enforce the schema server-side, so weaker gateways can produce half a result. Retry once, then switch:
103
+
104
+ ```bash
105
+ modlens -i <image> -p gemini-api
106
+ ```
107
+
108
+ ## Config file problems
109
+
110
+ ```
111
+ Cannot read /Users/you/.modlens/config.json: EACCES ... Fix the file or its permissions.
112
+ ```
113
+
114
+ The file exists but is unreadable. A missing file is fine, so this is a real problem rather than something to ignore.
115
+
116
+ ```
117
+ Failed to parse ... Fix or delete the file.
118
+ ```
119
+
120
+ Invalid JSON. `modlens config init --force` writes a clean one, losing the old contents.
121
+
122
+ ## Timeouts
123
+
124
+ ```
125
+ antigravity-cli provider timed out after 210000 ms.
126
+ ```
127
+
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
+
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
+
138
+ ## Still stuck
139
+
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,32 @@
1
1
  {
2
2
  "name": "@liustack/modlens",
3
- "version": "2.8.0",
3
+ "version": "3.1.0",
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",
24
+ "docs",
11
25
  "skills/modlens/SKILL.md",
12
- "skills/modlens/references"
26
+ "skills/modlens/scripts",
27
+ "skills/modlens/references",
28
+ "CHANGELOG.md",
29
+ "SECURITY.md"
13
30
  ],
14
31
  "keywords": [
15
32
  "cli",
@@ -34,23 +51,17 @@
34
51
  },
35
52
  "homepage": "https://github.com/liustack/modlens#readme",
36
53
  "engines": {
37
- "node": ">=18"
54
+ "node": ">=22.13"
38
55
  },
39
56
  "dependencies": {
40
57
  "commander": "^13.1.0"
41
58
  },
42
59
  "devDependencies": {
60
+ "@biomejs/biome": "^2.5.7",
43
61
  "@types/node": "^22.19.7",
62
+ "@vitest/coverage-v8": "^3.2.7",
44
63
  "typescript": "^5.9.3",
45
64
  "vite": "^6.4.1",
46
- "vitest": "^3.2.4"
47
- },
48
- "scripts": {
49
- "dev": "vite build --watch",
50
- "build": "vite build",
51
- "typecheck": "tsc --noEmit",
52
- "test": "vitest run",
53
- "release": "node scripts/release.mjs",
54
- "docs:list": "node scripts/docs-list.js"
65
+ "vitest": "^3.2.7"
55
66
  }
56
67
  }
@@ -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.0.0):
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.0.0: `modlens <args>`.
39
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.0.0 modlens <args>`.
40
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.0.0 <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:
@@ -101,7 +116,7 @@ Structure is enforced by schema on antigravity-cli and claude-cli (`--json-schem
101
116
 
102
117
  ## Failure Handling
103
118
 
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.
119
+ Every error this CLI prints names its cause, and most already name the fix, so read the message first.
105
120
 
106
121
  - `Provider CLI not found`: Antigravity CLI is not installed. Install it, or switch provider: `-p gemini-api`.
107
122
  - 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)
@@ -61,6 +95,8 @@ modlens config set anthropic.apiKey <sk-ant-key>
61
95
 
62
96
  Default model is Claude Haiku (`claude-haiku-4-5-20251001`). Schema is enforced through a forced tool call.
63
97
 
98
+ **`ANTHROPIC_BASE_URL` trap.** modlens binds `ANTHROPIC_BASE_URL` to `anthropic.baseUrl`, so it inherits whatever that variable points at. If the user set it in their shell to route Claude Code through a text-only gateway (a common way to run a non-Claude model behind the Claude Code UI), then `-p anthropic` silently sends the vision request to that gateway too, where it fails or comes back blind, with no hint that the endpoint was swapped. Check `echo $ANTHROPIC_BASE_URL` when anthropic vision misbehaves. Fixes: unset it for the modlens call, pin the real endpoint with `modlens config set anthropic.baseUrl https://api.anthropic.com`, or use `-p gemini-api` instead.
99
+
64
100
  ### claude-cli (Claude Code login, no key)
65
101
 
66
102
  Rides an existing `claude` sign-in, so it costs the user's Claude subscription quota, not a separate API bill. Requires Claude Code installed and logged in (`claude --version` to check). Runs with `--allowedTools Read` only. Local image files only; for remote URLs use gemini-api instead. Default model alias `haiku`.