@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.
Files changed (128) hide show
  1. package/README.md +261 -19
  2. package/bin/human-mcp.js +2 -0
  3. package/dist/index.js +65180 -1698
  4. package/package.json +19 -2
  5. package/.claude/agents/code-reviewer.md +0 -140
  6. package/.claude/agents/database-admin.md +0 -86
  7. package/.claude/agents/debugger.md +0 -119
  8. package/.claude/agents/docs-manager.md +0 -113
  9. package/.claude/agents/git-manager.md +0 -59
  10. package/.claude/agents/planner-researcher.md +0 -97
  11. package/.claude/agents/project-manager.md +0 -113
  12. package/.claude/agents/tester.md +0 -95
  13. package/.claude/commands/cook.md +0 -7
  14. package/.claude/commands/debug.md +0 -10
  15. package/.claude/commands/docs/init.md +0 -11
  16. package/.claude/commands/docs/update.md +0 -11
  17. package/.claude/commands/fix/ci.md +0 -8
  18. package/.claude/commands/fix/fast.md +0 -5
  19. package/.claude/commands/fix/hard.md +0 -7
  20. package/.claude/commands/fix/test.md +0 -16
  21. package/.claude/commands/git/cm.md +0 -5
  22. package/.claude/commands/git/cp.md +0 -4
  23. package/.claude/commands/plan/ci.md +0 -12
  24. package/.claude/commands/plan/two.md +0 -13
  25. package/.claude/commands/plan.md +0 -10
  26. package/.claude/commands/test.md +0 -7
  27. package/.claude/commands/watzup.md +0 -8
  28. package/.claude/hooks/telegram_notify.sh +0 -136
  29. package/.claude/send-discord.sh +0 -64
  30. package/.claude/settings.json +0 -7
  31. package/.claude/statusline.sh +0 -143
  32. package/.dockerignore +0 -81
  33. package/.env.example +0 -44
  34. package/.github/workflows/publish.yml +0 -88
  35. package/.opencode/agent/code-reviewer.md +0 -142
  36. package/.opencode/agent/debugger.md +0 -74
  37. package/.opencode/agent/docs-manager.md +0 -119
  38. package/.opencode/agent/git-manager.md +0 -60
  39. package/.opencode/agent/planner-researcher.md +0 -100
  40. package/.opencode/agent/project-manager.md +0 -113
  41. package/.opencode/agent/system-architecture.md +0 -200
  42. package/.opencode/agent/tester.md +0 -96
  43. package/.opencode/agent/ui-ux-developer.md +0 -97
  44. package/.opencode/command/cook.md +0 -7
  45. package/.opencode/command/debug.md +0 -10
  46. package/.opencode/command/fix/ci.md +0 -8
  47. package/.opencode/command/fix/fast.md +0 -5
  48. package/.opencode/command/fix/hard.md +0 -7
  49. package/.opencode/command/fix/test.md +0 -16
  50. package/.opencode/command/git/cm.md +0 -5
  51. package/.opencode/command/git/cp.md +0 -4
  52. package/.opencode/command/plan/ci.md +0 -12
  53. package/.opencode/command/plan/two.md +0 -13
  54. package/.opencode/command/plan.md +0 -10
  55. package/.opencode/command/test.md +0 -7
  56. package/.opencode/command/watzup.md +0 -8
  57. package/.releaserc.json +0 -26
  58. package/.serena/project.yml +0 -68
  59. package/CHANGELOG.md +0 -62
  60. package/CLAUDE.md +0 -141
  61. package/DEPLOYMENT.md +0 -329
  62. package/Dockerfile +0 -52
  63. package/QUICKSTART.md +0 -97
  64. package/bun.lock +0 -1872
  65. package/bunfig.toml +0 -15
  66. package/docker-compose.yaml +0 -128
  67. package/docs/README.md +0 -51
  68. package/docs/codebase-structure-architecture-code-standards.md +0 -428
  69. package/docs/codebase-summary.md +0 -321
  70. package/docs/project-overview-pdr.md +0 -286
  71. package/docs/project-roadmap.md +0 -494
  72. package/examples/debugging-session.ts +0 -96
  73. package/human-mcp.png +0 -0
  74. package/inspector-wrapper.mjs +0 -33
  75. package/plans/001-streamable-http-transport-plan.md +0 -905
  76. package/plans/002-sse-fallback-http-transport-plan.md +0 -161
  77. package/plans/003-fix-test-infrastructure-and-ci-plan.md +0 -699
  78. package/plans/003-http-transport-local-file-access-plan.md +0 -880
  79. package/plans/004-fix-typescript-compilation-errors-plan.md +0 -388
  80. package/plans/005-comprehensive-test-infrastructure-fix-plan.md +0 -854
  81. package/plans/templates/bug-fix-template.md +0 -69
  82. package/plans/templates/feature-implementation-template.md +0 -84
  83. package/plans/templates/refactor-template.md +0 -82
  84. package/plans/templates/template-usage-guide.md +0 -58
  85. package/src/index.ts +0 -49
  86. package/src/prompts/debugging-prompts.ts +0 -149
  87. package/src/prompts/index.ts +0 -55
  88. package/src/resources/documentation.ts +0 -316
  89. package/src/resources/index.ts +0 -49
  90. package/src/server.ts +0 -36
  91. package/src/tools/eyes/index.ts +0 -225
  92. package/src/tools/eyes/processors/gif.ts +0 -137
  93. package/src/tools/eyes/processors/image.ts +0 -213
  94. package/src/tools/eyes/processors/video.ts +0 -135
  95. package/src/tools/eyes/schemas.ts +0 -51
  96. package/src/tools/eyes/utils/formatters.ts +0 -126
  97. package/src/tools/eyes/utils/gemini-client.ts +0 -73
  98. package/src/transports/http/file-interceptor.ts +0 -134
  99. package/src/transports/http/middleware.ts +0 -46
  100. package/src/transports/http/routes.ts +0 -297
  101. package/src/transports/http/server.ts +0 -116
  102. package/src/transports/http/session.ts +0 -93
  103. package/src/transports/http/sse-routes.ts +0 -210
  104. package/src/transports/index.ts +0 -36
  105. package/src/transports/stdio.ts +0 -7
  106. package/src/transports/types.ts +0 -50
  107. package/src/types/index.ts +0 -41
  108. package/src/utils/cloudflare-r2.ts +0 -107
  109. package/src/utils/config.ts +0 -123
  110. package/src/utils/errors.ts +0 -40
  111. package/src/utils/logger.ts +0 -49
  112. package/tests/integration/http-transport-files.test.ts +0 -190
  113. package/tests/integration/server.test.ts +0 -27
  114. package/tests/integration/sse-transport.test.ts +0 -142
  115. package/tests/setup.ts +0 -55
  116. package/tests/types/api-responses.ts +0 -35
  117. package/tests/types/test-types.ts +0 -105
  118. package/tests/unit/cloudflare-r2.test.ts +0 -118
  119. package/tests/unit/config.test.ts +0 -40
  120. package/tests/unit/eyes-analyze.test.ts +0 -150
  121. package/tests/unit/formatters.test.ts +0 -85
  122. package/tests/unit/sse-routes.test.ts +0 -92
  123. package/tests/utils/error-scenarios.ts +0 -198
  124. package/tests/utils/index.ts +0 -3
  125. package/tests/utils/mock-helpers.ts +0 -99
  126. package/tests/utils/test-data-generators.ts +0 -217
  127. package/tests/utils/test-server-manager.ts +0 -172
  128. 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
- }