opencode-studio-server 1.28.0 → 1.28.2

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 +197 -119
  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
  }
@@ -490,8 +495,15 @@ const getPaths = () => {
490
495
  }
491
496
  }
492
497
 
493
- const studioConfig = loadStudioConfig();
494
- const manualPath = studioConfig.configPath;
498
+ const studioConfig = loadStudioConfig();
499
+ let manualPath = studioConfig.configPath;
500
+
501
+ if (manualPath && fs.existsSync(manualPath) && fs.statSync(manualPath).isDirectory()) {
502
+ const potentialFile = path.join(manualPath, 'opencode.json');
503
+ if (fs.existsSync(potentialFile)) {
504
+ manualPath = potentialFile;
505
+ }
506
+ }
495
507
 
496
508
  let detected = null;
497
509
  for (const p of candidates) {
@@ -597,13 +609,20 @@ app.get('/api/debug/auth', (req, res) => {
597
609
  });
598
610
  });
599
611
 
600
- app.post('/api/paths', (req, res) => {
601
- const { configPath } = req.body;
602
- const studioConfig = loadStudioConfig();
603
- studioConfig.configPath = configPath;
604
- saveStudioConfig(studioConfig);
605
- res.json({ success: true, current: getConfigPath() });
606
- });
612
+ app.post('/api/paths', (req, res) => {
613
+ const { configPath } = req.body;
614
+ const studioConfig = loadStudioConfig();
615
+
616
+ if (configPath && fs.existsSync(configPath) && fs.statSync(configPath).isDirectory()) {
617
+ const potentialFile = path.join(configPath, 'opencode.json');
618
+ studioConfig.configPath = potentialFile;
619
+ } else {
620
+ studioConfig.configPath = configPath;
621
+ }
622
+
623
+ saveStudioConfig(studioConfig);
624
+ res.json({ success: true, current: getConfigPath() });
625
+ });
607
626
 
608
627
  app.get('/api/config', (req, res) => {
609
628
  const config = loadConfig();
@@ -611,14 +630,15 @@ app.get('/api/config', (req, res) => {
611
630
  res.json(config);
612
631
  });
613
632
 
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
- });
633
+ app.post('/api/config', (req, res) => {
634
+ try {
635
+ saveConfig(req.body);
636
+ triggerGitHubAutoSync();
637
+ res.json({ success: true });
638
+ } catch (err) {
639
+ res.status(500).json({ error: err.message });
640
+ }
641
+ });
622
642
 
623
643
  app.get('/api/backup', (req, res) => {
624
644
  try {
@@ -1074,10 +1094,11 @@ app.post('/api/ohmyopencode', (req, res) => {
1074
1094
 
1075
1095
  saveOhMyOpenCodeConfig(currentConfig);
1076
1096
 
1077
- const ohMyPath = getOhMyOpenCodeConfigPath();
1078
- res.json({
1079
- success: true,
1080
- path: ohMyPath,
1097
+ const ohMyPath = getOhMyOpenCodeConfigPath();
1098
+ triggerGitHubAutoSync();
1099
+ res.json({
1100
+ success: true,
1101
+ path: ohMyPath,
1081
1102
  exists: true,
1082
1103
  config: currentConfig,
1083
1104
  preferences,
@@ -1181,6 +1202,107 @@ function execPromise(cmd, opts = {}) {
1181
1202
  });
1182
1203
  }
1183
1204
 
1205
+
1206
+ let autoSyncTimer = null;
1207
+
1208
+ async function performGitHubBackup(options = {}) {
1209
+ const { owner, repo, branch } = options;
1210
+ let tempDir = null;
1211
+ try {
1212
+ const token = await getGitHubToken();
1213
+ if (!token) throw new Error('Not logged in to gh CLI. Run: gh auth login');
1214
+
1215
+ const user = await getGitHubUser(token);
1216
+ if (!user) throw new Error('Failed to get GitHub user');
1217
+
1218
+ const studio = loadStudioConfig();
1219
+
1220
+ const finalOwner = owner || studio.githubBackup?.owner || user.login;
1221
+ const finalRepo = repo || studio.githubBackup?.repo;
1222
+ const finalBranch = branch || studio.githubBackup?.branch || 'main';
1223
+
1224
+ if (!finalRepo) throw new Error('No repo name provided');
1225
+
1226
+ const repoName = `${finalOwner}/${finalRepo}`;
1227
+
1228
+ await ensureGitHubRepo(token, repoName);
1229
+
1230
+ const opencodeConfig = getConfigPath();
1231
+ if (!opencodeConfig) throw new Error('No opencode config path found');
1232
+
1233
+ const opencodeDir = path.dirname(opencodeConfig);
1234
+ const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1235
+
1236
+ tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1237
+ fs.mkdirSync(tempDir, { recursive: true });
1238
+
1239
+ // Clone or init
1240
+ try {
1241
+ await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1242
+ } catch (e) {
1243
+ // If clone fails (empty repo?), try init
1244
+ await execPromise('git init', { cwd: tempDir });
1245
+ await execPromise(`git remote add origin https://x-access-token:${token}@github.com/${repoName}.git`, { cwd: tempDir });
1246
+ await execPromise(`git checkout -b ${finalBranch}`, { cwd: tempDir });
1247
+ }
1248
+
1249
+ const backupOpencodeDir = path.join(tempDir, 'opencode');
1250
+ const backupStudioDir = path.join(tempDir, 'opencode-studio');
1251
+
1252
+ if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
1253
+ if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
1254
+
1255
+ copyDirContents(opencodeDir, backupOpencodeDir);
1256
+ copyDirContents(studioDir, backupStudioDir);
1257
+
1258
+ await execPromise('git add -A', { cwd: tempDir });
1259
+
1260
+ const timestamp = new Date().toISOString();
1261
+ const commitMessage = `OpenCode Studio backup ${timestamp}`;
1262
+
1263
+ let result = { success: true, timestamp, url: `https://github.com/${repoName}` };
1264
+
1265
+ try {
1266
+ await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
1267
+ await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
1268
+ } catch (e) {
1269
+ if (e.message.includes('nothing to commit')) {
1270
+ result.message = 'No changes to backup';
1271
+ } else {
1272
+ throw e;
1273
+ }
1274
+ }
1275
+
1276
+ studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1277
+ studio.lastGithubBackup = timestamp;
1278
+ saveStudioConfig(studio);
1279
+
1280
+ return result;
1281
+ } finally {
1282
+ if (tempDir && fs.existsSync(tempDir)) {
1283
+ try { fs.rmSync(tempDir, { recursive: true }); } catch (e) {}
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ function triggerGitHubAutoSync() {
1289
+ const studio = loadStudioConfig();
1290
+ if (!studio.githubAutoSync) return;
1291
+
1292
+ if (autoSyncTimer) clearTimeout(autoSyncTimer);
1293
+
1294
+ console.log('[AutoSync] Change detected, scheduling GitHub backup in 15s...');
1295
+ autoSyncTimer = setTimeout(async () => {
1296
+ console.log('[AutoSync] Starting GitHub backup...');
1297
+ try {
1298
+ const result = await performGitHubBackup();
1299
+ console.log(`[AutoSync] Backup completed: ${result.message || 'Pushed to GitHub'}`);
1300
+ } catch (err) {
1301
+ console.error('[AutoSync] Backup failed:', err.message);
1302
+ }
1303
+ }, 15000); // 15s debounce
1304
+ }
1305
+
1184
1306
  app.get('/api/github/backup/status', async (req, res) => {
1185
1307
  try {
1186
1308
  const token = await getGitHubToken();
@@ -1212,77 +1334,15 @@ app.get('/api/github/backup/status', async (req, res) => {
1212
1334
  }
1213
1335
  });
1214
1336
 
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
- });
1337
+ app.post('/api/github/backup', async (req, res) => {
1338
+ try {
1339
+ const result = await performGitHubBackup(req.body);
1340
+ res.json(result);
1341
+ } catch (err) {
1342
+ console.error('GitHub backup error:', err);
1343
+ res.status(500).json({ error: err.message });
1344
+ }
1345
+ });
1286
1346
 
1287
1347
  app.post('/api/github/restore', async (req, res) => {
1288
1348
  let tempDir = null;
@@ -1341,9 +1401,10 @@ app.post('/api/github/restore', async (req, res) => {
1341
1401
  app.post('/api/github/autosync', async (req, res) => {
1342
1402
  const studio = loadStudioConfig();
1343
1403
  const enabled = req.body.enabled;
1344
- studio.githubAutoSync = enabled;
1345
- saveStudioConfig(studio);
1346
- res.json({ success: true, enabled });
1404
+ studio.githubAutoSync = enabled;
1405
+ saveStudioConfig(studio);
1406
+ if (enabled) triggerGitHubAutoSync();
1407
+ res.json({ success: true, enabled });
1347
1408
  });
1348
1409
 
1349
1410
  const getSkillDir = () => {
@@ -1380,8 +1441,9 @@ app.post('/api/skills/:name', (req, res) => {
1380
1441
  if (!sd) return res.status(404).json({ error: 'No config' });
1381
1442
  const dp = path.join(sd, req.params.name);
1382
1443
  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 });
1444
+ fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1445
+ triggerGitHubAutoSync();
1446
+ res.json({ success: true });
1385
1447
  });
1386
1448
 
1387
1449
  app.delete('/api/skills/:name', (req, res) => {
@@ -1390,8 +1452,9 @@ app.delete('/api/skills/:name', (req, res) => {
1390
1452
  }
1391
1453
  const sd = getSkillDir();
1392
1454
  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 });
1455
+ if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1456
+ triggerGitHubAutoSync();
1457
+ res.json({ success: true });
1395
1458
  });
1396
1459
 
1397
1460
  app.post('/api/skills/:name/toggle', (req, res) => {
@@ -1405,8 +1468,9 @@ app.post('/api/skills/:name/toggle', (req, res) => {
1405
1468
  studio.disabledSkills.push(name);
1406
1469
  }
1407
1470
 
1408
- saveStudioConfig(studio);
1409
- res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1471
+ saveStudioConfig(studio);
1472
+ triggerGitHubAutoSync();
1473
+ res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1410
1474
  });
1411
1475
 
1412
1476
  const getPluginDir = () => {
@@ -1483,9 +1547,10 @@ app.post('/api/plugins/:name', (req, res) => {
1483
1547
  if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
1484
1548
 
1485
1549
  // 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 });
1550
+ const filePath = path.join(pd, name.endsWith('.js') || name.endsWith('.ts') ? name : name + '.js');
1551
+ atomicWriteFileSync(filePath, content);
1552
+ triggerGitHubAutoSync();
1553
+ res.json({ success: true });
1489
1554
  });
1490
1555
 
1491
1556
  app.delete('/api/plugins/:name', (req, res) => {
@@ -1510,8 +1575,10 @@ app.delete('/api/plugins/:name', (req, res) => {
1510
1575
  }
1511
1576
  }
1512
1577
 
1513
- if (deleted) res.json({ success: true });
1514
- else res.status(404).json({ error: 'Plugin not found' });
1578
+ if (deleted) {
1579
+ triggerGitHubAutoSync();
1580
+ res.json({ success: true });
1581
+ } else res.status(404).json({ error: 'Plugin not found' });
1515
1582
  });
1516
1583
 
1517
1584
  app.post('/api/plugins/:name/toggle', (req, res) => {
@@ -1525,8 +1592,9 @@ app.post('/api/plugins/:name/toggle', (req, res) => {
1525
1592
  studio.disabledPlugins.push(name);
1526
1593
  }
1527
1594
 
1528
- saveStudioConfig(studio);
1529
- res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1595
+ saveStudioConfig(studio);
1596
+ triggerGitHubAutoSync();
1597
+ res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1530
1598
  });
1531
1599
 
1532
1600
  const getActiveGooglePlugin = () => {
@@ -3423,7 +3491,17 @@ app.post('/api/presets/:id/apply', (req, res) => {
3423
3491
  // Start watcher on server start
3424
3492
  function startServer() {
3425
3493
  ['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}`));
3494
+ app.listen(PORT, () => {
3495
+ console.log(`Server running at http://localhost:${PORT}`);
3496
+ // Initial sync on startup if enabled
3497
+ setTimeout(() => {
3498
+ const studio = loadStudioConfig();
3499
+ if (studio.githubAutoSync) {
3500
+ console.log('[AutoSync] Triggering initial sync...');
3501
+ triggerGitHubAutoSync();
3502
+ }
3503
+ }, 5000);
3504
+ });
3427
3505
  }
3428
3506
 
3429
3507
  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.2",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {