@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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sooink/ai-session-tidy",
3
- "version": "0.1.3",
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"
@@ -37,7 +37,7 @@ function formatSessionChoice(session: OrphanedSession): string {
37
37
  }
38
38
 
39
39
  interface GroupChoice {
40
- type: 'session-env' | 'todos' | 'file-history';
40
+ type: 'session-env' | 'todos' | 'file-history' | 'tasks';
41
41
  sessions: OrphanedSession[];
42
42
  totalSize: number;
43
43
  }
@@ -47,11 +47,13 @@ function formatGroupChoice(group: GroupChoice): string {
47
47
  'session-env': 'empty session-env folder',
48
48
  'todos': 'orphaned todos file',
49
49
  'file-history': 'orphaned file-history folder',
50
+ 'tasks': 'orphaned tasks folder',
50
51
  };
51
52
  const colors: Record<string, (s: string) => string> = {
52
53
  'session-env': chalk.green,
53
54
  'todos': chalk.magenta,
54
55
  'file-history': chalk.blue,
56
+ 'tasks': chalk.cyan,
55
57
  };
56
58
  const label = labels[group.type] || group.type;
57
59
  const count = group.sessions.length;
@@ -103,16 +105,18 @@ export const cleanCommand = new Command('clean')
103
105
  const fileHistoryEntries = allSessions.filter(
104
106
  (s) => s.type === 'file-history'
105
107
  );
108
+ const tasksEntries = allSessions.filter((s) => s.type === 'tasks');
106
109
 
107
110
  // Individual selection targets (session folders and config entries)
108
111
  const individualSessions = allSessions.filter(
109
112
  (s) =>
110
113
  s.type !== 'session-env' &&
111
114
  s.type !== 'todos' &&
112
- s.type !== 'file-history'
115
+ s.type !== 'file-history' &&
116
+ s.type !== 'tasks'
113
117
  );
114
118
 
115
- // Group selection targets (session-env, todos, file-history)
119
+ // Group selection targets (session-env, todos, file-history, tasks)
116
120
  const groupChoices: GroupChoice[] = [];
117
121
  if (sessionEnvEntries.length > 0) {
118
122
  groupChoices.push({
@@ -135,6 +139,13 @@ export const cleanCommand = new Command('clean')
135
139
  totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0),
136
140
  });
137
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
+ }
138
149
 
139
150
  // Output summary
140
151
  console.log();
@@ -154,6 +165,9 @@ export const cleanCommand = new Command('clean')
154
165
  if (fileHistoryEntries.length > 0) {
155
166
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
156
167
  }
168
+ if (tasksEntries.length > 0) {
169
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
170
+ }
157
171
  logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
158
172
 
159
173
  if (options.verbose && !options.interactive) {
@@ -283,6 +297,9 @@ export const cleanCommand = new Command('clean')
283
297
  const dryRunHistories = sessionsToClean.filter(
284
298
  (s) => s.type === 'file-history'
285
299
  );
300
+ const dryRunTasks = sessionsToClean.filter(
301
+ (s) => s.type === 'tasks'
302
+ );
286
303
 
287
304
  for (const session of dryRunFolders) {
288
305
  console.log(
@@ -319,6 +336,12 @@ export const cleanCommand = new Command('clean')
319
336
  ` ${chalk.blue('Would delete:')} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
320
337
  );
321
338
  }
339
+ if (dryRunTasks.length > 0) {
340
+ console.log();
341
+ console.log(
342
+ ` ${chalk.cyan('Would delete:')} ${dryRunTasks.length} tasks folder(s) (${formatSize(dryRunTasks.reduce((sum, s) => sum + s.size, 0))})`
343
+ );
344
+ }
322
345
  return;
323
346
  }
324
347
 
@@ -375,6 +398,9 @@ export const cleanCommand = new Command('clean')
375
398
  if (deletedByType.fileHistory > 0) {
376
399
  parts.push(`${deletedByType.fileHistory} file-history`);
377
400
  }
401
+ if (deletedByType.tasks > 0) {
402
+ parts.push(`${deletedByType.tasks} tasks`);
403
+ }
378
404
 
379
405
  const summary =
380
406
  parts.length > 0
@@ -85,6 +85,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
85
85
  const sessionEnvEntries = allSessions.filter((s) => s.type === 'session-env');
86
86
  const todosEntries = allSessions.filter((s) => s.type === 'todos');
87
87
  const fileHistoryEntries = allSessions.filter((s) => s.type === 'file-history');
88
+ const tasksEntries = allSessions.filter((s) => s.type === 'tasks');
88
89
  const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
89
90
 
90
91
  if (allSessions.length === 0) {
@@ -111,6 +112,9 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
111
112
  if (fileHistoryEntries.length > 0) {
112
113
  parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
113
114
  }
115
+ if (tasksEntries.length > 0) {
116
+ parts.push(`${tasksEntries.length} tasks folder(s)`);
117
+ }
114
118
  logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
115
119
  console.log();
116
120
 
@@ -123,6 +127,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
123
127
  chalk.cyan('Env'),
124
128
  chalk.cyan('Todos'),
125
129
  chalk.cyan('History'),
130
+ chalk.cyan('Tasks'),
126
131
  chalk.cyan('Size'),
127
132
  chalk.cyan('Scan Time'),
128
133
  ],
@@ -136,6 +141,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
136
141
  const envs = result.sessions.filter((s) => s.type === 'session-env').length;
137
142
  const todos = result.sessions.filter((s) => s.type === 'todos').length;
138
143
  const histories = result.sessions.filter((s) => s.type === 'file-history').length;
144
+ const tasks = result.sessions.filter((s) => s.type === 'tasks').length;
139
145
  summaryTable.push([
140
146
  result.toolName,
141
147
  folders > 0 ? String(folders) : '-',
@@ -143,6 +149,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
143
149
  envs > 0 ? String(envs) : '-',
144
150
  todos > 0 ? String(todos) : '-',
145
151
  histories > 0 ? String(histories) : '-',
152
+ tasks > 0 ? String(tasks) : '-',
146
153
  formatSize(result.totalSize),
147
154
  `${result.scanDuration.toFixed(0)}ms`,
148
155
  ]);
@@ -240,6 +247,22 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
240
247
  }
241
248
  }
242
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
+
243
266
  }
244
267
 
245
268
  console.log();
@@ -265,11 +265,12 @@ async function runWatcher(options: RunOptions): Promise<void> {
265
265
  watchPaths: validPaths,
266
266
  delayMs,
267
267
  depth,
268
- ignorePaths: getIgnorePaths(),
268
+ ignorePaths: getIgnorePaths() ?? [],
269
269
  onDelete: async (events) => {
270
270
  // Log batch events
271
- if (events.length === 1) {
272
- logger.info(`Detected deletion: ${tildify(events[0].path)}`);
271
+ const firstEvent = events[0];
272
+ if (events.length === 1 && firstEvent) {
273
+ logger.info(`Detected deletion: ${tildify(firstEvent.path)}`);
273
274
  } else {
274
275
  logger.info(`Detected ${events.length} deletions (debounced)`);
275
276
  if (options.verbose) {
@@ -313,6 +314,9 @@ async function runWatcher(options: RunOptions): Promise<void> {
313
314
  if (deletedByType.fileHistory > 0) {
314
315
  parts.push(`${deletedByType.fileHistory} file-history`);
315
316
  }
317
+ if (deletedByType.tasks > 0) {
318
+ parts.push(`${deletedByType.tasks} tasks`);
319
+ }
316
320
 
317
321
  const summary = parts.length > 0 ? parts.join(' + ') : `${cleanResult.deletedCount} item(s)`;
318
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
  }
@@ -6,6 +6,8 @@ import { execSync } from 'child_process';
6
6
 
7
7
  const SERVICE_LABEL = 'sooink.ai-session-tidy.watcher';
8
8
  const PLIST_FILENAME = `${SERVICE_LABEL}.plist`;
9
+ const BIN_NAME = 'ai-session-tidy';
10
+ const BUNDLE_IDENTIFIER = 'io.github.sooink.ai-session-tidy';
9
11
 
10
12
  export type ServiceStatus = 'running' | 'stopped' | 'not_installed';
11
13
 
@@ -20,13 +22,26 @@ function getPlistPath(): string {
20
22
  return join(homedir(), 'Library', 'LaunchAgents', PLIST_FILENAME);
21
23
  }
22
24
 
25
+ function getBinPath(): string | null {
26
+ try {
27
+ const binPath = execSync(`which ${BIN_NAME}`, {
28
+ encoding: 'utf-8',
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ }).trim();
31
+ if (binPath && existsSync(binPath)) {
32
+ return binPath;
33
+ }
34
+ } catch {
35
+ // not found in PATH
36
+ }
37
+ return null;
38
+ }
39
+
23
40
  function getNodePath(): string {
24
- // Get absolute path to node executable
25
41
  return process.execPath;
26
42
  }
27
43
 
28
44
  function getScriptPath(): string {
29
- // Get absolute path to the CLI script
30
45
  const scriptPath = process.argv[1];
31
46
  if (scriptPath && existsSync(scriptPath)) {
32
47
  return scriptPath;
@@ -34,27 +49,57 @@ function getScriptPath(): string {
34
49
  throw new Error('Could not determine script path');
35
50
  }
36
51
 
52
+ /**
53
+ * Build ProgramArguments for the LaunchAgent plist.
54
+ *
55
+ * Prefers the bin command path (e.g. /usr/local/bin/ai-session-tidy) so that
56
+ * macOS attributes the background activity to "ai-session-tidy" instead of
57
+ * "Node.js Foundation". Falls back to [node, script] if the bin is not found.
58
+ *
59
+ * @see https://developer.apple.com/forums/thread/735065
60
+ */
61
+ function getProgramArgs(args: string[]): string[] {
62
+ const binPath = getBinPath();
63
+ if (binPath) {
64
+ return [binPath, ...args];
65
+ }
66
+ return [getNodePath(), getScriptPath(), ...args];
67
+ }
68
+
37
69
  function generatePlist(options: {
38
70
  label: string;
39
- nodePath: string;
40
- scriptPath: string;
41
- args: string[];
71
+ programArgs: string[];
42
72
  }): string {
43
- const allArgs = [options.nodePath, options.scriptPath, ...options.args];
44
- const argsXml = allArgs.map((arg) => ` <string>${arg}</string>`).join('\n');
73
+ const argsXml = options.programArgs
74
+ .map((arg) => ` <string>${arg}</string>`)
75
+ .join('\n');
45
76
 
46
77
  const home = homedir();
47
78
 
79
+ // Include PATH so that shebang (#!/usr/bin/env node) can locate the node binary.
80
+ // This is necessary for nvm/fnm users where node is not in the default system PATH.
81
+ const nodeBinDir = dirname(process.execPath);
82
+ const systemPath = process.env['PATH'] ?? '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
83
+ const envPath = systemPath.includes(nodeBinDir)
84
+ ? systemPath
85
+ : `${nodeBinDir}:${systemPath}`;
86
+
48
87
  return `<?xml version="1.0" encoding="UTF-8"?>
49
88
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
50
89
  <plist version="1.0">
51
90
  <dict>
52
91
  <key>Label</key>
53
92
  <string>${options.label}</string>
93
+ <key>AssociatedBundleIdentifiers</key>
94
+ <array>
95
+ <string>${BUNDLE_IDENTIFIER}</string>
96
+ </array>
54
97
  <key>EnvironmentVariables</key>
55
98
  <dict>
56
99
  <key>HOME</key>
57
100
  <string>${home}</string>
101
+ <key>PATH</key>
102
+ <string>${envPath}</string>
58
103
  </dict>
59
104
  <key>ProgramArguments</key>
60
105
  <array>
@@ -108,9 +153,7 @@ export class ServiceManager {
108
153
 
109
154
  const plistContent = generatePlist({
110
155
  label: SERVICE_LABEL,
111
- nodePath: getNodePath(),
112
- scriptPath: getScriptPath(),
113
- args: ['watch', 'run'],
156
+ programArgs: getProgramArgs(['watch', 'run']),
114
157
  });
115
158
 
116
159
  await writeFile(this.plistPath, plistContent, 'utf-8');
@@ -11,6 +11,7 @@ import {
11
11
  getClaudeSessionEnvDir,
12
12
  getClaudeTodosDir,
13
13
  getClaudeFileHistoryDir,
14
+ getClaudeTasksDir,
14
15
  } from '../utils/paths.js';
15
16
  import { getDirectorySize } from '../utils/size.js';
16
17
 
@@ -36,6 +37,7 @@ export interface ClaudeCodeScannerOptions {
36
37
  sessionEnvDir?: string | null; // null to disable session-env scanning
37
38
  todosDir?: string | null; // null to disable todos scanning
38
39
  fileHistoryDir?: string | null; // null to disable file-history scanning
40
+ tasksDir?: string | null; // null to disable tasks scanning
39
41
  }
40
42
 
41
43
  export class ClaudeCodeScanner implements Scanner {
@@ -45,6 +47,7 @@ export class ClaudeCodeScanner implements Scanner {
45
47
  private readonly sessionEnvDir: string | null;
46
48
  private readonly todosDir: string | null;
47
49
  private readonly fileHistoryDir: string | null;
50
+ private readonly tasksDir: string | null;
48
51
 
49
52
  constructor(projectsDirOrOptions?: string | ClaudeCodeScannerOptions) {
50
53
  if (typeof projectsDirOrOptions === 'string') {
@@ -54,6 +57,7 @@ export class ClaudeCodeScanner implements Scanner {
54
57
  this.sessionEnvDir = null;
55
58
  this.todosDir = null;
56
59
  this.fileHistoryDir = null;
60
+ this.tasksDir = null;
57
61
  } else if (projectsDirOrOptions) {
58
62
  this.projectsDir = projectsDirOrOptions.projectsDir ?? getClaudeProjectsDir();
59
63
  this.configPath = projectsDirOrOptions.configPath === undefined
@@ -68,12 +72,16 @@ export class ClaudeCodeScanner implements Scanner {
68
72
  this.fileHistoryDir = projectsDirOrOptions.fileHistoryDir === undefined
69
73
  ? getClaudeFileHistoryDir()
70
74
  : projectsDirOrOptions.fileHistoryDir;
75
+ this.tasksDir = projectsDirOrOptions.tasksDir === undefined
76
+ ? getClaudeTasksDir()
77
+ : projectsDirOrOptions.tasksDir;
71
78
  } else {
72
79
  this.projectsDir = getClaudeProjectsDir();
73
80
  this.configPath = getClaudeConfigPath();
74
81
  this.sessionEnvDir = getClaudeSessionEnvDir();
75
82
  this.todosDir = getClaudeTodosDir();
76
83
  this.fileHistoryDir = getClaudeFileHistoryDir();
84
+ this.tasksDir = getClaudeTasksDir();
77
85
  }
78
86
  }
79
87
 
@@ -148,6 +156,10 @@ export class ClaudeCodeScanner implements Scanner {
148
156
  const fileHistorySessions = await this.scanFileHistoryDir(validSessionIds);
149
157
  sessions.push(...fileHistorySessions);
150
158
 
159
+ // 7. Scan ~/.claude/tasks for orphaned folders
160
+ const tasksSessions = await this.scanTasksDir(validSessionIds);
161
+ sessions.push(...tasksSessions);
162
+
151
163
  const totalSize = sessions.reduce((sum, s) => sum + s.size, 0);
152
164
 
153
165
  return {
@@ -360,6 +372,49 @@ export class ClaudeCodeScanner implements Scanner {
360
372
  return orphanedHistories;
361
373
  }
362
374
 
375
+ /**
376
+ * Detect orphaned folders from ~/.claude/tasks
377
+ * Folder name is the session UUID, contains .lock file
378
+ */
379
+ private async scanTasksDir(validSessionIds: Set<string>): Promise<OrphanedSession[]> {
380
+ if (!this.tasksDir) {
381
+ return [];
382
+ }
383
+
384
+ const orphanedTasks: OrphanedSession[] = [];
385
+
386
+ try {
387
+ await access(this.tasksDir);
388
+ const entries = await readdir(this.tasksDir, { withFileTypes: true });
389
+
390
+ for (const entry of entries) {
391
+ if (!entry.isDirectory()) continue;
392
+
393
+ const sessionId = entry.name;
394
+
395
+ // Orphan if not in valid session IDs
396
+ if (!validSessionIds.has(sessionId)) {
397
+ const taskPath = join(this.tasksDir, entry.name);
398
+ const size = await getDirectorySize(taskPath);
399
+ const taskStat = await stat(taskPath);
400
+
401
+ orphanedTasks.push({
402
+ toolName: this.name,
403
+ sessionPath: taskPath,
404
+ projectPath: sessionId, // Session UUID
405
+ size,
406
+ lastModified: taskStat.mtime,
407
+ type: 'tasks',
408
+ });
409
+ }
410
+ }
411
+ } catch {
412
+ // Ignore if directory doesn't exist or access fails
413
+ }
414
+
415
+ return orphanedTasks;
416
+ }
417
+
363
418
  /**
364
419
  * Extract project path (cwd) from JSONL file
365
420
  */
@@ -1,6 +1,6 @@
1
1
  export type ToolName = 'claude-code' | 'cursor';
2
2
 
3
- export type SessionType = 'session' | 'config' | 'session-env' | 'todos' | 'file-history';
3
+ export type SessionType = 'session' | 'config' | 'session-env' | 'todos' | 'file-history' | 'tasks';
4
4
 
5
5
  export interface ConfigStats {
6
6
  lastCost?: number;
@@ -112,6 +112,13 @@ export function getClaudeFileHistoryDir(): string {
112
112
  return join(homedir(), '.claude', 'file-history');
113
113
  }
114
114
 
115
+ /**
116
+ * Claude Code tasks directory path (~/.claude/tasks)
117
+ */
118
+ export function getClaudeTasksDir(): string {
119
+ return join(homedir(), '.claude', 'tasks');
120
+ }
121
+
115
122
  /**
116
123
  * Replace home directory with ~ for display
117
124
  * /Users/user/.ai-session-tidy → ~/.ai-session-tidy
package/README.ko.md DELETED
@@ -1,213 +0,0 @@
1
- <div align="center">
2
-
3
- # AI Session Tidy
4
-
5
- **AI 코딩 도구의 방치된 세션을 정리합니다**
6
-
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
- [![node](https://img.shields.io/badge/node-%3E%3D24-brightgreen?style=flat-square)](https://nodejs.org/)
9
- [![platform](https://img.shields.io/badge/platform-macOS-blue?style=flat-square)](https://github.com/sooink/ai-session-tidy)
10
- [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
11
-
12
- [English](README.md) · [한국어](README.ko.md)
13
-
14
- </div>
15
-
16
- ---
17
-
18
- ## 문제점
19
-
20
- **Claude Code**나 **Cursor** 같은 AI 코딩 도구는 세션 데이터를 로컬에 저장합니다—대화 기록, 파일 스냅샷, Todo 등.
21
-
22
- 프로젝트를 삭제, 이동, 이름 변경하면 세션 데이터가 방치됩니다:
23
-
24
- ```
25
- ~/.claude/
26
- ├── projects/
27
- │ ├── -Users-you-deleted-project/ # 👈 지난주 삭제한 프로젝트
28
- │ ├── -Users-you-temp-worktree/ # 👈 제거한 worktree
29
- │ └── -Users-you-renamed-project/ # 👈 이름 바꾼 프로젝트
30
- ├── todos/ # 방치된 Todo 파일
31
- └── file-history/ # 방치된 Rewind 스냅샷
32
- ```
33
-
34
- 특히 **git worktree 워크플로우**에서 이 문제가 심각해집니다. 브랜치가 자주 생성되고 삭제되면서 세션 데이터가 빠르게 쌓이기 때문입니다.
35
-
36
- Claude Code는 30일 후 오래된 세션을 삭제하지만, **Cursor는 자동 정리 기능이 없습니다.** Claude Code도 30일간은 방치된 데이터가 남아있습니다. worktree를 삭제할 때마다 수동으로 정리하는 건 번거롭습니다.
37
-
38
- **이 도구는 방치된 세션을 자동으로 감지하고 정리합니다.**
39
- `watch start`를 한 번만 실행하면 백그라운드에서 프로젝트 삭제를 감시하고 자동으로 정리합니다.
40
-
41
- ## 빠른 시작
42
-
43
- ```bash
44
- npm install -g @sooink/ai-session-tidy
45
-
46
- # 자동 정리 (권장)
47
- ai-session-tidy watch start
48
-
49
- # 수동 정리
50
- ai-session-tidy # 방치된 세션 스캔
51
- ai-session-tidy clean # 휴지통으로 이동
52
- ```
53
-
54
- ![Demo](https://raw.githubusercontent.com/sooink/ai-session-tidy/main/assets/demo.gif)
55
-
56
- ## 활용 사례
57
-
58
- ### Git Worktree 워크플로우
59
-
60
- [Git worktree](https://git-scm.com/docs/git-worktree)로 여러 브랜치에서 동시에 작업할 수 있습니다. 하지만 worktree를 제거해도 세션 데이터는 남습니다.
61
-
62
- ```bash
63
- git worktree add ../feature-branch feature
64
- cd ../feature-branch && claude # 세션 데이터 생성
65
-
66
- git worktree remove ../feature-branch
67
- # ~/.claude/projects/-...-feature-branch/ 가 그대로 남음
68
- ```
69
-
70
- **watch 모드를 사용하면** 자동으로 정리됩니다:
71
-
72
- ```bash
73
- ai-session-tidy watch start # 한 번 실행, 로그인 시 자동 시작
74
-
75
- git worktree remove ../feature # watch가 감지 → 5분 후 정리
76
- ```
77
-
78
- ### 멀티 에이전트 오케스트레이션
79
-
80
- [최신 AI 워크플로우](https://www.anthropic.com/engineering/multi-agent-research-system)는 여러 에이전트를 병렬로 실행하며, 각각 격리된 worktree에서 작업합니다.
81
-
82
- 이로 인해 세션 데이터 축적이 배가됩니다. watch 모드가 시스템을 자동으로 깔끔하게 유지합니다.
83
-
84
- ## 지원 도구
85
-
86
- | 도구 | 상태 |
87
- |-----|------|
88
- | Claude Code | ✅ 지원 |
89
- | Cursor | ✅ 지원 |
90
-
91
- ## 명령어
92
-
93
- ### `scan` (기본)
94
-
95
- 삭제 없이 방치된 세션을 찾습니다. `ai-session-tidy`만 실행하면 `ai-session-tidy scan`과 동일합니다.
96
-
97
- ```bash
98
- ai-session-tidy # 기본 스캔
99
- ai-session-tidy -v # 상세 출력
100
- ai-session-tidy --json # JSON 출력
101
- ```
102
-
103
- <details>
104
- <summary><code>-v</code> 출력 예시</summary>
105
-
106
- ```
107
- ⚠ Found 2 session folder(s) + 1 config entry(ies) + 3 session-env folder(s) (156.2 MB)
108
-
109
- ┌─────────────┬──────────┬────────┬─────┬───────┬─────────┬──────────┬───────────┐
110
- │ Tool │ Sessions │ Config │ Env │ Todos │ History │ Size │ Scan Time │
111
- ├─────────────┼──────────┼────────┼─────┼───────┼─────────┼──────────┼───────────┤
112
- │ claude-code │ 2 │ 1 │ 3 │ - │ - │ 156.2 MB │ 45ms │
113
- └─────────────┴──────────┴────────┴─────┴───────┴─────────┴──────────┴───────────┘
114
-
115
- Session Folders:
116
-
117
- [claude-code] deleted-project (128.5 MB)
118
- → /Users/you/deleted-project
119
- Modified: 1/15/2025
120
-
121
- [claude-code] old-worktree (27.7 MB)
122
- → /Users/you/old-worktree
123
- Modified: 1/10/2025
124
-
125
- Config Entries (~/.claude.json):
126
-
127
- [config] deleted-project
128
- → /Users/you/deleted-project
129
- Cost: $1.25 | Tokens: 150K in / 12K out
130
- ```
131
-
132
- </details>
133
-
134
- ### `clean`
135
-
136
- 방치된 세션을 삭제합니다.
137
-
138
- ```bash
139
- ai-session-tidy clean # 휴지통으로 이동 (확인 필요)
140
- ai-session-tidy clean -i # 대화형 선택 (TTY 필요)
141
- ai-session-tidy clean -f # 확인 생략 (스크립트/CI용)
142
- ai-session-tidy clean -n # 드라이런 (삭제 대상만 표시)
143
- ```
144
-
145
- ![Interactive Clean](https://raw.githubusercontent.com/sooink/ai-session-tidy/main/assets/demo-interactive.gif)
146
-
147
- ### `watch`
148
-
149
- 감시하고 자동으로 정리합니다.
150
-
151
- ```bash
152
- ai-session-tidy watch # 포그라운드 모드
153
- ai-session-tidy watch start # 백그라운드 데몬 (로그인 시 자동 시작)
154
- ai-session-tidy watch stop # 데몬 중지
155
- ai-session-tidy watch status # 상태 확인
156
- ai-session-tidy watch status -l # 최근 로그 표시
157
- ```
158
-
159
- ### `config`
160
-
161
- 설정을 관리합니다.
162
-
163
- ```bash
164
- ai-session-tidy config show # 전체 설정 보기
165
- ai-session-tidy config path add ~/projects # 감시 경로 추가
166
- ai-session-tidy config path list # 감시 경로 목록
167
- ai-session-tidy config ignore add ~/backup # 제외 경로 추가
168
- ai-session-tidy config ignore list # 제외 경로 목록
169
- ai-session-tidy config delay 1 # 정리 딜레이 설정 (분)
170
- ai-session-tidy config depth 5 # 감시 깊이 설정
171
- ai-session-tidy config reset # 기본값으로 초기화
172
- ```
173
-
174
- > [!TIP]
175
- > 숨김 폴더 (`.git`, `.cache` 등)와 macOS 시스템 폴더 (`Library`, `Music` 등)는 자동으로 제외됩니다.
176
-
177
- ## 정리 대상
178
-
179
- ### Claude Code
180
-
181
- | 위치 | 설명 | 조건 |
182
- |-----|------|-----|
183
- | `~/.claude/projects/{path}/` | 세션 폴더 | 프로젝트 삭제됨 |
184
- | `~/.claude.json` | Config 항목 | 프로젝트 삭제됨 |
185
- | `~/.claude/session-env/{uuid}/` | 세션 환경 | 빈 폴더 |
186
- | `~/.claude/todos/{uuid}-*.json` | Todo 파일 | 세션 없음 |
187
- | `~/.claude/file-history/{uuid}/` | Rewind 스냅샷 | 세션 없음 |
188
-
189
- ### Cursor
190
-
191
- | 위치 | 설명 | 조건 |
192
- |-----|------|-----|
193
- | `~/Library/.../workspaceStorage/{hash}/` | 워크스페이스 데이터 | 프로젝트 삭제됨 |
194
-
195
- ## 안전장치
196
-
197
- > [!NOTE]
198
- > 모든 작업은 기본적으로 안전합니다—명시적 조치 없이는 영구 삭제되지 않습니다.
199
-
200
- - **스캔은 읽기 전용** — `scan`은 아무것도 삭제하지 않음
201
- - **휴지통 우선** — `clean`은 휴지통으로 이동 (복구 가능)
202
- - **확인 필요** — `-f` 없이는 삭제 전 확인
203
- - **5분 딜레이** — watch 모드는 정리 전 대기 (설정 가능)
204
-
205
- ## 개발
206
-
207
- ```bash
208
- git clone https://github.com/sooink/ai-session-tidy.git
209
- cd ai-session-tidy
210
- pnpm install
211
- pnpm build
212
- pnpm test
213
- ```