opencode-pollinations-plugin 6.1.0-beta.1 β 6.1.0-beta.11
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 +140 -87
- package/dist/index.js +33 -154
- package/dist/server/commands.d.ts +2 -0
- package/dist/server/commands.js +106 -60
- package/dist/server/config.d.ts +27 -23
- package/dist/server/config.js +24 -50
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +172 -100
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +20 -0
- package/dist/server/proxy.js +187 -149
- package/dist/server/quota.d.ts +8 -0
- package/dist/server/quota.js +106 -61
- package/dist/server/toast.d.ts +3 -0
- package/dist/server/toast.js +16 -0
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.js +81 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +204 -0
- package/dist/tools/pollinations/gen_image.d.ts +13 -0
- package/dist/tools/pollinations/gen_image.js +239 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +139 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +222 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +170 -0
- package/dist/tools/pollinations/shared.js +454 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +17 -0
- package/dist/tools/pollinations/transcribe_audio.js +235 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +180 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +240 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +365 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +78 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +74 -0
- package/package.json +9 -3
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
package/dist/server/quota.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import * as https from 'https'; // Use Native HTTPS
|
|
4
|
+
import * as crypto from 'crypto';
|
|
3
5
|
import { loadConfig } from './config.js';
|
|
4
|
-
// === CACHE ===
|
|
6
|
+
// === CACHE & CONSTANTS ===
|
|
5
7
|
const CACHE_TTL = 30000; // 30 secondes
|
|
6
8
|
let cachedQuota = null;
|
|
7
9
|
let lastQuotaFetch = 0;
|
|
10
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const HISTORY_RETENTION_MS = 48 * 60 * 60 * 1000; // 48h history
|
|
8
12
|
// === TIER LIMITS ===
|
|
9
13
|
const TIER_LIMITS = {
|
|
14
|
+
microbe: { pollen: 0.1, emoji: 'π¦ ' },
|
|
10
15
|
spore: { pollen: 1, emoji: 'π¦ ' },
|
|
11
16
|
seed: { pollen: 3, emoji: 'π±' },
|
|
12
17
|
flower: { pollen: 10, emoji: 'πΈ' },
|
|
@@ -19,24 +24,71 @@ function logQuota(msg) {
|
|
|
19
24
|
}
|
|
20
25
|
catch (e) { }
|
|
21
26
|
}
|
|
22
|
-
// ===
|
|
27
|
+
// === HISTORY MANAGER (JSON) ===
|
|
28
|
+
function getHistoryFilePath() {
|
|
29
|
+
const homedir = process.env.HOME || '/tmp';
|
|
30
|
+
const historyDir = path.join(homedir, '.pollinations');
|
|
31
|
+
if (!fs.existsSync(historyDir)) {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
catch (e) { }
|
|
36
|
+
}
|
|
37
|
+
return path.join(historyDir, 'usage_history.json');
|
|
38
|
+
}
|
|
39
|
+
function computeEntrySignature(entry) {
|
|
40
|
+
// Unique signature per transaction: timestamp + model + cost + source
|
|
41
|
+
return crypto.createHash('md5').update(`${entry.timestamp}|${entry.model}|${entry.cost_usd}|${entry.meter_source}`).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
function updateLocalHistory(newEntries) {
|
|
44
|
+
const filePath = getHistoryFilePath();
|
|
45
|
+
let history = [];
|
|
46
|
+
// 1. Load existing
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(filePath)) {
|
|
49
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
+
history = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
logQuota(`Failed to load history: ${e}`);
|
|
55
|
+
history = [];
|
|
56
|
+
}
|
|
57
|
+
// 2. Merge (Deduplication via Signature)
|
|
58
|
+
const existingSignatures = new Set(history.map(computeEntrySignature));
|
|
59
|
+
let addedCount = 0;
|
|
60
|
+
for (const entry of newEntries) {
|
|
61
|
+
const sig = computeEntrySignature(entry);
|
|
62
|
+
if (!existingSignatures.has(sig)) {
|
|
63
|
+
history.push(entry);
|
|
64
|
+
existingSignatures.add(sig);
|
|
65
|
+
addedCount++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 3. Prune (> 48h)
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const beforePrune = history.length;
|
|
71
|
+
history = history.filter(e => {
|
|
72
|
+
const entryTime = new Date(e.timestamp.replace(' ', 'T') + 'Z').getTime();
|
|
73
|
+
return (now - entryTime) < HISTORY_RETENTION_MS;
|
|
74
|
+
});
|
|
75
|
+
// 4. Sort (Newest first)
|
|
76
|
+
history.sort((a, b) => new Date(b.timestamp.replace(' ', 'T') + 'Z').getTime() - new Date(a.timestamp.replace(' ', 'T') + 'Z').getTime());
|
|
77
|
+
// 5. Save
|
|
78
|
+
try {
|
|
79
|
+
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
|
|
80
|
+
logQuota(`History Update: Added ${addedCount}, Pruned ${beforePrune - history.length}, Total ${history.length} entries.`);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
logQuota(`Failed to save history: ${e}`);
|
|
84
|
+
}
|
|
85
|
+
return history;
|
|
86
|
+
}
|
|
87
|
+
// === MAIN QUOTA FUNCTION ===
|
|
23
88
|
export async function getQuotaStatus(forceRefresh = false) {
|
|
24
89
|
const config = loadConfig();
|
|
25
90
|
if (!config.apiKey) {
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
tierRemaining: 0,
|
|
29
|
-
tierUsed: 0,
|
|
30
|
-
tierLimit: 0,
|
|
31
|
-
walletBalance: 0,
|
|
32
|
-
nextResetAt: new Date(),
|
|
33
|
-
timeUntilReset: 0,
|
|
34
|
-
canUseEnterprise: false,
|
|
35
|
-
isUsingWallet: false,
|
|
36
|
-
needsAlert: false,
|
|
37
|
-
tier: 'none',
|
|
38
|
-
tierEmoji: 'β'
|
|
39
|
-
};
|
|
91
|
+
return createDefaultQuota('none', 0);
|
|
40
92
|
}
|
|
41
93
|
const now = Date.now();
|
|
42
94
|
if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
|
|
@@ -44,27 +96,33 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
44
96
|
}
|
|
45
97
|
try {
|
|
46
98
|
logQuota("Fetching Quota Data...");
|
|
99
|
+
// 1. Fetch API
|
|
47
100
|
// SEQUENTIAL FETCH (Avoid Rate Limits)
|
|
48
|
-
// We fetch one by one. If one fails, we catch and return fallback.
|
|
49
101
|
const profileRes = await fetchAPI('/account/profile', config.apiKey);
|
|
50
102
|
const balanceRes = await fetchAPI('/account/balance', config.apiKey);
|
|
51
103
|
const usageRes = await fetchAPI('/account/usage', config.apiKey);
|
|
52
104
|
logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
|
|
53
105
|
const profile = profileRes;
|
|
54
106
|
const balance = balanceRes.balance;
|
|
55
|
-
|
|
56
|
-
const
|
|
107
|
+
// 2. Update Local History (The Source of Truth)
|
|
108
|
+
const fullHistory = updateLocalHistory(usageRes.usage || []);
|
|
109
|
+
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: 'β' };
|
|
57
110
|
const tierLimit = tierInfo.pollen;
|
|
58
|
-
//
|
|
111
|
+
// 3. Calculate Reset & Usage from History
|
|
59
112
|
const resetInfo = calculateResetInfo(profile.nextResetAt);
|
|
60
|
-
|
|
61
|
-
|
|
113
|
+
const { tierUsed } = calculateCurrentPeriodUsage(fullHistory, resetInfo);
|
|
114
|
+
// 4. Calculate Balances
|
|
62
115
|
const tierRemaining = Math.max(0, tierLimit - tierUsed);
|
|
63
116
|
// Fix rounding errors
|
|
64
117
|
const cleanTierRemaining = Math.max(0, parseFloat(tierRemaining.toFixed(4)));
|
|
65
118
|
// Le wallet c'est le reste (balance totale - ce qu'il reste du tier gratuit non consommΓ©)
|
|
119
|
+
// Formula: Pollinations Balance = Wallet + TierRemaining.
|
|
66
120
|
const walletBalance = Math.max(0, balance - cleanTierRemaining);
|
|
67
121
|
const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
|
|
122
|
+
// needsAlert: check BOTH tier threshold AND wallet threshold
|
|
123
|
+
const tierAlertPercent = tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) : 0;
|
|
124
|
+
const tierNeedsAlert = tierLimit > 0 && tierAlertPercent <= config.thresholds.tier;
|
|
125
|
+
const walletNeedsAlert = cleanWalletBalance > 0 && cleanWalletBalance < (config.thresholds.wallet || 0.5);
|
|
68
126
|
cachedQuota = {
|
|
69
127
|
tierRemaining: cleanTierRemaining,
|
|
70
128
|
tierUsed,
|
|
@@ -74,7 +132,7 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
74
132
|
timeUntilReset: resetInfo.timeUntilReset,
|
|
75
133
|
canUseEnterprise: cleanTierRemaining > 0.05 || cleanWalletBalance > 0.05,
|
|
76
134
|
isUsingWallet: cleanTierRemaining <= 0.05 && cleanWalletBalance > 0.05,
|
|
77
|
-
needsAlert:
|
|
135
|
+
needsAlert: tierNeedsAlert || walletNeedsAlert,
|
|
78
136
|
tier: profile.tier,
|
|
79
137
|
tierEmoji: tierInfo.emoji
|
|
80
138
|
};
|
|
@@ -84,30 +142,29 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
84
142
|
catch (e) {
|
|
85
143
|
logQuota(`ERROR fetching quota: ${e.message}`);
|
|
86
144
|
let errorType = 'unknown';
|
|
87
|
-
if (e.message && e.message.includes('403'))
|
|
145
|
+
if (e.message && e.message.includes('403'))
|
|
88
146
|
errorType = 'auth_limited';
|
|
89
|
-
|
|
90
|
-
else if (e.message && e.message.includes('Network Error')) {
|
|
147
|
+
else if (e.message && e.message.includes('Network Error'))
|
|
91
148
|
errorType = 'network';
|
|
92
|
-
}
|
|
93
|
-
// Retourner le cache ou un Γ©tat par dΓ©faut safe
|
|
94
|
-
return cachedQuota || {
|
|
95
|
-
tierRemaining: 0,
|
|
96
|
-
tierUsed: 0,
|
|
97
|
-
tierLimit: 1,
|
|
98
|
-
walletBalance: 0,
|
|
99
|
-
nextResetAt: new Date(),
|
|
100
|
-
timeUntilReset: 0,
|
|
101
|
-
canUseEnterprise: false,
|
|
102
|
-
isUsingWallet: false,
|
|
103
|
-
needsAlert: true,
|
|
104
|
-
tier: 'error',
|
|
105
|
-
tierEmoji: 'β οΈ',
|
|
106
|
-
errorType
|
|
107
|
-
};
|
|
149
|
+
return cachedQuota || { ...createDefaultQuota('error', 1), errorType };
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
|
-
|
|
152
|
+
function createDefaultQuota(tierName, limit) {
|
|
153
|
+
return {
|
|
154
|
+
tierRemaining: 0,
|
|
155
|
+
tierUsed: 0,
|
|
156
|
+
tierLimit: limit,
|
|
157
|
+
walletBalance: 0,
|
|
158
|
+
nextResetAt: new Date(),
|
|
159
|
+
timeUntilReset: 0,
|
|
160
|
+
canUseEnterprise: false,
|
|
161
|
+
isUsingWallet: false,
|
|
162
|
+
needsAlert: false,
|
|
163
|
+
tier: tierName,
|
|
164
|
+
tierEmoji: TIER_LIMITS[tierName]?.emoji || 'β'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// === HELPERS ===
|
|
111
168
|
function fetchAPI(endpoint, apiKey) {
|
|
112
169
|
return new Promise((resolve, reject) => {
|
|
113
170
|
const options = {
|
|
@@ -146,28 +203,23 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
146
203
|
function calculateResetInfo(nextResetAt) {
|
|
147
204
|
const nextResetFromAPI = new Date(nextResetAt);
|
|
148
205
|
const now = new Date();
|
|
149
|
-
// Extraire l'heure de reset depuis l'API (varie par utilisateur!)
|
|
150
206
|
const resetHour = nextResetFromAPI.getUTCHours();
|
|
151
207
|
const resetMinute = nextResetFromAPI.getUTCMinutes();
|
|
152
208
|
const resetSecond = nextResetFromAPI.getUTCSeconds();
|
|
153
|
-
// Calculer le reset d'aujourd'hui Γ cette heure
|
|
154
209
|
const todayResetUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), resetHour, resetMinute, resetSecond));
|
|
155
210
|
let lastReset;
|
|
156
211
|
let nextReset;
|
|
157
212
|
if (now >= todayResetUTC) {
|
|
158
|
-
// Le reset d'aujourd'hui est passΓ©
|
|
159
213
|
lastReset = todayResetUTC;
|
|
160
|
-
nextReset = new Date(todayResetUTC.getTime() +
|
|
214
|
+
nextReset = new Date(todayResetUTC.getTime() + ONE_DAY_MS);
|
|
161
215
|
}
|
|
162
216
|
else {
|
|
163
|
-
|
|
164
|
-
lastReset = new Date(todayResetUTC.getTime() - 24 * 60 * 60 * 1000);
|
|
217
|
+
lastReset = new Date(todayResetUTC.getTime() - ONE_DAY_MS);
|
|
165
218
|
nextReset = todayResetUTC;
|
|
166
219
|
}
|
|
167
220
|
const timeUntilReset = nextReset.getTime() - now.getTime();
|
|
168
221
|
const timeSinceReset = now.getTime() - lastReset.getTime();
|
|
169
|
-
const
|
|
170
|
-
const progressPercent = (timeSinceReset / cycleDuration) * 100;
|
|
222
|
+
const progressPercent = (timeSinceReset / ONE_DAY_MS) * 100;
|
|
171
223
|
return {
|
|
172
224
|
nextReset,
|
|
173
225
|
lastReset,
|
|
@@ -182,15 +234,10 @@ function calculateResetInfo(nextResetAt) {
|
|
|
182
234
|
function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
183
235
|
let tierUsed = 0;
|
|
184
236
|
let packUsed = 0;
|
|
185
|
-
// Parser le timestamp de l'API avec Z pour UTC
|
|
186
|
-
function parseUsageTimestamp(timestamp) {
|
|
187
|
-
// Format: "2026-01-23 01:11:21"
|
|
188
|
-
const isoString = timestamp.replace(' ', 'T') + 'Z';
|
|
189
|
-
return new Date(isoString);
|
|
190
|
-
}
|
|
191
|
-
// FILTRER: Ne garder que les entrΓ©es APRΓS le dernier reset
|
|
192
237
|
const entriesAfterReset = usage.filter(entry => {
|
|
193
|
-
|
|
238
|
+
// Safe Parse
|
|
239
|
+
const timestamp = entry.timestamp.replace(' ', 'T') + 'Z';
|
|
240
|
+
const entryTime = new Date(timestamp);
|
|
194
241
|
return entryTime >= resetInfo.lastReset;
|
|
195
242
|
});
|
|
196
243
|
for (const entry of entriesAfterReset) {
|
|
@@ -203,7 +250,6 @@ function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
|
203
250
|
}
|
|
204
251
|
return { tierUsed, packUsed };
|
|
205
252
|
}
|
|
206
|
-
// === EXPORT POUR LES ALERTES ===
|
|
207
253
|
export function formatQuotaForToast(quota) {
|
|
208
254
|
if (quota.errorType === 'auth_limited') {
|
|
209
255
|
return `π CLE LIMITΓE (GΓ©nΓ©ration Seule) | π Wallet: N/A | β° Reset: N/A`;
|
|
@@ -211,7 +257,6 @@ export function formatQuotaForToast(quota) {
|
|
|
211
257
|
const tierPercent = quota.tierLimit > 0
|
|
212
258
|
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
213
259
|
: 0;
|
|
214
|
-
// Format compact: 1h23m
|
|
215
260
|
const ms = quota.timeUntilReset;
|
|
216
261
|
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
217
262
|
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 { resolveOutputDir, TOOL_DIRS } from '../shared.js';
|
|
6
|
+
const MERMAID_INK_BASE = 'https://mermaid.ink';
|
|
7
|
+
/**
|
|
8
|
+
* Encode Mermaid code for mermaid.ink API
|
|
9
|
+
* Uses base64 encoding of the diagram definition
|
|
10
|
+
*/
|
|
11
|
+
function encodeMermaid(code) {
|
|
12
|
+
return Buffer.from(code, 'utf-8').toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fetch binary content from URL
|
|
16
|
+
*/
|
|
17
|
+
function fetchBinary(url) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const req = https.get(url, { headers: { 'User-Agent': 'OpenCode-Pollinations-Plugin/6.0' } }, (res) => {
|
|
20
|
+
// Follow redirects
|
|
21
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
22
|
+
return fetchBinary(res.headers.location).then(resolve).catch(reject);
|
|
23
|
+
}
|
|
24
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
25
|
+
return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
26
|
+
}
|
|
27
|
+
const chunks = [];
|
|
28
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
29
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
30
|
+
});
|
|
31
|
+
req.on('error', reject);
|
|
32
|
+
req.setTimeout(15000, () => {
|
|
33
|
+
req.destroy();
|
|
34
|
+
reject(new Error('Timeout fetching diagram'));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export const genDiagramTool = tool({
|
|
39
|
+
description: `Render a Mermaid diagram to SVG or PNG image.
|
|
40
|
+
Uses mermaid.ink (free, no auth required). Supports all Mermaid syntax:
|
|
41
|
+
flowchart, sequenceDiagram, classDiagram, stateDiagram, erDiagram, gantt, pie, mindmap, timeline, etc.
|
|
42
|
+
The diagram code should be valid Mermaid syntax WITHOUT the \`\`\`mermaid fences.`,
|
|
43
|
+
args: {
|
|
44
|
+
code: tool.schema.string().describe('Mermaid diagram code (e.g. "graph LR; A-->B; B-->C")'),
|
|
45
|
+
format: tool.schema.enum(['svg', 'png']).optional().describe('Output format (default: svg)'),
|
|
46
|
+
theme: tool.schema.enum(['default', 'dark', 'forest', 'neutral']).optional().describe('Diagram theme (default: default)'),
|
|
47
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension). Auto-generated if omitted'),
|
|
48
|
+
output_path: tool.schema.string().optional().describe('Custom output directory. Default: ~/Downloads/pollinations/diagrams/'),
|
|
49
|
+
},
|
|
50
|
+
async execute(args, context) {
|
|
51
|
+
const format = args.format || 'svg';
|
|
52
|
+
const theme = args.theme || 'default';
|
|
53
|
+
const outputDir = resolveOutputDir(TOOL_DIRS.diagrams, args.output_path);
|
|
54
|
+
// Build mermaid.ink URL
|
|
55
|
+
// For themed rendering, we wrap with config
|
|
56
|
+
const themedCode = theme !== 'default'
|
|
57
|
+
? `%%{init: {'theme': '${theme}'}}%%\n${args.code}`
|
|
58
|
+
: args.code;
|
|
59
|
+
const encoded = encodeMermaid(themedCode);
|
|
60
|
+
const endpoint = format === 'svg' ? 'svg' : 'img';
|
|
61
|
+
const url = `${MERMAID_INK_BASE}/${endpoint}/${encoded}`;
|
|
62
|
+
// Generate filename
|
|
63
|
+
const safeName = args.filename
|
|
64
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
65
|
+
: `diagram_${Date.now()}`;
|
|
66
|
+
const filePath = path.join(outputDir, `${safeName}.${format}`);
|
|
67
|
+
try {
|
|
68
|
+
const data = await fetchBinary(url);
|
|
69
|
+
if (data.length < 50) {
|
|
70
|
+
return `β Diagram Error: mermaid.ink returned empty/invalid response. Check your Mermaid syntax.`;
|
|
71
|
+
}
|
|
72
|
+
fs.writeFileSync(filePath, data);
|
|
73
|
+
const fileSizeKB = (data.length / 1024).toFixed(1);
|
|
74
|
+
// Extract diagram type from first line
|
|
75
|
+
const firstLine = args.code.trim().split('\n')[0].trim();
|
|
76
|
+
const diagramType = firstLine.replace(/[;\s{].*/g, '');
|
|
77
|
+
context.metadata({ title: `π Diagram: ${diagramType}` });
|
|
78
|
+
return [
|
|
79
|
+
`π Diagram Rendered`,
|
|
80
|
+
`βββββββββββββββββββ`,
|
|
81
|
+
`Type: ${diagramType}`,
|
|
82
|
+
`Theme: ${theme}`,
|
|
83
|
+
`Format: ${format.toUpperCase()}`,
|
|
84
|
+
`File: ${filePath}`,
|
|
85
|
+
`Weight: ${fileSizeKB} KB`,
|
|
86
|
+
`URL: ${url}`,
|
|
87
|
+
`Cost: Free (mermaid.ink)`,
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return `β Diagram Error: ${err.message}\nπ‘ Verify your Mermaid syntax at https://mermaid.live`;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { resolveOutputDir, TOOL_DIRS } from '../shared.js';
|
|
5
|
+
function hexToHSL(hex) {
|
|
6
|
+
hex = hex.replace('#', '');
|
|
7
|
+
if (hex.length === 3)
|
|
8
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
9
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
10
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
11
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
12
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
13
|
+
let h = 0, s = 0;
|
|
14
|
+
const l = (max + min) / 2;
|
|
15
|
+
if (max !== min) {
|
|
16
|
+
const d = max - min;
|
|
17
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
18
|
+
switch (max) {
|
|
19
|
+
case r:
|
|
20
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
21
|
+
break;
|
|
22
|
+
case g:
|
|
23
|
+
h = ((b - r) / d + 2) / 6;
|
|
24
|
+
break;
|
|
25
|
+
case b:
|
|
26
|
+
h = ((r - g) / d + 4) / 6;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
31
|
+
}
|
|
32
|
+
function hslToHex(h, s, l) {
|
|
33
|
+
s /= 100;
|
|
34
|
+
l /= 100;
|
|
35
|
+
const a = s * Math.min(l, 1 - l);
|
|
36
|
+
const f = (n) => {
|
|
37
|
+
const k = (n + h / 30) % 12;
|
|
38
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
39
|
+
return Math.round(255 * color).toString(16).padStart(2, '0');
|
|
40
|
+
};
|
|
41
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
42
|
+
}
|
|
43
|
+
function generatePalette(baseHex, scheme, count) {
|
|
44
|
+
const base = hexToHSL(baseHex);
|
|
45
|
+
const colors = [];
|
|
46
|
+
switch (scheme) {
|
|
47
|
+
case 'complementary':
|
|
48
|
+
colors.push({ hex: baseHex, role: 'Base' });
|
|
49
|
+
colors.push({ hex: hslToHex((base.h + 180) % 360, base.s, base.l), role: 'Complement' });
|
|
50
|
+
// Fill shades
|
|
51
|
+
for (let i = 2; i < count; i++) {
|
|
52
|
+
const lShift = base.l + (i % 2 === 0 ? 15 : -15) * Math.ceil(i / 2);
|
|
53
|
+
colors.push({ hex: hslToHex(base.h, base.s, Math.max(10, Math.min(90, lShift))), role: `Shade ${i - 1}` });
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case 'analogous':
|
|
57
|
+
for (let i = 0; i < count; i++) {
|
|
58
|
+
const offset = (i - Math.floor(count / 2)) * 30;
|
|
59
|
+
colors.push({
|
|
60
|
+
hex: hslToHex((base.h + offset + 360) % 360, base.s, base.l),
|
|
61
|
+
role: offset === 0 ? 'Base' : `${offset > 0 ? '+' : ''}${offset}Β°`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case 'triadic':
|
|
66
|
+
colors.push({ hex: baseHex, role: 'Base' });
|
|
67
|
+
colors.push({ hex: hslToHex((base.h + 120) % 360, base.s, base.l), role: 'Triad +120Β°' });
|
|
68
|
+
colors.push({ hex: hslToHex((base.h + 240) % 360, base.s, base.l), role: 'Triad +240Β°' });
|
|
69
|
+
for (let i = 3; i < count; i++) {
|
|
70
|
+
const lShift = base.l + (i % 2 === 0 ? 12 : -12) * Math.ceil((i - 2) / 2);
|
|
71
|
+
colors.push({ hex: hslToHex((base.h + (i * 120)) % 360, base.s, Math.max(10, Math.min(90, lShift))), role: `Accent ${i - 2}` });
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case 'split-complementary':
|
|
75
|
+
colors.push({ hex: baseHex, role: 'Base' });
|
|
76
|
+
colors.push({ hex: hslToHex((base.h + 150) % 360, base.s, base.l), role: 'Split +150Β°' });
|
|
77
|
+
colors.push({ hex: hslToHex((base.h + 210) % 360, base.s, base.l), role: 'Split +210Β°' });
|
|
78
|
+
for (let i = 3; i < count; i++) {
|
|
79
|
+
colors.push({ hex: hslToHex(base.h, base.s, Math.max(10, Math.min(90, base.l + (i * 10 - 30)))), role: `Tone ${i - 2}` });
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case 'monochromatic':
|
|
83
|
+
default:
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
const l = Math.round(15 + (i / (count - 1)) * 70); // 15% to 85%
|
|
86
|
+
colors.push({
|
|
87
|
+
hex: hslToHex(base.h, base.s, l),
|
|
88
|
+
role: l < base.l ? `Dark ${Math.abs(i - Math.floor(count / 2))}` : l === base.l ? 'Base' : `Light ${Math.abs(i - Math.floor(count / 2))}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Mark closest to base
|
|
92
|
+
let closestIdx = 0;
|
|
93
|
+
let closestDiff = Infinity;
|
|
94
|
+
colors.forEach((c, i) => {
|
|
95
|
+
const diff = Math.abs(hexToHSL(c.hex).l - base.l);
|
|
96
|
+
if (diff < closestDiff) {
|
|
97
|
+
closestDiff = diff;
|
|
98
|
+
closestIdx = i;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
colors[closestIdx].role = 'Base';
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
return colors.slice(0, count);
|
|
105
|
+
}
|
|
106
|
+
function generateSVG(colors) {
|
|
107
|
+
const swatchW = 120;
|
|
108
|
+
const swatchH = 80;
|
|
109
|
+
const gap = 8;
|
|
110
|
+
const totalW = colors.length * (swatchW + gap) - gap + 40;
|
|
111
|
+
const totalH = swatchH + 60;
|
|
112
|
+
const swatches = colors.map((c, i) => {
|
|
113
|
+
const x = 20 + i * (swatchW + gap);
|
|
114
|
+
const textColor = hexToHSL(c.hex).l > 50 ? '#1a1a1a' : '#ffffff';
|
|
115
|
+
return `
|
|
116
|
+
<rect x="${x}" y="20" width="${swatchW}" height="${swatchH}" rx="8" fill="${c.hex}" stroke="#333" stroke-width="1"/>
|
|
117
|
+
<text x="${x + swatchW / 2}" y="${swatchH / 2 + 15}" text-anchor="middle" fill="${textColor}" font-family="monospace" font-size="13" font-weight="bold">${c.hex.toUpperCase()}</text>
|
|
118
|
+
<text x="${x + swatchW / 2}" y="${swatchH + 38}" text-anchor="middle" fill="#666" font-family="sans-serif" font-size="11">${c.role}</text>`;
|
|
119
|
+
}).join('');
|
|
120
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}">
|
|
121
|
+
<rect width="100%" height="100%" fill="#0d0d0d" rx="12"/>
|
|
122
|
+
${swatches}
|
|
123
|
+
</svg>`;
|
|
124
|
+
}
|
|
125
|
+
export const genPaletteTool = tool({
|
|
126
|
+
description: `Generate a harmonious color palette from a base hex color.
|
|
127
|
+
Outputs a visual SVG palette + JSON color codes. Works 100% offline.
|
|
128
|
+
Schemes: monochromatic, complementary, analogous, triadic, split-complementary.
|
|
129
|
+
Perfect for frontend design, branding, and UI theming.`,
|
|
130
|
+
args: {
|
|
131
|
+
color: tool.schema.string().describe('Base hex color (e.g. "#3B82F6" or "3B82F6")'),
|
|
132
|
+
scheme: tool.schema.enum(['monochromatic', 'complementary', 'analogous', 'triadic', 'split-complementary']).optional()
|
|
133
|
+
.describe('Color harmony scheme (default: analogous)'),
|
|
134
|
+
count: tool.schema.number().min(3).max(8).optional().describe('Number of colors (default: 5, max: 8)'),
|
|
135
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension). Auto-generated if omitted'),
|
|
136
|
+
output_path: tool.schema.string().optional().describe('Custom output directory. Default: ~/Downloads/pollinations/palettes/'),
|
|
137
|
+
},
|
|
138
|
+
async execute(args, context) {
|
|
139
|
+
const scheme = args.scheme || 'analogous';
|
|
140
|
+
const count = args.count || 5;
|
|
141
|
+
// Normalize hex
|
|
142
|
+
let hex = args.color.trim();
|
|
143
|
+
if (!hex.startsWith('#'))
|
|
144
|
+
hex = '#' + hex;
|
|
145
|
+
if (!/^#[0-9a-fA-F]{3,6}$/.test(hex)) {
|
|
146
|
+
return `β Invalid hex color: "${args.color}". Use format: #3B82F6 or 3B82F6`;
|
|
147
|
+
}
|
|
148
|
+
if (hex.length === 4)
|
|
149
|
+
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
|
|
150
|
+
// Generate palette
|
|
151
|
+
const colors = generatePalette(hex, scheme, count);
|
|
152
|
+
const outputDir = resolveOutputDir(TOOL_DIRS.palettes, args.output_path);
|
|
153
|
+
// Save SVG
|
|
154
|
+
const safeName = args.filename
|
|
155
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
156
|
+
: `palette_${hex.replace('#', '')}_${scheme}`;
|
|
157
|
+
const svgPath = path.join(outputDir, `${safeName}.svg`);
|
|
158
|
+
const svg = generateSVG(colors);
|
|
159
|
+
fs.writeFileSync(svgPath, svg);
|
|
160
|
+
// Build CSS custom properties snippet
|
|
161
|
+
const cssVars = colors.map((c, i) => ` --color-${i + 1}: ${c.hex};`).join('\n');
|
|
162
|
+
context.metadata({ title: `π¨ Palette: ${scheme} from ${hex}` });
|
|
163
|
+
const colorTable = colors.map(c => ` ${c.hex.toUpperCase()} ${c.role}`).join('\n');
|
|
164
|
+
return [
|
|
165
|
+
`π¨ Color Palette Generated`,
|
|
166
|
+
`βββββββββββββββββββββββββ`,
|
|
167
|
+
`Base: ${hex.toUpperCase()}`,
|
|
168
|
+
`Scheme: ${scheme}`,
|
|
169
|
+
`Colors (${count}):`,
|
|
170
|
+
colorTable,
|
|
171
|
+
``,
|
|
172
|
+
`File: ${svgPath}`,
|
|
173
|
+
``,
|
|
174
|
+
`CSS Variables:`,
|
|
175
|
+
`:root {`,
|
|
176
|
+
cssVars,
|
|
177
|
+
`}`,
|
|
178
|
+
``,
|
|
179
|
+
`Cost: Free (local computation)`,
|
|
180
|
+
].join('\n');
|
|
181
|
+
},
|
|
182
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as QRCode from 'qrcode';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { resolveOutputDir, TOOL_DIRS } from '../shared.js';
|
|
6
|
+
export const genQrcodeTool = tool({
|
|
7
|
+
description: `Generate a QR code image from text, URL, or WiFi credentials.
|
|
8
|
+
Outputs a PNG file saved locally. Works 100% offline, no API key needed.
|
|
9
|
+
Examples: URLs, plain text, WiFi (format: WIFI:T:WPA;S:NetworkName;P:Password;;)`,
|
|
10
|
+
args: {
|
|
11
|
+
content: tool.schema.string().describe('The text, URL, or WiFi string to encode into a QR code'),
|
|
12
|
+
size: tool.schema.number().min(128).max(2048).optional().describe('QR code size in pixels (default: 512)'),
|
|
13
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension). Auto-generated if omitted'),
|
|
14
|
+
output_path: tool.schema.string().optional().describe('Custom output directory. Default: ~/Downloads/pollinations/qrcodes/'),
|
|
15
|
+
},
|
|
16
|
+
async execute(args, context) {
|
|
17
|
+
const size = args.size || 512;
|
|
18
|
+
const outputDir = resolveOutputDir(TOOL_DIRS.qrcodes, args.output_path);
|
|
19
|
+
const safeName = args.filename
|
|
20
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
21
|
+
: `qr_${Date.now()}`;
|
|
22
|
+
const filePath = path.join(outputDir, `${safeName}.png`);
|
|
23
|
+
try {
|
|
24
|
+
await QRCode.toFile(filePath, args.content, {
|
|
25
|
+
width: size,
|
|
26
|
+
margin: 2,
|
|
27
|
+
color: { dark: '#000000', light: '#ffffff' },
|
|
28
|
+
errorCorrectionLevel: 'M',
|
|
29
|
+
});
|
|
30
|
+
const stats = fs.statSync(filePath);
|
|
31
|
+
const fileSizeKB = (stats.size / 1024).toFixed(1);
|
|
32
|
+
const displayContent = args.content.length > 80
|
|
33
|
+
? args.content.substring(0, 77) + '...'
|
|
34
|
+
: args.content;
|
|
35
|
+
context.metadata({ title: `π² QR Code: ${displayContent}` });
|
|
36
|
+
return [
|
|
37
|
+
`π² QR Code GΓ©nΓ©rΓ©`,
|
|
38
|
+
`ββββββββββββββββββ`,
|
|
39
|
+
`Contenu: ${displayContent}`,
|
|
40
|
+
`Taille: ${size}Γ${size}px`,
|
|
41
|
+
`Fichier: ${filePath}`,
|
|
42
|
+
`Poids: ${fileSizeKB} KB`,
|
|
43
|
+
`CoΓ»t: Gratuit (gΓ©nΓ©ration locale)`,
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return `β Erreur QR Code: ${err.message}`;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry β Conditional Injection System
|
|
3
|
+
*
|
|
4
|
+
* Free Universe (no key): 8 tools always available
|
|
5
|
+
* Enter Universe (with key): +6 Pollinations tools
|
|
6
|
+
*
|
|
7
|
+
* Tools are injected ONCE at plugin init. Restart needed after /poll connect.
|
|
8
|
+
*/
|
|
9
|
+
import { genImageTool } from './pollinations/gen_image.js';
|
|
10
|
+
import { genVideoTool } from './pollinations/gen_video.js';
|
|
11
|
+
import { genAudioTool } from './pollinations/gen_audio.js';
|
|
12
|
+
import { transcribeAudioTool } from './pollinations/transcribe_audio.js';
|
|
13
|
+
import { genMusicTool } from './pollinations/gen_music.js';
|
|
14
|
+
import { deepsearchTool } from './pollinations/deepsearch.js';
|
|
15
|
+
import { searchCrawlScrapeTool } from './pollinations/search_crawl_scrape.js';
|
|
16
|
+
/**
|
|
17
|
+
* Build the tool registry based on user's access level
|
|
18
|
+
*
|
|
19
|
+
* @returns Record<string, Tool> to be spread into the plugin's tool: {} property
|
|
20
|
+
*/
|
|
21
|
+
export declare function createToolRegistry(): Record<string, any>;
|
|
22
|
+
export { genImageTool, genVideoTool, genAudioTool, transcribeAudioTool, genMusicTool, deepsearchTool, searchCrawlScrapeTool, };
|