opencode-studio-server 1.7.0 → 1.8.0

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 +365 -140
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -590,91 +590,373 @@ app.post('/api/restore', (req, res) => {
590
590
  }
591
591
  });
592
592
 
593
- app.post('/api/sync/push', (req, res) => {
593
+ const DROPBOX_CLIENT_ID = 'your-dropbox-app-key';
594
+ const GDRIVE_CLIENT_ID = 'your-google-client-id';
595
+
596
+ function buildBackupData() {
597
+ const studio = loadStudioConfig();
598
+ const opencodeConfig = loadConfig();
599
+ const skills = [];
600
+ const plugins = [];
601
+
602
+ const sd = getSkillDir();
603
+ if (sd && fs.existsSync(sd)) {
604
+ fs.readdirSync(sd, { withFileTypes: true })
605
+ .filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
606
+ .forEach(e => {
607
+ skills.push({ name: e.name, content: fs.readFileSync(path.join(sd, e.name, 'SKILL.md'), 'utf8') });
608
+ });
609
+ }
610
+
611
+ const pd = getPluginDir();
612
+ if (pd && fs.existsSync(pd)) {
613
+ fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
614
+ if (e.isFile() && /\.(js|ts)$/.test(e.name)) {
615
+ plugins.push({ name: e.name.replace(/\.(js|ts)$/, ''), content: fs.readFileSync(path.join(pd, e.name), 'utf8') });
616
+ }
617
+ });
618
+ }
619
+
620
+ const cloudSettings = studio.cloudProvider ? { provider: studio.cloudProvider } : {};
621
+
622
+ return {
623
+ version: 1,
624
+ timestamp: new Date().toISOString(),
625
+ studioConfig: { ...studio, cloudToken: undefined, cloudProvider: undefined },
626
+ opencodeConfig,
627
+ skills,
628
+ plugins
629
+ };
630
+ }
631
+
632
+ function restoreFromBackup(backup, studio) {
633
+ if (backup.studioConfig) {
634
+ const merged = {
635
+ ...backup.studioConfig,
636
+ cloudProvider: studio.cloudProvider,
637
+ cloudToken: studio.cloudToken,
638
+ autoSync: studio.autoSync,
639
+ lastSyncAt: studio.lastSyncAt
640
+ };
641
+ saveStudioConfig(merged);
642
+ }
643
+ if (backup.opencodeConfig) saveConfig(backup.opencodeConfig);
644
+
645
+ const sd = getSkillDir();
646
+ if (sd && backup.skills && Array.isArray(backup.skills)) {
647
+ if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
648
+ backup.skills.forEach(s => {
649
+ const skillDir = path.join(sd, s.name);
650
+ if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
651
+ atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
652
+ });
653
+ }
654
+
655
+ const pd = getPluginDir();
656
+ if (pd && backup.plugins && Array.isArray(backup.plugins)) {
657
+ if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
658
+ backup.plugins.forEach(p => {
659
+ atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
660
+ });
661
+ }
662
+ }
663
+
664
+ app.get('/api/sync/status', (req, res) => {
665
+ const studio = loadStudioConfig();
666
+ res.json({
667
+ provider: studio.cloudProvider || null,
668
+ connected: !!(studio.cloudProvider && studio.cloudToken),
669
+ lastSync: studio.lastSyncAt || null,
670
+ autoSync: !!studio.autoSync
671
+ });
672
+ });
673
+
674
+ app.post('/api/sync/config', (req, res) => {
675
+ const { autoSync } = req.body;
676
+ const studio = loadStudioConfig();
677
+ if (autoSync !== undefined) studio.autoSync = !!autoSync;
678
+ saveStudioConfig(studio);
679
+ res.json({ success: true, autoSync: !!studio.autoSync });
680
+ });
681
+
682
+ app.post('/api/sync/disconnect', (req, res) => {
683
+ const studio = loadStudioConfig();
684
+ delete studio.cloudProvider;
685
+ delete studio.cloudToken;
686
+ delete studio.cloudRefreshToken;
687
+ saveStudioConfig(studio);
688
+ res.json({ success: true });
689
+ });
690
+
691
+ app.get('/api/sync/dropbox/auth-url', (req, res) => {
692
+ const state = crypto.randomBytes(16).toString('hex');
693
+ const studio = loadStudioConfig();
694
+ studio.oauthState = state;
695
+ saveStudioConfig(studio);
696
+
697
+ const redirectUri = req.query.redirect_uri || 'http://localhost:3000/settings';
698
+ studio.oauthRedirectUri = redirectUri;
699
+ saveStudioConfig(studio);
700
+
701
+ const params = new URLSearchParams({
702
+ client_id: DROPBOX_CLIENT_ID,
703
+ response_type: 'code',
704
+ token_access_type: 'offline',
705
+ redirect_uri: redirectUri,
706
+ state: state
707
+ });
708
+ res.json({ url: `https://www.dropbox.com/oauth2/authorize?${params}` });
709
+ });
710
+
711
+ app.post('/api/sync/dropbox/callback', async (req, res) => {
594
712
  try {
713
+ const { code, state } = req.body;
595
714
  const studio = loadStudioConfig();
596
- const syncFolder = studio.syncFolder;
597
- if (!syncFolder) return res.status(400).json({ error: 'Sync folder not configured' });
598
- if (!fs.existsSync(syncFolder)) return res.status(400).json({ error: 'Sync folder does not exist' });
599
715
 
600
- const opencodeConfig = loadConfig();
601
- const skills = [];
602
- const plugins = [];
716
+ if (state !== studio.oauthState) {
717
+ return res.status(400).json({ error: 'Invalid state' });
718
+ }
719
+ const redirectUri = studio.oauthRedirectUri || 'http://localhost:3000/settings';
720
+ delete studio.oauthState;
721
+ delete studio.oauthRedirectUri;
603
722
 
604
- const sd = getSkillDir();
605
- if (sd && fs.existsSync(sd)) {
606
- fs.readdirSync(sd, { withFileTypes: true })
607
- .filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
608
- .forEach(e => {
609
- skills.push({ name: e.name, content: fs.readFileSync(path.join(sd, e.name, 'SKILL.md'), 'utf8') });
610
- });
723
+ const response = await fetch('https://api.dropboxapi.com/oauth2/token', {
724
+ method: 'POST',
725
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
726
+ body: new URLSearchParams({
727
+ code,
728
+ grant_type: 'authorization_code',
729
+ client_id: DROPBOX_CLIENT_ID,
730
+ redirect_uri: redirectUri
731
+ })
732
+ });
733
+
734
+ if (!response.ok) {
735
+ const err = await response.text();
736
+ return res.status(400).json({ error: `Dropbox auth failed: ${err}` });
611
737
  }
612
738
 
613
- const pd = getPluginDir();
614
- if (pd && fs.existsSync(pd)) {
615
- fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
616
- if (e.isFile() && /\.(js|ts)$/.test(e.name)) {
617
- plugins.push({ name: e.name.replace(/\.(js|ts)$/, ''), content: fs.readFileSync(path.join(pd, e.name), 'utf8') });
618
- }
619
- });
739
+ const tokens = await response.json();
740
+ studio.cloudProvider = 'dropbox';
741
+ studio.cloudToken = tokens.access_token;
742
+ if (tokens.refresh_token) studio.cloudRefreshToken = tokens.refresh_token;
743
+ saveStudioConfig(studio);
744
+
745
+ res.json({ success: true, provider: 'dropbox' });
746
+ } catch (err) {
747
+ res.status(500).json({ error: err.message });
748
+ }
749
+ });
750
+
751
+ app.get('/api/sync/gdrive/auth-url', (req, res) => {
752
+ const state = crypto.randomBytes(16).toString('hex');
753
+ const studio = loadStudioConfig();
754
+ studio.oauthState = state;
755
+
756
+ const redirectUri = req.query.redirect_uri || 'http://localhost:3000/settings';
757
+ studio.oauthRedirectUri = redirectUri;
758
+ saveStudioConfig(studio);
759
+
760
+ const params = new URLSearchParams({
761
+ client_id: GDRIVE_CLIENT_ID,
762
+ response_type: 'code',
763
+ scope: 'https://www.googleapis.com/auth/drive.file',
764
+ access_type: 'offline',
765
+ prompt: 'consent',
766
+ redirect_uri: redirectUri,
767
+ state: state
768
+ });
769
+ res.json({ url: `https://accounts.google.com/o/oauth2/v2/auth?${params}` });
770
+ });
771
+
772
+ app.post('/api/sync/gdrive/callback', async (req, res) => {
773
+ try {
774
+ const { code, state } = req.body;
775
+ const studio = loadStudioConfig();
776
+
777
+ if (state !== studio.oauthState) {
778
+ return res.status(400).json({ error: 'Invalid state' });
620
779
  }
780
+ const redirectUri = studio.oauthRedirectUri || 'http://localhost:3000/settings';
781
+ delete studio.oauthState;
782
+ delete studio.oauthRedirectUri;
621
783
 
622
- const backup = {
623
- version: 1,
624
- timestamp: new Date().toISOString(),
625
- studioConfig: { ...studio, syncFolder: undefined },
626
- opencodeConfig,
627
- skills,
628
- plugins
629
- };
784
+ const response = await fetch('https://oauth2.googleapis.com/token', {
785
+ method: 'POST',
786
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
787
+ body: new URLSearchParams({
788
+ code,
789
+ client_id: GDRIVE_CLIENT_ID,
790
+ // client_secret: 'GDRIVE_CLIENT_SECRET', // NOTE: OAuth flow for installed apps usually requires client secret.
791
+ // For simplicity in this demo, we assume PKCE or a public client flow if supported,
792
+ // OR the user will need to provide the secret in environment variables.
793
+ // Since this is a local server, we might need the secret.
794
+ // Let's assume for now we'll put a placeholder or rely on env vars.
795
+ // Google "Installed App" flow doesn't always need secret if type is "Desktop".
796
+ // However, for web flow it does.
797
+ // Let's assume we need a client secret env var for now.
798
+ client_secret: process.env.GDRIVE_CLIENT_SECRET || 'your-google-client-secret',
799
+ redirect_uri: redirectUri,
800
+ grant_type: 'authorization_code'
801
+ })
802
+ });
630
803
 
631
- const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
632
- atomicWriteFileSync(backupPath, JSON.stringify(backup, null, 2));
804
+ if (!response.ok) {
805
+ const err = await response.text();
806
+ return res.status(400).json({ error: `Google auth failed: ${err}` });
807
+ }
633
808
 
634
- studio.lastSyncAt = backup.timestamp;
809
+ const tokens = await response.json();
810
+ studio.cloudProvider = 'gdrive';
811
+ studio.cloudToken = tokens.access_token;
812
+ if (tokens.refresh_token) studio.cloudRefreshToken = tokens.refresh_token;
635
813
  saveStudioConfig(studio);
636
814
 
637
- res.json({ success: true, path: backupPath, timestamp: backup.timestamp });
815
+ res.json({ success: true, provider: 'gdrive' });
638
816
  } catch (err) {
639
817
  res.status(500).json({ error: err.message });
640
818
  }
641
819
  });
642
820
 
643
- app.post('/api/sync/pull', (req, res) => {
821
+ app.post('/api/sync/push', async (req, res) => {
644
822
  try {
645
823
  const studio = loadStudioConfig();
646
- const syncFolder = studio.syncFolder;
647
- if (!syncFolder) return res.status(400).json({ error: 'Sync folder not configured' });
648
-
649
- const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
650
- if (!fs.existsSync(backupPath)) return res.status(404).json({ error: 'No sync file found in folder' });
824
+ if (!studio.cloudProvider || !studio.cloudToken) {
825
+ return res.status(400).json({ error: 'No cloud provider connected' });
826
+ }
651
827
 
652
- const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
828
+ const backup = buildBackupData();
829
+ const content = JSON.stringify(backup, null, 2);
653
830
 
654
- if (backup.studioConfig) {
655
- const merged = { ...backup.studioConfig, syncFolder: studio.syncFolder };
656
- saveStudioConfig(merged);
831
+ if (studio.cloudProvider === 'dropbox') {
832
+ const response = await fetch('https://content.dropboxapi.com/2/files/upload', {
833
+ method: 'POST',
834
+ headers: {
835
+ 'Authorization': `Bearer ${studio.cloudToken}`,
836
+ 'Content-Type': 'application/octet-stream',
837
+ 'Dropbox-API-Arg': JSON.stringify({
838
+ path: '/opencode-studio-sync.json',
839
+ mode: 'overwrite',
840
+ autorename: false
841
+ })
842
+ },
843
+ body: content
844
+ });
845
+
846
+ if (!response.ok) {
847
+ const err = await response.text();
848
+ return res.status(400).json({ error: `Dropbox upload failed: ${err}` });
849
+ }
850
+ } else if (studio.cloudProvider === 'gdrive') {
851
+ // Find existing file
852
+ const listRes = await fetch('https://www.googleapis.com/drive/v3/files?q=name=\'opencode-studio-sync.json\' and trashed=false', {
853
+ headers: { 'Authorization': `Bearer ${studio.cloudToken}` }
854
+ });
855
+ const listData = await listRes.json();
856
+ const existingFile = listData.files?.[0];
857
+
858
+ if (existingFile) {
859
+ // Update content
860
+ const updateRes = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${existingFile.id}?uploadType=media`, {
861
+ method: 'PATCH',
862
+ headers: {
863
+ 'Authorization': `Bearer ${studio.cloudToken}`,
864
+ 'Content-Type': 'application/json'
865
+ },
866
+ body: content
867
+ });
868
+ if (!updateRes.ok) {
869
+ const err = await updateRes.text();
870
+ return res.status(400).json({ error: `Google Drive update failed: ${err}` });
871
+ }
872
+ } else {
873
+ // Create new file (multipart)
874
+ const metadata = { name: 'opencode-studio-sync.json', mimeType: 'application/json' };
875
+ const boundary = '-------314159265358979323846';
876
+ const delimiter = "\r\n--" + boundary + "\r\n";
877
+ const close_delim = "\r\n--" + boundary + "--";
878
+
879
+ const multipartBody =
880
+ delimiter +
881
+ 'Content-Type: application/json\r\n\r\n' +
882
+ JSON.stringify(metadata) +
883
+ delimiter +
884
+ 'Content-Type: application/json\r\n\r\n' +
885
+ content +
886
+ close_delim;
887
+
888
+ const createRes = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
889
+ method: 'POST',
890
+ headers: {
891
+ 'Authorization': `Bearer ${studio.cloudToken}`,
892
+ 'Content-Type': `multipart/related; boundary="${boundary}"`
893
+ },
894
+ body: multipartBody
895
+ });
896
+ if (!createRes.ok) {
897
+ const err = await createRes.text();
898
+ return res.status(400).json({ error: `Google Drive create failed: ${err}` });
899
+ }
900
+ }
657
901
  }
658
- if (backup.opencodeConfig) saveConfig(backup.opencodeConfig);
659
902
 
660
- const sd = getSkillDir();
661
- if (sd && backup.skills && Array.isArray(backup.skills)) {
662
- if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
663
- backup.skills.forEach(s => {
664
- const skillDir = path.join(sd, s.name);
665
- if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
666
- atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
667
- });
903
+ studio.lastSyncAt = backup.timestamp;
904
+ saveStudioConfig(studio);
905
+ res.json({ success: true, timestamp: backup.timestamp });
906
+ } catch (err) {
907
+ res.status(500).json({ error: err.message });
908
+ }
909
+ });
910
+
911
+ app.post('/api/sync/pull', async (req, res) => {
912
+ try {
913
+ const studio = loadStudioConfig();
914
+ if (!studio.cloudProvider || !studio.cloudToken) {
915
+ return res.status(400).json({ error: 'No cloud provider connected' });
668
916
  }
669
917
 
670
- const pd = getPluginDir();
671
- if (pd && backup.plugins && Array.isArray(backup.plugins)) {
672
- if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
673
- backup.plugins.forEach(p => {
674
- atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
918
+ let content;
919
+
920
+ if (studio.cloudProvider === 'dropbox') {
921
+ const response = await fetch('https://content.dropboxapi.com/2/files/download', {
922
+ method: 'POST',
923
+ headers: {
924
+ 'Authorization': `Bearer ${studio.cloudToken}`,
925
+ 'Dropbox-API-Arg': JSON.stringify({ path: '/opencode-studio-sync.json' })
926
+ }
927
+ });
928
+
929
+ if (!response.ok) {
930
+ if (response.status === 409) {
931
+ return res.status(404).json({ error: 'No sync file found in cloud' });
932
+ }
933
+ const err = await response.text();
934
+ return res.status(400).json({ error: `Dropbox download failed: ${err}` });
935
+ }
936
+
937
+ content = await response.text();
938
+ } else if (studio.cloudProvider === 'gdrive') {
939
+ const listRes = await fetch('https://www.googleapis.com/drive/v3/files?q=name=\'opencode-studio-sync.json\' and trashed=false', {
940
+ headers: { 'Authorization': `Bearer ${studio.cloudToken}` }
941
+ });
942
+ const listData = await listRes.json();
943
+ const existingFile = listData.files?.[0];
944
+
945
+ if (!existingFile) return res.status(404).json({ error: 'No sync file found in cloud' });
946
+
947
+ const downloadRes = await fetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}?alt=media`, {
948
+ headers: { 'Authorization': `Bearer ${studio.cloudToken}` }
675
949
  });
950
+ if (!downloadRes.ok) {
951
+ const err = await downloadRes.text();
952
+ return res.status(400).json({ error: `Google Drive download failed: ${err}` });
953
+ }
954
+ content = await downloadRes.text();
676
955
  }
677
956
 
957
+ const backup = JSON.parse(content);
958
+ restoreFromBackup(backup, studio);
959
+
678
960
  const updated = loadStudioConfig();
679
961
  updated.lastSyncAt = new Date().toISOString();
680
962
  saveStudioConfig(updated);
@@ -685,98 +967,41 @@ app.post('/api/sync/pull', (req, res) => {
685
967
  }
686
968
  });
687
969
 
688
- app.get('/api/sync/status', (req, res) => {
689
- const studio = loadStudioConfig();
690
- const syncFolder = studio.syncFolder;
691
- let fileExists = false;
692
- let fileTimestamp = null;
693
-
694
- if (syncFolder) {
695
- const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
696
- if (fs.existsSync(backupPath)) {
697
- fileExists = true;
698
- try {
699
- const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
700
- fileTimestamp = backup.timestamp;
701
- } catch {}
702
- }
703
- }
704
-
705
- res.json({
706
- configured: !!syncFolder,
707
- folder: syncFolder || null,
708
- lastSync: studio.lastSyncAt || null,
709
- autoSync: !!studio.autoSync,
710
- fileExists,
711
- fileTimestamp
712
- });
713
- });
714
-
715
- app.post('/api/sync/config', (req, res) => {
716
- const { folder, autoSync } = req.body;
717
- const studio = loadStudioConfig();
718
-
719
- if (folder !== undefined) {
720
- if (folder) {
721
- if (!fs.existsSync(folder)) {
722
- return res.status(400).json({ error: 'Folder does not exist' });
723
- }
724
- studio.syncFolder = folder;
725
- } else {
726
- delete studio.syncFolder;
727
- }
728
- }
729
-
730
- if (autoSync !== undefined) {
731
- studio.autoSync = !!autoSync;
732
- }
733
-
734
- saveStudioConfig(studio);
735
- res.json({ success: true, folder: studio.syncFolder || null, autoSync: !!studio.autoSync });
736
- });
737
-
738
- app.post('/api/sync/auto', (req, res) => {
970
+ app.post('/api/sync/auto', async (req, res) => {
739
971
  const studio = loadStudioConfig();
740
- if (!studio.syncFolder || !studio.autoSync) {
972
+ if (!studio.cloudProvider || !studio.cloudToken || !studio.autoSync) {
741
973
  return res.json({ action: 'none', reason: 'auto-sync not configured' });
742
974
  }
743
975
 
744
- const syncFolder = studio.syncFolder;
745
- const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
746
-
747
- if (!fs.existsSync(backupPath)) {
748
- return res.json({ action: 'none', reason: 'no remote file' });
749
- }
750
-
751
976
  try {
752
- const remote = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
753
- const remoteTime = new Date(remote.timestamp).getTime();
754
- const localTime = studio.lastSyncAt ? new Date(studio.lastSyncAt).getTime() : 0;
977
+ let content;
755
978
 
756
- if (remoteTime > localTime) {
757
- if (remote.studioConfig) {
758
- const merged = { ...remote.studioConfig, syncFolder: studio.syncFolder, autoSync: studio.autoSync, lastSyncAt: studio.lastSyncAt };
759
- saveStudioConfig(merged);
760
- }
761
- if (remote.opencodeConfig) saveConfig(remote.opencodeConfig);
979
+ if (studio.cloudProvider === 'dropbox') {
980
+ const response = await fetch('https://content.dropboxapi.com/2/files/download', {
981
+ method: 'POST',
982
+ headers: {
983
+ 'Authorization': `Bearer ${studio.cloudToken}`,
984
+ 'Dropbox-API-Arg': JSON.stringify({ path: '/opencode-studio-sync.json' })
985
+ }
986
+ });
762
987
 
763
- const sd = getSkillDir();
764
- if (sd && remote.skills && Array.isArray(remote.skills)) {
765
- if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
766
- remote.skills.forEach(s => {
767
- const skillDir = path.join(sd, s.name);
768
- if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
769
- atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
770
- });
988
+ if (!response.ok) {
989
+ return res.json({ action: 'none', reason: 'no remote file' });
771
990
  }
772
991
 
773
- const pd = getPluginDir();
774
- if (pd && remote.plugins && Array.isArray(remote.plugins)) {
775
- if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
776
- remote.plugins.forEach(p => {
777
- atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
778
- });
779
- }
992
+ content = await response.text();
993
+ }
994
+
995
+ if (!content) {
996
+ return res.json({ action: 'none', reason: 'no content' });
997
+ }
998
+
999
+ const remote = JSON.parse(content);
1000
+ const remoteTime = new Date(remote.timestamp).getTime();
1001
+ const localTime = studio.lastSyncAt ? new Date(studio.lastSyncAt).getTime() : 0;
1002
+
1003
+ if (remoteTime > localTime) {
1004
+ restoreFromBackup(remote, studio);
780
1005
 
781
1006
  const updated = loadStudioConfig();
782
1007
  updated.lastSyncAt = new Date().toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {