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