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.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -25
- package/bin/ctx.js +1 -6
- package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-Bw-0r4Ys.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
- package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
- package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
- package/dist/web/assets/SessionList-D12muXvx.css +1 -0
- package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.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-CTeg1xG-.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-channels.js +4 -0
- package/src/server/api/codex-proxy.js +45 -2
- package/src/server/api/codex-sessions.js +89 -12
- 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 +83 -10
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +181 -27
- 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 +88 -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 +59 -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 +6 -16
- 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 +171 -183
- 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-Bjt6mrsV.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
- package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/index-Duc7QP4e.js +0 -2
- 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 -58
- 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,34 +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();
|
|
22
|
-
|
|
23
|
-
function getProjectsStatsCacheKey(config) {
|
|
24
|
-
return `${CacheKeys.PROJECTS}${config?.projectsDir || '__default__'}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getProjectSessionsCacheKey(projectName) {
|
|
28
|
-
return `${CacheKeys.SESSIONS}${projectName}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function invalidateProjectStatsCache(config) {
|
|
32
|
-
invalidateProjectsCache(config);
|
|
33
|
-
globalCache.delete(getProjectsStatsCacheKey(config));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function invalidateProjectSessionCache(projectName) {
|
|
37
|
-
if (!projectName) return;
|
|
38
|
-
globalCache.delete(getProjectSessionsCacheKey(projectName));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function invalidateProjectCaches(config, projectName) {
|
|
42
|
-
invalidateProjectStatsCache(config);
|
|
43
|
-
invalidateProjectSessionCache(projectName);
|
|
44
|
-
}
|
|
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');
|
|
45
19
|
|
|
46
20
|
// Base directory for cc-tool data
|
|
47
21
|
function getCcToolDir() {
|
|
@@ -112,96 +86,123 @@ function saveForkRelations(relations) {
|
|
|
112
86
|
fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
|
|
113
87
|
}
|
|
114
88
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
89
|
+
function invalidateSessionResultCache(projectName) {
|
|
90
|
+
if (!projectName) return;
|
|
91
|
+
globalCache.delete(`${CacheKeys.SESSIONS}${projectName}`);
|
|
92
|
+
}
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
94
|
+
function getClaudeForkMessageText(content) {
|
|
95
|
+
if (typeof content === 'string') {
|
|
96
|
+
return content === 'Warmup' ? '' : content;
|
|
121
97
|
}
|
|
122
98
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
if (!Array.isArray(content)) {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
}
|
|
128
111
|
|
|
129
|
-
|
|
130
|
-
return `${process.platform}:${encodedName}`;
|
|
112
|
+
return parts.join('\n\n').trim();
|
|
131
113
|
}
|
|
132
114
|
|
|
133
|
-
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');
|
|
134
118
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
119
|
+
lines: content.split(/\r?\n/),
|
|
120
|
+
eol,
|
|
121
|
+
hasTrailingEol
|
|
137
122
|
};
|
|
138
123
|
}
|
|
139
124
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
125
|
+
function joinTextPreserveEol(lines, eol, hasTrailingEol) {
|
|
126
|
+
const text = lines.join(eol);
|
|
127
|
+
return hasTrailingEol ? `${text}${eol}` : text;
|
|
128
|
+
}
|
|
146
129
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
130
|
+
function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber) {
|
|
131
|
+
if (!Number.isInteger(afterUserMessageNumber) || afterUserMessageNumber <= 0) {
|
|
132
|
+
return content;
|
|
150
133
|
}
|
|
151
134
|
|
|
152
|
-
|
|
153
|
-
|
|
135
|
+
const { lines, eol, hasTrailingEol } = splitTextPreserveEol(content);
|
|
136
|
+
const keptLines = [];
|
|
137
|
+
let matchedUserMessages = 0;
|
|
138
|
+
let targetUserReached = false;
|
|
154
139
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
if (!line.trim()) {
|
|
142
|
+
keptLines.push(line);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let json;
|
|
147
|
+
try {
|
|
148
|
+
json = JSON.parse(line);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
keptLines.push(line);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
162
153
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
break;
|
|
154
|
+
if (json.type !== 'user') {
|
|
155
|
+
keptLines.push(line);
|
|
156
|
+
continue;
|
|
167
157
|
}
|
|
168
|
-
|
|
158
|
+
|
|
159
|
+
const userText = getClaudeForkMessageText(json.message?.content);
|
|
160
|
+
if (!userText) {
|
|
161
|
+
keptLines.push(line);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (targetUserReached) {
|
|
166
|
+
return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
matchedUserMessages += 1;
|
|
170
|
+
keptLines.push(line);
|
|
171
|
+
if (matchedUserMessages >= afterUserMessageNumber) {
|
|
172
|
+
targetUserReached = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (targetUserReached) {
|
|
177
|
+
return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
169
178
|
}
|
|
179
|
+
|
|
180
|
+
throw new Error(`afterUserMessageNumber ${afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
|
|
170
181
|
}
|
|
171
182
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
// Get all projects with stats (async version)
|
|
184
|
+
async function getProjects(config) {
|
|
185
|
+
const projectsDir = config.projectsDir;
|
|
186
|
+
|
|
187
|
+
if (!fs.existsSync(projectsDir)) {
|
|
188
|
+
return [];
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
|
|
191
|
+
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
192
|
+
return entries
|
|
193
|
+
.filter(entry => entry.isDirectory())
|
|
194
|
+
.map(entry => entry.name);
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
// Parse real project path from encoded name
|
|
182
198
|
// macOS/Linux: "-Users-lilithgames-work-project" -> "/Users/lilithgames/work/project"
|
|
183
199
|
// Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
|
|
184
200
|
function parseRealProjectPath(encodedName) {
|
|
185
|
-
const normalizedEncodedName = String(encodedName || '').trim();
|
|
186
|
-
const cached = getCachedProjectPathResolution(normalizedEncodedName);
|
|
187
|
-
if (cached) {
|
|
188
|
-
return cached;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
201
|
const isWindows = process.platform === 'win32';
|
|
192
|
-
const fallbackFromSessions = tryResolvePathFromSessions(
|
|
193
|
-
|
|
194
|
-
if (fallbackFromSessions?.fullPath) {
|
|
195
|
-
const resolved = {
|
|
196
|
-
fullPath: fallbackFromSessions.fullPath,
|
|
197
|
-
projectName: fallbackFromSessions.projectName || path.basename(fallbackFromSessions.fullPath) || normalizedEncodedName
|
|
198
|
-
};
|
|
199
|
-
setCachedProjectPathResolution(normalizedEncodedName, resolved);
|
|
200
|
-
return resolved;
|
|
201
|
-
}
|
|
202
|
+
const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
|
|
202
203
|
|
|
203
204
|
// Detect Windows drive letter (e.g., "C--Users-admin")
|
|
204
|
-
const windowsDriveMatch =
|
|
205
|
+
const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
|
|
205
206
|
|
|
206
207
|
if (isWindows && windowsDriveMatch) {
|
|
207
208
|
// Windows path with drive letter
|
|
@@ -260,15 +261,13 @@ function parseRealProjectPath(encodedName) {
|
|
|
260
261
|
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
265
|
-
projectName: realSegments[realSegments.length - 1] ||
|
|
264
|
+
return {
|
|
265
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
266
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
266
267
|
};
|
|
267
|
-
setCachedProjectPathResolution(normalizedEncodedName, resolved);
|
|
268
|
-
return resolved;
|
|
269
268
|
} else {
|
|
270
269
|
// Unix-like path (macOS/Linux) or fallback
|
|
271
|
-
const pathStr =
|
|
270
|
+
const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
|
|
272
271
|
const segments = pathStr.split('/').filter(s => s);
|
|
273
272
|
|
|
274
273
|
// Build path from left to right, checking existence
|
|
@@ -320,12 +319,10 @@ function parseRealProjectPath(encodedName) {
|
|
|
320
319
|
currentPath = '/' + realSegments.join('/');
|
|
321
320
|
}
|
|
322
321
|
|
|
323
|
-
|
|
324
|
-
fullPath: validateProjectPath(currentPath) || pathStr,
|
|
325
|
-
projectName: realSegments[realSegments.length - 1] ||
|
|
322
|
+
return {
|
|
323
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
|
|
324
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
326
325
|
};
|
|
327
|
-
setCachedProjectPathResolution(normalizedEncodedName, resolved);
|
|
328
|
-
return resolved;
|
|
329
326
|
}
|
|
330
327
|
}
|
|
331
328
|
|
|
@@ -336,41 +333,6 @@ function validateProjectPath(candidatePath) {
|
|
|
336
333
|
return null;
|
|
337
334
|
}
|
|
338
335
|
|
|
339
|
-
function scanSessionsDir(sessionsDir) {
|
|
340
|
-
if (!sessionsDir || !fs.existsSync(sessionsDir)) {
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return fs.readdirSync(sessionsDir)
|
|
345
|
-
.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'))
|
|
346
|
-
.map((file) => {
|
|
347
|
-
const filePath = path.join(sessionsDir, file);
|
|
348
|
-
const stats = fs.statSync(filePath);
|
|
349
|
-
return {
|
|
350
|
-
sessionId: file.replace('.jsonl', ''),
|
|
351
|
-
filePath,
|
|
352
|
-
size: stats.size,
|
|
353
|
-
mtime: stats.mtime
|
|
354
|
-
};
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function getProjectSessionsDir(config, projectName) {
|
|
359
|
-
return path.join(config.projectsDir, projectName);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function getProjectSessionFiles(config, projectName) {
|
|
363
|
-
return scanSessionsDir(getProjectSessionsDir(config, projectName)).sort((a, b) => b.mtime - a.mtime);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function resolveSessionFile(config, projectName, sessionId) {
|
|
367
|
-
const sessionFile = path.join(getProjectSessionsDir(config, projectName), `${sessionId}.jsonl`);
|
|
368
|
-
if (fs.existsSync(sessionFile)) {
|
|
369
|
-
return sessionFile;
|
|
370
|
-
}
|
|
371
|
-
return '';
|
|
372
|
-
}
|
|
373
|
-
|
|
374
336
|
function tryResolvePathFromSessions(encodedName) {
|
|
375
337
|
try {
|
|
376
338
|
const projectDir = path.join(CLAUDE_PROJECTS_DIR, encodedName);
|
|
@@ -423,7 +385,7 @@ function extractCwdFromSessionHeader(sessionFile) {
|
|
|
423
385
|
async function getProjectsWithStats(config, options = {}) {
|
|
424
386
|
if (!options.force) {
|
|
425
387
|
// Check enhanced cache first
|
|
426
|
-
const cacheKey =
|
|
388
|
+
const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
|
|
427
389
|
const enhancedCached = globalCache.get(cacheKey);
|
|
428
390
|
if (enhancedCached) {
|
|
429
391
|
return enhancedCached;
|
|
@@ -444,7 +406,7 @@ async function getProjectsWithStats(config, options = {}) {
|
|
|
444
406
|
return [];
|
|
445
407
|
}
|
|
446
408
|
setCachedProjects(config, data);
|
|
447
|
-
globalCache.set(
|
|
409
|
+
globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
|
|
448
410
|
return data;
|
|
449
411
|
} catch (err) {
|
|
450
412
|
console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
|
|
@@ -476,19 +438,30 @@ async function buildProjectsWithStats(config) {
|
|
|
476
438
|
let lastUsed = null;
|
|
477
439
|
|
|
478
440
|
try {
|
|
441
|
+
const files = await fs.promises.readdir(projectPath);
|
|
442
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
443
|
+
|
|
444
|
+
// Filter: only count sessions that have actual messages (in parallel)
|
|
479
445
|
const sessionChecks = await Promise.all(
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const hasMessages = await hasActualMessages(
|
|
483
|
-
return hasMessages ?
|
|
446
|
+
jsonlFiles.map(async (f) => {
|
|
447
|
+
const filePath = path.join(projectPath, f);
|
|
448
|
+
const hasMessages = await hasActualMessages(filePath);
|
|
449
|
+
return hasMessages ? f : null;
|
|
484
450
|
})
|
|
485
451
|
);
|
|
486
452
|
|
|
487
|
-
const
|
|
488
|
-
sessionCount =
|
|
453
|
+
const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
|
|
454
|
+
sessionCount = sessionFilesWithMessages.length;
|
|
489
455
|
|
|
490
|
-
|
|
491
|
-
|
|
456
|
+
// Find most recent session (only from sessions with messages)
|
|
457
|
+
if (sessionFilesWithMessages.length > 0) {
|
|
458
|
+
const statPromises = sessionFilesWithMessages.map(async (f) => {
|
|
459
|
+
const filePath = path.join(projectPath, f);
|
|
460
|
+
const stat = await fs.promises.stat(filePath);
|
|
461
|
+
return stat.mtime.getTime();
|
|
462
|
+
});
|
|
463
|
+
const stats = await Promise.all(statPromises);
|
|
464
|
+
lastUsed = Math.max(...stats);
|
|
492
465
|
}
|
|
493
466
|
} catch (err) {
|
|
494
467
|
// Ignore errors
|
|
@@ -523,8 +496,10 @@ function getProjectAndSessionCounts(config) {
|
|
|
523
496
|
return;
|
|
524
497
|
}
|
|
525
498
|
projectCount += 1;
|
|
499
|
+
const projectPath = path.join(projectsDir, entry.name);
|
|
526
500
|
try {
|
|
527
|
-
|
|
501
|
+
const files = fs.readdirSync(projectPath);
|
|
502
|
+
sessionCount += files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-')).length;
|
|
528
503
|
} catch (err) {
|
|
529
504
|
// 忽略单个项目的读取错误
|
|
530
505
|
}
|
|
@@ -534,9 +509,9 @@ function getProjectAndSessionCounts(config) {
|
|
|
534
509
|
}
|
|
535
510
|
|
|
536
511
|
// Check if a session file has actual messages (async with enhanced caching)
|
|
537
|
-
async function hasActualMessages(filePath
|
|
512
|
+
async function hasActualMessages(filePath) {
|
|
538
513
|
try {
|
|
539
|
-
const stats =
|
|
514
|
+
const stats = await fs.promises.stat(filePath);
|
|
540
515
|
|
|
541
516
|
// Check enhanced cache first
|
|
542
517
|
const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
|
|
@@ -627,14 +602,14 @@ function scanSessionFileForMessagesAsync(filePath) {
|
|
|
627
602
|
// Get sessions for a project - async version
|
|
628
603
|
async function getSessionsForProject(config, projectName) {
|
|
629
604
|
// Check cache first
|
|
630
|
-
const cacheKey =
|
|
605
|
+
const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
|
|
631
606
|
const cached = globalCache.get(cacheKey);
|
|
632
607
|
if (cached) {
|
|
633
608
|
return cached;
|
|
634
609
|
}
|
|
635
610
|
|
|
636
611
|
const projectConfig = { ...config, currentProject: projectName };
|
|
637
|
-
const sessions =
|
|
612
|
+
const sessions = getAllSessions(projectConfig);
|
|
638
613
|
const forkRelations = getForkRelations();
|
|
639
614
|
const savedOrder = getSessionOrder(projectName);
|
|
640
615
|
|
|
@@ -679,10 +654,8 @@ async function getSessionsForProject(config, projectName) {
|
|
|
679
654
|
}
|
|
680
655
|
|
|
681
656
|
// Add remaining sessions (new ones not in saved order)
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
});
|
|
685
|
-
orderedSessions = [...newSessions, ...ordered];
|
|
657
|
+
ordered.push(...sessionMap.values());
|
|
658
|
+
orderedSessions = ordered;
|
|
686
659
|
}
|
|
687
660
|
|
|
688
661
|
const result = {
|
|
@@ -697,31 +670,38 @@ async function getSessionsForProject(config, projectName) {
|
|
|
697
670
|
|
|
698
671
|
// Delete a session
|
|
699
672
|
function deleteSession(config, projectName, sessionId) {
|
|
700
|
-
const
|
|
673
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
674
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
701
675
|
|
|
702
|
-
if (!
|
|
676
|
+
if (!fs.existsSync(sessionFile)) {
|
|
703
677
|
throw new Error('Session not found');
|
|
704
678
|
}
|
|
705
679
|
|
|
706
680
|
fs.unlinkSync(sessionFile);
|
|
707
|
-
|
|
681
|
+
invalidateProjectsCache(config);
|
|
682
|
+
invalidateSessionResultCache(projectName);
|
|
708
683
|
return { success: true };
|
|
709
684
|
}
|
|
710
685
|
|
|
711
686
|
// Fork a session
|
|
712
|
-
function forkSession(config, projectName, sessionId) {
|
|
713
|
-
const
|
|
687
|
+
function forkSession(config, projectName, sessionId, options = {}) {
|
|
688
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
689
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
714
690
|
|
|
715
|
-
if (!
|
|
691
|
+
if (!fs.existsSync(sessionFile)) {
|
|
716
692
|
throw new Error('Session not found');
|
|
717
693
|
}
|
|
718
694
|
|
|
719
695
|
// Read the original session
|
|
720
|
-
const
|
|
696
|
+
const originalContent = fs.readFileSync(sessionFile, 'utf8');
|
|
697
|
+
const content = sliceClaudeSessionContentByUserMessage(
|
|
698
|
+
originalContent,
|
|
699
|
+
options.afterUserMessageNumber
|
|
700
|
+
);
|
|
721
701
|
|
|
722
702
|
// Generate new session ID (UUID v4)
|
|
723
703
|
const newSessionId = crypto.randomUUID();
|
|
724
|
-
const newSessionFile = path.join(
|
|
704
|
+
const newSessionFile = path.join(projectDir, newSessionId + '.jsonl');
|
|
725
705
|
|
|
726
706
|
// Write to new file
|
|
727
707
|
fs.writeFileSync(newSessionFile, content, 'utf8');
|
|
@@ -730,15 +710,19 @@ function forkSession(config, projectName, sessionId) {
|
|
|
730
710
|
const forkRelations = getForkRelations();
|
|
731
711
|
forkRelations[newSessionId] = sessionId;
|
|
732
712
|
saveForkRelations(forkRelations);
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
saveSessionOrder(projectName, [newSessionId, ...savedOrder.filter(id => id !== newSessionId)]);
|
|
713
|
+
if (options.alias) {
|
|
714
|
+
const { setAlias } = require('./alias');
|
|
715
|
+
setAlias(newSessionId, options.alias);
|
|
737
716
|
}
|
|
717
|
+
invalidateProjectsCache(config);
|
|
718
|
+
invalidateSessionResultCache(projectName);
|
|
738
719
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
720
|
+
return {
|
|
721
|
+
newSessionId,
|
|
722
|
+
forkedFrom: sessionId,
|
|
723
|
+
alias: options.alias || null,
|
|
724
|
+
afterUserMessageNumber: options.afterUserMessageNumber || null
|
|
725
|
+
};
|
|
742
726
|
}
|
|
743
727
|
|
|
744
728
|
// Get session order for a project
|
|
@@ -778,19 +762,19 @@ function saveSessionOrder(projectName, order) {
|
|
|
778
762
|
// Update order for this project
|
|
779
763
|
allOrders[projectName] = order;
|
|
780
764
|
fs.writeFileSync(orderFile, JSON.stringify(allOrders, null, 2), 'utf8');
|
|
781
|
-
invalidateProjectSessionCache(projectName);
|
|
782
765
|
}
|
|
783
766
|
|
|
784
767
|
// Delete a project (remove the entire project directory)
|
|
785
768
|
function deleteProject(config, projectName) {
|
|
786
769
|
const projectDir = path.join(config.projectsDir, projectName);
|
|
787
|
-
const existed = fs.existsSync(projectDir);
|
|
788
770
|
|
|
789
|
-
if (
|
|
790
|
-
|
|
791
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
771
|
+
if (!fs.existsSync(projectDir)) {
|
|
772
|
+
throw new Error('Project not found');
|
|
792
773
|
}
|
|
793
774
|
|
|
775
|
+
// Recursively delete the directory
|
|
776
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
777
|
+
|
|
794
778
|
// Remove from order file if exists
|
|
795
779
|
const order = getProjectOrder(config);
|
|
796
780
|
const newOrder = order.filter(name => name !== projectName);
|
|
@@ -798,22 +782,26 @@ function deleteProject(config, projectName) {
|
|
|
798
782
|
saveProjectOrder(config, newOrder);
|
|
799
783
|
}
|
|
800
784
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
success: true,
|
|
805
|
-
alreadyDeleted: !existed
|
|
806
|
-
};
|
|
785
|
+
invalidateProjectsCache(config);
|
|
786
|
+
return { success: true };
|
|
807
787
|
}
|
|
808
788
|
|
|
809
789
|
// Search sessions for keyword
|
|
810
790
|
function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
791
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
792
|
+
|
|
793
|
+
if (!fs.existsSync(projectDir)) {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
|
|
811
797
|
const results = [];
|
|
798
|
+
const files = fs.readdirSync(projectDir);
|
|
799
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
812
800
|
const aliases = loadAliases();
|
|
813
801
|
|
|
814
|
-
for (const
|
|
815
|
-
const sessionId =
|
|
816
|
-
const filePath =
|
|
802
|
+
for (const file of jsonlFiles) {
|
|
803
|
+
const sessionId = file.replace('.jsonl', '');
|
|
804
|
+
const filePath = path.join(projectDir, file);
|
|
817
805
|
|
|
818
806
|
// Skip sessions with no actual messages
|
|
819
807
|
if (!hasActualMessages(filePath)) {
|
|
@@ -887,7 +875,7 @@ async function getRecentSessions(config, limit = 5) {
|
|
|
887
875
|
// Collect all sessions from all projects
|
|
888
876
|
projects.forEach(projectName => {
|
|
889
877
|
const projectConfig = { ...config, currentProject: projectName };
|
|
890
|
-
const sessions =
|
|
878
|
+
const sessions = getAllSessions(projectConfig);
|
|
891
879
|
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
892
880
|
|
|
893
881
|
sessions.forEach(session => {
|