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 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
- // setupLogWatcher(); // Disabled as per user request (manual switching only)
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.12.13",
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
  }
@@ -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
+ };