@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,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const toml = require('toml');
5
+ const tomlStringify = require('@iarna/toml').stringify;
5
6
 
6
7
  // Codex 配置文件路径
7
8
  function getConfigPath() {
@@ -35,6 +36,167 @@ function hasBackup() {
35
36
  return fs.existsSync(getConfigBackupPath()) || fs.existsSync(getAuthBackupPath());
36
37
  }
37
38
 
39
+ const INVALID_ENV_NAME_PATTERN = /[\r\n]/;
40
+ const SHELL_MARKER_PREFIX = '# Added by Coding-Tool for Codex';
41
+
42
+ function normalizeEnvName(envName) {
43
+ const normalized = String(envName || '').trim();
44
+ if (!normalized || INVALID_ENV_NAME_PATTERN.test(normalized)) {
45
+ return null;
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ function ensureParentDir(filePath) {
51
+ const dirPath = path.dirname(filePath);
52
+ if (!fs.existsSync(dirPath)) {
53
+ fs.mkdirSync(dirPath, { recursive: true });
54
+ }
55
+ }
56
+
57
+ function writeFileAtomic(filePath, content) {
58
+ ensureParentDir(filePath);
59
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
60
+
61
+ try {
62
+ fs.writeFileSync(tempPath, content, 'utf8');
63
+ fs.renameSync(tempPath, filePath);
64
+ } finally {
65
+ if (fs.existsSync(tempPath)) {
66
+ try {
67
+ fs.unlinkSync(tempPath);
68
+ } catch (cleanupErr) {
69
+ // ignore cleanup errors
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ function normalizeHomePath(filePath) {
76
+ const normalizedPath = String(filePath || '').replace(/\\/g, '/');
77
+ const normalizedHome = os.homedir().replace(/\\/g, '/');
78
+ if (normalizedPath.startsWith(normalizedHome)) {
79
+ return `~${normalizedPath.slice(normalizedHome.length)}`;
80
+ }
81
+ return filePath;
82
+ }
83
+
84
+ function compactBlankLines(lines) {
85
+ const compacted = [];
86
+ let previousIsBlank = false;
87
+
88
+ for (const line of lines) {
89
+ const isBlank = line.trim() === '';
90
+ if (isBlank) {
91
+ if (!previousIsBlank) {
92
+ compacted.push('');
93
+ }
94
+ previousIsBlank = true;
95
+ continue;
96
+ }
97
+
98
+ compacted.push(line);
99
+ previousIsBlank = false;
100
+ }
101
+
102
+ while (compacted.length > 0 && compacted[compacted.length - 1].trim() === '') {
103
+ compacted.pop();
104
+ }
105
+
106
+ return compacted;
107
+ }
108
+
109
+ function isPowerShellProfile(filePath) {
110
+ return String(filePath || '').toLowerCase().endsWith('.ps1');
111
+ }
112
+
113
+ function getShellConfigCandidates() {
114
+ const homeDir = os.homedir();
115
+ const shell = String(process.env.SHELL || '').toLowerCase();
116
+ const candidates = [];
117
+
118
+ if (process.platform === 'win32') {
119
+ if (shell.includes('zsh')) {
120
+ candidates.push(path.join(homeDir, '.zshrc'));
121
+ }
122
+
123
+ if (shell.includes('bash')) {
124
+ candidates.push(path.join(homeDir, '.bashrc'));
125
+ candidates.push(path.join(homeDir, '.bash_profile'));
126
+ }
127
+
128
+ candidates.push(path.join(homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
129
+ candidates.push(path.join(homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
130
+ candidates.push(path.join(homeDir, '.bashrc'));
131
+ candidates.push(path.join(homeDir, '.profile'));
132
+ } else if (shell.includes('zsh')) {
133
+ candidates.push(path.join(homeDir, '.zshrc'));
134
+ candidates.push(path.join(homeDir, '.zprofile'));
135
+ candidates.push(path.join(homeDir, '.profile'));
136
+ } else if (shell.includes('bash')) {
137
+ if (process.platform === 'darwin') {
138
+ candidates.push(path.join(homeDir, '.bash_profile'));
139
+ candidates.push(path.join(homeDir, '.bashrc'));
140
+ } else {
141
+ candidates.push(path.join(homeDir, '.bashrc'));
142
+ candidates.push(path.join(homeDir, '.bash_profile'));
143
+ }
144
+ candidates.push(path.join(homeDir, '.profile'));
145
+ } else {
146
+ candidates.push(path.join(homeDir, '.zshrc'));
147
+ candidates.push(path.join(homeDir, '.bashrc'));
148
+ candidates.push(path.join(homeDir, '.bash_profile'));
149
+ candidates.push(path.join(homeDir, '.profile'));
150
+ }
151
+
152
+ return [...new Set(candidates)];
153
+ }
154
+
155
+ function getShellReloadCommand(configPath) {
156
+ if (!configPath) {
157
+ return process.platform === 'win32' ? '重启终端' : 'source ~/.zshrc';
158
+ }
159
+
160
+ const displayPath = normalizeHomePath(configPath);
161
+ const normalized = String(displayPath || '').replace(/\\/g, '/').toLowerCase();
162
+
163
+ if (normalized.endsWith('microsoft.powershell_profile.ps1')) {
164
+ return '. $PROFILE';
165
+ }
166
+ if (normalized.endsWith('/.zshrc')) {
167
+ return 'source ~/.zshrc';
168
+ }
169
+ if (normalized.endsWith('/.bash_profile')) {
170
+ return 'source ~/.bash_profile';
171
+ }
172
+ if (normalized.endsWith('/.bashrc')) {
173
+ return 'source ~/.bashrc';
174
+ }
175
+ if (normalized.endsWith('/.profile')) {
176
+ return 'source ~/.profile';
177
+ }
178
+
179
+ if (process.platform === 'win32') {
180
+ return '. $PROFILE';
181
+ }
182
+
183
+ return `source ${displayPath}`;
184
+ }
185
+
186
+ function escapeShellValue(value) {
187
+ return String(value ?? '')
188
+ .replace(/\\/g, '\\\\')
189
+ .replace(/"/g, '\\"')
190
+ .replace(/\$/g, '\\$')
191
+ .replace(/`/g, '\\`');
192
+ }
193
+
194
+ function escapePowerShellValue(value) {
195
+ return String(value ?? '')
196
+ .replace(/`/g, '``')
197
+ .replace(/"/g, '`"');
198
+ }
199
+
38
200
  // 读取 config.toml
39
201
  function readConfig() {
40
202
  try {
@@ -89,7 +251,8 @@ function configToToml(config) {
89
251
  // 写入 config.toml
90
252
  function writeConfig(config) {
91
253
  try {
92
- const content = configToToml(config);
254
+ const safeConfig = JSON.parse(JSON.stringify(config || {}));
255
+ const content = tomlStringify(safeConfig);
93
256
  fs.writeFileSync(getConfigPath(), content, 'utf8');
94
257
  } catch (err) {
95
258
  throw new Error('Failed to write config.toml: ' + err.message);
@@ -182,28 +345,28 @@ function restoreSettings() {
182
345
 
183
346
  // 获取用户的 shell 配置文件路径
184
347
  function getShellConfigPath() {
185
- const shell = process.env.SHELL || '';
186
- if (shell.includes('zsh')) {
187
- return path.join(os.homedir(), '.zshrc');
188
- } else if (shell.includes('bash')) {
189
- // macOS 使用 .bash_profile,Linux 使用 .bashrc
190
- const bashProfile = path.join(os.homedir(), '.bash_profile');
191
- const bashrc = path.join(os.homedir(), '.bashrc');
192
- if (fs.existsSync(bashProfile)) {
193
- return bashProfile;
194
- }
195
- return bashrc;
196
- }
197
- // 默认使用 .zshrc (macOS 默认)
198
- return path.join(os.homedir(), '.zshrc');
348
+ const candidates = getShellConfigCandidates();
349
+ const existing = candidates.find(filePath => fs.existsSync(filePath));
350
+ return existing || candidates[0];
199
351
  }
200
352
 
201
353
  // 注入环境变量到 shell 配置文件
202
354
  function injectEnvToShell(envName, envValue) {
355
+ const normalizedEnvName = normalizeEnvName(envName);
356
+ if (!normalizedEnvName) {
357
+ return {
358
+ success: false,
359
+ error: `Invalid environment variable name: ${envName}`,
360
+ isFirstTime: false
361
+ };
362
+ }
363
+
203
364
  const configPath = getShellConfigPath();
204
- const exportLine = `export ${envName}="${envValue}"`;
205
- // 使用更具体的标记,包含环境变量名,方便后续精确移除
206
- const marker = `# Added by Coding-Tool for Codex [${envName}]`;
365
+ const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
366
+ const usePowerShell = isPowerShellProfile(configPath);
367
+ const exportLine = usePowerShell
368
+ ? `$env:${normalizedEnvName} = "${escapePowerShellValue(envValue)}"`
369
+ : `export ${normalizedEnvName}="${escapeShellValue(envValue)}"`;
207
370
 
208
371
  try {
209
372
  let content = '';
@@ -211,23 +374,52 @@ function injectEnvToShell(envName, envValue) {
211
374
  content = fs.readFileSync(configPath, 'utf8');
212
375
  }
213
376
 
214
- // 检查是否已经存在这个环境变量配置
215
- const regex = new RegExp(`^export ${envName}=`, 'm');
216
- const alreadyExists = regex.test(content);
377
+ const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
378
+ const envLineRegex = usePowerShell
379
+ ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
380
+ : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
217
381
 
218
- if (alreadyExists) {
219
- // 已存在,替换它(保留原有的标记注释)
220
- content = content.replace(
221
- new RegExp(`^(# Added by Coding-Tool for Codex \\[${envName}\\]\n)?export ${envName}=.*$`, 'm'),
222
- `${marker}\n${exportLine}`
223
- );
224
- } else {
225
- // 不存在,追加到文件末尾
226
- content = content.trimEnd() + `\n\n${marker}\n${exportLine}\n`;
382
+ const originalLines = content ? content.split(/\r?\n/) : [];
383
+ const cleanedLines = [];
384
+ let existed = false;
385
+
386
+ for (let i = 0; i < originalLines.length; i++) {
387
+ const currentLine = originalLines[i];
388
+ const trimmedLine = currentLine.trim();
389
+
390
+ if (trimmedLine === marker) {
391
+ const nextLine = originalLines[i + 1] || '';
392
+ if (envLineRegex.test(nextLine.trim())) {
393
+ i += 1;
394
+ }
395
+ existed = true;
396
+ continue;
397
+ }
398
+
399
+ if (envLineRegex.test(trimmedLine)) {
400
+ existed = true;
401
+ continue;
402
+ }
403
+
404
+ cleanedLines.push(currentLine);
405
+ }
406
+
407
+ while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
408
+ cleanedLines.pop();
409
+ }
410
+
411
+ if (cleanedLines.length > 0) {
412
+ cleanedLines.push('');
413
+ }
414
+
415
+ cleanedLines.push(marker, exportLine);
416
+
417
+ const nextContent = `${cleanedLines.join('\n')}\n`;
418
+ if (nextContent !== content) {
419
+ writeFileAtomic(configPath, nextContent);
227
420
  }
228
421
 
229
- fs.writeFileSync(configPath, content, 'utf8');
230
- return { success: true, path: configPath, isFirstTime: !alreadyExists };
422
+ return { success: true, path: configPath, isFirstTime: !existed };
231
423
  } catch (err) {
232
424
  // 不抛出错误,只是警告,因为这不是致命问题
233
425
  console.warn(`[Codex] Failed to inject env to shell config: ${err.message}`);
@@ -237,6 +429,14 @@ function injectEnvToShell(envName, envValue) {
237
429
 
238
430
  // 从 shell 配置文件移除环境变量
239
431
  function removeEnvFromShell(envName) {
432
+ const normalizedEnvName = normalizeEnvName(envName);
433
+ if (!normalizedEnvName) {
434
+ return {
435
+ success: false,
436
+ error: `Invalid environment variable name: ${envName}`
437
+ };
438
+ }
439
+
240
440
  const configPath = getShellConfigPath();
241
441
 
242
442
  try {
@@ -244,24 +444,46 @@ function removeEnvFromShell(envName) {
244
444
  return { success: true };
245
445
  }
246
446
 
247
- let content = fs.readFileSync(configPath, 'utf8');
447
+ const content = fs.readFileSync(configPath, 'utf8');
448
+ const usePowerShell = isPowerShellProfile(configPath);
449
+ const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
450
+ const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
451
+ const envLineRegex = usePowerShell
452
+ ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
453
+ : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
454
+
455
+ const originalLines = content ? content.split(/\r?\n/) : [];
456
+ const cleanedLines = [];
457
+ let changed = false;
458
+
459
+ for (let i = 0; i < originalLines.length; i++) {
460
+ const currentLine = originalLines[i];
461
+ const trimmedLine = currentLine.trim();
462
+
463
+ if (trimmedLine === marker) {
464
+ const nextLine = originalLines[i + 1] || '';
465
+ if (envLineRegex.test(nextLine.trim())) {
466
+ i += 1;
467
+ }
468
+ changed = true;
469
+ continue;
470
+ }
248
471
 
249
- // 移除具体标记的环境变量(推荐方式)
250
- content = content.replace(
251
- new RegExp(`\\n?# Added by Coding-Tool for Codex \\[${envName}\\]\\nexport ${envName}=.*\\n?`, 'g'),
252
- '\n'
253
- );
472
+ if (envLineRegex.test(trimmedLine)) {
473
+ changed = true;
474
+ continue;
475
+ }
254
476
 
255
- // 如果没有标记,也尝试移除(兼容旧数据)
256
- content = content.replace(
257
- new RegExp(`^export ${envName}=.*\\n?`, 'gm'),
258
- ''
259
- );
477
+ cleanedLines.push(currentLine);
478
+ }
260
479
 
261
- // 清理多余的空行
262
- content = content.replace(/\n\n\n+/g, '\n\n');
480
+ if (!changed) {
481
+ return { success: true };
482
+ }
263
483
 
264
- fs.writeFileSync(configPath, content, 'utf8');
484
+ const normalized = compactBlankLines(cleanedLines);
485
+ const nextContent = normalized.length > 0 ? `${normalized.join('\n')}\n` : '';
486
+ writeFileAtomic(configPath, nextContent);
265
487
  return { success: true };
266
488
  } catch (err) {
267
489
  console.warn(`[Codex] Failed to remove env from shell config: ${err.message}`);
@@ -306,8 +528,8 @@ function setProxyConfig(proxyPort) {
306
528
  const shellInjectResult = injectEnvToShell('CC_PROXY_KEY', 'PROXY_KEY');
307
529
 
308
530
  // 获取 shell 配置文件路径用于提示信息
309
- const shellConfigPath = getShellConfigPath();
310
- const sourceCommand = process.env.SHELL?.includes('zsh') ? 'source ~/.zshrc' : 'source ~/.bashrc';
531
+ const shellConfigPath = shellInjectResult.path || getShellConfigPath();
532
+ const sourceCommand = getShellReloadCommand(shellConfigPath);
311
533
 
312
534
  console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
313
535
  return {
@@ -341,7 +563,7 @@ function isProxyConfig() {
341
563
  const currentProvider = config.model_provider;
342
564
  if (currentProvider && config.model_providers && config.model_providers[currentProvider]) {
343
565
  const baseUrl = config.model_providers[currentProvider].base_url || '';
344
- if (baseUrl.includes('127.0.0.1') && baseUrl.includes('10089')) {
566
+ if (baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost')) {
345
567
  return true;
346
568
  }
347
569
  }
@@ -6,7 +6,7 @@ const os = require('os');
6
6
  * Codex 统计服务 - 数据采集和存储
7
7
  *
8
8
  * 文件结构:
9
- * ~/.claude/cc-tool/
9
+ * ~/.cc-tool/
10
10
  * ├── codex-statistics.json # Codex 总体统计
11
11
  * └── codex-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
  }
@@ -1,11 +1,7 @@
1
1
  /**
2
2
  * Commands 服务
3
3
  *
4
- * 管理 Claude Code 自定义命令的 CRUD 操作
5
- * 命令目录:
6
- * - 用户级: ~/.claude/commands/
7
- * - 项目级: .claude/commands/
8
- *
4
+ * 管理 Claude/OpenCode 自定义命令的 CRUD 操作
9
5
  * 支持从 GitHub 仓库扫描和安装命令
10
6
  */
11
7
 
@@ -13,6 +9,7 @@ const fs = require('fs');
13
9
  const path = require('path');
14
10
  const os = require('os');
15
11
  const { RepoScannerBase } = require('./repo-scanner-base');
12
+ const { NATIVE_PATHS } = require('../../config/paths');
16
13
  const {
17
14
  parseCommandContent,
18
15
  detectCommandFormat,
@@ -21,11 +18,35 @@ const {
21
18
  parseFrontmatter
22
19
  } = require('./format-converter');
23
20
 
24
- // 命令目录路径
25
- const USER_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
26
-
27
21
  // 默认仓库源
28
22
  const DEFAULT_REPOS = [];
23
+ const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
24
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
25
+
26
+ const PLATFORM_CONFIG = {
27
+ claude: {
28
+ userCommandsDir: path.join(os.homedir(), '.claude', 'commands'),
29
+ projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
30
+ repoType: 'commands'
31
+ },
32
+ opencode: {
33
+ userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
34
+ legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
35
+ projectCommandsDir: (projectPath) => {
36
+ const modern = path.join(projectPath, '.opencode', 'commands');
37
+ const legacy = path.join(projectPath, '.opencode', 'command');
38
+ if (fs.existsSync(legacy) && !fs.existsSync(modern)) {
39
+ return legacy;
40
+ }
41
+ return modern;
42
+ },
43
+ repoType: 'opencode-commands'
44
+ }
45
+ };
46
+
47
+ function normalizePlatform(platform) {
48
+ return SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
49
+ }
29
50
 
30
51
  /**
31
52
  * 确保目录存在
@@ -60,6 +81,9 @@ function generateCommandFrontmatter(data) {
60
81
  if (data.agent) {
61
82
  lines.push(`agent: ${data.agent}`);
62
83
  }
84
+ if (typeof data.subtask === 'boolean') {
85
+ lines.push(`subtask: ${data.subtask}`);
86
+ }
63
87
 
64
88
  lines.push('---');
65
89
  return lines.join('\n');
@@ -105,6 +129,9 @@ function scanCommandsDir(dir, basePath, scope) {
105
129
  description: frontmatter.description || '',
106
130
  allowedTools: frontmatter['allowed-tools'] || '',
107
131
  argumentHint: frontmatter['argument-hint'] || '',
132
+ agent: frontmatter.agent || '',
133
+ model: frontmatter.model || '',
134
+ subtask: frontmatter.subtask || '',
108
135
  body,
109
136
  fullContent: content,
110
137
  updatedAt: fs.statSync(fullPath).mtime.getTime()
@@ -125,10 +152,10 @@ function scanCommandsDir(dir, basePath, scope) {
125
152
  * Commands 仓库扫描器
126
153
  */
127
154
  class CommandsRepoScanner extends RepoScannerBase {
128
- constructor() {
155
+ constructor(platform, installDir) {
129
156
  super({
130
- type: 'commands',
131
- installDir: USER_COMMANDS_DIR,
157
+ type: PLATFORM_CONFIG[platform]?.repoType || 'commands',
158
+ installDir,
132
159
  markerFile: null, // 直接扫描 .md 文件
133
160
  fileExtension: '.md',
134
161
  defaultRepos: DEFAULT_REPOS
@@ -159,6 +186,9 @@ class CommandsRepoScanner extends RepoScannerBase {
159
186
  description: frontmatter.description || '',
160
187
  allowedTools: frontmatter['allowed-tools'] || '',
161
188
  argumentHint: frontmatter['argument-hint'] || '',
189
+ agent: frontmatter.agent || '',
190
+ model: frontmatter.model || '',
191
+ subtask: frontmatter.subtask || '',
162
192
  body,
163
193
  fullContent: content,
164
194
  installed: this.isInstalled(relativePath),
@@ -208,12 +238,28 @@ class CommandsRepoScanner extends RepoScannerBase {
208
238
  * Commands 服务类
209
239
  */
210
240
  class CommandsService {
211
- constructor() {
212
- this.userCommandsDir = USER_COMMANDS_DIR;
213
- this.repoScanner = new CommandsRepoScanner();
241
+ constructor(platform = 'claude') {
242
+ this.platform = normalizePlatform(platform);
243
+ const config = PLATFORM_CONFIG[this.platform];
244
+
245
+ this.userCommandsDir = config.userCommandsDir;
246
+ if (this.platform === 'opencode') {
247
+ const legacyUserDir = config.legacyUserCommandsDir;
248
+ if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userCommandsDir)) {
249
+ this.userCommandsDir = legacyUserDir;
250
+ }
251
+ }
252
+
253
+ this.projectCommandsDir = config.projectCommandsDir;
254
+ this.repoScanner = new CommandsRepoScanner(this.platform, this.userCommandsDir);
214
255
  ensureDir(this.userCommandsDir);
215
256
  }
216
257
 
258
+ getProjectCommandsDir(projectPath) {
259
+ if (!projectPath) return null;
260
+ return this.projectCommandsDir(projectPath);
261
+ }
262
+
217
263
  /**
218
264
  * 获取所有命令列表
219
265
  * @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
@@ -227,7 +273,7 @@ class CommandsService {
227
273
 
228
274
  // 获取项目级命令(如果提供了项目路径)
229
275
  if (projectPath) {
230
- const projectCommandsDir = path.join(projectPath, '.claude', 'commands');
276
+ const projectCommandsDir = this.getProjectCommandsDir(projectPath);
231
277
  const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
232
278
  commands.push(...projectCommands);
233
279
  }
@@ -295,7 +341,7 @@ class CommandsService {
295
341
  getCommand(name, scope, projectPath = null, namespace = null) {
296
342
  const baseDir = scope === 'user'
297
343
  ? this.userCommandsDir
298
- : path.join(projectPath, '.claude', 'commands');
344
+ : this.getProjectCommandsDir(projectPath);
299
345
 
300
346
  const relativePath = namespace
301
347
  ? path.join(namespace, `${name}.md`)
@@ -319,6 +365,9 @@ class CommandsService {
319
365
  description: frontmatter.description || '',
320
366
  allowedTools: frontmatter['allowed-tools'] || '',
321
367
  argumentHint: frontmatter['argument-hint'] || '',
368
+ agent: frontmatter.agent || '',
369
+ model: frontmatter.model || '',
370
+ subtask: frontmatter.subtask || '',
322
371
  body,
323
372
  fullContent: content,
324
373
  updatedAt: fs.statSync(fullPath).mtime.getTime()
@@ -328,7 +377,7 @@ class CommandsService {
328
377
  /**
329
378
  * 创建命令
330
379
  */
331
- createCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, body }) {
380
+ createCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
332
381
  if (!name || !name.trim()) {
333
382
  throw new Error('命令名称不能为空');
334
383
  }
@@ -340,7 +389,7 @@ class CommandsService {
340
389
 
341
390
  const baseDir = scope === 'user'
342
391
  ? this.userCommandsDir
343
- : path.join(projectPath, '.claude', 'commands');
392
+ : this.getProjectCommandsDir(projectPath);
344
393
 
345
394
  const targetDir = namespace ? path.join(baseDir, namespace) : baseDir;
346
395
  ensureDir(targetDir);
@@ -355,8 +404,13 @@ class CommandsService {
355
404
  // 生成文件内容
356
405
  const frontmatterData = {};
357
406
  if (description) frontmatterData.description = description;
358
- if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
359
- if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
407
+ if (this.platform !== 'opencode') {
408
+ if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
409
+ if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
410
+ }
411
+ if (agent) frontmatterData.agent = agent;
412
+ if (model) frontmatterData.model = model;
413
+ if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
360
414
 
361
415
  let content = '';
362
416
  if (Object.keys(frontmatterData).length > 0) {
@@ -372,10 +426,10 @@ class CommandsService {
372
426
  /**
373
427
  * 更新命令
374
428
  */
375
- updateCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, body }) {
429
+ updateCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
376
430
  const baseDir = scope === 'user'
377
431
  ? this.userCommandsDir
378
- : path.join(projectPath, '.claude', 'commands');
432
+ : this.getProjectCommandsDir(projectPath);
379
433
 
380
434
  const relativePath = namespace
381
435
  ? path.join(namespace, `${name}.md`)
@@ -390,8 +444,13 @@ class CommandsService {
390
444
  // 生成文件内容
391
445
  const frontmatterData = {};
392
446
  if (description) frontmatterData.description = description;
393
- if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
394
- if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
447
+ if (this.platform !== 'opencode') {
448
+ if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
449
+ if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
450
+ }
451
+ if (agent) frontmatterData.agent = agent;
452
+ if (model) frontmatterData.model = model;
453
+ if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
395
454
 
396
455
  let content = '';
397
456
  if (Object.keys(frontmatterData).length > 0) {
@@ -410,7 +469,7 @@ class CommandsService {
410
469
  deleteCommand(name, scope, projectPath = null, namespace = null) {
411
470
  const baseDir = scope === 'user'
412
471
  ? this.userCommandsDir
413
- : path.join(projectPath, '.claude', 'commands');
472
+ : this.getProjectCommandsDir(projectPath);
414
473
 
415
474
  const relativePath = namespace
416
475
  ? path.join(namespace, `${name}.md`)