opencode-studio-server 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +578 -854
  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,127 @@ 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
+ "gemini-claude-sonnet-4-5-thinking": {
133
+ "id": "gemini-claude-sonnet-4-5-thinking",
134
+ "name": "Sonnet 4.5",
135
+ "reasoning": true,
136
+ "limit": { "context": 200000, "output": 64000 },
137
+ "modalities": {
138
+ "input": ["text", "image", "pdf"],
139
+ "output": ["text"]
140
+ },
141
+ "variants": {
142
+ "none": { "reasoning": false, "options": { "thinkingConfig": { "includeThoughts": false } } },
143
+ "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
144
+ "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
145
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } }
146
+ }
147
+ },
148
+ "gemini-claude-opus-4-5-thinking": {
149
+ "id": "gemini-claude-opus-4-5-thinking",
150
+ "name": "Opus 4.5",
151
+ "release_date": "2025-11-24",
152
+ "reasoning": true,
153
+ "limit": { "context": 200000, "output": 64000 },
154
+ "modalities": {
155
+ "input": ["text", "image", "pdf"],
156
+ "output": ["text"]
157
+ },
158
+ "variants": {
159
+ "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
160
+ "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
161
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ };
167
+
168
+ if (!fs.existsSync(STUDIO_CONFIG_PATH)) return defaultConfig;
65
169
  try {
66
- return JSON.parse(fs.readFileSync(STUDIO_CONFIG_PATH, 'utf8'));
170
+ const config = JSON.parse(fs.readFileSync(STUDIO_CONFIG_PATH, 'utf8'));
171
+ return { ...defaultConfig, ...config };
67
172
  } catch {
68
- return {};
173
+ return defaultConfig;
69
174
  }
70
175
  }
71
176
 
72
177
  function saveStudioConfig(config) {
73
178
  try {
74
179
  const dir = path.dirname(STUDIO_CONFIG_PATH);
75
- if (!fs.existsSync(dir)) {
76
- fs.mkdirSync(dir, { recursive: true });
77
- }
180
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
78
181
  fs.writeFileSync(STUDIO_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
79
182
  return true;
80
183
  } catch (err) {
@@ -83,55 +186,16 @@ function saveStudioConfig(config) {
83
186
  }
84
187
  }
85
188
 
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
189
  const getPaths = () => {
119
190
  const platform = process.platform;
120
191
  const home = os.homedir();
121
-
122
- let candidates = [];
192
+ let candidates = [
193
+ path.join(home, '.config', 'opencode', 'opencode.json'),
194
+ path.join(home, '.local', 'share', 'opencode', 'opencode.json'),
195
+ path.join(home, '.opencode', 'opencode.json'),
196
+ ];
123
197
  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
- ];
198
+ candidates.push(path.join(process.env.APPDATA, 'opencode', 'opencode.json'));
135
199
  }
136
200
 
137
201
  const studioConfig = loadStudioConfig();
@@ -149,20 +213,15 @@ const getPaths = () => {
149
213
  detected,
150
214
  manual: manualPath,
151
215
  current: manualPath || detected,
152
- candidates
216
+ candidates: [...new Set(candidates)]
153
217
  };
154
218
  };
155
219
 
156
- const getConfigPath = () => {
157
- const paths = getPaths();
158
- return paths.current;
159
- };
220
+ const getConfigPath = () => getPaths().current;
160
221
 
161
222
  const loadConfig = () => {
162
223
  const configPath = getConfigPath();
163
- if (!configPath || !fs.existsSync(configPath)) {
164
- return null;
165
- }
224
+ if (!configPath || !fs.existsSync(configPath)) return null;
166
225
  try {
167
226
  return JSON.parse(fs.readFileSync(configPath, 'utf8'));
168
227
  } catch {
@@ -172,28 +231,20 @@ const loadConfig = () => {
172
231
 
173
232
  const saveConfig = (config) => {
174
233
  const configPath = getConfigPath();
175
- if (!configPath) {
176
- throw new Error('No config path found');
177
- }
234
+ if (!configPath) throw new Error('No config path found');
178
235
  const dir = path.dirname(configPath);
179
- if (!fs.existsSync(dir)) {
180
- fs.mkdirSync(dir, { recursive: true });
181
- }
236
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
182
237
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
183
238
  };
184
239
 
185
- app.get('/api/health', (req, res) => {
186
- res.json({ status: 'ok' });
187
- });
240
+ app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
188
241
 
189
242
  app.post('/api/shutdown', (req, res) => {
190
243
  res.json({ success: true });
191
244
  setTimeout(() => process.exit(0), 100);
192
245
  });
193
246
 
194
- app.get('/api/paths', (req, res) => {
195
- res.json(getPaths());
196
- });
247
+ app.get('/api/paths', (req, res) => res.json(getPaths()));
197
248
 
198
249
  app.post('/api/paths', (req, res) => {
199
250
  const { configPath } = req.body;
@@ -205,16 +256,13 @@ app.post('/api/paths', (req, res) => {
205
256
 
206
257
  app.get('/api/config', (req, res) => {
207
258
  const config = loadConfig();
208
- if (!config) {
209
- return res.status(404).json({ error: 'Config not found' });
210
- }
259
+ if (!config) return res.status(404).json({ error: 'Config not found' });
211
260
  res.json(config);
212
261
  });
213
262
 
214
263
  app.post('/api/config', (req, res) => {
215
- const config = req.body;
216
264
  try {
217
- saveConfig(config);
265
+ saveConfig(req.body);
218
266
  res.json({ success: true });
219
267
  } catch (err) {
220
268
  res.status(500).json({ error: err.message });
@@ -222,905 +270,581 @@ app.post('/api/config', (req, res) => {
222
270
  });
223
271
 
224
272
  const getSkillDir = () => {
225
- const configPath = getConfigPath();
226
- if (!configPath) return null;
227
- return path.join(path.dirname(configPath), 'skill');
273
+ const cp = getConfigPath();
274
+ return cp ? path.join(path.dirname(cp), 'skill') : null;
228
275
  };
229
276
 
230
277
  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
- }
278
+ const sd = getSkillDir();
279
+ if (!sd || !fs.existsSync(sd)) return res.json([]);
280
+ const skills = fs.readdirSync(sd, { withFileTypes: true })
281
+ .filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
282
+ .map(e => ({ name: e.name, path: path.join(sd, e.name, 'SKILL.md'), enabled: !e.name.endsWith('.disabled') }));
251
283
  res.json(skills);
252
284
  });
253
285
 
254
286
  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 });
287
+ const sd = getSkillDir();
288
+ const p = sd ? path.join(sd, req.params.name, 'SKILL.md') : null;
289
+ if (!p || !fs.existsSync(p)) return res.status(404).json({ error: 'Not found' });
290
+ res.json({ name: req.params.name, content: fs.readFileSync(p, 'utf8') });
267
291
  });
268
292
 
269
293
  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');
294
+ const sd = getSkillDir();
295
+ if (!sd) return res.status(404).json({ error: 'No config' });
296
+ const dp = path.join(sd, req.params.name);
297
+ if (!fs.existsSync(dp)) fs.mkdirSync(dp, { recursive: true });
298
+ fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
282
299
  res.json({ success: true });
283
300
  });
284
301
 
285
302
  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
- }
303
+ const sd = getSkillDir();
304
+ const dp = sd ? path.join(sd, req.params.name) : null;
305
+ if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
295
306
  res.json({ success: true });
296
307
  });
297
308
 
298
- app.post('/api/skills/:name/toggle', (req, res) => {
299
- res.json({ success: true, enabled: true });
300
- });
301
-
302
309
  const getPluginDir = () => {
303
- const configPath = getConfigPath();
304
- if (!configPath) return null;
305
- return path.join(path.dirname(configPath), 'plugin');
310
+ const cp = getConfigPath();
311
+ return cp ? path.join(path.dirname(cp), 'plugin') : null;
306
312
  };
307
313
 
308
314
  app.get('/api/plugins', (req, res) => {
309
- const pluginDir = getPluginDir();
310
- const configPath = getConfigPath();
311
- const configRoot = configPath ? path.dirname(configPath) : null;
312
-
315
+ const pd = getPluginDir();
316
+ const cp = getConfigPath();
317
+ const cr = cp ? path.dirname(cp) : null;
313
318
  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
- }
319
+ const add = (name, p, enabled = true) => {
320
+ if (!plugins.some(pl => pl.name === name)) plugins.push({ name, path: p, enabled });
319
321
  };
320
322
 
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);
323
+ if (pd && fs.existsSync(pd)) {
324
+ fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
325
+ const fp = path.join(pd, e.name);
326
+ const st = fs.lstatSync(fp);
327
+ if (st.isDirectory()) {
328
+ const j = path.join(fp, 'index.js'), t = path.join(fp, 'index.ts');
329
+ if (fs.existsSync(j) || fs.existsSync(t)) add(e.name, fs.existsSync(j) ? j : t);
330
+ } else if ((st.isFile() || st.isSymbolicLink()) && /\.(js|ts)$/.test(e.name)) {
331
+ add(e.name.replace(/\.(js|ts)$/, ''), fp);
334
332
  }
335
- }
333
+ });
336
334
  }
337
335
 
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
- }
336
+ if (cr && fs.existsSync(cr)) {
337
+ ['oh-my-opencode', 'superpowers', 'opencode-gemini-auth'].forEach(n => {
338
+ const fp = path.join(cr, n);
339
+ if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) add(n, fp);
340
+ });
348
341
  }
349
342
 
343
+ const cfg = loadConfig();
344
+ if (cfg && Array.isArray(cfg.plugin)) {
345
+ cfg.plugin.forEach(n => {
346
+ if (!n.includes('/') && !n.includes('\\') && !/\.(js|ts)$/.test(n)) add(n, 'npm');
347
+ });
348
+ }
350
349
  res.json(plugins);
351
350
  });
352
351
 
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
- });
352
+ const getActiveGooglePlugin = () => {
353
+ const studio = loadStudioConfig();
354
+ return studio.activeGooglePlugin || null;
355
+ };
381
356
 
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
- }
357
+ function loadAuthConfig() {
358
+ const paths = getPaths();
359
+ const allAuthConfigs = [];
393
360
 
394
- const filePath = path.join(dirPath, 'index.js');
395
- fs.writeFileSync(filePath, content, 'utf8');
396
- res.json({ success: true });
397
- });
361
+ // Check all candidate directories for auth.json
362
+ paths.candidates.forEach(p => {
363
+ const ap = path.join(path.dirname(p), 'auth.json');
364
+ if (fs.existsSync(ap)) {
365
+ try {
366
+ allAuthConfigs.push(JSON.parse(fs.readFileSync(ap, 'utf8')));
367
+ } catch {}
368
+ }
369
+ });
398
370
 
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'));
371
+ // Also check current active directory if not in candidates
372
+ if (paths.current) {
373
+ const ap = path.join(path.dirname(paths.current), 'auth.json');
374
+ if (fs.existsSync(ap)) {
375
+ try {
376
+ allAuthConfigs.push(JSON.parse(fs.readFileSync(ap, 'utf8')));
377
+ } catch {}
378
+ }
412
379
  }
413
- res.json({ success: true });
414
- });
415
380
 
416
- app.post('/api/plugins/:name/toggle', (req, res) => {
417
- res.json({ success: true, enabled: true });
418
- });
381
+ if (allAuthConfigs.length === 0) return null;
419
382
 
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' });
383
+ // Merge all configs, later ones (priority) overwrite earlier ones
384
+ const cfg = Object.assign({}, ...allAuthConfigs);
423
385
 
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';
386
+ try {
387
+ const activePlugin = getActiveGooglePlugin();
430
388
 
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
- });
389
+ // Find if any google auth exists in any merged config
390
+ const anyGoogleAuth = cfg.google || cfg['google.gemini'] || cfg['google.antigravity'];
391
+
392
+ if (anyGoogleAuth) {
393
+ // Ensure all 3 keys have at least some valid google auth data
394
+ const baseAuth = cfg.google || cfg['google.antigravity'] || cfg['google.gemini'];
395
+ if (!cfg.google) cfg.google = { ...baseAuth };
396
+ if (!cfg['google.antigravity']) cfg['google.antigravity'] = { ...baseAuth };
397
+ if (!cfg['google.gemini']) cfg['google.gemini'] = { ...baseAuth };
464
398
  }
465
- }
466
-
467
- res.json({ results });
468
- });
469
399
 
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
- }
400
+ // Always ensure the main 'google' key reflects the active plugin if it exists
401
+ if (activePlugin === 'antigravity' && cfg['google.antigravity']) {
402
+ cfg.google = { ...cfg['google.antigravity'] };
403
+ } else if (activePlugin === 'gemini' && cfg['google.gemini']) {
404
+ cfg.google = { ...cfg['google.gemini'] };
485
405
  }
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
- }
499
- }
500
- }
501
-
502
- res.json({
503
- version: 1,
504
- timestamp: new Date().toISOString(),
505
- studioConfig,
506
- opencodeConfig,
507
- skills,
508
- plugins
509
- });
510
- });
406
+
407
+ return cfg;
408
+ } catch { return cfg; }
409
+ }
511
410
 
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
- }
411
+ const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
412
+ const listAuthProfiles = (p, activePlugin) => {
413
+ let ns = p;
414
+ if (p === 'google') {
415
+ ns = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
545
416
  }
546
417
 
547
- res.json({ success: true });
548
- });
418
+ const d = path.join(AUTH_PROFILES_DIR, ns);
419
+ if (!fs.existsSync(d)) return [];
420
+ try { return fs.readdirSync(d).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '')); } catch { return []; }
421
+ };
549
422
 
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
- }
423
+ app.get('/api/auth/providers', (req, res) => {
424
+ const providers = [
425
+ { id: 'google', name: 'Google', type: 'oauth', description: 'Google Gemini API' },
426
+ { id: 'anthropic', name: 'Anthropic', type: 'api', description: 'Claude models' },
427
+ { id: 'openai', name: 'OpenAI', type: 'api', description: 'GPT models' },
428
+ { id: 'xai', name: 'xAI', type: 'api', description: 'Grok models' },
429
+ { id: 'openrouter', name: 'OpenRouter', type: 'api', description: 'Unified LLM API' },
430
+ { id: 'github-copilot', name: 'GitHub Copilot', type: 'api', description: 'Copilot models' }
431
+ ];
432
+ res.json(providers);
433
+ });
562
434
 
563
435
  app.get('/api/auth', (req, res) => {
564
- const authConfig = loadAuthConfig() || {};
565
- const activeProfiles = getActiveProfiles();
566
-
436
+ const authCfg = loadAuthConfig() || {};
437
+ const studio = loadStudioConfig();
438
+ const ac = studio.activeProfiles || {};
567
439
  const credentials = [];
440
+ const activePlugin = studio.activeGooglePlugin;
441
+
442
+ // DEBUG LOGGING
443
+ console.log('--- Auth Debug ---');
444
+ console.log('Active Google Plugin:', activePlugin);
445
+ console.log('Found keys in authCfg:', Object.keys(authCfg));
446
+ if (authCfg.google) console.log('Google Auth found!');
447
+ if (authCfg['google.antigravity']) console.log('google.antigravity found!');
448
+ if (authCfg['google.gemini']) console.log('google.gemini found!');
449
+
568
450
  const providers = [
569
- { id: 'google', name: 'Google AI', type: 'oauth' },
451
+ { id: 'google', name: 'Google', type: 'oauth' },
570
452
  { id: 'anthropic', name: 'Anthropic', type: 'api' },
571
453
  { id: 'openai', name: 'OpenAI', type: 'api' },
572
454
  { 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
455
  { id: 'openrouter', name: 'OpenRouter', type: 'api' },
578
- { id: 'amazon-bedrock', name: 'Amazon Bedrock', type: 'api' },
579
- { id: 'azure', name: 'Azure OpenAI', type: 'api' },
580
456
  { id: 'github-copilot', name: 'GitHub Copilot', type: 'api' }
581
457
  ];
582
458
 
459
+ const opencodeCfg = loadConfig();
460
+ const currentPlugins = opencodeCfg?.plugin || [];
461
+
462
+ let studioChanged = false;
463
+ if (!studio.availableGooglePlugins) studio.availableGooglePlugins = [];
464
+
465
+ if (currentPlugins.some(p => p.includes('gemini-auth')) && !studio.availableGooglePlugins.includes('gemini')) {
466
+ studio.availableGooglePlugins.push('gemini');
467
+ studioChanged = true;
468
+ }
469
+ if (currentPlugins.some(p => p.includes('antigravity-auth')) && !studio.availableGooglePlugins.includes('antigravity')) {
470
+ studio.availableGooglePlugins.push('antigravity');
471
+ studioChanged = true;
472
+ }
473
+
474
+ const geminiProfiles = path.join(AUTH_PROFILES_DIR, 'google.gemini');
475
+ const antiProfiles = path.join(AUTH_PROFILES_DIR, 'google.antigravity');
476
+
477
+ if (fs.existsSync(geminiProfiles) && !studio.availableGooglePlugins.includes('gemini')) {
478
+ studio.availableGooglePlugins.push('gemini');
479
+ studioChanged = true;
480
+ }
481
+ if (fs.existsSync(antiProfiles) && !studio.availableGooglePlugins.includes('antigravity')) {
482
+ studio.availableGooglePlugins.push('antigravity');
483
+ studioChanged = true;
484
+ }
485
+
486
+ if (studioChanged) saveStudioConfig(studio);
487
+
583
488
  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
- });
593
- }
489
+ const saved = listAuthProfiles(p.id, activePlugin);
490
+ const curr = !!authCfg[p.id];
491
+ credentials.push({ ...p, active: ac[p.id] || (curr ? 'current' : null), profiles: saved, hasCurrentAuth: curr });
594
492
  });
595
-
596
- res.json({
597
- ...authConfig,
598
- credentials,
599
- authFile: path.join(path.dirname(getConfigPath() || ''), 'auth.json'),
600
- hasGeminiAuthPlugin: true
493
+ res.json({
494
+ ...authCfg,
495
+ credentials,
496
+ installedGooglePlugins: studio.availableGooglePlugins || [],
497
+ activeGooglePlugin: activePlugin
601
498
  });
602
499
  });
603
500
 
604
- app.post('/api/auth/login', (req, res) => {
605
- const { provider } = req.body;
606
- const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
501
+ app.get('/api/auth/profiles', (req, res) => {
502
+ const authCfg = loadAuthConfig() || {};
503
+ const studio = loadStudioConfig();
504
+ const ac = studio.activeProfiles || {};
505
+ const activePlugin = studio.activeGooglePlugin;
506
+ const profiles = {};
507
+ const providers = ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'];
607
508
 
608
- const child = spawn(cmd, ['auth', 'login', provider], {
609
- stdio: 'inherit',
610
- shell: true
509
+ providers.forEach(p => {
510
+ const saved = listAuthProfiles(p, activePlugin);
511
+ const curr = authCfg[p];
512
+ if (saved.length > 0 || curr) {
513
+ profiles[p] = { active: ac[p], profiles: saved, hasCurrentAuth: !!curr };
514
+ }
611
515
  });
612
-
613
- res.json({ success: true, message: 'Launched auth flow', note: 'Please check the terminal window where the server is running' });
516
+ res.json(profiles);
614
517
  });
615
518
 
616
- app.delete('/api/auth/:provider', (req, res) => {
519
+ app.post('/api/auth/profiles/:provider', (req, res) => {
617
520
  const { provider } = req.params;
618
- const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
521
+ const { name } = req.body;
522
+ const activePlugin = getActiveGooglePlugin();
523
+ const namespace = provider === 'google'
524
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
525
+ : provider;
619
526
 
620
- spawn(cmd, ['auth', 'logout', provider], {
621
- stdio: 'inherit',
622
- shell: true
623
- });
527
+ const auth = loadAuthConfig() || {};
528
+ const dir = path.join(AUTH_PROFILES_DIR, namespace);
529
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
624
530
 
625
- res.json({ success: true });
531
+ const profilePath = path.join(dir, `${name || Date.now()}.json`);
532
+ fs.writeFileSync(profilePath, JSON.stringify(auth[provider], null, 2), 'utf8');
533
+ res.json({ success: true, name: path.basename(profilePath, '.json') });
626
534
  });
627
535
 
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);
643
- });
644
-
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 [];
536
+ app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
537
+ const { provider, name } = req.params;
538
+ const activePlugin = getActiveGooglePlugin();
539
+ const namespace = provider === 'google'
540
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
541
+ : provider;
660
542
 
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++;
675
- }
676
- return `account-${num}`;
677
- }
678
-
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
- }
689
-
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
- }
699
-
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;
543
+ const profilePath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
544
+ if (!fs.existsSync(profilePath)) return res.status(404).json({ error: 'Profile not found' });
545
+
546
+ const profileData = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
547
+ const studio = loadStudioConfig();
548
+ if (!studio.activeProfiles) studio.activeProfiles = {};
549
+ studio.activeProfiles[provider] = name;
550
+ saveStudioConfig(studio);
551
+
552
+ const authCfg = loadAuthConfig() || {};
553
+ authCfg[provider] = profileData;
554
+
555
+ // Save both to persistent namespaced key and the shared 'google' key
556
+ if (provider === 'google') {
557
+ const key = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
558
+ authCfg[key] = profileData;
705
559
  }
706
- return false;
707
- }
708
-
709
- function getActiveProfiles() {
710
- const studioConfig = loadStudioConfig();
711
- return studioConfig.activeProfiles || {};
712
- }
560
+
561
+ const cp = getConfigPath();
562
+ const ap = path.join(path.dirname(cp), 'auth.json');
563
+ fs.writeFileSync(ap, JSON.stringify(authCfg, null, 2), 'utf8');
564
+ res.json({ success: true });
565
+ });
713
566
 
714
- function setActiveProfile(provider, profileName) {
715
- const studioConfig = loadStudioConfig();
716
- studioConfig.activeProfiles = studioConfig.activeProfiles || {};
717
- studioConfig.activeProfiles[provider] = profileName;
718
- saveStudioConfig(studioConfig);
719
- }
567
+ app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
568
+ const { provider, name } = req.params;
569
+ const activePlugin = getActiveGooglePlugin();
570
+ const namespace = provider === 'google'
571
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
572
+ : provider;
573
+
574
+ const profilePath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
575
+ if (fs.existsSync(profilePath)) fs.unlinkSync(profilePath);
576
+ res.json({ success: true });
577
+ });
720
578
 
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); }
579
+ app.put('/api/auth/profiles/:provider/:name', (req, res) => {
580
+ const { provider, name } = req.params;
581
+ const { newName } = req.body;
582
+ const activePlugin = getActiveGooglePlugin();
583
+ const namespace = provider === 'google'
584
+ ? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
585
+ : provider;
586
+
587
+ const oldPath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
588
+ const newPath = path.join(AUTH_PROFILES_DIR, namespace, `${newName}.json`);
589
+ if (fs.existsSync(oldPath)) fs.renameSync(oldPath, newPath);
590
+ res.json({ success: true, name: newName });
591
+ });
722
592
 
723
- app.get('/api/auth/profiles', (req, res) => {
724
- ensureAuthProfilesDir();
725
- const activeProfiles = getActiveProfiles();
726
- const authConfig = loadAuthConfig() || {};
593
+ app.post('/api/auth/login', (req, res) => {
594
+ let { provider } = req.body;
595
+ if (typeof provider !== 'string') provider = "";
727
596
 
728
- const profiles = {};
597
+ let cmd = 'opencode auth login';
598
+ if (provider) cmd += ` ${provider}`;
599
+
600
+ const platform = process.platform;
601
+ let terminalCmd;
602
+ if (platform === 'win32') {
603
+ terminalCmd = `start "" cmd /c "call ${cmd} || pause"`;
604
+ } else if (platform === 'darwin') {
605
+ terminalCmd = `osascript -e 'tell application "Terminal" to do script "${cmd}"'`;
606
+ } else {
607
+ terminalCmd = `x-terminal-emulator -e "${cmd}"`;
608
+ }
729
609
 
730
- const savedProviders = fs.existsSync(AUTH_PROFILES_DIR) ? fs.readdirSync(AUTH_PROFILES_DIR) : [];
610
+ console.log('Executing terminal command:', terminalCmd);
731
611
 
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
- };
612
+ exec(terminalCmd, (err) => {
613
+ if (err) {
614
+ console.error('Failed to open terminal:', err);
615
+ return res.status(500).json({ error: 'Failed to open terminal', details: err.message });
753
616
  }
617
+ res.json({ success: true, message: 'Terminal opened', note: 'Complete login in the terminal window' });
754
618
  });
755
-
756
- res.json(profiles);
757
619
  });
758
620
 
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
- });
621
+ app.delete('/api/auth/:provider', (req, res) => {
622
+ const { provider } = req.params;
623
+ const authCfg = loadAuthConfig() || {};
624
+ delete authCfg[provider];
625
+ const cp = getConfigPath();
626
+ const ap = path.join(path.dirname(cp), 'auth.json');
627
+ fs.writeFileSync(ap, JSON.stringify(authCfg, null, 2), 'utf8');
628
+
629
+ const studio = loadStudioConfig();
630
+ if (studio.activeProfiles) delete studio.activeProfiles[provider];
631
+ saveStudioConfig(studio);
632
+
633
+ res.json({ success: true });
779
634
  });
780
635
 
781
636
  app.get('/api/usage', async (req, res) => {
782
637
  try {
783
- const { projectId: filterProjectId, granularity = 'daily', range = '30d' } = req.query;
638
+ const {projectId: fid, granularity = 'daily', range = '30d'} = req.query;
639
+ const cp = getConfigPath();
640
+ if (!cp) return res.json({ totalCost: 0, totalTokens: 0, byModel: [], byDay: [], byProject: [] });
641
+
784
642
  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
- };
643
+ const dataCandidates = [
644
+ path.dirname(cp),
645
+ path.join(home, '.local', 'share', 'opencode'),
646
+ path.join(home, '.opencode'),
647
+ process.env.APPDATA ? path.join(process.env.APPDATA, 'opencode') : null,
648
+ path.join(home, 'AppData', 'Local', 'opencode')
649
+ ].filter(Boolean);
650
+
651
+ let md = null;
652
+ let sd = null;
653
+
654
+ for (const d of dataCandidates) {
655
+ const mdp = path.join(d, 'storage', 'message');
656
+ const sdp = path.join(d, 'storage', 'session');
657
+ if (fs.existsSync(mdp)) {
658
+ md = mdp;
659
+ sd = sdp;
660
+ break;
661
+ }
662
+ }
794
663
 
795
- const sessionProjectMap = new Map();
664
+ if (!md) return res.json({ totalCost: 0, totalTokens: 0, byModel: [], byDay: [], byProject: [] });
796
665
 
797
- const loadProjects = (messageDir) => {
798
- const sessionDir = getSessionDir(messageDir);
799
- if (!fs.existsSync(sessionDir)) return;
800
666
 
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', '');
667
+ const pmap = new Map();
668
+ if (fs.existsSync(sd)) {
669
+ fs.readdirSync(sd).forEach(d => {
670
+ const fp = path.join(sd, d);
671
+ if (fs.statSync(fp).isDirectory()) {
672
+ fs.readdirSync(fp).forEach(f => {
673
+ if (f.startsWith('ses_') && f.endsWith('.json')) {
812
674
  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) {}
675
+ const m = JSON.parse(fs.readFileSync(path.join(fp, f), 'utf8'));
676
+ pmap.set(f.replace('.json', ''), { name: m.directory ? path.basename(m.directory) : (m.projectID ? m.projectID.substring(0, 8) : 'Unknown'), id: m.projectID || d });
677
+ } catch {}
822
678
  }
823
- }
679
+ });
824
680
  }
825
- } catch (e) {
826
- console.error('Error loading projects:', e);
827
- }
828
- };
829
-
830
- for (const logDir of candidatePaths) {
831
- loadProjects(logDir);
681
+ });
832
682
  }
833
683
 
834
- const stats = {
835
- totalCost: 0,
836
- totalTokens: 0,
837
- byModel: {},
838
- byTime: {},
839
- byProject: {}
840
- };
841
-
842
- const processedFiles = new Set();
684
+ const stats = { totalCost: 0, totalTokens: 0, byModel: {}, byTime: {}, byProject: {} };
685
+ const seen = new Set();
843
686
  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 };
687
+ let min = 0;
688
+ if (range === '24h') min = now - 86400000;
689
+ else if (range === '7d') min = now - 604800000;
690
+ else if (range === '30d') min = now - 2592000000;
691
+ else if (range === '1y') min = now - 31536000000;
692
+
693
+ fs.readdirSync(md).forEach(s => {
694
+ if (!s.startsWith('ses_')) return;
695
+ const sp = path.join(md, s);
696
+ if (fs.statSync(sp).isDirectory()) {
697
+ fs.readdirSync(sp).forEach(f => {
698
+ if (!f.endsWith('.json') || seen.has(path.join(sp, f))) return;
699
+ seen.add(path.join(sp, f));
700
+ try {
701
+ const msg = JSON.parse(fs.readFileSync(path.join(sp, f), 'utf8'));
702
+ const pid = pmap.get(s)?.id || 'unknown';
703
+ if (fid && fid !== 'all' && pid !== fid) return;
704
+ if (min > 0 && msg.time.created < min) return;
705
+ if (msg.role === 'assistant' && msg.tokens) {
706
+ const c = msg.cost || 0, it = msg.tokens.input || 0, ot = msg.tokens.output || 0, t = it + ot;
707
+ const d = new Date(msg.time.created);
708
+ let tk;
709
+ if (granularity === 'hourly') tk = d.toISOString().substring(0, 13) + ':00:00Z';
710
+ else if (granularity === 'weekly') {
711
+ const day = d.getDay(), diff = d.getDate() - day + (day === 0 ? -6 : 1);
712
+ tk = new Date(d.setDate(diff)).toISOString().split('T')[0];
713
+ } else if (granularity === 'monthly') tk = d.toISOString().substring(0, 7) + '-01';
714
+ else tk = d.toISOString().split('T')[0];
715
+
716
+ const mid = msg.modelID || (msg.model && (msg.model.modelID || msg.model.id)) || 'unknown';
717
+ stats.totalCost += c; stats.totalTokens += t;
718
+ [stats.byModel, stats.byTime, stats.byProject].forEach((obj, i) => {
719
+ const key = i === 0 ? mid : (i === 1 ? tk : pid);
720
+ if (!obj[key]) obj[key] = { name: key, id: key, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
721
+ if (i === 2) obj[key].name = pmap.get(s)?.name || 'Unassigned';
722
+ obj[key].cost += c; obj[key].tokens += t; obj[key].inputTokens += it; obj[key].outputTokens += ot;
723
+ });
916
724
  }
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
- }
940
- }
941
- }
942
- }
943
- } catch (err) {
944
- console.error(`Error reading log dir ${logDir}:`, err);
725
+ } catch {}
726
+ });
945
727
  }
946
- }
728
+ });
947
729
 
948
- const response = {
730
+ res.json({
949
731
  totalCost: stats.totalCost,
950
732
  totalTokens: stats.totalTokens,
951
733
  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)),
734
+ byDay: Object.values(stats.byTime).sort((a, b) => a.name.localeCompare(b.name)).map(v => ({ ...v, date: v.name })),
953
735
  byProject: Object.values(stats.byProject).sort((a, b) => b.cost - a.cost)
954
- };
955
-
956
- res.json(response);
736
+ });
957
737
  } catch (error) {
958
- console.error('Error fetching usage stats:', error);
959
- res.status(500).json({ error: 'Failed to fetch usage stats' });
738
+ res.status(500).json({ error: 'Failed' });
960
739
  }
961
740
  });
962
741
 
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];
742
+ app.post('/api/auth/google/plugin', (req, res) => {
743
+ const { plugin } = req.body;
744
+ const studio = loadStudioConfig();
745
+ studio.activeGooglePlugin = plugin;
746
+ saveStudioConfig(studio);
987
747
 
988
748
  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
- });
749
+ const opencode = loadConfig();
750
+ if (opencode) {
751
+ if (opencode.provider?.google) {
752
+ const models = studio.pluginModels[plugin];
753
+ if (models) {
754
+ opencode.provider.google.models = models;
755
+ }
756
+ }
996
757
 
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 });
758
+ if (!opencode.plugin) opencode.plugin = [];
759
+ const geminiPlugin = 'opencode-gemini-auth@latest';
760
+ const antigravityPlugin = 'opencode-google-antigravity-auth';
761
+
762
+ opencode.plugin = opencode.plugin.filter(p =>
763
+ !p.includes('gemini-auth') &&
764
+ !p.includes('antigravity-auth')
765
+ );
766
+
767
+ if (plugin === 'gemini') {
768
+ opencode.plugin.push(geminiPlugin);
769
+ } else if (plugin === 'antigravity') {
770
+ opencode.plugin.push(antigravityPlugin);
771
+ }
772
+
773
+ saveConfig(opencode);
1017
774
  }
1018
- } else {
1019
- res.status(500).json({ error: 'Config path not found' });
1020
- }
1021
- });
1022
775
 
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);
776
+ const cp = getConfigPath();
777
+ if (cp) {
778
+ const ap = path.join(path.dirname(cp), 'auth.json');
779
+ if (fs.existsSync(ap)) {
780
+ const authCfg = JSON.parse(fs.readFileSync(ap, 'utf8'));
781
+ if (plugin === 'antigravity' && authCfg['google.antigravity']) {
782
+ authCfg.google = { ...authCfg['google.antigravity'] };
783
+ } else if (plugin === 'gemini' && authCfg['google.gemini']) {
784
+ authCfg.google = { ...authCfg['google.gemini'] };
785
+ }
786
+ fs.writeFileSync(ap, JSON.stringify(authCfg, null, 2), 'utf8');
1033
787
  }
1034
788
  }
1035
- res.json({ success: true });
1036
- } else {
1037
- res.status(404).json({ error: 'Profile not found' });
789
+ } catch (err) {
790
+ console.error(err);
1038
791
  }
792
+
793
+ res.json({ success: true, activePlugin: plugin });
1039
794
  });
1040
795
 
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
- }
796
+ app.get('/api/auth/google/plugin', (req, res) => {
797
+ const studio = loadStudioConfig();
798
+ res.json({ activePlugin: studio.activeGooglePlugin || null });
1063
799
  });
1064
800
 
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' });
801
+ app.get('/api/pending-action', (req, res) => {
802
+ if (pendingActionMemory) return res.json({ action: pendingActionMemory });
803
+ if (fs.existsSync(PENDING_ACTION_PATH)) {
804
+ try {
805
+ const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
806
+ return res.json({ action });
807
+ } catch {}
1069
808
  }
809
+ res.json({ action: null });
810
+ });
1070
811
 
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
- }
812
+ app.delete('/api/pending-action', (req, res) => {
813
+ pendingActionMemory = null;
814
+ if (fs.existsSync(PENDING_ACTION_PATH)) fs.unlinkSync(PENDING_ACTION_PATH);
815
+ res.json({ success: true });
816
+ });
1091
817
 
1092
- if (config.plugins[pluginName]) {
1093
- result.skipped.push(`${pluginName} (already configured)`);
818
+ app.post('/api/plugins/config/add', (req, res) => {
819
+ const { plugins } = req.body;
820
+ const opencode = loadConfig();
821
+ if (!opencode) return res.status(404).json({ error: 'Config not found' });
822
+
823
+ if (!opencode.plugin) opencode.plugin = [];
824
+ const added = [];
825
+ const skipped = [];
826
+
827
+ plugins.forEach(p => {
828
+ if (!opencode.plugin.includes(p)) {
829
+ opencode.plugin.push(p);
830
+ added.push(p);
831
+
832
+ const studio = loadStudioConfig();
833
+ if (p.includes('gemini-auth') && !studio.availableGooglePlugins.includes('gemini')) {
834
+ studio.availableGooglePlugins.push('gemini');
835
+ saveStudioConfig(studio);
836
+ }
837
+ if (p.includes('antigravity-auth') && !studio.availableGooglePlugins.includes('antigravity')) {
838
+ studio.availableGooglePlugins.push('antigravity');
839
+ saveStudioConfig(studio);
840
+ }
1094
841
  } else {
1095
- config.plugins[pluginName] = { enabled: true };
1096
- result.added.push(pluginName);
842
+ skipped.push(p);
1097
843
  }
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();
844
+ });
1110
845
 
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
- }
846
+ saveConfig(opencode);
847
+ res.json({ added, skipped });
1122
848
  });
1123
849
 
1124
- app.listen(PORT, () => {
1125
- console.log(`Server running at http://localhost:${PORT}`);
1126
- });
850
+ app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));