@adversity/coding-tool-x 3.0.6 → 3.1.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +38 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +92 -13
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/services/permission-templates-service.js +0 -308
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
 
5
- const FAVORITES_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
5
+ const FAVORITES_DIR = path.join(os.homedir(), '.cc-tool');
6
6
  const FAVORITES_FILE = path.join(FAVORITES_DIR, 'favorites.json');
7
7
 
8
8
  // 内存缓存
@@ -12,7 +12,8 @@ let cacheInitialized = false;
12
12
  const DEFAULT_FAVORITES = {
13
13
  claude: [],
14
14
  codex: [],
15
- gemini: []
15
+ gemini: [],
16
+ opencode: []
16
17
  };
17
18
 
18
19
  // Ensure favorites directory exists
@@ -36,7 +37,8 @@ function readFavoritesFromFile() {
36
37
  return {
37
38
  claude: data.claude || [],
38
39
  codex: data.codex || [],
39
- gemini: data.gemini || []
40
+ gemini: data.gemini || [],
41
+ opencode: data.opencode || []
40
42
  };
41
43
  } catch (error) {
42
44
  console.error('Error reading favorites file:', error);
@@ -16,6 +16,14 @@ const crypto = require('crypto');
16
16
  * - 使用 weight 和 maxConcurrency 控制负载均衡
17
17
  */
18
18
 
19
+ function normalizeGatewaySourceType(value, fallback = 'gemini') {
20
+ const normalized = String(value || '').trim().toLowerCase();
21
+ if (normalized === 'claude') return 'claude';
22
+ if (normalized === 'codex') return 'codex';
23
+ if (normalized === 'gemini') return 'gemini';
24
+ return fallback;
25
+ }
26
+
19
27
  // 获取 Gemini 配置目录
20
28
  function getGeminiDir() {
21
29
  return path.join(os.homedir(), '.gemini');
@@ -23,7 +31,7 @@ function getGeminiDir() {
23
31
 
24
32
  // 获取渠道存储文件路径
25
33
  function getChannelsFilePath() {
26
- const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
34
+ const ccToolDir = path.join(os.homedir(), '.cc-tool');
27
35
  if (!fs.existsSync(ccToolDir)) {
28
36
  fs.mkdirSync(ccToolDir, { recursive: true });
29
37
  }
@@ -66,14 +74,17 @@ function loadChannels() {
66
74
  const data = JSON.parse(content);
67
75
  // 确保渠道有 enabled 字段(兼容旧数据)
68
76
  if (data.channels) {
69
- data.channels = data.channels.map(ch => ({
70
- ...ch,
71
- enabled: ch.enabled !== false, // 默认启用
72
- weight: ch.weight || 1,
73
- maxConcurrency: ch.maxConcurrency || null,
74
- modelRedirects: ch.modelRedirects || [],
75
- speedTestModel: ch.speedTestModel || null
76
- }));
77
+ data.channels = data.channels.map(ch => {
78
+ return {
79
+ ...ch,
80
+ enabled: ch.enabled !== false, // 默认启用
81
+ weight: ch.weight || 1,
82
+ maxConcurrency: ch.maxConcurrency || null,
83
+ modelRedirects: ch.modelRedirects || [],
84
+ speedTestModel: ch.speedTestModel || null,
85
+ gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'gemini')
86
+ };
87
+ });
77
88
  }
78
89
  return data;
79
90
  } catch (err) {
@@ -118,6 +129,7 @@ function initializeFromEnv() {
118
129
  enabled: true,
119
130
  weight: 1,
120
131
  maxConcurrency: null,
132
+ gatewaySourceType: 'gemini',
121
133
  createdAt: Date.now(),
122
134
  updatedAt: Date.now()
123
135
  };
@@ -175,6 +187,7 @@ function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraCon
175
187
  maxConcurrency: extraConfig.maxConcurrency || null,
176
188
  modelRedirects: extraConfig.modelRedirects || [],
177
189
  speedTestModel: extraConfig.speedTestModel || null,
190
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'gemini'),
178
191
  createdAt: Date.now(),
179
192
  updatedAt: Date.now()
180
193
  };
@@ -207,15 +220,17 @@ function updateChannel(channelId, updates) {
207
220
  }
208
221
  }
209
222
 
210
- data.channels[index] = {
211
- ...oldChannel,
212
- ...updates,
223
+ const merged = { ...oldChannel, ...updates };
224
+ const nextChannel = {
225
+ ...merged,
213
226
  id: channelId, // 保持 ID 不变
214
227
  createdAt: oldChannel.createdAt, // 保持创建时间
215
228
  modelRedirects: updates.modelRedirects !== undefined ? updates.modelRedirects : (oldChannel.modelRedirects || []),
216
229
  speedTestModel: updates.speedTestModel !== undefined ? updates.speedTestModel : (oldChannel.speedTestModel || null),
230
+ gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'gemini'),
217
231
  updatedAt: Date.now()
218
232
  };
233
+ data.channels[index] = nextChannel;
219
234
 
220
235
  // Get proxy status
221
236
  const { getGeminiProxyStatus } = require('../gemini-proxy-server');
@@ -279,8 +294,9 @@ function applyChannelToSettings(channelId, channels = null) {
279
294
  const envPath = path.join(geminiDir, '.env');
280
295
 
281
296
  // 构建 .env 内容
297
+ const effectiveApiKey = getEffectiveApiKey(channel) || '';
282
298
  const envContent = `GOOGLE_GEMINI_BASE_URL=${channel.baseUrl}
283
- GEMINI_API_KEY=${channel.apiKey}
299
+ GEMINI_API_KEY=${effectiveApiKey}
284
300
  GEMINI_MODEL=${channel.model}
285
301
  `;
286
302
 
@@ -315,7 +331,7 @@ GEMINI_MODEL=${channel.model}
315
331
  }
316
332
 
317
333
  // 删除渠道
318
- function deleteChannel(channelId) {
334
+ async function deleteChannel(channelId) {
319
335
  const data = loadChannels();
320
336
 
321
337
  const index = data.channels.findIndex(c => c.id === channelId);
@@ -354,8 +370,9 @@ function writeGeminiConfigForMultiChannel(allChannels) {
354
370
  }
355
371
 
356
372
  // 构建 .env 内容
373
+ const effectiveApiKey = getEffectiveApiKey(defaultChannel) || '';
357
374
  const envContent = `GOOGLE_GEMINI_BASE_URL=${defaultChannel.baseUrl}
358
- GEMINI_API_KEY=${defaultChannel.apiKey}
375
+ GEMINI_API_KEY=${effectiveApiKey}
359
376
  GEMINI_MODEL=${defaultChannel.model}
360
377
  `;
361
378
 
@@ -392,6 +409,10 @@ function getEnabledChannels() {
392
409
  return data.channels.filter(c => c.enabled !== false);
393
410
  }
394
411
 
412
+ function getEffectiveApiKey(channel) {
413
+ return channel.apiKey || null;
414
+ }
415
+
395
416
  // 保存渠道顺序
396
417
  function saveChannelOrder(order) {
397
418
  const data = loadChannels();
@@ -422,6 +443,7 @@ module.exports = {
422
443
  updateChannel,
423
444
  deleteChannel,
424
445
  getEnabledChannels,
446
+ getEffectiveApiKey,
425
447
  saveChannelOrder,
426
448
  isProxyConfig,
427
449
  getGeminiDir,
@@ -6,7 +6,7 @@ const os = require('os');
6
6
  * Gemini 统计服务 - 数据采集和存储
7
7
  *
8
8
  * 文件结构:
9
- * ~/.claude/cc-tool/
9
+ * ~/.cc-tool/
10
10
  * ├── gemini-statistics.json # Gemini 总体统计
11
11
  * └── gemini-daily-stats/
12
12
  * ├── 2025-12-05.json # 每日汇总统计
@@ -15,7 +15,7 @@ const os = require('os');
15
15
 
16
16
  // 获取基础目录
17
17
  function getBaseDir() {
18
- const dir = path.join(os.homedir(), '.claude', 'cc-tool');
18
+ const dir = path.join(os.homedir(), '.cc-tool');
19
19
  if (!fs.existsSync(dir)) {
20
20
  fs.mkdirSync(dir, { recursive: true });
21
21
  }
@@ -12,15 +12,22 @@ const { spawn } = require('child_process');
12
12
  const http = require('http');
13
13
  const https = require('https');
14
14
  const { McpClient } = require('./mcp-client');
15
+ const { NATIVE_PATHS } = require('../../config/paths');
15
16
 
16
17
  // MCP 配置文件路径
17
- const CC_TOOL_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
18
+ const CC_TOOL_DIR = path.join(os.homedir(), '.cc-tool');
18
19
  const MCP_SERVERS_FILE = path.join(CC_TOOL_DIR, 'mcp-servers.json');
19
20
 
20
21
  // 各平台配置文件路径
21
22
  const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
22
23
  const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
23
24
  const GEMINI_CONFIG_PATH = path.join(os.homedir(), '.gemini', 'settings.json');
25
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
26
+ const OPENCODE_CONFIG_PATHS = {
27
+ jsonc: path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
28
+ json: path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
29
+ legacy: path.join(OPENCODE_CONFIG_DIR, 'config.json')
30
+ };
24
31
 
25
32
  // MCP 客户端连接池
26
33
  // serverId -> { client, timestamp }
@@ -301,15 +308,140 @@ function writeTomlFile(filePath, data) {
301
308
  fs.renameSync(tempPath, filePath);
302
309
  }
303
310
 
311
+ /**
312
+ * 去除 JSONC 注释
313
+ */
314
+ function stripJsonComments(input) {
315
+ let result = '';
316
+ let inString = false;
317
+ let quote = '';
318
+ let index = 0;
319
+
320
+ while (index < input.length) {
321
+ const ch = input[index];
322
+ const next = input[index + 1];
323
+
324
+ if (inString) {
325
+ result += ch;
326
+ if (ch === '\\') {
327
+ if (next) {
328
+ result += next;
329
+ index += 2;
330
+ continue;
331
+ }
332
+ } else if (ch === quote) {
333
+ inString = false;
334
+ }
335
+ index += 1;
336
+ continue;
337
+ }
338
+
339
+ if (ch === '"' || ch === '\'') {
340
+ inString = true;
341
+ quote = ch;
342
+ result += ch;
343
+ index += 1;
344
+ continue;
345
+ }
346
+
347
+ if (ch === '/' && next === '/') {
348
+ index += 2;
349
+ while (index < input.length && input[index] !== '\n') {
350
+ index += 1;
351
+ }
352
+ continue;
353
+ }
354
+
355
+ if (ch === '/' && next === '*') {
356
+ index += 2;
357
+ while (index < input.length - 1 && !(input[index] === '*' && input[index + 1] === '/')) {
358
+ index += 1;
359
+ }
360
+ index += 2;
361
+ continue;
362
+ }
363
+
364
+ result += ch;
365
+ index += 1;
366
+ }
367
+
368
+ return result;
369
+ }
370
+
371
+ /**
372
+ * 选择 OpenCode 配置文件路径
373
+ */
374
+ function selectOpenCodeConfigPath() {
375
+ if (fs.existsSync(OPENCODE_CONFIG_PATHS.jsonc)) return OPENCODE_CONFIG_PATHS.jsonc;
376
+ if (fs.existsSync(OPENCODE_CONFIG_PATHS.json)) return OPENCODE_CONFIG_PATHS.json;
377
+ if (fs.existsSync(OPENCODE_CONFIG_PATHS.legacy)) return OPENCODE_CONFIG_PATHS.legacy;
378
+ return OPENCODE_CONFIG_PATHS.json;
379
+ }
380
+
381
+ /**
382
+ * 读取 OpenCode 配置
383
+ */
384
+ function readOpenCodeConfig() {
385
+ const filePath = selectOpenCodeConfigPath();
386
+
387
+ if (!fs.existsSync(filePath)) {
388
+ return { path: filePath, config: {} };
389
+ }
390
+
391
+ try {
392
+ const raw = fs.readFileSync(filePath, 'utf-8');
393
+ if (!raw.trim()) {
394
+ return { path: filePath, config: {} };
395
+ }
396
+
397
+ const content = filePath.endsWith('.jsonc') ? stripJsonComments(raw) : raw;
398
+ return {
399
+ path: filePath,
400
+ config: JSON.parse(content)
401
+ };
402
+ } catch (err) {
403
+ console.error(`[MCP] Failed to read OpenCode config:`, err.message);
404
+ return { path: filePath, config: {} };
405
+ }
406
+ }
407
+
408
+ /**
409
+ * 写入 OpenCode 配置(保持 JSON 格式)
410
+ */
411
+ function writeOpenCodeConfig(filePath, data) {
412
+ ensureDir(path.dirname(filePath));
413
+ const tempPath = filePath + '.tmp';
414
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
415
+ fs.renameSync(tempPath, filePath);
416
+ }
417
+
304
418
  // ============================================================================
305
419
  // MCP 数据管理
306
420
  // ============================================================================
307
421
 
422
+ function normalizeServerApps(apps = {}) {
423
+ return {
424
+ claude: apps.claude !== undefined ? !!apps.claude : true,
425
+ codex: !!apps.codex,
426
+ gemini: !!apps.gemini,
427
+ opencode: !!apps.opencode
428
+ };
429
+ }
430
+
308
431
  /**
309
432
  * 获取所有 MCP 服务器
310
433
  */
311
434
  function getAllServers() {
312
- return readJsonFile(MCP_SERVERS_FILE, {});
435
+ const servers = readJsonFile(MCP_SERVERS_FILE, {});
436
+
437
+ for (const server of Object.values(servers)) {
438
+ if (!server || typeof server !== 'object') {
439
+ continue;
440
+ }
441
+ server.apps = normalizeServerApps(server.apps);
442
+ }
443
+
444
+ return servers;
313
445
  }
314
446
 
315
447
  /**
@@ -341,7 +473,9 @@ async function saveServer(server) {
341
473
 
342
474
  // 确保 apps 字段存在
343
475
  if (!server.apps) {
344
- server.apps = { claude: true, codex: false, gemini: false };
476
+ server.apps = { claude: true, codex: false, gemini: false, opencode: false };
477
+ } else {
478
+ server.apps = normalizeServerApps(server.apps);
345
479
  }
346
480
 
347
481
  servers[server.id] = server;
@@ -384,7 +518,7 @@ async function toggleServerApp(serverId, app, enabled) {
384
518
  throw new Error(`MCP 服务器 "${serverId}" 不存在`);
385
519
  }
386
520
 
387
- if (!['claude', 'codex', 'gemini'].includes(app)) {
521
+ if (!['claude', 'codex', 'gemini', 'opencode'].includes(app)) {
388
522
  throw new Error(`无效的平台: ${app}`);
389
523
  }
390
524
 
@@ -466,6 +600,12 @@ async function syncServerToAllPlatforms(server) {
466
600
  } else {
467
601
  await removeServerFromPlatform(server.id, 'gemini');
468
602
  }
603
+
604
+ if (apps.opencode) {
605
+ await syncServerToPlatform(server, 'opencode');
606
+ } else {
607
+ await removeServerFromPlatform(server.id, 'opencode');
608
+ }
469
609
  }
470
610
 
471
611
  /**
@@ -475,6 +615,7 @@ async function removeServerFromAllPlatforms(serverId) {
475
615
  await removeServerFromPlatform(serverId, 'claude');
476
616
  await removeServerFromPlatform(serverId, 'codex');
477
617
  await removeServerFromPlatform(serverId, 'gemini');
618
+ await removeServerFromPlatform(serverId, 'opencode');
478
619
  }
479
620
 
480
621
  /**
@@ -492,6 +633,9 @@ async function syncServerToPlatform(server, platform) {
492
633
  case 'gemini':
493
634
  syncToGeminiConfig(server);
494
635
  break;
636
+ case 'opencode':
637
+ syncToOpenCodeConfig(server);
638
+ break;
495
639
  }
496
640
  console.log(`[MCP] Synced "${server.id}" to ${platform}`);
497
641
  } catch (err) {
@@ -515,6 +659,9 @@ async function removeServerFromPlatform(serverId, platform) {
515
659
  case 'gemini':
516
660
  removeFromGeminiConfig(serverId);
517
661
  break;
662
+ case 'opencode':
663
+ removeFromOpenCodeConfig(serverId);
664
+ break;
518
665
  }
519
666
  console.log(`[MCP] Removed "${serverId}" from ${platform}`);
520
667
  } catch (err) {
@@ -647,6 +794,143 @@ function removeFromGeminiConfig(serverId) {
647
794
  }
648
795
  }
649
796
 
797
+ // ============================================================================
798
+ // OpenCode 配置同步
799
+ // ============================================================================
800
+
801
+ /**
802
+ * 转换为 OpenCode 配置格式
803
+ */
804
+ function convertToOpenCodeFormat(spec) {
805
+ const sourceType = spec.type || 'stdio';
806
+
807
+ if (sourceType === 'local' || sourceType === 'remote') {
808
+ const result = { ...spec };
809
+ result.enabled = spec.enabled !== false;
810
+ if (sourceType === 'local' && typeof result.command === 'string') {
811
+ result.command = result.command ? [result.command] : [];
812
+ }
813
+ return result;
814
+ }
815
+
816
+ if (sourceType === 'stdio') {
817
+ const command = [];
818
+ if (spec.command) {
819
+ command.push(spec.command);
820
+ }
821
+ if (Array.isArray(spec.args) && spec.args.length > 0) {
822
+ command.push(...spec.args);
823
+ }
824
+
825
+ const result = {
826
+ type: 'local',
827
+ command,
828
+ enabled: true
829
+ };
830
+
831
+ if (spec.env && Object.keys(spec.env).length > 0) {
832
+ result.environment = spec.env;
833
+ }
834
+ if (spec.cwd) {
835
+ result.cwd = spec.cwd;
836
+ }
837
+
838
+ return result;
839
+ }
840
+
841
+ const result = {
842
+ type: 'remote',
843
+ url: spec.url || '',
844
+ enabled: true
845
+ };
846
+
847
+ if (spec.headers && Object.keys(spec.headers).length > 0) {
848
+ result.headers = spec.headers;
849
+ }
850
+
851
+ return result;
852
+ }
853
+
854
+ /**
855
+ * 从 OpenCode 格式转换到通用格式
856
+ */
857
+ function convertFromOpenCodeFormat(spec) {
858
+ const sourceType = spec.type || (Array.isArray(spec.command) ? 'local' : 'remote');
859
+
860
+ if (sourceType === 'local') {
861
+ const result = { type: 'stdio' };
862
+ if (Array.isArray(spec.command) && spec.command.length > 0) {
863
+ result.command = spec.command[0];
864
+ if (spec.command.length > 1) {
865
+ result.args = spec.command.slice(1);
866
+ }
867
+ } else if (typeof spec.command === 'string') {
868
+ result.command = spec.command;
869
+ } else {
870
+ result.command = '';
871
+ }
872
+
873
+ if (spec.environment && typeof spec.environment === 'object') {
874
+ result.env = spec.environment;
875
+ } else if (spec.env && typeof spec.env === 'object') {
876
+ result.env = spec.env;
877
+ }
878
+ if (spec.cwd) {
879
+ result.cwd = spec.cwd;
880
+ }
881
+ return result;
882
+ }
883
+
884
+ if (sourceType === 'remote') {
885
+ const result = {
886
+ type: 'http',
887
+ url: spec.url || ''
888
+ };
889
+ if (spec.headers && typeof spec.headers === 'object') {
890
+ result.headers = spec.headers;
891
+ }
892
+ return result;
893
+ }
894
+
895
+ // 已经是通用格式时直接兼容处理
896
+ if (sourceType === 'stdio' || sourceType === 'http' || sourceType === 'sse') {
897
+ return convertFromCodexFormat(spec);
898
+ }
899
+
900
+ return {
901
+ type: 'stdio',
902
+ command: ''
903
+ };
904
+ }
905
+
906
+ /**
907
+ * 同步到 OpenCode 配置
908
+ */
909
+ function syncToOpenCodeConfig(server) {
910
+ const { path: configPath, config } = readOpenCodeConfig();
911
+ const nextConfig = config && typeof config === 'object' ? config : {};
912
+
913
+ if (!nextConfig.mcp || typeof nextConfig.mcp !== 'object') {
914
+ nextConfig.mcp = {};
915
+ }
916
+
917
+ nextConfig.mcp[server.id] = convertToOpenCodeFormat(server.server);
918
+ writeOpenCodeConfig(configPath, nextConfig);
919
+ }
920
+
921
+ /**
922
+ * 从 OpenCode 配置移除
923
+ */
924
+ function removeFromOpenCodeConfig(serverId) {
925
+ const { path: configPath, config } = readOpenCodeConfig();
926
+ const nextConfig = config && typeof config === 'object' ? config : {};
927
+
928
+ if (nextConfig.mcp && nextConfig.mcp[serverId]) {
929
+ delete nextConfig.mcp[serverId];
930
+ writeOpenCodeConfig(configPath, nextConfig);
931
+ }
932
+ }
933
+
650
934
  // ============================================================================
651
935
  // 导入功能
652
936
  // ============================================================================
@@ -668,6 +952,9 @@ async function importFromPlatform(platform) {
668
952
  case 'gemini':
669
953
  importedCount = importFromGemini(servers);
670
954
  break;
955
+ case 'opencode':
956
+ importedCount = importFromOpenCode(servers);
957
+ break;
671
958
  default:
672
959
  throw new Error(`无效的平台: ${platform}`);
673
960
  }
@@ -700,7 +987,7 @@ function importFromClaude(servers) {
700
987
  id,
701
988
  name: id,
702
989
  server: spec,
703
- apps: { claude: true, codex: false, gemini: false },
990
+ apps: { claude: true, codex: false, gemini: false, opencode: false },
704
991
  createdAt: Date.now(),
705
992
  updatedAt: Date.now()
706
993
  };
@@ -735,7 +1022,7 @@ function importFromCodex(servers) {
735
1022
  id,
736
1023
  name: id,
737
1024
  server: convertedSpec,
738
- apps: { claude: false, codex: true, gemini: false },
1025
+ apps: { claude: false, codex: true, gemini: false, opencode: false },
739
1026
  createdAt: Date.now(),
740
1027
  updatedAt: Date.now()
741
1028
  };
@@ -767,7 +1054,39 @@ function importFromGemini(servers) {
767
1054
  id,
768
1055
  name: id,
769
1056
  server: spec,
770
- apps: { claude: false, codex: false, gemini: true },
1057
+ apps: { claude: false, codex: false, gemini: true, opencode: false },
1058
+ createdAt: Date.now(),
1059
+ updatedAt: Date.now()
1060
+ };
1061
+ count++;
1062
+ }
1063
+ }
1064
+
1065
+ return count;
1066
+ }
1067
+
1068
+ /**
1069
+ * 从 OpenCode 导入
1070
+ */
1071
+ function importFromOpenCode(servers) {
1072
+ const { config } = readOpenCodeConfig();
1073
+ const mcpServers = config.mcp || {};
1074
+ let count = 0;
1075
+
1076
+ for (const [id, spec] of Object.entries(mcpServers)) {
1077
+ const convertedSpec = convertFromOpenCodeFormat(spec || {});
1078
+
1079
+ if (servers[id]) {
1080
+ if (!servers[id].apps.opencode) {
1081
+ servers[id].apps.opencode = true;
1082
+ count++;
1083
+ }
1084
+ } else {
1085
+ servers[id] = {
1086
+ id,
1087
+ name: id,
1088
+ server: convertedSpec,
1089
+ apps: { claude: false, codex: false, gemini: false, opencode: true },
771
1090
  createdAt: Date.now(),
772
1091
  updatedAt: Date.now()
773
1092
  };
@@ -838,7 +1157,8 @@ function getStats() {
838
1157
  total: serverList.length,
839
1158
  claude: serverList.filter(s => s.apps?.claude).length,
840
1159
  codex: serverList.filter(s => s.apps?.codex).length,
841
- gemini: serverList.filter(s => s.apps?.gemini).length
1160
+ gemini: serverList.filter(s => s.apps?.gemini).length,
1161
+ opencode: serverList.filter(s => s.apps?.opencode).length
842
1162
  };
843
1163
  }
844
1164
 
@@ -1300,7 +1620,7 @@ function updateServerOrder(serverIds) {
1300
1620
 
1301
1621
  /**
1302
1622
  * 导出所有 MCP 配置
1303
- * @param {string} format - 导出格式: 'json' | 'claude' | 'codex'
1623
+ * @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode'
1304
1624
  */
1305
1625
  function exportServers(format = 'json') {
1306
1626
  const servers = getAllServers();
@@ -1310,6 +1630,8 @@ function exportServers(format = 'json') {
1310
1630
  return exportForClaude(servers);
1311
1631
  case 'codex':
1312
1632
  return exportForCodex(servers);
1633
+ case 'opencode':
1634
+ return exportForOpenCode(servers);
1313
1635
  case 'json':
1314
1636
  default:
1315
1637
  return exportAsJson(servers);
@@ -1371,6 +1693,25 @@ function exportForCodex(servers) {
1371
1693
  };
1372
1694
  }
1373
1695
 
1696
+ /**
1697
+ * 导出为 OpenCode 格式
1698
+ */
1699
+ function exportForOpenCode(servers) {
1700
+ const mcp = {};
1701
+
1702
+ for (const [id, server] of Object.entries(servers)) {
1703
+ if (server.apps?.opencode) {
1704
+ mcp[id] = convertToOpenCodeFormat(server.server);
1705
+ }
1706
+ }
1707
+
1708
+ return {
1709
+ format: 'opencode',
1710
+ content: JSON.stringify({ mcp }, null, 2),
1711
+ filename: 'opencode-mcp-config.json'
1712
+ };
1713
+ }
1714
+
1374
1715
  module.exports = {
1375
1716
  getAllServers,
1376
1717
  getServer,