@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/.github/workflows/release.yml +57 -0
- package/CHANGELOG.md +32 -0
- package/README.md +50 -11
- package/assets/demo-interactive.gif +0 -0
- package/assets/demo.gif +0 -0
- package/dist/index.js +369 -116
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/clean.ts +187 -78
- package/src/commands/config.ts +7 -2
- package/src/commands/scan.ts +74 -2
- package/src/commands/watch.ts +12 -7
- package/src/core/cleaner.ts +5 -0
- package/src/core/constants.ts +10 -10
- package/src/core/service.ts +64 -10
- package/src/scanners/claude-code.ts +55 -0
- package/src/scanners/types.ts +1 -1
- package/src/utils/paths.ts +43 -3
- package/README.ko.md +0 -176
package/src/core/service.ts
CHANGED
|
@@ -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
|
-
|
|
40
|
-
scriptPath: string;
|
|
41
|
-
args: string[];
|
|
71
|
+
programArgs: string[];
|
|
42
72
|
}): string {
|
|
43
|
-
const
|
|
44
|
-
|
|
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
|
-
|
|
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');
|
|
@@ -131,6 +174,17 @@ export class ServiceManager {
|
|
|
131
174
|
throw new Error('Service not installed. Run "watch start" to install and start.');
|
|
132
175
|
}
|
|
133
176
|
|
|
177
|
+
// Clear old log files before starting
|
|
178
|
+
const logDir = join(homedir(), '.ai-session-tidy');
|
|
179
|
+
const stdoutPath = join(logDir, 'watcher.log');
|
|
180
|
+
const stderrPath = join(logDir, 'watcher.error.log');
|
|
181
|
+
if (existsSync(stdoutPath)) {
|
|
182
|
+
await writeFile(stdoutPath, '', 'utf-8');
|
|
183
|
+
}
|
|
184
|
+
if (existsSync(stderrPath)) {
|
|
185
|
+
await writeFile(stderrPath, '', 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
try {
|
|
135
189
|
execSync(`launchctl load "${this.plistPath}"`, { stdio: 'pipe' });
|
|
136
190
|
} catch (error) {
|
|
@@ -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
|
*/
|
package/src/scanners/types.ts
CHANGED
|
@@ -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;
|
package/src/utils/paths.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
1
2
|
import { homedir } from 'os';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
|
|
@@ -14,13 +15,45 @@ export function encodePath(path: string): string {
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Decode Claude Code encoded path to Unix path
|
|
17
|
-
*
|
|
18
|
+
* Handles hyphenated folder names by checking filesystem
|
|
19
|
+
*
|
|
20
|
+
* -Users-kory-my-project → /Users/kory/my-project (if exists)
|
|
21
|
+
* -Users-kory-my-project → /Users/kory/my/project (fallback)
|
|
18
22
|
*/
|
|
19
23
|
export function decodePath(encoded: string): string {
|
|
20
24
|
if (encoded === '') return '';
|
|
21
|
-
// Treat as Unix encoding if it starts with a dash
|
|
22
25
|
if (!encoded.startsWith('-')) return encoded;
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
// Split by dash (remove leading dash first)
|
|
28
|
+
const parts = encoded.slice(1).split('-');
|
|
29
|
+
|
|
30
|
+
// Try to reconstruct path by checking filesystem
|
|
31
|
+
return reconstructPath(parts, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively reconstruct path by trying different combinations
|
|
36
|
+
*/
|
|
37
|
+
function reconstructPath(parts: string[], currentPath: string): string {
|
|
38
|
+
if (parts.length === 0) return currentPath;
|
|
39
|
+
|
|
40
|
+
// Try combining segments (longest first to prefer hyphenated names)
|
|
41
|
+
for (let len = parts.length; len >= 1; len--) {
|
|
42
|
+
const segment = parts.slice(0, len).join('-');
|
|
43
|
+
const testPath = currentPath + '/' + segment;
|
|
44
|
+
|
|
45
|
+
if (existsSync(testPath)) {
|
|
46
|
+
// Found existing path, continue with remaining parts
|
|
47
|
+
const result = reconstructPath(parts.slice(len), testPath);
|
|
48
|
+
// Verify the final path exists (or is the best we can do)
|
|
49
|
+
if (existsSync(result) || parts.slice(len).length === 0) {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No existing path found, use simple decode for remaining parts
|
|
56
|
+
return currentPath + '/' + parts.join('/');
|
|
24
57
|
}
|
|
25
58
|
|
|
26
59
|
/**
|
|
@@ -79,6 +112,13 @@ export function getClaudeFileHistoryDir(): string {
|
|
|
79
112
|
return join(homedir(), '.claude', 'file-history');
|
|
80
113
|
}
|
|
81
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Claude Code tasks directory path (~/.claude/tasks)
|
|
117
|
+
*/
|
|
118
|
+
export function getClaudeTasksDir(): string {
|
|
119
|
+
return join(homedir(), '.claude', 'tasks');
|
|
120
|
+
}
|
|
121
|
+
|
|
82
122
|
/**
|
|
83
123
|
* Replace home directory with ~ for display
|
|
84
124
|
* /Users/user/.ai-session-tidy → ~/.ai-session-tidy
|
package/README.ko.md
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# AI Session Tidy
|
|
4
|
-
|
|
5
|
-
**AI 코딩 도구의 방치된 세션을 정리합니다**
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@sooink/ai-session-tidy)
|
|
8
|
-
[](https://nodejs.org/)
|
|
9
|
-
[](https://github.com/sooink/ai-session-tidy)
|
|
10
|
-
[](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
|
-
Claude Code는 30일 후 오래된 세션을 삭제하지만, **Cursor는 자동 정리 기능이 없습니다.** Claude Code도 30일간은 방치된 데이터가 남아있습니다.
|
|
35
|
-
|
|
36
|
-
**이 도구는 방치된 세션을 즉시 찾아서 정리합니다.**
|
|
37
|
-
|
|
38
|
-
## 빠른 시작
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install -g @sooink/ai-session-tidy
|
|
42
|
-
|
|
43
|
-
ai-session-tidy # 방치된 세션 스캔
|
|
44
|
-
ai-session-tidy clean # 휴지통으로 이동
|
|
45
|
-
ai-session-tidy watch start # 자동 정리 데몬
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-

|
|
49
|
-
|
|
50
|
-
## 활용 사례
|
|
51
|
-
|
|
52
|
-
### Git Worktree 워크플로우
|
|
53
|
-
|
|
54
|
-
[Git worktree](https://git-scm.com/docs/git-worktree)로 여러 브랜치에서 동시에 작업할 수 있습니다. 하지만 worktree를 제거해도 세션 데이터는 남습니다.
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
git worktree add ../feature-branch feature
|
|
58
|
-
cd ../feature-branch && claude # 세션 데이터 생성
|
|
59
|
-
|
|
60
|
-
git worktree remove ../feature-branch
|
|
61
|
-
# ~/.claude/projects/-...-feature-branch/ 가 그대로 남음
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**watch 모드를 사용하면** 자동으로 정리됩니다:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
ai-session-tidy watch start # 한 번 실행, 로그인 시 자동 시작
|
|
68
|
-
|
|
69
|
-
git worktree remove ../feature # watch가 감지 → 5분 후 정리
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### 멀티 에이전트 오케스트레이션
|
|
73
|
-
|
|
74
|
-
[최신 AI 워크플로우](https://www.anthropic.com/engineering/multi-agent-research-system)는 여러 에이전트를 병렬로 실행하며, 각각 격리된 worktree에서 작업합니다.
|
|
75
|
-
|
|
76
|
-
이로 인해 세션 데이터 축적이 배가됩니다. watch 모드가 시스템을 자동으로 깔끔하게 유지합니다.
|
|
77
|
-
|
|
78
|
-
## 지원 도구
|
|
79
|
-
|
|
80
|
-
| 도구 | 상태 |
|
|
81
|
-
|-----|------|
|
|
82
|
-
| Claude Code | ✅ 지원 |
|
|
83
|
-
| Cursor | ✅ 지원 |
|
|
84
|
-
|
|
85
|
-
## 명령어
|
|
86
|
-
|
|
87
|
-
### `scan` (기본)
|
|
88
|
-
|
|
89
|
-
삭제 없이 방치된 세션을 찾습니다.
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
ai-session-tidy # 기본 스캔
|
|
93
|
-
ai-session-tidy -v # 상세 출력
|
|
94
|
-
ai-session-tidy --json # JSON 출력
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### `clean`
|
|
98
|
-
|
|
99
|
-
방치된 세션을 삭제합니다.
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
ai-session-tidy clean # 휴지통으로 이동 (확인 필요)
|
|
103
|
-
ai-session-tidy clean -i # 대화형 선택
|
|
104
|
-
ai-session-tidy clean -f # 확인 생략
|
|
105
|
-
ai-session-tidy clean -n # 드라이런 (삭제 대상만 표시)
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-

|
|
109
|
-
|
|
110
|
-
### `watch`
|
|
111
|
-
|
|
112
|
-
감시하고 자동으로 정리합니다.
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
ai-session-tidy watch # 포그라운드 모드
|
|
116
|
-
ai-session-tidy watch start # 백그라운드 데몬 (로그인 시 자동 시작)
|
|
117
|
-
ai-session-tidy watch stop # 데몬 중지
|
|
118
|
-
ai-session-tidy watch status # 상태 확인
|
|
119
|
-
ai-session-tidy watch status -l # 최근 로그 표시
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### `config`
|
|
123
|
-
|
|
124
|
-
설정을 관리합니다.
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
ai-session-tidy config show # 전체 설정 보기
|
|
128
|
-
ai-session-tidy config path add ~/projects # 감시 경로 추가
|
|
129
|
-
ai-session-tidy config path list # 감시 경로 목록
|
|
130
|
-
ai-session-tidy config ignore add ~/backup # 제외 경로 추가
|
|
131
|
-
ai-session-tidy config ignore list # 제외 경로 목록
|
|
132
|
-
ai-session-tidy config delay 1 # 정리 딜레이 설정 (분)
|
|
133
|
-
ai-session-tidy config depth 5 # 감시 깊이 설정
|
|
134
|
-
ai-session-tidy config reset # 기본값으로 초기화
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
> [!TIP]
|
|
138
|
-
> 숨김 폴더 (`.git`, `.cache` 등)와 macOS 시스템 폴더 (`Library`, `Music` 등)는 자동으로 제외됩니다.
|
|
139
|
-
|
|
140
|
-
## 정리 대상
|
|
141
|
-
|
|
142
|
-
### Claude Code
|
|
143
|
-
|
|
144
|
-
| 위치 | 설명 | 조건 |
|
|
145
|
-
|-----|------|-----|
|
|
146
|
-
| `~/.claude/projects/{path}/` | 세션 폴더 | 프로젝트 삭제됨 |
|
|
147
|
-
| `~/.claude.json` | Config 항목 | 프로젝트 삭제됨 |
|
|
148
|
-
| `~/.claude/session-env/{uuid}/` | 세션 환경 | 빈 폴더 |
|
|
149
|
-
| `~/.claude/todos/{uuid}-*.json` | Todo 파일 | 세션 없음 |
|
|
150
|
-
| `~/.claude/file-history/{uuid}/` | Rewind 스냅샷 | 세션 없음 |
|
|
151
|
-
|
|
152
|
-
### Cursor
|
|
153
|
-
|
|
154
|
-
| 위치 | 설명 | 조건 |
|
|
155
|
-
|-----|------|-----|
|
|
156
|
-
| `~/Library/.../workspaceStorage/{hash}/` | 워크스페이스 데이터 | 프로젝트 삭제됨 |
|
|
157
|
-
|
|
158
|
-
## 안전장치
|
|
159
|
-
|
|
160
|
-
> [!NOTE]
|
|
161
|
-
> 모든 작업은 기본적으로 안전합니다—명시적 조치 없이는 영구 삭제되지 않습니다.
|
|
162
|
-
|
|
163
|
-
- **스캔은 읽기 전용** — `scan`은 아무것도 삭제하지 않음
|
|
164
|
-
- **휴지통 우선** — `clean`은 휴지통으로 이동 (복구 가능)
|
|
165
|
-
- **확인 필요** — `-f` 없이는 삭제 전 확인
|
|
166
|
-
- **5분 딜레이** — watch 모드는 정리 전 대기 (설정 가능)
|
|
167
|
-
|
|
168
|
-
## 개발
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
git clone https://github.com/sooink/ai-session-tidy.git
|
|
172
|
-
cd ai-session-tidy
|
|
173
|
-
pnpm install
|
|
174
|
-
pnpm build
|
|
175
|
-
pnpm test
|
|
176
|
-
```
|