coding-tool-x 3.7.4 → 3.7.5

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 (42) hide show
  1. package/dist/web/assets/{Analytics-BmPIW7oK.js → Analytics-DnnufrQi.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CG4j4ua4.js → ConfigTemplates-BTVhMSfe.js} +1 -1
  3. package/dist/web/assets/Home-CqnBXFxj.css +1 -0
  4. package/dist/web/assets/Home-D03PEREx.js +1 -0
  5. package/dist/web/assets/{PluginManager-CCJ7mo35.js → PluginManager-BiMqEiyQ.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DVUs3o8x.js → ProjectList-C6BA3V49.js} +1 -1
  7. package/dist/web/assets/{ProjectList-D3a0oRum.css → ProjectList-DQ5ZCJO2.css} +1 -1
  8. package/dist/web/assets/SessionList--9Bl6EhV.css +1 -0
  9. package/dist/web/assets/SessionList-DChG04jz.js +1 -0
  10. package/dist/web/assets/{SkillManager-DmOTHcQ7.js → SkillManager-DAy8k5CL.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DLVj0zlu.js → WorkspaceManager-Bs2D6R9j.js} +1 -1
  12. package/dist/web/assets/index-DnDBHGCC.js +2 -0
  13. package/dist/web/assets/index-U3TOywtB.css +1 -0
  14. package/dist/web/index.html +2 -2
  15. package/package.json +1 -1
  16. package/src/server/api/codex-projects.js +7 -11
  17. package/src/server/api/codex-sessions.js +30 -21
  18. package/src/server/api/dashboard.js +24 -36
  19. package/src/server/api/gemini-projects.js +7 -11
  20. package/src/server/api/gemini-sessions.js +28 -26
  21. package/src/server/api/omp-projects.js +6 -11
  22. package/src/server/api/omp-sessions.js +31 -17
  23. package/src/server/api/opencode-projects.js +6 -11
  24. package/src/server/api/opencode-sessions.js +28 -21
  25. package/src/server/api/projects.js +7 -28
  26. package/src/server/api/sessions.js +32 -16
  27. package/src/server/services/channel-balance.js +2 -1
  28. package/src/server/services/codex-sessions.js +23 -10
  29. package/src/server/services/dashboard-snapshot-worker.js +216 -0
  30. package/src/server/services/gemini-sessions.js +15 -3
  31. package/src/server/services/omp-sessions.js +31 -11
  32. package/src/server/services/opencode-sessions.js +9 -6
  33. package/src/server/services/session-snapshot-worker.js +158 -0
  34. package/src/server/services/session-snapshots.js +137 -0
  35. package/src/server/services/sessions.js +6 -4
  36. package/src/server/services/snapshot-cache.js +77 -11
  37. package/dist/web/assets/Home-B9yAAZrD.js +0 -1
  38. package/dist/web/assets/Home-DSTUSxrT.css +0 -1
  39. package/dist/web/assets/SessionList-D12muXvx.css +0 -1
  40. package/dist/web/assets/SessionList-DwssBydw.js +0 -1
  41. package/dist/web/assets/index-BjdNYwSs.js +0 -2
  42. package/dist/web/assets/index-kQCC5dlu.css +0 -1
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ const { fork } = require('child_process');
4
+
5
+ const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS = 120 * 1000;
6
+
7
+ function sortClaudeProjects(projects, order = []) {
8
+ if (!Array.isArray(order) || order.length === 0) {
9
+ return projects;
10
+ }
11
+ const orderMap = new Map(order.map((name, idx) => [name, idx]));
12
+ return [...projects].sort((a, b) => {
13
+ const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
14
+ const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
15
+ if (aIdx === bIdx) {
16
+ return (b.lastUsed || 0) - (a.lastUsed || 0);
17
+ }
18
+ return aIdx - bIdx;
19
+ });
20
+ }
21
+
22
+ async function buildProjectsPayload(source, config = {}, options = {}) {
23
+ switch (source) {
24
+ case 'claude': {
25
+ const { getProjectsWithStats, getProjectOrder } = require('./sessions');
26
+ const projects = await getProjectsWithStats(config, { force: options.force === true });
27
+ const sortedProjects = sortClaudeProjects(projects, getProjectOrder(config));
28
+ return {
29
+ projects: sortedProjects,
30
+ currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
31
+ };
32
+ }
33
+ case 'codex': {
34
+ const { getProjects } = require('./codex-sessions');
35
+ const projects = getProjects({ force: options.force === true });
36
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
37
+ }
38
+ case 'gemini': {
39
+ const { getProjects } = require('./gemini-sessions');
40
+ const projects = getProjects({ force: options.force === true });
41
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
42
+ }
43
+ case 'opencode': {
44
+ const { getProjects } = require('./opencode-sessions');
45
+ const projects = getProjects({ force: options.force === true });
46
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
47
+ }
48
+ case 'omp': {
49
+ const { getProjects } = require('./omp-sessions');
50
+ const projects = getProjects({ force: options.force === true });
51
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
52
+ }
53
+ default:
54
+ throw new Error(`Unsupported project snapshot source: ${source}`);
55
+ }
56
+ }
57
+
58
+ function buildCountsPayload(source, config = {}, options = {}) {
59
+ switch (source) {
60
+ case 'claude':
61
+ return require('./sessions').getProjectAndSessionCounts(config, { force: options.force === true });
62
+ case 'codex':
63
+ return require('./codex-sessions').getProjectAndSessionCounts({ force: options.force === true });
64
+ case 'gemini':
65
+ return require('./gemini-sessions').getProjectAndSessionCounts({ force: options.force === true });
66
+ case 'opencode':
67
+ return require('./opencode-sessions').getProjectAndSessionCounts({ force: options.force === true });
68
+ case 'omp':
69
+ return require('./omp-sessions').getProjectAndSessionCounts({ force: options.force === true });
70
+ default:
71
+ throw new Error(`Unsupported counts snapshot source: ${source}`);
72
+ }
73
+ }
74
+
75
+ function buildTodayStatsPayload(source) {
76
+ switch (source) {
77
+ case 'claude':
78
+ return require('./claude-statistics-service').getTodayStatistics();
79
+ case 'codex':
80
+ return require('./codex-statistics-service').getTodayStatistics();
81
+ case 'gemini':
82
+ return require('./gemini-statistics-service').getTodayStatistics();
83
+ case 'opencode':
84
+ return require('./opencode-statistics-service').getTodayStatistics();
85
+ case 'omp':
86
+ return require('./omp-statistics-service').getTodayStatistics();
87
+ default:
88
+ throw new Error(`Unsupported today stats snapshot source: ${source}`);
89
+ }
90
+ }
91
+
92
+ function buildChannelsPayload(source) {
93
+ switch (source) {
94
+ case 'claude':
95
+ return require('./channels').getAllChannels();
96
+ case 'codex':
97
+ return require('./codex-channels').getChannels();
98
+ case 'gemini':
99
+ return require('./gemini-channels').getChannels();
100
+ case 'opencode':
101
+ return require('./opencode-channels').getChannels();
102
+ case 'omp':
103
+ return require('./omp-channels').getChannels();
104
+ default:
105
+ throw new Error(`Unsupported channels snapshot source: ${source}`);
106
+ }
107
+ }
108
+
109
+ async function buildPayload({ kind, source, config, options }) {
110
+ const snapshotOptions = options || {};
111
+ switch (kind) {
112
+ case 'projects':
113
+ return buildProjectsPayload(source, config || {}, snapshotOptions);
114
+ case 'counts':
115
+ return buildCountsPayload(source, config || {}, snapshotOptions);
116
+ case 'todayStats':
117
+ return buildTodayStatsPayload(source);
118
+ case 'channels':
119
+ return buildChannelsPayload(source);
120
+ default:
121
+ throw new Error(`Unsupported dashboard snapshot kind: ${kind}`);
122
+ }
123
+ }
124
+
125
+ function runDashboardSnapshotWorker(kind, source, config = {}, options = {}) {
126
+ if (process.env.NODE_ENV === 'test') {
127
+ return Promise.resolve(buildPayload({ kind, source, config, options }));
128
+ }
129
+
130
+ return new Promise((resolve, reject) => {
131
+ const child = fork(__filename, [], {
132
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
133
+ env: {
134
+ ...process.env,
135
+ CC_TOOL_DASHBOARD_SNAPSHOT_WORKER: '1'
136
+ }
137
+ });
138
+
139
+ let settled = false;
140
+ let stderr = '';
141
+ let timeout = null;
142
+
143
+ const finish = (error, value) => {
144
+ if (settled) return;
145
+ settled = true;
146
+ if (timeout) {
147
+ clearTimeout(timeout);
148
+ }
149
+ child.removeAllListeners();
150
+ if (!child.killed) {
151
+ child.kill();
152
+ }
153
+ if (error) {
154
+ reject(error);
155
+ } else {
156
+ resolve(value);
157
+ }
158
+ };
159
+
160
+ timeout = setTimeout(() => {
161
+ finish(new Error(`Dashboard snapshot refresh timed out for ${kind}/${source}`));
162
+ }, DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS);
163
+
164
+ child.stderr.on('data', (chunk) => {
165
+ stderr += chunk.toString();
166
+ if (stderr.length > 4096) {
167
+ stderr = stderr.slice(-4096);
168
+ }
169
+ });
170
+
171
+ child.on('message', (message) => {
172
+ if (!message || typeof message !== 'object') return;
173
+ if (message.ok) {
174
+ finish(null, message.value);
175
+ } else {
176
+ finish(new Error(message.error || `Dashboard snapshot refresh failed for ${kind}/${source}`));
177
+ }
178
+ });
179
+
180
+ child.on('error', (error) => finish(error));
181
+ child.on('exit', (code, signal) => {
182
+ if (settled) return;
183
+ const suffix = stderr ? `: ${stderr.trim()}` : '';
184
+ finish(new Error(`Dashboard snapshot worker exited (${code || signal || 'unknown'})${suffix}`));
185
+ });
186
+
187
+ child.send({ kind, source, config, options });
188
+ });
189
+ }
190
+
191
+ function attachWorkerHandler() {
192
+ process.on('message', async (message) => {
193
+ try {
194
+ const value = await buildPayload(message || {});
195
+ if (process.send) {
196
+ process.send({ ok: true, value }, () => process.exit(0));
197
+ return;
198
+ }
199
+ } catch (error) {
200
+ if (process.send) {
201
+ process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
202
+ return;
203
+ }
204
+ }
205
+ process.exit(0);
206
+ });
207
+ }
208
+
209
+ if (process.env.CC_TOOL_DASHBOARD_SNAPSHOT_WORKER === '1' || require.main === module) {
210
+ attachWorkerHandler();
211
+ }
212
+
213
+ module.exports = {
214
+ buildPayload,
215
+ runDashboardSnapshotWorker
216
+ };
@@ -615,7 +615,11 @@ function normalizeSession(geminiSession) {
615
615
  * @param {string} projectHash - 项目 hash
616
616
  * @returns {string|null} 项目路径
617
617
  */
618
- function getProjectPath(projectHash) {
618
+ function getProjectPath(projectHash, options = {}) {
619
+ if (options.force) {
620
+ pathMappingCache = null;
621
+ pathMappingCacheTime = 0;
622
+ }
619
623
  const pathMapping = buildPathMapping();
620
624
  return pathMapping.get(projectHash) || null;
621
625
  }
@@ -624,7 +628,11 @@ function getProjectPath(projectHash) {
624
628
  * 聚合项目列表
625
629
  * @returns {Array} 项目对象数组
626
630
  */
627
- function getProjects() {
631
+ function getProjects(options = {}) {
632
+ if (options.force) {
633
+ pathMappingCache = null;
634
+ pathMappingCacheTime = 0;
635
+ }
628
636
  const sessions = getAllSessions();
629
637
  const projectMap = new Map();
630
638
 
@@ -680,7 +688,11 @@ function getProjects() {
680
688
  * @param {string} projectHash - 项目 hash
681
689
  * @returns {Array} 会话对象数组
682
690
  */
683
- function getProjectSessions(projectHash) {
691
+ function getProjectSessions(projectHash, options = {}) {
692
+ if (options.force) {
693
+ pathMappingCache = null;
694
+ pathMappingCacheTime = 0;
695
+ }
684
696
  const allSessions = getAllSessions();
685
697
  return allSessions
686
698
  .filter(session => session.projectHash === projectHash)
@@ -6,6 +6,8 @@ const { getOmpCommand, getOmpPaths, isOmpInstalled, resolveOmpRuntime } = requir
6
6
 
7
7
  const PROJECT_ORDER_FILE = PATHS.ompProjectOrder;
8
8
  const SESSION_ORDER_FILE = PATHS.ompSessionOrder;
9
+ const OMP_INSTALL_CACHE_TTL_MS = 5 * 60 * 1000;
10
+ let ompInstallCache = { expiresAt: 0, value: false };
9
11
 
10
12
  function ensureDir(dirPath) {
11
13
  if (!fs.existsSync(dirPath)) {
@@ -259,7 +261,7 @@ function loadSessionOrder() {
259
261
  return safeReadJson(SESSION_ORDER_FILE, {});
260
262
  }
261
263
 
262
- function getProjects() {
264
+ function getProjects(_options = {}) {
263
265
  const sessions = getAllSessions();
264
266
  const projectMap = new Map();
265
267
  sessions.forEach((session) => {
@@ -299,7 +301,7 @@ function getProjects() {
299
301
  return projects;
300
302
  }
301
303
 
302
- function getSessionsByProject(projectName) {
304
+ function getSessionsByProject(projectName, _options = {}) {
303
305
  const orderMap = loadSessionOrder();
304
306
  const order = Array.isArray(orderMap[projectName]) ? orderMap[projectName] : [];
305
307
  const sessions = getAllSessions().filter(session => session.projectName === projectName);
@@ -425,7 +427,7 @@ function saveSessionOrder(projectName, order) {
425
427
  safeWriteJson(SESSION_ORDER_FILE, data);
426
428
  }
427
429
 
428
- function getProjectAndSessionCounts() {
430
+ function getProjectAndSessionCounts(_options = {}) {
429
431
  const sessions = getAllSessions();
430
432
  return {
431
433
  projectCount: new Set(sessions.map(session => session.projectName)).size,
@@ -454,15 +456,33 @@ function shellQuote(value) {
454
456
  }
455
457
 
456
458
  function isOmpCliInstalled() {
457
- if (isOmpInstalled()) return true;
458
- const runtime = resolveOmpRuntime();
459
- if (runtime.installed) return true;
460
- try {
461
- execFileSync(getOmpCommand(), ['--version'], { stdio: 'ignore', timeout: 3000 });
462
- return true;
463
- } catch {
464
- return false;
459
+ const now = Date.now();
460
+ if (ompInstallCache.expiresAt > now) {
461
+ return ompInstallCache.value;
462
+ }
463
+
464
+ let installed = false;
465
+ if (isOmpInstalled()) {
466
+ installed = true;
467
+ } else {
468
+ const runtime = resolveOmpRuntime();
469
+ if (runtime.installed) {
470
+ installed = true;
471
+ } else {
472
+ try {
473
+ execFileSync(getOmpCommand(), ['--version'], { stdio: 'ignore', timeout: 3000 });
474
+ installed = true;
475
+ } catch {
476
+ installed = false;
477
+ }
478
+ }
465
479
  }
480
+
481
+ ompInstallCache = {
482
+ value: installed,
483
+ expiresAt: now + OMP_INSTALL_CACHE_TTL_MS
484
+ };
485
+ return installed;
466
486
  }
467
487
 
468
488
  module.exports = {
@@ -493,7 +493,7 @@ function getProjectDisplayName(project) {
493
493
  }
494
494
 
495
495
  // 获取所有项目
496
- function getProjects() {
496
+ function getProjects(_options = {}) {
497
497
  const projects = getProjectRows().map((project) => ({
498
498
  name: project.id,
499
499
  displayName: getProjectDisplayName(project),
@@ -512,7 +512,7 @@ function getProjects() {
512
512
  }
513
513
 
514
514
  // 根据项目ID获取会话列表
515
- function getSessionsByProjectId(projectId) {
515
+ function getSessionsByProjectId(projectId, _options = {}) {
516
516
  const sessions = getSessionRowsByProjectId(projectId).map(session => normalizeSession(session, projectId));
517
517
  const order = getSessionOrder(projectId);
518
518
 
@@ -536,8 +536,8 @@ function getSessionsByProjectId(projectId) {
536
536
  }
537
537
 
538
538
  // 根据项目名获取会话列表
539
- function getSessionsByProject(projectName) {
540
- return getSessionsByProjectId(projectName);
539
+ function getSessionsByProject(projectName, options = {}) {
540
+ return getSessionsByProjectId(projectName, options);
541
541
  }
542
542
 
543
543
  function getSessionLocation(sessionId) {
@@ -628,9 +628,12 @@ function getSessionMessages(sessionId) {
628
628
  }
629
629
 
630
630
  // 获取项目和会话数量统计
631
- function getProjectAndSessionCounts() {
631
+ function getProjectAndSessionCounts(options = {}) {
632
632
  const now = Date.now();
633
- if (countsCache.expiresAt > now) {
633
+ if (options.force) {
634
+ countsCache.expiresAt = 0;
635
+ }
636
+ if (!options.force && countsCache.expiresAt > now) {
634
637
  return countsCache.value;
635
638
  }
636
639
 
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ function totalSizeOf(sessions) {
6
+ return sessions.reduce((sum, session) => sum + (Number(session.size) || 0), 0);
7
+ }
8
+
9
+ function getAliases() {
10
+ const { loadAliases } = require('./alias');
11
+ return loadAliases();
12
+ }
13
+
14
+ async function buildClaudePayload(projectName, config, options = {}) {
15
+ const {
16
+ getSessionsForProject,
17
+ parseRealProjectPath
18
+ } = require('./sessions');
19
+
20
+ const result = await getSessionsForProject(config, projectName, { force: options.force === true });
21
+ const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
22
+
23
+ return {
24
+ sessions: result.sessions,
25
+ totalSize: result.totalSize,
26
+ aliases: getAliases(),
27
+ projectInfo: {
28
+ name: projectName,
29
+ displayName,
30
+ fullPath
31
+ }
32
+ };
33
+ }
34
+
35
+ function buildCodexPayload(projectName, options = {}) {
36
+ const { getSessionsByProject } = require('./codex-sessions');
37
+ const sessions = getSessionsByProject(projectName, { force: options.force === true });
38
+
39
+ return {
40
+ sessions,
41
+ totalSize: totalSizeOf(sessions),
42
+ aliases: getAliases(),
43
+ projectInfo: {
44
+ name: projectName,
45
+ fullPath: projectName,
46
+ path: projectName,
47
+ displayName: projectName
48
+ }
49
+ };
50
+ }
51
+
52
+ function buildGeminiPayload(projectHash, options = {}) {
53
+ const { getProjectSessions, getProjectPath } = require('./gemini-sessions');
54
+ const sessions = getProjectSessions(projectHash, { force: options.force === true });
55
+ const realPath = getProjectPath(projectHash, { force: options.force === true });
56
+ const displayName = realPath ? path.basename(realPath) : `Project ${projectHash.substring(0, 8)}`;
57
+
58
+ return {
59
+ sessions,
60
+ totalSize: totalSizeOf(sessions),
61
+ aliases: getAliases(),
62
+ projectInfo: {
63
+ name: projectHash,
64
+ fullPath: realPath || projectHash,
65
+ path: realPath || projectHash,
66
+ displayName
67
+ }
68
+ };
69
+ }
70
+
71
+ function buildOpenCodePayload(projectName, options = {}) {
72
+ const { getSessionsByProject, getProjects } = require('./opencode-sessions');
73
+ const sessions = getSessionsByProject(projectName, { force: options.force === true });
74
+ const firstDirectory = sessions.find(session => session.directory)?.directory;
75
+ const project = firstDirectory ? null : getProjects({ force: options.force === true }).find(p => p.name === projectName);
76
+ const fullPath = firstDirectory || project?.fullPath || projectName;
77
+
78
+ return {
79
+ sessions,
80
+ totalSize: totalSizeOf(sessions),
81
+ aliases: getAliases(),
82
+ projectInfo: {
83
+ name: projectName,
84
+ fullPath,
85
+ path: project?.path || fullPath,
86
+ displayName: project?.displayName || (fullPath ? path.basename(fullPath) : projectName)
87
+ }
88
+ };
89
+ }
90
+
91
+ function buildOmpPayload(projectName, options = {}) {
92
+ const { getProjects, getSessionsByProject } = require('./omp-sessions');
93
+ const sessions = getSessionsByProject(projectName, { force: options.force === true });
94
+ const firstDirectory = sessions.find(session => session.directory)?.directory;
95
+ let project = null;
96
+ try {
97
+ project = getProjects({ force: options.force === true }).find(p => p.name === projectName) || null;
98
+ } catch {
99
+ project = null;
100
+ }
101
+ const fullPath = project?.fullPath || project?.path || firstDirectory || projectName;
102
+
103
+ return {
104
+ sessions,
105
+ totalSize: totalSizeOf(sessions),
106
+ aliases: getAliases(),
107
+ projectInfo: {
108
+ name: projectName,
109
+ fullPath,
110
+ path: project?.path || fullPath,
111
+ displayName: project?.displayName || (fullPath ? path.basename(fullPath) : projectName)
112
+ }
113
+ };
114
+ }
115
+
116
+ async function buildPayload({ source, projectName, config, options }) {
117
+ const snapshotOptions = options || {};
118
+ switch (source) {
119
+ case 'claude':
120
+ return buildClaudePayload(projectName, config || {}, snapshotOptions);
121
+ case 'codex':
122
+ return buildCodexPayload(projectName, snapshotOptions);
123
+ case 'gemini':
124
+ return buildGeminiPayload(projectName, snapshotOptions);
125
+ case 'opencode':
126
+ return buildOpenCodePayload(projectName, snapshotOptions);
127
+ case 'omp':
128
+ return buildOmpPayload(projectName, snapshotOptions);
129
+ default:
130
+ throw new Error(`Unsupported session snapshot source: ${source}`);
131
+ }
132
+ }
133
+
134
+ function attachWorkerHandler() {
135
+ process.on('message', async (message) => {
136
+ try {
137
+ const value = await buildPayload(message || {});
138
+ if (process.send) {
139
+ process.send({ ok: true, value }, () => process.exit(0));
140
+ return;
141
+ }
142
+ } catch (error) {
143
+ if (process.send) {
144
+ process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
145
+ return;
146
+ }
147
+ }
148
+ process.exit(0);
149
+ });
150
+ }
151
+
152
+ if (process.env.CC_TOOL_SESSION_SNAPSHOT_WORKER === '1' || require.main === module) {
153
+ attachWorkerHandler();
154
+ }
155
+
156
+ module.exports = {
157
+ buildPayload
158
+ };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { fork } = require('child_process');
5
+ const {
6
+ getSnapshot,
7
+ invalidateSnapshot
8
+ } = require('./snapshot-cache');
9
+
10
+ const SESSION_SNAPSHOT_TTL_MS = 60 * 1000;
11
+ const SESSION_SNAPSHOT_DEFER_MS = process.env.NODE_ENV === 'test' ? 0 : 750;
12
+ const SESSION_SNAPSHOT_WORKER_TIMEOUT_MS = 120 * 1000;
13
+
14
+ function sessionListKey(source, projectName) {
15
+ return `sessions:list:${source}:${projectName}`;
16
+ }
17
+
18
+ function defaultProjectInfo(projectName) {
19
+ return {
20
+ name: projectName,
21
+ fullPath: projectName,
22
+ path: projectName,
23
+ displayName: projectName
24
+ };
25
+ }
26
+
27
+ function emptySessionList(projectName, extra = {}) {
28
+ return {
29
+ sessions: [],
30
+ totalSize: 0,
31
+ aliases: {},
32
+ projectInfo: defaultProjectInfo(projectName),
33
+ ...extra
34
+ };
35
+ }
36
+
37
+ function runSessionSnapshotWorker(source, projectName, config = {}, options = {}) {
38
+ if (process.env.NODE_ENV === 'test') {
39
+ const { buildPayload } = require('./session-snapshot-worker');
40
+ return Promise.resolve(buildPayload({ source, projectName, config, options }));
41
+ }
42
+
43
+ return new Promise((resolve, reject) => {
44
+ const workerPath = path.join(__dirname, 'session-snapshot-worker.js');
45
+ const child = fork(workerPath, [], {
46
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
47
+ env: {
48
+ ...process.env,
49
+ CC_TOOL_SESSION_SNAPSHOT_WORKER: '1'
50
+ }
51
+ });
52
+
53
+ let settled = false;
54
+ let stderr = '';
55
+ let timeout = null;
56
+
57
+ const finish = (error, value) => {
58
+ if (settled) return;
59
+ settled = true;
60
+ if (timeout) {
61
+ clearTimeout(timeout);
62
+ }
63
+ child.removeAllListeners();
64
+ if (!child.killed) {
65
+ child.kill();
66
+ }
67
+ if (error) {
68
+ reject(error);
69
+ } else {
70
+ resolve(value);
71
+ }
72
+ };
73
+
74
+ timeout = setTimeout(() => {
75
+ finish(new Error(`Session snapshot refresh timed out for ${source}/${projectName}`));
76
+ }, SESSION_SNAPSHOT_WORKER_TIMEOUT_MS);
77
+
78
+ child.stderr.on('data', (chunk) => {
79
+ stderr += chunk.toString();
80
+ if (stderr.length > 4096) {
81
+ stderr = stderr.slice(-4096);
82
+ }
83
+ });
84
+
85
+ child.on('message', (message) => {
86
+ if (!message || typeof message !== 'object') return;
87
+ if (message.ok) {
88
+ finish(null, message.value);
89
+ } else {
90
+ finish(new Error(message.error || `Session snapshot refresh failed for ${source}/${projectName}`));
91
+ }
92
+ });
93
+
94
+ child.on('error', (error) => finish(error));
95
+ child.on('exit', (code, signal) => {
96
+ if (settled) return;
97
+ const suffix = stderr ? `: ${stderr.trim()}` : '';
98
+ finish(new Error(`Session snapshot worker exited (${code || signal || 'unknown'})${suffix}`));
99
+ });
100
+
101
+ child.send({ source, projectName, config, options });
102
+ });
103
+ }
104
+
105
+ async function getSessionListSnapshot(source, projectName, {
106
+ fallbackValue,
107
+ refresh,
108
+ force = false
109
+ }) {
110
+ return getSnapshot(sessionListKey(source, projectName), {
111
+ ttlMs: SESSION_SNAPSHOT_TTL_MS,
112
+ fallbackValue: fallbackValue || emptySessionList(projectName),
113
+ refresh,
114
+ force: process.env.NODE_ENV === 'test' ? true : force,
115
+ backgroundOnMiss: process.env.NODE_ENV === 'test' ? false : !force,
116
+ deferMs: SESSION_SNAPSHOT_DEFER_MS
117
+ });
118
+ }
119
+
120
+ function invalidateSessionSnapshots(source, projectName = null) {
121
+ if (!source) return;
122
+ if (projectName) {
123
+ invalidateSnapshot(sessionListKey(source, projectName));
124
+ return;
125
+ }
126
+ invalidateSnapshot(`sessions:list:${source}:`);
127
+ }
128
+
129
+ module.exports = {
130
+ SESSION_SNAPSHOT_TTL_MS,
131
+ defaultProjectInfo,
132
+ emptySessionList,
133
+ getSessionListSnapshot,
134
+ invalidateSessionSnapshots,
135
+ runSessionSnapshotWorker,
136
+ sessionListKey
137
+ };
@@ -600,12 +600,14 @@ function scanSessionFileForMessagesAsync(filePath) {
600
600
  }
601
601
 
602
602
  // Get sessions for a project - async version
603
- async function getSessionsForProject(config, projectName) {
603
+ async function getSessionsForProject(config, projectName, options = {}) {
604
604
  // Check cache first
605
605
  const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
606
- const cached = globalCache.get(cacheKey);
607
- if (cached) {
608
- return cached;
606
+ if (!options.force) {
607
+ const cached = globalCache.get(cacheKey);
608
+ if (cached) {
609
+ return cached;
610
+ }
609
611
  }
610
612
 
611
613
  const projectConfig = { ...config, currentProject: projectName };