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.
- package/dist/web/assets/{Analytics-BmPIW7oK.js → Analytics-DnnufrQi.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CG4j4ua4.js → ConfigTemplates-BTVhMSfe.js} +1 -1
- package/dist/web/assets/Home-CqnBXFxj.css +1 -0
- package/dist/web/assets/Home-D03PEREx.js +1 -0
- package/dist/web/assets/{PluginManager-CCJ7mo35.js → PluginManager-BiMqEiyQ.js} +1 -1
- package/dist/web/assets/{ProjectList-DVUs3o8x.js → ProjectList-C6BA3V49.js} +1 -1
- package/dist/web/assets/{ProjectList-D3a0oRum.css → ProjectList-DQ5ZCJO2.css} +1 -1
- package/dist/web/assets/SessionList--9Bl6EhV.css +1 -0
- package/dist/web/assets/SessionList-DChG04jz.js +1 -0
- package/dist/web/assets/{SkillManager-DmOTHcQ7.js → SkillManager-DAy8k5CL.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DLVj0zlu.js → WorkspaceManager-Bs2D6R9j.js} +1 -1
- package/dist/web/assets/index-DnDBHGCC.js +2 -0
- package/dist/web/assets/index-U3TOywtB.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/api/codex-projects.js +7 -11
- package/src/server/api/codex-sessions.js +30 -21
- package/src/server/api/dashboard.js +24 -36
- package/src/server/api/gemini-projects.js +7 -11
- package/src/server/api/gemini-sessions.js +28 -26
- package/src/server/api/omp-projects.js +6 -11
- package/src/server/api/omp-sessions.js +31 -17
- package/src/server/api/opencode-projects.js +6 -11
- package/src/server/api/opencode-sessions.js +28 -21
- package/src/server/api/projects.js +7 -28
- package/src/server/api/sessions.js +32 -16
- package/src/server/services/channel-balance.js +2 -1
- package/src/server/services/codex-sessions.js +23 -10
- package/src/server/services/dashboard-snapshot-worker.js +216 -0
- package/src/server/services/gemini-sessions.js +15 -3
- package/src/server/services/omp-sessions.js +31 -11
- package/src/server/services/opencode-sessions.js +9 -6
- package/src/server/services/session-snapshot-worker.js +158 -0
- package/src/server/services/session-snapshots.js +137 -0
- package/src/server/services/sessions.js +6 -4
- package/src/server/services/snapshot-cache.js +77 -11
- package/dist/web/assets/Home-B9yAAZrD.js +0 -1
- package/dist/web/assets/Home-DSTUSxrT.css +0 -1
- package/dist/web/assets/SessionList-D12muXvx.css +0 -1
- package/dist/web/assets/SessionList-DwssBydw.js +0 -1
- package/dist/web/assets/index-BjdNYwSs.js +0 -2
- 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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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 (
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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 };
|