@zzusp/ccsm 1.0.1 → 1.0.3

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 (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +236 -236
  3. package/bin/cli.mjs +52 -52
  4. package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
  5. package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
  6. package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
  7. package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
  8. package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
  9. package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
  10. package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
  11. package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
  12. package/dist/assets/index-CrWxV6sb.css +1 -0
  13. package/dist/assets/index-DTbWl1jb.js +11 -0
  14. package/dist/assets/index-DTbWl1jb.js.map +1 -0
  15. package/dist/assets/markdown-Bag5rX3T.js +30 -0
  16. package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
  17. package/dist/index.html +26 -26
  18. package/package.json +81 -83
  19. package/server/index.ts +130 -130
  20. package/server/lib/active-sessions.test.ts +119 -119
  21. package/server/lib/active-sessions.ts +95 -95
  22. package/server/lib/bundle.test.ts +182 -182
  23. package/server/lib/bundle.ts +86 -86
  24. package/server/lib/claude-paths.test.ts +126 -126
  25. package/server/lib/claude-paths.ts +43 -43
  26. package/server/lib/cleanup-suggestions.ts +131 -131
  27. package/server/lib/constants.ts +8 -8
  28. package/server/lib/delete-project.ts +100 -100
  29. package/server/lib/delete.test.ts +244 -244
  30. package/server/lib/delete.ts +192 -192
  31. package/server/lib/disk-usage.ts +81 -81
  32. package/server/lib/encode-cwd.ts +24 -24
  33. package/server/lib/export-bundle.ts +236 -236
  34. package/server/lib/export-import-bundle.test.ts +337 -337
  35. package/server/lib/fs-size.ts +38 -38
  36. package/server/lib/import-bundle.ts +488 -488
  37. package/server/lib/load-memory.ts +120 -120
  38. package/server/lib/load-session.ts +209 -209
  39. package/server/lib/modified-files.test.ts +280 -280
  40. package/server/lib/modified-files.ts +228 -228
  41. package/server/lib/open-folder.ts +47 -47
  42. package/server/lib/parse-jsonl.ts +160 -139
  43. package/server/lib/port.ts +23 -23
  44. package/server/lib/safe-id.test.ts +41 -41
  45. package/server/lib/safe-id.ts +6 -6
  46. package/server/lib/safe-remove.test.ts +73 -73
  47. package/server/lib/safe-remove.ts +25 -25
  48. package/server/lib/scan.ts +289 -286
  49. package/server/lib/search-all.ts +130 -130
  50. package/server/lib/search-session.ts +203 -203
  51. package/server/lib/system-tags.ts +20 -20
  52. package/server/lib/update.ts +67 -67
  53. package/server/lib/version.test.ts +39 -39
  54. package/server/lib/version.ts +117 -117
  55. package/server/routes/disk-cleanup.ts +54 -54
  56. package/server/routes/disk.ts +9 -9
  57. package/server/routes/import.ts +87 -87
  58. package/server/routes/projects.ts +104 -104
  59. package/server/routes/search.ts +79 -79
  60. package/server/routes/sessions.ts +130 -130
  61. package/server/routes/version.ts +34 -34
  62. package/server/types.ts +1 -1
  63. package/shared/constants.ts +7 -7
  64. package/shared/types.ts +513 -511
  65. package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
  66. package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
  67. package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
  68. package/dist/assets/index-7aMrnHJG.js +0 -7
  69. package/dist/assets/index-7aMrnHJG.js.map +0 -1
  70. package/dist/assets/index-BOeI_J4B.css +0 -1
@@ -1,192 +1,192 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import readline from 'node:readline';
5
- import { isUnderClaudeRoot, PATHS } from './claude-paths.ts';
6
- import { RECENT_ACTIVITY_WINDOW_MS } from './constants.ts';
7
- import { dirSize, fileSize } from './fs-size.ts';
8
- import { isSafeId } from './safe-id.ts';
9
- import { safeRemove } from './safe-remove.ts';
10
- import {
11
- buildActiveSessionMap,
12
- readActivePidEntries,
13
- } from './active-sessions.ts';
14
- import type {
15
- DeletedItem,
16
- DeleteRequestItem,
17
- DeleteResult,
18
- RelatedBytes,
19
- SkippedItem,
20
- } from '../types.ts';
21
-
22
- export type { DeleteRequestItem, DeleteResult } from '../types.ts';
23
-
24
- const HISTORY_TMP_SUFFIX = '.tmp-clean';
25
-
26
- export async function deleteSessions(items: DeleteRequestItem[]): Promise<DeleteResult> {
27
- const liveMap = buildActiveSessionMap();
28
- const deleted: DeletedItem[] = [];
29
- const skipped: SkippedItem[] = [];
30
- const targetIds = new Set<string>();
31
-
32
- for (const item of items) {
33
- if (!isSafeId(item.projectId) || !isSafeId(item.sessionId)) {
34
- skipped.push({ ...item, reason: 'invalid id' });
35
- continue;
36
- }
37
-
38
- const projectDir = path.join(PATHS.projects, item.projectId);
39
- const jsonlPath = path.join(projectDir, `${item.sessionId}.jsonl`);
40
- const subdirPath = path.join(projectDir, item.sessionId);
41
- const fhPath = path.join(PATHS.fileHistory, item.sessionId);
42
- const sePath = path.join(PATHS.sessionEnv, item.sessionId);
43
-
44
- const escaped = [jsonlPath, subdirPath, fhPath, sePath].find(
45
- (p) => !isUnderClaudeRoot(p),
46
- );
47
- if (escaped) {
48
- skipped.push({ ...item, reason: `path escapes ~/.claude: ${escaped}` });
49
- continue;
50
- }
51
-
52
- if (liveMap.has(item.sessionId)) {
53
- skipped.push({
54
- ...item,
55
- reason: `live PID ${liveMap.get(item.sessionId)} owns this session`,
56
- });
57
- continue;
58
- }
59
-
60
- if (isRecentlyActive(jsonlPath)) {
61
- skipped.push({
62
- ...item,
63
- reason: 'jsonl modified within the last 5 minutes — could still be in use',
64
- });
65
- continue;
66
- }
67
-
68
- if (!fs.existsSync(jsonlPath) && !fs.existsSync(subdirPath)) {
69
- skipped.push({ ...item, reason: 'no files for this session' });
70
- continue;
71
- }
72
-
73
- const related: RelatedBytes = {
74
- jsonl: fileSize(jsonlPath),
75
- subdir: dirSize(subdirPath),
76
- fileHistory: dirSize(fhPath),
77
- sessionEnv: dirSize(sePath),
78
- };
79
- const cleaned: string[] = [];
80
-
81
- if (safeRemove(jsonlPath)) cleaned.push('projects/<id>.jsonl');
82
- if (safeRemove(subdirPath)) cleaned.push('projects/<id>/');
83
- if (safeRemove(fhPath)) cleaned.push('file-history/<id>/');
84
- if (safeRemove(sePath)) cleaned.push('session-env/<id>/');
85
-
86
- deleted.push({
87
- ...item,
88
- freedBytes:
89
- related.jsonl + related.subdir + related.fileHistory + related.sessionEnv,
90
- cleaned,
91
- relatedBytes: related,
92
- });
93
- targetIds.add(item.sessionId);
94
- }
95
-
96
- let historyLinesRemoved = 0;
97
- if (targetIds.size > 0) {
98
- historyLinesRemoved = await rewriteHistoryWithout(targetIds);
99
- cleanupDeadPidFiles(targetIds);
100
- }
101
-
102
- return { deleted, skipped, historyLinesRemoved };
103
- }
104
-
105
- function isRecentlyActive(jsonlPath: string): boolean {
106
- try {
107
- return Date.now() - fs.statSync(jsonlPath).mtimeMs < RECENT_ACTIVITY_WINDOW_MS;
108
- } catch {
109
- return false;
110
- }
111
- }
112
-
113
- async function rewriteHistoryWithout(sessionIds: Set<string>): Promise<number> {
114
- if (!fs.existsSync(PATHS.history)) return 0;
115
-
116
- const tmpPath = PATHS.history + HISTORY_TMP_SUFFIX;
117
- if (fs.existsSync(tmpPath)) fs.rmSync(tmpPath, { force: true });
118
-
119
- let removed = 0;
120
- try {
121
- const out = fs.createWriteStream(tmpPath, { encoding: 'utf8' });
122
- const rl = readline.createInterface({
123
- input: fs.createReadStream(PATHS.history, { encoding: 'utf8' }),
124
- crlfDelay: Infinity,
125
- });
126
-
127
- for await (const raw of rl) {
128
- if (!raw) {
129
- out.write(os.EOL);
130
- continue;
131
- }
132
- let drop = false;
133
- try {
134
- const obj = JSON.parse(raw) as { sessionId?: unknown };
135
- if (typeof obj.sessionId === 'string' && sessionIds.has(obj.sessionId)) {
136
- drop = true;
137
- }
138
- } catch {
139
- /* keep malformed lines */
140
- }
141
- if (drop) {
142
- removed += 1;
143
- } else {
144
- out.write(raw);
145
- out.write(os.EOL);
146
- }
147
- }
148
- await new Promise<void>((resolve, reject) => {
149
- out.end((err: Error | null | undefined) => (err ? reject(err) : resolve()));
150
- });
151
- } catch (err) {
152
- fs.rmSync(tmpPath, { force: true });
153
- throw err;
154
- }
155
-
156
- if (removed === 0) {
157
- fs.rmSync(tmpPath, { force: true });
158
- return 0;
159
- }
160
-
161
- // Windows-safe atomic-ish replace: backup original, swap tmp in, drop backup.
162
- // unlink + rename is the alternative but loses recoverability mid-failure.
163
- const backup = PATHS.history + '.bak-' + Date.now();
164
- fs.renameSync(PATHS.history, backup);
165
- try {
166
- fs.renameSync(tmpPath, PATHS.history);
167
- fs.rmSync(backup, { force: true });
168
- } catch (err) {
169
- if (fs.existsSync(backup)) {
170
- try {
171
- fs.renameSync(backup, PATHS.history);
172
- } catch {
173
- /* keep backup for manual recovery */
174
- }
175
- }
176
- fs.rmSync(tmpPath, { force: true });
177
- throw err;
178
- }
179
- return removed;
180
- }
181
-
182
- function cleanupDeadPidFiles(sessionIds: Set<string>): void {
183
- for (const entry of readActivePidEntries()) {
184
- if (!sessionIds.has(entry.sessionId)) continue;
185
- if (entry.alive) continue;
186
- try {
187
- fs.rmSync(entry.sourceFile, { force: true });
188
- } catch {
189
- /* ignore */
190
- }
191
- }
192
- }
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { isUnderClaudeRoot, PATHS } from './claude-paths.ts';
6
+ import { RECENT_ACTIVITY_WINDOW_MS } from './constants.ts';
7
+ import { dirSize, fileSize } from './fs-size.ts';
8
+ import { isSafeId } from './safe-id.ts';
9
+ import { safeRemove } from './safe-remove.ts';
10
+ import {
11
+ buildActiveSessionMap,
12
+ readActivePidEntries,
13
+ } from './active-sessions.ts';
14
+ import type {
15
+ DeletedItem,
16
+ DeleteRequestItem,
17
+ DeleteResult,
18
+ RelatedBytes,
19
+ SkippedItem,
20
+ } from '../types.ts';
21
+
22
+ export type { DeleteRequestItem, DeleteResult } from '../types.ts';
23
+
24
+ const HISTORY_TMP_SUFFIX = '.tmp-clean';
25
+
26
+ export async function deleteSessions(items: DeleteRequestItem[]): Promise<DeleteResult> {
27
+ const liveMap = buildActiveSessionMap();
28
+ const deleted: DeletedItem[] = [];
29
+ const skipped: SkippedItem[] = [];
30
+ const targetIds = new Set<string>();
31
+
32
+ for (const item of items) {
33
+ if (!isSafeId(item.projectId) || !isSafeId(item.sessionId)) {
34
+ skipped.push({ ...item, reason: 'invalid id' });
35
+ continue;
36
+ }
37
+
38
+ const projectDir = path.join(PATHS.projects, item.projectId);
39
+ const jsonlPath = path.join(projectDir, `${item.sessionId}.jsonl`);
40
+ const subdirPath = path.join(projectDir, item.sessionId);
41
+ const fhPath = path.join(PATHS.fileHistory, item.sessionId);
42
+ const sePath = path.join(PATHS.sessionEnv, item.sessionId);
43
+
44
+ const escaped = [jsonlPath, subdirPath, fhPath, sePath].find(
45
+ (p) => !isUnderClaudeRoot(p),
46
+ );
47
+ if (escaped) {
48
+ skipped.push({ ...item, reason: `path escapes ~/.claude: ${escaped}` });
49
+ continue;
50
+ }
51
+
52
+ if (liveMap.has(item.sessionId)) {
53
+ skipped.push({
54
+ ...item,
55
+ reason: `live PID ${liveMap.get(item.sessionId)} owns this session`,
56
+ });
57
+ continue;
58
+ }
59
+
60
+ if (isRecentlyActive(jsonlPath)) {
61
+ skipped.push({
62
+ ...item,
63
+ reason: 'jsonl modified within the last 5 minutes — could still be in use',
64
+ });
65
+ continue;
66
+ }
67
+
68
+ if (!fs.existsSync(jsonlPath) && !fs.existsSync(subdirPath)) {
69
+ skipped.push({ ...item, reason: 'no files for this session' });
70
+ continue;
71
+ }
72
+
73
+ const related: RelatedBytes = {
74
+ jsonl: fileSize(jsonlPath),
75
+ subdir: dirSize(subdirPath),
76
+ fileHistory: dirSize(fhPath),
77
+ sessionEnv: dirSize(sePath),
78
+ };
79
+ const cleaned: string[] = [];
80
+
81
+ if (safeRemove(jsonlPath)) cleaned.push('projects/<id>.jsonl');
82
+ if (safeRemove(subdirPath)) cleaned.push('projects/<id>/');
83
+ if (safeRemove(fhPath)) cleaned.push('file-history/<id>/');
84
+ if (safeRemove(sePath)) cleaned.push('session-env/<id>/');
85
+
86
+ deleted.push({
87
+ ...item,
88
+ freedBytes:
89
+ related.jsonl + related.subdir + related.fileHistory + related.sessionEnv,
90
+ cleaned,
91
+ relatedBytes: related,
92
+ });
93
+ targetIds.add(item.sessionId);
94
+ }
95
+
96
+ let historyLinesRemoved = 0;
97
+ if (targetIds.size > 0) {
98
+ historyLinesRemoved = await rewriteHistoryWithout(targetIds);
99
+ cleanupDeadPidFiles(targetIds);
100
+ }
101
+
102
+ return { deleted, skipped, historyLinesRemoved };
103
+ }
104
+
105
+ function isRecentlyActive(jsonlPath: string): boolean {
106
+ try {
107
+ return Date.now() - fs.statSync(jsonlPath).mtimeMs < RECENT_ACTIVITY_WINDOW_MS;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ async function rewriteHistoryWithout(sessionIds: Set<string>): Promise<number> {
114
+ if (!fs.existsSync(PATHS.history)) return 0;
115
+
116
+ const tmpPath = PATHS.history + HISTORY_TMP_SUFFIX;
117
+ if (fs.existsSync(tmpPath)) fs.rmSync(tmpPath, { force: true });
118
+
119
+ let removed = 0;
120
+ try {
121
+ const out = fs.createWriteStream(tmpPath, { encoding: 'utf8' });
122
+ const rl = readline.createInterface({
123
+ input: fs.createReadStream(PATHS.history, { encoding: 'utf8' }),
124
+ crlfDelay: Infinity,
125
+ });
126
+
127
+ for await (const raw of rl) {
128
+ if (!raw) {
129
+ out.write(os.EOL);
130
+ continue;
131
+ }
132
+ let drop = false;
133
+ try {
134
+ const obj = JSON.parse(raw) as { sessionId?: unknown };
135
+ if (typeof obj.sessionId === 'string' && sessionIds.has(obj.sessionId)) {
136
+ drop = true;
137
+ }
138
+ } catch {
139
+ /* keep malformed lines */
140
+ }
141
+ if (drop) {
142
+ removed += 1;
143
+ } else {
144
+ out.write(raw);
145
+ out.write(os.EOL);
146
+ }
147
+ }
148
+ await new Promise<void>((resolve, reject) => {
149
+ out.end((err: Error | null | undefined) => (err ? reject(err) : resolve()));
150
+ });
151
+ } catch (err) {
152
+ fs.rmSync(tmpPath, { force: true });
153
+ throw err;
154
+ }
155
+
156
+ if (removed === 0) {
157
+ fs.rmSync(tmpPath, { force: true });
158
+ return 0;
159
+ }
160
+
161
+ // Windows-safe atomic-ish replace: backup original, swap tmp in, drop backup.
162
+ // unlink + rename is the alternative but loses recoverability mid-failure.
163
+ const backup = PATHS.history + '.bak-' + Date.now();
164
+ fs.renameSync(PATHS.history, backup);
165
+ try {
166
+ fs.renameSync(tmpPath, PATHS.history);
167
+ fs.rmSync(backup, { force: true });
168
+ } catch (err) {
169
+ if (fs.existsSync(backup)) {
170
+ try {
171
+ fs.renameSync(backup, PATHS.history);
172
+ } catch {
173
+ /* keep backup for manual recovery */
174
+ }
175
+ }
176
+ fs.rmSync(tmpPath, { force: true });
177
+ throw err;
178
+ }
179
+ return removed;
180
+ }
181
+
182
+ function cleanupDeadPidFiles(sessionIds: Set<string>): void {
183
+ for (const entry of readActivePidEntries()) {
184
+ if (!sessionIds.has(entry.sessionId)) continue;
185
+ if (entry.alive) continue;
186
+ try {
187
+ fs.rmSync(entry.sourceFile, { force: true });
188
+ } catch {
189
+ /* ignore */
190
+ }
191
+ }
192
+ }
@@ -1,81 +1,81 @@
1
- import { scanProjectsForDisk } from './scan.ts';
2
- import type {
3
- DiskUsage,
4
- DiskUsageMonthRow,
5
- DiskUsageProjectRow,
6
- DiskUsageTopSession,
7
- RelatedBytes,
8
- } from '../types.ts';
9
-
10
- const TOP_N = 20;
11
-
12
- export async function computeDiskUsage(): Promise<DiskUsage> {
13
- const projects = await scanProjectsForDisk();
14
-
15
- const byProject: DiskUsageProjectRow[] = [];
16
- const monthMap = new Map<string, { bytes: number; count: number }>();
17
- const flat: Array<{
18
- projectId: string;
19
- sessionId: string;
20
- title: string;
21
- customTitle: string | null;
22
- bytes: number;
23
- lastAt: string | null;
24
- }> = [];
25
-
26
- for (const p of projects) {
27
- byProject.push({
28
- projectId: p.id,
29
- decodedCwd: p.decodedCwd,
30
- totalBytes: p.totalBytes,
31
- sessionCount: p.sessionCount,
32
- });
33
-
34
- for (const s of p.sessions) {
35
- const total = sessionTotal(s.relatedBytes);
36
- const month = s.lastAt ? s.lastAt.slice(0, 7) : 'unknown';
37
- const acc = monthMap.get(month) ?? { bytes: 0, count: 0 };
38
- acc.bytes += total;
39
- acc.count += 1;
40
- monthMap.set(month, acc);
41
- flat.push({
42
- projectId: p.id,
43
- sessionId: s.id,
44
- title: s.title,
45
- customTitle: s.customTitle,
46
- bytes: total,
47
- lastAt: s.lastAt,
48
- });
49
- }
50
- }
51
-
52
- byProject.sort((a, b) => b.totalBytes - a.totalBytes);
53
-
54
- const byMonth: DiskUsageMonthRow[] = [...monthMap.entries()]
55
- .map(([month, v]) => ({ month, totalBytes: v.bytes, sessionCount: v.count }))
56
- .sort((a, b) => a.month.localeCompare(b.month));
57
-
58
- const topSessions: DiskUsageTopSession[] = flat
59
- .sort((a, b) => b.bytes - a.bytes)
60
- .slice(0, TOP_N)
61
- .map((f) => ({
62
- projectId: f.projectId,
63
- sessionId: f.sessionId,
64
- title: f.title,
65
- customTitle: f.customTitle,
66
- totalBytes: f.bytes,
67
- lastAt: f.lastAt,
68
- }));
69
-
70
- return {
71
- byProject,
72
- byMonth,
73
- topSessions,
74
- totalBytes: byProject.reduce((acc, r) => acc + r.totalBytes, 0),
75
- totalSessions: flat.length,
76
- };
77
- }
78
-
79
- function sessionTotal(r: RelatedBytes): number {
80
- return r.jsonl + r.subdir + r.fileHistory + r.sessionEnv;
81
- }
1
+ import { scanProjectsForDisk } from './scan.ts';
2
+ import type {
3
+ DiskUsage,
4
+ DiskUsageMonthRow,
5
+ DiskUsageProjectRow,
6
+ DiskUsageTopSession,
7
+ RelatedBytes,
8
+ } from '../types.ts';
9
+
10
+ const TOP_N = 20;
11
+
12
+ export async function computeDiskUsage(): Promise<DiskUsage> {
13
+ const projects = await scanProjectsForDisk();
14
+
15
+ const byProject: DiskUsageProjectRow[] = [];
16
+ const monthMap = new Map<string, { bytes: number; count: number }>();
17
+ const flat: Array<{
18
+ projectId: string;
19
+ sessionId: string;
20
+ title: string;
21
+ customTitle: string | null;
22
+ bytes: number;
23
+ lastAt: string | null;
24
+ }> = [];
25
+
26
+ for (const p of projects) {
27
+ byProject.push({
28
+ projectId: p.id,
29
+ decodedCwd: p.decodedCwd,
30
+ totalBytes: p.totalBytes,
31
+ sessionCount: p.sessionCount,
32
+ });
33
+
34
+ for (const s of p.sessions) {
35
+ const total = sessionTotal(s.relatedBytes);
36
+ const month = s.lastAt ? s.lastAt.slice(0, 7) : 'unknown';
37
+ const acc = monthMap.get(month) ?? { bytes: 0, count: 0 };
38
+ acc.bytes += total;
39
+ acc.count += 1;
40
+ monthMap.set(month, acc);
41
+ flat.push({
42
+ projectId: p.id,
43
+ sessionId: s.id,
44
+ title: s.title,
45
+ customTitle: s.customTitle,
46
+ bytes: total,
47
+ lastAt: s.lastAt,
48
+ });
49
+ }
50
+ }
51
+
52
+ byProject.sort((a, b) => b.totalBytes - a.totalBytes);
53
+
54
+ const byMonth: DiskUsageMonthRow[] = [...monthMap.entries()]
55
+ .map(([month, v]) => ({ month, totalBytes: v.bytes, sessionCount: v.count }))
56
+ .sort((a, b) => a.month.localeCompare(b.month));
57
+
58
+ const topSessions: DiskUsageTopSession[] = flat
59
+ .sort((a, b) => b.bytes - a.bytes)
60
+ .slice(0, TOP_N)
61
+ .map((f) => ({
62
+ projectId: f.projectId,
63
+ sessionId: f.sessionId,
64
+ title: f.title,
65
+ customTitle: f.customTitle,
66
+ totalBytes: f.bytes,
67
+ lastAt: f.lastAt,
68
+ }));
69
+
70
+ return {
71
+ byProject,
72
+ byMonth,
73
+ topSessions,
74
+ totalBytes: byProject.reduce((acc, r) => acc + r.totalBytes, 0),
75
+ totalSessions: flat.length,
76
+ };
77
+ }
78
+
79
+ function sessionTotal(r: RelatedBytes): number {
80
+ return r.jsonl + r.subdir + r.fileHistory + r.sessionEnv;
81
+ }
@@ -1,24 +1,24 @@
1
- import path from 'node:path';
2
-
3
- const WIN_DRIVE_DOUBLE_DASH = /^([A-Za-z])--/;
4
-
5
- export function decodeCwd(encoded: string): string {
6
- if (WIN_DRIVE_DOUBLE_DASH.test(encoded)) {
7
- const drive = encoded[0]!.toUpperCase();
8
- const rest = encoded.slice(3).replace(/-/g, '\\');
9
- return `${drive}:\\${rest}`;
10
- }
11
- if (encoded.startsWith('-')) {
12
- return '/' + encoded.slice(1).replace(/-/g, '/');
13
- }
14
- return encoded;
15
- }
16
-
17
- export function encodeCwd(cwd: string): string {
18
- if (path.isAbsolute(cwd) && /^[A-Za-z]:[\\/]/.test(cwd)) {
19
- const drive = cwd[0]!.toUpperCase();
20
- const rest = cwd.slice(3).replace(/[\\/]/g, '-');
21
- return `${drive}--${rest}`;
22
- }
23
- return cwd.replace(/\//g, '-');
24
- }
1
+ import path from 'node:path';
2
+
3
+ const WIN_DRIVE_DOUBLE_DASH = /^([A-Za-z])--/;
4
+
5
+ export function decodeCwd(encoded: string): string {
6
+ if (WIN_DRIVE_DOUBLE_DASH.test(encoded)) {
7
+ const drive = encoded[0]!.toUpperCase();
8
+ const rest = encoded.slice(3).replace(/-/g, '\\');
9
+ return `${drive}:\\${rest}`;
10
+ }
11
+ if (encoded.startsWith('-')) {
12
+ return '/' + encoded.slice(1).replace(/-/g, '/');
13
+ }
14
+ return encoded;
15
+ }
16
+
17
+ export function encodeCwd(cwd: string): string {
18
+ if (path.isAbsolute(cwd) && /^[A-Za-z]:[\\/]/.test(cwd)) {
19
+ const drive = cwd[0]!.toUpperCase();
20
+ const rest = cwd.slice(3).replace(/[\\/]/g, '-');
21
+ return `${drive}--${rest}`;
22
+ }
23
+ return cwd.replace(/\//g, '-');
24
+ }