@adversity/coding-tool-x 3.1.0 → 3.1.2

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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.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 +81 -12
  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/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -11,18 +11,263 @@ const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('
11
11
  const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
12
12
  const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
13
13
  const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
14
+ const { NATIVE_PATHS } = require('../../config/paths');
14
15
 
15
16
  const CLAUDE_PLUGINS_DIR = path.join(os.homedir(), '.claude', 'plugins');
16
17
  const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
17
18
  const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
19
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
20
+ const DEFAULT_REPOS_BY_PLATFORM = {
21
+ claude: [],
22
+ opencode: [
23
+ {
24
+ owner: 'Tommertom',
25
+ name: 'opencode-plugin-marketplace',
26
+ url: 'https://github.com/Tommertom/opencode-plugin-marketplace',
27
+ branch: 'main',
28
+ enabled: true,
29
+ source: 'opencode-default'
30
+ },
31
+ {
32
+ owner: 'avifenesh',
33
+ name: 'awesome-slash',
34
+ url: 'https://github.com/avifenesh/awesome-slash',
35
+ branch: 'main',
36
+ enabled: true,
37
+ source: 'opencode-default'
38
+ },
39
+ {
40
+ owner: 'NeoLabHQ',
41
+ name: 'context-engineering-kit',
42
+ url: 'https://github.com/NeoLabHQ/context-engineering-kit',
43
+ branch: 'master',
44
+ enabled: true,
45
+ source: 'opencode-default'
46
+ }
47
+ ]
48
+ };
49
+
50
+ function cloneRepos(repos = []) {
51
+ return repos.map(repo => ({ ...repo }));
52
+ }
53
+
54
+ function stripJsonComments(input = '') {
55
+ let result = '';
56
+ let inString = false;
57
+ let stringChar = '';
58
+ let i = 0;
59
+
60
+ while (i < input.length) {
61
+ const ch = input[i];
62
+ const next = input[i + 1];
63
+
64
+ if (inString) {
65
+ result += ch;
66
+ if (ch === '\\') {
67
+ if (next) {
68
+ result += next;
69
+ i += 2;
70
+ continue;
71
+ }
72
+ } else if (ch === stringChar) {
73
+ inString = false;
74
+ }
75
+ i += 1;
76
+ continue;
77
+ }
78
+
79
+ if (ch === '"' || ch === '\'') {
80
+ inString = true;
81
+ stringChar = ch;
82
+ result += ch;
83
+ i += 1;
84
+ continue;
85
+ }
86
+
87
+ if (ch === '/' && next === '/') {
88
+ i += 2;
89
+ while (i < input.length && input[i] !== '\n') i += 1;
90
+ continue;
91
+ }
92
+
93
+ if (ch === '/' && next === '*') {
94
+ i += 2;
95
+ while (i < input.length - 1 && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
96
+ i += 2;
97
+ continue;
98
+ }
99
+
100
+ result += ch;
101
+ i += 1;
102
+ }
103
+
104
+ return result;
105
+ }
18
106
 
19
107
  class PluginsService {
108
+ constructor(platform = 'claude') {
109
+ this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
110
+ this.ccToolConfigDir = path.join(os.homedir(), '.cc-tool');
111
+ this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
112
+ this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
113
+ }
114
+
115
+ _ensureDir(dirPath) {
116
+ if (!fs.existsSync(dirPath)) {
117
+ fs.mkdirSync(dirPath, { recursive: true });
118
+ }
119
+ }
120
+
121
+ _isOpenCode() {
122
+ return this.platform === 'opencode';
123
+ }
124
+
125
+ _getOpenCodePluginsDir() {
126
+ if (fs.existsSync(this.opencodeLegacyPluginsDir) && !fs.existsSync(this.opencodePluginsDir)) {
127
+ return this.opencodeLegacyPluginsDir;
128
+ }
129
+ return this.opencodePluginsDir;
130
+ }
131
+
132
+ _getOpenCodeConfigPath() {
133
+ const jsonc = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
134
+ const json = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
135
+ const config = path.join(OPENCODE_CONFIG_DIR, 'config.json');
136
+ if (fs.existsSync(jsonc)) return jsonc;
137
+ if (fs.existsSync(json)) return json;
138
+ if (fs.existsSync(config)) return config;
139
+ return json;
140
+ }
141
+
142
+ _readOpenCodeConfig() {
143
+ const filePath = this._getOpenCodeConfigPath();
144
+ if (!fs.existsSync(filePath)) return { filePath, config: {} };
145
+
146
+ try {
147
+ const raw = fs.readFileSync(filePath, 'utf8');
148
+ if (!raw.trim()) return { filePath, config: {} };
149
+ if (filePath.endsWith('.jsonc')) {
150
+ return { filePath, config: JSON.parse(stripJsonComments(raw)) };
151
+ }
152
+ return { filePath, config: JSON.parse(raw) };
153
+ } catch (err) {
154
+ console.error('[PluginsService] Failed to read opencode config:', err.message);
155
+ return { filePath, config: {} };
156
+ }
157
+ }
158
+
159
+ _writeOpenCodeConfig(filePath, config) {
160
+ this._ensureDir(path.dirname(filePath));
161
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
162
+ }
163
+
164
+ _listOpenCodeConfiguredPlugins() {
165
+ const { config } = this._readOpenCodeConfig();
166
+ if (!Array.isArray(config.plugin)) return [];
167
+ return config.plugin.filter(Boolean);
168
+ }
169
+
170
+ _setOpenCodeConfiguredPlugins(plugins) {
171
+ const { filePath, config } = this._readOpenCodeConfig();
172
+ const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
173
+ nextConfig.plugin = Array.from(new Set((plugins || []).filter(Boolean)));
174
+ this._writeOpenCodeConfig(filePath, nextConfig);
175
+ }
176
+
177
+ _listOpenCodeLocalPlugins() {
178
+ const pluginsDir = this._getOpenCodePluginsDir();
179
+ if (!fs.existsSync(pluginsDir)) return [];
180
+
181
+ const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
182
+ const plugins = [];
183
+
184
+ for (const entry of entries) {
185
+ if (entry.name.startsWith('.')) continue;
186
+ const fullPath = path.join(pluginsDir, entry.name);
187
+
188
+ if (entry.isDirectory()) {
189
+ const pkgPath = path.join(fullPath, 'package.json');
190
+ let packageName = entry.name;
191
+ let description = '';
192
+ let version = '1.0.0';
193
+ if (fs.existsSync(pkgPath)) {
194
+ try {
195
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
196
+ packageName = pkg.name || packageName;
197
+ description = pkg.description || '';
198
+ version = pkg.version || version;
199
+ } catch (err) {
200
+ // ignore invalid package.json
201
+ }
202
+ }
203
+ plugins.push({
204
+ name: packageName,
205
+ directory: entry.name,
206
+ installPath: fullPath,
207
+ source: 'opencode-local',
208
+ version,
209
+ description,
210
+ installed: true,
211
+ enabled: true,
212
+ pluginType: 'local'
213
+ });
214
+ continue;
215
+ }
216
+
217
+ const ext = path.extname(entry.name).toLowerCase();
218
+ if (['.js', '.mjs', '.cjs', '.ts'].includes(ext)) {
219
+ plugins.push({
220
+ name: entry.name.replace(ext, ''),
221
+ directory: entry.name,
222
+ installPath: fullPath,
223
+ source: 'opencode-local',
224
+ version: '1.0.0',
225
+ description: '',
226
+ installed: true,
227
+ enabled: true,
228
+ pluginType: 'local'
229
+ });
230
+ }
231
+ }
232
+
233
+ return plugins;
234
+ }
235
+
20
236
  /**
21
237
  * List all installed plugins with their status
22
238
  * Reads from Claude Code's native installed_plugins.json
23
239
  * @returns {Object} { plugins: Array }
24
240
  */
25
241
  listPlugins() {
242
+ if (this._isOpenCode()) {
243
+ const plugins = [];
244
+ const seen = new Set();
245
+
246
+ for (const pkg of this._listOpenCodeConfiguredPlugins()) {
247
+ if (seen.has(pkg)) continue;
248
+ seen.add(pkg);
249
+ plugins.push({
250
+ name: pkg,
251
+ directory: pkg,
252
+ source: 'opencode-config',
253
+ version: 'latest',
254
+ description: '',
255
+ installed: true,
256
+ enabled: true,
257
+ pluginType: 'npm'
258
+ });
259
+ }
260
+
261
+ for (const plugin of this._listOpenCodeLocalPlugins()) {
262
+ if (!seen.has(plugin.name)) {
263
+ seen.add(plugin.name);
264
+ plugins.push(plugin);
265
+ }
266
+ }
267
+
268
+ return { plugins };
269
+ }
270
+
26
271
  const plugins = [];
27
272
 
28
273
  // Read Claude Code's installed_plugins.json
@@ -101,6 +346,12 @@ class PluginsService {
101
346
  * @returns {Object|null} Plugin details or null
102
347
  */
103
348
  getPlugin(name) {
349
+ if (this._isOpenCode()) {
350
+ const plugin = this.listPlugins().plugins.find(p => p.name === name || p.directory === name);
351
+ if (!plugin) return null;
352
+ return plugin;
353
+ }
354
+
104
355
  const plugin = getPlugin(name);
105
356
  if (!plugin) {
106
357
  return null;
@@ -136,6 +387,36 @@ class PluginsService {
136
387
  * @returns {Promise<Object>} Installation result
137
388
  */
138
389
  async installPlugin(source, repoInfo = null) {
390
+ if (this._isOpenCode()) {
391
+ if (repoInfo && repoInfo.owner && repoInfo.name && repoInfo.directory) {
392
+ return this._installFromGitHubDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
393
+ }
394
+
395
+ const treeMatch = source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
396
+ if (treeMatch) {
397
+ const [, owner, name, branch, directory] = treeMatch;
398
+ return this._installFromGitHubDirectory({ owner, name, branch, directory }, { installRoot: this._getOpenCodePluginsDir() });
399
+ }
400
+
401
+ // OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
402
+ if (!/^https?:\/\//.test(source)) {
403
+ const plugins = this._listOpenCodeConfiguredPlugins();
404
+ if (!plugins.includes(source)) {
405
+ plugins.push(source);
406
+ this._setOpenCodeConfiguredPlugins(plugins);
407
+ }
408
+ return {
409
+ success: true,
410
+ plugin: { name: source, version: 'latest', description: '' }
411
+ };
412
+ }
413
+
414
+ return {
415
+ success: false,
416
+ error: 'OpenCode plugin install expects npm package name or GitHub tree URL'
417
+ };
418
+ }
419
+
139
420
  // If repoInfo is provided, download from GitHub directly
140
421
  if (repoInfo && repoInfo.owner && repoInfo.name && repoInfo.directory) {
141
422
  return await this._installFromGitHubDirectory(repoInfo);
@@ -156,10 +437,11 @@ class PluginsService {
156
437
  * Install plugin from GitHub directory
157
438
  * @private
158
439
  */
159
- async _installFromGitHubDirectory(repoInfo) {
440
+ async _installFromGitHubDirectory(repoInfo, options = {}) {
160
441
  const { owner, name, branch, directory } = repoInfo;
161
442
  const https = require('https');
162
443
  const pluginName = directory.split('/').pop();
444
+ const installRoot = options.installRoot || INSTALLED_DIR;
163
445
 
164
446
  try {
165
447
  // Fetch plugin.json from the directory
@@ -174,7 +456,7 @@ class PluginsService {
174
456
  }
175
457
 
176
458
  // Create plugin directory
177
- const pluginDir = path.join(INSTALLED_DIR, manifest.name || pluginName);
459
+ const pluginDir = path.join(installRoot, manifest.name || pluginName);
178
460
  if (!fs.existsSync(pluginDir)) {
179
461
  fs.mkdirSync(pluginDir, { recursive: true });
180
462
  }
@@ -196,14 +478,16 @@ class PluginsService {
196
478
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
197
479
  }
198
480
 
199
- // Register plugin
200
- const { addPlugin } = require('../../plugins/registry');
201
- addPlugin(manifest.name || pluginName, {
202
- version: manifest.version || '1.0.0',
203
- enabled: true,
204
- installedAt: new Date().toISOString(),
205
- source: `https://github.com/${owner}/${name}/tree/${branch}/${directory}`
206
- });
481
+ if (!this._isOpenCode()) {
482
+ // Register plugin for Claude plugin runtime
483
+ const { addPlugin } = require('../../plugins/registry');
484
+ addPlugin(manifest.name || pluginName, {
485
+ version: manifest.version || '1.0.0',
486
+ enabled: true,
487
+ installedAt: new Date().toISOString(),
488
+ source: `https://github.com/${owner}/${name}/tree/${branch}/${directory}`
489
+ });
490
+ }
207
491
 
208
492
  return {
209
493
  success: true,
@@ -250,6 +534,43 @@ class PluginsService {
250
534
  * @returns {Object} Uninstallation result
251
535
  */
252
536
  uninstallPlugin(name) {
537
+ if (this._isOpenCode()) {
538
+ const pluginsDir = this._getOpenCodePluginsDir();
539
+ let removed = false;
540
+
541
+ // Remove from opencode config.plugin (npm plugins)
542
+ const configured = this._listOpenCodeConfiguredPlugins();
543
+ const next = configured.filter(p => p !== name);
544
+ if (next.length !== configured.length) {
545
+ this._setOpenCodeConfiguredPlugins(next);
546
+ removed = true;
547
+ }
548
+
549
+ // Remove local plugin directory/file
550
+ if (fs.existsSync(pluginsDir)) {
551
+ const directPath = path.join(pluginsDir, name);
552
+ if (fs.existsSync(directPath)) {
553
+ fs.rmSync(directPath, { recursive: true, force: true });
554
+ removed = true;
555
+ } else {
556
+ const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
557
+ for (const entry of entries) {
558
+ const baseName = entry.name.replace(path.extname(entry.name), '');
559
+ if (entry.name === name || baseName === name) {
560
+ fs.rmSync(path.join(pluginsDir, entry.name), { recursive: true, force: true });
561
+ removed = true;
562
+ break;
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ return {
569
+ success: true,
570
+ message: removed ? 'Plugin removed successfully' : 'Plugin not found'
571
+ };
572
+ }
573
+
253
574
  return uninstallPluginCore(name);
254
575
  }
255
576
 
@@ -260,6 +581,22 @@ class PluginsService {
260
581
  * @returns {Object} Updated plugin info
261
582
  */
262
583
  togglePlugin(name, enabled) {
584
+ if (this._isOpenCode()) {
585
+ const configured = this._listOpenCodeConfiguredPlugins();
586
+ const exists = configured.includes(name);
587
+ if (enabled && !exists) {
588
+ configured.push(name);
589
+ this._setOpenCodeConfiguredPlugins(configured);
590
+ } else if (!enabled && exists) {
591
+ this._setOpenCodeConfiguredPlugins(configured.filter(p => p !== name));
592
+ }
593
+
594
+ return {
595
+ name,
596
+ enabled
597
+ };
598
+ }
599
+
263
600
  const plugin = getPlugin(name);
264
601
  if (!plugin) {
265
602
  throw new Error(`Plugin "${name}" not found`);
@@ -280,6 +617,17 @@ class PluginsService {
280
617
  * @returns {Object} Result
281
618
  */
282
619
  updatePluginConfig(name, config) {
620
+ if (this._isOpenCode()) {
621
+ const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
622
+ this._ensureDir(configDir);
623
+ const configFile = path.join(configDir, `${name}.json`);
624
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
625
+ return {
626
+ success: true,
627
+ message: `Configuration updated for plugin "${name}"`
628
+ };
629
+ }
630
+
283
631
  const plugin = getPlugin(name);
284
632
  if (!plugin) {
285
633
  throw new Error(`Plugin "${name}" not found`);
@@ -305,12 +653,15 @@ class PluginsService {
305
653
  * @returns {string} Config file path
306
654
  */
307
655
  getReposConfigPath() {
308
- const os = require('os');
309
- const configDir = path.join(os.homedir(), '.claude', 'cc-tool');
310
- if (!fs.existsSync(configDir)) {
311
- fs.mkdirSync(configDir, { recursive: true });
656
+ this._ensureDir(this.ccToolConfigDir);
657
+ if (this._isOpenCode()) {
658
+ return path.join(this.ccToolConfigDir, 'opencode-plugin-repos.json');
312
659
  }
313
- return path.join(configDir, 'plugin-repos.json');
660
+ return path.join(this.ccToolConfigDir, 'plugin-repos.json');
661
+ }
662
+
663
+ _getDefaultRepos() {
664
+ return cloneRepos(DEFAULT_REPOS_BY_PLATFORM[this.platform] || DEFAULT_REPOS_BY_PLATFORM.claude);
314
665
  }
315
666
 
316
667
  /**
@@ -319,14 +670,19 @@ class PluginsService {
319
670
  */
320
671
  loadReposConfig() {
321
672
  const configPath = this.getReposConfigPath();
673
+ const defaultRepos = this._getDefaultRepos();
322
674
  if (!fs.existsSync(configPath)) {
323
- return { repos: [] };
675
+ return { repos: defaultRepos };
324
676
  }
325
677
  try {
326
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
678
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
679
+ if (parsed && Array.isArray(parsed.repos)) {
680
+ return parsed;
681
+ }
682
+ return { repos: defaultRepos };
327
683
  } catch (err) {
328
684
  console.error('Failed to load repos config:', err);
329
- return { repos: [] };
685
+ return { repos: defaultRepos };
330
686
  }
331
687
  }
332
688
 
@@ -358,8 +714,8 @@ class PluginsService {
358
714
  }
359
715
  }
360
716
 
361
- // 2. Load Claude Code's native marketplace config
362
- if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
717
+ // 2. Load Claude Code's native marketplace config (Claude only)
718
+ if (!this._isOpenCode() && fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
363
719
  try {
364
720
  const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
365
721
 
@@ -484,6 +840,10 @@ class PluginsService {
484
840
  * @returns {Promise<Object>} Sync results
485
841
  */
486
842
  async syncRepos() {
843
+ if (this._isOpenCode()) {
844
+ return { success: true, results: [] };
845
+ }
846
+
487
847
  const repos = this.getRepos();
488
848
  const results = [];
489
849
  const { execSync } = require('child_process');
@@ -589,6 +949,73 @@ class PluginsService {
589
949
  }
590
950
  }
591
951
 
952
+ _parseGitHubRepo(url = '') {
953
+ const match = url.match(/github\.com\/([^\/]+)\/([^\/#?]+)/i);
954
+ if (!match) return null;
955
+ return {
956
+ owner: match[1],
957
+ name: match[2].replace(/\.git$/, '')
958
+ };
959
+ }
960
+
961
+ async _fetchOpenCodeMarketplacePlugins(repo, branch) {
962
+ if (!this._isOpenCode()) return [];
963
+
964
+ let entries;
965
+ try {
966
+ const indexUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/contents/plugins?ref=${branch}`;
967
+ entries = await this._fetchJson(indexUrl);
968
+ } catch (err) {
969
+ return [];
970
+ }
971
+
972
+ if (!Array.isArray(entries)) return [];
973
+
974
+ const manifestFiles = entries.filter(
975
+ item => item.type === 'file' && item.name.endsWith('.plugin.json')
976
+ );
977
+ if (manifestFiles.length === 0) return [];
978
+
979
+ const results = await Promise.allSettled(
980
+ manifestFiles.map(async (file) => {
981
+ const fileUrl = file.download_url ||
982
+ `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${file.path}`;
983
+ const manifest = await this._fetchJson(fileUrl);
984
+
985
+ const author = Array.isArray(manifest.authors)
986
+ ? manifest.authors.map(item => item?.name).filter(Boolean).join(', ')
987
+ : '';
988
+ const firstCategory = Array.isArray(manifest.categories) ? manifest.categories[0] : '';
989
+ const repoUrl = manifest.links?.repository || `https://github.com/${repo.owner}/${repo.name}`;
990
+ // OpenCode supports npm package plugins via opencode.json "plugin" array.
991
+ // Use package name as install source so UI install button is enabled.
992
+ const installSource = String(manifest.name || '').trim();
993
+ const githubRepo = this._parseGitHubRepo(repoUrl);
994
+
995
+ return {
996
+ name: manifest.name || file.name.replace(/\.plugin\.json$/, ''),
997
+ displayName: manifest.displayName || '',
998
+ description: manifest.description || '',
999
+ author: author || repo.owner,
1000
+ version: manifest.version || manifest.opencode?.minimumVersion || '1.0.0',
1001
+ category: firstCategory ? String(firstCategory).toLowerCase() : 'general',
1002
+ repoUrl,
1003
+ repoOwner: '',
1004
+ repoName: '',
1005
+ repoBranch: githubRepo ? 'main' : branch,
1006
+ directory: file.path,
1007
+ installSource,
1008
+ marketplaceFormat: 'opencode-plugin-json',
1009
+ isInstalled: false
1010
+ };
1011
+ })
1012
+ );
1013
+
1014
+ return results
1015
+ .filter(item => item.status === 'fulfilled' && item.value)
1016
+ .map(item => item.value);
1017
+ }
1018
+
592
1019
  /**
593
1020
  * Get market plugins from configured repositories
594
1021
  * @returns {Promise<Array>} List of available market plugins
@@ -629,7 +1056,16 @@ class PluginsService {
629
1056
  // marketplace.json not found, try legacy format
630
1057
  }
631
1058
 
632
- // Legacy format: each directory is a plugin with plugin.json
1059
+ // OpenCode plugin marketplace format: plugins/*.plugin.json
1060
+ if (this._isOpenCode()) {
1061
+ const openCodeMarketplacePlugins = await this._fetchOpenCodeMarketplacePlugins(repo, branch);
1062
+ if (openCodeMarketplacePlugins.length > 0) {
1063
+ marketPlugins.push(...openCodeMarketplacePlugins);
1064
+ continue;
1065
+ }
1066
+ }
1067
+
1068
+ // Legacy format: each directory is a plugin with plugin.json/package.json
633
1069
  const apiUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/contents?ref=${branch}`;
634
1070
  const contents = await this._fetchJson(apiUrl);
635
1071
  const pluginDirs = contents.filter(item => item.type === 'dir' && !item.name.startsWith('.'));
@@ -654,7 +1090,28 @@ class PluginsService {
654
1090
  isInstalled: false
655
1091
  });
656
1092
  } catch (e) {
657
- // No plugin.json in this directory, skip
1093
+ // OpenCode 仓库常见 package.json 格式
1094
+ if (this._isOpenCode()) {
1095
+ try {
1096
+ const pkgUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${dir.name}/package.json`;
1097
+ const pkg = await this._fetchJson(pkgUrl);
1098
+ const pluginName = pkg.name || dir.name;
1099
+ marketPlugins.push({
1100
+ name: pluginName,
1101
+ description: pkg.description || '',
1102
+ author: pkg.author || repo.owner,
1103
+ version: pkg.version || '1.0.0',
1104
+ repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
1105
+ repoOwner: repo.owner,
1106
+ repoName: repo.name,
1107
+ repoBranch: branch,
1108
+ directory: dir.name,
1109
+ isInstalled: false
1110
+ });
1111
+ } catch (pkgErr) {
1112
+ // neither plugin.json nor package.json
1113
+ }
1114
+ }
658
1115
  }
659
1116
  }
660
1117
  } catch (err) {