@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sooink/ai-session-tidy",
3
- "version": "0.1.2",
4
- "description": "CLI tool that detects and cleans orphaned session data from AI coding tools",
3
+ "version": "0.1.4",
4
+ "description": "CLI tool that automatically detects and cleans orphaned session data from AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ai-session-tidy": "./dist/index.js"
@@ -51,4 +51,4 @@
51
51
  "typescript-eslint": "^8.21.0",
52
52
  "vitest": "^3.0.4"
53
53
  }
54
- }
54
+ }
@@ -31,9 +31,35 @@ function formatSessionChoice(session: OrphanedSession): string {
31
31
  ? chalk.yellow('[config]')
32
32
  : chalk.cyan(`[${session.toolName}]`);
33
33
  const name = chalk.white(projectName);
34
- const size = isConfig ? '' : chalk.dim(`(${formatSize(session.size)})`);
35
- const path = chalk.dim(`→ ${session.projectPath}`);
36
- return `${toolTag} ${name} ${size}\n ${path}`;
34
+ const size = isConfig ? '' : ` ${chalk.dim(`(${formatSize(session.size)})`)}`;
35
+ const path = chalk.dim(`→ ${tildify(session.projectPath)}`);
36
+ return `${toolTag} ${name}${size}\n ${path}`;
37
+ }
38
+
39
+ interface GroupChoice {
40
+ type: 'session-env' | 'todos' | 'file-history' | 'tasks';
41
+ sessions: OrphanedSession[];
42
+ totalSize: number;
43
+ }
44
+
45
+ function formatGroupChoice(group: GroupChoice): string {
46
+ const labels: Record<string, string> = {
47
+ 'session-env': 'empty session-env folder',
48
+ 'todos': 'orphaned todos file',
49
+ 'file-history': 'orphaned file-history folder',
50
+ 'tasks': 'orphaned tasks folder',
51
+ };
52
+ const colors: Record<string, (s: string) => string> = {
53
+ 'session-env': chalk.green,
54
+ 'todos': chalk.magenta,
55
+ 'file-history': chalk.blue,
56
+ 'tasks': chalk.cyan,
57
+ };
58
+ const label = labels[group.type] || group.type;
59
+ const count = group.sessions.length;
60
+ const plural = count > 1 ? 's' : '';
61
+ const color = colors[group.type] || chalk.white;
62
+ return `${color(`[${group.type}]`)} ${chalk.white(`${count} ${label}${plural}`)} ${chalk.dim(`(${formatSize(group.totalSize)})`)}`;
37
63
  }
38
64
 
39
65
  export const cleanCommand = new Command('clean')
@@ -67,7 +93,7 @@ export const cleanCommand = new Command('clean')
67
93
  return;
68
94
  }
69
95
 
70
- // Separate session folders, config entries, and auto-cleanup targets
96
+ // Separate by type
71
97
  const folderSessions = allSessions.filter(
72
98
  (s) => s.type === 'session' || s.type === undefined
73
99
  );
@@ -79,22 +105,48 @@ export const cleanCommand = new Command('clean')
79
105
  const fileHistoryEntries = allSessions.filter(
80
106
  (s) => s.type === 'file-history'
81
107
  );
108
+ const tasksEntries = allSessions.filter((s) => s.type === 'tasks');
82
109
 
83
- // Auto-cleanup targets (session-env, todos, file-history)
84
- const autoCleanEntries = [
85
- ...sessionEnvEntries,
86
- ...todosEntries,
87
- ...fileHistoryEntries,
88
- ];
89
-
90
- // Interactive selection targets (excluding auto-cleanup targets)
91
- const selectableSessions = allSessions.filter(
110
+ // Individual selection targets (session folders and config entries)
111
+ const individualSessions = allSessions.filter(
92
112
  (s) =>
93
113
  s.type !== 'session-env' &&
94
114
  s.type !== 'todos' &&
95
- s.type !== 'file-history'
115
+ s.type !== 'file-history' &&
116
+ s.type !== 'tasks'
96
117
  );
97
118
 
119
+ // Group selection targets (session-env, todos, file-history, tasks)
120
+ const groupChoices: GroupChoice[] = [];
121
+ if (sessionEnvEntries.length > 0) {
122
+ groupChoices.push({
123
+ type: 'session-env',
124
+ sessions: sessionEnvEntries,
125
+ totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0),
126
+ });
127
+ }
128
+ if (todosEntries.length > 0) {
129
+ groupChoices.push({
130
+ type: 'todos',
131
+ sessions: todosEntries,
132
+ totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0),
133
+ });
134
+ }
135
+ if (fileHistoryEntries.length > 0) {
136
+ groupChoices.push({
137
+ type: 'file-history',
138
+ sessions: fileHistoryEntries,
139
+ totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0),
140
+ });
141
+ }
142
+ if (tasksEntries.length > 0) {
143
+ groupChoices.push({
144
+ type: 'tasks',
145
+ sessions: tasksEntries,
146
+ totalSize: tasksEntries.reduce((sum, s) => sum + s.size, 0),
147
+ });
148
+ }
149
+
98
150
  // Output summary
99
151
  console.log();
100
152
  const parts: string[] = [];
@@ -113,72 +165,115 @@ export const cleanCommand = new Command('clean')
113
165
  if (fileHistoryEntries.length > 0) {
114
166
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
115
167
  }
168
+ if (tasksEntries.length > 0) {
169
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
170
+ }
116
171
  logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
117
172
 
118
173
  if (options.verbose && !options.interactive) {
119
174
  console.log();
120
- // Exclude auto-cleanup targets from detailed list
121
- for (const session of selectableSessions) {
175
+ for (const session of individualSessions) {
122
176
  console.log(
123
- chalk.dim(` ${session.toolName}: ${session.projectPath}`)
177
+ chalk.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
124
178
  );
125
179
  }
126
180
  }
127
181
 
128
- // Interactive mode: session selection (excluding auto-cleanup targets)
182
+ // Interactive mode: session selection
129
183
  let sessionsToClean = allSessions;
130
184
  if (options.interactive) {
131
- if (selectableSessions.length === 0) {
132
- // Only auto-cleanup targets exist
133
- if (autoCleanEntries.length > 0) {
134
- sessionsToClean = autoCleanEntries;
135
- logger.info(
136
- `Only ${autoCleanEntries.length} auto-cleanup target(s) found`
137
- );
138
- } else {
139
- logger.info('No selectable sessions found.');
140
- return;
141
- }
142
- } else {
143
- console.log();
144
- const { selected } = await inquirer.prompt<{
145
- selected: OrphanedSession[];
146
- }>([
147
- {
148
- type: 'checkbox',
149
- name: 'selected',
150
- message: 'Select sessions to delete:',
151
- choices: selectableSessions.map((session) => ({
152
- name: formatSessionChoice(session),
153
- value: session,
154
- checked: false,
155
- })),
156
- pageSize: 15,
157
- loop: false,
158
- },
159
- ]);
160
-
161
- if (selected.length === 0) {
162
- logger.info('No sessions selected. Cancelled.');
163
- return;
185
+ if (!process.stdout.isTTY) {
186
+ logger.error('Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.');
187
+ return;
188
+ }
189
+ // Build choices: individual sessions + group choices
190
+ const choices: Array<{
191
+ name: string;
192
+ value: { type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice };
193
+ checked: boolean;
194
+ }> = [];
195
+
196
+ // Add individual sessions
197
+ for (const session of individualSessions) {
198
+ choices.push({
199
+ name: formatSessionChoice(session),
200
+ value: { type: 'individual', session },
201
+ checked: false,
202
+ });
203
+ }
204
+
205
+ // Add group choices
206
+ for (const group of groupChoices) {
207
+ choices.push({
208
+ name: formatGroupChoice(group),
209
+ value: { type: 'group', group },
210
+ checked: false,
211
+ });
212
+ }
213
+
214
+ if (choices.length === 0) {
215
+ logger.info('No selectable sessions found.');
216
+ return;
217
+ }
218
+
219
+ console.log();
220
+ const { selected } = await inquirer.prompt<{
221
+ selected: Array<{ type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice }>;
222
+ }>([
223
+ {
224
+ type: 'checkbox',
225
+ name: 'selected',
226
+ message: 'Select items to delete:',
227
+ choices,
228
+ pageSize: 15,
229
+ loop: false,
230
+ },
231
+ ]);
232
+
233
+ if (selected.length === 0) {
234
+ logger.info('No items selected. Cancelled.');
235
+ return;
236
+ }
237
+
238
+ // Clear inquirer's multi-line output (approximately N+1 lines for N selected items)
239
+ if (process.stdout.isTTY) {
240
+ const linesToClear = selected.length + 1;
241
+ for (let i = 0; i < linesToClear; i++) {
242
+ process.stdout.write('\x1B[1A\x1B[2K');
164
243
  }
244
+ }
165
245
 
166
- // Include selected sessions + auto-cleanup targets
167
- sessionsToClean = [...selected, ...autoCleanEntries];
168
- const selectedSize = selected.reduce((sum, s) => sum + s.size, 0);
169
- console.log();
170
- if (selected.length > 0) {
171
- logger.info(
172
- `Selected: ${selected.length} session(s) (${formatSize(selectedSize)})`
173
- );
246
+ // Display selected items in a clean list
247
+ console.log(chalk.green('✔') + ' ' + chalk.bold('Selected items:'));
248
+ for (const item of selected) {
249
+ if (item.type === 'individual') {
250
+ const s = item.session;
251
+ const tag = s.type === 'config'
252
+ ? chalk.yellow('[config]')
253
+ : chalk.cyan(`[${s.toolName}]`);
254
+ const size = s.type === 'config' ? '' : ` ${chalk.dim(`(${formatSize(s.size)})`)}`;
255
+ console.log(` ${tag} ${basename(s.projectPath)}${size}`);
256
+ console.log(` ${chalk.dim(`→ ${tildify(s.projectPath)}`)}`);
257
+ } else {
258
+ console.log(` ${formatGroupChoice(item.group)}`);
174
259
  }
175
- if (autoCleanEntries.length > 0) {
176
- const autoSize = autoCleanEntries.reduce((sum, s) => sum + s.size, 0);
177
- logger.info(
178
- `+ ${autoCleanEntries.length} auto-cleanup target(s) (${formatSize(autoSize)})`
179
- );
260
+ }
261
+
262
+ // Flatten selected items
263
+ sessionsToClean = [];
264
+ for (const item of selected) {
265
+ if (item.type === 'individual') {
266
+ sessionsToClean.push(item.session);
267
+ } else {
268
+ sessionsToClean.push(...item.group.sessions);
180
269
  }
181
270
  }
271
+
272
+ const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
273
+ console.log();
274
+ logger.info(
275
+ `Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
276
+ );
182
277
  }
183
278
 
184
279
  const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
@@ -202,10 +297,13 @@ export const cleanCommand = new Command('clean')
202
297
  const dryRunHistories = sessionsToClean.filter(
203
298
  (s) => s.type === 'file-history'
204
299
  );
300
+ const dryRunTasks = sessionsToClean.filter(
301
+ (s) => s.type === 'tasks'
302
+ );
205
303
 
206
304
  for (const session of dryRunFolders) {
207
305
  console.log(
208
- ` ${chalk.red('Would delete:')} ${session.sessionPath} (${formatSize(session.size)})`
306
+ ` ${chalk.red('Would delete:')} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
209
307
  );
210
308
  }
211
309
 
@@ -215,29 +313,33 @@ export const cleanCommand = new Command('clean')
215
313
  ` ${chalk.yellow('Would remove from ~/.claude.json:')}`
216
314
  );
217
315
  for (const config of dryRunConfigs) {
218
- console.log(` - ${config.projectPath}`);
316
+ console.log(` - ${tildify(config.projectPath)}`);
219
317
  }
220
318
  }
221
319
 
222
- // Auto-cleanup targets summary
223
- const autoCleanParts: string[] = [];
320
+ // Group items summary
224
321
  if (dryRunEnvs.length > 0) {
225
- autoCleanParts.push(`${dryRunEnvs.length} session-env`);
322
+ console.log();
323
+ console.log(
324
+ ` ${chalk.green('Would delete:')} ${dryRunEnvs.length} session-env folder(s)`
325
+ );
226
326
  }
227
327
  if (dryRunTodos.length > 0) {
228
- autoCleanParts.push(`${dryRunTodos.length} todos`);
328
+ console.log();
329
+ console.log(
330
+ ` ${chalk.magenta('Would delete:')} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
331
+ );
229
332
  }
230
333
  if (dryRunHistories.length > 0) {
231
- autoCleanParts.push(`${dryRunHistories.length} file-history`);
334
+ console.log();
335
+ console.log(
336
+ ` ${chalk.blue('Would delete:')} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
337
+ );
232
338
  }
233
- if (autoCleanParts.length > 0) {
234
- const autoSize =
235
- dryRunEnvs.reduce((sum, s) => sum + s.size, 0) +
236
- dryRunTodos.reduce((sum, s) => sum + s.size, 0) +
237
- dryRunHistories.reduce((sum, s) => sum + s.size, 0);
339
+ if (dryRunTasks.length > 0) {
238
340
  console.log();
239
341
  console.log(
240
- ` ${chalk.dim(`Would auto-delete: ${autoCleanParts.join(' + ')} (${formatSize(autoSize)})`)}`
342
+ ` ${chalk.cyan('Would delete:')} ${dryRunTasks.length} tasks folder(s) (${formatSize(dryRunTasks.reduce((sum, s) => sum + s.size, 0))})`
241
343
  );
242
344
  }
243
345
  return;
@@ -245,6 +347,10 @@ export const cleanCommand = new Command('clean')
245
347
 
246
348
  // Confirmation prompt (also in interactive mode)
247
349
  if (!options.force) {
350
+ if (!process.stdout.isTTY) {
351
+ logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
352
+ return;
353
+ }
248
354
  console.log();
249
355
  const action = options.noTrash ? 'permanently delete' : 'move to trash';
250
356
  const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
@@ -292,6 +398,9 @@ export const cleanCommand = new Command('clean')
292
398
  if (deletedByType.fileHistory > 0) {
293
399
  parts.push(`${deletedByType.fileHistory} file-history`);
294
400
  }
401
+ if (deletedByType.tasks > 0) {
402
+ parts.push(`${deletedByType.tasks} tasks`);
403
+ }
295
404
 
296
405
  const summary =
297
406
  parts.length > 0
@@ -321,7 +430,7 @@ export const cleanCommand = new Command('clean')
321
430
  logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
322
431
  if (options.verbose) {
323
432
  for (const err of cleanResult.errors) {
324
- console.log(chalk.red(` ${err.sessionPath}: ${err.error.message}`));
433
+ console.log(chalk.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
325
434
  }
326
435
  }
327
436
  }
@@ -4,6 +4,7 @@ import { homedir } from 'os';
4
4
  import { resolve } from 'path';
5
5
 
6
6
  import { logger } from '../utils/logger.js';
7
+ import { tildify } from '../utils/paths.js';
7
8
  import {
8
9
  loadConfig,
9
10
  addWatchPath,
@@ -67,7 +68,7 @@ pathCommand
67
68
  }
68
69
  console.log();
69
70
  for (const p of paths) {
70
- console.log(` ${p}`);
71
+ console.log(` ${tildify(p)}`);
71
72
  }
72
73
  });
73
74
 
@@ -106,7 +107,7 @@ ignoreCommand
106
107
  }
107
108
  console.log();
108
109
  for (const p of paths) {
109
- console.log(` ${p}`);
110
+ console.log(` ${tildify(p)}`);
110
111
  }
111
112
  });
112
113
 
@@ -179,6 +180,10 @@ configCommand
179
180
  .option('-f, --force', 'Skip confirmation prompt')
180
181
  .action(async (options: { force?: boolean }) => {
181
182
  if (!options.force) {
183
+ if (!process.stdout.isTTY) {
184
+ logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
185
+ return;
186
+ }
182
187
  const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
183
188
  {
184
189
  type: 'confirm',
@@ -5,6 +5,7 @@ import chalk from 'chalk';
5
5
 
6
6
  import { logger } from '../utils/logger.js';
7
7
  import { formatSize } from '../utils/size.js';
8
+ import { tildify } from '../utils/paths.js';
8
9
  import {
9
10
  createAllScanners,
10
11
  getAvailableScanners,
@@ -84,6 +85,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
84
85
  const sessionEnvEntries = allSessions.filter((s) => s.type === 'session-env');
85
86
  const todosEntries = allSessions.filter((s) => s.type === 'todos');
86
87
  const fileHistoryEntries = allSessions.filter((s) => s.type === 'file-history');
88
+ const tasksEntries = allSessions.filter((s) => s.type === 'tasks');
87
89
  const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
88
90
 
89
91
  if (allSessions.length === 0) {
@@ -110,6 +112,9 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
110
112
  if (fileHistoryEntries.length > 0) {
111
113
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
112
114
  }
115
+ if (tasksEntries.length > 0) {
116
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
117
+ }
113
118
  logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
114
119
  console.log();
115
120
 
@@ -122,6 +127,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
122
127
  chalk.cyan('Env'),
123
128
  chalk.cyan('Todos'),
124
129
  chalk.cyan('History'),
130
+ chalk.cyan('Tasks'),
125
131
  chalk.cyan('Size'),
126
132
  chalk.cyan('Scan Time'),
127
133
  ],
@@ -135,6 +141,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
135
141
  const envs = result.sessions.filter((s) => s.type === 'session-env').length;
136
142
  const todos = result.sessions.filter((s) => s.type === 'todos').length;
137
143
  const histories = result.sessions.filter((s) => s.type === 'file-history').length;
144
+ const tasks = result.sessions.filter((s) => s.type === 'tasks').length;
138
145
  summaryTable.push([
139
146
  result.toolName,
140
147
  folders > 0 ? String(folders) : '-',
@@ -142,6 +149,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
142
149
  envs > 0 ? String(envs) : '-',
143
150
  todos > 0 ? String(todos) : '-',
144
151
  histories > 0 ? String(histories) : '-',
152
+ tasks > 0 ? String(tasks) : '-',
145
153
  formatSize(result.totalSize),
146
154
  `${result.scanDuration.toFixed(0)}ms`,
147
155
  ]);
@@ -163,7 +171,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
163
171
  console.log(
164
172
  ` ${chalk.cyan(`[${session.toolName}]`)} ${chalk.white(projectName)} ${chalk.dim(`(${formatSize(session.size)})`)}`
165
173
  );
166
- console.log(` ${chalk.dim('→')} ${session.projectPath}`);
174
+ console.log(` ${chalk.dim('→')} ${tildify(session.projectPath)}`);
167
175
  console.log(` ${chalk.dim('Modified:')} ${session.lastModified.toLocaleDateString()}`);
168
176
  console.log();
169
177
  }
@@ -180,7 +188,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
180
188
  console.log(
181
189
  ` ${chalk.yellow('[config]')} ${chalk.white(projectName)}`
182
190
  );
183
- console.log(` ${chalk.dim('→')} ${entry.projectPath}`);
191
+ console.log(` ${chalk.dim('→')} ${tildify(entry.projectPath)}`);
184
192
  if (entry.configStats?.lastCost) {
185
193
  const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
186
194
  const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
@@ -191,6 +199,70 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
191
199
  }
192
200
  }
193
201
 
202
+ // Session Env (empty folders)
203
+ if (sessionEnvEntries.length > 0) {
204
+ console.log();
205
+ console.log(chalk.bold('Empty Session Env Folders:'));
206
+ console.log();
207
+
208
+ for (const entry of sessionEnvEntries) {
209
+ const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
210
+ console.log(
211
+ ` ${chalk.green('[session-env]')} ${chalk.white(folderName)} ${chalk.dim('(empty)')}`
212
+ );
213
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
214
+ console.log();
215
+ }
216
+ }
217
+
218
+ // Todos
219
+ if (todosEntries.length > 0) {
220
+ console.log();
221
+ console.log(chalk.bold('Orphaned Todos:'));
222
+ console.log();
223
+
224
+ for (const entry of todosEntries) {
225
+ const fileName = entry.sessionPath.split('/').pop() || entry.sessionPath;
226
+ console.log(
227
+ ` ${chalk.magenta('[todos]')} ${chalk.white(fileName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
228
+ );
229
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
230
+ console.log();
231
+ }
232
+ }
233
+
234
+ // File history
235
+ if (fileHistoryEntries.length > 0) {
236
+ console.log();
237
+ console.log(chalk.bold('Orphaned File History:'));
238
+ console.log();
239
+
240
+ for (const entry of fileHistoryEntries) {
241
+ const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
242
+ console.log(
243
+ ` ${chalk.blue('[file-history]')} ${chalk.white(folderName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
244
+ );
245
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
246
+ console.log();
247
+ }
248
+ }
249
+
250
+ // Tasks
251
+ if (tasksEntries.length > 0) {
252
+ console.log();
253
+ console.log(chalk.bold('Orphaned Tasks:'));
254
+ console.log();
255
+
256
+ for (const entry of tasksEntries) {
257
+ const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
258
+ console.log(
259
+ ` ${chalk.cyan('[tasks]')} ${chalk.white(folderName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
260
+ );
261
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
262
+ console.log();
263
+ }
264
+ }
265
+
194
266
  }
195
267
 
196
268
  console.log();
@@ -6,6 +6,7 @@ import { join, resolve } from 'path';
6
6
 
7
7
  import { logger } from '../utils/logger.js';
8
8
  import { formatSize } from '../utils/size.js';
9
+ import { tildify } from '../utils/paths.js';
9
10
  import { getWatchPaths as getConfigWatchPaths, setWatchPaths, getWatchDelay, getWatchDepth, getIgnorePaths } from '../utils/config.js';
10
11
  import {
11
12
  createAllScanners,
@@ -140,7 +141,7 @@ const statusCommand = new Command('status')
140
141
  if (status.pid) {
141
142
  console.log(`PID: ${status.pid}`);
142
143
  }
143
- console.log(`Plist: ${status.plistPath}`);
144
+ console.log(`Plist: ${tildify(status.plistPath)}`);
144
145
  console.log();
145
146
 
146
147
  if (options.logs) {
@@ -233,7 +234,7 @@ async function runWatcher(options: RunOptions): Promise<void> {
233
234
 
234
235
  if (validPaths.length < watchPaths.length) {
235
236
  const invalidPaths = watchPaths.filter((p) => !existsSync(p));
236
- logger.warn(`Skipping non-existent paths: ${invalidPaths.join(', ')}`);
237
+ logger.warn(`Skipping non-existent paths: ${invalidPaths.map(tildify).join(', ')}`);
237
238
  }
238
239
 
239
240
  // Check scanners
@@ -250,7 +251,7 @@ async function runWatcher(options: RunOptions): Promise<void> {
250
251
  logger.info(
251
252
  `Watching for project deletions (${availableScanners.map((s) => s.name).join(', ')})`
252
253
  );
253
- logger.info(`Watch paths: ${validPaths.join(', ')}`);
254
+ logger.info(`Watch paths: ${validPaths.map(tildify).join(', ')}`);
254
255
  logger.info(`Cleanup delay: ${String(delayMinutes)} minute(s)`);
255
256
  logger.info(`Watch depth: ${String(depth)}`);
256
257
  if (process.stdout.isTTY) {
@@ -264,16 +265,17 @@ async function runWatcher(options: RunOptions): Promise<void> {
264
265
  watchPaths: validPaths,
265
266
  delayMs,
266
267
  depth,
267
- ignorePaths: getIgnorePaths(),
268
+ ignorePaths: getIgnorePaths() ?? [],
268
269
  onDelete: async (events) => {
269
270
  // Log batch events
270
- if (events.length === 1) {
271
- logger.info(`Detected deletion: ${events[0].path}`);
271
+ const firstEvent = events[0];
272
+ if (events.length === 1 && firstEvent) {
273
+ logger.info(`Detected deletion: ${tildify(firstEvent.path)}`);
272
274
  } else {
273
275
  logger.info(`Detected ${events.length} deletions (debounced)`);
274
276
  if (options.verbose) {
275
277
  for (const event of events) {
276
- logger.debug(` - ${event.path}`);
278
+ logger.debug(` - ${tildify(event.path)}`);
277
279
  }
278
280
  }
279
281
  }
@@ -312,6 +314,9 @@ async function runWatcher(options: RunOptions): Promise<void> {
312
314
  if (deletedByType.fileHistory > 0) {
313
315
  parts.push(`${deletedByType.fileHistory} file-history`);
314
316
  }
317
+ if (deletedByType.tasks > 0) {
318
+ parts.push(`${deletedByType.tasks} tasks`);
319
+ }
315
320
 
316
321
  const summary = parts.length > 0 ? parts.join(' + ') : `${cleanResult.deletedCount} item(s)`;
317
322
  logger.success(
@@ -21,6 +21,7 @@ export interface CleanCountByType {
21
21
  sessionEnv: number;
22
22
  todos: number;
23
23
  fileHistory: number;
24
+ tasks: number;
24
25
  }
25
26
 
26
27
  export interface CleanResult {
@@ -55,6 +56,7 @@ export class Cleaner {
55
56
  sessionEnv: 0,
56
57
  todos: 0,
57
58
  fileHistory: 0,
59
+ tasks: 0,
58
60
  },
59
61
  skippedCount: 0,
60
62
  alreadyGoneCount: 0,
@@ -96,6 +98,9 @@ export class Cleaner {
96
98
  case 'file-history':
97
99
  result.deletedByType.fileHistory++;
98
100
  break;
101
+ case 'tasks':
102
+ result.deletedByType.tasks++;
103
+ break;
99
104
  default:
100
105
  result.deletedByType.session++;
101
106
  }
@@ -7,16 +7,16 @@ export const IGNORED_SYSTEM_PATTERNS: RegExp[] = [
7
7
  /(^|[/\\])\../,
8
8
 
9
9
  // macOS user folders (not project directories)
10
- /\/Library\//,
11
- /\/Applications\//,
12
- /\/Music\//,
13
- /\/Movies\//,
14
- /\/Pictures\//,
15
- /\/Downloads\//,
16
- /\/Documents\//,
17
- /\/Desktop\//,
18
- /\/Public\//,
10
+ /\/Library(\/|$)/,
11
+ /\/Applications(\/|$)/,
12
+ /\/Music(\/|$)/,
13
+ /\/Movies(\/|$)/,
14
+ /\/Pictures(\/|$)/,
15
+ /\/Downloads(\/|$)/,
16
+ /\/Documents(\/|$)/,
17
+ /\/Desktop(\/|$)/,
18
+ /\/Public(\/|$)/,
19
19
 
20
20
  // Development folders
21
- /node_modules/,
21
+ /node_modules(\/|$)/,
22
22
  ];