ai-chat-cleaner 0.1.4 → 0.1.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +76 -11
  2. package/package.json +2 -1
package/dist/cli.mjs CHANGED
@@ -2,17 +2,16 @@ import process from "node:process";
2
2
  import * as p from "@clack/prompts";
3
3
  import c from "ansis";
4
4
  import { cac } from "cac";
5
- import readline from "node:readline";
5
+ import readline, { createInterface } from "node:readline";
6
6
  import tildify from "tildify";
7
- import { execFile } from "node:child_process";
8
7
  import { existsSync } from "node:fs";
9
8
  import { readFile, readdir, stat, writeFile } from "node:fs/promises";
10
- import { promisify } from "node:util";
11
9
  import pLimit from "p-limit";
12
10
  import { basename, join } from "pathe";
13
11
  import { rimraf } from "rimraf";
14
12
  import { glob } from "tinyglobby";
15
13
  import { homedir } from "node:os";
14
+ import { x } from "tinyexec";
16
15
 
17
16
  //#region src/prompts.ts
18
17
  const FIG_CHECK = c.green("◉");
@@ -243,7 +242,6 @@ function clearScreen() {
243
242
 
244
243
  //#endregion
245
244
  //#region src/utils.ts
246
- const exec = promisify(execFile);
247
245
  async function readJSON(filepath) {
248
246
  if (!existsSync(filepath)) return;
249
247
  return parseJSON(await readFile(filepath, "utf-8"));
@@ -314,7 +312,7 @@ function isUUID(value) {
314
312
  //#endregion
315
313
  //#region package.json
316
314
  var name = "ai-chat-cleaner";
317
- var version = "0.1.4";
315
+ var version = "0.1.5";
318
316
 
319
317
  //#endregion
320
318
  //#region src/constants.ts
@@ -579,17 +577,63 @@ const SHELL_SNAPSHOTS_PATH = join(AGENTS_CONFIG.codex.path, "shell_snapshots");
579
577
 
580
578
  //#endregion
581
579
  //#region src/codex/db.ts
580
+ const SQLITE_COLUMN_SEPARATOR = "";
581
+ const THREAD_TITLE_MAX_LENGTH = 240;
582
+ const THREAD_TITLE_ELLIPSIS = "...";
583
+ const THREAD_TITLE_SLICE_LENGTH = THREAD_TITLE_MAX_LENGTH - 3;
584
+ const THREAD_COLUMNS_SQL = `
585
+ SELECT
586
+ id,
587
+ rollout_path,
588
+ created_at,
589
+ updated_at,
590
+ source,
591
+ cwd,
592
+ CASE
593
+ WHEN LENGTH(${normalizeTitleSql("title")}) <= ${THREAD_TITLE_MAX_LENGTH}
594
+ THEN ${normalizeTitleSql("title")}
595
+ ELSE SUBSTR(${normalizeTitleSql("title")}, 1, ${THREAD_TITLE_SLICE_LENGTH}) || '${THREAD_TITLE_ELLIPSIS}'
596
+ END AS title
597
+ FROM threads;
598
+ `.trim();
582
599
  async function readSQLite(filepath) {
583
- const { stdout } = await exec("sqlite3", [
584
- "-json",
600
+ const process = x("sqlite3", [
601
+ "-batch",
602
+ "-noheader",
603
+ "-readonly",
604
+ "-separator",
605
+ SQLITE_COLUMN_SEPARATOR,
585
606
  filepath,
586
- "SELECT * FROM threads;"
587
- ]);
588
- return parseJSON(stdout.trim());
607
+ THREAD_COLUMNS_SQL
608
+ ]).process;
609
+ if (!process?.stdout) throw new Error("Failed to start sqlite3 process");
610
+ const stderrChunks = [];
611
+ const waitForExit = new Promise((resolve, reject) => {
612
+ process.once("error", reject);
613
+ process.once("close", (code, signal) => {
614
+ if (code === 0) {
615
+ resolve();
616
+ return;
617
+ }
618
+ const suffix = stderrChunks.join("").trim();
619
+ const reason = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
620
+ reject(/* @__PURE__ */ new Error(suffix ? `sqlite3 exited with ${reason}: ${suffix}` : `sqlite3 exited with ${reason}`));
621
+ });
622
+ });
623
+ process.stderr?.setEncoding("utf8");
624
+ process.stderr?.on("data", (chunk) => stderrChunks.push(chunk.toString()));
625
+ const rows = [];
626
+ const output = createInterface({ input: process.stdout });
627
+ for await (const line of output) {
628
+ if (!line) continue;
629
+ rows.push(parseThreadRow(line));
630
+ }
631
+ await waitForExit;
632
+ return rows;
589
633
  }
590
634
  async function writeSQLite(filepath, ids) {
591
635
  if (ids.length === 0) return;
592
- await exec("sqlite3", [filepath, `DELETE FROM threads WHERE id IN (${ids.map(quoteSqlString).join(", ")});`]);
636
+ await x("sqlite3", [filepath, `DELETE FROM threads WHERE id IN (${ids.map(quoteSqlString).join(", ")});`], { throwOnError: true });
593
637
  }
594
638
  async function getDatabasePath(cwd) {
595
639
  return (await glob("state_*.sqlite", {
@@ -604,6 +648,27 @@ function extractVersion(path) {
604
648
  const version = Number.parseInt(matched[1], 10);
605
649
  return Number.isFinite(version) ? version : 0;
606
650
  }
651
+ function parseThreadRow(line) {
652
+ const [id, rollout_path, createdAt, updatedAt, source, cwd, title, ...rest] = line.split(SQLITE_COLUMN_SEPARATOR);
653
+ if (rest.length > 0) throw new Error(`Unexpected sqlite3 row format: ${line}`);
654
+ return {
655
+ id,
656
+ rollout_path,
657
+ created_at: parseInteger(createdAt, "created_at"),
658
+ updated_at: parseInteger(updatedAt, "updated_at"),
659
+ source,
660
+ cwd,
661
+ title
662
+ };
663
+ }
664
+ function parseInteger(value, field) {
665
+ const parsed = Number.parseInt(value, 10);
666
+ if (Number.isFinite(parsed)) return parsed;
667
+ throw new Error(`Invalid ${field} value: ${value}`);
668
+ }
669
+ function normalizeTitleSql(field) {
670
+ return `REPLACE(REPLACE(REPLACE(${field}, CHAR(31), ' '), CHAR(13), ' '), CHAR(10), ' ')`;
671
+ }
607
672
 
608
673
  //#endregion
609
674
  //#region src/codex/delete.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-chat-cleaner",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "description": "Clean and remove AI chat with an interactive terminal UI.",
6
6
  "author": "jinghaihan",
7
7
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "pathe": "^2.0.3",
43
43
  "rimraf": "^6.1.3",
44
44
  "tildify": "^3.0.0",
45
+ "tinyexec": "^1.0.4",
45
46
  "tinyglobby": "^0.2.15"
46
47
  },
47
48
  "devDependencies": {