opencode-studio-server 1.1.3 → 1.2.1

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 (3) hide show
  1. package/index.js +636 -851
  2. package/package.json +1 -1
  3. package/server.out +201 -0
package/index.js CHANGED
@@ -4,17 +4,15 @@ const bodyParser = require('body-parser');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
- const { exec, spawn } = require('child_process');
7
+ const { spawn, exec } = require('child_process');
8
8
 
9
9
  const app = express();
10
10
  const PORT = 3001;
11
11
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
12
12
 
13
- let lastActivityTime = Date.now();
14
13
  let idleTimer = null;
15
14
 
16
15
  function resetIdleTimer() {
17
- lastActivityTime = Date.now();
18
16
  if (idleTimer) clearTimeout(idleTimer);
19
17
  idleTimer = setTimeout(() => {
20
18
  console.log('Server idle for 30 minutes, shutting down...');
@@ -59,22 +57,155 @@ const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'p
59
57
  let pendingActionMemory = null;
60
58
 
61
59
  function loadStudioConfig() {
62
- if (!fs.existsSync(STUDIO_CONFIG_PATH)) {
63
- return {};
64
- }
60
+ const defaultConfig = {
61
+ disabledSkills: [],
62
+ disabledPlugins: [],
63
+ activeProfiles: {},
64
+ activeGooglePlugin: 'gemini',
65
+ availableGooglePlugins: [],
66
+ pluginModels: {
67
+ gemini: {
68
+ "gemini-3-pro-preview": {
69
+ "id": "gemini-3-pro-preview",
70
+ "name": "3 Pro (Gemini CLI)",
71
+ "reasoning": true,
72
+ "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } }
73
+ },
74
+ "gemini-2.5-flash": {
75
+ "id": "gemini-2.5-flash",
76
+ "name": "2.5 Flash (Gemini CLI)",
77
+ "reasoning": true,
78
+ "options": { "thinkingConfig": { "thinkingBudget": 8192, "includeThoughts": true } }
79
+ },
80
+ "gemini-2.0-flash": {
81
+ "id": "gemini-2.0-flash",
82
+ "name": "2.0 Flash (Free)",
83
+ "reasoning": false
84
+ },
85
+ "gemini-1.5-flash": {
86
+ "id": "gemini-1.5-flash",
87
+ "name": "1.5 Flash (Free)",
88
+ "reasoning": false
89
+ }
90
+ },
91
+ antigravity: {
92
+ "gemini-3-pro-preview": {
93
+ "id": "gemini-3-pro-preview",
94
+ "name": "3 Pro",
95
+ "release_date": "2025-11-18",
96
+ "reasoning": true,
97
+ "limit": { "context": 1000000, "output": 64000 },
98
+ "cost": { "input": 2, "output": 12, "cache_read": 0.2 },
99
+ "modalities": {
100
+ "input": ["text", "image", "video", "audio", "pdf"],
101
+ "output": ["text"]
102
+ },
103
+ "variants": {
104
+ "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
105
+ "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
106
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } }
107
+ }
108
+ },
109
+ "gemini-3-flash": {
110
+ "id": "gemini-3-flash",
111
+ "name": "3 Flash",
112
+ "release_date": "2025-12-17",
113
+ "reasoning": true,
114
+ "limit": { "context": 1048576, "output": 65536 },
115
+ "cost": { "input": 0.5, "output": 3, "cache_read": 0.05 },
116
+ "modalities": {
117
+ "input": ["text", "image", "video", "audio", "pdf"],
118
+ "output": ["text"]
119
+ },
120
+ "variants": {
121
+ "minimal": { "options": { "thinkingConfig": { "thinkingLevel": "minimal", "includeThoughts": true } } },
122
+ "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
123
+ "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
124
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } }
125
+ }
126
+ },
127
+ "gemini-2.5-flash-lite": {
128
+ "id": "gemini-2.5-flash-lite",
129
+ "name": "2.5 Flash Lite",
130
+ "reasoning": false
131
+ },
132
+ "google/gemini-3-flash": {
133
+ "id": "google/gemini-3-flash",
134
+ "name": "3 Flash (Google)",
135
+ "reasoning": true,
136
+ "limit": { "context": 1048576, "output": 65536 },
137
+ "cost": { "input": 0.5, "output": 3, "cache_read": 0.05 },
138
+ "modalities": {
139
+ "input": ["text", "image", "video", "audio", "pdf"],
140
+ "output": ["text"]
141
+ },
142
+ "variants": {
143
+ "minimal": { "options": { "thinkingConfig": { "thinkingLevel": "minimal", "includeThoughts": true } } },
144
+ "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
145
+ "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
146
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } }
147
+ }
148
+ },
149
+ "opencode/glm-4.7-free": {
150
+ "id": "opencode/glm-4.7-free",
151
+ "name": "GLM 4.7 Free",
152
+ "reasoning": false,
153
+ "limit": { "context": 128000, "output": 4096 },
154
+ "cost": { "input": 0, "output": 0 },
155
+ "modalities": {
156
+ "input": ["text"],
157
+ "output": ["text"]
158
+ }
159
+ },
160
+ "gemini-claude-sonnet-4-5-thinking": {
161
+ "id": "gemini-claude-sonnet-4-5-thinking",
162
+ "name": "Sonnet 4.5",
163
+ "reasoning": true,
164
+ "limit": { "context": 200000, "output": 64000 },
165
+ "modalities": {
166
+ "input": ["text", "image", "pdf"],
167
+ "output": ["text"]
168
+ },
169
+ "variants": {
170
+ "none": { "reasoning": false, "options": { "thinkingConfig": { "includeThoughts": false } } },
171
+ "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
172
+ "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
173
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } }
174
+ }
175
+ },
176
+ "gemini-claude-opus-4-5-thinking": {
177
+ "id": "gemini-claude-opus-4-5-thinking",
178
+ "name": "Opus 4.5",
179
+ "release_date": "2025-11-24",
180
+ "reasoning": true,
181
+ "limit": { "context": 200000, "output": 64000 },
182
+ "modalities": {
183
+ "input": ["text", "image", "pdf"],
184
+ "output": ["text"]
185
+ },
186
+ "variants": {
187
+ "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
188
+ "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
189
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ };
195
+
196
+ if (!fs.existsSync(STUDIO_CONFIG_PATH)) return defaultConfig;
65
197
  try {
66
- return JSON.parse(fs.readFileSync(STUDIO_CONFIG_PATH, 'utf8'));
198
+ const config = JSON.parse(fs.readFileSync(STUDIO_CONFIG_PATH, 'utf8'));
199
+ return { ...defaultConfig, ...config };
67
200
  } catch {
68
- return {};
201
+ return defaultConfig;
69
202
  }
70
203
  }
71
204
 
72
205
  function saveStudioConfig(config) {
73
206
  try {
74
207
  const dir = path.dirname(STUDIO_CONFIG_PATH);
75
- if (!fs.existsSync(dir)) {
76
- fs.mkdirSync(dir, { recursive: true });
77
- }
208
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
78
209
  fs.writeFileSync(STUDIO_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
79
210
  return true;
80
211
  } catch (err) {
@@ -83,55 +214,16 @@ function saveStudioConfig(config) {
83
214
  }
84
215
  }
85
216
 
86
- function loadPendingAction() {
87
- if (pendingActionMemory) return pendingActionMemory;
88
-
89
- if (fs.existsSync(PENDING_ACTION_PATH)) {
90
- try {
91
- const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
92
- if (action.timestamp && Date.now() - action.timestamp < 60000) {
93
- pendingActionMemory = action;
94
- fs.unlinkSync(PENDING_ACTION_PATH);
95
- return action;
96
- }
97
- fs.unlinkSync(PENDING_ACTION_PATH);
98
- } catch {
99
- try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
100
- }
101
- }
102
- return null;
103
- }
104
-
105
- app.get('/api/pending-action', (req, res) => {
106
- const action = loadPendingAction();
107
- res.json({ action });
108
- });
109
-
110
- app.delete('/api/pending-action', (req, res) => {
111
- pendingActionMemory = null;
112
- if (fs.existsSync(PENDING_ACTION_PATH)) {
113
- try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
114
- }
115
- res.json({ success: true });
116
- });
117
-
118
217
  const getPaths = () => {
119
218
  const platform = process.platform;
120
219
  const home = os.homedir();
121
-
122
- let candidates = [];
220
+ let candidates = [
221
+ path.join(home, '.config', 'opencode', 'opencode.json'),
222
+ path.join(home, '.local', 'share', 'opencode', 'opencode.json'),
223
+ path.join(home, '.opencode', 'opencode.json'),
224
+ ];
123
225
  if (platform === 'win32') {
124
- candidates = [
125
- path.join(process.env.APPDATA, 'opencode', 'opencode.json'),
126
- path.join(home, '.config', 'opencode', 'opencode.json'),
127
- path.join(home, '.local', 'share', 'opencode', 'opencode.json'),
128
- ];
129
- } else {
130
- candidates = [
131
- path.join(home, '.config', 'opencode', 'opencode.json'),
132
- path.join(home, '.opencode', 'opencode.json'),
133
- path.join(home, '.local', 'share', 'opencode', 'opencode.json'),
134
- ];
226
+ candidates.push(path.join(process.env.APPDATA, 'opencode', 'opencode.json'));
135
227
  }
136
228
 
137
229
  const studioConfig = loadStudioConfig();
@@ -149,22 +241,22 @@ const getPaths = () => {
149
241
  detected,
150
242
  manual: manualPath,
151
243
  current: manualPath || detected,
152
- candidates
244
+ candidates: [...new Set(candidates)]
153
245
  };
154
246
  };
155
247
 
156
- const getConfigPath = () => {
157
- const paths = getPaths();
158
- return paths.current;
159
- };
248
+ const getConfigPath = () => getPaths().current;
160
249
 
161
250
  const loadConfig = () => {
162
251
  const configPath = getConfigPath();
163
- if (!configPath || !fs.existsSync(configPath)) {
164
- return null;
165
- }
252
+ if (!configPath || !fs.existsSync(configPath)) return null;
166
253
  try {
167
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
254
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
255
+ const studioConfig = loadStudioConfig();
256
+ if (studioConfig.activeGooglePlugin === 'antigravity' && !config.small_model) {
257
+ config.small_model = "google/gemini-3-flash";
258
+ }
259
+ return config;
168
260
  } catch {
169
261
  return null;
170
262
  }
@@ -172,28 +264,20 @@ const loadConfig = () => {
172
264
 
173
265
  const saveConfig = (config) => {
174
266
  const configPath = getConfigPath();
175
- if (!configPath) {
176
- throw new Error('No config path found');
177
- }
267
+ if (!configPath) throw new Error('No config path found');
178
268
  const dir = path.dirname(configPath);
179
- if (!fs.existsSync(dir)) {
180
- fs.mkdirSync(dir, { recursive: true });
181
- }
269
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
182
270
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
183
271
  };
184
272
 
185
- app.get('/api/health', (req, res) => {
186
- res.json({ status: 'ok' });
187
- });
273
+ app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
188
274
 
189
275
  app.post('/api/shutdown', (req, res) => {
190
276
  res.json({ success: true });
191
277
  setTimeout(() => process.exit(0), 100);
192
278
  });
193
279
 
194
- app.get('/api/paths', (req, res) => {
195
- res.json(getPaths());
196
- });
280
+ app.get('/api/paths', (req, res) => res.json(getPaths()));
197
281
 
198
282
  app.post('/api/paths', (req, res) => {
199
283
  const { configPath } = req.body;
@@ -205,16 +289,13 @@ app.post('/api/paths', (req, res) => {
205
289
 
206
290
  app.get('/api/config', (req, res) => {
207
291
  const config = loadConfig();
208
- if (!config) {
209
- return res.status(404).json({ error: 'Config not found' });
210
- }
292
+ if (!config) return res.status(404).json({ error: 'Config not found' });
211
293
  res.json(config);
212
294
  });
213
295
 
214
296
  app.post('/api/config', (req, res) => {
215
- const config = req.body;
216
297
  try {
217
- saveConfig(config);
298
+ saveConfig(req.body);
218
299
  res.json({ success: true });
219
300
  } catch (err) {
220
301
  res.status(500).json({ error: err.message });
@@ -222,905 +303,609 @@ app.post('/api/config', (req, res) => {
222
303
  });
223
304
 
224
305
  const getSkillDir = () => {
225
- const configPath = getConfigPath();
226
- if (!configPath) return null;
227
- return path.join(path.dirname(configPath), 'skill');
306
+ const cp = getConfigPath();
307
+ return cp ? path.join(path.dirname(cp), 'skill') : null;
228
308
  };
229
309
 
230
310
  app.get('/api/skills', (req, res) => {
231
- const skillDir = getSkillDir();
232
- if (!skillDir || !fs.existsSync(skillDir)) {
233
- return res.json([]);
234
- }
235
-
236
- const skills = [];
237
- const entries = fs.readdirSync(skillDir, { withFileTypes: true });
238
-
239
- for (const entry of entries) {
240
- if (entry.isDirectory()) {
241
- const skillPath = path.join(skillDir, entry.name, 'SKILL.md');
242
- if (fs.existsSync(skillPath)) {
243
- skills.push({
244
- name: entry.name,
245
- path: skillPath,
246
- enabled: !entry.name.endsWith('.disabled')
247
- });
248
- }
249
- }
250
- }
311
+ const sd = getSkillDir();
312
+ if (!sd || !fs.existsSync(sd)) return res.json([]);
313
+ const skills = fs.readdirSync(sd, { withFileTypes: true })
314
+ .filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
315
+ .map(e => ({ name: e.name, path: path.join(sd, e.name, 'SKILL.md'), enabled: !e.name.endsWith('.disabled') }));
251
316
  res.json(skills);
252
317
  });
253
318
 
254
319
  app.get('/api/skills/:name', (req, res) => {
255
- const skillDir = getSkillDir();
256
- if (!skillDir) return res.status(404).json({ error: 'No config' });
257
-
258
- const name = req.params.name;
259
- const skillPath = path.join(skillDir, name, 'SKILL.md');
260
-
261
- if (!fs.existsSync(skillPath)) {
262
- return res.status(404).json({ error: 'Skill not found' });
263
- }
264
-
265
- const content = fs.readFileSync(skillPath, 'utf8');
266
- res.json({ name, content });
320
+ const sd = getSkillDir();
321
+ const p = sd ? path.join(sd, req.params.name, 'SKILL.md') : null;
322
+ if (!p || !fs.existsSync(p)) return res.status(404).json({ error: 'Not found' });
323
+ res.json({ name: req.params.name, content: fs.readFileSync(p, 'utf8') });
267
324
  });
268
325
 
269
326
  app.post('/api/skills/:name', (req, res) => {
270
- const skillDir = getSkillDir();
271
- if (!skillDir) return res.status(404).json({ error: 'No config' });
272
-
273
- const name = req.params.name;
274
- const { content } = req.body;
275
- const dirPath = path.join(skillDir, name);
276
-
277
- if (!fs.existsSync(dirPath)) {
278
- fs.mkdirSync(dirPath, { recursive: true });
279
- }
280
-
281
- fs.writeFileSync(path.join(dirPath, 'SKILL.md'), content, 'utf8');
327
+ const sd = getSkillDir();
328
+ if (!sd) return res.status(404).json({ error: 'No config' });
329
+ const dp = path.join(sd, req.params.name);
330
+ if (!fs.existsSync(dp)) fs.mkdirSync(dp, { recursive: true });
331
+ fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
282
332
  res.json({ success: true });
283
333
  });
284
334
 
285
335
  app.delete('/api/skills/:name', (req, res) => {
286
- const skillDir = getSkillDir();
287
- if (!skillDir) return res.status(404).json({ error: 'No config' });
288
-
289
- const name = req.params.name;
290
- const dirPath = path.join(skillDir, name);
291
-
292
- if (fs.existsSync(dirPath)) {
293
- fs.rmSync(dirPath, { recursive: true, force: true });
294
- }
336
+ const sd = getSkillDir();
337
+ const dp = sd ? path.join(sd, req.params.name) : null;
338
+ if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
295
339
  res.json({ success: true });
296
340
  });
297
341
 
298
- app.post('/api/skills/:name/toggle', (req, res) => {
299
- res.json({ success: true, enabled: true });
300
- });
301
-
302
342
  const getPluginDir = () => {
303
- const configPath = getConfigPath();
304
- if (!configPath) return null;
305
- return path.join(path.dirname(configPath), 'plugin');
343
+ const cp = getConfigPath();
344
+ return cp ? path.join(path.dirname(cp), 'plugin') : null;
306
345
  };
307
346
 
308
347
  app.get('/api/plugins', (req, res) => {
309
- const pluginDir = getPluginDir();
310
- const configPath = getConfigPath();
311
- const configRoot = configPath ? path.dirname(configPath) : null;
312
-
348
+ const pd = getPluginDir();
349
+ const cp = getConfigPath();
350
+ const cr = cp ? path.dirname(cp) : null;
313
351
  const plugins = [];
314
-
315
- const addPlugin = (name, p, enabled = true) => {
316
- if (!plugins.some(pl => pl.name === name)) {
317
- plugins.push({ name, path: p, enabled });
318
- }
352
+ const add = (name, p, enabled = true) => {
353
+ if (!plugins.some(pl => pl.name === name)) plugins.push({ name, path: p, enabled });
319
354
  };
320
355
 
321
- if (pluginDir && fs.existsSync(pluginDir)) {
322
- const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
323
- for (const entry of entries) {
324
- const fullPath = path.join(pluginDir, entry.name);
325
- const stats = fs.lstatSync(fullPath);
326
- if (stats.isDirectory()) {
327
- const jsPath = path.join(fullPath, 'index.js');
328
- const tsPath = path.join(fullPath, 'index.ts');
329
- if (fs.existsSync(jsPath) || fs.existsSync(tsPath)) {
330
- addPlugin(entry.name, fs.existsSync(jsPath) ? jsPath : tsPath);
331
- }
332
- } else if ((stats.isFile() || stats.isSymbolicLink()) && (entry.name.endsWith('.js') || entry.name.endsWith('.ts'))) {
333
- addPlugin(entry.name.replace(/\.(js|ts)$/, ''), fullPath);
356
+ if (pd && fs.existsSync(pd)) {
357
+ fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
358
+ const fp = path.join(pd, e.name);
359
+ const st = fs.lstatSync(fp);
360
+ if (st.isDirectory()) {
361
+ const j = path.join(fp, 'index.js'), t = path.join(fp, 'index.ts');
362
+ if (fs.existsSync(j) || fs.existsSync(t)) add(e.name, fs.existsSync(j) ? j : t);
363
+ } else if ((st.isFile() || st.isSymbolicLink()) && /\.(js|ts)$/.test(e.name)) {
364
+ add(e.name.replace(/\.(js|ts)$/, ''), fp);
334
365
  }
335
- }
366
+ });
336
367
  }
337
368
 
338
- if (configRoot && fs.existsSync(configRoot)) {
339
- const rootEntries = fs.readdirSync(configRoot, { withFileTypes: true });
340
- const knownPlugins = ['oh-my-opencode', 'superpowers', 'opencode-gemini-auth'];
341
-
342
- for (const entry of rootEntries) {
343
- if (knownPlugins.includes(entry.name) && entry.isDirectory()) {
344
- const fullPath = path.join(configRoot, entry.name);
345
- addPlugin(entry.name, fullPath);
346
- }
347
- }
369
+ if (cr && fs.existsSync(cr)) {
370
+ ['oh-my-opencode', 'superpowers', 'opencode-gemini-auth'].forEach(n => {
371
+ const fp = path.join(cr, n);
372
+ if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) add(n, fp);
373
+ });
348
374
  }
349
375
 
376
+ const cfg = loadConfig();
377
+ if (cfg && Array.isArray(cfg.plugin)) {
378
+ cfg.plugin.forEach(n => {
379
+ if (!n.includes('/') && !n.includes('\\') && !/\.(js|ts)$/.test(n)) add(n, 'npm');
380
+ });
381
+ }
350
382
  res.json(plugins);
351
383
  });
352
384
 
353
- app.get('/api/plugins/:name', (req, res) => {
354
- const pluginDir = getPluginDir();
355
- if (!pluginDir) return res.status(404).json({ error: 'No config' });
356
-
357
- const name = req.params.name;
358
- const dirPath = path.join(pluginDir, name);
359
-
360
- let content = '';
361
- let filename = '';
362
-
363
- if (fs.existsSync(path.join(dirPath, 'index.js'))) {
364
- content = fs.readFileSync(path.join(dirPath, 'index.js'), 'utf8');
365
- filename = 'index.js';
366
- } else if (fs.existsSync(path.join(dirPath, 'index.ts'))) {
367
- content = fs.readFileSync(path.join(dirPath, 'index.ts'), 'utf8');
368
- filename = 'index.ts';
369
- } else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
370
- content = fs.readFileSync(path.join(pluginDir, name + '.js'), 'utf8');
371
- filename = name + '.js';
372
- } else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
373
- content = fs.readFileSync(path.join(pluginDir, name + '.ts'), 'utf8');
374
- filename = name + '.ts';
375
- } else {
376
- return res.status(404).json({ error: 'Plugin not found' });
377
- }
378
-
379
- res.json({ name, content, filename });
380
- });
385
+ const getActiveGooglePlugin = () => {
386
+ const studio = loadStudioConfig();
387
+ return studio.activeGooglePlugin || null;
388
+ };
381
389
 
382
- app.post('/api/plugins/:name', (req, res) => {
383
- const pluginDir = getPluginDir();
384
- if (!pluginDir) return res.status(404).json({ error: 'No config' });
385
-
386
- const name = req.params.name;
387
- const { content } = req.body;
388
- const dirPath = path.join(pluginDir, name);
389
-
390
- if (!fs.existsSync(dirPath)) {
391
- fs.mkdirSync(dirPath, { recursive: true });
392
- }
390
+ function loadAuthConfig() {
391
+ const paths = getPaths();
392
+ const allAuthConfigs = [];
393
393
 
394
- const filePath = path.join(dirPath, 'index.js');
395
- fs.writeFileSync(filePath, content, 'utf8');
396
- res.json({ success: true });
397
- });
394
+ // Check all candidate directories for auth.json
395
+ paths.candidates.forEach(p => {
396
+ const ap = path.join(path.dirname(p), 'auth.json');
397
+ if (fs.existsSync(ap)) {
398
+ try {
399
+ allAuthConfigs.push(JSON.parse(fs.readFileSync(ap, 'utf8')));
400
+ } catch {}
401
+ }
402
+ });
398
403
 
399
- app.delete('/api/plugins/:name', (req, res) => {
400
- const pluginDir = getPluginDir();
401
- if (!pluginDir) return res.status(404).json({ error: 'No config' });
402
-
403
- const name = req.params.name;
404
- const dirPath = path.join(pluginDir, name);
405
-
406
- if (fs.existsSync(dirPath)) {
407
- fs.rmSync(dirPath, { recursive: true, force: true });
408
- } else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
409
- fs.unlinkSync(path.join(pluginDir, name + '.js'));
410
- } else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
411
- fs.unlinkSync(path.join(pluginDir, name + '.ts'));
404
+ // Also check current active directory if not in candidates
405
+ if (paths.current) {
406
+ const ap = path.join(path.dirname(paths.current), 'auth.json');
407
+ if (fs.existsSync(ap)) {
408
+ try {
409
+ allAuthConfigs.push(JSON.parse(fs.readFileSync(ap, 'utf8')));
410
+ } catch {}
411
+ }
412
412
  }
413
- res.json({ success: true });
414
- });
415
413
 
416
- app.post('/api/plugins/:name/toggle', (req, res) => {
417
- res.json({ success: true, enabled: true });
418
- });
414
+ if (allAuthConfigs.length === 0) return null;
419
415
 
420
- app.post('/api/fetch-url', async (req, res) => {
421
- const { url } = req.body;
422
- if (!url) return res.status(400).json({ error: 'URL required' });
416
+ // Merge all configs, later ones (priority) overwrite earlier ones
417
+ const cfg = Object.assign({}, ...allAuthConfigs);
423
418
 
424
- try {
425
- const fetch = (await import('node-fetch')).default;
426
- const response = await fetch(url);
427
- if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
428
- const content = await response.text();
429
- const filename = path.basename(new URL(url).pathname) || 'file.txt';
419
+ try {
420
+ const activePlugin = getActiveGooglePlugin();
430
421
 
431
- res.json({ content, filename, url });
432
- } catch (err) {
433
- res.status(500).json({ error: err.message });
434
- }
435
- });
436
-
437
- app.post('/api/bulk-fetch', async (req, res) => {
438
- const { urls } = req.body;
439
- if (!Array.isArray(urls)) return res.status(400).json({ error: 'URLs array required' });
440
-
441
- const fetch = (await import('node-fetch')).default;
442
- const results = [];
443
-
444
- for (const url of urls) {
445
- try {
446
- const response = await fetch(url);
447
- if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
448
- const content = await response.text();
449
- const filename = path.basename(new URL(url).pathname) || 'file.txt';
450
-
451
- results.push({
452
- url,
453
- success: true,
454
- content,
455
- filename,
456
- name: filename.replace(/\.(md|js|ts)$/, ''),
457
- });
458
- } catch (err) {
459
- results.push({
460
- url,
461
- success: false,
462
- error: err.message
463
- });
422
+ // Find if any google auth exists in any merged config
423
+ const anyGoogleAuth = cfg.google || cfg['google.gemini'] || cfg['google.antigravity'];
424
+
425
+ if (anyGoogleAuth) {
426
+ // Ensure all 3 keys have at least some valid google auth data
427
+ const baseAuth = cfg.google || cfg['google.antigravity'] || cfg['google.gemini'];
428
+ if (!cfg.google) cfg.google = { ...baseAuth };
429
+ if (!cfg['google.antigravity']) cfg['google.antigravity'] = { ...baseAuth };
430
+ if (!cfg['google.gemini']) cfg['google.gemini'] = { ...baseAuth };
464
431
  }
465
- }
466
-
467
- res.json({ results });
468
- });
469
432
 
470
- app.get('/api/backup', (req, res) => {
471
- const studioConfig = loadStudioConfig();
472
- const opencodeConfig = loadConfig();
473
-
474
- const skills = [];
475
- const skillDir = getSkillDir();
476
- if (skillDir && fs.existsSync(skillDir)) {
477
- const entries = fs.readdirSync(skillDir, { withFileTypes: true });
478
- for (const entry of entries) {
479
- if (entry.isDirectory()) {
480
- const p = path.join(skillDir, entry.name, 'SKILL.md');
481
- if (fs.existsSync(p)) {
482
- skills.push({ name: entry.name, content: fs.readFileSync(p, 'utf8') });
483
- }
484
- }
485
- }
486
- }
487
-
488
- const plugins = [];
489
- const pluginDir = getPluginDir();
490
- if (pluginDir && fs.existsSync(pluginDir)) {
491
- const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
492
- for (const entry of entries) {
493
- if (entry.isDirectory()) {
494
- const p = path.join(pluginDir, entry.name, 'index.js');
495
- if (fs.existsSync(p)) {
496
- plugins.push({ name: entry.name, content: fs.readFileSync(p, 'utf8') });
497
- }
498
- }
433
+ // Always ensure the main 'google' key reflects the active plugin if it exists
434
+ if (activePlugin === 'antigravity' && cfg['google.antigravity']) {
435
+ cfg.google = { ...cfg['google.antigravity'] };
436
+ } else if (activePlugin === 'gemini' && cfg['google.gemini']) {
437
+ cfg.google = { ...cfg['google.gemini'] };
499
438
  }
500
- }
501
-
502
- res.json({
503
- version: 1,
504
- timestamp: new Date().toISOString(),
505
- studioConfig,
506
- opencodeConfig,
507
- skills,
508
- plugins
509
- });
510
- });
439
+
440
+ return cfg;
441
+ } catch { return cfg; }
442
+ }
511
443
 
512
- app.post('/api/restore', (req, res) => {
513
- const backup = req.body;
514
-
515
- if (backup.studioConfig) {
516
- saveStudioConfig(backup.studioConfig);
517
- }
518
-
519
- if (backup.opencodeConfig) {
520
- saveConfig(backup.opencodeConfig);
521
- }
522
-
523
- if (Array.isArray(backup.skills)) {
524
- const skillDir = getSkillDir();
525
- if (skillDir) {
526
- if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
527
- for (const skill of backup.skills) {
528
- const dir = path.join(skillDir, skill.name);
529
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
530
- fs.writeFileSync(path.join(dir, 'SKILL.md'), skill.content, 'utf8');
531
- }
532
- }
533
- }
534
-
535
- if (Array.isArray(backup.plugins)) {
536
- const pluginDir = getPluginDir();
537
- if (pluginDir) {
538
- if (!fs.existsSync(pluginDir)) fs.mkdirSync(pluginDir, { recursive: true });
539
- for (const plugin of backup.plugins) {
540
- const dir = path.join(pluginDir, plugin.name);
541
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
542
- fs.writeFileSync(path.join(dir, 'index.js'), plugin.content, 'utf8');
543
- }
544
- }
444
+ const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
445
+ const listAuthProfiles = (p, activePlugin) => {
446
+ let ns = p;
447
+ if (p === 'google') {
448
+ ns = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
545
449
  }
546
450
 
547
- res.json({ success: true });
548
- });
451
+ const d = path.join(AUTH_PROFILES_DIR, ns);
452
+ if (!fs.existsSync(d)) return [];
453
+ try { return fs.readdirSync(d).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '')); } catch { return []; }
454
+ };
549
455
 
550
- function loadAuthConfig() {
551
- const configPath = getConfigPath();
552
- if (!configPath) return null;
553
-
554
- const authPath = path.join(path.dirname(configPath), 'auth.json');
555
- if (!fs.existsSync(authPath)) return null;
556
- try {
557
- return JSON.parse(fs.readFileSync(authPath, 'utf8'));
558
- } catch {
559
- return null;
560
- }
561
- }
456
+ app.get('/api/auth/providers', (req, res) => {
457
+ const providers = [
458
+ { id: 'google', name: 'Google', type: 'oauth', description: 'Google Gemini API' },
459
+ { id: 'anthropic', name: 'Anthropic', type: 'api', description: 'Claude models' },
460
+ { id: 'openai', name: 'OpenAI', type: 'api', description: 'GPT models' },
461
+ { id: 'xai', name: 'xAI', type: 'api', description: 'Grok models' },
462
+ { id: 'openrouter', name: 'OpenRouter', type: 'api', description: 'Unified LLM API' },
463
+ { id: 'github-copilot', name: 'GitHub Copilot', type: 'api', description: 'Copilot models' }
464
+ ];
465
+ res.json(providers);
466
+ });
562
467
 
563
468
  app.get('/api/auth', (req, res) => {
564
- const authConfig = loadAuthConfig() || {};
565
- const activeProfiles = getActiveProfiles();
566
-
469
+ const authCfg = loadAuthConfig() || {};
470
+ const studio = loadStudioConfig();
471
+ const ac = studio.activeProfiles || {};
567
472
  const credentials = [];
473
+ const activePlugin = studio.activeGooglePlugin;
474
+
475
+ // DEBUG LOGGING
476
+ console.log('--- Auth Debug ---');
477
+ console.log('Active Google Plugin:', activePlugin);
478
+ console.log('Found keys in authCfg:', Object.keys(authCfg));
479
+ if (authCfg.google) console.log('Google Auth found!');
480
+ if (authCfg['google.antigravity']) console.log('google.antigravity found!');
481
+ if (authCfg['google.gemini']) console.log('google.gemini found!');
482
+
568
483
  const providers = [
569
- { id: 'google', name: 'Google AI', type: 'oauth' },
484
+ { id: 'google', name: 'Google', type: 'oauth' },
570
485
  { id: 'anthropic', name: 'Anthropic', type: 'api' },
571
486
  { id: 'openai', name: 'OpenAI', type: 'api' },
572
487
  { id: 'xai', name: 'xAI', type: 'api' },
573
- { id: 'groq', name: 'Groq', type: 'api' },
574
- { id: 'together', name: 'Together AI', type: 'api' },
575
- { id: 'mistral', name: 'Mistral', type: 'api' },
576
- { id: 'deepseek', name: 'DeepSeek', type: 'api' },
577
488
  { id: 'openrouter', name: 'OpenRouter', type: 'api' },
578
- { id: 'amazon-bedrock', name: 'Amazon Bedrock', type: 'api' },
579
- { id: 'azure', name: 'Azure OpenAI', type: 'api' },
580
489
  { id: 'github-copilot', name: 'GitHub Copilot', type: 'api' }
581
490
  ];
582
491
 
492
+ const opencodeCfg = loadConfig();
493
+ const currentPlugins = opencodeCfg?.plugin || [];
494
+
495
+ let studioChanged = false;
496
+ if (!studio.availableGooglePlugins) studio.availableGooglePlugins = [];
497
+
498
+ if (currentPlugins.some(p => p.includes('gemini-auth')) && !studio.availableGooglePlugins.includes('gemini')) {
499
+ studio.availableGooglePlugins.push('gemini');
500
+ studioChanged = true;
501
+ }
502
+ if (currentPlugins.some(p => p.includes('antigravity-auth')) && !studio.availableGooglePlugins.includes('antigravity')) {
503
+ studio.availableGooglePlugins.push('antigravity');
504
+ studioChanged = true;
505
+ }
506
+
507
+ const geminiProfiles = path.join(AUTH_PROFILES_DIR, 'google.gemini');
508
+ const antiProfiles = path.join(AUTH_PROFILES_DIR, 'google.antigravity');
509
+
510
+ if (fs.existsSync(geminiProfiles) && !studio.availableGooglePlugins.includes('gemini')) {
511
+ studio.availableGooglePlugins.push('gemini');
512
+ studioChanged = true;
513
+ }
514
+ if (fs.existsSync(antiProfiles) && !studio.availableGooglePlugins.includes('antigravity')) {
515
+ studio.availableGooglePlugins.push('antigravity');
516
+ studioChanged = true;
517
+ }
518
+
519
+ if (studioChanged) saveStudioConfig(studio);
520
+
583
521
  providers.forEach(p => {
584
- const profileInfo = listAuthProfiles(p.id);
585
- const hasCurrent = !!authConfig[p.id];
586
- const hasProfiles = profileInfo.length > 0;
587
-
588
- if (hasCurrent || hasProfiles) {
589
- credentials.push({
590
- ...p,
591
- active: activeProfiles[p.id] || (hasCurrent ? 'current' : null)
592
- });
522
+ const saved = listAuthProfiles(p.id, activePlugin);
523
+ let curr = !!authCfg[p.id];
524
+ if (p.id === 'google') {
525
+ const key = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
526
+ curr = !!authCfg[key] || !!authCfg.google;
593
527
  }
528
+ credentials.push({ ...p, active: ac[p.id] || (curr ? 'current' : null), profiles: saved, hasCurrentAuth: curr });
594
529
  });
595
-
596
- res.json({
597
- ...authConfig,
598
- credentials,
599
- authFile: path.join(path.dirname(getConfigPath() || ''), 'auth.json'),
600
- hasGeminiAuthPlugin: true
530
+ res.json({
531
+ ...authCfg,
532
+ credentials,
533
+ installedGooglePlugins: studio.availableGooglePlugins || [],
534
+ activeGooglePlugin: activePlugin
601
535
  });
602
536
  });
603
537
 
604
- app.post('/api/auth/login', (req, res) => {
605
- const { provider } = req.body;
606
- const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
538
+ app.get('/api/auth/profiles', (req, res) => {
539
+ const authCfg = loadAuthConfig() || {};
540
+ const studio = loadStudioConfig();
541
+ const ac = studio.activeProfiles || {};
542
+ const activePlugin = studio.activeGooglePlugin;
543
+ const profiles = {};
544
+ const providers = ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'];
607
545
 
608
- const child = spawn(cmd, ['auth', 'login', provider], {
609
- stdio: 'inherit',
610
- shell: true
546
+ providers.forEach(p => {
547
+ const saved = listAuthProfiles(p, activePlugin);
548
+ // Correct current auth check: handle google vs google.gemini/antigravity
549
+ let curr = !!authCfg[p];
550
+ if (p === 'google') {
551
+ const key = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
552
+ curr = !!authCfg[key] || !!authCfg.google;
553
+ }
554
+
555
+ if (saved.length > 0 || curr) {
556
+ profiles[p] = { active: ac[p], profiles: saved, hasCurrentAuth: !!curr };
557
+ }
611
558
  });
612
-
613
- res.json({ success: true, message: 'Launched auth flow', note: 'Please check the terminal window where the server is running' });
559
+ res.json(profiles);
614
560
  });
615
561
 
616
- app.delete('/api/auth/:provider', (req, res) => {
562
+ app.post('/api/auth/profiles/:provider', (req, res) => {
617
563
  const { provider } = req.params;
618
- const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
564
+ const { name } = req.body;
565
+ const activePlugin = getActiveGooglePlugin();
566
+ const namespace = provider === 'google'
567
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
568
+ : provider;
619
569
 
620
- spawn(cmd, ['auth', 'logout', provider], {
621
- stdio: 'inherit',
622
- shell: true
623
- });
570
+ const auth = loadAuthConfig() || {};
571
+ const dir = path.join(AUTH_PROFILES_DIR, namespace);
572
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
624
573
 
625
- res.json({ success: true });
626
- });
627
-
628
- app.get('/api/auth/providers', (req, res) => {
629
- const providers = [
630
- { id: 'google', name: 'Google AI', type: 'oauth', description: 'Use Google Gemini models' },
631
- { id: 'anthropic', name: 'Anthropic', type: 'api', description: 'Use Claude models' },
632
- { id: 'openai', name: 'OpenAI', type: 'api', description: 'Use GPT models' },
633
- { id: 'xai', name: 'xAI', type: 'api', description: 'Use Grok models' },
634
- { id: 'groq', name: 'Groq', type: 'api', description: 'Fast inference' },
635
- { id: 'together', name: 'Together AI', type: 'api', description: 'Open source models' },
636
- { id: 'mistral', name: 'Mistral', type: 'api', description: 'Mistral models' },
637
- { id: 'deepseek', name: 'DeepSeek', type: 'api', description: 'DeepSeek models' },
638
- { id: 'openrouter', name: 'OpenRouter', type: 'api', description: 'Multiple providers' },
639
- { id: 'amazon-bedrock', name: 'Amazon Bedrock', type: 'api', description: 'AWS models' },
640
- { id: 'azure', name: 'Azure OpenAI', type: 'api', description: 'Azure GPT models' },
641
- ];
642
- res.json(providers);
574
+ const profilePath = path.join(dir, `${name || Date.now()}.json`);
575
+ fs.writeFileSync(profilePath, JSON.stringify(auth[provider], null, 2), 'utf8');
576
+ res.json({ success: true, name: path.basename(profilePath, '.json') });
643
577
  });
644
578
 
645
- const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
646
-
647
- function ensureAuthProfilesDir() {
648
- if (!fs.existsSync(AUTH_PROFILES_DIR)) {
649
- fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
650
- }
651
- }
652
-
653
- function getProviderProfilesDir(provider) {
654
- return path.join(AUTH_PROFILES_DIR, provider);
655
- }
656
-
657
- function listAuthProfiles(provider) {
658
- const dir = getProviderProfilesDir(provider);
659
- if (!fs.existsSync(dir)) return [];
579
+ app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
580
+ const { provider, name } = req.params;
581
+ const activePlugin = getActiveGooglePlugin();
582
+ const namespace = provider === 'google'
583
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
584
+ : provider;
660
585
 
661
- try {
662
- return fs.readdirSync(dir)
663
- .filter(f => f.endsWith('.json'))
664
- .map(f => f.replace('.json', ''));
665
- } catch {
666
- return [];
667
- }
668
- }
669
-
670
- function getNextProfileName(provider) {
671
- const existing = listAuthProfiles(provider);
672
- let num = 1;
673
- while (existing.includes(`account-${num}`)) {
674
- num++;
586
+ const profilePath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
587
+ if (!fs.existsSync(profilePath)) return res.status(404).json({ error: 'Profile not found' });
588
+
589
+ const profileData = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
590
+ const studio = loadStudioConfig();
591
+ if (!studio.activeProfiles) studio.activeProfiles = {};
592
+ studio.activeProfiles[provider] = name;
593
+ saveStudioConfig(studio);
594
+
595
+ const authCfg = loadAuthConfig() || {};
596
+ authCfg[provider] = profileData;
597
+
598
+ // Save both to persistent namespaced key and the shared 'google' key
599
+ if (provider === 'google') {
600
+ const key = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
601
+ authCfg[key] = profileData;
675
602
  }
676
- return `account-${num}`;
677
- }
603
+
604
+ const cp = getConfigPath();
605
+ const ap = path.join(path.dirname(cp), 'auth.json');
606
+ fs.writeFileSync(ap, JSON.stringify(authCfg, null, 2), 'utf8');
607
+ res.json({ success: true });
608
+ });
678
609
 
679
- function saveAuthProfile(provider, profileName, data) {
680
- ensureAuthProfilesDir();
681
- const dir = getProviderProfilesDir(provider);
682
- if (!fs.existsSync(dir)) {
683
- fs.mkdirSync(dir, { recursive: true });
684
- }
685
- const filePath = path.join(dir, `${profileName}.json`);
686
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
687
- return true;
688
- }
610
+ app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
611
+ const { provider, name } = req.params;
612
+ const activePlugin = getActiveGooglePlugin();
613
+ const namespace = provider === 'google'
614
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
615
+ : provider;
616
+
617
+ const profilePath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
618
+ if (fs.existsSync(profilePath)) fs.unlinkSync(profilePath);
619
+ res.json({ success: true });
620
+ });
689
621
 
690
- function loadAuthProfile(provider, profileName) {
691
- const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
692
- if (!fs.existsSync(filePath)) return null;
693
- try {
694
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
695
- } catch {
696
- return null;
697
- }
698
- }
622
+ app.put('/api/auth/profiles/:provider/:name', (req, res) => {
623
+ const { provider, name } = req.params;
624
+ const { newName } = req.body;
625
+ const activePlugin = getActiveGooglePlugin();
626
+ const namespace = provider === 'google'
627
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
628
+ : provider;
629
+
630
+ const oldPath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
631
+ const newPath = path.join(AUTH_PROFILES_DIR, namespace, `${newName}.json`);
632
+ if (fs.existsSync(oldPath)) fs.renameSync(oldPath, newPath);
699
633
 
700
- function deleteAuthProfile(provider, profileName) {
701
- const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
702
- if (fs.existsSync(filePath)) {
703
- fs.unlinkSync(filePath);
704
- return true;
634
+ // Update active profile name if it was the one renamed
635
+ const studio = loadStudioConfig();
636
+ if (studio.activeProfiles && studio.activeProfiles[provider] === name) {
637
+ studio.activeProfiles[provider] = newName;
638
+ saveStudioConfig(studio);
705
639
  }
706
- return false;
707
- }
708
-
709
- function getActiveProfiles() {
710
- const studioConfig = loadStudioConfig();
711
- return studioConfig.activeProfiles || {};
712
- }
713
640
 
714
- function setActiveProfile(provider, profileName) {
715
- const studioConfig = loadStudioConfig();
716
- studioConfig.activeProfiles = studioConfig.activeProfiles || {};
717
- studioConfig.activeProfiles[provider] = profileName;
718
- saveStudioConfig(studioConfig);
719
- }
720
-
721
- function verifyActiveProfile(p, n, c) { console.log("Verifying:", p, n); if (!n || !c) return false; const d = loadAuthProfile(p, n); if (d && c) { console.log("Profile refresh:", d.refresh); console.log("Current refresh:", c.refresh); } if (!d) return false; return JSON.stringify(d) === JSON.stringify(c); }
641
+ res.json({ success: true, name: newName });
642
+ });
722
643
 
723
- app.get('/api/auth/profiles', (req, res) => {
724
- ensureAuthProfilesDir();
725
- const activeProfiles = getActiveProfiles();
726
- const authConfig = loadAuthConfig() || {};
644
+ app.post('/api/auth/login', (req, res) => {
645
+ let { provider } = req.body;
646
+ if (typeof provider !== 'string') provider = "";
727
647
 
728
- const profiles = {};
648
+ let cmd = 'opencode auth login';
649
+ if (provider) cmd += ` ${provider}`;
729
650
 
730
- const savedProviders = fs.existsSync(AUTH_PROFILES_DIR) ? fs.readdirSync(AUTH_PROFILES_DIR) : [];
651
+ const platform = process.platform;
652
+ let terminalCmd;
653
+ if (platform === 'win32') {
654
+ terminalCmd = `start "" cmd /c "call ${cmd} || pause"`;
655
+ } else if (platform === 'darwin') {
656
+ terminalCmd = `osascript -e 'tell application "Terminal" to do script "${cmd}"'`;
657
+ } else {
658
+ terminalCmd = `x-terminal-emulator -e "${cmd}"`;
659
+ }
731
660
 
732
- const standardProviders = [
733
- 'google', 'anthropic', 'openai', 'xai', 'groq',
734
- 'together', 'mistral', 'deepseek', 'openrouter',
735
- 'amazon-bedrock', 'azure', 'github-copilot'
736
- ];
737
-
738
- const allProviders = [...new Set([...savedProviders, ...standardProviders])];
739
-
740
- allProviders.forEach(p => {
741
- const saved = listAuthProfiles(p);
742
- const active = activeProfiles[p];
743
- const current = authConfig[p];
744
-
745
- if (saved.length > 0 || current) {
746
- profiles[p] = {
747
- active: active,
748
- profiles: saved,
749
- saved: saved,
750
- hasCurrent: !!current,
751
- hasCurrentAuth: !!current
752
- };
661
+ console.log('Executing terminal command:', terminalCmd);
662
+
663
+ exec(terminalCmd, (err) => {
664
+ if (err) {
665
+ console.error('Failed to open terminal:', err);
666
+ return res.status(500).json({ error: 'Failed to open terminal', details: err.message });
753
667
  }
668
+ res.json({ success: true, message: 'Terminal opened', note: 'Complete login in the terminal window' });
754
669
  });
755
-
756
- res.json(profiles);
757
670
  });
758
671
 
759
- app.get('/api/debug/paths', (req, res) => {
760
- const home = os.homedir();
761
- const candidatePaths = [
762
- process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'opencode', 'storage', 'message') : null,
763
- path.join(home, '.local', 'share', 'opencode', 'storage', 'message'),
764
- path.join(home, '.opencode', 'storage', 'message')
765
- ].filter(p => p);
766
-
767
- const results = candidatePaths.map(p => ({
768
- path: p,
769
- exists: fs.existsSync(p),
770
- isDirectory: fs.existsSync(p) && fs.statSync(p).isDirectory(),
771
- fileCount: (fs.existsSync(p) && fs.statSync(p).isDirectory()) ? fs.readdirSync(p).length : 0
772
- }));
773
-
774
- res.json({
775
- home,
776
- platform: process.platform,
777
- candidates: results
778
- });
672
+ app.delete('/api/auth/:provider', (req, res) => {
673
+ const { provider } = req.params;
674
+ const authCfg = loadAuthConfig() || {};
675
+ delete authCfg[provider];
676
+ const cp = getConfigPath();
677
+ const ap = path.join(path.dirname(cp), 'auth.json');
678
+ fs.writeFileSync(ap, JSON.stringify(authCfg, null, 2), 'utf8');
679
+
680
+ const studio = loadStudioConfig();
681
+ if (studio.activeProfiles) delete studio.activeProfiles[provider];
682
+ saveStudioConfig(studio);
683
+
684
+ res.json({ success: true });
779
685
  });
780
686
 
781
687
  app.get('/api/usage', async (req, res) => {
782
688
  try {
783
- const { projectId: filterProjectId, granularity = 'daily', range = '30d' } = req.query;
689
+ const {projectId: fid, granularity = 'daily', range = '30d'} = req.query;
690
+ const cp = getConfigPath();
691
+ if (!cp) return res.json({ totalCost: 0, totalTokens: 0, byModel: [], byDay: [], byProject: [] });
692
+
784
693
  const home = os.homedir();
785
- const candidatePaths = [
786
- process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'opencode', 'storage', 'message') : null,
787
- path.join(home, '.local', 'share', 'opencode', 'storage', 'message'),
788
- path.join(home, '.opencode', 'storage', 'message')
789
- ].filter(p => p && fs.existsSync(p));
790
-
791
- const getSessionDir = (messageDir) => {
792
- return path.join(path.dirname(messageDir), 'session');
793
- };
694
+ const dataCandidates = [
695
+ path.dirname(cp),
696
+ path.join(home, '.local', 'share', 'opencode'),
697
+ path.join(home, '.opencode'),
698
+ process.env.APPDATA ? path.join(process.env.APPDATA, 'opencode') : null,
699
+ path.join(home, 'AppData', 'Local', 'opencode')
700
+ ].filter(Boolean);
701
+
702
+ let md = null;
703
+ let sd = null;
704
+
705
+ for (const d of dataCandidates) {
706
+ const mdp = path.join(d, 'storage', 'message');
707
+ const sdp = path.join(d, 'storage', 'session');
708
+ if (fs.existsSync(mdp)) {
709
+ md = mdp;
710
+ sd = sdp;
711
+ break;
712
+ }
713
+ }
794
714
 
795
- const sessionProjectMap = new Map();
715
+ if (!md) return res.json({ totalCost: 0, totalTokens: 0, byModel: [], byDay: [], byProject: [] });
796
716
 
797
- const loadProjects = (messageDir) => {
798
- const sessionDir = getSessionDir(messageDir);
799
- if (!fs.existsSync(sessionDir)) return;
800
717
 
801
- try {
802
- const projectDirs = fs.readdirSync(sessionDir);
803
-
804
- for (const projDir of projectDirs) {
805
- const fullProjPath = path.join(sessionDir, projDir);
806
- if (!fs.statSync(fullProjPath).isDirectory()) continue;
807
-
808
- const files = fs.readdirSync(fullProjPath);
809
- for (const file of files) {
810
- if (file.startsWith('ses_') && file.endsWith('.json')) {
811
- const sessionId = file.replace('.json', '');
718
+ const pmap = new Map();
719
+ if (fs.existsSync(sd)) {
720
+ fs.readdirSync(sd).forEach(d => {
721
+ const fp = path.join(sd, d);
722
+ if (fs.statSync(fp).isDirectory()) {
723
+ fs.readdirSync(fp).forEach(f => {
724
+ if (f.startsWith('ses_') && f.endsWith('.json')) {
812
725
  try {
813
- const meta = JSON.parse(fs.readFileSync(path.join(fullProjPath, file), 'utf8'));
814
- let projectName = 'Unknown Project';
815
- if (meta.directory) {
816
- projectName = path.basename(meta.directory);
817
- } else if (meta.projectID) {
818
- projectName = meta.projectID.substring(0, 8);
819
- }
820
- sessionProjectMap.set(sessionId, { name: projectName, id: meta.projectID || projDir });
821
- } catch (e) {}
726
+ const m = JSON.parse(fs.readFileSync(path.join(fp, f), 'utf8'));
727
+ pmap.set(f.replace('.json', ''), { name: m.directory ? path.basename(m.directory) : (m.projectID ? m.projectID.substring(0, 8) : 'Unknown'), id: m.projectID || d });
728
+ } catch {}
822
729
  }
823
- }
730
+ });
824
731
  }
825
- } catch (e) {
826
- console.error('Error loading projects:', e);
827
- }
828
- };
829
-
830
- for (const logDir of candidatePaths) {
831
- loadProjects(logDir);
732
+ });
832
733
  }
833
734
 
834
- const stats = {
835
- totalCost: 0,
836
- totalTokens: 0,
837
- byModel: {},
838
- byTime: {},
839
- byProject: {}
840
- };
841
-
842
- const processedFiles = new Set();
735
+ const stats = { totalCost: 0, totalTokens: 0, byModel: {}, byTime: {}, byProject: {} };
736
+ const seen = new Set();
843
737
  const now = Date.now();
844
- let minTimestamp = 0;
845
-
846
- if (range === '24h') minTimestamp = now - 24 * 60 * 60 * 1000;
847
- else if (range === '7d') minTimestamp = now - 7 * 24 * 60 * 60 * 1000;
848
- else if (range === '30d') minTimestamp = now - 30 * 24 * 60 * 60 * 1000;
849
-
850
- const processMessage = (filePath, sessionId) => {
851
- if (processedFiles.has(filePath)) return;
852
- processedFiles.add(filePath);
853
-
854
- try {
855
- const content = fs.readFileSync(filePath, 'utf8');
856
- const msg = JSON.parse(content);
857
-
858
- const model = msg.modelID || (msg.model && (msg.model.modelID || msg.model.id)) || 'unknown';
859
- const projectInfo = sessionProjectMap.get(sessionId) || { name: 'Unassigned', id: 'unknown' };
860
- const projectId = projectInfo.id || 'unknown';
861
-
862
- if (filterProjectId && filterProjectId !== 'all' && projectId !== filterProjectId) {
863
- return;
864
- }
865
-
866
- if (minTimestamp > 0 && msg.time.created < minTimestamp) {
867
- return;
868
- }
869
-
870
- if (msg.role === 'assistant' && msg.tokens) {
871
- const cost = msg.cost || 0;
872
- const inputTokens = msg.tokens.input || 0;
873
- const outputTokens = msg.tokens.output || 0;
874
- const tokens = inputTokens + outputTokens;
875
-
876
- const timestamp = msg.time.created;
877
- const dateObj = new Date(timestamp);
878
- let timeKey;
879
-
880
- if (granularity === 'hourly') {
881
- timeKey = dateObj.toISOString().substring(0, 13) + ':00:00Z';
882
- } else if (granularity === 'weekly') {
883
- const day = dateObj.getDay();
884
- const diff = dateObj.getDate() - day + (day === 0 ? -6 : 1);
885
- const monday = new Date(dateObj.setDate(diff));
886
- timeKey = monday.toISOString().split('T')[0];
887
- } else if (granularity === 'monthly') {
888
- timeKey = dateObj.toISOString().substring(0, 7) + '-01';
889
- } else {
890
- timeKey = dateObj.toISOString().split('T')[0];
891
- }
892
-
893
- if (tokens > 0) {
894
- stats.totalCost += cost;
895
- stats.totalTokens += tokens;
896
-
897
- if (!stats.byModel[model]) {
898
- stats.byModel[model] = { name: model, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
899
- }
900
- stats.byModel[model].cost += cost;
901
- stats.byModel[model].tokens += tokens;
902
- stats.byModel[model].inputTokens += inputTokens;
903
- stats.byModel[model].outputTokens += outputTokens;
904
-
905
- if (!stats.byTime[timeKey]) {
906
- stats.byTime[timeKey] = { date: timeKey, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
907
- }
908
- stats.byTime[timeKey].cost += cost;
909
- stats.byTime[timeKey].tokens += tokens;
910
- stats.byTime[timeKey].inputTokens += inputTokens;
911
- stats.byTime[timeKey].outputTokens += outputTokens;
912
-
913
- const projectName = projectInfo.name;
914
- if (!stats.byProject[projectId]) {
915
- stats.byProject[projectId] = { id: projectId, name: projectName, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
916
- }
917
- stats.byProject[projectId].cost += cost;
918
- stats.byProject[projectId].tokens += tokens;
919
- stats.byProject[projectId].inputTokens += inputTokens;
920
- stats.byProject[projectId].outputTokens += outputTokens;
921
- }
922
- }
923
- } catch (err) {
924
- }
925
- };
926
-
927
- for (const logDir of candidatePaths) {
928
- try {
929
- const sessions = fs.readdirSync(logDir);
930
- for (const session of sessions) {
931
- if (!session.startsWith('ses_')) continue;
932
-
933
- const sessionDir = path.join(logDir, session);
934
- if (fs.statSync(sessionDir).isDirectory()) {
935
- const messages = fs.readdirSync(sessionDir);
936
- for (const msgFile of messages) {
937
- if (msgFile.endsWith('.json')) {
938
- processMessage(path.join(sessionDir, msgFile), session);
939
- }
738
+ let min = 0;
739
+ if (range === '24h') min = now - 86400000;
740
+ else if (range === '7d') min = now - 604800000;
741
+ else if (range === '30d') min = now - 2592000000;
742
+ else if (range === '1y') min = now - 31536000000;
743
+
744
+ fs.readdirSync(md).forEach(s => {
745
+ if (!s.startsWith('ses_')) return;
746
+ const sp = path.join(md, s);
747
+ if (fs.statSync(sp).isDirectory()) {
748
+ fs.readdirSync(sp).forEach(f => {
749
+ if (!f.endsWith('.json') || seen.has(path.join(sp, f))) return;
750
+ seen.add(path.join(sp, f));
751
+ try {
752
+ const msg = JSON.parse(fs.readFileSync(path.join(sp, f), 'utf8'));
753
+ const pid = pmap.get(s)?.id || 'unknown';
754
+ if (fid && fid !== 'all' && pid !== fid) return;
755
+ if (min > 0 && msg.time.created < min) return;
756
+ if (msg.role === 'assistant' && msg.tokens) {
757
+ const c = msg.cost || 0, it = msg.tokens.input || 0, ot = msg.tokens.output || 0, t = it + ot;
758
+ const d = new Date(msg.time.created);
759
+ let tk;
760
+ if (granularity === 'hourly') tk = d.toISOString().substring(0, 13) + ':00:00Z';
761
+ else if (granularity === 'weekly') {
762
+ const day = d.getDay(), diff = d.getDate() - day + (day === 0 ? -6 : 1);
763
+ tk = new Date(d.setDate(diff)).toISOString().split('T')[0];
764
+ } else if (granularity === 'monthly') tk = d.toISOString().substring(0, 7) + '-01';
765
+ else tk = d.toISOString().split('T')[0];
766
+
767
+ const mid = msg.modelID || (msg.model && (msg.model.modelID || msg.model.id)) || 'unknown';
768
+ stats.totalCost += c; stats.totalTokens += t;
769
+ [stats.byModel, stats.byProject].forEach((obj, i) => {
770
+ const key = i === 0 ? mid : pid;
771
+ if (!obj[key]) obj[key] = { name: key, id: key, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
772
+ if (i === 1) obj[key].name = pmap.get(s)?.name || 'Unassigned';
773
+ obj[key].cost += c; obj[key].tokens += t; obj[key].inputTokens += it; obj[key].outputTokens += ot;
774
+ });
775
+
776
+ if (!stats.byTime[tk]) stats.byTime[tk] = { date: tk, name: tk, id: tk, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
777
+ const te = stats.byTime[tk];
778
+ te.cost += c; te.tokens += t; te.inputTokens += it; te.outputTokens += ot;
779
+ if (!te[mid]) te[mid] = 0;
780
+ te[mid] += c;
781
+
782
+ const kIn = `${mid}_input`, kOut = `${mid}_output`;
783
+ te[kIn] = (te[kIn] || 0) + it;
784
+ te[kOut] = (te[kOut] || 0) + ot;
940
785
  }
941
- }
942
- }
943
- } catch (err) {
944
- console.error(`Error reading log dir ${logDir}:`, err);
786
+ } catch {}
787
+ });
945
788
  }
946
- }
789
+ });
947
790
 
948
- const response = {
791
+ res.json({
949
792
  totalCost: stats.totalCost,
950
793
  totalTokens: stats.totalTokens,
951
794
  byModel: Object.values(stats.byModel).sort((a, b) => b.cost - a.cost),
952
- byDay: Object.values(stats.byTime).sort((a, b) => a.date.localeCompare(b.date)),
795
+ byDay: Object.values(stats.byTime).sort((a, b) => a.name.localeCompare(b.name)).map(v => ({ ...v, date: v.name })),
953
796
  byProject: Object.values(stats.byProject).sort((a, b) => b.cost - a.cost)
954
- };
955
-
956
- res.json(response);
797
+ });
957
798
  } catch (error) {
958
- console.error('Error fetching usage stats:', error);
959
- res.status(500).json({ error: 'Failed to fetch usage stats' });
799
+ res.status(500).json({ error: 'Failed' });
960
800
  }
961
801
  });
962
802
 
963
- app.get('/api/auth/profiles/:provider', (req, res) => {
964
- const { provider } = req.params;
965
- const providerProfiles = listAuthProfiles(provider);
966
- const activeProfiles = getActiveProfiles();
967
- const authConfig = loadAuthConfig() || {};
968
-
969
- res.json({
970
- profiles: providerProfiles,
971
- active: (activeProfiles[provider] && verifyActiveProfile(provider, activeProfiles[provider], authConfig[provider])) ? activeProfiles[provider] : null,
972
- hasCurrentAuth: !!authConfig[provider],
973
- });
974
- });
975
-
976
- app.post('/api/auth/profiles/:provider', (req, res) => {
977
- const { provider } = req.params;
978
- const { name } = req.body;
979
-
980
- const authConfig = loadAuthConfig();
981
- if (!authConfig || !authConfig[provider]) {
982
- return res.status(400).json({ error: `No active auth for ${provider} to save` });
983
- }
984
-
985
- const profileName = name || getNextProfileName(provider);
986
- const data = authConfig[provider];
803
+ app.post('/api/auth/google/plugin', (req, res) => {
804
+ const { plugin } = req.body;
805
+ const studio = loadStudioConfig();
806
+ studio.activeGooglePlugin = plugin;
807
+ saveStudioConfig(studio);
987
808
 
988
809
  try {
989
- saveAuthProfile(provider, profileName, data);
990
- setActiveProfile(provider, profileName);
991
- res.json({ success: true, name: profileName });
992
- } catch (err) {
993
- res.status(500).json({ error: 'Failed to save profile', details: err.message });
994
- }
995
- });
810
+ const opencode = loadConfig();
811
+ if (opencode) {
812
+ if (opencode.provider?.google) {
813
+ const models = studio.pluginModels[plugin];
814
+ if (models) {
815
+ opencode.provider.google.models = models;
816
+ }
817
+ }
996
818
 
997
- app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
998
- const { provider, name } = req.params;
999
-
1000
- const profileData = loadAuthProfile(provider, name);
1001
- if (!profileData) {
1002
- return res.status(404).json({ error: 'Profile not found' });
1003
- }
1004
-
1005
- const authConfig = loadAuthConfig() || {};
1006
- authConfig[provider] = profileData;
1007
-
1008
- const configPath = getConfigPath();
1009
- if (configPath) {
1010
- const authPath = path.join(path.dirname(configPath), 'auth.json');
1011
- try {
1012
- fs.writeFileSync(authPath, JSON.stringify(authConfig, null, 2), 'utf8');
1013
- setActiveProfile(provider, name);
1014
- res.json({ success: true });
1015
- } catch (err) {
1016
- res.status(500).json({ error: 'Failed to write auth config', details: err.message });
819
+ if (!opencode.plugin) opencode.plugin = [];
820
+ const geminiPlugin = 'opencode-gemini-auth@latest';
821
+ const antigravityPlugin = 'opencode-google-antigravity-auth';
822
+
823
+ opencode.plugin = opencode.plugin.filter(p =>
824
+ !p.includes('gemini-auth') &&
825
+ !p.includes('antigravity-auth')
826
+ );
827
+
828
+ if (plugin === 'gemini') {
829
+ opencode.plugin.push(geminiPlugin);
830
+ } else if (plugin === 'antigravity') {
831
+ opencode.plugin.push(antigravityPlugin);
832
+ }
833
+
834
+ saveConfig(opencode);
1017
835
  }
1018
- } else {
1019
- res.status(500).json({ error: 'Config path not found' });
1020
- }
1021
- });
1022
836
 
1023
- app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
1024
- const { provider, name } = req.params;
1025
- const success = deleteAuthProfile(provider, name);
1026
- if (success) {
1027
- const activeProfiles = getActiveProfiles();
1028
- if (activeProfiles[provider] === name) {
1029
- const studioConfig = loadStudioConfig();
1030
- if (studioConfig.activeProfiles) {
1031
- delete studioConfig.activeProfiles[provider];
1032
- saveStudioConfig(studioConfig);
837
+ const cp = getConfigPath();
838
+ if (cp) {
839
+ const ap = path.join(path.dirname(cp), 'auth.json');
840
+ if (fs.existsSync(ap)) {
841
+ const authCfg = JSON.parse(fs.readFileSync(ap, 'utf8'));
842
+ if (plugin === 'antigravity' && authCfg['google.antigravity']) {
843
+ authCfg.google = { ...authCfg['google.antigravity'] };
844
+ } else if (plugin === 'gemini' && authCfg['google.gemini']) {
845
+ authCfg.google = { ...authCfg['google.gemini'] };
846
+ }
847
+ fs.writeFileSync(ap, JSON.stringify(authCfg, null, 2), 'utf8');
1033
848
  }
1034
849
  }
1035
- res.json({ success: true });
1036
- } else {
1037
- res.status(404).json({ error: 'Profile not found' });
850
+ } catch (err) {
851
+ console.error(err);
1038
852
  }
853
+
854
+ res.json({ success: true, activePlugin: plugin });
1039
855
  });
1040
856
 
1041
- app.put('/api/auth/profiles/:provider/:name', (req, res) => {
1042
- const { provider, name } = req.params;
1043
- const { newName } = req.body;
1044
-
1045
- if (!newName) return res.status(400).json({ error: 'New name required' });
1046
-
1047
- const profileData = loadAuthProfile(provider, name);
1048
- if (!profileData) return res.status(404).json({ error: 'Profile not found' });
1049
-
1050
- const success = saveAuthProfile(provider, newName, profileData);
1051
- if (success) {
1052
- deleteAuthProfile(provider, name);
1053
-
1054
- const activeProfiles = getActiveProfiles();
1055
- if (activeProfiles[provider] === name) {
1056
- setActiveProfile(provider, newName);
1057
- }
1058
-
1059
- res.json({ success: true, name: newName });
1060
- } else {
1061
- res.status(500).json({ error: 'Failed to rename profile' });
1062
- }
857
+ app.get('/api/auth/google/plugin', (req, res) => {
858
+ const studio = loadStudioConfig();
859
+ res.json({ activePlugin: studio.activeGooglePlugin || null });
1063
860
  });
1064
861
 
1065
- app.post('/api/plugins/config/add', (req, res) => {
1066
- const { plugins } = req.body;
1067
- if (!Array.isArray(plugins) || plugins.length === 0) {
1068
- return res.status(400).json({ error: 'Plugins array required' });
862
+ app.get('/api/pending-action', (req, res) => {
863
+ if (pendingActionMemory) return res.json({ action: pendingActionMemory });
864
+ if (fs.existsSync(PENDING_ACTION_PATH)) {
865
+ try {
866
+ const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
867
+ return res.json({ action });
868
+ } catch {}
1069
869
  }
870
+ res.json({ action: null });
871
+ });
1070
872
 
1071
- const config = loadConfig();
1072
- if (!config) return res.status(404).json({ error: 'Config not found' });
1073
-
1074
- if (!config.plugins) config.plugins = {};
1075
-
1076
- const result = {
1077
- added: [],
1078
- skipped: []
1079
- };
1080
-
1081
- for (const pluginName of plugins) {
1082
- const pluginDir = getPluginDir();
1083
- const dirPath = path.join(pluginDir, pluginName);
1084
- const hasJs = fs.existsSync(path.join(dirPath, 'index.js'));
1085
- const hasTs = fs.existsSync(path.join(dirPath, 'index.ts'));
1086
-
1087
- if (!hasJs && !hasTs) {
1088
- result.skipped.push(`${pluginName} (not found)`);
1089
- continue;
1090
- }
873
+ app.delete('/api/pending-action', (req, res) => {
874
+ pendingActionMemory = null;
875
+ if (fs.existsSync(PENDING_ACTION_PATH)) fs.unlinkSync(PENDING_ACTION_PATH);
876
+ res.json({ success: true });
877
+ });
1091
878
 
1092
- if (config.plugins[pluginName]) {
1093
- result.skipped.push(`${pluginName} (already configured)`);
879
+ app.post('/api/plugins/config/add', (req, res) => {
880
+ const { plugins } = req.body;
881
+ const opencode = loadConfig();
882
+ if (!opencode) return res.status(404).json({ error: 'Config not found' });
883
+
884
+ if (!opencode.plugin) opencode.plugin = [];
885
+ const added = [];
886
+ const skipped = [];
887
+
888
+ plugins.forEach(p => {
889
+ if (!opencode.plugin.includes(p)) {
890
+ opencode.plugin.push(p);
891
+ added.push(p);
892
+
893
+ const studio = loadStudioConfig();
894
+ if (p.includes('gemini-auth') && !studio.availableGooglePlugins.includes('gemini')) {
895
+ studio.availableGooglePlugins.push('gemini');
896
+ saveStudioConfig(studio);
897
+ }
898
+ if (p.includes('antigravity-auth') && !studio.availableGooglePlugins.includes('antigravity')) {
899
+ studio.availableGooglePlugins.push('antigravity');
900
+ saveStudioConfig(studio);
901
+ }
1094
902
  } else {
1095
- config.plugins[pluginName] = { enabled: true };
1096
- result.added.push(pluginName);
903
+ skipped.push(p);
1097
904
  }
1098
- }
1099
-
1100
- if (result.added.length > 0) {
1101
- saveConfig(config);
1102
- }
1103
-
1104
- res.json(result);
1105
- });
1106
-
1107
- app.delete('/api/plugins/config/:name', (req, res) => {
1108
- const pluginName = decodeURIComponent(req.params.name);
1109
- const config = loadConfig();
905
+ });
1110
906
 
1111
- if (!config || !config.plugins) {
1112
- return res.status(404).json({ error: 'Config not found or no plugins' });
1113
- }
1114
-
1115
- if (config.plugins[pluginName]) {
1116
- delete config.plugins[pluginName];
1117
- saveConfig(config);
1118
- res.json({ success: true });
1119
- } else {
1120
- res.status(404).json({ error: 'Plugin not in config' });
1121
- }
907
+ saveConfig(opencode);
908
+ res.json({ added, skipped });
1122
909
  });
1123
910
 
1124
- app.listen(PORT, () => {
1125
- console.log(`Server running at http://localhost:${PORT}`);
1126
- });
911
+ app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));