opencode-studio-server 1.0.0 → 1.0.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/index.js +285 -48
- package/launcher.vbs +6 -0
- package/package.json +1 -1
- package/register-protocol.js +22 -3
package/index.js
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const cors = require('cors');
|
|
3
|
-
const bodyParser = require('body-parser');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const { exec, spawn } = require('child_process');
|
|
8
|
-
|
|
9
|
-
const app = express();
|
|
10
|
-
const PORT = 3001;
|
|
11
|
-
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const bodyParser = require('body-parser');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { exec, spawn } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const app = express();
|
|
10
|
+
const PORT = 3001;
|
|
11
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
12
|
+
|
|
13
|
+
let lastActivityTime = Date.now();
|
|
14
|
+
let idleTimer = null;
|
|
15
|
+
|
|
16
|
+
function resetIdleTimer() {
|
|
17
|
+
lastActivityTime = Date.now();
|
|
18
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
19
|
+
idleTimer = setTimeout(() => {
|
|
20
|
+
console.log('Server idle for 30 minutes, shutting down...');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}, IDLE_TIMEOUT_MS);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resetIdleTimer();
|
|
26
|
+
|
|
27
|
+
app.use((req, res, next) => {
|
|
28
|
+
resetIdleTimer();
|
|
29
|
+
next();
|
|
30
|
+
});
|
|
31
|
+
|
|
12
32
|
const ALLOWED_ORIGINS = [
|
|
13
33
|
'http://localhost:3000',
|
|
14
34
|
'http://127.0.0.1:3000',
|
|
@@ -169,6 +189,11 @@ app.get('/api/health', (req, res) => {
|
|
|
169
189
|
res.json({ status: 'ok', timestamp: Date.now() });
|
|
170
190
|
});
|
|
171
191
|
|
|
192
|
+
app.post('/api/shutdown', (req, res) => {
|
|
193
|
+
res.json({ success: true, message: 'Server shutting down' });
|
|
194
|
+
setTimeout(() => process.exit(0), 100);
|
|
195
|
+
});
|
|
196
|
+
|
|
172
197
|
app.get('/api/pending-action', (req, res) => {
|
|
173
198
|
const action = loadPendingAction();
|
|
174
199
|
res.json({ action });
|
|
@@ -764,15 +789,16 @@ function saveAuthConfig(config) {
|
|
|
764
789
|
}
|
|
765
790
|
}
|
|
766
791
|
|
|
767
|
-
const PROVIDER_DISPLAY_NAMES = {
|
|
768
|
-
'github-copilot': 'GitHub Copilot',
|
|
769
|
-
'google': 'Google',
|
|
770
|
-
'
|
|
771
|
-
'
|
|
772
|
-
'
|
|
773
|
-
'
|
|
774
|
-
'
|
|
775
|
-
'
|
|
792
|
+
const PROVIDER_DISPLAY_NAMES = {
|
|
793
|
+
'github-copilot': 'GitHub Copilot',
|
|
794
|
+
'google': 'Google',
|
|
795
|
+
'google-gemini-oauth': 'Google Gemini (OAuth)',
|
|
796
|
+
'anthropic': 'Anthropic',
|
|
797
|
+
'openai': 'OpenAI',
|
|
798
|
+
'zai': 'Z.AI',
|
|
799
|
+
'xai': 'xAI',
|
|
800
|
+
'groq': 'Groq',
|
|
801
|
+
'together': 'Together AI',
|
|
776
802
|
'mistral': 'Mistral',
|
|
777
803
|
'deepseek': 'DeepSeek',
|
|
778
804
|
'openrouter': 'OpenRouter',
|
|
@@ -780,31 +806,45 @@ const PROVIDER_DISPLAY_NAMES = {
|
|
|
780
806
|
'azure': 'Azure OpenAI',
|
|
781
807
|
};
|
|
782
808
|
|
|
783
|
-
app.get('/api/auth', (req, res) => {
|
|
784
|
-
const authConfig = loadAuthConfig();
|
|
785
|
-
const authFile = getAuthFile();
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
809
|
+
app.get('/api/auth', (req, res) => {
|
|
810
|
+
const authConfig = loadAuthConfig();
|
|
811
|
+
const authFile = getAuthFile();
|
|
812
|
+
const paths = getPaths();
|
|
813
|
+
|
|
814
|
+
let hasGeminiAuthPlugin = false;
|
|
815
|
+
if (paths && fs.existsSync(paths.opencodeJson)) {
|
|
816
|
+
try {
|
|
817
|
+
const config = JSON.parse(fs.readFileSync(paths.opencodeJson, 'utf8'));
|
|
818
|
+
if (Array.isArray(config.plugin)) {
|
|
819
|
+
hasGeminiAuthPlugin = config.plugin.some(p =>
|
|
820
|
+
typeof p === 'string' && p.includes('opencode-gemini-auth')
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
} catch {}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!authConfig) {
|
|
827
|
+
return res.json({
|
|
828
|
+
credentials: [],
|
|
829
|
+
authFile: null,
|
|
830
|
+
message: 'No auth file found',
|
|
831
|
+
hasGeminiAuthPlugin,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const credentials = Object.entries(authConfig).map(([id, config]) => {
|
|
836
|
+
const isExpired = config.expires ? Date.now() > config.expires : false;
|
|
837
|
+
return {
|
|
838
|
+
id,
|
|
839
|
+
name: PROVIDER_DISPLAY_NAMES[id] || id,
|
|
840
|
+
type: config.type,
|
|
841
|
+
isExpired,
|
|
842
|
+
expiresAt: config.expires || null,
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
res.json({ credentials, authFile, hasGeminiAuthPlugin });
|
|
847
|
+
});
|
|
808
848
|
|
|
809
849
|
app.post('/api/auth/login', (req, res) => {
|
|
810
850
|
const { provider } = req.body;
|
|
@@ -874,6 +914,203 @@ app.get('/api/auth/providers', (req, res) => {
|
|
|
874
914
|
res.json(providers);
|
|
875
915
|
});
|
|
876
916
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
917
|
+
const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
|
|
918
|
+
|
|
919
|
+
function ensureAuthProfilesDir() {
|
|
920
|
+
if (!fs.existsSync(AUTH_PROFILES_DIR)) {
|
|
921
|
+
fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function getProviderProfilesDir(provider) {
|
|
926
|
+
return path.join(AUTH_PROFILES_DIR, provider);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function listAuthProfiles(provider) {
|
|
930
|
+
const dir = getProviderProfilesDir(provider);
|
|
931
|
+
if (!fs.existsSync(dir)) return [];
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
return fs.readdirSync(dir)
|
|
935
|
+
.filter(f => f.endsWith('.json'))
|
|
936
|
+
.map(f => f.replace('.json', ''));
|
|
937
|
+
} catch {
|
|
938
|
+
return [];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function getNextProfileName(provider) {
|
|
943
|
+
const existing = listAuthProfiles(provider);
|
|
944
|
+
let num = 1;
|
|
945
|
+
while (existing.includes(`account-${num}`)) {
|
|
946
|
+
num++;
|
|
947
|
+
}
|
|
948
|
+
return `account-${num}`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function saveAuthProfile(provider, profileName, data) {
|
|
952
|
+
ensureAuthProfilesDir();
|
|
953
|
+
const dir = getProviderProfilesDir(provider);
|
|
954
|
+
if (!fs.existsSync(dir)) {
|
|
955
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
956
|
+
}
|
|
957
|
+
const filePath = path.join(dir, `${profileName}.json`);
|
|
958
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function loadAuthProfile(provider, profileName) {
|
|
963
|
+
const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
|
|
964
|
+
if (!fs.existsSync(filePath)) return null;
|
|
965
|
+
try {
|
|
966
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
967
|
+
} catch {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function deleteAuthProfile(provider, profileName) {
|
|
973
|
+
const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
|
|
974
|
+
if (fs.existsSync(filePath)) {
|
|
975
|
+
fs.unlinkSync(filePath);
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function getActiveProfiles() {
|
|
982
|
+
const studioConfig = loadStudioConfig();
|
|
983
|
+
return studioConfig.activeProfiles || {};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function setActiveProfile(provider, profileName) {
|
|
987
|
+
const studioConfig = loadStudioConfig();
|
|
988
|
+
studioConfig.activeProfiles = studioConfig.activeProfiles || {};
|
|
989
|
+
studioConfig.activeProfiles[provider] = profileName;
|
|
990
|
+
saveStudioConfig(studioConfig);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
app.get('/api/auth/profiles', (req, res) => {
|
|
994
|
+
ensureAuthProfilesDir();
|
|
995
|
+
const activeProfiles = getActiveProfiles();
|
|
996
|
+
const authConfig = loadAuthConfig() || {};
|
|
997
|
+
|
|
998
|
+
const profiles = {};
|
|
999
|
+
|
|
1000
|
+
Object.keys(PROVIDER_DISPLAY_NAMES).forEach(provider => {
|
|
1001
|
+
const providerProfiles = listAuthProfiles(provider);
|
|
1002
|
+
profiles[provider] = {
|
|
1003
|
+
profiles: providerProfiles,
|
|
1004
|
+
active: activeProfiles[provider] || null,
|
|
1005
|
+
hasCurrentAuth: !!authConfig[provider],
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
res.json(profiles);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
app.get('/api/auth/profiles/:provider', (req, res) => {
|
|
1013
|
+
const { provider } = req.params;
|
|
1014
|
+
const providerProfiles = listAuthProfiles(provider);
|
|
1015
|
+
const activeProfiles = getActiveProfiles();
|
|
1016
|
+
const authConfig = loadAuthConfig() || {};
|
|
1017
|
+
|
|
1018
|
+
res.json({
|
|
1019
|
+
profiles: providerProfiles,
|
|
1020
|
+
active: activeProfiles[provider] || null,
|
|
1021
|
+
hasCurrentAuth: !!authConfig[provider],
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
app.post('/api/auth/profiles/:provider', (req, res) => {
|
|
1026
|
+
const { provider } = req.params;
|
|
1027
|
+
const { name } = req.body;
|
|
1028
|
+
|
|
1029
|
+
const authConfig = loadAuthConfig();
|
|
1030
|
+
if (!authConfig || !authConfig[provider]) {
|
|
1031
|
+
return res.status(400).json({ error: `No active auth for ${provider} to save` });
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const profileName = name || getNextProfileName(provider);
|
|
1035
|
+
const data = authConfig[provider];
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
saveAuthProfile(provider, profileName, data);
|
|
1039
|
+
setActiveProfile(provider, profileName);
|
|
1040
|
+
res.json({ success: true, name: profileName });
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
res.status(500).json({ error: 'Failed to save profile', details: err.message });
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
|
|
1047
|
+
const { provider, name } = req.params;
|
|
1048
|
+
|
|
1049
|
+
const profileData = loadAuthProfile(provider, name);
|
|
1050
|
+
if (!profileData) {
|
|
1051
|
+
return res.status(404).json({ error: 'Profile not found' });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const authConfig = loadAuthConfig() || {};
|
|
1055
|
+
authConfig[provider] = profileData;
|
|
1056
|
+
|
|
1057
|
+
if (saveAuthConfig(authConfig)) {
|
|
1058
|
+
setActiveProfile(provider, name);
|
|
1059
|
+
res.json({ success: true });
|
|
1060
|
+
} else {
|
|
1061
|
+
res.status(500).json({ error: 'Failed to activate profile' });
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
|
|
1066
|
+
const { provider, name } = req.params;
|
|
1067
|
+
|
|
1068
|
+
if (deleteAuthProfile(provider, name)) {
|
|
1069
|
+
const activeProfiles = getActiveProfiles();
|
|
1070
|
+
if (activeProfiles[provider] === name) {
|
|
1071
|
+
const studioConfig = loadStudioConfig();
|
|
1072
|
+
delete studioConfig.activeProfiles[provider];
|
|
1073
|
+
saveStudioConfig(studioConfig);
|
|
1074
|
+
}
|
|
1075
|
+
res.json({ success: true });
|
|
1076
|
+
} else {
|
|
1077
|
+
res.status(404).json({ error: 'Profile not found' });
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
app.put('/api/auth/profiles/:provider/:name', (req, res) => {
|
|
1082
|
+
const { provider, name } = req.params;
|
|
1083
|
+
const { newName } = req.body;
|
|
1084
|
+
|
|
1085
|
+
if (!newName || newName === name) {
|
|
1086
|
+
return res.status(400).json({ error: 'New name is required and must be different' });
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const profileData = loadAuthProfile(provider, name);
|
|
1090
|
+
if (!profileData) {
|
|
1091
|
+
return res.status(404).json({ error: 'Profile not found' });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const existingProfiles = listAuthProfiles(provider);
|
|
1095
|
+
if (existingProfiles.includes(newName)) {
|
|
1096
|
+
return res.status(400).json({ error: 'Profile name already exists' });
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
saveAuthProfile(provider, newName, profileData);
|
|
1101
|
+
deleteAuthProfile(provider, name);
|
|
1102
|
+
|
|
1103
|
+
const activeProfiles = getActiveProfiles();
|
|
1104
|
+
if (activeProfiles[provider] === name) {
|
|
1105
|
+
setActiveProfile(provider, newName);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
res.json({ success: true, name: newName });
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
res.status(500).json({ error: 'Failed to rename profile', details: err.message });
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
app.listen(PORT, () => {
|
|
1115
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
1116
|
+
});
|
package/launcher.vbs
ADDED
package/package.json
CHANGED
package/register-protocol.js
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
const { exec } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
|
|
5
6
|
const PROTOCOL = 'opencodestudio';
|
|
6
7
|
|
|
7
8
|
function registerWindows() {
|
|
8
|
-
const nodePath = process.execPath
|
|
9
|
-
const scriptPath = path.join(__dirname, 'cli.js')
|
|
10
|
-
|
|
9
|
+
const nodePath = process.execPath;
|
|
10
|
+
const scriptPath = path.join(__dirname, 'cli.js');
|
|
11
|
+
|
|
12
|
+
// Create a VBScript launcher that runs node hidden
|
|
13
|
+
const vbsPath = path.join(__dirname, 'launcher.vbs');
|
|
14
|
+
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")
|
|
15
|
+
args = ""
|
|
16
|
+
If WScript.Arguments.Count > 0 Then
|
|
17
|
+
args = " """ & WScript.Arguments(0) & """"
|
|
18
|
+
End If
|
|
19
|
+
WshShell.Run """${nodePath.replace(/\\/g, '\\\\')}""" & " ""${scriptPath.replace(/\\/g, '\\\\')}"" " & args, 0, False
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
fs.writeFileSync(vbsPath, vbsContent);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Failed to create launcher.vbs:', err.message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Register protocol to use wscript with the VBS launcher
|
|
29
|
+
const command = `wscript.exe "${vbsPath.replace(/\\/g, '\\\\')}" "%1"`;
|
|
11
30
|
|
|
12
31
|
const regCommands = [
|
|
13
32
|
`reg add "HKCU\\Software\\Classes\\${PROTOCOL}" /ve /d "URL:OpenCode Studio Protocol" /f`,
|