ai-chat-cleaner 0.0.1 → 0.1.0

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/README.md CHANGED
@@ -9,26 +9,34 @@ Clean and remove AI chat with an interactive terminal UI.
9
9
 
10
10
  ```sh
11
11
  npx ai-chat-cleaner
12
+ npx ai-chat-cleaner --agent codex
12
13
  ```
13
14
 
14
- <p align='center'>
15
- <img src='./assets/screenshot.png' alt="screenshot" />
16
- </p>
17
-
18
15
  - Supported agents:
19
- - `codex`
16
+ - Codex (`codex`)
17
+ - Claude Code (`claude-code`)
20
18
 
21
19
  > [!WARNING]
22
20
  > Please restart your AI coding tool after deletion.
23
21
  >
24
22
  > It is recommended to clean history while Codex is not running, to avoid duplicate writes.
25
23
 
24
+ <p align='center'>
25
+ <img src='./assets/screenshot.png' alt="screenshot" />
26
+ </p>
27
+
26
28
  ## Why ?
27
29
 
28
30
  I am not entirely sure why `Codex` and `Claude Code` do not provide a way to delete a specific conversation history, perhaps to preserve context continuity.
29
31
 
30
32
  But in practice, one conversation that goes in the wrong direction can keep affecting later outputs, so I built this tool.
31
33
 
34
+ ## Credit
35
+
36
+ The terminal interaction mode is inspired by [taze](https://github.com/antfu-collective/taze).
37
+
38
+ Claude Code cleanup implementation references [claude-chats-delete](https://github.com/ataleckij/claude-chats-delete).
39
+
32
40
  ## License
33
41
 
34
42
  [MIT](./LICENSE) License © [jinghaihan](https://github.com/jinghaihan)
package/dist/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import { cac } from "cac";
5
5
  import { homedir } from "node:os";
6
6
  import readline from "node:readline";
7
7
  import { execFile } from "node:child_process";
8
- import { readFile, writeFile } from "node:fs/promises";
8
+ import { readFile, readdir, stat, writeFile } from "node:fs/promises";
9
9
  import { promisify } from "node:util";
10
10
  import pLimit from "p-limit";
11
11
  import { basename, join } from "pathe";
@@ -129,7 +129,7 @@ function renderGroups(state, groups) {
129
129
  const totalCount = groups.reduce((acc, group) => acc + group.items.length, 0);
130
130
  process.stdout.write(`${c.gray(`${Y("↑↓")} select ${Y("space")} toggle group ${Y("→")} enter group`)}\n`);
131
131
  process.stdout.write(`${c.gray(`${Y("enter")} confirm ${Y("esc")} cancel ${Y("a")} toggle all`)}\n\n`);
132
- process.stdout.write(`selected ${c.red(`${selectedCount}/${totalCount}`)}\n\n`);
132
+ process.stdout.write(`${c.gray("selected")} ${c.red(`${selectedCount}/${totalCount}`)}\n\n`);
133
133
  const labelWidth = Math.min(36, Math.max(12, ...groups.map((group) => group.label.length)));
134
134
  const countWidth = Math.max(7, ...groups.map((group) => {
135
135
  return `${countSelectedInGroup(group, state.selectedKeys)}/${group.items.length}`.length;
@@ -156,7 +156,7 @@ function renderItems(state, groups) {
156
156
  const totalCount = groups.reduce((acc, item) => acc + item.items.length, 0);
157
157
  process.stdout.write(`${c.gray(`${Y("↑↓")} select ${Y("space")} toggle ${Y("←")} back`)}\n`);
158
158
  process.stdout.write(`${c.gray(`${Y("enter")} back ${Y("esc")} back ${Y("a")} toggle group`)}\n\n`);
159
- process.stdout.write(`selected ${c.red(`${selectedCount}/${totalCount}`)}\n`);
159
+ process.stdout.write(`${c.gray("selected")} ${c.red(`${selectedCount}/${totalCount}`)}\n`);
160
160
  process.stdout.write("\n");
161
161
  process.stdout.write(`${c.green(group.label)} ${c.gray(group.path ? tildifyPath(group.path) : "(unknown cwd)")}\n\n`);
162
162
  const { start, end } = getRenderWindow(group.items.length, state.itemIndex, 8);
@@ -249,12 +249,27 @@ function clearScreen() {
249
249
  //#region src/utils.ts
250
250
  const exec = promisify(execFile);
251
251
  async function readJSON(filepath) {
252
- const content = await readFile(filepath, "utf-8");
253
- return JSON.parse(content);
252
+ return parseJSON(await readFile(filepath, "utf-8"));
254
253
  }
255
254
  async function writeJSON(filepath, data) {
256
255
  await writeFile(filepath, JSON.stringify(data, null, 2), "utf-8");
257
256
  }
257
+ function parseJSON(value) {
258
+ try {
259
+ return JSON.parse(value);
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+ function toUnix(value) {
265
+ return Math.floor(value / 1e3);
266
+ }
267
+ function parseDateToUnix(value) {
268
+ if (!value) return 0;
269
+ const ts = Date.parse(value);
270
+ if (!Number.isFinite(ts)) return 0;
271
+ return Math.floor(ts / 1e3);
272
+ }
258
273
  function formatRelativeTime(date) {
259
274
  const diff = (/* @__PURE__ */ new Date(date * 1e3)).getTime() - Date.now();
260
275
  const seconds = Math.round(diff / 1e3);
@@ -274,27 +289,296 @@ function formatRelativeTime(date) {
274
289
  function quoteSqlString(value) {
275
290
  return `'${value.replaceAll("'", "''")}'`;
276
291
  }
292
+ function normalizeInlineText(value) {
293
+ return value.replace(/\r?\n/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
294
+ }
295
+ function extractMessageText(content) {
296
+ if (typeof content === "string") return content;
297
+ if (!Array.isArray(content)) return "";
298
+ const texts = [];
299
+ for (const item of content) {
300
+ if (typeof item === "string") {
301
+ texts.push(item);
302
+ continue;
303
+ }
304
+ if (!item || typeof item !== "object") continue;
305
+ if (typeof item.text === "string") texts.push(item.text);
306
+ else if (typeof item.content === "string") texts.push(item.content);
307
+ }
308
+ return texts.join(" ");
309
+ }
310
+ function isCommandTitle(title) {
311
+ return /^\/[\w-]+(?:\s+[\w-]+)?$/.test(title);
312
+ }
313
+ function isUUID(value) {
314
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
315
+ }
277
316
 
278
317
  //#endregion
279
318
  //#region package.json
280
319
  var name = "ai-chat-cleaner";
281
- var version = "0.0.1";
320
+ var version = "0.1.0";
282
321
 
283
322
  //#endregion
284
323
  //#region src/constants.ts
285
324
  const NAME = name;
286
325
  const VERSION = version;
287
326
  const DEFAULT_OPTIONS = {};
288
- const AGENTS = { codex: {
289
- name: "codex",
290
- path: join(homedir(), ".codex")
291
- } };
327
+ const AGENTS_CHOICES = ["codex", "claude-code"];
328
+ const AGENTS_CONFIG = {
329
+ "codex": {
330
+ name: "codex",
331
+ path: process.env.CODEX_HOME?.trim() || join(homedir(), ".codex")
332
+ },
333
+ "claude-code": {
334
+ name: "claude-code",
335
+ path: process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude")
336
+ }
337
+ };
338
+
339
+ //#endregion
340
+ //#region src/claude-code/constants.ts
341
+ const ROOT_PATH = AGENTS_CONFIG["claude-code"].path;
342
+ const PROJECTS_PATH = join(ROOT_PATH, "projects");
343
+ const DEBUG_PATH = join(ROOT_PATH, "debug");
344
+ const TODOS_PATH = join(ROOT_PATH, "todos");
345
+ const SESSION_ENV_PATH = join(ROOT_PATH, "session-env");
346
+ const TASKS_PATH = join(ROOT_PATH, "tasks");
347
+ const FILE_HISTORY_PATH = join(ROOT_PATH, "file-history");
348
+ const PLANS_PATH = join(ROOT_PATH, "plans");
349
+ const AGENTS_PATH = join(ROOT_PATH, "agents");
350
+
351
+ //#endregion
352
+ //#region src/claude-code/delete.ts
353
+ async function deleteThreads$1(threads) {
354
+ const limit = pLimit(5);
355
+ await Promise.all(threads.map((thread) => limit(() => deleteThread$1(thread))));
356
+ }
357
+ async function deleteThread$1(thread) {
358
+ await rimraf(thread.path);
359
+ await rimraf(thread.path.replace(/\.jsonl$/i, ""));
360
+ await rimraf(join(DEBUG_PATH, `${thread.id}.txt`));
361
+ await rimraf(join(SESSION_ENV_PATH, thread.id));
362
+ await rimraf(join(TASKS_PATH, thread.id));
363
+ await rimraf(join(FILE_HISTORY_PATH, thread.id));
364
+ if (thread.slug) await rimraf(join(PLANS_PATH, `${thread.slug}.md`));
365
+ const todoFiles = await glob(`${thread.id}*.json`, {
366
+ cwd: TODOS_PATH,
367
+ absolute: true,
368
+ onlyFiles: true
369
+ });
370
+ await Promise.all(todoFiles.map((path) => rimraf(path)));
371
+ const agentIds = await readAgentIds(thread.path);
372
+ await Promise.all(agentIds.map((agentId) => rimraf(join(AGENTS_PATH, agentId, "memory-local.md"))));
373
+ await updateSessionsIndex(thread.project_dir, thread.id);
374
+ }
375
+ async function updateSessionsIndex(projectDir, threadId) {
376
+ const path = join(projectDir, "sessions-index.json");
377
+ const raw = await readFile(path, "utf8").catch(() => "");
378
+ if (!raw) return;
379
+ const data = parseJSON(raw);
380
+ if (!data || !Array.isArray(data.entries)) return;
381
+ const nextEntries = data.entries.filter((entry) => entry?.sessionId !== threadId);
382
+ if (nextEntries.length === data.entries.length) return;
383
+ data.entries = nextEntries;
384
+ await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
385
+ }
386
+ async function readAgentIds(path) {
387
+ const raw = await readFile(path, "utf8").catch(() => "");
388
+ if (!raw) return [];
389
+ const ids = /* @__PURE__ */ new Set();
390
+ const lines = raw.split("\n");
391
+ for (const line of lines) {
392
+ if (!line) continue;
393
+ const value = parseJSON(line)?.agent_id;
394
+ if (typeof value === "string" && value) ids.add(value);
395
+ }
396
+ return Array.from(ids);
397
+ }
398
+
399
+ //#endregion
400
+ //#region src/claude-code/detect.ts
401
+ async function detectClaudeCode(cwd = ROOT_PATH) {
402
+ const projectDirs = await readProjectDirs(join(cwd, "projects"));
403
+ const threads = [];
404
+ for (const projectDir of projectDirs) {
405
+ const sessions = await readSessionsIndex(projectDir);
406
+ const files = await glob("*.jsonl", {
407
+ cwd: projectDir,
408
+ absolute: true,
409
+ onlyFiles: true
410
+ });
411
+ for (const file of files) {
412
+ const id = basename(file, ".jsonl");
413
+ if (id.startsWith("agent-")) continue;
414
+ const info = await stat(file).catch(() => null);
415
+ if (!info) continue;
416
+ const session = sessions.get(id);
417
+ const meta = await readThreadMeta(file);
418
+ const updatedAt = parseDateToUnix(session?.modified) || toUnix(info.mtimeMs);
419
+ const createdAt = parseDateToUnix(session?.created) || toUnix(info.birthtimeMs || info.ctimeMs || info.mtimeMs);
420
+ const cwdPath = session?.projectPath || decodeProjectName(basename(projectDir));
421
+ const title = pickTitle([
422
+ session?.firstPrompt,
423
+ meta.title,
424
+ session?.summary
425
+ ]);
426
+ if (!title) continue;
427
+ threads.push({
428
+ id,
429
+ title,
430
+ path: file,
431
+ project_dir: projectDir,
432
+ cwd: cwdPath,
433
+ created_at: createdAt,
434
+ updated_at: updatedAt,
435
+ slug: meta.slug
436
+ });
437
+ }
438
+ }
439
+ threads.sort((a, b) => b.updated_at - a.updated_at);
440
+ return { threads };
441
+ }
442
+ async function readProjectDirs(projectsPath) {
443
+ return (await readdir(projectsPath, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => join(projectsPath, entry.name));
444
+ }
445
+ async function readSessionsIndex(projectDir) {
446
+ const raw = await readFile(join(projectDir, "sessions-index.json"), "utf8").catch(() => "");
447
+ if (!raw) return /* @__PURE__ */ new Map();
448
+ const data = parseJSON(raw);
449
+ const rows = Array.isArray(data?.entries) ? data.entries : [];
450
+ return new Map(rows.map((entry) => [entry.sessionId, entry]));
451
+ }
452
+ async function readThreadMeta(path) {
453
+ const lines = (await readFile(path, "utf8").catch(() => "")).split("\n").filter(Boolean);
454
+ let userTitle = "";
455
+ let summaryTitle = "";
456
+ let slug = "";
457
+ for (const line of lines) {
458
+ const row = parseJSON(line);
459
+ if (!row) continue;
460
+ if (!slug && typeof row.slug === "string") slug = row.slug;
461
+ if (!summaryTitle && row.type === "summary" && typeof row.summary === "string") summaryTitle = normalizeIfValidTitle(row.summary);
462
+ if (!userTitle && row.type === "user" && row.isMeta !== true) {
463
+ if (!isCommandEvent(row.message?.content)) userTitle = normalizeIfValidTitle(extractMessageText(row.message?.content));
464
+ }
465
+ if (userTitle && summaryTitle && slug) break;
466
+ }
467
+ return {
468
+ title: userTitle || summaryTitle,
469
+ slug: slug || void 0
470
+ };
471
+ }
472
+ function pickTitle(values) {
473
+ for (const value of values) {
474
+ const normalized = normalizeIfValidTitle(value);
475
+ if (normalized) return normalized;
476
+ }
477
+ return "";
478
+ }
479
+ function normalizeIfValidTitle(value) {
480
+ if (typeof value !== "string") return "";
481
+ const normalized = normalizeInlineText(value);
482
+ if (!normalized) return "";
483
+ if (normalized.toLowerCase() === "no prompt") return "";
484
+ if (isUUID(normalized)) return "";
485
+ if (isCommandTitle(normalized)) return "";
486
+ return normalized;
487
+ }
488
+ function isCommandEvent(content) {
489
+ const source = extractMessageText(content);
490
+ if (!source) return false;
491
+ return source.includes("<command-name>") || source.includes("<command-message>") || source.includes("<local-command-stdout>") || source.includes("<local-command-caveat>");
492
+ }
493
+ function decodeProjectName(projectName) {
494
+ if (!projectName) return "";
495
+ if (projectName.startsWith("-")) return `/${projectName.slice(1).replaceAll("-", "/")}`;
496
+ return projectName.replaceAll("-", "/");
497
+ }
498
+
499
+ //#endregion
500
+ //#region src/claude-code/group.ts
501
+ function groupClaudeCodeThreads(threads) {
502
+ const grouped = /* @__PURE__ */ new Map();
503
+ for (const thread of threads) {
504
+ const cwd = thread.cwd || "";
505
+ const id = cwd || "(unknown)";
506
+ const label = cwd ? basename(cwd) : "(unknown)";
507
+ const group = grouped.get(id);
508
+ if (group) {
509
+ group.threads.push(thread);
510
+ group.updatedAt = Math.max(group.updatedAt, thread.updated_at || thread.created_at || 0);
511
+ continue;
512
+ }
513
+ grouped.set(id, {
514
+ id,
515
+ label,
516
+ cwd,
517
+ threads: [thread],
518
+ updatedAt: thread.updated_at || thread.created_at || 0
519
+ });
520
+ }
521
+ const groups = Array.from(grouped.values());
522
+ for (const group of groups) group.threads.sort((a, b) => (b.updated_at || b.created_at || 0) - (a.updated_at || a.created_at || 0));
523
+ groups.sort((a, b) => {
524
+ const diff = b.updatedAt - a.updatedAt;
525
+ if (diff !== 0) return diff;
526
+ return a.label.localeCompare(b.label);
527
+ });
528
+ return groups;
529
+ }
530
+
531
+ //#endregion
532
+ //#region src/claude-code/index.ts
533
+ async function promptClaudeCode(_options) {
534
+ const spinner = p.spinner();
535
+ spinner.start("detecting claude-code threads...");
536
+ const { threads } = await detectClaudeCode();
537
+ spinner.stop(`detected ${c.yellow`${threads.length}`} threads`);
538
+ if (threads.length === 0) {
539
+ p.outro(c.yellow("no threads found"));
540
+ process.exit(0);
541
+ }
542
+ const resolved = await promptGroupedMultiSelect(formatThreadGroupOptions$1(groupClaudeCodeThreads(threads)));
543
+ if (resolved === null || resolved.length === 0) {
544
+ p.outro(c.red("aborting"));
545
+ process.exit(1);
546
+ }
547
+ const confirmed = await p.confirm({
548
+ message: `selected ${c.yellow`${resolved.length}`} records, continue?`,
549
+ initialValue: true
550
+ });
551
+ if (p.isCancel(confirmed) || !confirmed) {
552
+ p.outro(c.red("aborting"));
553
+ process.exit(1);
554
+ }
555
+ await deleteThreads$1(resolved);
556
+ p.outro(`cleaned ${c.yellow`${resolved.length}`} threads`);
557
+ }
558
+ function formatThreadGroupOptions$1(grouped) {
559
+ return grouped.map((group) => ({
560
+ id: group.id,
561
+ label: group.label,
562
+ path: group.cwd,
563
+ items: group.threads.map((thread) => ({
564
+ id: thread.id,
565
+ label: thread.title,
566
+ hint: formatThreadHint$1(thread),
567
+ value: thread
568
+ }))
569
+ }));
570
+ }
571
+ function formatThreadHint$1(thread) {
572
+ const updatedAt = thread.updated_at || thread.created_at;
573
+ const createdAt = thread.created_at || updatedAt;
574
+ return `updated ${formatRelativeTime(updatedAt)} · created ${formatRelativeTime(createdAt)}`;
575
+ }
292
576
 
293
577
  //#endregion
294
578
  //#region src/codex/constants.ts
295
- const GLOBAL_STATE_PATH = join(AGENTS.codex.path, ".codex-global-state.json");
296
- const HISTORY_FILE_PATH = join(AGENTS.codex.path, "history.jsonl");
297
- const SHELL_SNAPSHOTS_PATH = join(AGENTS.codex.path, "shell_snapshots");
579
+ const GLOBAL_STATE_PATH = join(AGENTS_CONFIG.codex.path, ".codex-global-state.json");
580
+ const HISTORY_FILE_PATH = join(AGENTS_CONFIG.codex.path, "history.jsonl");
581
+ const SHELL_SNAPSHOTS_PATH = join(AGENTS_CONFIG.codex.path, "shell_snapshots");
298
582
 
299
583
  //#endregion
300
584
  //#region src/codex/db.ts
@@ -304,7 +588,7 @@ async function readSQLite(filepath) {
304
588
  filepath,
305
589
  "SELECT * FROM threads;"
306
590
  ]);
307
- return JSON.parse(stdout.trim());
591
+ return parseJSON(stdout.trim());
308
592
  }
309
593
  async function writeSQLite(filepath, ids) {
310
594
  if (ids.length === 0) return;
@@ -346,13 +630,13 @@ async function updateGlobalState(threadIds, globalState) {
346
630
  }
347
631
  async function updateHistory(path, ids) {
348
632
  const remove = new Set(ids);
349
- const rows = (await readFile(path, "utf8")).split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((row) => !remove.has(row?.session_id));
633
+ const rows = (await readFile(path, "utf8")).split("\n").filter(Boolean).map((line) => parseJSON(line)).filter((row) => !remove.has(row?.session_id));
350
634
  await writeFile(HISTORY_FILE_PATH, rows.length > 0 ? `${rows.map((row) => JSON.stringify(row)).join("\n")}\n` : "", "utf-8");
351
635
  }
352
636
 
353
637
  //#endregion
354
638
  //#region src/codex/detect.ts
355
- async function detectCodex(cwd = AGENTS.codex.path) {
639
+ async function detectCodex(cwd = AGENTS_CONFIG.codex.path) {
356
640
  const globalState = await readJSON(GLOBAL_STATE_PATH);
357
641
  const sqlitePath = await getDatabasePath(cwd);
358
642
  const data = sqlitePath ? await readSQLite(sqlitePath) : [];
@@ -370,7 +654,7 @@ async function detectCodex(cwd = AGENTS.codex.path) {
370
654
  };
371
655
  }
372
656
  function normalizeTitle(thread) {
373
- return thread.title.replace(/\n/g, " ").replace(thread.cwd, "").replace(AGENTS.codex.path, "").trim();
657
+ return thread.title.replace(/\n/g, " ").replace(thread.cwd, "").replace(AGENTS_CONFIG.codex.path, "").trim();
374
658
  }
375
659
 
376
660
  //#endregion
@@ -408,18 +692,21 @@ function groupCodexThreads(threads) {
408
692
  //#endregion
409
693
  //#region src/codex/index.ts
410
694
  async function promptCodex(_options) {
695
+ const spinner = p.spinner();
696
+ spinner.start("detecting codex threads...");
411
697
  const { threads, globalState, sqlitePath } = await detectCodex();
698
+ spinner.stop(`detected ${c.yellow`${threads.length}`} threads`);
699
+ if (threads.length === 0) {
700
+ p.outro(c.yellow("no threads found"));
701
+ process.exit(0);
702
+ }
412
703
  const resolved = await promptGroupedMultiSelect(formatThreadGroupOptions(groupCodexThreads(threads)));
413
- if (resolved === null) {
704
+ if (resolved === null || resolved.length === 0) {
414
705
  p.outro(c.red("aborting"));
415
706
  process.exit(1);
416
707
  }
417
- if (resolved.length === 0) {
418
- p.outro(c.yellow("no threads selected"));
419
- return;
420
- }
421
708
  const confirmed = await p.confirm({
422
- message: `Selected ${c.yellow`${resolved.length}`} records, continue?`,
709
+ message: `selected ${c.yellow`${resolved.length}`} records, continue?`,
423
710
  initialValue: true
424
711
  });
425
712
  if (p.isCancel(confirmed) || !confirmed) {
@@ -461,21 +748,49 @@ function normalizeConfig(options) {
461
748
  async function resolveConfig(options) {
462
749
  const defaults = structuredClone(DEFAULT_OPTIONS);
463
750
  options = normalizeConfig(options);
464
- return {
751
+ const merged = {
465
752
  ...defaults,
466
753
  ...options
467
754
  };
755
+ const agents = AGENTS_CHOICES.find((agent) => agent === options.agents);
756
+ if (agents) merged.agents = agents;
757
+ else merged.agents = await resolveAgent();
758
+ return merged;
759
+ }
760
+ async function resolveAgent() {
761
+ const selected = await p.select({
762
+ message: "select agent to clean",
763
+ options: AGENTS_CHOICES.map((agent) => ({
764
+ value: agent,
765
+ label: AGENTS_CONFIG[agent].name,
766
+ hint: AGENTS_CONFIG[agent].path
767
+ }))
768
+ });
769
+ if (p.isCancel(selected)) {
770
+ p.outro(c.red("aborting"));
771
+ process.exit(1);
772
+ }
773
+ return selected;
468
774
  }
469
775
 
470
776
  //#endregion
471
777
  //#region src/cli.ts
472
778
  try {
473
779
  const cli = cac(NAME);
474
- cli.command("", "Clean and remove AI chat with an interactive terminal UI").allowUnknownOptions().action(async (options) => {
780
+ cli.command("", "Clean and remove AI chat with an interactive terminal UI").option("--agent, -a <agent>", "Agent to clean").allowUnknownOptions().action(async (options) => {
475
781
  p.intro(`${c.yellow`${NAME} `}${c.dim`v${VERSION}`}`);
476
782
  const config = await resolveConfig(options);
477
- p.log.info(`start detecting ${c.yellow`Codex`} threads...`);
478
- await promptCodex(config);
783
+ switch (config.agents) {
784
+ case "codex":
785
+ await promptCodex(config);
786
+ break;
787
+ case "claude-code":
788
+ await promptClaudeCode(config);
789
+ break;
790
+ default:
791
+ p.outro(`unknown agent: ${c.red(config.agents)}`);
792
+ process.exit(1);
793
+ }
479
794
  });
480
795
  cli.help();
481
796
  cli.version(VERSION);
package/dist/index.d.mts CHANGED
@@ -1,10 +1,11 @@
1
1
  //#region src/constants.d.ts
2
- declare const AGENTS_CHOICES: readonly ["codex"];
2
+ declare const AGENTS_CHOICES: readonly ["codex", "claude-code"];
3
3
  //#endregion
4
4
  //#region src/types.d.ts
5
5
  interface CommandOptions {
6
- cwd?: string;
6
+ agents?: AgentType;
7
7
  }
8
+ interface Options extends Required<CommandOptions> {}
8
9
  type AgentType = typeof AGENTS_CHOICES[number];
9
10
  interface AgentConfig {
10
11
  name: string;
@@ -14,4 +15,4 @@ interface AgentConfig {
14
15
  //#region src/index.d.ts
15
16
  declare function defineConfig(config: Partial<CommandOptions>): Partial<CommandOptions>;
16
17
  //#endregion
17
- export { AgentConfig, AgentType, CommandOptions, defineConfig };
18
+ export { AgentConfig, AgentType, CommandOptions, Options, defineConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-chat-cleaner",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.1.0",
5
5
  "description": "Clean and remove AI chat with an interactive terminal UI.",
6
6
  "author": "jinghaihan",
7
7
  "license": "MIT",
@@ -13,7 +13,14 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/jinghaihan/ai-chat-cleaner/issues"
15
15
  },
16
- "keywords": [],
16
+ "keywords": [
17
+ "codex",
18
+ "claude-code",
19
+ "conversation",
20
+ "threads",
21
+ "history",
22
+ "cleaner"
23
+ ],
17
24
  "exports": {
18
25
  ".": "./dist/index.mjs",
19
26
  "./cli": "./dist/cli.mjs",