@cleocode/core 2026.4.99 → 2026.4.100

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.
@@ -0,0 +1,669 @@
1
+ // packages/core/src/gc/daemon.ts
2
+ import { spawn } from "node:child_process";
3
+ import { createWriteStream } from "node:fs";
4
+ import { mkdir as mkdir2 } from "node:fs/promises";
5
+ import { join as join3 } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import cron from "node-cron";
8
+
9
+ // packages/core/src/gc/runner.ts
10
+ import { lstat, readdir, rm, stat } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { join as join2 } from "node:path";
13
+
14
+ // node_modules/.pnpm/check-disk-space@3.4.0/node_modules/check-disk-space/dist/check-disk-space.mjs
15
+ import { execFile } from "node:child_process";
16
+ import { access } from "node:fs/promises";
17
+ import { release } from "node:os";
18
+ import { normalize, sep } from "node:path";
19
+ import { platform } from "node:process";
20
+ import { promisify } from "node:util";
21
+ var InvalidPathError = class _InvalidPathError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "InvalidPathError";
25
+ Object.setPrototypeOf(this, _InvalidPathError.prototype);
26
+ }
27
+ };
28
+ var NoMatchError = class _NoMatchError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "NoMatchError";
32
+ Object.setPrototypeOf(this, _NoMatchError.prototype);
33
+ }
34
+ };
35
+ async function isDirectoryExisting(directoryPath, dependencies) {
36
+ try {
37
+ await dependencies.fsAccess(directoryPath);
38
+ return Promise.resolve(true);
39
+ } catch (error) {
40
+ return Promise.resolve(false);
41
+ }
42
+ }
43
+ async function getFirstExistingParentPath(directoryPath, dependencies) {
44
+ let parentDirectoryPath = directoryPath;
45
+ let parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);
46
+ while (!parentDirectoryFound) {
47
+ parentDirectoryPath = dependencies.pathNormalize(parentDirectoryPath + "/..");
48
+ parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);
49
+ }
50
+ return parentDirectoryPath;
51
+ }
52
+ async function hasPowerShell3(dependencies) {
53
+ const major = parseInt(dependencies.release.split(".")[0], 10);
54
+ if (major <= 6) {
55
+ return false;
56
+ }
57
+ try {
58
+ await dependencies.cpExecFile("where", ["powershell"], { windowsHide: true });
59
+ return true;
60
+ } catch (error) {
61
+ return false;
62
+ }
63
+ }
64
+ function checkDiskSpace(directoryPath, dependencies = {
65
+ platform,
66
+ release: release(),
67
+ fsAccess: access,
68
+ pathNormalize: normalize,
69
+ pathSep: sep,
70
+ cpExecFile: promisify(execFile)
71
+ }) {
72
+ function mapOutput(stdout, filter, mapping, coefficient) {
73
+ const parsed = stdout.split("\n").map((line) => line.trim()).filter((line) => line.length !== 0).slice(1).map((line) => line.split(/\s+(?=[\d/])/));
74
+ const filtered = parsed.filter(filter);
75
+ if (filtered.length === 0) {
76
+ throw new NoMatchError();
77
+ }
78
+ const diskData = filtered[0];
79
+ return {
80
+ diskPath: diskData[mapping.diskPath],
81
+ free: parseInt(diskData[mapping.free], 10) * coefficient,
82
+ size: parseInt(diskData[mapping.size], 10) * coefficient
83
+ };
84
+ }
85
+ async function check(cmd, filter, mapping, coefficient = 1) {
86
+ const [file, ...args] = cmd;
87
+ if (file === void 0) {
88
+ return Promise.reject(new Error("cmd must contain at least one item"));
89
+ }
90
+ try {
91
+ const { stdout } = await dependencies.cpExecFile(file, args, { windowsHide: true });
92
+ return mapOutput(stdout, filter, mapping, coefficient);
93
+ } catch (error) {
94
+ return Promise.reject(error);
95
+ }
96
+ }
97
+ async function checkWin32(directoryPath2) {
98
+ if (directoryPath2.charAt(1) !== ":") {
99
+ return Promise.reject(new InvalidPathError(`The following path is invalid (should be X:\\...): ${directoryPath2}`));
100
+ }
101
+ const powershellCmd = [
102
+ "powershell",
103
+ "Get-CimInstance -ClassName Win32_LogicalDisk | Select-Object Caption, FreeSpace, Size"
104
+ ];
105
+ const wmicCmd = [
106
+ "wmic",
107
+ "logicaldisk",
108
+ "get",
109
+ "size,freespace,caption"
110
+ ];
111
+ const cmd = await hasPowerShell3(dependencies) ? powershellCmd : wmicCmd;
112
+ return check(cmd, (driveData) => {
113
+ const driveLetter = driveData[0];
114
+ return directoryPath2.toUpperCase().startsWith(driveLetter.toUpperCase());
115
+ }, {
116
+ diskPath: 0,
117
+ free: 1,
118
+ size: 2
119
+ });
120
+ }
121
+ async function checkUnix(directoryPath2) {
122
+ if (!dependencies.pathNormalize(directoryPath2).startsWith(dependencies.pathSep)) {
123
+ return Promise.reject(new InvalidPathError(`The following path is invalid (should start by ${dependencies.pathSep}): ${directoryPath2}`));
124
+ }
125
+ const pathToCheck = await getFirstExistingParentPath(directoryPath2, dependencies);
126
+ return check(
127
+ [
128
+ "df",
129
+ "-Pk",
130
+ "--",
131
+ pathToCheck
132
+ ],
133
+ () => true,
134
+ // We should only get one line, so we did not need to filter
135
+ {
136
+ diskPath: 5,
137
+ free: 3,
138
+ size: 1
139
+ },
140
+ 1024
141
+ );
142
+ }
143
+ if (dependencies.platform === "win32") {
144
+ return checkWin32(directoryPath);
145
+ }
146
+ return checkUnix(directoryPath);
147
+ }
148
+
149
+ // packages/core/src/gc/state.ts
150
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
151
+ import { dirname, join } from "node:path";
152
+ var GC_STATE_SCHEMA_VERSION = "1.0";
153
+ var DEFAULT_GC_STATE = {
154
+ schemaVersion: GC_STATE_SCHEMA_VERSION,
155
+ lastRunAt: null,
156
+ lastRunResult: null,
157
+ lastRunBytesFreed: 0,
158
+ pendingPrune: null,
159
+ consecutiveFailures: 0,
160
+ diskThresholdBreached: false,
161
+ lastDiskUsedPct: null,
162
+ escalationNeeded: false,
163
+ escalationReason: null,
164
+ daemonPid: null,
165
+ daemonStartedAt: null
166
+ };
167
+ async function readGCState(statePath) {
168
+ try {
169
+ const raw = await readFile(statePath, "utf-8");
170
+ const parsed = JSON.parse(raw);
171
+ return { ...DEFAULT_GC_STATE, ...parsed };
172
+ } catch {
173
+ return { ...DEFAULT_GC_STATE };
174
+ }
175
+ }
176
+ async function writeGCState(statePath, state) {
177
+ const dir = dirname(statePath);
178
+ await mkdir(dir, { recursive: true });
179
+ const tmpPath = join(dir, `.gc-state-${process.pid}.tmp`);
180
+ const json = JSON.stringify(state, null, 2);
181
+ await writeFile(tmpPath, json, "utf-8");
182
+ await rename(tmpPath, statePath);
183
+ }
184
+ async function patchGCState(statePath, patch) {
185
+ const current = await readGCState(statePath);
186
+ const updated = { ...current, ...patch };
187
+ await writeGCState(statePath, updated);
188
+ return updated;
189
+ }
190
+
191
+ // packages/core/src/gc/runner.ts
192
+ var checkDiskSpace2 = checkDiskSpace;
193
+ var DISK_THRESHOLDS = {
194
+ WATCH: 70,
195
+ WARN: 85,
196
+ URGENT: 90,
197
+ EMERGENCY: 95
198
+ };
199
+ function classifyDiskTier(pct) {
200
+ if (pct >= DISK_THRESHOLDS.EMERGENCY) return "emergency";
201
+ if (pct >= DISK_THRESHOLDS.URGENT) return "urgent";
202
+ if (pct >= DISK_THRESHOLDS.WARN) return "warn";
203
+ if (pct >= DISK_THRESHOLDS.WATCH) return "watch";
204
+ return "ok";
205
+ }
206
+ function retentionMs(tier) {
207
+ switch (tier) {
208
+ case "emergency":
209
+ return 1 * 24 * 60 * 60 * 1e3;
210
+ // 1 day
211
+ case "urgent":
212
+ return 3 * 24 * 60 * 60 * 1e3;
213
+ // 3 days
214
+ case "warn":
215
+ return 7 * 24 * 60 * 60 * 1e3;
216
+ // 7 days
217
+ default:
218
+ return 30 * 24 * 60 * 60 * 1e3;
219
+ }
220
+ }
221
+ async function getPathBytes(targetPath) {
222
+ try {
223
+ const info = await lstat(targetPath);
224
+ if (info.isFile()) return info.size;
225
+ if (!info.isDirectory()) return 0;
226
+ const entries = await readdir(targetPath, { withFileTypes: true });
227
+ let total = 0;
228
+ for (const entry of entries) {
229
+ total += await getPathBytes(join2(targetPath, entry.name));
230
+ }
231
+ return total;
232
+ } catch {
233
+ return 0;
234
+ }
235
+ }
236
+ async function idempotentRm(targetPath) {
237
+ try {
238
+ await rm(targetPath, { recursive: true, force: true });
239
+ } catch (err) {
240
+ const nodeErr = err;
241
+ if (nodeErr.code === "ENOENT") return;
242
+ throw err;
243
+ }
244
+ }
245
+ async function gatherPruneCandidates(maxAgeMs, projectsDir) {
246
+ const resolvedProjectsDir = projectsDir ?? join2(homedir(), ".claude", "projects");
247
+ const candidates = [];
248
+ const now = Date.now();
249
+ let projectSlugs;
250
+ try {
251
+ const entries = await readdir(resolvedProjectsDir, { withFileTypes: true });
252
+ projectSlugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
253
+ } catch {
254
+ return candidates;
255
+ }
256
+ for (const slug of projectSlugs) {
257
+ const slugDir = join2(resolvedProjectsDir, slug);
258
+ let slugEntries;
259
+ try {
260
+ slugEntries = await readdir(slugDir, { withFileTypes: true });
261
+ } catch {
262
+ continue;
263
+ }
264
+ for (const entry of slugEntries) {
265
+ const entryPath = join2(slugDir, entry.name);
266
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
267
+ try {
268
+ const info = await stat(entryPath);
269
+ const ageMs = now - info.mtimeMs;
270
+ if (ageMs > maxAgeMs) {
271
+ candidates.push(entryPath);
272
+ }
273
+ } catch {
274
+ }
275
+ } else if (entry.isDirectory()) {
276
+ try {
277
+ const info = await stat(entryPath);
278
+ const ageMs = now - info.mtimeMs;
279
+ if (ageMs > maxAgeMs) {
280
+ candidates.push(entryPath);
281
+ }
282
+ } catch {
283
+ }
284
+ }
285
+ }
286
+ }
287
+ return candidates;
288
+ }
289
+ async function runGC(opts = {}) {
290
+ const cleoDir = opts.cleoDir ?? join2(homedir(), ".cleo");
291
+ const statePath = join2(cleoDir, "gc-state.json");
292
+ const dryRun = opts.dryRun ?? false;
293
+ const projectsDir = opts.projectsDir;
294
+ const initialState = await readGCState(statePath);
295
+ const resumePaths = opts.resumeFrom ?? initialState.pendingPrune ?? [];
296
+ let diskUsedPct = 0;
297
+ try {
298
+ const { free, size } = await checkDiskSpace2(cleoDir);
299
+ diskUsedPct = size > 0 ? (size - free) / size * 100 : 0;
300
+ } catch {
301
+ diskUsedPct = 0;
302
+ }
303
+ const tier = classifyDiskTier(diskUsedPct);
304
+ const maxAgeMs = retentionMs(tier);
305
+ const candidatesFromScan = resumePaths.length > 0 ? resumePaths : await gatherPruneCandidates(maxAgeMs, projectsDir);
306
+ if (!dryRun && candidatesFromScan.length > 0) {
307
+ await patchGCState(statePath, { pendingPrune: candidatesFromScan });
308
+ }
309
+ const pruned = [];
310
+ let bytesFreed = 0;
311
+ const remaining = [...candidatesFromScan];
312
+ for (const candidatePath of candidatesFromScan) {
313
+ const bytes = await getPathBytes(candidatePath);
314
+ if (dryRun) {
315
+ pruned.push({ path: candidatePath, bytes });
316
+ bytesFreed += bytes;
317
+ continue;
318
+ }
319
+ try {
320
+ await idempotentRm(candidatePath);
321
+ pruned.push({ path: candidatePath, bytes });
322
+ bytesFreed += bytes;
323
+ const idx = remaining.indexOf(candidatePath);
324
+ if (idx !== -1) remaining.splice(idx, 1);
325
+ await patchGCState(statePath, {
326
+ pendingPrune: remaining.length > 0 ? remaining : null
327
+ });
328
+ } catch {
329
+ }
330
+ }
331
+ const escalationSet = tier === "warn" || tier === "urgent" || tier === "emergency";
332
+ let escalationReason = null;
333
+ if (escalationSet) {
334
+ escalationReason = `Disk at ${diskUsedPct.toFixed(1)}% (${tier.toUpperCase()}): ${pruned.length} paths pruned, ${bytesFreed} bytes freed`;
335
+ }
336
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
337
+ if (!dryRun) {
338
+ await patchGCState(statePath, {
339
+ lastRunAt: completedAt,
340
+ lastRunResult: remaining.length === 0 ? "success" : "partial",
341
+ lastRunBytesFreed: bytesFreed,
342
+ pendingPrune: remaining.length > 0 ? remaining : null,
343
+ consecutiveFailures: remaining.length > 0 ? initialState.consecutiveFailures + 1 : 0,
344
+ diskThresholdBreached: diskUsedPct >= DISK_THRESHOLDS.WATCH,
345
+ lastDiskUsedPct: diskUsedPct,
346
+ escalationNeeded: escalationSet || initialState.escalationNeeded,
347
+ escalationReason: escalationReason ?? initialState.escalationReason
348
+ });
349
+ }
350
+ return {
351
+ diskUsedPct,
352
+ threshold: tier,
353
+ pruned,
354
+ bytesFreed,
355
+ escalationSet,
356
+ escalationReason,
357
+ completedAt
358
+ };
359
+ }
360
+
361
+ // packages/core/src/gc/daemon.ts
362
+ var GC_CRON_EXPR = "0 3 * * *";
363
+ var GC_INTERVAL_MS = 24 * 60 * 60 * 1e3;
364
+ async function bootstrapDaemon(cleoDir) {
365
+ const statePath = join3(cleoDir, "gc-state.json");
366
+ await patchGCState(statePath, {
367
+ daemonPid: process.pid,
368
+ daemonStartedAt: (/* @__PURE__ */ new Date()).toISOString()
369
+ });
370
+ const state = await readGCState(statePath);
371
+ if (state.pendingPrune && state.pendingPrune.length > 0) {
372
+ try {
373
+ await runGC({ cleoDir, resumeFrom: state.pendingPrune });
374
+ } catch {
375
+ }
376
+ }
377
+ const lastRunTs = state.lastRunAt ? new Date(state.lastRunAt).getTime() : 0;
378
+ const elapsed = Date.now() - lastRunTs;
379
+ if (elapsed > GC_INTERVAL_MS) {
380
+ try {
381
+ await runGC({ cleoDir });
382
+ } catch {
383
+ }
384
+ }
385
+ cron.schedule(
386
+ GC_CRON_EXPR,
387
+ async () => {
388
+ try {
389
+ await runGC({ cleoDir });
390
+ } catch {
391
+ const state2 = await readGCState(statePath);
392
+ await patchGCState(statePath, {
393
+ consecutiveFailures: state2.consecutiveFailures + 1,
394
+ lastRunResult: "failed",
395
+ escalationNeeded: state2.consecutiveFailures + 1 >= 3,
396
+ escalationReason: state2.consecutiveFailures + 1 >= 3 ? `GC daemon: ${state2.consecutiveFailures + 1} consecutive failures. Check logs.` : state2.escalationReason
397
+ });
398
+ }
399
+ },
400
+ {
401
+ timezone: "UTC",
402
+ noOverlap: true,
403
+ name: "cleo-gc"
404
+ }
405
+ );
406
+ }
407
+ async function spawnGCDaemon(cleoDir) {
408
+ const logsDir = join3(cleoDir, "logs");
409
+ await mkdir2(logsDir, { recursive: true });
410
+ const logPath = join3(logsDir, "gc.log");
411
+ const errPath = join3(logsDir, "gc.err");
412
+ const outStream = createWriteStream(logPath, { flags: "a" });
413
+ const errStream = createWriteStream(errPath, { flags: "a" });
414
+ const daemonEntry = join3(fileURLToPath(import.meta.url), "..", "daemon-entry.js");
415
+ const child = spawn(process.execPath, [daemonEntry, cleoDir], {
416
+ detached: true,
417
+ stdio: ["ignore", outStream, errStream],
418
+ env: { ...process.env, CLEO_GC_DAEMON: "1" }
419
+ });
420
+ child.unref();
421
+ const pid = child.pid ?? 0;
422
+ await patchGCState(join3(cleoDir, "gc-state.json"), {
423
+ daemonPid: pid,
424
+ daemonStartedAt: (/* @__PURE__ */ new Date()).toISOString()
425
+ });
426
+ return pid;
427
+ }
428
+ async function stopGCDaemon(cleoDir) {
429
+ const statePath = join3(cleoDir, "gc-state.json");
430
+ const state = await readGCState(statePath);
431
+ const pid = state.daemonPid;
432
+ if (!pid) {
433
+ return { stopped: false, pid: null, reason: "Daemon PID not found in gc-state.json" };
434
+ }
435
+ try {
436
+ process.kill(pid, 0);
437
+ } catch {
438
+ await patchGCState(statePath, { daemonPid: null });
439
+ return {
440
+ stopped: false,
441
+ pid,
442
+ reason: `Daemon PID ${pid} is not running (stale state cleared)`
443
+ };
444
+ }
445
+ try {
446
+ process.kill(pid, "SIGTERM");
447
+ await patchGCState(statePath, { daemonPid: null });
448
+ return { stopped: true, pid, reason: `SIGTERM sent to PID ${pid}` };
449
+ } catch (err) {
450
+ const msg = err instanceof Error ? err.message : String(err);
451
+ return { stopped: false, pid, reason: `Failed to send SIGTERM to PID ${pid}: ${msg}` };
452
+ }
453
+ }
454
+ async function getGCDaemonStatus(cleoDir) {
455
+ const state = await readGCState(join3(cleoDir, "gc-state.json"));
456
+ const pid = state.daemonPid;
457
+ let running = false;
458
+ if (pid) {
459
+ try {
460
+ process.kill(pid, 0);
461
+ running = true;
462
+ } catch {
463
+ running = false;
464
+ }
465
+ }
466
+ return {
467
+ running,
468
+ pid: running ? pid : null,
469
+ startedAt: state.daemonStartedAt,
470
+ lastRunAt: state.lastRunAt,
471
+ lastDiskUsedPct: state.lastDiskUsedPct,
472
+ escalationNeeded: state.escalationNeeded
473
+ };
474
+ }
475
+
476
+ // packages/core/src/gc/transcript.ts
477
+ import { lstat as lstat2, readdir as readdir2, stat as stat2 } from "node:fs/promises";
478
+ import { homedir as homedir2 } from "node:os";
479
+ import { join as join4 } from "node:path";
480
+ var HOT_MAX_MS = 24 * 60 * 60 * 1e3;
481
+ var WARM_MAX_MS = 7 * 24 * 60 * 60 * 1e3;
482
+ function classifyTranscriptTier(ageMs) {
483
+ if (ageMs < HOT_MAX_MS) return "hot";
484
+ if (ageMs < WARM_MAX_MS) return "warm";
485
+ return "cold";
486
+ }
487
+ function parseSessionId(filename) {
488
+ return filename.replace(/\.jsonl$/, "");
489
+ }
490
+ async function scanTranscripts(projectsDir) {
491
+ const resolvedProjectsDir = projectsDir ?? join4(homedir2(), ".claude", "projects");
492
+ const now = Date.now();
493
+ const hot = [];
494
+ const warm = [];
495
+ let totalBytes = 0;
496
+ let slugs;
497
+ try {
498
+ const entries = await readdir2(resolvedProjectsDir, { withFileTypes: true });
499
+ slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
500
+ } catch {
501
+ return { totalSessions: 0, hot, warm, totalBytes, projectsDir: resolvedProjectsDir };
502
+ }
503
+ for (const slug of slugs) {
504
+ const slugDir = join4(resolvedProjectsDir, slug);
505
+ let entries;
506
+ try {
507
+ entries = await readdir2(slugDir, { withFileTypes: true });
508
+ } catch {
509
+ continue;
510
+ }
511
+ for (const entry of entries) {
512
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
513
+ const jsonlPath = join4(slugDir, entry.name);
514
+ const sessionId = parseSessionId(entry.name);
515
+ let fileInfo;
516
+ try {
517
+ fileInfo = await stat2(jsonlPath);
518
+ } catch {
519
+ continue;
520
+ }
521
+ const mtimeMs = fileInfo.mtimeMs;
522
+ const ageMs = now - mtimeMs;
523
+ const tier = classifyTranscriptTier(ageMs);
524
+ const bytes = fileInfo.size;
525
+ const candidateSessionDir = join4(slugDir, sessionId);
526
+ let sessionDir = null;
527
+ let sessionDirBytes = 0;
528
+ try {
529
+ const dirInfo = await lstat2(candidateSessionDir);
530
+ if (dirInfo.isDirectory()) {
531
+ sessionDir = candidateSessionDir;
532
+ sessionDirBytes = await getPathBytes(candidateSessionDir);
533
+ }
534
+ } catch {
535
+ }
536
+ const info = {
537
+ jsonlPath,
538
+ projectSlug: slug,
539
+ sessionId,
540
+ mtimeMs,
541
+ ageMs,
542
+ tier,
543
+ bytes,
544
+ sessionDir,
545
+ sessionDirBytes
546
+ };
547
+ totalBytes += bytes + sessionDirBytes;
548
+ if (tier === "hot") {
549
+ hot.push(info);
550
+ } else if (tier === "warm") {
551
+ warm.push(info);
552
+ }
553
+ }
554
+ }
555
+ const totalSessions = hot.length + warm.length;
556
+ return { totalSessions, hot, warm, totalBytes, projectsDir: resolvedProjectsDir };
557
+ }
558
+ async function pruneTranscripts(opts) {
559
+ const { olderThanMs, confirm, projectsDir } = opts;
560
+ const dryRun = !confirm;
561
+ const hasApiKey = Boolean(process.env["ANTHROPIC_API_KEY"]);
562
+ const effectiveMaxAgeMs = hasApiKey ? olderThanMs : Math.max(olderThanMs, 30 * 24 * 60 * 60 * 1e3);
563
+ const now = Date.now();
564
+ const deletedPaths = [];
565
+ let bytesFreed = 0;
566
+ let pruned = 0;
567
+ const resolvedProjectsDir = projectsDir ?? join4(homedir2(), ".claude", "projects");
568
+ let slugs;
569
+ try {
570
+ const entries = await readdir2(resolvedProjectsDir, { withFileTypes: true });
571
+ slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
572
+ } catch {
573
+ return { pruned: 0, bytesFreed: 0, deletedPaths: [], dryRun };
574
+ }
575
+ for (const slug of slugs) {
576
+ const slugDir = join4(resolvedProjectsDir, slug);
577
+ let entries;
578
+ try {
579
+ entries = await readdir2(slugDir, { withFileTypes: true });
580
+ } catch {
581
+ continue;
582
+ }
583
+ for (const entry of entries) {
584
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
585
+ const jsonlPath = join4(slugDir, entry.name);
586
+ let fileInfo;
587
+ try {
588
+ fileInfo = await stat2(jsonlPath);
589
+ } catch {
590
+ continue;
591
+ }
592
+ const ageMs = now - fileInfo.mtimeMs;
593
+ if (ageMs <= effectiveMaxAgeMs) continue;
594
+ const sessionId = parseSessionId(entry.name);
595
+ const sessionDir = join4(slugDir, sessionId);
596
+ const jsonlBytes = fileInfo.size;
597
+ let sessionDirBytes = 0;
598
+ try {
599
+ const dirInfo = await lstat2(sessionDir);
600
+ if (dirInfo.isDirectory()) {
601
+ sessionDirBytes = await getPathBytes(sessionDir);
602
+ }
603
+ } catch {
604
+ }
605
+ if (dryRun) {
606
+ deletedPaths.push(jsonlPath);
607
+ if (sessionDirBytes > 0) deletedPaths.push(sessionDir);
608
+ bytesFreed += jsonlBytes + sessionDirBytes;
609
+ pruned++;
610
+ continue;
611
+ }
612
+ try {
613
+ await idempotentRm(jsonlPath);
614
+ deletedPaths.push(jsonlPath);
615
+ bytesFreed += jsonlBytes;
616
+ pruned++;
617
+ } catch {
618
+ continue;
619
+ }
620
+ try {
621
+ const dirInfo = await lstat2(sessionDir);
622
+ if (dirInfo.isDirectory()) {
623
+ await idempotentRm(sessionDir);
624
+ deletedPaths.push(sessionDir);
625
+ bytesFreed += sessionDirBytes;
626
+ }
627
+ } catch {
628
+ }
629
+ }
630
+ }
631
+ return { pruned, bytesFreed, deletedPaths, dryRun };
632
+ }
633
+ function parseDurationMs(duration) {
634
+ const match = /^(\d+(\.\d+)?)(d|h|m|s)$/.exec(duration.trim());
635
+ if (!match?.[1] || !match[3]) {
636
+ throw new Error(`Invalid duration format: "${duration}". Use format like 7d, 24h, 30m, 60s.`);
637
+ }
638
+ const value = parseFloat(match[1]);
639
+ const unit = match[3];
640
+ const multipliers = {
641
+ d: 24 * 60 * 60 * 1e3,
642
+ h: 60 * 60 * 1e3,
643
+ m: 60 * 1e3,
644
+ s: 1e3
645
+ };
646
+ return value * (multipliers[unit] ?? 1e3);
647
+ }
648
+ export {
649
+ DEFAULT_GC_STATE,
650
+ DISK_THRESHOLDS,
651
+ GC_STATE_SCHEMA_VERSION,
652
+ bootstrapDaemon,
653
+ classifyDiskTier,
654
+ classifyTranscriptTier,
655
+ getGCDaemonStatus,
656
+ getPathBytes,
657
+ idempotentRm,
658
+ parseDurationMs,
659
+ patchGCState,
660
+ pruneTranscripts,
661
+ readGCState,
662
+ retentionMs,
663
+ runGC,
664
+ scanTranscripts,
665
+ spawnGCDaemon,
666
+ stopGCDaemon,
667
+ writeGCState
668
+ };
669
+ //# sourceMappingURL=index.js.map