@goonnguyen/human-mcp 1.3.0 → 2.0.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/README.md +261 -19
- package/bin/human-mcp.js +2 -0
- package/dist/index.js +65180 -1698
- package/package.json +19 -2
- package/.claude/agents/code-reviewer.md +0 -140
- package/.claude/agents/database-admin.md +0 -86
- package/.claude/agents/debugger.md +0 -119
- package/.claude/agents/docs-manager.md +0 -113
- package/.claude/agents/git-manager.md +0 -59
- package/.claude/agents/planner-researcher.md +0 -97
- package/.claude/agents/project-manager.md +0 -113
- package/.claude/agents/tester.md +0 -95
- package/.claude/commands/cook.md +0 -7
- package/.claude/commands/debug.md +0 -10
- package/.claude/commands/docs/init.md +0 -11
- package/.claude/commands/docs/update.md +0 -11
- package/.claude/commands/fix/ci.md +0 -8
- package/.claude/commands/fix/fast.md +0 -5
- package/.claude/commands/fix/hard.md +0 -7
- package/.claude/commands/fix/test.md +0 -16
- package/.claude/commands/git/cm.md +0 -5
- package/.claude/commands/git/cp.md +0 -4
- package/.claude/commands/plan/ci.md +0 -12
- package/.claude/commands/plan/two.md +0 -13
- package/.claude/commands/plan.md +0 -10
- package/.claude/commands/test.md +0 -7
- package/.claude/commands/watzup.md +0 -8
- package/.claude/hooks/telegram_notify.sh +0 -136
- package/.claude/send-discord.sh +0 -64
- package/.claude/settings.json +0 -7
- package/.claude/statusline.sh +0 -143
- package/.dockerignore +0 -81
- package/.env.example +0 -44
- package/.github/workflows/publish.yml +0 -88
- package/.opencode/agent/code-reviewer.md +0 -142
- package/.opencode/agent/debugger.md +0 -74
- package/.opencode/agent/docs-manager.md +0 -119
- package/.opencode/agent/git-manager.md +0 -60
- package/.opencode/agent/planner-researcher.md +0 -100
- package/.opencode/agent/project-manager.md +0 -113
- package/.opencode/agent/system-architecture.md +0 -200
- package/.opencode/agent/tester.md +0 -96
- package/.opencode/agent/ui-ux-developer.md +0 -97
- package/.opencode/command/cook.md +0 -7
- package/.opencode/command/debug.md +0 -10
- package/.opencode/command/fix/ci.md +0 -8
- package/.opencode/command/fix/fast.md +0 -5
- package/.opencode/command/fix/hard.md +0 -7
- package/.opencode/command/fix/test.md +0 -16
- package/.opencode/command/git/cm.md +0 -5
- package/.opencode/command/git/cp.md +0 -4
- package/.opencode/command/plan/ci.md +0 -12
- package/.opencode/command/plan/two.md +0 -13
- package/.opencode/command/plan.md +0 -10
- package/.opencode/command/test.md +0 -7
- package/.opencode/command/watzup.md +0 -8
- package/.releaserc.json +0 -26
- package/.serena/project.yml +0 -68
- package/CHANGELOG.md +0 -62
- package/CLAUDE.md +0 -141
- package/DEPLOYMENT.md +0 -329
- package/Dockerfile +0 -52
- package/QUICKSTART.md +0 -97
- package/bun.lock +0 -1872
- package/bunfig.toml +0 -15
- package/docker-compose.yaml +0 -128
- package/docs/README.md +0 -51
- package/docs/codebase-structure-architecture-code-standards.md +0 -428
- package/docs/codebase-summary.md +0 -321
- package/docs/project-overview-pdr.md +0 -286
- package/docs/project-roadmap.md +0 -494
- package/examples/debugging-session.ts +0 -96
- package/human-mcp.png +0 -0
- package/inspector-wrapper.mjs +0 -33
- package/plans/001-streamable-http-transport-plan.md +0 -905
- package/plans/002-sse-fallback-http-transport-plan.md +0 -161
- package/plans/003-fix-test-infrastructure-and-ci-plan.md +0 -699
- package/plans/003-http-transport-local-file-access-plan.md +0 -880
- package/plans/004-fix-typescript-compilation-errors-plan.md +0 -388
- package/plans/005-comprehensive-test-infrastructure-fix-plan.md +0 -854
- package/plans/templates/bug-fix-template.md +0 -69
- package/plans/templates/feature-implementation-template.md +0 -84
- package/plans/templates/refactor-template.md +0 -82
- package/plans/templates/template-usage-guide.md +0 -58
- package/src/index.ts +0 -49
- package/src/prompts/debugging-prompts.ts +0 -149
- package/src/prompts/index.ts +0 -55
- package/src/resources/documentation.ts +0 -316
- package/src/resources/index.ts +0 -49
- package/src/server.ts +0 -36
- package/src/tools/eyes/index.ts +0 -225
- package/src/tools/eyes/processors/gif.ts +0 -137
- package/src/tools/eyes/processors/image.ts +0 -213
- package/src/tools/eyes/processors/video.ts +0 -135
- package/src/tools/eyes/schemas.ts +0 -51
- package/src/tools/eyes/utils/formatters.ts +0 -126
- package/src/tools/eyes/utils/gemini-client.ts +0 -73
- package/src/transports/http/file-interceptor.ts +0 -134
- package/src/transports/http/middleware.ts +0 -46
- package/src/transports/http/routes.ts +0 -297
- package/src/transports/http/server.ts +0 -116
- package/src/transports/http/session.ts +0 -93
- package/src/transports/http/sse-routes.ts +0 -210
- package/src/transports/index.ts +0 -36
- package/src/transports/stdio.ts +0 -7
- package/src/transports/types.ts +0 -50
- package/src/types/index.ts +0 -41
- package/src/utils/cloudflare-r2.ts +0 -107
- package/src/utils/config.ts +0 -123
- package/src/utils/errors.ts +0 -40
- package/src/utils/logger.ts +0 -49
- package/tests/integration/http-transport-files.test.ts +0 -190
- package/tests/integration/server.test.ts +0 -27
- package/tests/integration/sse-transport.test.ts +0 -142
- package/tests/setup.ts +0 -55
- package/tests/types/api-responses.ts +0 -35
- package/tests/types/test-types.ts +0 -105
- package/tests/unit/cloudflare-r2.test.ts +0 -118
- package/tests/unit/config.test.ts +0 -40
- package/tests/unit/eyes-analyze.test.ts +0 -150
- package/tests/unit/formatters.test.ts +0 -85
- package/tests/unit/sse-routes.test.ts +0 -92
- package/tests/utils/error-scenarios.ts +0 -198
- package/tests/utils/index.ts +0 -3
- package/tests/utils/mock-helpers.ts +0 -99
- package/tests/utils/test-data-generators.ts +0 -217
- package/tests/utils/test-server-manager.ts +0 -172
- package/tsconfig.json +0 -26
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { GenerativeModel } from "@google/generative-ai";
|
|
2
|
-
import sharp from "sharp";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import type { AnalysisOptions, ProcessingResult } from "@/types";
|
|
5
|
-
import { createPrompt, parseAnalysisResponse } from "../utils/formatters.js";
|
|
6
|
-
import { logger } from "@/utils/logger.js";
|
|
7
|
-
import { ProcessingError } from "@/utils/errors.js";
|
|
8
|
-
|
|
9
|
-
export async function processGif(
|
|
10
|
-
model: GenerativeModel,
|
|
11
|
-
source: string,
|
|
12
|
-
options: AnalysisOptions
|
|
13
|
-
): Promise<ProcessingResult> {
|
|
14
|
-
const startTime = Date.now();
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
logger.debug(`Processing GIF: ${source.substring(0, 50)}...`);
|
|
18
|
-
|
|
19
|
-
const gifData = await loadGif(source);
|
|
20
|
-
const frames = await extractGifFrames(gifData);
|
|
21
|
-
|
|
22
|
-
if (frames.length === 0) {
|
|
23
|
-
throw new ProcessingError("No frames could be extracted from GIF");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const prompt = createPrompt(options) + `
|
|
27
|
-
|
|
28
|
-
This is an animated GIF analysis with ${frames.length} frames. Pay attention to:
|
|
29
|
-
- Animation timing and smoothness
|
|
30
|
-
- UI state transitions
|
|
31
|
-
- Loading states or progress indicators
|
|
32
|
-
- Error animations or feedback
|
|
33
|
-
- Interactive element hover states
|
|
34
|
-
- Any visual glitches in the animation`;
|
|
35
|
-
|
|
36
|
-
const mediaData = frames.map(frame => ({
|
|
37
|
-
mimeType: 'image/png',
|
|
38
|
-
data: frame
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
const response = await model.generateContent([
|
|
42
|
-
{ text: prompt },
|
|
43
|
-
...mediaData.map(data => ({
|
|
44
|
-
inlineData: {
|
|
45
|
-
mimeType: data.mimeType,
|
|
46
|
-
data: data.data
|
|
47
|
-
}
|
|
48
|
-
}))
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
const result = await response.response;
|
|
52
|
-
const analysisText = result.text();
|
|
53
|
-
|
|
54
|
-
if (!analysisText) {
|
|
55
|
-
throw new ProcessingError("No analysis result from Gemini");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const parsed = parseAnalysisResponse(analysisText);
|
|
59
|
-
const processingTime = Date.now() - startTime;
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
description: parsed.description || "GIF analysis completed",
|
|
63
|
-
analysis: parsed.analysis || analysisText,
|
|
64
|
-
elements: parsed.elements || [],
|
|
65
|
-
insights: parsed.insights || [],
|
|
66
|
-
recommendations: parsed.recommendations || [],
|
|
67
|
-
metadata: {
|
|
68
|
-
processing_time_ms: processingTime,
|
|
69
|
-
model_used: model.model,
|
|
70
|
-
frames_analyzed: frames.length
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
} catch (error) {
|
|
75
|
-
logger.error("GIF processing error:", error);
|
|
76
|
-
throw new ProcessingError(`Failed to process GIF: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function loadGif(source: string): Promise<Buffer> {
|
|
81
|
-
if (source.startsWith('data:image/gif')) {
|
|
82
|
-
const [, data] = source.split(',');
|
|
83
|
-
if (!data) {
|
|
84
|
-
throw new ProcessingError("Invalid base64 GIF format");
|
|
85
|
-
}
|
|
86
|
-
return Buffer.from(data, 'base64');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
90
|
-
const response = await fetch(source);
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
throw new ProcessingError(`Failed to fetch GIF: ${response.statusText}`);
|
|
93
|
-
}
|
|
94
|
-
return Buffer.from(await response.arrayBuffer());
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
return await fs.readFile(source);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
throw new ProcessingError(`Failed to load GIF file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function extractGifFrames(gifBuffer: Buffer): Promise<string[]> {
|
|
105
|
-
try {
|
|
106
|
-
const image = sharp(gifBuffer, { animated: true });
|
|
107
|
-
const { pages } = await image.metadata();
|
|
108
|
-
|
|
109
|
-
if (!pages || pages <= 1) {
|
|
110
|
-
const singleFrame = await image
|
|
111
|
-
.resize(512, 512, { fit: 'inside', withoutEnlargement: true })
|
|
112
|
-
.png()
|
|
113
|
-
.toBuffer();
|
|
114
|
-
return [singleFrame.toString('base64')];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const frames: string[] = [];
|
|
118
|
-
const maxFrames = Math.min(pages, 16);
|
|
119
|
-
|
|
120
|
-
for (let i = 0; i < maxFrames; i++) {
|
|
121
|
-
const frame = await sharp(gifBuffer, {
|
|
122
|
-
animated: true,
|
|
123
|
-
page: i
|
|
124
|
-
})
|
|
125
|
-
.resize(512, 512, { fit: 'inside', withoutEnlargement: true })
|
|
126
|
-
.png()
|
|
127
|
-
.toBuffer();
|
|
128
|
-
|
|
129
|
-
frames.push(frame.toString('base64'));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return frames;
|
|
133
|
-
|
|
134
|
-
} catch (error) {
|
|
135
|
-
throw new ProcessingError(`Failed to extract GIF frames: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { GenerativeModel } from "@google/generative-ai";
|
|
2
|
-
import sharp from "sharp";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import type { AnalysisOptions, ProcessingResult } from "@/types";
|
|
5
|
-
import { createPrompt, parseAnalysisResponse } from "../utils/formatters.js";
|
|
6
|
-
import { logger } from "@/utils/logger.js";
|
|
7
|
-
import { ProcessingError } from "@/utils/errors.js";
|
|
8
|
-
import { getCloudflareR2 } from "@/utils/cloudflare-r2.js";
|
|
9
|
-
|
|
10
|
-
export async function processImage(
|
|
11
|
-
model: GenerativeModel,
|
|
12
|
-
source: string,
|
|
13
|
-
options: AnalysisOptions
|
|
14
|
-
): Promise<ProcessingResult> {
|
|
15
|
-
const startTime = Date.now();
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
logger.debug(`Processing image: ${source.substring(0, 50)}...`);
|
|
19
|
-
|
|
20
|
-
const { imageData, mimeType } = await loadImage(source, options.fetchTimeout);
|
|
21
|
-
const prompt = createPrompt(options);
|
|
22
|
-
|
|
23
|
-
const response = await model.generateContent([
|
|
24
|
-
{ text: prompt },
|
|
25
|
-
{
|
|
26
|
-
inlineData: {
|
|
27
|
-
mimeType,
|
|
28
|
-
data: imageData
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
const result = await response.response;
|
|
34
|
-
const analysisText = result.text();
|
|
35
|
-
|
|
36
|
-
if (!analysisText) {
|
|
37
|
-
throw new ProcessingError("No analysis result from Gemini");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const parsed = parseAnalysisResponse(analysisText);
|
|
41
|
-
const processingTime = Date.now() - startTime;
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
description: parsed.description || "Image analysis completed",
|
|
45
|
-
analysis: parsed.analysis || analysisText,
|
|
46
|
-
elements: parsed.elements || [],
|
|
47
|
-
insights: parsed.insights || [],
|
|
48
|
-
recommendations: parsed.recommendations || [],
|
|
49
|
-
metadata: {
|
|
50
|
-
processing_time_ms: processingTime,
|
|
51
|
-
model_used: model.model,
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
} catch (error) {
|
|
56
|
-
logger.error("Image processing error:", error);
|
|
57
|
-
throw new ProcessingError(`Failed to process image: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function loadImage(source: string, fetchTimeout?: number): Promise<{ imageData: string; mimeType: string }> {
|
|
62
|
-
// Detect Claude Desktop virtual paths and auto-upload to Cloudflare
|
|
63
|
-
if (source.startsWith('/mnt/user-data/') || source.startsWith('/mnt/')) {
|
|
64
|
-
logger.info(`Detected Claude Desktop virtual path: ${source}`);
|
|
65
|
-
|
|
66
|
-
// Extract filename from path
|
|
67
|
-
const filename = source.split('/').pop() || 'upload.jpg';
|
|
68
|
-
|
|
69
|
-
// Try to read from a temporary upload directory (if middleware saved it)
|
|
70
|
-
const tempPath = `/tmp/mcp-uploads/${filename}`;
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// Check if file was temporarily saved by middleware
|
|
74
|
-
if (await fs.access(tempPath).then(() => true).catch(() => false)) {
|
|
75
|
-
const buffer = await fs.readFile(tempPath);
|
|
76
|
-
|
|
77
|
-
// Upload to Cloudflare R2 if configured
|
|
78
|
-
const cloudflare = getCloudflareR2();
|
|
79
|
-
if (cloudflare) {
|
|
80
|
-
const publicUrl = await cloudflare.uploadFile(buffer, filename);
|
|
81
|
-
|
|
82
|
-
// Clean up temp file
|
|
83
|
-
await fs.unlink(tempPath).catch(() => {});
|
|
84
|
-
|
|
85
|
-
// Now fetch from the CDN URL
|
|
86
|
-
return loadImage(publicUrl, fetchTimeout);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} catch (error) {
|
|
90
|
-
logger.warn(`Could not process temp file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// If no temp file or Cloudflare not configured, provide helpful error
|
|
94
|
-
throw new ProcessingError(
|
|
95
|
-
`Local file access not supported via HTTP transport.\n` +
|
|
96
|
-
`The file path "${source}" is not accessible.\n\n` +
|
|
97
|
-
`Solutions:\n` +
|
|
98
|
-
`1. Upload your file to Cloudflare R2 first using the /mcp/upload endpoint\n` +
|
|
99
|
-
`2. Use a public URL instead of a local file path\n` +
|
|
100
|
-
`3. Convert the image to a base64 data URI\n` +
|
|
101
|
-
`4. Use the stdio transport for local file access`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Existing base64 handling
|
|
106
|
-
if (source.startsWith('data:image/')) {
|
|
107
|
-
const [header, data] = source.split(',');
|
|
108
|
-
if (!header || !data) {
|
|
109
|
-
throw new ProcessingError("Invalid base64 image format");
|
|
110
|
-
}
|
|
111
|
-
const mimeMatch = header.match(/data:(image\/[^;]+)/);
|
|
112
|
-
if (!mimeMatch || !mimeMatch[1]) {
|
|
113
|
-
throw new ProcessingError("Invalid base64 image format");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Optional: For large base64 images, upload to Cloudflare R2 if configured
|
|
117
|
-
const cloudflare = getCloudflareR2();
|
|
118
|
-
if (cloudflare && data.length > 1024 * 1024) { // > 1MB base64
|
|
119
|
-
logger.info('Large base64 image detected, uploading to Cloudflare R2');
|
|
120
|
-
try {
|
|
121
|
-
const publicUrl = await cloudflare.uploadBase64(data, mimeMatch[1]);
|
|
122
|
-
return loadImage(publicUrl, fetchTimeout);
|
|
123
|
-
} catch (error) {
|
|
124
|
-
logger.warn('Failed to upload large base64 to Cloudflare R2:', error);
|
|
125
|
-
// Continue with base64 processing
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
imageData: data,
|
|
131
|
-
mimeType: mimeMatch[1]
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Existing URL handling
|
|
136
|
-
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
137
|
-
const controller = new AbortController();
|
|
138
|
-
const timeoutId = setTimeout(() => controller.abort(), fetchTimeout || 30000);
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const response = await fetch(source, { signal: controller.signal });
|
|
142
|
-
clearTimeout(timeoutId);
|
|
143
|
-
|
|
144
|
-
if (!response.ok) {
|
|
145
|
-
throw new ProcessingError(`Failed to fetch image: ${response.statusText}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const buffer = await response.arrayBuffer();
|
|
149
|
-
const uint8Array = new Uint8Array(buffer);
|
|
150
|
-
|
|
151
|
-
const processedImage = await sharp(uint8Array)
|
|
152
|
-
.resize(1024, 1024, { fit: 'inside', withoutEnlargement: true })
|
|
153
|
-
.jpeg({ quality: 85 })
|
|
154
|
-
.toBuffer();
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
imageData: processedImage.toString('base64'),
|
|
158
|
-
mimeType: 'image/jpeg'
|
|
159
|
-
};
|
|
160
|
-
} catch (error) {
|
|
161
|
-
clearTimeout(timeoutId);
|
|
162
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
163
|
-
throw new ProcessingError(`Fetch timeout: Failed to download image from ${source}`);
|
|
164
|
-
}
|
|
165
|
-
throw new ProcessingError(`Failed to fetch image: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Local file handling - auto-upload to Cloudflare for HTTP transport
|
|
170
|
-
try {
|
|
171
|
-
const stats = await fs.stat(source);
|
|
172
|
-
if (!stats.isFile()) {
|
|
173
|
-
throw new ProcessingError(`Path is not a file: ${source}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// If using HTTP transport, upload to Cloudflare R2 if configured
|
|
177
|
-
const cloudflare = getCloudflareR2();
|
|
178
|
-
if (process.env.TRANSPORT_TYPE === 'http' && cloudflare) {
|
|
179
|
-
logger.info(`HTTP transport detected, uploading local file to Cloudflare R2: ${source}`);
|
|
180
|
-
try {
|
|
181
|
-
const buffer = await fs.readFile(source);
|
|
182
|
-
const filename = source.split('/').pop() || 'upload.jpg';
|
|
183
|
-
const publicUrl = await cloudflare.uploadFile(buffer, filename);
|
|
184
|
-
|
|
185
|
-
// Fetch from CDN
|
|
186
|
-
return loadImage(publicUrl, fetchTimeout);
|
|
187
|
-
} catch (error) {
|
|
188
|
-
logger.warn(`Failed to upload to Cloudflare R2: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
189
|
-
// Continue with local file processing
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// For stdio transport or when Cloudflare is not configured, process locally
|
|
194
|
-
const buffer = await fs.readFile(source);
|
|
195
|
-
const processedImage = await sharp(buffer)
|
|
196
|
-
.resize(1024, 1024, { fit: 'inside', withoutEnlargement: true })
|
|
197
|
-
.jpeg({ quality: 85 })
|
|
198
|
-
.toBuffer();
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
imageData: processedImage.toString('base64'),
|
|
202
|
-
mimeType: 'image/jpeg'
|
|
203
|
-
};
|
|
204
|
-
} catch (error) {
|
|
205
|
-
if (error instanceof Error && error.message.includes('ENOENT')) {
|
|
206
|
-
throw new ProcessingError(
|
|
207
|
-
`File not found: ${source}\n` +
|
|
208
|
-
`When using HTTP transport, files are automatically uploaded to Cloudflare R2 if configured.`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
throw new ProcessingError(`Failed to load image file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { GenerativeModel } from "@google/generative-ai";
|
|
2
|
-
import ffmpeg from "fluent-ffmpeg";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import os from "os";
|
|
6
|
-
import sharp from "sharp";
|
|
7
|
-
import type { VideoOptions, ProcessingResult } from "@/types";
|
|
8
|
-
import { createPrompt, parseAnalysisResponse } from "../utils/formatters.js";
|
|
9
|
-
import { logger } from "@/utils/logger.js";
|
|
10
|
-
import { ProcessingError } from "@/utils/errors.js";
|
|
11
|
-
|
|
12
|
-
export async function processVideo(
|
|
13
|
-
model: GenerativeModel,
|
|
14
|
-
source: string,
|
|
15
|
-
options: VideoOptions
|
|
16
|
-
): Promise<ProcessingResult> {
|
|
17
|
-
const startTime = Date.now();
|
|
18
|
-
const maxFrames = options.max_frames || 32;
|
|
19
|
-
const sampleRate = options.sample_rate || 1;
|
|
20
|
-
|
|
21
|
-
let tempDir: string | null = null;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
logger.debug(`Processing video: ${source.substring(0, 50)}... (max ${maxFrames} frames)`);
|
|
25
|
-
|
|
26
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'human-mcp-video-'));
|
|
27
|
-
const frames = await extractFrames(source, tempDir, maxFrames, sampleRate);
|
|
28
|
-
|
|
29
|
-
if (frames.length === 0) {
|
|
30
|
-
throw new ProcessingError("No frames could be extracted from video");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const prompt = createPrompt(options) + `
|
|
34
|
-
|
|
35
|
-
This is a video analysis with ${frames.length} frames extracted. Focus on:
|
|
36
|
-
- Temporal changes between frames
|
|
37
|
-
- Animation or transition issues
|
|
38
|
-
- Error states that appear over time
|
|
39
|
-
- UI state changes and interactions
|
|
40
|
-
- Any progressive degradation or improvement`;
|
|
41
|
-
|
|
42
|
-
const mediaData = await Promise.all(
|
|
43
|
-
frames.map(async (framePath) => {
|
|
44
|
-
const buffer = await fs.readFile(framePath);
|
|
45
|
-
const processedFrame = await sharp(buffer)
|
|
46
|
-
.resize(512, 512, { fit: 'inside', withoutEnlargement: true })
|
|
47
|
-
.jpeg({ quality: 80 })
|
|
48
|
-
.toBuffer();
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
mimeType: 'image/jpeg',
|
|
52
|
-
data: processedFrame.toString('base64')
|
|
53
|
-
};
|
|
54
|
-
})
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const response = await model.generateContent([
|
|
58
|
-
{ text: prompt },
|
|
59
|
-
...mediaData.map(data => ({
|
|
60
|
-
inlineData: {
|
|
61
|
-
mimeType: data.mimeType,
|
|
62
|
-
data: data.data
|
|
63
|
-
}
|
|
64
|
-
}))
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
const result = await response.response;
|
|
68
|
-
const analysisText = result.text();
|
|
69
|
-
|
|
70
|
-
if (!analysisText) {
|
|
71
|
-
throw new ProcessingError("No analysis result from Gemini");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const parsed = parseAnalysisResponse(analysisText);
|
|
75
|
-
const processingTime = Date.now() - startTime;
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
description: parsed.description || "Video analysis completed",
|
|
79
|
-
analysis: parsed.analysis || analysisText,
|
|
80
|
-
elements: parsed.elements || [],
|
|
81
|
-
insights: parsed.insights || [],
|
|
82
|
-
recommendations: parsed.recommendations || [],
|
|
83
|
-
metadata: {
|
|
84
|
-
processing_time_ms: processingTime,
|
|
85
|
-
model_used: model.model,
|
|
86
|
-
frames_analyzed: frames.length
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
} catch (error) {
|
|
91
|
-
logger.error("Video processing error:", error);
|
|
92
|
-
throw new ProcessingError(`Failed to process video: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
93
|
-
} finally {
|
|
94
|
-
if (tempDir) {
|
|
95
|
-
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function extractFrames(
|
|
101
|
-
videoSource: string,
|
|
102
|
-
outputDir: string,
|
|
103
|
-
maxFrames: number,
|
|
104
|
-
sampleRate: number
|
|
105
|
-
): Promise<string[]> {
|
|
106
|
-
return new Promise((resolve, reject) => {
|
|
107
|
-
const framePattern = path.join(outputDir, 'frame_%04d.jpg');
|
|
108
|
-
const frames: string[] = [];
|
|
109
|
-
|
|
110
|
-
ffmpeg(videoSource)
|
|
111
|
-
.outputOptions([
|
|
112
|
-
'-vf', `fps=1/${sampleRate}`,
|
|
113
|
-
'-vframes', maxFrames.toString(),
|
|
114
|
-
'-q:v', '2'
|
|
115
|
-
])
|
|
116
|
-
.output(framePattern)
|
|
117
|
-
.on('end', async () => {
|
|
118
|
-
try {
|
|
119
|
-
const files = await fs.readdir(outputDir);
|
|
120
|
-
const frameFiles = files
|
|
121
|
-
.filter(file => file.startsWith('frame_') && file.endsWith('.jpg'))
|
|
122
|
-
.sort()
|
|
123
|
-
.map(file => path.join(outputDir, file));
|
|
124
|
-
|
|
125
|
-
resolve(frameFiles);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
reject(error);
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
.on('error', (error) => {
|
|
131
|
-
reject(new ProcessingError(`FFmpeg error: ${error.message}`));
|
|
132
|
-
})
|
|
133
|
-
.run();
|
|
134
|
-
});
|
|
135
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const EyesInputSchema = z.object({
|
|
4
|
-
source: z.string().describe("URL, file path, or base64 encoded content"),
|
|
5
|
-
type: z.enum(["image", "video", "gif"]).describe("Type of visual content"),
|
|
6
|
-
analysis_type: z.enum([
|
|
7
|
-
"general",
|
|
8
|
-
"ui_debug",
|
|
9
|
-
"error_detection",
|
|
10
|
-
"accessibility",
|
|
11
|
-
"performance",
|
|
12
|
-
"layout"
|
|
13
|
-
]).default("general"),
|
|
14
|
-
detail_level: z.enum(["quick", "detailed"]).default("detailed"),
|
|
15
|
-
specific_focus: z.string().optional().describe("Specific areas or elements to focus on"),
|
|
16
|
-
extract_text: z.boolean().default(true),
|
|
17
|
-
detect_ui_elements: z.boolean().default(true),
|
|
18
|
-
analyze_colors: z.boolean().default(false),
|
|
19
|
-
check_accessibility: z.boolean().default(false)
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export const EyesOutputSchema = z.object({
|
|
23
|
-
analysis: z.string(),
|
|
24
|
-
detected_elements: z.array(z.object({
|
|
25
|
-
type: z.string(),
|
|
26
|
-
location: z.object({
|
|
27
|
-
x: z.number(),
|
|
28
|
-
y: z.number(),
|
|
29
|
-
width: z.number(),
|
|
30
|
-
height: z.number()
|
|
31
|
-
}),
|
|
32
|
-
properties: z.record(z.any())
|
|
33
|
-
})),
|
|
34
|
-
debugging_insights: z.array(z.string()),
|
|
35
|
-
recommendations: z.array(z.string()),
|
|
36
|
-
metadata: z.object({
|
|
37
|
-
processing_time_ms: z.number(),
|
|
38
|
-
model_used: z.string(),
|
|
39
|
-
frames_analyzed: z.number().optional()
|
|
40
|
-
})
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export const CompareInputSchema = z.object({
|
|
44
|
-
source1: z.string(),
|
|
45
|
-
source2: z.string(),
|
|
46
|
-
comparison_type: z.enum(["pixel", "structural", "semantic"]).default("semantic")
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
export type EyesInput = z.infer<typeof EyesInputSchema>;
|
|
50
|
-
export type EyesOutput = z.infer<typeof EyesOutputSchema>;
|
|
51
|
-
export type CompareInput = z.infer<typeof CompareInputSchema>;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { AnalysisOptions, ProcessingResult, DetectedElement } from "@/types";
|
|
2
|
-
|
|
3
|
-
export function createPrompt(options: AnalysisOptions): string {
|
|
4
|
-
const { analysis_type, detail_level, specific_focus } = options;
|
|
5
|
-
|
|
6
|
-
let basePrompt = "";
|
|
7
|
-
|
|
8
|
-
switch (analysis_type) {
|
|
9
|
-
case "ui_debug":
|
|
10
|
-
basePrompt = `You are a UI debugging expert. Analyze this visual content for layout issues, rendering problems, misalignments, broken elements, and visual bugs. Focus on identifying what's wrong with the user interface.`;
|
|
11
|
-
break;
|
|
12
|
-
case "error_detection":
|
|
13
|
-
basePrompt = `You are an error detection specialist. Look for visible error messages, error states, broken functionality, missing content, and any signs of system failures or exceptions.`;
|
|
14
|
-
break;
|
|
15
|
-
case "accessibility":
|
|
16
|
-
basePrompt = `You are an accessibility expert. Analyze this content for accessibility issues including color contrast, text readability, missing alt text, poor focus indicators, and compliance with WCAG guidelines.`;
|
|
17
|
-
break;
|
|
18
|
-
case "performance":
|
|
19
|
-
basePrompt = `You are a performance analysis expert. Look for signs of slow loading, layout shifts, render blocking, large images, and other performance-related visual indicators.`;
|
|
20
|
-
break;
|
|
21
|
-
case "layout":
|
|
22
|
-
basePrompt = `You are a layout analysis expert. Focus on responsive design issues, element positioning, spacing, alignment, overflow problems, and overall visual hierarchy.`;
|
|
23
|
-
break;
|
|
24
|
-
default:
|
|
25
|
-
basePrompt = `You are a visual analysis expert. Provide a comprehensive analysis of this visual content.`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const detailInstructions = {
|
|
29
|
-
quick: "Provide a concise analysis focusing on the most important findings.",
|
|
30
|
-
detailed: "Provide a thorough analysis with specific details about each finding."
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const focusInstruction = specific_focus
|
|
34
|
-
? `\n\nPay special attention to: ${specific_focus}`
|
|
35
|
-
: "";
|
|
36
|
-
|
|
37
|
-
return `${basePrompt}
|
|
38
|
-
|
|
39
|
-
${detailInstructions[detail_level]}
|
|
40
|
-
|
|
41
|
-
Please structure your response as follows:
|
|
42
|
-
1. OVERVIEW: Brief summary of what you see
|
|
43
|
-
2. KEY FINDINGS: Main issues or points of interest
|
|
44
|
-
3. DETAILED ANALYSIS: Comprehensive breakdown
|
|
45
|
-
4. UI ELEMENTS: List detected interactive elements with approximate positions
|
|
46
|
-
5. RECOMMENDATIONS: Specific actionable suggestions
|
|
47
|
-
6. DEBUGGING INSIGHTS: Technical insights for developers
|
|
48
|
-
|
|
49
|
-
${focusInstruction}
|
|
50
|
-
|
|
51
|
-
Be specific, technical, and provide exact details where possible. Include coordinates, colors, sizes, and any measurable properties you can identify.`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parseAnalysisResponse(response: string): Partial<ProcessingResult> {
|
|
55
|
-
const sections = {
|
|
56
|
-
overview: extractSection(response, "OVERVIEW"),
|
|
57
|
-
findings: extractSection(response, "KEY FINDINGS"),
|
|
58
|
-
analysis: extractSection(response, "DETAILED ANALYSIS"),
|
|
59
|
-
elements: extractSection(response, "UI ELEMENTS"),
|
|
60
|
-
recommendations: extractSection(response, "RECOMMENDATIONS"),
|
|
61
|
-
insights: extractSection(response, "DEBUGGING INSIGHTS")
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
description: sections.overview || response.substring(0, 500),
|
|
66
|
-
analysis: sections.analysis || response,
|
|
67
|
-
elements: parseUIElements(sections.elements),
|
|
68
|
-
insights: parseList(sections.insights),
|
|
69
|
-
recommendations: parseList(sections.recommendations)
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function extractSection(text: string, sectionName: string): string {
|
|
74
|
-
const regex = new RegExp(`${sectionName}:?\\s*([\\s\\S]*?)(?=\\n\\n[A-Z]+:|$)`, 'i');
|
|
75
|
-
const match = text.match(regex);
|
|
76
|
-
return match?.[1]?.trim() || "";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseList(text: string): string[] {
|
|
80
|
-
if (!text) return [];
|
|
81
|
-
return text
|
|
82
|
-
.split('\n')
|
|
83
|
-
.map(line => line.replace(/^[-*•]\s*/, '').trim())
|
|
84
|
-
.filter(line => line.length > 0);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function parseUIElements(text: string): DetectedElement[] {
|
|
88
|
-
if (!text) return [];
|
|
89
|
-
|
|
90
|
-
const elements: DetectedElement[] = [];
|
|
91
|
-
const lines = text.split('\n').filter(line => line.trim());
|
|
92
|
-
|
|
93
|
-
for (const line of lines) {
|
|
94
|
-
const coordMatch = line.match(/(\d+),\s*(\d+).*?(\d+)x(\d+)|x:\s*(\d+).*?y:\s*(\d+).*?w:\s*(\d+).*?h:\s*(\d+)/i);
|
|
95
|
-
if (coordMatch) {
|
|
96
|
-
const [, x1, y1, w1, h1, x2, y2, w2, h2] = coordMatch;
|
|
97
|
-
const x = parseInt(x1 || x2 || "0");
|
|
98
|
-
const y = parseInt(y1 || y2 || "0");
|
|
99
|
-
const width = parseInt(w1 || w2 || "0");
|
|
100
|
-
const height = parseInt(h1 || h2 || "0");
|
|
101
|
-
|
|
102
|
-
if (!isNaN(x) && !isNaN(y) && !isNaN(width) && !isNaN(height)) {
|
|
103
|
-
elements.push({
|
|
104
|
-
type: extractElementType(line),
|
|
105
|
-
location: { x, y, width, height },
|
|
106
|
-
properties: { description: line.trim() }
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return elements;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function extractElementType(line: string): string {
|
|
116
|
-
const types = ["button", "input", "link", "image", "text", "menu", "modal", "form", "icon"];
|
|
117
|
-
const lowerLine = line.toLowerCase();
|
|
118
|
-
|
|
119
|
-
for (const type of types) {
|
|
120
|
-
if (lowerLine.includes(type)) {
|
|
121
|
-
return type;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return "element";
|
|
126
|
-
}
|