cc-vision-hook 0.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +158 -0
  4. package/README.zh-CN.md +158 -0
  5. package/dist/cache.d.ts +70 -0
  6. package/dist/cache.js +164 -0
  7. package/dist/cache.js.map +1 -0
  8. package/dist/claudeSettings.d.ts +31 -0
  9. package/dist/claudeSettings.js +115 -0
  10. package/dist/claudeSettings.js.map +1 -0
  11. package/dist/cli.d.ts +7 -0
  12. package/dist/cli.js +106 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/configCmd.d.ts +18 -0
  15. package/dist/commands/configCmd.js +104 -0
  16. package/dist/commands/configCmd.js.map +1 -0
  17. package/dist/commands/doctor.d.ts +10 -0
  18. package/dist/commands/doctor.js +60 -0
  19. package/dist/commands/doctor.js.map +1 -0
  20. package/dist/commands/install.d.ts +16 -0
  21. package/dist/commands/install.js +48 -0
  22. package/dist/commands/install.js.map +1 -0
  23. package/dist/commands/status.d.ts +9 -0
  24. package/dist/commands/status.js +35 -0
  25. package/dist/commands/status.js.map +1 -0
  26. package/dist/commands/testImage.d.ts +11 -0
  27. package/dist/commands/testImage.js +40 -0
  28. package/dist/commands/testImage.js.map +1 -0
  29. package/dist/commands/toggle.d.ts +10 -0
  30. package/dist/commands/toggle.js +21 -0
  31. package/dist/commands/toggle.js.map +1 -0
  32. package/dist/config.d.ts +72 -0
  33. package/dist/config.js +94 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/extract.d.ts +32 -0
  36. package/dist/extract.js +105 -0
  37. package/dist/extract.js.map +1 -0
  38. package/dist/hooks/postToolUse.d.ts +27 -0
  39. package/dist/hooks/postToolUse.js +53 -0
  40. package/dist/hooks/postToolUse.js.map +1 -0
  41. package/dist/hooks/userPromptSubmit.d.ts +29 -0
  42. package/dist/hooks/userPromptSubmit.js +64 -0
  43. package/dist/hooks/userPromptSubmit.js.map +1 -0
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/pasteScanner.d.ts +37 -0
  48. package/dist/pasteScanner.js +68 -0
  49. package/dist/pasteScanner.js.map +1 -0
  50. package/dist/sdk/index.d.ts +12 -0
  51. package/dist/sdk/index.js +12 -0
  52. package/dist/sdk/index.js.map +1 -0
  53. package/dist/seenTracker.d.ts +23 -0
  54. package/dist/seenTracker.js +46 -0
  55. package/dist/seenTracker.js.map +1 -0
  56. package/dist/vision.d.ts +40 -0
  57. package/dist/vision.js +87 -0
  58. package/dist/vision.js.map +1 -0
  59. package/package.json +66 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ 本项目遵循 [Semantic Versioning](https://semver.org/)。
4
+
5
+ ## [0.1.0] - 2026-07-08
6
+
7
+ 首个可发布版本(P0 范围)。
8
+
9
+ ### 新增
10
+
11
+ - `UserPromptSubmit` Hook:扫描 `~/.claude/image-cache/<session_id>/` 识别用户粘贴的图片。
12
+ - `PostToolUse` Hook(`matcher: "*"`):通用递归提取器识别任意工具的图片输出,已验证覆盖
13
+ 三种真实 schema(`Read` 判别式对象 / MCP content block 数组 / `Bash`/`PowerShell` 的
14
+ `isImage` 旁路标志 + data URI 字符串)。
15
+ - 两个 Hook 均只输出 `additionalContext`(追加文字描述),不替换/删除原始内容——
16
+ 仅对"静默忽略图片"型模型有效,对"协议层硬拒绝"型模型无效(已在 README/`doctor` 中显著声明)。
17
+ - 视觉解析统一通过 [AI SDK](https://sdk.vercel.ai/) 对接四个 provider:
18
+ `oai`(Chat Completions)、`responses`(Responses API)、`anthropic`、`gemini`。
19
+ - 图片解析结果按内容 hash 缓存在本地磁盘(`~/.claude/cc-vision-hook/cache/`),
20
+ 全局共享、TTL 7 天惰性清理,跨来源(粘贴/工具产图)同一张图只解析一次。
21
+ - Session 级"已展示过的图片"标记(`seenTracker.ts`),避免多轮对话后
22
+ `additionalContext` 随对话轮数线性膨胀。
23
+ - CLI 命令:`install`/`uninstall [--purge]`/`enable`/`disable`/`status`/`doctor`/
24
+ `config get`/`config set`/`test-image`,全部支持 `--json` 输出。
25
+ - SDK 导出(`cc-vision-hook/sdk`),供其他 Node/Bun 项目直接内置调用。
26
+
27
+ ### 修复
28
+
29
+ - **AI SDK provider 混淆**:`"oai"` 配置项曾经直接用 `openai(modelId)` 默认调用,
30
+ 在当前 AI SDK 版本下实际解析成 Responses API,导致 `"oai"`/`"responses"` 两个
31
+ 配置项形同虚设。已改为显式 `openai.chat(modelId)`,并加回归测试。
32
+ - `PostToolUse` Hook 注册补充显式 `matcher: "*"`(不依赖未经验证的默认留空行为)。
33
+ - `config set` 增加前置值校验(provider 枚举、baseUrl URL 格式、timeoutMs/maxTokens
34
+ 正数校验),避免非法值要等到真正调用视觉模型时才报错。
35
+
36
+ ### 测试
37
+
38
+ - 55+ 单测覆盖 `extract`/`cache`/`claudeSettings`/`vision`/两个 hook,均为 fixture
39
+ 驱动(fixture 来自真实 Claude Code payload 样本)。
40
+ - 真机验证:用真实 API Key + Claude Code 2.1.144,在隔离 `HOME` 下验证了完整链路——
41
+ 一个协议层"静默忽略图片"型模型在未装 cvh 时会瞎猜图片颜色,装了 cvh 后
42
+ transcript 证实 `additionalContext` 被正确注入并最终答对颜色。
43
+
44
+ ### 已知限制
45
+
46
+ 见 README「已知限制」章节:不支持协议层硬拒绝型模型;粘贴图片场景依赖非官方
47
+ 实现细节(`~/.claude/image-cache/`);不做模型能力自动判断;只面向 Claude Code。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cc-vision-hook contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ <div align="center">
2
+
3
+ # cc-vision-hook
4
+
5
+ **Give vision-blind Claude Code models eyes — without touching your prompt.**
6
+
7
+ `cvh` is a local-only Claude Code Hook toolkit. When you paste an image, or a tool (`Read` / `Bash` / MCP) produces a screenshot, `cvh` sends it to a vision model of your choice and injects the description back into Claude Code's context as `additionalContext` — so a model that would otherwise silently ignore the image can actually "see" it.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/cc-vision-hook.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/cc-vision-hook)
10
+ [![npm downloads](https://img.shields.io/npm/dm/cc-vision-hook.svg?color=blue)](https://www.npmjs.com/package/cc-vision-hook)
11
+ [![GitHub stars](https://img.shields.io/github/stars/RainSunMe/cc-vision-hook?style=social)](https://github.com/RainSunMe/cc-vision-hook)
12
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
13
+ [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](package.json)
14
+
15
+ [English](./README.md) · [简体中文](./README.zh-CN.md)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## Why
22
+
23
+ Not every model behind Claude Code understands images. Depending on the backend, an unsupported image request fails in one of two very different ways:
24
+
25
+ | Failure mode | What happens | Can `cvh` fix it? |
26
+ |---|---|---|
27
+ | **Silently ignoring** | The API call succeeds (HTTP 200), but the model never actually looks at the image — it guesses, hallucinates, or just says "I can't see the image." | ✅ Yes — this is exactly what `cvh` is built for |
28
+ | **Protocol-level hard rejection** | The API call fails outright (e.g. `404 No endpoints found that support image input`), and the *entire* turn fails, text included. | ❌ No — see [Scope & limitations](#scope--limitations) |
29
+
30
+ `cvh` targets the first case. It never touches the original image content block — it only **appends** a text description generated by a vision model you configure, via Claude Code's `additionalContext` hook output. That's the whole trick: no schema surgery, no risky content replacement, just an honest text description sitting next to the image the main model can't parse.
31
+
32
+ > ✅ **Verified against a real Claude Code session.** A model that silently ignores images was tested with and without `cvh`: without it, it guessed the wrong color for a test image; with `cvh` installed, the hook transcript shows `additionalContext` being injected and the model answering correctly. See [`CHANGELOG.md`](./CHANGELOG.md) for details.
33
+
34
+ ## How it works
35
+
36
+ ```mermaid
37
+ flowchart LR
38
+ A["User pastes image /<br/>tool returns screenshot"] --> B{cvh enabled?}
39
+ B -- no --> Z["passthrough, no-op"]
40
+ B -- yes --> C["Extract image bytes<br/>(UserPromptSubmit / PostToolUse)"]
41
+ C --> D{"Cached by<br/>content hash?"}
42
+ D -- hit --> F
43
+ D -- miss --> E["Ask your vision model<br/>(via AI SDK)"]
44
+ E --> F["additionalContext<br/>injected into Claude Code"]
45
+ ```
46
+
47
+ - **`UserPromptSubmit`** hook scans `~/.claude/image-cache/<session_id>/` for newly pasted images.
48
+ - **`PostToolUse`** hook (`matcher: "*"`) runs a generic recursive extractor over any tool's `tool_response` — verified against three real, structurally different shapes: `Read`'s discriminated object, MCP's content-block array, and `Bash`/`PowerShell`'s flat `isImage` + data-URI string.
49
+ - Both hooks **only ever output `additionalContext`** — never `updatedToolOutput`. This keeps `cvh` simple and safe: no per-tool schema replication, no risk of a silently-dropped hook output because the replacement shape didn't match.
50
+ - Vision inference goes through the [Vercel AI SDK](https://sdk.vercel.ai/), so you can point `cvh` at OpenAI (Chat Completions or Responses), Anthropic, or Gemini — or any OpenAI/Anthropic-compatible gateway.
51
+ - Descriptions are cached on disk by image content hash (`~/.claude/cc-vision-hook/cache/`), shared globally, with a 7-day lazy-expiring TTL — the same image is only ever described once, no matter where it came from.
52
+
53
+ ## Scope & limitations
54
+
55
+ **`cvh` only helps with models that silently ignore images.** If your model hard-rejects image input at the protocol level (the request itself fails), `cvh` cannot help — it never replaces or removes the original image content block, so the upstream will still see (and reject) it. Run `cvh doctor` or send a manual test request to figure out which category your model falls into before enabling `cvh`.
56
+
57
+ Other known limitations:
58
+
59
+ 1. Hard-rejection models are out of scope (see above).
60
+ 2. Support for "user pastes an image" relies on an **unofficial implementation detail** of Claude Code (`~/.claude/image-cache/` directory scanning). This could break in a future Claude Code release; `cvh` does not attempt version detection or graceful degradation.
61
+ 3. `cvh` does not auto-detect model capabilities. It's a plain `enable`/`disable` switch — you decide when to turn it on.
62
+ 4. Claude Code only. No support for other agent hosts.
63
+ 5. Images are sent to whichever third-party vision provider you configure. You are responsible for that data flow — review your provider's data handling policy if your screenshots may contain sensitive information.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ npm install -g cc-vision-hook
69
+
70
+ cvh install # create config + register hooks (idempotent)
71
+ cvh config set provider anthropic # or: oai / responses / gemini
72
+ cvh config set model <your-vision-model>
73
+ cvh config set apiKey <your-api-key>
74
+ cvh doctor # sanity-check config + connectivity
75
+ cvh enable
76
+ ```
77
+
78
+ ## Commands
79
+
80
+ | Command | Description |
81
+ |---|---|
82
+ | `cvh install` | Create config + register hooks (idempotent, safe to re-run) |
83
+ | `cvh uninstall [--purge]` | Remove hook registration; `--purge` also deletes config and cache |
84
+ | `cvh enable` / `cvh disable` | The one and only runtime switch |
85
+ | `cvh status` | Show current state, hook registration, cache stats |
86
+ | `cvh doctor` | Self-check config / hooks / vision model connectivity, plus a boundary reminder |
87
+ | `cvh config get` / `cvh config set <key> <value>` | Read/write config (`provider`/`model`/`baseUrl`/`apiKey`/`timeoutMs`/`maxTokens`) |
88
+ | `cvh test-image <path>` | Manually verify the pipeline: local image → vision model → description |
89
+
90
+ Every command supports `--json` output for scripting.
91
+
92
+ ## Configuration
93
+
94
+ `~/.claude/cc-vision-hook.json`:
95
+
96
+ ```json
97
+ {
98
+ "enabled": true,
99
+ "provider": "oai",
100
+ "model": "gpt-4o-mini",
101
+ "baseUrl": "https://api.openai.com/v1",
102
+ "apiKey": "sk-...",
103
+ "timeoutMs": 45000,
104
+ "maxTokens": 1200,
105
+ "cache": { "ttlDays": 7 }
106
+ }
107
+ ```
108
+
109
+ The API key is stored in plaintext; the file permission is automatically set to `0600` (owner read/write only).
110
+
111
+ Environment variable overrides (take priority over the config file): `CVH_ENABLED` / `CVH_PROVIDER` / `CVH_MODEL` / `CVH_BASE_URL` / `CVH_API_KEY` / `CVH_TIMEOUT_MS` / `CVH_MAX_TOKENS`.
112
+
113
+ ## Roadmap
114
+
115
+ - [ ] `cvh init` — interactive setup wizard.
116
+ - [ ] MCP tools (`vision_ask`, `vision_describe_image`, `vision_describe_data_url`) so an agent can follow up on a previously seen image. The on-disk cache already stores everything needed — the MCP surface just hasn't been wired up yet.
117
+
118
+ ## Troubleshooting
119
+
120
+ - **Installed but nothing happens** — Claude Code reads `$HOME/.claude/settings.json` (not `$HOME` itself). Make sure the path `cvh install` wrote to matches the one Claude Code actually loads. Run `cvh status` and confirm both hooks are `true` and `enabled` is `true`.
121
+ - **`doctor` reports a connectivity failure** — run `cvh test-image <local-image>` first to isolate whether the problem is in the vision-model call itself or in hook wiring.
122
+ - **The model errors out entirely / the whole turn fails when an image is involved** — you're using a hard-rejection model. `cvh` cannot help here; see [Scope & limitations](#scope--limitations).
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ bun install
128
+ bun run typecheck # tsc --noEmit
129
+ bun run build # emit dist/
130
+ bun test # fixture-driven unit tests, no real network calls
131
+ ```
132
+
133
+ For local/integration testing, override `~/.claude` with the `CVH_CLAUDE_HOME` environment variable so you never touch your real Claude Code configuration:
134
+
135
+ ```bash
136
+ export CVH_CLAUDE_HOME=/tmp/some-isolated-dir/.claude
137
+ node dist/cli.js install
138
+ ```
139
+
140
+ When wiring this up against a real Claude Code session, set `CVH_CLAUDE_HOME` to `$HOME/.claude` (not `$HOME` itself), and also set `HOME=<isolated dir>` so the `claude` process reads the same `settings.json`.
141
+
142
+ ## Contributing
143
+
144
+ Issues and PRs are welcome. Please run `bun run ci` (typecheck + test + build) before submitting.
145
+
146
+ Contributions that add or change behavior should include a [changeset](https://github.com/changesets/changesets):
147
+
148
+ ```bash
149
+ bun run changeset
150
+ ```
151
+
152
+ ## Releasing
153
+
154
+ Releases are published to npm via [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC, no long-lived tokens) whenever a `v*` tag is pushed. See [`docs/releasing.md`](./docs/releasing.md) for the full process and [`docs/release-checklist.md`](./docs/release-checklist.md) for the pre-flight checklist.
155
+
156
+ ## License
157
+
158
+ [MIT](./LICENSE)
@@ -0,0 +1,158 @@
1
+ <div align="center">
2
+
3
+ # cc-vision-hook
4
+
5
+ **让"看不懂图片"的 Claude Code 模型拥有眼睛——完全不动你的 prompt。**
6
+
7
+ `cvh` 是一个纯本地运行的 Claude Code Hook 工具集。当你粘贴图片、或某个工具(`Read` / `Bash` / MCP)产出截图时,`cvh` 会把图片发给你自己配置的视觉模型,再把描述文字以 `additionalContext` 的形式注入回 Claude Code 的上下文——让原本"看不懂图片"的主模型也能真正"看到"图片内容。
8
+
9
+ [![npm version](https://img.shields.io/npm/v/cc-vision-hook.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/cc-vision-hook)
10
+ [![npm downloads](https://img.shields.io/npm/dm/cc-vision-hook.svg?color=blue)](https://www.npmjs.com/package/cc-vision-hook)
11
+ [![GitHub stars](https://img.shields.io/github/stars/RainSunMe/cc-vision-hook?style=social)](https://github.com/RainSunMe/cc-vision-hook)
12
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
13
+ [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](package.json)
14
+
15
+ [English](./README.md) · [简体中文](./README.zh-CN.md)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## 为什么需要它
22
+
23
+ Claude Code 背后接的模型不一定都支持图片。当一个不支持视觉的模型收到图片请求时,通常会出现两种完全不同的失败方式:
24
+
25
+ | 失败模式 | 表现 | `cvh` 能解决吗 |
26
+ |---|---|---|
27
+ | **静默忽略** | API 调用成功(HTTP 200),但模型根本没真正理解图片内容——它会瞎猜、幻觉,或者直接说"我看不到图片"。 | ✅ 能——这正是 `cvh` 的目标场景 |
28
+ | **协议层硬拒绝** | API 调用直接失败(例如常见的 `404 No endpoints found that support image input`),**整轮对话**都会失败,连文本部分也一起废弃。 | ❌ 不能——见[适用范围与已知限制](#适用范围与已知限制) |
29
+
30
+ `cvh` 只解决第一种场景。它从不修改原始的图片内容块,只通过 Claude Code 的 `additionalContext` Hook 输出,**追加**一段由你配置的视觉模型生成的文字描述。整个方案就这么简单:不需要精确还原任何工具的输出 schema,不需要冒险替换原始内容,只是在模型看不懂的图片旁边,放一段诚实的文字描述。
31
+
32
+ > ✅ **已通过真实 Claude Code 会话验证。** 一个"静默忽略图片"型模型分别在装/未装 `cvh` 的情况下测试:未装时,它把测试图片的颜色猜错了;装了 `cvh` 后,Hook 执行记录显示 `additionalContext` 被正确注入,模型给出了正确答案。详见 [`CHANGELOG.md`](./CHANGELOG.md)。
33
+
34
+ ## 工作原理
35
+
36
+ ```mermaid
37
+ flowchart LR
38
+ A["用户粘贴图片 /<br/>工具返回截图"] --> B{cvh 已启用?}
39
+ B -- 否 --> Z["直接放行,不做任何事"]
40
+ B -- 是 --> C["提取图片字节<br/>(UserPromptSubmit / PostToolUse)"]
41
+ C --> D{"内容 hash<br/>命中缓存?"}
42
+ D -- 命中 --> F
43
+ D -- 未命中 --> E["调用你配置的视觉模型<br/>(经由 AI SDK)"]
44
+ E --> F["additionalContext<br/>注入 Claude Code 上下文"]
45
+ ```
46
+
47
+ - **`UserPromptSubmit`** Hook 扫描 `~/.claude/image-cache/<session_id>/` 目录,找出新粘贴的图片。
48
+ - **`PostToolUse`** Hook(`matcher: "*"`)用通用递归提取器扫描任意工具的 `tool_response`——已验证覆盖三种结构完全不同的真实样本:`Read` 的判别式对象、MCP 的 content block 数组、`Bash`/`PowerShell` 的扁平 `isImage` 标志 + data URI 字符串。
49
+ - 两个 Hook **永远只输出 `additionalContext`**,从不使用 `updatedToolOutput`。这让 `cvh` 保持简单又安全:不需要为每个工具单独复刻输出 schema,也不用担心替换值 shape 不匹配导致 Hook 输出被 Claude Code 静默丢弃。
50
+ - 视觉推理统一通过 [Vercel AI SDK](https://sdk.vercel.ai/) 完成,你可以把 `cvh` 指向 OpenAI(Chat Completions 或 Responses)、Anthropic、Gemini,或任何兼容 OpenAI/Anthropic 协议的网关。
51
+ - 解析结果按图片内容 hash 缓存在本地磁盘(`~/.claude/cc-vision-hook/cache/`),全局共享,TTL 7 天惰性过期——同一张图无论来自哪里,只会被解析一次。
52
+
53
+ ## 适用范围与已知限制
54
+
55
+ **`cvh` 只对"静默忽略图片"型模型有效。** 如果你的模型在协议层就直接硬拒绝图片输入(请求本身就失败),`cvh` 无法帮上忙——它从不替换或移除原始的图片内容块,上游依然会看到(并拒绝)它。启用前先跑一下 `cvh doctor` 或手动发一次测试请求,确认你的模型属于哪一类。
56
+
57
+ 其他已知限制:
58
+
59
+ 1. 协议层硬拒绝型模型不在解决范围内(见上)。
60
+ 2. "用户粘贴图片"场景的支持依赖 Claude Code 的一个**非官方实现细节**(扫描 `~/.claude/image-cache/` 目录)。未来的 Claude Code 版本可能会让这条路径失效;`cvh` 不做版本检测或优雅降级。
61
+ 3. `cvh` 不会自动判断模型能力,只有简单的 `enable`/`disable` 开关——启用与否由你决定。
62
+ 4. 仅支持 Claude Code,不支持其他 Agent 宿主。
63
+ 5. 图片会被发送给你配置的第三方视觉模型服务商,数据流向由你自己负责——如果截图可能包含敏感信息,请自行评估该服务商的数据处理政策。
64
+
65
+ ## 安装
66
+
67
+ ```bash
68
+ npm install -g cc-vision-hook
69
+
70
+ cvh install # 创建配置 + 注册 Hook(幂等)
71
+ cvh config set provider anthropic # 或 oai / responses / gemini
72
+ cvh config set model <你的视觉模型>
73
+ cvh config set apiKey <你的-api-key>
74
+ cvh doctor # 自检配置与连通性
75
+ cvh enable
76
+ ```
77
+
78
+ ## 命令
79
+
80
+ | 命令 | 说明 |
81
+ |---|---|
82
+ | `cvh install` | 创建配置 + 注册 Hook(幂等,可重复执行) |
83
+ | `cvh uninstall [--purge]` | 移除 Hook 注册;`--purge` 连带删除配置和缓存 |
84
+ | `cvh enable` / `cvh disable` | 唯一的运行开关 |
85
+ | `cvh status` | 查看当前状态、Hook 注册情况、缓存统计 |
86
+ | `cvh doctor` | 自检配置 / Hook / 视觉模型连通性,并提示适用边界 |
87
+ | `cvh config get` / `cvh config set <key> <value>` | 读写配置(`provider`/`model`/`baseUrl`/`apiKey`/`timeoutMs`/`maxTokens`) |
88
+ | `cvh test-image <path>` | 手动验证:本地图片 → 视觉模型 → 描述文字 |
89
+
90
+ 所有命令均支持 `--json` 输出,方便脚本调用。
91
+
92
+ ## 配置文件
93
+
94
+ `~/.claude/cc-vision-hook.json`:
95
+
96
+ ```json
97
+ {
98
+ "enabled": true,
99
+ "provider": "oai",
100
+ "model": "gpt-4o-mini",
101
+ "baseUrl": "https://api.openai.com/v1",
102
+ "apiKey": "sk-...",
103
+ "timeoutMs": 45000,
104
+ "maxTokens": 1200,
105
+ "cache": { "ttlDays": 7 }
106
+ }
107
+ ```
108
+
109
+ API Key 以明文存储,文件权限会自动设为 `0600`(仅当前用户可读写)。
110
+
111
+ 环境变量覆盖(优先级高于配置文件):`CVH_ENABLED` / `CVH_PROVIDER` / `CVH_MODEL` / `CVH_BASE_URL` / `CVH_API_KEY` / `CVH_TIMEOUT_MS` / `CVH_MAX_TOKENS`。
112
+
113
+ ## 路线图
114
+
115
+ - [ ] `cvh init` —— 交互式安装向导。
116
+ - [ ] MCP 工具(`vision_ask`、`vision_describe_image`、`vision_describe_data_url`),支持 Agent 对已解析过的图片进行事后追问。底层磁盘缓存已经具备所需数据,只是还没有暴露 MCP 入口。
117
+
118
+ ## 排障
119
+
120
+ - **装了但看起来没生效**:Claude Code 读取的是 `$HOME/.claude/settings.json`(不是 `$HOME` 本身),确认 `cvh install` 写入的路径和 Claude Code 实际加载的路径一致。用 `cvh status` 确认两个 Hook 均为 `true`,`enabled` 为 `true`。
121
+ - **`doctor` 报连通性失败**:先用 `cvh test-image <本地图片>` 单独验证视觉模型调用链路,排除 Hook 集成层面的问题。
122
+ - **模型收到图片直接报错、整轮对话失败**:说明你用的是「协议层硬拒绝」型模型,`cvh` 对此无效,见[适用范围与已知限制](#适用范围与已知限制)。
123
+
124
+ ## 开发
125
+
126
+ ```bash
127
+ bun install
128
+ bun run typecheck # tsc --noEmit
129
+ bun run build # 产出 dist/
130
+ bun test # fixture 驱动的单测,不发真实网络请求
131
+ ```
132
+
133
+ 本地/集成测试时,可用 `CVH_CLAUDE_HOME` 环境变量覆盖 `~/.claude` 根目录,避免影响你真实的 Claude Code 配置:
134
+
135
+ ```bash
136
+ export CVH_CLAUDE_HOME=/tmp/some-isolated-dir/.claude
137
+ node dist/cli.js install
138
+ ```
139
+
140
+ 如果要联调真实的 Claude Code 会话,需要把 `CVH_CLAUDE_HOME` 设为 `$HOME/.claude`(不是 `$HOME` 本身),同时设置 `HOME=<隔离目录>`,这样 `claude` 进程才会读到同一份 `settings.json`。
141
+
142
+ ## 贡献
143
+
144
+ 欢迎提交 Issue 和 PR。提交前请先跑通 `bun run ci`(typecheck + test + build)。
145
+
146
+ 涉及行为变更的贡献请附带一个 [changeset](https://github.com/changesets/changesets):
147
+
148
+ ```bash
149
+ bun run changeset
150
+ ```
151
+
152
+ ## 发布
153
+
154
+ 发布通过 npm [Trusted Publishing](https://docs.npmjs.com/trusted-publishers)(基于 OIDC,不存任何长期 token)完成,推送 `v*` tag 即触发。完整流程见 [`docs/releasing.md`](./docs/releasing.md),发布前检查清单见 [`docs/release-checklist.md`](./docs/release-checklist.md)。
155
+
156
+ ## 许可证
157
+
158
+ [MIT](./LICENSE)
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 图片解析结果的本地磁盘缓存。
3
+ *
4
+ * 设计决策:
5
+ * - Key:图片字节内容的 sha256(内容寻址,天然去重,跨来源——粘贴/工具产图——同一张图只解析一次);
6
+ * - 作用域:全局共享,不按 session/project 隔离;
7
+ * - TTL:7 天,惰性清理(读写时顺带清理过期条目,不需要常驻后台任务);
8
+ * - 存储位置:`~/.claude/cc-vision-hook/cache/`,每个条目一对文件(`.json` 元数据 + `.bin` 原图字节)。
9
+ */
10
+ export interface CacheEntry {
11
+ /** 缓存条目 ID(等于内容 sha256 的前缀,作为对外暴露的 image_id)。 */
12
+ imageId: string;
13
+ mimeType: string;
14
+ createdAt: string;
15
+ expiresAt: string;
16
+ /** 产生这张图片的工具名(如 "Read"、"Bash"、"mcp__xxx__yyy"),粘贴场景下为 "paste"。 */
17
+ sourceTool: string;
18
+ description: string;
19
+ }
20
+ /**
21
+ * 对图片原始字节计算内容 hash,作为缓存 key(image_id)。
22
+ *
23
+ * @param bytes - 图片的原始二进制数据
24
+ * @returns 64 位十六进制 sha256 摘要,取前 16 位加 `img_` 前缀作为短 ID
25
+ */
26
+ export declare const computeImageId: (bytes: Buffer) => string;
27
+ /**
28
+ * 按 image_id 查询缓存条目(元数据),命中且未过期才返回。
29
+ *
30
+ * @param imageId - 缓存条目 ID
31
+ * @returns 命中的元数据,未命中或已过期返回 null
32
+ */
33
+ export declare const getCacheEntry: (imageId: string) => Promise<CacheEntry | null>;
34
+ /**
35
+ * 读取缓存条目对应的原图字节(用于 `vision_ask` 追问时重新提问)。
36
+ *
37
+ * @param imageId - 缓存条目 ID
38
+ * @returns 原图二进制数据,不存在时返回 null
39
+ */
40
+ export declare const getCachedImageBytes: (imageId: string) => Promise<Buffer | null>;
41
+ /**
42
+ * 按内容 hash 查找是否已有缓存(用于"命中即跳过视觉解析调用")。
43
+ *
44
+ * @param bytes - 图片原始字节
45
+ * @returns 命中的缓存条目,未命中返回 null
46
+ */
47
+ export declare const findCacheByBytes: (bytes: Buffer) => Promise<CacheEntry | null>;
48
+ /**
49
+ * 写入一条新的缓存条目:落盘原图字节 + 元数据(含描述文本),并计算好 TTL 过期时间。
50
+ *
51
+ * @param bytes - 图片原始字节
52
+ * @param mimeType - 图片 MIME 类型
53
+ * @param sourceTool - 产生这张图片的工具名
54
+ * @param description - 视觉模型给出的描述文本
55
+ * @param ttlDays - 缓存有效期(天),来自用户配置
56
+ * @returns 写入后的缓存条目(含 image_id,供 additionalContext 引用)
57
+ */
58
+ export declare const putCacheEntry: (bytes: Buffer, mimeType: string, sourceTool: string, description: string, ttlDays: number) => Promise<CacheEntry>;
59
+ /**
60
+ * 统计当前缓存条目数量,用于 `cvh status` 展示。
61
+ *
62
+ * @returns 未过期的缓存条目数量
63
+ */
64
+ export declare const countCacheEntries: () => Promise<number>;
65
+ /**
66
+ * 获取缓存目录占用的磁盘大小(字节),用于 `cvh status`/`doctor` 展示。
67
+ *
68
+ * @returns 缓存目录总大小(字节),目录不存在时返回 0
69
+ */
70
+ export declare const getCacheDiskUsage: () => Promise<number>;
package/dist/cache.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 图片解析结果的本地磁盘缓存。
3
+ *
4
+ * 设计决策:
5
+ * - Key:图片字节内容的 sha256(内容寻址,天然去重,跨来源——粘贴/工具产图——同一张图只解析一次);
6
+ * - 作用域:全局共享,不按 session/project 隔离;
7
+ * - TTL:7 天,惰性清理(读写时顺带清理过期条目,不需要常驻后台任务);
8
+ * - 存储位置:`~/.claude/cc-vision-hook/cache/`,每个条目一对文件(`.json` 元数据 + `.bin` 原图字节)。
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ import { mkdir, readFile, writeFile, readdir, stat, unlink } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { getCvhHomeDir } from "./config.js";
14
+ const getCacheDir = () => join(getCvhHomeDir(), "cache");
15
+ const metaPath = (imageId) => join(getCacheDir(), `${imageId}.json`);
16
+ const binPath = (imageId) => join(getCacheDir(), `${imageId}.bin`);
17
+ /**
18
+ * 对图片原始字节计算内容 hash,作为缓存 key(image_id)。
19
+ *
20
+ * @param bytes - 图片的原始二进制数据
21
+ * @returns 64 位十六进制 sha256 摘要,取前 16 位加 `img_` 前缀作为短 ID
22
+ */
23
+ export const computeImageId = (bytes) => {
24
+ const full = createHash("sha256").update(bytes).digest("hex");
25
+ return `img_${full.slice(0, 16)}`;
26
+ };
27
+ /**
28
+ * 按 TTL 惰性清理过期缓存条目。每次读写缓存时调用一次,成本很低(只扫元数据文件)。
29
+ *
30
+ * @param ttlDays - 配置里的 TTL 天数,仅用于新建条目时计算 expiresAt,本函数只看条目自带的 expiresAt
31
+ */
32
+ const cleanupExpired = async () => {
33
+ const dir = getCacheDir();
34
+ let files;
35
+ try {
36
+ files = await readdir(dir);
37
+ }
38
+ catch {
39
+ return; // 目录还不存在,无需清理
40
+ }
41
+ const now = Date.now();
42
+ for (const file of files) {
43
+ if (!file.endsWith(".json"))
44
+ continue;
45
+ const imageId = file.slice(0, -".json".length);
46
+ try {
47
+ const raw = await readFile(join(dir, file), "utf8");
48
+ const entry = JSON.parse(raw);
49
+ if (new Date(entry.expiresAt).getTime() < now) {
50
+ await unlink(metaPath(imageId)).catch(() => { });
51
+ await unlink(binPath(imageId)).catch(() => { });
52
+ }
53
+ }
54
+ catch {
55
+ // 元数据损坏,直接按过期处理,避免脏数据常驻
56
+ await unlink(join(dir, file)).catch(() => { });
57
+ }
58
+ }
59
+ };
60
+ /**
61
+ * 按 image_id 查询缓存条目(元数据),命中且未过期才返回。
62
+ *
63
+ * @param imageId - 缓存条目 ID
64
+ * @returns 命中的元数据,未命中或已过期返回 null
65
+ */
66
+ export const getCacheEntry = async (imageId) => {
67
+ try {
68
+ const raw = await readFile(metaPath(imageId), "utf8");
69
+ const entry = JSON.parse(raw);
70
+ if (new Date(entry.expiresAt).getTime() < Date.now())
71
+ return null;
72
+ return entry;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ };
78
+ /**
79
+ * 读取缓存条目对应的原图字节(用于 `vision_ask` 追问时重新提问)。
80
+ *
81
+ * @param imageId - 缓存条目 ID
82
+ * @returns 原图二进制数据,不存在时返回 null
83
+ */
84
+ export const getCachedImageBytes = async (imageId) => {
85
+ try {
86
+ return await readFile(binPath(imageId));
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ };
92
+ /**
93
+ * 按内容 hash 查找是否已有缓存(用于"命中即跳过视觉解析调用")。
94
+ *
95
+ * @param bytes - 图片原始字节
96
+ * @returns 命中的缓存条目,未命中返回 null
97
+ */
98
+ export const findCacheByBytes = async (bytes) => {
99
+ await cleanupExpired();
100
+ return getCacheEntry(computeImageId(bytes));
101
+ };
102
+ /**
103
+ * 写入一条新的缓存条目:落盘原图字节 + 元数据(含描述文本),并计算好 TTL 过期时间。
104
+ *
105
+ * @param bytes - 图片原始字节
106
+ * @param mimeType - 图片 MIME 类型
107
+ * @param sourceTool - 产生这张图片的工具名
108
+ * @param description - 视觉模型给出的描述文本
109
+ * @param ttlDays - 缓存有效期(天),来自用户配置
110
+ * @returns 写入后的缓存条目(含 image_id,供 additionalContext 引用)
111
+ */
112
+ export const putCacheEntry = async (bytes, mimeType, sourceTool, description, ttlDays) => {
113
+ await mkdir(getCacheDir(), { recursive: true });
114
+ const imageId = computeImageId(bytes);
115
+ const now = new Date();
116
+ const expiresAt = new Date(now.getTime() + ttlDays * 24 * 60 * 60 * 1000);
117
+ const entry = {
118
+ imageId,
119
+ mimeType,
120
+ createdAt: now.toISOString(),
121
+ expiresAt: expiresAt.toISOString(),
122
+ sourceTool,
123
+ description,
124
+ };
125
+ await writeFile(binPath(imageId), bytes);
126
+ await writeFile(metaPath(imageId), JSON.stringify(entry, null, 2));
127
+ return entry;
128
+ };
129
+ /**
130
+ * 统计当前缓存条目数量,用于 `cvh status` 展示。
131
+ *
132
+ * @returns 未过期的缓存条目数量
133
+ */
134
+ export const countCacheEntries = async () => {
135
+ await cleanupExpired();
136
+ try {
137
+ const files = await readdir(getCacheDir());
138
+ return files.filter((f) => f.endsWith(".json")).length;
139
+ }
140
+ catch {
141
+ return 0;
142
+ }
143
+ };
144
+ /**
145
+ * 获取缓存目录占用的磁盘大小(字节),用于 `cvh status`/`doctor` 展示。
146
+ *
147
+ * @returns 缓存目录总大小(字节),目录不存在时返回 0
148
+ */
149
+ export const getCacheDiskUsage = async () => {
150
+ let total = 0;
151
+ try {
152
+ const dir = getCacheDir();
153
+ const files = await readdir(dir);
154
+ for (const file of files) {
155
+ const s = await stat(join(dir, file));
156
+ total += s.size;
157
+ }
158
+ }
159
+ catch {
160
+ return 0;
161
+ }
162
+ return total;
163
+ };
164
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 对 `~/.claude/settings.json` 里 Hook 注册条目的幂等读写。
3
+ *
4
+ * 幂等标识:用固定的 command 字符串("cvh hook user-prompt-submit"/"cvh hook post-tool-use")
5
+ * 作为唯一标识——install 前先检查是否已存在同样的条目,避免重复注册;uninstall 时精确按这个
6
+ * 字符串过滤移除,不误删用户自己配置的其他 Hook。
7
+ */
8
+ export declare const USER_PROMPT_SUBMIT_COMMAND = "cvh hook user-prompt-submit";
9
+ export declare const POST_TOOL_USE_COMMAND = "cvh hook post-tool-use";
10
+ /**
11
+ * 幂等地在 `~/.claude/settings.json` 中注册 `UserPromptSubmit` 和 `PostToolUse` 两个 Hook。
12
+ * 已存在同样 command 的条目时跳过,不重复添加;不会动用户已有的其他 Hook 配置。
13
+ *
14
+ * @returns 本次实际新增的 Hook 数量(0 表示已经是安装好的状态,重复执行安全)
15
+ */
16
+ export declare const installHooks: () => Promise<number>;
17
+ /**
18
+ * 精确移除 cvh 注册的两个 Hook 条目,不影响用户配置的其他 Hook。
19
+ *
20
+ * @returns 本次实际移除的 Hook 数量
21
+ */
22
+ export declare const uninstallHooks: () => Promise<number>;
23
+ /**
24
+ * 检查两个 Hook 是否都已注册(用于 `cvh status`/`doctor`)。
25
+ *
26
+ * @returns 分别表示两个 Hook 是否已注册
27
+ */
28
+ export declare const checkHooksInstalled: () => Promise<{
29
+ userPromptSubmit: boolean;
30
+ postToolUse: boolean;
31
+ }>;