opencode-pollinations-plugin 6.1.0-beta.1 → 6.1.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +140 -87
  2. package/dist/index.js +33 -154
  3. package/dist/server/commands.d.ts +2 -0
  4. package/dist/server/commands.js +106 -60
  5. package/dist/server/config.d.ts +27 -23
  6. package/dist/server/config.js +24 -50
  7. package/dist/server/generate-config.d.ts +3 -30
  8. package/dist/server/generate-config.js +172 -100
  9. package/dist/server/index.d.ts +2 -1
  10. package/dist/server/index.js +124 -149
  11. package/dist/server/pollinations-api.d.ts +11 -0
  12. package/dist/server/pollinations-api.js +20 -0
  13. package/dist/server/proxy.js +187 -149
  14. package/dist/server/quota.d.ts +8 -0
  15. package/dist/server/quota.js +106 -61
  16. package/dist/server/toast.d.ts +3 -0
  17. package/dist/server/toast.js +16 -0
  18. package/dist/tools/design/gen_diagram.d.ts +2 -0
  19. package/dist/tools/design/gen_diagram.js +94 -0
  20. package/dist/tools/design/gen_palette.d.ts +2 -0
  21. package/dist/tools/design/gen_palette.js +182 -0
  22. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  23. package/dist/tools/design/gen_qrcode.js +50 -0
  24. package/dist/tools/index.d.ts +22 -0
  25. package/dist/tools/index.js +81 -0
  26. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  27. package/dist/tools/pollinations/deepsearch.js +80 -0
  28. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  29. package/dist/tools/pollinations/gen_audio.js +204 -0
  30. package/dist/tools/pollinations/gen_image.d.ts +13 -0
  31. package/dist/tools/pollinations/gen_image.js +239 -0
  32. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  33. package/dist/tools/pollinations/gen_music.js +139 -0
  34. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  35. package/dist/tools/pollinations/gen_video.js +222 -0
  36. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  37. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  38. package/dist/tools/pollinations/shared.d.ts +170 -0
  39. package/dist/tools/pollinations/shared.js +454 -0
  40. package/dist/tools/pollinations/transcribe_audio.d.ts +17 -0
  41. package/dist/tools/pollinations/transcribe_audio.js +235 -0
  42. package/dist/tools/power/extract_audio.d.ts +2 -0
  43. package/dist/tools/power/extract_audio.js +180 -0
  44. package/dist/tools/power/extract_frames.d.ts +2 -0
  45. package/dist/tools/power/extract_frames.js +240 -0
  46. package/dist/tools/power/file_to_url.d.ts +2 -0
  47. package/dist/tools/power/file_to_url.js +217 -0
  48. package/dist/tools/power/remove_background.d.ts +2 -0
  49. package/dist/tools/power/remove_background.js +365 -0
  50. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  51. package/dist/tools/power/rmbg_keys.js +78 -0
  52. package/dist/tools/shared.d.ts +30 -0
  53. package/dist/tools/shared.js +74 -0
  54. package/package.json +9 -3
  55. package/dist/server/models-seed.d.ts +0 -18
  56. package/dist/server/models-seed.js +0 -55
@@ -1,12 +1,11 @@
1
- /**
2
- * generate-config.ts - v6.0 Simplified
3
- *
4
- * Single endpoint: gen.pollinations.ai/text/models
5
- * No more Free tier, no cache ETag, no prefixes
6
- */
7
1
  import * as https from 'https';
8
2
  import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
9
5
  import { loadConfig } from './config.js';
6
+ const HOMEDIR = os.homedir();
7
+ const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
10
9
  // --- LOGGING ---
11
10
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
12
11
  function log(msg) {
@@ -14,18 +13,15 @@ function log(msg) {
14
13
  const ts = new Date().toISOString();
15
14
  if (!fs.existsSync(LOG_FILE))
16
15
  fs.writeFileSync(LOG_FILE, '');
17
- fs.appendFileSync(LOG_FILE, `[ConfigGen v6.0] ${ts} ${msg}\n`);
16
+ fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
18
17
  }
19
18
  catch (e) { }
19
+ // Force output to stderr for CLI visibility if needed, but clean.
20
20
  }
21
- // --- NETWORK HELPER ---
21
+ // Fetch Helper
22
22
  function fetchJson(url, headers = {}) {
23
23
  return new Promise((resolve, reject) => {
24
- const finalHeaders = {
25
- ...headers,
26
- 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode-Pollinations/6.0; +https://opencode.ai)'
27
- };
28
- const req = https.get(url, { headers: finalHeaders }, (res) => {
24
+ const req = https.get(url, { headers }, (res) => {
29
25
  let data = '';
30
26
  res.on('data', chunk => data += chunk);
31
27
  res.on('end', () => {
@@ -35,7 +31,7 @@ function fetchJson(url, headers = {}) {
35
31
  }
36
32
  catch (e) {
37
33
  log(`JSON Parse Error for ${url}: ${e}`);
38
- resolve([]);
34
+ resolve([]); // Fail safe -> empty list
39
35
  }
40
36
  });
41
37
  });
@@ -43,132 +39,208 @@ function fetchJson(url, headers = {}) {
43
39
  log(`Network Error for ${url}: ${e.message}`);
44
40
  reject(e);
45
41
  });
46
- req.setTimeout(15000, () => {
42
+ req.setTimeout(5000, () => {
47
43
  req.destroy();
48
44
  reject(new Error('Timeout'));
49
45
  });
50
46
  });
51
47
  }
52
- // --- GENERATOR LOGIC ---
53
- export async function generatePollinationsConfig(forceApiKey) {
48
+ function formatName(id, censored = true) {
49
+ let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
50
+ clean = clean.replace(/\b\w/g, l => l.toUpperCase());
51
+ if (!censored)
52
+ clean += " (Uncensored)";
53
+ return clean;
54
+ }
55
+ // --- MAIN GENERATOR logic ---
56
+ // --- MAIN GENERATOR logic ---
57
+ export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
54
58
  const config = loadConfig();
55
59
  const modelsOutput = [];
60
+ log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
61
+ // Use forced key (from Hook) or cached key
56
62
  const effectiveKey = forceApiKey || config.apiKey;
57
- log(`Starting Configuration v6.0...`);
58
- log(`API Key present: ${!!effectiveKey}`);
59
- // 1. ALWAYS add "connect" placeholder model
60
- modelsOutput.push({
61
- id: 'connect',
62
- name: '🔑 Connect your Pollinations Account',
63
- modalities: { input: ['text'], output: ['text'] },
64
- tool_call: false
65
- });
66
- // 2. If no API key, return only connect placeholder
67
- if (!effectiveKey || effectiveKey.length < 5 || effectiveKey === 'dummy') {
68
- log('No API key configured. Returning connect placeholder only.');
69
- return modelsOutput;
70
- }
71
- // 3. Fetch models from Enterprise endpoint
63
+ // 1. FREE UNIVERSE
72
64
  try {
73
- log('Fetching models from gen.pollinations.ai/text/models...');
74
- const rawList = await fetchJson('https://gen.pollinations.ai/text/models', {
75
- 'Authorization': `Bearer ${effectiveKey}`
76
- });
77
- const modelsList = Array.isArray(rawList) ? rawList : (rawList.data || []);
78
- log(`Received ${modelsList.length} models from API`);
79
- modelsList.forEach((m) => {
80
- // Skip models without tools support (except nomnom - special handling)
81
- if (m.tools === false && m.name !== 'nomnom') {
82
- log(`Skipping ${m.name} (no tools)`);
83
- return;
84
- }
85
- const mapped = mapModel(m);
65
+ // Switch to main models endpoint (User provided curl confirms it has 'description')
66
+ const freeList = await fetchJson('https://text.pollinations.ai/models');
67
+ const list = Array.isArray(freeList) ? freeList : (freeList.data || []);
68
+ list.forEach((m) => {
69
+ const mapped = mapModel(m, 'free/', '');
86
70
  modelsOutput.push(mapped);
87
71
  });
88
- log(`Total models registered: ${modelsOutput.length}`);
89
- log(`Model IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
72
+ log(`Fetched ${modelsOutput.length} Free models.`);
90
73
  }
91
74
  catch (e) {
92
- log(`Error fetching models: ${e.message}`);
93
- // Return connect placeholder only on error
75
+ log(`Error fetching Free models: ${e}`);
76
+ // Fallback Robust (Offline support)
77
+ modelsOutput.push({ id: "free/mistral", name: "Mistral Nemo (Fallback)", object: "model", variants: {} });
78
+ modelsOutput.push({ id: "free/openai", name: "OpenAI (Fallback)", object: "model", variants: {} });
79
+ modelsOutput.push({ id: "free/gemini", name: "Gemini Flash (Fallback)", object: "model", variants: {} });
80
+ }
81
+ // 1.5 FALLBACK: Si aucun modèle, ajouter le modèle de connexion
82
+ if (modelsOutput.length === 0) {
83
+ log(`[ConfigGen] No models available. Adding connect-pollinations fallback.`);
84
+ modelsOutput.unshift({
85
+ id: 'connect-pollinations',
86
+ name: '⚡ Pollinations',
87
+ object: 'model',
88
+ variants: {}
89
+ });
90
+ }
91
+ // ALIAS Removed for Clean Config
92
+ // const hasGeminiAlias = modelsOutput.find(m => m.id === 'pollinations/free/gemini');
93
+ // if (!hasGeminiAlias) {
94
+ // modelsOutput.push({ id: "pollinations/free/gemini", name: "[Free] Gemini Flash (Alias)", object: "model", variants: {} });
95
+ // }
96
+ // 2. ENTERPRISE UNIVERSE
97
+ if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
98
+ try {
99
+ // Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
100
+ const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
101
+ 'Authorization': `Bearer ${effectiveKey}`
102
+ });
103
+ const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
104
+ const paidModels = [];
105
+ enterList.forEach((m) => {
106
+ if (m.tools === false)
107
+ return;
108
+ const mapped = mapModel(m, 'enter/', '');
109
+ modelsOutput.push(mapped);
110
+ if (m.paid_only) {
111
+ paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
112
+ }
113
+ });
114
+ log(`Total models (Free+Pro): ${modelsOutput.length}`);
115
+ // Save Paid Models List for Proxy
116
+ try {
117
+ const paidListPath = path.join(config.gui ? path.dirname(CONFIG_FILE) : '/tmp', 'pollinations-paid-models.json');
118
+ // Ensure dir exists (re-use config dir logic from config.ts if possible, or just assume it exists since config loaded)
119
+ if (fs.existsSync(path.dirname(paidListPath))) {
120
+ fs.writeFileSync(paidListPath, JSON.stringify(paidModels));
121
+ }
122
+ }
123
+ catch (e) {
124
+ log(`Error saving paid models list: ${e}`);
125
+ }
126
+ }
127
+ catch (e) {
128
+ log(`Error fetching Enterprise models: ${e}`);
129
+ // STRICT MODE (Validation): Do not return fake fallback models.
130
+ if (forceStrict)
131
+ throw e;
132
+ // Fallback Robust for Enterprise (User has Key but discovery failed)
133
+ modelsOutput.push({ id: "enter/gpt-4o", name: "GPT-4o (Fallback)", object: "model", variants: {} });
134
+ // ...
135
+ modelsOutput.push({ id: "enter/claude-3-5-sonnet", name: "Claude 3.5 Sonnet (Fallback)", object: "model", variants: {} });
136
+ modelsOutput.push({ id: "enter/deepseek-reasoner", name: "DeepSeek R1 (Fallback)", object: "model", variants: {} });
137
+ }
94
138
  }
95
139
  return modelsOutput;
96
140
  }
97
- // --- UTILS ---
141
+ // --- CAPABILITY ICONS ---
98
142
  function getCapabilityIcons(raw) {
99
143
  const icons = [];
100
- // Vision: check both input_modalities and legacy vision flag
101
- if (raw.input_modalities?.includes('image') || raw.vision === true) {
144
+ // Vision: accepts images
145
+ if (raw.input_modalities?.includes('image'))
102
146
  icons.push('👁️');
103
- }
104
- // Audio input
105
- if (raw.input_modalities?.includes('audio') || raw.audio === true) {
147
+ // Audio Input
148
+ if (raw.input_modalities?.includes('audio'))
106
149
  icons.push('🎙️');
107
- }
108
- // Audio output
109
- if (raw.output_modalities?.includes('audio')) {
150
+ // Audio Output
151
+ if (raw.output_modalities?.includes('audio'))
110
152
  icons.push('🔊');
111
- }
112
- // Reasoning
113
- if (raw.reasoning === true) {
153
+ // Reasoning capability
154
+ if (raw.reasoning === true)
114
155
  icons.push('🧠');
115
- }
116
- // Search capability
117
- if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search')) {
156
+ // Web Search (from description)
157
+ if (raw.description?.toLowerCase().includes('search') ||
158
+ raw.name?.includes('search') ||
159
+ raw.name?.includes('perplexity')) {
118
160
  icons.push('🔍');
119
161
  }
120
- // Tools
121
- if (raw.tools === true) {
122
- icons.push('🛠️');
123
- }
124
- // Paid only
125
- if (raw.paid_only === true) {
126
- icons.push('💎');
127
- }
162
+ // Tool/Function calling
163
+ if (raw.tools === true)
164
+ icons.push('💻');
128
165
  return icons.length > 0 ? ` ${icons.join('')}` : '';
129
166
  }
130
- function formatName(id) {
131
- let clean = id.replace(/-/g, ' ');
132
- clean = clean.replace(/\b\w/g, l => l.toUpperCase());
133
- return clean;
134
- }
135
- function mapModel(raw) {
136
- const rawId = raw.name;
137
- // Build display name
167
+ // --- MAPPING ENGINE ---
168
+ function mapModel(raw, prefix, namePrefix) {
169
+ const rawId = raw.id || raw.name;
170
+ const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
138
171
  let baseName = raw.description;
139
172
  if (!baseName || baseName === rawId) {
140
- baseName = formatName(rawId);
173
+ baseName = formatName(rawId, raw.censored !== false);
141
174
  }
142
- // Truncate after first " - "
175
+ // CLEANUP: Simple Truncation Rule (Requested by User)
176
+ // "Start from left, find ' - ', delete everything after."
143
177
  if (baseName && baseName.includes(' - ')) {
144
178
  baseName = baseName.split(' - ')[0].trim();
145
179
  }
180
+ // Gérer les icônes pour paid_only et modèles FREE
181
+ let paidPrefix = '';
182
+ let freeSuffix = '';
183
+ if (raw.paid_only) {
184
+ paidPrefix = '💎 '; // Icône diamant devant les modèles payants
185
+ }
186
+ if (prefix === 'free/') {
187
+ freeSuffix = ' (free)'; // Suffixe pour l'univers FREE
188
+ }
189
+ // Get capability icons from API metadata
146
190
  const capabilityIcons = getCapabilityIcons(raw);
147
- const finalName = `${baseName}${capabilityIcons}`;
148
- // Determine modalities for OpenCode
149
- const inputMods = raw.input_modalities || ['text'];
150
- const outputMods = raw.output_modalities || ['text'];
191
+ const finalName = `${paidPrefix}${baseName}${capabilityIcons}${freeSuffix}`;
151
192
  const modelObj = {
152
- id: rawId, // No prefix! Direct model ID
193
+ id: fullId,
153
194
  name: finalName,
195
+ object: 'model',
196
+ variants: {},
197
+ // Declare modalities for OpenCode vision support
154
198
  modalities: {
155
- input: inputMods,
156
- output: outputMods
157
- },
158
- tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
199
+ input: raw.input_modalities || ['text'],
200
+ output: raw.output_modalities || ['text']
201
+ }
159
202
  };
160
- // Model-specific limits
203
+ // --- ENRICHISSEMENT ---
204
+ if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
205
+ modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
206
+ }
207
+ if (rawId.includes('gemini') && !rawId.includes('fast')) {
208
+ if (!modelObj.variants.high_reasoning && (rawId === 'gemini' || rawId === 'gemini-large')) {
209
+ modelObj.variants.high_reasoning = { options: { reasoningEffort: "high", budgetTokens: 16000 } };
210
+ }
211
+ }
212
+ if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
213
+ modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
214
+ }
215
+ // NOVA FIX: Bedrock limit ~10k (User reported error > 10000)
216
+ // We MUST set the limit on the model object itself so OpenCode respects it by default.
161
217
  if (rawId.includes('nova')) {
162
- modelObj.limit = { output: 8000, context: 128000 };
218
+ modelObj.limit = {
219
+ output: 8000,
220
+ context: 128000 // Nova Micro/Lite/Pro usually 128k
221
+ };
222
+ // Also keep variant just in case
223
+ modelObj.variants.bedrock_safe = { options: { maxTokens: 8000 } };
224
+ }
225
+ // BEDROCK/ENTERPRISE LIMITS (Chickytutor only)
226
+ if (rawId.includes('chickytutor')) {
227
+ modelObj.limit = {
228
+ output: 8192,
229
+ context: 128000
230
+ };
163
231
  }
164
- if (rawId === 'nomnom') {
165
- modelObj.limit = { output: 2048, context: 32768 };
166
- modelObj.tool_call = false; // NomNom is a router, no external tools
232
+ // NOMNOM FIX: User reported error if max_tokens is missing.
233
+ // Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
234
+ if (rawId.includes('nomnom') || rawId.includes('scrape')) {
235
+ modelObj.limit = {
236
+ output: 2048, // User used 1500 successfully
237
+ context: 32768
238
+ };
167
239
  }
168
- if (rawId.includes('chicky') || rawId.includes('mistral')) {
169
- modelObj.limit = { output: 4096, context: 8192 };
170
- modelObj.options = { maxTokens: 4096 };
240
+ if (rawId.includes('fast') || rawId.includes('flash') || rawId.includes('lite')) {
241
+ if (!rawId.includes('gemini')) {
242
+ modelObj.variants.speed = { options: { thinking: { disabled: true } } };
243
+ }
171
244
  }
172
- log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
173
245
  return modelObj;
174
246
  }
@@ -1 +1,2 @@
1
- export {};
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+ export declare const plugin: Plugin;
@@ -4,7 +4,9 @@ import * as path from 'path';
4
4
  import { getAggregatedModels } from './pollinations-api.js';
5
5
  import { loadConfig, saveConfig } from './config.js';
6
6
  import { handleChatCompletion } from './proxy.js';
7
- const LOG_FILE = path.join(process.env.HOME || '/tmp', '.config/opencode/plugins/pollinations-v3.log');
7
+ import { createCommandHooks, setClientForCommands, checkKeyPermissions } from './commands.js';
8
+ import { fileURLToPath } from 'url';
9
+ const LOG_FILE = path.join(process.env.HOME || '/tmp', '.config/opencode/plugins/pollinations-v6.log');
8
10
  // Simple file logger
9
11
  function log(msg) {
10
12
  const ts = new Date().toISOString();
@@ -14,170 +16,143 @@ function log(msg) {
14
16
  }
15
17
  fs.appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
16
18
  }
17
- catch (e) {
18
- // silent fail
19
- }
20
- }
21
- // CRASH GUARD
22
- const CRASH_LOG = '/tmp/opencode_pollinations_crash.log';
23
- process.on('uncaughtException', (err) => {
24
- try {
25
- const msg = `[CRASH] Uncaught Exception: ${err.message}\n${err.stack}\n`;
26
- fs.appendFileSync(CRASH_LOG, msg);
27
- console.error(msg);
28
- }
29
19
  catch (e) { }
30
- process.exit(1);
31
- });
32
- process.on('unhandledRejection', (reason, promise) => {
33
- try {
34
- const msg = `[CRASH] Unhandled Rejection: ${reason}\n`;
35
- fs.appendFileSync(CRASH_LOG, msg);
36
- }
37
- catch (e) { }
38
- });
39
- const server = http.createServer(async (req, res) => {
40
- log(`${req.method} ${req.url}`);
41
- // CORS Headers
42
- res.setHeader('Access-Control-Allow-Origin', '*');
43
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
44
- res.setHeader('Access-Control-Allow-Headers', '*');
45
- if (req.method === 'OPTIONS') {
46
- res.writeHead(204);
47
- res.end();
20
+ }
21
+ const PORT = parseInt(process.env.POLLINATIONS_PORT || '10001', 10);
22
+ let serverInstance = null;
23
+ // --- SERVER LOGIC ---
24
+ function startServer() {
25
+ if (serverInstance)
48
26
  return;
49
- }
50
- // AUTH ENDPOINT (Kept for compatibility, though Native Auth is preferred)
51
- if (req.method === 'POST' && req.url === '/v1/auth') {
52
- const chunks = [];
53
- req.on('data', chunk => chunks.push(chunk));
54
- req.on('end', async () => {
55
- try {
56
- const body = JSON.parse(Buffer.concat(chunks).toString());
57
- if (body && body.apiKey) {
58
- saveConfig({ apiKey: body.apiKey, mode: 'pro' });
59
- log(`[AUTH] Key saved via Server Endpoint`);
60
- res.writeHead(200, { 'Content-Type': 'application/json' });
61
- res.end(JSON.stringify({ status: "ok" }));
27
+ serverInstance = http.createServer(async (req, res) => {
28
+ log(`${req.method} ${req.url}`);
29
+ // CORS Headers
30
+ res.setHeader('Access-Control-Allow-Origin', '*');
31
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
32
+ res.setHeader('Access-Control-Allow-Headers', '*');
33
+ if (req.method === 'OPTIONS') {
34
+ res.writeHead(204);
35
+ res.end();
36
+ return;
37
+ }
38
+ // AUTH ENDPOINT
39
+ if (req.method === 'POST' && req.url === '/v1/auth') {
40
+ const chunks = [];
41
+ req.on('data', chunk => chunks.push(chunk));
42
+ req.on('end', async () => {
43
+ try {
44
+ const body = JSON.parse(Buffer.concat(chunks).toString());
45
+ if (body && body.apiKey) {
46
+ saveConfig({ apiKey: body.apiKey, mode: 'pro' });
47
+ log(`[AUTH] Key saved via Server Endpoint`);
48
+ res.writeHead(200, { 'Content-Type': 'application/json' });
49
+ res.end(JSON.stringify({ status: "ok" }));
50
+ }
51
+ else {
52
+ res.writeHead(400);
53
+ res.end(JSON.stringify({ error: "Missing apiKey" }));
54
+ }
62
55
  }
63
- else {
64
- res.writeHead(400);
65
- res.end(JSON.stringify({ error: "Missing apiKey" }));
56
+ catch (e) {
57
+ log(`[AUTH] Error: ${e}`);
58
+ res.writeHead(500);
59
+ res.end(JSON.stringify({ error: String(e) }));
66
60
  }
67
- }
68
- catch (e) {
69
- log(`[AUTH] Error: ${e}`);
70
- res.writeHead(500);
71
- res.end(JSON.stringify({ error: String(e) }));
72
- }
73
- });
74
- return;
75
- }
76
- if (req.method === 'GET' && req.url === '/health') {
77
- const config = loadConfig();
78
- res.writeHead(200, { 'Content-Type': 'application/json' });
79
- res.end(JSON.stringify({
80
- status: "ok",
81
- version: "v3.0.0-phase3",
82
- mode: config.mode,
83
- hasKey: !!config.apiKey
84
- }));
85
- return;
86
- }
87
- if (req.method === 'GET' && req.url === '/v1/models') {
88
- try {
89
- const models = await getAggregatedModels();
90
- res.writeHead(200, { 'Content-Type': 'application/json' });
91
- res.end(JSON.stringify(models));
61
+ });
62
+ return;
92
63
  }
93
- catch (e) {
94
- log(`Error fetching models: ${e}`);
95
- res.writeHead(500, { 'Content-Type': 'application/json' });
96
- res.end(JSON.stringify({ error: "Failed to fetch models" }));
64
+ if (req.method === 'GET' && req.url === '/health') {
65
+ const config = loadConfig();
66
+ res.writeHead(200, { 'Content-Type': 'application/json' });
67
+ res.end(JSON.stringify({
68
+ status: "ok",
69
+ version: "v6.0.0-beta.99",
70
+ mode: config.mode,
71
+ hasKey: !!config.apiKey
72
+ }));
73
+ return;
97
74
  }
98
- return;
99
- }
100
- if (req.method === 'POST' && req.url === '/v1/chat/completions') {
101
- // Accumulate body for the proxy
102
- const chunks = [];
103
- req.on('data', chunk => chunks.push(chunk));
104
- req.on('end', async () => {
75
+ if (req.method === 'GET' && req.url === '/v1/models') {
105
76
  try {
106
- const bodyRaw = Buffer.concat(chunks).toString();
107
- await handleChatCompletion(req, res, bodyRaw);
77
+ const models = await getAggregatedModels();
78
+ res.writeHead(200, { 'Content-Type': 'application/json' });
79
+ res.end(JSON.stringify(models));
108
80
  }
109
81
  catch (e) {
110
- log(`Error in chat handler: ${e}`);
82
+ log(`Error fetching models: ${e}`);
111
83
  res.writeHead(500, { 'Content-Type': 'application/json' });
112
- res.end(JSON.stringify({ error: "Internal Server Error in Chat Handler" }));
84
+ res.end(JSON.stringify({ error: "Failed to fetch models" }));
113
85
  }
114
- });
115
- return;
116
- }
117
- res.writeHead(404);
118
- res.end("Not Found");
119
- });
120
- const PORT = parseInt(process.env.POLLINATIONS_PORT || '10001', 10);
121
- // ANTI-ZOMBIE (Visible Logs Restored)
122
- try {
123
- const { execSync } = require('child_process');
124
- try {
125
- console.log(`[POLLINATIONS] Checking port ${PORT}...`);
126
- execSync(`fuser -k ${PORT}/tcp || true`);
127
- console.log(`[POLLINATIONS] Port ${PORT} cleared.`);
128
- }
129
- catch (e) {
130
- console.log(`[POLLINATIONS] Port check skipped (cmd missing?)`);
131
- }
132
- }
133
- catch (e) { }
134
- // LIFECYCLE DEBUG (Sync Write)
135
- const LIFE_LOG = '/tmp/POLLI_LIFECYCLE.log';
136
- const LOC_LOG = '/tmp/POLLI_LOCATION.log'; // NEW: Track source location
137
- try {
138
- fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [STARTUP] PID:${process.pid} Initializing...\n`);
139
- fs.writeFileSync(LOC_LOG, `[${new Date().toISOString()}] RUNNING FROM: ${__filename}\n`);
140
- }
141
- catch (e) { }
142
- process.on('exit', (code) => {
86
+ return;
87
+ }
88
+ if (req.method === 'POST' && req.url === '/v1/chat/completions') {
89
+ const chunks = [];
90
+ req.on('data', chunk => chunks.push(chunk));
91
+ req.on('end', async () => {
92
+ try {
93
+ const bodyRaw = Buffer.concat(chunks).toString();
94
+ await handleChatCompletion(req, res, bodyRaw);
95
+ }
96
+ catch (e) {
97
+ log(`Error in chat handler: ${e}`);
98
+ res.writeHead(500, { 'Content-Type': 'application/json' });
99
+ res.end(JSON.stringify({ error: "Internal Server Error in Chat Handler" }));
100
+ }
101
+ });
102
+ return;
103
+ }
104
+ res.writeHead(404);
105
+ res.end("Not Found");
106
+ });
107
+ // ANTI-ZOMBIE
143
108
  try {
144
- fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [EXIT] PID:${process.pid} Exiting with code ${code}\n`);
109
+ const { execSync } = require('child_process');
110
+ try {
111
+ console.log(`[POLLINATIONS] Checking port ${PORT}...`);
112
+ execSync(`fuser -k ${PORT}/tcp || true`);
113
+ console.log(`[POLLINATIONS] Port ${PORT} cleared.`);
114
+ }
115
+ catch (e) { }
145
116
  }
146
117
  catch (e) { }
147
- });
148
- // STARTUP CHECK: Re-validate Key (in case of upgrade/config drift)
149
- import { checkKeyPermissions } from './commands.js';
150
- (async () => {
151
- const config = loadConfig();
152
- if (config.apiKey) {
153
- try {
154
- console.log('Pollinations Plugin: Verifying API Key on startup...');
155
- const check = await checkKeyPermissions(config.apiKey);
156
- if (!check.ok) {
157
- console.warn(`Pollinations Plugin: Limited Key Detected on Startup (${check.reason}). Enforcing Manual Mode.`);
158
- saveConfig({
159
- apiKey: config.apiKey,
160
- mode: 'manual',
161
- keyHasAccessToProfile: false
162
- });
163
- }
164
- else {
165
- if (config.keyHasAccessToProfile === false) {
166
- saveConfig({ apiKey: config.apiKey, keyHasAccessToProfile: true });
118
+ // STARTUP CHECK
119
+ (async () => {
120
+ const config = loadConfig();
121
+ if (config.apiKey) {
122
+ try {
123
+ console.log('Pollinations Plugin: Verifying API Key on startup...');
124
+ const check = await checkKeyPermissions(config.apiKey);
125
+ if (!check.ok) {
126
+ console.warn(`Pollinations Plugin: Limited Key Detected on Startup (${check.reason}). Enforcing Manual Mode.`);
127
+ saveConfig({ apiKey: config.apiKey, mode: 'manual', keyHasAccessToProfile: false });
128
+ }
129
+ else {
130
+ if (config.keyHasAccessToProfile === false)
131
+ saveConfig({ apiKey: config.apiKey, keyHasAccessToProfile: true });
167
132
  }
168
133
  }
134
+ catch (e) {
135
+ console.error('Pollinations Plugin: Startup Check Failed:', e);
136
+ }
169
137
  }
170
- catch (e) {
171
- console.error('Pollinations Plugin: Startup Check Failed:', e);
172
- }
173
- }
174
- server.listen(PORT, '127.0.0.1', () => {
138
+ })();
139
+ serverInstance.listen(PORT, '127.0.0.1', () => {
175
140
  const url = `http://127.0.0.1:${PORT}`;
176
- log(`[SERVER] Started V3 Phase 3 (Auth Enabled) on port ${PORT}`);
177
- try {
178
- fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [LISTEN] PID:${process.pid} Listening on ${PORT}\n`);
179
- }
180
- catch (e) { }
181
- console.log(`POLLINATIONS_V3_URL=${url}`);
141
+ log(`[SERVER] Started V6 (Plugin Mode) on port ${PORT}`);
142
+ console.log(`POLLINATIONS_V6_URL=${url}`);
182
143
  });
183
- })();
144
+ }
145
+ // --- OPENCODE PLUGIN EXPORT ---
146
+ export const plugin = async ({ client }) => {
147
+ // 1. Inject Client for Command Handling
148
+ setClientForCommands(client);
149
+ // 2. Start Local Proxy Server
150
+ startServer();
151
+ // 3. Register Hooks (Commands, TUI, etc.)
152
+ return createCommandHooks();
153
+ };
154
+ // --- STANDALONE SUPPORT ---
155
+ // If run directly via `node dist/index.js`, start server immediately
156
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
157
+ startServer();
158
+ }