opencode-pollinations-plugin 6.2.7-1 โ†’ 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 (47) 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/locales/de.json +94 -6
  8. package/dist/locales/en.json +94 -6
  9. package/dist/locales/es.json +94 -6
  10. package/dist/locales/fr.json +94 -6
  11. package/dist/locales/index.js +3 -1
  12. package/dist/locales/it.json +94 -6
  13. package/dist/locales/zh.json +461 -0
  14. package/dist/server/commands.d.ts +8 -0
  15. package/dist/server/commands.js +203 -13
  16. package/dist/server/connect-response.js +1 -1
  17. package/dist/server/quota.js +29 -8
  18. package/dist/server/tier-info.d.ts +9 -4
  19. package/dist/server/tier-info.js +29 -18
  20. package/dist/tools/index.d.ts +2 -1
  21. package/dist/tools/index.js +14 -1
  22. package/dist/tools/pollinations/gen_edit_image_free.d.ts +14 -0
  23. package/dist/tools/pollinations/gen_edit_image_free.js +146 -0
  24. package/dist/tools/pollinations/gen_video_free.d.ts +19 -0
  25. package/dist/tools/pollinations/gen_video_free.js +246 -0
  26. package/dist/tools/pollinations/polli_config.js +1 -1
  27. package/dist/tools/pollinations/polli_login.d.ts +13 -0
  28. package/dist/tools/pollinations/polli_login.js +30 -0
  29. package/dist/tools/pollinations/polli_quests.d.ts +3 -0
  30. package/dist/tools/pollinations/polli_quests.js +135 -0
  31. package/package.json +2 -2
  32. package/dist/server/index.d.ts +0 -2
  33. package/dist/server/index.js +0 -158
  34. package/dist/server/scripts/test_cost_endpoints.d.ts +0 -1
  35. package/dist/server/scripts/test_cost_endpoints.js +0 -61
  36. package/dist/server/scripts/test_dynamic_pricing.d.ts +0 -1
  37. package/dist/server/scripts/test_dynamic_pricing.js +0 -39
  38. package/dist/server/scripts/test_freetier_audit.d.ts +0 -11
  39. package/dist/server/scripts/test_freetier_audit.js +0 -215
  40. package/dist/server/scripts/test_parallel_cost.d.ts +0 -1
  41. package/dist/server/scripts/test_parallel_cost.js +0 -104
  42. package/dist/tools/pollinations/deepsearch.d.ts +0 -7
  43. package/dist/tools/pollinations/deepsearch.js +0 -80
  44. package/dist/tools/pollinations/search_crawl_scrape.d.ts +0 -7
  45. package/dist/tools/pollinations/search_crawl_scrape.js +0 -85
  46. package/dist/tools/pollinations/test_estimators.d.ts +0 -1
  47. 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) {
@@ -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 };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * gen_edit_image_free โ€” Always-Free image generation & editing (BONUS tool)
3
+ *
4
+ * Independent FREE-bucket tool: works for ANY OpenCode model, with or WITHOUT a
5
+ * Pollinations API key. Acts as an "always free" fallback for image gen/edit,
6
+ * outside the Pollinations economy (no Pollen, no cost guard).
7
+ *
8
+ * Backed by a public image playground (reverse-engineered open endpoint).
9
+ * Direct-only: the request goes from the END USER's IP, respecting the
10
+ * playground's own 20-generations/IP/day free limit (gen + edit SHARE the same
11
+ * counter). Past the daily quota, prefer Pollinations models.
12
+ */
13
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
14
+ export declare const genEditImageFreeTool: ToolDefinition;