@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sooink/ai-session-tidy",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool that detects and cleans orphaned session data from AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,4 +51,4 @@
51
51
  "typescript-eslint": "^8.21.0",
52
52
  "vitest": "^3.0.4"
53
53
  }
54
- }
54
+ }
@@ -31,9 +31,33 @@ 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';
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
+ };
51
+ const colors: Record<string, (s: string) => string> = {
52
+ 'session-env': chalk.green,
53
+ 'todos': chalk.magenta,
54
+ 'file-history': chalk.blue,
55
+ };
56
+ const label = labels[group.type] || group.type;
57
+ const count = group.sessions.length;
58
+ const plural = count > 1 ? 's' : '';
59
+ const color = colors[group.type] || chalk.white;
60
+ return `${color(`[${group.type}]`)} ${chalk.white(`${count} ${label}${plural}`)} ${chalk.dim(`(${formatSize(group.totalSize)})`)}`;
37
61
  }
38
62
 
39
63
  export const cleanCommand = new Command('clean')
@@ -67,7 +91,7 @@ export const cleanCommand = new Command('clean')
67
91
  return;
68
92
  }
69
93
 
70
- // Separate session folders, config entries, and auto-cleanup targets
94
+ // Separate by type
71
95
  const folderSessions = allSessions.filter(
72
96
  (s) => s.type === 'session' || s.type === undefined
73
97
  );
@@ -80,21 +104,38 @@ export const cleanCommand = new Command('clean')
80
104
  (s) => s.type === 'file-history'
81
105
  );
82
106
 
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(
107
+ // Individual selection targets (session folders and config entries)
108
+ const individualSessions = allSessions.filter(
92
109
  (s) =>
93
110
  s.type !== 'session-env' &&
94
111
  s.type !== 'todos' &&
95
112
  s.type !== 'file-history'
96
113
  );
97
114
 
115
+ // Group selection targets (session-env, todos, file-history)
116
+ const groupChoices: GroupChoice[] = [];
117
+ if (sessionEnvEntries.length > 0) {
118
+ groupChoices.push({
119
+ type: 'session-env',
120
+ sessions: sessionEnvEntries,
121
+ totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0),
122
+ });
123
+ }
124
+ if (todosEntries.length > 0) {
125
+ groupChoices.push({
126
+ type: 'todos',
127
+ sessions: todosEntries,
128
+ totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0),
129
+ });
130
+ }
131
+ if (fileHistoryEntries.length > 0) {
132
+ groupChoices.push({
133
+ type: 'file-history',
134
+ sessions: fileHistoryEntries,
135
+ totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0),
136
+ });
137
+ }
138
+
98
139
  // Output summary
99
140
  console.log();
100
141
  const parts: string[] = [];
@@ -117,68 +158,108 @@ export const cleanCommand = new Command('clean')
117
158
 
118
159
  if (options.verbose && !options.interactive) {
119
160
  console.log();
120
- // Exclude auto-cleanup targets from detailed list
121
- for (const session of selectableSessions) {
161
+ for (const session of individualSessions) {
122
162
  console.log(
123
- chalk.dim(` ${session.toolName}: ${session.projectPath}`)
163
+ chalk.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
124
164
  );
125
165
  }
126
166
  }
127
167
 
128
- // Interactive mode: session selection (excluding auto-cleanup targets)
168
+ // Interactive mode: session selection
129
169
  let sessionsToClean = allSessions;
130
170
  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;
171
+ if (!process.stdout.isTTY) {
172
+ logger.error('Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.');
173
+ return;
174
+ }
175
+ // Build choices: individual sessions + group choices
176
+ const choices: Array<{
177
+ name: string;
178
+ value: { type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice };
179
+ checked: boolean;
180
+ }> = [];
181
+
182
+ // Add individual sessions
183
+ for (const session of individualSessions) {
184
+ choices.push({
185
+ name: formatSessionChoice(session),
186
+ value: { type: 'individual', session },
187
+ checked: false,
188
+ });
189
+ }
190
+
191
+ // Add group choices
192
+ for (const group of groupChoices) {
193
+ choices.push({
194
+ name: formatGroupChoice(group),
195
+ value: { type: 'group', group },
196
+ checked: false,
197
+ });
198
+ }
199
+
200
+ if (choices.length === 0) {
201
+ logger.info('No selectable sessions found.');
202
+ return;
203
+ }
204
+
205
+ console.log();
206
+ const { selected } = await inquirer.prompt<{
207
+ selected: Array<{ type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice }>;
208
+ }>([
209
+ {
210
+ type: 'checkbox',
211
+ name: 'selected',
212
+ message: 'Select items to delete:',
213
+ choices,
214
+ pageSize: 15,
215
+ loop: false,
216
+ },
217
+ ]);
218
+
219
+ if (selected.length === 0) {
220
+ logger.info('No items selected. Cancelled.');
221
+ return;
222
+ }
223
+
224
+ // Clear inquirer's multi-line output (approximately N+1 lines for N selected items)
225
+ if (process.stdout.isTTY) {
226
+ const linesToClear = selected.length + 1;
227
+ for (let i = 0; i < linesToClear; i++) {
228
+ process.stdout.write('\x1B[1A\x1B[2K');
164
229
  }
230
+ }
165
231
 
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
- );
232
+ // Display selected items in a clean list
233
+ console.log(chalk.green('✔') + ' ' + chalk.bold('Selected items:'));
234
+ for (const item of selected) {
235
+ if (item.type === 'individual') {
236
+ const s = item.session;
237
+ const tag = s.type === 'config'
238
+ ? chalk.yellow('[config]')
239
+ : chalk.cyan(`[${s.toolName}]`);
240
+ const size = s.type === 'config' ? '' : ` ${chalk.dim(`(${formatSize(s.size)})`)}`;
241
+ console.log(` ${tag} ${basename(s.projectPath)}${size}`);
242
+ console.log(` ${chalk.dim(`→ ${tildify(s.projectPath)}`)}`);
243
+ } else {
244
+ console.log(` ${formatGroupChoice(item.group)}`);
174
245
  }
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
- );
246
+ }
247
+
248
+ // Flatten selected items
249
+ sessionsToClean = [];
250
+ for (const item of selected) {
251
+ if (item.type === 'individual') {
252
+ sessionsToClean.push(item.session);
253
+ } else {
254
+ sessionsToClean.push(...item.group.sessions);
180
255
  }
181
256
  }
257
+
258
+ const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
259
+ console.log();
260
+ logger.info(
261
+ `Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
262
+ );
182
263
  }
183
264
 
184
265
  const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
@@ -205,7 +286,7 @@ export const cleanCommand = new Command('clean')
205
286
 
206
287
  for (const session of dryRunFolders) {
207
288
  console.log(
208
- ` ${chalk.red('Would delete:')} ${session.sessionPath} (${formatSize(session.size)})`
289
+ ` ${chalk.red('Would delete:')} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
209
290
  );
210
291
  }
211
292
 
@@ -215,29 +296,27 @@ export const cleanCommand = new Command('clean')
215
296
  ` ${chalk.yellow('Would remove from ~/.claude.json:')}`
216
297
  );
217
298
  for (const config of dryRunConfigs) {
218
- console.log(` - ${config.projectPath}`);
299
+ console.log(` - ${tildify(config.projectPath)}`);
219
300
  }
220
301
  }
221
302
 
222
- // Auto-cleanup targets summary
223
- const autoCleanParts: string[] = [];
303
+ // Group items summary
224
304
  if (dryRunEnvs.length > 0) {
225
- autoCleanParts.push(`${dryRunEnvs.length} session-env`);
305
+ console.log();
306
+ console.log(
307
+ ` ${chalk.green('Would delete:')} ${dryRunEnvs.length} session-env folder(s)`
308
+ );
226
309
  }
227
310
  if (dryRunTodos.length > 0) {
228
- autoCleanParts.push(`${dryRunTodos.length} todos`);
311
+ console.log();
312
+ console.log(
313
+ ` ${chalk.magenta('Would delete:')} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
314
+ );
229
315
  }
230
316
  if (dryRunHistories.length > 0) {
231
- autoCleanParts.push(`${dryRunHistories.length} file-history`);
232
- }
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);
238
317
  console.log();
239
318
  console.log(
240
- ` ${chalk.dim(`Would auto-delete: ${autoCleanParts.join(' + ')} (${formatSize(autoSize)})`)}`
319
+ ` ${chalk.blue('Would delete:')} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
241
320
  );
242
321
  }
243
322
  return;
@@ -245,6 +324,10 @@ export const cleanCommand = new Command('clean')
245
324
 
246
325
  // Confirmation prompt (also in interactive mode)
247
326
  if (!options.force) {
327
+ if (!process.stdout.isTTY) {
328
+ logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
329
+ return;
330
+ }
248
331
  console.log();
249
332
  const action = options.noTrash ? 'permanently delete' : 'move to trash';
250
333
  const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
@@ -321,7 +404,7 @@ export const cleanCommand = new Command('clean')
321
404
  logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
322
405
  if (options.verbose) {
323
406
  for (const err of cleanResult.errors) {
324
- console.log(chalk.red(` ${err.sessionPath}: ${err.error.message}`));
407
+ console.log(chalk.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
325
408
  }
326
409
  }
327
410
  }
@@ -1,7 +1,10 @@
1
1
  import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
+ import { homedir } from 'os';
4
+ import { resolve } from 'path';
3
5
 
4
6
  import { logger } from '../utils/logger.js';
7
+ import { tildify } from '../utils/paths.js';
5
8
  import {
6
9
  loadConfig,
7
10
  addWatchPath,
@@ -12,23 +15,37 @@ import {
12
15
  getWatchDepth,
13
16
  setWatchDepth,
14
17
  resetConfig,
18
+ addIgnorePath,
19
+ removeIgnorePath,
20
+ getIgnorePaths,
15
21
  } from '../utils/config.js';
16
22
 
17
23
  export const configCommand = new Command('config').description(
18
24
  'Manage configuration'
19
25
  );
20
26
 
21
- const pathsCommand = new Command('paths').description('Manage watch paths');
27
+ const pathCommand = new Command('path').description('Manage watch paths');
22
28
 
23
- pathsCommand
29
+ pathCommand
24
30
  .command('add <path>')
25
31
  .description('Add a watch path')
26
32
  .action((path: string) => {
33
+ const resolved = resolve(path.replace(/^~/, homedir()));
34
+ const home = homedir();
35
+
36
+ // Warn if adding home directory or its parent
37
+ if (resolved === home || home.startsWith(resolved + '/')) {
38
+ logger.warn('Warning: Watching home directory is not recommended.');
39
+ logger.warn('Many system folders will be excluded automatically.');
40
+ logger.warn('Consider adding specific project folders instead.');
41
+ console.log();
42
+ }
43
+
27
44
  addWatchPath(path);
28
45
  logger.success(`Added: ${path}`);
29
46
  });
30
47
 
31
- pathsCommand
48
+ pathCommand
32
49
  .command('remove <path>')
33
50
  .description('Remove a watch path')
34
51
  .action((path: string) => {
@@ -40,7 +57,7 @@ pathsCommand
40
57
  }
41
58
  });
42
59
 
43
- pathsCommand
60
+ pathCommand
44
61
  .command('list')
45
62
  .description('List watch paths')
46
63
  .action(() => {
@@ -51,11 +68,50 @@ pathsCommand
51
68
  }
52
69
  console.log();
53
70
  for (const p of paths) {
54
- console.log(` ${p}`);
71
+ console.log(` ${tildify(p)}`);
55
72
  }
56
73
  });
57
74
 
58
- configCommand.addCommand(pathsCommand);
75
+ configCommand.addCommand(pathCommand);
76
+
77
+ const ignoreCommand = new Command('ignore').description('Manage ignore paths');
78
+
79
+ ignoreCommand
80
+ .command('add <path>')
81
+ .description('Add a path to ignore from watching')
82
+ .action((path: string) => {
83
+ addIgnorePath(path);
84
+ logger.success(`Added to ignore: ${path}`);
85
+ });
86
+
87
+ ignoreCommand
88
+ .command('remove <path>')
89
+ .description('Remove a path from ignore list')
90
+ .action((path: string) => {
91
+ const removed = removeIgnorePath(path);
92
+ if (removed) {
93
+ logger.success(`Removed from ignore: ${path}`);
94
+ } else {
95
+ logger.warn(`Path not found: ${path}`);
96
+ }
97
+ });
98
+
99
+ ignoreCommand
100
+ .command('list')
101
+ .description('List ignored paths')
102
+ .action(() => {
103
+ const paths = getIgnorePaths();
104
+ if (!paths || paths.length === 0) {
105
+ logger.info('No ignore paths configured.');
106
+ return;
107
+ }
108
+ console.log();
109
+ for (const p of paths) {
110
+ console.log(` ${tildify(p)}`);
111
+ }
112
+ });
113
+
114
+ configCommand.addCommand(ignoreCommand);
59
115
 
60
116
  const DEFAULT_DELAY_MINUTES = 5;
61
117
  const MAX_DELAY_MINUTES = 10;
@@ -124,6 +180,10 @@ configCommand
124
180
  .option('-f, --force', 'Skip confirmation prompt')
125
181
  .action(async (options: { force?: boolean }) => {
126
182
  if (!options.force) {
183
+ if (!process.stdout.isTTY) {
184
+ logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
185
+ return;
186
+ }
127
187
  const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
128
188
  {
129
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,
@@ -163,7 +164,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
163
164
  console.log(
164
165
  ` ${chalk.cyan(`[${session.toolName}]`)} ${chalk.white(projectName)} ${chalk.dim(`(${formatSize(session.size)})`)}`
165
166
  );
166
- console.log(` ${chalk.dim('→')} ${session.projectPath}`);
167
+ console.log(` ${chalk.dim('→')} ${tildify(session.projectPath)}`);
167
168
  console.log(` ${chalk.dim('Modified:')} ${session.lastModified.toLocaleDateString()}`);
168
169
  console.log();
169
170
  }
@@ -180,7 +181,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
180
181
  console.log(
181
182
  ` ${chalk.yellow('[config]')} ${chalk.white(projectName)}`
182
183
  );
183
- console.log(` ${chalk.dim('→')} ${entry.projectPath}`);
184
+ console.log(` ${chalk.dim('→')} ${tildify(entry.projectPath)}`);
184
185
  if (entry.configStats?.lastCost) {
185
186
  const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
186
187
  const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
@@ -191,6 +192,54 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
191
192
  }
192
193
  }
193
194
 
195
+ // Session Env (empty folders)
196
+ if (sessionEnvEntries.length > 0) {
197
+ console.log();
198
+ console.log(chalk.bold('Empty Session Env Folders:'));
199
+ console.log();
200
+
201
+ for (const entry of sessionEnvEntries) {
202
+ const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
203
+ console.log(
204
+ ` ${chalk.green('[session-env]')} ${chalk.white(folderName)} ${chalk.dim('(empty)')}`
205
+ );
206
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
207
+ console.log();
208
+ }
209
+ }
210
+
211
+ // Todos
212
+ if (todosEntries.length > 0) {
213
+ console.log();
214
+ console.log(chalk.bold('Orphaned Todos:'));
215
+ console.log();
216
+
217
+ for (const entry of todosEntries) {
218
+ const fileName = entry.sessionPath.split('/').pop() || entry.sessionPath;
219
+ console.log(
220
+ ` ${chalk.magenta('[todos]')} ${chalk.white(fileName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
221
+ );
222
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
223
+ console.log();
224
+ }
225
+ }
226
+
227
+ // File history
228
+ if (fileHistoryEntries.length > 0) {
229
+ console.log();
230
+ console.log(chalk.bold('Orphaned File History:'));
231
+ console.log();
232
+
233
+ for (const entry of fileHistoryEntries) {
234
+ const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
235
+ console.log(
236
+ ` ${chalk.blue('[file-history]')} ${chalk.white(folderName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
237
+ );
238
+ console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
239
+ console.log();
240
+ }
241
+ }
242
+
194
243
  }
195
244
 
196
245
  console.log();
@@ -6,7 +6,8 @@ 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 { getWatchPaths as getConfigWatchPaths, setWatchPaths, getWatchDelay, getWatchDepth } from '../utils/config.js';
9
+ import { tildify } from '../utils/paths.js';
10
+ import { getWatchPaths as getConfigWatchPaths, setWatchPaths, getWatchDelay, getWatchDepth, getIgnorePaths } from '../utils/config.js';
10
11
  import {
11
12
  createAllScanners,
12
13
  getAvailableScanners,
@@ -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,15 +265,16 @@ async function runWatcher(options: RunOptions): Promise<void> {
264
265
  watchPaths: validPaths,
265
266
  delayMs,
266
267
  depth,
268
+ ignorePaths: getIgnorePaths(),
267
269
  onDelete: async (events) => {
268
270
  // Log batch events
269
271
  if (events.length === 1) {
270
- logger.info(`Detected deletion: ${events[0].path}`);
272
+ logger.info(`Detected deletion: ${tildify(events[0].path)}`);
271
273
  } else {
272
274
  logger.info(`Detected ${events.length} deletions (debounced)`);
273
275
  if (options.verbose) {
274
276
  for (const event of events) {
275
- logger.debug(` - ${event.path}`);
277
+ logger.debug(` - ${tildify(event.path)}`);
276
278
  }
277
279
  }
278
280
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * System folders to ignore when watching (macOS)
3
+ * These are hidden folders that should never be monitored
4
+ */
5
+ export const IGNORED_SYSTEM_PATTERNS: RegExp[] = [
6
+ // All hidden folders (starting with .)
7
+ /(^|[/\\])\../,
8
+
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(\/|$)/,
19
+
20
+ // Development folders
21
+ /node_modules(\/|$)/,
22
+ ];
@@ -131,6 +131,17 @@ export class ServiceManager {
131
131
  throw new Error('Service not installed. Run "watch start" to install and start.');
132
132
  }
133
133
 
134
+ // Clear old log files before starting
135
+ const logDir = join(homedir(), '.ai-session-tidy');
136
+ const stdoutPath = join(logDir, 'watcher.log');
137
+ const stderrPath = join(logDir, 'watcher.error.log');
138
+ if (existsSync(stdoutPath)) {
139
+ await writeFile(stdoutPath, '', 'utf-8');
140
+ }
141
+ if (existsSync(stderrPath)) {
142
+ await writeFile(stderrPath, '', 'utf-8');
143
+ }
144
+
134
145
  try {
135
146
  execSync(`launchctl load "${this.plistPath}"`, { stdio: 'pipe' });
136
147
  } catch (error) {