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.
- package/index.js +578 -854
- 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
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,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
|
-
|
|
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(
|
|
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
|
|
226
|
-
|
|
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
|
|
232
|
-
if (!
|
|
233
|
-
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
271
|
-
if (!
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
287
|
-
|
|
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
|
|
304
|
-
|
|
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
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
315
|
+
const pd = getPluginDir();
|
|
316
|
+
const cp = getConfigPath();
|
|
317
|
+
const cr = cp ? path.dirname(cp) : null;
|
|
313
318
|
const plugins = [];
|
|
314
|
-
|
|
315
|
-
|
|
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 (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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 (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
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
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
417
|
-
res.json({ success: true, enabled: true });
|
|
418
|
-
});
|
|
381
|
+
if (allAuthConfigs.length === 0) return null;
|
|
419
382
|
|
|
420
|
-
|
|
421
|
-
const
|
|
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
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
565
|
-
const
|
|
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
|
|
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
|
|
585
|
-
const
|
|
586
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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.
|
|
605
|
-
const
|
|
606
|
-
const
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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.
|
|
519
|
+
app.post('/api/auth/profiles/:provider', (req, res) => {
|
|
617
520
|
const { provider } = req.params;
|
|
618
|
-
const
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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.
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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.
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
610
|
+
console.log('Executing terminal command:', terminalCmd);
|
|
731
611
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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.
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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 {
|
|
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
|
|
786
|
-
|
|
787
|
-
path.join(home, '.local', 'share', 'opencode'
|
|
788
|
-
path.join(home, '.opencode',
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
845
|
-
|
|
846
|
-
if (range === '
|
|
847
|
-
else if (range === '
|
|
848
|
-
else if (range === '
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
964
|
-
const {
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
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.
|
|
1042
|
-
const
|
|
1043
|
-
|
|
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.
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
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}`));
|