opencode-studio-server 1.12.13 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/README.md +1 -0
- package/index.js +188 -84
- package/package.json +3 -2
- package/proxy-manager.js +162 -0
package/AGENTS.md
CHANGED
|
@@ -7,6 +7,7 @@ Express API backend (port 3001). Single-file architecture.
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|------|---------|
|
|
9
9
|
| `index.js` | All routes, config IO, auth, skills, plugins, usage stats |
|
|
10
|
+
| `proxy-manager.js` | Manages CLIProxyAPI process and config |
|
|
10
11
|
| `cli.js` | npm bin entry, protocol URL parser, pending action queue |
|
|
11
12
|
| `register-protocol.js` | OS-specific `opencodestudio://` handler registration |
|
|
12
13
|
|
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ The server runs on port **3001** and provides an API for managing your local Ope
|
|
|
31
31
|
- **Protocol Handler**: `opencodestudio://` support for one-click actions
|
|
32
32
|
- **Config Management**: Reads/writes `~/.config/opencode/opencode.json`
|
|
33
33
|
- **MCP Management**: Add/remove/toggle MCP servers
|
|
34
|
+
- **Proxy Manager**: Manage CLIProxyAPI instance
|
|
34
35
|
- **Auth**: Manage authentication profiles
|
|
35
36
|
|
|
36
37
|
## License
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const crypto = require('crypto');
|
|
|
8
8
|
const { spawn, exec } = require('child_process');
|
|
9
9
|
|
|
10
10
|
const pkg = require('./package.json');
|
|
11
|
+
const proxyManager = require('./proxy-manager');
|
|
11
12
|
const SERVER_VERSION = pkg.version;
|
|
12
13
|
|
|
13
14
|
// Atomic file write: write to temp file then rename to prevent corruption
|
|
@@ -1406,22 +1407,6 @@ app.get('/api/auth/providers', (req, res) => {
|
|
|
1406
1407
|
});
|
|
1407
1408
|
|
|
1408
1409
|
app.get('/api/auth', (req, res) => {
|
|
1409
|
-
importCurrentGoogleAuthToPool();
|
|
1410
|
-
syncAntigravityPool();
|
|
1411
|
-
const authCfg = loadAuthConfig() || {};
|
|
1412
|
-
const studio = loadStudioConfig();
|
|
1413
|
-
const ac = studio.activeProfiles || {};
|
|
1414
|
-
const credentials = [];
|
|
1415
|
-
const activePlugin = studio.activeGooglePlugin;
|
|
1416
|
-
|
|
1417
|
-
// DEBUG LOGGING
|
|
1418
|
-
console.log('--- Auth Debug ---');
|
|
1419
|
-
console.log('Active Google Plugin:', activePlugin);
|
|
1420
|
-
console.log('Found keys in authCfg:', Object.keys(authCfg));
|
|
1421
|
-
if (authCfg.google) console.log('Google Auth found!');
|
|
1422
|
-
if (authCfg['google.antigravity']) console.log('google.antigravity found!');
|
|
1423
|
-
if (authCfg['google.gemini']) console.log('google.gemini found!');
|
|
1424
|
-
|
|
1425
1410
|
const providers = [
|
|
1426
1411
|
{ id: 'google', name: 'Google', type: 'oauth' },
|
|
1427
1412
|
{ id: 'anthropic', name: 'Anthropic', type: 'api' },
|
|
@@ -1436,6 +1421,15 @@ app.get('/api/auth', (req, res) => {
|
|
|
1436
1421
|
{ id: 'azure', name: 'Azure OpenAI', type: 'api' }
|
|
1437
1422
|
];
|
|
1438
1423
|
|
|
1424
|
+
providers.forEach(p => importCurrentAuthToPool(p.id));
|
|
1425
|
+
syncAntigravityPool();
|
|
1426
|
+
|
|
1427
|
+
const authCfg = loadAuthConfig() || {};
|
|
1428
|
+
const studio = loadStudioConfig();
|
|
1429
|
+
const ac = studio.activeProfiles || {};
|
|
1430
|
+
const credentials = [];
|
|
1431
|
+
const activePlugin = studio.activeGooglePlugin;
|
|
1432
|
+
|
|
1439
1433
|
const opencodeCfg = loadConfig();
|
|
1440
1434
|
const currentPlugins = opencodeCfg?.plugin || [];
|
|
1441
1435
|
|
|
@@ -1483,13 +1477,15 @@ app.get('/api/auth', (req, res) => {
|
|
|
1483
1477
|
});
|
|
1484
1478
|
|
|
1485
1479
|
app.get('/api/auth/profiles', (req, res) => {
|
|
1480
|
+
const providers = ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'];
|
|
1481
|
+
providers.forEach(p => importCurrentAuthToPool(p));
|
|
1486
1482
|
syncAntigravityPool();
|
|
1483
|
+
|
|
1487
1484
|
const authCfg = loadAuthConfig() || {};
|
|
1488
1485
|
const studio = loadStudioConfig();
|
|
1489
1486
|
const ac = studio.activeProfiles || {};
|
|
1490
1487
|
const activePlugin = studio.activeGooglePlugin;
|
|
1491
1488
|
const profiles = {};
|
|
1492
|
-
const providers = ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'];
|
|
1493
1489
|
|
|
1494
1490
|
providers.forEach(p => {
|
|
1495
1491
|
const saved = listAuthProfiles(p, activePlugin);
|
|
@@ -1882,6 +1878,78 @@ function listAntigravityAccounts() {
|
|
|
1882
1878
|
}));
|
|
1883
1879
|
}
|
|
1884
1880
|
|
|
1881
|
+
function decodeJWT(token) {
|
|
1882
|
+
try {
|
|
1883
|
+
const parts = token.split('.');
|
|
1884
|
+
if (parts.length !== 3) return null;
|
|
1885
|
+
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
|
|
1886
|
+
return JSON.parse(payload);
|
|
1887
|
+
} catch {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function importCurrentAuthToPool(provider) {
|
|
1893
|
+
if (provider === 'google') {
|
|
1894
|
+
return importCurrentGoogleAuthToPool();
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
const authCfg = loadAuthConfig();
|
|
1898
|
+
if (!authCfg || !authCfg[provider]) return;
|
|
1899
|
+
|
|
1900
|
+
const creds = authCfg[provider];
|
|
1901
|
+
let email = creds.email;
|
|
1902
|
+
|
|
1903
|
+
if (!email && provider === 'openai' && creds.access) {
|
|
1904
|
+
const decoded = decodeJWT(creds.access);
|
|
1905
|
+
if (decoded && decoded['https://api.openai.com/profile']?.email) {
|
|
1906
|
+
email = decoded['https://api.openai.com/profile'].email;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const name = email || creds.accountId || creds.id || `profile-${Date.now()}`;
|
|
1911
|
+
const profileDir = path.join(AUTH_PROFILES_DIR, provider);
|
|
1912
|
+
if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
|
|
1913
|
+
|
|
1914
|
+
const profilePath = path.join(profileDir, `${name}.json`);
|
|
1915
|
+
|
|
1916
|
+
let shouldSync = true;
|
|
1917
|
+
if (fs.existsSync(profilePath)) {
|
|
1918
|
+
try {
|
|
1919
|
+
const current = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
|
1920
|
+
if (JSON.stringify(current) === JSON.stringify(creds)) {
|
|
1921
|
+
shouldSync = false;
|
|
1922
|
+
}
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
if (shouldSync) {
|
|
1928
|
+
console.log(`[Auth] Syncing ${provider} login for ${name} to pool.`);
|
|
1929
|
+
atomicWriteFileSync(profilePath, JSON.stringify(creds, null, 2));
|
|
1930
|
+
|
|
1931
|
+
const metadata = loadPoolMetadata();
|
|
1932
|
+
if (!metadata[provider]) metadata[provider] = {};
|
|
1933
|
+
|
|
1934
|
+
metadata[provider][name] = {
|
|
1935
|
+
...(metadata[provider][name] || {}),
|
|
1936
|
+
email: email || null,
|
|
1937
|
+
createdAt: metadata[provider][name]?.createdAt || Date.now(),
|
|
1938
|
+
lastUsed: Date.now(),
|
|
1939
|
+
usageCount: metadata[provider][name]?.usageCount || 0,
|
|
1940
|
+
imported: true
|
|
1941
|
+
};
|
|
1942
|
+
savePoolMetadata(metadata);
|
|
1943
|
+
|
|
1944
|
+
const studio = loadStudioConfig();
|
|
1945
|
+
if (!studio.activeProfiles) studio.activeProfiles = {};
|
|
1946
|
+
if (studio.activeProfiles[provider] !== name) {
|
|
1947
|
+
studio.activeProfiles[provider] = name;
|
|
1948
|
+
saveStudioConfig(studio);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1885
1953
|
function importCurrentGoogleAuthToPool() {
|
|
1886
1954
|
const studio = loadStudioConfig();
|
|
1887
1955
|
// Only applies if Antigravity is active
|
|
@@ -1982,70 +2050,6 @@ function syncAntigravityPool() {
|
|
|
1982
2050
|
savePoolMetadata(metadata);
|
|
1983
2051
|
}
|
|
1984
2052
|
|
|
1985
|
-
function importExistingAuth() {
|
|
1986
|
-
const authCfg = loadAuthConfig();
|
|
1987
|
-
if (!authCfg) return;
|
|
1988
|
-
|
|
1989
|
-
const studio = loadStudioConfig();
|
|
1990
|
-
const activeProfiles = studio.activeProfiles || {};
|
|
1991
|
-
let changed = false;
|
|
1992
|
-
|
|
1993
|
-
// Standard providers to check
|
|
1994
|
-
const providers = ['google', 'openai', 'anthropic', 'xai', 'openrouter', 'github-copilot', 'mistral', 'deepseek', 'amazon-bedrock', 'azure'];
|
|
1995
|
-
|
|
1996
|
-
providers.forEach(provider => {
|
|
1997
|
-
if (!authCfg[provider]) return; // No creds for this provider
|
|
1998
|
-
|
|
1999
|
-
// Determine namespace
|
|
2000
|
-
// For auto-import, we target the standard 'google' namespace unless antigravity plugin is active?
|
|
2001
|
-
// Actually, auth.json 'google' key usually means Gemini/Vertex standard auth.
|
|
2002
|
-
const namespace = provider === 'google' && studio.activeGooglePlugin === 'antigravity'
|
|
2003
|
-
? 'google.antigravity'
|
|
2004
|
-
: (provider === 'google' ? 'google.gemini' : provider);
|
|
2005
|
-
|
|
2006
|
-
const profileDir = path.join(AUTH_PROFILES_DIR, namespace);
|
|
2007
|
-
|
|
2008
|
-
// If we already have an active profile for this provider, skip import
|
|
2009
|
-
if (activeProfiles[provider]) return;
|
|
2010
|
-
|
|
2011
|
-
// If directory exists and has files, check if empty
|
|
2012
|
-
if (fs.existsSync(profileDir) && fs.readdirSync(profileDir).filter(f => f.endsWith('.json')).length > 0) {
|
|
2013
|
-
return;
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// Import!
|
|
2017
|
-
if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
|
|
2018
|
-
|
|
2019
|
-
const email = authCfg[provider].email || null;
|
|
2020
|
-
const name = email || `imported-${Date.now()}`;
|
|
2021
|
-
const profilePath = path.join(profileDir, `${name}.json`);
|
|
2022
|
-
|
|
2023
|
-
console.log(`[AutoImport] Importing existing ${provider} credentials as ${name}`);
|
|
2024
|
-
atomicWriteFileSync(profilePath, JSON.stringify(authCfg[provider], null, 2));
|
|
2025
|
-
|
|
2026
|
-
// Set as active
|
|
2027
|
-
if (!studio.activeProfiles) studio.activeProfiles = {};
|
|
2028
|
-
studio.activeProfiles[provider] = name;
|
|
2029
|
-
changed = true;
|
|
2030
|
-
|
|
2031
|
-
// Update metadata
|
|
2032
|
-
const metadata = loadPoolMetadata();
|
|
2033
|
-
if (!metadata[namespace]) metadata[namespace] = {};
|
|
2034
|
-
metadata[namespace][name] = {
|
|
2035
|
-
email: email,
|
|
2036
|
-
createdAt: Date.now(),
|
|
2037
|
-
lastUsed: Date.now(),
|
|
2038
|
-
usageCount: 0,
|
|
2039
|
-
imported: true
|
|
2040
|
-
};
|
|
2041
|
-
savePoolMetadata(metadata);
|
|
2042
|
-
});
|
|
2043
|
-
|
|
2044
|
-
if (changed) {
|
|
2045
|
-
saveStudioConfig(studio);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
2053
|
function getAccountStatus(meta, now) {
|
|
2050
2054
|
if (!meta) return 'ready';
|
|
2051
2055
|
if (meta.cooldownUntil && meta.cooldownUntil > now) return 'cooldown';
|
|
@@ -2436,6 +2440,84 @@ app.post('/api/auth/pool/quota/limit', (req, res) => {
|
|
|
2436
2440
|
res.json({ success: true, dailyLimit: limit });
|
|
2437
2441
|
});
|
|
2438
2442
|
|
|
2443
|
+
app.get('/api/proxy/status', async (req, res) => {
|
|
2444
|
+
const status = await proxyManager.getStatus();
|
|
2445
|
+
res.json(status);
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
app.post('/api/proxy/start', async (req, res) => {
|
|
2449
|
+
const result = await proxyManager.startProxy();
|
|
2450
|
+
res.json(result);
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
app.post('/api/proxy/stop', async (req, res) => {
|
|
2454
|
+
const result = proxyManager.stopProxy();
|
|
2455
|
+
res.json(result);
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
app.post('/api/proxy/login', async (req, res) => {
|
|
2459
|
+
const { provider } = req.body;
|
|
2460
|
+
const result = await proxyManager.runLogin(provider);
|
|
2461
|
+
if (!result.success) return res.status(400).json(result);
|
|
2462
|
+
|
|
2463
|
+
const cmd = result.command;
|
|
2464
|
+
const cp = getConfigPath();
|
|
2465
|
+
const configDir = cp ? path.dirname(cp) : process.cwd();
|
|
2466
|
+
const safeDir = configDir.replace(/"/g, '\\"');
|
|
2467
|
+
const platform = process.platform;
|
|
2468
|
+
|
|
2469
|
+
if (platform === 'win32') {
|
|
2470
|
+
const terminalCmd = `start "" /d "${safeDir}" cmd /c "call ${cmd} || pause"`;
|
|
2471
|
+
exec(terminalCmd, (err) => {
|
|
2472
|
+
if (err) return res.status(500).json({ error: 'Failed to open terminal', details: err.message });
|
|
2473
|
+
res.json({ success: true, message: 'Terminal opened' });
|
|
2474
|
+
});
|
|
2475
|
+
} else if (platform === 'darwin') {
|
|
2476
|
+
const terminalCmd = `osascript -e 'tell application "Terminal" to do script "cd ${safeDir} && ${cmd}"'`;
|
|
2477
|
+
exec(terminalCmd, (err) => {
|
|
2478
|
+
if (err) return res.status(500).json({ error: 'Failed to open terminal', details: err.message });
|
|
2479
|
+
res.json({ success: true, message: 'Terminal opened' });
|
|
2480
|
+
});
|
|
2481
|
+
} else {
|
|
2482
|
+
const linuxTerminals = [
|
|
2483
|
+
{ name: 'x-terminal-emulator', cmd: `x-terminal-emulator -e "bash -c 'cd ${safeDir} && ${cmd}'"` },
|
|
2484
|
+
{ name: 'gnome-terminal', cmd: `gnome-terminal -- bash -c "cd ${safeDir} && ${cmd}; read -p 'Press Enter to close...'"` },
|
|
2485
|
+
{ name: 'konsole', cmd: `konsole -e bash -c "cd ${safeDir} && ${cmd}; read -p 'Press Enter to close...'"` },
|
|
2486
|
+
{ name: 'xfce4-terminal', cmd: `xfce4-terminal -e "bash -c \"cd ${safeDir} && ${cmd}; read -p 'Press Enter to close...'\"" ` },
|
|
2487
|
+
{ name: 'xterm', cmd: `xterm -e "bash -c 'cd ${safeDir} && ${cmd}; read -p Press_Enter_to_close...'"` }
|
|
2488
|
+
];
|
|
2489
|
+
|
|
2490
|
+
const tryTerminal = (index) => {
|
|
2491
|
+
if (index >= linuxTerminals.length) {
|
|
2492
|
+
return res.json({
|
|
2493
|
+
success: false,
|
|
2494
|
+
message: 'No terminal emulator found',
|
|
2495
|
+
note: 'Run this command manually:',
|
|
2496
|
+
command: cmd
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
const terminal = linuxTerminals[index];
|
|
2500
|
+
exec(terminal.cmd, (err) => {
|
|
2501
|
+
if (err) {
|
|
2502
|
+
tryTerminal(index + 1);
|
|
2503
|
+
} else {
|
|
2504
|
+
res.json({ success: true, message: 'Terminal opened' });
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
};
|
|
2508
|
+
tryTerminal(0);
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
app.get('/api/proxy/config', (req, res) => {
|
|
2513
|
+
res.json(proxyManager.loadProxyConfig());
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
app.post('/api/proxy/config', (req, res) => {
|
|
2517
|
+
proxyManager.saveProxyConfig(req.body);
|
|
2518
|
+
res.json({ success: true });
|
|
2519
|
+
});
|
|
2520
|
+
|
|
2439
2521
|
// ============================================
|
|
2440
2522
|
// END ACCOUNT POOL MANAGEMENT
|
|
2441
2523
|
// ============================================
|
|
@@ -3035,8 +3117,7 @@ app.post('/api/presets/:id/apply', (req, res) => {
|
|
|
3035
3117
|
|
|
3036
3118
|
// Start watcher on server start
|
|
3037
3119
|
function startServer() {
|
|
3038
|
-
|
|
3039
|
-
importExistingAuth();
|
|
3120
|
+
['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'].forEach(p => importCurrentAuthToPool(p));
|
|
3040
3121
|
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
|
|
3041
3122
|
}
|
|
3042
3123
|
|
|
@@ -3053,4 +3134,27 @@ module.exports = {
|
|
|
3053
3134
|
loadStudioConfig,
|
|
3054
3135
|
saveStudioConfig,
|
|
3055
3136
|
buildAccountPool
|
|
3056
|
-
};
|
|
3137
|
+
};
|
|
3138
|
+
app.get('/api/prompts/global', (req, res) => {
|
|
3139
|
+
const cp = getConfigPath();
|
|
3140
|
+
const globalPath = path.join(path.dirname(cp), 'OPENCODE.md');
|
|
3141
|
+
|
|
3142
|
+
if (fs.existsSync(globalPath)) {
|
|
3143
|
+
res.json({ content: fs.readFileSync(globalPath, 'utf8') });
|
|
3144
|
+
} else {
|
|
3145
|
+
res.json({ content: '' });
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
|
|
3149
|
+
app.post('/api/prompts/global', (req, res) => {
|
|
3150
|
+
const { content } = req.body;
|
|
3151
|
+
const cp = getConfigPath();
|
|
3152
|
+
const globalPath = path.join(path.dirname(cp), 'OPENCODE.md');
|
|
3153
|
+
|
|
3154
|
+
try {
|
|
3155
|
+
atomicWriteFileSync(globalPath, content);
|
|
3156
|
+
res.json({ success: true });
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
res.status(500).json({ error: err.message });
|
|
3159
|
+
}
|
|
3160
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-studio-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Backend server for OpenCode Studio - manages opencode configurations",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"body-parser": "^2.2.1",
|
|
29
29
|
"cors": "^2.8.5",
|
|
30
|
-
"express": "^5.2.1"
|
|
30
|
+
"express": "^5.2.1",
|
|
31
|
+
"js-yaml": "^4.1.1"
|
|
31
32
|
}
|
|
32
33
|
}
|
package/proxy-manager.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { spawn, exec } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
|
|
7
|
+
const HOME_DIR = os.homedir();
|
|
8
|
+
const CONFIG_DIR = path.join(HOME_DIR, '.config', 'opencode-studio');
|
|
9
|
+
const PROXY_CONFIG_FILE = path.join(CONFIG_DIR, 'cliproxy.yaml');
|
|
10
|
+
const PROXY_AUTH_DIR = path.join(HOME_DIR, '.cli-proxy-api');
|
|
11
|
+
|
|
12
|
+
let proxyProcess = null;
|
|
13
|
+
let isProxyRunning = false;
|
|
14
|
+
|
|
15
|
+
// Helper to check if binary exists
|
|
16
|
+
const checkBinary = (cmd) => {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
|
|
19
|
+
exec(checkCmd, (err) => {
|
|
20
|
+
resolve(!err);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getProxyCommand = async () => {
|
|
26
|
+
if (await checkBinary('cliproxyapi')) return 'cliproxyapi';
|
|
27
|
+
if (await checkBinary('cliproxy')) return 'cliproxy';
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Config Management
|
|
32
|
+
const loadProxyConfig = () => {
|
|
33
|
+
if (!fs.existsSync(PROXY_CONFIG_FILE)) {
|
|
34
|
+
// Default config
|
|
35
|
+
const defaultConfig = {
|
|
36
|
+
port: 8317,
|
|
37
|
+
"auth-dir": PROXY_AUTH_DIR,
|
|
38
|
+
routing: { strategy: "round-robin" },
|
|
39
|
+
"quota-exceeded": {
|
|
40
|
+
"switch-project": true,
|
|
41
|
+
"switch-preview-model": true
|
|
42
|
+
},
|
|
43
|
+
"gemini-api-key": []
|
|
44
|
+
};
|
|
45
|
+
saveProxyConfig(defaultConfig);
|
|
46
|
+
return defaultConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(PROXY_CONFIG_FILE, 'utf8');
|
|
51
|
+
return yaml.load(content);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("Failed to load proxy config:", e);
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const saveProxyConfig = (config) => {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
61
|
+
const content = yaml.dump(config);
|
|
62
|
+
fs.writeFileSync(PROXY_CONFIG_FILE, content);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error("Failed to save proxy config:", e);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Process Management
|
|
69
|
+
const startProxy = async () => {
|
|
70
|
+
if (isProxyRunning) return { success: true, message: "Already running" };
|
|
71
|
+
|
|
72
|
+
const cmd = await getProxyCommand();
|
|
73
|
+
if (!cmd) return { success: false, error: "CLIProxyAPI binary not found. Please install it." };
|
|
74
|
+
|
|
75
|
+
// Ensure config exists
|
|
76
|
+
if (!fs.existsSync(PROXY_CONFIG_FILE)) loadProxyConfig();
|
|
77
|
+
|
|
78
|
+
console.log(`Starting proxy with command: ${cmd} -config ${PROXY_CONFIG_FILE}`);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// -no-browser flag to prevent it from trying to open browser on startup if that's a thing
|
|
82
|
+
proxyProcess = spawn(cmd, ['-config', PROXY_CONFIG_FILE], {
|
|
83
|
+
detached: false,
|
|
84
|
+
stdio: 'pipe'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
proxyProcess.stdout.on('data', (data) => {
|
|
88
|
+
console.log(`[Proxy] ${data}`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
proxyProcess.stderr.on('data', (data) => {
|
|
92
|
+
console.error(`[Proxy Err] ${data}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
proxyProcess.on('close', (code) => {
|
|
96
|
+
console.log(`[Proxy] Exited with code ${code}`);
|
|
97
|
+
isProxyRunning = false;
|
|
98
|
+
proxyProcess = null;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
isProxyRunning = true;
|
|
102
|
+
return { success: true, pid: proxyProcess.pid };
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { success: false, error: e.message };
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const stopProxy = () => {
|
|
109
|
+
if (proxyProcess) {
|
|
110
|
+
proxyProcess.kill();
|
|
111
|
+
proxyProcess = null;
|
|
112
|
+
isProxyRunning = false;
|
|
113
|
+
return { success: true };
|
|
114
|
+
}
|
|
115
|
+
return { success: false, error: "Not running" };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getStatus = async () => {
|
|
119
|
+
const cmd = await getProxyCommand();
|
|
120
|
+
return {
|
|
121
|
+
running: isProxyRunning,
|
|
122
|
+
pid: proxyProcess?.pid,
|
|
123
|
+
configFile: PROXY_CONFIG_FILE,
|
|
124
|
+
port: 8317,
|
|
125
|
+
installed: !!cmd,
|
|
126
|
+
binary: cmd
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const runLogin = async (provider) => {
|
|
131
|
+
const cmd = await getProxyCommand();
|
|
132
|
+
if (!cmd) return { success: false, error: "Binary not found" };
|
|
133
|
+
|
|
134
|
+
let loginFlag = '';
|
|
135
|
+
switch(provider) {
|
|
136
|
+
case 'google':
|
|
137
|
+
case 'antigravity': loginFlag = '-antigravity-login'; break;
|
|
138
|
+
case 'openai':
|
|
139
|
+
case 'codex': loginFlag = '-codex-login'; break;
|
|
140
|
+
case 'anthropic': loginFlag = '-claude-login'; break;
|
|
141
|
+
default: return { success: false, error: "Unknown provider" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Return the command so the UI can spawn a terminal for it
|
|
145
|
+
// We pass the config file so it saves the token to the right place/knows the auth-dir
|
|
146
|
+
const fullCmd = `${cmd} ${loginFlag} -config "${PROXY_CONFIG_FILE}"`;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
command: fullCmd,
|
|
151
|
+
message: "Terminal launching..."
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
startProxy,
|
|
157
|
+
stopProxy,
|
|
158
|
+
getStatus,
|
|
159
|
+
loadProxyConfig,
|
|
160
|
+
saveProxyConfig,
|
|
161
|
+
runLogin
|
|
162
|
+
};
|