opencode-studio-server 1.28.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 (2) hide show
  1. package/index.js +174 -110
  2. package/package.json +1 -1
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
  }
@@ -611,14 +616,15 @@ app.get('/api/config', (req, res) => {
611
616
  res.json(config);
612
617
  });
613
618
 
614
- app.post('/api/config', (req, res) => {
615
- try {
616
- saveConfig(req.body);
617
- res.json({ success: true });
618
- } catch (err) {
619
- res.status(500).json({ error: err.message });
620
- }
621
- });
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
+ });
622
628
 
623
629
  app.get('/api/backup', (req, res) => {
624
630
  try {
@@ -1074,10 +1080,11 @@ app.post('/api/ohmyopencode', (req, res) => {
1074
1080
 
1075
1081
  saveOhMyOpenCodeConfig(currentConfig);
1076
1082
 
1077
- const ohMyPath = getOhMyOpenCodeConfigPath();
1078
- res.json({
1079
- success: true,
1080
- path: ohMyPath,
1083
+ const ohMyPath = getOhMyOpenCodeConfigPath();
1084
+ triggerGitHubAutoSync();
1085
+ res.json({
1086
+ success: true,
1087
+ path: ohMyPath,
1081
1088
  exists: true,
1082
1089
  config: currentConfig,
1083
1090
  preferences,
@@ -1181,6 +1188,107 @@ function execPromise(cmd, opts = {}) {
1181
1188
  });
1182
1189
  }
1183
1190
 
1191
+
1192
+ let autoSyncTimer = null;
1193
+
1194
+ async function performGitHubBackup(options = {}) {
1195
+ const { owner, repo, branch } = options;
1196
+ let tempDir = null;
1197
+ try {
1198
+ const token = await getGitHubToken();
1199
+ if (!token) throw new Error('Not logged in to gh CLI. Run: gh auth login');
1200
+
1201
+ const user = await getGitHubUser(token);
1202
+ if (!user) throw new Error('Failed to get GitHub user');
1203
+
1204
+ const studio = loadStudioConfig();
1205
+
1206
+ const finalOwner = owner || studio.githubBackup?.owner || user.login;
1207
+ const finalRepo = repo || studio.githubBackup?.repo;
1208
+ const finalBranch = branch || studio.githubBackup?.branch || 'main';
1209
+
1210
+ if (!finalRepo) throw new Error('No repo name provided');
1211
+
1212
+ const repoName = `${finalOwner}/${finalRepo}`;
1213
+
1214
+ await ensureGitHubRepo(token, repoName);
1215
+
1216
+ const opencodeConfig = getConfigPath();
1217
+ if (!opencodeConfig) throw new Error('No opencode config path found');
1218
+
1219
+ const opencodeDir = path.dirname(opencodeConfig);
1220
+ const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1221
+
1222
+ tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1223
+ fs.mkdirSync(tempDir, { recursive: true });
1224
+
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
+ }
1234
+
1235
+ const backupOpencodeDir = path.join(tempDir, 'opencode');
1236
+ const backupStudioDir = path.join(tempDir, 'opencode-studio');
1237
+
1238
+ if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
1239
+ if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
1240
+
1241
+ copyDirContents(opencodeDir, backupOpencodeDir);
1242
+ copyDirContents(studioDir, backupStudioDir);
1243
+
1244
+ await execPromise('git add -A', { cwd: tempDir });
1245
+
1246
+ const timestamp = new Date().toISOString();
1247
+ const commitMessage = `OpenCode Studio backup ${timestamp}`;
1248
+
1249
+ let result = { success: true, timestamp, url: `https://github.com/${repoName}` };
1250
+
1251
+ try {
1252
+ await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
1253
+ await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
1254
+ } catch (e) {
1255
+ if (e.message.includes('nothing to commit')) {
1256
+ result.message = 'No changes to backup';
1257
+ } else {
1258
+ throw e;
1259
+ }
1260
+ }
1261
+
1262
+ studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1263
+ studio.lastGithubBackup = timestamp;
1264
+ saveStudioConfig(studio);
1265
+
1266
+ return result;
1267
+ } finally {
1268
+ if (tempDir && fs.existsSync(tempDir)) {
1269
+ try { fs.rmSync(tempDir, { recursive: true }); } catch (e) {}
1270
+ }
1271
+ }
1272
+ }
1273
+
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);
1288
+ }
1289
+ }, 15000); // 15s debounce
1290
+ }
1291
+
1184
1292
  app.get('/api/github/backup/status', async (req, res) => {
1185
1293
  try {
1186
1294
  const token = await getGitHubToken();
@@ -1212,77 +1320,15 @@ app.get('/api/github/backup/status', async (req, res) => {
1212
1320
  }
1213
1321
  });
1214
1322
 
1215
- app.post('/api/github/backup', async (req, res) => {
1216
- let tempDir = null;
1217
- try {
1218
- const token = await getGitHubToken();
1219
- if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
1220
-
1221
- const user = await getGitHubUser(token);
1222
- if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
1223
-
1224
- const { owner, repo, branch } = req.body;
1225
- const studio = loadStudioConfig();
1226
-
1227
- const finalOwner = owner || studio.githubBackup?.owner || user.login;
1228
- const finalRepo = repo || studio.githubBackup?.repo;
1229
- const finalBranch = branch || studio.githubBackup?.branch || 'main';
1230
-
1231
- if (!finalRepo) return res.status(400).json({ error: 'No repo name provided' });
1232
-
1233
- const repoName = `${finalOwner}/${finalRepo}`;
1234
-
1235
- await ensureGitHubRepo(token, repoName);
1236
-
1237
- const opencodeConfig = getConfigPath();
1238
- if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
1239
-
1240
- const opencodeDir = path.dirname(opencodeConfig);
1241
- const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1242
-
1243
- tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1244
- fs.mkdirSync(tempDir, { recursive: true });
1245
-
1246
- await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1247
-
1248
- const backupOpencodeDir = path.join(tempDir, 'opencode');
1249
- const backupStudioDir = path.join(tempDir, 'opencode-studio');
1250
-
1251
- if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
1252
- if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
1253
-
1254
- copyDirContents(opencodeDir, backupOpencodeDir);
1255
- copyDirContents(studioDir, backupStudioDir);
1256
-
1257
- await execPromise('git add -A', { cwd: tempDir });
1258
-
1259
- const timestamp = new Date().toISOString();
1260
- const commitMessage = `OpenCode Studio backup ${timestamp}`;
1261
-
1262
- try {
1263
- await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
1264
- await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
1265
- } catch (e) {
1266
- if (e.message.includes('nothing to commit')) {
1267
- fs.rmSync(tempDir, { recursive: true });
1268
- return res.json({ success: true, timestamp, message: 'No changes to backup', url: `https://github.com/${repoName}` });
1269
- }
1270
- throw e;
1271
- }
1272
-
1273
- fs.rmSync(tempDir, { recursive: true });
1274
-
1275
- studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1276
- studio.lastGithubBackup = timestamp;
1277
- saveStudioConfig(studio);
1278
-
1279
- res.json({ success: true, timestamp, url: `https://github.com/${repoName}` });
1280
- } catch (err) {
1281
- if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
1282
- console.error('GitHub backup error:', err);
1283
- res.status(500).json({ error: err.message });
1284
- }
1285
- });
1323
+ app.post('/api/github/backup', async (req, res) => {
1324
+ try {
1325
+ const result = await performGitHubBackup(req.body);
1326
+ res.json(result);
1327
+ } catch (err) {
1328
+ console.error('GitHub backup error:', err);
1329
+ res.status(500).json({ error: err.message });
1330
+ }
1331
+ });
1286
1332
 
1287
1333
  app.post('/api/github/restore', async (req, res) => {
1288
1334
  let tempDir = null;
@@ -1341,9 +1387,10 @@ app.post('/api/github/restore', async (req, res) => {
1341
1387
  app.post('/api/github/autosync', async (req, res) => {
1342
1388
  const studio = loadStudioConfig();
1343
1389
  const enabled = req.body.enabled;
1344
- studio.githubAutoSync = enabled;
1345
- saveStudioConfig(studio);
1346
- res.json({ success: true, enabled });
1390
+ studio.githubAutoSync = enabled;
1391
+ saveStudioConfig(studio);
1392
+ if (enabled) triggerGitHubAutoSync();
1393
+ res.json({ success: true, enabled });
1347
1394
  });
1348
1395
 
1349
1396
  const getSkillDir = () => {
@@ -1380,8 +1427,9 @@ app.post('/api/skills/:name', (req, res) => {
1380
1427
  if (!sd) return res.status(404).json({ error: 'No config' });
1381
1428
  const dp = path.join(sd, req.params.name);
1382
1429
  if (!fs.existsSync(dp)) fs.mkdirSync(dp, { recursive: true });
1383
- fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1384
- res.json({ success: true });
1430
+ fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1431
+ triggerGitHubAutoSync();
1432
+ res.json({ success: true });
1385
1433
  });
1386
1434
 
1387
1435
  app.delete('/api/skills/:name', (req, res) => {
@@ -1390,8 +1438,9 @@ app.delete('/api/skills/:name', (req, res) => {
1390
1438
  }
1391
1439
  const sd = getSkillDir();
1392
1440
  const dp = sd ? path.join(sd, req.params.name) : null;
1393
- if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1394
- res.json({ success: true });
1441
+ if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1442
+ triggerGitHubAutoSync();
1443
+ res.json({ success: true });
1395
1444
  });
1396
1445
 
1397
1446
  app.post('/api/skills/:name/toggle', (req, res) => {
@@ -1405,8 +1454,9 @@ app.post('/api/skills/:name/toggle', (req, res) => {
1405
1454
  studio.disabledSkills.push(name);
1406
1455
  }
1407
1456
 
1408
- saveStudioConfig(studio);
1409
- res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1457
+ saveStudioConfig(studio);
1458
+ triggerGitHubAutoSync();
1459
+ res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1410
1460
  });
1411
1461
 
1412
1462
  const getPluginDir = () => {
@@ -1483,9 +1533,10 @@ app.post('/api/plugins/:name', (req, res) => {
1483
1533
  if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
1484
1534
 
1485
1535
  // Default to .js if new
1486
- const filePath = path.join(pd, name.endsWith('.js') || name.endsWith('.ts') ? name : name + '.js');
1487
- atomicWriteFileSync(filePath, content);
1488
- 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 });
1489
1540
  });
1490
1541
 
1491
1542
  app.delete('/api/plugins/:name', (req, res) => {
@@ -1510,8 +1561,10 @@ app.delete('/api/plugins/:name', (req, res) => {
1510
1561
  }
1511
1562
  }
1512
1563
 
1513
- if (deleted) res.json({ success: true });
1514
- 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' });
1515
1568
  });
1516
1569
 
1517
1570
  app.post('/api/plugins/:name/toggle', (req, res) => {
@@ -1525,8 +1578,9 @@ app.post('/api/plugins/:name/toggle', (req, res) => {
1525
1578
  studio.disabledPlugins.push(name);
1526
1579
  }
1527
1580
 
1528
- saveStudioConfig(studio);
1529
- res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1581
+ saveStudioConfig(studio);
1582
+ triggerGitHubAutoSync();
1583
+ res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1530
1584
  });
1531
1585
 
1532
1586
  const getActiveGooglePlugin = () => {
@@ -3423,7 +3477,17 @@ app.post('/api/presets/:id/apply', (req, res) => {
3423
3477
  // Start watcher on server start
3424
3478
  function startServer() {
3425
3479
  ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'].forEach(p => importCurrentAuthToPool(p));
3426
- 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
+ });
3427
3491
  }
3428
3492
 
3429
3493
  if (require.main === module) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.28.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": {