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.
Files changed (92) 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-Bw-0r4Ys.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
  7. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  8. package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
  9. package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
  10. package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
  13. package/dist/web/assets/SessionList-D12muXvx.css +1 -0
  14. package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.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-CTeg1xG-.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-channels.js +4 -0
  37. package/src/server/api/codex-proxy.js +45 -2
  38. package/src/server/api/codex-sessions.js +89 -12
  39. package/src/server/api/commands.js +2 -38
  40. package/src/server/api/dashboard.js +1 -25
  41. package/src/server/api/gemini-proxy.js +4 -2
  42. package/src/server/api/gemini-sessions.js +86 -9
  43. package/src/server/api/hooks.js +1 -17
  44. package/src/server/api/opencode-proxy.js +7 -2
  45. package/src/server/api/opencode-sessions.js +83 -10
  46. package/src/server/api/plugins.js +34 -128
  47. package/src/server/api/proxy.js +5 -3
  48. package/src/server/api/sessions.js +181 -27
  49. package/src/server/api/workspaces.js +0 -14
  50. package/src/server/index.js +9 -31
  51. package/src/server/services/agents-service.js +62 -269
  52. package/src/server/services/base/base-channel-service.js +3 -1
  53. package/src/server/services/channels.js +5 -0
  54. package/src/server/services/codex-channels.js +2 -0
  55. package/src/server/services/codex-sessions.js +88 -108
  56. package/src/server/services/commands-service.js +81 -281
  57. package/src/server/services/config-export-service.js +7 -7
  58. package/src/server/services/config-registry-service.js +9 -8
  59. package/src/server/services/config-sync-manager.js +41 -61
  60. package/src/server/services/config-sync-service.js +3 -3
  61. package/src/server/services/gemini-channels.js +17 -6
  62. package/src/server/services/gemini-config.js +4 -3
  63. package/src/server/services/gemini-sessions.js +59 -26
  64. package/src/server/services/gemini-settings-manager.js +3 -2
  65. package/src/server/services/mcp-service.js +14 -9
  66. package/src/server/services/native-oauth-adapters.js +3 -3
  67. package/src/server/services/network-access.js +2 -61
  68. package/src/server/services/notification-hooks.js +19 -184
  69. package/src/server/services/opencode-channels.js +9 -0
  70. package/src/server/services/opencode-sessions.js +6 -16
  71. package/src/server/services/opencode-settings-manager.js +3 -3
  72. package/src/server/services/plugins-service.js +62 -996
  73. package/src/server/services/prompts-service.js +9 -5
  74. package/src/server/services/sessions.js +171 -183
  75. package/src/server/services/skill-service.js +18 -155
  76. package/src/server/websocket-server.js +4 -35
  77. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  78. package/dist/web/assets/Home-qzk118Of.css +0 -1
  79. package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
  80. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  81. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  82. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  83. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  84. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  85. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  86. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  87. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  88. package/docs/Caddyfile.example +0 -19
  89. package/docs/reverse-proxy-https.md +0 -57
  90. package/src/server/services/https-cert.js +0 -171
  91. package/src/server/services/session-launch-command.js +0 -58
  92. 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,123 @@ 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;
138
+ let targetUserReached = false;
154
139
 
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
- });
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
- while (projectPathResolutionCache.size > MAX_PROJECT_PATH_CACHE_ENTRIES) {
164
- const oldestKey = projectPathResolutionCache.keys().next().value;
165
- if (!oldestKey) {
166
- break;
154
+ if (json.type !== 'user') {
155
+ keptLines.push(line);
156
+ continue;
167
157
  }
168
- projectPathResolutionCache.delete(oldestKey);
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
- function clearProjectPathResolutionCache(encodedName) {
173
- if (!encodedName) {
174
- projectPathResolutionCache.clear();
175
- return;
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
- projectPathResolutionCache.delete(getProjectPathCacheKey(encodedName));
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(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
- }
202
+ const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
202
203
 
203
204
  // Detect Windows drive letter (e.g., "C--Users-admin")
204
- const windowsDriveMatch = normalizedEncodedName.match(/^([A-Z])--(.+)$/);
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
- const resolved = {
264
- fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
265
- projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
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 = normalizedEncodedName.replace(/^-/, '/').replace(/-/g, '/');
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
- const resolved = {
324
- fullPath: validateProjectPath(currentPath) || pathStr,
325
- projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
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 = getProjectsStatsCacheKey(config);
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(getProjectsStatsCacheKey(config), data, 300000);
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
- 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;
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 activeSessionTimes = sessionChecks.filter((mtimeMs) => Number.isFinite(mtimeMs));
488
- sessionCount = activeSessionTimes.length;
453
+ const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
454
+ sessionCount = sessionFilesWithMessages.length;
489
455
 
490
- if (activeSessionTimes.length > 0) {
491
- lastUsed = Math.max(...activeSessionTimes);
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
- sessionCount += getProjectSessionFiles(config, entry.name).length;
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, statsOverride = null) {
512
+ async function hasActualMessages(filePath) {
538
513
  try {
539
- const stats = statsOverride || await fs.promises.stat(filePath);
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 = getProjectSessionsCacheKey(projectName);
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 = getProjectSessionFiles(projectConfig, projectName);
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
- const newSessions = [...sessionMap.values()].sort((a, b) => {
683
- return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
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 sessionFile = resolveSessionFile(config, projectName, sessionId);
673
+ const projectDir = path.join(config.projectsDir, projectName);
674
+ const sessionFile = path.join(projectDir, sessionId + '.jsonl');
701
675
 
702
- if (!sessionFile || !fs.existsSync(sessionFile)) {
676
+ if (!fs.existsSync(sessionFile)) {
703
677
  throw new Error('Session not found');
704
678
  }
705
679
 
706
680
  fs.unlinkSync(sessionFile);
707
- invalidateProjectCaches(config, projectName);
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 sessionFile = resolveSessionFile(config, projectName, sessionId);
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 (!sessionFile || !fs.existsSync(sessionFile)) {
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 content = fs.readFileSync(sessionFile, 'utf8');
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(path.dirname(sessionFile), newSessionId + '.jsonl');
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
- const savedOrder = getSessionOrder(projectName);
735
- if (savedOrder.length > 0) {
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
- invalidateProjectCaches(config, projectName);
740
-
741
- return { newSessionId, forkedFrom: sessionId };
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 (existed) {
790
- // Recursively delete the directory
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
- invalidateProjectCaches(config, projectName);
802
- clearProjectPathResolutionCache(projectName);
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 session of getProjectSessionFiles(config, projectName)) {
815
- const sessionId = session.sessionId;
816
- const filePath = session.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 = getProjectSessionFiles(projectConfig, projectName);
878
+ const sessions = getAllSessions(projectConfig);
891
879
  const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
892
880
 
893
881
  sessions.forEach(session => {