codex-cleaner 0.0.1 → 0.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/dist/cleaner.d.ts +5 -5
- package/dist/cleaner.js +41 -30
- package/dist/cli.js +24 -2
- package/package.json +4 -6
package/dist/cleaner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
3
|
import type { BlockingProcess, CleanerOptions, CompactWhere } from "./types.js";
|
|
4
4
|
type CodexSpawnCommand = {
|
|
5
5
|
args: string[];
|
|
@@ -30,21 +30,21 @@ export declare function compactWhere(args: {
|
|
|
30
30
|
protectRecent: boolean;
|
|
31
31
|
protectedIds: Set<string>;
|
|
32
32
|
}): CompactWhere;
|
|
33
|
-
export declare function collectCompactCandidateStats(db:
|
|
33
|
+
export declare function collectCompactCandidateStats(db: DatabaseSync, args: {
|
|
34
34
|
archivedOnly: boolean;
|
|
35
35
|
cutoffMs: number;
|
|
36
36
|
maxChars: number;
|
|
37
37
|
protectRecent: boolean;
|
|
38
38
|
protectedIds: Set<string>;
|
|
39
39
|
}): Record<string, unknown>;
|
|
40
|
-
export declare function collectStaleArchiveCandidateStats(db:
|
|
40
|
+
export declare function collectStaleArchiveCandidateStats(db: DatabaseSync, codexHome: string, args: {
|
|
41
41
|
cutoffMs: number;
|
|
42
42
|
protectedIds: Set<string>;
|
|
43
43
|
statRollouts?: boolean;
|
|
44
44
|
}): Record<string, unknown>;
|
|
45
|
-
export declare function collectLogCleanupStats(db:
|
|
45
|
+
export declare function collectLogCleanupStats(db: DatabaseSync, options: CleanerOptions): Record<string, unknown>;
|
|
46
46
|
export declare function collectTuiLogCleanupStats(logPath: string, keepMib: number): Record<string, unknown>;
|
|
47
|
-
export declare function collectOrphanRolloutArchiveStats(db:
|
|
47
|
+
export declare function collectOrphanRolloutArchiveStats(db: DatabaseSync, codexHome: string, args: {
|
|
48
48
|
cutoffMs: number;
|
|
49
49
|
}): Record<string, unknown>;
|
|
50
50
|
export declare function nextBackupPath(dbPath: string, backupDir: string, now?: Date): string;
|
package/dist/cleaner.js
CHANGED
|
@@ -2,8 +2,8 @@ import { execFile, spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
|
-
import Database from "better-sqlite3";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
const THREAD_COLUMNS_TO_CAP = ["title", "preview", "first_user_message"];
|
|
9
9
|
const APP_SERVER_REQUEST_TIMEOUT_MS = 60_000;
|
|
@@ -438,7 +438,7 @@ export async function compactMetadata(options) {
|
|
|
438
438
|
throw new Error("--apply requires --confirm-lossy-metadata");
|
|
439
439
|
}
|
|
440
440
|
if (options.apply && Number(before.rows) > 0) {
|
|
441
|
-
backupPath = await
|
|
441
|
+
backupPath = await backupOpenSqliteDatabase(db, stateDb, resolveBackupDir(options, codexHome));
|
|
442
442
|
const where = compactWhere({
|
|
443
443
|
archivedOnly: options.archivedOnly,
|
|
444
444
|
cutoffMs,
|
|
@@ -447,9 +447,8 @@ export async function compactMetadata(options) {
|
|
|
447
447
|
protectedIds,
|
|
448
448
|
});
|
|
449
449
|
const assignments = THREAD_COLUMNS_TO_CAP.map((column) => `${column} = CASE WHEN length(${column}) > @maxChars THEN substr(${column}, 1, @maxChars) ELSE ${column} END`).join(", ");
|
|
450
|
-
const update = db
|
|
451
|
-
|
|
452
|
-
changedRows = Number(tx().changes);
|
|
450
|
+
const update = prepareStatement(db, `UPDATE threads SET ${assignments} WHERE ${where.sql}`);
|
|
451
|
+
changedRows = Number(runTransaction(db, () => update.run(where.params).changes));
|
|
453
452
|
}
|
|
454
453
|
const after = collectCompactCandidateStats(db, {
|
|
455
454
|
archivedOnly: options.archivedOnly,
|
|
@@ -749,23 +748,20 @@ export async function cleanLogs(options) {
|
|
|
749
748
|
const hasRowChanges = Number(before.cap_rows) > 0 || Number(before.delete_rows) > 0;
|
|
750
749
|
const shouldVacuum = Number(beforeSpace.freelist_count) > 0 || hasRowChanges;
|
|
751
750
|
if (options.apply && shouldVacuum) {
|
|
752
|
-
backupPath = await
|
|
751
|
+
backupPath = await backupOpenSqliteDatabase(db, logsDb, resolveBackupDir(options, codexHome));
|
|
753
752
|
const cutoffSeconds = logCutoffSeconds(options.keepLogDays);
|
|
754
753
|
if (hasRowChanges) {
|
|
755
|
-
|
|
756
|
-
deletedRows = Number(db
|
|
757
|
-
cappedRows = Number(db
|
|
758
|
-
.prepare(`
|
|
754
|
+
runTransaction(db, () => {
|
|
755
|
+
deletedRows = Number(prepareStatement(db, "DELETE FROM logs WHERE ts < @cutoffSeconds").run({ cutoffSeconds }).changes);
|
|
756
|
+
cappedRows = Number(prepareStatement(db, `
|
|
759
757
|
UPDATE logs
|
|
760
758
|
SET estimated_bytes = max(0, estimated_bytes - (length(feedback_log_body) - @maxLogBodyChars)),
|
|
761
759
|
feedback_log_body = substr(feedback_log_body, 1, @maxLogBodyChars)
|
|
762
760
|
WHERE ts >= @cutoffSeconds
|
|
763
761
|
AND feedback_log_body IS NOT NULL
|
|
764
762
|
AND length(feedback_log_body) > @maxLogBodyChars
|
|
765
|
-
`)
|
|
766
|
-
.run({ cutoffSeconds, maxLogBodyChars: options.maxLogBodyChars }).changes);
|
|
763
|
+
`).run({ cutoffSeconds, maxLogBodyChars: options.maxLogBodyChars }).changes);
|
|
767
764
|
});
|
|
768
|
-
tx();
|
|
769
765
|
}
|
|
770
766
|
db.exec("VACUUM");
|
|
771
767
|
queryAll(db, "PRAGMA wal_checkpoint(TRUNCATE)");
|
|
@@ -912,9 +908,7 @@ export async function scheduleBackupPrune(options) {
|
|
|
912
908
|
};
|
|
913
909
|
}
|
|
914
910
|
export function compactWhere(args) {
|
|
915
|
-
const clauses = [
|
|
916
|
-
`(${THREAD_COLUMNS_TO_CAP.map((column) => `length(${column}) > @maxChars`).join(" OR ")})`,
|
|
917
|
-
];
|
|
911
|
+
const clauses = [`(${THREAD_COLUMNS_TO_CAP.map((column) => `length(${column}) > @maxChars`).join(" OR ")})`];
|
|
918
912
|
const params = {
|
|
919
913
|
cutoffMs: args.cutoffMs,
|
|
920
914
|
maxChars: args.maxChars,
|
|
@@ -1368,15 +1362,18 @@ function backupFileStats(files) {
|
|
|
1368
1362
|
};
|
|
1369
1363
|
}
|
|
1370
1364
|
async function backupSqliteDatabase(dbPath, backupDir) {
|
|
1371
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
1372
|
-
const backupPath = nextBackupPath(dbPath, backupDir);
|
|
1373
1365
|
const db = openWritableDb(dbPath);
|
|
1374
1366
|
try {
|
|
1375
|
-
await db
|
|
1367
|
+
return await backupOpenSqliteDatabase(db, dbPath, backupDir);
|
|
1376
1368
|
}
|
|
1377
1369
|
finally {
|
|
1378
1370
|
db.close();
|
|
1379
1371
|
}
|
|
1372
|
+
}
|
|
1373
|
+
async function backupOpenSqliteDatabase(db, dbPath, backupDir) {
|
|
1374
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
1375
|
+
const backupPath = nextBackupPath(dbPath, backupDir);
|
|
1376
|
+
await sqliteBackup(db, backupPath);
|
|
1380
1377
|
return backupPath;
|
|
1381
1378
|
}
|
|
1382
1379
|
function backupRegularFile(filePath, backupDir) {
|
|
@@ -1393,7 +1390,7 @@ async function vacuumSqliteDatabase(args) {
|
|
|
1393
1390
|
const beforeSpace = collectSqliteSpaceStats(db);
|
|
1394
1391
|
if (args.apply && Number(beforeSpace.freelist_count) > 0) {
|
|
1395
1392
|
if (args.backupBeforeVacuum) {
|
|
1396
|
-
backupPath = await
|
|
1393
|
+
backupPath = await backupOpenSqliteDatabase(db, args.dbPath, args.backupDir);
|
|
1397
1394
|
}
|
|
1398
1395
|
db.exec("VACUUM");
|
|
1399
1396
|
queryAll(db, "PRAGMA wal_checkpoint(TRUNCATE)");
|
|
@@ -1424,10 +1421,7 @@ function nextOrphanRolloutManifestPath(backupDir, now = new Date()) {
|
|
|
1424
1421
|
return nextTimestampedPath(backupDir, "orphan-rollouts", ".manifest.bak", now);
|
|
1425
1422
|
}
|
|
1426
1423
|
function nextTimestampedPath(backupDir, name, suffix, now) {
|
|
1427
|
-
const stamp = now
|
|
1428
|
-
.toISOString()
|
|
1429
|
-
.replace(/[-:]/g, "")
|
|
1430
|
-
.replace(".", "_");
|
|
1424
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(".", "_");
|
|
1431
1425
|
const base = `${name}.${stamp}`;
|
|
1432
1426
|
let candidate = path.join(backupDir, `${base}${suffix}`);
|
|
1433
1427
|
let collision = 2;
|
|
@@ -1604,21 +1598,38 @@ function timestampForName(date) {
|
|
|
1604
1598
|
function openReadonlyDb(file) {
|
|
1605
1599
|
if (!fs.existsSync(file))
|
|
1606
1600
|
throw new Error(`SQLite database not found: ${file}`);
|
|
1607
|
-
return new
|
|
1601
|
+
return new DatabaseSync(file, { readOnly: true });
|
|
1608
1602
|
}
|
|
1609
1603
|
function openWritableDb(file) {
|
|
1610
1604
|
if (!fs.existsSync(file))
|
|
1611
1605
|
throw new Error(`SQLite database not found: ${file}`);
|
|
1612
|
-
const db = new
|
|
1613
|
-
db.
|
|
1614
|
-
db.
|
|
1606
|
+
const db = new DatabaseSync(file);
|
|
1607
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
1608
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
1615
1609
|
return db;
|
|
1616
1610
|
}
|
|
1617
1611
|
function queryAll(db, sql, params) {
|
|
1618
|
-
return db
|
|
1612
|
+
return prepareStatement(db, sql).all(params ?? {});
|
|
1619
1613
|
}
|
|
1620
1614
|
function queryOne(db, sql, params) {
|
|
1621
|
-
return db
|
|
1615
|
+
return prepareStatement(db, sql).get(params ?? {}) ?? {};
|
|
1616
|
+
}
|
|
1617
|
+
function prepareStatement(db, sql) {
|
|
1618
|
+
const statement = db.prepare(sql);
|
|
1619
|
+
statement.setAllowUnknownNamedParameters(true);
|
|
1620
|
+
return statement;
|
|
1621
|
+
}
|
|
1622
|
+
function runTransaction(db, fn) {
|
|
1623
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1624
|
+
try {
|
|
1625
|
+
const result = fn();
|
|
1626
|
+
db.exec("COMMIT");
|
|
1627
|
+
return result;
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
db.exec("ROLLBACK");
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1622
1633
|
}
|
|
1623
1634
|
function tryQueryAll(db, sql) {
|
|
1624
1635
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { buildScanReport, archiveOrphanRollouts, checkpointWal, cleanCodex, compactMetadata, emitReport, pruneBackups, requireStoppedOrReadonlyAllowed, scanBackups, scheduleBackupPrune, } from "./cleaner.js";
|
|
4
|
-
import { runWizard } from "./wizard.js";
|
|
5
4
|
const USAGE = `
|
|
6
5
|
codex-cleaner [command] [options]
|
|
7
6
|
|
|
@@ -131,7 +130,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
131
130
|
pruneLogs: Boolean(parsed.values["prune-logs"]),
|
|
132
131
|
pruneTuiLog: Boolean(parsed.values["prune-tui-log"]),
|
|
133
132
|
};
|
|
133
|
+
const reexecCode = reexecWithSqliteWarningDisabled(argv);
|
|
134
|
+
if (reexecCode != null)
|
|
135
|
+
return reexecCode;
|
|
136
|
+
const { archiveOrphanRollouts, buildScanReport, checkpointWal, cleanCodex, compactMetadata, emitReport, pruneBackups, requireStoppedOrReadonlyAllowed, scanBackups, scheduleBackupPrune, } = await import("./cleaner.js");
|
|
134
137
|
if (!command) {
|
|
138
|
+
const { runWizard } = await import("./wizard.js");
|
|
135
139
|
return runWizard(options);
|
|
136
140
|
}
|
|
137
141
|
if (command !== "backups") {
|
|
@@ -178,6 +182,24 @@ function isMutating(command, options) {
|
|
|
178
182
|
function allowsRunningFileOnlyMutation(command, options) {
|
|
179
183
|
return command === "archive-orphan-rollouts" && options.apply && options.allowRunningOrphanRolloutArchive;
|
|
180
184
|
}
|
|
185
|
+
function reexecWithSqliteWarningDisabled(argv) {
|
|
186
|
+
const alreadyDisabled = process.env.NODE_NO_WARNINGS === "1" ||
|
|
187
|
+
process.execArgv.includes("--no-warnings") ||
|
|
188
|
+
process.execArgv.includes("--disable-warning=ExperimentalWarning");
|
|
189
|
+
if (alreadyDisabled)
|
|
190
|
+
return null;
|
|
191
|
+
if (!process.allowedNodeEnvironmentFlags.has("--disable-warning=ExperimentalWarning"))
|
|
192
|
+
return null;
|
|
193
|
+
if (!process.argv[1])
|
|
194
|
+
return null;
|
|
195
|
+
const result = spawnSync(process.execPath, [...process.execArgv, "--disable-warning=ExperimentalWarning", process.argv[1], ...argv], {
|
|
196
|
+
stdio: "inherit",
|
|
197
|
+
windowsHide: true,
|
|
198
|
+
});
|
|
199
|
+
if (result.error)
|
|
200
|
+
throw result.error;
|
|
201
|
+
return result.status ?? 1;
|
|
202
|
+
}
|
|
181
203
|
function parsePositiveInt(raw, name) {
|
|
182
204
|
const value = Number(raw);
|
|
183
205
|
if (!Number.isInteger(value) || value <= 0) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-cleaner",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Guarded cleanup and compaction utility for local Codex state.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"packageManager": "npm@11.
|
|
7
|
+
"packageManager": "npm@11.15.0",
|
|
8
8
|
"bin": {
|
|
9
9
|
"codex-cleaner": "dist/cli.js"
|
|
10
10
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=22.
|
|
16
|
+
"node": ">=22.16"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
@@ -21,16 +21,14 @@
|
|
|
21
21
|
"format": "prettier --check .",
|
|
22
22
|
"lint": "eslint .",
|
|
23
23
|
"prepack": "npm run build",
|
|
24
|
-
"test": "
|
|
24
|
+
"test": "node scripts/run-vitest.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@inquirer/prompts": "^8.4.3",
|
|
28
|
-
"better-sqlite3": "^12.10.0",
|
|
29
28
|
"picocolors": "^1.1.1"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"@eslint/js": "^10.0.1",
|
|
33
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
34
32
|
"@types/node": "^25.9.1",
|
|
35
33
|
"eslint": "^10.4.0",
|
|
36
34
|
"globals": "^17.6.0",
|