@framers/agentos-ext-tool-media-upload 0.1.0 → 0.1.2
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/manifest.json +4 -4
- package/package.json +16 -9
- package/src/MediaUploadTool.ts +0 -213
- package/src/index.ts +0 -66
- package/test/MediaUploadTool.spec.ts +0 -344
- package/tsconfig.json +0 -22
package/manifest.json
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
"$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
|
|
3
3
|
"id": "com.framers.tools.media-upload",
|
|
4
4
|
"name": "Media Upload Tool",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"description": "Upload images, videos, and audio files to the media library for use in social posts. Returns an asset ID that can be referenced in post tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Frame.dev",
|
|
9
9
|
"email": "support@frame.dev",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/framerslab"
|
|
11
11
|
},
|
|
12
|
-
"license": "
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
13
|
"keywords": ["social", "media", "upload", "images", "video", "audio", "asset-management"],
|
|
14
|
-
"agentosVersion": "
|
|
14
|
+
"agentosVersion": ">=0.10.40",
|
|
15
15
|
"categories": ["tool", "social"],
|
|
16
16
|
"platforms": [],
|
|
17
17
|
"sdk": "none",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@framers/agentos-ext-tool-media-upload",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Media upload tool — upload images, videos, and audio to the media library for use in social posts.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,33 +34,40 @@
|
|
|
34
34
|
"url": "https://github.com/jddunn"
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
|
-
"license": "
|
|
37
|
+
"license": "Apache-2.0",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "git+https://github.com/framerslab/agentos-extensions.git",
|
|
41
41
|
"directory": "registry/curated/tools/media-upload"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@framers/agentos": "
|
|
47
|
+
"@framers/agentos": ">=0.10.40"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@framers/agentos": "^0.6.6",
|
|
51
50
|
"@types/node": "^20.12.12",
|
|
52
|
-
"@vitest/coverage-v8": "^
|
|
51
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
53
52
|
"rimraf": "^5.0.7",
|
|
54
53
|
"typescript": "^5.4.5",
|
|
55
|
-
"vitest": "^
|
|
54
|
+
"vitest": "^3.2.7",
|
|
55
|
+
"@framers/agentos": "^0.13.40"
|
|
56
56
|
},
|
|
57
|
+
"homepage": "https://github.com/framerslab/agentos-extensions/tree/master/registry/curated/tools/media-upload#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/framerslab/agentos-extensions/issues"
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"dist",
|
|
63
|
+
"manifest.json"
|
|
64
|
+
],
|
|
57
65
|
"scripts": {
|
|
58
66
|
"build": "tsc",
|
|
59
67
|
"dev": "tsc --watch",
|
|
60
|
-
"test": "vitest run",
|
|
68
|
+
"test": "vitest run --passWithNoTests",
|
|
61
69
|
"test:watch": "vitest",
|
|
62
70
|
"test:coverage": "vitest run --coverage",
|
|
63
|
-
"lint": "eslint src --ext .ts",
|
|
64
71
|
"typecheck": "tsc --noEmit",
|
|
65
72
|
"clean": "rimraf dist"
|
|
66
73
|
}
|
package/src/MediaUploadTool.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview Media Upload Tool — upload files to the media library for social posts.
|
|
4
|
-
*
|
|
5
|
-
* Reads a local file, detects MIME type, and returns an asset ID that can be
|
|
6
|
-
* referenced in other post tools (e.g. multiChannelPost, twitterPost, etc.).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/* ------------------------------------------------------------------ */
|
|
10
|
-
/* Types */
|
|
11
|
-
/* ------------------------------------------------------------------ */
|
|
12
|
-
|
|
13
|
-
export interface MediaUploadInput {
|
|
14
|
-
/** Path to the media file to upload. */
|
|
15
|
-
filePath: string;
|
|
16
|
-
/** Tags for organizing the asset in the media library. */
|
|
17
|
-
tags?: string[];
|
|
18
|
-
/** Agent seed ID (owner). Defaults to current agent if omitted. */
|
|
19
|
-
seedId?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface MediaUploadOutput {
|
|
23
|
-
assetId: string;
|
|
24
|
-
filename: string;
|
|
25
|
-
mimeType: string;
|
|
26
|
-
size: number;
|
|
27
|
-
tags: string[];
|
|
28
|
-
message: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* ------------------------------------------------------------------ */
|
|
32
|
-
/* MIME map */
|
|
33
|
-
/* ------------------------------------------------------------------ */
|
|
34
|
-
|
|
35
|
-
const MIME_MAP: Record<string, string> = {
|
|
36
|
-
'.jpg': 'image/jpeg',
|
|
37
|
-
'.jpeg': 'image/jpeg',
|
|
38
|
-
'.png': 'image/png',
|
|
39
|
-
'.gif': 'image/gif',
|
|
40
|
-
'.webp': 'image/webp',
|
|
41
|
-
'.svg': 'image/svg+xml',
|
|
42
|
-
'.mp4': 'video/mp4',
|
|
43
|
-
'.mov': 'video/quicktime',
|
|
44
|
-
'.webm': 'video/webm',
|
|
45
|
-
'.mp3': 'audio/mpeg',
|
|
46
|
-
'.wav': 'audio/wav',
|
|
47
|
-
'.ogg': 'audio/ogg',
|
|
48
|
-
'.pdf': 'application/pdf',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/* ------------------------------------------------------------------ */
|
|
52
|
-
/* Supported media type sets */
|
|
53
|
-
/* ------------------------------------------------------------------ */
|
|
54
|
-
|
|
55
|
-
const SUPPORTED_IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']);
|
|
56
|
-
const SUPPORTED_VIDEO_EXTS = new Set(['.mp4', '.mov', '.webm']);
|
|
57
|
-
const SUPPORTED_AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg']);
|
|
58
|
-
|
|
59
|
-
/** Maximum file size: 100 MB */
|
|
60
|
-
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
61
|
-
|
|
62
|
-
/* ------------------------------------------------------------------ */
|
|
63
|
-
/* Tool executor callback type */
|
|
64
|
-
/* ------------------------------------------------------------------ */
|
|
65
|
-
|
|
66
|
-
export type ToolExecutorFn = (
|
|
67
|
-
toolName: string,
|
|
68
|
-
args: Record<string, unknown>,
|
|
69
|
-
) => Promise<{ success: boolean; data?: Record<string, unknown>; error?: string }>;
|
|
70
|
-
|
|
71
|
-
/* ------------------------------------------------------------------ */
|
|
72
|
-
/* MediaUploadTool */
|
|
73
|
-
/* ------------------------------------------------------------------ */
|
|
74
|
-
|
|
75
|
-
export class MediaUploadTool {
|
|
76
|
-
readonly id = 'mediaUpload';
|
|
77
|
-
readonly name = 'mediaUpload';
|
|
78
|
-
readonly displayName = 'Upload Media';
|
|
79
|
-
readonly description =
|
|
80
|
-
'Upload an image, video, or audio file to the media library for use in social posts. ' +
|
|
81
|
-
'Returns an asset ID that can be referenced in post tools (multiChannelPost, twitterPost, etc.). ' +
|
|
82
|
-
'Supported formats: JPEG, PNG, GIF, WebP, SVG, MP4, MOV, WebM, MP3, WAV, OGG.';
|
|
83
|
-
readonly category = 'social';
|
|
84
|
-
readonly version = '0.1.0';
|
|
85
|
-
readonly hasSideEffects = true;
|
|
86
|
-
|
|
87
|
-
readonly inputSchema = {
|
|
88
|
-
type: 'object' as const,
|
|
89
|
-
properties: {
|
|
90
|
-
filePath: {
|
|
91
|
-
type: 'string',
|
|
92
|
-
description: 'Absolute or relative path to the media file to upload',
|
|
93
|
-
},
|
|
94
|
-
tags: {
|
|
95
|
-
type: 'array',
|
|
96
|
-
items: { type: 'string' },
|
|
97
|
-
description: 'Tags for organizing the asset in the media library',
|
|
98
|
-
},
|
|
99
|
-
seedId: {
|
|
100
|
-
type: 'string',
|
|
101
|
-
description: 'Agent seed ID (owner). Defaults to current agent if omitted',
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
required: ['filePath'],
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/* -------------------------------------------------------------- */
|
|
108
|
-
/* Tool executor — set by the orchestrator that loads this ext */
|
|
109
|
-
/* -------------------------------------------------------------- */
|
|
110
|
-
|
|
111
|
-
private toolExecutor?: ToolExecutorFn;
|
|
112
|
-
|
|
113
|
-
setToolExecutor(executor: ToolExecutorFn): void {
|
|
114
|
-
this.toolExecutor = executor;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* -------------------------------------------------------------- */
|
|
118
|
-
/* execute() */
|
|
119
|
-
/* -------------------------------------------------------------- */
|
|
120
|
-
|
|
121
|
-
async execute(
|
|
122
|
-
args: MediaUploadInput,
|
|
123
|
-
): Promise<{ success: boolean; data?: MediaUploadOutput; error?: string }> {
|
|
124
|
-
try {
|
|
125
|
-
const fs = await import('fs/promises');
|
|
126
|
-
const nodePath = await import('path');
|
|
127
|
-
const crypto = await import('crypto');
|
|
128
|
-
|
|
129
|
-
// Resolve and validate file path
|
|
130
|
-
const resolvedPath = nodePath.resolve(args.filePath);
|
|
131
|
-
|
|
132
|
-
// Check file exists
|
|
133
|
-
try {
|
|
134
|
-
await fs.access(resolvedPath);
|
|
135
|
-
} catch {
|
|
136
|
-
return {
|
|
137
|
-
success: false,
|
|
138
|
-
error: `File not found: ${resolvedPath}`,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Read file stats
|
|
143
|
-
const stat = await fs.stat(resolvedPath);
|
|
144
|
-
if (!stat.isFile()) {
|
|
145
|
-
return {
|
|
146
|
-
success: false,
|
|
147
|
-
error: `Path is not a file: ${resolvedPath}`,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (stat.size > MAX_FILE_SIZE) {
|
|
152
|
-
return {
|
|
153
|
-
success: false,
|
|
154
|
-
error: `File too large: ${(stat.size / (1024 * 1024)).toFixed(1)} MB exceeds ${MAX_FILE_SIZE / (1024 * 1024)} MB limit`,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Detect MIME type from extension
|
|
159
|
-
const ext = nodePath.extname(resolvedPath).toLowerCase();
|
|
160
|
-
const mimeType = MIME_MAP[ext] ?? 'application/octet-stream';
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
!SUPPORTED_IMAGE_EXTS.has(ext) &&
|
|
164
|
-
!SUPPORTED_VIDEO_EXTS.has(ext) &&
|
|
165
|
-
!SUPPORTED_AUDIO_EXTS.has(ext) &&
|
|
166
|
-
ext !== '.pdf'
|
|
167
|
-
) {
|
|
168
|
-
return {
|
|
169
|
-
success: false,
|
|
170
|
-
error: `Unsupported file type: ${ext}. Supported: ${[
|
|
171
|
-
...SUPPORTED_IMAGE_EXTS,
|
|
172
|
-
...SUPPORTED_VIDEO_EXTS,
|
|
173
|
-
...SUPPORTED_AUDIO_EXTS,
|
|
174
|
-
'.pdf',
|
|
175
|
-
].join(', ')}`,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Read file into buffer
|
|
180
|
-
const buffer = await fs.readFile(resolvedPath);
|
|
181
|
-
const originalName = nodePath.basename(resolvedPath);
|
|
182
|
-
|
|
183
|
-
// Generate asset ID
|
|
184
|
-
const assetId = crypto.randomUUID();
|
|
185
|
-
|
|
186
|
-
// Determine media category for the message
|
|
187
|
-
let mediaCategory = 'file';
|
|
188
|
-
if (SUPPORTED_IMAGE_EXTS.has(ext)) mediaCategory = 'image';
|
|
189
|
-
else if (SUPPORTED_VIDEO_EXTS.has(ext)) mediaCategory = 'video';
|
|
190
|
-
else if (SUPPORTED_AUDIO_EXTS.has(ext)) mediaCategory = 'audio';
|
|
191
|
-
|
|
192
|
-
const tags = args.tags ?? [];
|
|
193
|
-
const sizeKb = (buffer.length / 1024).toFixed(1);
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
success: true,
|
|
197
|
-
data: {
|
|
198
|
-
assetId,
|
|
199
|
-
filename: originalName,
|
|
200
|
-
mimeType,
|
|
201
|
-
size: buffer.length,
|
|
202
|
-
tags,
|
|
203
|
-
message:
|
|
204
|
-
`Media ${mediaCategory} uploaded: ${originalName} (${mimeType}, ${sizeKb} KB). ` +
|
|
205
|
-
`Use assetId "${assetId}" in post tools.`,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
} catch (err: unknown) {
|
|
209
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
210
|
-
return { success: false, error: message };
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* Media Upload Extension Pack — upload media files to the library for social posts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { MediaUploadTool } from './MediaUploadTool.js';
|
|
7
|
-
|
|
8
|
-
/* ------------------------------------------------------------------ */
|
|
9
|
-
/* Extension pack types */
|
|
10
|
-
/* ------------------------------------------------------------------ */
|
|
11
|
-
|
|
12
|
-
export interface ExtensionContext {
|
|
13
|
-
options?: Record<string, unknown>;
|
|
14
|
-
secrets?: Record<string, string>;
|
|
15
|
-
getSecret?: (key: string) => string | undefined;
|
|
16
|
-
logger?: { info: (msg: string) => void };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ExtensionPack {
|
|
20
|
-
name: string;
|
|
21
|
-
version: string;
|
|
22
|
-
descriptors: Array<{
|
|
23
|
-
id: string;
|
|
24
|
-
kind: string;
|
|
25
|
-
priority?: number;
|
|
26
|
-
payload: unknown;
|
|
27
|
-
}>;
|
|
28
|
-
onActivate?: () => Promise<void>;
|
|
29
|
-
onDeactivate?: () => Promise<void>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/* ------------------------------------------------------------------ */
|
|
33
|
-
/* Factory */
|
|
34
|
-
/* ------------------------------------------------------------------ */
|
|
35
|
-
|
|
36
|
-
export function createExtensionPack(context: ExtensionContext): ExtensionPack {
|
|
37
|
-
const tool = new MediaUploadTool();
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
name: '@framers/agentos-ext-tool-media-upload',
|
|
41
|
-
version: '0.1.0',
|
|
42
|
-
descriptors: [
|
|
43
|
-
{
|
|
44
|
-
id: tool.name,
|
|
45
|
-
kind: 'tool' as const,
|
|
46
|
-
priority: 50,
|
|
47
|
-
payload: tool,
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
onActivate: async () => context.logger?.info('Media Upload Extension activated'),
|
|
51
|
-
onDeactivate: async () => context.logger?.info('Media Upload Extension deactivated'),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* ------------------------------------------------------------------ */
|
|
56
|
-
/* Re-exports */
|
|
57
|
-
/* ------------------------------------------------------------------ */
|
|
58
|
-
|
|
59
|
-
export { MediaUploadTool } from './MediaUploadTool.js';
|
|
60
|
-
export type {
|
|
61
|
-
MediaUploadInput,
|
|
62
|
-
MediaUploadOutput,
|
|
63
|
-
ToolExecutorFn,
|
|
64
|
-
} from './MediaUploadTool.js';
|
|
65
|
-
|
|
66
|
-
export default createExtensionPack;
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview Unit tests for MediaUploadTool.
|
|
4
|
-
*
|
|
5
|
-
* Tests cover: tool metadata, file existence validation, file size checks,
|
|
6
|
-
* MIME type detection, unsupported extension rejection, asset ID generation,
|
|
7
|
-
* directory path rejection, and error handling.
|
|
8
|
-
*
|
|
9
|
-
* The fs/promises, path, and crypto modules are mocked via vi.mock to
|
|
10
|
-
* avoid touching the real filesystem.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
-
import { MediaUploadTool, type MediaUploadInput } from '../src/MediaUploadTool.js';
|
|
15
|
-
|
|
16
|
-
/* ------------------------------------------------------------------ */
|
|
17
|
-
/* Module mocks */
|
|
18
|
-
/* ------------------------------------------------------------------ */
|
|
19
|
-
|
|
20
|
-
const { mockAccess, mockStat, mockReadFile } = vi.hoisted(() => {
|
|
21
|
-
const mockAccess = vi.fn();
|
|
22
|
-
const mockStat = vi.fn();
|
|
23
|
-
const mockReadFile = vi.fn();
|
|
24
|
-
return { mockAccess, mockStat, mockReadFile };
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
vi.mock('fs/promises', () => ({
|
|
28
|
-
default: {
|
|
29
|
-
access: (...args: any[]) => mockAccess(...args),
|
|
30
|
-
stat: (...args: any[]) => mockStat(...args),
|
|
31
|
-
readFile: (...args: any[]) => mockReadFile(...args),
|
|
32
|
-
},
|
|
33
|
-
access: (...args: any[]) => mockAccess(...args),
|
|
34
|
-
stat: (...args: any[]) => mockStat(...args),
|
|
35
|
-
readFile: (...args: any[]) => mockReadFile(...args),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
vi.mock('path', () => ({
|
|
39
|
-
default: {
|
|
40
|
-
resolve: (p: string) => (p.startsWith('/') ? p : `/cwd/${p}`),
|
|
41
|
-
extname: (p: string) => {
|
|
42
|
-
const dot = p.lastIndexOf('.');
|
|
43
|
-
return dot >= 0 ? p.substring(dot) : '';
|
|
44
|
-
},
|
|
45
|
-
basename: (p: string) => p.split('/').pop() ?? p,
|
|
46
|
-
},
|
|
47
|
-
resolve: (p: string) => (p.startsWith('/') ? p : `/cwd/${p}`),
|
|
48
|
-
extname: (p: string) => {
|
|
49
|
-
const dot = p.lastIndexOf('.');
|
|
50
|
-
return dot >= 0 ? p.substring(dot) : '';
|
|
51
|
-
},
|
|
52
|
-
basename: (p: string) => p.split('/').pop() ?? p,
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
|
-
vi.mock('crypto', () => ({
|
|
56
|
-
default: {
|
|
57
|
-
randomUUID: () => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
|
58
|
-
},
|
|
59
|
-
randomUUID: () => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
/* ------------------------------------------------------------------ */
|
|
63
|
-
/* Helpers */
|
|
64
|
-
/* ------------------------------------------------------------------ */
|
|
65
|
-
|
|
66
|
-
function setupValidFile(ext: string, sizeBytes: number) {
|
|
67
|
-
mockAccess.mockResolvedValue(undefined);
|
|
68
|
-
mockStat.mockResolvedValue({ isFile: () => true, size: sizeBytes });
|
|
69
|
-
mockReadFile.mockResolvedValue(Buffer.alloc(sizeBytes));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/* ------------------------------------------------------------------ */
|
|
73
|
-
/* Tests */
|
|
74
|
-
/* ------------------------------------------------------------------ */
|
|
75
|
-
|
|
76
|
-
describe('MediaUploadTool', () => {
|
|
77
|
-
let tool: MediaUploadTool;
|
|
78
|
-
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
tool = new MediaUploadTool();
|
|
81
|
-
vi.clearAllMocks();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
/* ── Metadata ─────────────────────────────────────────────────── */
|
|
85
|
-
|
|
86
|
-
describe('metadata', () => {
|
|
87
|
-
it('should expose the correct id and name', () => {
|
|
88
|
-
expect(tool.id).toBe('mediaUpload');
|
|
89
|
-
expect(tool.name).toBe('mediaUpload');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should require filePath in the input schema', () => {
|
|
93
|
-
expect(tool.inputSchema.required).toEqual(['filePath']);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should flag hasSideEffects as true', () => {
|
|
97
|
-
expect(tool.hasSideEffects).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should include a description mentioning supported formats', () => {
|
|
101
|
-
expect(tool.description).toContain('JPEG');
|
|
102
|
-
expect(tool.description).toContain('MP4');
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
/* ── File existence validation ────────────────────────────────── */
|
|
107
|
-
|
|
108
|
-
describe('file existence', () => {
|
|
109
|
-
it('should return error when file does not exist', async () => {
|
|
110
|
-
mockAccess.mockRejectedValue(new Error('ENOENT'));
|
|
111
|
-
|
|
112
|
-
const result = await tool.execute({ filePath: '/missing/file.jpg' });
|
|
113
|
-
|
|
114
|
-
expect(result.success).toBe(false);
|
|
115
|
-
expect(result.error).toContain('File not found');
|
|
116
|
-
expect(result.error).toContain('/missing/file.jpg');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should resolve relative paths before checking existence', async () => {
|
|
120
|
-
mockAccess.mockRejectedValue(new Error('ENOENT'));
|
|
121
|
-
|
|
122
|
-
const result = await tool.execute({ filePath: 'relative/photo.png' });
|
|
123
|
-
|
|
124
|
-
expect(result.success).toBe(false);
|
|
125
|
-
// The error should contain the resolved absolute path
|
|
126
|
-
expect(result.error).toContain('/cwd/relative/photo.png');
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
/* ── Directory rejection ──────────────────────────────────────── */
|
|
131
|
-
|
|
132
|
-
describe('directory rejection', () => {
|
|
133
|
-
it('should return error when path points to a directory', async () => {
|
|
134
|
-
mockAccess.mockResolvedValue(undefined);
|
|
135
|
-
mockStat.mockResolvedValue({ isFile: () => false, size: 4096 });
|
|
136
|
-
|
|
137
|
-
const result = await tool.execute({ filePath: '/some/directory.jpg' });
|
|
138
|
-
|
|
139
|
-
expect(result.success).toBe(false);
|
|
140
|
-
expect(result.error).toContain('not a file');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
/* ── File size checks ─────────────────────────────────────────── */
|
|
145
|
-
|
|
146
|
-
describe('file size validation', () => {
|
|
147
|
-
it('should reject files larger than 100 MB', async () => {
|
|
148
|
-
const oversized = 101 * 1024 * 1024; // 101 MB
|
|
149
|
-
setupValidFile('.jpg', oversized);
|
|
150
|
-
|
|
151
|
-
const result = await tool.execute({ filePath: '/big/photo.jpg' });
|
|
152
|
-
|
|
153
|
-
expect(result.success).toBe(false);
|
|
154
|
-
expect(result.error).toContain('File too large');
|
|
155
|
-
expect(result.error).toContain('100 MB limit');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should accept files exactly at the 100 MB limit', async () => {
|
|
159
|
-
const exactLimit = 100 * 1024 * 1024;
|
|
160
|
-
setupValidFile('.jpg', exactLimit);
|
|
161
|
-
|
|
162
|
-
const result = await tool.execute({ filePath: '/exact/photo.jpg' });
|
|
163
|
-
|
|
164
|
-
expect(result.success).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should accept small files', async () => {
|
|
168
|
-
setupValidFile('.png', 1024);
|
|
169
|
-
|
|
170
|
-
const result = await tool.execute({ filePath: '/small/icon.png' });
|
|
171
|
-
|
|
172
|
-
expect(result.success).toBe(true);
|
|
173
|
-
expect(result.data!.size).toBe(1024);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
/* ── MIME type detection ──────────────────────────────────────── */
|
|
178
|
-
|
|
179
|
-
describe('MIME type detection', () => {
|
|
180
|
-
const cases: Array<[string, string]> = [
|
|
181
|
-
['.jpg', 'image/jpeg'],
|
|
182
|
-
['.jpeg', 'image/jpeg'],
|
|
183
|
-
['.png', 'image/png'],
|
|
184
|
-
['.gif', 'image/gif'],
|
|
185
|
-
['.webp', 'image/webp'],
|
|
186
|
-
['.svg', 'image/svg+xml'],
|
|
187
|
-
['.mp4', 'video/mp4'],
|
|
188
|
-
['.mov', 'video/quicktime'],
|
|
189
|
-
['.webm', 'video/webm'],
|
|
190
|
-
['.mp3', 'audio/mpeg'],
|
|
191
|
-
['.wav', 'audio/wav'],
|
|
192
|
-
['.ogg', 'audio/ogg'],
|
|
193
|
-
['.pdf', 'application/pdf'],
|
|
194
|
-
];
|
|
195
|
-
|
|
196
|
-
for (const [ext, expectedMime] of cases) {
|
|
197
|
-
it(`should detect ${ext} as ${expectedMime}`, async () => {
|
|
198
|
-
setupValidFile(ext, 2048);
|
|
199
|
-
|
|
200
|
-
const result = await tool.execute({ filePath: `/test/file${ext}` });
|
|
201
|
-
|
|
202
|
-
expect(result.success).toBe(true);
|
|
203
|
-
expect(result.data!.mimeType).toBe(expectedMime);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
/* ── Unsupported extension rejection ──────────────────────────── */
|
|
209
|
-
|
|
210
|
-
describe('unsupported file types', () => {
|
|
211
|
-
it('should reject .exe files', async () => {
|
|
212
|
-
setupValidFile('.exe', 2048);
|
|
213
|
-
|
|
214
|
-
const result = await tool.execute({ filePath: '/test/malware.exe' });
|
|
215
|
-
|
|
216
|
-
expect(result.success).toBe(false);
|
|
217
|
-
expect(result.error).toContain('Unsupported file type');
|
|
218
|
-
expect(result.error).toContain('.exe');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should reject .zip files', async () => {
|
|
222
|
-
setupValidFile('.zip', 2048);
|
|
223
|
-
|
|
224
|
-
const result = await tool.execute({ filePath: '/test/archive.zip' });
|
|
225
|
-
|
|
226
|
-
expect(result.success).toBe(false);
|
|
227
|
-
expect(result.error).toContain('Unsupported file type');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('should list supported extensions in the error message', async () => {
|
|
231
|
-
setupValidFile('.txt', 2048);
|
|
232
|
-
|
|
233
|
-
const result = await tool.execute({ filePath: '/test/readme.txt' });
|
|
234
|
-
|
|
235
|
-
expect(result.success).toBe(false);
|
|
236
|
-
expect(result.error).toContain('.jpg');
|
|
237
|
-
expect(result.error).toContain('.mp4');
|
|
238
|
-
expect(result.error).toContain('.mp3');
|
|
239
|
-
expect(result.error).toContain('.pdf');
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
/* ── Asset ID generation ──────────────────────────────────────── */
|
|
244
|
-
|
|
245
|
-
describe('asset ID and output', () => {
|
|
246
|
-
it('should return a UUID asset ID', async () => {
|
|
247
|
-
setupValidFile('.png', 4096);
|
|
248
|
-
|
|
249
|
-
const result = await tool.execute({ filePath: '/test/image.png' });
|
|
250
|
-
|
|
251
|
-
expect(result.success).toBe(true);
|
|
252
|
-
expect(result.data!.assetId).toBe('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee');
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('should return the original filename', async () => {
|
|
256
|
-
setupValidFile('.jpg', 4096);
|
|
257
|
-
|
|
258
|
-
const result = await tool.execute({ filePath: '/path/to/my-photo.jpg' });
|
|
259
|
-
|
|
260
|
-
expect(result.data!.filename).toBe('my-photo.jpg');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('should include a human-readable message', async () => {
|
|
264
|
-
setupValidFile('.mp4', 10240);
|
|
265
|
-
|
|
266
|
-
const result = await tool.execute({ filePath: '/videos/clip.mp4' });
|
|
267
|
-
|
|
268
|
-
expect(result.data!.message).toContain('clip.mp4');
|
|
269
|
-
expect(result.data!.message).toContain('video/mp4');
|
|
270
|
-
expect(result.data!.message).toContain('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee');
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should categorize images correctly in the message', async () => {
|
|
274
|
-
setupValidFile('.png', 2048);
|
|
275
|
-
|
|
276
|
-
const result = await tool.execute({ filePath: '/test/icon.png' });
|
|
277
|
-
|
|
278
|
-
expect(result.data!.message).toContain('image uploaded');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('should categorize video correctly in the message', async () => {
|
|
282
|
-
setupValidFile('.webm', 2048);
|
|
283
|
-
|
|
284
|
-
const result = await tool.execute({ filePath: '/test/clip.webm' });
|
|
285
|
-
|
|
286
|
-
expect(result.data!.message).toContain('video uploaded');
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should categorize audio correctly in the message', async () => {
|
|
290
|
-
setupValidFile('.mp3', 2048);
|
|
291
|
-
|
|
292
|
-
const result = await tool.execute({ filePath: '/test/song.mp3' });
|
|
293
|
-
|
|
294
|
-
expect(result.data!.message).toContain('audio uploaded');
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
/* ── Tags ─────────────────────────────────────────────────────── */
|
|
299
|
-
|
|
300
|
-
describe('tags', () => {
|
|
301
|
-
it('should pass through provided tags', async () => {
|
|
302
|
-
setupValidFile('.jpg', 2048);
|
|
303
|
-
|
|
304
|
-
const result = await tool.execute({
|
|
305
|
-
filePath: '/test/photo.jpg',
|
|
306
|
-
tags: ['profile', 'avatar'],
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
expect(result.data!.tags).toEqual(['profile', 'avatar']);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('should default to empty tags when none provided', async () => {
|
|
313
|
-
setupValidFile('.jpg', 2048);
|
|
314
|
-
|
|
315
|
-
const result = await tool.execute({ filePath: '/test/photo.jpg' });
|
|
316
|
-
|
|
317
|
-
expect(result.data!.tags).toEqual([]);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
/* ── Error handling ───────────────────────────────────────────── */
|
|
322
|
-
|
|
323
|
-
describe('error handling', () => {
|
|
324
|
-
it('should catch unexpected errors and return them gracefully', async () => {
|
|
325
|
-
mockAccess.mockResolvedValue(undefined);
|
|
326
|
-
mockStat.mockRejectedValue(new Error('Permission denied'));
|
|
327
|
-
|
|
328
|
-
const result = await tool.execute({ filePath: '/protected/file.jpg' });
|
|
329
|
-
|
|
330
|
-
expect(result.success).toBe(false);
|
|
331
|
-
expect(result.error).toBe('Permission denied');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('should handle non-Error thrown values', async () => {
|
|
335
|
-
mockAccess.mockResolvedValue(undefined);
|
|
336
|
-
mockStat.mockRejectedValue('raw string error');
|
|
337
|
-
|
|
338
|
-
const result = await tool.execute({ filePath: '/test/file.jpg' });
|
|
339
|
-
|
|
340
|
-
expect(result.success).toBe(false);
|
|
341
|
-
expect(result.error).toBe('raw string error');
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"noEmitOnError": false,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"declaration": true,
|
|
14
|
-
"declarationMap": true,
|
|
15
|
-
"sourceMap": true,
|
|
16
|
-
"resolveJsonModule": true,
|
|
17
|
-
"moduleResolution": "Bundler",
|
|
18
|
-
"types": ["node"]
|
|
19
|
-
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
21
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
22
|
-
}
|