coding-tool-x 3.5.8 → 3.5.10

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 (91) 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-BzoNzfbi.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-O4ikBt1o.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-BQjsnblU.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-DS_DJnVc.js → PluginManager-jgeQOE24.js} +1 -1
  10. package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
  13. package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
  14. package/dist/web/assets/{SkillManager-DpNE02r0.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-DMY7_SHh.js → WorkspaceManager-D3B0Y5XE.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-BnyNm74G.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-proxy.js +4 -2
  37. package/src/server/api/codex-sessions.js +88 -9
  38. package/src/server/api/commands.js +2 -38
  39. package/src/server/api/dashboard.js +1 -25
  40. package/src/server/api/gemini-proxy.js +4 -2
  41. package/src/server/api/gemini-sessions.js +86 -9
  42. package/src/server/api/hooks.js +1 -17
  43. package/src/server/api/opencode-proxy.js +7 -2
  44. package/src/server/api/opencode-sessions.js +82 -7
  45. package/src/server/api/plugins.js +34 -128
  46. package/src/server/api/proxy.js +5 -3
  47. package/src/server/api/sessions.js +180 -24
  48. package/src/server/api/workspaces.js +0 -14
  49. package/src/server/index.js +9 -31
  50. package/src/server/services/agents-service.js +62 -269
  51. package/src/server/services/base/base-channel-service.js +3 -1
  52. package/src/server/services/channels.js +5 -0
  53. package/src/server/services/codex-channels.js +2 -0
  54. package/src/server/services/codex-sessions.js +78 -108
  55. package/src/server/services/commands-service.js +81 -281
  56. package/src/server/services/config-export-service.js +7 -7
  57. package/src/server/services/config-registry-service.js +9 -8
  58. package/src/server/services/config-sync-manager.js +41 -61
  59. package/src/server/services/config-sync-service.js +3 -3
  60. package/src/server/services/gemini-channels.js +17 -6
  61. package/src/server/services/gemini-config.js +4 -3
  62. package/src/server/services/gemini-sessions.js +51 -26
  63. package/src/server/services/gemini-settings-manager.js +3 -2
  64. package/src/server/services/mcp-service.js +14 -9
  65. package/src/server/services/native-oauth-adapters.js +3 -3
  66. package/src/server/services/network-access.js +2 -61
  67. package/src/server/services/notification-hooks.js +19 -184
  68. package/src/server/services/opencode-channels.js +9 -0
  69. package/src/server/services/opencode-sessions.js +4 -4
  70. package/src/server/services/opencode-settings-manager.js +3 -3
  71. package/src/server/services/plugins-service.js +62 -996
  72. package/src/server/services/prompts-service.js +9 -5
  73. package/src/server/services/sessions.js +128 -99
  74. package/src/server/services/skill-service.js +18 -155
  75. package/src/server/websocket-server.js +4 -35
  76. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  77. package/dist/web/assets/Home-qzk118Of.css +0 -1
  78. package/dist/web/assets/ProjectList-CqYDtsHx.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-CfPtcq6Y.css +0 -1
  81. package/dist/web/assets/SessionList-DMlLtMCz.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Clf0l3wc.js +0 -2
  84. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  85. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  86. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  87. package/docs/Caddyfile.example +0 -19
  88. package/docs/reverse-proxy-https.md +0 -57
  89. package/src/server/services/https-cert.js +0 -171
  90. package/src/server/services/session-launch-command.js +0 -81
  91. package/src/server/services/web-ui-runtime.js +0 -54
@@ -6,16 +6,20 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const { PATHS, getNativePlatformPromptPath } = require('../../config/paths');
9
+ const os = require('os');
10
+ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
11
+ const { resolvePreferredHomeDir } = require('../../utils/home-dir');
12
+
13
+ const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
10
14
 
11
15
  // Prompts 配置文件路径
12
16
  const PROMPTS_FILE = PATHS.prompts;
13
17
 
14
18
  // 各平台提示词文件路径
15
- const CLAUDE_PROMPT_PATH = getNativePlatformPromptPath('claude');
16
- const CODEX_PROMPT_PATH = getNativePlatformPromptPath('codex');
17
- const GEMINI_PROMPT_PATH = getNativePlatformPromptPath('gemini');
18
- const OPENCODE_PROMPT_PATH = getNativePlatformPromptPath('opencode');
19
+ const CLAUDE_PROMPT_PATH = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
20
+ const CODEX_PROMPT_PATH = path.join(HOME_DIR, '.codex', 'AGENTS.md');
21
+ const GEMINI_PROMPT_PATH = path.join(HOME_DIR, '.gemini', 'GEMINI.md');
22
+ const OPENCODE_PROMPT_PATH = path.join(NATIVE_PATHS.opencode.config, 'AGENTS.md');
19
23
 
20
24
  function normalizeApps(apps = {}, defaults = { claude: true, codex: true, gemini: true, opencode: false }) {
21
25
  return {
@@ -14,11 +14,8 @@ const { globalCache, CacheKeys } = require('./enhanced-cache');
14
14
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
15
15
 
16
16
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
17
- const CODEX_PROJECTS_DIR = NATIVE_PATHS.codex.projects;
18
- const GEMINI_PROJECTS_DIR = NATIVE_PATHS.gemini.projects;
19
- const PROJECT_PATH_CACHE_TTL_MS = 5 * 60 * 1000;
20
- const MAX_PROJECT_PATH_CACHE_ENTRIES = 500;
21
- let projectPathResolutionCache = new Map();
17
+ const CODEX_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
18
+ const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
22
19
 
23
20
  // Base directory for cc-tool data
24
21
  function getCcToolDir() {
@@ -89,96 +86,113 @@ function saveForkRelations(relations) {
89
86
  fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
90
87
  }
91
88
 
92
- // Get all projects with stats (async version)
93
- async function getProjects(config) {
94
- const projectsDir = config.projectsDir;
89
+ function invalidateSessionResultCache(projectName) {
90
+ if (!projectName) return;
91
+ globalCache.delete(`${CacheKeys.SESSIONS}${projectName}`);
92
+ }
95
93
 
96
- if (!fs.existsSync(projectsDir)) {
97
- return [];
94
+ function getClaudeForkMessageText(content) {
95
+ if (typeof content === 'string') {
96
+ return content === 'Warmup' ? '' : content;
98
97
  }
99
98
 
100
- const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
101
- return entries
102
- .filter(entry => entry.isDirectory())
103
- .map(entry => entry.name);
104
- }
99
+ if (!Array.isArray(content)) {
100
+ return '';
101
+ }
105
102
 
106
- function getProjectPathCacheKey(encodedName) {
107
- return `${process.platform}:${encodedName}`;
103
+ const parts = [];
104
+ for (const item of content) {
105
+ if (item?.type === 'text' && item.text) {
106
+ parts.push(item.text);
107
+ } else if (item?.type === 'image') {
108
+ parts.push('[图片]');
109
+ }
110
+ }
111
+
112
+ return parts.join('\n\n').trim();
108
113
  }
109
114
 
110
- function cloneProjectPathResolution(value) {
115
+ function splitTextPreserveEol(content) {
116
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
117
+ const hasTrailingEol = content.endsWith('\r\n') || content.endsWith('\n');
111
118
  return {
112
- fullPath: value?.fullPath || '',
113
- projectName: value?.projectName || ''
119
+ lines: content.split(/\r?\n/),
120
+ eol,
121
+ hasTrailingEol
114
122
  };
115
123
  }
116
124
 
117
- function getCachedProjectPathResolution(encodedName) {
118
- const cacheKey = getProjectPathCacheKey(encodedName);
119
- const cached = projectPathResolutionCache.get(cacheKey);
120
- if (!cached) {
121
- return null;
122
- }
125
+ function joinTextPreserveEol(lines, eol, hasTrailingEol) {
126
+ const text = lines.join(eol);
127
+ return hasTrailingEol ? `${text}${eol}` : text;
128
+ }
123
129
 
124
- if (cached.expiresAt <= Date.now()) {
125
- projectPathResolutionCache.delete(cacheKey);
126
- return null;
130
+ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber) {
131
+ if (!Number.isInteger(afterUserMessageNumber) || afterUserMessageNumber <= 0) {
132
+ return content;
127
133
  }
128
134
 
129
- return cloneProjectPathResolution(cached.value);
130
- }
135
+ const { lines, eol, hasTrailingEol } = splitTextPreserveEol(content);
136
+ const keptLines = [];
137
+ let matchedUserMessages = 0;
131
138
 
132
- function setCachedProjectPathResolution(encodedName, value) {
133
- const cacheKey = getProjectPathCacheKey(encodedName);
134
- projectPathResolutionCache.delete(cacheKey);
135
- projectPathResolutionCache.set(cacheKey, {
136
- expiresAt: Date.now() + PROJECT_PATH_CACHE_TTL_MS,
137
- value: cloneProjectPathResolution(value)
138
- });
139
+ for (const line of lines) {
140
+ if (!line.trim()) {
141
+ keptLines.push(line);
142
+ continue;
143
+ }
144
+
145
+ let json;
146
+ try {
147
+ json = JSON.parse(line);
148
+ } catch (err) {
149
+ keptLines.push(line);
150
+ continue;
151
+ }
152
+
153
+ keptLines.push(line);
154
+
155
+ if (json.type !== 'user') {
156
+ continue;
157
+ }
139
158
 
140
- while (projectPathResolutionCache.size > MAX_PROJECT_PATH_CACHE_ENTRIES) {
141
- const oldestKey = projectPathResolutionCache.keys().next().value;
142
- if (!oldestKey) {
143
- break;
159
+ const userText = getClaudeForkMessageText(json.message?.content);
160
+ if (!userText) {
161
+ continue;
162
+ }
163
+
164
+ matchedUserMessages += 1;
165
+ if (matchedUserMessages >= afterUserMessageNumber) {
166
+ return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
144
167
  }
145
- projectPathResolutionCache.delete(oldestKey);
146
168
  }
169
+
170
+ throw new Error(`afterUserMessageNumber ${afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
147
171
  }
148
172
 
149
- function clearProjectPathResolutionCache(encodedName) {
150
- if (!encodedName) {
151
- projectPathResolutionCache.clear();
152
- return;
173
+ // Get all projects with stats (async version)
174
+ async function getProjects(config) {
175
+ const projectsDir = config.projectsDir;
176
+
177
+ if (!fs.existsSync(projectsDir)) {
178
+ return [];
153
179
  }
154
180
 
155
- projectPathResolutionCache.delete(getProjectPathCacheKey(encodedName));
181
+ const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
182
+ return entries
183
+ .filter(entry => entry.isDirectory())
184
+ .map(entry => entry.name);
156
185
  }
157
186
 
158
187
  // Parse real project path from encoded name
159
188
  // macOS/Linux: "-Users-lilithgames-work-project" -> "/Users/lilithgames/work/project"
160
189
  // Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
161
190
  function parseRealProjectPath(encodedName) {
162
- const normalizedEncodedName = String(encodedName || '').trim();
163
- const cached = getCachedProjectPathResolution(normalizedEncodedName);
164
- if (cached) {
165
- return cached;
166
- }
167
-
168
191
  const isWindows = process.platform === 'win32';
169
- const fallbackFromSessions = tryResolvePathFromSessions(normalizedEncodedName);
170
-
171
- if (fallbackFromSessions?.fullPath) {
172
- const resolved = {
173
- fullPath: fallbackFromSessions.fullPath,
174
- projectName: fallbackFromSessions.projectName || path.basename(fallbackFromSessions.fullPath) || normalizedEncodedName
175
- };
176
- setCachedProjectPathResolution(normalizedEncodedName, resolved);
177
- return resolved;
178
- }
192
+ const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
179
193
 
180
194
  // Detect Windows drive letter (e.g., "C--Users-admin")
181
- const windowsDriveMatch = normalizedEncodedName.match(/^([A-Z])--(.+)$/);
195
+ const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
182
196
 
183
197
  if (isWindows && windowsDriveMatch) {
184
198
  // Windows path with drive letter
@@ -237,15 +251,13 @@ function parseRealProjectPath(encodedName) {
237
251
  currentPath = driveLetter + ':\\' + realSegments.join('\\');
238
252
  }
239
253
 
240
- const resolved = {
241
- fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
242
- projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
254
+ return {
255
+ fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
256
+ projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
243
257
  };
244
- setCachedProjectPathResolution(normalizedEncodedName, resolved);
245
- return resolved;
246
258
  } else {
247
259
  // Unix-like path (macOS/Linux) or fallback
248
- const pathStr = normalizedEncodedName.replace(/^-/, '/').replace(/-/g, '/');
260
+ const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
249
261
  const segments = pathStr.split('/').filter(s => s);
250
262
 
251
263
  // Build path from left to right, checking existence
@@ -297,12 +309,10 @@ function parseRealProjectPath(encodedName) {
297
309
  currentPath = '/' + realSegments.join('/');
298
310
  }
299
311
 
300
- const resolved = {
301
- fullPath: validateProjectPath(currentPath) || pathStr,
302
- projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
312
+ return {
313
+ fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
314
+ projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
303
315
  };
304
- setCachedProjectPathResolution(normalizedEncodedName, resolved);
305
- return resolved;
306
316
  }
307
317
  }
308
318
 
@@ -421,20 +431,27 @@ async function buildProjectsWithStats(config) {
421
431
  const files = await fs.promises.readdir(projectPath);
422
432
  const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
423
433
 
434
+ // Filter: only count sessions that have actual messages (in parallel)
424
435
  const sessionChecks = await Promise.all(
425
- jsonlFiles.map(async (fileName) => {
426
- const filePath = path.join(projectPath, fileName);
427
- const stats = await fs.promises.stat(filePath);
428
- const hasMessages = await hasActualMessages(filePath, stats);
429
- return hasMessages ? stats.mtime.getTime() : null;
436
+ jsonlFiles.map(async (f) => {
437
+ const filePath = path.join(projectPath, f);
438
+ const hasMessages = await hasActualMessages(filePath);
439
+ return hasMessages ? f : null;
430
440
  })
431
441
  );
432
442
 
433
- const activeSessionTimes = sessionChecks.filter((mtimeMs) => Number.isFinite(mtimeMs));
434
- sessionCount = activeSessionTimes.length;
443
+ const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
444
+ sessionCount = sessionFilesWithMessages.length;
435
445
 
436
- if (activeSessionTimes.length > 0) {
437
- lastUsed = Math.max(...activeSessionTimes);
446
+ // Find most recent session (only from sessions with messages)
447
+ if (sessionFilesWithMessages.length > 0) {
448
+ const statPromises = sessionFilesWithMessages.map(async (f) => {
449
+ const filePath = path.join(projectPath, f);
450
+ const stat = await fs.promises.stat(filePath);
451
+ return stat.mtime.getTime();
452
+ });
453
+ const stats = await Promise.all(statPromises);
454
+ lastUsed = Math.max(...stats);
438
455
  }
439
456
  } catch (err) {
440
457
  // Ignore errors
@@ -482,9 +499,9 @@ function getProjectAndSessionCounts(config) {
482
499
  }
483
500
 
484
501
  // Check if a session file has actual messages (async with enhanced caching)
485
- async function hasActualMessages(filePath, statsOverride = null) {
502
+ async function hasActualMessages(filePath) {
486
503
  try {
487
- const stats = statsOverride || await fs.promises.stat(filePath);
504
+ const stats = await fs.promises.stat(filePath);
488
505
 
489
506
  // Check enhanced cache first
490
507
  const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
@@ -652,11 +669,12 @@ function deleteSession(config, projectName, sessionId) {
652
669
 
653
670
  fs.unlinkSync(sessionFile);
654
671
  invalidateProjectsCache(config);
672
+ invalidateSessionResultCache(projectName);
655
673
  return { success: true };
656
674
  }
657
675
 
658
676
  // Fork a session
659
- function forkSession(config, projectName, sessionId) {
677
+ function forkSession(config, projectName, sessionId, options = {}) {
660
678
  const projectDir = path.join(config.projectsDir, projectName);
661
679
  const sessionFile = path.join(projectDir, sessionId + '.jsonl');
662
680
 
@@ -665,7 +683,11 @@ function forkSession(config, projectName, sessionId) {
665
683
  }
666
684
 
667
685
  // Read the original session
668
- const content = fs.readFileSync(sessionFile, 'utf8');
686
+ const originalContent = fs.readFileSync(sessionFile, 'utf8');
687
+ const content = sliceClaudeSessionContentByUserMessage(
688
+ originalContent,
689
+ options.afterUserMessageNumber
690
+ );
669
691
 
670
692
  // Generate new session ID (UUID v4)
671
693
  const newSessionId = crypto.randomUUID();
@@ -678,9 +700,19 @@ function forkSession(config, projectName, sessionId) {
678
700
  const forkRelations = getForkRelations();
679
701
  forkRelations[newSessionId] = sessionId;
680
702
  saveForkRelations(forkRelations);
703
+ if (options.alias) {
704
+ const { setAlias } = require('./alias');
705
+ setAlias(newSessionId, options.alias);
706
+ }
681
707
  invalidateProjectsCache(config);
708
+ invalidateSessionResultCache(projectName);
682
709
 
683
- return { newSessionId, forkedFrom: sessionId };
710
+ return {
711
+ newSessionId,
712
+ forkedFrom: sessionId,
713
+ alias: options.alias || null,
714
+ afterUserMessageNumber: options.afterUserMessageNumber || null
715
+ };
684
716
  }
685
717
 
686
718
  // Get session order for a project
@@ -725,13 +757,14 @@ function saveSessionOrder(projectName, order) {
725
757
  // Delete a project (remove the entire project directory)
726
758
  function deleteProject(config, projectName) {
727
759
  const projectDir = path.join(config.projectsDir, projectName);
728
- const existed = fs.existsSync(projectDir);
729
760
 
730
- if (existed) {
731
- // Recursively delete the directory
732
- fs.rmSync(projectDir, { recursive: true, force: true });
761
+ if (!fs.existsSync(projectDir)) {
762
+ throw new Error('Project not found');
733
763
  }
734
764
 
765
+ // Recursively delete the directory
766
+ fs.rmSync(projectDir, { recursive: true, force: true });
767
+
735
768
  // Remove from order file if exists
736
769
  const order = getProjectOrder(config);
737
770
  const newOrder = order.filter(name => name !== projectName);
@@ -740,11 +773,7 @@ function deleteProject(config, projectName) {
740
773
  }
741
774
 
742
775
  invalidateProjectsCache(config);
743
- clearProjectPathResolutionCache(projectName);
744
- return {
745
- success: true,
746
- alreadyDeleted: !existed
747
- };
776
+ return { success: true };
748
777
  }
749
778
 
750
779
  // Search sessions for keyword
@@ -18,7 +18,7 @@ const {
18
18
  parseSkillContent,
19
19
  } = require('./format-converter');
20
20
  const { maskToken } = require('./oauth-utils');
21
- const { NATIVE_PATHS, HOME_DIR, PATHS, getNativePlatformSkillsDir } = require('../../config/paths');
21
+ const { NATIVE_PATHS, HOME_DIR, PATHS } = require('../../config/paths');
22
22
 
23
23
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
24
24
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
@@ -166,25 +166,25 @@ const DEFAULT_REPOS_BY_PLATFORM = {
166
166
 
167
167
  const PLATFORM_CONFIG = {
168
168
  claude: {
169
- installDir: getNativePlatformSkillsDir('claude'),
169
+ installDir: path.join(HOME_DIR, '.claude', 'skills'),
170
170
  storageDir: PATHS.localSkills.claude,
171
171
  reposFile: PATHS.skillRepos.claude,
172
172
  cacheFile: PATHS.skillCaches.claude
173
173
  },
174
174
  codex: {
175
- installDir: getNativePlatformSkillsDir('codex'),
175
+ installDir: path.join(HOME_DIR, '.codex', 'skills'),
176
176
  storageDir: PATHS.localSkills.codex,
177
177
  reposFile: PATHS.skillRepos.codex,
178
178
  cacheFile: PATHS.skillCaches.codex
179
179
  },
180
180
  gemini: {
181
- installDir: getNativePlatformSkillsDir('gemini'),
181
+ installDir: path.join(HOME_DIR, '.gemini', 'skills'),
182
182
  storageDir: PATHS.localSkills.gemini,
183
183
  reposFile: PATHS.skillRepos.gemini,
184
184
  cacheFile: PATHS.skillCaches.gemini
185
185
  },
186
186
  opencode: {
187
- installDir: getNativePlatformSkillsDir('opencode'),
187
+ installDir: path.join(NATIVE_PATHS.opencode.config, 'skills'),
188
188
  storageDir: PATHS.localSkills.opencode,
189
189
  reposFile: PATHS.skillRepos.opencode,
190
190
  cacheFile: PATHS.skillCaches.opencode
@@ -248,12 +248,11 @@ class SkillService {
248
248
  }
249
249
  }
250
250
 
251
- prepareSkills(skills = [], options = {}) {
251
+ prepareSkills(skills = []) {
252
252
  const preparedSkills = Array.isArray(skills)
253
253
  ? skills.map(skill => ({ ...skill }))
254
254
  : [];
255
255
 
256
- this.mergeInstalledSkills(preparedSkills, options);
257
256
  this.mergeLocalSkills(preparedSkills);
258
257
  this.deduplicateSkills(preparedSkills);
259
258
  preparedSkills.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
@@ -538,8 +537,6 @@ class SkillService {
538
537
  * 获取所有技能列表(带缓存)
539
538
  */
540
539
  async listSkills(forceRefresh = false) {
541
- const prepareOptions = { syncManagedLocalSkills: forceRefresh };
542
-
543
540
  // 强制刷新时仅清空内存缓存,保留磁盘缓存作为回退来源
544
541
  if (forceRefresh) {
545
542
  this.clearCache();
@@ -550,11 +547,11 @@ class SkillService {
550
547
  // 检查内存缓存
551
548
  if (!forceRefresh && Array.isArray(this.skillsCache) && this.skillsCache.length > 0) {
552
549
  if (Array.isArray(fileCache) && fileCache.length > this.skillsCache.length) {
553
- this.skillsCache = this.prepareSkills(fileCache, prepareOptions);
550
+ this.skillsCache = this.prepareSkills(fileCache);
554
551
  this.cacheTime = Date.now();
555
552
  return this.skillsCache;
556
553
  }
557
- this.skillsCache = this.prepareSkills(this.skillsCache, prepareOptions);
554
+ this.skillsCache = this.prepareSkills(this.skillsCache);
558
555
  this.cacheTime = Date.now();
559
556
  return this.skillsCache;
560
557
  }
@@ -562,7 +559,7 @@ class SkillService {
562
559
  // 检查文件缓存
563
560
  if (!forceRefresh) {
564
561
  if (fileCache && fileCache.length > 0) {
565
- this.skillsCache = this.prepareSkills(fileCache, prepareOptions);
562
+ this.skillsCache = this.prepareSkills(fileCache);
566
563
  this.cacheTime = Date.now();
567
564
  return this.skillsCache;
568
565
  }
@@ -603,10 +600,10 @@ class SkillService {
603
600
  }
604
601
  }
605
602
 
606
- const preparedSkills = this.prepareSkills(skills, prepareOptions);
603
+ const preparedSkills = this.prepareSkills(skills);
607
604
 
608
605
  const hasUsableFileCache = Array.isArray(fileCache) && fileCache.length > 0;
609
- const preparedFileCache = hasUsableFileCache ? this.prepareSkills(fileCache, prepareOptions) : null;
606
+ const preparedFileCache = hasUsableFileCache ? this.prepareSkills(fileCache) : null;
610
607
  const shouldUseStaleFileCache = hasUsableFileCache && (
611
608
  (enabledRemoteRepos.length > 0 && remoteFailureCount === enabledRemoteRepos.length) ||
612
609
  (remoteFailureCount > 0 && preparedFileCache.length > preparedSkills.length)
@@ -1350,75 +1347,6 @@ class SkillService {
1350
1347
  this.scanLocalDir(this.storageDir, this.storageDir, skills);
1351
1348
  }
1352
1349
 
1353
- /**
1354
- * 合并平台原生安装目录中的技能,补齐“已安装但未被仓库/本地托管覆盖”的条目
1355
- */
1356
- mergeInstalledSkills(skills, options = {}) {
1357
- if (!fs.existsSync(this.installDir)) return;
1358
-
1359
- const syncManagedLocalSkills = options.syncManagedLocalSkills === true;
1360
- const installedSkills = [];
1361
- this.scanInstalledDir(this.installDir, this.installDir, installedSkills);
1362
-
1363
- for (const installedSkill of installedSkills) {
1364
- const normalizedDirectory = normalizeRepoPath(installedSkill.directory).toLowerCase();
1365
- const existing = skills.find(skill =>
1366
- normalizeRepoPath(skill.directory).toLowerCase() === normalizedDirectory
1367
- );
1368
-
1369
- if (existing) {
1370
- const managedSkillMdPath = path.join(this.storageDir, installedSkill.directory, 'SKILL.md');
1371
- if (syncManagedLocalSkills && fs.existsSync(managedSkillMdPath)) {
1372
- const installedSkillPath = path.join(this.installDir, installedSkill.directory);
1373
- this.ensureManagedSkillCopy(installedSkill.directory, installedSkillPath, { overwrite: true });
1374
- }
1375
- existing.installed = true;
1376
- if (!existing.name && installedSkill.name) {
1377
- existing.name = installedSkill.name;
1378
- }
1379
- if (!existing.description && installedSkill.description) {
1380
- existing.description = installedSkill.description;
1381
- }
1382
- if (!existing.license && installedSkill.license) {
1383
- existing.license = installedSkill.license;
1384
- }
1385
- continue;
1386
- }
1387
-
1388
- const installedSkillPath = path.join(this.installDir, installedSkill.directory);
1389
- this.ensureManagedSkillCopy(installedSkill.directory, installedSkillPath, {
1390
- overwrite: syncManagedLocalSkills
1391
- });
1392
- skills.push(installedSkill);
1393
- }
1394
- }
1395
-
1396
- ensureManagedSkillCopy(directory, sourceDir, options = {}) {
1397
- const normalizedDirectory = normalizeRepoPath(directory);
1398
- const overwrite = options.overwrite === true;
1399
- if (!normalizedDirectory || !sourceDir || !fs.existsSync(sourceDir)) {
1400
- return false;
1401
- }
1402
-
1403
- const sourceSkillMdPath = path.join(sourceDir, 'SKILL.md');
1404
- if (!fs.existsSync(sourceSkillMdPath)) {
1405
- return false;
1406
- }
1407
-
1408
- const managedDir = path.join(this.storageDir, normalizedDirectory);
1409
- const managedSkillMdPath = path.join(managedDir, 'SKILL.md');
1410
- if (fs.existsSync(managedSkillMdPath)) {
1411
- if (!overwrite) {
1412
- return false;
1413
- }
1414
- fs.rmSync(managedDir, { recursive: true, force: true });
1415
- }
1416
-
1417
- fs.mkdirSync(managedDir, { recursive: true });
1418
- this.copyDirRecursive(sourceDir, managedDir);
1419
- return true;
1420
- }
1421
-
1422
1350
  /**
1423
1351
  * 递归扫描本地目录
1424
1352
  */
@@ -1482,54 +1410,6 @@ class SkillService {
1482
1410
  }
1483
1411
  }
1484
1412
 
1485
- /**
1486
- * 递归扫描平台原生安装目录
1487
- */
1488
- scanInstalledDir(currentDir, baseDir, skills) {
1489
- const skillMdPath = path.join(currentDir, 'SKILL.md');
1490
-
1491
- if (fs.existsSync(skillMdPath)) {
1492
- const directory = currentDir === baseDir
1493
- ? path.basename(currentDir)
1494
- : path.relative(baseDir, currentDir);
1495
-
1496
- try {
1497
- const content = fs.readFileSync(skillMdPath, 'utf-8');
1498
- const metadata = this.parseSkillMd(content);
1499
-
1500
- skills.push({
1501
- key: `installed:${directory}`,
1502
- name: metadata.name || directory,
1503
- description: metadata.description || '',
1504
- directory,
1505
- installed: true,
1506
- isLocal: false,
1507
- readmeUrl: null,
1508
- repoOwner: null,
1509
- repoName: null,
1510
- repoBranch: null,
1511
- license: metadata.license,
1512
- source: 'native-installed'
1513
- });
1514
- } catch (err) {
1515
- console.warn(`[SkillService] Parse installed skill ${directory} error:`, err.message);
1516
- }
1517
-
1518
- return;
1519
- }
1520
-
1521
- try {
1522
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
1523
- for (const entry of entries) {
1524
- if (entry.isDirectory() && !entry.name.startsWith('.')) {
1525
- this.scanInstalledDir(path.join(currentDir, entry.name), baseDir, skills);
1526
- }
1527
- }
1528
- } catch (err) {
1529
- // 忽略读取错误
1530
- }
1531
- }
1532
-
1533
1413
  /**
1534
1414
  * 去重技能列表
1535
1415
  */
@@ -2041,8 +1921,11 @@ ${content}
2041
1921
  * 获取技能详情(完整内容)
2042
1922
  */
2043
1923
  async getSkillDetail(directory, repoHint = null, fullDirectoryHint = '') {
2044
- const parseLocalSkillDetail = (skillMdPath, { installed, isLocal }) => {
2045
- const content = fs.readFileSync(skillMdPath, 'utf-8');
1924
+ // 先检查本地是否安装
1925
+ const localPath = path.join(this.installDir, directory, 'SKILL.md');
1926
+
1927
+ if (fs.existsSync(localPath)) {
1928
+ const content = fs.readFileSync(localPath, 'utf-8');
2046
1929
  const metadata = this.parseSkillMd(content);
2047
1930
 
2048
1931
  // 提取正文内容(去除 frontmatter)
@@ -2055,29 +1938,9 @@ ${content}
2055
1938
  description: metadata.description || '',
2056
1939
  content: body,
2057
1940
  fullContent: content,
2058
- installed,
2059
- isLocal,
1941
+ installed: true,
2060
1942
  source: 'local'
2061
1943
  };
2062
- };
2063
-
2064
- // 先检查本地是否安装
2065
- const localPath = path.join(this.installDir, directory, 'SKILL.md');
2066
-
2067
- if (fs.existsSync(localPath)) {
2068
- const managedSkillMdPath = path.join(this.storageDir, directory, 'SKILL.md');
2069
- return parseLocalSkillDetail(localPath, {
2070
- installed: true,
2071
- isLocal: fs.existsSync(managedSkillMdPath)
2072
- });
2073
- }
2074
-
2075
- const managedLocalPath = path.join(this.storageDir, directory, 'SKILL.md');
2076
- if (fs.existsSync(managedLocalPath)) {
2077
- return parseLocalSkillDetail(managedLocalPath, {
2078
- installed: false,
2079
- isLocal: true
2080
- });
2081
1944
  }
2082
1945
 
2083
1946
  const parseRemoteSkillContent = (content, repo, fullDirectory = '') => {
@@ -2225,7 +2088,7 @@ ${content}
2225
2088
  */
2226
2089
  getInstalledSkills() {
2227
2090
  const skills = [];
2228
- this.scanInstalledDir(this.installDir, this.installDir, skills);
2091
+ this.scanLocalDir(this.installDir, this.installDir, skills);
2229
2092
  return skills;
2230
2093
  }
2231
2094
  }