opencode-studio-server 1.27.0 → 1.28.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.
Files changed (4) hide show
  1. package/AGENTS.md +0 -1
  2. package/README.md +0 -1
  3. package/index.js +328 -247
  4. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -7,7 +7,6 @@ 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 |
11
10
  | `cli.js` | npm bin entry, protocol URL parser, pending action queue |
12
11
  | `register-protocol.js` | OS-specific `opencodestudio://` handler registration |
13
12
 
package/README.md CHANGED
@@ -31,7 +31,6 @@ 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
35
34
  - **Auth**: Manage authentication profiles
36
35
 
37
36
  ## License
package/index.js CHANGED
@@ -341,8 +341,9 @@ function loadStudioConfig() {
341
341
  "variants": {
342
342
  "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
343
343
  "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
344
- "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } }
345
- }
344
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
345
+ "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
346
+ }
346
347
  },
347
348
  "gemini-3-flash": {
348
349
  "id": "gemini-3-flash",
@@ -359,8 +360,9 @@ function loadStudioConfig() {
359
360
  "minimal": { "options": { "thinkingConfig": { "thinkingLevel": "minimal", "includeThoughts": true } } },
360
361
  "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
361
362
  "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
362
- "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } }
363
- }
363
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
364
+ "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
365
+ }
364
366
  },
365
367
  "gemini-2.5-flash-lite": {
366
368
  "id": "gemini-2.5-flash-lite",
@@ -381,8 +383,9 @@ function loadStudioConfig() {
381
383
  "minimal": { "options": { "thinkingConfig": { "thinkingLevel": "minimal", "includeThoughts": true } } },
382
384
  "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
383
385
  "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
384
- "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } }
385
- }
386
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
387
+ "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
388
+ }
386
389
  },
387
390
  "opencode/glm-4.7-free": {
388
391
  "id": "opencode/glm-4.7-free",
@@ -408,8 +411,9 @@ function loadStudioConfig() {
408
411
  "none": { "reasoning": false, "options": { "thinkingConfig": { "includeThoughts": false } } },
409
412
  "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
410
413
  "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
411
- "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } }
412
- }
414
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } },
415
+ "xhigh": { "options": { "thinkingConfig": { "thinkingBudget": 64000, "includeThoughts": true } } }
416
+ }
413
417
  },
414
418
  "gemini-claude-opus-4-5-thinking": {
415
419
  "id": "gemini-claude-opus-4-5-thinking",
@@ -424,8 +428,9 @@ function loadStudioConfig() {
424
428
  "variants": {
425
429
  "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
426
430
  "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
427
- "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } }
428
- }
431
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } },
432
+ "xhigh": { "options": { "thinkingConfig": { "thinkingBudget": 64000, "includeThoughts": true } } }
433
+ }
429
434
  }
430
435
  }
431
436
  }
@@ -461,6 +466,16 @@ function saveStudioConfig(config) {
461
466
  }
462
467
  }
463
468
 
469
+ const getWslDistributions = () => {
470
+ try {
471
+ const { execSync } = require('child_process');
472
+ const stdout = execSync('wsl.exe -l -q', { encoding: 'utf16le', stdio: ['ignore', 'pipe', 'pipe'], timeout: 1000 });
473
+ return stdout.trim().split('\n').filter(d => d.length > 0);
474
+ } catch {
475
+ return [];
476
+ }
477
+ };
478
+
464
479
  const getPaths = () => {
465
480
  const platform = process.platform;
466
481
  const home = os.homedir();
@@ -473,6 +488,13 @@ const getPaths = () => {
473
488
  candidates.push(path.join(process.env.APPDATA, 'opencode', 'opencode.json'));
474
489
  }
475
490
 
491
+ if (platform === 'win32') {
492
+ const distros = getWslDistributions();
493
+ for (const distro of distros) {
494
+ candidates.push(`\\\\wsl$\\${distro}\\home\\${os.userInfo().username}\\.config\\opencode\\opencode.json`);
495
+ }
496
+ }
497
+
476
498
  const studioConfig = loadStudioConfig();
477
499
  const manualPath = studioConfig.configPath;
478
500
 
@@ -594,14 +616,15 @@ app.get('/api/config', (req, res) => {
594
616
  res.json(config);
595
617
  });
596
618
 
597
- app.post('/api/config', (req, res) => {
598
- try {
599
- saveConfig(req.body);
600
- res.json({ success: true });
601
- } catch (err) {
602
- res.status(500).json({ error: err.message });
603
- }
604
- });
619
+ app.post('/api/config', (req, res) => {
620
+ try {
621
+ saveConfig(req.body);
622
+ triggerGitHubAutoSync();
623
+ res.json({ success: true });
624
+ } catch (err) {
625
+ res.status(500).json({ error: err.message });
626
+ }
627
+ });
605
628
 
606
629
  app.get('/api/backup', (req, res) => {
607
630
  try {
@@ -1020,56 +1043,57 @@ app.get('/api/ohmyopencode', (req, res) => {
1020
1043
  res.json({ path: ohMyPath, exists, config, preferences });
1021
1044
  });
1022
1045
 
1023
- app.post('/api/ohmyopencode', (req, res) => {
1024
- try {
1025
- const { preferences } = req.body;
1026
- if (!preferences || !preferences.agents) {
1027
- return res.status(400).json({ error: 'Missing preferences.agents' });
1028
- }
1029
-
1030
- const studio = loadStudioConfig();
1031
- studio.ohmy = preferences;
1032
- saveStudioConfig(studio);
1033
-
1034
- const currentConfig = loadOhMyOpenCodeConfig() || {};
1035
- const warnings = [];
1036
-
1037
- for (const [agentName, agentPrefs] of Object.entries(preferences.agents)) {
1038
- const choices = agentPrefs.choices || [];
1039
- const available = choices.find(c => c.available);
1040
- if (available) {
1041
- if (!currentConfig.agents) currentConfig.agents = {};
1042
- const agentConfig = { model: available.model };
1043
-
1044
- if (available.thinking && available.thinking.type === 'enabled') {
1045
- agentConfig.thinking = { type: 'enabled' };
1046
- }
1047
-
1048
- if (available.reasoning && available.reasoning.effort) {
1049
- agentConfig.reasoning = { effort: available.reasoning.effort };
1050
- }
1051
-
1052
- currentConfig.agents[agentName] = agentConfig;
1053
- } else if (choices.length > 0) {
1054
- warnings.push(`No available model for agent "${agentName}"`);
1055
- }
1056
- }
1057
-
1058
- saveOhMyOpenCodeConfig(currentConfig);
1059
-
1046
+ app.post('/api/ohmyopencode', (req, res) => {
1047
+ try {
1048
+ const { preferences } = req.body;
1049
+ if (!preferences || !preferences.agents) {
1050
+ return res.status(400).json({ error: 'Missing preferences.agents' });
1051
+ }
1052
+
1053
+ const studio = loadStudioConfig();
1054
+ studio.ohmy = preferences;
1055
+ saveStudioConfig(studio);
1056
+
1057
+ const currentConfig = loadOhMyOpenCodeConfig() || {};
1058
+ const warnings = [];
1059
+
1060
+ for (const [agentName, agentPrefs] of Object.entries(preferences.agents)) {
1061
+ const choices = agentPrefs.choices || [];
1062
+ const available = choices.find(c => c.available);
1063
+ if (available) {
1064
+ if (!currentConfig.agents) currentConfig.agents = {};
1065
+ const agentConfig = { model: available.model };
1066
+
1067
+ if (available.thinking && available.thinking.type === 'enabled') {
1068
+ agentConfig.thinking = { type: 'enabled' };
1069
+ }
1070
+
1071
+ if (available.reasoning && available.reasoning.effort) {
1072
+ agentConfig.reasoning = { effort: available.reasoning.effort };
1073
+ }
1074
+
1075
+ currentConfig.agents[agentName] = agentConfig;
1076
+ } else if (choices.length > 0) {
1077
+ warnings.push(`No available model for agent "${agentName}"`);
1078
+ }
1079
+ }
1080
+
1081
+ saveOhMyOpenCodeConfig(currentConfig);
1082
+
1060
1083
  const ohMyPath = getOhMyOpenCodeConfigPath();
1084
+ triggerGitHubAutoSync();
1061
1085
  res.json({
1062
1086
  success: true,
1063
- path: ohMyPath,
1064
- exists: true,
1065
- config: currentConfig,
1066
- preferences,
1067
- warnings: warnings.length > 0 ? warnings : undefined
1068
- });
1069
- } catch (err) {
1070
- res.status(500).json({ error: err.message });
1071
- }
1072
- });
1087
+ path: ohMyPath,
1088
+ exists: true,
1089
+ config: currentConfig,
1090
+ preferences,
1091
+ warnings: warnings.length > 0 ? warnings : undefined
1092
+ });
1093
+ } catch (err) {
1094
+ res.status(500).json({ error: err.message });
1095
+ }
1096
+ });
1073
1097
 
1074
1098
  // ============================================
1075
1099
  // GITHUB BACKUP
@@ -1095,130 +1119,102 @@ async function getGitHubUser(token) {
1095
1119
  return await response.json();
1096
1120
  }
1097
1121
 
1098
- async function ensureGitHubRepo(token, repoName) {
1099
- const [owner, repo] = repoName.split('/');
1100
-
1101
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
1102
- headers: { 'Authorization': `Bearer ${token}` }
1103
- });
1104
-
1105
- if (response.ok) {
1106
- return await response.json();
1107
- }
1108
-
1109
- if (response.status === 404) {
1110
- const createRes = await fetch(`https://api.github.com/user/repos`, {
1111
- method: 'POST',
1112
- headers: {
1113
- 'Authorization': `Bearer ${token}`,
1114
- 'Content-Type': 'application/json'
1115
- },
1116
- body: JSON.stringify({
1117
- name: repo,
1118
- private: true,
1119
- description: 'OpenCode Studio backup',
1120
- auto_init: true
1121
- })
1122
- });
1123
-
1124
- if (createRes.ok) {
1125
- await new Promise(r => setTimeout(r, 2000));
1126
- return await createRes.json();
1127
- }
1128
-
1129
- const err = await createRes.text();
1130
- throw new Error(`Failed to create repo: ${err}`);
1131
- }
1132
-
1133
- const err = await response.text();
1134
- throw new Error(`Failed to check repo: ${err}`);
1135
- }
1136
-
1137
- function copyDirContents(src, dest) {
1138
- if (!fs.existsSync(src)) return;
1139
- fs.mkdirSync(dest, { recursive: true });
1140
- const SKIP_DIRS = ['node_modules', '.git', '.next', 'cache'];
1141
- const SKIP_EXT = ['.log', '.tmp', '.db', '.sqlite', '.cache', '.pack', '.idx'];
1142
-
1143
- for (const name of fs.readdirSync(src)) {
1144
- const srcPath = path.join(src, name);
1145
- const destPath = path.join(dest, name);
1146
- const stat = fs.statSync(srcPath);
1147
-
1148
- if (stat.isDirectory()) {
1149
- if (SKIP_DIRS.includes(name)) continue;
1150
- copyDirContents(srcPath, destPath);
1151
- } else {
1152
- if (SKIP_EXT.some(ext => name.endsWith(ext))) continue;
1153
- fs.copyFileSync(srcPath, destPath);
1154
- }
1155
- }
1156
- }
1157
-
1158
- function execPromise(cmd, opts = {}) {
1159
- return new Promise((resolve, reject) => {
1160
- exec(cmd, opts, (err, stdout, stderr) => {
1161
- if (err) reject(new Error(stderr || err.message));
1162
- else resolve(stdout.trim());
1163
- });
1164
- });
1165
- }
1166
-
1167
- app.get('/api/github/backup/status', async (req, res) => {
1168
- try {
1169
- const token = await getGitHubToken();
1170
- const studio = loadStudioConfig();
1171
- const backupConfig = studio.githubBackup || {};
1172
-
1173
- if (!token) return res.json({ connected: false, config: backupConfig, error: 'Not logged in to gh CLI. Run: gh auth login' });
1174
-
1175
- const user = await getGitHubUser(token);
1176
- if (!user) return res.json({ connected: false, config: backupConfig, error: 'Failed to get GitHub user' });
1177
-
1178
- if (!backupConfig.repo) {
1179
- return res.json({ connected: true, user: user.login, config: backupConfig, autoSync: studio.githubAutoSync || false });
1180
- }
1181
-
1182
- const owner = backupConfig.owner || user.login;
1183
- const response = await fetch(`https://api.github.com/repos/${owner}/${backupConfig.repo}`, {
1184
- headers: { 'Authorization': `Bearer ${token}` }
1122
+ async function ensureGitHubRepo(token, repoName) {
1123
+ const [owner, repo] = repoName.split('/');
1124
+
1125
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
1126
+ headers: { 'Authorization': `Bearer ${token}` }
1127
+ });
1128
+
1129
+ if (response.ok) {
1130
+ return await response.json();
1131
+ }
1132
+
1133
+ if (response.status === 404) {
1134
+ const createRes = await fetch(`https://api.github.com/user/repos`, {
1135
+ method: 'POST',
1136
+ headers: {
1137
+ 'Authorization': `Bearer ${token}`,
1138
+ 'Content-Type': 'application/json'
1139
+ },
1140
+ body: JSON.stringify({
1141
+ name: repo,
1142
+ private: true,
1143
+ description: 'OpenCode Studio backup',
1144
+ auto_init: true
1145
+ })
1185
1146
  });
1186
1147
 
1187
- if (!response.ok) {
1188
- return res.json({ connected: true, user: user.login, config: backupConfig, repoExists: false, autoSync: studio.githubAutoSync || false });
1189
- }
1148
+ if (createRes.ok) {
1149
+ await new Promise(r => setTimeout(r, 2000));
1150
+ return await createRes.json();
1151
+ }
1190
1152
 
1191
- const data = await response.json();
1192
- res.json({ connected: true, user: user.login, config: backupConfig, repoExists: true, lastUpdated: data.pushed_at, autoSync: studio.githubAutoSync || false });
1193
- } catch (err) {
1194
- res.json({ connected: false, error: err.message });
1153
+ const err = await createRes.text();
1154
+ throw new Error(`Failed to create repo: ${err}`);
1195
1155
  }
1196
- });
1156
+
1157
+ const err = await response.text();
1158
+ throw new Error(`Failed to check repo: ${err}`);
1159
+ }
1197
1160
 
1198
- app.post('/api/github/backup', async (req, res) => {
1161
+ function copyDirContents(src, dest) {
1162
+ if (!fs.existsSync(src)) return;
1163
+ fs.mkdirSync(dest, { recursive: true });
1164
+ const SKIP_DIRS = ['node_modules', '.git', '.next', 'cache'];
1165
+ const SKIP_EXT = ['.log', '.tmp', '.db', '.sqlite', '.cache', '.pack', '.idx'];
1166
+
1167
+ for (const name of fs.readdirSync(src)) {
1168
+ const srcPath = path.join(src, name);
1169
+ const destPath = path.join(dest, name);
1170
+ const stat = fs.statSync(srcPath);
1171
+
1172
+ if (stat.isDirectory()) {
1173
+ if (SKIP_DIRS.includes(name)) continue;
1174
+ copyDirContents(srcPath, destPath);
1175
+ } else {
1176
+ if (SKIP_EXT.some(ext => name.endsWith(ext))) continue;
1177
+ fs.copyFileSync(srcPath, destPath);
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ function execPromise(cmd, opts = {}) {
1183
+ return new Promise((resolve, reject) => {
1184
+ exec(cmd, opts, (err, stdout, stderr) => {
1185
+ if (err) reject(new Error(stderr || err.message));
1186
+ else resolve(stdout.trim());
1187
+ });
1188
+ });
1189
+ }
1190
+
1191
+
1192
+ let autoSyncTimer = null;
1193
+
1194
+ async function performGitHubBackup(options = {}) {
1195
+ const { owner, repo, branch } = options;
1199
1196
  let tempDir = null;
1200
1197
  try {
1201
1198
  const token = await getGitHubToken();
1202
- if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
1199
+ if (!token) throw new Error('Not logged in to gh CLI. Run: gh auth login');
1203
1200
 
1204
1201
  const user = await getGitHubUser(token);
1205
- if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
1202
+ if (!user) throw new Error('Failed to get GitHub user');
1206
1203
 
1207
- const { owner, repo, branch } = req.body;
1208
1204
  const studio = loadStudioConfig();
1209
1205
 
1210
1206
  const finalOwner = owner || studio.githubBackup?.owner || user.login;
1211
1207
  const finalRepo = repo || studio.githubBackup?.repo;
1212
1208
  const finalBranch = branch || studio.githubBackup?.branch || 'main';
1213
1209
 
1214
- if (!finalRepo) return res.status(400).json({ error: 'No repo name provided' });
1210
+ if (!finalRepo) throw new Error('No repo name provided');
1215
1211
 
1216
1212
  const repoName = `${finalOwner}/${finalRepo}`;
1217
1213
 
1218
1214
  await ensureGitHubRepo(token, repoName);
1219
1215
 
1220
1216
  const opencodeConfig = getConfigPath();
1221
- if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
1217
+ if (!opencodeConfig) throw new Error('No opencode config path found');
1222
1218
 
1223
1219
  const opencodeDir = path.dirname(opencodeConfig);
1224
1220
  const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
@@ -1226,7 +1222,15 @@ app.post('/api/github/backup', async (req, res) => {
1226
1222
  tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1227
1223
  fs.mkdirSync(tempDir, { recursive: true });
1228
1224
 
1229
- await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1225
+ // Clone or init
1226
+ try {
1227
+ await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1228
+ } catch (e) {
1229
+ // If clone fails (empty repo?), try init
1230
+ await execPromise('git init', { cwd: tempDir });
1231
+ await execPromise(`git remote add origin https://x-access-token:${token}@github.com/${repoName}.git`, { cwd: tempDir });
1232
+ await execPromise(`git checkout -b ${finalBranch}`, { cwd: tempDir });
1233
+ }
1230
1234
 
1231
1235
  const backupOpencodeDir = path.join(tempDir, 'opencode');
1232
1236
  const backupStudioDir = path.join(tempDir, 'opencode-studio');
@@ -1242,94 +1246,154 @@ app.post('/api/github/backup', async (req, res) => {
1242
1246
  const timestamp = new Date().toISOString();
1243
1247
  const commitMessage = `OpenCode Studio backup ${timestamp}`;
1244
1248
 
1249
+ let result = { success: true, timestamp, url: `https://github.com/${repoName}` };
1250
+
1245
1251
  try {
1246
1252
  await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
1247
1253
  await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
1248
1254
  } catch (e) {
1249
1255
  if (e.message.includes('nothing to commit')) {
1250
- fs.rmSync(tempDir, { recursive: true });
1251
- return res.json({ success: true, timestamp, message: 'No changes to backup', url: `https://github.com/${repoName}` });
1256
+ result.message = 'No changes to backup';
1257
+ } else {
1258
+ throw e;
1252
1259
  }
1253
- throw e;
1254
1260
  }
1255
1261
 
1256
- fs.rmSync(tempDir, { recursive: true });
1257
-
1258
1262
  studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1259
1263
  studio.lastGithubBackup = timestamp;
1260
1264
  saveStudioConfig(studio);
1261
1265
 
1262
- res.json({ success: true, timestamp, url: `https://github.com/${repoName}` });
1263
- } catch (err) {
1264
- if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
1265
- console.error('GitHub backup error:', err);
1266
- res.status(500).json({ error: err.message });
1266
+ return result;
1267
+ } finally {
1268
+ if (tempDir && fs.existsSync(tempDir)) {
1269
+ try { fs.rmSync(tempDir, { recursive: true }); } catch (e) {}
1270
+ }
1267
1271
  }
1268
- });
1272
+ }
1269
1273
 
1270
- app.post('/api/github/restore', async (req, res) => {
1271
- let tempDir = null;
1272
- try {
1273
- const token = await getGitHubToken();
1274
- if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
1275
-
1276
- const user = await getGitHubUser(token);
1277
- if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
1278
-
1279
- const { owner, repo, branch } = req.body;
1280
- const studio = loadStudioConfig();
1281
-
1282
- const finalOwner = owner || studio.githubBackup?.owner || user.login;
1283
- const finalRepo = repo || studio.githubBackup?.repo;
1284
- const finalBranch = branch || studio.githubBackup?.branch || 'main';
1285
-
1286
- if (!finalRepo) return res.status(400).json({ error: 'No repo configured' });
1287
-
1288
- const repoName = `${finalOwner}/${finalRepo}`;
1289
-
1290
- const opencodeConfig = getConfigPath();
1291
- if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
1292
-
1293
- const opencodeDir = path.dirname(opencodeConfig);
1294
- const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1295
-
1296
- tempDir = path.join(os.tmpdir(), `opencode-restore-${Date.now()}`);
1297
- fs.mkdirSync(tempDir, { recursive: true });
1298
-
1299
- await execPromise(`git clone --depth 1 -b ${finalBranch} https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1300
-
1301
- const backupOpencodeDir = path.join(tempDir, 'opencode');
1302
- const backupStudioDir = path.join(tempDir, 'opencode-studio');
1303
-
1304
- if (!fs.existsSync(backupOpencodeDir)) {
1305
- fs.rmSync(tempDir, { recursive: true });
1306
- return res.status(400).json({ error: 'No opencode backup found in repository' });
1307
- }
1308
-
1309
- copyDirContents(backupOpencodeDir, opencodeDir);
1310
- if (fs.existsSync(backupStudioDir)) {
1311
- copyDirContents(backupStudioDir, studioDir);
1274
+ function triggerGitHubAutoSync() {
1275
+ const studio = loadStudioConfig();
1276
+ if (!studio.githubAutoSync) return;
1277
+
1278
+ if (autoSyncTimer) clearTimeout(autoSyncTimer);
1279
+
1280
+ console.log('[AutoSync] Change detected, scheduling GitHub backup in 15s...');
1281
+ autoSyncTimer = setTimeout(async () => {
1282
+ console.log('[AutoSync] Starting GitHub backup...');
1283
+ try {
1284
+ const result = await performGitHubBackup();
1285
+ console.log(`[AutoSync] Backup completed: ${result.message || 'Pushed to GitHub'}`);
1286
+ } catch (err) {
1287
+ console.error('[AutoSync] Backup failed:', err.message);
1312
1288
  }
1313
-
1314
- fs.rmSync(tempDir, { recursive: true });
1315
-
1316
- res.json({ success: true, message: 'Config restored from GitHub' });
1289
+ }, 15000); // 15s debounce
1290
+ }
1291
+
1292
+ app.get('/api/github/backup/status', async (req, res) => {
1293
+ try {
1294
+ const token = await getGitHubToken();
1295
+ const studio = loadStudioConfig();
1296
+ const backupConfig = studio.githubBackup || {};
1297
+
1298
+ if (!token) return res.json({ connected: false, config: backupConfig, error: 'Not logged in to gh CLI. Run: gh auth login' });
1299
+
1300
+ const user = await getGitHubUser(token);
1301
+ if (!user) return res.json({ connected: false, config: backupConfig, error: 'Failed to get GitHub user' });
1302
+
1303
+ if (!backupConfig.repo) {
1304
+ return res.json({ connected: true, user: user.login, config: backupConfig, autoSync: studio.githubAutoSync || false });
1305
+ }
1306
+
1307
+ const owner = backupConfig.owner || user.login;
1308
+ const response = await fetch(`https://api.github.com/repos/${owner}/${backupConfig.repo}`, {
1309
+ headers: { 'Authorization': `Bearer ${token}` }
1310
+ });
1311
+
1312
+ if (!response.ok) {
1313
+ return res.json({ connected: true, user: user.login, config: backupConfig, repoExists: false, autoSync: studio.githubAutoSync || false });
1314
+ }
1315
+
1316
+ const data = await response.json();
1317
+ res.json({ connected: true, user: user.login, config: backupConfig, repoExists: true, lastUpdated: data.pushed_at, autoSync: studio.githubAutoSync || false });
1318
+ } catch (err) {
1319
+ res.json({ connected: false, error: err.message });
1320
+ }
1321
+ });
1322
+
1323
+ app.post('/api/github/backup', async (req, res) => {
1324
+ try {
1325
+ const result = await performGitHubBackup(req.body);
1326
+ res.json(result);
1317
1327
  } catch (err) {
1318
- if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
1319
- console.error('GitHub restore error:', err);
1328
+ console.error('GitHub backup error:', err);
1320
1329
  res.status(500).json({ error: err.message });
1321
1330
  }
1322
1331
  });
1323
-
1324
- app.post('/api/github/autosync', async (req, res) => {
1325
- const studio = loadStudioConfig();
1326
- const enabled = req.body.enabled;
1332
+
1333
+ app.post('/api/github/restore', async (req, res) => {
1334
+ let tempDir = null;
1335
+ try {
1336
+ const token = await getGitHubToken();
1337
+ if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
1338
+
1339
+ const user = await getGitHubUser(token);
1340
+ if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
1341
+
1342
+ const { owner, repo, branch } = req.body;
1343
+ const studio = loadStudioConfig();
1344
+
1345
+ const finalOwner = owner || studio.githubBackup?.owner || user.login;
1346
+ const finalRepo = repo || studio.githubBackup?.repo;
1347
+ const finalBranch = branch || studio.githubBackup?.branch || 'main';
1348
+
1349
+ if (!finalRepo) return res.status(400).json({ error: 'No repo configured' });
1350
+
1351
+ const repoName = `${finalOwner}/${finalRepo}`;
1352
+
1353
+ const opencodeConfig = getConfigPath();
1354
+ if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
1355
+
1356
+ const opencodeDir = path.dirname(opencodeConfig);
1357
+ const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1358
+
1359
+ tempDir = path.join(os.tmpdir(), `opencode-restore-${Date.now()}`);
1360
+ fs.mkdirSync(tempDir, { recursive: true });
1361
+
1362
+ await execPromise(`git clone --depth 1 -b ${finalBranch} https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1363
+
1364
+ const backupOpencodeDir = path.join(tempDir, 'opencode');
1365
+ const backupStudioDir = path.join(tempDir, 'opencode-studio');
1366
+
1367
+ if (!fs.existsSync(backupOpencodeDir)) {
1368
+ fs.rmSync(tempDir, { recursive: true });
1369
+ return res.status(400).json({ error: 'No opencode backup found in repository' });
1370
+ }
1371
+
1372
+ copyDirContents(backupOpencodeDir, opencodeDir);
1373
+ if (fs.existsSync(backupStudioDir)) {
1374
+ copyDirContents(backupStudioDir, studioDir);
1375
+ }
1376
+
1377
+ fs.rmSync(tempDir, { recursive: true });
1378
+
1379
+ res.json({ success: true, message: 'Config restored from GitHub' });
1380
+ } catch (err) {
1381
+ if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
1382
+ console.error('GitHub restore error:', err);
1383
+ res.status(500).json({ error: err.message });
1384
+ }
1385
+ });
1386
+
1387
+ app.post('/api/github/autosync', async (req, res) => {
1388
+ const studio = loadStudioConfig();
1389
+ const enabled = req.body.enabled;
1327
1390
  studio.githubAutoSync = enabled;
1328
1391
  saveStudioConfig(studio);
1392
+ if (enabled) triggerGitHubAutoSync();
1329
1393
  res.json({ success: true, enabled });
1330
- });
1331
-
1332
- const getSkillDir = () => {
1394
+ });
1395
+
1396
+ const getSkillDir = () => {
1333
1397
  const cp = getConfigPath();
1334
1398
  return cp ? path.join(path.dirname(cp), 'skill') : null;
1335
1399
  };
@@ -1363,8 +1427,9 @@ app.post('/api/skills/:name', (req, res) => {
1363
1427
  if (!sd) return res.status(404).json({ error: 'No config' });
1364
1428
  const dp = path.join(sd, req.params.name);
1365
1429
  if (!fs.existsSync(dp)) fs.mkdirSync(dp, { recursive: true });
1366
- fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1367
- res.json({ success: true });
1430
+ fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1431
+ triggerGitHubAutoSync();
1432
+ res.json({ success: true });
1368
1433
  });
1369
1434
 
1370
1435
  app.delete('/api/skills/:name', (req, res) => {
@@ -1373,8 +1438,9 @@ app.delete('/api/skills/:name', (req, res) => {
1373
1438
  }
1374
1439
  const sd = getSkillDir();
1375
1440
  const dp = sd ? path.join(sd, req.params.name) : null;
1376
- if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1377
- res.json({ success: true });
1441
+ if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1442
+ triggerGitHubAutoSync();
1443
+ res.json({ success: true });
1378
1444
  });
1379
1445
 
1380
1446
  app.post('/api/skills/:name/toggle', (req, res) => {
@@ -1388,8 +1454,9 @@ app.post('/api/skills/:name/toggle', (req, res) => {
1388
1454
  studio.disabledSkills.push(name);
1389
1455
  }
1390
1456
 
1391
- saveStudioConfig(studio);
1392
- res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1457
+ saveStudioConfig(studio);
1458
+ triggerGitHubAutoSync();
1459
+ res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1393
1460
  });
1394
1461
 
1395
1462
  const getPluginDir = () => {
@@ -1466,9 +1533,10 @@ app.post('/api/plugins/:name', (req, res) => {
1466
1533
  if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
1467
1534
 
1468
1535
  // Default to .js if new
1469
- const filePath = path.join(pd, name.endsWith('.js') || name.endsWith('.ts') ? name : name + '.js');
1470
- atomicWriteFileSync(filePath, content);
1471
- res.json({ success: true });
1536
+ const filePath = path.join(pd, name.endsWith('.js') || name.endsWith('.ts') ? name : name + '.js');
1537
+ atomicWriteFileSync(filePath, content);
1538
+ triggerGitHubAutoSync();
1539
+ res.json({ success: true });
1472
1540
  });
1473
1541
 
1474
1542
  app.delete('/api/plugins/:name', (req, res) => {
@@ -1493,8 +1561,10 @@ app.delete('/api/plugins/:name', (req, res) => {
1493
1561
  }
1494
1562
  }
1495
1563
 
1496
- if (deleted) res.json({ success: true });
1497
- else res.status(404).json({ error: 'Plugin not found' });
1564
+ if (deleted) {
1565
+ triggerGitHubAutoSync();
1566
+ res.json({ success: true });
1567
+ } else res.status(404).json({ error: 'Plugin not found' });
1498
1568
  });
1499
1569
 
1500
1570
  app.post('/api/plugins/:name/toggle', (req, res) => {
@@ -1508,8 +1578,9 @@ app.post('/api/plugins/:name/toggle', (req, res) => {
1508
1578
  studio.disabledPlugins.push(name);
1509
1579
  }
1510
1580
 
1511
- saveStudioConfig(studio);
1512
- res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1581
+ saveStudioConfig(studio);
1582
+ triggerGitHubAutoSync();
1583
+ res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1513
1584
  });
1514
1585
 
1515
1586
  const getActiveGooglePlugin = () => {
@@ -3406,7 +3477,17 @@ app.post('/api/presets/:id/apply', (req, res) => {
3406
3477
  // Start watcher on server start
3407
3478
  function startServer() {
3408
3479
  ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'].forEach(p => importCurrentAuthToPool(p));
3409
- app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
3480
+ app.listen(PORT, () => {
3481
+ console.log(`Server running at http://localhost:${PORT}`);
3482
+ // Initial sync on startup if enabled
3483
+ setTimeout(() => {
3484
+ const studio = loadStudioConfig();
3485
+ if (studio.githubAutoSync) {
3486
+ console.log('[AutoSync] Triggering initial sync...');
3487
+ triggerGitHubAutoSync();
3488
+ }
3489
+ }, 5000);
3490
+ });
3410
3491
  }
3411
3492
 
3412
3493
  if (require.main === module) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.27.0",
3
+ "version": "1.28.1",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {