opencode-studio-server 1.3.6 → 1.3.8
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 +433 -30
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -69,6 +69,158 @@ app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
|
|
69
69
|
const HOME_DIR = os.homedir();
|
|
70
70
|
const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
|
|
71
71
|
const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
|
|
72
|
+
const ANTIGRAVITY_ACCOUNTS_PATH = path.join(HOME_DIR, '.config', 'opencode', 'antigravity-accounts.json');
|
|
73
|
+
const LOG_DIR = path.join(HOME_DIR, '.local', 'share', 'opencode', 'log');
|
|
74
|
+
|
|
75
|
+
let logWatcher = null;
|
|
76
|
+
let currentLogFile = null;
|
|
77
|
+
let logReadStream = null;
|
|
78
|
+
|
|
79
|
+
function setupLogWatcher() {
|
|
80
|
+
if (!fs.existsSync(LOG_DIR)) return;
|
|
81
|
+
|
|
82
|
+
// Find latest log file
|
|
83
|
+
const getLatestLog = () => {
|
|
84
|
+
try {
|
|
85
|
+
const files = fs.readdirSync(LOG_DIR)
|
|
86
|
+
.filter(f => f.endsWith('.log'))
|
|
87
|
+
.map(f => ({ name: f, time: fs.statSync(path.join(LOG_DIR, f)).mtime.getTime() }))
|
|
88
|
+
.sort((a, b) => b.time - a.time);
|
|
89
|
+
return files[0] ? path.join(LOG_DIR, files[0].name) : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const startTailing = (filePath) => {
|
|
96
|
+
if (currentLogFile === filePath) return;
|
|
97
|
+
if (logReadStream) logReadStream.destroy();
|
|
98
|
+
|
|
99
|
+
currentLogFile = filePath;
|
|
100
|
+
console.log(`Watching log file: ${filePath}`);
|
|
101
|
+
|
|
102
|
+
let fileSize = 0;
|
|
103
|
+
try {
|
|
104
|
+
fileSize = fs.statSync(filePath).size;
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
// Tail from end initially
|
|
108
|
+
let start = Math.max(0, fileSize - 10000);
|
|
109
|
+
|
|
110
|
+
const checkFile = () => {
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.statSync(filePath);
|
|
113
|
+
if (stat.size > fileSize) {
|
|
114
|
+
const stream = fs.createReadStream(filePath, {
|
|
115
|
+
start: fileSize,
|
|
116
|
+
end: stat.size,
|
|
117
|
+
encoding: 'utf8'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
stream.on('data', (chunk) => {
|
|
121
|
+
const lines = chunk.split('\n');
|
|
122
|
+
lines.forEach(processLogLine);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
fileSize = stat.size;
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// File might be rotated/deleted
|
|
129
|
+
if (e.code === 'ENOENT') {
|
|
130
|
+
clearInterval(tailInterval);
|
|
131
|
+
const newLog = getLatestLog();
|
|
132
|
+
if (newLog && newLog !== currentLogFile) startTailing(newLog);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const tailInterval = setInterval(checkFile, 2000);
|
|
138
|
+
logWatcher = tailInterval;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const initialLog = getLatestLog();
|
|
142
|
+
if (initialLog) startTailing(initialLog);
|
|
143
|
+
|
|
144
|
+
// Watch dir for new logs
|
|
145
|
+
fs.watch(LOG_DIR, (eventType, filename) => {
|
|
146
|
+
if (filename && filename.endsWith('.log')) {
|
|
147
|
+
const latest = getLatestLog();
|
|
148
|
+
if (latest && latest !== currentLogFile) {
|
|
149
|
+
if (logWatcher) clearInterval(logWatcher);
|
|
150
|
+
startTailing(latest);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function processLogLine(line) {
|
|
157
|
+
// Detect LLM usage: service=llm providerID=... modelID=...
|
|
158
|
+
// Example: service=llm providerID=openai modelID=gpt-5.2-codex sessionID=...
|
|
159
|
+
const isUsage = line.includes('service=llm') && line.includes('stream');
|
|
160
|
+
const isError = line.includes('service=llm') && (line.includes('error=') || line.includes('status=429'));
|
|
161
|
+
|
|
162
|
+
if (!isUsage && !isError) return;
|
|
163
|
+
|
|
164
|
+
const providerMatch = line.match(/providerID=([^\s]+)/);
|
|
165
|
+
const modelMatch = line.match(/modelID=([^\s]+)/);
|
|
166
|
+
|
|
167
|
+
if (providerMatch) {
|
|
168
|
+
const provider = providerMatch[1];
|
|
169
|
+
const model = modelMatch ? modelMatch[1] : 'unknown';
|
|
170
|
+
|
|
171
|
+
// Map provider to pool namespace
|
|
172
|
+
let namespace = provider;
|
|
173
|
+
if (provider === 'google') {
|
|
174
|
+
const activePlugin = getActiveGooglePlugin();
|
|
175
|
+
namespace = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const metadata = loadPoolMetadata();
|
|
179
|
+
if (!metadata._quota) metadata._quota = {};
|
|
180
|
+
if (!metadata._quota[namespace]) metadata._quota[namespace] = {};
|
|
181
|
+
|
|
182
|
+
const today = new Date().toISOString().split('T')[0];
|
|
183
|
+
|
|
184
|
+
if (isUsage) {
|
|
185
|
+
// Increment usage
|
|
186
|
+
metadata._quota[namespace][today] = (metadata._quota[namespace][today] || 0) + 1;
|
|
187
|
+
|
|
188
|
+
// Also update active account usage if possible
|
|
189
|
+
const studio = loadStudioConfig();
|
|
190
|
+
const activeProfile = studio.activeProfiles?.[provider];
|
|
191
|
+
if (activeProfile && metadata[namespace]?.[activeProfile]) {
|
|
192
|
+
metadata[namespace][activeProfile].usageCount = (metadata[namespace][activeProfile].usageCount || 0) + 1;
|
|
193
|
+
metadata[namespace][activeProfile].lastUsed = Date.now();
|
|
194
|
+
}
|
|
195
|
+
} else if (isError) {
|
|
196
|
+
// Check for quota exhaustion patterns
|
|
197
|
+
const errorMsg = line.match(/error=(.+)/)?.[1] || '';
|
|
198
|
+
const isQuotaError = line.includes('status=429') ||
|
|
199
|
+
errorMsg.toLowerCase().includes('quota') ||
|
|
200
|
+
errorMsg.toLowerCase().includes('rate limit');
|
|
201
|
+
|
|
202
|
+
if (isQuotaError) {
|
|
203
|
+
console.log(`[LogWatcher] Detected quota exhaustion for ${namespace}`);
|
|
204
|
+
// Mark exhausted for today
|
|
205
|
+
metadata._quota[namespace].exhausted = true;
|
|
206
|
+
metadata._quota[namespace].exhaustedDate = today;
|
|
207
|
+
|
|
208
|
+
// Adaptive limit learning: if we hit a limit, maybe that's the ceiling?
|
|
209
|
+
// Only update if we have meaningful usage (>5) to avoid false positives on glitches
|
|
210
|
+
const currentUsage = metadata._quota[namespace][today] || 0;
|
|
211
|
+
if (currentUsage > 5) {
|
|
212
|
+
// Update daily limit to current usage (maybe round up)
|
|
213
|
+
metadata._quota[namespace].dailyLimit = currentUsage;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
savePoolMetadata(metadata);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Start watcher on server start
|
|
223
|
+
setupLogWatcher();
|
|
72
224
|
|
|
73
225
|
let pendingActionMemory = null;
|
|
74
226
|
|
|
@@ -576,6 +728,7 @@ app.get('/api/auth/providers', (req, res) => {
|
|
|
576
728
|
});
|
|
577
729
|
|
|
578
730
|
app.get('/api/auth', (req, res) => {
|
|
731
|
+
syncAntigravityPool();
|
|
579
732
|
const authCfg = loadAuthConfig() || {};
|
|
580
733
|
const studio = loadStudioConfig();
|
|
581
734
|
const ac = studio.activeProfiles || {};
|
|
@@ -646,6 +799,7 @@ app.get('/api/auth', (req, res) => {
|
|
|
646
799
|
});
|
|
647
800
|
|
|
648
801
|
app.get('/api/auth/profiles', (req, res) => {
|
|
802
|
+
syncAntigravityPool();
|
|
649
803
|
const authCfg = loadAuthConfig() || {};
|
|
650
804
|
const studio = loadStudioConfig();
|
|
651
805
|
const ac = studio.activeProfiles || {};
|
|
@@ -681,8 +835,25 @@ app.post('/api/auth/profiles/:provider', (req, res) => {
|
|
|
681
835
|
const dir = path.join(AUTH_PROFILES_DIR, namespace);
|
|
682
836
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
683
837
|
|
|
684
|
-
|
|
838
|
+
if (!auth[provider]) {
|
|
839
|
+
return res.status(400).json({ error: 'No current auth for provider' });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const profileName = name || auth[provider].email || `profile-${Date.now()}`;
|
|
843
|
+
const profilePath = path.join(dir, `${profileName}.json`);
|
|
685
844
|
atomicWriteFileSync(profilePath, JSON.stringify(auth[provider], null, 2));
|
|
845
|
+
|
|
846
|
+
const metadata = loadPoolMetadata();
|
|
847
|
+
if (!metadata[namespace]) metadata[namespace] = {};
|
|
848
|
+
metadata[namespace][profileName] = {
|
|
849
|
+
...(metadata[namespace][profileName] || {}),
|
|
850
|
+
email: auth[provider].email || metadata[namespace][profileName]?.email || null,
|
|
851
|
+
createdAt: metadata[namespace][profileName]?.createdAt || Date.now(),
|
|
852
|
+
lastUsed: Date.now(),
|
|
853
|
+
usageCount: metadata[namespace][profileName]?.usageCount || 0
|
|
854
|
+
};
|
|
855
|
+
savePoolMetadata(metadata);
|
|
856
|
+
|
|
686
857
|
res.json({ success: true, name: path.basename(profilePath, '.json') });
|
|
687
858
|
});
|
|
688
859
|
|
|
@@ -714,6 +885,22 @@ app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
|
|
|
714
885
|
const cp = getConfigPath();
|
|
715
886
|
const ap = path.join(path.dirname(cp), 'auth.json');
|
|
716
887
|
atomicWriteFileSync(ap, JSON.stringify(authCfg, null, 2));
|
|
888
|
+
|
|
889
|
+
const metadata = loadPoolMetadata();
|
|
890
|
+
if (!metadata[namespace]) metadata[namespace] = {};
|
|
891
|
+
metadata[namespace][name] = {
|
|
892
|
+
...(metadata[namespace][name] || {}),
|
|
893
|
+
email: profileData.email || metadata[namespace][name]?.email || null,
|
|
894
|
+
createdAt: metadata[namespace][name]?.createdAt || Date.now(),
|
|
895
|
+
lastUsed: Date.now(),
|
|
896
|
+
usageCount: (metadata[namespace][name]?.usageCount || 0) + 1
|
|
897
|
+
};
|
|
898
|
+
if (!metadata._quota) metadata._quota = {};
|
|
899
|
+
if (!metadata._quota[namespace]) metadata._quota[namespace] = {};
|
|
900
|
+
const today = new Date().toISOString().split('T')[0];
|
|
901
|
+
metadata._quota[namespace][today] = (metadata._quota[namespace][today] || 0) + 1;
|
|
902
|
+
savePoolMetadata(metadata);
|
|
903
|
+
|
|
717
904
|
res.json({ success: true });
|
|
718
905
|
});
|
|
719
906
|
|
|
@@ -726,6 +913,30 @@ app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
|
|
|
726
913
|
|
|
727
914
|
const profilePath = path.join(AUTH_PROFILES_DIR, namespace, `${name}.json`);
|
|
728
915
|
if (fs.existsSync(profilePath)) fs.unlinkSync(profilePath);
|
|
916
|
+
|
|
917
|
+
const studio = loadStudioConfig();
|
|
918
|
+
if (studio.activeProfiles && studio.activeProfiles[provider] === name) {
|
|
919
|
+
delete studio.activeProfiles[provider];
|
|
920
|
+
saveStudioConfig(studio);
|
|
921
|
+
|
|
922
|
+
const authCfg = loadAuthConfig() || {};
|
|
923
|
+
delete authCfg[provider];
|
|
924
|
+
if (provider === 'google') {
|
|
925
|
+
const key = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
|
|
926
|
+
delete authCfg.google;
|
|
927
|
+
delete authCfg[key];
|
|
928
|
+
}
|
|
929
|
+
const cp = getConfigPath();
|
|
930
|
+
const ap = path.join(path.dirname(cp), 'auth.json');
|
|
931
|
+
atomicWriteFileSync(ap, JSON.stringify(authCfg, null, 2));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const metadata = loadPoolMetadata();
|
|
935
|
+
if (metadata[namespace]?.[name]) {
|
|
936
|
+
delete metadata[namespace][name];
|
|
937
|
+
savePoolMetadata(metadata);
|
|
938
|
+
}
|
|
939
|
+
|
|
729
940
|
res.json({ success: true });
|
|
730
941
|
});
|
|
731
942
|
|
|
@@ -741,6 +952,13 @@ app.put('/api/auth/profiles/:provider/:name', (req, res) => {
|
|
|
741
952
|
const newPath = path.join(AUTH_PROFILES_DIR, namespace, `${newName}.json`);
|
|
742
953
|
if (fs.existsSync(oldPath)) fs.renameSync(oldPath, newPath);
|
|
743
954
|
|
|
955
|
+
const metadata = loadPoolMetadata();
|
|
956
|
+
if (metadata[namespace]?.[name]) {
|
|
957
|
+
metadata[namespace][newName] = metadata[namespace][name];
|
|
958
|
+
delete metadata[namespace][name];
|
|
959
|
+
savePoolMetadata(metadata);
|
|
960
|
+
}
|
|
961
|
+
|
|
744
962
|
// Update active profile name if it was the one renamed
|
|
745
963
|
const studio = loadStudioConfig();
|
|
746
964
|
if (studio.activeProfiles && studio.activeProfiles[provider] === name) {
|
|
@@ -770,10 +988,13 @@ app.post('/api/auth/login', (req, res) => {
|
|
|
770
988
|
let cmd = 'opencode auth login';
|
|
771
989
|
if (provider) cmd += ` ${provider}`;
|
|
772
990
|
|
|
991
|
+
const cp = getConfigPath();
|
|
992
|
+
const configDir = cp ? path.dirname(cp) : process.cwd();
|
|
993
|
+
const safeDir = configDir.replace(/"/g, '\\"');
|
|
773
994
|
const platform = process.platform;
|
|
774
995
|
|
|
775
996
|
if (platform === 'win32') {
|
|
776
|
-
const terminalCmd = `start "" cmd /c "call ${cmd} || pause"`;
|
|
997
|
+
const terminalCmd = `start "" /d "${safeDir}" cmd /c "call ${cmd} || pause"`;
|
|
777
998
|
console.log('Executing terminal command:', terminalCmd);
|
|
778
999
|
exec(terminalCmd, (err) => {
|
|
779
1000
|
if (err) {
|
|
@@ -783,7 +1004,7 @@ app.post('/api/auth/login', (req, res) => {
|
|
|
783
1004
|
res.json({ success: true, message: 'Terminal opened', note: 'Complete login in the terminal window' });
|
|
784
1005
|
});
|
|
785
1006
|
} else if (platform === 'darwin') {
|
|
786
|
-
const terminalCmd = `osascript -e 'tell application "Terminal" to do script "${cmd}"'`;
|
|
1007
|
+
const terminalCmd = `osascript -e 'tell application "Terminal" to do script "cd ${safeDir} && ${cmd}"'`;
|
|
787
1008
|
console.log('Executing terminal command:', terminalCmd);
|
|
788
1009
|
exec(terminalCmd, (err) => {
|
|
789
1010
|
if (err) {
|
|
@@ -794,11 +1015,11 @@ app.post('/api/auth/login', (req, res) => {
|
|
|
794
1015
|
});
|
|
795
1016
|
} else {
|
|
796
1017
|
const linuxTerminals = [
|
|
797
|
-
{ name: 'x-terminal-emulator', cmd: `x-terminal-emulator -e "${cmd}"` },
|
|
798
|
-
{ name: 'gnome-terminal', cmd: `gnome-terminal -- bash -c "${cmd}; read -p 'Press Enter to close...'"` },
|
|
799
|
-
{ name: 'konsole', cmd: `konsole -e bash -c "${cmd}; read -p 'Press Enter to close...'"` },
|
|
800
|
-
{ name: 'xfce4-terminal', cmd: `xfce4-terminal -e "bash -c
|
|
801
|
-
{ name: 'xterm', cmd: `xterm -e "bash -c '${cmd}; read -p Press_Enter_to_close...'"` }
|
|
1018
|
+
{ name: 'x-terminal-emulator', cmd: `x-terminal-emulator -e "bash -c 'cd ${safeDir} && ${cmd}'"` },
|
|
1019
|
+
{ name: 'gnome-terminal', cmd: `gnome-terminal -- bash -c "cd ${safeDir} && ${cmd}; read -p 'Press Enter to close...'"` },
|
|
1020
|
+
{ name: 'konsole', cmd: `konsole -e bash -c "cd ${safeDir} && ${cmd}; read -p 'Press Enter to close...'"` },
|
|
1021
|
+
{ name: 'xfce4-terminal', cmd: `xfce4-terminal -e "bash -c \"cd ${safeDir} && ${cmd}; read -p 'Press Enter to close...'\"" ` },
|
|
1022
|
+
{ name: 'xterm', cmd: `xterm -e "bash -c 'cd ${safeDir} && ${cmd}; read -p Press_Enter_to_close...'"` }
|
|
802
1023
|
];
|
|
803
1024
|
|
|
804
1025
|
const tryTerminal = (index) => {
|
|
@@ -828,18 +1049,57 @@ app.post('/api/auth/login', (req, res) => {
|
|
|
828
1049
|
}
|
|
829
1050
|
});
|
|
830
1051
|
|
|
1052
|
+
|
|
831
1053
|
app.delete('/api/auth/:provider', (req, res) => {
|
|
832
1054
|
const { provider } = req.params;
|
|
833
1055
|
const authCfg = loadAuthConfig() || {};
|
|
834
|
-
|
|
1056
|
+
const studio = loadStudioConfig();
|
|
1057
|
+
const activePlugin = studio.activeGooglePlugin;
|
|
1058
|
+
|
|
1059
|
+
if (provider === 'google') {
|
|
1060
|
+
delete authCfg.google;
|
|
1061
|
+
delete authCfg['google.gemini'];
|
|
1062
|
+
delete authCfg['google.antigravity'];
|
|
1063
|
+
|
|
1064
|
+
if (studio.activeProfiles) delete studio.activeProfiles.google;
|
|
1065
|
+
saveStudioConfig(studio);
|
|
1066
|
+
|
|
1067
|
+
const geminiDir = path.join(AUTH_PROFILES_DIR, 'google.gemini');
|
|
1068
|
+
const antiDir = path.join(AUTH_PROFILES_DIR, 'google.antigravity');
|
|
1069
|
+
[geminiDir, antiDir].forEach(dir => {
|
|
1070
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
const metadata = loadPoolMetadata();
|
|
1074
|
+
delete metadata['google.gemini'];
|
|
1075
|
+
delete metadata['google.antigravity'];
|
|
1076
|
+
savePoolMetadata(metadata);
|
|
1077
|
+
|
|
1078
|
+
if (activePlugin === 'antigravity' && fs.existsSync(ANTIGRAVITY_ACCOUNTS_PATH)) {
|
|
1079
|
+
fs.rmSync(ANTIGRAVITY_ACCOUNTS_PATH, { force: true });
|
|
1080
|
+
}
|
|
1081
|
+
} else {
|
|
1082
|
+
delete authCfg[provider];
|
|
1083
|
+
if (studio.activeProfiles) delete studio.activeProfiles[provider];
|
|
1084
|
+
saveStudioConfig(studio);
|
|
1085
|
+
|
|
1086
|
+
const providerDir = path.join(AUTH_PROFILES_DIR, provider);
|
|
1087
|
+
if (fs.existsSync(providerDir)) fs.rmSync(providerDir, { recursive: true, force: true });
|
|
1088
|
+
|
|
1089
|
+
const metadata = loadPoolMetadata();
|
|
1090
|
+
delete metadata[provider];
|
|
1091
|
+
savePoolMetadata(metadata);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (provider === 'google' && activePlugin) {
|
|
1095
|
+
const key = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
|
|
1096
|
+
delete authCfg[key];
|
|
1097
|
+
}
|
|
1098
|
+
|
|
835
1099
|
const cp = getConfigPath();
|
|
836
1100
|
const ap = path.join(path.dirname(cp), 'auth.json');
|
|
837
1101
|
atomicWriteFileSync(ap, JSON.stringify(authCfg, null, 2));
|
|
838
|
-
|
|
839
|
-
const studio = loadStudioConfig();
|
|
840
|
-
if (studio.activeProfiles) delete studio.activeProfiles[provider];
|
|
841
|
-
saveStudioConfig(studio);
|
|
842
|
-
|
|
1102
|
+
|
|
843
1103
|
res.json({ success: true });
|
|
844
1104
|
});
|
|
845
1105
|
|
|
@@ -864,6 +1124,74 @@ function savePoolMetadata(metadata) {
|
|
|
864
1124
|
atomicWriteFileSync(POOL_METADATA_FILE, JSON.stringify(metadata, null, 2));
|
|
865
1125
|
}
|
|
866
1126
|
|
|
1127
|
+
function loadAntigravityAccounts() {
|
|
1128
|
+
if (!fs.existsSync(ANTIGRAVITY_ACCOUNTS_PATH)) return null;
|
|
1129
|
+
try {
|
|
1130
|
+
return JSON.parse(fs.readFileSync(ANTIGRAVITY_ACCOUNTS_PATH, 'utf8'));
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function listAntigravityAccounts() {
|
|
1137
|
+
const data = loadAntigravityAccounts();
|
|
1138
|
+
if (!data?.accounts || !Array.isArray(data.accounts)) return [];
|
|
1139
|
+
return data.accounts.map(a => ({
|
|
1140
|
+
email: a.email || null,
|
|
1141
|
+
refreshToken: a.refreshToken || null
|
|
1142
|
+
}));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function syncAntigravityPool() {
|
|
1146
|
+
const accounts = listAntigravityAccounts();
|
|
1147
|
+
const namespace = 'google.antigravity';
|
|
1148
|
+
const profileDir = path.join(AUTH_PROFILES_DIR, namespace);
|
|
1149
|
+
|
|
1150
|
+
if (!accounts.length) {
|
|
1151
|
+
if (fs.existsSync(profileDir)) fs.rmSync(profileDir, { recursive: true, force: true });
|
|
1152
|
+
const metadata = loadPoolMetadata();
|
|
1153
|
+
if (metadata[namespace]) {
|
|
1154
|
+
delete metadata[namespace];
|
|
1155
|
+
savePoolMetadata(metadata);
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
|
|
1161
|
+
|
|
1162
|
+
const metadata = loadPoolMetadata();
|
|
1163
|
+
if (!metadata[namespace]) metadata[namespace] = {};
|
|
1164
|
+
|
|
1165
|
+
const seen = new Set();
|
|
1166
|
+
accounts.forEach((account, idx) => {
|
|
1167
|
+
const name = account.email || `account-${idx + 1}`;
|
|
1168
|
+
seen.add(name);
|
|
1169
|
+
const profilePath = path.join(profileDir, `${name}.json`);
|
|
1170
|
+
if (!fs.existsSync(profilePath)) {
|
|
1171
|
+
atomicWriteFileSync(profilePath, JSON.stringify({ email: account.email }, null, 2));
|
|
1172
|
+
}
|
|
1173
|
+
if (!metadata[namespace][name]) {
|
|
1174
|
+
metadata[namespace][name] = {
|
|
1175
|
+
email: account.email || null,
|
|
1176
|
+
createdAt: Date.now(),
|
|
1177
|
+
lastUsed: 0,
|
|
1178
|
+
usageCount: 0
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
const profileFiles = fs.existsSync(profileDir) ? fs.readdirSync(profileDir).filter(f => f.endsWith('.json')) : [];
|
|
1184
|
+
profileFiles.forEach(file => {
|
|
1185
|
+
const name = file.replace('.json', '');
|
|
1186
|
+
if (!seen.has(name)) {
|
|
1187
|
+
fs.unlinkSync(path.join(profileDir, file));
|
|
1188
|
+
if (metadata[namespace]?.[name]) delete metadata[namespace][name];
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
savePoolMetadata(metadata);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
867
1195
|
function getAccountStatus(meta, now) {
|
|
868
1196
|
if (!meta) return 'ready';
|
|
869
1197
|
if (meta.cooldownUntil && meta.cooldownUntil > now) return 'cooldown';
|
|
@@ -892,11 +1220,18 @@ function buildAccountPool(provider) {
|
|
|
892
1220
|
files.forEach(file => {
|
|
893
1221
|
const name = file.replace('.json', '');
|
|
894
1222
|
const meta = providerMeta[name] || {};
|
|
895
|
-
|
|
1223
|
+
let profileEmail = null;
|
|
1224
|
+
try {
|
|
1225
|
+
const raw = fs.readFileSync(path.join(profileDir, file), 'utf8');
|
|
1226
|
+
const parsed = JSON.parse(raw);
|
|
1227
|
+
profileEmail = parsed?.email || null;
|
|
1228
|
+
} catch {}
|
|
1229
|
+
let status = getAccountStatus(meta, now);
|
|
1230
|
+
if (name === activeProfile && status === 'ready') status = 'active';
|
|
896
1231
|
|
|
897
1232
|
profiles.push({
|
|
898
1233
|
name,
|
|
899
|
-
email: meta.email || null,
|
|
1234
|
+
email: meta.email || profileEmail || null,
|
|
900
1235
|
status,
|
|
901
1236
|
lastUsed: meta.lastUsed || 0,
|
|
902
1237
|
usageCount: meta.usageCount || 0,
|
|
@@ -927,12 +1262,7 @@ function buildAccountPool(provider) {
|
|
|
927
1262
|
};
|
|
928
1263
|
}
|
|
929
1264
|
|
|
930
|
-
|
|
931
|
-
app.get('/api/auth/pool', (req, res) => {
|
|
932
|
-
const provider = req.query.provider || 'google';
|
|
933
|
-
const pool = buildAccountPool(provider);
|
|
934
|
-
|
|
935
|
-
// Also include quota estimate (local tracking)
|
|
1265
|
+
function getPoolQuota(provider, pool) {
|
|
936
1266
|
const metadata = loadPoolMetadata();
|
|
937
1267
|
const activePlugin = getActiveGooglePlugin();
|
|
938
1268
|
const namespace = provider === 'google'
|
|
@@ -948,7 +1278,7 @@ app.get('/api/auth/pool', (req, res) => {
|
|
|
948
1278
|
const remaining = Math.max(0, dailyLimit - todayUsage);
|
|
949
1279
|
const percentage = Math.round((remaining / dailyLimit) * 100);
|
|
950
1280
|
|
|
951
|
-
|
|
1281
|
+
return {
|
|
952
1282
|
dailyLimit,
|
|
953
1283
|
remaining,
|
|
954
1284
|
used: todayUsage,
|
|
@@ -961,7 +1291,14 @@ app.get('/api/auth/pool', (req, res) => {
|
|
|
961
1291
|
limit: Math.floor(dailyLimit / Math.max(1, pool.totalAccounts))
|
|
962
1292
|
}))
|
|
963
1293
|
};
|
|
964
|
-
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// GET /api/auth/pool - Get account pool for Google (or specified provider)
|
|
1297
|
+
app.get('/api/auth/pool', (req, res) => {
|
|
1298
|
+
const provider = req.query.provider || 'google';
|
|
1299
|
+
syncAntigravityPool();
|
|
1300
|
+
const pool = buildAccountPool(provider);
|
|
1301
|
+
const quota = getPoolQuota(provider, pool);
|
|
965
1302
|
res.json({ pool, quota });
|
|
966
1303
|
});
|
|
967
1304
|
|
|
@@ -1022,8 +1359,15 @@ app.post('/api/auth/pool/rotate', (req, res) => {
|
|
|
1022
1359
|
if (!metadata[namespace]) metadata[namespace] = {};
|
|
1023
1360
|
metadata[namespace][next.name] = {
|
|
1024
1361
|
...metadata[namespace][next.name],
|
|
1025
|
-
lastUsed: now
|
|
1362
|
+
lastUsed: now,
|
|
1363
|
+
usageCount: (metadata[namespace][next.name]?.usageCount || 0) + 1
|
|
1026
1364
|
};
|
|
1365
|
+
|
|
1366
|
+
if (!metadata._quota) metadata._quota = {};
|
|
1367
|
+
if (!metadata._quota[namespace]) metadata._quota[namespace] = {};
|
|
1368
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1369
|
+
metadata._quota[namespace][today] = (metadata._quota[namespace][today] || 0) + 1;
|
|
1370
|
+
|
|
1027
1371
|
savePoolMetadata(metadata);
|
|
1028
1372
|
|
|
1029
1373
|
res.json({
|
|
@@ -1034,6 +1378,31 @@ app.post('/api/auth/pool/rotate', (req, res) => {
|
|
|
1034
1378
|
});
|
|
1035
1379
|
});
|
|
1036
1380
|
|
|
1381
|
+
// POST /api/auth/pool/limit - Set daily quota limit
|
|
1382
|
+
app.post('/api/auth/pool/limit', (req, res) => {
|
|
1383
|
+
const { provider, limit } = req.body;
|
|
1384
|
+
|
|
1385
|
+
if (!provider || typeof limit !== 'number' || limit < 0) {
|
|
1386
|
+
return res.status(400).json({ error: 'Invalid provider or limit' });
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const activePlugin = getActiveGooglePlugin();
|
|
1390
|
+
const namespace = provider === 'google'
|
|
1391
|
+
? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
|
|
1392
|
+
: provider;
|
|
1393
|
+
|
|
1394
|
+
const metadata = loadPoolMetadata();
|
|
1395
|
+
if (!metadata._quota) metadata._quota = {};
|
|
1396
|
+
if (!metadata._quota[namespace]) metadata._quota[namespace] = {};
|
|
1397
|
+
|
|
1398
|
+
metadata._quota[namespace].dailyLimit = limit;
|
|
1399
|
+
// Reset exhaustion if limit increased significantly? Maybe user wants to retry.
|
|
1400
|
+
// For now, simple update.
|
|
1401
|
+
|
|
1402
|
+
savePoolMetadata(metadata);
|
|
1403
|
+
res.json({ success: true, limit });
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1037
1406
|
// PUT /api/auth/pool/:name/cooldown - Mark account as in cooldown
|
|
1038
1407
|
app.put('/api/auth/pool/:name/cooldown', (req, res) => {
|
|
1039
1408
|
const { name } = req.params;
|
|
@@ -1374,6 +1743,14 @@ app.get('/api/auth/google/plugin', (req, res) => {
|
|
|
1374
1743
|
|
|
1375
1744
|
const GEMINI_CLIENT_ID = process.env.GEMINI_CLIENT_ID || "";
|
|
1376
1745
|
const GEMINI_CLIENT_SECRET = process.env.GEMINI_CLIENT_SECRET || "";
|
|
1746
|
+
|
|
1747
|
+
function getGeminiClientId() {
|
|
1748
|
+
if (GEMINI_CLIENT_ID) return GEMINI_CLIENT_ID;
|
|
1749
|
+
const opencodeCfg = loadConfig();
|
|
1750
|
+
const oauth = opencodeCfg?.mcp?.google?.oauth;
|
|
1751
|
+
return oauth?.clientId || "";
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1377
1754
|
const GEMINI_SCOPES = [
|
|
1378
1755
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
1379
1756
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
@@ -1399,14 +1776,19 @@ app.post('/api/auth/google/start', async (req, res) => {
|
|
|
1399
1776
|
if (oauthCallbackServer) {
|
|
1400
1777
|
return res.status(400).json({ error: 'OAuth flow already in progress' });
|
|
1401
1778
|
}
|
|
1402
|
-
|
|
1779
|
+
|
|
1780
|
+
const clientId = getGeminiClientId();
|
|
1781
|
+
if (!clientId) {
|
|
1782
|
+
return res.status(400).json({ error: 'Missing Gemini OAuth client_id. Set GEMINI_CLIENT_ID or mcp.google.oauth.clientId.' });
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1403
1785
|
const { verifier, challenge } = generatePKCE();
|
|
1404
1786
|
const state = encodeOAuthState({ verifier });
|
|
1405
1787
|
|
|
1406
1788
|
pendingOAuthState = { verifier, status: 'pending', startedAt: Date.now() };
|
|
1407
1789
|
|
|
1408
1790
|
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
|
|
1409
|
-
authUrl.searchParams.set('client_id',
|
|
1791
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
1410
1792
|
authUrl.searchParams.set('response_type', 'code');
|
|
1411
1793
|
authUrl.searchParams.set('redirect_uri', GEMINI_REDIRECT_URI);
|
|
1412
1794
|
authUrl.searchParams.set('scope', GEMINI_SCOPES.join(' '));
|
|
@@ -1440,7 +1822,7 @@ app.post('/api/auth/google/start', async (req, res) => {
|
|
|
1440
1822
|
method: 'POST',
|
|
1441
1823
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1442
1824
|
body: new URLSearchParams({
|
|
1443
|
-
client_id:
|
|
1825
|
+
client_id: clientId,
|
|
1444
1826
|
client_secret: GEMINI_CLIENT_SECRET,
|
|
1445
1827
|
code,
|
|
1446
1828
|
grant_type: 'authorization_code',
|
|
@@ -1471,8 +1853,8 @@ app.post('/api/auth/google/start', async (req, res) => {
|
|
|
1471
1853
|
const ap = path.join(path.dirname(cp), 'auth.json');
|
|
1472
1854
|
const authCfg = fs.existsSync(ap) ? JSON.parse(fs.readFileSync(ap, 'utf8')) : {};
|
|
1473
1855
|
|
|
1474
|
-
const
|
|
1475
|
-
const activePlugin =
|
|
1856
|
+
const studioConfig = loadStudioConfig();
|
|
1857
|
+
const activePlugin = studioConfig.activeGooglePlugin || 'gemini';
|
|
1476
1858
|
const namespace = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
|
|
1477
1859
|
|
|
1478
1860
|
const credentials = {
|
|
@@ -1486,6 +1868,27 @@ app.post('/api/auth/google/start', async (req, res) => {
|
|
|
1486
1868
|
authCfg[namespace] = credentials;
|
|
1487
1869
|
|
|
1488
1870
|
atomicWriteFileSync(ap, JSON.stringify(authCfg, null, 2));
|
|
1871
|
+
|
|
1872
|
+
const profileDir = path.join(AUTH_PROFILES_DIR, namespace);
|
|
1873
|
+
if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
|
|
1874
|
+
const profileName = email || `google-${Date.now()}`;
|
|
1875
|
+
const profilePath = path.join(profileDir, `${profileName}.json`);
|
|
1876
|
+
atomicWriteFileSync(profilePath, JSON.stringify(credentials, null, 2));
|
|
1877
|
+
|
|
1878
|
+
const metadata = loadPoolMetadata();
|
|
1879
|
+
if (!metadata[namespace]) metadata[namespace] = {};
|
|
1880
|
+
metadata[namespace][profileName] = {
|
|
1881
|
+
...(metadata[namespace][profileName] || {}),
|
|
1882
|
+
email: email || null,
|
|
1883
|
+
createdAt: metadata[namespace][profileName]?.createdAt || Date.now(),
|
|
1884
|
+
lastUsed: Date.now(),
|
|
1885
|
+
usageCount: metadata[namespace][profileName]?.usageCount || 0
|
|
1886
|
+
};
|
|
1887
|
+
savePoolMetadata(metadata);
|
|
1888
|
+
|
|
1889
|
+
if (!studioConfig.activeProfiles) studioConfig.activeProfiles = {};
|
|
1890
|
+
studioConfig.activeProfiles.google = profileName;
|
|
1891
|
+
saveStudioConfig(studioConfig);
|
|
1489
1892
|
|
|
1490
1893
|
pendingOAuthState = { ...pendingOAuthState, status: 'success', email };
|
|
1491
1894
|
|