opencode-pollinations-plugin 6.2.7 → 6.3.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 (54) hide show
  1. package/README.de.md +9 -1
  2. package/README.es.md +9 -1
  3. package/README.fr.md +9 -1
  4. package/README.it.md +9 -1
  5. package/README.md +35 -16
  6. package/README.zh.md +9 -1
  7. package/dist/index.js +11 -7
  8. package/dist/locales/de.json +94 -6
  9. package/dist/locales/en.json +94 -6
  10. package/dist/locales/es.json +94 -6
  11. package/dist/locales/fr.json +94 -6
  12. package/dist/locales/index.js +3 -1
  13. package/dist/locales/it.json +94 -6
  14. package/dist/locales/zh.json +461 -0
  15. package/dist/server/commands.d.ts +8 -0
  16. package/dist/server/commands.js +203 -13
  17. package/dist/server/connect-response.js +1 -1
  18. package/dist/server/generate-config.js +4 -6
  19. package/dist/server/models/cache.js +1 -1
  20. package/dist/server/models/fetcher.js +24 -2
  21. package/dist/server/quota.js +29 -8
  22. package/dist/server/tier-info.d.ts +9 -4
  23. package/dist/server/tier-info.js +29 -18
  24. package/dist/tools/index.d.ts +2 -1
  25. package/dist/tools/index.js +14 -1
  26. package/dist/tools/pollinations/beta_discovery.d.ts +11 -4
  27. package/dist/tools/pollinations/beta_discovery.js +288 -136
  28. package/dist/tools/pollinations/gen_edit_image_free.d.ts +14 -0
  29. package/dist/tools/pollinations/gen_edit_image_free.js +146 -0
  30. package/dist/tools/pollinations/gen_video.js +2 -2
  31. package/dist/tools/pollinations/gen_video_free.d.ts +19 -0
  32. package/dist/tools/pollinations/gen_video_free.js +246 -0
  33. package/dist/tools/pollinations/polli_config.js +1 -1
  34. package/dist/tools/pollinations/polli_login.d.ts +13 -0
  35. package/dist/tools/pollinations/polli_login.js +30 -0
  36. package/dist/tools/pollinations/polli_quests.d.ts +3 -0
  37. package/dist/tools/pollinations/polli_quests.js +135 -0
  38. package/package.json +2 -2
  39. package/dist/server/index.d.ts +0 -2
  40. package/dist/server/index.js +0 -158
  41. package/dist/server/scripts/test_cost_endpoints.d.ts +0 -1
  42. package/dist/server/scripts/test_cost_endpoints.js +0 -61
  43. package/dist/server/scripts/test_dynamic_pricing.d.ts +0 -1
  44. package/dist/server/scripts/test_dynamic_pricing.js +0 -39
  45. package/dist/server/scripts/test_freetier_audit.d.ts +0 -11
  46. package/dist/server/scripts/test_freetier_audit.js +0 -215
  47. package/dist/server/scripts/test_parallel_cost.d.ts +0 -1
  48. package/dist/server/scripts/test_parallel_cost.js +0 -104
  49. package/dist/tools/pollinations/deepsearch.d.ts +0 -7
  50. package/dist/tools/pollinations/deepsearch.js +0 -80
  51. package/dist/tools/pollinations/search_crawl_scrape.d.ts +0 -7
  52. package/dist/tools/pollinations/search_crawl_scrape.js +0 -85
  53. package/dist/tools/pollinations/test_estimators.d.ts +0 -1
  54. package/dist/tools/pollinations/test_estimators.js +0 -22
@@ -6,6 +6,7 @@ import { generatePollinationsConfig } from './generate-config.js';
6
6
  import { ModelRegistry } from './models/index.js';
7
7
  import { t } from '../locales/index.js';
8
8
  import { formatTierTable } from './tier-info.js';
9
+ import { buildQuestsReport } from '../tools/pollinations/polli_quests.js';
9
10
  function checkEndpoint(ep, key) {
10
11
  return new Promise((resolve) => {
11
12
  const req = https.request({
@@ -54,14 +55,6 @@ export async function checkKeyPermissions(key) {
54
55
  }
55
56
  return { ok: true };
56
57
  }
57
- // === CONSTANTS & PRICING ===
58
- const TIER_LIMITS = {
59
- microbe: { pollen: 0.1, emoji: '🦠' },
60
- spore: { pollen: 1, emoji: '🦠' },
61
- seed: { pollen: 3, emoji: '🌱' },
62
- flower: { pollen: 10, emoji: '🌸' },
63
- nectar: { pollen: 20, emoji: '🍯' },
64
- };
65
58
  // === MARKDOWN HELPERS ===
66
59
  function formatPollen(amount) {
67
60
  return `${amount.toFixed(2)} 🌼`;
@@ -88,8 +81,9 @@ function parseUsageTimestamp(timestamp) {
88
81
  return new Date(timestamp.replace(' ', 'T') + 'Z');
89
82
  }
90
83
  function calculateResetDate(nextResetAt) {
91
- const now = new Date();
92
- const lastReset = new Date(nextResetAt.getTime() - 24 * 60 * 60 * 1000);
84
+ // Hourly quota system (reset at :00). The "current period" is the last hour,
85
+ // matching the tier window computed in quota.ts. (Previously 24h stale daily model.)
86
+ const lastReset = new Date(nextResetAt.getTime() - 60 * 60 * 1000);
93
87
  return lastReset;
94
88
  }
95
89
  function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
@@ -148,6 +142,8 @@ export async function handleCommand(command) {
148
142
  return await handleUsageCommand(args);
149
143
  case 'connect':
150
144
  return await handleConnectCommand(args);
145
+ case 'login':
146
+ return await startDeviceLogin();
151
147
  case 'fallback':
152
148
  return handleFallbackCommand(args);
153
149
  case 'config':
@@ -160,6 +156,8 @@ export async function handleCommand(command) {
160
156
  return await handlePricingCommand();
161
157
  case 'infos':
162
158
  return await handleInfosCommand();
159
+ case 'quests':
160
+ return await handleQuestsCommand(args);
163
161
  case 'addKey': // External trigger
164
162
  // UI Pollution Fix: User hates appendPrompt.
165
163
  // Just return a message telling them to use the tool.
@@ -393,6 +391,185 @@ async function handleConnectCommand(args) {
393
391
  };
394
392
  }
395
393
  }
394
+ // ─── DEVICE FLOW LOGIN (option C: background poller) ───────────────────────
395
+ /** Best-effort cross-platform browser open. Never throws (headless-safe). */
396
+ function openBrowser(url) {
397
+ try {
398
+ const cp = require('child_process');
399
+ const platform = process.platform;
400
+ const cmd = platform === 'win32' ? 'start ""'
401
+ : platform === 'darwin' ? 'open'
402
+ : 'xdg-open';
403
+ // Detached + ignore stdio so it never blocks the proxy process.
404
+ const child = cp.spawn(cmd, [url], {
405
+ shell: platform === 'win32',
406
+ detached: true,
407
+ stdio: 'ignore',
408
+ });
409
+ child.unref?.();
410
+ return true;
411
+ }
412
+ catch {
413
+ return false;
414
+ }
415
+ }
416
+ function postJsonEnter(path, body) {
417
+ return new Promise((resolve, reject) => {
418
+ const payload = JSON.stringify(body);
419
+ const req = https.request({
420
+ hostname: 'enter.pollinations.ai',
421
+ path,
422
+ method: 'POST',
423
+ headers: {
424
+ 'Content-Type': 'application/json',
425
+ 'Content-Length': Buffer.byteLength(payload),
426
+ 'User-Agent': 'opencode-pollinations-plugin',
427
+ },
428
+ }, (res) => {
429
+ let data = '';
430
+ res.on('data', c => data += c);
431
+ res.on('end', () => {
432
+ try {
433
+ resolve(JSON.parse(data));
434
+ }
435
+ catch (e) {
436
+ reject(new Error(`Bad JSON: ${data.slice(0, 120)}`));
437
+ }
438
+ });
439
+ });
440
+ req.on('error', reject);
441
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error('Timeout')); });
442
+ req.write(payload);
443
+ req.end();
444
+ });
445
+ }
446
+ let loginPollActive = false;
447
+ // Publishable app key (pk_) — embedded for BYOP attribution: the consent screen
448
+ // shows "plugin by fkom13" and traffic is credited to this app. Safe to ship
449
+ // publicly (publishable by design); earningsEnabled=false so users pay nothing extra.
450
+ const APP_CLIENT_ID = 'pk_sATzVHuna3I5e7Sf';
451
+ let loginResultPromise = null;
452
+ let lastLoginPrompt = null; // code+URL prompt, reused on wait timeout
453
+ /**
454
+ * Wait mode for the tool: ensures a login is running (auto-starts + opens the
455
+ * browser if needed), then waits up to ~90s and returns the final outcome.
456
+ * On timeout it returns the code/URL prompt so the agent can ask the user to
457
+ * finish authorizing, then be called again with wait:true.
458
+ */
459
+ export async function awaitDeviceLogin() {
460
+ // Auto-start if nothing is in progress (single-call UX: open + wait + report).
461
+ if (!loginPollActive || !loginResultPromise) {
462
+ const started = await startDeviceLogin();
463
+ if (started.error)
464
+ return started.error;
465
+ // If it reported "already running" without a promise, fall through to wait.
466
+ }
467
+ if (!loginResultPromise) {
468
+ return lastLoginPrompt || t('commands.login.nothing_pending');
469
+ }
470
+ const WAIT_CAP_MS = 120000;
471
+ const timeout = new Promise((res) => setTimeout(() => res({ status: 'error', message: '__TIMEOUT__' }), WAIT_CAP_MS));
472
+ const outcome = await Promise.race([loginResultPromise, timeout]);
473
+ if (outcome.message === '__TIMEOUT__') {
474
+ // Still pending — hand back the code/URL so the user can finish, then retry.
475
+ return (lastLoginPrompt ? lastLoginPrompt + '\n\n' : '') + t('commands.login.still_waiting');
476
+ }
477
+ return outcome.message;
478
+ }
479
+ export async function startDeviceLogin() {
480
+ if (loginPollActive) {
481
+ return { handled: true, response: t('commands.login.already_running') };
482
+ }
483
+ let codeResp;
484
+ try {
485
+ codeResp = await postJsonEnter('/api/device/code', {
486
+ client_id: APP_CLIENT_ID,
487
+ scope: 'profile usage keys', // all scopes shown for transparency; keys (Account Admin) checked by default, user can uncheck
488
+ });
489
+ }
490
+ catch (e) {
491
+ return { handled: true, error: t('commands.login.code_error', { error: e.message }) };
492
+ }
493
+ const userCode = codeResp.user_code;
494
+ const deviceCode = codeResp.device_code;
495
+ const verifyUri = codeResp.verification_uri || 'https://enter.pollinations.ai/device';
496
+ // Standard device link (proven reliable). Scope is applied server-side via the
497
+ // /api/device/code POST body — passing budget/expiry/scope in the /authorize URL
498
+ // breaks submission (their form coerces empty->default and array-scope fails validation).
499
+ // For an unlimited key: user clears Budget + Expiry fields on the form before Authorize.
500
+ const verifyComplete = codeResp.verification_uri_complete || `${verifyUri}?user_code=${userCode}`;
501
+ const interval = (codeResp.interval || 5) * 1000;
502
+ const expiresIn = (codeResp.expires_in || 900) * 1000;
503
+ if (!userCode || !deviceCode) {
504
+ return { handled: true, error: t('commands.login.code_error', { error: 'no code returned' }) };
505
+ }
506
+ // Background poller — non-blocking. Resolves the shared promise on completion.
507
+ loginPollActive = true;
508
+ const deadline = Date.now() + Math.min(expiresIn, 300000); // cap 5 min for UX
509
+ let resolveOutcome;
510
+ loginResultPromise = new Promise((res) => { resolveOutcome = res; });
511
+ const finish = (o) => { loginPollActive = false; resolveOutcome(o); };
512
+ const poll = async () => {
513
+ if (Date.now() > deadline) {
514
+ const msg = t('commands.login.expired');
515
+ emitStatusToast('warning', msg, 'Pollinations Login');
516
+ finish({ status: 'expired', message: msg });
517
+ return;
518
+ }
519
+ try {
520
+ const tok = await postJsonEnter('/api/device/token', { device_code: deviceCode });
521
+ if (tok.access_token) {
522
+ // Got the key — validate & hot-load it (no restart needed)
523
+ const key = tok.access_token;
524
+ try {
525
+ await generatePollinationsConfig(key, true);
526
+ // Verify what the user actually granted (they choose on the consent form).
527
+ // Do NOT presume profile access — check it, like /poll connect does.
528
+ let limited = false;
529
+ try {
530
+ const check = await checkKeyPermissions(key);
531
+ limited = !check.ok;
532
+ }
533
+ catch {
534
+ limited = true;
535
+ }
536
+ saveConfig({ apiKey: key, keyHasAccessToProfile: !limited, ...(limited ? { mode: 'manual' } : {}) });
537
+ saveKeyToAuthJson(key);
538
+ const msg = limited
539
+ ? t('commands.login.success_limited')
540
+ : t('commands.login.success_toast');
541
+ emitStatusToast(limited ? 'warning' : 'success', msg, 'Pollinations Login');
542
+ finish({ status: 'connected', message: msg });
543
+ }
544
+ catch (e) {
545
+ const msg = t('commands.login.validate_error', { error: e.message });
546
+ emitStatusToast('error', msg, 'Pollinations Login');
547
+ finish({ status: 'error', message: msg });
548
+ }
549
+ return;
550
+ }
551
+ // pending → keep polling
552
+ setTimeout(poll, interval);
553
+ }
554
+ catch (e) {
555
+ // authorization_pending / slow_down / transient → keep polling
556
+ setTimeout(poll, interval);
557
+ }
558
+ };
559
+ setTimeout(poll, interval);
560
+ // Try to open the consent page automatically (headless-safe; URL shown as fallback).
561
+ const opened = openBrowser(verifyComplete);
562
+ const promptText = (opened ? t('commands.login.opened') + '\n\n' : '') + t('commands.login.prompt', {
563
+ code: userCode,
564
+ uri: verifyUri,
565
+ uri_complete: verifyComplete,
566
+ });
567
+ lastLoginPrompt = promptText;
568
+ return {
569
+ handled: true,
570
+ response: promptText,
571
+ };
572
+ }
396
573
  function handleConfigCommand(args) {
397
574
  const [key, value] = args;
398
575
  if (!key) {
@@ -425,7 +602,7 @@ ${t('commands.config.table_divider')}
425
602
  };
426
603
  }
427
604
  if (key === 'lang' && value) {
428
- if (!['en', 'fr', 'es', 'de', 'it'].includes(value)) {
605
+ if (!['en', 'fr', 'es', 'de', 'it', 'zh'].includes(value)) {
429
606
  return { handled: true, error: "Valeurs supportées: en, fr, es, de, it" };
430
607
  }
431
608
  saveConfig({ lang: value });
@@ -687,7 +864,7 @@ export async function handleInfosCommand() {
687
864
  }
688
865
  }
689
866
  const emojis = {
690
- microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', anonymous: '👤'
867
+ microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', router: '🐝', anonymous: '👤'
691
868
  };
692
869
  const tierEmoji = emojis[tier] || '❓';
693
870
  // Get dynamic tier table based on user's language
@@ -708,10 +885,12 @@ ${t('commands.infos.levels_title')}
708
885
 
709
886
  ${tierTable}
710
887
 
711
- > ⚠️ **v6.2.4+** : Les quotas sont **horaires** (reset à :00). Le quota journalier est une estimation (~24h × taux horaire).
888
+ ${t('commands.infos.hourly_note')}
712
889
 
713
890
  ${t('commands.infos.beta_note')}
714
891
 
892
+ ${t('commands.infos.quests')}
893
+
715
894
  ${t('commands.infos.pollen_title')}
716
895
 
717
896
  ${t('commands.infos.pollen_get')}
@@ -719,6 +898,17 @@ ${t('commands.infos.pollen_get')}
719
898
  ${t('commands.infos.pollen_spend')}`;
720
899
  return { handled: true, response };
721
900
  }
901
+ async function handleQuestsCommand(args) {
902
+ const arg = (args[0] || 'all').toLowerCase();
903
+ const filter = arg === 'available' ? 'available' : arg === 'claimable' ? 'claimable' : 'all';
904
+ try {
905
+ const report = await buildQuestsReport(filter);
906
+ return { handled: true, response: report };
907
+ }
908
+ catch (e) {
909
+ return { handled: true, error: `Erreur: ${e.message || e}` };
910
+ }
911
+ }
722
912
  // === INTEGRATION OPENCODE ===
723
913
  export function createCommandHooks() {
724
914
  return {
@@ -21,7 +21,7 @@ export async function buildConnectResponse(config) {
21
21
  }
22
22
  }
23
23
  const emojis = {
24
- microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', anonymous: '👤'
24
+ microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', router: '🐝', anonymous: '👤'
25
25
  };
26
26
  const tierEmoji = emojis[tier] || '❓';
27
27
  if (hasKey) {
@@ -2,6 +2,7 @@ import * as https from 'https';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { loadConfig, CONFIG_FILE } from './config.js';
5
+ import { ModelRegistry } from './models/cache.js';
5
6
  import { log as logSystem } from './logger.js';
6
7
  // --- LOGGING ---
7
8
  function log(msg) {
@@ -82,15 +83,12 @@ export async function generatePollinationsConfig(forceApiKey, forceStrict = fals
82
83
  // 2. ENTERPRISE UNIVERSE
83
84
  if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
84
85
  try {
85
- // Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
86
- const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
87
- 'Authorization': `Bearer ${effectiveKey}`
88
- });
89
- const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
86
+ // Utilise le cache centralisé fusionné (V1 + Détaillé) au lieu de re-télécharger
87
+ const enterList = ModelRegistry.list('text');
90
88
  const paidModels = [];
91
89
  enterList.forEach((m) => {
92
90
  if (m.tools === false)
93
- return;
91
+ return; // OpenCode UI chat nécessite explicitement les tools
94
92
  const mapped = mapModel(m, 'enter/', '');
95
93
  modelsOutput.push(mapped);
96
94
  if (m.paid_only) {
@@ -19,7 +19,7 @@ const STATIC_FALLBACK = [
19
19
  { name: 'klein', description: 'FLUX.2 Klein 4B', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.008 }, paid_only: false, supportsI2X: true, outputType: 'image', input_modalities: ['text', 'image'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
20
20
  { name: 'kontext', description: 'FLUX.1 Kontext', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.04 }, paid_only: true, supportsI2X: true, outputType: 'image', input_modalities: ['text', 'image'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
21
21
  // Video — essential
22
- { name: 'grok-video', description: 'Grok Video', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.0025 }, paid_only: false, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [1, 15], aspectRatios: ['16:9', '9:16', '1:1', '4:3'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~10s' },
22
+ { name: 'grok-video-pro', description: 'Grok Video Pro', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.0025 }, paid_only: false, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [1, 15], aspectRatios: ['16:9', '9:16', '1:1', '4:3'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~10s' },
23
23
  { name: 'veo', description: 'Veo 3.1 Fast', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.15 }, paid_only: true, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [4, 8], aspectRatios: ['16:9', '9:16', '1:1'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~45-68s' },
24
24
  // Audio — essential
25
25
  { name: 'elevenlabs', description: 'ElevenLabs v3 TTS', category: 'audio', aliases: [], pricing: { currency: 'pollen', completionAudioTokens: 0.00018 }, paid_only: false, supportsI2X: false, outputType: 'audio', input_modalities: ['text'], output_modalities: ['audio'] },
@@ -78,7 +78,7 @@ function mapRawToModel(raw, fallbackCategory, averageCost) {
78
78
  tools: raw.tools,
79
79
  reasoning: raw.reasoning,
80
80
  is_specialized: raw.is_specialized,
81
- context_window: raw.context_window,
81
+ context_window: raw.context_window || raw.context_length,
82
82
  averageCost: averageCost !== undefined && !isNaN(averageCost) ? averageCost : undefined,
83
83
  };
84
84
  // Apply local patches from manual.ts
@@ -112,6 +112,7 @@ export async function fetchAllModels(apiKey) {
112
112
  ];
113
113
  const statsPromise = fetchJson('https://enter.pollinations.ai/api/model-stats', headers).catch(() => ({ data: [] }));
114
114
  const openapiPromise = fetchJson('https://enter.pollinations.ai/api/docs/open-api/generate-schema', headers).catch(() => ({}));
115
+ const v1ModelsPromise = fetchJson('https://gen.pollinations.ai/v1/models', headers).catch(() => ({ data: [] }));
115
116
  const fetches = endpoints.map(async ({ url, fallbackCategory }) => {
116
117
  try {
117
118
  const raw = await fetchJson(url, headers);
@@ -122,9 +123,16 @@ export async function fetchAllModels(apiKey) {
122
123
  return { url, fallbackCategory, raw: [] };
123
124
  }
124
125
  });
125
- const resultsRaw = await Promise.all([...fetches, statsPromise, openapiPromise]);
126
+ const resultsRaw = await Promise.all([...fetches, statsPromise, openapiPromise, v1ModelsPromise]);
127
+ const v1ModelsRaw = resultsRaw.pop();
126
128
  const openapiRaw = resultsRaw.pop();
127
129
  const statsRaw = resultsRaw.pop();
130
+ // Index V1 endpoints to extract structural modalities and properties
131
+ const v1List = Array.isArray(v1ModelsRaw) ? v1ModelsRaw : (v1ModelsRaw?.data || []);
132
+ const v1Map = new Map();
133
+ for (const v of v1List) {
134
+ v1Map.set(v.id || v.name, v);
135
+ }
128
136
  const statsList = Array.isArray(statsRaw?.data) ? statsRaw.data : [];
129
137
  const statsMap = new Map();
130
138
  for (const s of statsList) {
@@ -137,6 +145,20 @@ export async function fetchAllModels(apiKey) {
137
145
  for (const item of list) {
138
146
  const modelId = item.name || item.id;
139
147
  const avgCost = statsMap.get(modelId);
148
+ const v1Item = v1Map.get(modelId);
149
+ // Merge structuraux de la V1 vers l'Item
150
+ if (v1Item) {
151
+ if (v1Item.input_modalities)
152
+ item.input_modalities = v1Item.input_modalities;
153
+ if (v1Item.output_modalities)
154
+ item.output_modalities = v1Item.output_modalities;
155
+ if (v1Item.context_length)
156
+ item.context_length = v1Item.context_length;
157
+ if (v1Item.tools !== undefined)
158
+ item.tools = v1Item.tools;
159
+ if (v1Item.reasoning !== undefined)
160
+ item.reasoning = v1Item.reasoning;
161
+ }
140
162
  const model = mapRawToModel(item, res.fallbackCategory, avgCost);
141
163
  const uniqueId = model.name;
142
164
  if (!seen.has(uniqueId)) {
@@ -8,12 +8,16 @@ const ONE_HOUR_MS = 60 * 60 * 1000;
8
8
  const HISTORY_RETENTION_MS = 48 * 60 * 60 * 1000; // 48h history
9
9
  // === TIER LIMITS (HOURLY) ===
10
10
  // https://pollinations.ai/pricing - Quotas refreshed every hour
11
+ // Source of truth mirrored from official pollinations/shared/tier-config.ts (2026-06)
12
+ // cadence 'none' => no hourly refill => API returns nextResetAt: null
11
13
  const TIER_LIMITS = {
12
- microbe: { pollen: 0.01, emoji: '🦠' },
13
- spore: { pollen: 0.01, emoji: '🍄' },
14
- seed: { pollen: 0.15, emoji: '🌱' },
15
- flower: { pollen: 0.4, emoji: '🌸' },
16
- nectar: { pollen: 0.8, emoji: '🍯' },
14
+ anonymous: { pollen: 0, emoji: '👤', cadence: 'none' },
15
+ microbe: { pollen: 0, emoji: '🦠', cadence: 'none' },
16
+ spore: { pollen: 0.01, emoji: '🍄', cadence: 'hourly' },
17
+ seed: { pollen: 0.15, emoji: '🌱', cadence: 'hourly' },
18
+ flower: { pollen: 0.4, emoji: '🌸', cadence: 'hourly' },
19
+ nectar: { pollen: 0.8, emoji: '🍯', cadence: 'hourly' },
20
+ router: { pollen: 10, emoji: '🐝', cadence: 'hourly' },
17
21
  };
18
22
  // === LOGGING ===
19
23
  import { logApi } from './logger.js';
@@ -79,7 +83,7 @@ export async function getQuotaStatus(forceRefresh = false) {
79
83
  logQuota(`Fetch Success. Tier: ${profile.tier}, Balance: ${balance}, Next Reset: ${profile.nextResetAt}`);
80
84
  // 3. Smart Fetch : Récupérer uniquement les dépenses du jour (depuis lastReset)
81
85
  const periodUsage = await fetchUsageForPeriod(config.apiKey, resetInfo.lastReset);
82
- const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' };
86
+ const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 0, emoji: '❓', cadence: 'none' };
83
87
  const tierLimit = tierInfo.pollen;
84
88
  // 4. Calcul Strict FreeTier / Wallet
85
89
  const { tierUsed } = calculateCurrentPeriodUsage(periodUsage, resetInfo);
@@ -173,9 +177,26 @@ function fetchAPI(endpoint, apiKey) {
173
177
  });
174
178
  }
175
179
  function calculateResetInfo(nextResetAt) {
176
- const nextReset = new Date(nextResetAt);
177
- const lastReset = new Date(nextReset.getTime() - ONE_HOUR_MS);
178
180
  const now = new Date();
181
+ // Tiers with cadence 'none' (anonymous/microbe) return nextResetAt: null.
182
+ // Guard also against malformed timestamps that would yield Invalid Date (NaN).
183
+ let nextReset = nextResetAt ? new Date(nextResetAt) : null;
184
+ if (!nextReset || isNaN(nextReset.getTime())) {
185
+ // No refill: there is no upcoming reset. Use a stable current-hour window
186
+ // so usage SmartFetch still has a sane lower bound, and surface 0 countdown.
187
+ const lastReset = new Date(now.getTime() - ONE_HOUR_MS);
188
+ return {
189
+ nextReset: now,
190
+ lastReset,
191
+ timeUntilReset: 0,
192
+ timeSinceReset: ONE_HOUR_MS,
193
+ resetHour: now.getUTCHours(),
194
+ resetMinute: now.getUTCMinutes(),
195
+ resetSecond: now.getUTCSeconds(),
196
+ progressPercent: 100
197
+ };
198
+ }
199
+ const lastReset = new Date(nextReset.getTime() - ONE_HOUR_MS);
179
200
  const timeUntilReset = Math.max(0, nextReset.getTime() - now.getTime());
180
201
  const timeSinceReset = Math.max(0, now.getTime() - lastReset.getTime());
181
202
  const progressPercent = Math.min(100, (timeSinceReset / ONE_HOUR_MS) * 100);
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * Tier Information - Central Configuration
3
3
  *
4
- * Hourly quota system (Pollinations API v2026-03)
5
- * Quotas reset every hour at :00
4
+ * Hourly quota system (Pollinations API). Quotas refill every hour at :00.
5
+ * Tiers mirror the official pollinations/shared/tier-config.ts.
6
+ *
7
+ * NOTE (2026-06): The legacy account-level upgrade paths (dev-points → Seed,
8
+ * publish-app → Flower, admin tier-update) were removed upstream. Pollen is now
9
+ * primarily earned by completing Quests. The `condition` fields below describe
10
+ * the broad tier profile, not an automated upgrade trigger.
6
11
  */
7
12
  export interface TierInfo {
8
13
  name: string;
@@ -24,8 +29,8 @@ export declare function getAllTiers(): TierInfo[];
24
29
  /**
25
30
  * Format tier list for display (markdown table)
26
31
  */
27
- export declare function formatTierTable(lang?: 'en' | 'fr' | 'es' | 'de' | 'it'): string;
32
+ export declare function formatTierTable(lang?: 'en' | 'fr' | 'es' | 'de' | 'it' | 'zh'): string;
28
33
  /**
29
34
  * Get dynamic tier description with hourly rates
30
35
  */
31
- export declare function getTierDescription(lang?: 'en' | 'fr' | 'es' | 'de' | 'it'): string;
36
+ export declare function getTierDescription(lang?: 'en' | 'fr' | 'es' | 'de' | 'it' | 'zh'): string;
@@ -1,15 +1,21 @@
1
1
  /**
2
2
  * Tier Information - Central Configuration
3
3
  *
4
- * Hourly quota system (Pollinations API v2026-03)
5
- * Quotas reset every hour at :00
4
+ * Hourly quota system (Pollinations API). Quotas refill every hour at :00.
5
+ * Tiers mirror the official pollinations/shared/tier-config.ts.
6
+ *
7
+ * NOTE (2026-06): The legacy account-level upgrade paths (dev-points → Seed,
8
+ * publish-app → Flower, admin tier-update) were removed upstream. Pollen is now
9
+ * primarily earned by completing Quests. The `condition` fields below describe
10
+ * the broad tier profile, not an automated upgrade trigger.
6
11
  */
12
+ import { t } from '../locales/index.js';
7
13
  export const TIER_INFO = {
8
14
  microbe: {
9
15
  name: 'Microbe',
10
16
  emoji: '🦠',
11
- hourlyPollen: 0.01,
12
- dailyEstimate: 0.24,
17
+ hourlyPollen: 0,
18
+ dailyEstimate: 0,
13
19
  condition: 'Just register!',
14
20
  conditionKey: 'tier.condition.signup',
15
21
  },
@@ -26,8 +32,8 @@ export const TIER_INFO = {
26
32
  emoji: '🌱',
27
33
  hourlyPollen: 0.15,
28
34
  dailyEstimate: 3.6,
29
- condition: '8+ dev points (weekly auto-upgrade)',
30
- conditionKey: 'tier.condition.dev_points',
35
+ condition: 'Active community member',
36
+ conditionKey: 'tier.condition.community',
31
37
  },
32
38
  flower: {
33
39
  name: 'Flower',
@@ -42,8 +48,16 @@ export const TIER_INFO = {
42
48
  emoji: '🍯',
43
49
  hourlyPollen: 0.8,
44
50
  dailyEstimate: 19.2,
45
- condition: 'Coming soon 🔮',
46
- conditionKey: 'tier.condition.coming_soon',
51
+ condition: 'Top contributor',
52
+ conditionKey: 'tier.condition.top_contributor',
53
+ },
54
+ router: {
55
+ name: 'Router',
56
+ emoji: '🐝',
57
+ hourlyPollen: 10,
58
+ dailyEstimate: 240,
59
+ condition: 'Special / invite-only',
60
+ conditionKey: 'tier.condition.special',
47
61
  },
48
62
  };
49
63
  /**
@@ -69,28 +83,25 @@ export function formatTierTable(lang = 'en') {
69
83
  es: '| Nivel | Por hora | Diario (est.) | Condición |',
70
84
  de: '| Stufe | Pro Stunde | Täglich (ca.) | Bedingung |',
71
85
  it: '| Livello | Orario | Giornaliero (stima) | Condizione |',
86
+ zh: '| 等级 | 每小时 | 每日 (估算) | 条件 |',
72
87
  };
73
88
  const separator = '|------|---------|----------------|-----------|';
74
89
  const rows = tiers.map(tier => {
75
- const conditionText = lang === 'fr' ? tier.condition : tier.condition;
90
+ const conditionText = t(tier.conditionKey);
76
91
  return `| ${tier.emoji} **${tier.name}** | **${tier.hourlyPollen} pollen/h** | ~${tier.dailyEstimate}/day | ${conditionText} |`;
77
92
  });
78
- return [headers[lang], separator, ...rows].join('\n');
93
+ return [headers[lang] || headers.en, separator, ...rows].join('\n');
79
94
  }
80
95
  /**
81
96
  * Get dynamic tier description with hourly rates
82
97
  */
83
98
  export function getTierDescription(lang = 'en') {
84
- const isFrench = lang === 'fr';
85
- const pollenWord = isFrench ? 'Pollen' : 'Pollen';
86
- const perHour = isFrench ? '/heure' : '/hour';
87
- const perDay = isFrench ? '/jour (est.)' : '/day (est.)';
99
+ const perHour = lang === 'fr' ? '/heure' : '/hour';
100
+ const perDay = lang === 'fr' ? '/jour (est.)' : '/day (est.)';
88
101
  const tiers = getAllTiers();
89
102
  const lines = tiers.map(tier => {
90
- const conditionText = isFrench ?
91
- (tier.conditionKey === 'tier.condition.publish_app' ? '**Publier une App** (comme ce plugin !)' : tier.condition) :
92
- tier.condition;
93
- return `- ${tier.emoji} **${tier.name}** (**${tier.hourlyPollen} ${pollenWord}${perHour}** ≈ ~${tier.dailyEstimate}${perDay}) : ${conditionText}`;
103
+ const conditionText = t(tier.conditionKey);
104
+ return `- ${tier.emoji} **${tier.name}** (**${tier.hourlyPollen} Pollen${perHour}** ~${tier.dailyEstimate}${perDay}) : ${conditionText}`;
94
105
  });
95
106
  return lines.join('\n');
96
107
  }
@@ -16,10 +16,11 @@ import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
16
16
  import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
17
17
  import { polliStatusTool } from './pollinations/polli_status.js';
18
18
  import { polliConfigTool } from './pollinations/polli_config.js';
19
+ import { polliQuestsTool } from './pollinations/polli_quests.js';
19
20
  /**
20
21
  * Build the tool registry based on user's access level
21
22
  *
22
23
  * @returns Record<string, Tool> to be spread into the plugin's tool: {} property
23
24
  */
24
25
  export declare function createToolRegistry(): Record<string, any>;
25
- export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool };
26
+ export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
@@ -16,6 +16,10 @@ import { removeBackgroundTool } from './power/remove_background.js';
16
16
  import { extractFramesTool } from './power/extract_frames.js';
17
17
  import { extractAudioTool } from './power/extract_audio.js';
18
18
  import { rmbgKeysTool } from './power/rmbg_keys.js';
19
+ // === FREE BONUS: standalone always-free image gen/edit + video (no key, no Pollen) ===
20
+ import { genEditImageFreeTool } from './pollinations/gen_edit_image_free.js';
21
+ import { genVideoFreeTool } from './pollinations/gen_video_free.js';
22
+ import { polliLoginTool } from './pollinations/polli_login.js';
19
23
  // === ENTER TOOLS (Require API key) ===
20
24
  import { polliGenImageTool } from './pollinations/gen_image.js';
21
25
  import { polliGenVideoTool } from './pollinations/gen_video.js';
@@ -27,6 +31,7 @@ import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
27
31
  import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
28
32
  import { polliStatusTool } from './pollinations/polli_status.js';
29
33
  import { polliConfigTool } from './pollinations/polli_config.js';
34
+ import { polliQuestsTool } from './pollinations/polli_quests.js';
30
35
  import { log } from '../server/logger.js';
31
36
  /**
32
37
  * Detect if a valid API key is present
@@ -54,6 +59,12 @@ export function createToolRegistry() {
54
59
  tools['extract_frames'] = extractFramesTool;
55
60
  tools['extract_audio'] = extractAudioTool;
56
61
  tools['rmbg_keys'] = rmbgKeysTool;
62
+ // Bonus tool: always-free image gen/edit (works without a Pollinations key)
63
+ tools['gen_edit_image_free'] = genEditImageFreeTool;
64
+ // Bonus tool: always-free video generation (works without a Pollinations key)
65
+ tools['gen_video_free'] = genVideoFreeTool;
66
+ // Login tool: device-flow login, callable by any model (no key needed to run)
67
+ tools['polli_login'] = polliLoginTool;
57
68
  log(`Free tools injected: ${Object.keys(tools).length}`);
58
69
  // === ENTER UNIVERSE: Only with valid API key (+6 tools) ===
59
70
  if (keyPresent) {
@@ -73,6 +84,8 @@ export function createToolRegistry() {
73
84
  tools['polli_config'] = polliConfigTool;
74
85
  // Plugin Status / Info / Pricing helper map
75
86
  tools['polli_status'] = polliStatusTool;
87
+ // Quests — read-only quest status & claimable Pollen nudge
88
+ tools['polli_quests'] = polliQuestsTool;
76
89
  log(`Enter tools injected (key detected). Total: ${Object.keys(tools).length}`);
77
90
  }
78
91
  else {
@@ -83,4 +96,4 @@ export function createToolRegistry() {
83
96
  return tools;
84
97
  }
85
98
  // Re-export for convenience
86
- export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool };
99
+ export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
@@ -1,9 +1,16 @@
1
1
  /**
2
- * beta_discovery Tool (API Explorer V3 - Hybrid Probe)
2
+ * beta_discovery Tool (API Explorer V4 — Defense-in-Depth)
3
3
  *
4
- * Combines reading the official OpenAPI Specification with active
5
- * blackbox probing (triggering HTTP 400/422 ValidationErrors) to
6
- * discover hidden or undocumented enums and parameters.
4
+ * Combines reading the official OpenAPI Specification with hardened
5
+ * blackbox fuzzing that GUARANTEES HTTP 400 responses by forcefully
6
+ * injecting invalid values. The AI agent NEVER controls the actual
7
+ * values sent to the API.
8
+ *
9
+ * Security Layers:
10
+ * 1. Command Whitelist — only 4 commands exist
11
+ * 2. Endpoint Whitelist — only 4 API routes are probeable
12
+ * 3. Value Injection — fuzz values are hardcoded, AI cannot override
13
+ * 4. Payload Sabotage — fuzz corrupts, probe_missing removes keys
7
14
  */
8
15
  import { type ToolDefinition } from '@opencode-ai/plugin/tool';
9
16
  export declare const polliBetaDiscoveryTool: ToolDefinition;