opencode-pollinations-plugin 5.1.11 → 5.1.12

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.
@@ -22,6 +22,7 @@ export interface PollinationsConfigV5 {
22
22
  enablePaidTools: boolean;
23
23
  statusBar: boolean;
24
24
  }
25
+ export declare function subscribeToConfigChange(callback: () => void): void;
25
26
  export declare function loadConfig(): PollinationsConfigV5;
26
27
  export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
27
28
  version: number;
@@ -41,7 +41,57 @@ function logConfig(msg) {
41
41
  }
42
42
  catch (e) { }
43
43
  }
44
+ // CACHE & WATCHER
45
+ let cachedConfig = null;
46
+ const listeners = [];
47
+ export function subscribeToConfigChange(callback) {
48
+ listeners.push(callback);
49
+ }
50
+ function notifyListeners() {
51
+ listeners.forEach(cb => {
52
+ try {
53
+ cb();
54
+ }
55
+ catch (e) {
56
+ logConfig(`Listener Error: ${e}`);
57
+ }
58
+ });
59
+ }
60
+ // Watchers (Debounced)
61
+ const watchedFiles = new Set();
62
+ function watchFileSafe(filePath) {
63
+ if (watchedFiles.has(filePath))
64
+ return;
65
+ try {
66
+ if (!fs.existsSync(filePath))
67
+ return;
68
+ fs.watchFile(filePath, { interval: 2000 }, (curr, prev) => {
69
+ if (curr.mtime !== prev.mtime) {
70
+ logConfig(`File Changed: ${path.basename(filePath)}. Reloading Config...`);
71
+ cachedConfig = readConfigFromDisk();
72
+ notifyListeners();
73
+ }
74
+ });
75
+ watchedFiles.add(filePath);
76
+ }
77
+ catch (e) {
78
+ logConfig(`Watch Error for ${filePath}: ${e}`);
79
+ }
80
+ }
44
81
  export function loadConfig() {
82
+ // 1. Return Cache if available
83
+ if (cachedConfig)
84
+ return cachedConfig;
85
+ // 2. Or Read Fresh
86
+ cachedConfig = readConfigFromDisk();
87
+ // 3. Initiate Watchers (Lazy)
88
+ watchFileSafe(CONFIG_FILE);
89
+ watchFileSafe(AUTH_FILE);
90
+ watchFileSafe(OPENCODE_CONFIG_FILE);
91
+ return cachedConfig;
92
+ }
93
+ // INTERNAL READER (The old loadConfig logic)
94
+ function readConfigFromDisk() {
45
95
  let config = { ...DEFAULT_CONFIG_V5 };
46
96
  let keyFound = false;
47
97
  // 1. Try Custom Config
@@ -49,45 +99,16 @@ export function loadConfig() {
49
99
  if (fs.existsSync(CONFIG_FILE)) {
50
100
  const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
51
101
  const custom = JSON.parse(raw);
52
- // === MIGRATION LOGIC V4 -> V5 ===
102
+ // ... (Migration Logic is handled on Save, we trust Disk content here mostly)
53
103
  if (!custom.version || custom.version < 5) {
54
- logConfig(`Migrating Config V${custom.version || 0} -> V5`);
55
- // Migrate GUI
56
- if (custom.toastVerbosity) {
57
- if (custom.toastVerbosity === 'none') {
58
- config.gui.status = 'none';
59
- config.gui.logs = 'none';
60
- }
61
- if (custom.toastVerbosity === 'alert') {
62
- config.gui.status = 'alert';
63
- config.gui.logs = 'error';
64
- }
65
- if (custom.toastVerbosity === 'all') {
66
- config.gui.status = 'all';
67
- config.gui.logs = 'verbose';
68
- }
69
- }
70
- // Migrate Fallbacks
71
- if (custom.fallbackModels) {
72
- if (custom.fallbackModels.main)
73
- config.fallbacks.free.main = custom.fallbackModels.main;
74
- if (custom.fallbackModels.agent) {
75
- config.fallbacks.free.agent = custom.fallbackModels.agent;
76
- config.fallbacks.enter.agent = custom.fallbackModels.agent;
77
- }
78
- }
79
- // Preserve others
104
+ // If old, we don't migrate inside read to avoid write-loops.
105
+ // We just read what we can.
80
106
  if (custom.apiKey)
81
107
  config.apiKey = custom.apiKey;
82
108
  if (custom.mode)
83
109
  config.mode = custom.mode;
84
- if (custom.statusBar !== undefined)
85
- config.statusBar = custom.statusBar;
86
- // Save Migrated
87
- saveConfig(config);
88
110
  }
89
111
  else {
90
- // Already V5
91
112
  config = { ...config, ...custom };
92
113
  }
93
114
  if (config.apiKey)
@@ -103,14 +124,15 @@ export function loadConfig() {
103
124
  if (fs.existsSync(AUTH_FILE)) {
104
125
  const raw = fs.readFileSync(AUTH_FILE, 'utf-8');
105
126
  const authData = JSON.parse(raw);
106
- const entry = authData['pollinations'] || authData['pollinations_enter'];
127
+ // Supports flat key or nested object
128
+ const entry = authData['pollinations'] || authData['pollinations_enter'] || authData['pollinations_api_key'];
107
129
  if (entry) {
108
130
  const key = (typeof entry === 'object' && entry.key) ? entry.key : entry;
109
131
  if (key && typeof key === 'string' && key.length > 10) {
110
132
  config.apiKey = key;
111
133
  config.mode = 'pro';
112
134
  keyFound = true;
113
- logConfig(`Recovered API Key from Auth Store`);
135
+ logConfig(`Hot-Loaded API Key from Auth Store`);
114
136
  }
115
137
  }
116
138
  }
@@ -119,7 +141,7 @@ export function loadConfig() {
119
141
  logConfig(`Error reading auth.json: ${e}`);
120
142
  }
121
143
  }
122
- // 3. Try OpenCode Config
144
+ // 3. Try OpenCode Config (Fallback)
123
145
  if (!keyFound) {
124
146
  try {
125
147
  if (fs.existsSync(OPENCODE_CONFIG_FILE)) {
@@ -136,7 +158,7 @@ export function loadConfig() {
136
158
  }
137
159
  catch (e) { }
138
160
  }
139
- // Default mode logic
161
+ // Default mode correction
140
162
  if (!keyFound && config.mode === 'pro') {
141
163
  config.mode = 'manual';
142
164
  }
@@ -144,12 +166,14 @@ export function loadConfig() {
144
166
  }
145
167
  export function saveConfig(updates) {
146
168
  try {
147
- const current = loadConfig();
169
+ // We must base updates on current state (even if cached is slightly old, we refresh first?)
170
+ const current = readConfigFromDisk(); // Read disk for safety before write
148
171
  const updated = { ...current, ...updates, version: 5 };
149
172
  if (!fs.existsSync(CONFIG_DIR_POLLI)) {
150
173
  fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
151
174
  }
152
175
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
176
+ cachedConfig = updated; // Update Cache immediately
153
177
  return updated;
154
178
  }
155
179
  catch (e) {
@@ -2,9 +2,18 @@ import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { getAggregatedModels } from './pollinations-api.js';
5
- import { loadConfig, saveConfig } from './config.js';
5
+ import { loadConfig, saveConfig, subscribeToConfigChange } from './config.js';
6
6
  import { handleChatCompletion } from './proxy.js';
7
+ import { emitStatusToast } from './toast.js';
7
8
  const LOG_FILE = path.join(process.env.HOME || '/tmp', '.config/opencode/plugins/pollinations-v3.log');
9
+ // Hot Reload Listener
10
+ subscribeToConfigChange(() => {
11
+ const config = loadConfig();
12
+ const mode = config.mode.toUpperCase();
13
+ const hasKey = !!config.apiKey;
14
+ log(`[HOT RELOAD] Config Updated. Mode: ${mode}, HasKey: ${hasKey}`);
15
+ emitStatusToast('info', `Configuration Updated (Hot Reload) | Mode: ${mode}`, 'Pollinations System');
16
+ });
8
17
  // Simple file logger
9
18
  function log(msg) {
10
19
  const ts = new Date().toISOString();
@@ -18,6 +27,24 @@ function log(msg) {
18
27
  // silent fail
19
28
  }
20
29
  }
30
+ // CRASH GUARD
31
+ const CRASH_LOG = '/tmp/opencode_pollinations_crash.log';
32
+ process.on('uncaughtException', (err) => {
33
+ try {
34
+ const msg = `[CRASH] Uncaught Exception: ${err.message}\n${err.stack}\n`;
35
+ fs.appendFileSync(CRASH_LOG, msg);
36
+ console.error(msg);
37
+ }
38
+ catch (e) { }
39
+ process.exit(1);
40
+ });
41
+ process.on('unhandledRejection', (reason, promise) => {
42
+ try {
43
+ const msg = `[CRASH] Unhandled Rejection: ${reason}\n`;
44
+ fs.appendFileSync(CRASH_LOG, msg);
45
+ }
46
+ catch (e) { }
47
+ });
21
48
  const server = http.createServer(async (req, res) => {
22
49
  log(`${req.method} ${req.url}`);
23
50
  // CORS Headers
@@ -113,8 +140,24 @@ try {
113
140
  }
114
141
  }
115
142
  catch (e) { }
143
+ // LIFECYCLE DEBUG (Sync Write)
144
+ const LIFE_LOG = '/tmp/POLLI_LIFECYCLE.log';
145
+ try {
146
+ fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [STARTUP] PID:${process.pid} Initializing...\n`);
147
+ }
148
+ catch (e) { }
149
+ process.on('exit', (code) => {
150
+ try {
151
+ fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [EXIT] PID:${process.pid} Exiting with code ${code}\n`);
152
+ }
153
+ catch (e) { }
154
+ });
116
155
  server.listen(PORT, '127.0.0.1', () => {
117
156
  const url = `http://127.0.0.1:${PORT}`;
118
157
  log(`[SERVER] Started V3 Phase 3 (Auth Enabled) on port ${PORT}`);
158
+ try {
159
+ fs.appendFileSync(LIFE_LOG, `[${new Date().toISOString()}] [LISTEN] PID:${process.pid} Listening on ${PORT}\n`);
160
+ }
161
+ catch (e) { }
119
162
  console.log(`POLLINATIONS_V3_URL=${url}`);
120
163
  });
@@ -149,6 +149,8 @@ export async function handleChatCompletion(req, res, bodyRaw) {
149
149
  try {
150
150
  const body = JSON.parse(bodyRaw);
151
151
  const config = loadConfig();
152
+ // DEBUG: Trace Config State for Hot Reload verification
153
+ log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
152
154
  // 0. COMMAND HANDLING
153
155
  if (body.messages && body.messages.length > 0) {
154
156
  const lastMsg = body.messages[body.messages.length - 1];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.1)",
4
- "version": "5.1.11",
4
+ "version": "5.1.12",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {