opencode-studio-server 2.0.0 → 2.1.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 (4) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/index.js +655 -243
  4. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SERVER LAYER
2
2
 
3
- Express API backend (port 3001). Single-file architecture.
3
+ Express API backend (port 1920). Single-file architecture.
4
4
 
5
5
  ## STRUCTURE
6
6
 
package/README.md CHANGED
@@ -24,7 +24,7 @@ Start the server manually:
24
24
  opencode-studio-server
25
25
  ```
26
26
 
27
- The server runs on port **3001** and provides an API for managing your local OpenCode configuration.
27
+ The server runs on port **1920** (auto-detects next available) and provides an API for managing your local OpenCode configuration.
28
28
 
29
29
  ## Features
30
30
 
package/index.js CHANGED
@@ -5,7 +5,8 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
- const { spawn, exec } = require('child_process');
8
+ const { spawn, exec, execSync } = require('child_process');
9
+ const yaml = require('js-yaml');
9
10
 
10
11
  const pkg = require('./package.json');
11
12
  const profileManager = require('./profile-manager');
@@ -40,23 +41,23 @@ const atomicWriteFileSync = (filePath, data, options = 'utf8') => {
40
41
  }
41
42
  };
42
43
 
43
- const app = express();
44
- const DEFAULT_PORT = 1920;
45
- const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
46
-
47
- function findAvailablePort(startPort) {
48
- const net = require('net');
49
- return new Promise((resolve) => {
50
- const server = net.createServer();
51
- server.listen(startPort, () => {
52
- server.once('close', () => resolve(startPort));
53
- server.close();
54
- });
55
- server.on('error', () => {
56
- findAvailablePort(startPort + 1).then(resolve);
57
- });
58
- });
59
- }
44
+ const app = express();
45
+ const DEFAULT_PORT = 1920;
46
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
47
+
48
+ function findAvailablePort(startPort) {
49
+ const net = require('net');
50
+ return new Promise((resolve) => {
51
+ const server = net.createServer();
52
+ server.listen(startPort, () => {
53
+ server.once('close', () => resolve(startPort));
54
+ server.close();
55
+ });
56
+ server.on('error', () => {
57
+ findAvailablePort(startPort + 1).then(resolve);
58
+ });
59
+ });
60
+ }
60
61
 
61
62
  let idleTimer = null;
62
63
 
@@ -75,17 +76,17 @@ app.use((req, res, next) => {
75
76
  next();
76
77
  });
77
78
 
78
- const ALLOWED_ORIGINS = [
79
- 'http://localhost:1080',
80
- 'http://127.0.0.1:1080',
81
- /^http:\/\/localhost:108\d$/,
82
- /^http:\/\/127\.0\.0\.1:108\d$/,
83
- 'https://opencode-studio.vercel.app',
84
- 'https://opencode.micr.dev',
85
- 'https://opencode-studio.micr.dev',
86
- /\.vercel\.app$/,
87
- /\.micr\.dev$/,
88
- ];
79
+ const ALLOWED_ORIGINS = [
80
+ 'http://localhost:1080',
81
+ 'http://127.0.0.1:1080',
82
+ /^http:\/\/localhost:108\d$/,
83
+ /^http:\/\/127\.0\.0\.1:108\d$/,
84
+ 'https://opencode-studio.vercel.app',
85
+ 'https://opencode.micr.dev',
86
+ 'https://opencode-studio.micr.dev',
87
+ /\.vercel\.app$/,
88
+ /\.micr\.dev$/,
89
+ ];
89
90
 
90
91
  app.use(cors({
91
92
  origin: (origin, callback) => {
@@ -101,15 +102,47 @@ app.use(bodyParser.json({ limit: '50mb' }));
101
102
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
102
103
  app.use(bodyParser.text({ type: ['text/*', 'application/yaml'], limit: '50mb' }));
103
104
 
104
- const HOME_DIR = os.homedir();
105
- const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
106
- const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
107
- const ANTIGRAVITY_ACCOUNTS_PATH = path.join(HOME_DIR, '.config', 'opencode', 'antigravity-accounts.json');
108
- const LOG_DIR = path.join(HOME_DIR, '.local', 'share', 'opencode', 'log');
105
+ const HOME_DIR = os.homedir();
106
+ const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
107
+ const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
108
+ const ANTIGRAVITY_ACCOUNTS_PATH = path.join(HOME_DIR, '.config', 'opencode', 'antigravity-accounts.json');
109
+ const LOG_DIR = path.join(HOME_DIR, '.local', 'share', 'opencode', 'log');
110
+
111
+ const LOG_BUFFER_SIZE = 100;
112
+ const logBuffer = [];
113
+ const logSubscribers = new Set();
109
114
 
110
- let logWatcher = null;
111
- let currentLogFile = null;
112
- let logReadStream = null;
115
+ let logWatcher = null;
116
+ let currentLogFile = null;
117
+ let logReadStream = null;
118
+
119
+ function enqueueLogLine(line) {
120
+ const entry = { timestamp: Date.now(), line };
121
+ logBuffer.push(entry);
122
+ if (logBuffer.length > LOG_BUFFER_SIZE) logBuffer.shift();
123
+
124
+ for (const sub of logSubscribers) {
125
+ sub.queue.push(`data: ${JSON.stringify(entry)}\n\n`);
126
+ flushSubscriber(sub);
127
+ }
128
+ }
129
+
130
+ function flushSubscriber(sub) {
131
+ if (sub.draining || sub.closed) return;
132
+ while (sub.queue.length > 0) {
133
+ const chunk = sub.queue[0];
134
+ const ok = sub.res.write(chunk);
135
+ if (!ok) {
136
+ sub.draining = true;
137
+ sub.res.once('drain', () => {
138
+ sub.draining = false;
139
+ flushSubscriber(sub);
140
+ });
141
+ return;
142
+ }
143
+ sub.queue.shift();
144
+ }
145
+ }
113
146
 
114
147
  function setupLogWatcher() {
115
148
  if (!fs.existsSync(LOG_DIR)) return;
@@ -188,11 +221,12 @@ function setupLogWatcher() {
188
221
  });
189
222
  }
190
223
 
191
- function processLogLine(line) {
192
- // Detect LLM usage: service=llm providerID=... modelID=...
193
- // Example: service=llm providerID=openai modelID=gpt-5.2-codex sessionID=...
194
- const isUsage = line.includes('service=llm') && line.includes('stream');
195
- const isError = line.includes('service=llm') && (line.includes('error=') || line.includes('status=429'));
224
+ function processLogLine(line) {
225
+ enqueueLogLine(line);
226
+ // Detect LLM usage: service=llm providerID=... modelID=...
227
+ // Example: service=llm providerID=openai modelID=gpt-5.2-codex sessionID=...
228
+ const isUsage = line.includes('service=llm') && line.includes('stream');
229
+ const isError = line.includes('service=llm') && (line.includes('error=') || line.includes('status=429'));
196
230
 
197
231
  if (!isUsage && !isError) return;
198
232
 
@@ -304,12 +338,13 @@ function processLogLine(line) {
304
338
 
305
339
  let pendingActionMemory = null;
306
340
 
307
- function loadStudioConfig() {
308
- const defaultConfig = {
309
- disabledSkills: [],
310
- disabledPlugins: [],
311
- activeProfiles: {},
312
- activeGooglePlugin: 'gemini',
341
+ function loadStudioConfig() {
342
+ const defaultConfig = {
343
+ disabledSkills: [],
344
+ disabledPlugins: [],
345
+ disabledAgents: [],
346
+ activeProfiles: {},
347
+ activeGooglePlugin: 'gemini',
313
348
  availableGooglePlugins: [],
314
349
  presets: [],
315
350
  githubRepo: null,
@@ -357,9 +392,9 @@ function loadStudioConfig() {
357
392
  "variants": {
358
393
  "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
359
394
  "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
360
- "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
361
- "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
362
- }
395
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
396
+ "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
397
+ }
363
398
  },
364
399
  "gemini-3-flash": {
365
400
  "id": "gemini-3-flash",
@@ -376,9 +411,9 @@ function loadStudioConfig() {
376
411
  "minimal": { "options": { "thinkingConfig": { "thinkingLevel": "minimal", "includeThoughts": true } } },
377
412
  "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
378
413
  "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
379
- "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
380
- "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
381
- }
414
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
415
+ "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
416
+ }
382
417
  },
383
418
  "gemini-2.5-flash-lite": {
384
419
  "id": "gemini-2.5-flash-lite",
@@ -399,9 +434,9 @@ function loadStudioConfig() {
399
434
  "minimal": { "options": { "thinkingConfig": { "thinkingLevel": "minimal", "includeThoughts": true } } },
400
435
  "low": { "options": { "thinkingConfig": { "thinkingLevel": "low", "includeThoughts": true } } },
401
436
  "medium": { "options": { "thinkingConfig": { "thinkingLevel": "medium", "includeThoughts": true } } },
402
- "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
403
- "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
404
- }
437
+ "high": { "options": { "thinkingConfig": { "thinkingLevel": "high", "includeThoughts": true } } },
438
+ "xhigh": { "options": { "thinkingConfig": { "thinkingLevel": "xhigh", "includeThoughts": true } } }
439
+ }
405
440
  },
406
441
  "opencode/glm-4.7-free": {
407
442
  "id": "opencode/glm-4.7-free",
@@ -427,9 +462,9 @@ function loadStudioConfig() {
427
462
  "none": { "reasoning": false, "options": { "thinkingConfig": { "includeThoughts": false } } },
428
463
  "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
429
464
  "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
430
- "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } },
431
- "xhigh": { "options": { "thinkingConfig": { "thinkingBudget": 64000, "includeThoughts": true } } }
432
- }
465
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } },
466
+ "xhigh": { "options": { "thinkingConfig": { "thinkingBudget": 64000, "includeThoughts": true } } }
467
+ }
433
468
  },
434
469
  "gemini-claude-opus-4-5-thinking": {
435
470
  "id": "gemini-claude-opus-4-5-thinking",
@@ -444,9 +479,9 @@ function loadStudioConfig() {
444
479
  "variants": {
445
480
  "low": { "options": { "thinkingConfig": { "thinkingBudget": 4000, "includeThoughts": true } } },
446
481
  "medium": { "options": { "thinkingConfig": { "thinkingBudget": 16000, "includeThoughts": true } } },
447
- "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } },
448
- "xhigh": { "options": { "thinkingConfig": { "thinkingBudget": 64000, "includeThoughts": true } } }
449
- }
482
+ "high": { "options": { "thinkingConfig": { "thinkingBudget": 32000, "includeThoughts": true } } },
483
+ "xhigh": { "options": { "thinkingConfig": { "thinkingBudget": 64000, "includeThoughts": true } } }
484
+ }
450
485
  }
451
486
  }
452
487
  }
@@ -511,15 +546,15 @@ const getPaths = () => {
511
546
  }
512
547
  }
513
548
 
514
- const studioConfig = loadStudioConfig();
515
- let manualPath = studioConfig.configPath;
516
-
517
- if (manualPath && fs.existsSync(manualPath) && fs.statSync(manualPath).isDirectory()) {
518
- const potentialFile = path.join(manualPath, 'opencode.json');
519
- if (fs.existsSync(potentialFile)) {
520
- manualPath = potentialFile;
521
- }
522
- }
549
+ const studioConfig = loadStudioConfig();
550
+ let manualPath = studioConfig.configPath;
551
+
552
+ if (manualPath && fs.existsSync(manualPath) && fs.statSync(manualPath).isDirectory()) {
553
+ const potentialFile = path.join(manualPath, 'opencode.json');
554
+ if (fs.existsSync(potentialFile)) {
555
+ manualPath = potentialFile;
556
+ }
557
+ }
523
558
 
524
559
  let detected = null;
525
560
  for (const p of candidates) {
@@ -543,7 +578,101 @@ const getOhMyOpenCodeConfigPath = () => {
543
578
  return path.join(path.dirname(cp), 'oh-my-opencode.json');
544
579
  };
545
580
 
546
- const getConfigPath = () => getPaths().current;
581
+ const getConfigPath = () => getPaths().current;
582
+
583
+ const getAgentDirs = () => {
584
+ const dirs = [];
585
+ const configPath = getConfigPath();
586
+ const configDir = configPath ? path.dirname(configPath) : null;
587
+
588
+ if (configDir) {
589
+ dirs.push(path.join(configDir, '.opencode', 'agents'));
590
+ dirs.push(path.join(configDir, '.opencode', 'agent'));
591
+ }
592
+
593
+ const xdg = process.env.XDG_CONFIG_HOME;
594
+ if (xdg) {
595
+ dirs.push(path.join(xdg, 'opencode', 'agents'));
596
+ dirs.push(path.join(xdg, 'opencode', 'agent'));
597
+ }
598
+
599
+ dirs.push(path.join(HOME_DIR, '.config', 'opencode', 'agents'));
600
+ dirs.push(path.join(HOME_DIR, '.config', 'opencode', 'agent'));
601
+
602
+ return [...new Set(dirs)];
603
+ };
604
+
605
+ const parseAgentMarkdown = (content) => {
606
+ const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?([\s\S]*)$/);
607
+ if (!match) return { data: {}, body: content };
608
+ let data = {};
609
+ try {
610
+ data = yaml.load(match[1]) || {};
611
+ } catch {
612
+ data = {};
613
+ }
614
+ return { data, body: match[2] || '' };
615
+ };
616
+
617
+ const buildAgentMarkdown = (frontmatter, body) => {
618
+ const yamlText = yaml.dump(frontmatter, { lineWidth: 120, noRefs: true, quotingType: '"' });
619
+ const content = body || '';
620
+ return `---\n${yamlText}---\n\n${content}`;
621
+ };
622
+
623
+ const validatePermissionValue = (value) => {
624
+ const allowed = ['ask', 'allow', 'deny'];
625
+ if (value === undefined || value === null) return true;
626
+ if (typeof value === 'string') return allowed.includes(value);
627
+ if (typeof value !== 'object') return false;
628
+
629
+ const keys = Object.keys(value);
630
+ const isAllowDeny = keys.every((k) => k === 'allow' || k === 'deny');
631
+ if (isAllowDeny) {
632
+ return (!value.allow || Array.isArray(value.allow)) && (!value.deny || Array.isArray(value.deny));
633
+ }
634
+
635
+ for (const v of Object.values(value)) {
636
+ if (typeof v === 'string') {
637
+ if (!allowed.includes(v)) return false;
638
+ } else if (typeof v === 'object') {
639
+ if (!validatePermissionValue(v)) return false;
640
+ } else if (v !== undefined && v !== null) {
641
+ return false;
642
+ }
643
+ }
644
+ return true;
645
+ };
646
+
647
+ const findRulesFile = () => {
648
+ const configPath = getConfigPath();
649
+ if (!configPath) return { path: null, source: 'none' };
650
+
651
+ let dir = path.dirname(configPath);
652
+ let last = null;
653
+ while (dir && dir !== last) {
654
+ const agentsPath = path.join(dir, 'AGENTS.md');
655
+ if (fs.existsSync(agentsPath)) return { path: agentsPath, source: 'AGENTS.md' };
656
+ const claudePath = path.join(dir, 'CLAUDE.md');
657
+ if (fs.existsSync(claudePath)) return { path: claudePath, source: 'CLAUDE.md' };
658
+ last = dir;
659
+ dir = path.dirname(dir);
660
+ }
661
+
662
+ return { path: null, source: 'none' };
663
+ };
664
+
665
+ const detectTool = (tool) => {
666
+ const command = process.platform === 'win32' ? `where ${tool}` : `which ${tool}`;
667
+ try {
668
+ const output = execSync(command, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
669
+ if (!output) return { name: tool, available: false };
670
+ const first = output.split(/\r?\n/)[0];
671
+ return { name: tool, available: true, path: first };
672
+ } catch {
673
+ return { name: tool, available: false };
674
+ }
675
+ };
547
676
 
548
677
  const loadConfig = () => {
549
678
  const configPath = getConfigPath();
@@ -625,20 +754,20 @@ app.get('/api/debug/auth', (req, res) => {
625
754
  });
626
755
  });
627
756
 
628
- app.post('/api/paths', (req, res) => {
629
- const { configPath } = req.body;
630
- const studioConfig = loadStudioConfig();
631
-
632
- if (configPath && fs.existsSync(configPath) && fs.statSync(configPath).isDirectory()) {
633
- const potentialFile = path.join(configPath, 'opencode.json');
634
- studioConfig.configPath = potentialFile;
635
- } else {
636
- studioConfig.configPath = configPath;
637
- }
638
-
639
- saveStudioConfig(studioConfig);
640
- res.json({ success: true, current: getConfigPath() });
641
- });
757
+ app.post('/api/paths', (req, res) => {
758
+ const { configPath } = req.body;
759
+ const studioConfig = loadStudioConfig();
760
+
761
+ if (configPath && fs.existsSync(configPath) && fs.statSync(configPath).isDirectory()) {
762
+ const potentialFile = path.join(configPath, 'opencode.json');
763
+ studioConfig.configPath = potentialFile;
764
+ } else {
765
+ studioConfig.configPath = configPath;
766
+ }
767
+
768
+ saveStudioConfig(studioConfig);
769
+ res.json({ success: true, current: getConfigPath() });
770
+ });
642
771
 
643
772
  app.get('/api/config', (req, res) => {
644
773
  const config = loadConfig();
@@ -648,6 +777,9 @@ app.get('/api/config', (req, res) => {
648
777
 
649
778
  app.post('/api/config', (req, res) => {
650
779
  try {
780
+ if (!validatePermissionValue(req.body?.permission)) {
781
+ return res.status(400).json({ error: 'Invalid permission value. Must be ask, allow, or deny.' });
782
+ }
651
783
  saveConfig(req.body);
652
784
  triggerGitHubAutoSync();
653
785
  res.json({ success: true });
@@ -655,6 +787,286 @@ app.post('/api/config', (req, res) => {
655
787
  res.status(500).json({ error: err.message });
656
788
  }
657
789
  });
790
+
791
+ app.get('/api/agents', (req, res) => {
792
+ try {
793
+ const config = loadConfig() || {};
794
+ const studio = loadStudioConfig();
795
+ const disabledAgents = studio.disabledAgents || [];
796
+ const agentMap = new Map();
797
+
798
+ const configAgents = config.agent || {};
799
+ for (const [name, agentConfig] of Object.entries(configAgents)) {
800
+ agentMap.set(name, {
801
+ name,
802
+ source: 'json',
803
+ disabled: disabledAgents.includes(name),
804
+ ...agentConfig,
805
+ permission: agentConfig.permission || agentConfig.permissions,
806
+ permissions: agentConfig.permission || agentConfig.permissions
807
+ });
808
+ }
809
+
810
+ for (const dir of getAgentDirs()) {
811
+ if (!fs.existsSync(dir)) continue;
812
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
813
+ files.forEach((file) => {
814
+ const fp = path.join(dir, file);
815
+ const content = fs.readFileSync(fp, 'utf8');
816
+ const { data, body } = parseAgentMarkdown(content);
817
+ const name = path.basename(file, '.md');
818
+ agentMap.set(name, {
819
+ name,
820
+ source: 'markdown',
821
+ disabled: disabledAgents.includes(name),
822
+ description: data.description,
823
+ mode: data.mode,
824
+ model: data.model,
825
+ temperature: data.temperature,
826
+ tools: data.tools,
827
+ permission: data.permission,
828
+ permissions: data.permission,
829
+ maxSteps: data.maxSteps,
830
+ disable: data.disable,
831
+ hidden: data.hidden,
832
+ prompt: body
833
+ });
834
+ });
835
+ }
836
+
837
+ ['build', 'plan'].forEach((name) => {
838
+ if (!agentMap.has(name)) {
839
+ agentMap.set(name, { name, source: 'builtin', mode: 'primary', disabled: disabledAgents.includes(name) });
840
+ }
841
+ });
842
+
843
+ res.json({ agents: Array.from(agentMap.values()) });
844
+ } catch (err) {
845
+ res.status(500).json({ error: err.message });
846
+ }
847
+ });
848
+
849
+ app.post('/api/agents', (req, res) => {
850
+ try {
851
+ const { name, config: agentConfig, source, scope } = req.body || {};
852
+ if (!name || typeof name !== 'string') return res.status(400).json({ error: 'Missing agent name' });
853
+ if (!/^[a-zA-Z0-9 _-]+$/.test(name)) return res.status(400).json({ error: 'Invalid agent name' });
854
+
855
+ const config = loadConfig() || {};
856
+ if (!config.agent) config.agent = {};
857
+
858
+ const normalizedConfig = { ...(agentConfig || {}) };
859
+ if (normalizedConfig.permissions && !normalizedConfig.permission) {
860
+ normalizedConfig.permission = normalizedConfig.permissions;
861
+ delete normalizedConfig.permissions;
862
+ }
863
+
864
+ const shouldWriteMarkdown = source === 'markdown' || !!normalizedConfig?.mode;
865
+ if (shouldWriteMarkdown) {
866
+ const configPath = getConfigPath();
867
+ const baseDir = configPath ? path.dirname(configPath) : HOME_DIR;
868
+ const projectDir = path.join(baseDir, '.opencode', 'agents');
869
+ const globalDir = path.join(HOME_DIR, '.config', 'opencode', 'agents');
870
+ const targetDir = scope === 'project' ? projectDir : globalDir;
871
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
872
+
873
+ const frontmatter = {
874
+ description: normalizedConfig?.description,
875
+ mode: normalizedConfig?.mode,
876
+ model: normalizedConfig?.model,
877
+ temperature: normalizedConfig?.temperature,
878
+ tools: normalizedConfig?.tools,
879
+ permission: normalizedConfig?.permission,
880
+ maxSteps: normalizedConfig?.maxSteps,
881
+ disable: normalizedConfig?.disable,
882
+ hidden: normalizedConfig?.hidden
883
+ };
884
+
885
+ const markdown = buildAgentMarkdown(frontmatter, normalizedConfig?.prompt || '');
886
+ atomicWriteFileSync(path.join(targetDir, `${name}.md`), markdown);
887
+
888
+ if (config.agent[name]) {
889
+ delete config.agent[name];
890
+ saveConfig(config);
891
+ }
892
+ } else {
893
+ config.agent[name] = normalizedConfig;
894
+ saveConfig(config);
895
+ }
896
+
897
+ triggerGitHubAutoSync();
898
+ res.json({ success: true });
899
+ } catch (err) {
900
+ res.status(500).json({ error: err.message });
901
+ }
902
+ });
903
+
904
+ app.put('/api/agents/:name', (req, res) => {
905
+ try {
906
+ const { name } = req.params;
907
+ const { config: agentConfig } = req.body || {};
908
+
909
+ const normalizedConfig = { ...(agentConfig || {}) };
910
+ if (normalizedConfig.permissions && !normalizedConfig.permission) {
911
+ normalizedConfig.permission = normalizedConfig.permissions;
912
+ delete normalizedConfig.permissions;
913
+ }
914
+
915
+ const config = loadConfig() || {};
916
+ if (!config.agent) config.agent = {};
917
+
918
+ const markdownDirs = getAgentDirs().filter((d) => fs.existsSync(d));
919
+ const markdownPath = markdownDirs
920
+ .map((d) => path.join(d, `${name}.md`))
921
+ .find((p) => fs.existsSync(p));
922
+
923
+ if (markdownPath) {
924
+ const frontmatter = {
925
+ description: normalizedConfig?.description,
926
+ mode: normalizedConfig?.mode,
927
+ model: normalizedConfig?.model,
928
+ temperature: normalizedConfig?.temperature,
929
+ tools: normalizedConfig?.tools,
930
+ permission: normalizedConfig?.permission,
931
+ maxSteps: normalizedConfig?.maxSteps,
932
+ disable: normalizedConfig?.disable,
933
+ hidden: normalizedConfig?.hidden
934
+ };
935
+ const markdown = buildAgentMarkdown(frontmatter, normalizedConfig?.prompt || '');
936
+ atomicWriteFileSync(markdownPath, markdown);
937
+ } else {
938
+ config.agent[name] = normalizedConfig;
939
+ saveConfig(config);
940
+ }
941
+
942
+ triggerGitHubAutoSync();
943
+ res.json({ success: true });
944
+ } catch (err) {
945
+ res.status(500).json({ error: err.message });
946
+ }
947
+ });
948
+
949
+ app.delete('/api/agents/:name', (req, res) => {
950
+ try {
951
+ const { name } = req.params;
952
+ const config = loadConfig() || {};
953
+
954
+ if (config.agent && config.agent[name]) {
955
+ delete config.agent[name];
956
+ saveConfig(config);
957
+ }
958
+
959
+ for (const dir of getAgentDirs()) {
960
+ const fp = path.join(dir, `${name}.md`);
961
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
962
+ }
963
+
964
+ triggerGitHubAutoSync();
965
+ res.json({ success: true });
966
+ } catch (err) {
967
+ res.status(500).json({ error: err.message });
968
+ }
969
+ });
970
+
971
+ app.post('/api/agents/:name/toggle', (req, res) => {
972
+ try {
973
+ const { name } = req.params;
974
+ const studio = loadStudioConfig();
975
+ const disabled = new Set(studio.disabledAgents || []);
976
+ if (disabled.has(name)) disabled.delete(name); else disabled.add(name);
977
+ studio.disabledAgents = Array.from(disabled);
978
+ saveStudioConfig(studio);
979
+ res.json({ success: true, disabled: studio.disabledAgents.includes(name) });
980
+ } catch (err) {
981
+ res.status(500).json({ error: err.message });
982
+ }
983
+ });
984
+
985
+ app.get('/api/logs/stream', (req, res) => {
986
+ res.setHeader('Content-Type', 'text/event-stream');
987
+ res.setHeader('Cache-Control', 'no-cache');
988
+ res.setHeader('Connection', 'keep-alive');
989
+ res.setHeader('X-Accel-Buffering', 'no');
990
+ if (res.flushHeaders) res.flushHeaders();
991
+
992
+ setupLogWatcher();
993
+
994
+ const sub = { res, queue: [], draining: false, closed: false };
995
+ logSubscribers.add(sub);
996
+
997
+ logBuffer.forEach((entry) => {
998
+ sub.queue.push(`data: ${JSON.stringify(entry)}\n\n`);
999
+ });
1000
+ flushSubscriber(sub);
1001
+
1002
+ const keepalive = setInterval(() => {
1003
+ if (sub.closed) return;
1004
+ res.write(': keepalive\n\n');
1005
+ }, 20000);
1006
+
1007
+ req.on('close', () => {
1008
+ sub.closed = true;
1009
+ clearInterval(keepalive);
1010
+ logSubscribers.delete(sub);
1011
+ try { res.end(); } catch {}
1012
+ });
1013
+ });
1014
+
1015
+ app.get('/api/system/tools', (req, res) => {
1016
+ const knownTools = [
1017
+ 'go', 'gofmt', 'gopls',
1018
+ 'rust-analyzer', 'rustfmt',
1019
+ 'prettier', 'eslint',
1020
+ 'typescript-language-server', 'tsserver',
1021
+ 'pyright', 'ruff', 'python',
1022
+ 'clangd', 'clang-format',
1023
+ 'dart', 'jdtls',
1024
+ 'kotlin-language-server', 'ktlint',
1025
+ 'deno', 'lua-language-server',
1026
+ 'ocamllsp', 'nixd',
1027
+ 'swift', 'sourcekit-lsp'
1028
+ ];
1029
+
1030
+ const tools = knownTools.map((tool) => detectTool(tool));
1031
+ res.json(tools);
1032
+ });
1033
+
1034
+ app.get('/api/project/rules', (req, res) => {
1035
+ try {
1036
+ const found = findRulesFile();
1037
+ if (!found.path) return res.json({ content: '', source: 'none', path: null });
1038
+ const content = fs.readFileSync(found.path, 'utf8');
1039
+ res.json({ content, source: found.source, path: found.path });
1040
+ } catch (err) {
1041
+ res.status(500).json({ error: err.message });
1042
+ }
1043
+ });
1044
+
1045
+ app.post('/api/project/rules', (req, res) => {
1046
+ try {
1047
+ const { content, source } = req.body || {};
1048
+ const configPath = getConfigPath();
1049
+ if (!configPath) return res.status(400).json({ error: 'No config path found' });
1050
+
1051
+ const targetName = source === 'CLAUDE.md' ? 'CLAUDE.md' : 'AGENTS.md';
1052
+ const found = findRulesFile();
1053
+
1054
+ let targetPath = null;
1055
+ if (found.path && path.basename(found.path) === targetName) {
1056
+ targetPath = found.path;
1057
+ }
1058
+
1059
+ if (!targetPath) {
1060
+ const baseDir = path.dirname(configPath);
1061
+ targetPath = path.join(baseDir, targetName);
1062
+ }
1063
+
1064
+ atomicWriteFileSync(targetPath, content || '');
1065
+ res.json({ success: true, path: targetPath, source: targetName });
1066
+ } catch (err) {
1067
+ res.status(500).json({ error: err.message });
1068
+ }
1069
+ });
658
1070
 
659
1071
  app.get('/api/backup', (req, res) => {
660
1072
  try {
@@ -1110,11 +1522,11 @@ app.post('/api/ohmyopencode', (req, res) => {
1110
1522
 
1111
1523
  saveOhMyOpenCodeConfig(currentConfig);
1112
1524
 
1113
- const ohMyPath = getOhMyOpenCodeConfigPath();
1114
- triggerGitHubAutoSync();
1115
- res.json({
1116
- success: true,
1117
- path: ohMyPath,
1525
+ const ohMyPath = getOhMyOpenCodeConfigPath();
1526
+ triggerGitHubAutoSync();
1527
+ res.json({
1528
+ success: true,
1529
+ path: ohMyPath,
1118
1530
  exists: true,
1119
1531
  config: currentConfig,
1120
1532
  preferences,
@@ -1218,107 +1630,107 @@ function execPromise(cmd, opts = {}) {
1218
1630
  });
1219
1631
  }
1220
1632
 
1221
-
1222
- let autoSyncTimer = null;
1223
-
1224
- async function performGitHubBackup(options = {}) {
1225
- const { owner, repo, branch } = options;
1226
- let tempDir = null;
1227
- try {
1228
- const token = await getGitHubToken();
1229
- if (!token) throw new Error('Not logged in to gh CLI. Run: gh auth login');
1230
-
1231
- const user = await getGitHubUser(token);
1232
- if (!user) throw new Error('Failed to get GitHub user');
1233
-
1234
- const studio = loadStudioConfig();
1235
-
1236
- const finalOwner = owner || studio.githubBackup?.owner || user.login;
1237
- const finalRepo = repo || studio.githubBackup?.repo;
1238
- const finalBranch = branch || studio.githubBackup?.branch || 'main';
1239
-
1240
- if (!finalRepo) throw new Error('No repo name provided');
1241
-
1242
- const repoName = `${finalOwner}/${finalRepo}`;
1243
-
1244
- await ensureGitHubRepo(token, repoName);
1245
-
1246
- const opencodeConfig = getConfigPath();
1247
- if (!opencodeConfig) throw new Error('No opencode config path found');
1248
-
1249
- const opencodeDir = path.dirname(opencodeConfig);
1250
- const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1251
-
1252
- tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1253
- fs.mkdirSync(tempDir, { recursive: true });
1254
-
1255
- // Clone or init
1256
- try {
1257
- await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1258
- } catch (e) {
1259
- // If clone fails (empty repo?), try init
1260
- await execPromise('git init', { cwd: tempDir });
1261
- await execPromise(`git remote add origin https://x-access-token:${token}@github.com/${repoName}.git`, { cwd: tempDir });
1262
- await execPromise(`git checkout -b ${finalBranch}`, { cwd: tempDir });
1263
- }
1264
-
1265
- const backupOpencodeDir = path.join(tempDir, 'opencode');
1266
- const backupStudioDir = path.join(tempDir, 'opencode-studio');
1267
-
1268
- if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
1269
- if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
1270
-
1271
- copyDirContents(opencodeDir, backupOpencodeDir);
1272
- copyDirContents(studioDir, backupStudioDir);
1273
-
1274
- await execPromise('git add -A', { cwd: tempDir });
1275
-
1276
- const timestamp = new Date().toISOString();
1277
- const commitMessage = `OpenCode Studio backup ${timestamp}`;
1278
-
1279
- let result = { success: true, timestamp, url: `https://github.com/${repoName}` };
1280
-
1281
- try {
1282
- await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
1283
- await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
1284
- } catch (e) {
1285
- if (e.message.includes('nothing to commit')) {
1286
- result.message = 'No changes to backup';
1287
- } else {
1288
- throw e;
1289
- }
1290
- }
1291
-
1292
- studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1293
- studio.lastGithubBackup = timestamp;
1294
- saveStudioConfig(studio);
1295
-
1296
- return result;
1297
- } finally {
1298
- if (tempDir && fs.existsSync(tempDir)) {
1299
- try { fs.rmSync(tempDir, { recursive: true }); } catch (e) {}
1300
- }
1301
- }
1302
- }
1303
-
1304
- function triggerGitHubAutoSync() {
1305
- const studio = loadStudioConfig();
1306
- if (!studio.githubAutoSync) return;
1307
-
1308
- if (autoSyncTimer) clearTimeout(autoSyncTimer);
1309
-
1310
- console.log('[AutoSync] Change detected, scheduling GitHub backup in 15s...');
1311
- autoSyncTimer = setTimeout(async () => {
1312
- console.log('[AutoSync] Starting GitHub backup...');
1313
- try {
1314
- const result = await performGitHubBackup();
1315
- console.log(`[AutoSync] Backup completed: ${result.message || 'Pushed to GitHub'}`);
1316
- } catch (err) {
1317
- console.error('[AutoSync] Backup failed:', err.message);
1318
- }
1319
- }, 15000); // 15s debounce
1320
- }
1321
-
1633
+
1634
+ let autoSyncTimer = null;
1635
+
1636
+ async function performGitHubBackup(options = {}) {
1637
+ const { owner, repo, branch } = options;
1638
+ let tempDir = null;
1639
+ try {
1640
+ const token = await getGitHubToken();
1641
+ if (!token) throw new Error('Not logged in to gh CLI. Run: gh auth login');
1642
+
1643
+ const user = await getGitHubUser(token);
1644
+ if (!user) throw new Error('Failed to get GitHub user');
1645
+
1646
+ const studio = loadStudioConfig();
1647
+
1648
+ const finalOwner = owner || studio.githubBackup?.owner || user.login;
1649
+ const finalRepo = repo || studio.githubBackup?.repo;
1650
+ const finalBranch = branch || studio.githubBackup?.branch || 'main';
1651
+
1652
+ if (!finalRepo) throw new Error('No repo name provided');
1653
+
1654
+ const repoName = `${finalOwner}/${finalRepo}`;
1655
+
1656
+ await ensureGitHubRepo(token, repoName);
1657
+
1658
+ const opencodeConfig = getConfigPath();
1659
+ if (!opencodeConfig) throw new Error('No opencode config path found');
1660
+
1661
+ const opencodeDir = path.dirname(opencodeConfig);
1662
+ const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1663
+
1664
+ tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1665
+ fs.mkdirSync(tempDir, { recursive: true });
1666
+
1667
+ // Clone or init
1668
+ try {
1669
+ await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
1670
+ } catch (e) {
1671
+ // If clone fails (empty repo?), try init
1672
+ await execPromise('git init', { cwd: tempDir });
1673
+ await execPromise(`git remote add origin https://x-access-token:${token}@github.com/${repoName}.git`, { cwd: tempDir });
1674
+ await execPromise(`git checkout -b ${finalBranch}`, { cwd: tempDir });
1675
+ }
1676
+
1677
+ const backupOpencodeDir = path.join(tempDir, 'opencode');
1678
+ const backupStudioDir = path.join(tempDir, 'opencode-studio');
1679
+
1680
+ if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
1681
+ if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
1682
+
1683
+ copyDirContents(opencodeDir, backupOpencodeDir);
1684
+ copyDirContents(studioDir, backupStudioDir);
1685
+
1686
+ await execPromise('git add -A', { cwd: tempDir });
1687
+
1688
+ const timestamp = new Date().toISOString();
1689
+ const commitMessage = `OpenCode Studio backup ${timestamp}`;
1690
+
1691
+ let result = { success: true, timestamp, url: `https://github.com/${repoName}` };
1692
+
1693
+ try {
1694
+ await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
1695
+ await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
1696
+ } catch (e) {
1697
+ if (e.message.includes('nothing to commit')) {
1698
+ result.message = 'No changes to backup';
1699
+ } else {
1700
+ throw e;
1701
+ }
1702
+ }
1703
+
1704
+ studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1705
+ studio.lastGithubBackup = timestamp;
1706
+ saveStudioConfig(studio);
1707
+
1708
+ return result;
1709
+ } finally {
1710
+ if (tempDir && fs.existsSync(tempDir)) {
1711
+ try { fs.rmSync(tempDir, { recursive: true }); } catch (e) {}
1712
+ }
1713
+ }
1714
+ }
1715
+
1716
+ function triggerGitHubAutoSync() {
1717
+ const studio = loadStudioConfig();
1718
+ if (!studio.githubAutoSync) return;
1719
+
1720
+ if (autoSyncTimer) clearTimeout(autoSyncTimer);
1721
+
1722
+ console.log('[AutoSync] Change detected, scheduling GitHub backup in 15s...');
1723
+ autoSyncTimer = setTimeout(async () => {
1724
+ console.log('[AutoSync] Starting GitHub backup...');
1725
+ try {
1726
+ const result = await performGitHubBackup();
1727
+ console.log(`[AutoSync] Backup completed: ${result.message || 'Pushed to GitHub'}`);
1728
+ } catch (err) {
1729
+ console.error('[AutoSync] Backup failed:', err.message);
1730
+ }
1731
+ }, 15000); // 15s debounce
1732
+ }
1733
+
1322
1734
  app.get('/api/github/backup/status', async (req, res) => {
1323
1735
  try {
1324
1736
  const token = await getGitHubToken();
@@ -1350,15 +1762,15 @@ app.get('/api/github/backup/status', async (req, res) => {
1350
1762
  }
1351
1763
  });
1352
1764
 
1353
- app.post('/api/github/backup', async (req, res) => {
1354
- try {
1355
- const result = await performGitHubBackup(req.body);
1356
- res.json(result);
1357
- } catch (err) {
1358
- console.error('GitHub backup error:', err);
1359
- res.status(500).json({ error: err.message });
1360
- }
1361
- });
1765
+ app.post('/api/github/backup', async (req, res) => {
1766
+ try {
1767
+ const result = await performGitHubBackup(req.body);
1768
+ res.json(result);
1769
+ } catch (err) {
1770
+ console.error('GitHub backup error:', err);
1771
+ res.status(500).json({ error: err.message });
1772
+ }
1773
+ });
1362
1774
 
1363
1775
  app.post('/api/github/restore', async (req, res) => {
1364
1776
  let tempDir = null;
@@ -1417,10 +1829,10 @@ app.post('/api/github/restore', async (req, res) => {
1417
1829
  app.post('/api/github/autosync', async (req, res) => {
1418
1830
  const studio = loadStudioConfig();
1419
1831
  const enabled = req.body.enabled;
1420
- studio.githubAutoSync = enabled;
1421
- saveStudioConfig(studio);
1422
- if (enabled) triggerGitHubAutoSync();
1423
- res.json({ success: true, enabled });
1832
+ studio.githubAutoSync = enabled;
1833
+ saveStudioConfig(studio);
1834
+ if (enabled) triggerGitHubAutoSync();
1835
+ res.json({ success: true, enabled });
1424
1836
  });
1425
1837
 
1426
1838
  const getSkillDir = () => {
@@ -1457,9 +1869,9 @@ app.post('/api/skills/:name', (req, res) => {
1457
1869
  if (!sd) return res.status(404).json({ error: 'No config' });
1458
1870
  const dp = path.join(sd, req.params.name);
1459
1871
  if (!fs.existsSync(dp)) fs.mkdirSync(dp, { recursive: true });
1460
- fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1461
- triggerGitHubAutoSync();
1462
- res.json({ success: true });
1872
+ fs.writeFileSync(path.join(dp, 'SKILL.md'), req.body.content, 'utf8');
1873
+ triggerGitHubAutoSync();
1874
+ res.json({ success: true });
1463
1875
  });
1464
1876
 
1465
1877
  app.delete('/api/skills/:name', (req, res) => {
@@ -1468,9 +1880,9 @@ app.delete('/api/skills/:name', (req, res) => {
1468
1880
  }
1469
1881
  const sd = getSkillDir();
1470
1882
  const dp = sd ? path.join(sd, req.params.name) : null;
1471
- if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1472
- triggerGitHubAutoSync();
1473
- res.json({ success: true });
1883
+ if (dp && fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
1884
+ triggerGitHubAutoSync();
1885
+ res.json({ success: true });
1474
1886
  });
1475
1887
 
1476
1888
  app.post('/api/skills/:name/toggle', (req, res) => {
@@ -1484,9 +1896,9 @@ app.post('/api/skills/:name/toggle', (req, res) => {
1484
1896
  studio.disabledSkills.push(name);
1485
1897
  }
1486
1898
 
1487
- saveStudioConfig(studio);
1488
- triggerGitHubAutoSync();
1489
- res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1899
+ saveStudioConfig(studio);
1900
+ triggerGitHubAutoSync();
1901
+ res.json({ success: true, enabled: !studio.disabledSkills.includes(name) });
1490
1902
  });
1491
1903
 
1492
1904
  const getPluginDir = () => {
@@ -1563,10 +1975,10 @@ app.post('/api/plugins/:name', (req, res) => {
1563
1975
  if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
1564
1976
 
1565
1977
  // Default to .js if new
1566
- const filePath = path.join(pd, name.endsWith('.js') || name.endsWith('.ts') ? name : name + '.js');
1567
- atomicWriteFileSync(filePath, content);
1568
- triggerGitHubAutoSync();
1569
- res.json({ success: true });
1978
+ const filePath = path.join(pd, name.endsWith('.js') || name.endsWith('.ts') ? name : name + '.js');
1979
+ atomicWriteFileSync(filePath, content);
1980
+ triggerGitHubAutoSync();
1981
+ res.json({ success: true });
1570
1982
  });
1571
1983
 
1572
1984
  app.delete('/api/plugins/:name', (req, res) => {
@@ -1591,10 +2003,10 @@ app.delete('/api/plugins/:name', (req, res) => {
1591
2003
  }
1592
2004
  }
1593
2005
 
1594
- if (deleted) {
1595
- triggerGitHubAutoSync();
1596
- res.json({ success: true });
1597
- } else res.status(404).json({ error: 'Plugin not found' });
2006
+ if (deleted) {
2007
+ triggerGitHubAutoSync();
2008
+ res.json({ success: true });
2009
+ } else res.status(404).json({ error: 'Plugin not found' });
1598
2010
  });
1599
2011
 
1600
2012
  app.post('/api/plugins/:name/toggle', (req, res) => {
@@ -1608,9 +2020,9 @@ app.post('/api/plugins/:name/toggle', (req, res) => {
1608
2020
  studio.disabledPlugins.push(name);
1609
2021
  }
1610
2022
 
1611
- saveStudioConfig(studio);
1612
- triggerGitHubAutoSync();
1613
- res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
2023
+ saveStudioConfig(studio);
2024
+ triggerGitHubAutoSync();
2025
+ res.json({ success: true, enabled: !studio.disabledPlugins.includes(name) });
1614
2026
  });
1615
2027
 
1616
2028
  const getActiveGooglePlugin = () => {
@@ -3504,23 +3916,23 @@ app.post('/api/presets/:id/apply', (req, res) => {
3504
3916
  res.json({ success: true });
3505
3917
  });
3506
3918
 
3507
- // Start watcher on server start
3508
- async function startServer() {
3509
- ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'].forEach(p => importCurrentAuthToPool(p));
3510
-
3511
- const port = await findAvailablePort(DEFAULT_PORT);
3512
- app.listen(port, () => {
3513
- console.log(`Server running at http://localhost:${port}`);
3514
- // Initial sync on startup if enabled
3515
- setTimeout(() => {
3516
- const studio = loadStudioConfig();
3517
- if (studio.githubAutoSync) {
3518
- console.log('[AutoSync] Triggering initial sync...');
3519
- triggerGitHubAutoSync();
3520
- }
3521
- }, 5000);
3522
- });
3523
- }
3919
+ // Start watcher on server start
3920
+ async function startServer() {
3921
+ ['google', 'anthropic', 'openai', 'xai', 'openrouter', 'together', 'mistral', 'deepseek', 'amazon-bedrock', 'azure', 'github-copilot'].forEach(p => importCurrentAuthToPool(p));
3922
+
3923
+ const port = await findAvailablePort(DEFAULT_PORT);
3924
+ app.listen(port, () => {
3925
+ console.log(`Server running at http://localhost:${port}`);
3926
+ // Initial sync on startup if enabled
3927
+ setTimeout(() => {
3928
+ const studio = loadStudioConfig();
3929
+ if (studio.githubAutoSync) {
3930
+ console.log('[AutoSync] Triggering initial sync...');
3931
+ triggerGitHubAutoSync();
3932
+ }
3933
+ }, 5000);
3934
+ });
3935
+ }
3524
3936
 
3525
3937
  if (require.main === module) {
3526
3938
  startServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {