opencode-studio-server 1.1.2 → 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.
- package/index.js +588 -820
- package/package.json +1 -1
- 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 {
|
|
7
|
+
const { spawn, exec } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const app = express();
|
|
10
10
|
const PORT = 3001;
|
|
11
|
-
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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,53 +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
|
-
|
|
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
|
-
];
|
|
128
|
-
} else {
|
|
129
|
-
candidates = [
|
|
130
|
-
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
131
|
-
path.join(home, '.opencode', 'opencode.json'),
|
|
132
|
-
];
|
|
198
|
+
candidates.push(path.join(process.env.APPDATA, 'opencode', 'opencode.json'));
|
|
133
199
|
}
|
|
134
200
|
|
|
135
201
|
const studioConfig = loadStudioConfig();
|
|
@@ -147,20 +213,15 @@ const getPaths = () => {
|
|
|
147
213
|
detected,
|
|
148
214
|
manual: manualPath,
|
|
149
215
|
current: manualPath || detected,
|
|
150
|
-
candidates
|
|
216
|
+
candidates: [...new Set(candidates)]
|
|
151
217
|
};
|
|
152
218
|
};
|
|
153
219
|
|
|
154
|
-
const getConfigPath = () =>
|
|
155
|
-
const paths = getPaths();
|
|
156
|
-
return paths.current;
|
|
157
|
-
};
|
|
220
|
+
const getConfigPath = () => getPaths().current;
|
|
158
221
|
|
|
159
222
|
const loadConfig = () => {
|
|
160
223
|
const configPath = getConfigPath();
|
|
161
|
-
if (!configPath || !fs.existsSync(configPath))
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
224
|
+
if (!configPath || !fs.existsSync(configPath)) return null;
|
|
164
225
|
try {
|
|
165
226
|
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
166
227
|
} catch {
|
|
@@ -170,28 +231,20 @@ const loadConfig = () => {
|
|
|
170
231
|
|
|
171
232
|
const saveConfig = (config) => {
|
|
172
233
|
const configPath = getConfigPath();
|
|
173
|
-
if (!configPath)
|
|
174
|
-
throw new Error('No config path found');
|
|
175
|
-
}
|
|
234
|
+
if (!configPath) throw new Error('No config path found');
|
|
176
235
|
const dir = path.dirname(configPath);
|
|
177
|
-
if (!fs.existsSync(dir)) {
|
|
178
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
179
|
-
}
|
|
236
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
180
237
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
181
238
|
};
|
|
182
239
|
|
|
183
|
-
app.get('/api/health', (req, res) => {
|
|
184
|
-
res.json({ status: 'ok' });
|
|
185
|
-
});
|
|
240
|
+
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
|
|
186
241
|
|
|
187
242
|
app.post('/api/shutdown', (req, res) => {
|
|
188
243
|
res.json({ success: true });
|
|
189
244
|
setTimeout(() => process.exit(0), 100);
|
|
190
245
|
});
|
|
191
246
|
|
|
192
|
-
app.get('/api/paths', (req, res) =>
|
|
193
|
-
res.json(getPaths());
|
|
194
|
-
});
|
|
247
|
+
app.get('/api/paths', (req, res) => res.json(getPaths()));
|
|
195
248
|
|
|
196
249
|
app.post('/api/paths', (req, res) => {
|
|
197
250
|
const { configPath } = req.body;
|
|
@@ -203,880 +256,595 @@ app.post('/api/paths', (req, res) => {
|
|
|
203
256
|
|
|
204
257
|
app.get('/api/config', (req, res) => {
|
|
205
258
|
const config = loadConfig();
|
|
206
|
-
if (!config) {
|
|
207
|
-
return res.status(404).json({ error: 'Config not found' });
|
|
208
|
-
}
|
|
259
|
+
if (!config) return res.status(404).json({ error: 'Config not found' });
|
|
209
260
|
res.json(config);
|
|
210
261
|
});
|
|
211
262
|
|
|
212
263
|
app.post('/api/config', (req, res) => {
|
|
213
|
-
const config = req.body;
|
|
214
264
|
try {
|
|
215
|
-
saveConfig(
|
|
265
|
+
saveConfig(req.body);
|
|
216
266
|
res.json({ success: true });
|
|
217
267
|
} catch (err) {
|
|
218
268
|
res.status(500).json({ error: err.message });
|
|
219
269
|
}
|
|
220
270
|
});
|
|
221
271
|
|
|
222
|
-
// Helper to get skill dir
|
|
223
272
|
const getSkillDir = () => {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
return path.join(path.dirname(configPath), 'skill');
|
|
273
|
+
const cp = getConfigPath();
|
|
274
|
+
return cp ? path.join(path.dirname(cp), 'skill') : null;
|
|
227
275
|
};
|
|
228
276
|
|
|
229
277
|
app.get('/api/skills', (req, res) => {
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const skills = [];
|
|
236
|
-
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
|
237
|
-
|
|
238
|
-
for (const entry of entries) {
|
|
239
|
-
if (entry.isDirectory()) {
|
|
240
|
-
const skillPath = path.join(skillDir, entry.name, 'SKILL.md');
|
|
241
|
-
if (fs.existsSync(skillPath)) {
|
|
242
|
-
skills.push({
|
|
243
|
-
name: entry.name,
|
|
244
|
-
path: skillPath,
|
|
245
|
-
enabled: !entry.name.endsWith('.disabled') // Simplified logic
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
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') }));
|
|
250
283
|
res.json(skills);
|
|
251
284
|
});
|
|
252
285
|
|
|
253
286
|
app.get('/api/skills/:name', (req, res) => {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const skillPath = path.join(skillDir, name, 'SKILL.md');
|
|
259
|
-
|
|
260
|
-
if (!fs.existsSync(skillPath)) {
|
|
261
|
-
return res.status(404).json({ error: 'Skill not found' });
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const content = fs.readFileSync(skillPath, 'utf8');
|
|
265
|
-
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') });
|
|
266
291
|
});
|
|
267
292
|
|
|
268
293
|
app.post('/api/skills/:name', (req, res) => {
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const dirPath = path.join(skillDir, name);
|
|
275
|
-
|
|
276
|
-
if (!fs.existsSync(dirPath)) {
|
|
277
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
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');
|
|
281
299
|
res.json({ success: true });
|
|
282
300
|
});
|
|
283
301
|
|
|
284
302
|
app.delete('/api/skills/:name', (req, res) => {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const name = req.params.name;
|
|
289
|
-
const dirPath = path.join(skillDir, name);
|
|
290
|
-
|
|
291
|
-
if (fs.existsSync(dirPath)) {
|
|
292
|
-
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
293
|
-
}
|
|
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 });
|
|
294
306
|
res.json({ success: true });
|
|
295
307
|
});
|
|
296
308
|
|
|
297
|
-
app.post('/api/skills/:name/toggle', (req, res) => {
|
|
298
|
-
// Simplified: renaming not implemented for now to avoid complexity
|
|
299
|
-
// In real implementation we would rename folder to .disabled
|
|
300
|
-
res.json({ success: true, enabled: true });
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Helper to get plugin dir
|
|
304
309
|
const getPluginDir = () => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
return path.join(path.dirname(configPath), 'plugin');
|
|
310
|
+
const cp = getConfigPath();
|
|
311
|
+
return cp ? path.join(path.dirname(cp), 'plugin') : null;
|
|
308
312
|
};
|
|
309
313
|
|
|
310
314
|
app.get('/api/plugins', (req, res) => {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
315
|
+
const pd = getPluginDir();
|
|
316
|
+
const cp = getConfigPath();
|
|
317
|
+
const cr = cp ? path.dirname(cp) : null;
|
|
316
318
|
const plugins = [];
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
319
|
+
const add = (name, p, enabled = true) => {
|
|
320
|
+
if (!plugins.some(pl => pl.name === name)) plugins.push({ name, path: p, enabled });
|
|
321
|
+
};
|
|
322
|
+
|
|
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);
|
|
330
332
|
}
|
|
331
|
-
}
|
|
332
|
-
plugins.push({
|
|
333
|
-
name: entry.name.replace(/\.(js|ts)$/, ''),
|
|
334
|
-
path: path.join(pluginDir, entry.name),
|
|
335
|
-
enabled: true
|
|
336
|
-
});
|
|
337
|
-
}
|
|
333
|
+
});
|
|
338
334
|
}
|
|
339
|
-
res.json(plugins);
|
|
340
|
-
});
|
|
341
335
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const dirPath = path.join(pluginDir, name);
|
|
348
|
-
|
|
349
|
-
let content = '';
|
|
350
|
-
let filename = '';
|
|
351
|
-
|
|
352
|
-
if (fs.existsSync(path.join(dirPath, 'index.js'))) {
|
|
353
|
-
content = fs.readFileSync(path.join(dirPath, 'index.js'), 'utf8');
|
|
354
|
-
filename = 'index.js';
|
|
355
|
-
} else if (fs.existsSync(path.join(dirPath, 'index.ts'))) {
|
|
356
|
-
content = fs.readFileSync(path.join(dirPath, 'index.ts'), 'utf8');
|
|
357
|
-
filename = 'index.ts';
|
|
358
|
-
} else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
|
|
359
|
-
content = fs.readFileSync(path.join(pluginDir, name + '.js'), 'utf8');
|
|
360
|
-
filename = name + '.js';
|
|
361
|
-
} else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
|
|
362
|
-
content = fs.readFileSync(path.join(pluginDir, name + '.ts'), 'utf8');
|
|
363
|
-
filename = name + '.ts';
|
|
364
|
-
} else {
|
|
365
|
-
return res.status(404).json({ error: 'Plugin not found' });
|
|
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
|
+
});
|
|
366
341
|
}
|
|
367
|
-
|
|
368
|
-
res.json({ name, content, filename });
|
|
369
|
-
});
|
|
370
342
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const { content } = req.body;
|
|
377
|
-
const dirPath = path.join(pluginDir, name);
|
|
378
|
-
|
|
379
|
-
if (!fs.existsSync(dirPath)) {
|
|
380
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
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
|
+
});
|
|
381
348
|
}
|
|
382
|
-
|
|
383
|
-
// Determine file extension based on content? Default to .js if new
|
|
384
|
-
const filePath = path.join(dirPath, 'index.js');
|
|
385
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
386
|
-
res.json({ success: true });
|
|
349
|
+
res.json(plugins);
|
|
387
350
|
});
|
|
388
351
|
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
352
|
+
const getActiveGooglePlugin = () => {
|
|
353
|
+
const studio = loadStudioConfig();
|
|
354
|
+
return studio.activeGooglePlugin || null;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
function loadAuthConfig() {
|
|
358
|
+
const paths = getPaths();
|
|
359
|
+
const allAuthConfigs = [];
|
|
395
360
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
fs.
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
});
|
|
370
|
+
|
|
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
|
+
}
|
|
402
379
|
}
|
|
403
|
-
res.json({ success: true });
|
|
404
|
-
});
|
|
405
380
|
|
|
406
|
-
|
|
407
|
-
res.json({ success: true, enabled: true });
|
|
408
|
-
});
|
|
381
|
+
if (allAuthConfigs.length === 0) return null;
|
|
409
382
|
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
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);
|
|
413
385
|
|
|
414
|
-
try {
|
|
415
|
-
const
|
|
416
|
-
const response = await fetch(url);
|
|
417
|
-
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
418
|
-
const content = await response.text();
|
|
419
|
-
const filename = path.basename(new URL(url).pathname) || 'file.txt';
|
|
386
|
+
try {
|
|
387
|
+
const activePlugin = getActiveGooglePlugin();
|
|
420
388
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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 };
|
|
398
|
+
}
|
|
426
399
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const results = [];
|
|
433
|
-
|
|
434
|
-
// Limit concurrency? For now sequential is safer
|
|
435
|
-
for (const url of urls) {
|
|
436
|
-
try {
|
|
437
|
-
const response = await fetch(url);
|
|
438
|
-
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
439
|
-
const content = await response.text();
|
|
440
|
-
const filename = path.basename(new URL(url).pathname) || 'file.txt';
|
|
441
|
-
|
|
442
|
-
// Try to extract name/description from content (simple regex for markdown/js)
|
|
443
|
-
// This is basic heuristic
|
|
444
|
-
results.push({
|
|
445
|
-
url,
|
|
446
|
-
success: true,
|
|
447
|
-
content,
|
|
448
|
-
filename,
|
|
449
|
-
name: filename.replace(/\.(md|js|ts)$/, ''),
|
|
450
|
-
});
|
|
451
|
-
} catch (err) {
|
|
452
|
-
results.push({
|
|
453
|
-
url,
|
|
454
|
-
success: false,
|
|
455
|
-
error: err.message
|
|
456
|
-
});
|
|
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'] };
|
|
457
405
|
}
|
|
406
|
+
|
|
407
|
+
return cfg;
|
|
408
|
+
} catch { return cfg; }
|
|
409
|
+
}
|
|
410
|
+
|
|
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';
|
|
458
416
|
}
|
|
459
417
|
|
|
460
|
-
|
|
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
|
+
};
|
|
422
|
+
|
|
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);
|
|
461
433
|
});
|
|
462
434
|
|
|
463
|
-
app.get('/api/
|
|
464
|
-
const
|
|
465
|
-
const
|
|
435
|
+
app.get('/api/auth', (req, res) => {
|
|
436
|
+
const authCfg = loadAuthConfig() || {};
|
|
437
|
+
const studio = loadStudioConfig();
|
|
438
|
+
const ac = studio.activeProfiles || {};
|
|
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!');
|
|
466
449
|
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
}
|
|
450
|
+
const providers = [
|
|
451
|
+
{ id: 'google', name: 'Google', type: 'oauth' },
|
|
452
|
+
{ id: 'anthropic', name: 'Anthropic', type: 'api' },
|
|
453
|
+
{ id: 'openai', name: 'OpenAI', type: 'api' },
|
|
454
|
+
{ id: 'xai', name: 'xAI', type: 'api' },
|
|
455
|
+
{ id: 'openrouter', name: 'OpenRouter', type: 'api' },
|
|
456
|
+
{ id: 'github-copilot', name: 'GitHub Copilot', type: 'api' }
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
const opencodeCfg = loadConfig();
|
|
460
|
+
const currentPlugins = opencodeCfg?.plugin || [];
|
|
480
461
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (pluginDir && fs.existsSync(pluginDir)) {
|
|
484
|
-
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
|
|
485
|
-
for (const entry of entries) {
|
|
486
|
-
if (entry.isDirectory()) {
|
|
487
|
-
const p = path.join(pluginDir, entry.name, 'index.js');
|
|
488
|
-
if (fs.existsSync(p)) {
|
|
489
|
-
plugins.push({ name: entry.name, content: fs.readFileSync(p, 'utf8') });
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
462
|
+
let studioChanged = false;
|
|
463
|
+
if (!studio.availableGooglePlugins) studio.availableGooglePlugins = [];
|
|
494
464
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
});
|
|
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
|
+
}
|
|
504
473
|
|
|
505
|
-
|
|
506
|
-
const
|
|
474
|
+
const geminiProfiles = path.join(AUTH_PROFILES_DIR, 'google.gemini');
|
|
475
|
+
const antiProfiles = path.join(AUTH_PROFILES_DIR, 'google.antigravity');
|
|
507
476
|
|
|
508
|
-
if (
|
|
509
|
-
|
|
477
|
+
if (fs.existsSync(geminiProfiles) && !studio.availableGooglePlugins.includes('gemini')) {
|
|
478
|
+
studio.availableGooglePlugins.push('gemini');
|
|
479
|
+
studioChanged = true;
|
|
510
480
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
481
|
+
if (fs.existsSync(antiProfiles) && !studio.availableGooglePlugins.includes('antigravity')) {
|
|
482
|
+
studio.availableGooglePlugins.push('antigravity');
|
|
483
|
+
studioChanged = true;
|
|
514
484
|
}
|
|
515
485
|
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
486
|
+
if (studioChanged) saveStudioConfig(studio);
|
|
487
|
+
|
|
488
|
+
providers.forEach(p => {
|
|
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 });
|
|
492
|
+
});
|
|
493
|
+
res.json({
|
|
494
|
+
...authCfg,
|
|
495
|
+
credentials,
|
|
496
|
+
installedGooglePlugins: studio.availableGooglePlugins || [],
|
|
497
|
+
activeGooglePlugin: activePlugin
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
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'];
|
|
527
508
|
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const dir = path.join(pluginDir, plugin.name);
|
|
534
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
535
|
-
fs.writeFileSync(path.join(dir, 'index.js'), plugin.content, 'utf8');
|
|
536
|
-
}
|
|
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 };
|
|
537
514
|
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
res.json({ success: true });
|
|
515
|
+
});
|
|
516
|
+
res.json(profiles);
|
|
541
517
|
});
|
|
542
518
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
519
|
+
app.post('/api/auth/profiles/:provider', (req, res) => {
|
|
520
|
+
const { provider } = req.params;
|
|
521
|
+
const { name } = req.body;
|
|
522
|
+
const activePlugin = getActiveGooglePlugin();
|
|
523
|
+
const namespace = provider === 'google'
|
|
524
|
+
? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
|
|
525
|
+
: provider;
|
|
547
526
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// But for this studio, we might just look at opencode.json "providers" section or similar.
|
|
527
|
+
const auth = loadAuthConfig() || {};
|
|
528
|
+
const dir = path.join(AUTH_PROFILES_DIR, namespace);
|
|
529
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
552
530
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
return JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
558
|
-
} catch {
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
app.get('/api/auth', (req, res) => {
|
|
564
|
-
const authConfig = loadAuthConfig();
|
|
565
|
-
res.json(authConfig || {});
|
|
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') });
|
|
566
534
|
});
|
|
567
535
|
|
|
568
|
-
app.post('/api/auth/
|
|
569
|
-
const { provider } = req.
|
|
570
|
-
|
|
571
|
-
|
|
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;
|
|
572
542
|
|
|
573
|
-
const
|
|
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' });
|
|
574
545
|
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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);
|
|
580
551
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
app.delete('/api/auth/:provider', (req, res) => {
|
|
585
|
-
// Logout logic
|
|
586
|
-
// Maybe just delete from auth.json? Or run opencode auth logout?
|
|
587
|
-
const { provider } = req.params;
|
|
588
|
-
const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
|
|
552
|
+
const authCfg = loadAuthConfig() || {};
|
|
553
|
+
authCfg[provider] = profileData;
|
|
589
554
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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;
|
|
559
|
+
}
|
|
594
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');
|
|
595
564
|
res.json({ success: true });
|
|
596
565
|
});
|
|
597
566
|
|
|
598
|
-
app.
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
{ id: 'deepseek', name: 'DeepSeek', type: 'api', description: 'DeepSeek models' },
|
|
609
|
-
{ id: 'openrouter', name: 'OpenRouter', type: 'api', description: 'Multiple providers' },
|
|
610
|
-
{ id: 'amazon-bedrock', name: 'Amazon Bedrock', type: 'api', description: 'AWS models' },
|
|
611
|
-
{ id: 'azure', name: 'Azure OpenAI', type: 'api', description: 'Azure GPT models' },
|
|
612
|
-
];
|
|
613
|
-
res.json(providers);
|
|
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 });
|
|
614
577
|
});
|
|
615
578
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
function getProviderProfilesDir(provider) {
|
|
625
|
-
return path.join(AUTH_PROFILES_DIR, provider);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function listAuthProfiles(provider) {
|
|
629
|
-
const dir = getProviderProfilesDir(provider);
|
|
630
|
-
if (!fs.existsSync(dir)) return [];
|
|
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;
|
|
631
586
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
return [];
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function getNextProfileName(provider) {
|
|
642
|
-
const existing = listAuthProfiles(provider);
|
|
643
|
-
let num = 1;
|
|
644
|
-
while (existing.includes(`account-${num}`)) {
|
|
645
|
-
num++;
|
|
646
|
-
}
|
|
647
|
-
return `account-${num}`;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function saveAuthProfile(provider, profileName, data) {
|
|
651
|
-
ensureAuthProfilesDir();
|
|
652
|
-
const dir = getProviderProfilesDir(provider);
|
|
653
|
-
if (!fs.existsSync(dir)) {
|
|
654
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
655
|
-
}
|
|
656
|
-
const filePath = path.join(dir, `${profileName}.json`);
|
|
657
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function loadAuthProfile(provider, profileName) {
|
|
662
|
-
const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
|
|
663
|
-
if (!fs.existsSync(filePath)) return null;
|
|
664
|
-
try {
|
|
665
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
666
|
-
} catch {
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
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
|
+
});
|
|
670
592
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
593
|
+
app.post('/api/auth/login', (req, res) => {
|
|
594
|
+
let { provider } = req.body;
|
|
595
|
+
if (typeof provider !== 'string') provider = "";
|
|
596
|
+
|
|
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}"`;
|
|
676
608
|
}
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function getActiveProfiles() {
|
|
681
|
-
const studioConfig = loadStudioConfig();
|
|
682
|
-
return studioConfig.activeProfiles || {};
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function setActiveProfile(provider, profileName) {
|
|
686
|
-
const studioConfig = loadStudioConfig();
|
|
687
|
-
studioConfig.activeProfiles = studioConfig.activeProfiles || {};
|
|
688
|
-
studioConfig.activeProfiles[provider] = profileName;
|
|
689
|
-
saveStudioConfig(studioConfig);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
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); }
|
|
693
|
-
|
|
694
|
-
app.get('/api/auth/profiles', (req, res) => {
|
|
695
|
-
ensureAuthProfilesDir();
|
|
696
|
-
const activeProfiles = getActiveProfiles();
|
|
697
|
-
const authConfig = loadAuthConfig() || {};
|
|
698
609
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
providers.forEach(p => {
|
|
707
|
-
const saved = listAuthProfiles(p);
|
|
708
|
-
const active = activeProfiles[p];
|
|
709
|
-
const current = authConfig[p];
|
|
710
|
-
|
|
711
|
-
if (saved.length > 0 || current) {
|
|
712
|
-
profiles[p] = {
|
|
713
|
-
active: active,
|
|
714
|
-
saved: saved,
|
|
715
|
-
hasCurrent: !!current
|
|
716
|
-
};
|
|
610
|
+
console.log('Executing terminal command:', terminalCmd);
|
|
611
|
+
|
|
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 });
|
|
717
616
|
}
|
|
617
|
+
res.json({ success: true, message: 'Terminal opened', note: 'Complete login in the terminal window' });
|
|
718
618
|
});
|
|
719
|
-
|
|
720
|
-
res.json(profiles);
|
|
721
619
|
});
|
|
722
620
|
|
|
723
|
-
app.
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}));
|
|
737
|
-
|
|
738
|
-
res.json({
|
|
739
|
-
home,
|
|
740
|
-
platform: process.platform,
|
|
741
|
-
candidates: results
|
|
742
|
-
});
|
|
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 });
|
|
743
634
|
});
|
|
744
635
|
|
|
745
636
|
app.get('/api/usage', async (req, res) => {
|
|
746
637
|
try {
|
|
747
|
-
const {
|
|
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
|
+
|
|
748
642
|
const home = os.homedir();
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
path.join(home, '.local', 'share', 'opencode'
|
|
752
|
-
path.join(home, '.opencode',
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
if (
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
for (const projDir of projectDirs) {
|
|
769
|
-
const fullProjPath = path.join(sessionDir, projDir);
|
|
770
|
-
if (!fs.statSync(fullProjPath).isDirectory()) continue;
|
|
771
|
-
|
|
772
|
-
const files = fs.readdirSync(fullProjPath);
|
|
773
|
-
for (const file of files) {
|
|
774
|
-
if (file.startsWith('ses_') && file.endsWith('.json')) {
|
|
775
|
-
const sessionId = file.replace('.json', '');
|
|
776
|
-
try {
|
|
777
|
-
const meta = JSON.parse(fs.readFileSync(path.join(fullProjPath, file), 'utf8'));
|
|
778
|
-
let projectName = 'Unknown Project';
|
|
779
|
-
if (meta.directory) {
|
|
780
|
-
projectName = path.basename(meta.directory);
|
|
781
|
-
} else if (meta.projectID) {
|
|
782
|
-
projectName = meta.projectID.substring(0, 8);
|
|
783
|
-
}
|
|
784
|
-
sessionProjectMap.set(sessionId, { name: projectName, id: meta.projectID || projDir });
|
|
785
|
-
} catch (e) {}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
} catch (e) {
|
|
790
|
-
console.error('Error loading projects:', e);
|
|
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;
|
|
791
661
|
}
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
for (const logDir of candidatePaths) {
|
|
795
|
-
loadProjects(logDir);
|
|
796
662
|
}
|
|
797
663
|
|
|
798
|
-
|
|
799
|
-
totalCost: 0,
|
|
800
|
-
totalTokens: 0,
|
|
801
|
-
byModel: {},
|
|
802
|
-
byTime: {},
|
|
803
|
-
byProject: {}
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
const processedFiles = new Set();
|
|
807
|
-
|
|
808
|
-
const processMessage = (filePath, sessionId) => {
|
|
809
|
-
if (processedFiles.has(filePath)) return;
|
|
810
|
-
processedFiles.add(filePath);
|
|
664
|
+
if (!md) return res.json({ totalCost: 0, totalTokens: 0, byModel: [], byDay: [], byProject: [] });
|
|
811
665
|
|
|
812
|
-
try {
|
|
813
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
814
|
-
const msg = JSON.parse(content);
|
|
815
|
-
|
|
816
|
-
const model = msg.modelID || (msg.model && (msg.model.modelID || msg.model.id)) || 'unknown';
|
|
817
|
-
const projectInfo = sessionProjectMap.get(sessionId) || { name: 'Unassigned', id: 'unknown' };
|
|
818
|
-
const projectId = projectInfo.id || 'unknown';
|
|
819
|
-
|
|
820
|
-
if (filterProjectId && filterProjectId !== 'all' && projectId !== filterProjectId) {
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (msg.role === 'assistant' && msg.tokens) {
|
|
825
|
-
const cost = msg.cost || 0;
|
|
826
|
-
const inputTokens = msg.tokens.input || 0;
|
|
827
|
-
const outputTokens = msg.tokens.output || 0;
|
|
828
|
-
const tokens = inputTokens + outputTokens;
|
|
829
|
-
|
|
830
|
-
const timestamp = msg.time.created;
|
|
831
|
-
const dateObj = new Date(timestamp);
|
|
832
|
-
let timeKey;
|
|
833
|
-
|
|
834
|
-
if (granularity === 'hourly') {
|
|
835
|
-
timeKey = dateObj.toISOString().substring(0, 13) + ':00:00Z';
|
|
836
|
-
} else if (granularity === 'weekly') {
|
|
837
|
-
const day = dateObj.getDay();
|
|
838
|
-
const diff = dateObj.getDate() - day + (day === 0 ? -6 : 1);
|
|
839
|
-
const monday = new Date(dateObj.setDate(diff));
|
|
840
|
-
timeKey = monday.toISOString().split('T')[0];
|
|
841
|
-
} else {
|
|
842
|
-
timeKey = dateObj.toISOString().split('T')[0];
|
|
843
|
-
}
|
|
844
666
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if (!stats.byTime[timeKey]) {
|
|
858
|
-
stats.byTime[timeKey] = { date: timeKey, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
859
|
-
}
|
|
860
|
-
stats.byTime[timeKey].cost += cost;
|
|
861
|
-
stats.byTime[timeKey].tokens += tokens;
|
|
862
|
-
stats.byTime[timeKey].inputTokens += inputTokens;
|
|
863
|
-
stats.byTime[timeKey].outputTokens += outputTokens;
|
|
864
|
-
|
|
865
|
-
const projectName = projectInfo.name;
|
|
866
|
-
if (!stats.byProject[projectId]) {
|
|
867
|
-
stats.byProject[projectId] = { id: projectId, name: projectName, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
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')) {
|
|
674
|
+
try {
|
|
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 {}
|
|
868
678
|
}
|
|
869
|
-
|
|
870
|
-
stats.byProject[projectId].tokens += tokens;
|
|
871
|
-
stats.byProject[projectId].inputTokens += inputTokens;
|
|
872
|
-
stats.byProject[projectId].outputTokens += outputTokens;
|
|
873
|
-
}
|
|
679
|
+
});
|
|
874
680
|
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
};
|
|
681
|
+
});
|
|
682
|
+
}
|
|
878
683
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
684
|
+
const stats = { totalCost: 0, totalTokens: 0, byModel: {}, byTime: {}, byProject: {} };
|
|
685
|
+
const seen = new Set();
|
|
686
|
+
const now = Date.now();
|
|
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
|
+
});
|
|
892
724
|
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
} catch (err) {
|
|
896
|
-
console.error(`Error reading log dir ${logDir}:`, err);
|
|
725
|
+
} catch {}
|
|
726
|
+
});
|
|
897
727
|
}
|
|
898
|
-
}
|
|
728
|
+
});
|
|
899
729
|
|
|
900
|
-
|
|
730
|
+
res.json({
|
|
901
731
|
totalCost: stats.totalCost,
|
|
902
732
|
totalTokens: stats.totalTokens,
|
|
903
733
|
byModel: Object.values(stats.byModel).sort((a, b) => b.cost - a.cost),
|
|
904
|
-
byDay: Object.values(stats.byTime).sort((a, b) => a.
|
|
734
|
+
byDay: Object.values(stats.byTime).sort((a, b) => a.name.localeCompare(b.name)).map(v => ({ ...v, date: v.name })),
|
|
905
735
|
byProject: Object.values(stats.byProject).sort((a, b) => b.cost - a.cost)
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
res.json(response);
|
|
736
|
+
});
|
|
909
737
|
} catch (error) {
|
|
910
|
-
|
|
911
|
-
res.status(500).json({ error: 'Failed to fetch usage stats' });
|
|
738
|
+
res.status(500).json({ error: 'Failed' });
|
|
912
739
|
}
|
|
913
740
|
});
|
|
914
741
|
|
|
915
|
-
app.
|
|
916
|
-
const {
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
res.json({
|
|
922
|
-
profiles: providerProfiles,
|
|
923
|
-
active: (activeProfiles[provider] && verifyActiveProfile(provider, activeProfiles[provider], authConfig[provider])) ? activeProfiles[provider] : null,
|
|
924
|
-
hasCurrentAuth: !!authConfig[provider],
|
|
925
|
-
});
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
app.post('/api/auth/profiles/:provider', (req, res) => {
|
|
929
|
-
const { provider } = req.params;
|
|
930
|
-
const { name } = req.body;
|
|
931
|
-
|
|
932
|
-
const authConfig = loadAuthConfig();
|
|
933
|
-
if (!authConfig || !authConfig[provider]) {
|
|
934
|
-
return res.status(400).json({ error: `No active auth for ${provider} to save` });
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const profileName = name || getNextProfileName(provider);
|
|
938
|
-
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);
|
|
939
747
|
|
|
940
748
|
try {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
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
|
+
}
|
|
948
757
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
fs.writeFileSync(authPath, JSON.stringify(authConfig, null, 2), 'utf8');
|
|
966
|
-
setActiveProfile(provider, name);
|
|
967
|
-
res.json({ success: true });
|
|
968
|
-
} catch (err) {
|
|
969
|
-
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);
|
|
970
774
|
}
|
|
971
|
-
} else {
|
|
972
|
-
res.status(500).json({ error: 'Config path not found' });
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
775
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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');
|
|
986
787
|
}
|
|
987
788
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
res.status(404).json({ error: 'Profile not found' });
|
|
789
|
+
} catch (err) {
|
|
790
|
+
console.error(err);
|
|
991
791
|
}
|
|
792
|
+
|
|
793
|
+
res.json({ success: true, activePlugin: plugin });
|
|
992
794
|
});
|
|
993
795
|
|
|
994
|
-
app.
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (!newName) return res.status(400).json({ error: 'New name required' });
|
|
999
|
-
|
|
1000
|
-
const profileData = loadAuthProfile(provider, name);
|
|
1001
|
-
if (!profileData) return res.status(404).json({ error: 'Profile not found' });
|
|
1002
|
-
|
|
1003
|
-
const success = saveAuthProfile(provider, newName, profileData);
|
|
1004
|
-
if (success) {
|
|
1005
|
-
deleteAuthProfile(provider, name);
|
|
1006
|
-
|
|
1007
|
-
const activeProfiles = getActiveProfiles();
|
|
1008
|
-
if (activeProfiles[provider] === name) {
|
|
1009
|
-
setActiveProfile(provider, newName);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
res.json({ success: true, name: newName });
|
|
1013
|
-
} else {
|
|
1014
|
-
res.status(500).json({ error: 'Failed to rename profile' });
|
|
1015
|
-
}
|
|
796
|
+
app.get('/api/auth/google/plugin', (req, res) => {
|
|
797
|
+
const studio = loadStudioConfig();
|
|
798
|
+
res.json({ activePlugin: studio.activeGooglePlugin || null });
|
|
1016
799
|
});
|
|
1017
800
|
|
|
1018
|
-
app.
|
|
1019
|
-
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
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 {}
|
|
1022
808
|
}
|
|
809
|
+
res.json({ action: null });
|
|
810
|
+
});
|
|
1023
811
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const result = {
|
|
1030
|
-
added: [],
|
|
1031
|
-
skipped: []
|
|
1032
|
-
};
|
|
1033
|
-
|
|
1034
|
-
for (const pluginName of plugins) {
|
|
1035
|
-
// Check if plugin exists in studio (for validation)
|
|
1036
|
-
const pluginDir = getPluginDir();
|
|
1037
|
-
const dirPath = path.join(pluginDir, pluginName);
|
|
1038
|
-
const hasJs = fs.existsSync(path.join(dirPath, 'index.js'));
|
|
1039
|
-
const hasTs = fs.existsSync(path.join(dirPath, 'index.ts'));
|
|
1040
|
-
|
|
1041
|
-
if (!hasJs && !hasTs) {
|
|
1042
|
-
result.skipped.push(`${pluginName} (not found)`);
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
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
|
+
});
|
|
1045
817
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
+
}
|
|
1050
841
|
} else {
|
|
1051
|
-
|
|
1052
|
-
result.added.push(pluginName);
|
|
842
|
+
skipped.push(p);
|
|
1053
843
|
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (result.added.length > 0) {
|
|
1057
|
-
saveConfig(config);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
res.json(result);
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
app.delete('/api/plugins/config/:name', (req, res) => {
|
|
1064
|
-
const pluginName = decodeURIComponent(req.params.name);
|
|
1065
|
-
const config = loadConfig();
|
|
844
|
+
});
|
|
1066
845
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (config.plugins[pluginName]) {
|
|
1072
|
-
delete config.plugins[pluginName];
|
|
1073
|
-
saveConfig(config);
|
|
1074
|
-
res.json({ success: true });
|
|
1075
|
-
} else {
|
|
1076
|
-
res.status(404).json({ error: 'Plugin not in config' });
|
|
1077
|
-
}
|
|
846
|
+
saveConfig(opencode);
|
|
847
|
+
res.json({ added, skipped });
|
|
1078
848
|
});
|
|
1079
849
|
|
|
1080
|
-
app.listen(PORT, () => {
|
|
1081
|
-
console.log(`Server running at http://localhost:${PORT}`);
|
|
1082
|
-
});
|
|
850
|
+
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
|