@framers/agentos-ext-image-search 1.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 ADDED
@@ -0,0 +1,35 @@
1
+ # Image Search Extension for AgentOS
2
+
3
+ Unified stock photo search across Pexels, Unsplash, and Pixabay with automatic provider fallback.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @framers/agentos-ext-image-search
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Set one or more API keys:
14
+
15
+ ```bash
16
+ PEXELS_API_KEY=your-key
17
+ UNSPLASH_ACCESS_KEY=your-key
18
+ PIXABAY_API_KEY=your-key
19
+ ```
20
+
21
+ ## Tool: image_search
22
+
23
+ **Input:**
24
+ - `query` (string, required) -- Search query
25
+ - `provider` (string: pexels/unsplash/pixabay/auto, default: auto) -- Image provider
26
+ - `limit` (number, 1-10, default: 3) -- Number of results
27
+ - `orientation` (string: landscape/portrait/square) -- Image orientation
28
+
29
+ **Output:** Array of images with `url`, `thumbnailUrl`, `photographer`, `attribution`.
30
+
31
+ Auto mode tries providers in order: Pexels -> Unsplash -> Pixabay.
32
+
33
+ ## License
34
+
35
+ MIT - Frame.dev
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Image Search Extension Pack — stock photo search from Pexels, Unsplash, Pixabay.
3
+ */
4
+ import { ImageSearchTool } from './tools/imageSearch.js';
5
+ export interface ImageSearchExtensionOptions {
6
+ pexelsApiKey?: string;
7
+ unsplashApiKey?: string;
8
+ pixabayApiKey?: string;
9
+ priority?: number;
10
+ }
11
+ export declare function createExtensionPack(context: any): {
12
+ name: string;
13
+ version: string;
14
+ descriptors: {
15
+ id: string;
16
+ kind: "tool";
17
+ priority: number;
18
+ payload: ImageSearchTool;
19
+ }[];
20
+ onActivate: () => Promise<any>;
21
+ onDeactivate: () => Promise<any>;
22
+ };
23
+ export { ImageSearchTool };
24
+ export type { ImageSearchInput, ImageSearchOutput, SearchImage } from './tools/imageSearch.js';
25
+ export default createExtensionPack;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,WAAW,2BAA2B;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,GAAG;;;;;;;;;;;EAiB/C;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC/F,eAAe,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Image Search Extension Pack — stock photo search from Pexels, Unsplash, Pixabay.
3
+ */
4
+ import { ImageSearchTool } from './tools/imageSearch.js';
5
+ export function createExtensionPack(context) {
6
+ const options = (context.options || {});
7
+ const tool = new ImageSearchTool({
8
+ pexels: options.pexelsApiKey || context.getSecret?.('pexels.apiKey') || process.env.PEXELS_API_KEY,
9
+ unsplash: options.unsplashApiKey || context.getSecret?.('unsplash.apiKey') || process.env.UNSPLASH_ACCESS_KEY,
10
+ pixabay: options.pixabayApiKey || context.getSecret?.('pixabay.apiKey') || process.env.PIXABAY_API_KEY,
11
+ });
12
+ return {
13
+ name: '@framers/agentos-ext-image-search',
14
+ version: '1.0.0',
15
+ descriptors: [
16
+ { id: 'imageSearch', kind: 'tool', priority: options.priority || 50, payload: tool },
17
+ ],
18
+ onActivate: async () => context.logger?.info('Image Search Extension activated'),
19
+ onDeactivate: async () => context.logger?.info('Image Search Extension deactivated'),
20
+ };
21
+ }
22
+ export { ImageSearchTool };
23
+ export default createExtensionPack;
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AASzD,MAAM,UAAU,mBAAmB,CAAC,OAAY;IAC9C,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAgC,CAAC;IACvE,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;QAC/B,MAAM,EAAE,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,eAAe,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAClG,QAAQ,EAAE,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,iBAAiB,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB;QAC7G,OAAO,EAAE,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe;KACvG,CAAC,CAAC;IAEH,OAAO;QACL,IAAI,EAAE,mCAAmC;QACzC,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE;YACX,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,MAAe,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;SAC9F;QACD,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,kCAAkC,CAAC;QAChF,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,oCAAoC,CAAC;KACrF,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Image Search Tool — unified stock photo search across Pexels, Unsplash, Pixabay.
3
+ */
4
+ import type { ITool, ToolExecutionContext, ToolExecutionResult, JSONSchemaObject } from '../../../../../../agentos/src/core/tools/ITool.js';
5
+ export interface ImageSearchInput {
6
+ query: string;
7
+ provider?: 'pexels' | 'unsplash' | 'pixabay' | 'auto';
8
+ limit?: number;
9
+ orientation?: 'landscape' | 'portrait' | 'square';
10
+ }
11
+ export interface SearchImage {
12
+ id: string;
13
+ provider: string;
14
+ url: string;
15
+ thumbnailUrl: string;
16
+ width: number;
17
+ height: number;
18
+ photographer?: string;
19
+ description?: string;
20
+ attribution: string;
21
+ }
22
+ export interface ImageSearchOutput {
23
+ query: string;
24
+ provider: string;
25
+ images: SearchImage[];
26
+ totalResults: number;
27
+ }
28
+ export declare class ImageSearchTool implements ITool<ImageSearchInput, ImageSearchOutput> {
29
+ readonly id = "image-search-v1";
30
+ readonly name = "image_search";
31
+ readonly displayName = "Image Search";
32
+ readonly description: string;
33
+ readonly category = "media";
34
+ readonly version = "1.0.0";
35
+ readonly hasSideEffects = false;
36
+ readonly inputSchema: JSONSchemaObject;
37
+ readonly requiredCapabilities: string[];
38
+ private pexelsKey;
39
+ private unsplashKey;
40
+ private pixabayKey;
41
+ constructor(keys?: {
42
+ pexels?: string;
43
+ unsplash?: string;
44
+ pixabay?: string;
45
+ });
46
+ execute(args: ImageSearchInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<ImageSearchOutput>>;
47
+ private searchPexels;
48
+ private searchUnsplash;
49
+ private searchPixabay;
50
+ }
51
+ //# sourceMappingURL=imageSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageSearch.d.ts","sourceRoot":"","sources":["../../src/tools/imageSearch.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,mDAAmD,CAAC;AAE5I,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC;CACnD;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,eAAgB,YAAW,KAAK,CAAC,gBAAgB,EAAE,iBAAiB,CAAC;IAChF,QAAQ,CAAC,EAAE,qBAAqB;IAChC,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,WAAW,kBAAkB;IACtC,QAAQ,CAAC,WAAW,SAE4C;IAChE,QAAQ,CAAC,QAAQ,WAAW;IAC5B,QAAQ,CAAC,OAAO,WAAW;IAC3B,QAAQ,CAAC,cAAc,SAAS;IAEhC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CASpC;IAEF,QAAQ,CAAC,oBAAoB,WAA+B;IAE5D,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAS;gBAEf,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAMrE,OAAO,CACX,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;YAyBpC,YAAY;YAqBZ,cAAc;YAqBd,aAAa;CAqB5B"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Image Search Tool — unified stock photo search across Pexels, Unsplash, Pixabay.
3
+ */
4
+ export class ImageSearchTool {
5
+ id = 'image-search-v1';
6
+ name = 'image_search';
7
+ displayName = 'Image Search';
8
+ description = 'Search for stock photos from Pexels, Unsplash, or Pixabay. ' +
9
+ 'Returns image URLs with attribution for embedding in posts.';
10
+ category = 'media';
11
+ version = '1.0.0';
12
+ hasSideEffects = false;
13
+ inputSchema = {
14
+ type: 'object',
15
+ properties: {
16
+ query: { type: 'string', description: 'Image search query.' },
17
+ provider: { type: 'string', enum: ['pexels', 'unsplash', 'pixabay', 'auto'], default: 'auto' },
18
+ limit: { type: 'integer', minimum: 1, maximum: 10, default: 3 },
19
+ orientation: { type: 'string', enum: ['landscape', 'portrait', 'square'] },
20
+ },
21
+ required: ['query'],
22
+ };
23
+ requiredCapabilities = ['capability:media_search'];
24
+ pexelsKey;
25
+ unsplashKey;
26
+ pixabayKey;
27
+ constructor(keys) {
28
+ this.pexelsKey = keys?.pexels || process.env.PEXELS_API_KEY || '';
29
+ this.unsplashKey = keys?.unsplash || process.env.UNSPLASH_ACCESS_KEY || '';
30
+ this.pixabayKey = keys?.pixabay || process.env.PIXABAY_API_KEY || '';
31
+ }
32
+ async execute(args, _context) {
33
+ const provider = args.provider || 'auto';
34
+ const limit = args.limit || 3;
35
+ let selected;
36
+ if (provider === 'auto') {
37
+ if (this.pexelsKey)
38
+ selected = 'pexels';
39
+ else if (this.unsplashKey)
40
+ selected = 'unsplash';
41
+ else if (this.pixabayKey)
42
+ selected = 'pixabay';
43
+ else
44
+ return { success: false, error: 'No image API keys configured.' };
45
+ }
46
+ else {
47
+ selected = provider;
48
+ }
49
+ try {
50
+ switch (selected) {
51
+ case 'pexels': return await this.searchPexels(args.query, limit, args.orientation);
52
+ case 'unsplash': return await this.searchUnsplash(args.query, limit, args.orientation);
53
+ case 'pixabay': return await this.searchPixabay(args.query, limit, args.orientation);
54
+ }
55
+ }
56
+ catch (err) {
57
+ return { success: false, error: `Image search failed: ${err.message}` };
58
+ }
59
+ }
60
+ async searchPexels(query, limit, orientation) {
61
+ if (!this.pexelsKey)
62
+ return { success: false, error: 'PEXELS_API_KEY not set.' };
63
+ const params = new URLSearchParams({ query, per_page: String(limit) });
64
+ if (orientation)
65
+ params.set('orientation', orientation);
66
+ const res = await fetch(`https://api.pexels.com/v1/search?${params}`, { headers: { Authorization: this.pexelsKey } });
67
+ if (!res.ok)
68
+ return { success: false, error: `Pexels: ${res.status}` };
69
+ const data = await res.json();
70
+ return {
71
+ success: true,
72
+ output: {
73
+ query, provider: 'pexels', totalResults: data.total_results || 0,
74
+ images: (data.photos || []).map((p) => ({
75
+ id: String(p.id), provider: 'pexels', url: p.src?.large || p.src?.original,
76
+ thumbnailUrl: p.src?.medium, width: p.width, height: p.height,
77
+ photographer: p.photographer, description: p.alt || '',
78
+ attribution: `Photo by ${p.photographer} on Pexels`,
79
+ })),
80
+ },
81
+ };
82
+ }
83
+ async searchUnsplash(query, limit, orientation) {
84
+ if (!this.unsplashKey)
85
+ return { success: false, error: 'UNSPLASH_ACCESS_KEY not set.' };
86
+ const params = new URLSearchParams({ query, per_page: String(limit) });
87
+ if (orientation)
88
+ params.set('orientation', orientation);
89
+ const res = await fetch(`https://api.unsplash.com/search/photos?${params}`, { headers: { Authorization: `Client-ID ${this.unsplashKey}` } });
90
+ if (!res.ok)
91
+ return { success: false, error: `Unsplash: ${res.status}` };
92
+ const data = await res.json();
93
+ return {
94
+ success: true,
95
+ output: {
96
+ query, provider: 'unsplash', totalResults: data.total || 0,
97
+ images: (data.results || []).map((p) => ({
98
+ id: p.id, provider: 'unsplash', url: p.urls?.regular, thumbnailUrl: p.urls?.small,
99
+ width: p.width, height: p.height, photographer: p.user?.name,
100
+ description: p.description || p.alt_description || '',
101
+ attribution: `Photo by ${p.user?.name} on Unsplash`,
102
+ })),
103
+ },
104
+ };
105
+ }
106
+ async searchPixabay(query, limit, orientation) {
107
+ if (!this.pixabayKey)
108
+ return { success: false, error: 'PIXABAY_API_KEY not set.' };
109
+ // Pixabay requires per_page 3–200
110
+ const params = new URLSearchParams({ key: this.pixabayKey, q: query, per_page: String(Math.max(3, limit)), safesearch: 'true' });
111
+ if (orientation && orientation !== 'square')
112
+ params.set('orientation', orientation);
113
+ const res = await fetch(`https://pixabay.com/api/?${params}`);
114
+ if (!res.ok)
115
+ return { success: false, error: `Pixabay: ${res.status}` };
116
+ const data = await res.json();
117
+ return {
118
+ success: true,
119
+ output: {
120
+ query, provider: 'pixabay', totalResults: data.totalHits || 0,
121
+ images: (data.hits || []).map((h) => ({
122
+ id: String(h.id), provider: 'pixabay', url: h.largeImageURL || h.webformatURL,
123
+ thumbnailUrl: h.previewURL, width: h.imageWidth, height: h.imageHeight,
124
+ photographer: h.user, description: h.tags || '',
125
+ attribution: `Image by ${h.user} on Pixabay`,
126
+ })),
127
+ },
128
+ };
129
+ }
130
+ }
131
+ //# sourceMappingURL=imageSearch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageSearch.js","sourceRoot":"","sources":["../../src/tools/imageSearch.ts"],"names":[],"mappings":"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"}
package/manifest.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
3
+ "id": "com.framers.media.image-search",
4
+ "name": "Image Search Extension",
5
+ "version": "1.0.0",
6
+ "description": "Search for stock photos from Pexels, Unsplash, and Pixabay with automatic fallback",
7
+ "author": { "name": "Frame.dev" },
8
+ "license": "MIT",
9
+ "keywords": ["images", "photos", "pexels", "unsplash", "pixabay", "stock"],
10
+ "agentosVersion": "^2.0.0",
11
+ "categories": ["media"],
12
+ "extensions": [
13
+ { "kind": "tool", "id": "imageSearch", "displayName": "Image Search", "entry": "./src/tools/imageSearch.ts" }
14
+ ],
15
+ "configuration": {
16
+ "properties": {
17
+ "pexels.apiKey": { "type": "string", "secret": true },
18
+ "unsplash.apiKey": { "type": "string", "secret": true },
19
+ "pixabay.apiKey": { "type": "string", "secret": true }
20
+ }
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@framers/agentos-ext-image-search",
3
+ "version": "1.0.0",
4
+ "description": "Unified stock photo search across Pexels, Unsplash, and Pixabay for AgentOS",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
11
+ "./manifest.json": "./manifest.json",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:coverage": "vitest run --coverage",
19
+ "lint": "eslint src --ext .ts",
20
+ "typecheck": "tsc --noEmit",
21
+ "clean": "rimraf dist"
22
+ },
23
+ "keywords": ["agentos", "extension", "image", "search", "pexels", "unsplash", "pixabay", "media"],
24
+ "author": { "name": "Framers AI", "email": "team@frame.dev", "url": "https://frame.dev" },
25
+ "contributors": [{ "name": "Johnny Dunn", "email": "johnnyfived@protonmail.com", "url": "https://github.com/jddunn" }],
26
+ "license": "MIT",
27
+ "repository": { "type": "git", "url": "https://github.com/framersai/agentos-extensions.git", "directory": "registry/curated/media/image-search" },
28
+ "publishConfig": { "access": "public" },
29
+ "peerDependencies": { "@framers/agentos": "^0.1.0" },
30
+ "devDependencies": {
31
+ "@framers/agentos": "^0.1.0",
32
+ "@types/node": "^20.12.12",
33
+ "@vitest/coverage-v8": "^1.6.0",
34
+ "rimraf": "^5.0.7",
35
+ "typescript": "^5.4.5",
36
+ "vitest": "^1.6.0"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Image Search Extension Pack — stock photo search from Pexels, Unsplash, Pixabay.
3
+ */
4
+
5
+ import { ImageSearchTool } from './tools/imageSearch.js';
6
+
7
+ export interface ImageSearchExtensionOptions {
8
+ pexelsApiKey?: string;
9
+ unsplashApiKey?: string;
10
+ pixabayApiKey?: string;
11
+ priority?: number;
12
+ }
13
+
14
+ export function createExtensionPack(context: any) {
15
+ const options = (context.options || {}) as ImageSearchExtensionOptions;
16
+ const tool = new ImageSearchTool({
17
+ pexels: options.pexelsApiKey || context.getSecret?.('pexels.apiKey') || process.env.PEXELS_API_KEY,
18
+ unsplash: options.unsplashApiKey || context.getSecret?.('unsplash.apiKey') || process.env.UNSPLASH_ACCESS_KEY,
19
+ pixabay: options.pixabayApiKey || context.getSecret?.('pixabay.apiKey') || process.env.PIXABAY_API_KEY,
20
+ });
21
+
22
+ return {
23
+ name: '@framers/agentos-ext-image-search',
24
+ version: '1.0.0',
25
+ descriptors: [
26
+ { id: 'imageSearch', kind: 'tool' as const, priority: options.priority || 50, payload: tool },
27
+ ],
28
+ onActivate: async () => context.logger?.info('Image Search Extension activated'),
29
+ onDeactivate: async () => context.logger?.info('Image Search Extension deactivated'),
30
+ };
31
+ }
32
+
33
+ export { ImageSearchTool };
34
+ export type { ImageSearchInput, ImageSearchOutput, SearchImage } from './tools/imageSearch.js';
35
+ export default createExtensionPack;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Image Search Tool — unified stock photo search across Pexels, Unsplash, Pixabay.
3
+ */
4
+
5
+ import type { ITool, ToolExecutionContext, ToolExecutionResult, JSONSchemaObject } from '../../../../../../agentos/src/core/tools/ITool.js';
6
+
7
+ export interface ImageSearchInput {
8
+ query: string;
9
+ provider?: 'pexels' | 'unsplash' | 'pixabay' | 'auto';
10
+ limit?: number;
11
+ orientation?: 'landscape' | 'portrait' | 'square';
12
+ }
13
+
14
+ export interface SearchImage {
15
+ id: string;
16
+ provider: string;
17
+ url: string;
18
+ thumbnailUrl: string;
19
+ width: number;
20
+ height: number;
21
+ photographer?: string;
22
+ description?: string;
23
+ attribution: string;
24
+ }
25
+
26
+ export interface ImageSearchOutput {
27
+ query: string;
28
+ provider: string;
29
+ images: SearchImage[];
30
+ totalResults: number;
31
+ }
32
+
33
+ export class ImageSearchTool implements ITool<ImageSearchInput, ImageSearchOutput> {
34
+ readonly id = 'image-search-v1';
35
+ readonly name = 'image_search';
36
+ readonly displayName = 'Image Search';
37
+ readonly description =
38
+ 'Search for stock photos from Pexels, Unsplash, or Pixabay. ' +
39
+ 'Returns image URLs with attribution for embedding in posts.';
40
+ readonly category = 'media';
41
+ readonly version = '1.0.0';
42
+ readonly hasSideEffects = false;
43
+
44
+ readonly inputSchema: JSONSchemaObject = {
45
+ type: 'object',
46
+ properties: {
47
+ query: { type: 'string', description: 'Image search query.' },
48
+ provider: { type: 'string', enum: ['pexels', 'unsplash', 'pixabay', 'auto'], default: 'auto' },
49
+ limit: { type: 'integer', minimum: 1, maximum: 10, default: 3 },
50
+ orientation: { type: 'string', enum: ['landscape', 'portrait', 'square'] },
51
+ },
52
+ required: ['query'],
53
+ };
54
+
55
+ readonly requiredCapabilities = ['capability:media_search'];
56
+
57
+ private pexelsKey: string;
58
+ private unsplashKey: string;
59
+ private pixabayKey: string;
60
+
61
+ constructor(keys?: { pexels?: string; unsplash?: string; pixabay?: string }) {
62
+ this.pexelsKey = keys?.pexels || process.env.PEXELS_API_KEY || '';
63
+ this.unsplashKey = keys?.unsplash || process.env.UNSPLASH_ACCESS_KEY || '';
64
+ this.pixabayKey = keys?.pixabay || process.env.PIXABAY_API_KEY || '';
65
+ }
66
+
67
+ async execute(
68
+ args: ImageSearchInput,
69
+ _context: ToolExecutionContext,
70
+ ): Promise<ToolExecutionResult<ImageSearchOutput>> {
71
+ const provider = args.provider || 'auto';
72
+ const limit = args.limit || 3;
73
+
74
+ let selected: 'pexels' | 'unsplash' | 'pixabay';
75
+ if (provider === 'auto') {
76
+ if (this.pexelsKey) selected = 'pexels';
77
+ else if (this.unsplashKey) selected = 'unsplash';
78
+ else if (this.pixabayKey) selected = 'pixabay';
79
+ else return { success: false, error: 'No image API keys configured.' };
80
+ } else {
81
+ selected = provider;
82
+ }
83
+
84
+ try {
85
+ switch (selected) {
86
+ case 'pexels': return await this.searchPexels(args.query, limit, args.orientation);
87
+ case 'unsplash': return await this.searchUnsplash(args.query, limit, args.orientation);
88
+ case 'pixabay': return await this.searchPixabay(args.query, limit, args.orientation);
89
+ }
90
+ } catch (err: any) {
91
+ return { success: false, error: `Image search failed: ${err.message}` };
92
+ }
93
+ }
94
+
95
+ private async searchPexels(query: string, limit: number, orientation?: string): Promise<ToolExecutionResult<ImageSearchOutput>> {
96
+ if (!this.pexelsKey) return { success: false, error: 'PEXELS_API_KEY not set.' };
97
+ const params = new URLSearchParams({ query, per_page: String(limit) });
98
+ if (orientation) params.set('orientation', orientation);
99
+ const res = await fetch(`https://api.pexels.com/v1/search?${params}`, { headers: { Authorization: this.pexelsKey } });
100
+ if (!res.ok) return { success: false, error: `Pexels: ${res.status}` };
101
+ const data = await res.json();
102
+ return {
103
+ success: true,
104
+ output: {
105
+ query, provider: 'pexels', totalResults: data.total_results || 0,
106
+ images: (data.photos || []).map((p: any) => ({
107
+ id: String(p.id), provider: 'pexels', url: p.src?.large || p.src?.original,
108
+ thumbnailUrl: p.src?.medium, width: p.width, height: p.height,
109
+ photographer: p.photographer, description: p.alt || '',
110
+ attribution: `Photo by ${p.photographer} on Pexels`,
111
+ })),
112
+ },
113
+ };
114
+ }
115
+
116
+ private async searchUnsplash(query: string, limit: number, orientation?: string): Promise<ToolExecutionResult<ImageSearchOutput>> {
117
+ if (!this.unsplashKey) return { success: false, error: 'UNSPLASH_ACCESS_KEY not set.' };
118
+ const params = new URLSearchParams({ query, per_page: String(limit) });
119
+ if (orientation) params.set('orientation', orientation);
120
+ const res = await fetch(`https://api.unsplash.com/search/photos?${params}`, { headers: { Authorization: `Client-ID ${this.unsplashKey}` } });
121
+ if (!res.ok) return { success: false, error: `Unsplash: ${res.status}` };
122
+ const data = await res.json();
123
+ return {
124
+ success: true,
125
+ output: {
126
+ query, provider: 'unsplash', totalResults: data.total || 0,
127
+ images: (data.results || []).map((p: any) => ({
128
+ id: p.id, provider: 'unsplash', url: p.urls?.regular, thumbnailUrl: p.urls?.small,
129
+ width: p.width, height: p.height, photographer: p.user?.name,
130
+ description: p.description || p.alt_description || '',
131
+ attribution: `Photo by ${p.user?.name} on Unsplash`,
132
+ })),
133
+ },
134
+ };
135
+ }
136
+
137
+ private async searchPixabay(query: string, limit: number, orientation?: string): Promise<ToolExecutionResult<ImageSearchOutput>> {
138
+ if (!this.pixabayKey) return { success: false, error: 'PIXABAY_API_KEY not set.' };
139
+ // Pixabay requires per_page 3–200
140
+ const params = new URLSearchParams({ key: this.pixabayKey, q: query, per_page: String(Math.max(3, limit)), safesearch: 'true' });
141
+ if (orientation && orientation !== 'square') params.set('orientation', orientation);
142
+ const res = await fetch(`https://pixabay.com/api/?${params}`);
143
+ if (!res.ok) return { success: false, error: `Pixabay: ${res.status}` };
144
+ const data = await res.json();
145
+ return {
146
+ success: true,
147
+ output: {
148
+ query, provider: 'pixabay', totalResults: data.totalHits || 0,
149
+ images: (data.hits || []).map((h: any) => ({
150
+ id: String(h.id), provider: 'pixabay', url: h.largeImageURL || h.webformatURL,
151
+ thumbnailUrl: h.previewURL, width: h.imageWidth, height: h.imageHeight,
152
+ photographer: h.user, description: h.tags || '',
153
+ attribution: `Image by ${h.user} on Pixabay`,
154
+ })),
155
+ },
156
+ };
157
+ }
158
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const mockFetch = vi.fn();
4
+ vi.stubGlobal('fetch', mockFetch);
5
+
6
+ const { ImageSearchTool } = await import('../src/tools/imageSearch.js');
7
+ const { createExtensionPack } = await import('../src/index.js');
8
+
9
+ describe('ImageSearchTool', () => {
10
+ let tool: InstanceType<typeof ImageSearchTool>;
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ tool = new ImageSearchTool({ pexels: 'pexels-key', unsplash: 'unsplash-key', pixabay: 'pixabay-key' });
15
+ });
16
+
17
+ describe('metadata', () => {
18
+ it('has correct id and name', () => {
19
+ expect(tool.id).toBe('image-search-v1');
20
+ expect(tool.name).toBe('image_search');
21
+ });
22
+
23
+ it('has valid input schema', () => {
24
+ expect(tool.inputSchema.type).toBe('object');
25
+ expect(tool.inputSchema.required).toContain('query');
26
+ });
27
+ });
28
+
29
+ describe('execute', () => {
30
+ const ctx = {} as any;
31
+
32
+ it('returns error when no API keys configured', async () => {
33
+ const noKeyTool = new ImageSearchTool({});
34
+ const result = await noKeyTool.execute({ query: 'cats' }, ctx);
35
+ expect(result.success).toBe(false);
36
+ expect(result.error).toContain('No image API keys');
37
+ });
38
+
39
+ it('auto-selects pexels when available', async () => {
40
+ mockFetch.mockResolvedValueOnce({
41
+ ok: true,
42
+ json: async () => ({
43
+ total_results: 50,
44
+ photos: [{ id: 1, src: { large: 'https://pexels.com/1.jpg', medium: 'https://pexels.com/1_m.jpg' }, width: 1920, height: 1080, photographer: 'Test', alt: 'Cat' }],
45
+ }),
46
+ });
47
+
48
+ const result = await tool.execute({ query: 'cats' }, ctx);
49
+ expect(result.success).toBe(true);
50
+ expect(result.output!.provider).toBe('pexels');
51
+ expect(result.output!.images).toHaveLength(1);
52
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('api.pexels.com'), expect.any(Object));
53
+ });
54
+
55
+ it('uses specified provider', async () => {
56
+ mockFetch.mockResolvedValueOnce({
57
+ ok: true,
58
+ json: async () => ({ total: 10, results: [{ id: 'u1', urls: { regular: 'https://unsplash.com/u1.jpg', small: 'https://unsplash.com/u1_s.jpg' }, width: 1920, height: 1080, user: { name: 'Photographer' }, description: 'A cat' }] }),
59
+ });
60
+
61
+ const result = await tool.execute({ query: 'cats', provider: 'unsplash' }, ctx);
62
+ expect(result.success).toBe(true);
63
+ expect(result.output!.provider).toBe('unsplash');
64
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('api.unsplash.com'), expect.any(Object));
65
+ });
66
+
67
+ it('searches pixabay with clamped per_page', async () => {
68
+ mockFetch.mockResolvedValueOnce({
69
+ ok: true,
70
+ json: async () => ({ totalHits: 20, hits: [{ id: 1, largeImageURL: 'https://pixabay.com/1.jpg', previewURL: 'https://pixabay.com/1_p.jpg', imageWidth: 800, imageHeight: 600, user: 'Test', tags: 'cat' }] }),
71
+ });
72
+
73
+ const result = await tool.execute({ query: 'cats', provider: 'pixabay', limit: 1 }, ctx);
74
+ expect(result.success).toBe(true);
75
+ expect(result.output!.provider).toBe('pixabay');
76
+ // per_page should be clamped to minimum 3
77
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('per_page=3'));
78
+ });
79
+
80
+ it('handles API errors', async () => {
81
+ mockFetch.mockResolvedValueOnce({ ok: false, status: 401 });
82
+ const result = await tool.execute({ query: 'cats', provider: 'pexels' }, ctx);
83
+ expect(result.success).toBe(false);
84
+ });
85
+
86
+ it('handles network errors', async () => {
87
+ mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
88
+ const result = await tool.execute({ query: 'cats' }, ctx);
89
+ expect(result.success).toBe(false);
90
+ expect(result.error).toContain('Connection refused');
91
+ });
92
+ });
93
+ });
94
+
95
+ describe('createExtensionPack', () => {
96
+ it('creates pack with correct metadata', () => {
97
+ const pack = createExtensionPack({ options: { pexelsApiKey: 'test' }, logger: { info: vi.fn() } });
98
+ expect(pack.name).toBe('@framers/agentos-ext-image-search');
99
+ expect(pack.descriptors).toHaveLength(1);
100
+ });
101
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['test/**/*.spec.ts'],
8
+ testTimeout: 10000,
9
+ },
10
+ });