opencode-pollinations-plugin 6.0.0-beta.3 β 6.0.0-beta.6
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/dist/index.js +4 -2
- package/dist/server/commands.d.ts +2 -0
- package/dist/server/commands.js +67 -15
- package/dist/server/toast.d.ts +3 -0
- package/dist/server/toast.js +16 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/power/remove_background.js +247 -56
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +140 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import * as fs from 'fs';
|
|
|
3
3
|
import { generatePollinationsConfig } from './server/generate-config.js';
|
|
4
4
|
import { loadConfig } from './server/config.js';
|
|
5
5
|
import { handleChatCompletion } from './server/proxy.js';
|
|
6
|
-
import { createToastHooks, setGlobalClient } from './server/toast.js';
|
|
6
|
+
import { createToastHooks, createToolHooks, setGlobalClient } from './server/toast.js';
|
|
7
7
|
import { createStatusHooks } from './server/status.js';
|
|
8
|
-
import { createCommandHooks } from './server/commands.js';
|
|
8
|
+
import { createCommandHooks, setClientForCommands } from './server/commands.js';
|
|
9
9
|
import { createToolRegistry } from './tools/index.js';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
@@ -83,6 +83,7 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
83
83
|
const port = await startProxy();
|
|
84
84
|
const localBaseUrl = `http://127.0.0.1:${port}/v1`;
|
|
85
85
|
setGlobalClient(ctx.client);
|
|
86
|
+
setClientForCommands(ctx.client);
|
|
86
87
|
const toastHooks = createToastHooks(ctx.client);
|
|
87
88
|
const commandHooks = createCommandHooks();
|
|
88
89
|
// Build tool registry (conditional on API key presence)
|
|
@@ -112,6 +113,7 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
112
113
|
log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
|
|
113
114
|
},
|
|
114
115
|
...toastHooks,
|
|
116
|
+
...createToolHooks(ctx.client),
|
|
115
117
|
...createStatusHooks(ctx.client),
|
|
116
118
|
...commandHooks
|
|
117
119
|
};
|
|
@@ -9,8 +9,10 @@ interface CommandResult {
|
|
|
9
9
|
response?: string;
|
|
10
10
|
error?: string;
|
|
11
11
|
}
|
|
12
|
+
export declare function setClientForCommands(client: any): void;
|
|
12
13
|
export declare function handleCommand(command: string): Promise<CommandResult>;
|
|
13
14
|
export declare function createCommandHooks(): {
|
|
14
15
|
'tui.command.execute': (input: any, output: any) => Promise<void>;
|
|
16
|
+
'command.execute.before': (input: any, output: any) => Promise<void>;
|
|
15
17
|
};
|
|
16
18
|
export {};
|
package/dist/server/commands.js
CHANGED
|
@@ -130,6 +130,10 @@ function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
// === COMMAND HANDLER ===
|
|
133
|
+
let globalClient = null;
|
|
134
|
+
export function setClientForCommands(client) {
|
|
135
|
+
globalClient = client;
|
|
136
|
+
}
|
|
133
137
|
export async function handleCommand(command) {
|
|
134
138
|
const parts = command.trim().split(/\s+/);
|
|
135
139
|
if (!parts[0].startsWith('/poll')) {
|
|
@@ -150,6 +154,22 @@ export async function handleCommand(command) {
|
|
|
150
154
|
return handleConfigCommand(args);
|
|
151
155
|
case 'help':
|
|
152
156
|
return handleHelpCommand();
|
|
157
|
+
case 'addKey': // Internal command for UI
|
|
158
|
+
if (globalClient) {
|
|
159
|
+
globalClient.tui.appendPrompt({ value: "/pollinations rmbg_key_add " }); // Using the old command? No, use tool?
|
|
160
|
+
// Wait, tools are not commands.
|
|
161
|
+
// But we removed rmbg_key_add command!
|
|
162
|
+
// We should instruct user to use the tool?
|
|
163
|
+
// Or user types: rmbg_keys { action: "add", key: "..." }
|
|
164
|
+
// That's painful to type.
|
|
165
|
+
// Can we alias a command to a tool call?
|
|
166
|
+
// /pollinations rmbg_key_add is GONE.
|
|
167
|
+
// Let's re-introduce a helper command just for this UI flow?
|
|
168
|
+
// Or better: appendPrompt({ value: 'rmbg_keys { action: "add", key: "' });
|
|
169
|
+
globalClient.tui.appendPrompt({ value: 'rmbg_keys { action: "add", key: "' });
|
|
170
|
+
return { handled: true };
|
|
171
|
+
}
|
|
172
|
+
return { handled: true, error: "TUI not available" };
|
|
153
173
|
default:
|
|
154
174
|
return {
|
|
155
175
|
handled: true,
|
|
@@ -442,17 +462,21 @@ function handleConfigCommand(args) {
|
|
|
442
462
|
}
|
|
443
463
|
function handleHelpCommand() {
|
|
444
464
|
const help = `
|
|
445
|
-
### πΈ Pollinations Plugin - Commandes
|
|
465
|
+
### πΈ Pollinations Plugin - Commandes V6
|
|
446
466
|
|
|
467
|
+
**Mode & Usage**
|
|
447
468
|
- **\`/pollinations mode [mode]\`**: Change le mode (manual, alwaysfree, pro).
|
|
448
469
|
- **\`/pollinations usage [full]\`**: Affiche le dashboard (full = dΓ©tail).
|
|
449
|
-
- **\`/pollinations fallback <main> [agent]\`**: Configure le Safety Net
|
|
470
|
+
- **\`/pollinations fallback <main> [agent]\`**: Configure le Safety Net.
|
|
471
|
+
|
|
472
|
+
**Configuration**
|
|
450
473
|
- **\`/pollinations config [key] [value]\`**:
|
|
451
|
-
- \`status_gui\`: none, alert, all
|
|
452
|
-
- \`logs_gui\`: none, error, verbose
|
|
453
|
-
- \`threshold_tier\`: 0-100
|
|
454
|
-
- \`
|
|
455
|
-
|
|
474
|
+
- \`status_gui\`: none, alert, all
|
|
475
|
+
- \`logs_gui\`: none, error, verbose
|
|
476
|
+
- \`threshold_tier\` / \`threshold_wallet\`: 0-100
|
|
477
|
+
- \`status_bar\`: true/false
|
|
478
|
+
|
|
479
|
+
> π‘ **RMBG keys**: Use the \`rmbg_keys\` tool (works with any model).
|
|
456
480
|
`.trim();
|
|
457
481
|
return { handled: true, response: help };
|
|
458
482
|
}
|
|
@@ -460,16 +484,44 @@ function handleHelpCommand() {
|
|
|
460
484
|
export function createCommandHooks() {
|
|
461
485
|
return {
|
|
462
486
|
'tui.command.execute': async (input, output) => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
487
|
+
if (!input.command.startsWith('/pollinations')) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
// Parse command
|
|
492
|
+
const rawArgs = input.command.replace('/pollinations', '').trim();
|
|
493
|
+
const result = await handleCommand(rawArgs);
|
|
494
|
+
if (result.handled) {
|
|
495
|
+
if (result.error) {
|
|
496
|
+
output.error = `β **Erreur:** ${result.error}`;
|
|
497
|
+
}
|
|
498
|
+
else if (result.response) {
|
|
499
|
+
output.response = result.response;
|
|
500
|
+
}
|
|
501
|
+
// If no response and no error, assume handled silently (like appendPrompt)
|
|
471
502
|
}
|
|
472
503
|
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
output.error = `β **Erreur Critique:** ${err.message}`;
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
// Hook for UI Commands (Palette / Buttons)
|
|
509
|
+
'command.execute.before': async (input, output) => {
|
|
510
|
+
const cmd = input.command;
|
|
511
|
+
if (cmd === 'pollinations.addKey') {
|
|
512
|
+
handleCommand('addKey'); // Trigger UI helper
|
|
513
|
+
}
|
|
514
|
+
else if (cmd === 'pollinations.usage') {
|
|
515
|
+
const res = await handleCommand('usage');
|
|
516
|
+
// For native commands, we might need a different way to show output if not in chat context
|
|
517
|
+
// But here we are likely in a context where we can't easily hijack output unless we use a toast or open a file?
|
|
518
|
+
// Let's use toast for simple feedback or appendPrompt for complex interaction.
|
|
519
|
+
if (res.response)
|
|
520
|
+
globalClient?.tui.showToast({ title: "Pollinations Usage", metadata: { type: 'info', message: "Usage info sent to chat/logs" } });
|
|
521
|
+
}
|
|
522
|
+
else if (cmd === 'pollinations.mode') {
|
|
523
|
+
globalClient?.tui.appendPrompt({ value: "/pollinations mode " });
|
|
524
|
+
}
|
|
473
525
|
}
|
|
474
526
|
};
|
|
475
527
|
}
|
package/dist/server/toast.d.ts
CHANGED
|
@@ -4,3 +4,6 @@ export declare function emitStatusToast(type: 'info' | 'warning' | 'error' | 'su
|
|
|
4
4
|
export declare function createToastHooks(client: any): {
|
|
5
5
|
'session.idle': ({ event }: any) => Promise<void>;
|
|
6
6
|
};
|
|
7
|
+
export declare function createToolHooks(client: any): {
|
|
8
|
+
'tool.execute.after': (input: any, output: any) => Promise<void>;
|
|
9
|
+
};
|
package/dist/server/toast.js
CHANGED
|
@@ -76,3 +76,19 @@ export function createToastHooks(client) {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
+
// 3. CANAL TOOLS (Natif)
|
|
80
|
+
export function createToolHooks(client) {
|
|
81
|
+
return {
|
|
82
|
+
'tool.execute.after': async (input, output) => {
|
|
83
|
+
// Check for metadata in the output
|
|
84
|
+
if (output.metadata && output.metadata.message) {
|
|
85
|
+
const meta = output.metadata;
|
|
86
|
+
const type = meta.type || 'info';
|
|
87
|
+
// If title is not in metadata, try to use the one from output or default
|
|
88
|
+
const title = meta.title || output.title || 'Pollinations Tool';
|
|
89
|
+
// Emit the toast
|
|
90
|
+
emitStatusToast(type, meta.message, title);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { fileToUrlTool } from './power/file_to_url.js';
|
|
|
15
15
|
import { removeBackgroundTool } from './power/remove_background.js';
|
|
16
16
|
import { extractFramesTool } from './power/extract_frames.js';
|
|
17
17
|
import { extractAudioTool } from './power/extract_audio.js';
|
|
18
|
+
import { rmbgKeysTool } from './power/rmbg_keys.js';
|
|
18
19
|
// === ENTER TOOLS (Require API key) ===
|
|
19
20
|
// Phase 4D: Pollinations tools β TO BE IMPLEMENTED
|
|
20
21
|
// import { genImageTool } from './pollinations/gen_image.js';
|
|
@@ -57,6 +58,7 @@ export function createToolRegistry() {
|
|
|
57
58
|
tools['remove_background'] = removeBackgroundTool;
|
|
58
59
|
tools['extract_frames'] = extractFramesTool;
|
|
59
60
|
tools['extract_audio'] = extractAudioTool;
|
|
61
|
+
tools['rmbg_keys'] = rmbgKeysTool;
|
|
60
62
|
// gen_image (free version) β TODO Phase 4D
|
|
61
63
|
// tools['gen_image'] = genImageTool;
|
|
62
64
|
log(`Free tools injected: ${Object.keys(tools).length}`);
|
|
@@ -3,66 +3,167 @@ import * as https from 'https';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { resolveOutputDir, formatFileSize, TOOL_DIRS } from '../shared.js';
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// βββ Provider Defaults βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
7
|
+
const CUT_API_URL = 'https://cut.esprit-artificiel.com';
|
|
8
|
+
const CUT_API_KEY = 'sk-cut-fkomEA2026-hybridsoap161200';
|
|
9
|
+
const BACKGROUNDCUT_API_URL = 'https://backgroundcut.co/api/v1/cut/';
|
|
10
|
+
// βββ Key Storage βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
11
|
+
const KEYS_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.pollinations', 'backgroundcut_keys.json');
|
|
12
|
+
function loadKeys() {
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(KEYS_FILE)) {
|
|
15
|
+
return JSON.parse(fs.readFileSync(KEYS_FILE, 'utf-8'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
return { keys: [], currentIndex: 0 };
|
|
20
|
+
}
|
|
21
|
+
function getNextKey() {
|
|
22
|
+
const store = loadKeys();
|
|
23
|
+
if (store.keys.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const key = store.keys[store.currentIndex % store.keys.length];
|
|
26
|
+
// Rotate for next call
|
|
27
|
+
store.currentIndex = (store.currentIndex + 1) % store.keys.length;
|
|
28
|
+
try {
|
|
29
|
+
const dir = path.dirname(KEYS_FILE);
|
|
30
|
+
if (!fs.existsSync(dir))
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(KEYS_FILE, JSON.stringify(store, null, 2));
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
// βββ HTTP Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
38
|
+
function httpRequest(url, options, body) {
|
|
12
39
|
return new Promise((resolve, reject) => {
|
|
13
|
-
const
|
|
14
|
-
const boundary = `----FormBoundary${Date.now()}`;
|
|
15
|
-
const parts = [];
|
|
16
|
-
const ext = path.extname(imagePath).toLowerCase();
|
|
17
|
-
const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
|
18
|
-
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${path.basename(imagePath)}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
|
|
19
|
-
parts.push(imageData);
|
|
20
|
-
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
21
|
-
const body = Buffer.concat(parts);
|
|
22
|
-
const url = new URL(`${REMBG_API_URL}/remove-bg`);
|
|
23
|
-
const req = https.request({
|
|
24
|
-
hostname: url.hostname,
|
|
25
|
-
path: url.pathname,
|
|
26
|
-
method: 'POST',
|
|
27
|
-
headers: {
|
|
28
|
-
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
29
|
-
'Content-Length': body.length,
|
|
30
|
-
'X-Api-Key': REMBG_API_KEY,
|
|
31
|
-
'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
|
|
32
|
-
},
|
|
33
|
-
}, (res) => {
|
|
34
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
35
|
-
let errData = '';
|
|
36
|
-
res.on('data', (chunk) => errData += chunk);
|
|
37
|
-
res.on('end', () => reject(new Error(`API Error ${res.statusCode}: ${errData.substring(0, 200)}`)));
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
+
const req = https.request(url, options, (res) => {
|
|
40
41
|
const chunks = [];
|
|
41
42
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
42
|
-
res.on('end', () =>
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
const responseBody = Buffer.concat(chunks);
|
|
45
|
+
const statusCode = res.statusCode || 500;
|
|
46
|
+
let json;
|
|
47
|
+
try {
|
|
48
|
+
json = JSON.parse(responseBody.toString());
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
resolve({ statusCode, body: responseBody, json });
|
|
52
|
+
});
|
|
43
53
|
});
|
|
44
54
|
req.on('error', reject);
|
|
45
|
-
req.setTimeout(60000, () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
req.write(body);
|
|
55
|
+
req.setTimeout(60000, () => { req.destroy(); reject(new Error('Request timeout (60s)')); });
|
|
56
|
+
if (body)
|
|
57
|
+
req.write(body);
|
|
50
58
|
req.end();
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
|
-
|
|
61
|
+
function downloadFile(url) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const protocol = url.startsWith('https') ? https : require('http');
|
|
64
|
+
protocol.get(url, (res) => {
|
|
65
|
+
// Handle redirects
|
|
66
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
67
|
+
return downloadFile(res.headers.location).then(resolve).catch(reject);
|
|
68
|
+
}
|
|
69
|
+
const chunks = [];
|
|
70
|
+
res.on('data', (c) => chunks.push(c));
|
|
71
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
72
|
+
}).on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// βββ Provider: cut.esprit-artificiel.com (returns binary PNG directly) ββββββββ
|
|
76
|
+
async function removeViaCut(imageData, filename, mimeType) {
|
|
77
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
78
|
+
const parts = [];
|
|
79
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
|
|
80
|
+
parts.push(imageData);
|
|
81
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
82
|
+
const body = Buffer.concat(parts);
|
|
83
|
+
const url = new URL(`${CUT_API_URL}/remove-bg`);
|
|
84
|
+
const res = await httpRequest(url.toString(), {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
88
|
+
'Content-Length': body.length,
|
|
89
|
+
'X-Api-Key': CUT_API_KEY,
|
|
90
|
+
'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
|
|
91
|
+
},
|
|
92
|
+
}, body);
|
|
93
|
+
if (res.statusCode >= 400) {
|
|
94
|
+
throw new Error(`CUT API Error ${res.statusCode}: ${res.body.toString().substring(0, 200)}`);
|
|
95
|
+
}
|
|
96
|
+
return res.body;
|
|
97
|
+
}
|
|
98
|
+
// βββ Provider: BackgroundCut.co (returns JSON with output_image_url) βββββββββ
|
|
99
|
+
async function removeViaBackgroundCut(imageData, filename, mimeType, apiKey, quality = 'medium', returnFormat = 'png', maxResolution) {
|
|
100
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
101
|
+
const parts = [];
|
|
102
|
+
// File field
|
|
103
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
|
|
104
|
+
parts.push(imageData);
|
|
105
|
+
// Quality
|
|
106
|
+
parts.push(Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="quality"\r\n\r\n${quality}`));
|
|
107
|
+
// Return format
|
|
108
|
+
parts.push(Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="return_format"\r\n\r\n${returnFormat.toUpperCase()}`));
|
|
109
|
+
// Max resolution
|
|
110
|
+
if (maxResolution) {
|
|
111
|
+
parts.push(Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="max_resolution"\r\n\r\n${maxResolution}`));
|
|
112
|
+
}
|
|
113
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
114
|
+
const body = Buffer.concat(parts);
|
|
115
|
+
const res = await httpRequest(BACKGROUNDCUT_API_URL, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
119
|
+
'Content-Length': body.length,
|
|
120
|
+
'Authorization': `Token ${apiKey}`,
|
|
121
|
+
'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
|
|
122
|
+
},
|
|
123
|
+
}, body);
|
|
124
|
+
// Error handling with specific codes
|
|
125
|
+
if (res.statusCode === 401)
|
|
126
|
+
throw new Error('BGCUT_401:Invalid or missing API key');
|
|
127
|
+
if (res.statusCode === 402)
|
|
128
|
+
throw new Error('BGCUT_402:No credits remaining');
|
|
129
|
+
if (res.statusCode === 429)
|
|
130
|
+
throw new Error('BGCUT_429:Rate limit exceeded');
|
|
131
|
+
if (res.statusCode === 413)
|
|
132
|
+
throw new Error('BGCUT_413:File too large (max 12MB)');
|
|
133
|
+
if (res.statusCode >= 400) {
|
|
134
|
+
const msg = res.json?.error || res.body.toString().substring(0, 200);
|
|
135
|
+
throw new Error(`BGCUT_${res.statusCode}:${msg}`);
|
|
136
|
+
}
|
|
137
|
+
// Download the result image from the URL
|
|
138
|
+
const outputUrl = res.json?.output_image_url;
|
|
139
|
+
if (!outputUrl)
|
|
140
|
+
throw new Error('BackgroundCut returned no output URL');
|
|
141
|
+
return await downloadFile(outputUrl);
|
|
142
|
+
}
|
|
143
|
+
// βββ Main Tool βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
54
144
|
export const removeBackgroundTool = tool({
|
|
55
|
-
description: `Remove the background from an image, producing a transparent PNG.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
145
|
+
description: `Remove the background from an image, producing a transparent PNG or WebP.
|
|
146
|
+
|
|
147
|
+
**Providers:**
|
|
148
|
+
- \`cut\` (default free) β Built-in u2netp AI, no API key needed. Slower, rate-limited.
|
|
149
|
+
- \`backgroundcut\` β Premium API, requires API key (5$ free credits at signup on backgroundcut.co). Much faster, higher quality.
|
|
150
|
+
|
|
151
|
+
**Setup:** Use \`/poll key add backgroundcut <your_key>\` to store your BackgroundCut API key.
|
|
152
|
+
Multiple keys supported with automatic rotation.
|
|
153
|
+
|
|
154
|
+
**Auto mode:** If a BackgroundCut key is stored, uses it first. Falls back to free provider on error (402/429).`,
|
|
59
155
|
args: {
|
|
60
156
|
image_path: tool.schema.string().describe('Absolute path to the image file'),
|
|
61
|
-
filename: tool.schema.string().optional().describe('Custom output filename (without extension)
|
|
157
|
+
filename: tool.schema.string().optional().describe('Custom output filename (without extension)'),
|
|
62
158
|
output_path: tool.schema.string().optional().describe('Custom output directory. Default: ~/Downloads/pollinations/rembg/'),
|
|
159
|
+
provider: tool.schema.string().optional().describe('Provider: "auto" (default), "cut" (free), or "backgroundcut" (premium)'),
|
|
160
|
+
api_key: tool.schema.string().optional().describe('BackgroundCut API key (overrides stored keys)'),
|
|
161
|
+
quality: tool.schema.string().optional().describe('BackgroundCut quality: "low", "medium" (default), "high"'),
|
|
162
|
+
return_format: tool.schema.string().optional().describe('Output format: "png" (default), "webp"'),
|
|
163
|
+
max_resolution: tool.schema.number().optional().describe('Max output resolution in pixels (e.g., 4000000 for 4MP). BackgroundCut only.'),
|
|
63
164
|
},
|
|
64
165
|
async execute(args, context) {
|
|
65
|
-
// Validate input
|
|
166
|
+
// ββ Validate input ββ
|
|
66
167
|
if (!fs.existsSync(args.image_path)) {
|
|
67
168
|
return `β File not found: ${args.image_path}`;
|
|
68
169
|
}
|
|
@@ -71,34 +172,124 @@ Free to use β no API key or pollen required.`,
|
|
|
71
172
|
return `β Unsupported format: ${ext}. Use PNG, JPEG, or WebP.`;
|
|
72
173
|
}
|
|
73
174
|
const inputStats = fs.statSync(args.image_path);
|
|
74
|
-
if (inputStats.size >
|
|
75
|
-
return `β File too large (${formatFileSize(inputStats.size)}). Max:
|
|
175
|
+
if (inputStats.size > 12 * 1024 * 1024) {
|
|
176
|
+
return `β File too large (${formatFileSize(inputStats.size)}). Max: 12MB`;
|
|
177
|
+
}
|
|
178
|
+
// ββ Resolve provider ββ
|
|
179
|
+
const provider = (args.provider || 'auto');
|
|
180
|
+
const quality = (args.quality || 'medium');
|
|
181
|
+
const returnFormat = (args.return_format || 'png');
|
|
182
|
+
const imageData = fs.readFileSync(args.image_path);
|
|
183
|
+
const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
|
184
|
+
const basename = path.basename(args.image_path);
|
|
185
|
+
// ββ Resolve API key (arg > stored > none) ββ
|
|
186
|
+
let bgcutKey = args.api_key || null;
|
|
187
|
+
if (!bgcutKey)
|
|
188
|
+
bgcutKey = getNextKey();
|
|
189
|
+
// ββ Determine effective provider ββ
|
|
190
|
+
let effectiveProvider = provider;
|
|
191
|
+
if (provider === 'auto') {
|
|
192
|
+
effectiveProvider = bgcutKey ? 'backgroundcut' : 'cut';
|
|
193
|
+
}
|
|
194
|
+
// ββ Info message when no BackgroundCut key ββ
|
|
195
|
+
if (!bgcutKey && (provider === 'auto' || provider === 'backgroundcut')) {
|
|
196
|
+
console.log("No BackgroundCut key found. Using free provider.");
|
|
197
|
+
context.metadata({
|
|
198
|
+
title: "RMBG (Free)",
|
|
199
|
+
metadata: { type: 'info', message: "Mode Gratuit (Pas de clΓ© dΓ©tectΓ©e)" }
|
|
200
|
+
});
|
|
201
|
+
const noKeyMsg = [
|
|
202
|
+
`βΉοΈ **No BackgroundCut API key configured** β using free provider (slower, rate-limited).`,
|
|
203
|
+
``,
|
|
204
|
+
`π **Want faster, higher-quality results?**`,
|
|
205
|
+
`1. Sign up at https://backgroundcut.co (5$ free credits, 60 days)`,
|
|
206
|
+
`2. Run: \`/poll key add backgroundcut YOUR_API_KEY\``,
|
|
207
|
+
`3. Multiple keys supported β they rotate automatically!`,
|
|
208
|
+
].join('\n');
|
|
209
|
+
if (provider === 'backgroundcut') {
|
|
210
|
+
return `β BackgroundCut provider selected but no API key found.\n\n${noKeyMsg}`;
|
|
211
|
+
}
|
|
212
|
+
// In auto mode, we continue with free provider but show the message
|
|
213
|
+
context.metadata({ title: `β‘ Using free provider (no BackgroundCut key)` });
|
|
76
214
|
}
|
|
215
|
+
// ββ Output path ββ
|
|
77
216
|
const outputDir = resolveOutputDir(TOOL_DIRS.rembg, args.output_path);
|
|
78
217
|
const safeName = args.filename
|
|
79
218
|
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
80
219
|
: `${path.basename(args.image_path, ext)}_nobg`;
|
|
81
|
-
const
|
|
220
|
+
const outputExt = returnFormat === 'webp' && effectiveProvider === 'backgroundcut' ? 'webp' : 'png';
|
|
221
|
+
const outputPath = path.join(outputDir, `${safeName}.${outputExt}`);
|
|
222
|
+
// ββ Execute ββ
|
|
82
223
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
224
|
+
let resultBuffer;
|
|
225
|
+
let usedProvider;
|
|
226
|
+
let fallbackUsed = false;
|
|
227
|
+
if (effectiveProvider === 'backgroundcut' && bgcutKey) {
|
|
228
|
+
context.metadata({ title: `βοΈ BackgroundCut: ${basename}` }); // 1. TRY BACKGROUNDCUT
|
|
229
|
+
try {
|
|
230
|
+
console.log(`[RMBG] Attempting BackgroundCut with key ${bgcutKey.substring(0, 8)}...`);
|
|
231
|
+
resultBuffer = await removeViaBackgroundCut(imageData, basename, mimeType, bgcutKey, quality, returnFormat, args.max_resolution);
|
|
232
|
+
context.metadata({
|
|
233
|
+
title: "RMBG (Premium)",
|
|
234
|
+
metadata: { type: 'success', message: "DΓ©tourage HD rΓ©ussi (BackgroundCut)" }
|
|
235
|
+
});
|
|
236
|
+
usedProvider = 'backgroundcut';
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
// Fallback to CUT on specific errors
|
|
240
|
+
const isFallbackable = err.message.startsWith('BGCUT_402') ||
|
|
241
|
+
err.message.startsWith('BGCUT_429') ||
|
|
242
|
+
err.message.startsWith('BGCUT_401');
|
|
243
|
+
if (isFallbackable && provider === 'auto') {
|
|
244
|
+
context.metadata({ title: `β οΈ BackgroundCut failed (${err.message.split(':')[0]}), falling back to free...` });
|
|
245
|
+
resultBuffer = await removeViaCut(imageData, basename, mimeType);
|
|
246
|
+
usedProvider = 'cut (fallback)';
|
|
247
|
+
fallbackUsed = true;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
context.metadata({ title: `βοΈ Free RMBG: ${basename}` });
|
|
256
|
+
resultBuffer = await removeViaCut(imageData, basename, mimeType);
|
|
257
|
+
// 2. FREE PROVIDER EXECUTION
|
|
258
|
+
context.metadata({
|
|
259
|
+
title: "RMBG (Free)",
|
|
260
|
+
metadata: { type: 'success', message: "DΓ©tourage Standard rΓ©ussi" }
|
|
261
|
+
});
|
|
262
|
+
usedProvider = 'cut (free)';
|
|
263
|
+
}
|
|
85
264
|
if (resultBuffer.length < 100) {
|
|
86
265
|
return `β Background removal returned invalid data. The API might be temporarily unavailable.`;
|
|
87
266
|
}
|
|
88
267
|
fs.writeFileSync(outputPath, resultBuffer);
|
|
89
|
-
|
|
268
|
+
const lines = [
|
|
90
269
|
`βοΈ Background Removed`,
|
|
91
270
|
`βββββββββββββββββββββ`,
|
|
92
|
-
`Input: ${
|
|
271
|
+
`Input: ${basename} (${formatFileSize(inputStats.size)})`,
|
|
93
272
|
`Output: ${outputPath}`,
|
|
94
273
|
`Size: ${formatFileSize(resultBuffer.length)}`,
|
|
95
|
-
`Format:
|
|
274
|
+
`Format: ${outputExt.toUpperCase()} (transparent)`,
|
|
275
|
+
`Provider: ${usedProvider}`,
|
|
96
276
|
`Cost: Free`,
|
|
97
|
-
]
|
|
277
|
+
];
|
|
278
|
+
if (fallbackUsed) {
|
|
279
|
+
lines.push(``, `β οΈ BackgroundCut key may be expired/rate-limited. Check with \`/poll key list backgroundcut\``);
|
|
280
|
+
}
|
|
281
|
+
// Add info message for users without key
|
|
282
|
+
if (!bgcutKey && usedProvider.includes('free')) {
|
|
283
|
+
lines.push(``, `π‘ Tip: Add a BackgroundCut key for faster HD results: \`/poll key add backgroundcut <key>\``);
|
|
284
|
+
}
|
|
285
|
+
return lines.join('\n');
|
|
98
286
|
}
|
|
99
287
|
catch (err) {
|
|
100
288
|
if (err.message.includes('429') || err.message.includes('rate')) {
|
|
101
|
-
return `β³ Rate limited.
|
|
289
|
+
return `β³ Rate limited. Please try again in 30 seconds.`;
|
|
290
|
+
}
|
|
291
|
+
if (err.message.startsWith('BGCUT_402')) {
|
|
292
|
+
return `π³ BackgroundCut: No credits remaining. Add a new key or wait for renewal.\nRun: \`/poll key add backgroundcut <new_key>\``;
|
|
102
293
|
}
|
|
103
294
|
return `β Background Removal Error: ${err.message}`;
|
|
104
295
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
// βββ Key Storage (shared with remove_background.ts) ββββββββββββββββββββββββββ
|
|
6
|
+
const KEYS_FILE = path.join(os.homedir(), '.pollinations', 'backgroundcut_keys.json');
|
|
7
|
+
function loadKeys() {
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(KEYS_FILE)) {
|
|
10
|
+
return JSON.parse(fs.readFileSync(KEYS_FILE, 'utf-8'));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
return { keys: [], currentIndex: 0 };
|
|
15
|
+
}
|
|
16
|
+
function saveKeys(store) {
|
|
17
|
+
const dir = path.dirname(KEYS_FILE);
|
|
18
|
+
if (!fs.existsSync(dir))
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(KEYS_FILE, JSON.stringify(store, null, 2));
|
|
21
|
+
}
|
|
22
|
+
// βββ Tool ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
23
|
+
export const rmbgKeysTool = tool({
|
|
24
|
+
description: `Manage BackgroundCut API keys for the remove_background tool.
|
|
25
|
+
|
|
26
|
+
**Actions:**
|
|
27
|
+
- \`add\` β Add a new API key (supports multiple keys with auto-rotation).
|
|
28
|
+
- \`list\` β Show all stored keys and which one is next.
|
|
29
|
+
- \`clear\` β Remove all stored keys (reverts to free provider).
|
|
30
|
+
|
|
31
|
+
**Why?** The remove_background tool has 2 providers:
|
|
32
|
+
- **Free** (cut.esprit-artificiel.com): No key needed, but slow and rate-limited.
|
|
33
|
+
- **BackgroundCut** (backgroundcut.co): Fast, HD, up to 12MP. Requires an API key.
|
|
34
|
+
|
|
35
|
+
**Get a key:** Sign up at https://backgroundcut.co β 5$ free credits (60 days).
|
|
36
|
+
Keys rotate automatically β add multiple keys for uninterrupted usage.`,
|
|
37
|
+
args: {
|
|
38
|
+
action: tool.schema.string().describe('Action: "add", "list", or "clear"'),
|
|
39
|
+
key: tool.schema.string().optional().describe('API key to add (required for "add" action)'),
|
|
40
|
+
},
|
|
41
|
+
async execute(args, context) {
|
|
42
|
+
const action = (args.action || '').toLowerCase();
|
|
43
|
+
// ββ ADD ββ
|
|
44
|
+
if (action === 'add') {
|
|
45
|
+
if (!args.key) {
|
|
46
|
+
return [
|
|
47
|
+
`β **Missing key.** Usage:`,
|
|
48
|
+
`\`rmbg_keys { action: "add", key: "YOUR_API_KEY" }\``,
|
|
49
|
+
``,
|
|
50
|
+
`π Get your key at https://backgroundcut.co (5$ free, 60 days).`,
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
// Validate format (hex, 30-60 chars)
|
|
54
|
+
if (!/^[a-f0-9]{30,60}$/i.test(args.key)) {
|
|
55
|
+
context.metadata({
|
|
56
|
+
metadata: { type: 'error', message: "ClΓ© BackgroundCut invalide (format hex incorrect)." }
|
|
57
|
+
});
|
|
58
|
+
return `β Invalid key format. BackgroundCut keys are 40-character hexadecimal strings.`;
|
|
59
|
+
}
|
|
60
|
+
const store = loadKeys();
|
|
61
|
+
if (store.keys.includes(args.key)) {
|
|
62
|
+
context.metadata({
|
|
63
|
+
metadata: { type: 'warning', message: "Cette clΓ© est dΓ©jΓ enregistrΓ©e." }
|
|
64
|
+
});
|
|
65
|
+
return `β οΈ This key is already stored (${store.keys.length} key(s) total).`;
|
|
66
|
+
}
|
|
67
|
+
store.keys.push(args.key);
|
|
68
|
+
saveKeys(store);
|
|
69
|
+
const masked = args.key.substring(0, 8) + '...' + args.key.substring(args.key.length - 4);
|
|
70
|
+
context.metadata({
|
|
71
|
+
title: "Pollinations Keys",
|
|
72
|
+
metadata: { type: 'success', message: `ClΓ© ajoutΓ©e ! (${store.keys.length} actives)` }
|
|
73
|
+
});
|
|
74
|
+
return [
|
|
75
|
+
`β
**BackgroundCut key added!**`,
|
|
76
|
+
``,
|
|
77
|
+
`π Key: \`${masked}\``,
|
|
78
|
+
`π¦ Total: ${store.keys.length} key(s) β auto-rotation active`,
|
|
79
|
+
``,
|
|
80
|
+
`The \`remove_background\` tool will now use BackgroundCut automatically.`,
|
|
81
|
+
`Faster processing, HD quality, up to 12MP.`,
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
// ββ LIST ββ
|
|
85
|
+
if (action === 'list') {
|
|
86
|
+
const store = loadKeys();
|
|
87
|
+
if (store.keys.length === 0) {
|
|
88
|
+
return [
|
|
89
|
+
`π **No BackgroundCut keys stored.**`,
|
|
90
|
+
``,
|
|
91
|
+
`The \`remove_background\` tool uses the free provider (slower, rate-limited).`,
|
|
92
|
+
``,
|
|
93
|
+
`π **Want faster, HD results?**`,
|
|
94
|
+
`1. Sign up at https://backgroundcut.co (5$ free credits, 60 days)`,
|
|
95
|
+
`2. Add your key: \`rmbg_keys { action: "add", key: "YOUR_KEY" }\``,
|
|
96
|
+
`3. Multiple keys supported β they rotate automatically!`,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
const lines = [
|
|
100
|
+
`π **BackgroundCut Keys** (${store.keys.length} stored)`,
|
|
101
|
+
``,
|
|
102
|
+
];
|
|
103
|
+
store.keys.forEach((k, i) => {
|
|
104
|
+
const masked = k.substring(0, 8) + '...' + k.substring(k.length - 4);
|
|
105
|
+
const marker = i === (store.currentIndex % store.keys.length) ? ' β next' : '';
|
|
106
|
+
lines.push(`${i + 1}. \`${masked}\`${marker}`);
|
|
107
|
+
});
|
|
108
|
+
lines.push(``, `Auto-rotation: next call uses key #${(store.currentIndex % store.keys.length) + 1}.`, `To clear all: \`rmbg_keys { action: "clear" }\``);
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
// ββ CLEAR ββ
|
|
112
|
+
if (action === 'clear') {
|
|
113
|
+
const store = loadKeys();
|
|
114
|
+
const count = store.keys.length;
|
|
115
|
+
saveKeys({ keys: [], currentIndex: 0 });
|
|
116
|
+
if (count === 0) {
|
|
117
|
+
return `π No keys to clear β already empty.`;
|
|
118
|
+
}
|
|
119
|
+
context.metadata({
|
|
120
|
+
title: "Pollinations Keys",
|
|
121
|
+
metadata: { type: 'info', message: "Toutes les clΓ©s BackgroundCut ont Γ©tΓ© supprimΓ©es." }
|
|
122
|
+
});
|
|
123
|
+
return [
|
|
124
|
+
`ποΈ **${count} BackgroundCut key(s) cleared.**`,
|
|
125
|
+
``,
|
|
126
|
+
`The \`remove_background\` tool will now use the free provider.`,
|
|
127
|
+
`Add keys again anytime: \`rmbg_keys { action: "add", key: "..." }\``,
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
// ββ UNKNOWN ββ
|
|
131
|
+
return [
|
|
132
|
+
`β Unknown action: "${action}"`,
|
|
133
|
+
``,
|
|
134
|
+
`Available actions:`,
|
|
135
|
+
`- \`add\` β Add a BackgroundCut API key`,
|
|
136
|
+
`- \`list\` β Show stored keys`,
|
|
137
|
+
`- \`clear\` β Remove all keys`,
|
|
138
|
+
].join('\n');
|
|
139
|
+
},
|
|
140
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-pollinations-plugin",
|
|
3
3
|
"displayName": "Pollinations AI (V5.9)",
|
|
4
|
-
"version": "6.0.0-beta.
|
|
4
|
+
"version": "6.0.0-beta.6",
|
|
5
5
|
"description": "Native Pollinations.ai Provider Plugin for OpenCode",
|
|
6
6
|
"publisher": "pollinations",
|
|
7
7
|
"repository": {
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
{
|
|
42
42
|
"command": "pollinations.usage",
|
|
43
43
|
"title": "Pollinations: Show Usage"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"command": "pollinations.addKey",
|
|
47
|
+
"title": "Pollinations: Add BackgroundCut Key"
|
|
44
48
|
}
|
|
45
49
|
]
|
|
46
50
|
},
|