coding-tool-x 3.5.9 → 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-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
- package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{PluginManager-9B_brLWT.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-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-ov1KgRXR.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 +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 +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 +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 +161 -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,113 @@ 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;
|
|
154
138
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
154
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
if (json.type !== 'user') {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
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);
|
|
167
167
|
}
|
|
168
|
-
projectPathResolutionCache.delete(oldestKey);
|
|
169
168
|
}
|
|
169
|
+
|
|
170
|
+
throw new Error(`afterUserMessageNumber ${afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 [];
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
|
|
181
|
+
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
182
|
+
return entries
|
|
183
|
+
.filter(entry => entry.isDirectory())
|
|
184
|
+
.map(entry => entry.name);
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
// Parse real project path from encoded name
|
|
182
188
|
// macOS/Linux: "-Users-lilithgames-work-project" -> "/Users/lilithgames/work/project"
|
|
183
189
|
// Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
|
|
184
190
|
function parseRealProjectPath(encodedName) {
|
|
185
|
-
const normalizedEncodedName = String(encodedName || '').trim();
|
|
186
|
-
const cached = getCachedProjectPathResolution(normalizedEncodedName);
|
|
187
|
-
if (cached) {
|
|
188
|
-
return cached;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
191
|
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
|
-
}
|
|
192
|
+
const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
|
|
202
193
|
|
|
203
194
|
// Detect Windows drive letter (e.g., "C--Users-admin")
|
|
204
|
-
const windowsDriveMatch =
|
|
195
|
+
const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
|
|
205
196
|
|
|
206
197
|
if (isWindows && windowsDriveMatch) {
|
|
207
198
|
// Windows path with drive letter
|
|
@@ -260,15 +251,13 @@ function parseRealProjectPath(encodedName) {
|
|
|
260
251
|
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
261
252
|
}
|
|
262
253
|
|
|
263
|
-
|
|
264
|
-
fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
265
|
-
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
|
|
266
257
|
};
|
|
267
|
-
setCachedProjectPathResolution(normalizedEncodedName, resolved);
|
|
268
|
-
return resolved;
|
|
269
258
|
} else {
|
|
270
259
|
// Unix-like path (macOS/Linux) or fallback
|
|
271
|
-
const pathStr =
|
|
260
|
+
const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
|
|
272
261
|
const segments = pathStr.split('/').filter(s => s);
|
|
273
262
|
|
|
274
263
|
// Build path from left to right, checking existence
|
|
@@ -320,12 +309,10 @@ function parseRealProjectPath(encodedName) {
|
|
|
320
309
|
currentPath = '/' + realSegments.join('/');
|
|
321
310
|
}
|
|
322
311
|
|
|
323
|
-
|
|
324
|
-
fullPath: validateProjectPath(currentPath) || pathStr,
|
|
325
|
-
projectName: realSegments[realSegments.length - 1] ||
|
|
312
|
+
return {
|
|
313
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
|
|
314
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
326
315
|
};
|
|
327
|
-
setCachedProjectPathResolution(normalizedEncodedName, resolved);
|
|
328
|
-
return resolved;
|
|
329
316
|
}
|
|
330
317
|
}
|
|
331
318
|
|
|
@@ -336,41 +323,6 @@ function validateProjectPath(candidatePath) {
|
|
|
336
323
|
return null;
|
|
337
324
|
}
|
|
338
325
|
|
|
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
326
|
function tryResolvePathFromSessions(encodedName) {
|
|
375
327
|
try {
|
|
376
328
|
const projectDir = path.join(CLAUDE_PROJECTS_DIR, encodedName);
|
|
@@ -423,7 +375,7 @@ function extractCwdFromSessionHeader(sessionFile) {
|
|
|
423
375
|
async function getProjectsWithStats(config, options = {}) {
|
|
424
376
|
if (!options.force) {
|
|
425
377
|
// Check enhanced cache first
|
|
426
|
-
const cacheKey =
|
|
378
|
+
const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
|
|
427
379
|
const enhancedCached = globalCache.get(cacheKey);
|
|
428
380
|
if (enhancedCached) {
|
|
429
381
|
return enhancedCached;
|
|
@@ -444,7 +396,7 @@ async function getProjectsWithStats(config, options = {}) {
|
|
|
444
396
|
return [];
|
|
445
397
|
}
|
|
446
398
|
setCachedProjects(config, data);
|
|
447
|
-
globalCache.set(
|
|
399
|
+
globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
|
|
448
400
|
return data;
|
|
449
401
|
} catch (err) {
|
|
450
402
|
console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
|
|
@@ -476,19 +428,30 @@ async function buildProjectsWithStats(config) {
|
|
|
476
428
|
let lastUsed = null;
|
|
477
429
|
|
|
478
430
|
try {
|
|
431
|
+
const files = await fs.promises.readdir(projectPath);
|
|
432
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
433
|
+
|
|
434
|
+
// Filter: only count sessions that have actual messages (in parallel)
|
|
479
435
|
const sessionChecks = await Promise.all(
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const hasMessages = await hasActualMessages(
|
|
483
|
-
return hasMessages ?
|
|
436
|
+
jsonlFiles.map(async (f) => {
|
|
437
|
+
const filePath = path.join(projectPath, f);
|
|
438
|
+
const hasMessages = await hasActualMessages(filePath);
|
|
439
|
+
return hasMessages ? f : null;
|
|
484
440
|
})
|
|
485
441
|
);
|
|
486
442
|
|
|
487
|
-
const
|
|
488
|
-
sessionCount =
|
|
443
|
+
const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
|
|
444
|
+
sessionCount = sessionFilesWithMessages.length;
|
|
489
445
|
|
|
490
|
-
|
|
491
|
-
|
|
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);
|
|
492
455
|
}
|
|
493
456
|
} catch (err) {
|
|
494
457
|
// Ignore errors
|
|
@@ -523,8 +486,10 @@ function getProjectAndSessionCounts(config) {
|
|
|
523
486
|
return;
|
|
524
487
|
}
|
|
525
488
|
projectCount += 1;
|
|
489
|
+
const projectPath = path.join(projectsDir, entry.name);
|
|
526
490
|
try {
|
|
527
|
-
|
|
491
|
+
const files = fs.readdirSync(projectPath);
|
|
492
|
+
sessionCount += files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-')).length;
|
|
528
493
|
} catch (err) {
|
|
529
494
|
// 忽略单个项目的读取错误
|
|
530
495
|
}
|
|
@@ -534,9 +499,9 @@ function getProjectAndSessionCounts(config) {
|
|
|
534
499
|
}
|
|
535
500
|
|
|
536
501
|
// Check if a session file has actual messages (async with enhanced caching)
|
|
537
|
-
async function hasActualMessages(filePath
|
|
502
|
+
async function hasActualMessages(filePath) {
|
|
538
503
|
try {
|
|
539
|
-
const stats =
|
|
504
|
+
const stats = await fs.promises.stat(filePath);
|
|
540
505
|
|
|
541
506
|
// Check enhanced cache first
|
|
542
507
|
const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
|
|
@@ -627,14 +592,14 @@ function scanSessionFileForMessagesAsync(filePath) {
|
|
|
627
592
|
// Get sessions for a project - async version
|
|
628
593
|
async function getSessionsForProject(config, projectName) {
|
|
629
594
|
// Check cache first
|
|
630
|
-
const cacheKey =
|
|
595
|
+
const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
|
|
631
596
|
const cached = globalCache.get(cacheKey);
|
|
632
597
|
if (cached) {
|
|
633
598
|
return cached;
|
|
634
599
|
}
|
|
635
600
|
|
|
636
601
|
const projectConfig = { ...config, currentProject: projectName };
|
|
637
|
-
const sessions =
|
|
602
|
+
const sessions = getAllSessions(projectConfig);
|
|
638
603
|
const forkRelations = getForkRelations();
|
|
639
604
|
const savedOrder = getSessionOrder(projectName);
|
|
640
605
|
|
|
@@ -679,10 +644,8 @@ async function getSessionsForProject(config, projectName) {
|
|
|
679
644
|
}
|
|
680
645
|
|
|
681
646
|
// Add remaining sessions (new ones not in saved order)
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
});
|
|
685
|
-
orderedSessions = [...newSessions, ...ordered];
|
|
647
|
+
ordered.push(...sessionMap.values());
|
|
648
|
+
orderedSessions = ordered;
|
|
686
649
|
}
|
|
687
650
|
|
|
688
651
|
const result = {
|
|
@@ -697,31 +660,38 @@ async function getSessionsForProject(config, projectName) {
|
|
|
697
660
|
|
|
698
661
|
// Delete a session
|
|
699
662
|
function deleteSession(config, projectName, sessionId) {
|
|
700
|
-
const
|
|
663
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
664
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
701
665
|
|
|
702
|
-
if (!
|
|
666
|
+
if (!fs.existsSync(sessionFile)) {
|
|
703
667
|
throw new Error('Session not found');
|
|
704
668
|
}
|
|
705
669
|
|
|
706
670
|
fs.unlinkSync(sessionFile);
|
|
707
|
-
|
|
671
|
+
invalidateProjectsCache(config);
|
|
672
|
+
invalidateSessionResultCache(projectName);
|
|
708
673
|
return { success: true };
|
|
709
674
|
}
|
|
710
675
|
|
|
711
676
|
// Fork a session
|
|
712
|
-
function forkSession(config, projectName, sessionId) {
|
|
713
|
-
const
|
|
677
|
+
function forkSession(config, projectName, sessionId, options = {}) {
|
|
678
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
679
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
714
680
|
|
|
715
|
-
if (!
|
|
681
|
+
if (!fs.existsSync(sessionFile)) {
|
|
716
682
|
throw new Error('Session not found');
|
|
717
683
|
}
|
|
718
684
|
|
|
719
685
|
// Read the original session
|
|
720
|
-
const
|
|
686
|
+
const originalContent = fs.readFileSync(sessionFile, 'utf8');
|
|
687
|
+
const content = sliceClaudeSessionContentByUserMessage(
|
|
688
|
+
originalContent,
|
|
689
|
+
options.afterUserMessageNumber
|
|
690
|
+
);
|
|
721
691
|
|
|
722
692
|
// Generate new session ID (UUID v4)
|
|
723
693
|
const newSessionId = crypto.randomUUID();
|
|
724
|
-
const newSessionFile = path.join(
|
|
694
|
+
const newSessionFile = path.join(projectDir, newSessionId + '.jsonl');
|
|
725
695
|
|
|
726
696
|
// Write to new file
|
|
727
697
|
fs.writeFileSync(newSessionFile, content, 'utf8');
|
|
@@ -730,15 +700,19 @@ function forkSession(config, projectName, sessionId) {
|
|
|
730
700
|
const forkRelations = getForkRelations();
|
|
731
701
|
forkRelations[newSessionId] = sessionId;
|
|
732
702
|
saveForkRelations(forkRelations);
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
saveSessionOrder(projectName, [newSessionId, ...savedOrder.filter(id => id !== newSessionId)]);
|
|
703
|
+
if (options.alias) {
|
|
704
|
+
const { setAlias } = require('./alias');
|
|
705
|
+
setAlias(newSessionId, options.alias);
|
|
737
706
|
}
|
|
707
|
+
invalidateProjectsCache(config);
|
|
708
|
+
invalidateSessionResultCache(projectName);
|
|
738
709
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
710
|
+
return {
|
|
711
|
+
newSessionId,
|
|
712
|
+
forkedFrom: sessionId,
|
|
713
|
+
alias: options.alias || null,
|
|
714
|
+
afterUserMessageNumber: options.afterUserMessageNumber || null
|
|
715
|
+
};
|
|
742
716
|
}
|
|
743
717
|
|
|
744
718
|
// Get session order for a project
|
|
@@ -778,19 +752,19 @@ function saveSessionOrder(projectName, order) {
|
|
|
778
752
|
// Update order for this project
|
|
779
753
|
allOrders[projectName] = order;
|
|
780
754
|
fs.writeFileSync(orderFile, JSON.stringify(allOrders, null, 2), 'utf8');
|
|
781
|
-
invalidateProjectSessionCache(projectName);
|
|
782
755
|
}
|
|
783
756
|
|
|
784
757
|
// Delete a project (remove the entire project directory)
|
|
785
758
|
function deleteProject(config, projectName) {
|
|
786
759
|
const projectDir = path.join(config.projectsDir, projectName);
|
|
787
|
-
const existed = fs.existsSync(projectDir);
|
|
788
760
|
|
|
789
|
-
if (
|
|
790
|
-
|
|
791
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
761
|
+
if (!fs.existsSync(projectDir)) {
|
|
762
|
+
throw new Error('Project not found');
|
|
792
763
|
}
|
|
793
764
|
|
|
765
|
+
// Recursively delete the directory
|
|
766
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
767
|
+
|
|
794
768
|
// Remove from order file if exists
|
|
795
769
|
const order = getProjectOrder(config);
|
|
796
770
|
const newOrder = order.filter(name => name !== projectName);
|
|
@@ -798,22 +772,26 @@ function deleteProject(config, projectName) {
|
|
|
798
772
|
saveProjectOrder(config, newOrder);
|
|
799
773
|
}
|
|
800
774
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
success: true,
|
|
805
|
-
alreadyDeleted: !existed
|
|
806
|
-
};
|
|
775
|
+
invalidateProjectsCache(config);
|
|
776
|
+
return { success: true };
|
|
807
777
|
}
|
|
808
778
|
|
|
809
779
|
// Search sessions for keyword
|
|
810
780
|
function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
781
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
782
|
+
|
|
783
|
+
if (!fs.existsSync(projectDir)) {
|
|
784
|
+
return [];
|
|
785
|
+
}
|
|
786
|
+
|
|
811
787
|
const results = [];
|
|
788
|
+
const files = fs.readdirSync(projectDir);
|
|
789
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
812
790
|
const aliases = loadAliases();
|
|
813
791
|
|
|
814
|
-
for (const
|
|
815
|
-
const sessionId =
|
|
816
|
-
const filePath =
|
|
792
|
+
for (const file of jsonlFiles) {
|
|
793
|
+
const sessionId = file.replace('.jsonl', '');
|
|
794
|
+
const filePath = path.join(projectDir, file);
|
|
817
795
|
|
|
818
796
|
// Skip sessions with no actual messages
|
|
819
797
|
if (!hasActualMessages(filePath)) {
|
|
@@ -887,7 +865,7 @@ async function getRecentSessions(config, limit = 5) {
|
|
|
887
865
|
// Collect all sessions from all projects
|
|
888
866
|
projects.forEach(projectName => {
|
|
889
867
|
const projectConfig = { ...config, currentProject: projectName };
|
|
890
|
-
const sessions =
|
|
868
|
+
const sessions = getAllSessions(projectConfig);
|
|
891
869
|
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
892
870
|
|
|
893
871
|
sessions.forEach(session => {
|