@yanickxia/agent-vision 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.
- package/LICENSE +21 -0
- package/NOTICE +8 -0
- package/README.md +188 -0
- package/dist/analyze.d.ts +14 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +32 -0
- package/dist/analyze.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +89 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +92 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/image.d.ts +9 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +108 -0
- package/dist/image.js.map +1 -0
- package/dist/network.d.ts +18 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +169 -0
- package/dist/network.js.map +1 -0
- package/dist/provider.d.ts +16 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +115 -0
- package/dist/provider.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +62 -0
- package/dist/server.js.map +1 -0
- package/dist/video.d.ts +13 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +200 -0
- package/dist/video.js.map +1 -0
- package/package.json +59 -0
- package/skills/agent-vision/SKILL.md +64 -0
- package/skills/agent-vision/references/configuration.md +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yanick Xia
|
|
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/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
agent-vision is an independent implementation.
|
|
2
|
+
|
|
3
|
+
It uses the Model Context Protocol SDK and an ffmpeg binary distributed by the
|
|
4
|
+
@ffmpeg-installer/ffmpeg npm package. Those dependencies and platform binaries
|
|
5
|
+
retain their own licenses.
|
|
6
|
+
|
|
7
|
+
Design references and inspiration are listed in README.md. No GPL-licensed
|
|
8
|
+
source code from farhanic017/vision-tool is included in this project.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# agent-vision
|
|
2
|
+
|
|
3
|
+
一个小而直接的视觉 MCP Server + Agent Skill:让纯文本 Agent 通过一个
|
|
4
|
+
OpenAI-compatible 视觉模型理解图片和视频。
|
|
5
|
+
|
|
6
|
+
- 纯 Node.js / TypeScript,`npx` 自动下载运行
|
|
7
|
+
- MCP 工具:`analyze_image`、`analyze_video`
|
|
8
|
+
- Agent Skills 标准目录:`skills/agent-vision/`
|
|
9
|
+
- 图片:本地路径、HTTP(S) URL、Data URI
|
|
10
|
+
- 视频:本地路径或 URL,自动下载 npm 内的 ffmpeg 并均匀抽帧
|
|
11
|
+
- 单一 provider,无免费模型列表、fallback 竞速、watchdog 或透明代理
|
|
12
|
+
|
|
13
|
+
## 为什么做这个项目
|
|
14
|
+
|
|
15
|
+
参考项目各有优点,但组合起来偏重:
|
|
16
|
+
|
|
17
|
+
- [luma-mcp](https://github.com/JochenYang/luma-mcp):Node.js、npx 和图片预处理体验好,但主要面向单图。
|
|
18
|
+
- [deepseek-vision-mcp](https://github.com/JunHua-ECJTU/deepseek-vision-mcp):MCP 执行层与 Skill 决策层分离得很清楚,但依赖 Python,且本地视频受 provider 限制。
|
|
19
|
+
- [vision-tool](https://github.com/farhanic017/vision-tool):支持视频抽帧,但包含大量 provider 探测、并行 fallback 和安装逻辑,并采用 GPL-3.0。
|
|
20
|
+
- [agent-vision-toolkit](https://github.com/Anionex/agent-vision-toolkit):Skill 和任务工作流很强,但核心是多组 Python/Shell CLI,不是 MCP 视频服务。
|
|
21
|
+
|
|
22
|
+
`agent-vision` 只保留一条链路:**MCP/CLI → 图片或视频帧 → 你配置的视觉 API → 文本**。
|
|
23
|
+
本仓库是独立实现,没有复制 `vision-tool` 的 GPL 源码。
|
|
24
|
+
|
|
25
|
+
## 要求
|
|
26
|
+
|
|
27
|
+
- Node.js 20+
|
|
28
|
+
- 一个支持 `/chat/completions` 和 `image_url` 的 OpenAI-compatible 视觉模型
|
|
29
|
+
|
|
30
|
+
ffmpeg 由 `@ffmpeg-installer/ffmpeg` 按平台自动安装,无需另行安装。
|
|
31
|
+
|
|
32
|
+
## 配置
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export AGENT_VISION_BASE_URL="https://your-endpoint.example/v1"
|
|
36
|
+
export AGENT_VISION_API_KEY="your-key"
|
|
37
|
+
export AGENT_VISION_MODEL="your-vision-model"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
本地无鉴权端点可以不设置 API key。变量优先级:
|
|
41
|
+
|
|
42
|
+
| 设置 | 兼容回退 |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `AGENT_VISION_API_KEY` | `VISION_API_KEY` → `OPENAI_API_KEY` |
|
|
45
|
+
| `AGENT_VISION_BASE_URL` | `VISION_BASE_URL` → `OPENAI_BASE_URL` |
|
|
46
|
+
| `AGENT_VISION_MODEL` | `VISION_MODEL` → `OPENAI_MODEL` |
|
|
47
|
+
|
|
48
|
+
可选变量:
|
|
49
|
+
|
|
50
|
+
| 变量 | 默认值 | 说明 |
|
|
51
|
+
|---|---:|---|
|
|
52
|
+
| `AGENT_VISION_MAX_TOKENS` | `4096` | 最大输出 tokens |
|
|
53
|
+
| `AGENT_VISION_TIMEOUT_MS` | `120000` | 下载和 API 超时 |
|
|
54
|
+
| `AGENT_VISION_MAX_IMAGE_MB` | `20` | 图片大小上限 |
|
|
55
|
+
| `AGENT_VISION_MAX_VIDEO_MB` | `200` | 视频大小上限 |
|
|
56
|
+
| `AGENT_VISION_HEADERS` | `{}` | 额外请求头 JSON |
|
|
57
|
+
| `AGENT_VISION_ALLOW_PRIVATE_URLS` | `false` | 允许私网图片/视频来源 URL |
|
|
58
|
+
| `AGENT_VISION_FFMPEG_PATH` | 自动发现 | 指定 ffmpeg 可执行文件;默认先用 PATH,再用 npm 内置版本 |
|
|
59
|
+
|
|
60
|
+
## MCP 安装
|
|
61
|
+
|
|
62
|
+
### npm(发布后推荐)
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"agent-vision": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["-y", "@yanickxia/agent-vision"],
|
|
70
|
+
"env": {
|
|
71
|
+
"AGENT_VISION_BASE_URL": "https://your-endpoint.example/v1",
|
|
72
|
+
"AGENT_VISION_API_KEY": "your-key",
|
|
73
|
+
"AGENT_VISION_MODEL": "your-vision-model"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 直接从 GitHub 运行
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"agent-vision": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "github:yanickxia/agent-vision"],
|
|
88
|
+
"env": {
|
|
89
|
+
"AGENT_VISION_BASE_URL": "https://your-endpoint.example/v1",
|
|
90
|
+
"AGENT_VISION_API_KEY": "your-key",
|
|
91
|
+
"AGENT_VISION_MODEL": "your-vision-model"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Claude Desktop、Claude Code、Cursor、Cline 等使用上面的标准
|
|
99
|
+
`mcpServers` 格式。
|
|
100
|
+
|
|
101
|
+
### OpenCode
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
{
|
|
105
|
+
"mcp": {
|
|
106
|
+
"agent-vision": {
|
|
107
|
+
"type": "local",
|
|
108
|
+
"command": ["npx", "-y", "@yanickxia/agent-vision"],
|
|
109
|
+
"enabled": true,
|
|
110
|
+
"environment": {
|
|
111
|
+
"AGENT_VISION_BASE_URL": "https://your-endpoint.example/v1",
|
|
112
|
+
"AGENT_VISION_API_KEY": "your-key",
|
|
113
|
+
"AGENT_VISION_MODEL": "your-vision-model"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Agent Skill
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx skills add yanickxia/agent-vision --skill agent-vision -g -y
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
也可以复制 `skills/agent-vision/` 到 Agent 的 skills 目录。Skill 负责告诉
|
|
127
|
+
Agent 何时调用图片/视频工具、如何传参和如何处理失败;MCP 负责实际读取文件、
|
|
128
|
+
抽帧和请求模型。
|
|
129
|
+
|
|
130
|
+
## CLI
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx -y @yanickxia/agent-vision image ./screenshot.png \
|
|
134
|
+
--prompt "读取报错并给出可能原因"
|
|
135
|
+
|
|
136
|
+
npx -y @yanickxia/agent-vision video ./demo.mp4 \
|
|
137
|
+
--prompt "按时间顺序总结 UI 操作" --frames 8
|
|
138
|
+
|
|
139
|
+
npx -y @yanickxia/agent-vision doctor
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## MCP 工具
|
|
143
|
+
|
|
144
|
+
### `analyze_image`
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"source": "/absolute/path/to/image.png",
|
|
149
|
+
"prompt": "这个页面有哪些可用性问题?"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
支持 JPEG、PNG、WebP、GIF、BMP。GIF 是否能体现动画取决于视觉模型;需要稳定的
|
|
154
|
+
时间线分析时请转为视频并使用 `analyze_video`。
|
|
155
|
+
|
|
156
|
+
### `analyze_video`
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"source": "/absolute/path/to/video.mp4",
|
|
161
|
+
"prompt": "概括操作流程和关键变化",
|
|
162
|
+
"max_frames": 8
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`max_frames` 范围 1–16,默认 8。实现会均匀抽取 JPEG 帧,并带时间点一起发给
|
|
167
|
+
模型。它不是逐帧转写,快速变化可能被漏掉。
|
|
168
|
+
|
|
169
|
+
## 隐私与安全
|
|
170
|
+
|
|
171
|
+
- 图片会发送到你配置的视觉 API。
|
|
172
|
+
- 视频不会整体发给模型;远程视频先下载到本地临时目录,然后只发送抽出的 JPEG 帧。
|
|
173
|
+
- 临时目录在成功或失败后都会删除。
|
|
174
|
+
- 用户提供的远程图片/视频 URL 默认拒绝 localhost、私网、链路本地和云元数据地址。
|
|
175
|
+
- stdio 模式不向 stdout 写日志,避免破坏 MCP 协议。
|
|
176
|
+
|
|
177
|
+
## 开发
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm install
|
|
181
|
+
npm run typecheck
|
|
182
|
+
npm test
|
|
183
|
+
npm pack --dry-run
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT。第三方依赖保留各自许可证,见 `NOTICE`。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { VisionConfig } from "./config.js";
|
|
2
|
+
import { type VisionCompletion } from "./provider.js";
|
|
3
|
+
export declare function analyzeImage(options: {
|
|
4
|
+
source: string;
|
|
5
|
+
prompt?: string;
|
|
6
|
+
config?: VisionConfig;
|
|
7
|
+
}): Promise<VisionCompletion>;
|
|
8
|
+
export declare function analyzeVideo(options: {
|
|
9
|
+
source: string;
|
|
10
|
+
prompt?: string;
|
|
11
|
+
maxFrames?: number;
|
|
12
|
+
config?: VisionConfig;
|
|
13
|
+
}): Promise<VisionCompletion>;
|
|
14
|
+
//# sourceMappingURL=analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAS/E,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAQ5B;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAe5B"}
|
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { loadImage } from "./image.js";
|
|
3
|
+
import { requestVisionCompletion } from "./provider.js";
|
|
4
|
+
import { extractVideoFrames } from "./video.js";
|
|
5
|
+
const DEFAULT_IMAGE_PROMPT = "Describe this image accurately. Include visible text, layout, objects, colors, and details relevant to understanding it. Do not invent details that are not visible.";
|
|
6
|
+
const DEFAULT_VIDEO_PROMPT = "Analyze these evenly sampled video frames as one timeline. Describe the sequence of events, scene or UI changes, actions, and visible text. Distinguish observations from uncertainty.";
|
|
7
|
+
export async function analyzeImage(options) {
|
|
8
|
+
const config = options.config ?? loadConfig();
|
|
9
|
+
const image = await loadImage(options.source, config);
|
|
10
|
+
return requestVisionCompletion({
|
|
11
|
+
config,
|
|
12
|
+
prompt: options.prompt?.trim() || DEFAULT_IMAGE_PROMPT,
|
|
13
|
+
images: [{ dataUrl: image.dataUrl, label: image.label }],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export async function analyzeVideo(options) {
|
|
17
|
+
const config = options.config ?? loadConfig();
|
|
18
|
+
const frames = await extractVideoFrames({
|
|
19
|
+
source: options.source,
|
|
20
|
+
maxFrames: options.maxFrames ?? 8,
|
|
21
|
+
config,
|
|
22
|
+
});
|
|
23
|
+
const userPrompt = options.prompt?.trim();
|
|
24
|
+
return requestVisionCompletion({
|
|
25
|
+
config,
|
|
26
|
+
prompt: userPrompt
|
|
27
|
+
? `${DEFAULT_VIDEO_PROMPT}\n\nUser question: ${userPrompt}`
|
|
28
|
+
: DEFAULT_VIDEO_PROMPT,
|
|
29
|
+
images: frames,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,uBAAuB,EAAyB,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,oBAAoB,GACxB,sKAAsK,CAAC;AAEzK,MAAM,oBAAoB,GACxB,wLAAwL,CAAC;AAE3L,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAIlC;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtD,OAAO,uBAAuB,CAAC;QAC7B,MAAM;QACN,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,oBAAoB;QACtD,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KACzD,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAKlC;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC;QACtC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,CAAC;QACjC,MAAM;KACP,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1C,OAAO,uBAAuB,CAAC;QAC7B,MAAM;QACN,MAAM,EAAE,UAAU;YAChB,CAAC,CAAC,GAAG,oBAAoB,sBAAsB,UAAU,EAAE;YAC3D,CAAC,CAAC,oBAAoB;QACxB,MAAM,EAAE,MAAM;KACf,CAAC,CAAC;AACL,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { analyzeImage, analyzeVideo } from "./analyze.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { errorMessage } from "./errors.js";
|
|
5
|
+
import { SERVER_VERSION, startStdioServer } from "./server.js";
|
|
6
|
+
import { ensureFfmpeg } from "./video.js";
|
|
7
|
+
const HELP = `agent-vision ${SERVER_VERSION}
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
agent-vision Start the MCP stdio server
|
|
11
|
+
agent-vision image <source> [options]
|
|
12
|
+
agent-vision video <source> [options]
|
|
13
|
+
agent-vision doctor
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-p, --prompt <text> Question or focus for the analysis
|
|
17
|
+
--frames <1-16> Video frames to sample (default: 8)
|
|
18
|
+
-h, --help Show help
|
|
19
|
+
-v, --version Show version
|
|
20
|
+
`;
|
|
21
|
+
function option(args, long, short) {
|
|
22
|
+
const prefix = `${long}=`;
|
|
23
|
+
const inline = args.find((arg) => arg.startsWith(prefix));
|
|
24
|
+
if (inline)
|
|
25
|
+
return inline.slice(prefix.length);
|
|
26
|
+
const index = args.findIndex((arg) => arg === long || (short && arg === short));
|
|
27
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
28
|
+
}
|
|
29
|
+
async function runCli(args) {
|
|
30
|
+
const command = args[0];
|
|
31
|
+
if (!command) {
|
|
32
|
+
await startStdioServer();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
36
|
+
process.stdout.write(HELP);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (command === "--version" || command === "-v") {
|
|
40
|
+
process.stdout.write(`${SERVER_VERSION}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (command === "doctor") {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
let ffmpeg = "unavailable";
|
|
46
|
+
try {
|
|
47
|
+
ffmpeg = await ensureFfmpeg();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Report below without failing before the full doctor output is printed.
|
|
51
|
+
}
|
|
52
|
+
const report = [
|
|
53
|
+
`model: ${config.model || "missing"}`,
|
|
54
|
+
`base URL: ${config.baseUrl}`,
|
|
55
|
+
`API key: ${config.apiKey ? "configured" : "not configured (allowed for local endpoints)"}`,
|
|
56
|
+
`ffmpeg: ${ffmpeg}`,
|
|
57
|
+
];
|
|
58
|
+
process.stdout.write(`${report.join("\n")}\n`);
|
|
59
|
+
if (!config.model || ffmpeg === "unavailable")
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (command !== "image" && command !== "video") {
|
|
64
|
+
throw new Error(`Unknown command: ${command}\n\n${HELP}`);
|
|
65
|
+
}
|
|
66
|
+
const source = args[1];
|
|
67
|
+
if (!source || source.startsWith("-")) {
|
|
68
|
+
throw new Error(`${command} requires a source path or URL`);
|
|
69
|
+
}
|
|
70
|
+
const prompt = option(args.slice(2), "--prompt", "-p");
|
|
71
|
+
if (command === "image") {
|
|
72
|
+
const result = await analyzeImage({ source, ...(prompt ? { prompt } : {}) });
|
|
73
|
+
process.stdout.write(`${result.text}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const framesRaw = option(args.slice(2), "--frames");
|
|
77
|
+
const maxFrames = framesRaw ? Number(framesRaw) : undefined;
|
|
78
|
+
const result = await analyzeVideo({
|
|
79
|
+
source,
|
|
80
|
+
...(prompt ? { prompt } : {}),
|
|
81
|
+
...(maxFrames !== undefined ? { maxFrames } : {}),
|
|
82
|
+
});
|
|
83
|
+
process.stdout.write(`${result.text}\n`);
|
|
84
|
+
}
|
|
85
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
86
|
+
process.stderr.write(`agent-vision: ${errorMessage(error)}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface VisionConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
model: string;
|
|
5
|
+
maxTokens: number;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
maxImageBytes: number;
|
|
9
|
+
maxVideoBytes: number;
|
|
10
|
+
allowPrivateUrls: boolean;
|
|
11
|
+
}
|
|
12
|
+
type Environment = Record<string, string | undefined>;
|
|
13
|
+
export declare function loadConfig(env?: Environment): VisionConfig;
|
|
14
|
+
export declare function requireModel(config: VisionConfig): void;
|
|
15
|
+
export declare function chatCompletionsUrl(baseUrl: string): string;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAgDtD,wBAAgB,UAAU,CAAC,GAAG,GAAE,WAAyB,GAAG,YAAY,CAiCvE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAOvD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqB1D"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { VisionError } from "./errors.js";
|
|
2
|
+
function firstNonEmpty(env, names) {
|
|
3
|
+
for (const name of names) {
|
|
4
|
+
const value = env[name]?.trim();
|
|
5
|
+
if (value)
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
function positiveInteger(env, name, fallback) {
|
|
11
|
+
const raw = env[name]?.trim();
|
|
12
|
+
if (!raw)
|
|
13
|
+
return fallback;
|
|
14
|
+
const value = Number(raw);
|
|
15
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
16
|
+
throw new VisionError("CONFIG_ERROR", `${name} must be a positive integer`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function parseHeaders(raw) {
|
|
21
|
+
if (!raw?.trim())
|
|
22
|
+
return {};
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(raw);
|
|
25
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
26
|
+
throw new Error("expected a JSON object");
|
|
27
|
+
}
|
|
28
|
+
const headers = {};
|
|
29
|
+
for (const [key, headerValue] of Object.entries(value)) {
|
|
30
|
+
if (typeof headerValue !== "string") {
|
|
31
|
+
throw new Error(`header ${key} must be a string`);
|
|
32
|
+
}
|
|
33
|
+
headers[key] = headerValue;
|
|
34
|
+
}
|
|
35
|
+
return headers;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
39
|
+
throw new VisionError("CONFIG_ERROR", `AGENT_VISION_HEADERS must be a JSON object of string values: ${detail}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function loadConfig(env = process.env) {
|
|
43
|
+
const apiKey = firstNonEmpty(env, [
|
|
44
|
+
"AGENT_VISION_API_KEY",
|
|
45
|
+
"VISION_API_KEY",
|
|
46
|
+
"OPENAI_API_KEY",
|
|
47
|
+
]);
|
|
48
|
+
const baseUrl = firstNonEmpty(env, [
|
|
49
|
+
"AGENT_VISION_BASE_URL",
|
|
50
|
+
"VISION_BASE_URL",
|
|
51
|
+
"OPENAI_BASE_URL",
|
|
52
|
+
]) ?? "https://api.openai.com/v1";
|
|
53
|
+
const model = firstNonEmpty(env, [
|
|
54
|
+
"AGENT_VISION_MODEL",
|
|
55
|
+
"VISION_MODEL",
|
|
56
|
+
"OPENAI_MODEL",
|
|
57
|
+
]) ?? "";
|
|
58
|
+
return {
|
|
59
|
+
...(apiKey ? { apiKey } : {}),
|
|
60
|
+
baseUrl,
|
|
61
|
+
model,
|
|
62
|
+
maxTokens: positiveInteger(env, "AGENT_VISION_MAX_TOKENS", 4096),
|
|
63
|
+
timeoutMs: positiveInteger(env, "AGENT_VISION_TIMEOUT_MS", 120_000),
|
|
64
|
+
headers: parseHeaders(env.AGENT_VISION_HEADERS),
|
|
65
|
+
maxImageBytes: positiveInteger(env, "AGENT_VISION_MAX_IMAGE_MB", 20) * 1024 * 1024,
|
|
66
|
+
maxVideoBytes: positiveInteger(env, "AGENT_VISION_MAX_VIDEO_MB", 200) * 1024 * 1024,
|
|
67
|
+
allowPrivateUrls: env.AGENT_VISION_ALLOW_PRIVATE_URLS?.trim().toLowerCase() === "true",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function requireModel(config) {
|
|
71
|
+
if (!config.model) {
|
|
72
|
+
throw new VisionError("CONFIG_ERROR", "Set AGENT_VISION_MODEL (or VISION_MODEL / OPENAI_MODEL)");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function chatCompletionsUrl(baseUrl) {
|
|
76
|
+
let url;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(baseUrl);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new VisionError("CONFIG_ERROR", `Invalid AGENT_VISION_BASE_URL: ${baseUrl}`);
|
|
82
|
+
}
|
|
83
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
84
|
+
throw new VisionError("CONFIG_ERROR", "AGENT_VISION_BASE_URL must use http or https");
|
|
85
|
+
}
|
|
86
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
87
|
+
if (!path.endsWith("/chat/completions")) {
|
|
88
|
+
url.pathname = `${path}/chat/completions`;
|
|
89
|
+
}
|
|
90
|
+
return url.toString();
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKlE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQnD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class VisionError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "VisionError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function errorMessage(error) {
|
|
10
|
+
if (error instanceof VisionError) {
|
|
11
|
+
return `${error.code}: ${error.message}`;
|
|
12
|
+
}
|
|
13
|
+
if (error instanceof Error) {
|
|
14
|
+
return error.message;
|
|
15
|
+
}
|
|
16
|
+
return String(error);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,OAAe,EAAE,OAAsB;QAC/D,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QACjC,OAAO,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;IAC3C,CAAC;IACD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { VisionConfig } from "./config.js";
|
|
2
|
+
export interface LoadedImage {
|
|
3
|
+
dataUrl: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
bytes: number;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function loadImage(source: string, config: VisionConfig): Promise<LoadedImage>;
|
|
9
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AA8ED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,WAAW,CAAC,CAqCtB"}
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { VisionError } from "./errors.js";
|
|
5
|
+
import { fetchRemoteBuffer } from "./network.js";
|
|
6
|
+
const SUPPORTED_MIME_TYPES = new Set([
|
|
7
|
+
"image/jpeg",
|
|
8
|
+
"image/png",
|
|
9
|
+
"image/webp",
|
|
10
|
+
"image/gif",
|
|
11
|
+
"image/bmp",
|
|
12
|
+
]);
|
|
13
|
+
function mimeFromMagic(buffer) {
|
|
14
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
15
|
+
return "image/jpeg";
|
|
16
|
+
}
|
|
17
|
+
if (buffer.length >= 8 &&
|
|
18
|
+
buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
19
|
+
return "image/png";
|
|
20
|
+
}
|
|
21
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
22
|
+
return "image/webp";
|
|
23
|
+
}
|
|
24
|
+
if (buffer.length >= 6 && /^GIF8[79]a$/.test(buffer.toString("ascii", 0, 6))) {
|
|
25
|
+
return "image/gif";
|
|
26
|
+
}
|
|
27
|
+
if (buffer.length >= 2 && buffer.toString("ascii", 0, 2) === "BM") {
|
|
28
|
+
return "image/bmp";
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function expandLocalPath(source) {
|
|
33
|
+
const withoutAt = source.startsWith("@") ? source.slice(1) : source;
|
|
34
|
+
if (withoutAt === "~")
|
|
35
|
+
return homedir();
|
|
36
|
+
if (withoutAt.startsWith("~/")) {
|
|
37
|
+
return path.join(homedir(), withoutAt.slice(2));
|
|
38
|
+
}
|
|
39
|
+
return path.resolve(withoutAt);
|
|
40
|
+
}
|
|
41
|
+
function toLoadedImage(buffer, label, declaredMime) {
|
|
42
|
+
const detectedMime = mimeFromMagic(buffer);
|
|
43
|
+
const normalizedDeclared = declaredMime?.toLowerCase();
|
|
44
|
+
const mimeType = detectedMime ??
|
|
45
|
+
(normalizedDeclared && SUPPORTED_MIME_TYPES.has(normalizedDeclared)
|
|
46
|
+
? normalizedDeclared
|
|
47
|
+
: undefined);
|
|
48
|
+
if (!mimeType) {
|
|
49
|
+
throw new VisionError("UNSUPPORTED_IMAGE", "Unsupported or invalid image. Supported formats: JPEG, PNG, WebP, GIF, BMP");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
dataUrl: `data:${mimeType};base64,${buffer.toString("base64")}`,
|
|
53
|
+
mimeType,
|
|
54
|
+
bytes: buffer.length,
|
|
55
|
+
label,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function parseDataUri(source, maxBytes) {
|
|
59
|
+
const match = source.match(/^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]+)$/i);
|
|
60
|
+
if (!match?.[1] || !match[2]) {
|
|
61
|
+
throw new VisionError("INVALID_SOURCE", "Invalid base64 image data URI");
|
|
62
|
+
}
|
|
63
|
+
const compact = match[2].replace(/\s/g, "");
|
|
64
|
+
if (compact.length % 4 !== 0) {
|
|
65
|
+
throw new VisionError("INVALID_SOURCE", "Invalid base64 image data URI");
|
|
66
|
+
}
|
|
67
|
+
const buffer = Buffer.from(compact, "base64");
|
|
68
|
+
if (buffer.length > maxBytes) {
|
|
69
|
+
throw new VisionError("SOURCE_TOO_LARGE", "Image data URI exceeds the configured limit");
|
|
70
|
+
}
|
|
71
|
+
return toLoadedImage(buffer, "data-uri", match[1]);
|
|
72
|
+
}
|
|
73
|
+
export async function loadImage(source, config) {
|
|
74
|
+
const trimmed = source.trim();
|
|
75
|
+
if (!trimmed) {
|
|
76
|
+
throw new VisionError("INVALID_SOURCE", "Image source cannot be empty");
|
|
77
|
+
}
|
|
78
|
+
if (trimmed.startsWith("data:")) {
|
|
79
|
+
return parseDataUri(trimmed, config.maxImageBytes);
|
|
80
|
+
}
|
|
81
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
82
|
+
const remote = await fetchRemoteBuffer({
|
|
83
|
+
url: trimmed,
|
|
84
|
+
maxBytes: config.maxImageBytes,
|
|
85
|
+
timeoutMs: config.timeoutMs,
|
|
86
|
+
allowPrivateUrls: config.allowPrivateUrls,
|
|
87
|
+
});
|
|
88
|
+
return toLoadedImage(remote.buffer, trimmed, remote.contentType);
|
|
89
|
+
}
|
|
90
|
+
const filePath = expandLocalPath(trimmed);
|
|
91
|
+
let fileStat;
|
|
92
|
+
try {
|
|
93
|
+
fileStat = await stat(filePath);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new VisionError("FILE_NOT_FOUND", `Image file not found: ${filePath}`, {
|
|
97
|
+
cause: error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!fileStat.isFile()) {
|
|
101
|
+
throw new VisionError("INVALID_SOURCE", `Not a file: ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
if (fileStat.size > config.maxImageBytes) {
|
|
104
|
+
throw new VisionError("SOURCE_TOO_LARGE", `Image is ${(fileStat.size / 1024 / 1024).toFixed(1)} MB; limit is ${(config.maxImageBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
105
|
+
}
|
|
106
|
+
return toLoadedImage(await readFile(filePath), path.basename(filePath));
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=image.js.map
|