@sooink/ai-session-tidy 0.1.2 → 0.1.4

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/index.js CHANGED
@@ -67,19 +67,29 @@ async function getDirectorySize(dirPath) {
67
67
  }
68
68
  }
69
69
 
70
- // src/scanners/claude-code.ts
71
- import { readdir as readdir2, readFile, stat as stat2, access } from "fs/promises";
72
- import { join as join3 } from "path";
73
- import { createReadStream } from "fs";
74
- import { createInterface } from "readline";
75
-
76
70
  // src/utils/paths.ts
71
+ import { existsSync } from "fs";
77
72
  import { homedir } from "os";
78
73
  import { join as join2 } from "path";
79
74
  function decodePath(encoded) {
80
75
  if (encoded === "") return "";
81
76
  if (!encoded.startsWith("-")) return encoded;
82
- return encoded.replace(/-/g, "/");
77
+ const parts = encoded.slice(1).split("-");
78
+ return reconstructPath(parts, "");
79
+ }
80
+ function reconstructPath(parts, currentPath) {
81
+ if (parts.length === 0) return currentPath;
82
+ for (let len = parts.length; len >= 1; len--) {
83
+ const segment = parts.slice(0, len).join("-");
84
+ const testPath = currentPath + "/" + segment;
85
+ if (existsSync(testPath)) {
86
+ const result = reconstructPath(parts.slice(len), testPath);
87
+ if (existsSync(result) || parts.slice(len).length === 0) {
88
+ return result;
89
+ }
90
+ }
91
+ }
92
+ return currentPath + "/" + parts.join("/");
83
93
  }
84
94
  function getConfigDir(appName) {
85
95
  switch (process.platform) {
@@ -109,6 +119,9 @@ function getClaudeTodosDir() {
109
119
  function getClaudeFileHistoryDir() {
110
120
  return join2(homedir(), ".claude", "file-history");
111
121
  }
122
+ function getClaudeTasksDir() {
123
+ return join2(homedir(), ".claude", "tasks");
124
+ }
112
125
  function tildify(path) {
113
126
  const home = homedir();
114
127
  if (path.startsWith(home)) {
@@ -118,6 +131,10 @@ function tildify(path) {
118
131
  }
119
132
 
120
133
  // src/scanners/claude-code.ts
134
+ import { readdir as readdir2, readFile, stat as stat2, access } from "fs/promises";
135
+ import { join as join3 } from "path";
136
+ import { createReadStream } from "fs";
137
+ import { createInterface } from "readline";
121
138
  var ClaudeCodeScanner = class {
122
139
  name = "claude-code";
123
140
  projectsDir;
@@ -125,6 +142,7 @@ var ClaudeCodeScanner = class {
125
142
  sessionEnvDir;
126
143
  todosDir;
127
144
  fileHistoryDir;
145
+ tasksDir;
128
146
  constructor(projectsDirOrOptions) {
129
147
  if (typeof projectsDirOrOptions === "string") {
130
148
  this.projectsDir = projectsDirOrOptions;
@@ -132,18 +150,21 @@ var ClaudeCodeScanner = class {
132
150
  this.sessionEnvDir = null;
133
151
  this.todosDir = null;
134
152
  this.fileHistoryDir = null;
153
+ this.tasksDir = null;
135
154
  } else if (projectsDirOrOptions) {
136
155
  this.projectsDir = projectsDirOrOptions.projectsDir ?? getClaudeProjectsDir();
137
156
  this.configPath = projectsDirOrOptions.configPath === void 0 ? getClaudeConfigPath() : projectsDirOrOptions.configPath;
138
157
  this.sessionEnvDir = projectsDirOrOptions.sessionEnvDir === void 0 ? getClaudeSessionEnvDir() : projectsDirOrOptions.sessionEnvDir;
139
158
  this.todosDir = projectsDirOrOptions.todosDir === void 0 ? getClaudeTodosDir() : projectsDirOrOptions.todosDir;
140
159
  this.fileHistoryDir = projectsDirOrOptions.fileHistoryDir === void 0 ? getClaudeFileHistoryDir() : projectsDirOrOptions.fileHistoryDir;
160
+ this.tasksDir = projectsDirOrOptions.tasksDir === void 0 ? getClaudeTasksDir() : projectsDirOrOptions.tasksDir;
141
161
  } else {
142
162
  this.projectsDir = getClaudeProjectsDir();
143
163
  this.configPath = getClaudeConfigPath();
144
164
  this.sessionEnvDir = getClaudeSessionEnvDir();
145
165
  this.todosDir = getClaudeTodosDir();
146
166
  this.fileHistoryDir = getClaudeFileHistoryDir();
167
+ this.tasksDir = getClaudeTasksDir();
147
168
  }
148
169
  }
149
170
  async isAvailable() {
@@ -190,6 +211,8 @@ var ClaudeCodeScanner = class {
190
211
  sessions.push(...todosSessions);
191
212
  const fileHistorySessions = await this.scanFileHistoryDir(validSessionIds);
192
213
  sessions.push(...fileHistorySessions);
214
+ const tasksSessions = await this.scanTasksDir(validSessionIds);
215
+ sessions.push(...tasksSessions);
193
216
  const totalSize = sessions.reduce((sum, s) => sum + s.size, 0);
194
217
  return {
195
218
  toolName: this.name,
@@ -359,6 +382,40 @@ var ClaudeCodeScanner = class {
359
382
  }
360
383
  return orphanedHistories;
361
384
  }
385
+ /**
386
+ * Detect orphaned folders from ~/.claude/tasks
387
+ * Folder name is the session UUID, contains .lock file
388
+ */
389
+ async scanTasksDir(validSessionIds) {
390
+ if (!this.tasksDir) {
391
+ return [];
392
+ }
393
+ const orphanedTasks = [];
394
+ try {
395
+ await access(this.tasksDir);
396
+ const entries = await readdir2(this.tasksDir, { withFileTypes: true });
397
+ for (const entry of entries) {
398
+ if (!entry.isDirectory()) continue;
399
+ const sessionId = entry.name;
400
+ if (!validSessionIds.has(sessionId)) {
401
+ const taskPath = join3(this.tasksDir, entry.name);
402
+ const size = await getDirectorySize(taskPath);
403
+ const taskStat = await stat2(taskPath);
404
+ orphanedTasks.push({
405
+ toolName: this.name,
406
+ sessionPath: taskPath,
407
+ projectPath: sessionId,
408
+ // Session UUID
409
+ size,
410
+ lastModified: taskStat.mtime,
411
+ type: "tasks"
412
+ });
413
+ }
414
+ }
415
+ } catch {
416
+ }
417
+ return orphanedTasks;
418
+ }
362
419
  /**
363
420
  * Extract project path (cwd) from JSONL file
364
421
  */
@@ -602,6 +659,7 @@ function outputTable(results, verbose) {
602
659
  const sessionEnvEntries = allSessions.filter((s) => s.type === "session-env");
603
660
  const todosEntries = allSessions.filter((s) => s.type === "todos");
604
661
  const fileHistoryEntries = allSessions.filter((s) => s.type === "file-history");
662
+ const tasksEntries = allSessions.filter((s) => s.type === "tasks");
605
663
  const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
606
664
  if (allSessions.length === 0) {
607
665
  logger.success("No orphaned sessions found.");
@@ -624,6 +682,9 @@ function outputTable(results, verbose) {
624
682
  if (fileHistoryEntries.length > 0) {
625
683
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
626
684
  }
685
+ if (tasksEntries.length > 0) {
686
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
687
+ }
627
688
  logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
628
689
  console.log();
629
690
  const summaryTable = new Table({
@@ -634,6 +695,7 @@ function outputTable(results, verbose) {
634
695
  chalk2.cyan("Env"),
635
696
  chalk2.cyan("Todos"),
636
697
  chalk2.cyan("History"),
698
+ chalk2.cyan("Tasks"),
637
699
  chalk2.cyan("Size"),
638
700
  chalk2.cyan("Scan Time")
639
701
  ],
@@ -646,6 +708,7 @@ function outputTable(results, verbose) {
646
708
  const envs = result.sessions.filter((s) => s.type === "session-env").length;
647
709
  const todos = result.sessions.filter((s) => s.type === "todos").length;
648
710
  const histories = result.sessions.filter((s) => s.type === "file-history").length;
711
+ const tasks = result.sessions.filter((s) => s.type === "tasks").length;
649
712
  summaryTable.push([
650
713
  result.toolName,
651
714
  folders > 0 ? String(folders) : "-",
@@ -653,6 +716,7 @@ function outputTable(results, verbose) {
653
716
  envs > 0 ? String(envs) : "-",
654
717
  todos > 0 ? String(todos) : "-",
655
718
  histories > 0 ? String(histories) : "-",
719
+ tasks > 0 ? String(tasks) : "-",
656
720
  formatSize(result.totalSize),
657
721
  `${result.scanDuration.toFixed(0)}ms`
658
722
  ]);
@@ -669,7 +733,7 @@ function outputTable(results, verbose) {
669
733
  console.log(
670
734
  ` ${chalk2.cyan(`[${session.toolName}]`)} ${chalk2.white(projectName)} ${chalk2.dim(`(${formatSize(session.size)})`)}`
671
735
  );
672
- console.log(` ${chalk2.dim("\u2192")} ${session.projectPath}`);
736
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(session.projectPath)}`);
673
737
  console.log(` ${chalk2.dim("Modified:")} ${session.lastModified.toLocaleDateString()}`);
674
738
  console.log();
675
739
  }
@@ -683,7 +747,7 @@ function outputTable(results, verbose) {
683
747
  console.log(
684
748
  ` ${chalk2.yellow("[config]")} ${chalk2.white(projectName)}`
685
749
  );
686
- console.log(` ${chalk2.dim("\u2192")} ${entry.projectPath}`);
750
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.projectPath)}`);
687
751
  if (entry.configStats?.lastCost) {
688
752
  const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
689
753
  const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
@@ -693,6 +757,58 @@ function outputTable(results, verbose) {
693
757
  console.log();
694
758
  }
695
759
  }
760
+ if (sessionEnvEntries.length > 0) {
761
+ console.log();
762
+ console.log(chalk2.bold("Empty Session Env Folders:"));
763
+ console.log();
764
+ for (const entry of sessionEnvEntries) {
765
+ const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
766
+ console.log(
767
+ ` ${chalk2.green("[session-env]")} ${chalk2.white(folderName)} ${chalk2.dim("(empty)")}`
768
+ );
769
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
770
+ console.log();
771
+ }
772
+ }
773
+ if (todosEntries.length > 0) {
774
+ console.log();
775
+ console.log(chalk2.bold("Orphaned Todos:"));
776
+ console.log();
777
+ for (const entry of todosEntries) {
778
+ const fileName = entry.sessionPath.split("/").pop() || entry.sessionPath;
779
+ console.log(
780
+ ` ${chalk2.magenta("[todos]")} ${chalk2.white(fileName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
781
+ );
782
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
783
+ console.log();
784
+ }
785
+ }
786
+ if (fileHistoryEntries.length > 0) {
787
+ console.log();
788
+ console.log(chalk2.bold("Orphaned File History:"));
789
+ console.log();
790
+ for (const entry of fileHistoryEntries) {
791
+ const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
792
+ console.log(
793
+ ` ${chalk2.blue("[file-history]")} ${chalk2.white(folderName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
794
+ );
795
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
796
+ console.log();
797
+ }
798
+ }
799
+ if (tasksEntries.length > 0) {
800
+ console.log();
801
+ console.log(chalk2.bold("Orphaned Tasks:"));
802
+ console.log();
803
+ for (const entry of tasksEntries) {
804
+ const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
805
+ console.log(
806
+ ` ${chalk2.cyan("[tasks]")} ${chalk2.white(folderName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
807
+ );
808
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
809
+ console.log();
810
+ }
811
+ }
696
812
  }
697
813
  console.log();
698
814
  console.log(
@@ -709,7 +825,7 @@ import inquirer from "inquirer";
709
825
 
710
826
  // src/core/cleaner.ts
711
827
  import { readFile as readFile3, writeFile, mkdir, copyFile } from "fs/promises";
712
- import { existsSync } from "fs";
828
+ import { existsSync as existsSync2 } from "fs";
713
829
  import { join as join5 } from "path";
714
830
  import { homedir as homedir2 } from "os";
715
831
 
@@ -751,7 +867,8 @@ var Cleaner = class {
751
867
  session: 0,
752
868
  sessionEnv: 0,
753
869
  todos: 0,
754
- fileHistory: 0
870
+ fileHistory: 0,
871
+ tasks: 0
755
872
  },
756
873
  skippedCount: 0,
757
874
  alreadyGoneCount: 0,
@@ -782,6 +899,9 @@ var Cleaner = class {
782
899
  case "file-history":
783
900
  result.deletedByType.fileHistory++;
784
901
  break;
902
+ case "tasks":
903
+ result.deletedByType.tasks++;
904
+ break;
785
905
  default:
786
906
  result.deletedByType.session++;
787
907
  }
@@ -821,7 +941,7 @@ var Cleaner = class {
821
941
  const configPath = entries[0].sessionPath;
822
942
  const projectPathsToRemove = new Set(entries.map((e) => e.projectPath));
823
943
  const backupDir = getBackupDir();
824
- if (!existsSync(backupDir)) {
944
+ if (!existsSync2(backupDir)) {
825
945
  await mkdir(backupDir, { recursive: true });
826
946
  }
827
947
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -852,10 +972,29 @@ function formatSessionChoice(session) {
852
972
  const isConfig = session.type === "config";
853
973
  const toolTag = isConfig ? chalk3.yellow("[config]") : chalk3.cyan(`[${session.toolName}]`);
854
974
  const name = chalk3.white(projectName);
855
- const size = isConfig ? "" : chalk3.dim(`(${formatSize(session.size)})`);
856
- const path = chalk3.dim(`\u2192 ${session.projectPath}`);
857
- return `${toolTag} ${name} ${size}
858
- ${path}`;
975
+ const size = isConfig ? "" : ` ${chalk3.dim(`(${formatSize(session.size)})`)}`;
976
+ const path = chalk3.dim(`\u2192 ${tildify(session.projectPath)}`);
977
+ return `${toolTag} ${name}${size}
978
+ ${path}`;
979
+ }
980
+ function formatGroupChoice(group) {
981
+ const labels = {
982
+ "session-env": "empty session-env folder",
983
+ "todos": "orphaned todos file",
984
+ "file-history": "orphaned file-history folder",
985
+ "tasks": "orphaned tasks folder"
986
+ };
987
+ const colors = {
988
+ "session-env": chalk3.green,
989
+ "todos": chalk3.magenta,
990
+ "file-history": chalk3.blue,
991
+ "tasks": chalk3.cyan
992
+ };
993
+ const label = labels[group.type] || group.type;
994
+ const count = group.sessions.length;
995
+ const plural = count > 1 ? "s" : "";
996
+ const color = colors[group.type] || chalk3.white;
997
+ return `${color(`[${group.type}]`)} ${chalk3.white(`${count} ${label}${plural}`)} ${chalk3.dim(`(${formatSize(group.totalSize)})`)}`;
859
998
  }
860
999
  var cleanCommand = new Command2("clean").description("Remove orphaned session data").option("-f, --force", "Skip confirmation prompt").option("-n, --dry-run", "Show what would be deleted without deleting").option("-i, --interactive", "Select sessions to delete interactively").option("--no-trash", "Permanently delete instead of moving to trash").option("-v, --verbose", "Enable verbose output").action(async (options) => {
861
1000
  const spinner = ora2("Scanning for orphaned sessions...").start();
@@ -885,14 +1024,39 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
885
1024
  const fileHistoryEntries = allSessions.filter(
886
1025
  (s) => s.type === "file-history"
887
1026
  );
888
- const autoCleanEntries = [
889
- ...sessionEnvEntries,
890
- ...todosEntries,
891
- ...fileHistoryEntries
892
- ];
893
- const selectableSessions = allSessions.filter(
894
- (s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history"
1027
+ const tasksEntries = allSessions.filter((s) => s.type === "tasks");
1028
+ const individualSessions = allSessions.filter(
1029
+ (s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history" && s.type !== "tasks"
895
1030
  );
1031
+ const groupChoices = [];
1032
+ if (sessionEnvEntries.length > 0) {
1033
+ groupChoices.push({
1034
+ type: "session-env",
1035
+ sessions: sessionEnvEntries,
1036
+ totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0)
1037
+ });
1038
+ }
1039
+ if (todosEntries.length > 0) {
1040
+ groupChoices.push({
1041
+ type: "todos",
1042
+ sessions: todosEntries,
1043
+ totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0)
1044
+ });
1045
+ }
1046
+ if (fileHistoryEntries.length > 0) {
1047
+ groupChoices.push({
1048
+ type: "file-history",
1049
+ sessions: fileHistoryEntries,
1050
+ totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0)
1051
+ });
1052
+ }
1053
+ if (tasksEntries.length > 0) {
1054
+ groupChoices.push({
1055
+ type: "tasks",
1056
+ sessions: tasksEntries,
1057
+ totalSize: tasksEntries.reduce((sum, s) => sum + s.size, 0)
1058
+ });
1059
+ }
896
1060
  console.log();
897
1061
  const parts = [];
898
1062
  if (folderSessions.length > 0) {
@@ -910,62 +1074,89 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
910
1074
  if (fileHistoryEntries.length > 0) {
911
1075
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
912
1076
  }
1077
+ if (tasksEntries.length > 0) {
1078
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
1079
+ }
913
1080
  logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
914
1081
  if (options.verbose && !options.interactive) {
915
1082
  console.log();
916
- for (const session of selectableSessions) {
1083
+ for (const session of individualSessions) {
917
1084
  console.log(
918
- chalk3.dim(` ${session.toolName}: ${session.projectPath}`)
1085
+ chalk3.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
919
1086
  );
920
1087
  }
921
1088
  }
922
1089
  let sessionsToClean = allSessions;
923
1090
  if (options.interactive) {
924
- if (selectableSessions.length === 0) {
925
- if (autoCleanEntries.length > 0) {
926
- sessionsToClean = autoCleanEntries;
927
- logger.info(
928
- `Only ${autoCleanEntries.length} auto-cleanup target(s) found`
929
- );
930
- } else {
931
- logger.info("No selectable sessions found.");
932
- return;
1091
+ if (!process.stdout.isTTY) {
1092
+ logger.error("Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.");
1093
+ return;
1094
+ }
1095
+ const choices = [];
1096
+ for (const session of individualSessions) {
1097
+ choices.push({
1098
+ name: formatSessionChoice(session),
1099
+ value: { type: "individual", session },
1100
+ checked: false
1101
+ });
1102
+ }
1103
+ for (const group of groupChoices) {
1104
+ choices.push({
1105
+ name: formatGroupChoice(group),
1106
+ value: { type: "group", group },
1107
+ checked: false
1108
+ });
1109
+ }
1110
+ if (choices.length === 0) {
1111
+ logger.info("No selectable sessions found.");
1112
+ return;
1113
+ }
1114
+ console.log();
1115
+ const { selected } = await inquirer.prompt([
1116
+ {
1117
+ type: "checkbox",
1118
+ name: "selected",
1119
+ message: "Select items to delete:",
1120
+ choices,
1121
+ pageSize: 15,
1122
+ loop: false
933
1123
  }
934
- } else {
935
- console.log();
936
- const { selected } = await inquirer.prompt([
937
- {
938
- type: "checkbox",
939
- name: "selected",
940
- message: "Select sessions to delete:",
941
- choices: selectableSessions.map((session) => ({
942
- name: formatSessionChoice(session),
943
- value: session,
944
- checked: false
945
- })),
946
- pageSize: 15,
947
- loop: false
948
- }
949
- ]);
950
- if (selected.length === 0) {
951
- logger.info("No sessions selected. Cancelled.");
952
- return;
1124
+ ]);
1125
+ if (selected.length === 0) {
1126
+ logger.info("No items selected. Cancelled.");
1127
+ return;
1128
+ }
1129
+ if (process.stdout.isTTY) {
1130
+ const linesToClear = selected.length + 1;
1131
+ for (let i = 0; i < linesToClear; i++) {
1132
+ process.stdout.write("\x1B[1A\x1B[2K");
953
1133
  }
954
- sessionsToClean = [...selected, ...autoCleanEntries];
955
- const selectedSize = selected.reduce((sum, s) => sum + s.size, 0);
956
- console.log();
957
- if (selected.length > 0) {
958
- logger.info(
959
- `Selected: ${selected.length} session(s) (${formatSize(selectedSize)})`
960
- );
1134
+ }
1135
+ console.log(chalk3.green("\u2714") + " " + chalk3.bold("Selected items:"));
1136
+ for (const item of selected) {
1137
+ if (item.type === "individual") {
1138
+ const s = item.session;
1139
+ const tag = s.type === "config" ? chalk3.yellow("[config]") : chalk3.cyan(`[${s.toolName}]`);
1140
+ const size = s.type === "config" ? "" : ` ${chalk3.dim(`(${formatSize(s.size)})`)}`;
1141
+ console.log(` ${tag} ${basename(s.projectPath)}${size}`);
1142
+ console.log(` ${chalk3.dim(`\u2192 ${tildify(s.projectPath)}`)}`);
1143
+ } else {
1144
+ console.log(` ${formatGroupChoice(item.group)}`);
961
1145
  }
962
- if (autoCleanEntries.length > 0) {
963
- const autoSize = autoCleanEntries.reduce((sum, s) => sum + s.size, 0);
964
- logger.info(
965
- `+ ${autoCleanEntries.length} auto-cleanup target(s) (${formatSize(autoSize)})`
966
- );
1146
+ }
1147
+ sessionsToClean = [];
1148
+ for (const item of selected) {
1149
+ if (item.type === "individual") {
1150
+ sessionsToClean.push(item.session);
1151
+ } else {
1152
+ sessionsToClean.push(...item.group.sessions);
967
1153
  }
968
1154
  }
1155
+ const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
1156
+ console.log();
1157
+ logger.info(
1158
+ `Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
1159
+ );
969
1160
  }
970
1161
  const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
971
1162
  if (options.dryRun) {
@@ -985,9 +1176,12 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
985
1176
  const dryRunHistories = sessionsToClean.filter(
986
1177
  (s) => s.type === "file-history"
987
1178
  );
1179
+ const dryRunTasks = sessionsToClean.filter(
1180
+ (s) => s.type === "tasks"
1181
+ );
988
1182
  for (const session of dryRunFolders) {
989
1183
  console.log(
990
- ` ${chalk3.red("Would delete:")} ${session.sessionPath} (${formatSize(session.size)})`
1184
+ ` ${chalk3.red("Would delete:")} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
991
1185
  );
992
1186
  }
993
1187
  if (dryRunConfigs.length > 0) {
@@ -996,29 +1190,40 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
996
1190
  ` ${chalk3.yellow("Would remove from ~/.claude.json:")}`
997
1191
  );
998
1192
  for (const config of dryRunConfigs) {
999
- console.log(` - ${config.projectPath}`);
1193
+ console.log(` - ${tildify(config.projectPath)}`);
1000
1194
  }
1001
1195
  }
1002
- const autoCleanParts = [];
1003
1196
  if (dryRunEnvs.length > 0) {
1004
- autoCleanParts.push(`${dryRunEnvs.length} session-env`);
1197
+ console.log();
1198
+ console.log(
1199
+ ` ${chalk3.green("Would delete:")} ${dryRunEnvs.length} session-env folder(s)`
1200
+ );
1005
1201
  }
1006
1202
  if (dryRunTodos.length > 0) {
1007
- autoCleanParts.push(`${dryRunTodos.length} todos`);
1203
+ console.log();
1204
+ console.log(
1205
+ ` ${chalk3.magenta("Would delete:")} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
1206
+ );
1008
1207
  }
1009
1208
  if (dryRunHistories.length > 0) {
1010
- autoCleanParts.push(`${dryRunHistories.length} file-history`);
1209
+ console.log();
1210
+ console.log(
1211
+ ` ${chalk3.blue("Would delete:")} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
1212
+ );
1011
1213
  }
1012
- if (autoCleanParts.length > 0) {
1013
- const autoSize = dryRunEnvs.reduce((sum, s) => sum + s.size, 0) + dryRunTodos.reduce((sum, s) => sum + s.size, 0) + dryRunHistories.reduce((sum, s) => sum + s.size, 0);
1214
+ if (dryRunTasks.length > 0) {
1014
1215
  console.log();
1015
1216
  console.log(
1016
- ` ${chalk3.dim(`Would auto-delete: ${autoCleanParts.join(" + ")} (${formatSize(autoSize)})`)}`
1217
+ ` ${chalk3.cyan("Would delete:")} ${dryRunTasks.length} tasks folder(s) (${formatSize(dryRunTasks.reduce((sum, s) => sum + s.size, 0))})`
1017
1218
  );
1018
1219
  }
1019
1220
  return;
1020
1221
  }
1021
1222
  if (!options.force) {
1223
+ if (!process.stdout.isTTY) {
1224
+ logger.error("Confirmation requires a TTY. Use -f to skip confirmation.");
1225
+ return;
1226
+ }
1022
1227
  console.log();
1023
1228
  const action = options.noTrash ? "permanently delete" : "move to trash";
1024
1229
  const { confirmed } = await inquirer.prompt([
@@ -1058,6 +1263,9 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1058
1263
  if (deletedByType.fileHistory > 0) {
1059
1264
  parts2.push(`${deletedByType.fileHistory} file-history`);
1060
1265
  }
1266
+ if (deletedByType.tasks > 0) {
1267
+ parts2.push(`${deletedByType.tasks} tasks`);
1268
+ }
1061
1269
  const summary = parts2.length > 0 ? parts2.join(" + ") : `${cleanResult.deletedCount} item(s)`;
1062
1270
  logger.success(
1063
1271
  `${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`
@@ -1080,7 +1288,7 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1080
1288
  logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
1081
1289
  if (options.verbose) {
1082
1290
  for (const err of cleanResult.errors) {
1083
- console.log(chalk3.red(` ${err.sessionPath}: ${err.error.message}`));
1291
+ console.log(chalk3.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
1084
1292
  }
1085
1293
  }
1086
1294
  }
@@ -1096,12 +1304,12 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1096
1304
  // src/commands/watch.ts
1097
1305
  import { Command as Command3 } from "commander";
1098
1306
  import chalk4 from "chalk";
1099
- import { existsSync as existsSync4 } from "fs";
1307
+ import { existsSync as existsSync5 } from "fs";
1100
1308
  import { homedir as homedir5 } from "os";
1101
1309
  import { join as join8, resolve as resolve2 } from "path";
1102
1310
 
1103
1311
  // src/utils/config.ts
1104
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
1312
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
1105
1313
  import { homedir as homedir3 } from "os";
1106
1314
  import { dirname as dirname2, join as join6, resolve } from "path";
1107
1315
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1116,7 +1324,7 @@ function getAppVersion() {
1116
1324
  ];
1117
1325
  for (const packagePath of paths) {
1118
1326
  try {
1119
- if (existsSync2(packagePath)) {
1327
+ if (existsSync3(packagePath)) {
1120
1328
  const content = readFileSync(packagePath, "utf-8");
1121
1329
  const pkg = JSON.parse(content);
1122
1330
  return pkg.version;
@@ -1132,7 +1340,7 @@ function getConfigPath() {
1132
1340
  }
1133
1341
  function loadConfig() {
1134
1342
  const configPath = getConfigPath();
1135
- if (!existsSync2(configPath)) {
1343
+ if (!existsSync3(configPath)) {
1136
1344
  return {};
1137
1345
  }
1138
1346
  try {
@@ -1145,7 +1353,7 @@ function loadConfig() {
1145
1353
  function saveConfig(config) {
1146
1354
  const configPath = getConfigPath();
1147
1355
  const configDir = dirname2(configPath);
1148
- if (!existsSync2(configDir)) {
1356
+ if (!existsSync3(configDir)) {
1149
1357
  mkdirSync(configDir, { recursive: true });
1150
1358
  }
1151
1359
  const { version: _removed, ...cleanConfig } = config;
@@ -1241,17 +1449,17 @@ var IGNORED_SYSTEM_PATTERNS = [
1241
1449
  // All hidden folders (starting with .)
1242
1450
  /(^|[/\\])\../,
1243
1451
  // macOS user folders (not project directories)
1244
- /\/Library\//,
1245
- /\/Applications\//,
1246
- /\/Music\//,
1247
- /\/Movies\//,
1248
- /\/Pictures\//,
1249
- /\/Downloads\//,
1250
- /\/Documents\//,
1251
- /\/Desktop\//,
1252
- /\/Public\//,
1452
+ /\/Library(\/|$)/,
1453
+ /\/Applications(\/|$)/,
1454
+ /\/Music(\/|$)/,
1455
+ /\/Movies(\/|$)/,
1456
+ /\/Pictures(\/|$)/,
1457
+ /\/Downloads(\/|$)/,
1458
+ /\/Documents(\/|$)/,
1459
+ /\/Desktop(\/|$)/,
1460
+ /\/Public(\/|$)/,
1253
1461
  // Development folders
1254
- /node_modules/
1462
+ /node_modules(\/|$)/
1255
1463
  ];
1256
1464
 
1257
1465
  // src/core/watcher.ts
@@ -1384,37 +1592,67 @@ var Watcher = class {
1384
1592
  import { homedir as homedir4 } from "os";
1385
1593
  import { join as join7, dirname as dirname3 } from "path";
1386
1594
  import { readFile as readFile4, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1387
- import { existsSync as existsSync3 } from "fs";
1595
+ import { existsSync as existsSync4 } from "fs";
1388
1596
  import { execSync } from "child_process";
1389
1597
  var SERVICE_LABEL = "sooink.ai-session-tidy.watcher";
1390
1598
  var PLIST_FILENAME = `${SERVICE_LABEL}.plist`;
1599
+ var BIN_NAME = "ai-session-tidy";
1600
+ var BUNDLE_IDENTIFIER = "io.github.sooink.ai-session-tidy";
1391
1601
  function getPlistPath() {
1392
1602
  return join7(homedir4(), "Library", "LaunchAgents", PLIST_FILENAME);
1393
1603
  }
1604
+ function getBinPath() {
1605
+ try {
1606
+ const binPath = execSync(`which ${BIN_NAME}`, {
1607
+ encoding: "utf-8",
1608
+ stdio: ["pipe", "pipe", "pipe"]
1609
+ }).trim();
1610
+ if (binPath && existsSync4(binPath)) {
1611
+ return binPath;
1612
+ }
1613
+ } catch {
1614
+ }
1615
+ return null;
1616
+ }
1394
1617
  function getNodePath() {
1395
1618
  return process.execPath;
1396
1619
  }
1397
1620
  function getScriptPath() {
1398
1621
  const scriptPath = process.argv[1];
1399
- if (scriptPath && existsSync3(scriptPath)) {
1622
+ if (scriptPath && existsSync4(scriptPath)) {
1400
1623
  return scriptPath;
1401
1624
  }
1402
1625
  throw new Error("Could not determine script path");
1403
1626
  }
1627
+ function getProgramArgs(args) {
1628
+ const binPath = getBinPath();
1629
+ if (binPath) {
1630
+ return [binPath, ...args];
1631
+ }
1632
+ return [getNodePath(), getScriptPath(), ...args];
1633
+ }
1404
1634
  function generatePlist(options) {
1405
- const allArgs = [options.nodePath, options.scriptPath, ...options.args];
1406
- const argsXml = allArgs.map((arg) => ` <string>${arg}</string>`).join("\n");
1635
+ const argsXml = options.programArgs.map((arg) => ` <string>${arg}</string>`).join("\n");
1407
1636
  const home = homedir4();
1637
+ const nodeBinDir = dirname3(process.execPath);
1638
+ const systemPath = process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
1639
+ const envPath = systemPath.includes(nodeBinDir) ? systemPath : `${nodeBinDir}:${systemPath}`;
1408
1640
  return `<?xml version="1.0" encoding="UTF-8"?>
1409
1641
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1410
1642
  <plist version="1.0">
1411
1643
  <dict>
1412
1644
  <key>Label</key>
1413
1645
  <string>${options.label}</string>
1646
+ <key>AssociatedBundleIdentifiers</key>
1647
+ <array>
1648
+ <string>${BUNDLE_IDENTIFIER}</string>
1649
+ </array>
1414
1650
  <key>EnvironmentVariables</key>
1415
1651
  <dict>
1416
1652
  <key>HOME</key>
1417
1653
  <string>${home}</string>
1654
+ <key>PATH</key>
1655
+ <string>${envPath}</string>
1418
1656
  </dict>
1419
1657
  <key>ProgramArguments</key>
1420
1658
  <array>
@@ -1444,11 +1682,11 @@ var ServiceManager = class {
1444
1682
  throw new Error("Service management is only supported on macOS");
1445
1683
  }
1446
1684
  const launchAgentsDir = dirname3(this.plistPath);
1447
- if (!existsSync3(launchAgentsDir)) {
1685
+ if (!existsSync4(launchAgentsDir)) {
1448
1686
  await mkdir2(launchAgentsDir, { recursive: true });
1449
1687
  }
1450
1688
  const logDir = join7(homedir4(), ".ai-session-tidy");
1451
- if (!existsSync3(logDir)) {
1689
+ if (!existsSync4(logDir)) {
1452
1690
  await mkdir2(logDir, { recursive: true });
1453
1691
  }
1454
1692
  const stdoutPath = join7(logDir, "watcher.log");
@@ -1457,14 +1695,12 @@ var ServiceManager = class {
1457
1695
  await writeFile2(stderrPath, "", "utf-8");
1458
1696
  const plistContent = generatePlist({
1459
1697
  label: SERVICE_LABEL,
1460
- nodePath: getNodePath(),
1461
- scriptPath: getScriptPath(),
1462
- args: ["watch", "run"]
1698
+ programArgs: getProgramArgs(["watch", "run"])
1463
1699
  });
1464
1700
  await writeFile2(this.plistPath, plistContent, "utf-8");
1465
1701
  }
1466
1702
  async uninstall() {
1467
- if (existsSync3(this.plistPath)) {
1703
+ if (existsSync4(this.plistPath)) {
1468
1704
  await unlink(this.plistPath);
1469
1705
  }
1470
1706
  }
@@ -1472,9 +1708,18 @@ var ServiceManager = class {
1472
1708
  if (!this.isSupported()) {
1473
1709
  throw new Error("Service management is only supported on macOS");
1474
1710
  }
1475
- if (!existsSync3(this.plistPath)) {
1711
+ if (!existsSync4(this.plistPath)) {
1476
1712
  throw new Error('Service not installed. Run "watch start" to install and start.');
1477
1713
  }
1714
+ const logDir = join7(homedir4(), ".ai-session-tidy");
1715
+ const stdoutPath = join7(logDir, "watcher.log");
1716
+ const stderrPath = join7(logDir, "watcher.error.log");
1717
+ if (existsSync4(stdoutPath)) {
1718
+ await writeFile2(stdoutPath, "", "utf-8");
1719
+ }
1720
+ if (existsSync4(stderrPath)) {
1721
+ await writeFile2(stderrPath, "", "utf-8");
1722
+ }
1478
1723
  try {
1479
1724
  execSync(`launchctl load "${this.plistPath}"`, { stdio: "pipe" });
1480
1725
  } catch (error) {
@@ -1488,7 +1733,7 @@ var ServiceManager = class {
1488
1733
  if (!this.isSupported()) {
1489
1734
  throw new Error("Service management is only supported on macOS");
1490
1735
  }
1491
- if (!existsSync3(this.plistPath)) {
1736
+ if (!existsSync4(this.plistPath)) {
1492
1737
  return;
1493
1738
  }
1494
1739
  try {
@@ -1505,7 +1750,7 @@ var ServiceManager = class {
1505
1750
  if (!this.isSupported()) {
1506
1751
  return info;
1507
1752
  }
1508
- if (!existsSync3(this.plistPath)) {
1753
+ if (!existsSync4(this.plistPath)) {
1509
1754
  return info;
1510
1755
  }
1511
1756
  try {
@@ -1538,7 +1783,7 @@ var ServiceManager = class {
1538
1783
  let stdout = "";
1539
1784
  let stderr = "";
1540
1785
  try {
1541
- if (existsSync3(stdoutPath)) {
1786
+ if (existsSync4(stdoutPath)) {
1542
1787
  const content = await readFile4(stdoutPath, "utf-8");
1543
1788
  const logLines = content.split("\n");
1544
1789
  stdout = logLines.slice(-lines).join("\n");
@@ -1546,7 +1791,7 @@ var ServiceManager = class {
1546
1791
  } catch {
1547
1792
  }
1548
1793
  try {
1549
- if (existsSync3(stderrPath)) {
1794
+ if (existsSync4(stderrPath)) {
1550
1795
  const content = await readFile4(stderrPath, "utf-8");
1551
1796
  const logLines = content.split("\n");
1552
1797
  stderr = logLines.slice(-lines).join("\n");
@@ -1638,7 +1883,7 @@ var statusCommand = new Command3("status").description("Show watcher service sta
1638
1883
  if (status.pid) {
1639
1884
  console.log(`PID: ${status.pid}`);
1640
1885
  }
1641
- console.log(`Plist: ${status.plistPath}`);
1886
+ console.log(`Plist: ${tildify(status.plistPath)}`);
1642
1887
  console.log();
1643
1888
  if (options.logs) {
1644
1889
  const lines = parseInt(options.logs, 10) || 20;
@@ -1701,14 +1946,14 @@ async function runWatcher(options) {
1701
1946
  logger.info(`Using default watch paths.`);
1702
1947
  }
1703
1948
  }
1704
- const validPaths = watchPaths.filter((p) => existsSync4(p));
1949
+ const validPaths = watchPaths.filter((p) => existsSync5(p));
1705
1950
  if (validPaths.length === 0) {
1706
1951
  logger.error("No valid watch paths found. Use -p to specify paths.");
1707
1952
  return;
1708
1953
  }
1709
1954
  if (validPaths.length < watchPaths.length) {
1710
- const invalidPaths = watchPaths.filter((p) => !existsSync4(p));
1711
- logger.warn(`Skipping non-existent paths: ${invalidPaths.join(", ")}`);
1955
+ const invalidPaths = watchPaths.filter((p) => !existsSync5(p));
1956
+ logger.warn(`Skipping non-existent paths: ${invalidPaths.map(tildify).join(", ")}`);
1712
1957
  }
1713
1958
  const allScanners = createAllScanners();
1714
1959
  const availableScanners = await getAvailableScanners(allScanners);
@@ -1720,7 +1965,7 @@ async function runWatcher(options) {
1720
1965
  logger.info(
1721
1966
  `Watching for project deletions (${availableScanners.map((s) => s.name).join(", ")})`
1722
1967
  );
1723
- logger.info(`Watch paths: ${validPaths.join(", ")}`);
1968
+ logger.info(`Watch paths: ${validPaths.map(tildify).join(", ")}`);
1724
1969
  logger.info(`Cleanup delay: ${String(delayMinutes)} minute(s)`);
1725
1970
  logger.info(`Watch depth: ${String(depth)}`);
1726
1971
  if (process.stdout.isTTY) {
@@ -1732,15 +1977,16 @@ async function runWatcher(options) {
1732
1977
  watchPaths: validPaths,
1733
1978
  delayMs,
1734
1979
  depth,
1735
- ignorePaths: getIgnorePaths(),
1980
+ ignorePaths: getIgnorePaths() ?? [],
1736
1981
  onDelete: async (events) => {
1737
- if (events.length === 1) {
1738
- logger.info(`Detected deletion: ${events[0].path}`);
1982
+ const firstEvent = events[0];
1983
+ if (events.length === 1 && firstEvent) {
1984
+ logger.info(`Detected deletion: ${tildify(firstEvent.path)}`);
1739
1985
  } else {
1740
1986
  logger.info(`Detected ${events.length} deletions (debounced)`);
1741
1987
  if (options.verbose) {
1742
1988
  for (const event of events) {
1743
- logger.debug(` - ${event.path}`);
1989
+ logger.debug(` - ${tildify(event.path)}`);
1744
1990
  }
1745
1991
  }
1746
1992
  }
@@ -1772,6 +2018,9 @@ async function runWatcher(options) {
1772
2018
  if (deletedByType.fileHistory > 0) {
1773
2019
  parts.push(`${deletedByType.fileHistory} file-history`);
1774
2020
  }
2021
+ if (deletedByType.tasks > 0) {
2022
+ parts.push(`${deletedByType.tasks} tasks`);
2023
+ }
1775
2024
  const summary = parts.length > 0 ? parts.join(" + ") : `${cleanResult.deletedCount} item(s)`;
1776
2025
  logger.success(
1777
2026
  `${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`
@@ -1904,7 +2153,7 @@ pathCommand.command("list").description("List watch paths").action(() => {
1904
2153
  }
1905
2154
  console.log();
1906
2155
  for (const p of paths) {
1907
- console.log(` ${p}`);
2156
+ console.log(` ${tildify(p)}`);
1908
2157
  }
1909
2158
  });
1910
2159
  configCommand.addCommand(pathCommand);
@@ -1929,7 +2178,7 @@ ignoreCommand.command("list").description("List ignored paths").action(() => {
1929
2178
  }
1930
2179
  console.log();
1931
2180
  for (const p of paths) {
1932
- console.log(` ${p}`);
2181
+ console.log(` ${tildify(p)}`);
1933
2182
  }
1934
2183
  });
1935
2184
  configCommand.addCommand(ignoreCommand);
@@ -1980,6 +2229,10 @@ configCommand.command("show").description("Show all configuration").action(() =>
1980
2229
  });
1981
2230
  configCommand.command("reset").description("Reset configuration to defaults").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
1982
2231
  if (!options.force) {
2232
+ if (!process.stdout.isTTY) {
2233
+ logger.error("Confirmation requires a TTY. Use -f to skip confirmation.");
2234
+ return;
2235
+ }
1983
2236
  const { confirmed } = await inquirer2.prompt([
1984
2237
  {
1985
2238
  type: "confirm",