@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/dist/video.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, chmod, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { mkdtemp } from "node:fs/promises";
|
|
7
|
+
import { VisionError } from "./errors.js";
|
|
8
|
+
import { downloadRemoteFile } from "./network.js";
|
|
9
|
+
let ffmpegReady;
|
|
10
|
+
async function usableFile(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
await access(filePath, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function systemFfmpeg() {
|
|
20
|
+
const executableName = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
|
|
21
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
22
|
+
if (!directory)
|
|
23
|
+
continue;
|
|
24
|
+
const candidate = path.join(directory, executableName);
|
|
25
|
+
if (await usableFile(candidate))
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
async function bundledFfmpeg() {
|
|
31
|
+
try {
|
|
32
|
+
const imported = await import("@ffmpeg-installer/ffmpeg");
|
|
33
|
+
const installer = imported.default;
|
|
34
|
+
return installer?.path;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function ensureFfmpeg() {
|
|
41
|
+
ffmpegReady ??= (async () => {
|
|
42
|
+
const configured = process.env.AGENT_VISION_FFMPEG_PATH?.trim();
|
|
43
|
+
if (configured) {
|
|
44
|
+
const resolved = path.resolve(configured);
|
|
45
|
+
if (!(await usableFile(resolved))) {
|
|
46
|
+
throw new VisionError("FFMPEG_NOT_AVAILABLE", `AGENT_VISION_FFMPEG_PATH is not executable: ${resolved}`);
|
|
47
|
+
}
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
const ffmpegPath = (await systemFfmpeg()) ?? (await bundledFfmpeg());
|
|
51
|
+
if (!ffmpegPath) {
|
|
52
|
+
throw new VisionError("FFMPEG_NOT_AVAILABLE", "The bundled ffmpeg binary is unavailable on this platform");
|
|
53
|
+
}
|
|
54
|
+
if (process.platform !== "win32") {
|
|
55
|
+
try {
|
|
56
|
+
await chmod(ffmpegPath, 0o755);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new VisionError("FFMPEG_NOT_AVAILABLE", "The bundled ffmpeg binary could not be made executable", { cause: error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return ffmpegPath;
|
|
63
|
+
})();
|
|
64
|
+
return ffmpegReady;
|
|
65
|
+
}
|
|
66
|
+
function expandLocalPath(source) {
|
|
67
|
+
if (source === "~")
|
|
68
|
+
return homedir();
|
|
69
|
+
if (source.startsWith("~/"))
|
|
70
|
+
return path.join(homedir(), source.slice(2));
|
|
71
|
+
return path.resolve(source.startsWith("@") ? source.slice(1) : source);
|
|
72
|
+
}
|
|
73
|
+
async function runFfmpeg(args, allowFailure = false, timeoutMs = 120_000) {
|
|
74
|
+
const command = await ensureFfmpeg();
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
77
|
+
let stderr = "";
|
|
78
|
+
let timedOut = false;
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
timedOut = true;
|
|
81
|
+
child.kill("SIGKILL");
|
|
82
|
+
}, timeoutMs);
|
|
83
|
+
child.stderr.setEncoding("utf8");
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
if (stderr.length < 1_000_000)
|
|
86
|
+
stderr += chunk;
|
|
87
|
+
});
|
|
88
|
+
child.on("error", (error) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(new VisionError("FFMPEG_ERROR", "Could not start ffmpeg", { cause: error }));
|
|
91
|
+
});
|
|
92
|
+
child.on("close", (code) => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
if (timedOut) {
|
|
95
|
+
reject(new VisionError("FFMPEG_TIMEOUT", `ffmpeg timed out after ${timeoutMs} ms`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (code === 0 || allowFailure)
|
|
99
|
+
resolve(stderr);
|
|
100
|
+
else
|
|
101
|
+
reject(new VisionError("FFMPEG_ERROR", stderr.trim().slice(-1000) || `ffmpeg exited with code ${code}`));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function videoDuration(filePath, timeoutMs) {
|
|
106
|
+
const output = await runFfmpeg(["-hide_banner", "-i", filePath], true, timeoutMs);
|
|
107
|
+
const match = output.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/);
|
|
108
|
+
if (!match?.[1] || !match[2] || !match[3]) {
|
|
109
|
+
throw new VisionError("UNSUPPORTED_VIDEO", "Could not determine video duration");
|
|
110
|
+
}
|
|
111
|
+
const duration = Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);
|
|
112
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
113
|
+
throw new VisionError("UNSUPPORTED_VIDEO", "Video duration is invalid");
|
|
114
|
+
}
|
|
115
|
+
return duration;
|
|
116
|
+
}
|
|
117
|
+
async function prepareInput(source, workDir, config) {
|
|
118
|
+
if (/^https?:\/\//i.test(source)) {
|
|
119
|
+
const destination = path.join(workDir, "input-video");
|
|
120
|
+
await downloadRemoteFile({
|
|
121
|
+
url: source,
|
|
122
|
+
destination,
|
|
123
|
+
maxBytes: config.maxVideoBytes,
|
|
124
|
+
timeoutMs: config.timeoutMs,
|
|
125
|
+
allowPrivateUrls: config.allowPrivateUrls,
|
|
126
|
+
});
|
|
127
|
+
return destination;
|
|
128
|
+
}
|
|
129
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(source)) {
|
|
130
|
+
throw new VisionError("INVALID_SOURCE", "Video source must be a local path or HTTP(S) URL");
|
|
131
|
+
}
|
|
132
|
+
const filePath = expandLocalPath(source);
|
|
133
|
+
let fileStat;
|
|
134
|
+
try {
|
|
135
|
+
fileStat = await stat(filePath);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw new VisionError("FILE_NOT_FOUND", `Video file not found: ${filePath}`, {
|
|
139
|
+
cause: error,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (!fileStat.isFile()) {
|
|
143
|
+
throw new VisionError("INVALID_SOURCE", `Not a file: ${filePath}`);
|
|
144
|
+
}
|
|
145
|
+
if (fileStat.size > config.maxVideoBytes) {
|
|
146
|
+
throw new VisionError("SOURCE_TOO_LARGE", `Video is ${(fileStat.size / 1024 / 1024).toFixed(1)} MB; limit is ${(config.maxVideoBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
147
|
+
}
|
|
148
|
+
return filePath;
|
|
149
|
+
}
|
|
150
|
+
export async function extractVideoFrames(options) {
|
|
151
|
+
if (!Number.isInteger(options.maxFrames) || options.maxFrames < 1 || options.maxFrames > 16) {
|
|
152
|
+
throw new VisionError("INVALID_ARGUMENT", "maxFrames must be an integer from 1 to 16");
|
|
153
|
+
}
|
|
154
|
+
const prefix = path.join(options.tempRoot ?? tmpdir(), "agent-vision-");
|
|
155
|
+
const workDir = await mkdtemp(prefix);
|
|
156
|
+
try {
|
|
157
|
+
const inputPath = await prepareInput(options.source.trim(), workDir, options.config);
|
|
158
|
+
const duration = await videoDuration(inputPath, options.config.timeoutMs);
|
|
159
|
+
const timestamps = Array.from({ length: options.maxFrames }, (_, index) => ((index + 0.5) * duration) / options.maxFrames);
|
|
160
|
+
for (const [index, timestamp] of timestamps.entries()) {
|
|
161
|
+
const outputPath = path.join(workDir, `frame-${String(index + 1).padStart(3, "0")}.jpg`);
|
|
162
|
+
await runFfmpeg([
|
|
163
|
+
"-hide_banner",
|
|
164
|
+
"-loglevel",
|
|
165
|
+
"error",
|
|
166
|
+
"-ss",
|
|
167
|
+
timestamp.toFixed(3),
|
|
168
|
+
"-i",
|
|
169
|
+
inputPath,
|
|
170
|
+
"-frames:v",
|
|
171
|
+
"1",
|
|
172
|
+
"-vf",
|
|
173
|
+
"scale=1600:1600:force_original_aspect_ratio=decrease",
|
|
174
|
+
"-q:v",
|
|
175
|
+
"3",
|
|
176
|
+
"-y",
|
|
177
|
+
outputPath,
|
|
178
|
+
], false, options.config.timeoutMs);
|
|
179
|
+
}
|
|
180
|
+
const frameNames = (await readdir(workDir))
|
|
181
|
+
.filter((name) => /^frame-\d+\.jpg$/.test(name))
|
|
182
|
+
.sort();
|
|
183
|
+
if (frameNames.length === 0) {
|
|
184
|
+
throw new VisionError("FFMPEG_ERROR", "ffmpeg did not produce any video frames");
|
|
185
|
+
}
|
|
186
|
+
return Promise.all(frameNames.map(async (name, index) => {
|
|
187
|
+
const timestamp = timestamps[index] ?? 0;
|
|
188
|
+
const buffer = await readFile(path.join(workDir, name));
|
|
189
|
+
return {
|
|
190
|
+
dataUrl: `data:image/jpeg;base64,${buffer.toString("base64")}`,
|
|
191
|
+
label: `frame at ${timestamp.toFixed(2)} seconds`,
|
|
192
|
+
timestampSeconds: timestamp,
|
|
193
|
+
};
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
await rm(workDir, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=video.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video.js","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yanickxia/agent-vision",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A small MCP server and Agent Skill for image and video understanding.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-vision": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"skills/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"NOTICE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "npm run build && tsx --test test/*.test.ts",
|
|
21
|
+
"prepare": "npm run build",
|
|
22
|
+
"prepack": "npm run typecheck && npm run build"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"agent-skills",
|
|
30
|
+
"vision",
|
|
31
|
+
"image-understanding",
|
|
32
|
+
"video-understanding",
|
|
33
|
+
"openai-compatible"
|
|
34
|
+
],
|
|
35
|
+
"author": "Yanick Xia",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/yanickxia/agent-vision.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/yanickxia/agent-vision/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/yanickxia/agent-vision#readme",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
|
+
"zod": "^3.25.76"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.18.0",
|
|
56
|
+
"tsx": "^4.20.6",
|
|
57
|
+
"typescript": "^5.9.2"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-vision
|
|
3
|
+
description: Analyze images, screenshots, diagrams, and videos with the agent-vision MCP tools. Use for image questions, OCR, UI inspection, visual debugging, screen recordings, video summaries, and timeline analysis.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Requires the agent-vision MCP server or Node.js 20+ for the npx fallback, plus an OpenAI-compatible vision API.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# agent-vision
|
|
9
|
+
|
|
10
|
+
Use this skill when the user asks you to inspect an image or video. The MCP
|
|
11
|
+
server is the execution layer; this skill explains when and how to call it.
|
|
12
|
+
|
|
13
|
+
## Choose the tool
|
|
14
|
+
|
|
15
|
+
| Input | Tool |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Screenshot, photo, diagram, UI, scanned text | `analyze_image` |
|
|
18
|
+
| Screen recording, demo, clip, animation timeline | `analyze_video` |
|
|
19
|
+
|
|
20
|
+
Do not call a vision tool for a text-only task.
|
|
21
|
+
|
|
22
|
+
## Calling rules
|
|
23
|
+
|
|
24
|
+
1. Pass the user's original question as `prompt`. A focused question is better
|
|
25
|
+
than a generic request for a description.
|
|
26
|
+
2. Prefer an absolute local path. HTTP(S) URLs are also supported. Images may
|
|
27
|
+
additionally be supplied as a `data:image/...;base64,...` URI.
|
|
28
|
+
3. For videos, keep the default `max_frames=8` unless the user needs a faster
|
|
29
|
+
result or a denser timeline. The allowed range is 1–16.
|
|
30
|
+
4. If the result is uncertain, say so. Never invent visual details after a
|
|
31
|
+
failed tool call.
|
|
32
|
+
5. Retry at most once for a transient API or timeout error. Configuration,
|
|
33
|
+
unsupported-format, and missing-file errors require correction, not retries.
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
analyze_image({
|
|
39
|
+
"source": "/absolute/path/to/error.png",
|
|
40
|
+
"prompt": "Read the error message and identify the likely cause."
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
analyze_video({
|
|
46
|
+
"source": "/absolute/path/to/demo.mp4",
|
|
47
|
+
"prompt": "Summarize the UI flow in chronological order.",
|
|
48
|
+
"max_frames": 8
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## npx fallback
|
|
53
|
+
|
|
54
|
+
If the MCP tools are not available but shell execution is allowed, run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -y @yanickxia/agent-vision image "/absolute/path/to/image.png" --prompt "user question"
|
|
58
|
+
npx -y @yanickxia/agent-vision video "/absolute/path/to/video.mp4" --prompt "user question" --frames 8
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Before the npm package is available, replace the package name with
|
|
62
|
+
`github:yanickxia/agent-vision`.
|
|
63
|
+
|
|
64
|
+
Configuration details are in `references/configuration.md`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Required for normal use:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
export AGENT_VISION_BASE_URL="https://your-openai-compatible-endpoint/v1"
|
|
7
|
+
export AGENT_VISION_API_KEY="your-key"
|
|
8
|
+
export AGENT_VISION_MODEL="your-vision-model"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`AGENT_VISION_API_KEY` may be omitted for a trusted local endpoint that does
|
|
12
|
+
not require authentication. The server also recognizes `VISION_*` and
|
|
13
|
+
`OPENAI_*` aliases.
|
|
14
|
+
|
|
15
|
+
Useful optional variables:
|
|
16
|
+
|
|
17
|
+
- `AGENT_VISION_MAX_TOKENS` (default `4096`)
|
|
18
|
+
- `AGENT_VISION_TIMEOUT_MS` (default `120000`)
|
|
19
|
+
- `AGENT_VISION_MAX_IMAGE_MB` (default `20`)
|
|
20
|
+
- `AGENT_VISION_MAX_VIDEO_MB` (default `200`)
|
|
21
|
+
- `AGENT_VISION_HEADERS` (JSON object of extra HTTP headers)
|
|
22
|
+
- `AGENT_VISION_ALLOW_PRIVATE_URLS=true` (allow private image/video source URLs)
|
|
23
|
+
- `AGENT_VISION_FFMPEG_PATH` (override ffmpeg; otherwise PATH, then bundled binary)
|
|
24
|
+
|
|
25
|
+
The configured API endpoint itself may be private; the private-URL guard only
|
|
26
|
+
applies to user-provided image and video source URLs.
|