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.
- package/AGENTS.md +0 -1
- package/README.md +0 -1
- package/index.js +328 -247
- 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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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 (
|
|
1188
|
-
|
|
1189
|
-
|
|
1148
|
+
if (createRes.ok) {
|
|
1149
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1150
|
+
return await createRes.json();
|
|
1151
|
+
}
|
|
1190
1152
|
|
|
1191
|
-
const
|
|
1192
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
1263
|
-
}
|
|
1264
|
-
if (tempDir && fs.existsSync(tempDir))
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
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/
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1497
|
-
|
|
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
|
-
|
|
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, () =>
|
|
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) {
|