maistro 1.0.390

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 (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +107 -0
  3. package/dist/app.d.ts +247 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +4971 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/buildInfo.d.ts +5 -0
  8. package/dist/buildInfo.d.ts.map +1 -0
  9. package/dist/buildInfo.js +2 -0
  10. package/dist/buildInfo.js.map +1 -0
  11. package/dist/caffeinate.d.ts +72 -0
  12. package/dist/caffeinate.d.ts.map +1 -0
  13. package/dist/caffeinate.js +258 -0
  14. package/dist/caffeinate.js.map +1 -0
  15. package/dist/claudePath.d.ts +10 -0
  16. package/dist/claudePath.d.ts.map +1 -0
  17. package/dist/claudePath.js +34 -0
  18. package/dist/claudePath.js.map +1 -0
  19. package/dist/clipboard.d.ts +44 -0
  20. package/dist/clipboard.d.ts.map +1 -0
  21. package/dist/clipboard.js +442 -0
  22. package/dist/clipboard.js.map +1 -0
  23. package/dist/config.d.ts +211 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +933 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/constants.d.ts +50 -0
  28. package/dist/constants.d.ts.map +1 -0
  29. package/dist/constants.js +81 -0
  30. package/dist/constants.js.map +1 -0
  31. package/dist/contextBuilder.d.ts +38 -0
  32. package/dist/contextBuilder.d.ts.map +1 -0
  33. package/dist/contextBuilder.js +113 -0
  34. package/dist/contextBuilder.js.map +1 -0
  35. package/dist/dependencyDetector.d.ts +57 -0
  36. package/dist/dependencyDetector.d.ts.map +1 -0
  37. package/dist/dependencyDetector.js +505 -0
  38. package/dist/dependencyDetector.js.map +1 -0
  39. package/dist/executor.d.ts +83 -0
  40. package/dist/executor.d.ts.map +1 -0
  41. package/dist/executor.js +583 -0
  42. package/dist/executor.js.map +1 -0
  43. package/dist/git.d.ts +85 -0
  44. package/dist/git.d.ts.map +1 -0
  45. package/dist/git.js +283 -0
  46. package/dist/git.js.map +1 -0
  47. package/dist/imageManager.d.ts +161 -0
  48. package/dist/imageManager.d.ts.map +1 -0
  49. package/dist/imageManager.js +674 -0
  50. package/dist/imageManager.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +437 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/input-visual-test.d.ts +9 -0
  56. package/dist/input-visual-test.d.ts.map +1 -0
  57. package/dist/input-visual-test.js +108 -0
  58. package/dist/input-visual-test.js.map +1 -0
  59. package/dist/inputBox.d.ts +228 -0
  60. package/dist/inputBox.d.ts.map +1 -0
  61. package/dist/inputBox.js +966 -0
  62. package/dist/inputBox.js.map +1 -0
  63. package/dist/logger.d.ts +136 -0
  64. package/dist/logger.d.ts.map +1 -0
  65. package/dist/logger.js +347 -0
  66. package/dist/logger.js.map +1 -0
  67. package/dist/orchestrator.d.ts +149 -0
  68. package/dist/orchestrator.d.ts.map +1 -0
  69. package/dist/orchestrator.js +821 -0
  70. package/dist/orchestrator.js.map +1 -0
  71. package/dist/planner.d.ts +86 -0
  72. package/dist/planner.d.ts.map +1 -0
  73. package/dist/planner.js +830 -0
  74. package/dist/planner.js.map +1 -0
  75. package/dist/pty-test-runner.d.ts +87 -0
  76. package/dist/pty-test-runner.d.ts.map +1 -0
  77. package/dist/pty-test-runner.js +721 -0
  78. package/dist/pty-test-runner.js.map +1 -0
  79. package/dist/screen.d.ts +44 -0
  80. package/dist/screen.d.ts.map +1 -0
  81. package/dist/screen.js +152 -0
  82. package/dist/screen.js.map +1 -0
  83. package/dist/taskQueue.d.ts +70 -0
  84. package/dist/taskQueue.d.ts.map +1 -0
  85. package/dist/taskQueue.js +282 -0
  86. package/dist/taskQueue.js.map +1 -0
  87. package/dist/tui-test-harness.d.ts +216 -0
  88. package/dist/tui-test-harness.d.ts.map +1 -0
  89. package/dist/tui-test-harness.js +527 -0
  90. package/dist/tui-test-harness.js.map +1 -0
  91. package/dist/types.d.ts +257 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +46 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/ui-visual-test.d.ts +15 -0
  96. package/dist/ui-visual-test.d.ts.map +1 -0
  97. package/dist/ui-visual-test.js +141 -0
  98. package/dist/ui-visual-test.js.map +1 -0
  99. package/dist/ui.d.ts +272 -0
  100. package/dist/ui.d.ts.map +1 -0
  101. package/dist/ui.js +1531 -0
  102. package/dist/ui.js.map +1 -0
  103. package/dist/validator.d.ts +53 -0
  104. package/dist/validator.d.ts.map +1 -0
  105. package/dist/validator.js +491 -0
  106. package/dist/validator.js.map +1 -0
  107. package/dist/versionCheck.d.ts +63 -0
  108. package/dist/versionCheck.d.ts.map +1 -0
  109. package/dist/versionCheck.js +261 -0
  110. package/dist/versionCheck.js.map +1 -0
  111. package/package.json +62 -0
@@ -0,0 +1,674 @@
1
+ import { mkdir, copyFile, unlink, stat, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, basename, extname } from 'node:path';
4
+ import { loadState, saveState } from './taskQueue.js';
5
+ import { DEFAULT_CONFIG } from './types.js';
6
+ import { getLogger } from './logger.js';
7
+ /**
8
+ * Supported image extensions
9
+ */
10
+ const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
11
+ /**
12
+ * MIME types for supported image formats
13
+ */
14
+ const MIME_TYPES = {
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.webp': 'image/webp',
20
+ '.bmp': 'image/bmp',
21
+ '.svg': 'image/svg+xml',
22
+ };
23
+ /**
24
+ * Get the images directory path for a project
25
+ */
26
+ export function getImagesDir(projectPath) {
27
+ return join(projectPath, '.maistro', 'images');
28
+ }
29
+ /**
30
+ * Get the state file path for a project
31
+ */
32
+ function getStatePath(projectPath) {
33
+ return join(projectPath, DEFAULT_CONFIG.statePath);
34
+ }
35
+ /**
36
+ * Sanitize an image ID for use as a filename
37
+ * - Lowercase
38
+ * - Replace spaces and special chars with hyphens
39
+ * - Remove consecutive hyphens
40
+ */
41
+ export function sanitizeImageId(id) {
42
+ return id
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9-_]/g, '-')
45
+ .replace(/-+/g, '-')
46
+ .replace(/^-|-$/g, '');
47
+ }
48
+ /**
49
+ * Check if a string looks like an image file path
50
+ */
51
+ export function looksLikeImagePath(input) {
52
+ const ext = extname(input).toLowerCase();
53
+ return IMAGE_EXTENSIONS.includes(ext);
54
+ }
55
+ /**
56
+ * Check if a string looks like an image URL
57
+ * Accepts any HTTP/HTTPS URL that:
58
+ * - Has an image extension in the path (before query params)
59
+ * - Includes common image-related path segments
60
+ * - Is from known image hosting services
61
+ * - Has an image extension anywhere in the URL (including query params)
62
+ */
63
+ export function looksLikeImageUrl(input) {
64
+ try {
65
+ const url = new URL(input);
66
+ if (!['http:', 'https:'].includes(url.protocol)) {
67
+ return false;
68
+ }
69
+ const pathname = url.pathname.toLowerCase();
70
+ const fullUrl = input.toLowerCase();
71
+ // Check if path ends with image extension
72
+ if (IMAGE_EXTENSIONS.some(ext => pathname.endsWith(ext))) {
73
+ return true;
74
+ }
75
+ // Check if URL (including query params) contains image extension
76
+ // This handles URLs like: image.png?token=xxx or ?file=photo.jpg
77
+ if (IMAGE_EXTENSIONS.some(ext => fullUrl.includes(ext))) {
78
+ return true;
79
+ }
80
+ // Check common image-related path patterns
81
+ if (pathname.includes('/image') || pathname.includes('/photo') ||
82
+ pathname.includes('/picture') || pathname.includes('/media') ||
83
+ pathname.includes('/upload') || pathname.includes('/asset')) {
84
+ return true;
85
+ }
86
+ // Check known image hosting services
87
+ const hostname = url.hostname.toLowerCase();
88
+ const imageHosts = [
89
+ 'imgur.com', 'i.imgur.com',
90
+ 'cloudinary.com', 'res.cloudinary.com',
91
+ 'unsplash.com', 'images.unsplash.com',
92
+ 'pexels.com', 'images.pexels.com',
93
+ 'flickr.com', 'staticflickr.com',
94
+ 'giphy.com', 'media.giphy.com',
95
+ 'gravatar.com',
96
+ 'googleusercontent.com',
97
+ 'githubusercontent.com', 'raw.githubusercontent.com',
98
+ 'drive.google.com',
99
+ 'dropbox.com', 'dl.dropboxusercontent.com',
100
+ 'ibb.co', 'i.ibb.co',
101
+ 'imgbb.com',
102
+ 'postimg.cc', 'i.postimg.cc',
103
+ 'prnt.sc', 'prntscr.com',
104
+ 's3.amazonaws.com',
105
+ 'blob.core.windows.net',
106
+ 'storage.googleapis.com',
107
+ ];
108
+ if (imageHosts.some(host => hostname === host || hostname.endsWith('.' + host))) {
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ /**
118
+ * Extract image placeholders from text
119
+ * Supports:
120
+ * - [image:name], [img:name] - generic image references
121
+ * - [task-X-image-Y] - task-specific image references (e.g., [task-1-image-1])
122
+ */
123
+ export function extractImagePlaceholders(text) {
124
+ const patterns = [
125
+ /\[image:([a-zA-Z0-9_-]+)\]/gi,
126
+ /\[img:([a-zA-Z0-9_-]+)\]/gi,
127
+ /\[(task-[a-zA-Z0-9]+-image-\d+)\]/gi, // task-specific format
128
+ ];
129
+ const refs = new Set();
130
+ for (const pattern of patterns) {
131
+ let match;
132
+ while ((match = pattern.exec(text)) !== null) {
133
+ refs.add(match[1].toLowerCase());
134
+ }
135
+ }
136
+ return Array.from(refs);
137
+ }
138
+ /**
139
+ * Extract image URLs from text
140
+ * Returns an array of unique image URLs found in the text
141
+ */
142
+ export function extractImageUrls(text) {
143
+ // Match URLs that look like images
144
+ // Pattern matches http:// or https:// followed by URL characters
145
+ const urlPattern = /https?:\/\/[^\s<>"')\]]+/gi;
146
+ const matches = text.match(urlPattern) || [];
147
+ const urls = new Set();
148
+ for (const match of matches) {
149
+ // Clean up trailing punctuation that might have been captured
150
+ let url = match.replace(/[.,;:!?]+$/, '');
151
+ // Also remove trailing parentheses if not balanced
152
+ if (url.endsWith(')') && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
153
+ url = url.slice(0, -1);
154
+ }
155
+ if (looksLikeImageUrl(url)) {
156
+ urls.add(url);
157
+ }
158
+ }
159
+ return Array.from(urls);
160
+ }
161
+ /**
162
+ * Process a task description to download image URLs and replace them with placeholders
163
+ * Returns the updated description and any image metadata that was created
164
+ */
165
+ export async function processDescriptionImageUrls(projectPath, description, taskId) {
166
+ const logger = getLogger();
167
+ const urls = extractImageUrls(description);
168
+ const downloadedImages = [];
169
+ const errors = [];
170
+ let updatedDescription = description;
171
+ if (urls.length === 0) {
172
+ return { updatedDescription, downloadedImages, errors };
173
+ }
174
+ logger?.info('Processing image URLs in description', {
175
+ taskId,
176
+ urlCount: urls.length,
177
+ urls,
178
+ });
179
+ // Get the next available image index for this task
180
+ let imageIndex = await getNextTaskImageIndex(projectPath, taskId);
181
+ for (const url of urls) {
182
+ const imageId = getTaskImageId(taskId, imageIndex);
183
+ const result = await addImageFromUrl(projectPath, url, imageId);
184
+ if (result.success && result.metadata) {
185
+ // Replace the URL with the placeholder
186
+ const placeholder = getImagePlaceholder(taskId, imageIndex);
187
+ updatedDescription = updatedDescription.replace(url, placeholder);
188
+ downloadedImages.push(result.metadata);
189
+ imageIndex++;
190
+ logger?.info('Image downloaded and replaced', {
191
+ url,
192
+ imageId,
193
+ placeholder,
194
+ });
195
+ }
196
+ else {
197
+ const error = `Failed to download image from ${url}: ${result.error}`;
198
+ errors.push(error);
199
+ logger?.warn('Failed to download image', { url, error: result.error });
200
+ }
201
+ }
202
+ return { updatedDescription, downloadedImages, errors };
203
+ }
204
+ /**
205
+ * Get the storage path for an image
206
+ */
207
+ export function getImageStoragePath(imageId, extension) {
208
+ return `${sanitizeImageId(imageId)}${extension}`;
209
+ }
210
+ /**
211
+ * Get the absolute path to an image file
212
+ */
213
+ export function getImageAbsolutePath(projectPath, storagePath) {
214
+ return join(getImagesDir(projectPath), storagePath);
215
+ }
216
+ /**
217
+ * Add an image from a local file path
218
+ */
219
+ export async function addImageFromFile(projectPath, filePath, imageId, description) {
220
+ try {
221
+ // Validate file exists
222
+ if (!existsSync(filePath)) {
223
+ return { success: false, error: `File not found: ${filePath}` };
224
+ }
225
+ // Validate extension
226
+ const ext = extname(filePath).toLowerCase();
227
+ if (!IMAGE_EXTENSIONS.includes(ext)) {
228
+ return { success: false, error: `Unsupported image format: ${ext}` };
229
+ }
230
+ // Get file stats
231
+ const stats = await stat(filePath);
232
+ // Ensure images directory exists
233
+ const imagesDir = getImagesDir(projectPath);
234
+ await mkdir(imagesDir, { recursive: true });
235
+ // Create storage path
236
+ const storagePath = getImageStoragePath(imageId, ext);
237
+ const destPath = join(imagesDir, storagePath);
238
+ // Copy file
239
+ await copyFile(filePath, destPath);
240
+ // Create metadata
241
+ const metadata = {
242
+ id: sanitizeImageId(imageId),
243
+ originalName: basename(filePath),
244
+ storagePath,
245
+ mimeType: MIME_TYPES[ext] || 'application/octet-stream',
246
+ size: stats.size,
247
+ description,
248
+ source: 'file',
249
+ createdAt: new Date().toISOString(),
250
+ };
251
+ // Save to state
252
+ await saveImageMetadata(projectPath, metadata);
253
+ return { success: true, metadata };
254
+ }
255
+ catch (err) {
256
+ return { success: false, error: String(err) };
257
+ }
258
+ }
259
+ /**
260
+ * Add an image from a URL (download)
261
+ */
262
+ export async function addImageFromUrl(projectPath, url, imageId, description) {
263
+ try {
264
+ // Validate URL
265
+ const parsedUrl = new URL(url);
266
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
267
+ return { success: false, error: 'Only HTTP/HTTPS URLs are supported' };
268
+ }
269
+ // Download the image
270
+ const response = await fetch(url);
271
+ if (!response.ok) {
272
+ return { success: false, error: `Failed to download: ${response.status} ${response.statusText}` };
273
+ }
274
+ // Get content type and determine extension
275
+ const contentType = response.headers.get('content-type') || '';
276
+ let ext = '.png'; // Default
277
+ for (const [extension, mime] of Object.entries(MIME_TYPES)) {
278
+ if (contentType.includes(mime.split('/')[1])) {
279
+ ext = extension;
280
+ break;
281
+ }
282
+ }
283
+ // If content-type didn't help, try URL path
284
+ const urlExt = extname(parsedUrl.pathname).toLowerCase();
285
+ if (IMAGE_EXTENSIONS.includes(urlExt)) {
286
+ ext = urlExt;
287
+ }
288
+ // Get image data
289
+ const buffer = Buffer.from(await response.arrayBuffer());
290
+ // Ensure images directory exists
291
+ const imagesDir = getImagesDir(projectPath);
292
+ await mkdir(imagesDir, { recursive: true });
293
+ // Create storage path
294
+ const storagePath = getImageStoragePath(imageId, ext);
295
+ const destPath = join(imagesDir, storagePath);
296
+ // Write file
297
+ await writeFile(destPath, buffer);
298
+ // Create metadata
299
+ const metadata = {
300
+ id: sanitizeImageId(imageId),
301
+ originalName: basename(parsedUrl.pathname) || `${imageId}${ext}`,
302
+ storagePath,
303
+ mimeType: MIME_TYPES[ext] || contentType,
304
+ size: buffer.length,
305
+ description,
306
+ source: 'url',
307
+ sourceUrl: url,
308
+ createdAt: new Date().toISOString(),
309
+ };
310
+ // Save to state
311
+ await saveImageMetadata(projectPath, metadata);
312
+ return { success: true, metadata };
313
+ }
314
+ catch (err) {
315
+ return { success: false, error: String(err) };
316
+ }
317
+ }
318
+ /**
319
+ * Get all images for a project
320
+ */
321
+ export async function getImages(projectPath) {
322
+ const statePath = getStatePath(projectPath);
323
+ const state = await loadState(statePath);
324
+ return state?.images || {};
325
+ }
326
+ /**
327
+ * Get a single image by ID
328
+ */
329
+ export async function getImage(projectPath, imageId) {
330
+ const images = await getImages(projectPath);
331
+ return images[sanitizeImageId(imageId)] || null;
332
+ }
333
+ /**
334
+ * Delete an image
335
+ */
336
+ export async function deleteImage(projectPath, imageId) {
337
+ try {
338
+ const sanitizedId = sanitizeImageId(imageId);
339
+ const images = await getImages(projectPath);
340
+ const metadata = images[sanitizedId];
341
+ if (!metadata) {
342
+ return { success: false, error: `Image not found: ${imageId}` };
343
+ }
344
+ // Delete the file
345
+ const filePath = getImageAbsolutePath(projectPath, metadata.storagePath);
346
+ if (existsSync(filePath)) {
347
+ await unlink(filePath);
348
+ }
349
+ // Remove from state
350
+ await removeImageMetadata(projectPath, sanitizedId);
351
+ return { success: true };
352
+ }
353
+ catch (err) {
354
+ return { success: false, error: String(err) };
355
+ }
356
+ }
357
+ /**
358
+ * Save image metadata to project state
359
+ */
360
+ async function saveImageMetadata(projectPath, metadata) {
361
+ const statePath = getStatePath(projectPath);
362
+ const state = await loadState(statePath);
363
+ if (!state) {
364
+ throw new Error('No project state found. Initialize a project first.');
365
+ }
366
+ if (!state.images) {
367
+ state.images = {};
368
+ }
369
+ state.images[metadata.id] = metadata;
370
+ await saveState(statePath, state);
371
+ }
372
+ /**
373
+ * Remove image metadata from project state
374
+ */
375
+ async function removeImageMetadata(projectPath, imageId) {
376
+ const statePath = getStatePath(projectPath);
377
+ const state = await loadState(statePath);
378
+ if (!state || !state.images)
379
+ return;
380
+ delete state.images[imageId];
381
+ await saveState(statePath, state);
382
+ }
383
+ /**
384
+ * Format image size for display
385
+ */
386
+ export function formatImageSize(bytes) {
387
+ if (bytes < 1024) {
388
+ return `${bytes}B`;
389
+ }
390
+ else if (bytes < 1024 * 1024) {
391
+ return `${Math.round(bytes / 1024)}KB`;
392
+ }
393
+ else {
394
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
395
+ }
396
+ }
397
+ /**
398
+ * Generate a task-specific image ID
399
+ * Format: task-{taskId}-image-{index}
400
+ */
401
+ export function getTaskImageId(taskId, imageIndex) {
402
+ return `task-${taskId}-image-${imageIndex}`;
403
+ }
404
+ /**
405
+ * Generate a placeholder string for a task image
406
+ * Format: [task-{taskId}-image-{index}]
407
+ */
408
+ export function getImagePlaceholder(taskId, imageIndex) {
409
+ return `[${getTaskImageId(taskId, imageIndex)}]`;
410
+ }
411
+ /**
412
+ * Save an image for a specific task
413
+ * Convenience wrapper that uses task-specific naming convention
414
+ */
415
+ export async function saveTaskImage(projectPath, sourcePath, taskId, imageIndex, description) {
416
+ const imageId = getTaskImageId(taskId, imageIndex);
417
+ const result = await addImageFromFile(projectPath, sourcePath, imageId, description);
418
+ if (result.success) {
419
+ return {
420
+ ...result,
421
+ placeholder: getImagePlaceholder(taskId, imageIndex),
422
+ };
423
+ }
424
+ return result;
425
+ }
426
+ /**
427
+ * Get the next available image index for a task
428
+ */
429
+ export async function getNextTaskImageIndex(projectPath, taskId) {
430
+ const images = await getImages(projectPath);
431
+ const prefix = `task-${taskId}-image-`;
432
+ let maxIndex = 0;
433
+ for (const id of Object.keys(images)) {
434
+ if (id.startsWith(prefix)) {
435
+ const indexStr = id.slice(prefix.length);
436
+ const index = parseInt(indexStr, 10);
437
+ if (!isNaN(index) && index >= maxIndex) {
438
+ maxIndex = index + 1;
439
+ }
440
+ }
441
+ }
442
+ return maxIndex;
443
+ }
444
+ /**
445
+ * Magic bytes for common image formats
446
+ * These are the first few bytes that identify image file types
447
+ */
448
+ const IMAGE_MAGIC_BYTES = {
449
+ png: { bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], ext: '.png', mime: 'image/png' },
450
+ jpeg: { bytes: [0xFF, 0xD8, 0xFF], ext: '.jpg', mime: 'image/jpeg' },
451
+ gif: { bytes: [0x47, 0x49, 0x46, 0x38], ext: '.gif', mime: 'image/gif' },
452
+ webp: { bytes: [0x52, 0x49, 0x46, 0x46], ext: '.webp', mime: 'image/webp' }, // RIFF header, followed by WEBP
453
+ bmp: { bytes: [0x42, 0x4D], ext: '.bmp', mime: 'image/bmp' },
454
+ };
455
+ /**
456
+ * Detect if a string contains base64-encoded image data
457
+ *
458
+ * Supports:
459
+ * - Raw base64 data (detects by magic bytes after decoding)
460
+ * - Data URLs (data:image/png;base64,...)
461
+ * - iTerm2 inline image sequences (ESC]1337;File=...)
462
+ *
463
+ * @param input The string to check for image data
464
+ * @returns Detection result with buffer and format info if found
465
+ */
466
+ export function detectPastedImage(input) {
467
+ const logger = getLogger();
468
+ // Skip short strings and obvious non-image content
469
+ if (!input || input.length < 20) {
470
+ return { detected: false };
471
+ }
472
+ // Check for data URL format: data:image/TYPE;base64,DATA
473
+ const dataUrlMatch = input.match(/^data:image\/([a-z+]+);base64,(.+)$/i);
474
+ if (dataUrlMatch) {
475
+ const mimeSubtype = dataUrlMatch[1].toLowerCase();
476
+ const base64Data = dataUrlMatch[2];
477
+ try {
478
+ const buffer = Buffer.from(base64Data, 'base64');
479
+ if (buffer.length < 8) {
480
+ return { detected: false, error: 'Decoded data too small' };
481
+ }
482
+ // Find matching format
483
+ const format = detectFormatFromBuffer(buffer);
484
+ if (format) {
485
+ return {
486
+ detected: true,
487
+ buffer,
488
+ format: format.name,
489
+ extension: format.ext,
490
+ mimeType: format.mime,
491
+ };
492
+ }
493
+ // Trust the data URL MIME type even if magic bytes don't match exactly
494
+ const ext = mimeSubtype === 'jpeg' || mimeSubtype === 'jpg' ? '.jpg' : `.${mimeSubtype}`;
495
+ return {
496
+ detected: true,
497
+ buffer,
498
+ format: mimeSubtype,
499
+ extension: ext,
500
+ mimeType: `image/${mimeSubtype}`,
501
+ };
502
+ }
503
+ catch (err) {
504
+ logger?.warn('detectPastedImage: failed to decode data URL', { error: String(err) });
505
+ return { detected: false, error: 'Failed to decode base64 data' };
506
+ }
507
+ }
508
+ // Check for iTerm2 inline image protocol: ESC]1337;File=...
509
+ // Format: \x1b]1337;File=[params]:BASE64_DATA\x07 or \x1b]1337;File=[params]:BASE64_DATA\x1b\\
510
+ const iterm2Match = input.match(/\x1b\]1337;File=[^:]*:([A-Za-z0-9+/=]+)(?:\x07|\x1b\\)/);
511
+ if (iterm2Match) {
512
+ const base64Data = iterm2Match[1];
513
+ try {
514
+ const buffer = Buffer.from(base64Data, 'base64');
515
+ if (buffer.length < 8) {
516
+ return { detected: false, error: 'Decoded data too small' };
517
+ }
518
+ const format = detectFormatFromBuffer(buffer);
519
+ if (format) {
520
+ return {
521
+ detected: true,
522
+ buffer,
523
+ format: format.name,
524
+ extension: format.ext,
525
+ mimeType: format.mime,
526
+ };
527
+ }
528
+ // Default to PNG if format detection fails
529
+ return {
530
+ detected: true,
531
+ buffer,
532
+ format: 'png',
533
+ extension: '.png',
534
+ mimeType: 'image/png',
535
+ };
536
+ }
537
+ catch (err) {
538
+ logger?.warn('detectPastedImage: failed to decode iTerm2 data', { error: String(err) });
539
+ return { detected: false, error: 'Failed to decode iTerm2 image data' };
540
+ }
541
+ }
542
+ // Check for raw base64 data (must look like valid base64 and decode to valid image)
543
+ // Only consider if the string is mostly base64 characters
544
+ const trimmed = input.trim();
545
+ if (/^[A-Za-z0-9+/=]+$/.test(trimmed) && trimmed.length >= 100) {
546
+ try {
547
+ const buffer = Buffer.from(trimmed, 'base64');
548
+ if (buffer.length < 8) {
549
+ return { detected: false };
550
+ }
551
+ const format = detectFormatFromBuffer(buffer);
552
+ if (format) {
553
+ return {
554
+ detected: true,
555
+ buffer,
556
+ format: format.name,
557
+ extension: format.ext,
558
+ mimeType: format.mime,
559
+ };
560
+ }
561
+ }
562
+ catch {
563
+ // Not valid base64
564
+ }
565
+ }
566
+ return { detected: false };
567
+ }
568
+ /**
569
+ * Detect image format from buffer by checking magic bytes
570
+ */
571
+ function detectFormatFromBuffer(buffer) {
572
+ for (const [name, { bytes, ext, mime }] of Object.entries(IMAGE_MAGIC_BYTES)) {
573
+ if (buffer.length >= bytes.length) {
574
+ let matches = true;
575
+ for (let i = 0; i < bytes.length; i++) {
576
+ if (buffer[i] !== bytes[i]) {
577
+ matches = false;
578
+ break;
579
+ }
580
+ }
581
+ if (matches) {
582
+ // For WebP, also check for 'WEBP' signature at offset 8
583
+ if (name === 'webp') {
584
+ if (buffer.length >= 12 &&
585
+ buffer[8] === 0x57 && // W
586
+ buffer[9] === 0x45 && // E
587
+ buffer[10] === 0x42 && // B
588
+ buffer[11] === 0x50) { // P
589
+ return { name, ext, mime };
590
+ }
591
+ continue; // Not actually WebP
592
+ }
593
+ return { name, ext, mime };
594
+ }
595
+ }
596
+ }
597
+ return null;
598
+ }
599
+ /**
600
+ * Add an image from pasted base64 data
601
+ *
602
+ * @param projectPath Project directory path
603
+ * @param pastedData The pasted string (base64, data URL, or escape sequence)
604
+ * @param imageId Identifier for the image
605
+ * @param description Optional description
606
+ * @returns Result with success status and metadata
607
+ */
608
+ export async function addImageFromPaste(projectPath, pastedData, imageId, description) {
609
+ const detected = detectPastedImage(pastedData);
610
+ if (!detected.detected || !detected.buffer) {
611
+ return { success: false, error: detected.error || 'No image data detected in pasted content' };
612
+ }
613
+ try {
614
+ // Ensure images directory exists
615
+ const imagesDir = getImagesDir(projectPath);
616
+ await mkdir(imagesDir, { recursive: true });
617
+ // Create storage path
618
+ const ext = detected.extension || '.png';
619
+ const storagePath = getImageStoragePath(imageId, ext);
620
+ const destPath = join(imagesDir, storagePath);
621
+ // Write file
622
+ await writeFile(destPath, detected.buffer);
623
+ // Create metadata
624
+ const metadata = {
625
+ id: sanitizeImageId(imageId),
626
+ originalName: `pasted-image${ext}`,
627
+ storagePath,
628
+ mimeType: detected.mimeType || 'application/octet-stream',
629
+ size: detected.buffer.length,
630
+ description,
631
+ source: 'paste',
632
+ createdAt: new Date().toISOString(),
633
+ };
634
+ // Save to state
635
+ await saveImageMetadata(projectPath, metadata);
636
+ return { success: true, metadata };
637
+ }
638
+ catch (err) {
639
+ return { success: false, error: String(err) };
640
+ }
641
+ }
642
+ /**
643
+ * Check if input looks like it might contain pasted image data
644
+ * This is a quick check before doing full detection
645
+ */
646
+ export function mightBePastedImage(input) {
647
+ if (!input || input.length < 50) {
648
+ return false;
649
+ }
650
+ // Check for data URL
651
+ if (input.startsWith('data:image/')) {
652
+ return true;
653
+ }
654
+ // Check for iTerm2 sequence
655
+ if (input.includes('\x1b]1337;File=')) {
656
+ return true;
657
+ }
658
+ // Check for long base64-looking string (at least 100 chars of mostly base64 alphabet)
659
+ const trimmed = input.trim();
660
+ if (trimmed.length >= 100 && /^[A-Za-z0-9+/=]+$/.test(trimmed)) {
661
+ return true;
662
+ }
663
+ return false;
664
+ }
665
+ /**
666
+ * Generate a unique image ID for pasted images
667
+ * Format: paste-{timestamp}-{random}
668
+ */
669
+ export function generatePasteImageId() {
670
+ const timestamp = Date.now().toString(36);
671
+ const random = Math.random().toString(36).slice(2, 6);
672
+ return `paste-${timestamp}-${random}`;
673
+ }
674
+ //# sourceMappingURL=imageManager.js.map