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.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -25
- package/bin/ctx.js +1 -6
- package/dist/web/assets/{Analytics-BzoNzfbi.js → Analytics-DPb1zErn.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-O4ikBt1o.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
- package/dist/web/assets/{Home-BQjsnblU.js → Home-BKYHKSax.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{PluginManager-DS_DJnVc.js → PluginManager-jgeQOE24.js} +1 -1
- package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
- package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
- package/dist/web/assets/{SkillManager-DpNE02r0.js → SkillManager-CtEz5gGp.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DMY7_SHh.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
- package/dist/web/assets/icons-lKCTySl7.js +1 -0
- package/dist/web/assets/index-BS7aplYp.css +1 -0
- package/dist/web/assets/index-BnyNm74G.js +2 -0
- package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
- package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
- package/dist/web/index.html +8 -8
- package/package.json +1 -2
- package/src/commands/daemon.js +5 -33
- package/src/commands/export-config.js +6 -6
- package/src/commands/stats.js +4 -41
- package/src/commands/ui.js +3 -12
- package/src/config/channel-preset-websites.js +114 -0
- package/src/config/default.js +9 -5
- package/src/config/loader.js +2 -2
- package/src/config/model-metadata.json +16 -20
- package/src/config/paths.js +33 -166
- package/src/index.js +34 -125
- package/src/server/api/agents.js +2 -52
- package/src/server/api/codex-proxy.js +4 -2
- package/src/server/api/codex-sessions.js +88 -9
- package/src/server/api/commands.js +2 -38
- package/src/server/api/dashboard.js +1 -25
- package/src/server/api/gemini-proxy.js +4 -2
- package/src/server/api/gemini-sessions.js +86 -9
- package/src/server/api/hooks.js +1 -17
- package/src/server/api/opencode-proxy.js +7 -2
- package/src/server/api/opencode-sessions.js +82 -7
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +180 -24
- package/src/server/api/workspaces.js +0 -14
- package/src/server/index.js +9 -31
- package/src/server/services/agents-service.js +62 -269
- package/src/server/services/base/base-channel-service.js +3 -1
- package/src/server/services/channels.js +5 -0
- package/src/server/services/codex-channels.js +2 -0
- package/src/server/services/codex-sessions.js +78 -108
- package/src/server/services/commands-service.js +81 -281
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +9 -8
- package/src/server/services/config-sync-manager.js +41 -61
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +17 -6
- package/src/server/services/gemini-config.js +4 -3
- package/src/server/services/gemini-sessions.js +51 -26
- package/src/server/services/gemini-settings-manager.js +3 -2
- package/src/server/services/mcp-service.js +14 -9
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/network-access.js +2 -61
- package/src/server/services/notification-hooks.js +19 -184
- package/src/server/services/opencode-channels.js +9 -0
- package/src/server/services/opencode-sessions.js +4 -4
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +62 -996
- package/src/server/services/prompts-service.js +9 -5
- package/src/server/services/sessions.js +128 -99
- package/src/server/services/skill-service.js +18 -155
- package/src/server/websocket-server.js +4 -35
- package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
- package/dist/web/assets/Home-qzk118Of.css +0 -1
- package/dist/web/assets/ProjectList-CqYDtsHx.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-CfPtcq6Y.css +0 -1
- package/dist/web/assets/SessionList-DMlLtMCz.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Clf0l3wc.js +0 -2
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
- package/docs/Caddyfile.example +0 -19
- package/docs/reverse-proxy-https.md +0 -57
- package/src/server/services/https-cert.js +0 -171
- package/src/server/services/session-launch-command.js +0 -81
- 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
|
|
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 =
|
|
16
|
-
const CODEX_PROMPT_PATH =
|
|
17
|
-
const GEMINI_PROMPT_PATH =
|
|
18
|
-
const OPENCODE_PROMPT_PATH =
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
function invalidateSessionResultCache(projectName) {
|
|
90
|
+
if (!projectName) return;
|
|
91
|
+
globalCache.delete(`${CacheKeys.SESSIONS}${projectName}`);
|
|
92
|
+
}
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
function getClaudeForkMessageText(content) {
|
|
95
|
+
if (typeof content === 'string') {
|
|
96
|
+
return content === 'Warmup' ? '' : content;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.map(entry => entry.name);
|
|
104
|
-
}
|
|
99
|
+
if (!Array.isArray(content)) {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
119
|
+
lines: content.split(/\r?\n/),
|
|
120
|
+
eol,
|
|
121
|
+
hasTrailingEol
|
|
114
122
|
};
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
function
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
return
|
|
130
|
+
function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber) {
|
|
131
|
+
if (!Number.isInteger(afterUserMessageNumber) || afterUserMessageNumber <= 0) {
|
|
132
|
+
return content;
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
const { lines, eol, hasTrailingEol } = splitTextPreserveEol(content);
|
|
136
|
+
const keptLines = [];
|
|
137
|
+
let matchedUserMessages = 0;
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
241
|
-
fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
242
|
-
projectName: realSegments[realSegments.length - 1] ||
|
|
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 =
|
|
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
|
-
|
|
301
|
-
fullPath: validateProjectPath(currentPath) || pathStr,
|
|
302
|
-
projectName: realSegments[realSegments.length - 1] ||
|
|
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 (
|
|
426
|
-
const filePath = path.join(projectPath,
|
|
427
|
-
const
|
|
428
|
-
|
|
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
|
|
434
|
-
sessionCount =
|
|
443
|
+
const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
|
|
444
|
+
sessionCount = sessionFilesWithMessages.length;
|
|
435
445
|
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
502
|
+
async function hasActualMessages(filePath) {
|
|
486
503
|
try {
|
|
487
|
-
const stats =
|
|
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
|
|
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 {
|
|
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 (
|
|
731
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 = []
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2045
|
-
|
|
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.
|
|
2091
|
+
this.scanLocalDir(this.installDir, this.installDir, skills);
|
|
2229
2092
|
return skills;
|
|
2230
2093
|
}
|
|
2231
2094
|
}
|