@zzusp/ccsm 1.0.1 → 1.0.2
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/LICENSE +21 -21
- package/README.md +236 -236
- package/bin/cli.mjs +52 -52
- package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
- package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
- package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
- package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
- package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
- package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
- package/dist/assets/index-CrWxV6sb.css +1 -0
- package/dist/assets/index-DTbWl1jb.js +11 -0
- package/dist/assets/index-DTbWl1jb.js.map +1 -0
- package/dist/assets/markdown-Bag5rX3T.js +30 -0
- package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
- package/dist/index.html +26 -26
- package/package.json +85 -83
- package/server/index.ts +130 -130
- package/server/lib/active-sessions.test.ts +119 -119
- package/server/lib/active-sessions.ts +95 -95
- package/server/lib/bundle.test.ts +182 -182
- package/server/lib/bundle.ts +86 -86
- package/server/lib/claude-paths.test.ts +126 -126
- package/server/lib/claude-paths.ts +43 -43
- package/server/lib/cleanup-suggestions.ts +131 -131
- package/server/lib/constants.ts +8 -8
- package/server/lib/delete-project.ts +100 -100
- package/server/lib/delete.test.ts +244 -244
- package/server/lib/delete.ts +192 -192
- package/server/lib/disk-usage.ts +81 -81
- package/server/lib/encode-cwd.ts +24 -24
- package/server/lib/export-bundle.ts +236 -236
- package/server/lib/export-import-bundle.test.ts +337 -337
- package/server/lib/fs-size.ts +38 -38
- package/server/lib/import-bundle.ts +488 -488
- package/server/lib/load-memory.ts +120 -120
- package/server/lib/load-session.ts +209 -209
- package/server/lib/modified-files.test.ts +280 -280
- package/server/lib/modified-files.ts +228 -228
- package/server/lib/open-folder.ts +47 -47
- package/server/lib/parse-jsonl.ts +160 -139
- package/server/lib/port.ts +23 -23
- package/server/lib/safe-id.test.ts +41 -41
- package/server/lib/safe-id.ts +6 -6
- package/server/lib/safe-remove.test.ts +73 -73
- package/server/lib/safe-remove.ts +25 -25
- package/server/lib/scan.ts +289 -286
- package/server/lib/search-all.ts +130 -130
- package/server/lib/search-session.ts +203 -203
- package/server/lib/system-tags.ts +20 -20
- package/server/lib/update.ts +67 -67
- package/server/lib/version.test.ts +39 -39
- package/server/lib/version.ts +117 -117
- package/server/routes/disk-cleanup.ts +54 -54
- package/server/routes/disk.ts +9 -9
- package/server/routes/import.ts +87 -87
- package/server/routes/projects.ts +104 -104
- package/server/routes/search.ts +79 -79
- package/server/routes/sessions.ts +130 -130
- package/server/routes/version.ts +34 -34
- package/server/types.ts +1 -1
- package/shared/constants.ts +7 -7
- package/shared/types.ts +513 -511
- package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
- package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
- package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
- package/dist/assets/index-7aMrnHJG.js +0 -7
- package/dist/assets/index-7aMrnHJG.js.map +0 -1
- package/dist/assets/index-BOeI_J4B.css +0 -1
package/server/lib/scan.ts
CHANGED
|
@@ -1,286 +1,289 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { PATHS } from './claude-paths.ts';
|
|
4
|
-
import { RECENT_ACTIVITY_WINDOW_MS } from './constants.ts';
|
|
5
|
-
import { decodeCwd } from './encode-cwd.ts';
|
|
6
|
-
import { dirSize, fileSize } from './fs-size.ts';
|
|
7
|
-
import { parseJsonlMeta } from './parse-jsonl.ts';
|
|
8
|
-
import { buildActiveSessionMap } from './active-sessions.ts';
|
|
9
|
-
import type { ProjectSummary, RelatedBytes, SessionSummary } from '../types.ts';
|
|
10
|
-
|
|
11
|
-
const JSONL_EXT = '.jsonl';
|
|
12
|
-
|
|
13
|
-
function listSessionIdsInProject(projectDir: string): string[] {
|
|
14
|
-
if (!fs.existsSync(projectDir)) return [];
|
|
15
|
-
const ids: string[] = [];
|
|
16
|
-
for (const ent of fs.readdirSync(projectDir, { withFileTypes: true })) {
|
|
17
|
-
if (ent.isFile() && ent.name.endsWith(JSONL_EXT)) {
|
|
18
|
-
ids.push(ent.name.slice(0, -JSONL_EXT.length));
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return ids;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function decodeProjectId(encoded: string, sampleCwd: string | null): {
|
|
25
|
-
decoded: string;
|
|
26
|
-
resolved: boolean;
|
|
27
|
-
} {
|
|
28
|
-
if (sampleCwd) return { decoded: sampleCwd, resolved: true };
|
|
29
|
-
const decoded = decodeCwd(encoded);
|
|
30
|
-
let resolved = false;
|
|
31
|
-
try {
|
|
32
|
-
resolved = fs.statSync(decoded).isDirectory();
|
|
33
|
-
} catch {
|
|
34
|
-
resolved = false;
|
|
35
|
-
}
|
|
36
|
-
return { decoded, resolved };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function resolveProjectCwd(
|
|
40
|
-
projectId: string,
|
|
41
|
-
): Promise<{ decoded: string; resolved: boolean } | null> {
|
|
42
|
-
const projectDir = path.join(PATHS.projects, projectId);
|
|
43
|
-
if (!fs.existsSync(projectDir)) return null;
|
|
44
|
-
|
|
45
|
-
const sessionIds = listSessionIdsInProject(projectDir);
|
|
46
|
-
let sampleCwd: string | null = null;
|
|
47
|
-
for (const id of sessionIds) {
|
|
48
|
-
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
49
|
-
if (!fs.existsSync(jsonlPath)) continue;
|
|
50
|
-
const meta = await parseJsonlMeta(jsonlPath);
|
|
51
|
-
if (meta.cwdFromMessages) {
|
|
52
|
-
sampleCwd = meta.cwdFromMessages;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return decodeProjectId(projectId, sampleCwd);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function listProjects(): Promise<ProjectSummary[]> {
|
|
60
|
-
if (!fs.existsSync(PATHS.projects)) return [];
|
|
61
|
-
const result: ProjectSummary[] = [];
|
|
62
|
-
|
|
63
|
-
for (const ent of fs.readdirSync(PATHS.projects, { withFileTypes: true })) {
|
|
64
|
-
if (!ent.isDirectory()) continue;
|
|
65
|
-
const projectId = ent.name;
|
|
66
|
-
const projectDir = path.join(PATHS.projects, projectId);
|
|
67
|
-
|
|
68
|
-
const sessionIds = listSessionIdsInProject(projectDir);
|
|
69
|
-
let sampleCwd: string | null = null;
|
|
70
|
-
let totalBytes = 0;
|
|
71
|
-
let lastActiveAt: string | null = null;
|
|
72
|
-
|
|
73
|
-
for (const id of sessionIds) {
|
|
74
|
-
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
75
|
-
const subdirPath = path.join(projectDir, id);
|
|
76
|
-
totalBytes += fileSize(jsonlPath);
|
|
77
|
-
totalBytes += dirSize(subdirPath);
|
|
78
|
-
totalBytes += dirSize(path.join(PATHS.fileHistory, id));
|
|
79
|
-
totalBytes += dirSize(path.join(PATHS.sessionEnv, id));
|
|
80
|
-
|
|
81
|
-
if (!sampleCwd && fs.existsSync(jsonlPath)) {
|
|
82
|
-
const meta = await parseJsonlMeta(jsonlPath);
|
|
83
|
-
sampleCwd = meta.cwdFromMessages;
|
|
84
|
-
if (meta.lastAt && (!lastActiveAt || meta.lastAt > lastActiveAt)) {
|
|
85
|
-
lastActiveAt = meta.lastAt;
|
|
86
|
-
}
|
|
87
|
-
} else if (fs.existsSync(jsonlPath)) {
|
|
88
|
-
try {
|
|
89
|
-
const mtime = fs.statSync(jsonlPath).mtime.toISOString();
|
|
90
|
-
if (!lastActiveAt || mtime > lastActiveAt) lastActiveAt = mtime;
|
|
91
|
-
} catch {
|
|
92
|
-
// ignore
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const { decoded, resolved } = decodeProjectId(projectId, sampleCwd);
|
|
98
|
-
|
|
99
|
-
result.push({
|
|
100
|
-
id: projectId,
|
|
101
|
-
encodedCwd: projectId,
|
|
102
|
-
decodedCwd: decoded,
|
|
103
|
-
cwdResolved: resolved,
|
|
104
|
-
sessionCount: sessionIds.length,
|
|
105
|
-
totalBytes,
|
|
106
|
-
lastActiveAt,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
result.sort((a, b) => {
|
|
111
|
-
const at = a.lastActiveAt ?? '';
|
|
112
|
-
const bt = b.lastActiveAt ?? '';
|
|
113
|
-
return bt.localeCompare(at);
|
|
114
|
-
});
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export async function listSessionsForProject(projectId: string): Promise<SessionSummary[]> {
|
|
119
|
-
const projectDir = path.join(PATHS.projects, projectId);
|
|
120
|
-
if (!fs.existsSync(projectDir)) return [];
|
|
121
|
-
|
|
122
|
-
const activeMap = buildActiveSessionMap();
|
|
123
|
-
const ids = listSessionIdsInProject(projectDir);
|
|
124
|
-
const out: SessionSummary[] = [];
|
|
125
|
-
|
|
126
|
-
for (const id of ids) {
|
|
127
|
-
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
128
|
-
const subdirPath = path.join(projectDir, id);
|
|
129
|
-
const fhPath = path.join(PATHS.fileHistory, id);
|
|
130
|
-
const sePath = path.join(PATHS.sessionEnv, id);
|
|
131
|
-
|
|
132
|
-
const related: RelatedBytes = {
|
|
133
|
-
jsonl: fileSize(jsonlPath),
|
|
134
|
-
subdir: dirSize(subdirPath),
|
|
135
|
-
fileHistory: dirSize(fhPath),
|
|
136
|
-
sessionEnv: dirSize(sePath),
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
let title = '(no jsonl)';
|
|
140
|
-
let customTitle: string | null = null;
|
|
141
|
-
let firstAt: string | null = null;
|
|
142
|
-
let lastAt: string | null = null;
|
|
143
|
-
let messageCount = 0;
|
|
144
|
-
let
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* active map
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PATHS } from './claude-paths.ts';
|
|
4
|
+
import { RECENT_ACTIVITY_WINDOW_MS } from './constants.ts';
|
|
5
|
+
import { decodeCwd } from './encode-cwd.ts';
|
|
6
|
+
import { dirSize, fileSize } from './fs-size.ts';
|
|
7
|
+
import { parseJsonlMeta } from './parse-jsonl.ts';
|
|
8
|
+
import { buildActiveSessionMap } from './active-sessions.ts';
|
|
9
|
+
import type { ProjectSummary, RelatedBytes, SessionSummary } from '../types.ts';
|
|
10
|
+
|
|
11
|
+
const JSONL_EXT = '.jsonl';
|
|
12
|
+
|
|
13
|
+
function listSessionIdsInProject(projectDir: string): string[] {
|
|
14
|
+
if (!fs.existsSync(projectDir)) return [];
|
|
15
|
+
const ids: string[] = [];
|
|
16
|
+
for (const ent of fs.readdirSync(projectDir, { withFileTypes: true })) {
|
|
17
|
+
if (ent.isFile() && ent.name.endsWith(JSONL_EXT)) {
|
|
18
|
+
ids.push(ent.name.slice(0, -JSONL_EXT.length));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return ids;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decodeProjectId(encoded: string, sampleCwd: string | null): {
|
|
25
|
+
decoded: string;
|
|
26
|
+
resolved: boolean;
|
|
27
|
+
} {
|
|
28
|
+
if (sampleCwd) return { decoded: sampleCwd, resolved: true };
|
|
29
|
+
const decoded = decodeCwd(encoded);
|
|
30
|
+
let resolved = false;
|
|
31
|
+
try {
|
|
32
|
+
resolved = fs.statSync(decoded).isDirectory();
|
|
33
|
+
} catch {
|
|
34
|
+
resolved = false;
|
|
35
|
+
}
|
|
36
|
+
return { decoded, resolved };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function resolveProjectCwd(
|
|
40
|
+
projectId: string,
|
|
41
|
+
): Promise<{ decoded: string; resolved: boolean } | null> {
|
|
42
|
+
const projectDir = path.join(PATHS.projects, projectId);
|
|
43
|
+
if (!fs.existsSync(projectDir)) return null;
|
|
44
|
+
|
|
45
|
+
const sessionIds = listSessionIdsInProject(projectDir);
|
|
46
|
+
let sampleCwd: string | null = null;
|
|
47
|
+
for (const id of sessionIds) {
|
|
48
|
+
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
49
|
+
if (!fs.existsSync(jsonlPath)) continue;
|
|
50
|
+
const meta = await parseJsonlMeta(jsonlPath);
|
|
51
|
+
if (meta.cwdFromMessages) {
|
|
52
|
+
sampleCwd = meta.cwdFromMessages;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return decodeProjectId(projectId, sampleCwd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listProjects(): Promise<ProjectSummary[]> {
|
|
60
|
+
if (!fs.existsSync(PATHS.projects)) return [];
|
|
61
|
+
const result: ProjectSummary[] = [];
|
|
62
|
+
|
|
63
|
+
for (const ent of fs.readdirSync(PATHS.projects, { withFileTypes: true })) {
|
|
64
|
+
if (!ent.isDirectory()) continue;
|
|
65
|
+
const projectId = ent.name;
|
|
66
|
+
const projectDir = path.join(PATHS.projects, projectId);
|
|
67
|
+
|
|
68
|
+
const sessionIds = listSessionIdsInProject(projectDir);
|
|
69
|
+
let sampleCwd: string | null = null;
|
|
70
|
+
let totalBytes = 0;
|
|
71
|
+
let lastActiveAt: string | null = null;
|
|
72
|
+
|
|
73
|
+
for (const id of sessionIds) {
|
|
74
|
+
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
75
|
+
const subdirPath = path.join(projectDir, id);
|
|
76
|
+
totalBytes += fileSize(jsonlPath);
|
|
77
|
+
totalBytes += dirSize(subdirPath);
|
|
78
|
+
totalBytes += dirSize(path.join(PATHS.fileHistory, id));
|
|
79
|
+
totalBytes += dirSize(path.join(PATHS.sessionEnv, id));
|
|
80
|
+
|
|
81
|
+
if (!sampleCwd && fs.existsSync(jsonlPath)) {
|
|
82
|
+
const meta = await parseJsonlMeta(jsonlPath);
|
|
83
|
+
sampleCwd = meta.cwdFromMessages;
|
|
84
|
+
if (meta.lastAt && (!lastActiveAt || meta.lastAt > lastActiveAt)) {
|
|
85
|
+
lastActiveAt = meta.lastAt;
|
|
86
|
+
}
|
|
87
|
+
} else if (fs.existsSync(jsonlPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const mtime = fs.statSync(jsonlPath).mtime.toISOString();
|
|
90
|
+
if (!lastActiveAt || mtime > lastActiveAt) lastActiveAt = mtime;
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { decoded, resolved } = decodeProjectId(projectId, sampleCwd);
|
|
98
|
+
|
|
99
|
+
result.push({
|
|
100
|
+
id: projectId,
|
|
101
|
+
encodedCwd: projectId,
|
|
102
|
+
decodedCwd: decoded,
|
|
103
|
+
cwdResolved: resolved,
|
|
104
|
+
sessionCount: sessionIds.length,
|
|
105
|
+
totalBytes,
|
|
106
|
+
lastActiveAt,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
result.sort((a, b) => {
|
|
111
|
+
const at = a.lastActiveAt ?? '';
|
|
112
|
+
const bt = b.lastActiveAt ?? '';
|
|
113
|
+
return bt.localeCompare(at);
|
|
114
|
+
});
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function listSessionsForProject(projectId: string): Promise<SessionSummary[]> {
|
|
119
|
+
const projectDir = path.join(PATHS.projects, projectId);
|
|
120
|
+
if (!fs.existsSync(projectDir)) return [];
|
|
121
|
+
|
|
122
|
+
const activeMap = buildActiveSessionMap();
|
|
123
|
+
const ids = listSessionIdsInProject(projectDir);
|
|
124
|
+
const out: SessionSummary[] = [];
|
|
125
|
+
|
|
126
|
+
for (const id of ids) {
|
|
127
|
+
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
128
|
+
const subdirPath = path.join(projectDir, id);
|
|
129
|
+
const fhPath = path.join(PATHS.fileHistory, id);
|
|
130
|
+
const sePath = path.join(PATHS.sessionEnv, id);
|
|
131
|
+
|
|
132
|
+
const related: RelatedBytes = {
|
|
133
|
+
jsonl: fileSize(jsonlPath),
|
|
134
|
+
subdir: dirSize(subdirPath),
|
|
135
|
+
fileHistory: dirSize(fhPath),
|
|
136
|
+
sessionEnv: dirSize(sePath),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
let title = '(no jsonl)';
|
|
140
|
+
let customTitle: string | null = null;
|
|
141
|
+
let firstAt: string | null = null;
|
|
142
|
+
let lastAt: string | null = null;
|
|
143
|
+
let messageCount = 0;
|
|
144
|
+
let errorCount = 0;
|
|
145
|
+
let lastTurnIncomplete = false;
|
|
146
|
+
|
|
147
|
+
if (fs.existsSync(jsonlPath)) {
|
|
148
|
+
const meta = await parseJsonlMeta(jsonlPath);
|
|
149
|
+
title = meta.title;
|
|
150
|
+
customTitle = meta.customTitle;
|
|
151
|
+
firstAt = meta.firstAt;
|
|
152
|
+
lastAt = meta.lastAt;
|
|
153
|
+
messageCount = meta.messageCount;
|
|
154
|
+
errorCount = meta.errorCount;
|
|
155
|
+
lastTurnIncomplete = meta.lastTurnIncomplete;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const livePid = activeMap.get(id) ?? null;
|
|
159
|
+
let isRecentlyActive = false;
|
|
160
|
+
if (fs.existsSync(jsonlPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const mtimeMs = fs.statSync(jsonlPath).mtimeMs;
|
|
163
|
+
isRecentlyActive = Date.now() - mtimeMs < RECENT_ACTIVITY_WINDOW_MS;
|
|
164
|
+
} catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
out.push({
|
|
170
|
+
id,
|
|
171
|
+
projectId,
|
|
172
|
+
title,
|
|
173
|
+
customTitle,
|
|
174
|
+
firstAt,
|
|
175
|
+
lastAt,
|
|
176
|
+
messageCount,
|
|
177
|
+
errorCount,
|
|
178
|
+
bytes: related.jsonl,
|
|
179
|
+
relatedBytes: related,
|
|
180
|
+
isLivePid: livePid !== null,
|
|
181
|
+
isRecentlyActive,
|
|
182
|
+
livePid,
|
|
183
|
+
// "Working" narrows "live" to actively-processing: a live PID, fresh file
|
|
184
|
+
// activity, and an unfinished last turn. The live-PID gate keeps a session
|
|
185
|
+
// that crashed mid-turn (file frozen with a trailing `user` record) from
|
|
186
|
+
// reading as "working".
|
|
187
|
+
isWorking: livePid !== null && isRecentlyActive && lastTurnIncomplete,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
out.sort((a, b) => (b.lastAt ?? '').localeCompare(a.lastAt ?? ''));
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface DiskScanSession {
|
|
196
|
+
id: string;
|
|
197
|
+
title: string;
|
|
198
|
+
customTitle: string | null;
|
|
199
|
+
lastAt: string | null;
|
|
200
|
+
relatedBytes: RelatedBytes;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface DiskScanProject {
|
|
204
|
+
id: string;
|
|
205
|
+
decodedCwd: string;
|
|
206
|
+
cwdResolved: boolean;
|
|
207
|
+
totalBytes: number;
|
|
208
|
+
sessionCount: number;
|
|
209
|
+
sessions: DiskScanSession[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 磁盘视图(disk-usage + cleanup-suggestions)专用的单遍扫描:逐会话算
|
|
214
|
+
* relatedBytes + 解析一次 jsonl meta,项目 totalBytes = 其会话之和。
|
|
215
|
+
*
|
|
216
|
+
* 刻意不做 live-PID / recently-active 探测:两个磁盘视图都不展示这些,而
|
|
217
|
+
* `listSessionsForProject` 会按项目各建一次 active map(Windows 下 = 一次
|
|
218
|
+
* `tasklist` spawn,~400-700ms),被这两个接口按项目数放大成几十次 tasklist,
|
|
219
|
+
* 是磁盘页加载慢的主因。size/meta 原语与 `listSessionsForProject` 复用,只去掉
|
|
220
|
+
* active map 与「listProjects + listSessionsForProject」之间重复的 size 遍历。
|
|
221
|
+
*/
|
|
222
|
+
export async function scanProjectsForDisk(): Promise<DiskScanProject[]> {
|
|
223
|
+
if (!fs.existsSync(PATHS.projects)) return [];
|
|
224
|
+
|
|
225
|
+
const projectIds = fs
|
|
226
|
+
.readdirSync(PATHS.projects, { withFileTypes: true })
|
|
227
|
+
.filter((ent) => ent.isDirectory())
|
|
228
|
+
.map((ent) => ent.name);
|
|
229
|
+
|
|
230
|
+
const result: DiskScanProject[] = [];
|
|
231
|
+
for (const projectId of projectIds) {
|
|
232
|
+
const projectDir = path.join(PATHS.projects, projectId);
|
|
233
|
+
const ids = listSessionIdsInProject(projectDir);
|
|
234
|
+
|
|
235
|
+
const scanned = await Promise.all(ids.map((id) => scanDiskSession(projectDir, id)));
|
|
236
|
+
|
|
237
|
+
let totalBytes = 0;
|
|
238
|
+
let sampleCwd: string | null = null;
|
|
239
|
+
const sessions: DiskScanSession[] = [];
|
|
240
|
+
for (const { session, cwdFromMessages } of scanned) {
|
|
241
|
+
const r = session.relatedBytes;
|
|
242
|
+
totalBytes += r.jsonl + r.subdir + r.fileHistory + r.sessionEnv;
|
|
243
|
+
if (!sampleCwd && cwdFromMessages) sampleCwd = cwdFromMessages;
|
|
244
|
+
sessions.push(session);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { decoded, resolved } = decodeProjectId(projectId, sampleCwd);
|
|
248
|
+
result.push({
|
|
249
|
+
id: projectId,
|
|
250
|
+
decodedCwd: decoded,
|
|
251
|
+
cwdResolved: resolved,
|
|
252
|
+
totalBytes,
|
|
253
|
+
sessionCount: ids.length,
|
|
254
|
+
sessions,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function scanDiskSession(
|
|
262
|
+
projectDir: string,
|
|
263
|
+
id: string,
|
|
264
|
+
): Promise<{ session: DiskScanSession; cwdFromMessages: string | null }> {
|
|
265
|
+
const jsonlPath = path.join(projectDir, `${id}${JSONL_EXT}`);
|
|
266
|
+
const relatedBytes: RelatedBytes = {
|
|
267
|
+
jsonl: fileSize(jsonlPath),
|
|
268
|
+
subdir: dirSize(path.join(projectDir, id)),
|
|
269
|
+
fileHistory: dirSize(path.join(PATHS.fileHistory, id)),
|
|
270
|
+
sessionEnv: dirSize(path.join(PATHS.sessionEnv, id)),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
let title = '(no jsonl)';
|
|
274
|
+
let customTitle: string | null = null;
|
|
275
|
+
let lastAt: string | null = null;
|
|
276
|
+
let cwdFromMessages: string | null = null;
|
|
277
|
+
if (fs.existsSync(jsonlPath)) {
|
|
278
|
+
const meta = await parseJsonlMeta(jsonlPath);
|
|
279
|
+
title = meta.title;
|
|
280
|
+
customTitle = meta.customTitle;
|
|
281
|
+
lastAt = meta.lastAt;
|
|
282
|
+
cwdFromMessages = meta.cwdFromMessages;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
session: { id, title, customTitle, lastAt, relatedBytes },
|
|
287
|
+
cwdFromMessages,
|
|
288
|
+
};
|
|
289
|
+
}
|