opencode-pollinations-plugin 6.1.0-beta.1 β†’ 6.1.0-beta.10

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.
Files changed (56) hide show
  1. package/README.md +140 -87
  2. package/dist/index.js +33 -154
  3. package/dist/server/commands.d.ts +2 -0
  4. package/dist/server/commands.js +106 -60
  5. package/dist/server/config.d.ts +27 -23
  6. package/dist/server/config.js +24 -50
  7. package/dist/server/generate-config.d.ts +3 -30
  8. package/dist/server/generate-config.js +172 -100
  9. package/dist/server/index.d.ts +2 -1
  10. package/dist/server/index.js +124 -149
  11. package/dist/server/pollinations-api.d.ts +11 -0
  12. package/dist/server/pollinations-api.js +20 -0
  13. package/dist/server/proxy.js +187 -149
  14. package/dist/server/quota.d.ts +8 -0
  15. package/dist/server/quota.js +106 -61
  16. package/dist/server/toast.d.ts +3 -0
  17. package/dist/server/toast.js +16 -0
  18. package/dist/tools/design/gen_diagram.d.ts +2 -0
  19. package/dist/tools/design/gen_diagram.js +94 -0
  20. package/dist/tools/design/gen_palette.d.ts +2 -0
  21. package/dist/tools/design/gen_palette.js +182 -0
  22. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  23. package/dist/tools/design/gen_qrcode.js +50 -0
  24. package/dist/tools/index.d.ts +22 -0
  25. package/dist/tools/index.js +81 -0
  26. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  27. package/dist/tools/pollinations/deepsearch.js +80 -0
  28. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  29. package/dist/tools/pollinations/gen_audio.js +204 -0
  30. package/dist/tools/pollinations/gen_image.d.ts +13 -0
  31. package/dist/tools/pollinations/gen_image.js +239 -0
  32. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  33. package/dist/tools/pollinations/gen_music.js +139 -0
  34. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  35. package/dist/tools/pollinations/gen_video.js +222 -0
  36. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  37. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  38. package/dist/tools/pollinations/shared.d.ts +170 -0
  39. package/dist/tools/pollinations/shared.js +454 -0
  40. package/dist/tools/pollinations/transcribe_audio.d.ts +17 -0
  41. package/dist/tools/pollinations/transcribe_audio.js +235 -0
  42. package/dist/tools/power/extract_audio.d.ts +2 -0
  43. package/dist/tools/power/extract_audio.js +180 -0
  44. package/dist/tools/power/extract_frames.d.ts +2 -0
  45. package/dist/tools/power/extract_frames.js +240 -0
  46. package/dist/tools/power/file_to_url.d.ts +2 -0
  47. package/dist/tools/power/file_to_url.js +217 -0
  48. package/dist/tools/power/remove_background.d.ts +2 -0
  49. package/dist/tools/power/remove_background.js +365 -0
  50. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  51. package/dist/tools/power/rmbg_keys.js +78 -0
  52. package/dist/tools/shared.d.ts +30 -0
  53. package/dist/tools/shared.js +74 -0
  54. package/package.json +9 -3
  55. package/dist/server/models-seed.d.ts +0 -18
  56. package/dist/server/models-seed.js +0 -55
@@ -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
- // === FONCTIONS PRINCIPALES ===
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
- // Pas de clΓ© = Mode manual par dΓ©faut, pas de quota
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
- const usage = usageRes.usage || [];
56
- const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' }; // Default 1 (Spore)
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
- // Calculer le reset
111
+ // 3. Calculate Reset & Usage from History
59
112
  const resetInfo = calculateResetInfo(profile.nextResetAt);
60
- // Calculer l'usage de la pΓ©riode actuelle
61
- const { tierUsed } = calculateCurrentPeriodUsage(usage, resetInfo);
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: tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) <= config.thresholds.tier : false,
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
- // === HELPERS (Native HTTPS) ===
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() + 24 * 60 * 60 * 1000);
214
+ nextReset = new Date(todayResetUTC.getTime() + ONE_DAY_MS);
161
215
  }
162
216
  else {
163
- // Le reset d'aujourd'hui n'est pas encore passΓ©
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 cycleDuration = 24 * 60 * 60 * 1000;
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
- const entryTime = parseUsageTimestamp(entry.timestamp);
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));
@@ -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
+ };
@@ -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,2 @@
1
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
2
+ export declare const genDiagramTool: ToolDefinition;
@@ -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,2 @@
1
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
2
+ export declare const genPaletteTool: ToolDefinition;
@@ -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,2 @@
1
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
2
+ export declare const genQrcodeTool: ToolDefinition;
@@ -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, };