opencode-pollinations-plugin 5.9.0 → 6.0.0-beta.1
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 +5 -3
- package/dist/index.js +5 -0
- package/dist/server/generate-config.js +7 -0
- package/dist/server/proxy.js +81 -12
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +97 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +185 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +60 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +75 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +215 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +117 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +115 -0
- package/package.json +5 -3
- package/dist/debug_check.js +0 -36
- package/dist/provider.d.ts +0 -1
- package/dist/provider.js +0 -135
- package/dist/provider_v1.d.ts +0 -1
- package/dist/provider_v1.js +0 -135
- package/dist/test-require.js +0 -9
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as https from 'https';
|
|
6
|
+
import * as http from 'http';
|
|
7
|
+
const SAVE_DIR = path.join(os.homedir(), 'Downloads', 'pollinations', 'frames');
|
|
8
|
+
/**
|
|
9
|
+
* Download a video from URL to a temp file
|
|
10
|
+
*/
|
|
11
|
+
function downloadVideo(url) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const tempPath = path.join(os.tmpdir(), `video_${Date.now()}.mp4`);
|
|
14
|
+
const proto = url.startsWith('https') ? https : http;
|
|
15
|
+
const req = proto.get(url, { headers: { 'User-Agent': 'OpenCode-Pollinations-Plugin/6.0' } }, (res) => {
|
|
16
|
+
// Follow redirects
|
|
17
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
18
|
+
return downloadVideo(res.headers.location).then(resolve).catch(reject);
|
|
19
|
+
}
|
|
20
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
21
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
22
|
+
}
|
|
23
|
+
const ws = fs.createWriteStream(tempPath);
|
|
24
|
+
res.pipe(ws);
|
|
25
|
+
ws.on('finish', () => {
|
|
26
|
+
ws.close();
|
|
27
|
+
resolve(tempPath);
|
|
28
|
+
});
|
|
29
|
+
ws.on('error', reject);
|
|
30
|
+
});
|
|
31
|
+
req.on('error', reject);
|
|
32
|
+
req.setTimeout(120000, () => {
|
|
33
|
+
req.destroy();
|
|
34
|
+
reject(new Error('Download timeout (120s)'));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Try to load @ffmpeg/ffmpeg dynamically — it's an optional peer dependency
|
|
40
|
+
*/
|
|
41
|
+
async function loadFFmpeg() {
|
|
42
|
+
try {
|
|
43
|
+
// @ts-ignore — optional peer dependency
|
|
44
|
+
const { FFmpeg } = await import('@ffmpeg/ffmpeg');
|
|
45
|
+
// @ts-ignore — optional peer dependency
|
|
46
|
+
const { toBlobURL } = await import('@ffmpeg/util');
|
|
47
|
+
return { FFmpeg, toBlobURL };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if system ffmpeg is available (fallback)
|
|
55
|
+
*/
|
|
56
|
+
function hasSystemFFmpeg() {
|
|
57
|
+
try {
|
|
58
|
+
const { execSync } = require('child_process');
|
|
59
|
+
execSync('ffmpeg -version', { stdio: 'ignore' });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract frames using system ffmpeg
|
|
68
|
+
*/
|
|
69
|
+
function extractWithSystemFFmpeg(videoPath, outputDir, baseName, options) {
|
|
70
|
+
const { execSync } = require('child_process');
|
|
71
|
+
const outputPattern = path.join(outputDir, `${baseName}_%03d.png`);
|
|
72
|
+
const outputs = [];
|
|
73
|
+
let cmd = `ffmpeg -y -i "${videoPath}"`;
|
|
74
|
+
if (options.at_time) {
|
|
75
|
+
// Single frame at specific time
|
|
76
|
+
cmd += ` -ss ${options.at_time} -frames:v 1`;
|
|
77
|
+
const singleOutput = path.join(outputDir, `${baseName}_at_${options.at_time.replace(/:/g, '-')}.png`);
|
|
78
|
+
cmd += ` "${singleOutput}"`;
|
|
79
|
+
execSync(cmd, { stdio: 'ignore', timeout: 60000 });
|
|
80
|
+
if (fs.existsSync(singleOutput))
|
|
81
|
+
outputs.push(singleOutput);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Range extraction
|
|
85
|
+
if (options.start)
|
|
86
|
+
cmd += ` -ss ${options.start}`;
|
|
87
|
+
if (options.end)
|
|
88
|
+
cmd += ` -to ${options.end}`;
|
|
89
|
+
const fps = options.fps || 1; // Default 1 fps
|
|
90
|
+
cmd += ` -vf "fps=${fps}" "${outputPattern}"`;
|
|
91
|
+
execSync(cmd, { stdio: 'ignore', timeout: 120000 });
|
|
92
|
+
// Collect generated files
|
|
93
|
+
const files = fs.readdirSync(outputDir);
|
|
94
|
+
files.filter(f => f.startsWith(baseName) && f.endsWith('.png'))
|
|
95
|
+
.sort()
|
|
96
|
+
.forEach(f => outputs.push(path.join(outputDir, f)));
|
|
97
|
+
}
|
|
98
|
+
return outputs;
|
|
99
|
+
}
|
|
100
|
+
function formatFileSize(bytes) {
|
|
101
|
+
if (bytes < 1024)
|
|
102
|
+
return `${bytes} B`;
|
|
103
|
+
if (bytes < 1024 * 1024)
|
|
104
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
105
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
106
|
+
}
|
|
107
|
+
export const extractFramesTool = tool({
|
|
108
|
+
description: `Extract image frames from a video file or URL.
|
|
109
|
+
Can extract a single frame at a specific timestamp, or multiple frames from a time range.
|
|
110
|
+
Requires either system ffmpeg installed or @ffmpeg/ffmpeg npm package.
|
|
111
|
+
Supports MP4, WebM, and other common video formats.
|
|
112
|
+
Free to use — no API key needed.`,
|
|
113
|
+
args: {
|
|
114
|
+
source: tool.schema.string().describe('Video file path (absolute) or URL'),
|
|
115
|
+
at_time: tool.schema.string().optional().describe('Extract single frame at timestamp (e.g. "00:00:05" or "5")'),
|
|
116
|
+
start: tool.schema.string().optional().describe('Start time for range extraction (e.g. "00:00:02")'),
|
|
117
|
+
end: tool.schema.string().optional().describe('End time for range extraction (e.g. "00:00:10")'),
|
|
118
|
+
fps: tool.schema.number().min(0.1).max(30).optional().describe('Frames per second for range extraction (default: 1)'),
|
|
119
|
+
filename: tool.schema.string().optional().describe('Base filename prefix. Auto-generated if omitted'),
|
|
120
|
+
},
|
|
121
|
+
async execute(args, context) {
|
|
122
|
+
// Check ffmpeg availability
|
|
123
|
+
const systemFFmpeg = hasSystemFFmpeg();
|
|
124
|
+
if (!systemFFmpeg) {
|
|
125
|
+
// Try wasm ffmpeg
|
|
126
|
+
const ffmpegWasm = await loadFFmpeg();
|
|
127
|
+
if (!ffmpegWasm) {
|
|
128
|
+
return [
|
|
129
|
+
`❌ FFmpeg not found!`,
|
|
130
|
+
``,
|
|
131
|
+
`This tool requires ffmpeg. Install one of:`,
|
|
132
|
+
` • System: sudo apt install ffmpeg (or brew install ffmpeg)`,
|
|
133
|
+
` • Node.js: npm install @ffmpeg/ffmpeg @ffmpeg/util @ffmpeg/core`,
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
|
136
|
+
// TODO: Implement wasm extraction path (system ffmpeg is sufficient for MVP)
|
|
137
|
+
return `❌ @ffmpeg/ffmpeg wasm path not yet implemented. Please install system ffmpeg: sudo apt install ffmpeg`;
|
|
138
|
+
}
|
|
139
|
+
// Ensure save directory
|
|
140
|
+
if (!fs.existsSync(SAVE_DIR)) {
|
|
141
|
+
fs.mkdirSync(SAVE_DIR, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
// Resolve source: URL → download, path → validate
|
|
144
|
+
let videoPath;
|
|
145
|
+
let isRemote = false;
|
|
146
|
+
if (args.source.startsWith('http://') || args.source.startsWith('https://')) {
|
|
147
|
+
isRemote = true;
|
|
148
|
+
context.metadata({ title: `🎬 Downloading video...` });
|
|
149
|
+
try {
|
|
150
|
+
videoPath = await downloadVideo(args.source);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return `❌ Download error: ${err.message}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
videoPath = args.source;
|
|
158
|
+
if (!fs.existsSync(videoPath)) {
|
|
159
|
+
return `❌ File not found: ${videoPath}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const baseName = args.filename
|
|
163
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
164
|
+
: `frame_${Date.now()}`;
|
|
165
|
+
try {
|
|
166
|
+
context.metadata({ title: `🎬 Extracting frames...` });
|
|
167
|
+
const extractedFiles = extractWithSystemFFmpeg(videoPath, SAVE_DIR, baseName, {
|
|
168
|
+
at_time: args.at_time,
|
|
169
|
+
start: args.start,
|
|
170
|
+
end: args.end,
|
|
171
|
+
fps: args.fps,
|
|
172
|
+
});
|
|
173
|
+
// Cleanup temp video if downloaded
|
|
174
|
+
if (isRemote && fs.existsSync(videoPath)) {
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(videoPath);
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
179
|
+
}
|
|
180
|
+
if (extractedFiles.length === 0) {
|
|
181
|
+
return `❌ No frames extracted. Check your timestamps and video source.`;
|
|
182
|
+
}
|
|
183
|
+
// Calculate total size
|
|
184
|
+
const totalSize = extractedFiles.reduce((sum, f) => sum + fs.statSync(f).size, 0);
|
|
185
|
+
const fileList = extractedFiles.length <= 5
|
|
186
|
+
? extractedFiles.map(f => ` ${path.basename(f)}`).join('\n')
|
|
187
|
+
: [
|
|
188
|
+
...extractedFiles.slice(0, 3).map(f => ` ${path.basename(f)}`),
|
|
189
|
+
` ... and ${extractedFiles.length - 3} more`,
|
|
190
|
+
].join('\n');
|
|
191
|
+
return [
|
|
192
|
+
`🎬 Frames Extracted`,
|
|
193
|
+
`━━━━━━━━━━━━━━━━━━━`,
|
|
194
|
+
`Source: ${isRemote ? args.source : path.basename(videoPath)}`,
|
|
195
|
+
`Frames: ${extractedFiles.length}`,
|
|
196
|
+
`Directory: ${SAVE_DIR}`,
|
|
197
|
+
`Total size: ${formatFileSize(totalSize)}`,
|
|
198
|
+
`Files:`,
|
|
199
|
+
fileList,
|
|
200
|
+
``,
|
|
201
|
+
`Cost: Free (local ffmpeg)`,
|
|
202
|
+
].join('\n');
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
// Cleanup on error
|
|
206
|
+
if (isRemote && fs.existsSync(videoPath)) {
|
|
207
|
+
try {
|
|
208
|
+
fs.unlinkSync(videoPath);
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
return `❌ Frame extraction error: ${err.message}`;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
const LITTERBOX_URL = 'https://litterbox.catbox.moe/resources/internals/api.php';
|
|
6
|
+
/**
|
|
7
|
+
* Upload a file to litterbox.catbox.moe (free, anonymous, temporary hosting)
|
|
8
|
+
*/
|
|
9
|
+
function uploadToLitterbox(filePath, expiry) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const fileName = path.basename(filePath);
|
|
12
|
+
const fileData = fs.readFileSync(filePath);
|
|
13
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
14
|
+
// Build multipart form-data manually (no external deps)
|
|
15
|
+
const parts = [];
|
|
16
|
+
// reqtype field
|
|
17
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="reqtype"\r\n\r\nfileupload\r\n`));
|
|
18
|
+
// time field (expiry)
|
|
19
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="time"\r\n\r\n${expiry}\r\n`));
|
|
20
|
+
// file field
|
|
21
|
+
const mimeType = getMimeType(fileName);
|
|
22
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="fileToUpload"; filename="${fileName}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
|
|
23
|
+
parts.push(fileData);
|
|
24
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
25
|
+
const body = Buffer.concat(parts);
|
|
26
|
+
const url = new URL(LITTERBOX_URL);
|
|
27
|
+
const req = https.request({
|
|
28
|
+
hostname: url.hostname,
|
|
29
|
+
path: url.pathname,
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
33
|
+
'Content-Length': body.length,
|
|
34
|
+
'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
|
|
35
|
+
},
|
|
36
|
+
}, (res) => {
|
|
37
|
+
let data = '';
|
|
38
|
+
res.on('data', (chunk) => data += chunk);
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
const trimmed = data.trim();
|
|
41
|
+
if (res.statusCode === 200 && trimmed.startsWith('https://')) {
|
|
42
|
+
resolve(trimmed);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
reject(new Error(`Upload failed (${res.statusCode}): ${trimmed.substring(0, 200)}`));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
req.on('error', reject);
|
|
50
|
+
req.setTimeout(30000, () => {
|
|
51
|
+
req.destroy();
|
|
52
|
+
reject(new Error('Upload timeout (30s)'));
|
|
53
|
+
});
|
|
54
|
+
req.write(body);
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function getMimeType(filename) {
|
|
59
|
+
const ext = path.extname(filename).toLowerCase();
|
|
60
|
+
const types = {
|
|
61
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
62
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
63
|
+
'.mp4': 'video/mp4', '.webm': 'video/webm', '.mp3': 'audio/mpeg',
|
|
64
|
+
'.wav': 'audio/wav', '.pdf': 'application/pdf', '.txt': 'text/plain',
|
|
65
|
+
'.json': 'application/json', '.html': 'text/html', '.css': 'text/css',
|
|
66
|
+
'.js': 'application/javascript', '.ts': 'text/typescript',
|
|
67
|
+
'.zip': 'application/zip', '.tar': 'application/x-tar',
|
|
68
|
+
};
|
|
69
|
+
return types[ext] || 'application/octet-stream';
|
|
70
|
+
}
|
|
71
|
+
function formatFileSize(bytes) {
|
|
72
|
+
if (bytes < 1024)
|
|
73
|
+
return `${bytes} B`;
|
|
74
|
+
if (bytes < 1024 * 1024)
|
|
75
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
76
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
77
|
+
}
|
|
78
|
+
export const fileToUrlTool = tool({
|
|
79
|
+
description: `Upload a local file to get a temporary public URL.
|
|
80
|
+
Uses a free, anonymous hosting service (litterbox.catbox.moe).
|
|
81
|
+
Perfect for sharing images, videos, or files with external APIs that need URLs.
|
|
82
|
+
Max file size: ~200MB. URLs expire after chosen duration (1h to 72h).
|
|
83
|
+
No API key needed, no account required.`,
|
|
84
|
+
args: {
|
|
85
|
+
file_path: tool.schema.string().describe('Absolute path to the local file to upload'),
|
|
86
|
+
expiry: tool.schema.enum(['1h', '12h', '24h', '72h']).optional()
|
|
87
|
+
.describe('How long the URL stays active (default: 24h)'),
|
|
88
|
+
},
|
|
89
|
+
async execute(args, context) {
|
|
90
|
+
const expiry = args.expiry || '24h';
|
|
91
|
+
// Validate file exists
|
|
92
|
+
if (!fs.existsSync(args.file_path)) {
|
|
93
|
+
return `❌ File not found: ${args.file_path}`;
|
|
94
|
+
}
|
|
95
|
+
const stats = fs.statSync(args.file_path);
|
|
96
|
+
if (stats.size > 200 * 1024 * 1024) {
|
|
97
|
+
return `❌ File too large (${formatFileSize(stats.size)}). Max: 200MB`;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
context.metadata({ title: `📤 Uploading: ${path.basename(args.file_path)}` });
|
|
101
|
+
const publicUrl = await uploadToLitterbox(args.file_path, expiry);
|
|
102
|
+
return [
|
|
103
|
+
`📤 File Uploaded`,
|
|
104
|
+
`━━━━━━━━━━━━━━━━`,
|
|
105
|
+
`File: ${path.basename(args.file_path)}`,
|
|
106
|
+
`Size: ${formatFileSize(stats.size)}`,
|
|
107
|
+
`URL: ${publicUrl}`,
|
|
108
|
+
`Expires: ${expiry}`,
|
|
109
|
+
``,
|
|
110
|
+
`Cost: Free (anonymous hosting)`,
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return `❌ Upload Error: ${err.message}`;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
const SAVE_DIR = path.join(os.homedir(), 'Downloads', 'pollinations', 'rembg');
|
|
7
|
+
const REMBG_API_URL = 'https://cut.esprit-artificiel.com';
|
|
8
|
+
const REMBG_API_KEY = 'sk-cut-fkomEA2026-hybridsoap161200';
|
|
9
|
+
/**
|
|
10
|
+
* Send image to rembg API for background removal
|
|
11
|
+
*/
|
|
12
|
+
function removeBackground(imagePath) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const imageData = fs.readFileSync(imagePath);
|
|
15
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
16
|
+
const parts = [];
|
|
17
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
18
|
+
const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
|
19
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${path.basename(imagePath)}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
|
|
20
|
+
parts.push(imageData);
|
|
21
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
22
|
+
const body = Buffer.concat(parts);
|
|
23
|
+
const url = new URL(`${REMBG_API_URL}/remove`);
|
|
24
|
+
const req = https.request({
|
|
25
|
+
hostname: url.hostname,
|
|
26
|
+
path: url.pathname,
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
30
|
+
'Content-Length': body.length,
|
|
31
|
+
'X-Api-Key': REMBG_API_KEY,
|
|
32
|
+
'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
|
|
33
|
+
},
|
|
34
|
+
}, (res) => {
|
|
35
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
36
|
+
let errData = '';
|
|
37
|
+
res.on('data', (chunk) => errData += chunk);
|
|
38
|
+
res.on('end', () => reject(new Error(`API Error ${res.statusCode}: ${errData.substring(0, 200)}`)));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
43
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
44
|
+
});
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
req.setTimeout(60000, () => {
|
|
47
|
+
req.destroy();
|
|
48
|
+
reject(new Error('Background removal timeout (60s)'));
|
|
49
|
+
});
|
|
50
|
+
req.write(body);
|
|
51
|
+
req.end();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function formatFileSize(bytes) {
|
|
55
|
+
if (bytes < 1024)
|
|
56
|
+
return `${bytes} B`;
|
|
57
|
+
if (bytes < 1024 * 1024)
|
|
58
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
59
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
60
|
+
}
|
|
61
|
+
export const removeBackgroundTool = tool({
|
|
62
|
+
description: `Remove the background from an image, producing a transparent PNG.
|
|
63
|
+
Uses AI-powered background removal (u2netp model).
|
|
64
|
+
Supports PNG, JPEG, and WebP input formats.
|
|
65
|
+
Free to use — no API key or pollen required.`,
|
|
66
|
+
args: {
|
|
67
|
+
image_path: tool.schema.string().describe('Absolute path to the image file'),
|
|
68
|
+
filename: tool.schema.string().optional().describe('Custom output filename (without extension). Auto-generated if omitted'),
|
|
69
|
+
},
|
|
70
|
+
async execute(args, context) {
|
|
71
|
+
// Validate input
|
|
72
|
+
if (!fs.existsSync(args.image_path)) {
|
|
73
|
+
return `❌ File not found: ${args.image_path}`;
|
|
74
|
+
}
|
|
75
|
+
const ext = path.extname(args.image_path).toLowerCase();
|
|
76
|
+
if (!['.png', '.jpg', '.jpeg', '.webp'].includes(ext)) {
|
|
77
|
+
return `❌ Unsupported format: ${ext}. Use PNG, JPEG, or WebP.`;
|
|
78
|
+
}
|
|
79
|
+
const inputStats = fs.statSync(args.image_path);
|
|
80
|
+
if (inputStats.size > 10 * 1024 * 1024) {
|
|
81
|
+
return `❌ File too large (${formatFileSize(inputStats.size)}). Max: 10MB`;
|
|
82
|
+
}
|
|
83
|
+
// Ensure save directory
|
|
84
|
+
if (!fs.existsSync(SAVE_DIR)) {
|
|
85
|
+
fs.mkdirSync(SAVE_DIR, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
const safeName = args.filename
|
|
88
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
89
|
+
: `${path.basename(args.image_path, ext)}_nobg`;
|
|
90
|
+
const outputPath = path.join(SAVE_DIR, `${safeName}.png`);
|
|
91
|
+
try {
|
|
92
|
+
context.metadata({ title: `✂️ Removing background: ${path.basename(args.image_path)}` });
|
|
93
|
+
const resultBuffer = await removeBackground(args.image_path);
|
|
94
|
+
if (resultBuffer.length < 100) {
|
|
95
|
+
return `❌ Background removal returned invalid data. The API might be temporarily unavailable.`;
|
|
96
|
+
}
|
|
97
|
+
fs.writeFileSync(outputPath, resultBuffer);
|
|
98
|
+
return [
|
|
99
|
+
`✂️ Background Removed`,
|
|
100
|
+
`━━━━━━━━━━━━━━━━━━━━━`,
|
|
101
|
+
`Input: ${path.basename(args.image_path)} (${formatFileSize(inputStats.size)})`,
|
|
102
|
+
`Output: ${outputPath}`,
|
|
103
|
+
`Size: ${formatFileSize(resultBuffer.length)}`,
|
|
104
|
+
`Format: PNG (transparent)`,
|
|
105
|
+
`Cost: Free`,
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err.message.includes('429') || err.message.includes('rate')) {
|
|
110
|
+
return `⏳ Rate limited. The background removal service is busy. Please try again in 30 seconds.`;
|
|
111
|
+
}
|
|
112
|
+
return `❌ Background Removal Error: ${err.message}`;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-pollinations-plugin",
|
|
3
|
-
"displayName": "Pollinations AI (V5.
|
|
4
|
-
"version": "
|
|
3
|
+
"displayName": "Pollinations AI (V5.9)",
|
|
4
|
+
"version": "6.0.0-beta.1",
|
|
5
5
|
"description": "Native Pollinations.ai Provider Plugin for OpenCode",
|
|
6
6
|
"publisher": "pollinations",
|
|
7
7
|
"repository": {
|
|
@@ -49,10 +49,12 @@
|
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@opencode-ai/plugin": "^1.0.85",
|
|
52
|
-
"
|
|
52
|
+
"qrcode": "^1.5.4",
|
|
53
|
+
"zod": "^3.25.76"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/node": "^20.0.0",
|
|
57
|
+
"@types/qrcode": "^1.5.6",
|
|
56
58
|
"typescript": "^5.0.0"
|
|
57
59
|
}
|
|
58
60
|
}
|
package/dist/debug_check.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import * as https from 'https';
|
|
2
|
-
|
|
3
|
-
function checkEndpoint(ep, key) {
|
|
4
|
-
return new Promise((resolve) => {
|
|
5
|
-
console.log(`Checking ${ep}...`);
|
|
6
|
-
const req = https.request({
|
|
7
|
-
hostname: 'gen.pollinations.ai',
|
|
8
|
-
path: ep,
|
|
9
|
-
method: 'GET',
|
|
10
|
-
headers: { 'Authorization': `Bearer ${key}` }
|
|
11
|
-
}, (res) => {
|
|
12
|
-
console.log(`Status Code: ${res.statusCode}`);
|
|
13
|
-
let data = '';
|
|
14
|
-
res.on('data', chunk => data += chunk);
|
|
15
|
-
res.on('end', () => {
|
|
16
|
-
console.log(`Headers:`, res.headers);
|
|
17
|
-
console.log(`Body Full: ${data}`);
|
|
18
|
-
if (res.statusCode === 200) resolve({ ok: true, body: data });
|
|
19
|
-
else resolve({ ok: false, status: res.statusCode, body: data });
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
req.on('error', (e) => {
|
|
23
|
-
console.log(`Error: ${e.message}`);
|
|
24
|
-
resolve({ ok: false, status: e.message || 'Error' });
|
|
25
|
-
});
|
|
26
|
-
req.setTimeout(10000, () => req.destroy());
|
|
27
|
-
req.end();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const KEY = "plln_sk_F7a4RcBG4AVCeBSo6lnS36EKwm0nPn1O";
|
|
32
|
-
|
|
33
|
-
(async () => {
|
|
34
|
-
const res = await checkEndpoint('/account/profile', KEY);
|
|
35
|
-
console.log('Result:', res);
|
|
36
|
-
})();
|
package/dist/provider.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
package/dist/provider.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Removed invalid imports
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
// --- Sanitization Helpers (Ported from Gateway/Upstream) ---
|
|
4
|
-
function safeId(id) {
|
|
5
|
-
if (!id)
|
|
6
|
-
return id;
|
|
7
|
-
if (id.length > 30)
|
|
8
|
-
return id.substring(0, 30);
|
|
9
|
-
return id;
|
|
10
|
-
}
|
|
11
|
-
function logDebug(message, data) {
|
|
12
|
-
try {
|
|
13
|
-
const timestamp = new Date().toISOString();
|
|
14
|
-
let logMsg = `[${timestamp}] ${message}`;
|
|
15
|
-
if (data) {
|
|
16
|
-
logMsg += `\n${JSON.stringify(data, null, 2)}`;
|
|
17
|
-
}
|
|
18
|
-
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
|
|
19
|
-
}
|
|
20
|
-
catch (e) {
|
|
21
|
-
// ignore logging errors
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function sanitizeTools(tools) {
|
|
25
|
-
if (!Array.isArray(tools))
|
|
26
|
-
return tools;
|
|
27
|
-
const cleanSchema = (schema) => {
|
|
28
|
-
if (!schema || typeof schema !== "object")
|
|
29
|
-
return;
|
|
30
|
-
if (schema.optional !== undefined)
|
|
31
|
-
delete schema.optional;
|
|
32
|
-
if (schema.ref !== undefined)
|
|
33
|
-
delete schema.ref;
|
|
34
|
-
if (schema["$ref"] !== undefined)
|
|
35
|
-
delete schema["$ref"];
|
|
36
|
-
if (schema.properties) {
|
|
37
|
-
for (const key in schema.properties)
|
|
38
|
-
cleanSchema(schema.properties[key]);
|
|
39
|
-
}
|
|
40
|
-
if (schema.items)
|
|
41
|
-
cleanSchema(schema.items);
|
|
42
|
-
};
|
|
43
|
-
return tools.map((tool) => {
|
|
44
|
-
const newTool = { ...tool };
|
|
45
|
-
if (newTool.function && newTool.function.parameters) {
|
|
46
|
-
cleanSchema(newTool.function.parameters);
|
|
47
|
-
}
|
|
48
|
-
return newTool;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function filterTools(tools, maxCount = 120) {
|
|
52
|
-
if (!Array.isArray(tools))
|
|
53
|
-
return [];
|
|
54
|
-
if (tools.length <= maxCount)
|
|
55
|
-
return tools;
|
|
56
|
-
const priorities = [
|
|
57
|
-
"bash", "read", "write", "edit", "webfetch", "glob", "grep",
|
|
58
|
-
"searxng_remote_search", "deepsearch_deep_search", "google_search",
|
|
59
|
-
"task", "todowrite"
|
|
60
|
-
];
|
|
61
|
-
const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
|
|
62
|
-
const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
|
|
63
|
-
const slotsLeft = maxCount - priorityTools.length;
|
|
64
|
-
const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
|
|
65
|
-
logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
|
|
66
|
-
return [...priorityTools, ...othersKept];
|
|
67
|
-
}
|
|
68
|
-
// --- Fetch Implementation ---
|
|
69
|
-
export const createPollinationsFetch = (apiKey) => async (input, init) => {
|
|
70
|
-
let url = input.toString();
|
|
71
|
-
const options = init || {};
|
|
72
|
-
let body = null;
|
|
73
|
-
if (options.body && typeof options.body === "string") {
|
|
74
|
-
try {
|
|
75
|
-
body = JSON.parse(options.body);
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
// Not JSON, ignore
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// --- INTERCEPTION & SANITIZATION ---
|
|
82
|
-
if (body) {
|
|
83
|
-
let model = body.model || "";
|
|
84
|
-
// 0. Model Name Normalization
|
|
85
|
-
if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
|
|
86
|
-
body.model = model.replace("pollinations/enter/", "");
|
|
87
|
-
model = body.model;
|
|
88
|
-
}
|
|
89
|
-
// FIX: Remove stream_options (causes 400 on some OpenAI proxies)
|
|
90
|
-
if (body.stream_options) {
|
|
91
|
-
delete body.stream_options;
|
|
92
|
-
}
|
|
93
|
-
// 1. Azure Tool Limit Fix
|
|
94
|
-
if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
|
|
95
|
-
if (body.tools.length > 120) {
|
|
96
|
-
body.tools = filterTools(body.tools, 120);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// 2. Vertex/Gemini Schema Fix
|
|
100
|
-
if (model.includes("gemini") && body.tools) {
|
|
101
|
-
body.tools = sanitizeTools(body.tools);
|
|
102
|
-
}
|
|
103
|
-
// Re-serialize body
|
|
104
|
-
options.body = JSON.stringify(body);
|
|
105
|
-
}
|
|
106
|
-
// Ensure Headers
|
|
107
|
-
const headers = new Headers(options.headers || {});
|
|
108
|
-
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
109
|
-
headers.set("Content-Type", "application/json");
|
|
110
|
-
options.headers = headers;
|
|
111
|
-
logDebug(`Req: ${url}`, body);
|
|
112
|
-
try {
|
|
113
|
-
const response = await global.fetch(url, options);
|
|
114
|
-
// Log response status
|
|
115
|
-
// We clone to read text for debugging errors
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
try {
|
|
118
|
-
const clone = response.clone();
|
|
119
|
-
const text = await clone.text();
|
|
120
|
-
logDebug(`Res (Error): ${response.status}`, text);
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
logDebug(`Res (Error): ${response.status} (Read failed)`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
logDebug(`Res (OK): ${response.status}`);
|
|
128
|
-
}
|
|
129
|
-
return response;
|
|
130
|
-
}
|
|
131
|
-
catch (e) {
|
|
132
|
-
logDebug(`Fetch Error: ${e.message}`);
|
|
133
|
-
throw e;
|
|
134
|
-
}
|
|
135
|
-
};
|
package/dist/provider_v1.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|