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.
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-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-9B_brLWT.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-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.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 +89 -12
  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 +83 -10
  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 +181 -27
  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 +6 -16
  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 +161 -183
  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-Bjt6mrsV.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  81. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  84. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  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 -58
  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,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
- // Get all projects with stats (async version)
116
- async function getProjects(config) {
117
- const projectsDir = config.projectsDir;
89
+ function invalidateSessionResultCache(projectName) {
90
+ if (!projectName) return;
91
+ globalCache.delete(`${CacheKeys.SESSIONS}${projectName}`);
92
+ }
118
93
 
119
- if (!fs.existsSync(projectsDir)) {
120
- return [];
94
+ function getClaudeForkMessageText(content) {
95
+ if (typeof content === 'string') {
96
+ return content === 'Warmup' ? '' : content;
121
97
  }
122
98
 
123
- const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
124
- return entries
125
- .filter(entry => entry.isDirectory())
126
- .map(entry => entry.name);
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
- function getProjectPathCacheKey(encodedName) {
130
- return `${process.platform}:${encodedName}`;
112
+ return parts.join('\n\n').trim();
131
113
  }
132
114
 
133
- 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');
134
118
  return {
135
- fullPath: value?.fullPath || '',
136
- projectName: value?.projectName || ''
119
+ lines: content.split(/\r?\n/),
120
+ eol,
121
+ hasTrailingEol
137
122
  };
138
123
  }
139
124
 
140
- function getCachedProjectPathResolution(encodedName) {
141
- const cacheKey = getProjectPathCacheKey(encodedName);
142
- const cached = projectPathResolutionCache.get(cacheKey);
143
- if (!cached) {
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
- if (cached.expiresAt <= Date.now()) {
148
- projectPathResolutionCache.delete(cacheKey);
149
- return null;
130
+ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber) {
131
+ if (!Number.isInteger(afterUserMessageNumber) || afterUserMessageNumber <= 0) {
132
+ return content;
150
133
  }
151
134
 
152
- return cloneProjectPathResolution(cached.value);
153
- }
135
+ const { lines, eol, hasTrailingEol } = splitTextPreserveEol(content);
136
+ const keptLines = [];
137
+ let matchedUserMessages = 0;
154
138
 
155
- function setCachedProjectPathResolution(encodedName, value) {
156
- const cacheKey = getProjectPathCacheKey(encodedName);
157
- projectPathResolutionCache.delete(cacheKey);
158
- projectPathResolutionCache.set(cacheKey, {
159
- expiresAt: Date.now() + PROJECT_PATH_CACHE_TTL_MS,
160
- value: cloneProjectPathResolution(value)
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
- while (projectPathResolutionCache.size > MAX_PROJECT_PATH_CACHE_ENTRIES) {
164
- const oldestKey = projectPathResolutionCache.keys().next().value;
165
- if (!oldestKey) {
166
- break;
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
- function clearProjectPathResolutionCache(encodedName) {
173
- if (!encodedName) {
174
- projectPathResolutionCache.clear();
175
- 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 [];
176
179
  }
177
180
 
178
- 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);
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(normalizedEncodedName);
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 = normalizedEncodedName.match(/^([A-Z])--(.+)$/);
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
- const resolved = {
264
- fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
265
- 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
266
257
  };
267
- setCachedProjectPathResolution(normalizedEncodedName, resolved);
268
- return resolved;
269
258
  } else {
270
259
  // Unix-like path (macOS/Linux) or fallback
271
- const pathStr = normalizedEncodedName.replace(/^-/, '/').replace(/-/g, '/');
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
- const resolved = {
324
- fullPath: validateProjectPath(currentPath) || pathStr,
325
- projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
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 = getProjectsStatsCacheKey(config);
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(getProjectsStatsCacheKey(config), data, 300000);
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
- getProjectSessionFiles(config, projectName).map(async (session) => {
481
- const stats = await fs.promises.stat(session.filePath);
482
- const hasMessages = await hasActualMessages(session.filePath, stats);
483
- 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;
484
440
  })
485
441
  );
486
442
 
487
- const activeSessionTimes = sessionChecks.filter((mtimeMs) => Number.isFinite(mtimeMs));
488
- sessionCount = activeSessionTimes.length;
443
+ const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
444
+ sessionCount = sessionFilesWithMessages.length;
489
445
 
490
- if (activeSessionTimes.length > 0) {
491
- 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);
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
- sessionCount += getProjectSessionFiles(config, entry.name).length;
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, statsOverride = null) {
502
+ async function hasActualMessages(filePath) {
538
503
  try {
539
- const stats = statsOverride || await fs.promises.stat(filePath);
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 = getProjectSessionsCacheKey(projectName);
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 = getProjectSessionFiles(projectConfig, projectName);
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
- const newSessions = [...sessionMap.values()].sort((a, b) => {
683
- return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
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 sessionFile = resolveSessionFile(config, projectName, sessionId);
663
+ const projectDir = path.join(config.projectsDir, projectName);
664
+ const sessionFile = path.join(projectDir, sessionId + '.jsonl');
701
665
 
702
- if (!sessionFile || !fs.existsSync(sessionFile)) {
666
+ if (!fs.existsSync(sessionFile)) {
703
667
  throw new Error('Session not found');
704
668
  }
705
669
 
706
670
  fs.unlinkSync(sessionFile);
707
- invalidateProjectCaches(config, projectName);
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 sessionFile = resolveSessionFile(config, projectName, sessionId);
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 (!sessionFile || !fs.existsSync(sessionFile)) {
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 content = fs.readFileSync(sessionFile, 'utf8');
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(path.dirname(sessionFile), newSessionId + '.jsonl');
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
- const savedOrder = getSessionOrder(projectName);
735
- if (savedOrder.length > 0) {
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
- invalidateProjectCaches(config, projectName);
740
-
741
- return { newSessionId, forkedFrom: sessionId };
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 (existed) {
790
- // Recursively delete the directory
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
- invalidateProjectCaches(config, projectName);
802
- clearProjectPathResolutionCache(projectName);
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 session of getProjectSessionFiles(config, projectName)) {
815
- const sessionId = session.sessionId;
816
- const filePath = session.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 = getProjectSessionFiles(projectConfig, projectName);
868
+ const sessions = getAllSessions(projectConfig);
891
869
  const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
892
870
 
893
871
  sessions.forEach(session => {