@sooink/ai-session-tidy 0.1.3 → 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.
@@ -0,0 +1,57 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ fetch-tags: true
20
+
21
+ - name: Get version from tag
22
+ id: version
23
+ run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
24
+
25
+ - name: Get previous tag
26
+ id: prev_tag
27
+ run: |
28
+ PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
29
+ echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT
30
+
31
+ - name: Read CHANGELOG
32
+ id: changelog
33
+ env:
34
+ VERSION: ${{ steps.version.outputs.version }}
35
+ PREV_TAG: ${{ steps.prev_tag.outputs.tag }}
36
+ CURRENT_TAG: ${{ github.ref_name }}
37
+ REPO: ${{ github.repository }}
38
+ run: |
39
+ # Extract section for this version only (between version header and next ## header)
40
+ CHANGES=$(sed -n "/^## ${VERSION}$/,/^## /p" CHANGELOG.md | sed '1d;$d')
41
+
42
+ # Add Full Changelog link if previous tag exists
43
+ if [ -n "$PREV_TAG" ]; then
44
+ LINK="**Full Changelog**: https://github.com/${REPO}/compare/${PREV_TAG}...${CURRENT_TAG}"
45
+ CHANGES=$(printf "%s\n\n---\n\n%s" "$CHANGES" "$LINK")
46
+ fi
47
+
48
+ echo "changes<<EOF" >> $GITHUB_OUTPUT
49
+ echo "$CHANGES" >> $GITHUB_OUTPUT
50
+ echo "EOF" >> $GITHUB_OUTPUT
51
+
52
+ - name: Create Release
53
+ uses: softprops/action-gh-release@v2
54
+ with:
55
+ body: ${{ steps.changelog.outputs.changes }}
56
+ env:
57
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4
4
+
5
+ - Added support for Claude Code tasks
6
+ - Improve macOS background activity attribution for LaunchAgent (displays as `ai-session-tidy`)
7
+
8
+ ## 0.1.3
9
+
10
+ - Fixed ignored folder patterns to match folders themselves (e.g., `/Downloads`, `/Documents`)
11
+ - Fixed log files not being cleared on service start, causing stale error messages
12
+ - Added TTY checks for interactive mode (`-i`) and confirmation prompts
13
+ - Added helpful error messages in non-TTY environments (scripts/CI)
14
+ - Changed `clean -i` to show clean "Selected items" list after selection
15
+
16
+ ## 0.1.2
17
+
18
+ - Added system folder ignore patterns for watch mode
19
+ - Added automatic exclusion of macOS system folders (Library, Applications, Downloads, etc.)
20
+ - Added exclusion of hidden folders and node_modules from watching
21
+
22
+ ## 0.1.1
23
+
24
+ - Fixed npm publish configuration
25
+
26
+ ## 0.1.0
27
+
28
+ - Added scan command to find orphaned sessions from Claude Code and Cursor
29
+ - Added clean command with interactive mode and trash support
30
+ - Added watch command for automatic cleanup on project deletion
31
+ - Added background daemon with auto-start at login (macOS)
32
+ - Added config command for managing watch paths, delay, and depth
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # AI Session Tidy
4
4
 
5
- **Clean up orphaned sessions from AI coding tools**
5
+ **Automatically clean up orphaned sessions from AI coding tools**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@sooink/ai-session-tidy.svg?style=flat-square)](https://www.npmjs.com/package/@sooink/ai-session-tidy)
8
8
  [![node](https://img.shields.io/badge/node-%3E%3D24-brightgreen?style=flat-square)](https://nodejs.org/)
@@ -15,7 +15,7 @@
15
15
 
16
16
  ## The Problem
17
17
 
18
- AI coding tools like **Claude Code** and **Cursor** store session data locally—conversation history, file snapshots, todos, and more.
18
+ AI coding tools like **Claude Code** and **Cursor** store session data locally—conversation history, file snapshots, todos, tasks, and more.
19
19
 
20
20
  When you delete, move, or rename a project, the session data becomes orphaned:
21
21
 
@@ -26,7 +26,8 @@ When you delete, move, or rename a project, the session data becomes orphaned:
26
26
  │ ├── -Users-you-temp-worktree/ # 👈 Worktree removed
27
27
  │ └── -Users-you-renamed-project/ # 👈 Project renamed
28
28
  ├── todos/ # Orphaned todo files
29
- └── file-history/ # Orphaned rewind snapshots
29
+ ├── file-history/ # Orphaned rewind snapshots
30
+ └── tasks/ # Orphaned task folders
30
31
  ```
31
32
 
32
33
  This becomes especially problematic with **git worktree workflows**, where branches are frequently created and removed—each leaving behind session data that accumulates quickly.
@@ -104,11 +105,11 @@ ai-session-tidy --json # JSON output
104
105
  ```
105
106
  ⚠ Found 2 session folder(s) + 1 config entry(ies) + 3 session-env folder(s) (156.2 MB)
106
107
 
107
- ┌─────────────┬──────────┬────────┬─────┬───────┬─────────┬──────────┬───────────┐
108
- │ Tool │ Sessions │ Config │ Env │ Todos │ History │ Size │ Scan Time │
109
- ├─────────────┼──────────┼────────┼─────┼───────┼─────────┼──────────┼───────────┤
110
- │ claude-code │ 2 │ 1 │ 3 │ - │ - │ 156.2 MB │ 45ms │
111
- └─────────────┴──────────┴────────┴─────┴───────┴─────────┴──────────┴───────────┘
108
+ ┌─────────────┬──────────┬────────┬─────┬───────┬─────────┬───────┬──────────┬───────────┐
109
+ │ Tool │ Sessions │ Config │ Env │ Todos │ History │ Tasks │ Size │ Scan Time │
110
+ ├─────────────┼──────────┼────────┼─────┼───────┼─────────┼───────┼──────────┼───────────┤
111
+ │ claude-code │ 2 │ 1 │ 3 │ - │ - │ - │ 156.2 MB │ 45ms │
112
+ └─────────────┴──────────┴────────┴─────┴───────┴─────────┴───────┴──────────┴───────────┘
112
113
 
113
114
  Session Folders:
114
115
 
@@ -183,6 +184,7 @@ ai-session-tidy config reset # Reset to defaults
183
184
  | `~/.claude/session-env/{uuid}/` | Session environment | Empty folder |
184
185
  | `~/.claude/todos/{uuid}-*.json` | Todo files | Session gone |
185
186
  | `~/.claude/file-history/{uuid}/` | Rewind snapshots | Session gone |
187
+ | `~/.claude/tasks/{uuid}/` | Task folders | Session gone |
186
188
 
187
189
  ### Cursor
188
190
 
Binary file
package/assets/demo.gif CHANGED
Binary file
package/dist/index.js CHANGED
@@ -119,6 +119,9 @@ function getClaudeTodosDir() {
119
119
  function getClaudeFileHistoryDir() {
120
120
  return join2(homedir(), ".claude", "file-history");
121
121
  }
122
+ function getClaudeTasksDir() {
123
+ return join2(homedir(), ".claude", "tasks");
124
+ }
122
125
  function tildify(path) {
123
126
  const home = homedir();
124
127
  if (path.startsWith(home)) {
@@ -139,6 +142,7 @@ var ClaudeCodeScanner = class {
139
142
  sessionEnvDir;
140
143
  todosDir;
141
144
  fileHistoryDir;
145
+ tasksDir;
142
146
  constructor(projectsDirOrOptions) {
143
147
  if (typeof projectsDirOrOptions === "string") {
144
148
  this.projectsDir = projectsDirOrOptions;
@@ -146,18 +150,21 @@ var ClaudeCodeScanner = class {
146
150
  this.sessionEnvDir = null;
147
151
  this.todosDir = null;
148
152
  this.fileHistoryDir = null;
153
+ this.tasksDir = null;
149
154
  } else if (projectsDirOrOptions) {
150
155
  this.projectsDir = projectsDirOrOptions.projectsDir ?? getClaudeProjectsDir();
151
156
  this.configPath = projectsDirOrOptions.configPath === void 0 ? getClaudeConfigPath() : projectsDirOrOptions.configPath;
152
157
  this.sessionEnvDir = projectsDirOrOptions.sessionEnvDir === void 0 ? getClaudeSessionEnvDir() : projectsDirOrOptions.sessionEnvDir;
153
158
  this.todosDir = projectsDirOrOptions.todosDir === void 0 ? getClaudeTodosDir() : projectsDirOrOptions.todosDir;
154
159
  this.fileHistoryDir = projectsDirOrOptions.fileHistoryDir === void 0 ? getClaudeFileHistoryDir() : projectsDirOrOptions.fileHistoryDir;
160
+ this.tasksDir = projectsDirOrOptions.tasksDir === void 0 ? getClaudeTasksDir() : projectsDirOrOptions.tasksDir;
155
161
  } else {
156
162
  this.projectsDir = getClaudeProjectsDir();
157
163
  this.configPath = getClaudeConfigPath();
158
164
  this.sessionEnvDir = getClaudeSessionEnvDir();
159
165
  this.todosDir = getClaudeTodosDir();
160
166
  this.fileHistoryDir = getClaudeFileHistoryDir();
167
+ this.tasksDir = getClaudeTasksDir();
161
168
  }
162
169
  }
163
170
  async isAvailable() {
@@ -204,6 +211,8 @@ var ClaudeCodeScanner = class {
204
211
  sessions.push(...todosSessions);
205
212
  const fileHistorySessions = await this.scanFileHistoryDir(validSessionIds);
206
213
  sessions.push(...fileHistorySessions);
214
+ const tasksSessions = await this.scanTasksDir(validSessionIds);
215
+ sessions.push(...tasksSessions);
207
216
  const totalSize = sessions.reduce((sum, s) => sum + s.size, 0);
208
217
  return {
209
218
  toolName: this.name,
@@ -373,6 +382,40 @@ var ClaudeCodeScanner = class {
373
382
  }
374
383
  return orphanedHistories;
375
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
+ }
376
419
  /**
377
420
  * Extract project path (cwd) from JSONL file
378
421
  */
@@ -616,6 +659,7 @@ function outputTable(results, verbose) {
616
659
  const sessionEnvEntries = allSessions.filter((s) => s.type === "session-env");
617
660
  const todosEntries = allSessions.filter((s) => s.type === "todos");
618
661
  const fileHistoryEntries = allSessions.filter((s) => s.type === "file-history");
662
+ const tasksEntries = allSessions.filter((s) => s.type === "tasks");
619
663
  const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
620
664
  if (allSessions.length === 0) {
621
665
  logger.success("No orphaned sessions found.");
@@ -638,6 +682,9 @@ function outputTable(results, verbose) {
638
682
  if (fileHistoryEntries.length > 0) {
639
683
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
640
684
  }
685
+ if (tasksEntries.length > 0) {
686
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
687
+ }
641
688
  logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
642
689
  console.log();
643
690
  const summaryTable = new Table({
@@ -648,6 +695,7 @@ function outputTable(results, verbose) {
648
695
  chalk2.cyan("Env"),
649
696
  chalk2.cyan("Todos"),
650
697
  chalk2.cyan("History"),
698
+ chalk2.cyan("Tasks"),
651
699
  chalk2.cyan("Size"),
652
700
  chalk2.cyan("Scan Time")
653
701
  ],
@@ -660,6 +708,7 @@ function outputTable(results, verbose) {
660
708
  const envs = result.sessions.filter((s) => s.type === "session-env").length;
661
709
  const todos = result.sessions.filter((s) => s.type === "todos").length;
662
710
  const histories = result.sessions.filter((s) => s.type === "file-history").length;
711
+ const tasks = result.sessions.filter((s) => s.type === "tasks").length;
663
712
  summaryTable.push([
664
713
  result.toolName,
665
714
  folders > 0 ? String(folders) : "-",
@@ -667,6 +716,7 @@ function outputTable(results, verbose) {
667
716
  envs > 0 ? String(envs) : "-",
668
717
  todos > 0 ? String(todos) : "-",
669
718
  histories > 0 ? String(histories) : "-",
719
+ tasks > 0 ? String(tasks) : "-",
670
720
  formatSize(result.totalSize),
671
721
  `${result.scanDuration.toFixed(0)}ms`
672
722
  ]);
@@ -746,6 +796,19 @@ function outputTable(results, verbose) {
746
796
  console.log();
747
797
  }
748
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
+ }
749
812
  }
750
813
  console.log();
751
814
  console.log(
@@ -804,7 +867,8 @@ var Cleaner = class {
804
867
  session: 0,
805
868
  sessionEnv: 0,
806
869
  todos: 0,
807
- fileHistory: 0
870
+ fileHistory: 0,
871
+ tasks: 0
808
872
  },
809
873
  skippedCount: 0,
810
874
  alreadyGoneCount: 0,
@@ -835,6 +899,9 @@ var Cleaner = class {
835
899
  case "file-history":
836
900
  result.deletedByType.fileHistory++;
837
901
  break;
902
+ case "tasks":
903
+ result.deletedByType.tasks++;
904
+ break;
838
905
  default:
839
906
  result.deletedByType.session++;
840
907
  }
@@ -914,12 +981,14 @@ function formatGroupChoice(group) {
914
981
  const labels = {
915
982
  "session-env": "empty session-env folder",
916
983
  "todos": "orphaned todos file",
917
- "file-history": "orphaned file-history folder"
984
+ "file-history": "orphaned file-history folder",
985
+ "tasks": "orphaned tasks folder"
918
986
  };
919
987
  const colors = {
920
988
  "session-env": chalk3.green,
921
989
  "todos": chalk3.magenta,
922
- "file-history": chalk3.blue
990
+ "file-history": chalk3.blue,
991
+ "tasks": chalk3.cyan
923
992
  };
924
993
  const label = labels[group.type] || group.type;
925
994
  const count = group.sessions.length;
@@ -955,8 +1024,9 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
955
1024
  const fileHistoryEntries = allSessions.filter(
956
1025
  (s) => s.type === "file-history"
957
1026
  );
1027
+ const tasksEntries = allSessions.filter((s) => s.type === "tasks");
958
1028
  const individualSessions = allSessions.filter(
959
- (s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history"
1029
+ (s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history" && s.type !== "tasks"
960
1030
  );
961
1031
  const groupChoices = [];
962
1032
  if (sessionEnvEntries.length > 0) {
@@ -980,6 +1050,13 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
980
1050
  totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0)
981
1051
  });
982
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
+ }
983
1060
  console.log();
984
1061
  const parts = [];
985
1062
  if (folderSessions.length > 0) {
@@ -997,6 +1074,9 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
997
1074
  if (fileHistoryEntries.length > 0) {
998
1075
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
999
1076
  }
1077
+ if (tasksEntries.length > 0) {
1078
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
1079
+ }
1000
1080
  logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
1001
1081
  if (options.verbose && !options.interactive) {
1002
1082
  console.log();
@@ -1096,6 +1176,9 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1096
1176
  const dryRunHistories = sessionsToClean.filter(
1097
1177
  (s) => s.type === "file-history"
1098
1178
  );
1179
+ const dryRunTasks = sessionsToClean.filter(
1180
+ (s) => s.type === "tasks"
1181
+ );
1099
1182
  for (const session of dryRunFolders) {
1100
1183
  console.log(
1101
1184
  ` ${chalk3.red("Would delete:")} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
@@ -1128,6 +1211,12 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1128
1211
  ` ${chalk3.blue("Would delete:")} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
1129
1212
  );
1130
1213
  }
1214
+ if (dryRunTasks.length > 0) {
1215
+ console.log();
1216
+ console.log(
1217
+ ` ${chalk3.cyan("Would delete:")} ${dryRunTasks.length} tasks folder(s) (${formatSize(dryRunTasks.reduce((sum, s) => sum + s.size, 0))})`
1218
+ );
1219
+ }
1131
1220
  return;
1132
1221
  }
1133
1222
  if (!options.force) {
@@ -1174,6 +1263,9 @@ var cleanCommand = new Command2("clean").description("Remove orphaned session da
1174
1263
  if (deletedByType.fileHistory > 0) {
1175
1264
  parts2.push(`${deletedByType.fileHistory} file-history`);
1176
1265
  }
1266
+ if (deletedByType.tasks > 0) {
1267
+ parts2.push(`${deletedByType.tasks} tasks`);
1268
+ }
1177
1269
  const summary = parts2.length > 0 ? parts2.join(" + ") : `${cleanResult.deletedCount} item(s)`;
1178
1270
  logger.success(
1179
1271
  `${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`
@@ -1504,9 +1596,24 @@ import { existsSync as existsSync4 } from "fs";
1504
1596
  import { execSync } from "child_process";
1505
1597
  var SERVICE_LABEL = "sooink.ai-session-tidy.watcher";
1506
1598
  var PLIST_FILENAME = `${SERVICE_LABEL}.plist`;
1599
+ var BIN_NAME = "ai-session-tidy";
1600
+ var BUNDLE_IDENTIFIER = "io.github.sooink.ai-session-tidy";
1507
1601
  function getPlistPath() {
1508
1602
  return join7(homedir4(), "Library", "LaunchAgents", PLIST_FILENAME);
1509
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
+ }
1510
1617
  function getNodePath() {
1511
1618
  return process.execPath;
1512
1619
  }
@@ -1517,20 +1624,35 @@ function getScriptPath() {
1517
1624
  }
1518
1625
  throw new Error("Could not determine script path");
1519
1626
  }
1627
+ function getProgramArgs(args) {
1628
+ const binPath = getBinPath();
1629
+ if (binPath) {
1630
+ return [binPath, ...args];
1631
+ }
1632
+ return [getNodePath(), getScriptPath(), ...args];
1633
+ }
1520
1634
  function generatePlist(options) {
1521
- const allArgs = [options.nodePath, options.scriptPath, ...options.args];
1522
- const argsXml = allArgs.map((arg) => ` <string>${arg}</string>`).join("\n");
1635
+ const argsXml = options.programArgs.map((arg) => ` <string>${arg}</string>`).join("\n");
1523
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}`;
1524
1640
  return `<?xml version="1.0" encoding="UTF-8"?>
1525
1641
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1526
1642
  <plist version="1.0">
1527
1643
  <dict>
1528
1644
  <key>Label</key>
1529
1645
  <string>${options.label}</string>
1646
+ <key>AssociatedBundleIdentifiers</key>
1647
+ <array>
1648
+ <string>${BUNDLE_IDENTIFIER}</string>
1649
+ </array>
1530
1650
  <key>EnvironmentVariables</key>
1531
1651
  <dict>
1532
1652
  <key>HOME</key>
1533
1653
  <string>${home}</string>
1654
+ <key>PATH</key>
1655
+ <string>${envPath}</string>
1534
1656
  </dict>
1535
1657
  <key>ProgramArguments</key>
1536
1658
  <array>
@@ -1573,9 +1695,7 @@ var ServiceManager = class {
1573
1695
  await writeFile2(stderrPath, "", "utf-8");
1574
1696
  const plistContent = generatePlist({
1575
1697
  label: SERVICE_LABEL,
1576
- nodePath: getNodePath(),
1577
- scriptPath: getScriptPath(),
1578
- args: ["watch", "run"]
1698
+ programArgs: getProgramArgs(["watch", "run"])
1579
1699
  });
1580
1700
  await writeFile2(this.plistPath, plistContent, "utf-8");
1581
1701
  }
@@ -1857,10 +1977,11 @@ async function runWatcher(options) {
1857
1977
  watchPaths: validPaths,
1858
1978
  delayMs,
1859
1979
  depth,
1860
- ignorePaths: getIgnorePaths(),
1980
+ ignorePaths: getIgnorePaths() ?? [],
1861
1981
  onDelete: async (events) => {
1862
- if (events.length === 1) {
1863
- logger.info(`Detected deletion: ${tildify(events[0].path)}`);
1982
+ const firstEvent = events[0];
1983
+ if (events.length === 1 && firstEvent) {
1984
+ logger.info(`Detected deletion: ${tildify(firstEvent.path)}`);
1864
1985
  } else {
1865
1986
  logger.info(`Detected ${events.length} deletions (debounced)`);
1866
1987
  if (options.verbose) {
@@ -1897,6 +2018,9 @@ async function runWatcher(options) {
1897
2018
  if (deletedByType.fileHistory > 0) {
1898
2019
  parts.push(`${deletedByType.fileHistory} file-history`);
1899
2020
  }
2021
+ if (deletedByType.tasks > 0) {
2022
+ parts.push(`${deletedByType.tasks} tasks`);
2023
+ }
1900
2024
  const summary = parts.length > 0 ? parts.join(" + ") : `${cleanResult.deletedCount} item(s)`;
1901
2025
  logger.success(
1902
2026
  `${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`