opencode-pollinations-plugin 6.1.0-beta.9 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.de.md +130 -0
  2. package/README.es.md +130 -0
  3. package/README.fr.md +130 -0
  4. package/README.it.md +130 -0
  5. package/README.md +87 -73
  6. package/dist/index.js +52 -161
  7. package/dist/locales/de.json +374 -0
  8. package/dist/locales/en.json +373 -0
  9. package/dist/locales/es.json +374 -0
  10. package/dist/locales/fr.json +373 -0
  11. package/dist/locales/index.d.ts +1 -0
  12. package/dist/locales/index.js +37 -0
  13. package/dist/locales/it.json +374 -0
  14. package/dist/server/commands.d.ts +6 -0
  15. package/dist/server/commands.js +394 -125
  16. package/dist/server/config.d.ts +34 -23
  17. package/dist/server/config.js +200 -108
  18. package/dist/server/connect-response.d.ts +2 -0
  19. package/dist/server/connect-response.js +59 -0
  20. package/dist/server/generate-config.d.ts +3 -30
  21. package/dist/server/generate-config.js +164 -106
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/index.js +124 -149
  24. package/dist/server/logger.d.ts +8 -0
  25. package/dist/server/logger.js +38 -0
  26. package/dist/server/models/cache.d.ts +35 -0
  27. package/dist/server/models/cache.js +160 -0
  28. package/dist/server/models/fetcher.d.ts +18 -0
  29. package/dist/server/models/fetcher.js +194 -0
  30. package/dist/server/models/index.d.ts +6 -0
  31. package/dist/server/models/index.js +5 -0
  32. package/dist/server/models/manual.d.ts +15 -0
  33. package/dist/server/models/manual.js +92 -0
  34. package/dist/server/models/types.d.ts +55 -0
  35. package/dist/server/models/types.js +7 -0
  36. package/dist/server/models/worker.d.ts +22 -0
  37. package/dist/server/models/worker.js +174 -0
  38. package/dist/server/pollinations-api.d.ts +11 -0
  39. package/dist/server/pollinations-api.js +21 -8
  40. package/dist/server/proxy.js +222 -307
  41. package/dist/server/quota.d.ts +2 -0
  42. package/dist/server/quota.js +89 -86
  43. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  44. package/dist/server/scripts/pollinations_pricing.js +246 -0
  45. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  46. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  47. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  48. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  49. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  50. package/dist/server/scripts/test_freetier_audit.js +215 -0
  51. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  52. package/dist/server/scripts/test_parallel_cost.js +104 -0
  53. package/dist/server/toast.d.ts +7 -1
  54. package/dist/server/toast.js +43 -10
  55. package/dist/tools/design/gen_diagram.d.ts +2 -0
  56. package/dist/tools/design/gen_diagram.js +94 -0
  57. package/dist/tools/design/gen_palette.d.ts +2 -0
  58. package/dist/tools/design/gen_palette.js +182 -0
  59. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  60. package/dist/tools/design/gen_qrcode.js +50 -0
  61. package/dist/tools/ffmpeg.d.ts +24 -0
  62. package/dist/tools/ffmpeg.js +54 -0
  63. package/dist/tools/index.d.ts +25 -0
  64. package/dist/tools/index.js +86 -0
  65. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  66. package/dist/tools/pollinations/beta_discovery.js +201 -0
  67. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  68. package/dist/tools/pollinations/cost-guard.js +136 -0
  69. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  70. package/dist/tools/pollinations/deepsearch.js +80 -0
  71. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  72. package/dist/tools/pollinations/gen_audio.js +220 -0
  73. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  74. package/dist/tools/pollinations/gen_image.js +211 -0
  75. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  76. package/dist/tools/pollinations/gen_music.js +157 -0
  77. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  78. package/dist/tools/pollinations/gen_video.js +249 -0
  79. package/dist/tools/pollinations/polli_config.d.ts +2 -0
  80. package/dist/tools/pollinations/polli_config.js +95 -0
  81. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  82. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  83. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  84. package/dist/tools/pollinations/polli_status.js +31 -0
  85. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  86. package/dist/tools/pollinations/polli_web_search.js +126 -0
  87. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  88. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  89. package/dist/tools/pollinations/shared.d.ts +181 -0
  90. package/dist/tools/pollinations/shared.js +758 -0
  91. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  92. package/dist/tools/pollinations/test_estimators.js +22 -0
  93. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  94. package/dist/tools/pollinations/transcribe_audio.js +171 -0
  95. package/dist/tools/power/extract_audio.d.ts +2 -0
  96. package/dist/tools/power/extract_audio.js +179 -0
  97. package/dist/tools/power/extract_frames.d.ts +2 -0
  98. package/dist/tools/power/extract_frames.js +237 -0
  99. package/dist/tools/power/file_to_url.d.ts +2 -0
  100. package/dist/tools/power/file_to_url.js +217 -0
  101. package/dist/tools/power/remove_background.d.ts +2 -0
  102. package/dist/tools/power/remove_background.js +404 -0
  103. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  104. package/dist/tools/power/rmbg_keys.js +79 -0
  105. package/dist/tools/shared.d.ts +30 -0
  106. package/dist/tools/shared.js +80 -0
  107. package/package.json +10 -4
  108. package/dist/server/models-seed.d.ts +0 -18
  109. package/dist/server/models-seed.js +0 -55
@@ -1,9 +1,10 @@
1
1
  import * as https from 'https';
2
- import { loadConfig, saveConfig } from './config.js';
3
- import { getQuotaStatus } from './quota.js';
2
+ import { loadConfig, saveConfig, saveKeyToAuthJson } from './config.js';
3
+ import { getQuotaStatus, fetchUsageForPeriod } from './quota.js';
4
4
  import { emitStatusToast } from './toast.js';
5
- import { getDetailedUsage, getAggregatedModels } from './pollinations-api.js';
6
5
  import { generatePollinationsConfig } from './generate-config.js';
6
+ import { ModelRegistry } from './models/index.js';
7
+ import { t } from '../locales/index.js';
7
8
  function checkEndpoint(ep, key) {
8
9
  return new Promise((resolve) => {
9
10
  const req = https.request({
@@ -45,18 +46,16 @@ function checkEndpoint(ep, key) {
45
46
  });
46
47
  }
47
48
  export async function checkKeyPermissions(key) {
48
- // SEQUENTIAL CHECK (Avoid Rate Limits on Key Verification)
49
- const endpoints = ['/account/profile', '/account/balance', '/account/usage'];
50
- for (const ep of endpoints) {
51
- const res = await checkEndpoint(ep, key);
52
- if (!res.ok) {
53
- return { ok: false, reason: `${ep} (${res.status})` };
54
- }
49
+ // SINGLE CHECK to reduce latency and avoid rate-limits (HIGH-01)
50
+ const res = await checkEndpoint('/account/profile', key);
51
+ if (!res.ok) {
52
+ return { ok: false, reason: `/account/profile (${res.status})` };
55
53
  }
56
54
  return { ok: true };
57
55
  }
58
56
  // === CONSTANTS & PRICING ===
59
57
  const TIER_LIMITS = {
58
+ microbe: { pollen: 0.1, emoji: '🦠' },
60
59
  spore: { pollen: 1, emoji: '🦠' },
61
60
  seed: { pollen: 3, emoji: '🌱' },
62
61
  flower: { pollen: 10, emoji: '🌸' },
@@ -130,6 +129,10 @@ function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
130
129
  };
131
130
  }
132
131
  // === COMMAND HANDLER ===
132
+ let globalClient = null;
133
+ export function setClientForCommands(client) {
134
+ globalClient = client;
135
+ }
133
136
  export async function handleCommand(command) {
134
137
  const parts = command.trim().split(/\s+/);
135
138
  if (!parts[0].startsWith('/poll')) {
@@ -145,48 +148,59 @@ export async function handleCommand(command) {
145
148
  case 'connect':
146
149
  return await handleConnectCommand(args);
147
150
  case 'fallback':
148
- return await handleFallbackCommand(args);
151
+ return handleFallbackCommand(args);
149
152
  case 'config':
150
153
  return handleConfigCommand(args);
151
154
  case 'help':
152
155
  return handleHelpCommand();
156
+ case 'models':
157
+ return await handleModelsCommand(args);
158
+ case 'pricing':
159
+ return await handlePricingCommand();
160
+ case 'infos':
161
+ return await handleInfosCommand();
162
+ case 'addKey': // External trigger
163
+ // UI Pollution Fix: User hates appendPrompt.
164
+ // Just return a message telling them to use the tool.
165
+ return {
166
+ handled: true,
167
+ response: t('commands.generic.add_key_hint')
168
+ };
153
169
  default:
154
170
  return {
155
171
  handled: true,
156
- error: `Commande inconnue: ${subCommand}. Utilisez /pollinations help`
172
+ response: t('commands.generic.unknown_command', { cmd: subCommand })
157
173
  };
158
174
  }
159
175
  }
160
176
  // === SUB-COMMANDS ===
161
177
  async function handleModeCommand(args) {
162
- let mode = args[0];
178
+ const mode = args[0];
163
179
  if (!mode) {
164
180
  const config = loadConfig();
165
181
  return {
166
182
  handled: true,
167
- response: `Mode actuel: ${config.mode}`
183
+ response: t('commands.mode.current', { mode: config.mode })
168
184
  };
169
185
  }
170
- if (mode === 'alwaysfree')
171
- mode = 'economy'; // Alias support
172
- if (!['manual', 'economy', 'pro'].includes(mode)) {
186
+ if (!['manual', 'alwaysfree', 'pro'].includes(mode)) {
173
187
  return {
174
188
  handled: true,
175
- error: `❌ Mode invalide: ${mode}. Valeurs: manual, economy, pro`
189
+ error: t('commands.mode.invalid', { mode })
176
190
  };
177
191
  }
178
192
  const checkConfig = loadConfig();
179
- // JIT VERIFICATION for PRO and ECONOMY Mode
180
- if (mode === 'pro' || mode === 'economy') {
193
+ // JIT VERIFICATION for PRO and ALWAYSFREE Mode
194
+ if (mode === 'pro' || mode === 'alwaysfree') {
181
195
  const checkConfig = loadConfig(); // Reload to be sure
182
196
  const key = checkConfig.apiKey;
183
197
  if (!key) {
184
198
  // If NO key, allow alwaysfree? Yes.
185
199
  // If HAS key, verify it? Yes.
186
200
  if (mode === 'pro')
187
- return { handled: true, error: "❌ Mode Pro nécessite une Clé API configurée." };
201
+ return { handled: true, error: t('commands.mode.pro_requires_key') };
188
202
  }
189
- emitStatusToast('info', 'Vérification des droits...', 'Mode Pro');
203
+ emitStatusToast('info', t('commands.mode.verifying'), 'Mode Pro');
190
204
  try {
191
205
  // Force verify permissions NOW
192
206
  const check = await checkKeyPermissions(key);
@@ -194,28 +208,28 @@ async function handleModeCommand(args) {
194
208
  saveConfig({ mode: 'manual', keyHasAccessToProfile: false });
195
209
  return {
196
210
  handled: true,
197
- error: `❌ **Mode Refusé**\nVotre clé est limitée (Code ${check.status}: ${check.reason}).\nPassage en mode **manual**.`
211
+ error: t('commands.mode.denied', { status: check.status || '?', reason: check.reason || '?' })
198
212
  };
199
213
  }
200
214
  // Valid -> Ensure flag is true
201
215
  saveConfig({ keyHasAccessToProfile: true });
202
216
  }
203
217
  catch (e) {
204
- return { handled: true, error: `❌ Erreur de vérification: ${e.message}` };
218
+ return { handled: true, error: t('commands.mode.verify_error', { error: e.message }) };
205
219
  }
206
220
  }
207
- // Allow switch (if economy or manual, or verified pro)
221
+ // Allow switch (if alwaysfree or manual, or verified pro)
208
222
  saveConfig({ mode: mode });
209
223
  const config = loadConfig();
210
224
  if (config.gui.status !== 'none') {
211
- emitStatusToast('success', `Mode changé vers: ${mode}`, 'Pollinations Config');
225
+ emitStatusToast('success', t('commands.mode.success', { mode }), 'Pollinations Config');
212
226
  }
213
227
  return {
214
228
  handled: true,
215
- response: `✅ Mode changé: ${mode}`
229
+ response: t('commands.mode.success', { mode })
216
230
  };
217
231
  }
218
- async function handleUsageCommand(args) {
232
+ export async function handleUsageCommand(args) {
219
233
  const isFull = args[0] === 'full';
220
234
  try {
221
235
  const quota = await getQuotaStatus(true);
@@ -223,41 +237,41 @@ async function handleUsageCommand(args) {
223
237
  const resetDate = quota.nextResetAt.toLocaleString('fr-FR', { hour: '2-digit', minute: '2-digit' });
224
238
  const timeUntilReset = quota.nextResetAt.getTime() - Date.now();
225
239
  const durationStr = formatDuration(Math.max(0, timeUntilReset));
226
- let response = `### 🌸 Dashboard Pollinations (${config.mode.toUpperCase()})\n\n`;
227
- response += `**Ressources**\n`;
228
- response += `- **Tier**: ${quota.tierEmoji} ${quota.tier.toUpperCase()} (${quota.tierLimit} pollen/jour)\n`;
229
- response += `- **Quota**: ${formatPollen(quota.tierLimit - quota.tierRemaining)} / ${formatPollen(quota.tierLimit)}\n`;
230
- response += `- **Usage**: ${progressBar(quota.tierLimit - quota.tierRemaining, quota.tierLimit)}\n`;
231
- response += `- **Wallet**: $${quota.walletBalance.toFixed(2)}\n`;
232
- response += `- **Reset**: ${resetDate} (dans ${durationStr})\n`;
240
+ let response = t('commands.usage.title', { mode: config.mode.toUpperCase() });
241
+ response += t('commands.usage.resources');
242
+ response += t('commands.usage.tier', { emoji: quota.tierEmoji, tier: quota.tier.toUpperCase(), limit: quota.tierLimit });
243
+ response += t('commands.usage.quota', { remaining: formatPollen(quota.tierLimit - quota.tierRemaining), limit: formatPollen(quota.tierLimit) });
244
+ response += t('commands.usage.usage_bar', { bar: progressBar(quota.tierLimit - quota.tierRemaining, quota.tierLimit) });
245
+ response += t('commands.usage.wallet', { balance: quota.walletBalance.toFixed(2) });
246
+ response += t('commands.usage.reset', { date: resetDate, duration: durationStr });
233
247
  if (isFull && config.apiKey) {
234
248
  if (config.keyHasAccessToProfile === false) {
235
- response += `\n> ⚠️ *Votre clé API ne permet pas l'accès aux détails d'usage (Restriction).*`;
249
+ response += t('commands.usage.restricted_key');
236
250
  }
237
251
  else {
238
- const usageData = await getDetailedUsage(config.apiKey);
239
- if (usageData && usageData.usage) {
240
- const lastReset = calculateResetDate(quota.nextResetAt);
241
- const stats = calculateCurrentPeriodStats(usageData.usage, lastReset, quota.tierLimit);
242
- response += `\n### 📊 Détail Période (depuis ${lastReset.toLocaleTimeString()})\n`;
243
- response += `**Total Requêtes**: ${stats.totalRequests} | **Tokens**: In ${formatTokens(stats.inputTokens)} / Out ${formatTokens(stats.outputTokens)}\n\n`;
244
- response += `| Modèle | Reqs | Coût | Tokens |\n`;
245
- response += `| :--- | :---: | :---: | :---: |\n`;
252
+ const lastReset = calculateResetDate(quota.nextResetAt);
253
+ const usageData = await fetchUsageForPeriod(config.apiKey, lastReset);
254
+ if (usageData && usageData.length > 0) {
255
+ const stats = calculateCurrentPeriodStats(usageData, lastReset, quota.tierLimit);
256
+ response += t('commands.usage.period_detail', { time: lastReset.toLocaleTimeString() });
257
+ response += t('commands.usage.total_reqs', { reqs: stats.totalRequests, inTok: formatTokens(stats.inputTokens), outTok: formatTokens(stats.outputTokens) });
258
+ response += t('commands.usage.table_head1');
259
+ response += t('commands.usage.table_head2');
246
260
  const sorted = Array.from(stats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
247
261
  for (const [model, data] of sorted) {
248
262
  response += `| \`${model}\` | ${data.requests} | ${formatPollen(data.cost)} | ${formatTokens(data.inputTokens + data.outputTokens)} |\n`;
249
263
  }
250
264
  }
251
265
  else {
252
- response += `\n> ⚠️ *Impossible de récupérer l'historique détaillé.*\n`;
266
+ response += t('commands.usage.no_history');
253
267
  }
254
268
  }
255
269
  }
256
270
  else if (isFull) {
257
- response += `\n> ⚠️ *Mode Full nécessite une API Key.*\n`;
271
+ response += t('commands.usage.full_requires_key');
258
272
  }
259
273
  else {
260
- response += `\n_Tapez_ \`/pollinations usage full\` _pour le détail._\n`;
274
+ response += t('commands.usage.hint_full');
261
275
  }
262
276
  return { handled: true, response: response.trim() };
263
277
  }
@@ -265,44 +279,33 @@ async function handleUsageCommand(args) {
265
279
  return { handled: true, error: `Erreur: ${e}` };
266
280
  }
267
281
  }
268
- async function handleFallbackCommand(args) {
269
- let targetMode = args[0];
270
- const targetModel = args[1];
271
- if (!targetMode || !targetModel) {
282
+ function handleFallbackCommand(args) {
283
+ const [main, agent] = args;
284
+ if (!main) {
285
+ const config = loadConfig();
286
+ const freeConfig = `Free: main=${config.fallbacks.free.main}, agent=${config.fallbacks.free.agent}`;
287
+ const enterConfig = `Enter: agent=${config.fallbacks.enter.agent}`;
272
288
  return {
273
289
  handled: true,
274
- error: `Usage: /pollinations fallback [economy|pro] <modèle>`
290
+ response: t('commands.fallback.current', { free: freeConfig, enter: enterConfig })
275
291
  };
276
292
  }
277
- // Alias support
278
- if (targetMode === 'alwaysfree')
279
- targetMode = 'economy';
280
- if (targetMode !== 'economy' && targetMode !== 'pro') {
281
- return { handled: true, error: 'Target mode invalide (economy ou pro)' };
282
- }
283
- // DYNAMIC MODEL VALIDATION (BUG 2 FIX)
284
- try {
285
- const { data: models } = await getAggregatedModels();
286
- const validIds = models.map(m => m.id.replace(/^pollinations\/(free|enter)\//, ''));
287
- if (!validIds.includes(targetModel)) {
288
- const suggestions = validIds.slice(0, 10).join(', ');
289
- return {
290
- handled: true,
291
- error: `⚠️ Modèle inconnu: "${targetModel}".\nExemples valides: ${suggestions}...`
292
- };
293
- }
294
- }
295
- catch (e) {
296
- // Fail-open si API down
297
- console.warn('[Fallback] Model validation failed, allowing:', e);
298
- }
293
+ // Default behavior for "/poll fallback <model> <agent>" is setting FREE fallbacks
294
+ // User needs to use commands (maybe add /poll fallback enter ...) later
295
+ // For now, map to Free Fallback as it's the primary Safety Net
299
296
  const config = loadConfig();
300
- const updates = { fallbacks: { ...config.fallbacks } };
301
- updates.fallbacks[targetMode] = targetModel;
302
- saveConfig(updates);
297
+ saveConfig({
298
+ fallbacks: {
299
+ ...config.fallbacks,
300
+ free: {
301
+ main: main,
302
+ agent: agent || config.fallbacks.free.agent
303
+ }
304
+ }
305
+ });
303
306
  return {
304
307
  handled: true,
305
- response: `✅ Fallback ${targetMode.charAt(0).toUpperCase() + targetMode.slice(1)} configuré: ${targetModel}`
308
+ response: t('commands.fallback.success', { main, agent: agent || config.fallbacks.free.agent })
306
309
  };
307
310
  }
308
311
  async function handleConnectCommand(args) {
@@ -310,21 +313,22 @@ async function handleConnectCommand(args) {
310
313
  if (!key) {
311
314
  return {
312
315
  handled: true,
313
- error: `Utilisation: /pollinations connect <votre_clé_api>`
316
+ error: t('commands.connect.usage')
314
317
  };
315
318
  }
316
319
  // 1. Universal Validation (No Syntax Check) - Functional Check
317
- emitStatusToast('info', 'Vérification de la clé...', 'Pollinations Config');
320
+ emitStatusToast('info', t('commands.connect.verifying'), 'Pollinations Config');
318
321
  try {
319
- const models = await generatePollinationsConfig(key);
320
- // 2. Check if we got real models (not just connect placeholder)
321
- const realModels = models.filter(m => m.id !== 'connect');
322
- if (realModels.length > 0) {
322
+ const models = await generatePollinationsConfig(key, true);
323
+ // 2. Check if we got Enterprise models
324
+ const enterpriseModels = models.filter(m => m.id.startsWith('enter/'));
325
+ if (enterpriseModels.length > 0) {
323
326
  // SUCCESS
324
327
  saveConfig({ apiKey: key }); // Don't force mode 'pro'. Let user decide.
328
+ saveKeyToAuthJson(key); // NATIVE SYNC: Hot-reload on OpenCode bypasses restart requirement !
325
329
  const masked = key.substring(0, 6) + '...';
326
330
  // Count Paid Only models found
327
- const diamondCount = realModels.filter(m => m.name.includes('💎')).length;
331
+ const diamondCount = enterpriseModels.filter(m => m.name.includes('💎')).length;
328
332
  // CHECK RESTRICTIONS: Strict Check (Usage + Profile + Balance)
329
333
  let forcedModeMsg = "";
330
334
  let isLimited = false;
@@ -344,15 +348,15 @@ async function handleConnectCommand(args) {
344
348
  // If Limited -> FORCE MANUAL
345
349
  if (isLimited) {
346
350
  saveConfig({ apiKey: key, mode: 'manual', keyHasAccessToProfile: false });
347
- forcedModeMsg = `\n⚠️ **Clé Limitée** (Echec: ${limitReason}) -> Mode **MANUEL** forcé.\n*Requis pour mode Auto: Profile, Balance & Usage.*`;
351
+ forcedModeMsg = t('commands.connect.limited', { reason: limitReason });
348
352
  }
349
353
  else {
350
354
  saveConfig({ apiKey: key, keyHasAccessToProfile: true }); // Let user keep current mode or default
351
355
  }
352
- emitStatusToast('success', `Clé Valide! (${realModels.length} modèles débloqués)`, 'Pollinations Config');
356
+ emitStatusToast('success', t('commands.connect.success_toast', { count: enterpriseModels.length }), 'Pollinations Config');
353
357
  return {
354
358
  handled: true,
355
- response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${realModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
359
+ response: t('commands.connect.success_response', { key: masked, count: enterpriseModels.length, diamond: diamondCount, forced_msg: forcedModeMsg })
356
360
  };
357
361
  }
358
362
  else {
@@ -364,17 +368,27 @@ async function handleConnectCommand(args) {
364
368
  // Wait, generate-config falls back to providing a list containing "[Enter] GPT-4o (Fallback)" if fetch failed.
365
369
  // So we need to detect if it's a "REAL" fetch or a "FALLBACK" fetch.
366
370
  // The fallback models have `variants: {}` usually, but real ones might too.
367
- // v6.0: No fallback prefix check needed. If models is empty (only connect), key is invalid.
368
- throw new Error("Aucun modèle détecté pour cette clé. Clé invalide ou expirée.");
371
+ // A better check: The fallback list is hardcoded in generate-config.ts catch block.
372
+ // Let's modify generate-config to return EMPTY list on error?
373
+ // Or just check if the returned models work?
374
+ // Simplest: If `generatePollinationsConfig` returns any model starting with `enter/` that includes "(Fallback)" in name, we assume failure?
375
+ // "GPT-4o (Fallback)" is the name.
376
+ const isFallback = models.some(m => m.name.includes('(Fallback)') && m.id.startsWith('enter/'));
377
+ if (isFallback) {
378
+ throw new Error(t('proxy.errors.key_rejected'));
379
+ }
380
+ // If we are here, we got no enter models, or empty list?
381
+ // If key is valid but has no access?
382
+ throw new Error(t('proxy.errors.no_enter_models'));
369
383
  }
370
384
  }
371
385
  catch (e) {
372
386
  // 3. FAILURE HANDLING - Revert to FREE
373
387
  saveConfig({ apiKey: undefined, mode: 'manual' }); // Clear Key, Set Manual
374
- emitStatusToast('error', `Clé Invalide. Retour au mode Gratuit.`, 'Pollinations Config');
388
+ emitStatusToast('error', t('toasts.invalid_key_revert'), 'Pollinations Config');
375
389
  return {
376
390
  handled: true,
377
- error: `❌ **Échec Connexion**: ${e.message || e}\n\nLa configuration a été réinitialisée (Mode Gratuit/Manuel).`
391
+ error: t('proxy.errors.invalid_key_free_mode', { error: e.message || String(e) })
378
392
  };
379
393
  }
380
394
  }
@@ -382,11 +396,40 @@ function handleConfigCommand(args) {
382
396
  const [key, value] = args;
383
397
  if (!key) {
384
398
  const config = loadConfig();
399
+ const k = config.apiKey ? (config.apiKey.length > 8 ? `${config.apiKey.substring(0, 5)}****${config.apiKey.substring(config.apiKey.length - 4)}` : '****') : t('commands.config.not_configured');
400
+ const markdownResponse = `${t('commands.config.title', { version: config.version || 'inconnue' })}
401
+ ${t('commands.config.alias_note')}
402
+ ${t('commands.config.intro')}
403
+
404
+ ${t('commands.config.table_headers')}
405
+ ${t('commands.config.table_divider')}
406
+ | **apiKey** | \`${k}\` | ${t('commands.config.api_key_role')} | \`/poll connect <key>\` |
407
+ | **mode** | \`${config.mode}\` | ${t('commands.config.mode_role')} | \`/poll mode <manual/pro/alwaysfree>\` |
408
+ | **enablePaidTools**| \`${config.enablePaidTools ?? true}\` | ${t('commands.config.enablePaidTools_role')} | \`/poll config enablePaidTools <true/false>\` |
409
+ | **costConfirmationRequired**| \`${config.costConfirmationRequired ?? true}\` | ${t('commands.config.costConfirmationRequired_role')} | \`/poll config costConfirmationRequired <true/false>\` |
410
+ | **costThreshold**| \`${config.costThreshold ?? 0.15} 🌻\` | ${t('commands.config.costThreshold_role')} | \`/poll config costThreshold <X>\` |
411
+ | **cost_estimator**| \`${config.costEstimator ?? true}\` | ${t('commands.config.cost_estimator_role')} | \`/poll config cost_estimator <true/false>\` |
412
+ | **fallbacks.free.main** | \`${config.fallbacks?.free?.main || 'free/mistral'}\` | ${t('commands.config.fallback_main_role')} | \`/poll fallback <main> <agent>\` |
413
+ | **fallbacks.free.agent** | \`${config.fallbacks?.free?.agent || 'free/openai-fast'}\`| ${t('commands.config.fallback_agent_role')} | \`/poll fallback <main> <agent>\` |
414
+ | **fallbacks.enter.agent** | \`${config.fallbacks?.enter?.agent || 'free/openai-fast'}\`| ${t('commands.config.fallback_enter_role')} | *${t('commands.config.managed_auto')}* |
415
+ | **status_gui** | \`${config.gui?.status || 'all'}\` | ${t('commands.config.status_gui_role')} | \`/poll config status_gui <all/alert/none>\` |
416
+ | **logs_gui** | \`${config.gui?.logs || 'error'}\` | ${t('commands.config.logs_gui_role')} | \`/poll config logs_gui <verbose/error/none>\` |
417
+ | **threshold_tier** | \`${config.thresholds?.tier || 80}%\` | ${t('commands.config.threshold_tier_role')} | \`/poll config threshold_tier <1-100>\` |
418
+ | **threshold_wallet** | \`${config.thresholds?.wallet || 80}%\` | ${t('commands.config.threshold_wallet_role')} | \`/poll config threshold_wallet <1-100>\` |
419
+ | **status_bar** | \`${config.statusBar ?? true}\` | ${t('commands.config.status_bar_role')} | \`/poll config status_bar <true/false>\` |
420
+ | **lang** | \`${config.lang || 'en'}\` | ${t('commands.config.lang_role')} | \`/poll config lang <en/fr/es/de/it>\` |`;
385
421
  return {
386
422
  handled: true,
387
- response: JSON.stringify(config, null, 2)
423
+ response: markdownResponse
388
424
  };
389
425
  }
426
+ if (key === 'lang' && value) {
427
+ if (!['en', 'fr', 'es', 'de', 'it'].includes(value)) {
428
+ return { handled: true, error: "Valeurs supportées: en, fr, es, de, it" };
429
+ }
430
+ saveConfig({ lang: value });
431
+ return { handled: true, response: `✅ lang = ${value} (redémarrage recommandé)` };
432
+ }
390
433
  if (key === 'toast_verbosity' && value) {
391
434
  // BACKWARD COMPAT (Maps to Status GUI)
392
435
  if (!['none', 'alert', 'all'].includes(value)) {
@@ -424,65 +467,291 @@ function handleConfigCommand(args) {
424
467
  saveConfig({ thresholds: { ...config.thresholds, tier: threshold } });
425
468
  return { handled: true, response: `✅ threshold_tier = ${threshold}%` };
426
469
  }
427
- if (key === 'threshold_wallet_warn' && value) {
470
+ if (key === 'threshold_wallet' && value) {
428
471
  const threshold = parseInt(value);
429
472
  if (isNaN(threshold) || threshold < 0 || threshold > 100) {
430
473
  return { handled: true, error: 'Valeur entre 0 et 100 requise' };
431
474
  }
432
475
  const config = loadConfig();
433
- saveConfig({ thresholds: { ...config.thresholds, wallet_warn: threshold } });
434
- return { handled: true, response: `✅ threshold_wallet_warn = ${threshold}%` };
435
- }
436
- if (key === 'threshold_wallet_stop' && value) {
437
- const stopValue = parseFloat(value);
438
- if (isNaN(stopValue) || stopValue < 0) {
439
- return { handled: true, error: 'Valeur $ positive requise' };
440
- }
441
- const config = loadConfig();
442
- saveConfig({ thresholds: { ...config.thresholds, wallet_stop: stopValue } });
443
- return { handled: true, response: `✅ threshold_wallet_stop = $${stopValue}` };
476
+ saveConfig({ thresholds: { ...config.thresholds, wallet: threshold } });
477
+ return { handled: true, response: `✅ threshold_wallet = ${threshold}%` };
444
478
  }
445
479
  if (key === 'status_bar' && value) {
446
480
  const enabled = value === 'true';
447
481
  saveConfig({ statusBar: enabled });
448
482
  return { handled: true, response: `✅ status_bar = ${enabled}` };
449
483
  }
484
+ if (key === 'cost_estimator' && value) {
485
+ const enabled = value === 'true';
486
+ const config = loadConfig();
487
+ saveConfig({ ...config, costEstimator: enabled });
488
+ return { handled: true, response: `✅ cost_estimator = ${enabled}` };
489
+ }
490
+ if (key === 'enablePaidTools' && value) {
491
+ const enabled = value === 'true';
492
+ saveConfig({ enablePaidTools: enabled });
493
+ return { handled: true, response: `✅ enablePaidTools = ${enabled}${!enabled ? ' (wallet protection active)' : ''}` };
494
+ }
495
+ if (key === 'costThreshold' && value) {
496
+ const threshold = parseFloat(value);
497
+ if (isNaN(threshold) || threshold < 0) {
498
+ return { handled: true, error: 'Valeur numérique positive requise (en pollen). Ex: 0.15' };
499
+ }
500
+ saveConfig({ costThreshold: threshold });
501
+ return { handled: true, response: `✅ costThreshold = ${threshold} 🌻` };
502
+ }
503
+ if (key === 'costConfirmationRequired' && value) {
504
+ const enabled = value === 'true';
505
+ saveConfig({ costConfirmationRequired: enabled });
506
+ return { handled: true, response: `✅ costConfirmationRequired = ${enabled}` };
507
+ }
450
508
  return {
451
509
  handled: true,
452
- error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet_warn, threshold_wallet_stop, status_bar`
510
+ error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet, status_bar, cost_estimator, enablePaidTools, costThreshold, costConfirmationRequired, lang`
453
511
  };
454
512
  }
455
513
  function handleHelpCommand() {
456
514
  const help = `
457
- ### 🌸 Pollinations Plugin - Commandes V6 (Economy/Pro)
515
+ ${t('commands.help.title')}
516
+ ${t('commands.help.alias_note')}
517
+
518
+ ${t('commands.help.mode_usage')}
519
+
520
+ ${t('commands.help.configuration')}
458
521
 
459
- - **\`/pollinations mode [mode]\`**: Change le mode (economy, pro, manual).
460
- - **\`/pollinations usage [full]\`**: Affiche le dashboard (full = détail).
461
- - **\`/pollinations fallback [mode] [model]\`**: Configure le Safety Net pour economy/pro.
462
- - **\`/pollinations config [key] [value]\`**:
463
- - \`threshold_tier\`: 0-100 (% Alerte Tier).
464
- - \`threshold_wallet_warn\`: 0-100 (% Alerte Wallet Session).
465
- - \`threshold_wallet_stop\`: Montage absolu $ (Stop Wallet).
466
- - \`status_gui\`: none, alert, all.
467
- - \`logs_gui\`: none, error, verbose.
468
- - \`status_bar\`: true/false (Widget).
522
+ ${t('commands.help.models_pricing')}
469
523
  `.trim();
470
524
  return { handled: true, response: help };
471
525
  }
526
+ // === MODELS & PRICING COMMANDS ===
527
+ function parseNameDesc(m) {
528
+ const fullDesc = m.description || m.name;
529
+ const parts = fullDesc.split(" - ");
530
+ if (parts.length > 1) {
531
+ return { nom: parts[0].trim(), desc: parts.slice(1).join(" - ").trim() };
532
+ }
533
+ return { nom: fullDesc, desc: "" };
534
+ }
535
+ export async function handleModelsCommand(args) {
536
+ const filter = args[0]; // optional: image, video, audio, text
537
+ if (!ModelRegistry.isReady()) {
538
+ return {
539
+ handled: true,
540
+ response: t('commands.models.loading')
541
+ };
542
+ }
543
+ const sections = [];
544
+ // --- FETCH FREE UNIVERSE GITHUB/LEGACY MODELS ---
545
+ if (!filter || filter === 'text') {
546
+ try {
547
+ const freeRes = await fetch('https://text.pollinations.ai/models', { signal: AbortSignal.timeout(4000) });
548
+ if (freeRes.ok) {
549
+ const freeData = await freeRes.json();
550
+ sections.push(t('commands.models.free_title'));
551
+ sections.push(t('commands.models.free_desc'));
552
+ sections.push(t('commands.models.free_headers1'));
553
+ sections.push(t('commands.models.free_headers2'));
554
+ for (const m of freeData) {
555
+ const desc = m.description || m.name;
556
+ const aliases = m.aliases ? m.aliases.join(', ') : m.name;
557
+ sections.push(`| \`${m.name}\` | ${aliases} | ${desc.substring(0, 40)} | ${m.vision ? '👁️' : '❌'} | ${m.tools ? '🛠️' : '❌'} |`);
558
+ }
559
+ sections.push('');
560
+ }
561
+ }
562
+ catch (e) {
563
+ sections.push(t('commands.models.free_error'));
564
+ }
565
+ }
566
+ sections.push(t('commands.models.enter_title'));
567
+ const categories = [
568
+ { cat: 'image', emoji: '🎨', label: t('commands.models.cats.image') },
569
+ { cat: 'video', emoji: '🎬', label: t('commands.models.cats.video') },
570
+ { cat: 'audio', emoji: '🔊', label: t('commands.models.cats.audio') },
571
+ { cat: 'text', emoji: '📝', label: t('commands.models.cats.text') },
572
+ ];
573
+ for (const { cat, emoji, label } of categories) {
574
+ if (filter && filter !== cat)
575
+ continue;
576
+ const models = ModelRegistry.list(cat);
577
+ if (models.length === 0)
578
+ continue;
579
+ const sorted = [...models].sort((a, b) => a.name.localeCompare(b.name));
580
+ sections.push(t('commands.models.cat_title', { emoji, label, count: models.length }));
581
+ sections.push(t('commands.models.enter_headers1'));
582
+ sections.push(t('commands.models.enter_headers2'));
583
+ for (const m of sorted) {
584
+ const { nom, desc } = parseNameDesc(m);
585
+ const badges = buildBadges(m);
586
+ const input = buildInputIcons(m);
587
+ const output = buildOutputCost(m);
588
+ sections.push(`| ${nom} | \`${m.name}\` | ${desc.substring(0, 40)} | ${badges} | ${input} | ${output} |`);
589
+ }
590
+ sections.push('');
591
+ }
592
+ sections.push(t('commands.models.capabilities'));
593
+ sections.push(t('commands.models.other'));
594
+ return { handled: true, response: sections.join('\n') };
595
+ }
596
+ export async function handlePricingCommand() {
597
+ try {
598
+ const cp = require('child_process');
599
+ const path = require('path');
600
+ // Pointeur __dirname -> dist/server. Donc on cible scripts/pollinations_pricing.js
601
+ const scriptPath = path.join(__dirname, 'scripts', 'pollinations_pricing.js');
602
+ // Exécution locale via Node (sécurisé pour le bundle prod, pas de npx tsx)
603
+ const output = cp.execSync(`node "${scriptPath}"`, { encoding: 'utf-8', stdio: 'pipe' });
604
+ return { handled: true, response: output };
605
+ }
606
+ catch (e) {
607
+ return { handled: true, error: `Erreur lors de la récupération des prix: ${e.message}` };
608
+ }
609
+ }
610
+ // ─── Formatting Helpers for Models/Pricing ────────────────────────────────
611
+ function buildBadges(m) {
612
+ const f = [];
613
+ if (m.paid_only)
614
+ f.push('💎');
615
+ const allFlags = [...(m.input_modalities || []), ...(m.output_modalities || []), m.name];
616
+ if (m.supportsI2X)
617
+ allFlags.push("👁️");
618
+ const str = allFlags.join(" ").toLowerCase();
619
+ if (str.includes("image") || str.includes("👁️"))
620
+ f.push("👁️");
621
+ if (m.reasoning || str.includes("reasoning"))
622
+ f.push("🧠");
623
+ if (str.includes("audio") || str.includes("whisper") || str.includes("scribe") || str.includes("🎙️"))
624
+ f.push("🎙️");
625
+ if (str.includes("search") || str.includes("sonar") || str.includes("gemini"))
626
+ f.push("🔍");
627
+ if (m.output_modalities.includes("audio") || (m.voices && m.voices.length > 0) || str.includes("tts") || str.includes("music"))
628
+ f.push("🔊");
629
+ if (str.includes("coder") || str.includes("code") || str.includes("gemini"))
630
+ f.push("💻");
631
+ return f.filter((v, i, a) => a.indexOf(v) === i).join(" ");
632
+ }
633
+ function buildInputIcons(m) {
634
+ const icons = [];
635
+ if (m.input_modalities.includes('text'))
636
+ icons.push('📝');
637
+ if (m.input_modalities.includes('image'))
638
+ icons.push('🖼️');
639
+ if (m.input_modalities.includes('audio'))
640
+ icons.push('🎤');
641
+ return icons.join('') || '📝';
642
+ }
643
+ function buildOutputCost(m) {
644
+ const p = m.pricing;
645
+ const tokens = t('commands.pricing_units.tokens');
646
+ const s = t('commands.pricing_units.s');
647
+ const img = t('commands.pricing_units.img');
648
+ const tok = t('commands.pricing_units.tok');
649
+ if (p.completionImageTokens) {
650
+ return p.completionImageTokens < 0.0001
651
+ ? tokens
652
+ : `${p.completionImageTokens} ${img}`;
653
+ }
654
+ if (p.completionVideoSeconds)
655
+ return `${p.completionVideoSeconds} ${s}`;
656
+ if (p.completionVideoTokens)
657
+ return `${tokens}/s`;
658
+ if (p.completionAudioTokens)
659
+ return `${p.completionAudioTokens} ${tok}`;
660
+ if (p.completionAudioSeconds)
661
+ return `${p.completionAudioSeconds} ${s}`;
662
+ if (p.promptAudioSeconds)
663
+ return `${p.promptAudioSeconds} ${s}`;
664
+ if (p.completionTextTokens)
665
+ return `${p.completionTextTokens} ${tok}`;
666
+ return tokens;
667
+ }
668
+ export async function handleInfosCommand() {
669
+ const config = loadConfig();
670
+ let name = "Developer";
671
+ let tier = "anonymous";
672
+ if (config.apiKey) {
673
+ try {
674
+ const res = await fetch('https://gen.pollinations.ai/account/profile', {
675
+ headers: { 'Authorization': `Bearer ${config.apiKey}` }
676
+ });
677
+ if (res.ok) {
678
+ const data = await res.json();
679
+ if (data.name)
680
+ name = data.name;
681
+ tier = data.tier || "anonymous";
682
+ }
683
+ }
684
+ catch (e) {
685
+ // Ignorer l'erreur réseau et garder les valeurs par défaut
686
+ }
687
+ }
688
+ const emojis = {
689
+ microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', anonymous: '👤'
690
+ };
691
+ const tierEmoji = emojis[tier] || '❓';
692
+ const response = `${t('commands.infos.title', { name })}
693
+ ${t('commands.infos.features_title')}
694
+ ${t('commands.infos.features_free')}
695
+
696
+ ${t('commands.infos.features_pro')}
697
+
698
+ ${t('commands.infos.features_config')}
699
+
700
+ ${t('commands.infos.tiers_title', { emoji: tierEmoji, tier: tier.toUpperCase() })}
701
+ ${t('commands.infos.about')}
702
+
703
+ ${t('commands.infos.levels_title')}
704
+ ${t('commands.infos.levels_list')}
705
+
706
+ ${t('commands.infos.beta_note')}
707
+
708
+ ${t('commands.infos.pollen_title')}
709
+
710
+ ${t('commands.infos.pollen_get')}
711
+
712
+ ${t('commands.infos.pollen_spend')}`;
713
+ return { handled: true, response };
714
+ }
472
715
  // === INTEGRATION OPENCODE ===
473
716
  export function createCommandHooks() {
474
717
  return {
475
718
  'tui.command.execute': async (input, output) => {
476
- const result = await handleCommand(input.command);
477
- if (result.handled) {
478
- output.handled = true;
479
- if (result.response) {
480
- output.response = result.response;
481
- }
482
- if (result.error) {
483
- output.error = result.error;
719
+ if (!input.command.startsWith('/pollinations')) {
720
+ return;
721
+ }
722
+ try {
723
+ // Parse command
724
+ const rawArgs = input.command.replace('/pollinations', '').trim();
725
+ const result = await handleCommand(rawArgs);
726
+ if (result.handled) {
727
+ if (result.error) {
728
+ output.error = t('commands.generic.tui_error', { error: result.error });
729
+ }
730
+ else if (result.response) {
731
+ output.response = result.response;
732
+ }
733
+ // If no response and no error, assume handled silently (like appendPrompt)
484
734
  }
485
735
  }
736
+ catch (err) {
737
+ output.error = t('commands.generic.tui_critical', { error: err.message });
738
+ }
739
+ },
740
+ // Hook for UI Commands (Palette / Buttons)
741
+ 'command.execute.before': async (input, output) => {
742
+ const cmd = input.command;
743
+ if (cmd === 'pollinations.addKey') {
744
+ handleCommand('addKey'); // Return help message
745
+ }
746
+ else if (cmd === 'pollinations.usage') {
747
+ const res = await handleCommand('usage');
748
+ if (res.response)
749
+ globalClient?.tui.showToast({ title: "Pollinations Usage", metadata: { type: 'info', message: t('commands.generic.tui_usage_msg') } });
750
+ }
751
+ else if (cmd === 'pollinations.mode') {
752
+ // UI Pollution Fix: SILENCE.
753
+ // User explicitly requested NO messages.
754
+ }
486
755
  }
487
756
  };
488
757
  }