opencode-pollinations-plugin 6.4.9 → 6.5.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 +67 -54
  2. package/README.es.md +79 -66
  3. package/README.fr.md +70 -57
  4. package/README.it.md +78 -65
  5. package/README.md +33 -29
  6. package/README.zh.md +77 -64
  7. package/dist/locales/de.json +82 -47
  8. package/dist/locales/en.json +84 -49
  9. package/dist/locales/es.json +82 -47
  10. package/dist/locales/fr.json +81 -46
  11. package/dist/locales/it.json +82 -47
  12. package/dist/locales/zh.json +82 -47
  13. package/dist/server/commands.js +118 -178
  14. package/dist/server/config.d.ts +22 -6
  15. package/dist/server/config.js +45 -5
  16. package/dist/server/connect-response.js +7 -7
  17. package/dist/server/generate-config.js +2 -2
  18. package/dist/server/models/cache.d.ts +23 -10
  19. package/dist/server/models/cache.js +46 -24
  20. package/dist/server/models/fetcher.js +2 -0
  21. package/dist/server/models/types.d.ts +2 -0
  22. package/dist/server/models/worker.js +4 -4
  23. package/dist/server/proxy.d.ts +6 -0
  24. package/dist/server/proxy.js +318 -218
  25. package/dist/server/quota.d.ts +23 -32
  26. package/dist/server/quota.js +44 -184
  27. package/dist/server/scripts/pollinations_pricing.js +6 -3
  28. package/dist/server/status.js +1 -2
  29. package/dist/server/toast.js +1 -1
  30. package/dist/tools/index.d.ts +2 -1
  31. package/dist/tools/index.js +3 -1
  32. package/dist/tools/pollinations/artifact-core.d.ts +53 -0
  33. package/dist/tools/pollinations/artifact-core.js +159 -0
  34. package/dist/tools/pollinations/beta_discovery.js +2 -1
  35. package/dist/tools/pollinations/cost-guard.d.ts +2 -2
  36. package/dist/tools/pollinations/error-parser.d.ts +38 -0
  37. package/dist/tools/pollinations/error-parser.js +112 -0
  38. package/dist/tools/pollinations/gen_3d.d.ts +17 -0
  39. package/dist/tools/pollinations/gen_3d.js +207 -0
  40. package/dist/tools/pollinations/gen_image.js +29 -10
  41. package/dist/tools/pollinations/gen_music.js +3 -2
  42. package/dist/tools/pollinations/gen_video.js +13 -2
  43. package/dist/tools/pollinations/polli_config.js +15 -21
  44. package/dist/tools/pollinations/polli_gen_confirm.js +2 -0
  45. package/dist/tools/pollinations/shared.d.ts +1 -1
  46. package/dist/tools/pollinations/shared.js +53 -142
  47. package/dist/tools/pollinations/timeout-policy.d.ts +80 -0
  48. package/dist/tools/pollinations/timeout-policy.js +124 -0
  49. package/dist/tools/pollinations/tool-capability-registry.d.ts +51 -0
  50. package/dist/tools/pollinations/tool-capability-registry.js +215 -0
  51. package/dist/tools/pollinations/transcribe_audio.js +5 -24
  52. package/package.json +64 -62
  53. package/dist/server/tier-info.d.ts +0 -36
  54. package/dist/server/tier-info.js +0 -107
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Tool Capability Registry (v6.5) — capability-centric, declarative.
3
+ *
4
+ * Answers "HOW to execute this capability" — endpoint, transport, execution
5
+ * mode, timeouts, retry/recovery policy, artifact format, backend overrides.
6
+ *
7
+ * The Model Registry remains model-centric ("WHAT exists"). The TCR
8
+ * references/enriches it and does NOT duplicate model fields (pricing,
9
+ * paid_only, modalities stay in the Model Registry).
10
+ *
11
+ * Overrides that the live catalog does NOT expose (special headers, endpoint
12
+ * quirks, token formats) live here — never as `if (model === ...)` scattered
13
+ * across tools.
14
+ */
15
+ import { resolveEffectiveTimeout } from './timeout-policy.js';
16
+ export const TOOL_CAPABILITIES = [
17
+ // ── Pollinations generation (cache-backed, LONG_BLOCKING, SERVER_DEDUP) ──
18
+ {
19
+ capability: 'gen_image',
20
+ backend: 'pollinations',
21
+ transport: { endpoint: 'https://gen.pollinations.ai/image/{prompt}', method: 'GET', mode: 'LONG_BLOCKING' },
22
+ execution: {
23
+ defaultTimeoutSeconds: 600,
24
+ retryPolicy: 'RECOVER_SAME_REQUEST',
25
+ idempotency: 'SERVER_DEDUP',
26
+ supportsCancel: false,
27
+ recovery: 'same request (same model/params/seed) → cache hit, not rebilled',
28
+ },
29
+ backendOverrides: {
30
+ 'gpt-image-2': { quality: ['low', 'med', 'high'], transparent: true },
31
+ kontext: { header: 'x-usage-completion-image-tokens' },
32
+ klein: { header: 'x-usage-completion-image-tokens' },
33
+ },
34
+ },
35
+ {
36
+ capability: 'gen_video',
37
+ backend: 'pollinations',
38
+ // v6.5: /video/{prompt} is the canonical route (SDK/CLI).
39
+ transport: { endpoint: 'https://gen.pollinations.ai/video/{prompt}', method: 'GET', mode: 'LONG_BLOCKING' },
40
+ execution: {
41
+ defaultTimeoutSeconds: 1800,
42
+ retryPolicy: 'RECOVER_SAME_REQUEST',
43
+ idempotency: 'SERVER_DEDUP',
44
+ supportsCancel: false,
45
+ recovery: 'same request (same model/params/seed) → cache hit, not rebilled',
46
+ },
47
+ backendOverrides: {
48
+ wan: { requiresReferenceImage: true, t2vDummyImage: true },
49
+ veo: { interpolationRefs: 2 },
50
+ 'seedance-*': { headerVideoTokens: true, fixedDuration4s: true },
51
+ 'ltx-2': { error520Known: true },
52
+ 'grok-video-pro': { forceAudio: true },
53
+ },
54
+ },
55
+ {
56
+ capability: 'gen_3d',
57
+ backend: 'pollinations',
58
+ transport: { endpoint: 'https://gen.pollinations.ai/3d/{prompt}', method: 'GET', mode: 'LONG_BLOCKING' },
59
+ outputs: { artifactFormat: 'glb', mime: 'model/gltf-binary' },
60
+ execution: {
61
+ defaultTimeoutSeconds: 1800,
62
+ retryPolicy: 'RECOVER_SAME_REQUEST',
63
+ idempotency: 'SERVER_DEDUP',
64
+ supportsCancel: false,
65
+ recovery: 'same request (same model/params/seed) → model3dCache hit, not rebilled',
66
+ },
67
+ backendOverrides: {
68
+ 'trellis-2': { resolutions: ['low', 'medium', 'high'], timeoutSeconds: 1200, maxRefImages: 1 },
69
+ 'hyper3d-rodin': { timeoutSeconds: 1800 },
70
+ },
71
+ },
72
+ {
73
+ capability: 'tts',
74
+ backend: 'pollinations',
75
+ transport: { endpoint: 'https://gen.pollinations.ai/v1/audio/speech', method: 'POST', mode: 'SHORT_REQUEST' },
76
+ execution: { defaultTimeoutSeconds: 600, retryPolicy: 'NO_AUTOMATIC_RETRY', idempotency: 'SERVER_DEDUP' },
77
+ backendOverrides: {
78
+ elevenlabs: { voices: 'registry' },
79
+ 'openai-audio': { voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'], formats: ['mp3', 'wav', 'pcm16'] },
80
+ },
81
+ },
82
+ {
83
+ capability: 'music',
84
+ backend: 'pollinations',
85
+ transport: { endpoint: 'https://gen.pollinations.ai/audio/{prompt}', method: 'GET', mode: 'LONG_BLOCKING' },
86
+ execution: { defaultTimeoutSeconds: 600, retryPolicy: 'NO_AUTOMATIC_RETRY', idempotency: 'SERVER_DEDUP' },
87
+ constraints: { maxDurationSeconds: 300 },
88
+ },
89
+ {
90
+ capability: 'stt',
91
+ backend: 'pollinations',
92
+ transport: { endpoint: 'https://gen.pollinations.ai/v1/audio/transcriptions', method: 'POST', mode: 'SHORT_REQUEST' },
93
+ execution: { defaultTimeoutSeconds: 600, retryPolicy: 'NO_AUTOMATIC_RETRY' },
94
+ },
95
+ {
96
+ capability: 'web_search',
97
+ backend: 'pollinations',
98
+ transport: { endpoint: 'https://gen.pollinations.ai/v1/chat/completions', method: 'POST', mode: 'SHORT_REQUEST' },
99
+ execution: { defaultTimeoutSeconds: 300, retryPolicy: 'NO_AUTOMATIC_RETRY' },
100
+ },
101
+ {
102
+ capability: 'embed',
103
+ backend: 'pollinations',
104
+ transport: { endpoint: 'https://gen.pollinations.ai/v1/embeddings', method: 'POST', mode: 'SHORT_REQUEST' },
105
+ // Billable POST → never auto-replay (NOT a read-only operation).
106
+ execution: { defaultTimeoutSeconds: 60, retryPolicy: 'NO_AUTOMATIC_RETRY', idempotency: 'NONE' },
107
+ },
108
+ // ── Tier services (true ASYNC_JOB with job.id, re-poll safe) ──
109
+ {
110
+ capability: 'video_free',
111
+ backend: 'playground',
112
+ transport: { endpoint: 'https://pruna-playground-production-861e.up.railway.app/api/p-video/generate', method: 'POST', mode: 'ASYNC_JOB' },
113
+ execution: {
114
+ defaultTimeoutSeconds: 900,
115
+ retryPolicy: 'REPOLL_JOB',
116
+ idempotency: 'NONE',
117
+ pollIntervalSeconds: 4,
118
+ supportsCancel: false,
119
+ recovery: 're-poll job.id (never resubmit)',
120
+ },
121
+ },
122
+ {
123
+ capability: 'remove_background',
124
+ backend: 'imgtools',
125
+ // No modelId — the TCR is capability-centric, not model-centric.
126
+ transport: { endpoint: 'https://rmbg.bgeraser.com/api/rmbg', method: 'POST', mode: 'ASYNC_JOB' },
127
+ execution: {
128
+ defaultTimeoutSeconds: 300,
129
+ retryPolicy: 'REPOLL_JOB',
130
+ pollIntervalSeconds: 5,
131
+ supportsCancel: false,
132
+ recovery: 're-poll job.id / key rotation + provider fallback',
133
+ },
134
+ },
135
+ {
136
+ capability: 'object_remove',
137
+ backend: 'imgtools',
138
+ transport: { endpoint: 'https://objectremover.com/api/remove', method: 'POST', mode: 'ASYNC_JOB' },
139
+ execution: { defaultTimeoutSeconds: 300, retryPolicy: 'REPOLL_JOB', pollIntervalSeconds: 3 },
140
+ },
141
+ {
142
+ capability: 'image_upscale',
143
+ backend: 'imgtools',
144
+ transport: { endpoint: 'https://imgupscaler.com/api/upscale', method: 'POST', mode: 'ASYNC_JOB' },
145
+ execution: { defaultTimeoutSeconds: 300, retryPolicy: 'REPOLL_JOB', pollIntervalSeconds: 5 },
146
+ },
147
+ {
148
+ capability: 'image_enhance',
149
+ backend: 'imgtools',
150
+ transport: { endpoint: 'https://imgupscaler.com/api/enhance', method: 'POST', mode: 'ASYNC_JOB' },
151
+ execution: { defaultTimeoutSeconds: 300, retryPolicy: 'REPOLL_JOB', pollIntervalSeconds: 3 },
152
+ },
153
+ {
154
+ capability: 'upload',
155
+ backend: 'filehost',
156
+ transport: { endpoint: 'multi-provider cascade', method: 'POST', mode: 'SHORT_REQUEST' },
157
+ // POST upload → NOT a read-only op. The provider cascade is explicit,
158
+ // not an automatic same-request replay.
159
+ execution: { defaultTimeoutSeconds: 45, retryPolicy: 'NO_AUTOMATIC_RETRY', idempotency: 'NONE' },
160
+ },
161
+ // ── Local (offline) ──
162
+ {
163
+ capability: 'qrcode',
164
+ backend: 'local',
165
+ transport: { endpoint: 'local', method: 'GET', mode: 'LOCAL' },
166
+ execution: { retryPolicy: 'SAFE_READ_ONLY' },
167
+ },
168
+ {
169
+ capability: 'palette',
170
+ backend: 'local',
171
+ transport: { endpoint: 'local', method: 'GET', mode: 'LOCAL' },
172
+ execution: { retryPolicy: 'SAFE_READ_ONLY' },
173
+ },
174
+ {
175
+ capability: 'diagram',
176
+ backend: 'mermaid-ink',
177
+ transport: { endpoint: 'https://mermaid.ink', method: 'GET', mode: 'SHORT_REQUEST' },
178
+ execution: { defaultTimeoutSeconds: 15, retryPolicy: 'NO_AUTOMATIC_RETRY' },
179
+ },
180
+ ];
181
+ /** Resolve a capability declaration by name (optionally filtered by backend). */
182
+ export function resolveCapability(capability, backend) {
183
+ return TOOL_CAPABILITIES.find(c => c.capability === capability && (backend === undefined || c.backend === backend));
184
+ }
185
+ export function listCapabilities() {
186
+ return TOOL_CAPABILITIES.map(c => c.capability);
187
+ }
188
+ /**
189
+ * Effective timeout for a capability+model with full v6.5 precedence:
190
+ * per-call > USER model override > USER capability override
191
+ * > built-in model default > built-in capability > global.
192
+ */
193
+ export function resolveCapabilityTimeout(capability, model, perCall, userConfig) {
194
+ const cap = resolveCapability(capability);
195
+ return resolveEffectiveTimeout({
196
+ perCall,
197
+ model,
198
+ capabilityKey: mapCapabilityToTimeoutKey(capability),
199
+ longRunning: cap?.transport.mode === 'LONG_BLOCKING',
200
+ user: userConfig ?? null,
201
+ });
202
+ }
203
+ function mapCapabilityToTimeoutKey(capability) {
204
+ if (capability === 'gen_image')
205
+ return 'image';
206
+ if (capability === 'gen_video')
207
+ return 'video';
208
+ if (capability === 'gen_3d')
209
+ return 'threeD';
210
+ if (capability === 'tts' || capability === 'music' || capability === 'stt')
211
+ return 'audio';
212
+ if (capability === 'embed')
213
+ return 'embed';
214
+ return undefined;
215
+ }
@@ -50,30 +50,11 @@ export const polliSttTool = tool({
50
50
  // Download from URL
51
51
  context.metadata({ title: t('tools.polli_transcribe_audio.toast_dl') });
52
52
  try {
53
- const https = await import('https');
54
- const http = await import('http');
55
- const protocol = audioPath.startsWith('https') ? https : http;
56
- audioBuffer = await new Promise((resolve, reject) => {
57
- const chunks = [];
58
- protocol.get(audioPath, (res) => {
59
- if (res.statusCode === 301 || res.statusCode === 302) {
60
- // Follow redirect
61
- const redirectUrl = res.headers.location;
62
- if (redirectUrl) {
63
- const redirectProtocol = redirectUrl.startsWith('https') ? https : http;
64
- redirectProtocol.get(redirectUrl, (res2) => {
65
- res2.on('data', chunk => chunks.push(chunk));
66
- res2.on('end', () => resolve(Buffer.concat(chunks)));
67
- res2.on('error', reject);
68
- }).on('error', reject);
69
- return;
70
- }
71
- }
72
- res.on('data', chunk => chunks.push(chunk));
73
- res.on('end', () => resolve(Buffer.concat(chunks)));
74
- res.on('error', reject);
75
- }).on('error', reject);
76
- });
53
+ // v6.5: use a single bounded fetch (timeout) instead of unbounded http(s).get.
54
+ audioBuffer = Buffer.from(await (await fetch(audioPath, {
55
+ signal: AbortSignal.timeout(60000),
56
+ redirect: 'follow',
57
+ })).arrayBuffer());
77
58
  // Extract filename from URL
78
59
  try {
79
60
  const urlPath = new URL(audioPath).pathname;
package/package.json CHANGED
@@ -1,65 +1,67 @@
1
1
  {
2
- "name": "opencode-pollinations-plugin",
3
- "displayName": "Pollinations",
4
- "version": "6.4.9",
5
- "description": "Native Pollinations.ai Provider Plugin for OpenCode — free & premium AI models, media tools, quests, cost guards",
6
- "author": "fkom13",
7
- "license": "MIT",
8
- "keywords": [
9
- "opencode",
10
- "pollinations",
11
- "ai",
12
- "plugin",
13
- "image-generation",
14
- "video-generation",
15
- "llm",
16
- "pollen"
17
- ],
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/fkom13/opencode-pollinations-plugin.git"
21
- },
22
- "bugs": {
23
- "url": "https://github.com/fkom13/opencode-pollinations-plugin/issues"
24
- },
25
- "homepage": "https://github.com/fkom13/opencode-pollinations-plugin#readme",
26
- "type": "module",
27
- "exports": {
28
- ".": {
29
- "types": "./dist/index.d.ts",
30
- "default": "./dist/index.js"
31
- }
32
- },
33
- "bin": {
34
- "opencode-pollinations-plugin": "./bin/setup.js"
35
- },
36
- "main": "./dist/index.js",
37
- "types": "./dist/index.d.ts",
38
- "engines": {
39
- "node": ">=18"
40
- },
41
- "scripts": {
42
- "build": "tsc",
43
- "test": "npm run build && node scripts/tests/test-suite.cjs",
44
- "test:unit": "node scripts/tests/test-suite.cjs",
45
- "test:i18n": "node scripts/tests/test-i18n.cjs",
46
- "prepare": "npm run build",
47
- "prepublishOnly": "npm run build && npm run test:unit && npm run test:i18n"
48
- },
49
- "files": [
50
- "dist",
51
- "bin",
52
- "README.md",
53
- "LICENSE.md"
54
- ],
55
- "dependencies": {
56
- "@opencode-ai/plugin": "^1.0.85",
57
- "qrcode": "^1.5.4",
58
- "zod": "^3.25.76"
59
- },
60
- "devDependencies": {
61
- "@types/node": "^20.0.0",
62
- "@types/qrcode": "^1.5.6",
63
- "typescript": "^5.0.0"
2
+ "name": "opencode-pollinations-plugin",
3
+ "displayName": "Pollinations",
4
+ "version": "6.5.0",
5
+ "description": "Native Pollinations.ai Provider Plugin for OpenCode — free & premium AI models, media tools, quests, cost guards",
6
+ "author": "fkom13",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "opencode",
10
+ "pollinations",
11
+ "ai",
12
+ "plugin",
13
+ "image-generation",
14
+ "video-generation",
15
+ "llm",
16
+ "pollen"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/fkom13/opencode-pollinations-plugin.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/fkom13/opencode-pollinations-plugin/issues"
24
+ },
25
+ "homepage": "https://github.com/fkom13/opencode-pollinations-plugin#readme",
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
64
31
  }
32
+ },
33
+ "bin": {
34
+ "opencode-pollinations-plugin": "bin/setup.js"
35
+ },
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "test": "npm run build && node scripts/tests/test-suite.cjs && node scripts/tests/test-v65.cjs && node scripts/tests/test-i18n.cjs && node scripts/tests/test-ux-vocab.cjs",
44
+ "test:unit": "node scripts/tests/test-suite.cjs",
45
+ "test:v65": "node scripts/tests/test-v65.cjs",
46
+ "test:i18n": "node scripts/tests/test-i18n.cjs",
47
+ "prepare": "npm run build",
48
+ "prepublishOnly": "npm run build && npm run test:unit && npm run test:v65 && npm run test:i18n && npm run test:vocab",
49
+ "test:vocab": "node scripts/tests/test-ux-vocab.cjs"
50
+ },
51
+ "files": [
52
+ "dist",
53
+ "bin",
54
+ "README.md",
55
+ "LICENSE.md"
56
+ ],
57
+ "dependencies": {
58
+ "@opencode-ai/plugin": "^1.0.85",
59
+ "qrcode": "^1.5.4",
60
+ "zod": "^3.25.76"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^20.0.0",
64
+ "@types/qrcode": "^1.5.6",
65
+ "typescript": "^5.0.0"
66
+ }
65
67
  }
@@ -1,36 +0,0 @@
1
- /**
2
- * Tier Information - Central Configuration
3
- *
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.
11
- */
12
- export interface TierInfo {
13
- name: string;
14
- emoji: string;
15
- hourlyPollen: number;
16
- dailyEstimate: number;
17
- condition: string;
18
- conditionKey: string;
19
- }
20
- export declare const TIER_INFO: Record<string, TierInfo>;
21
- /**
22
- * Get tier info by name
23
- */
24
- export declare function getTierInfo(tierName: string): TierInfo | undefined;
25
- /**
26
- * Get all tiers as array (sorted by hourlyPollen)
27
- */
28
- export declare function getAllTiers(): TierInfo[];
29
- /**
30
- * Format tier list for display (markdown table)
31
- */
32
- export declare function formatTierTable(lang?: 'en' | 'fr' | 'es' | 'de' | 'it' | 'zh'): string;
33
- /**
34
- * Get dynamic tier description with hourly rates
35
- */
36
- export declare function getTierDescription(lang?: 'en' | 'fr' | 'es' | 'de' | 'it' | 'zh'): string;
@@ -1,107 +0,0 @@
1
- /**
2
- * Tier Information - Central Configuration
3
- *
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.
11
- */
12
- import { t } from '../locales/index.js';
13
- export const TIER_INFO = {
14
- microbe: {
15
- name: 'Microbe',
16
- emoji: '🦠',
17
- hourlyPollen: 0,
18
- dailyEstimate: 0,
19
- condition: 'Account under review',
20
- conditionKey: 'tier.condition.under_review',
21
- },
22
- spore: {
23
- name: 'Spore',
24
- emoji: '🍄',
25
- hourlyPollen: 0.01,
26
- dailyEstimate: 0.24,
27
- condition: 'New account (default)',
28
- conditionKey: 'tier.condition.default',
29
- },
30
- seed: {
31
- name: 'Seed',
32
- emoji: '🌱',
33
- hourlyPollen: 0.15,
34
- dailyEstimate: 3.6,
35
- condition: 'Active community member',
36
- conditionKey: 'tier.condition.community',
37
- },
38
- flower: {
39
- name: 'Flower',
40
- emoji: '🌸',
41
- hourlyPollen: 0.4,
42
- dailyEstimate: 9.6,
43
- condition: 'Complete Quests & contribute',
44
- conditionKey: 'tier.condition.quests',
45
- },
46
- nectar: {
47
- name: 'Nectar',
48
- emoji: '🍯',
49
- hourlyPollen: 0.8,
50
- dailyEstimate: 19.2,
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',
61
- },
62
- };
63
- /**
64
- * Get tier info by name
65
- */
66
- export function getTierInfo(tierName) {
67
- return TIER_INFO[tierName.toLowerCase()];
68
- }
69
- /**
70
- * Get all tiers as array (sorted by hourlyPollen)
71
- */
72
- export function getAllTiers() {
73
- return Object.values(TIER_INFO).sort((a, b) => a.hourlyPollen - b.hourlyPollen);
74
- }
75
- /**
76
- * Format tier list for display (markdown table)
77
- */
78
- export function formatTierTable(lang = 'en') {
79
- const tiers = getAllTiers();
80
- const headers = {
81
- en: '| Tier | Hourly | Daily (est.) | Condition |',
82
- fr: '| Palier | Horaire | Journalier (est.) | Condition |',
83
- es: '| Nivel | Por hora | Diario (est.) | Condición |',
84
- de: '| Stufe | Pro Stunde | Täglich (ca.) | Bedingung |',
85
- it: '| Livello | Orario | Giornaliero (stima) | Condizione |',
86
- zh: '| 等级 | 每小时 | 每日 (估算) | 条件 |',
87
- };
88
- const separator = '|------|---------|----------------|-----------|';
89
- const rows = tiers.map(tier => {
90
- const conditionText = t(tier.conditionKey);
91
- return `| ${tier.emoji} **${tier.name}** | **${tier.hourlyPollen} pollen/h** | ~${tier.dailyEstimate}/day | ${conditionText} |`;
92
- });
93
- return [headers[lang] || headers.en, separator, ...rows].join('\n');
94
- }
95
- /**
96
- * Get dynamic tier description with hourly rates
97
- */
98
- export function getTierDescription(lang = 'en') {
99
- const perHour = lang === 'fr' ? '/heure' : '/hour';
100
- const perDay = lang === 'fr' ? '/jour (est.)' : '/day (est.)';
101
- const tiers = getAllTiers();
102
- const lines = tiers.map(tier => {
103
- const conditionText = t(tier.conditionKey);
104
- return `- ${tier.emoji} **${tier.name}** (**${tier.hourlyPollen} Pollen${perHour}** ≈ ~${tier.dailyEstimate}${perDay}) : ${conditionText}`;
105
- });
106
- return lines.join('\n');
107
- }