coding-tool-x 3.5.9 → 3.5.11

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 (92) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -25
  3. package/bin/ctx.js +1 -6
  4. package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-Bw-0r4Ys.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
  7. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  8. package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
  9. package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
  10. package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
  13. package/dist/web/assets/SessionList-D12muXvx.css +1 -0
  14. package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.js} +1 -1
  16. package/dist/web/assets/icons-lKCTySl7.js +1 -0
  17. package/dist/web/assets/index-BS7aplYp.css +1 -0
  18. package/dist/web/assets/index-CTeg1xG-.js +2 -0
  19. package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
  20. package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
  21. package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
  22. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
  23. package/dist/web/index.html +8 -8
  24. package/package.json +1 -2
  25. package/src/commands/daemon.js +5 -33
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/stats.js +4 -41
  28. package/src/commands/ui.js +3 -12
  29. package/src/config/channel-preset-websites.js +114 -0
  30. package/src/config/default.js +9 -5
  31. package/src/config/loader.js +2 -2
  32. package/src/config/model-metadata.json +16 -20
  33. package/src/config/paths.js +33 -166
  34. package/src/index.js +34 -125
  35. package/src/server/api/agents.js +2 -52
  36. package/src/server/api/codex-channels.js +4 -0
  37. package/src/server/api/codex-proxy.js +45 -2
  38. package/src/server/api/codex-sessions.js +89 -12
  39. package/src/server/api/commands.js +2 -38
  40. package/src/server/api/dashboard.js +1 -25
  41. package/src/server/api/gemini-proxy.js +4 -2
  42. package/src/server/api/gemini-sessions.js +86 -9
  43. package/src/server/api/hooks.js +1 -17
  44. package/src/server/api/opencode-proxy.js +7 -2
  45. package/src/server/api/opencode-sessions.js +83 -10
  46. package/src/server/api/plugins.js +34 -128
  47. package/src/server/api/proxy.js +5 -3
  48. package/src/server/api/sessions.js +181 -27
  49. package/src/server/api/workspaces.js +0 -14
  50. package/src/server/index.js +9 -31
  51. package/src/server/services/agents-service.js +62 -269
  52. package/src/server/services/base/base-channel-service.js +3 -1
  53. package/src/server/services/channels.js +5 -0
  54. package/src/server/services/codex-channels.js +2 -0
  55. package/src/server/services/codex-sessions.js +88 -108
  56. package/src/server/services/commands-service.js +81 -281
  57. package/src/server/services/config-export-service.js +7 -7
  58. package/src/server/services/config-registry-service.js +9 -8
  59. package/src/server/services/config-sync-manager.js +41 -61
  60. package/src/server/services/config-sync-service.js +3 -3
  61. package/src/server/services/gemini-channels.js +17 -6
  62. package/src/server/services/gemini-config.js +4 -3
  63. package/src/server/services/gemini-sessions.js +59 -26
  64. package/src/server/services/gemini-settings-manager.js +3 -2
  65. package/src/server/services/mcp-service.js +14 -9
  66. package/src/server/services/native-oauth-adapters.js +3 -3
  67. package/src/server/services/network-access.js +2 -61
  68. package/src/server/services/notification-hooks.js +19 -184
  69. package/src/server/services/opencode-channels.js +9 -0
  70. package/src/server/services/opencode-sessions.js +6 -16
  71. package/src/server/services/opencode-settings-manager.js +3 -3
  72. package/src/server/services/plugins-service.js +62 -996
  73. package/src/server/services/prompts-service.js +9 -5
  74. package/src/server/services/sessions.js +171 -183
  75. package/src/server/services/skill-service.js +18 -155
  76. package/src/server/websocket-server.js +4 -35
  77. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  78. package/dist/web/assets/Home-qzk118Of.css +0 -1
  79. package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
  80. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  81. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  82. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  83. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  84. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  85. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  86. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  87. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  88. package/docs/Caddyfile.example +0 -19
  89. package/docs/reverse-proxy-https.md +0 -57
  90. package/src/server/services/https-cert.js +0 -171
  91. package/src/server/services/session-launch-command.js +0 -58
  92. package/src/server/services/web-ui-runtime.js +0 -54
@@ -226,13 +226,6 @@ router.post('/', (req, res) => {
226
226
  });
227
227
  }
228
228
 
229
- if (proj.createWorktree && (!proj.branch || !proj.branch.trim())) {
230
- return res.status(400).json({
231
- success: false,
232
- message: '创建 worktree 时必须指定分支名'
233
- });
234
- }
235
-
236
229
  const normalizedBranch = normalizeBranchName(proj.branch);
237
230
  if (normalizedBranch) {
238
231
  const branchValidation = validateBranchName(normalizedBranch);
@@ -347,13 +340,6 @@ router.post('/:id/projects', (req, res) => {
347
340
  });
348
341
  }
349
342
 
350
- if (createWorktree && !normalizedBranch) {
351
- return res.status(400).json({
352
- success: false,
353
- message: '创建 worktree 时必须指定分支名'
354
- });
355
- }
356
-
357
343
  if (normalizedBranch) {
358
344
  const branchValidation = validateBranchName(normalizedBranch);
359
345
  if (!branchValidation.valid) {
@@ -1,5 +1,4 @@
1
1
  const express = require('express');
2
- const https = require('https');
3
2
  const path = require('path');
4
3
  const chalk = require('chalk');
5
4
  const { loadConfig } = require('../config/loader');
@@ -22,10 +21,8 @@ const { startProxyServer } = require('./proxy-server');
22
21
  const { startCodexProxyServer } = require('./codex-proxy-server');
23
22
  const { startGeminiProxyServer } = require('./gemini-proxy-server');
24
23
  const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
25
- const { createRemoteMutationGuard, isRemoteMutationAllowed } = require('./services/network-access');
24
+ const { createRemoteMutationGuard } = require('./services/network-access');
26
25
  const { createApiRequestLogger } = require('./services/request-logger');
27
- const { getLocalHttpsCredentials } = require('./services/https-cert');
28
- const { getWebUiProtocol, isHttpsEnabled, getWebUiBaseUrl, getWebSocketBaseUrl } = require('./services/web-ui-runtime');
29
26
 
30
27
  function getInquirer() {
31
28
  return require('inquirer');
@@ -62,8 +59,6 @@ function printPortToolIssue(issue = getPortToolIssue()) {
62
59
  async function startServer(port, host = '127.0.0.1', options = {}) {
63
60
  ensureStorageDirMigrated();
64
61
  const config = loadConfig();
65
- const webUiProtocol = getWebUiProtocol({ https: options.https });
66
- const httpsEnabled = isHttpsEnabled({ protocol: webUiProtocol });
67
62
  // 使用配置的端口,如果没有传入参数
68
63
  if (!port) {
69
64
  port = config.ports?.webUI || 19999;
@@ -137,9 +132,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
137
132
 
138
133
  const app = express();
139
134
  const lanMode = host === '0.0.0.0';
140
- const allowRemoteMutation = lanMode
141
- ? isRemoteMutationAllowed(process.env.CC_TOOL_ALLOW_REMOTE_WRITE)
142
- : true;
135
+ const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
143
136
 
144
137
  // Middleware
145
138
  app.use(express.json({ limit: '100mb' }));
@@ -163,7 +156,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
163
156
  app.use('/api', createRemoteMutationGuard({
164
157
  enabled: true,
165
158
  allowRemoteMutation,
166
- message: '当前已禁用 LAN 远程写操作,可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 重新开启。'
159
+ message: '出于安全考虑,LAN 模式默认仅允许本机执行写操作。可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 覆盖。'
167
160
  }));
168
161
 
169
162
  }
@@ -252,18 +245,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
252
245
  }
253
246
 
254
247
  // Start server(确保监听成功后才返回,避免命令误报“已启动”)
255
- let httpsCertificate = null;
256
- const server = httpsEnabled
257
- ? (() => {
258
- httpsCertificate = getLocalHttpsCredentials();
259
- const httpsServer = https.createServer({
260
- key: httpsCertificate.key,
261
- cert: httpsCertificate.cert
262
- }, app);
263
- httpsServer.listen(port, host);
264
- return httpsServer;
265
- })()
266
- : app.listen(port, host);
248
+ const server = app.listen(port, host);
267
249
  await new Promise((resolve) => {
268
250
  const onListening = () => {
269
251
  server.off('error', onError);
@@ -290,22 +272,18 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
290
272
  console.log(`\n[START] Coding-Tool Web UI running at:`);
291
273
  if (host === '0.0.0.0') {
292
274
  console.log(chalk.yellow(` [WARN] 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
293
- console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
294
- console.log(chalk.gray(` ${webUiProtocol}://<your-ip>:${port} (LAN 访问)`));
275
+ console.log(` http://localhost:${port}`);
276
+ console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
295
277
  } else {
296
- console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
278
+ console.log(` http://localhost:${port}`);
297
279
  }
298
280
 
299
281
  // 附加 WebSocket 服务器到同一个端口
300
282
  attachWebSocketServer(server, { host });
301
- console.log(` ${getWebSocketBaseUrl(port, { protocol: webUiProtocol })}/ws\n`);
302
-
303
- if (httpsEnabled && httpsCertificate?.generated) {
304
- console.log(chalk.gray(` [LOCK] 已生成本地 HTTPS 证书: ${httpsCertificate.certPath}`));
305
- }
283
+ console.log(` ws://localhost:${port}/ws\n`);
306
284
 
307
285
  if (host === '0.0.0.0' && !allowRemoteMutation) {
308
- console.log(chalk.yellow(' [LOCK] 已禁用 LAN 远程写操作 (CC_TOOL_ALLOW_REMOTE_WRITE=false)'));
286
+ console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
309
287
  }
310
288
  // 自动恢复代理状态
311
289
  autoRestoreProxies();
@@ -7,42 +7,32 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const os = require('os');
10
11
  const toml = require('toml');
11
12
  const tomlStringify = require('@iarna/toml').stringify;
12
13
  const { RepoScannerBase } = require('./repo-scanner-base');
13
- const {
14
- NATIVE_PATHS,
15
- PATHS,
16
- HOME_DIR,
17
- isWindowsAbsolutePath,
18
- isNativeAbsolutePath,
19
- joinNativeBasePath,
20
- resolveNativeBasePath,
21
- getNativePathDir
22
- } = require('../../config/paths');
14
+ const { NATIVE_PATHS } = require('../../config/paths');
15
+ const { resolvePreferredHomeDir } = require('../../utils/home-dir');
23
16
 
24
17
  // 默认仓库源
25
18
  const DEFAULT_REPOS = [];
26
19
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
27
- const OPENCODE_AGENTS_DIR = NATIVE_PATHS.opencode.agents;
28
- const OPENCODE_LEGACY_AGENTS_DIR = NATIVE_PATHS.opencode.agentsLegacy;
29
- const CODEX_DIR = NATIVE_PATHS.codex.dir;
20
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
30
21
  const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
31
- const CODEX_AGENTS_DIR = NATIVE_PATHS.codex.agents;
32
- const CLAUDE_AGENTS_DIR = NATIVE_PATHS.claude.agents;
22
+ const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
23
+ const CODEX_AGENTS_DIR = path.join(path.dirname(CODEX_CONFIG_PATH), 'agents');
24
+ const CLAUDE_AGENTS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'agents');
33
25
  const CODEX_CONFIG_MODES = new Set(['none', 'managed', 'custom']);
34
26
 
35
27
  const PLATFORM_CONFIG = {
36
28
  claude: {
37
29
  userAgentsDir: CLAUDE_AGENTS_DIR,
38
- storageDir: PATHS.localAgents.claude,
39
30
  projectAgentsDir: (projectPath) => path.join(projectPath, '.claude', 'agents'),
40
31
  repoType: 'agents'
41
32
  },
42
33
  opencode: {
43
- userAgentsDir: OPENCODE_AGENTS_DIR,
44
- storageDir: PATHS.localAgents.opencode,
45
- legacyUserAgentsDir: OPENCODE_LEGACY_AGENTS_DIR,
34
+ userAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agents'),
35
+ legacyUserAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agent'),
46
36
  projectAgentsDir: (projectPath) => {
47
37
  const modern = path.join(projectPath, '.opencode', 'agents');
48
38
  const legacy = path.join(projectPath, '.opencode', 'agent');
@@ -55,7 +45,6 @@ const PLATFORM_CONFIG = {
55
45
  },
56
46
  codex: {
57
47
  userAgentsDir: CODEX_AGENTS_DIR,
58
- storageDir: PATHS.localAgents.codex,
59
48
  projectAgentsDir: () => null,
60
49
  repoType: 'agents'
61
50
  }
@@ -160,7 +149,7 @@ function readCodexTomlConfig() {
160
149
  }
161
150
 
162
151
  function writeCodexTomlConfig(config) {
163
- ensureDir(getNativePathDir(CODEX_CONFIG_PATH));
152
+ ensureDir(path.dirname(CODEX_CONFIG_PATH));
164
153
  writeFileAtomic(CODEX_CONFIG_PATH, tomlStringify(config));
165
154
  }
166
155
 
@@ -169,7 +158,7 @@ function isPlainObject(value) {
169
158
  }
170
159
 
171
160
  function getCodexManagedAgentConfigPath(fileName) {
172
- return joinNativeBasePath(CODEX_AGENTS_DIR, `${fileName}.toml`);
161
+ return path.join(CODEX_AGENTS_DIR, `${fileName}.toml`);
173
162
  }
174
163
 
175
164
  function normalizeCodexConfigPath(configPath) {
@@ -198,7 +187,7 @@ function assertSafeCodexConfigPath(configPath) {
198
187
  return `~/${relative}`;
199
188
  }
200
189
 
201
- if (isNativeAbsolutePath(normalized)) {
190
+ if (path.isAbsolute(normalized)) {
202
191
  return normalized;
203
192
  }
204
193
 
@@ -220,35 +209,21 @@ function resolveCodexConfigPath(configPath) {
220
209
  if (!normalized) return '';
221
210
 
222
211
  if (normalized.startsWith('~/')) {
223
- return joinNativeBasePath(HOME_DIR, normalized.slice(2));
212
+ return path.join(HOME_DIR, normalized.slice(2));
224
213
  }
225
214
 
226
- if (isNativeAbsolutePath(normalized)) {
215
+ if (path.isAbsolute(normalized)) {
227
216
  return normalized;
228
217
  }
229
218
 
230
- return resolveNativeBasePath(CODEX_DIR, normalized);
231
- }
232
-
233
- function isPathInsideNativeDir(targetPath, baseDir) {
234
- const shouldFoldCase = isWindowsAbsolutePath(baseDir);
235
- const normalize = (value) => {
236
- const normalized = String(value || '')
237
- .replace(/[\\/]+/g, '/')
238
- .replace(/\/+$/, '');
239
- return shouldFoldCase ? normalized.toLowerCase() : normalized;
240
- };
241
-
242
- const normalizedTarget = normalize(targetPath);
243
- const normalizedBase = normalize(baseDir);
244
- return normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`);
219
+ return path.resolve(path.dirname(CODEX_CONFIG_PATH), normalized);
245
220
  }
246
221
 
247
222
  function isManagedCodexConfigPath(configPath) {
248
223
  const resolved = resolveCodexConfigPath(configPath);
249
224
  if (!resolved) return false;
250
- const managedRoot = resolveNativeBasePath(CODEX_AGENTS_DIR);
251
- return isPathInsideNativeDir(resolved, managedRoot);
225
+ const managedRoot = path.resolve(CODEX_AGENTS_DIR) + path.sep;
226
+ return resolved.startsWith(managedRoot) || resolved === path.resolve(CODEX_AGENTS_DIR);
252
227
  }
253
228
 
254
229
  function getManagedCodexConfigResolvedPath(configPath) {
@@ -412,7 +387,7 @@ function generateFrontmatter(data, platform = 'claude') {
412
387
  /**
413
388
  * 扫描目录获取代理文件(agents 约定为扁平目录)
414
389
  */
415
- function scanAgentsDir(dir, basePath, scope, options = {}) {
390
+ function scanAgentsDir(dir, basePath, scope) {
416
391
  const agents = [];
417
392
 
418
393
  if (!fs.existsSync(dir)) {
@@ -435,7 +410,7 @@ function scanAgentsDir(dir, basePath, scope, options = {}) {
435
410
  const relativePath = path.relative(basePath, fullPath);
436
411
  const fileName = entry.name.replace(/\.md$/, '');
437
412
 
438
- const agent = {
413
+ agents.push({
439
414
  name: frontmatter.name || fileName,
440
415
  fileName,
441
416
  scope,
@@ -449,15 +424,7 @@ function scanAgentsDir(dir, basePath, scope, options = {}) {
449
424
  systemPrompt: body,
450
425
  fullContent: content,
451
426
  updatedAt: fs.statSync(fullPath).mtime.getTime()
452
- };
453
- const decoratedAgent = typeof options.decorate === 'function'
454
- ? {
455
- ...agent,
456
- ...options.decorate(agent)
457
- }
458
- : agent;
459
-
460
- agents.push(decoratedAgent);
427
+ });
461
428
  } catch (err) {
462
429
  console.warn(`[AgentsService] Failed to parse ${fullPath}:`, err.message);
463
430
  }
@@ -566,7 +533,6 @@ class AgentsService {
566
533
  const config = PLATFORM_CONFIG[this.platform];
567
534
 
568
535
  this.userAgentsDir = config.userAgentsDir;
569
- this.storageDir = config.storageDir;
570
536
  if (this.platform === 'opencode') {
571
537
  const legacyUserDir = config.legacyUserAgentsDir;
572
538
  if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userAgentsDir)) {
@@ -577,9 +543,6 @@ class AgentsService {
577
543
  this.projectAgentsDir = config.projectAgentsDir;
578
544
  this.repoScanner = new AgentsRepoScanner(this.platform, this.userAgentsDir);
579
545
  ensureDir(this.userAgentsDir);
580
- if (this.platform !== 'codex') {
581
- ensureDir(this.storageDir);
582
- }
583
546
  }
584
547
 
585
548
  getProjectAgentsDir(projectPath) {
@@ -588,146 +551,36 @@ class AgentsService {
588
551
  return this.projectAgentsDir(safeProjectPath);
589
552
  }
590
553
 
591
- getManagedAgentPath(fileName) {
592
- assertSafeAgentFileName(fileName);
593
- return path.join(this.storageDir, `${fileName}.md`);
594
- }
595
-
596
- isInstalledAgent(fileName) {
597
- assertSafeAgentFileName(fileName);
598
- return fs.existsSync(path.join(this.userAgentsDir, `${fileName}.md`));
599
- }
600
-
601
- buildAgentContent({ fileName, name, description, tools, model, permissionMode, skills, systemPrompt }) {
602
- const frontmatterData = {
603
- name: name || fileName,
604
- description: description || ''
605
- };
606
- if (tools) frontmatterData.tools = tools;
607
- if (model) frontmatterData.model = model;
608
- if (permissionMode) frontmatterData.permissionMode = permissionMode;
609
- if (skills) frontmatterData.skills = skills;
610
-
611
- return generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
612
- }
613
-
614
- ensureManagedAgentCopy(fileName, sourcePath, options = {}) {
615
- const overwrite = options.overwrite === true;
616
- assertSafeAgentFileName(fileName);
617
- if (!sourcePath || !fs.existsSync(sourcePath)) {
618
- return false;
619
- }
620
-
621
- const managedPath = this.getManagedAgentPath(fileName);
622
- if (fs.existsSync(managedPath) && !overwrite) {
623
- return false;
624
- }
625
-
626
- ensureDir(path.dirname(managedPath));
627
- fs.copyFileSync(sourcePath, managedPath);
628
- return true;
629
- }
630
-
631
- mergeInstalledUserAgents(agents, options = {}) {
632
- const syncManagedLocalAgents = options.syncManagedLocalAgents === true;
633
- const installedAgents = scanAgentsDir(this.userAgentsDir, this.userAgentsDir, 'user', {
634
- decorate: () => ({
635
- installed: true,
636
- isManagedLocal: false,
637
- source: 'native-installed'
638
- })
639
- });
640
-
641
- for (const installedAgent of installedAgents) {
642
- const existing = agents.find(agent =>
643
- agent.scope === 'user' &&
644
- agent.fileName.toLowerCase() === installedAgent.fileName.toLowerCase()
645
- );
646
- const managedPath = this.getManagedAgentPath(installedAgent.fileName);
647
-
648
- if (existing) {
649
- if (syncManagedLocalAgents && fs.existsSync(managedPath)) {
650
- this.ensureManagedAgentCopy(installedAgent.fileName, installedAgent.fullPath, { overwrite: true });
651
- }
652
- existing.installed = true;
653
- if (!existing.description && installedAgent.description) existing.description = installedAgent.description;
654
- if (!existing.systemPrompt && installedAgent.systemPrompt) existing.systemPrompt = installedAgent.systemPrompt;
655
- if (!existing.fullContent && installedAgent.fullContent) existing.fullContent = installedAgent.fullContent;
656
- continue;
657
- }
658
-
659
- this.ensureManagedAgentCopy(installedAgent.fileName, installedAgent.fullPath, {
660
- overwrite: syncManagedLocalAgents
661
- });
662
- agents.push(installedAgent);
663
- }
664
- }
665
-
666
- mergeLocalUserAgents(agents) {
667
- if (!fs.existsSync(this.storageDir)) return;
668
-
669
- const localAgents = scanAgentsDir(this.storageDir, this.storageDir, 'user', {
670
- decorate: (agent) => ({
671
- installed: this.isInstalledAgent(agent.fileName),
672
- isManagedLocal: true,
673
- source: 'local'
674
- })
675
- });
676
-
677
- for (const localAgent of localAgents) {
678
- const existing = agents.find(agent =>
679
- agent.scope === 'user' &&
680
- agent.fileName.toLowerCase() === localAgent.fileName.toLowerCase()
681
- );
682
-
683
- if (existing) {
684
- existing.isManagedLocal = true;
685
- existing.installed = this.isInstalledAgent(existing.fileName);
686
- continue;
687
- }
688
-
689
- agents.push(localAgent);
690
- }
691
- }
692
-
693
554
  /**
694
555
  * 获取所有代理列表
695
556
  * @param {string} projectPath - 项目路径(可选,用于获取项目级代理)
696
557
  */
697
- listAgents(projectPath = null, options = {}) {
558
+ listAgents(projectPath = null) {
698
559
  if (this.platform === 'codex') {
699
560
  return this.listCodexAgents();
700
561
  }
701
562
 
702
563
  const agents = [];
703
- const syncManagedLocalAgents = options.syncManagedLocalAgents === true;
704
564
 
705
- this.mergeInstalledUserAgents(agents, { syncManagedLocalAgents });
706
- this.mergeLocalUserAgents(agents);
565
+ // 获取用户级代理
566
+ const userAgents = scanAgentsDir(this.userAgentsDir, this.userAgentsDir, 'user');
567
+ agents.push(...userAgents);
707
568
 
708
569
  // 获取项目级代理(如果提供了项目路径)
709
570
  if (projectPath) {
710
571
  const projectAgentsDir = this.getProjectAgentsDir(projectPath);
711
- const projectAgents = scanAgentsDir(projectAgentsDir, projectAgentsDir, 'project', {
712
- decorate: () => ({
713
- installed: true,
714
- isManagedLocal: false,
715
- source: 'native-installed'
716
- })
717
- });
572
+ const projectAgents = scanAgentsDir(projectAgentsDir, projectAgentsDir, 'project');
718
573
  agents.push(...projectAgents);
719
574
  }
720
575
 
721
576
  // 按名称排序
722
577
  agents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
723
- const userCount = agents.filter(agent => agent.scope === 'user').length;
724
- const projectCount = agents.length - userCount;
725
578
 
726
579
  return {
727
580
  agents,
728
581
  total: agents.length,
729
- userCount,
730
- projectCount
582
+ userCount: userAgents.length,
583
+ projectCount: agents.length - userAgents.length
731
584
  };
732
585
  }
733
586
 
@@ -747,9 +600,7 @@ class AgentsService {
747
600
  }
748
601
 
749
602
  // 获取本地代理
750
- const { agents: localAgents, userCount, projectCount } = this.listAgents(projectPath, {
751
- syncManagedLocalAgents: forceRefresh
752
- });
603
+ const { agents: localAgents, userCount, projectCount } = this.listAgents(projectPath);
753
604
 
754
605
  // 获取远程代理
755
606
  let remoteAgents = [];
@@ -801,26 +652,20 @@ class AgentsService {
801
652
  : this.getProjectAgentsDir(projectPath);
802
653
 
803
654
  const filePath = path.join(baseDir, `${fileName}.md`);
804
- const managedPath = scope === 'user' ? this.getManagedAgentPath(fileName) : '';
805
- const activePath = fs.existsSync(filePath)
806
- ? filePath
807
- : (managedPath && fs.existsSync(managedPath) ? managedPath : '');
808
655
 
809
- if (!activePath) {
656
+ if (!fs.existsSync(filePath)) {
810
657
  return null;
811
658
  }
812
659
 
813
- const content = fs.readFileSync(activePath, 'utf-8');
660
+ const content = fs.readFileSync(filePath, 'utf-8');
814
661
  const { frontmatter, body } = parseFrontmatter(content);
815
- const installed = fs.existsSync(filePath);
816
- const isManagedLocal = scope === 'user' && !!managedPath && fs.existsSync(managedPath);
817
662
 
818
663
  return {
819
664
  name: frontmatter.name || fileName,
820
665
  fileName,
821
666
  scope,
822
667
  path: `${fileName}.md`,
823
- fullPath: activePath,
668
+ fullPath: filePath,
824
669
  description: frontmatter.description || '',
825
670
  tools: frontmatter.tools || '',
826
671
  model: frontmatter.model || '',
@@ -828,10 +673,7 @@ class AgentsService {
828
673
  skills: frontmatter.skills || '',
829
674
  systemPrompt: body,
830
675
  fullContent: content,
831
- installed,
832
- isManagedLocal,
833
- source: installed ? 'native-installed' : 'local',
834
- updatedAt: fs.statSync(activePath).mtime.getTime()
676
+ updatedAt: fs.statSync(filePath).mtime.getTime()
835
677
  };
836
678
  }
837
679
 
@@ -860,29 +702,22 @@ class AgentsService {
860
702
  ensureDir(baseDir);
861
703
 
862
704
  const filePath = path.join(baseDir, `${fileName}.md`);
863
- const managedPath = scope === 'user' ? this.getManagedAgentPath(fileName) : '';
864
705
 
865
706
  // 检查是否已存在
866
- if (fs.existsSync(filePath) || (managedPath && fs.existsSync(managedPath))) {
707
+ if (fs.existsSync(filePath)) {
867
708
  throw new Error(`代理 "${fileName}" 已存在`);
868
709
  }
869
710
 
870
- const content = this.buildAgentContent({
871
- fileName,
872
- name,
873
- description,
874
- tools,
875
- model,
876
- permissionMode,
877
- skills,
878
- systemPrompt
879
- });
711
+ // 生成文件内容
712
+ const frontmatterData = { name: (name || fileName), description };
713
+ if (tools) frontmatterData.tools = tools;
714
+ if (model) frontmatterData.model = model;
715
+ if (permissionMode) frontmatterData.permissionMode = permissionMode;
716
+ if (skills) frontmatterData.skills = skills;
717
+
718
+ const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
880
719
 
881
720
  fs.writeFileSync(filePath, content, 'utf-8');
882
- if (managedPath) {
883
- ensureDir(path.dirname(managedPath));
884
- fs.writeFileSync(managedPath, content, 'utf-8');
885
- }
886
721
 
887
722
  return this.getAgent(fileName, scope, projectPath);
888
723
  }
@@ -902,31 +737,24 @@ class AgentsService {
902
737
  : this.getProjectAgentsDir(projectPath);
903
738
 
904
739
  const filePath = path.join(baseDir, `${fileName}.md`);
905
- const managedPath = scope === 'user' ? this.getManagedAgentPath(fileName) : '';
906
- const hasManagedCopy = managedPath && fs.existsSync(managedPath);
907
740
 
908
- if (!fs.existsSync(filePath) && !hasManagedCopy) {
741
+ if (!fs.existsSync(filePath)) {
909
742
  throw new Error(`代理 "${fileName}" 不存在`);
910
743
  }
911
744
 
912
- const content = this.buildAgentContent({
913
- fileName,
914
- name,
915
- description,
916
- tools,
917
- model,
918
- permissionMode,
919
- skills,
920
- systemPrompt
921
- });
745
+ // 生成文件内容
746
+ const frontmatterData = {
747
+ name: name || fileName,
748
+ description: description || ''
749
+ };
750
+ if (tools) frontmatterData.tools = tools;
751
+ if (model) frontmatterData.model = model;
752
+ if (permissionMode) frontmatterData.permissionMode = permissionMode;
753
+ if (skills) frontmatterData.skills = skills;
922
754
 
923
- if (fs.existsSync(filePath)) {
924
- fs.writeFileSync(filePath, content, 'utf-8');
925
- }
926
- if (managedPath) {
927
- ensureDir(path.dirname(managedPath));
928
- fs.writeFileSync(managedPath, content, 'utf-8');
929
- }
755
+ const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
756
+
757
+ fs.writeFileSync(filePath, content, 'utf-8');
930
758
 
931
759
  return this.getAgent(fileName, scope, projectPath);
932
760
  }
@@ -946,23 +774,13 @@ class AgentsService {
946
774
  : this.getProjectAgentsDir(projectPath);
947
775
 
948
776
  const filePath = path.join(baseDir, `${fileName}.md`);
949
- const managedPath = scope === 'user' ? this.getManagedAgentPath(fileName) : '';
950
- let removed = false;
951
777
 
952
- if (fs.existsSync(filePath)) {
953
- fs.unlinkSync(filePath);
954
- removed = true;
955
- }
956
-
957
- if (managedPath && fs.existsSync(managedPath)) {
958
- fs.unlinkSync(managedPath);
959
- removed = true;
960
- }
961
-
962
- if (!removed) {
778
+ if (!fs.existsSync(filePath)) {
963
779
  return { success: false, message: '代理不存在' };
964
780
  }
965
781
 
782
+ fs.unlinkSync(filePath);
783
+
966
784
  return { success: true, message: '代理已删除' };
967
785
  }
968
786
 
@@ -1042,37 +860,12 @@ class AgentsService {
1042
860
  return this.repoScanner.installAgent(agent);
1043
861
  }
1044
862
 
1045
- installLocalAgent(fileName) {
1046
- assertSafeAgentFileName(fileName);
1047
- const managedPath = this.getManagedAgentPath(fileName);
1048
- const targetPath = path.join(this.userAgentsDir, `${fileName}.md`);
1049
-
1050
- if (!fs.existsSync(managedPath)) {
1051
- throw new Error(`本地代理 "${fileName}" 不存在`);
1052
- }
1053
-
1054
- if (fs.existsSync(targetPath)) {
1055
- return { success: true, message: 'Already installed' };
1056
- }
1057
-
1058
- ensureDir(path.dirname(targetPath));
1059
- fs.copyFileSync(managedPath, targetPath);
1060
- return { success: true, message: 'Installed successfully' };
1061
- }
1062
-
1063
863
  /**
1064
864
  * 卸载代理
1065
865
  */
1066
866
  uninstallAgent(fileName) {
1067
867
  assertSafeAgentFileName(fileName);
1068
- const targetPath = path.join(this.userAgentsDir, `${fileName}.md`);
1069
-
1070
- if (fs.existsSync(targetPath)) {
1071
- fs.unlinkSync(targetPath);
1072
- return { success: true, message: 'Uninstalled successfully' };
1073
- }
1074
-
1075
- return { success: true, message: 'Not installed' };
868
+ return this.repoScanner.uninstall(`${fileName}.md`);
1076
869
  }
1077
870
 
1078
871
  listCodexAgents() {
@@ -1193,7 +986,7 @@ class AgentsService {
1193
986
  } else if (normalizedMode === 'custom') {
1194
987
  const safeConfigFile = assertSafeCodexConfigPath(configFile);
1195
988
  const resolvedPath = resolveCodexConfigPath(safeConfigFile);
1196
- ensureDir(getNativePathDir(resolvedPath));
989
+ ensureDir(path.dirname(resolvedPath));
1197
990
  const content = resolveCodexConfigContent({ configContent, model });
1198
991
  writeFileAtomic(resolvedPath, content);
1199
992
  agentConfig.config_file = safeConfigFile;
@@ -1239,7 +1032,7 @@ class AgentsService {
1239
1032
  const parsedConfigFile = readCodexAgentConfigFile(resolvedConfigFilePath);
1240
1033
  const configFileData = isPlainObject(parsedConfigFile?.data) ? parsedConfigFile.data : {};
1241
1034
  configFileData.model = trimmedModel;
1242
- ensureDir(getNativePathDir(resolvedConfigFilePath));
1035
+ ensureDir(path.dirname(resolvedConfigFilePath));
1243
1036
  writeFileAtomic(resolvedConfigFilePath, tomlStringify(configFileData));
1244
1037
  agentConfig.config_file = configFilePath;
1245
1038
  } else if (isExistingManagedConfig) {
@@ -1265,7 +1058,7 @@ class AgentsService {
1265
1058
  model,
1266
1059
  fallbackContent: existingManagedContent
1267
1060
  });
1268
- ensureDir(getNativePathDir(resolvedConfigFilePath));
1061
+ ensureDir(path.dirname(resolvedConfigFilePath));
1269
1062
  writeFileAtomic(resolvedConfigFilePath, content);
1270
1063
  agentConfig.config_file = configFilePath;
1271
1064
  if (resolvedExistingConfigFile &&
@@ -1294,7 +1087,7 @@ class AgentsService {
1294
1087
  model,
1295
1088
  fallbackContent: existingContent
1296
1089
  });
1297
- ensureDir(getNativePathDir(resolvedTargetConfigPath));
1090
+ ensureDir(path.dirname(resolvedTargetConfigPath));
1298
1091
  writeFileAtomic(resolvedTargetConfigPath, content);
1299
1092
  agentConfig.config_file = targetConfigPath;
1300
1093
  if (resolvedExistingConfigFile &&