@sooink/ai-session-tidy 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,1917 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command6 } from "commander";
5
+
6
+ // src/commands/scan.ts
7
+ import { Command } from "commander";
8
+ import Table from "cli-table3";
9
+ import ora from "ora";
10
+ import chalk2 from "chalk";
11
+
12
+ // src/utils/logger.ts
13
+ import chalk from "chalk";
14
+ var logger = {
15
+ info(message) {
16
+ console.log(chalk.blue("\u2139"), message);
17
+ },
18
+ warn(message) {
19
+ console.log(chalk.yellow("\u26A0"), message);
20
+ },
21
+ error(message) {
22
+ console.log(chalk.red("\u2716"), message);
23
+ },
24
+ success(message) {
25
+ console.log(chalk.green("\u2714"), message);
26
+ },
27
+ debug(message) {
28
+ if (process.env["DEBUG"]) {
29
+ console.log(chalk.gray("\u{1F41B}"), message);
30
+ }
31
+ }
32
+ };
33
+
34
+ // src/utils/size.ts
35
+ import { readdir, stat } from "fs/promises";
36
+ import { join } from "path";
37
+ var UNITS = ["B", "KB", "MB", "GB", "TB"];
38
+ function formatSize(bytes) {
39
+ if (bytes <= 0) return "0 B";
40
+ let size = bytes;
41
+ let unitIndex = 0;
42
+ while (size >= 1024 && unitIndex < UNITS.length - 1) {
43
+ size /= 1024;
44
+ unitIndex++;
45
+ }
46
+ if (unitIndex === 0) {
47
+ return `${Math.floor(size)} ${UNITS[unitIndex]}`;
48
+ }
49
+ return `${size.toFixed(1)} ${UNITS[unitIndex]}`;
50
+ }
51
+ async function getDirectorySize(dirPath) {
52
+ try {
53
+ const entries = await readdir(dirPath, { withFileTypes: true });
54
+ let totalSize = 0;
55
+ for (const entry of entries) {
56
+ const fullPath = join(dirPath, entry.name);
57
+ if (entry.isDirectory()) {
58
+ totalSize += await getDirectorySize(fullPath);
59
+ } else if (entry.isFile()) {
60
+ const fileStat = await stat(fullPath);
61
+ totalSize += fileStat.size;
62
+ }
63
+ }
64
+ return totalSize;
65
+ } catch {
66
+ return 0;
67
+ }
68
+ }
69
+
70
+ // src/scanners/claude-code.ts
71
+ import { readdir as readdir2, readFile, stat as stat2, access } from "fs/promises";
72
+ import { join as join3 } from "path";
73
+ import { createReadStream } from "fs";
74
+ import { createInterface } from "readline";
75
+
76
+ // src/utils/paths.ts
77
+ import { homedir } from "os";
78
+ import { join as join2 } from "path";
79
+ function decodePath(encoded) {
80
+ if (encoded === "") return "";
81
+ if (!encoded.startsWith("-")) return encoded;
82
+ return encoded.replace(/-/g, "/");
83
+ }
84
+ function getConfigDir(appName) {
85
+ switch (process.platform) {
86
+ case "darwin":
87
+ return join2(homedir(), "Library/Application Support", appName);
88
+ case "win32":
89
+ return join2(process.env["APPDATA"] || "", appName);
90
+ default:
91
+ return join2(homedir(), ".config", appName);
92
+ }
93
+ }
94
+ function getClaudeProjectsDir() {
95
+ return join2(homedir(), ".claude", "projects");
96
+ }
97
+ function getClaudeConfigPath() {
98
+ return join2(homedir(), ".claude.json");
99
+ }
100
+ function getCursorWorkspaceDir() {
101
+ return join2(getConfigDir("Cursor"), "User", "workspaceStorage");
102
+ }
103
+ function getClaudeSessionEnvDir() {
104
+ return join2(homedir(), ".claude", "session-env");
105
+ }
106
+ function getClaudeTodosDir() {
107
+ return join2(homedir(), ".claude", "todos");
108
+ }
109
+ function getClaudeFileHistoryDir() {
110
+ return join2(homedir(), ".claude", "file-history");
111
+ }
112
+ function tildify(path) {
113
+ const home = homedir();
114
+ if (path.startsWith(home)) {
115
+ return path.replace(home, "~");
116
+ }
117
+ return path;
118
+ }
119
+
120
+ // src/scanners/claude-code.ts
121
+ var ClaudeCodeScanner = class {
122
+ name = "claude-code";
123
+ projectsDir;
124
+ configPath;
125
+ sessionEnvDir;
126
+ todosDir;
127
+ fileHistoryDir;
128
+ constructor(projectsDirOrOptions) {
129
+ if (typeof projectsDirOrOptions === "string") {
130
+ this.projectsDir = projectsDirOrOptions;
131
+ this.configPath = null;
132
+ this.sessionEnvDir = null;
133
+ this.todosDir = null;
134
+ this.fileHistoryDir = null;
135
+ } else if (projectsDirOrOptions) {
136
+ this.projectsDir = projectsDirOrOptions.projectsDir ?? getClaudeProjectsDir();
137
+ this.configPath = projectsDirOrOptions.configPath === void 0 ? getClaudeConfigPath() : projectsDirOrOptions.configPath;
138
+ this.sessionEnvDir = projectsDirOrOptions.sessionEnvDir === void 0 ? getClaudeSessionEnvDir() : projectsDirOrOptions.sessionEnvDir;
139
+ this.todosDir = projectsDirOrOptions.todosDir === void 0 ? getClaudeTodosDir() : projectsDirOrOptions.todosDir;
140
+ this.fileHistoryDir = projectsDirOrOptions.fileHistoryDir === void 0 ? getClaudeFileHistoryDir() : projectsDirOrOptions.fileHistoryDir;
141
+ } else {
142
+ this.projectsDir = getClaudeProjectsDir();
143
+ this.configPath = getClaudeConfigPath();
144
+ this.sessionEnvDir = getClaudeSessionEnvDir();
145
+ this.todosDir = getClaudeTodosDir();
146
+ this.fileHistoryDir = getClaudeFileHistoryDir();
147
+ }
148
+ }
149
+ async isAvailable() {
150
+ try {
151
+ await access(this.projectsDir);
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ async scan() {
158
+ const startTime = performance.now();
159
+ const sessions = [];
160
+ if (await this.isAvailable()) {
161
+ const entries = await readdir2(this.projectsDir, { withFileTypes: true });
162
+ for (const entry of entries) {
163
+ if (!entry.isDirectory()) continue;
164
+ const sessionPath = join3(this.projectsDir, entry.name);
165
+ let projectPath = await this.extractProjectPath(sessionPath);
166
+ if (!projectPath) {
167
+ projectPath = decodePath(entry.name);
168
+ }
169
+ const projectExists = await this.pathExists(projectPath);
170
+ if (projectExists) continue;
171
+ const size = await getDirectorySize(sessionPath);
172
+ if (size === 0) continue;
173
+ const lastModified = await this.getLastModified(sessionPath);
174
+ sessions.push({
175
+ toolName: this.name,
176
+ sessionPath,
177
+ projectPath,
178
+ size,
179
+ lastModified,
180
+ type: "session"
181
+ });
182
+ }
183
+ }
184
+ const configSessions = await this.scanConfigFile();
185
+ sessions.push(...configSessions);
186
+ const sessionEnvSessions = await this.scanSessionEnvDir();
187
+ sessions.push(...sessionEnvSessions);
188
+ const validSessionIds = await this.collectValidSessionIds();
189
+ const todosSessions = await this.scanTodosDir(validSessionIds);
190
+ sessions.push(...todosSessions);
191
+ const fileHistorySessions = await this.scanFileHistoryDir(validSessionIds);
192
+ sessions.push(...fileHistorySessions);
193
+ const totalSize = sessions.reduce((sum, s) => sum + s.size, 0);
194
+ return {
195
+ toolName: this.name,
196
+ sessions,
197
+ totalSize,
198
+ scanDuration: performance.now() - startTime
199
+ };
200
+ }
201
+ /**
202
+ * Detect orphaned projects from ~/.claude.json projects entries
203
+ */
204
+ async scanConfigFile() {
205
+ if (!this.configPath) {
206
+ return [];
207
+ }
208
+ const configPath = this.configPath;
209
+ const orphanedConfigs = [];
210
+ try {
211
+ const content = await readFile(configPath, "utf-8");
212
+ const config = JSON.parse(content);
213
+ const configStat = await stat2(configPath);
214
+ if (!config.projects || typeof config.projects !== "object") {
215
+ return [];
216
+ }
217
+ for (const [projectPath, projectData] of Object.entries(config.projects)) {
218
+ const projectExists = await this.pathExists(projectPath);
219
+ if (!projectExists) {
220
+ orphanedConfigs.push({
221
+ toolName: this.name,
222
+ sessionPath: configPath,
223
+ projectPath,
224
+ size: 0,
225
+ // config entries have negligible size
226
+ lastModified: configStat.mtime,
227
+ type: "config",
228
+ configStats: {
229
+ lastCost: projectData.lastCost,
230
+ lastTotalInputTokens: projectData.lastTotalInputTokens,
231
+ lastTotalOutputTokens: projectData.lastTotalOutputTokens
232
+ }
233
+ });
234
+ }
235
+ }
236
+ } catch {
237
+ }
238
+ return orphanedConfigs;
239
+ }
240
+ /**
241
+ * Detect empty session environment folders from ~/.claude/session-env
242
+ */
243
+ async scanSessionEnvDir() {
244
+ if (!this.sessionEnvDir) {
245
+ return [];
246
+ }
247
+ const orphanedEnvs = [];
248
+ try {
249
+ await access(this.sessionEnvDir);
250
+ const entries = await readdir2(this.sessionEnvDir, { withFileTypes: true });
251
+ for (const entry of entries) {
252
+ if (!entry.isDirectory()) continue;
253
+ const envPath = join3(this.sessionEnvDir, entry.name);
254
+ const files = await readdir2(envPath);
255
+ if (files.length === 0) {
256
+ const envStat = await stat2(envPath);
257
+ orphanedEnvs.push({
258
+ toolName: this.name,
259
+ sessionPath: envPath,
260
+ projectPath: entry.name,
261
+ // UUID
262
+ size: 0,
263
+ lastModified: envStat.mtime,
264
+ type: "session-env"
265
+ });
266
+ }
267
+ }
268
+ } catch {
269
+ }
270
+ return orphanedEnvs;
271
+ }
272
+ /**
273
+ * Collect valid session UUIDs from all projects
274
+ */
275
+ async collectValidSessionIds() {
276
+ const sessionIds = /* @__PURE__ */ new Set();
277
+ try {
278
+ const projectDirs = await readdir2(this.projectsDir, { withFileTypes: true });
279
+ for (const projectDir of projectDirs) {
280
+ if (!projectDir.isDirectory()) continue;
281
+ const projectPath = join3(this.projectsDir, projectDir.name);
282
+ const files = await readdir2(projectPath);
283
+ for (const file of files) {
284
+ if (file.endsWith(".jsonl")) {
285
+ const sessionId = file.replace(".jsonl", "");
286
+ sessionIds.add(sessionId);
287
+ }
288
+ }
289
+ }
290
+ } catch {
291
+ }
292
+ return sessionIds;
293
+ }
294
+ /**
295
+ * Detect orphaned todo files from ~/.claude/todos
296
+ * Filename pattern: {session-uuid}-agent-{agent-uuid}.json
297
+ */
298
+ async scanTodosDir(validSessionIds) {
299
+ if (!this.todosDir) {
300
+ return [];
301
+ }
302
+ const orphanedTodos = [];
303
+ try {
304
+ await access(this.todosDir);
305
+ const entries = await readdir2(this.todosDir, { withFileTypes: true });
306
+ for (const entry of entries) {
307
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
308
+ const sessionId = entry.name.split("-agent-")[0];
309
+ if (!sessionId) continue;
310
+ if (!validSessionIds.has(sessionId)) {
311
+ const todoPath = join3(this.todosDir, entry.name);
312
+ const todoStat = await stat2(todoPath);
313
+ orphanedTodos.push({
314
+ toolName: this.name,
315
+ sessionPath: todoPath,
316
+ projectPath: sessionId,
317
+ // Session UUID
318
+ size: todoStat.size,
319
+ lastModified: todoStat.mtime,
320
+ type: "todos"
321
+ });
322
+ }
323
+ }
324
+ } catch {
325
+ }
326
+ return orphanedTodos;
327
+ }
328
+ /**
329
+ * Detect orphaned folders from ~/.claude/file-history
330
+ * Folder name is the session UUID
331
+ */
332
+ async scanFileHistoryDir(validSessionIds) {
333
+ if (!this.fileHistoryDir) {
334
+ return [];
335
+ }
336
+ const orphanedHistories = [];
337
+ try {
338
+ await access(this.fileHistoryDir);
339
+ const entries = await readdir2(this.fileHistoryDir, { withFileTypes: true });
340
+ for (const entry of entries) {
341
+ if (!entry.isDirectory()) continue;
342
+ const sessionId = entry.name;
343
+ if (!validSessionIds.has(sessionId)) {
344
+ const historyPath = join3(this.fileHistoryDir, entry.name);
345
+ const size = await getDirectorySize(historyPath);
346
+ const historyStat = await stat2(historyPath);
347
+ orphanedHistories.push({
348
+ toolName: this.name,
349
+ sessionPath: historyPath,
350
+ projectPath: sessionId,
351
+ // Session UUID
352
+ size,
353
+ lastModified: historyStat.mtime,
354
+ type: "file-history"
355
+ });
356
+ }
357
+ }
358
+ } catch {
359
+ }
360
+ return orphanedHistories;
361
+ }
362
+ /**
363
+ * Extract project path (cwd) from JSONL file
364
+ */
365
+ async extractProjectPath(sessionDir) {
366
+ try {
367
+ const files = await readdir2(sessionDir);
368
+ const jsonlFile = files.find((f) => f.endsWith(".jsonl"));
369
+ if (!jsonlFile) return null;
370
+ const jsonlPath = join3(sessionDir, jsonlFile);
371
+ const cwd = await this.findCwdInJsonl(jsonlPath);
372
+ return cwd;
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+ async findCwdInJsonl(jsonlPath) {
378
+ return new Promise((resolve3) => {
379
+ const stream = createReadStream(jsonlPath, { encoding: "utf-8" });
380
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
381
+ let found = false;
382
+ let lineCount = 0;
383
+ const maxLines = 10;
384
+ rl.on("line", (line) => {
385
+ if (found || lineCount >= maxLines) {
386
+ rl.close();
387
+ return;
388
+ }
389
+ lineCount++;
390
+ try {
391
+ const entry = JSON.parse(line);
392
+ if (entry.cwd) {
393
+ found = true;
394
+ rl.close();
395
+ stream.destroy();
396
+ resolve3(entry.cwd);
397
+ }
398
+ } catch {
399
+ }
400
+ });
401
+ rl.on("close", () => {
402
+ if (!found) resolve3(null);
403
+ });
404
+ rl.on("error", () => {
405
+ resolve3(null);
406
+ });
407
+ });
408
+ }
409
+ async pathExists(path) {
410
+ try {
411
+ await access(path);
412
+ return true;
413
+ } catch {
414
+ return false;
415
+ }
416
+ }
417
+ async getLastModified(dirPath) {
418
+ try {
419
+ const entries = await readdir2(dirPath, { withFileTypes: true });
420
+ let latestTime = 0;
421
+ for (const entry of entries) {
422
+ const fullPath = join3(dirPath, entry.name);
423
+ const fileStat = await stat2(fullPath);
424
+ const mtime = fileStat.mtimeMs;
425
+ if (mtime > latestTime) {
426
+ latestTime = mtime;
427
+ }
428
+ }
429
+ return latestTime > 0 ? new Date(latestTime) : /* @__PURE__ */ new Date();
430
+ } catch {
431
+ return /* @__PURE__ */ new Date();
432
+ }
433
+ }
434
+ };
435
+
436
+ // src/scanners/cursor.ts
437
+ import { readdir as readdir3, readFile as readFile2, stat as stat3, access as access2 } from "fs/promises";
438
+ import { join as join4 } from "path";
439
+ import { fileURLToPath } from "url";
440
+ var CursorScanner = class {
441
+ name = "cursor";
442
+ workspaceDir;
443
+ constructor(workspaceDir) {
444
+ this.workspaceDir = workspaceDir ?? getCursorWorkspaceDir();
445
+ }
446
+ async isAvailable() {
447
+ try {
448
+ await access2(this.workspaceDir);
449
+ return true;
450
+ } catch {
451
+ return false;
452
+ }
453
+ }
454
+ async scan() {
455
+ const startTime = performance.now();
456
+ const sessions = [];
457
+ if (!await this.isAvailable()) {
458
+ return {
459
+ toolName: this.name,
460
+ sessions: [],
461
+ totalSize: 0,
462
+ scanDuration: performance.now() - startTime
463
+ };
464
+ }
465
+ const entries = await readdir3(this.workspaceDir, { withFileTypes: true });
466
+ for (const entry of entries) {
467
+ if (!entry.isDirectory()) continue;
468
+ const sessionPath = join4(this.workspaceDir, entry.name);
469
+ const workspaceJsonPath = join4(sessionPath, "workspace.json");
470
+ const projectPath = await this.parseWorkspaceJson(workspaceJsonPath);
471
+ if (!projectPath) continue;
472
+ const projectExists = await this.pathExists(projectPath);
473
+ if (projectExists) continue;
474
+ const size = await getDirectorySize(sessionPath);
475
+ const lastModified = await this.getLastModified(sessionPath);
476
+ sessions.push({
477
+ toolName: this.name,
478
+ sessionPath,
479
+ projectPath,
480
+ size,
481
+ lastModified
482
+ });
483
+ }
484
+ const totalSize = sessions.reduce((sum, s) => sum + s.size, 0);
485
+ return {
486
+ toolName: this.name,
487
+ sessions,
488
+ totalSize,
489
+ scanDuration: performance.now() - startTime
490
+ };
491
+ }
492
+ async parseWorkspaceJson(workspaceJsonPath) {
493
+ try {
494
+ const content = await readFile2(workspaceJsonPath, "utf-8");
495
+ const data = JSON.parse(content);
496
+ if (!data.folder) return null;
497
+ if (data.folder.startsWith("file://")) {
498
+ return fileURLToPath(data.folder);
499
+ }
500
+ return data.folder;
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+ async pathExists(path) {
506
+ try {
507
+ await access2(path);
508
+ return true;
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+ async getLastModified(dirPath) {
514
+ try {
515
+ const entries = await readdir3(dirPath, { withFileTypes: true });
516
+ let latestTime = 0;
517
+ for (const entry of entries) {
518
+ const fullPath = join4(dirPath, entry.name);
519
+ const fileStat = await stat3(fullPath);
520
+ const mtime = fileStat.mtimeMs;
521
+ if (mtime > latestTime) {
522
+ latestTime = mtime;
523
+ }
524
+ }
525
+ return latestTime > 0 ? new Date(latestTime) : /* @__PURE__ */ new Date();
526
+ } catch {
527
+ return /* @__PURE__ */ new Date();
528
+ }
529
+ }
530
+ };
531
+
532
+ // src/scanners/index.ts
533
+ function createAllScanners() {
534
+ return [new ClaudeCodeScanner(), new CursorScanner()];
535
+ }
536
+ async function getAvailableScanners(scanners) {
537
+ const results = await Promise.all(
538
+ scanners.map(async (scanner) => ({
539
+ scanner,
540
+ available: await scanner.isAvailable()
541
+ }))
542
+ );
543
+ return results.filter((r) => r.available).map((r) => r.scanner);
544
+ }
545
+ async function runAllScanners(scanners) {
546
+ return Promise.all(scanners.map((scanner) => scanner.scan()));
547
+ }
548
+
549
+ // src/commands/scan.ts
550
+ function formatTokens(count) {
551
+ if (!count) return "0";
552
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
553
+ if (count >= 1e3) return `${(count / 1e3).toFixed(0)}K`;
554
+ return count.toString();
555
+ }
556
+ var scanCommand = new Command("scan").description("Scan for orphaned session data from AI coding tools").option("-v, --verbose", "Enable verbose output").option("--json", "Output results as JSON").action(async (options) => {
557
+ const spinner = ora("Scanning for orphaned sessions...").start();
558
+ try {
559
+ const allScanners = createAllScanners();
560
+ const availableScanners = await getAvailableScanners(allScanners);
561
+ if (availableScanners.length === 0) {
562
+ spinner.warn("No AI coding tools detected on this system.");
563
+ return;
564
+ }
565
+ if (options.verbose) {
566
+ spinner.text = `Found ${availableScanners.length} tools: ${availableScanners.map((s) => s.name).join(", ")}`;
567
+ }
568
+ const results = await runAllScanners(availableScanners);
569
+ spinner.stop();
570
+ if (options.json) {
571
+ outputJson(results);
572
+ } else {
573
+ outputTable(results, options.verbose);
574
+ }
575
+ } catch (error) {
576
+ spinner.fail("Scan failed");
577
+ logger.error(
578
+ error instanceof Error ? error.message : "Unknown error occurred"
579
+ );
580
+ process.exit(1);
581
+ }
582
+ });
583
+ function outputJson(results) {
584
+ const allSessions = results.flatMap((r) => r.sessions);
585
+ const output = {
586
+ totalSessions: allSessions.length,
587
+ totalSize: results.reduce((sum, r) => sum + r.totalSize, 0),
588
+ results: results.map((r) => ({
589
+ tool: r.toolName,
590
+ sessionCount: r.sessions.length,
591
+ totalSize: r.totalSize,
592
+ scanDuration: r.scanDuration,
593
+ sessions: r.sessions
594
+ }))
595
+ };
596
+ console.log(JSON.stringify(output, null, 2));
597
+ }
598
+ function outputTable(results, verbose) {
599
+ const allSessions = results.flatMap((r) => r.sessions);
600
+ const folderSessions = allSessions.filter((s) => s.type === "session" || s.type === void 0);
601
+ const configEntries = allSessions.filter((s) => s.type === "config");
602
+ const sessionEnvEntries = allSessions.filter((s) => s.type === "session-env");
603
+ const todosEntries = allSessions.filter((s) => s.type === "todos");
604
+ const fileHistoryEntries = allSessions.filter((s) => s.type === "file-history");
605
+ const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
606
+ if (allSessions.length === 0) {
607
+ logger.success("No orphaned sessions found.");
608
+ return;
609
+ }
610
+ console.log();
611
+ const parts = [];
612
+ if (folderSessions.length > 0) {
613
+ parts.push(`${folderSessions.length} session folder(s)`);
614
+ }
615
+ if (configEntries.length > 0) {
616
+ parts.push(`${configEntries.length} config entry(ies)`);
617
+ }
618
+ if (sessionEnvEntries.length > 0) {
619
+ parts.push(`${sessionEnvEntries.length} session-env folder(s)`);
620
+ }
621
+ if (todosEntries.length > 0) {
622
+ parts.push(`${todosEntries.length} todos file(s)`);
623
+ }
624
+ if (fileHistoryEntries.length > 0) {
625
+ parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
626
+ }
627
+ logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
628
+ console.log();
629
+ const summaryTable = new Table({
630
+ head: [
631
+ chalk2.cyan("Tool"),
632
+ chalk2.cyan("Sessions"),
633
+ chalk2.cyan("Config"),
634
+ chalk2.cyan("Env"),
635
+ chalk2.cyan("Todos"),
636
+ chalk2.cyan("History"),
637
+ chalk2.cyan("Size"),
638
+ chalk2.cyan("Scan Time")
639
+ ],
640
+ style: { head: [] }
641
+ });
642
+ for (const result of results) {
643
+ if (result.sessions.length > 0) {
644
+ const folders = result.sessions.filter((s) => s.type === "session" || s.type === void 0).length;
645
+ const configs = result.sessions.filter((s) => s.type === "config").length;
646
+ const envs = result.sessions.filter((s) => s.type === "session-env").length;
647
+ const todos = result.sessions.filter((s) => s.type === "todos").length;
648
+ const histories = result.sessions.filter((s) => s.type === "file-history").length;
649
+ summaryTable.push([
650
+ result.toolName,
651
+ folders > 0 ? String(folders) : "-",
652
+ configs > 0 ? String(configs) : "-",
653
+ envs > 0 ? String(envs) : "-",
654
+ todos > 0 ? String(todos) : "-",
655
+ histories > 0 ? String(histories) : "-",
656
+ formatSize(result.totalSize),
657
+ `${result.scanDuration.toFixed(0)}ms`
658
+ ]);
659
+ }
660
+ }
661
+ console.log(summaryTable.toString());
662
+ if (verbose && allSessions.length > 0) {
663
+ if (folderSessions.length > 0) {
664
+ console.log();
665
+ console.log(chalk2.bold("Session Folders:"));
666
+ console.log();
667
+ for (const session of folderSessions) {
668
+ const projectName = session.projectPath.split("/").pop() || session.projectPath;
669
+ console.log(
670
+ ` ${chalk2.cyan(`[${session.toolName}]`)} ${chalk2.white(projectName)} ${chalk2.dim(`(${formatSize(session.size)})`)}`
671
+ );
672
+ console.log(` ${chalk2.dim("\u2192")} ${session.projectPath}`);
673
+ console.log(` ${chalk2.dim("Modified:")} ${session.lastModified.toLocaleDateString()}`);
674
+ console.log();
675
+ }
676
+ }
677
+ if (configEntries.length > 0) {
678
+ console.log();
679
+ console.log(chalk2.bold("Config Entries (~/.claude.json):"));
680
+ console.log();
681
+ for (const entry of configEntries) {
682
+ const projectName = entry.projectPath.split("/").pop() || entry.projectPath;
683
+ console.log(
684
+ ` ${chalk2.yellow("[config]")} ${chalk2.white(projectName)}`
685
+ );
686
+ console.log(` ${chalk2.dim("\u2192")} ${entry.projectPath}`);
687
+ if (entry.configStats?.lastCost) {
688
+ const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
689
+ const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
690
+ const outTokens = formatTokens(entry.configStats.lastTotalOutputTokens);
691
+ console.log(` ${chalk2.dim(`Cost: ${cost} | Tokens: ${inTokens} in / ${outTokens} out`)}`);
692
+ }
693
+ console.log();
694
+ }
695
+ }
696
+ }
697
+ console.log();
698
+ console.log(
699
+ chalk2.dim('Run "ai-session-tidy clean" to remove orphaned sessions.')
700
+ );
701
+ }
702
+
703
+ // src/commands/clean.ts
704
+ import { basename } from "path";
705
+ import { Command as Command2 } from "commander";
706
+ import ora2 from "ora";
707
+ import chalk3 from "chalk";
708
+ import inquirer from "inquirer";
709
+
710
+ // src/core/cleaner.ts
711
+ import { readFile as readFile3, writeFile, mkdir, copyFile } from "fs/promises";
712
+ import { existsSync } from "fs";
713
+ import { join as join5 } from "path";
714
+ import { homedir as homedir2 } from "os";
715
+
716
+ // src/core/trash.ts
717
+ import { rm, access as access3 } from "fs/promises";
718
+ import trash from "trash";
719
+ async function pathExists(path) {
720
+ try {
721
+ await access3(path);
722
+ return true;
723
+ } catch {
724
+ return false;
725
+ }
726
+ }
727
+ async function moveToTrash(path) {
728
+ if (!await pathExists(path)) {
729
+ return false;
730
+ }
731
+ await trash(path);
732
+ return true;
733
+ }
734
+ async function permanentDelete(path) {
735
+ if (!await pathExists(path)) {
736
+ return false;
737
+ }
738
+ await rm(path, { recursive: true, force: true });
739
+ return true;
740
+ }
741
+
742
+ // src/core/cleaner.ts
743
+ function getBackupDir() {
744
+ return join5(homedir2(), ".ai-session-tidy", "backups");
745
+ }
746
+ var Cleaner = class {
747
+ async clean(sessions, options) {
748
+ const result = {
749
+ deletedCount: 0,
750
+ deletedByType: {
751
+ session: 0,
752
+ sessionEnv: 0,
753
+ todos: 0,
754
+ fileHistory: 0
755
+ },
756
+ skippedCount: 0,
757
+ alreadyGoneCount: 0,
758
+ configEntriesRemoved: 0,
759
+ totalSizeDeleted: 0,
760
+ errors: []
761
+ };
762
+ const useTrash = options.useTrash ?? true;
763
+ const folderSessions = sessions.filter((s) => s.type !== "config");
764
+ const configEntries = sessions.filter((s) => s.type === "config");
765
+ for (const session of folderSessions) {
766
+ if (options.dryRun) {
767
+ result.skippedCount++;
768
+ continue;
769
+ }
770
+ try {
771
+ const deleted = useTrash ? await moveToTrash(session.sessionPath) : await permanentDelete(session.sessionPath);
772
+ if (deleted) {
773
+ result.deletedCount++;
774
+ result.totalSizeDeleted += session.size;
775
+ switch (session.type) {
776
+ case "session-env":
777
+ result.deletedByType.sessionEnv++;
778
+ break;
779
+ case "todos":
780
+ result.deletedByType.todos++;
781
+ break;
782
+ case "file-history":
783
+ result.deletedByType.fileHistory++;
784
+ break;
785
+ default:
786
+ result.deletedByType.session++;
787
+ }
788
+ } else {
789
+ result.alreadyGoneCount++;
790
+ }
791
+ } catch (error) {
792
+ result.errors.push({
793
+ sessionPath: session.sessionPath,
794
+ error: error instanceof Error ? error : new Error(String(error))
795
+ });
796
+ }
797
+ }
798
+ if (configEntries.length > 0 && !options.dryRun) {
799
+ try {
800
+ const configResult = await this.cleanConfigEntries(configEntries);
801
+ result.configEntriesRemoved = configResult.removed;
802
+ result.backupPath = configResult.backupPath;
803
+ } catch (error) {
804
+ result.errors.push({
805
+ sessionPath: configEntries[0].sessionPath,
806
+ error: error instanceof Error ? error : new Error(String(error))
807
+ });
808
+ }
809
+ } else if (options.dryRun) {
810
+ result.skippedCount += configEntries.length;
811
+ }
812
+ return result;
813
+ }
814
+ /**
815
+ * Remove orphaned project entries from ~/.claude.json
816
+ */
817
+ async cleanConfigEntries(entries) {
818
+ if (entries.length === 0) {
819
+ return { removed: 0, backupPath: "" };
820
+ }
821
+ const configPath = entries[0].sessionPath;
822
+ const projectPathsToRemove = new Set(entries.map((e) => e.projectPath));
823
+ const backupDir = getBackupDir();
824
+ if (!existsSync(backupDir)) {
825
+ await mkdir(backupDir, { recursive: true });
826
+ }
827
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
828
+ const backupPath = join5(backupDir, `claude.json.${timestamp}.bak`);
829
+ await copyFile(configPath, backupPath);
830
+ const content = await readFile3(configPath, "utf-8");
831
+ const config = JSON.parse(content);
832
+ if (!config.projects) {
833
+ return { removed: 0, backupPath };
834
+ }
835
+ let removedCount = 0;
836
+ for (const projectPath of projectPathsToRemove) {
837
+ if (projectPath in config.projects) {
838
+ delete config.projects[projectPath];
839
+ removedCount++;
840
+ }
841
+ }
842
+ if (removedCount > 0) {
843
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
844
+ }
845
+ return { removed: removedCount, backupPath };
846
+ }
847
+ };
848
+
849
+ // src/commands/clean.ts
850
+ function formatSessionChoice(session) {
851
+ const projectName = basename(session.projectPath);
852
+ const isConfig = session.type === "config";
853
+ const toolTag = isConfig ? chalk3.yellow("[config]") : chalk3.cyan(`[${session.toolName}]`);
854
+ const name = chalk3.white(projectName);
855
+ const size = isConfig ? "" : chalk3.dim(`(${formatSize(session.size)})`);
856
+ const path = chalk3.dim(`\u2192 ${session.projectPath}`);
857
+ return `${toolTag} ${name} ${size}
858
+ ${path}`;
859
+ }
860
+ var cleanCommand = new Command2("clean").description("Remove orphaned session data").option("-f, --force", "Skip confirmation prompt").option("-n, --dry-run", "Show what would be deleted without deleting").option("-i, --interactive", "Select sessions to delete interactively").option("--no-trash", "Permanently delete instead of moving to trash").option("-v, --verbose", "Enable verbose output").action(async (options) => {
861
+ const spinner = ora2("Scanning for orphaned sessions...").start();
862
+ try {
863
+ const allScanners = createAllScanners();
864
+ const availableScanners = await getAvailableScanners(allScanners);
865
+ if (availableScanners.length === 0) {
866
+ spinner.warn("No AI coding tools detected on this system.");
867
+ return;
868
+ }
869
+ const results = await runAllScanners(availableScanners);
870
+ const allSessions = results.flatMap((r) => r.sessions);
871
+ const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
872
+ spinner.stop();
873
+ if (allSessions.length === 0) {
874
+ logger.success("No orphaned sessions found.");
875
+ return;
876
+ }
877
+ const folderSessions = allSessions.filter(
878
+ (s) => s.type === "session" || s.type === void 0
879
+ );
880
+ const configEntries = allSessions.filter((s) => s.type === "config");
881
+ const sessionEnvEntries = allSessions.filter(
882
+ (s) => s.type === "session-env"
883
+ );
884
+ const todosEntries = allSessions.filter((s) => s.type === "todos");
885
+ const fileHistoryEntries = allSessions.filter(
886
+ (s) => s.type === "file-history"
887
+ );
888
+ const autoCleanEntries = [
889
+ ...sessionEnvEntries,
890
+ ...todosEntries,
891
+ ...fileHistoryEntries
892
+ ];
893
+ const selectableSessions = allSessions.filter(
894
+ (s) => s.type !== "session-env" && s.type !== "todos" && s.type !== "file-history"
895
+ );
896
+ console.log();
897
+ const parts = [];
898
+ if (folderSessions.length > 0) {
899
+ parts.push(`${folderSessions.length} session folder(s)`);
900
+ }
901
+ if (configEntries.length > 0) {
902
+ parts.push(`${configEntries.length} config entry(ies)`);
903
+ }
904
+ if (sessionEnvEntries.length > 0) {
905
+ parts.push(`${sessionEnvEntries.length} session-env folder(s)`);
906
+ }
907
+ if (todosEntries.length > 0) {
908
+ parts.push(`${todosEntries.length} todos file(s)`);
909
+ }
910
+ if (fileHistoryEntries.length > 0) {
911
+ parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
912
+ }
913
+ logger.warn(`Found ${parts.join(" + ")} (${formatSize(totalSize)})`);
914
+ if (options.verbose && !options.interactive) {
915
+ console.log();
916
+ for (const session of selectableSessions) {
917
+ console.log(
918
+ chalk3.dim(` ${session.toolName}: ${session.projectPath}`)
919
+ );
920
+ }
921
+ }
922
+ let sessionsToClean = allSessions;
923
+ if (options.interactive) {
924
+ if (selectableSessions.length === 0) {
925
+ if (autoCleanEntries.length > 0) {
926
+ sessionsToClean = autoCleanEntries;
927
+ logger.info(
928
+ `Only ${autoCleanEntries.length} auto-cleanup target(s) found`
929
+ );
930
+ } else {
931
+ logger.info("No selectable sessions found.");
932
+ return;
933
+ }
934
+ } else {
935
+ console.log();
936
+ const { selected } = await inquirer.prompt([
937
+ {
938
+ type: "checkbox",
939
+ name: "selected",
940
+ message: "Select sessions to delete:",
941
+ choices: selectableSessions.map((session) => ({
942
+ name: formatSessionChoice(session),
943
+ value: session,
944
+ checked: false
945
+ })),
946
+ pageSize: 15,
947
+ loop: false
948
+ }
949
+ ]);
950
+ if (selected.length === 0) {
951
+ logger.info("No sessions selected. Cancelled.");
952
+ return;
953
+ }
954
+ sessionsToClean = [...selected, ...autoCleanEntries];
955
+ const selectedSize = selected.reduce((sum, s) => sum + s.size, 0);
956
+ console.log();
957
+ if (selected.length > 0) {
958
+ logger.info(
959
+ `Selected: ${selected.length} session(s) (${formatSize(selectedSize)})`
960
+ );
961
+ }
962
+ if (autoCleanEntries.length > 0) {
963
+ const autoSize = autoCleanEntries.reduce((sum, s) => sum + s.size, 0);
964
+ logger.info(
965
+ `+ ${autoCleanEntries.length} auto-cleanup target(s) (${formatSize(autoSize)})`
966
+ );
967
+ }
968
+ }
969
+ }
970
+ const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
971
+ if (options.dryRun) {
972
+ console.log();
973
+ logger.info(
974
+ chalk3.yellow("Dry-run mode: No files will be deleted.")
975
+ );
976
+ console.log();
977
+ const dryRunFolders = sessionsToClean.filter(
978
+ (s) => s.type === "session" || s.type === void 0
979
+ );
980
+ const dryRunConfigs = sessionsToClean.filter((s) => s.type === "config");
981
+ const dryRunEnvs = sessionsToClean.filter(
982
+ (s) => s.type === "session-env"
983
+ );
984
+ const dryRunTodos = sessionsToClean.filter((s) => s.type === "todos");
985
+ const dryRunHistories = sessionsToClean.filter(
986
+ (s) => s.type === "file-history"
987
+ );
988
+ for (const session of dryRunFolders) {
989
+ console.log(
990
+ ` ${chalk3.red("Would delete:")} ${session.sessionPath} (${formatSize(session.size)})`
991
+ );
992
+ }
993
+ if (dryRunConfigs.length > 0) {
994
+ console.log();
995
+ console.log(
996
+ ` ${chalk3.yellow("Would remove from ~/.claude.json:")}`
997
+ );
998
+ for (const config of dryRunConfigs) {
999
+ console.log(` - ${config.projectPath}`);
1000
+ }
1001
+ }
1002
+ const autoCleanParts = [];
1003
+ if (dryRunEnvs.length > 0) {
1004
+ autoCleanParts.push(`${dryRunEnvs.length} session-env`);
1005
+ }
1006
+ if (dryRunTodos.length > 0) {
1007
+ autoCleanParts.push(`${dryRunTodos.length} todos`);
1008
+ }
1009
+ if (dryRunHistories.length > 0) {
1010
+ autoCleanParts.push(`${dryRunHistories.length} file-history`);
1011
+ }
1012
+ if (autoCleanParts.length > 0) {
1013
+ const autoSize = dryRunEnvs.reduce((sum, s) => sum + s.size, 0) + dryRunTodos.reduce((sum, s) => sum + s.size, 0) + dryRunHistories.reduce((sum, s) => sum + s.size, 0);
1014
+ console.log();
1015
+ console.log(
1016
+ ` ${chalk3.dim(`Would auto-delete: ${autoCleanParts.join(" + ")} (${formatSize(autoSize)})`)}`
1017
+ );
1018
+ }
1019
+ return;
1020
+ }
1021
+ if (!options.force) {
1022
+ console.log();
1023
+ const action = options.noTrash ? "permanently delete" : "move to trash";
1024
+ const { confirmed } = await inquirer.prompt([
1025
+ {
1026
+ type: "confirm",
1027
+ name: "confirmed",
1028
+ message: `${action} ${sessionsToClean.length} session(s) (${formatSize(cleanSize)})?`,
1029
+ default: false
1030
+ }
1031
+ ]);
1032
+ if (!confirmed) {
1033
+ logger.info("Cancelled.");
1034
+ return;
1035
+ }
1036
+ }
1037
+ const cleanSpinner = ora2("Cleaning orphaned sessions...").start();
1038
+ const cleaner = new Cleaner();
1039
+ const cleanResult = await cleaner.clean(sessionsToClean, {
1040
+ dryRun: false,
1041
+ useTrash: !options.noTrash
1042
+ });
1043
+ cleanSpinner.stop();
1044
+ console.log();
1045
+ if (cleanResult.deletedCount > 0) {
1046
+ const action = options.noTrash ? "Deleted" : "Moved to trash";
1047
+ const parts2 = [];
1048
+ const { deletedByType } = cleanResult;
1049
+ if (deletedByType.session > 0) {
1050
+ parts2.push(`${deletedByType.session} session`);
1051
+ }
1052
+ if (deletedByType.sessionEnv > 0) {
1053
+ parts2.push(`${deletedByType.sessionEnv} session-env`);
1054
+ }
1055
+ if (deletedByType.todos > 0) {
1056
+ parts2.push(`${deletedByType.todos} todos`);
1057
+ }
1058
+ if (deletedByType.fileHistory > 0) {
1059
+ parts2.push(`${deletedByType.fileHistory} file-history`);
1060
+ }
1061
+ const summary = parts2.length > 0 ? parts2.join(" + ") : `${cleanResult.deletedCount} item(s)`;
1062
+ logger.success(
1063
+ `${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`
1064
+ );
1065
+ }
1066
+ if (cleanResult.alreadyGoneCount > 0 && options.verbose) {
1067
+ logger.info(
1068
+ `Skipped ${cleanResult.alreadyGoneCount} already-deleted item(s)`
1069
+ );
1070
+ }
1071
+ if (cleanResult.configEntriesRemoved > 0) {
1072
+ logger.success(
1073
+ `Removed ${cleanResult.configEntriesRemoved} config entry(ies) from ~/.claude.json`
1074
+ );
1075
+ if (cleanResult.backupPath) {
1076
+ logger.info(`Backup saved to: ${tildify(cleanResult.backupPath)}`);
1077
+ }
1078
+ }
1079
+ if (cleanResult.errors.length > 0) {
1080
+ logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
1081
+ if (options.verbose) {
1082
+ for (const err of cleanResult.errors) {
1083
+ console.log(chalk3.red(` ${err.sessionPath}: ${err.error.message}`));
1084
+ }
1085
+ }
1086
+ }
1087
+ } catch (error) {
1088
+ spinner.fail("Clean failed");
1089
+ logger.error(
1090
+ error instanceof Error ? error.message : "Unknown error occurred"
1091
+ );
1092
+ process.exit(1);
1093
+ }
1094
+ });
1095
+
1096
+ // src/commands/watch.ts
1097
+ import { Command as Command3 } from "commander";
1098
+ import chalk4 from "chalk";
1099
+ import { existsSync as existsSync4 } from "fs";
1100
+ import { homedir as homedir5 } from "os";
1101
+ import { join as join8, resolve as resolve2 } from "path";
1102
+
1103
+ // src/utils/config.ts
1104
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
1105
+ import { homedir as homedir3 } from "os";
1106
+ import { dirname as dirname2, join as join6, resolve } from "path";
1107
+ import { fileURLToPath as fileURLToPath2 } from "url";
1108
+ var CONFIG_VERSION = "0.1";
1109
+ function getAppVersion() {
1110
+ const __dirname = dirname2(fileURLToPath2(import.meta.url));
1111
+ const paths = [
1112
+ join6(__dirname, "..", "..", "package.json"),
1113
+ // from src/utils/
1114
+ join6(__dirname, "..", "package.json")
1115
+ // from dist/
1116
+ ];
1117
+ for (const packagePath of paths) {
1118
+ try {
1119
+ if (existsSync2(packagePath)) {
1120
+ const content = readFileSync(packagePath, "utf-8");
1121
+ const pkg = JSON.parse(content);
1122
+ return pkg.version;
1123
+ }
1124
+ } catch {
1125
+ continue;
1126
+ }
1127
+ }
1128
+ return "unknown";
1129
+ }
1130
+ function getConfigPath() {
1131
+ return join6(homedir3(), ".config", "ai-session-tidy", "config.json");
1132
+ }
1133
+ function loadConfig() {
1134
+ const configPath = getConfigPath();
1135
+ if (!existsSync2(configPath)) {
1136
+ return {};
1137
+ }
1138
+ try {
1139
+ const content = readFileSync(configPath, "utf-8");
1140
+ return JSON.parse(content);
1141
+ } catch {
1142
+ return {};
1143
+ }
1144
+ }
1145
+ function saveConfig(config) {
1146
+ const configPath = getConfigPath();
1147
+ const configDir = dirname2(configPath);
1148
+ if (!existsSync2(configDir)) {
1149
+ mkdirSync(configDir, { recursive: true });
1150
+ }
1151
+ const configWithVersion = {
1152
+ version: getAppVersion(),
1153
+ configVersion: CONFIG_VERSION,
1154
+ ...config
1155
+ };
1156
+ writeFileSync(configPath, JSON.stringify(configWithVersion, null, 2), "utf-8");
1157
+ }
1158
+ function getWatchPaths() {
1159
+ const config = loadConfig();
1160
+ return config.watchPaths;
1161
+ }
1162
+ function setWatchPaths(paths) {
1163
+ const config = loadConfig();
1164
+ config.watchPaths = paths;
1165
+ saveConfig(config);
1166
+ }
1167
+ function addWatchPath(path) {
1168
+ const config = loadConfig();
1169
+ const resolved = resolve(path.replace(/^~/, homedir3()));
1170
+ const paths = config.watchPaths ?? [];
1171
+ if (!paths.includes(resolved)) {
1172
+ paths.push(resolved);
1173
+ config.watchPaths = paths;
1174
+ saveConfig(config);
1175
+ }
1176
+ }
1177
+ function removeWatchPath(path) {
1178
+ const config = loadConfig();
1179
+ const resolved = resolve(path.replace(/^~/, homedir3()));
1180
+ const paths = config.watchPaths ?? [];
1181
+ const index = paths.indexOf(resolved);
1182
+ if (index === -1) return false;
1183
+ paths.splice(index, 1);
1184
+ config.watchPaths = paths;
1185
+ saveConfig(config);
1186
+ return true;
1187
+ }
1188
+ function getWatchDelay() {
1189
+ const config = loadConfig();
1190
+ return config.watchDelay;
1191
+ }
1192
+ function setWatchDelay(minutes) {
1193
+ const config = loadConfig();
1194
+ config.watchDelay = minutes;
1195
+ saveConfig(config);
1196
+ }
1197
+ function getWatchDepth() {
1198
+ const config = loadConfig();
1199
+ return config.watchDepth;
1200
+ }
1201
+ function setWatchDepth(depth) {
1202
+ const config = loadConfig();
1203
+ config.watchDepth = Math.min(depth, 5);
1204
+ saveConfig(config);
1205
+ }
1206
+ function resetConfig() {
1207
+ saveConfig({});
1208
+ }
1209
+
1210
+ // src/core/watcher.ts
1211
+ import { watch } from "chokidar";
1212
+ import { access as access4 } from "fs/promises";
1213
+ var Watcher = class {
1214
+ options;
1215
+ debounceMs;
1216
+ watcher = null;
1217
+ /** Per-path delay timers */
1218
+ pendingDeletes = /* @__PURE__ */ new Map();
1219
+ /** Events waiting for debounce */
1220
+ batchedEvents = [];
1221
+ /** Debounce timer */
1222
+ debounceTimer = null;
1223
+ constructor(options) {
1224
+ this.options = options;
1225
+ this.debounceMs = options.debounceMs ?? 1e4;
1226
+ }
1227
+ start() {
1228
+ if (this.watcher) return;
1229
+ this.watcher = watch(this.options.watchPaths, {
1230
+ ignoreInitial: true,
1231
+ persistent: true,
1232
+ depth: this.options.depth ?? 3
1233
+ });
1234
+ this.watcher.on("unlinkDir", (path) => {
1235
+ this.handleDelete(path);
1236
+ });
1237
+ this.watcher.on("addDir", (path) => {
1238
+ this.handleRecovery(path);
1239
+ });
1240
+ }
1241
+ stop() {
1242
+ if (!this.watcher) return;
1243
+ this.watcher.close();
1244
+ this.watcher = null;
1245
+ for (const timeout of this.pendingDeletes.values()) {
1246
+ clearTimeout(timeout);
1247
+ }
1248
+ this.pendingDeletes.clear();
1249
+ if (this.debounceTimer) {
1250
+ clearTimeout(this.debounceTimer);
1251
+ this.debounceTimer = null;
1252
+ }
1253
+ this.batchedEvents = [];
1254
+ }
1255
+ isWatching() {
1256
+ return this.watcher !== null;
1257
+ }
1258
+ /**
1259
+ * Handle folder deletion event
1260
+ *
1261
+ * Stage 1: Set per-path delay timer
1262
+ * - Don't process immediately to allow recovery
1263
+ * - Add to batch if still deleted after delay
1264
+ */
1265
+ handleDelete(path) {
1266
+ if (this.pendingDeletes.has(path)) return;
1267
+ const timeout = setTimeout(async () => {
1268
+ const stillDeleted = !await this.pathExists(path);
1269
+ if (stillDeleted) {
1270
+ this.addToBatch({
1271
+ path,
1272
+ timestamp: /* @__PURE__ */ new Date()
1273
+ });
1274
+ }
1275
+ this.pendingDeletes.delete(path);
1276
+ }, this.options.delayMs);
1277
+ this.pendingDeletes.set(path, timeout);
1278
+ }
1279
+ /**
1280
+ * Add event to batch and reset debounce timer
1281
+ *
1282
+ * Stage 2: Debounce
1283
+ * - Reset timer on each new event
1284
+ * - Execute batch when no new events for debounce period
1285
+ * - This combines multiple subfolder deletions into single cleanup
1286
+ */
1287
+ addToBatch(event) {
1288
+ this.batchedEvents.push(event);
1289
+ if (this.debounceTimer) {
1290
+ clearTimeout(this.debounceTimer);
1291
+ }
1292
+ this.debounceTimer = setTimeout(() => {
1293
+ this.flushBatch();
1294
+ }, this.debounceMs);
1295
+ }
1296
+ /**
1297
+ * Deliver batched events to callback
1298
+ * - Scan/cleanup runs only once
1299
+ */
1300
+ flushBatch() {
1301
+ if (this.batchedEvents.length === 0) return;
1302
+ const events = [...this.batchedEvents];
1303
+ this.batchedEvents = [];
1304
+ this.debounceTimer = null;
1305
+ this.options.onDelete(events);
1306
+ }
1307
+ handleRecovery(path) {
1308
+ const timeout = this.pendingDeletes.get(path);
1309
+ if (timeout) {
1310
+ clearTimeout(timeout);
1311
+ this.pendingDeletes.delete(path);
1312
+ }
1313
+ }
1314
+ async pathExists(path) {
1315
+ try {
1316
+ await access4(path);
1317
+ return true;
1318
+ } catch {
1319
+ return false;
1320
+ }
1321
+ }
1322
+ };
1323
+
1324
+ // src/core/service.ts
1325
+ import { homedir as homedir4 } from "os";
1326
+ import { join as join7, dirname as dirname3 } from "path";
1327
+ import { readFile as readFile4, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1328
+ import { existsSync as existsSync3 } from "fs";
1329
+ import { execSync } from "child_process";
1330
+ var SERVICE_LABEL = "sooink.ai-session-tidy.watcher";
1331
+ var PLIST_FILENAME = `${SERVICE_LABEL}.plist`;
1332
+ function getPlistPath() {
1333
+ return join7(homedir4(), "Library", "LaunchAgents", PLIST_FILENAME);
1334
+ }
1335
+ function getNodePath() {
1336
+ return process.execPath;
1337
+ }
1338
+ function getScriptPath() {
1339
+ const scriptPath = process.argv[1];
1340
+ if (scriptPath && existsSync3(scriptPath)) {
1341
+ return scriptPath;
1342
+ }
1343
+ throw new Error("Could not determine script path");
1344
+ }
1345
+ function generatePlist(options) {
1346
+ const allArgs = [options.nodePath, options.scriptPath, ...options.args];
1347
+ const argsXml = allArgs.map((arg) => ` <string>${arg}</string>`).join("\n");
1348
+ const home = homedir4();
1349
+ return `<?xml version="1.0" encoding="UTF-8"?>
1350
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1351
+ <plist version="1.0">
1352
+ <dict>
1353
+ <key>Label</key>
1354
+ <string>${options.label}</string>
1355
+ <key>EnvironmentVariables</key>
1356
+ <dict>
1357
+ <key>HOME</key>
1358
+ <string>${home}</string>
1359
+ </dict>
1360
+ <key>ProgramArguments</key>
1361
+ <array>
1362
+ ${argsXml}
1363
+ </array>
1364
+ <key>RunAtLoad</key>
1365
+ <true/>
1366
+ <key>KeepAlive</key>
1367
+ <true/>
1368
+ <key>StandardOutPath</key>
1369
+ <string>${join7(home, ".ai-session-tidy", "watcher.log")}</string>
1370
+ <key>StandardErrorPath</key>
1371
+ <string>${join7(home, ".ai-session-tidy", "watcher.error.log")}</string>
1372
+ </dict>
1373
+ </plist>`;
1374
+ }
1375
+ var ServiceManager = class {
1376
+ plistPath;
1377
+ constructor() {
1378
+ this.plistPath = getPlistPath();
1379
+ }
1380
+ isSupported() {
1381
+ return process.platform === "darwin";
1382
+ }
1383
+ async install() {
1384
+ if (!this.isSupported()) {
1385
+ throw new Error("Service management is only supported on macOS");
1386
+ }
1387
+ const launchAgentsDir = dirname3(this.plistPath);
1388
+ if (!existsSync3(launchAgentsDir)) {
1389
+ await mkdir2(launchAgentsDir, { recursive: true });
1390
+ }
1391
+ const logDir = join7(homedir4(), ".ai-session-tidy");
1392
+ if (!existsSync3(logDir)) {
1393
+ await mkdir2(logDir, { recursive: true });
1394
+ }
1395
+ const stdoutPath = join7(logDir, "watcher.log");
1396
+ const stderrPath = join7(logDir, "watcher.error.log");
1397
+ await writeFile2(stdoutPath, "", "utf-8");
1398
+ await writeFile2(stderrPath, "", "utf-8");
1399
+ const plistContent = generatePlist({
1400
+ label: SERVICE_LABEL,
1401
+ nodePath: getNodePath(),
1402
+ scriptPath: getScriptPath(),
1403
+ args: ["watch", "run"]
1404
+ });
1405
+ await writeFile2(this.plistPath, plistContent, "utf-8");
1406
+ }
1407
+ async uninstall() {
1408
+ if (existsSync3(this.plistPath)) {
1409
+ await unlink(this.plistPath);
1410
+ }
1411
+ }
1412
+ async start() {
1413
+ if (!this.isSupported()) {
1414
+ throw new Error("Service management is only supported on macOS");
1415
+ }
1416
+ if (!existsSync3(this.plistPath)) {
1417
+ throw new Error('Service not installed. Run "watch start" to install and start.');
1418
+ }
1419
+ try {
1420
+ execSync(`launchctl load "${this.plistPath}"`, { stdio: "pipe" });
1421
+ } catch (error) {
1422
+ const message = error instanceof Error ? error.message : String(error);
1423
+ if (!message.includes("already loaded")) {
1424
+ throw error;
1425
+ }
1426
+ }
1427
+ }
1428
+ async stop() {
1429
+ if (!this.isSupported()) {
1430
+ throw new Error("Service management is only supported on macOS");
1431
+ }
1432
+ if (!existsSync3(this.plistPath)) {
1433
+ return;
1434
+ }
1435
+ try {
1436
+ execSync(`launchctl unload "${this.plistPath}"`, { stdio: "pipe" });
1437
+ } catch {
1438
+ }
1439
+ }
1440
+ async status() {
1441
+ const info = {
1442
+ status: "not_installed",
1443
+ label: SERVICE_LABEL,
1444
+ plistPath: this.plistPath
1445
+ };
1446
+ if (!this.isSupported()) {
1447
+ return info;
1448
+ }
1449
+ if (!existsSync3(this.plistPath)) {
1450
+ return info;
1451
+ }
1452
+ try {
1453
+ const output = execSync("launchctl list", { encoding: "utf-8" });
1454
+ const lines = output.split("\n");
1455
+ for (const line of lines) {
1456
+ if (line.includes(SERVICE_LABEL)) {
1457
+ const parts = line.split(/\s+/);
1458
+ const pid = parseInt(parts[0] ?? "", 10);
1459
+ if (!isNaN(pid) && pid > 0) {
1460
+ info.status = "running";
1461
+ info.pid = pid;
1462
+ } else {
1463
+ info.status = "stopped";
1464
+ }
1465
+ return info;
1466
+ }
1467
+ }
1468
+ info.status = "stopped";
1469
+ return info;
1470
+ } catch {
1471
+ info.status = "stopped";
1472
+ return info;
1473
+ }
1474
+ }
1475
+ async getLogs(lines = 50) {
1476
+ const logDir = join7(homedir4(), ".ai-session-tidy");
1477
+ const stdoutPath = join7(logDir, "watcher.log");
1478
+ const stderrPath = join7(logDir, "watcher.error.log");
1479
+ let stdout = "";
1480
+ let stderr = "";
1481
+ try {
1482
+ if (existsSync3(stdoutPath)) {
1483
+ const content = await readFile4(stdoutPath, "utf-8");
1484
+ const logLines = content.split("\n");
1485
+ stdout = logLines.slice(-lines).join("\n");
1486
+ }
1487
+ } catch {
1488
+ }
1489
+ try {
1490
+ if (existsSync3(stderrPath)) {
1491
+ const content = await readFile4(stderrPath, "utf-8");
1492
+ const logLines = content.split("\n");
1493
+ stderr = logLines.slice(-lines).join("\n");
1494
+ }
1495
+ } catch {
1496
+ }
1497
+ return { stdout, stderr };
1498
+ }
1499
+ };
1500
+ var serviceManager = new ServiceManager();
1501
+
1502
+ // src/commands/watch.ts
1503
+ var DEFAULT_DELAY_MINUTES = 5;
1504
+ var MAX_DELAY_MINUTES = 10;
1505
+ var watchCommand = new Command3("watch").description("Watch for project deletions and auto-clean orphaned sessions");
1506
+ var runCommand = new Command3("run").description("Run watcher in foreground (default)").option(
1507
+ "-p, --path <path>",
1508
+ "Path to watch (can be used multiple times, saves to config)",
1509
+ (value, previous) => previous.concat([value]),
1510
+ []
1511
+ ).option("--no-save", "Do not save paths to config").option(
1512
+ "-d, --delay <minutes>",
1513
+ `Delay before cleanup (default: ${DEFAULT_DELAY_MINUTES}, max: ${MAX_DELAY_MINUTES} minutes)`
1514
+ ).option("--no-trash", "Permanently delete instead of moving to trash").option("-v, --verbose", "Enable verbose output").action(runWatcher);
1515
+ var startCommand = new Command3("start").description("Start watcher as OS service (background + auto-start at login)").action(async () => {
1516
+ const supported = serviceManager.isSupported();
1517
+ if (!supported) {
1518
+ logger.error("Service management is only supported on macOS.");
1519
+ logger.info('Use "watch run" to run the watcher in foreground.');
1520
+ return;
1521
+ }
1522
+ try {
1523
+ const currentStatus = await serviceManager.status();
1524
+ if (currentStatus.status === "running") {
1525
+ logger.info("Watcher service is already running.");
1526
+ return;
1527
+ }
1528
+ logger.info("Installing watcher service...");
1529
+ await serviceManager.install();
1530
+ logger.info("Starting watcher service...");
1531
+ await serviceManager.start();
1532
+ const status = await serviceManager.status();
1533
+ if (status.status === "running") {
1534
+ logger.success(`Watcher service started (PID: ${status.pid})`);
1535
+ logger.info("The watcher will automatically start at login.");
1536
+ logger.info(`Logs: ~/.ai-session-tidy/watcher.log`);
1537
+ } else {
1538
+ logger.warn("Service installed but may not be running yet.");
1539
+ logger.info("Check status with: ai-session-tidy watch status");
1540
+ }
1541
+ } catch (error) {
1542
+ logger.error(`Failed to start service: ${error instanceof Error ? error.message : String(error)}`);
1543
+ }
1544
+ });
1545
+ var stopCommand = new Command3("stop").description("Stop watcher service and disable auto-start").action(async () => {
1546
+ const supported = serviceManager.isSupported();
1547
+ if (!supported) {
1548
+ logger.error("Service management is only supported on macOS.");
1549
+ return;
1550
+ }
1551
+ try {
1552
+ const currentStatus = await serviceManager.status();
1553
+ if (currentStatus.status === "not_installed") {
1554
+ logger.info("Watcher service is not installed.");
1555
+ return;
1556
+ }
1557
+ logger.info("Stopping watcher service...");
1558
+ await serviceManager.stop();
1559
+ logger.info("Removing service configuration...");
1560
+ await serviceManager.uninstall();
1561
+ logger.success("Watcher service stopped and removed.");
1562
+ } catch (error) {
1563
+ logger.error(`Failed to stop service: ${error instanceof Error ? error.message : String(error)}`);
1564
+ }
1565
+ });
1566
+ var statusCommand = new Command3("status").description("Show watcher service status").option("-l, --logs [lines]", "Show recent logs", "20").action(async (options) => {
1567
+ const supported = serviceManager.isSupported();
1568
+ if (!supported) {
1569
+ logger.error("Service management is only supported on macOS.");
1570
+ return;
1571
+ }
1572
+ try {
1573
+ const status = await serviceManager.status();
1574
+ console.log();
1575
+ console.log(chalk4.bold("Watcher Service Status"));
1576
+ console.log("\u2500".repeat(40));
1577
+ console.log(`Label: ${status.label}`);
1578
+ console.log(`Status: ${formatStatus(status.status)}`);
1579
+ if (status.pid) {
1580
+ console.log(`PID: ${status.pid}`);
1581
+ }
1582
+ console.log(`Plist: ${status.plistPath}`);
1583
+ console.log();
1584
+ if (options.logs) {
1585
+ const lines = parseInt(options.logs, 10) || 20;
1586
+ const logs = await serviceManager.getLogs(lines);
1587
+ if (logs.stdout) {
1588
+ console.log(chalk4.bold("Recent Logs:"));
1589
+ console.log("\u2500".repeat(40));
1590
+ console.log(logs.stdout);
1591
+ console.log();
1592
+ }
1593
+ if (logs.stderr) {
1594
+ console.log(chalk4.bold.red("Error Logs:"));
1595
+ console.log("\u2500".repeat(40));
1596
+ console.log(logs.stderr);
1597
+ console.log();
1598
+ }
1599
+ }
1600
+ } catch (error) {
1601
+ logger.error(`Failed to get status: ${error instanceof Error ? error.message : String(error)}`);
1602
+ }
1603
+ });
1604
+ function formatStatus(status) {
1605
+ switch (status) {
1606
+ case "running":
1607
+ return chalk4.green("running");
1608
+ case "stopped":
1609
+ return chalk4.yellow("stopped");
1610
+ case "not_installed":
1611
+ return chalk4.dim("not installed");
1612
+ default:
1613
+ return status;
1614
+ }
1615
+ }
1616
+ watchCommand.addCommand(runCommand, { isDefault: true });
1617
+ watchCommand.addCommand(startCommand);
1618
+ watchCommand.addCommand(stopCommand);
1619
+ watchCommand.addCommand(statusCommand);
1620
+ async function runWatcher(options) {
1621
+ const configDelay = getWatchDelay();
1622
+ let delayMinutes = options.delay ? parseInt(options.delay, 10) : configDelay ?? DEFAULT_DELAY_MINUTES;
1623
+ if (delayMinutes > MAX_DELAY_MINUTES) {
1624
+ logger.warn(`Maximum delay is ${MAX_DELAY_MINUTES} minutes. Using ${MAX_DELAY_MINUTES}.`);
1625
+ delayMinutes = MAX_DELAY_MINUTES;
1626
+ }
1627
+ const delayMs = delayMinutes * 60 * 1e3;
1628
+ let watchPaths;
1629
+ if (options.path && options.path.length > 0) {
1630
+ watchPaths = options.path.map((p) => resolve2(p.replace(/^~/, homedir5())));
1631
+ if (!options.noSave) {
1632
+ setWatchPaths(watchPaths);
1633
+ logger.info(`Saved watch paths to config.`);
1634
+ }
1635
+ } else {
1636
+ const configPaths = getWatchPaths();
1637
+ if (configPaths && configPaths.length > 0) {
1638
+ watchPaths = configPaths;
1639
+ logger.info(`Using saved watch paths from config.`);
1640
+ } else {
1641
+ watchPaths = getDefaultWatchPaths();
1642
+ logger.info(`Using default watch paths.`);
1643
+ }
1644
+ }
1645
+ const validPaths = watchPaths.filter((p) => existsSync4(p));
1646
+ if (validPaths.length === 0) {
1647
+ logger.error("No valid watch paths found. Use -p to specify paths.");
1648
+ return;
1649
+ }
1650
+ if (validPaths.length < watchPaths.length) {
1651
+ const invalidPaths = watchPaths.filter((p) => !existsSync4(p));
1652
+ logger.warn(`Skipping non-existent paths: ${invalidPaths.join(", ")}`);
1653
+ }
1654
+ const allScanners = createAllScanners();
1655
+ const availableScanners = await getAvailableScanners(allScanners);
1656
+ if (availableScanners.length === 0) {
1657
+ logger.warn("No AI coding tools detected on this system.");
1658
+ return;
1659
+ }
1660
+ const depth = getWatchDepth() ?? 3;
1661
+ logger.info(
1662
+ `Watching for project deletions (${availableScanners.map((s) => s.name).join(", ")})`
1663
+ );
1664
+ logger.info(`Watch paths: ${validPaths.join(", ")}`);
1665
+ logger.info(`Cleanup delay: ${String(delayMinutes)} minute(s)`);
1666
+ logger.info(`Watch depth: ${String(depth)}`);
1667
+ if (process.stdout.isTTY) {
1668
+ logger.info(chalk4.dim("Press Ctrl+C to stop watching."));
1669
+ }
1670
+ console.log();
1671
+ const cleaner = new Cleaner();
1672
+ const watcher = new Watcher({
1673
+ watchPaths: validPaths,
1674
+ delayMs,
1675
+ depth,
1676
+ onDelete: async (events) => {
1677
+ if (events.length === 1) {
1678
+ logger.info(`Detected deletion: ${events[0].path}`);
1679
+ } else {
1680
+ logger.info(`Detected ${events.length} deletions (debounced)`);
1681
+ if (options.verbose) {
1682
+ for (const event of events) {
1683
+ logger.debug(` - ${event.path}`);
1684
+ }
1685
+ }
1686
+ }
1687
+ const results = await runAllScanners(availableScanners);
1688
+ const allSessions = results.flatMap((r) => r.sessions);
1689
+ if (allSessions.length === 0) {
1690
+ if (options.verbose) {
1691
+ logger.debug("No orphaned sessions found after deletion.");
1692
+ }
1693
+ return;
1694
+ }
1695
+ const cleanResult = await cleaner.clean(allSessions, {
1696
+ dryRun: false,
1697
+ useTrash: !options.noTrash
1698
+ });
1699
+ if (cleanResult.deletedCount > 0) {
1700
+ const action = options.noTrash ? "Deleted" : "Moved to trash";
1701
+ const parts = [];
1702
+ const { deletedByType } = cleanResult;
1703
+ if (deletedByType.session > 0) {
1704
+ parts.push(`${deletedByType.session} session`);
1705
+ }
1706
+ if (deletedByType.sessionEnv > 0) {
1707
+ parts.push(`${deletedByType.sessionEnv} session-env`);
1708
+ }
1709
+ if (deletedByType.todos > 0) {
1710
+ parts.push(`${deletedByType.todos} todos`);
1711
+ }
1712
+ if (deletedByType.fileHistory > 0) {
1713
+ parts.push(`${deletedByType.fileHistory} file-history`);
1714
+ }
1715
+ const summary = parts.length > 0 ? parts.join(" + ") : `${cleanResult.deletedCount} item(s)`;
1716
+ logger.success(
1717
+ `${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`
1718
+ );
1719
+ }
1720
+ if (cleanResult.configEntriesRemoved > 0) {
1721
+ logger.success(
1722
+ `Removed ${cleanResult.configEntriesRemoved} config entry(ies) from ~/.claude.json`
1723
+ );
1724
+ }
1725
+ if (cleanResult.errors.length > 0) {
1726
+ logger.error(
1727
+ `Failed to clean ${cleanResult.errors.length} item(s)`
1728
+ );
1729
+ }
1730
+ }
1731
+ });
1732
+ watcher.start();
1733
+ process.on("SIGINT", () => {
1734
+ console.log();
1735
+ logger.info("Stopping watcher...");
1736
+ watcher.stop();
1737
+ process.exit(0);
1738
+ });
1739
+ await new Promise(() => {
1740
+ });
1741
+ }
1742
+ function getDefaultWatchPaths() {
1743
+ const home = homedir5();
1744
+ return [
1745
+ join8(home, "dev"),
1746
+ join8(home, "code"),
1747
+ join8(home, "projects"),
1748
+ join8(home, "Development"),
1749
+ join8(home, "Developer"),
1750
+ // macOS Xcode
1751
+ join8(home, "Documents")
1752
+ ];
1753
+ }
1754
+
1755
+ // src/commands/list.ts
1756
+ import { Command as Command4 } from "commander";
1757
+ import Table2 from "cli-table3";
1758
+ import chalk5 from "chalk";
1759
+ var listCommand = new Command4("list").description("List detected AI coding tools and their data locations").option("-v, --verbose", "Show detailed information").action(async (options) => {
1760
+ const allScanners = createAllScanners();
1761
+ const availableScanners = await getAvailableScanners(allScanners);
1762
+ console.log();
1763
+ console.log(chalk5.bold("AI Coding Tools Status:"));
1764
+ console.log();
1765
+ const table = new Table2({
1766
+ head: [
1767
+ chalk5.cyan("Tool"),
1768
+ chalk5.cyan("Status"),
1769
+ chalk5.cyan("Data Location")
1770
+ ],
1771
+ style: { head: [] }
1772
+ });
1773
+ const toolLocations = {
1774
+ "claude-code": getClaudeProjectsDir(),
1775
+ cursor: getCursorWorkspaceDir()
1776
+ };
1777
+ for (const scanner of allScanners) {
1778
+ const isAvailable = availableScanners.some((s) => s.name === scanner.name);
1779
+ const status = isAvailable ? chalk5.green("Available") : chalk5.dim("Not found");
1780
+ const location = toolLocations[scanner.name] || "Unknown";
1781
+ table.push([scanner.name, status, isAvailable ? location : chalk5.dim(location)]);
1782
+ }
1783
+ console.log(table.toString());
1784
+ if (options.verbose && availableScanners.length > 0) {
1785
+ console.log();
1786
+ console.log(chalk5.bold("Tool Details:"));
1787
+ console.log();
1788
+ for (const scanner of availableScanners) {
1789
+ console.log(chalk5.cyan(`${scanner.name}:`));
1790
+ switch (scanner.name) {
1791
+ case "claude-code":
1792
+ console.log(" Session format: Encoded project path directories");
1793
+ console.log(" Path encoding: /path/to/project \u2192 -path-to-project");
1794
+ break;
1795
+ case "cursor":
1796
+ console.log(" Session format: Hash-named directories with workspace.json");
1797
+ console.log(' Project info: Stored in workspace.json "folder" field');
1798
+ break;
1799
+ }
1800
+ console.log();
1801
+ }
1802
+ }
1803
+ console.log(
1804
+ chalk5.dim(
1805
+ `${availableScanners.length} of ${allScanners.length} tools detected on this system.`
1806
+ )
1807
+ );
1808
+ });
1809
+
1810
+ // src/commands/config.ts
1811
+ import { Command as Command5 } from "commander";
1812
+ import inquirer2 from "inquirer";
1813
+ var configCommand = new Command5("config").description(
1814
+ "Manage configuration"
1815
+ );
1816
+ var pathsCommand = new Command5("paths").description("Manage watch paths");
1817
+ pathsCommand.command("add <path>").description("Add a watch path").action((path) => {
1818
+ addWatchPath(path);
1819
+ logger.success(`Added: ${path}`);
1820
+ });
1821
+ pathsCommand.command("remove <path>").description("Remove a watch path").action((path) => {
1822
+ const removed = removeWatchPath(path);
1823
+ if (removed) {
1824
+ logger.success(`Removed: ${path}`);
1825
+ } else {
1826
+ logger.warn(`Path not found: ${path}`);
1827
+ }
1828
+ });
1829
+ pathsCommand.command("list").description("List watch paths").action(() => {
1830
+ const paths = getWatchPaths();
1831
+ if (!paths || paths.length === 0) {
1832
+ logger.info("No watch paths configured.");
1833
+ return;
1834
+ }
1835
+ console.log();
1836
+ for (const p of paths) {
1837
+ console.log(` ${p}`);
1838
+ }
1839
+ });
1840
+ configCommand.addCommand(pathsCommand);
1841
+ var DEFAULT_DELAY_MINUTES2 = 5;
1842
+ var MAX_DELAY_MINUTES2 = 10;
1843
+ configCommand.command("delay [minutes]").description(`Get or set watch delay in minutes (default: ${DEFAULT_DELAY_MINUTES2}, max: ${MAX_DELAY_MINUTES2})`).action((minutes) => {
1844
+ if (minutes === void 0) {
1845
+ const delay = getWatchDelay() ?? DEFAULT_DELAY_MINUTES2;
1846
+ console.log(`Watch delay: ${String(delay)} minute(s)`);
1847
+ } else {
1848
+ const value = parseInt(minutes, 10);
1849
+ if (isNaN(value) || value < 1) {
1850
+ logger.error("Invalid delay value. Must be a positive number.");
1851
+ return;
1852
+ }
1853
+ if (value > MAX_DELAY_MINUTES2) {
1854
+ logger.warn(`Maximum delay is ${String(MAX_DELAY_MINUTES2)} minutes. Setting to ${String(MAX_DELAY_MINUTES2)}.`);
1855
+ setWatchDelay(MAX_DELAY_MINUTES2);
1856
+ return;
1857
+ }
1858
+ setWatchDelay(value);
1859
+ logger.success(`Watch delay set to ${String(value)} minute(s)`);
1860
+ }
1861
+ });
1862
+ var DEFAULT_DEPTH = 3;
1863
+ var MAX_DEPTH = 5;
1864
+ configCommand.command("depth [level]").description("Get or set watch depth (default: 3, max: 5)").action((level) => {
1865
+ if (level === void 0) {
1866
+ const depth = getWatchDepth() ?? DEFAULT_DEPTH;
1867
+ console.log(`Watch depth: ${String(depth)}`);
1868
+ } else {
1869
+ const value = parseInt(level, 10);
1870
+ if (isNaN(value) || value < 1) {
1871
+ logger.error("Invalid depth value. Must be a positive number.");
1872
+ return;
1873
+ }
1874
+ if (value > MAX_DEPTH) {
1875
+ logger.warn(`Maximum depth is ${String(MAX_DEPTH)}. Setting to ${String(MAX_DEPTH)}.`);
1876
+ }
1877
+ setWatchDepth(value);
1878
+ const actualValue = Math.min(value, MAX_DEPTH);
1879
+ logger.success(`Watch depth set to ${String(actualValue)}`);
1880
+ }
1881
+ });
1882
+ configCommand.command("show").description("Show all configuration").action(() => {
1883
+ const config = loadConfig();
1884
+ console.log(JSON.stringify(config, null, 2));
1885
+ });
1886
+ configCommand.command("reset").description("Reset configuration to defaults").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
1887
+ if (!options.force) {
1888
+ const { confirmed } = await inquirer2.prompt([
1889
+ {
1890
+ type: "confirm",
1891
+ name: "confirmed",
1892
+ message: "Reset all configuration to defaults?",
1893
+ default: false
1894
+ }
1895
+ ]);
1896
+ if (!confirmed) {
1897
+ logger.info("Cancelled.");
1898
+ return;
1899
+ }
1900
+ }
1901
+ resetConfig();
1902
+ logger.success("Configuration reset to defaults.");
1903
+ });
1904
+
1905
+ // src/cli.ts
1906
+ var cli = new Command6().name("ai-session-tidy").description(
1907
+ "CLI tool that detects and cleans orphaned session data from AI coding tools"
1908
+ ).version(getAppVersion());
1909
+ cli.addCommand(scanCommand, { isDefault: true });
1910
+ cli.addCommand(cleanCommand);
1911
+ cli.addCommand(watchCommand);
1912
+ cli.addCommand(listCommand);
1913
+ cli.addCommand(configCommand);
1914
+
1915
+ // src/index.ts
1916
+ cli.parse(process.argv);
1917
+ //# sourceMappingURL=index.js.map