@sooink/ai-session-tidy 0.1.1 → 0.1.3

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) {
@@ -118,6 +128,10 @@ function tildify(path) {
118
128
  }
119
129
 
120
130
  // src/scanners/claude-code.ts
131
+ import { readdir as readdir2, readFile, stat as stat2, access } from "fs/promises";
132
+ import { join as join3 } from "path";
133
+ import { createReadStream } from "fs";
134
+ import { createInterface } from "readline";
121
135
  var ClaudeCodeScanner = class {
122
136
  name = "claude-code";
123
137
  projectsDir;
@@ -375,7 +389,7 @@ var ClaudeCodeScanner = class {
375
389
  }
376
390
  }
377
391
  async findCwdInJsonl(jsonlPath) {
378
- return new Promise((resolve3) => {
392
+ return new Promise((resolve4) => {
379
393
  const stream = createReadStream(jsonlPath, { encoding: "utf-8" });
380
394
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
381
395
  let found = false;
@@ -393,16 +407,16 @@ var ClaudeCodeScanner = class {
393
407
  found = true;
394
408
  rl.close();
395
409
  stream.destroy();
396
- resolve3(entry.cwd);
410
+ resolve4(entry.cwd);
397
411
  }
398
412
  } catch {
399
413
  }
400
414
  });
401
415
  rl.on("close", () => {
402
- if (!found) resolve3(null);
416
+ if (!found) resolve4(null);
403
417
  });
404
418
  rl.on("error", () => {
405
- resolve3(null);
419
+ resolve4(null);
406
420
  });
407
421
  });
408
422
  }
@@ -669,7 +683,7 @@ function outputTable(results, verbose) {
669
683
  console.log(
670
684
  ` ${chalk2.cyan(`[${session.toolName}]`)} ${chalk2.white(projectName)} ${chalk2.dim(`(${formatSize(session.size)})`)}`
671
685
  );
672
- console.log(` ${chalk2.dim("\u2192")} ${session.projectPath}`);
686
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(session.projectPath)}`);
673
687
  console.log(` ${chalk2.dim("Modified:")} ${session.lastModified.toLocaleDateString()}`);
674
688
  console.log();
675
689
  }
@@ -683,7 +697,7 @@ function outputTable(results, verbose) {
683
697
  console.log(
684
698
  ` ${chalk2.yellow("[config]")} ${chalk2.white(projectName)}`
685
699
  );
686
- console.log(` ${chalk2.dim("\u2192")} ${entry.projectPath}`);
700
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.projectPath)}`);
687
701
  if (entry.configStats?.lastCost) {
688
702
  const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
689
703
  const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
@@ -693,6 +707,45 @@ function outputTable(results, verbose) {
693
707
  console.log();
694
708
  }
695
709
  }
710
+ if (sessionEnvEntries.length > 0) {
711
+ console.log();
712
+ console.log(chalk2.bold("Empty Session Env Folders:"));
713
+ console.log();
714
+ for (const entry of sessionEnvEntries) {
715
+ const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
716
+ console.log(
717
+ ` ${chalk2.green("[session-env]")} ${chalk2.white(folderName)} ${chalk2.dim("(empty)")}`
718
+ );
719
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
720
+ console.log();
721
+ }
722
+ }
723
+ if (todosEntries.length > 0) {
724
+ console.log();
725
+ console.log(chalk2.bold("Orphaned Todos:"));
726
+ console.log();
727
+ for (const entry of todosEntries) {
728
+ const fileName = entry.sessionPath.split("/").pop() || entry.sessionPath;
729
+ console.log(
730
+ ` ${chalk2.magenta("[todos]")} ${chalk2.white(fileName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
731
+ );
732
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
733
+ console.log();
734
+ }
735
+ }
736
+ if (fileHistoryEntries.length > 0) {
737
+ console.log();
738
+ console.log(chalk2.bold("Orphaned File History:"));
739
+ console.log();
740
+ for (const entry of fileHistoryEntries) {
741
+ const folderName = entry.sessionPath.split("/").pop() || entry.sessionPath;
742
+ console.log(
743
+ ` ${chalk2.blue("[file-history]")} ${chalk2.white(folderName)} ${chalk2.dim(`(${formatSize(entry.size)})`)}`
744
+ );
745
+ console.log(` ${chalk2.dim("\u2192")} ${tildify(entry.sessionPath)}`);
746
+ console.log();
747
+ }
748
+ }
696
749
  }
697
750
  console.log();
698
751
  console.log(
@@ -709,7 +762,7 @@ import inquirer from "inquirer";
709
762
 
710
763
  // src/core/cleaner.ts
711
764
  import { readFile as readFile3, writeFile, mkdir, copyFile } from "fs/promises";
712
- import { existsSync } from "fs";
765
+ import { existsSync as existsSync2 } from "fs";
713
766
  import { join as join5 } from "path";
714
767
  import { homedir as homedir2 } from "os";
715
768
 
@@ -821,7 +874,7 @@ var Cleaner = class {
821
874
  const configPath = entries[0].sessionPath;
822
875
  const projectPathsToRemove = new Set(entries.map((e) => e.projectPath));
823
876
  const backupDir = getBackupDir();
824
- if (!existsSync(backupDir)) {
877
+ if (!existsSync2(backupDir)) {
825
878
  await mkdir(backupDir, { recursive: true });
826
879
  }
827
880
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -852,10 +905,27 @@ function formatSessionChoice(session) {
852
905
  const isConfig = session.type === "config";
853
906
  const toolTag = isConfig ? chalk3.yellow("[config]") : chalk3.cyan(`[${session.toolName}]`);
854
907
  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}`;
908
+ const size = isConfig ? "" : ` ${chalk3.dim(`(${formatSize(session.size)})`)}`;
909
+ const path = chalk3.dim(`\u2192 ${tildify(session.projectPath)}`);
910
+ return `${toolTag} ${name}${size}
911
+ ${path}`;
912
+ }
913
+ function formatGroupChoice(group) {
914
+ const labels = {
915
+ "session-env": "empty session-env folder",
916
+ "todos": "orphaned todos file",
917
+ "file-history": "orphaned file-history folder"
918
+ };
919
+ const colors = {
920
+ "session-env": chalk3.green,
921
+ "todos": chalk3.magenta,
922
+ "file-history": chalk3.blue
923
+ };
924
+ const label = labels[group.type] || group.type;
925
+ const count = group.sessions.length;
926
+ const plural = count > 1 ? "s" : "";
927
+ const color = colors[group.type] || chalk3.white;
928
+ return `${color(`[${group.type}]`)} ${chalk3.white(`${count} ${label}${plural}`)} ${chalk3.dim(`(${formatSize(group.totalSize)})`)}`;
859
929
  }
860
930
  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
931
  const spinner = ora2("Scanning for orphaned sessions...").start();
@@ -885,14 +955,31 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
885
955
  const fileHistoryEntries = allSessions.filter(
886
956
  (s) => s.type === "file-history"
887
957
  );
888
- const autoCleanEntries = [
889
- ...sessionEnvEntries,
890
- ...todosEntries,
891
- ...fileHistoryEntries
892
- ];
893
- const selectableSessions = allSessions.filter(
958
+ const individualSessions = allSessions.filter(
894
959
  (s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history"
895
960
  );
961
+ const groupChoices = [];
962
+ if (sessionEnvEntries.length > 0) {
963
+ groupChoices.push({
964
+ type: "session-env",
965
+ sessions: sessionEnvEntries,
966
+ totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0)
967
+ });
968
+ }
969
+ if (todosEntries.length > 0) {
970
+ groupChoices.push({
971
+ type: "todos",
972
+ sessions: todosEntries,
973
+ totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0)
974
+ });
975
+ }
976
+ if (fileHistoryEntries.length > 0) {
977
+ groupChoices.push({
978
+ type: "file-history",
979
+ sessions: fileHistoryEntries,
980
+ totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0)
981
+ });
982
+ }
896
983
  console.log();
897
984
  const parts = [];
898
985
  if (folderSessions.length > 0) {
@@ -913,59 +1000,83 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
913
1000
  logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
914
1001
  if (options.verbose && !options.interactive) {
915
1002
  console.log();
916
- for (const session of selectableSessions) {
1003
+ for (const session of individualSessions) {
917
1004
  console.log(
918
- chalk3.dim(` ${session.toolName}: ${session.projectPath}`)
1005
+ chalk3.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
919
1006
  );
920
1007
  }
921
1008
  }
922
1009
  let sessionsToClean = allSessions;
923
1010
  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;
1011
+ if (!process.stdout.isTTY) {
1012
+ logger.error("Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.");
1013
+ return;
1014
+ }
1015
+ const choices = [];
1016
+ for (const session of individualSessions) {
1017
+ choices.push({
1018
+ name: formatSessionChoice(session),
1019
+ value: { type: "individual", session },
1020
+ checked: false
1021
+ });
1022
+ }
1023
+ for (const group of groupChoices) {
1024
+ choices.push({
1025
+ name: formatGroupChoice(group),
1026
+ value: { type: "group", group },
1027
+ checked: false
1028
+ });
1029
+ }
1030
+ if (choices.length === 0) {
1031
+ logger.info("No selectable sessions found.");
1032
+ return;
1033
+ }
1034
+ console.log();
1035
+ const { selected } = await inquirer.prompt([
1036
+ {
1037
+ type: "checkbox",
1038
+ name: "selected",
1039
+ message: "Select items to delete:",
1040
+ choices,
1041
+ pageSize: 15,
1042
+ loop: false
933
1043
  }
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;
1044
+ ]);
1045
+ if (selected.length === 0) {
1046
+ logger.info("No items selected. Cancelled.");
1047
+ return;
1048
+ }
1049
+ if (process.stdout.isTTY) {
1050
+ const linesToClear = selected.length + 1;
1051
+ for (let i = 0; i < linesToClear; i++) {
1052
+ process.stdout.write("\x1B[1A\x1B[2K");
953
1053
  }
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
- );
1054
+ }
1055
+ console.log(chalk3.green("\u2714") + " " + chalk3.bold("Selected items:"));
1056
+ for (const item of selected) {
1057
+ if (item.type === "individual") {
1058
+ const s = item.session;
1059
+ const tag = s.type === "config" ? chalk3.yellow("[config]") : chalk3.cyan(`[${s.toolName}]`);
1060
+ const size = s.type === "config" ? "" : ` ${chalk3.dim(`(${formatSize(s.size)})`)}`;
1061
+ console.log(` ${tag} ${basename(s.projectPath)}${size}`);
1062
+ console.log(` ${chalk3.dim(`\u2192 ${tildify(s.projectPath)}`)}`);
1063
+ } else {
1064
+ console.log(` ${formatGroupChoice(item.group)}`);
961
1065
  }
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
- );
1066
+ }
1067
+ sessionsToClean = [];
1068
+ for (const item of selected) {
1069
+ if (item.type === "individual") {
1070
+ sessionsToClean.push(item.session);
1071
+ } else {
1072
+ sessionsToClean.push(...item.group.sessions);
967
1073
  }
968
1074
  }
1075
+ const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
1076
+ console.log();
1077
+ logger.info(
1078
+ `Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
1079
+ );
969
1080
  }
970
1081
  const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
971
1082
  if (options.dryRun) {
@@ -987,7 +1098,7 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
987
1098
  );
988
1099
  for (const session of dryRunFolders) {
989
1100
  console.log(
990
- ` ${chalk3.red("Would delete:")} ${session.sessionPath} (${formatSize(session.size)})`
1101
+ ` ${chalk3.red("Would delete:")} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
991
1102
  );
992
1103
  }
993
1104
  if (dryRunConfigs.length > 0) {
@@ -996,29 +1107,34 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
996
1107
  ` ${chalk3.yellow("Would remove from ~/.claude.json:")}`
997
1108
  );
998
1109
  for (const config of dryRunConfigs) {
999
- console.log(` - ${config.projectPath}`);
1110
+ console.log(` - ${tildify(config.projectPath)}`);
1000
1111
  }
1001
1112
  }
1002
- const autoCleanParts = [];
1003
1113
  if (dryRunEnvs.length > 0) {
1004
- autoCleanParts.push(`${dryRunEnvs.length} session-env`);
1114
+ console.log();
1115
+ console.log(
1116
+ ` ${chalk3.green("Would delete:")} ${dryRunEnvs.length} session-env folder(s)`
1117
+ );
1005
1118
  }
1006
1119
  if (dryRunTodos.length > 0) {
1007
- autoCleanParts.push(`${dryRunTodos.length} todos`);
1120
+ console.log();
1121
+ console.log(
1122
+ ` ${chalk3.magenta("Would delete:")} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
1123
+ );
1008
1124
  }
1009
1125
  if (dryRunHistories.length > 0) {
1010
- autoCleanParts.push(`${dryRunHistories.length} file-history`);
1011
- }
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);
1014
1126
  console.log();
1015
1127
  console.log(
1016
- ` ${chalk3.dim(`Would auto-delete: ${autoCleanParts.join(" + ")} (${formatSize(autoSize)})`)}`
1128
+ ` ${chalk3.blue("Would delete:")} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
1017
1129
  );
1018
1130
  }
1019
1131
  return;
1020
1132
  }
1021
1133
  if (!options.force) {
1134
+ if (!process.stdout.isTTY) {
1135
+ logger.error("Confirmation requires a TTY. Use -f to skip confirmation.");
1136
+ return;
1137
+ }
1022
1138
  console.log();
1023
1139
  const action = options.noTrash ? "permanently delete" : "move to trash";
1024
1140
  const { confirmed } = await inquirer.prompt([
@@ -1080,7 +1196,7 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1080
1196
  logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
1081
1197
  if (options.verbose) {
1082
1198
  for (const err of cleanResult.errors) {
1083
- console.log(chalk3.red(` ${err.sessionPath}: ${err.error.message}`));
1199
+ console.log(chalk3.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
1084
1200
  }
1085
1201
  }
1086
1202
  }
@@ -1096,12 +1212,12 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1096
1212
  // src/commands/watch.ts
1097
1213
  import { Command as Command3 } from "commander";
1098
1214
  import chalk4 from "chalk";
1099
- import { existsSync as existsSync4 } from "fs";
1215
+ import { existsSync as existsSync5 } from "fs";
1100
1216
  import { homedir as homedir5 } from "os";
1101
1217
  import { join as join8, resolve as resolve2 } from "path";
1102
1218
 
1103
1219
  // src/utils/config.ts
1104
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
1220
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
1105
1221
  import { homedir as homedir3 } from "os";
1106
1222
  import { dirname as dirname2, join as join6, resolve } from "path";
1107
1223
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1116,7 +1232,7 @@ function getAppVersion() {
1116
1232
  ];
1117
1233
  for (const packagePath of paths) {
1118
1234
  try {
1119
- if (existsSync2(packagePath)) {
1235
+ if (existsSync3(packagePath)) {
1120
1236
  const content = readFileSync(packagePath, "utf-8");
1121
1237
  const pkg = JSON.parse(content);
1122
1238
  return pkg.version;
@@ -1132,7 +1248,7 @@ function getConfigPath() {
1132
1248
  }
1133
1249
  function loadConfig() {
1134
1250
  const configPath = getConfigPath();
1135
- if (!existsSync2(configPath)) {
1251
+ if (!existsSync3(configPath)) {
1136
1252
  return {};
1137
1253
  }
1138
1254
  try {
@@ -1145,13 +1261,13 @@ function loadConfig() {
1145
1261
  function saveConfig(config) {
1146
1262
  const configPath = getConfigPath();
1147
1263
  const configDir = dirname2(configPath);
1148
- if (!existsSync2(configDir)) {
1264
+ if (!existsSync3(configDir)) {
1149
1265
  mkdirSync(configDir, { recursive: true });
1150
1266
  }
1267
+ const { version: _removed, ...cleanConfig } = config;
1151
1268
  const configWithVersion = {
1152
- version: getAppVersion(),
1153
1269
  configVersion: CONFIG_VERSION,
1154
- ...config
1270
+ ...cleanConfig
1155
1271
  };
1156
1272
  writeFileSync(configPath, JSON.stringify(configWithVersion, null, 2), "utf-8");
1157
1273
  }
@@ -1206,10 +1322,55 @@ function setWatchDepth(depth) {
1206
1322
  function resetConfig() {
1207
1323
  saveConfig({});
1208
1324
  }
1325
+ function getIgnorePaths() {
1326
+ const config = loadConfig();
1327
+ return config.ignorePaths;
1328
+ }
1329
+ function addIgnorePath(path) {
1330
+ const config = loadConfig();
1331
+ const resolved = resolve(path.replace(/^~/, homedir3()));
1332
+ const paths = config.ignorePaths ?? [];
1333
+ if (!paths.includes(resolved)) {
1334
+ paths.push(resolved);
1335
+ config.ignorePaths = paths;
1336
+ saveConfig(config);
1337
+ }
1338
+ }
1339
+ function removeIgnorePath(path) {
1340
+ const config = loadConfig();
1341
+ const resolved = resolve(path.replace(/^~/, homedir3()));
1342
+ const paths = config.ignorePaths ?? [];
1343
+ const index = paths.indexOf(resolved);
1344
+ if (index === -1) return false;
1345
+ paths.splice(index, 1);
1346
+ config.ignorePaths = paths;
1347
+ saveConfig(config);
1348
+ return true;
1349
+ }
1209
1350
 
1210
1351
  // src/core/watcher.ts
1211
1352
  import { watch } from "chokidar";
1212
1353
  import { access as access4 } from "fs/promises";
1354
+
1355
+ // src/core/constants.ts
1356
+ var IGNORED_SYSTEM_PATTERNS = [
1357
+ // All hidden folders (starting with .)
1358
+ /(^|[/\\])\../,
1359
+ // macOS user folders (not project directories)
1360
+ /\/Library(\/|$)/,
1361
+ /\/Applications(\/|$)/,
1362
+ /\/Music(\/|$)/,
1363
+ /\/Movies(\/|$)/,
1364
+ /\/Pictures(\/|$)/,
1365
+ /\/Downloads(\/|$)/,
1366
+ /\/Documents(\/|$)/,
1367
+ /\/Desktop(\/|$)/,
1368
+ /\/Public(\/|$)/,
1369
+ // Development folders
1370
+ /node_modules(\/|$)/
1371
+ ];
1372
+
1373
+ // src/core/watcher.ts
1213
1374
  var Watcher = class {
1214
1375
  options;
1215
1376
  debounceMs;
@@ -1220,16 +1381,23 @@ var Watcher = class {
1220
1381
  batchedEvents = [];
1221
1382
  /** Debounce timer */
1222
1383
  debounceTimer = null;
1384
+ /** Paths that errored (to avoid duplicate logs) */
1385
+ erroredPaths = /* @__PURE__ */ new Set();
1223
1386
  constructor(options) {
1224
1387
  this.options = options;
1225
1388
  this.debounceMs = options.debounceMs ?? 1e4;
1226
1389
  }
1227
1390
  start() {
1228
1391
  if (this.watcher) return;
1392
+ const ignored = [...IGNORED_SYSTEM_PATTERNS];
1393
+ if (this.options.ignorePaths) {
1394
+ ignored.push(...this.options.ignorePaths);
1395
+ }
1229
1396
  this.watcher = watch(this.options.watchPaths, {
1230
1397
  ignoreInitial: true,
1231
1398
  persistent: true,
1232
- depth: this.options.depth ?? 3
1399
+ depth: this.options.depth ?? 3,
1400
+ ignored
1233
1401
  });
1234
1402
  this.watcher.on("unlinkDir", (path) => {
1235
1403
  this.handleDelete(path);
@@ -1237,6 +1405,13 @@ var Watcher = class {
1237
1405
  this.watcher.on("addDir", (path) => {
1238
1406
  this.handleRecovery(path);
1239
1407
  });
1408
+ this.watcher.on("error", (error) => {
1409
+ const pathInfo = error.path;
1410
+ if (pathInfo && !this.erroredPaths.has(pathInfo)) {
1411
+ this.erroredPaths.add(pathInfo);
1412
+ console.error(`[watch] Cannot watch: ${pathInfo} (${error.code})`);
1413
+ }
1414
+ });
1240
1415
  }
1241
1416
  stop() {
1242
1417
  if (!this.watcher) return;
@@ -1325,7 +1500,7 @@ var Watcher = class {
1325
1500
  import { homedir as homedir4 } from "os";
1326
1501
  import { join as join7, dirname as dirname3 } from "path";
1327
1502
  import { readFile as readFile4, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1328
- import { existsSync as existsSync3 } from "fs";
1503
+ import { existsSync as existsSync4 } from "fs";
1329
1504
  import { execSync } from "child_process";
1330
1505
  var SERVICE_LABEL = "sooink.ai-session-tidy.watcher";
1331
1506
  var PLIST_FILENAME = `${SERVICE_LABEL}.plist`;
@@ -1337,7 +1512,7 @@ function getNodePath() {
1337
1512
  }
1338
1513
  function getScriptPath() {
1339
1514
  const scriptPath = process.argv[1];
1340
- if (scriptPath && existsSync3(scriptPath)) {
1515
+ if (scriptPath && existsSync4(scriptPath)) {
1341
1516
  return scriptPath;
1342
1517
  }
1343
1518
  throw new Error("Could not determine script path");
@@ -1385,11 +1560,11 @@ var ServiceManager = class {
1385
1560
  throw new Error("Service management is only supported on macOS");
1386
1561
  }
1387
1562
  const launchAgentsDir = dirname3(this.plistPath);
1388
- if (!existsSync3(launchAgentsDir)) {
1563
+ if (!existsSync4(launchAgentsDir)) {
1389
1564
  await mkdir2(launchAgentsDir, { recursive: true });
1390
1565
  }
1391
1566
  const logDir = join7(homedir4(), ".ai-session-tidy");
1392
- if (!existsSync3(logDir)) {
1567
+ if (!existsSync4(logDir)) {
1393
1568
  await mkdir2(logDir, { recursive: true });
1394
1569
  }
1395
1570
  const stdoutPath = join7(logDir, "watcher.log");
@@ -1405,7 +1580,7 @@ var ServiceManager = class {
1405
1580
  await writeFile2(this.plistPath, plistContent, "utf-8");
1406
1581
  }
1407
1582
  async uninstall() {
1408
- if (existsSync3(this.plistPath)) {
1583
+ if (existsSync4(this.plistPath)) {
1409
1584
  await unlink(this.plistPath);
1410
1585
  }
1411
1586
  }
@@ -1413,9 +1588,18 @@ var ServiceManager = class {
1413
1588
  if (!this.isSupported()) {
1414
1589
  throw new Error("Service management is only supported on macOS");
1415
1590
  }
1416
- if (!existsSync3(this.plistPath)) {
1591
+ if (!existsSync4(this.plistPath)) {
1417
1592
  throw new Error('Service not installed. Run "watch start" to install and start.');
1418
1593
  }
1594
+ const logDir = join7(homedir4(), ".ai-session-tidy");
1595
+ const stdoutPath = join7(logDir, "watcher.log");
1596
+ const stderrPath = join7(logDir, "watcher.error.log");
1597
+ if (existsSync4(stdoutPath)) {
1598
+ await writeFile2(stdoutPath, "", "utf-8");
1599
+ }
1600
+ if (existsSync4(stderrPath)) {
1601
+ await writeFile2(stderrPath, "", "utf-8");
1602
+ }
1419
1603
  try {
1420
1604
  execSync(`launchctl load "${this.plistPath}"`, { stdio: "pipe" });
1421
1605
  } catch (error) {
@@ -1429,7 +1613,7 @@ var ServiceManager = class {
1429
1613
  if (!this.isSupported()) {
1430
1614
  throw new Error("Service management is only supported on macOS");
1431
1615
  }
1432
- if (!existsSync3(this.plistPath)) {
1616
+ if (!existsSync4(this.plistPath)) {
1433
1617
  return;
1434
1618
  }
1435
1619
  try {
@@ -1446,7 +1630,7 @@ var ServiceManager = class {
1446
1630
  if (!this.isSupported()) {
1447
1631
  return info;
1448
1632
  }
1449
- if (!existsSync3(this.plistPath)) {
1633
+ if (!existsSync4(this.plistPath)) {
1450
1634
  return info;
1451
1635
  }
1452
1636
  try {
@@ -1479,7 +1663,7 @@ var ServiceManager = class {
1479
1663
  let stdout = "";
1480
1664
  let stderr = "";
1481
1665
  try {
1482
- if (existsSync3(stdoutPath)) {
1666
+ if (existsSync4(stdoutPath)) {
1483
1667
  const content = await readFile4(stdoutPath, "utf-8");
1484
1668
  const logLines = content.split("\n");
1485
1669
  stdout = logLines.slice(-lines).join("\n");
@@ -1487,7 +1671,7 @@ var ServiceManager = class {
1487
1671
  } catch {
1488
1672
  }
1489
1673
  try {
1490
- if (existsSync3(stderrPath)) {
1674
+ if (existsSync4(stderrPath)) {
1491
1675
  const content = await readFile4(stderrPath, "utf-8");
1492
1676
  const logLines = content.split("\n");
1493
1677
  stderr = logLines.slice(-lines).join("\n");
@@ -1579,7 +1763,7 @@ var statusCommand = new Command3("status").description("Show watcher service sta
1579
1763
  if (status.pid) {
1580
1764
  console.log(`PID: ${status.pid}`);
1581
1765
  }
1582
- console.log(`Plist: ${status.plistPath}`);
1766
+ console.log(`Plist: ${tildify(status.plistPath)}`);
1583
1767
  console.log();
1584
1768
  if (options.logs) {
1585
1769
  const lines = parseInt(options.logs, 10) || 20;
@@ -1642,14 +1826,14 @@ async function runWatcher(options) {
1642
1826
  logger.info(`Using default watch paths.`);
1643
1827
  }
1644
1828
  }
1645
- const validPaths = watchPaths.filter((p) => existsSync4(p));
1829
+ const validPaths = watchPaths.filter((p) => existsSync5(p));
1646
1830
  if (validPaths.length === 0) {
1647
1831
  logger.error("No valid watch paths found. Use -p to specify paths.");
1648
1832
  return;
1649
1833
  }
1650
1834
  if (validPaths.length < watchPaths.length) {
1651
- const invalidPaths = watchPaths.filter((p) => !existsSync4(p));
1652
- logger.warn(`Skipping non-existent paths: ${invalidPaths.join(", ")}`);
1835
+ const invalidPaths = watchPaths.filter((p) => !existsSync5(p));
1836
+ logger.warn(`Skipping non-existent paths: ${invalidPaths.map(tildify).join(", ")}`);
1653
1837
  }
1654
1838
  const allScanners = createAllScanners();
1655
1839
  const availableScanners = await getAvailableScanners(allScanners);
@@ -1661,7 +1845,7 @@ async function runWatcher(options) {
1661
1845
  logger.info(
1662
1846
  `Watching for project deletions (${availableScanners.map((s) => s.name).join(", ")})`
1663
1847
  );
1664
- logger.info(`Watch paths: ${validPaths.join(", ")}`);
1848
+ logger.info(`Watch paths: ${validPaths.map(tildify).join(", ")}`);
1665
1849
  logger.info(`Cleanup delay: ${String(delayMinutes)} minute(s)`);
1666
1850
  logger.info(`Watch depth: ${String(depth)}`);
1667
1851
  if (process.stdout.isTTY) {
@@ -1673,14 +1857,15 @@ async function runWatcher(options) {
1673
1857
  watchPaths: validPaths,
1674
1858
  delayMs,
1675
1859
  depth,
1860
+ ignorePaths: getIgnorePaths(),
1676
1861
  onDelete: async (events) => {
1677
1862
  if (events.length === 1) {
1678
- logger.info(`Detected deletion: ${events[0].path}`);
1863
+ logger.info(`Detected deletion: ${tildify(events[0].path)}`);
1679
1864
  } else {
1680
1865
  logger.info(`Detected ${events.length} deletions (debounced)`);
1681
1866
  if (options.verbose) {
1682
1867
  for (const event of events) {
1683
- logger.debug(` - ${event.path}`);
1868
+ logger.debug(` - ${tildify(event.path)}`);
1684
1869
  }
1685
1870
  }
1686
1871
  }
@@ -1810,15 +1995,25 @@ var listCommand = new Command4("list").description("List detected AI coding tool
1810
1995
  // src/commands/config.ts
1811
1996
  import { Command as Command5 } from "commander";
1812
1997
  import inquirer2 from "inquirer";
1998
+ import { homedir as homedir6 } from "os";
1999
+ import { resolve as resolve3 } from "path";
1813
2000
  var configCommand = new Command5("config").description(
1814
2001
  "Manage configuration"
1815
2002
  );
1816
- var pathsCommand = new Command5("paths").description("Manage watch paths");
1817
- pathsCommand.command("add <path>").description("Add a watch path").action((path) => {
2003
+ var pathCommand = new Command5("path").description("Manage watch paths");
2004
+ pathCommand.command("add <path>").description("Add a watch path").action((path) => {
2005
+ const resolved = resolve3(path.replace(/^~/, homedir6()));
2006
+ const home = homedir6();
2007
+ if (resolved === home || home.startsWith(resolved + "/")) {
2008
+ logger.warn("Warning: Watching home directory is not recommended.");
2009
+ logger.warn("Many system folders will be excluded automatically.");
2010
+ logger.warn("Consider adding specific project folders instead.");
2011
+ console.log();
2012
+ }
1818
2013
  addWatchPath(path);
1819
2014
  logger.success(`Added: ${path}`);
1820
2015
  });
1821
- pathsCommand.command("remove <path>").description("Remove a watch path").action((path) => {
2016
+ pathCommand.command("remove <path>").description("Remove a watch path").action((path) => {
1822
2017
  const removed = removeWatchPath(path);
1823
2018
  if (removed) {
1824
2019
  logger.success(`Removed: ${path}`);
@@ -1826,7 +2021,7 @@ pathsCommand.command("remove <path>").description("Remove a watch path").action(
1826
2021
  logger.warn(`Path not found: ${path}`);
1827
2022
  }
1828
2023
  });
1829
- pathsCommand.command("list").description("List watch paths").action(() => {
2024
+ pathCommand.command("list").description("List watch paths").action(() => {
1830
2025
  const paths = getWatchPaths();
1831
2026
  if (!paths || paths.length === 0) {
1832
2027
  logger.info("No watch paths configured.");
@@ -1834,10 +2029,35 @@ pathsCommand.command("list").description("List watch paths").action(() => {
1834
2029
  }
1835
2030
  console.log();
1836
2031
  for (const p of paths) {
1837
- console.log(` ${p}`);
2032
+ console.log(` ${tildify(p)}`);
2033
+ }
2034
+ });
2035
+ configCommand.addCommand(pathCommand);
2036
+ var ignoreCommand = new Command5("ignore").description("Manage ignore paths");
2037
+ ignoreCommand.command("add <path>").description("Add a path to ignore from watching").action((path) => {
2038
+ addIgnorePath(path);
2039
+ logger.success(`Added to ignore: ${path}`);
2040
+ });
2041
+ ignoreCommand.command("remove <path>").description("Remove a path from ignore list").action((path) => {
2042
+ const removed = removeIgnorePath(path);
2043
+ if (removed) {
2044
+ logger.success(`Removed from ignore: ${path}`);
2045
+ } else {
2046
+ logger.warn(`Path not found: ${path}`);
2047
+ }
2048
+ });
2049
+ ignoreCommand.command("list").description("List ignored paths").action(() => {
2050
+ const paths = getIgnorePaths();
2051
+ if (!paths || paths.length === 0) {
2052
+ logger.info("No ignore paths configured.");
2053
+ return;
2054
+ }
2055
+ console.log();
2056
+ for (const p of paths) {
2057
+ console.log(` ${tildify(p)}`);
1838
2058
  }
1839
2059
  });
1840
- configCommand.addCommand(pathsCommand);
2060
+ configCommand.addCommand(ignoreCommand);
1841
2061
  var DEFAULT_DELAY_MINUTES2 = 5;
1842
2062
  var MAX_DELAY_MINUTES2 = 10;
1843
2063
  configCommand.command("delay [minutes]").description(`Get or set watch delay in minutes (default: ${DEFAULT_DELAY_MINUTES2}, max: ${MAX_DELAY_MINUTES2})`).action((minutes) => {
@@ -1885,6 +2105,10 @@ configCommand.command("show").description("Show all configuration").action(() =>
1885
2105
  });
1886
2106
  configCommand.command("reset").description("Reset configuration to defaults").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
1887
2107
  if (!options.force) {
2108
+ if (!process.stdout.isTTY) {
2109
+ logger.error("Confirmation requires a TTY. Use -f to skip confirmation.");
2110
+ return;
2111
+ }
1888
2112
  const { confirmed } = await inquirer2.prompt([
1889
2113
  {
1890
2114
  type: "confirm",