@adversity/coding-tool-x 2.3.0 → 2.4.1

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 (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +8 -0
  3. package/dist/web/assets/icons-Dom8a0SN.js +1 -0
  4. package/dist/web/assets/index-CQeUIH7E.css +41 -0
  5. package/dist/web/assets/index-YrjlFzC4.js +14 -0
  6. package/dist/web/assets/naive-ui-BjMHakwv.js +1 -0
  7. package/dist/web/assets/vendors-DtJKdpSj.js +7 -0
  8. package/dist/web/assets/vue-vendor-VFuFB5f4.js +44 -0
  9. package/dist/web/index.html +6 -2
  10. package/package.json +2 -2
  11. package/src/commands/export-config.js +205 -0
  12. package/src/config/default.js +1 -1
  13. package/src/server/api/config-export.js +122 -0
  14. package/src/server/api/config-sync.js +155 -0
  15. package/src/server/api/config-templates.js +12 -6
  16. package/src/server/api/health-check.js +1 -89
  17. package/src/server/api/permissions.js +92 -69
  18. package/src/server/api/projects.js +2 -2
  19. package/src/server/api/sessions.js +70 -70
  20. package/src/server/api/skills.js +206 -0
  21. package/src/server/api/terminal.js +26 -0
  22. package/src/server/index.js +7 -11
  23. package/src/server/services/config-export-service.js +415 -0
  24. package/src/server/services/config-sync-service.js +515 -0
  25. package/src/server/services/config-templates-service.js +61 -38
  26. package/src/server/services/enhanced-cache.js +196 -0
  27. package/src/server/services/health-check.js +1 -315
  28. package/src/server/services/permission-templates-service.js +339 -0
  29. package/src/server/services/pty-manager.js +35 -1
  30. package/src/server/services/sessions.js +122 -44
  31. package/src/server/services/skill-service.js +252 -2
  32. package/src/server/services/workspace-service.js +44 -84
  33. package/src/server/websocket-server.js +4 -1
  34. package/dist/web/assets/index-dhun1bYQ.js +0 -3555
  35. package/dist/web/assets/index-hHb7DAda.css +0 -41
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Permission Templates Service
3
+ *
4
+ * 管理权限配置模版的 CRUD 操作
5
+ * 支持内置模版和自定义模版
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { PATHS } = require('../../config/paths');
11
+
12
+ // 权限模版存储文件
13
+ const TEMPLATES_FILE = path.join(PATHS.config, 'permission-templates.json');
14
+
15
+ // 内置权限模版
16
+ const BUILTIN_TEMPLATES = [
17
+ {
18
+ id: 'safe',
19
+ name: '安全模式',
20
+ description: '仅允许只读命令,危险操作需要确认',
21
+ permissions: {
22
+ allow: [
23
+ 'Bash(cat:*)',
24
+ 'Bash(ls:*)',
25
+ 'Bash(pwd)',
26
+ 'Bash(echo:*)',
27
+ 'Bash(head:*)',
28
+ 'Bash(tail:*)',
29
+ 'Bash(grep:*)',
30
+ 'Read(*)'
31
+ ],
32
+ deny: [
33
+ 'Bash(rm:*)',
34
+ 'Bash(sudo:*)',
35
+ 'Bash(git push:*)',
36
+ 'Bash(git reset --hard:*)',
37
+ 'Bash(chmod:*)',
38
+ 'Bash(chown:*)',
39
+ 'Edit(*)'
40
+ ]
41
+ },
42
+ isBuiltin: true
43
+ },
44
+ {
45
+ id: 'balanced',
46
+ name: '平衡模式',
47
+ description: '允许常用开发命令,危险操作需要确认',
48
+ permissions: {
49
+ allow: [
50
+ 'Bash(cat:*)',
51
+ 'Bash(ls:*)',
52
+ 'Bash(pwd)',
53
+ 'Bash(echo:*)',
54
+ 'Bash(head:*)',
55
+ 'Bash(tail:*)',
56
+ 'Bash(grep:*)',
57
+ 'Bash(find:*)',
58
+ 'Bash(git status)',
59
+ 'Bash(git diff:*)',
60
+ 'Bash(git log:*)',
61
+ 'Bash(npm run:*)',
62
+ 'Bash(pnpm:*)',
63
+ 'Bash(yarn:*)',
64
+ 'Read(*)',
65
+ 'Edit(*)'
66
+ ],
67
+ deny: [
68
+ 'Bash(rm -rf:*)',
69
+ 'Bash(sudo:*)',
70
+ 'Bash(git push --force:*)',
71
+ 'Bash(git reset --hard:*)'
72
+ ]
73
+ },
74
+ isBuiltin: true
75
+ },
76
+ {
77
+ id: 'permissive',
78
+ name: '宽松模式',
79
+ description: '允许大多数命令,仅阻止极度危险的操作',
80
+ permissions: {
81
+ allow: [
82
+ 'Bash(*)',
83
+ 'Read(*)',
84
+ 'Edit(*)'
85
+ ],
86
+ deny: [
87
+ 'Bash(rm -rf /*)',
88
+ 'Bash(sudo rm -rf:*)'
89
+ ]
90
+ },
91
+ isBuiltin: true
92
+ },
93
+ {
94
+ id: 'mcp-full',
95
+ name: 'MCP 全功能',
96
+ description: '允许所有 MCP 工具和常用命令,适合使用 MCP 服务器的项目',
97
+ permissions: {
98
+ allow: [
99
+ 'Read(*)',
100
+ 'Edit(*)',
101
+ 'Bash(cat:*)',
102
+ 'Bash(ls:*)',
103
+ 'Bash(find:*)',
104
+ 'Bash(grep:*)',
105
+ 'Bash(tree:*)',
106
+ 'Bash(git:*)',
107
+ 'Bash(npm:*)',
108
+ 'Bash(pnpm:*)',
109
+ 'Bash(yarn:*)',
110
+ 'WebSearch',
111
+ 'mcp__Serena__*',
112
+ 'mcp__fetch__fetch',
113
+ 'mcp__memory__*',
114
+ 'mcp__github__*',
115
+ 'mcp__context7__*'
116
+ ],
117
+ deny: [
118
+ 'Bash(rm -rf:*)',
119
+ 'Bash(sudo:*)'
120
+ ]
121
+ },
122
+ isBuiltin: true
123
+ }
124
+ ];
125
+
126
+ /**
127
+ * 确保配置目录存在
128
+ */
129
+ function ensureDir(dirPath) {
130
+ if (!fs.existsSync(dirPath)) {
131
+ fs.mkdirSync(dirPath, { recursive: true });
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 加载所有模版(内置 + 自定义)
137
+ */
138
+ /**
139
+ * 加载所有模版(内置 + 自定义)
140
+ * 内置模版可被用户覆盖修改
141
+ */
142
+ /**
143
+ * 加载所有模版(内置 + 自定义)
144
+ * 内置模版可被用户覆盖修改或隐藏
145
+ */
146
+ function loadTemplates() {
147
+ try {
148
+ if (fs.existsSync(TEMPLATES_FILE)) {
149
+ const content = fs.readFileSync(TEMPLATES_FILE, 'utf8');
150
+ const data = JSON.parse(content);
151
+
152
+ // 分离自定义模版和隐藏标记
153
+ const customTemplates = (data.custom || []).filter(t => !t._hidden);
154
+ const hiddenIds = (data.custom || []).filter(t => t._hidden).map(t => t.id);
155
+
156
+ const customIds = customTemplates.map(t => t.id);
157
+
158
+ // 过滤掉已被自定义覆盖或隐藏的内置模版
159
+ const effectiveBuiltin = BUILTIN_TEMPLATES.filter(t =>
160
+ !customIds.includes(t.id) && !hiddenIds.includes(t.id)
161
+ );
162
+
163
+ return {
164
+ builtin: effectiveBuiltin,
165
+ custom: customTemplates,
166
+ all: [...customTemplates, ...effectiveBuiltin] // 自定义优先
167
+ };
168
+ }
169
+ } catch (error) {
170
+ console.error('[PermissionTemplates] 加载模版失败:', error.message);
171
+ }
172
+
173
+ return {
174
+ builtin: BUILTIN_TEMPLATES,
175
+ custom: [],
176
+ all: BUILTIN_TEMPLATES
177
+ };
178
+ }
179
+
180
+ /**
181
+ * 保存自定义模版
182
+ */
183
+ function saveCustomTemplates(customTemplates) {
184
+ ensureDir(PATHS.config);
185
+ fs.writeFileSync(TEMPLATES_FILE, JSON.stringify({ custom: customTemplates }, null, 2), 'utf8');
186
+ }
187
+
188
+ /**
189
+ * 获取所有模版
190
+ */
191
+ /**
192
+ * 获取所有模版(合并后的最终列表)
193
+ */
194
+ function getAllTemplates() {
195
+ const { all } = loadTemplates();
196
+ return all;
197
+ }
198
+
199
+ /**
200
+ * 根据 ID 获取模版
201
+ */
202
+ function getTemplateById(id) {
203
+ const templates = getAllTemplates();
204
+ return templates.find(t => t.id === id) || null;
205
+ }
206
+
207
+ /**
208
+ * 创建自定义模版
209
+ */
210
+ function createTemplate(template) {
211
+ const { custom } = loadTemplates();
212
+
213
+ // 验证必填字段
214
+ if (!template.name || !template.name.trim()) {
215
+ throw new Error('模版名称不能为空');
216
+ }
217
+
218
+ // 生成唯一 ID
219
+ const id = `custom-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
220
+
221
+ const newTemplate = {
222
+ id,
223
+ name: template.name.trim(),
224
+ description: template.description || '',
225
+ permissions: {
226
+ allow: template.permissions?.allow || [],
227
+ deny: template.permissions?.deny || []
228
+ },
229
+ isBuiltin: false,
230
+ createdAt: new Date().toISOString()
231
+ };
232
+
233
+ custom.push(newTemplate);
234
+ saveCustomTemplates(custom);
235
+
236
+ return newTemplate;
237
+ }
238
+
239
+ /**
240
+ * 更新自定义模版
241
+ */
242
+ /**
243
+ * 更新权限模版(包括内置模版)
244
+ * 编辑内置模版时,会在自定义列表中创建覆盖版本
245
+ */
246
+ function updateTemplate(id, updates) {
247
+ const { custom, all } = loadTemplates();
248
+
249
+ // 查找模版(先查自定义,再查所有)
250
+ const existingTemplate = all.find(t => t.id === id);
251
+ if (!existingTemplate) {
252
+ throw new Error('模版不存在');
253
+ }
254
+
255
+ // 验证名称
256
+ if (updates.name !== undefined && !updates.name.trim()) {
257
+ throw new Error('模版名称不能为空');
258
+ }
259
+
260
+ const updated = {
261
+ ...existingTemplate,
262
+ name: updates.name?.trim() ?? existingTemplate.name,
263
+ description: updates.description ?? existingTemplate.description,
264
+ permissions: updates.permissions ?? existingTemplate.permissions,
265
+ isBuiltin: false, // 编辑后标记为自定义(覆盖内置)
266
+ updatedAt: new Date().toISOString()
267
+ };
268
+
269
+ // 如果是编辑内置模版,则添加到自定义列表中(覆盖)
270
+ const customIndex = custom.findIndex(t => t.id === id);
271
+ if (customIndex !== -1) {
272
+ custom[customIndex] = updated;
273
+ } else {
274
+ custom.push(updated);
275
+ }
276
+
277
+ saveCustomTemplates(custom);
278
+ return updated;
279
+ }
280
+
281
+ /**
282
+ * 删除自定义模版
283
+ */
284
+ /**
285
+ * 删除权限模版(包括内置模版)
286
+ * 删除内置模版时,会在自定义列表中添加隐藏标记
287
+ */
288
+ /**
289
+ * 删除权限模版(包括内置模版)
290
+ * 删除内置模版时,会在自定义列表中添加隐藏标记
291
+ */
292
+ function deleteTemplate(id) {
293
+ try {
294
+ if (!fs.existsSync(TEMPLATES_FILE)) {
295
+ ensureDir(PATHS.config);
296
+ fs.writeFileSync(TEMPLATES_FILE, JSON.stringify({ custom: [] }, null, 2), 'utf8');
297
+ }
298
+
299
+ const content = fs.readFileSync(TEMPLATES_FILE, 'utf8');
300
+ const data = JSON.parse(content);
301
+ const customList = data.custom || [];
302
+
303
+ // 检查是否为内置模版
304
+ const isBuiltin = BUILTIN_TEMPLATES.some(t => t.id === id);
305
+
306
+ if (isBuiltin) {
307
+ // 内置模版:添加隐藏标记
308
+ const hideMarker = {
309
+ id,
310
+ _hidden: true,
311
+ deletedAt: new Date().toISOString()
312
+ };
313
+ customList.push(hideMarker);
314
+ fs.writeFileSync(TEMPLATES_FILE, JSON.stringify({ custom: customList }, null, 2), 'utf8');
315
+ } else {
316
+ // 自定义模版:直接删除
317
+ const index = customList.findIndex(t => t.id === id && !t._hidden);
318
+ if (index === -1) {
319
+ throw new Error('模版不存在');
320
+ }
321
+ customList.splice(index, 1);
322
+ fs.writeFileSync(TEMPLATES_FILE, JSON.stringify({ custom: customList }, null, 2), 'utf8');
323
+ }
324
+
325
+ return true;
326
+ } catch (error) {
327
+ if (error.message === '模版不存在') throw error;
328
+ throw new Error('删除模版失败: ' + error.message);
329
+ }
330
+ }
331
+
332
+ module.exports = {
333
+ getAllTemplates,
334
+ getTemplateById,
335
+ createTemplate,
336
+ updateTemplate,
337
+ deleteTemplate,
338
+ BUILTIN_TEMPLATES
339
+ };
@@ -5,6 +5,7 @@
5
5
 
6
6
  const os = require('os');
7
7
  const path = require('path');
8
+ const fs = require('fs');
8
9
 
9
10
  // 尝试加载 node-pty,如果失败则提示
10
11
  let pty = null;
@@ -40,7 +41,22 @@ class PtyManager {
40
41
  if (process.platform === 'win32') {
41
42
  return process.env.COMSPEC || 'cmd.exe';
42
43
  }
43
- return process.env.SHELL || '/bin/zsh';
44
+
45
+ // 优先使用环境变量指定的 shell
46
+ if (process.env.SHELL && fs.existsSync(process.env.SHELL)) {
47
+ return process.env.SHELL;
48
+ }
49
+
50
+ // 回退到常见的 shell,按优先级检查
51
+ const commonShells = ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/sh'];
52
+ for (const shell of commonShells) {
53
+ if (fs.existsSync(shell)) {
54
+ return shell;
55
+ }
56
+ }
57
+
58
+ // 最后回退
59
+ return '/bin/sh';
44
60
  }
45
61
 
46
62
  /**
@@ -78,6 +94,9 @@ class PtyManager {
78
94
  // 检查 PTY 是否可用
79
95
  if (!pty) {
80
96
  const errMsg = this.getPtyError() || 'node-pty is not available';
97
+ console.error('[PTY] Cannot create terminal:', errMsg);
98
+ console.error('[PTY] Node version:', process.version);
99
+ console.error('[PTY] Platform:', process.platform);
81
100
  throw new Error(`Cannot create terminal: ${errMsg}`);
82
101
  }
83
102
 
@@ -94,6 +113,21 @@ class PtyManager {
94
113
  startCommand = null
95
114
  } = options;
96
115
 
116
+ // 验证 shell 和 cwd 存在
117
+ if (!fs.existsSync(shell)) {
118
+ const error = `Shell not found: ${shell}`;
119
+ console.error('[PTY]', error);
120
+ throw new Error(error);
121
+ }
122
+ if (!fs.existsSync(cwd)) {
123
+ const error = `Working directory not found: ${cwd}`;
124
+ console.error('[PTY]', error);
125
+ throw new Error(error);
126
+ }
127
+
128
+ console.log(`[PTY] Creating terminal: shell=${shell}, cwd=${cwd}`);
129
+
130
+
97
131
  const terminalId = `term_${this.nextId++}_${Date.now()}`;
98
132
 
99
133
  // 合并环境变量
@@ -11,6 +11,7 @@ const {
11
11
  checkHasMessagesCache,
12
12
  rememberHasMessages
13
13
  } = require('./session-cache');
14
+ const { globalCache, CacheKeys } = require('./enhanced-cache');
14
15
  const { PATHS } = require('../../config/paths');
15
16
 
16
17
  // Base directory for cc-tool data
@@ -82,15 +83,15 @@ function saveForkRelations(relations) {
82
83
  fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
83
84
  }
84
85
 
85
- // Get all projects with stats
86
- function getProjects(config) {
86
+ // Get all projects with stats (async version)
87
+ async function getProjects(config) {
87
88
  const projectsDir = config.projectsDir;
88
89
 
89
90
  if (!fs.existsSync(projectsDir)) {
90
91
  return [];
91
92
  }
92
93
 
93
- const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
94
+ const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
94
95
  return entries
95
96
  .filter(entry => entry.isDirectory())
96
97
  .map(entry => entry.name);
@@ -283,32 +284,43 @@ function extractCwdFromSessionHeader(sessionFile) {
283
284
  return null;
284
285
  }
285
286
 
286
- // Get projects with detailed stats (with caching)
287
- function getProjectsWithStats(config, options = {}) {
287
+ // Get projects with detailed stats (with caching) - async version
288
+ async function getProjectsWithStats(config, options = {}) {
288
289
  if (!options.force) {
290
+ // Check enhanced cache first
291
+ const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
292
+ const enhancedCached = globalCache.get(cacheKey);
293
+ if (enhancedCached) {
294
+ return enhancedCached;
295
+ }
296
+
297
+ // Check old cache
289
298
  const cached = getCachedProjects(config);
290
299
  if (cached) {
300
+ globalCache.set(cacheKey, cached, 300000); // 5分钟
291
301
  return cached;
292
302
  }
293
303
  }
294
304
 
295
- const data = buildProjectsWithStats(config);
305
+ const data = await buildProjectsWithStats(config);
296
306
  setCachedProjects(config, data);
307
+ globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
297
308
  return data;
298
309
  }
299
310
 
300
- function buildProjectsWithStats(config) {
311
+ async function buildProjectsWithStats(config) {
301
312
  const projectsDir = config.projectsDir;
302
313
 
303
314
  if (!fs.existsSync(projectsDir)) {
304
315
  return [];
305
316
  }
306
317
 
307
- const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
318
+ const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
308
319
 
309
- return entries
320
+ // Process all projects concurrently
321
+ const projectPromises = entries
310
322
  .filter(entry => entry.isDirectory())
311
- .map(entry => {
323
+ .map(async (entry) => {
312
324
  const projectName = entry.name;
313
325
  const projectPath = path.join(projectsDir, projectName);
314
326
 
@@ -320,24 +332,29 @@ function buildProjectsWithStats(config) {
320
332
  let lastUsed = null;
321
333
 
322
334
  try {
323
- const files = fs.readdirSync(projectPath);
335
+ const files = await fs.promises.readdir(projectPath);
324
336
  const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
325
337
 
326
- // Filter: only count sessions that have actual messages (not just file-history-snapshots)
327
- const sessionFilesWithMessages = jsonlFiles.filter(f => {
328
- const filePath = path.join(projectPath, f);
329
- return hasActualMessages(filePath);
330
- });
338
+ // Filter: only count sessions that have actual messages (in parallel)
339
+ const sessionChecks = await Promise.all(
340
+ jsonlFiles.map(async (f) => {
341
+ const filePath = path.join(projectPath, f);
342
+ const hasMessages = await hasActualMessages(filePath);
343
+ return hasMessages ? f : null;
344
+ })
345
+ );
331
346
 
347
+ const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
332
348
  sessionCount = sessionFilesWithMessages.length;
333
349
 
334
350
  // Find most recent session (only from sessions with messages)
335
351
  if (sessionFilesWithMessages.length > 0) {
336
- const stats = sessionFilesWithMessages.map(f => {
352
+ const statPromises = sessionFilesWithMessages.map(async (f) => {
337
353
  const filePath = path.join(projectPath, f);
338
- const stat = fs.statSync(filePath);
354
+ const stat = await fs.promises.stat(filePath);
339
355
  return stat.mtime.getTime();
340
356
  });
357
+ const stats = await Promise.all(statPromises);
341
358
  lastUsed = Math.max(...stats);
342
359
  }
343
360
  } catch (err) {
@@ -351,8 +368,10 @@ function buildProjectsWithStats(config) {
351
368
  sessionCount,
352
369
  lastUsed
353
370
  };
354
- })
355
- .sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)); // Sort by last used
371
+ });
372
+
373
+ const projects = await Promise.all(projectPromises);
374
+ return projects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)); // Sort by last used
356
375
  }
357
376
 
358
377
  // 获取 Claude 项目/会话数量(轻量统计)
@@ -383,16 +402,27 @@ function getProjectAndSessionCounts(config) {
383
402
  return { projectCount, sessionCount };
384
403
  }
385
404
 
386
- // Check if a session file has actual messages (not just file-history-snapshots)
387
- function hasActualMessages(filePath) {
405
+ // Check if a session file has actual messages (async with enhanced caching)
406
+ async function hasActualMessages(filePath) {
388
407
  try {
389
- const stats = fs.statSync(filePath);
390
- const cached = checkHasMessagesCache(filePath, stats);
408
+ const stats = await fs.promises.stat(filePath);
409
+
410
+ // Check enhanced cache first
411
+ const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
412
+ const cached = globalCache.get(cacheKey);
391
413
  if (typeof cached === 'boolean') {
392
414
  return cached;
393
415
  }
394
416
 
395
- const result = scanSessionFileForMessages(filePath);
417
+ // Check old cache mechanism
418
+ const oldCached = checkHasMessagesCache(filePath, stats);
419
+ if (typeof oldCached === 'boolean') {
420
+ globalCache.set(cacheKey, oldCached, 600000); // 10分钟
421
+ return oldCached;
422
+ }
423
+
424
+ const result = await scanSessionFileForMessagesAsync(filePath);
425
+ globalCache.set(cacheKey, result, 600000);
396
426
  rememberHasMessages(filePath, stats, result);
397
427
  return result;
398
428
  } catch (err) {
@@ -434,30 +464,74 @@ function scanSessionFileForMessages(filePath) {
434
464
  }
435
465
  }
436
466
 
437
- // Get sessions for a project
438
- function getSessionsForProject(config, projectName) {
467
+ // Async version using streams for better performance
468
+ function scanSessionFileForMessagesAsync(filePath) {
469
+ return new Promise((resolve) => {
470
+ const stream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 64 * 1024 });
471
+ const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
472
+ let found = false;
473
+ let leftover = '';
474
+
475
+ stream.on('data', (chunk) => {
476
+ if (found) return;
477
+ const combined = leftover + chunk;
478
+ if (pattern.test(combined)) {
479
+ found = true;
480
+ stream.destroy();
481
+ resolve(true);
482
+ }
483
+ leftover = combined.slice(-64);
484
+ });
485
+
486
+ stream.on('end', () => {
487
+ if (!found) resolve(false);
488
+ });
489
+
490
+ stream.on('error', () => {
491
+ resolve(false);
492
+ });
493
+ });
494
+ }
495
+
496
+ // Get sessions for a project - async version
497
+ async function getSessionsForProject(config, projectName) {
498
+ // Check cache first
499
+ const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
500
+ const cached = globalCache.get(cacheKey);
501
+ if (cached) {
502
+ return cached;
503
+ }
504
+
439
505
  const projectConfig = { ...config, currentProject: projectName };
440
506
  const sessions = getAllSessions(projectConfig);
441
507
  const forkRelations = getForkRelations();
442
508
  const savedOrder = getSessionOrder(projectName);
443
509
 
444
- // Parse session info and calculate total size, filter out sessions with no messages
510
+ // Parse session info and calculate total size, filter out sessions with no messages (in parallel)
445
511
  let totalSize = 0;
446
- const sessionsWithInfo = sessions
447
- .filter(session => hasActualMessages(session.filePath))
448
- .map(session => {
449
- const info = parseSessionInfoFast(session.filePath);
450
- totalSize += session.size || 0;
451
- return {
452
- sessionId: session.sessionId,
453
- mtime: session.mtime,
454
- size: session.size,
455
- filePath: session.filePath,
456
- gitBranch: info.gitBranch || null,
457
- firstMessage: info.firstMessage || null,
458
- forkedFrom: forkRelations[session.sessionId] || null
459
- };
460
- });
512
+
513
+ const sessionChecks = await Promise.all(
514
+ sessions.map(async (session) => {
515
+ const hasMessages = await hasActualMessages(session.filePath);
516
+ return hasMessages ? session : null;
517
+ })
518
+ );
519
+
520
+ const validSessions = sessionChecks.filter(s => s !== null);
521
+
522
+ const sessionsWithInfo = validSessions.map(session => {
523
+ const info = parseSessionInfoFast(session.filePath);
524
+ totalSize += session.size || 0;
525
+ return {
526
+ sessionId: session.sessionId,
527
+ mtime: session.mtime,
528
+ size: session.size,
529
+ filePath: session.filePath,
530
+ gitBranch: info.gitBranch || null,
531
+ firstMessage: info.firstMessage || null,
532
+ forkedFrom: forkRelations[session.sessionId] || null
533
+ };
534
+ });
461
535
 
462
536
  // Apply saved order if exists
463
537
  let orderedSessions = sessionsWithInfo;
@@ -478,10 +552,14 @@ function getSessionsForProject(config, projectName) {
478
552
  orderedSessions = ordered;
479
553
  }
480
554
 
481
- return {
555
+ const result = {
482
556
  sessions: orderedSessions,
483
557
  totalSize
484
558
  };
559
+
560
+ // Cache for 2 minutes
561
+ globalCache.set(cacheKey, result, 120000);
562
+ return result;
485
563
  }
486
564
 
487
565
  // Delete a session