@asifkibria/claude-code-toolkit 1.0.2 → 1.2.0

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.
Files changed (69) hide show
  1. package/README.md +165 -214
  2. package/dist/CLAUDE.md +7 -0
  3. package/dist/__tests__/dashboard.test.d.ts +2 -0
  4. package/dist/__tests__/dashboard.test.d.ts.map +1 -0
  5. package/dist/__tests__/dashboard.test.js +606 -0
  6. package/dist/__tests__/dashboard.test.js.map +1 -0
  7. package/dist/__tests__/mcp-validator.test.d.ts +2 -0
  8. package/dist/__tests__/mcp-validator.test.d.ts.map +1 -0
  9. package/dist/__tests__/mcp-validator.test.js +217 -0
  10. package/dist/__tests__/mcp-validator.test.js.map +1 -0
  11. package/dist/__tests__/scanner.test.js +350 -1
  12. package/dist/__tests__/scanner.test.js.map +1 -1
  13. package/dist/__tests__/security.test.d.ts +2 -0
  14. package/dist/__tests__/security.test.d.ts.map +1 -0
  15. package/dist/__tests__/security.test.js +375 -0
  16. package/dist/__tests__/security.test.js.map +1 -0
  17. package/dist/__tests__/session-recovery.test.d.ts +2 -0
  18. package/dist/__tests__/session-recovery.test.d.ts.map +1 -0
  19. package/dist/__tests__/session-recovery.test.js +230 -0
  20. package/dist/__tests__/session-recovery.test.js.map +1 -0
  21. package/dist/__tests__/storage.test.d.ts +2 -0
  22. package/dist/__tests__/storage.test.d.ts.map +1 -0
  23. package/dist/__tests__/storage.test.js +241 -0
  24. package/dist/__tests__/storage.test.js.map +1 -0
  25. package/dist/__tests__/trace.test.d.ts +2 -0
  26. package/dist/__tests__/trace.test.d.ts.map +1 -0
  27. package/dist/__tests__/trace.test.js +376 -0
  28. package/dist/__tests__/trace.test.js.map +1 -0
  29. package/dist/cli.js +501 -20
  30. package/dist/cli.js.map +1 -1
  31. package/dist/index.js +950 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/dashboard-ui.d.ts +2 -0
  34. package/dist/lib/dashboard-ui.d.ts.map +1 -0
  35. package/dist/lib/dashboard-ui.js +2075 -0
  36. package/dist/lib/dashboard-ui.js.map +1 -0
  37. package/dist/lib/dashboard.d.ts +15 -0
  38. package/dist/lib/dashboard.d.ts.map +1 -0
  39. package/dist/lib/dashboard.js +1422 -0
  40. package/dist/lib/dashboard.js.map +1 -0
  41. package/dist/lib/logs.d.ts +42 -0
  42. package/dist/lib/logs.d.ts.map +1 -0
  43. package/dist/lib/logs.js +166 -0
  44. package/dist/lib/logs.js.map +1 -0
  45. package/dist/lib/mcp-validator.d.ts +86 -0
  46. package/dist/lib/mcp-validator.d.ts.map +1 -0
  47. package/dist/lib/mcp-validator.js +463 -0
  48. package/dist/lib/mcp-validator.js.map +1 -0
  49. package/dist/lib/scanner.d.ts +187 -2
  50. package/dist/lib/scanner.d.ts.map +1 -1
  51. package/dist/lib/scanner.js +1224 -14
  52. package/dist/lib/scanner.js.map +1 -1
  53. package/dist/lib/security.d.ts +57 -0
  54. package/dist/lib/security.d.ts.map +1 -0
  55. package/dist/lib/security.js +423 -0
  56. package/dist/lib/security.js.map +1 -0
  57. package/dist/lib/session-recovery.d.ts +60 -0
  58. package/dist/lib/session-recovery.d.ts.map +1 -0
  59. package/dist/lib/session-recovery.js +433 -0
  60. package/dist/lib/session-recovery.js.map +1 -0
  61. package/dist/lib/storage.d.ts +68 -0
  62. package/dist/lib/storage.d.ts.map +1 -0
  63. package/dist/lib/storage.js +500 -0
  64. package/dist/lib/storage.js.map +1 -0
  65. package/dist/lib/trace.d.ts +119 -0
  66. package/dist/lib/trace.d.ts.map +1 -0
  67. package/dist/lib/trace.js +649 -0
  68. package/dist/lib/trace.js.map +1 -0
  69. package/package.json +11 -3
@@ -0,0 +1,1422 @@
1
+ import * as http from "http";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { execFile } from "child_process";
6
+ import { generateDashboardHTML } from "./dashboard-ui.js";
7
+ import { analyzeClaudeStorage, cleanClaudeDirectory } from "./storage.js";
8
+ import { listSessions, diagnoseSession, repairSession, extractSessionContent } from "./session-recovery.js";
9
+ import { scanForSecrets, auditSession, enforceRetention, generateComplianceReport } from "./security.js";
10
+ import { inventoryTraces, cleanTraces, wipeAllTraces, generateEnhancedPreview } from "./trace.js";
11
+ import { diagnoseMcpServers, probeMcpServer } from "./mcp-validator.js";
12
+ import { listLogFiles, parseAllLogs, getLogSummary } from "./logs.js";
13
+ import { findAllJsonlFiles, findBackupFiles, scanFile, fixFile, getConversationStats, estimateContextSize, generateUsageAnalytics, findDuplicates, findArchiveCandidates, archiveConversations, runMaintenance, deleteOldBackups, restoreFromBackup, exportConversation, } from "./scanner.js";
14
+ import { saveStorageSnapshot, listStorageSnapshots, loadStorageSnapshot, compareStorageSnapshots, deleteStorageSnapshot, // Missing export in storage.ts? I added it.
15
+ } from "./storage.js";
16
+ const CLAUDE_DIR = path.join(os.homedir(), ".claude");
17
+ const PROJECTS_DIR = path.join(CLAUDE_DIR, "projects");
18
+ const PID_FILE = path.join(CLAUDE_DIR, "dashboard.pid");
19
+ const SECRET_PATTERNS = [
20
+ { name: "AWS Access Key ID", type: "aws_key", regex: /AKIA[0-9A-Z]{16}/g, severity: "critical" },
21
+ { name: "AWS Secret Access Key", type: "aws_secret", regex: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)['"=:\s]+([A-Za-z0-9/+=]{40})/gi, severity: "critical" },
22
+ { name: "API Token (ghp_, xoxb-, sk-)", type: "api_token", regex: /(?:ghp_[A-Za-z0-9]{36,}|xoxb-[A-Za-z0-9-]+|xoxp-[A-Za-z0-9-]+)/g, severity: "high" },
23
+ { name: "sk- API Key", type: "api_key", regex: /sk-[A-Za-z0-9]{20,}/g, severity: "high" },
24
+ { name: "Private Key", type: "private_key", regex: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g, severity: "critical" },
25
+ { name: "Connection String", type: "connection_string", regex: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/g, severity: "high" },
26
+ { name: "JWT Token", type: "jwt", regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, severity: "medium" },
27
+ { name: "Password in Config", type: "password", regex: /(?:password|passwd|pwd)\s*[=:]\s*["'][^"']{4,}["']/gi, severity: "high" },
28
+ { name: "Slack Token", type: "slack_token", regex: /xox[bpras]-[A-Za-z0-9-]+/g, severity: "high" },
29
+ { name: "Generic Secret Assignment", type: "generic_secret", regex: /(?:secret|api_key|apikey|access_token)\s*[=:]\s*["'][A-Za-z0-9+/=]{16,}["']/gi, severity: "medium" },
30
+ ];
31
+ function parseUrl(url) {
32
+ const [pathname, query] = (url || "/").split("?");
33
+ const params = {};
34
+ if (query) {
35
+ for (const pair of query.split("&")) {
36
+ const [k, v] = pair.split("=");
37
+ if (k)
38
+ params[decodeURIComponent(k)] = decodeURIComponent(v || "");
39
+ }
40
+ }
41
+ return { pathname, params };
42
+ }
43
+ function matchRoute(pathname, pattern) {
44
+ const pathParts = pathname.split("/").filter(Boolean);
45
+ const patternParts = pattern.split("/").filter(Boolean);
46
+ if (pathParts.length !== patternParts.length)
47
+ return null;
48
+ const params = {};
49
+ for (let i = 0; i < patternParts.length; i++) {
50
+ if (patternParts[i].startsWith(":")) {
51
+ params[patternParts[i].slice(1)] = pathParts[i];
52
+ }
53
+ else if (patternParts[i] !== pathParts[i]) {
54
+ return null;
55
+ }
56
+ }
57
+ return params;
58
+ }
59
+ function readBody(req) {
60
+ return new Promise((resolve) => {
61
+ let data = "";
62
+ req.on("data", (chunk) => { data += chunk.toString(); });
63
+ req.on("end", () => {
64
+ try {
65
+ resolve(JSON.parse(data));
66
+ }
67
+ catch {
68
+ resolve({});
69
+ }
70
+ });
71
+ });
72
+ }
73
+ // ===== GET handlers =====
74
+ function getOverview() {
75
+ const files = findAllJsonlFiles(PROJECTS_DIR);
76
+ let issueCount = 0;
77
+ let totalIssueSize = 0;
78
+ for (const file of files) {
79
+ try {
80
+ const result = scanFile(file);
81
+ issueCount += result.issues.length;
82
+ for (const issue of result.issues) {
83
+ totalIssueSize += issue.estimatedSize;
84
+ }
85
+ }
86
+ catch { /* skip */ }
87
+ }
88
+ const backups = findBackupFiles(PROJECTS_DIR);
89
+ let backupSize = 0;
90
+ for (const b of backups) {
91
+ try {
92
+ backupSize += fs.statSync(b).size;
93
+ }
94
+ catch { /* skip */ }
95
+ }
96
+ const candidates = findArchiveCandidates(PROJECTS_DIR, { minDaysInactive: 30 });
97
+ const maintenance = runMaintenance(PROJECTS_DIR, { dryRun: true });
98
+ const storage = analyzeClaudeStorage();
99
+ const sessions = listSessions();
100
+ const healthySessions = sessions.filter(s => s.status === "healthy").length;
101
+ const uniqueProjects = new Set(sessions.map(s => s.project)).size;
102
+ return {
103
+ totalConversations: files.length,
104
+ issueCount,
105
+ totalIssueSize,
106
+ backupCount: backups.length,
107
+ backupSize,
108
+ archiveCandidates: candidates.length,
109
+ maintenanceActions: maintenance.actions.length,
110
+ maintenanceStatus: maintenance.status,
111
+ systemInfo: {
112
+ claudeDir: CLAUDE_DIR,
113
+ projectsDir: PROJECTS_DIR,
114
+ totalStorage: storage.totalSize,
115
+ totalSessions: sessions.length,
116
+ healthySessions,
117
+ uniqueProjects,
118
+ platform: os.platform(),
119
+ nodeVersion: process.version,
120
+ },
121
+ };
122
+ }
123
+ function getStorage() {
124
+ const analysis = analyzeClaudeStorage();
125
+ return {
126
+ totalSize: analysis.totalSize,
127
+ categories: analysis.categories.map(c => ({
128
+ name: c.name,
129
+ totalSize: c.totalSize,
130
+ fileCount: c.fileCount,
131
+ cleanableSize: c.cleanableSize,
132
+ })),
133
+ largestFiles: analysis.largestFiles,
134
+ recommendations: analysis.recommendations,
135
+ };
136
+ }
137
+ function getSessions() {
138
+ const sessions = listSessions();
139
+ return sessions.map(s => ({
140
+ id: s.id,
141
+ project: s.project,
142
+ projectPath: s.projectPath,
143
+ messageCount: s.messageCount,
144
+ sizeBytes: s.sizeBytes,
145
+ status: s.status,
146
+ created: s.created,
147
+ modified: s.modified,
148
+ subagentCount: s.subagentCount,
149
+ filePath: s.filePath,
150
+ }));
151
+ }
152
+ function getSessionDetail(sessionId) {
153
+ const sessions = listSessions();
154
+ const match = sessions.find(s => s.id === sessionId || s.id.startsWith(sessionId));
155
+ if (!match)
156
+ return null;
157
+ const diag = diagnoseSession(match.filePath);
158
+ return {
159
+ ...diag,
160
+ sessionId: match.id,
161
+ project: match.project,
162
+ status: match.status,
163
+ sizeBytes: match.sizeBytes,
164
+ messageCount: match.messageCount,
165
+ filePath: match.filePath,
166
+ };
167
+ }
168
+ function getSessionAudit(sessionId) {
169
+ const sessions = listSessions();
170
+ const match = sessions.find(s => s.id === sessionId || s.id.startsWith(sessionId));
171
+ if (!match)
172
+ return null;
173
+ const audit = auditSession(match.filePath);
174
+ return {
175
+ sessionId: match.id,
176
+ project: match.project,
177
+ filesRead: audit.filesRead,
178
+ filesWritten: audit.filesWritten,
179
+ commandsRun: audit.commandsRun,
180
+ mcpToolsUsed: audit.mcpToolsUsed,
181
+ urlsFetched: audit.urlsFetched,
182
+ totalActions: audit.actions.length,
183
+ };
184
+ }
185
+ function getSecurity() {
186
+ const result = scanForSecrets();
187
+ return {
188
+ filesScanned: result.filesScanned,
189
+ totalFindings: result.totalFindings,
190
+ findings: result.findings.slice(0, 100),
191
+ summary: result.summary,
192
+ };
193
+ }
194
+ function getSecurityFindingPreview(filePath, lineNum) {
195
+ try {
196
+ const fullPath = path.join(PROJECTS_DIR, filePath);
197
+ const target = fs.existsSync(fullPath) ? fullPath : filePath;
198
+ if (!fs.existsSync(target))
199
+ return null;
200
+ const content = fs.readFileSync(target, "utf-8");
201
+ const lines = content.split("\n");
202
+ if (lineNum < 1 || lineNum > lines.length)
203
+ return null;
204
+ const line = lines[lineNum - 1];
205
+ let parsed;
206
+ try {
207
+ parsed = JSON.parse(line);
208
+ }
209
+ catch {
210
+ return { lineNum, raw: line.slice(0, 500) };
211
+ }
212
+ let text = "";
213
+ const extractText = (obj) => {
214
+ if (typeof obj === "string") {
215
+ text += obj + "\n";
216
+ return;
217
+ }
218
+ if (Array.isArray(obj)) {
219
+ obj.forEach(extractText);
220
+ return;
221
+ }
222
+ if (obj && typeof obj === "object") {
223
+ Object.values(obj).forEach(extractText);
224
+ }
225
+ };
226
+ extractText(parsed);
227
+ const preview = text.slice(0, 2000);
228
+ for (const pat of SECRET_PATTERNS) {
229
+ pat.regex.lastIndex = 0;
230
+ }
231
+ const masked = SECRET_PATTERNS.reduce((t, p) => {
232
+ p.regex.lastIndex = 0;
233
+ return t.replace(p.regex, (m) => m.slice(0, 4) + "****" + m.slice(-4));
234
+ }, preview);
235
+ return { lineNum, preview: masked, fileSize: content.length, totalLines: lines.length };
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ function getCompliance() {
242
+ const report = generateComplianceReport();
243
+ const files = findAllJsonlFiles(PROJECTS_DIR);
244
+ let totalSessionSize = 0;
245
+ let newestSession = null;
246
+ const ageBrackets = { week: 0, month: 0, quarter: 0, older: 0 };
247
+ const now = Date.now();
248
+ for (const f of files) {
249
+ try {
250
+ const stat = fs.statSync(f);
251
+ totalSessionSize += stat.size;
252
+ if (!newestSession || stat.mtime > newestSession)
253
+ newestSession = stat.mtime;
254
+ const days = (now - stat.mtime.getTime()) / (24 * 60 * 60 * 1000);
255
+ if (days <= 7)
256
+ ageBrackets.week++;
257
+ else if (days <= 30)
258
+ ageBrackets.month++;
259
+ else if (days <= 90)
260
+ ageBrackets.quarter++;
261
+ else
262
+ ageBrackets.older++;
263
+ }
264
+ catch { /* skip */ }
265
+ }
266
+ const oldestDays = report.oldestSession
267
+ ? Math.round((now - report.oldestSession.getTime()) / (24 * 60 * 60 * 1000))
268
+ : 0;
269
+ const newestDays = newestSession
270
+ ? Math.round((now - newestSession.getTime()) / (24 * 60 * 60 * 1000))
271
+ : 0;
272
+ return {
273
+ secretsScan: {
274
+ filesScanned: report.secretsScan.filesScanned,
275
+ totalFindings: report.secretsScan.totalFindings,
276
+ summary: report.secretsScan.summary,
277
+ },
278
+ sessionCount: report.sessionCount,
279
+ oldestSession: report.oldestSession,
280
+ oldestDays,
281
+ newestDays,
282
+ totalSessionSize,
283
+ retentionStatus: report.retentionStatus,
284
+ ageBrackets,
285
+ generatedAt: report.generatedAt,
286
+ };
287
+ }
288
+ function getTraces() {
289
+ const inv = inventoryTraces();
290
+ return {
291
+ totalSize: inv.totalSize,
292
+ totalFiles: inv.totalFiles,
293
+ criticalItems: inv.criticalItems,
294
+ highItems: inv.highItems,
295
+ analyzedAt: inv.analyzedAt,
296
+ categories: inv.categories.map(c => ({
297
+ name: c.name,
298
+ sensitivity: c.sensitivity,
299
+ fileCount: c.fileCount,
300
+ totalSize: c.totalSize,
301
+ description: c.description,
302
+ oldestFile: c.oldestFile,
303
+ newestFile: c.newestFile,
304
+ sampleFiles: c.items?.slice(0, 5).map(item => ({
305
+ path: path.basename(item.path),
306
+ fullPath: item.path,
307
+ size: item.size,
308
+ modified: item.modified,
309
+ projectName: extractProjectName(item.path),
310
+ })) || [],
311
+ allFiles: c.items?.map(item => ({
312
+ path: path.basename(item.path),
313
+ fullPath: item.path,
314
+ size: item.size,
315
+ modified: item.modified,
316
+ projectName: extractProjectName(item.path),
317
+ })) || [],
318
+ })),
319
+ };
320
+ }
321
+ async function getMcp() {
322
+ const report = await diagnoseMcpServers();
323
+ return {
324
+ configs: report.configs.map(c => ({
325
+ configPath: c.configPath,
326
+ servers: c.servers.map(s => ({ name: s.name, command: s.command, type: s.type, args: s.args, env: s.env })),
327
+ issues: c.issues,
328
+ valid: c.valid,
329
+ })),
330
+ totalServers: report.totalServers,
331
+ healthyServers: report.healthyServers,
332
+ duplicateServers: report.duplicateServers,
333
+ recommendations: report.recommendations,
334
+ };
335
+ }
336
+ async function getMcpServerCapabilities(serverName) {
337
+ const report = await diagnoseMcpServers();
338
+ const allServers = report.configs.flatMap(c => c.servers);
339
+ const server = allServers.find(s => s.name === serverName);
340
+ if (!server)
341
+ return null;
342
+ const capabilities = await probeMcpServer(server);
343
+ return {
344
+ serverName: server.name,
345
+ command: server.command,
346
+ args: server.args,
347
+ ...capabilities,
348
+ };
349
+ }
350
+ function getLogs(params) {
351
+ const options = {
352
+ limit: params.limit ? parseInt(params.limit, 10) : 100,
353
+ };
354
+ if (params.search)
355
+ options.search = params.search;
356
+ if (params.level) {
357
+ const levels = params.level.split(",").map(l => l.trim().toUpperCase());
358
+ options.level = levels;
359
+ }
360
+ if (params.component)
361
+ options.component = params.component;
362
+ if (params.startDate)
363
+ options.startDate = new Date(params.startDate);
364
+ if (params.endDate)
365
+ options.endDate = new Date(params.endDate);
366
+ const entries = parseAllLogs(options);
367
+ const summary = getLogSummary();
368
+ const files = listLogFiles();
369
+ const sessions = listSessions();
370
+ const sessionToProject = new Map();
371
+ for (const s of sessions) {
372
+ sessionToProject.set(s.id, { project: extractProjectName(s.filePath), projectPath: s.projectPath || s.project });
373
+ }
374
+ return {
375
+ entries: entries.map(e => ({
376
+ timestamp: e.timestamp,
377
+ level: e.level,
378
+ component: e.component,
379
+ message: e.message,
380
+ file: path.basename(e.file),
381
+ line: e.line,
382
+ })),
383
+ summary: {
384
+ totalFiles: summary.totalFiles,
385
+ totalSize: summary.totalSize,
386
+ oldestLog: summary.oldestLog,
387
+ newestLog: summary.newestLog,
388
+ levelCounts: summary.levelCounts,
389
+ topComponents: Object.entries(summary.componentCounts)
390
+ .sort((a, b) => b[1] - a[1])
391
+ .slice(0, 10)
392
+ .map(([name, count]) => ({ name, count })),
393
+ },
394
+ files: files.slice(0, 10).map(f => {
395
+ const sessionId = f.name.replace(/\.txt$/, "");
396
+ const projectInfo = sessionToProject.get(sessionId);
397
+ return {
398
+ name: f.name,
399
+ size: f.size,
400
+ modified: f.modified,
401
+ isLatest: f.isLatest,
402
+ sessionId,
403
+ projectName: projectInfo?.project || null,
404
+ projectPath: projectInfo?.projectPath || null,
405
+ };
406
+ }),
407
+ };
408
+ }
409
+ function getConfig() {
410
+ const configs = {};
411
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
412
+ if (fs.existsSync(settingsPath)) {
413
+ try {
414
+ const content = fs.readFileSync(settingsPath, "utf-8");
415
+ configs.settings = {
416
+ path: settingsPath,
417
+ content,
418
+ exists: true,
419
+ size: fs.statSync(settingsPath).size,
420
+ modified: fs.statSync(settingsPath).mtime,
421
+ };
422
+ }
423
+ catch (e) {
424
+ configs.settings = { path: settingsPath, exists: false, error: e instanceof Error ? e.message : String(e) };
425
+ }
426
+ }
427
+ else {
428
+ configs.settings = { path: settingsPath, exists: false };
429
+ }
430
+ const globalClaudeMd = path.join(CLAUDE_DIR, "CLAUDE.md");
431
+ if (fs.existsSync(globalClaudeMd)) {
432
+ try {
433
+ const content = fs.readFileSync(globalClaudeMd, "utf-8");
434
+ configs.globalClaudeMd = {
435
+ path: globalClaudeMd,
436
+ content,
437
+ exists: true,
438
+ size: fs.statSync(globalClaudeMd).size,
439
+ modified: fs.statSync(globalClaudeMd).mtime,
440
+ };
441
+ }
442
+ catch (e) {
443
+ configs.globalClaudeMd = { path: globalClaudeMd, exists: false, error: e instanceof Error ? e.message : String(e) };
444
+ }
445
+ }
446
+ else {
447
+ configs.globalClaudeMd = { path: globalClaudeMd, exists: false };
448
+ }
449
+ const globalMcpConfig = path.join(os.homedir(), ".claude.json");
450
+ if (fs.existsSync(globalMcpConfig)) {
451
+ try {
452
+ const content = fs.readFileSync(globalMcpConfig, "utf-8");
453
+ configs.globalMcp = {
454
+ path: globalMcpConfig,
455
+ content,
456
+ exists: true,
457
+ size: fs.statSync(globalMcpConfig).size,
458
+ modified: fs.statSync(globalMcpConfig).mtime,
459
+ };
460
+ }
461
+ catch (e) {
462
+ configs.globalMcp = { path: globalMcpConfig, exists: false, error: e instanceof Error ? e.message : String(e) };
463
+ }
464
+ }
465
+ else {
466
+ configs.globalMcp = { path: globalMcpConfig, exists: false };
467
+ }
468
+ const projectMcp = path.join(process.cwd(), ".mcp.json");
469
+ if (fs.existsSync(projectMcp)) {
470
+ try {
471
+ const content = fs.readFileSync(projectMcp, "utf-8");
472
+ configs.projectMcp = {
473
+ path: projectMcp,
474
+ content,
475
+ exists: true,
476
+ size: fs.statSync(projectMcp).size,
477
+ modified: fs.statSync(projectMcp).mtime,
478
+ };
479
+ }
480
+ catch (e) {
481
+ configs.projectMcp = { path: projectMcp, exists: false, error: e instanceof Error ? e.message : String(e) };
482
+ }
483
+ }
484
+ else {
485
+ configs.projectMcp = { path: projectMcp, exists: false };
486
+ }
487
+ const projectClaudeMd = path.join(process.cwd(), "CLAUDE.md");
488
+ if (fs.existsSync(projectClaudeMd)) {
489
+ try {
490
+ const content = fs.readFileSync(projectClaudeMd, "utf-8");
491
+ configs.projectClaudeMd = {
492
+ path: projectClaudeMd,
493
+ content,
494
+ exists: true,
495
+ size: fs.statSync(projectClaudeMd).size,
496
+ modified: fs.statSync(projectClaudeMd).mtime,
497
+ };
498
+ }
499
+ catch (e) {
500
+ configs.projectClaudeMd = { path: projectClaudeMd, exists: false, error: e instanceof Error ? e.message : String(e) };
501
+ }
502
+ }
503
+ else {
504
+ configs.projectClaudeMd = { path: projectClaudeMd, exists: false };
505
+ }
506
+ return configs;
507
+ }
508
+ function actionSaveConfig(body) {
509
+ const configType = body.type;
510
+ const content = body.content;
511
+ if (!configType || content === undefined) {
512
+ return { success: false, error: "type and content are required" };
513
+ }
514
+ let configPath;
515
+ switch (configType) {
516
+ case "settings":
517
+ configPath = path.join(CLAUDE_DIR, "settings.json");
518
+ break;
519
+ case "globalClaudeMd":
520
+ configPath = path.join(CLAUDE_DIR, "CLAUDE.md");
521
+ break;
522
+ case "globalMcp":
523
+ configPath = path.join(os.homedir(), ".claude.json");
524
+ break;
525
+ case "projectMcp":
526
+ configPath = path.join(process.cwd(), ".mcp.json");
527
+ break;
528
+ case "projectClaudeMd":
529
+ configPath = path.join(process.cwd(), "CLAUDE.md");
530
+ break;
531
+ default:
532
+ return { success: false, error: "Invalid config type" };
533
+ }
534
+ if (configType === "settings" || configType === "globalMcp" || configType === "projectMcp") {
535
+ try {
536
+ JSON.parse(content);
537
+ }
538
+ catch (e) {
539
+ return { success: false, error: "Invalid JSON: " + (e instanceof Error ? e.message : String(e)) };
540
+ }
541
+ }
542
+ try {
543
+ const dir = path.dirname(configPath);
544
+ if (!fs.existsSync(dir)) {
545
+ fs.mkdirSync(dir, { recursive: true });
546
+ }
547
+ if (fs.existsSync(configPath)) {
548
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
549
+ const backupPath = configPath + ".backup." + timestamp;
550
+ fs.copyFileSync(configPath, backupPath);
551
+ }
552
+ fs.writeFileSync(configPath, content, "utf-8");
553
+ return { success: true, path: configPath };
554
+ }
555
+ catch (e) {
556
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
557
+ }
558
+ }
559
+ function getAnalytics() {
560
+ const analytics = generateUsageAnalytics(PROJECTS_DIR, 30);
561
+ const files = findAllJsonlFiles(PROJECTS_DIR);
562
+ let totalTokens = 0;
563
+ let tokenWarnings = 0;
564
+ for (const f of files.slice(0, 50)) {
565
+ try {
566
+ const est = estimateContextSize(f);
567
+ totalTokens += est.totalTokens;
568
+ if (est.totalTokens > 100000)
569
+ tokenWarnings++;
570
+ }
571
+ catch { /* skip */ }
572
+ }
573
+ return {
574
+ totalSessions: analytics.overview.totalConversations,
575
+ totalMessages: analytics.overview.totalMessages,
576
+ totalTokens: analytics.overview.totalTokens,
577
+ totalSize: analytics.overview.totalSize,
578
+ activeDays: analytics.dailyActivity.length,
579
+ topProjects: analytics.topProjects.map(p => ({
580
+ name: p.project,
581
+ sessions: p.conversations,
582
+ messages: p.messages,
583
+ size: p.tokens,
584
+ })),
585
+ toolUsage: Object.fromEntries(analytics.toolUsage.map(t => [t.name, t.count])),
586
+ dailyActivity: analytics.dailyActivity,
587
+ mediaStats: analytics.mediaStats,
588
+ contextTokens: totalTokens,
589
+ tokenWarnings,
590
+ avgTokensPerSession: files.length > 0 ? Math.round(totalTokens / Math.min(files.length, 50)) : 0,
591
+ };
592
+ }
593
+ function getDuplicates() {
594
+ const report = findDuplicates(PROJECTS_DIR);
595
+ const allGroups = [...report.conversationDuplicates, ...report.contentDuplicates];
596
+ return {
597
+ totalDuplicates: report.totalDuplicateGroups,
598
+ totalWastedSize: report.totalWastedSize,
599
+ groups: allGroups.slice(0, 20).map((g) => ({
600
+ type: g.type,
601
+ hash: g.hash?.slice(0, 8),
602
+ count: g.locations.length,
603
+ wastedSize: g.wastedSize,
604
+ locations: g.locations.map((l) => ({
605
+ file: l.file.split("/").pop(),
606
+ path: l.file,
607
+ })),
608
+ })),
609
+ };
610
+ }
611
+ function extractProjectName(filePath) {
612
+ const rel = path.relative(PROJECTS_DIR, filePath);
613
+ const parts = rel.split(path.sep);
614
+ if (parts.length > 0) {
615
+ const projectFolder = parts[0];
616
+ const projectPath = projectFolder.replace(/^-/, "/").replace(/-/g, "/");
617
+ const pathParts = projectPath.split("/").filter(Boolean);
618
+ if (pathParts.length >= 2) {
619
+ return pathParts.slice(-2).join("/");
620
+ }
621
+ return pathParts[pathParts.length - 1] || projectFolder;
622
+ }
623
+ return path.basename(filePath);
624
+ }
625
+ function extractFirstPrompt(filePath) {
626
+ try {
627
+ const content = fs.readFileSync(filePath, "utf-8");
628
+ const lines = content.split("\n");
629
+ for (const line of lines.slice(0, 20)) {
630
+ if (!line.trim())
631
+ continue;
632
+ try {
633
+ const data = JSON.parse(line);
634
+ if (data.type === "user" || data.message?.role === "user") {
635
+ const msgContent = data.message?.content || data.content;
636
+ if (typeof msgContent === "string") {
637
+ return msgContent.slice(0, 100) + (msgContent.length > 100 ? "..." : "");
638
+ }
639
+ if (Array.isArray(msgContent)) {
640
+ for (const part of msgContent) {
641
+ if (part.type === "text" && part.text) {
642
+ return part.text.slice(0, 100) + (part.text.length > 100 ? "..." : "");
643
+ }
644
+ }
645
+ }
646
+ }
647
+ }
648
+ catch { /* skip */ }
649
+ }
650
+ }
651
+ catch { /* skip */ }
652
+ return null;
653
+ }
654
+ function getContext() {
655
+ const files = findAllJsonlFiles(PROJECTS_DIR);
656
+ const estimates = [];
657
+ let totalTokens = 0;
658
+ let warnings = 0;
659
+ for (const f of files) {
660
+ try {
661
+ const est = estimateContextSize(f);
662
+ totalTokens += est.totalTokens;
663
+ if (est.warnings.length > 0)
664
+ warnings++;
665
+ const projectName = extractProjectName(f);
666
+ const firstPrompt = extractFirstPrompt(f);
667
+ const sessionId = path.basename(f, ".jsonl");
668
+ estimates.push({
669
+ file: path.relative(PROJECTS_DIR, f),
670
+ fullPath: f,
671
+ sessionId,
672
+ projectName,
673
+ firstPrompt,
674
+ tokens: est.totalTokens,
675
+ messages: est.messageCount,
676
+ images: est.breakdown.imageTokens,
677
+ documents: est.breakdown.documentTokens,
678
+ tools: est.breakdown.toolResultTokens,
679
+ warnings: est.warnings,
680
+ });
681
+ }
682
+ catch { /* skip */ }
683
+ }
684
+ estimates.sort((a, b) => b.tokens - a.tokens);
685
+ return { totalTokens, totalFiles: files.length, warnings, estimates: estimates.slice(0, 50) };
686
+ }
687
+ function getBackups() {
688
+ const backups = findBackupFiles(PROJECTS_DIR);
689
+ let totalSize = 0;
690
+ const items = backups.map(b => {
691
+ try {
692
+ const stat = fs.statSync(b);
693
+ totalSize += stat.size;
694
+ return {
695
+ path: b,
696
+ file: path.basename(b),
697
+ dir: path.relative(PROJECTS_DIR, path.dirname(b)),
698
+ size: stat.size,
699
+ created: stat.mtime,
700
+ };
701
+ }
702
+ catch {
703
+ return { path: b, file: path.basename(b), dir: "", size: 0, created: new Date(0) };
704
+ }
705
+ });
706
+ items.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
707
+ return { totalBackups: backups.length, totalSize, backups: items.slice(0, 100) };
708
+ }
709
+ function getStats() {
710
+ const files = findAllJsonlFiles(PROJECTS_DIR);
711
+ const stats = [];
712
+ let totalMessages = 0;
713
+ let totalImages = 0;
714
+ let totalSize = 0;
715
+ for (const f of files) {
716
+ try {
717
+ const s = getConversationStats(f);
718
+ totalMessages += s.totalMessages;
719
+ totalImages += s.imageCount;
720
+ totalSize += s.fileSizeBytes;
721
+ const projectName = extractProjectName(f);
722
+ const firstPrompt = extractFirstPrompt(f);
723
+ const sessionId = path.basename(f, ".jsonl");
724
+ stats.push({
725
+ file: path.relative(PROJECTS_DIR, f),
726
+ fullPath: f,
727
+ projectName,
728
+ firstPrompt,
729
+ sessionId,
730
+ messages: s.totalMessages,
731
+ images: s.imageCount,
732
+ documents: s.documentCount,
733
+ problematic: s.problematicContent,
734
+ size: s.fileSizeBytes,
735
+ modified: s.lastModified,
736
+ });
737
+ }
738
+ catch { /* skip */ }
739
+ }
740
+ stats.sort((a, b) => b.size - a.size);
741
+ return { totalFiles: files.length, totalMessages, totalImages, totalSize, stats: stats.slice(0, 50) };
742
+ }
743
+ function getArchiveCandidates() {
744
+ const candidates = findArchiveCandidates(PROJECTS_DIR, { minDaysInactive: 30 });
745
+ let totalSize = 0;
746
+ for (const c of candidates)
747
+ totalSize += c.sizeBytes;
748
+ return {
749
+ totalCandidates: candidates.length,
750
+ totalSize,
751
+ candidates: candidates.slice(0, 50).map(c => ({
752
+ file: path.relative(PROJECTS_DIR, c.file),
753
+ fullPath: c.file,
754
+ projectName: extractProjectName(c.file),
755
+ firstPrompt: extractFirstPrompt(c.file),
756
+ size: c.sizeBytes,
757
+ lastModified: c.lastModified,
758
+ daysInactive: c.daysSinceActivity,
759
+ messageCount: c.messageCount,
760
+ })),
761
+ };
762
+ }
763
+ function getMaintenanceCheck() {
764
+ const report = runMaintenance(PROJECTS_DIR, { dryRun: true });
765
+ return {
766
+ status: report.status,
767
+ actions: report.actions.map(a => ({
768
+ type: a.type,
769
+ description: a.description,
770
+ sizeBytes: a.sizeBytes,
771
+ count: a.count,
772
+ })),
773
+ totalActions: report.actions.length,
774
+ estimatedSpace: report.actions.reduce((s, a) => s + (a.sizeBytes || 0), 0),
775
+ };
776
+ }
777
+ function getScan() {
778
+ const config = getConfig();
779
+ let options = {};
780
+ try {
781
+ const settings = config.settings;
782
+ if (settings && settings.exists && typeof settings.content === 'string') {
783
+ const parsed = JSON.parse(settings.content);
784
+ if (parsed.scanner)
785
+ options = parsed.scanner;
786
+ }
787
+ }
788
+ catch { }
789
+ const files = findAllJsonlFiles(PROJECTS_DIR);
790
+ const results = [];
791
+ let totalIssues = 0;
792
+ for (const f of files) {
793
+ try {
794
+ const r = scanFile(f, options);
795
+ if (r.issues.length > 0) {
796
+ totalIssues += r.issues.length;
797
+ results.push({
798
+ file: path.relative(PROJECTS_DIR, f),
799
+ fullPath: f,
800
+ issues: r.issues.map(i => ({
801
+ line: i.line,
802
+ type: i.contentType,
803
+ size: i.estimatedSize,
804
+ })),
805
+ });
806
+ }
807
+ }
808
+ catch { /* skip */ }
809
+ }
810
+ return { totalFiles: files.length, totalIssues, filesWithIssues: results.length, results };
811
+ }
812
+ function getSearch(params) {
813
+ const query = params.q || params.query;
814
+ if (!query || query.length < 2)
815
+ return { results: [], count: 0, error: "Query too short" };
816
+ const files = findAllJsonlFiles(PROJECTS_DIR);
817
+ const results = [];
818
+ let count = 0;
819
+ const maxResults = 50;
820
+ const lowerQuery = query.toLowerCase();
821
+ for (const file of files) {
822
+ if (count >= maxResults)
823
+ break;
824
+ try {
825
+ const content = fs.readFileSync(file, 'utf-8');
826
+ const lines = content.split('\n');
827
+ for (let i = 0; i < lines.length; i++) {
828
+ const line = lines[i];
829
+ if (line.toLowerCase().includes(lowerQuery)) {
830
+ let preview = line;
831
+ try {
832
+ const data = JSON.parse(line);
833
+ // Try to extract readable text
834
+ if (data.message?.content) {
835
+ if (typeof data.message.content === 'string')
836
+ preview = data.message.content;
837
+ else if (Array.isArray(data.message.content))
838
+ preview = data.message.content.map((c) => c.text || '').join(' ');
839
+ }
840
+ else if (data.type === 'message' && data.content) {
841
+ preview = typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
842
+ }
843
+ }
844
+ catch { }
845
+ if (preview.length > 200)
846
+ preview = preview.slice(0, 200) + '...';
847
+ results.push({
848
+ file: path.relative(PROJECTS_DIR, file),
849
+ line: i + 1,
850
+ preview,
851
+ match: true
852
+ });
853
+ count++;
854
+ if (count >= maxResults)
855
+ break;
856
+ }
857
+ }
858
+ }
859
+ catch { }
860
+ }
861
+ return { results, count, query };
862
+ }
863
+ // ===== POST action handlers =====
864
+ function actionClean(body) {
865
+ const dryRun = body.dryRun !== false;
866
+ const days = body.days || 7;
867
+ const category = body.category;
868
+ const result = cleanClaudeDirectory(undefined, {
869
+ dryRun,
870
+ days,
871
+ categories: category ? [category] : undefined,
872
+ });
873
+ return { success: true, deleted: result.deleted.length, freed: result.freed, dryRun, errors: result.errors, items: result.deleted.slice(0, 50) };
874
+ }
875
+ function actionFix(body) {
876
+ const filePath = body.file;
877
+ if (!filePath)
878
+ return { success: false, error: "file path required" };
879
+ const result = fixFile(filePath);
880
+ return { success: result.fixed, issues: result.issues.length, backupPath: result.backupPath, error: result.error };
881
+ }
882
+ function actionFixAll() {
883
+ const files = findAllJsonlFiles(PROJECTS_DIR);
884
+ let fixed = 0;
885
+ let errors = 0;
886
+ const fixedFiles = [];
887
+ const errorFiles = [];
888
+ for (const file of files) {
889
+ try {
890
+ const scan = scanFile(file);
891
+ if (scan.issues.length > 0) {
892
+ const result = fixFile(file);
893
+ if (result.fixed) {
894
+ fixed++;
895
+ fixedFiles.push(file);
896
+ }
897
+ else {
898
+ errors++;
899
+ errorFiles.push(file);
900
+ }
901
+ }
902
+ }
903
+ catch {
904
+ errors++;
905
+ errorFiles.push(file);
906
+ }
907
+ }
908
+ return { success: true, fixed, errors, total: files.length, fixedFiles: fixedFiles.slice(0, 50), errorFiles: errorFiles.slice(0, 10) };
909
+ }
910
+ function actionRepair(body) {
911
+ const sessionId = body.sessionId;
912
+ if (!sessionId)
913
+ return { success: false, error: "sessionId required" };
914
+ const sessions = listSessions();
915
+ const match = sessions.find(s => s.id === sessionId || s.id.startsWith(sessionId));
916
+ if (!match)
917
+ return { success: false, error: "Session not found" };
918
+ const result = repairSession(match.filePath);
919
+ return { success: result.success, linesRemoved: result.linesRemoved, linesFixed: result.linesFixed, backupPath: result.backupPath, error: result.error };
920
+ }
921
+ function actionExtract(body) {
922
+ const sessionId = body.sessionId;
923
+ if (!sessionId)
924
+ return { success: false, error: "sessionId required" };
925
+ const sessions = listSessions();
926
+ const match = sessions.find(s => s.id === sessionId || s.id.startsWith(sessionId));
927
+ if (!match)
928
+ return { success: false, error: "Session not found" };
929
+ const content = extractSessionContent(match.filePath);
930
+ return {
931
+ success: true,
932
+ userMessages: content.userMessages.length,
933
+ assistantMessages: content.assistantMessages.length,
934
+ fileEdits: content.fileEdits.length,
935
+ commandsRun: content.commandsRun.length,
936
+ sampleMessages: content.userMessages.slice(0, 5).map(m => m.length > 200 ? m.slice(0, 200) + "..." : m),
937
+ sampleCommands: content.commandsRun.slice(0, 10),
938
+ editedFiles: content.fileEdits.slice(0, 20).map(e => e.path),
939
+ };
940
+ }
941
+ function actionRetention(body) {
942
+ const dryRun = body.dryRun !== false;
943
+ const days = body.days || 30;
944
+ const result = enforceRetention(undefined, { days, dryRun });
945
+ return { success: true, sessionsDeleted: result.sessionsDeleted, spaceFreed: result.spaceFreed, dryRun, errors: result.errors };
946
+ }
947
+ function actionCleanTraces(body) {
948
+ const dryRun = body.dryRun !== false;
949
+ const days = body.days;
950
+ const categories = body.categories;
951
+ const exclusions = body.exclusions;
952
+ const result = cleanTraces(undefined, { dryRun, days, categories, exclusions });
953
+ return { success: true, deleted: result.deleted.length, freed: result.freed, dryRun, categoriesAffected: result.categoriesAffected, errors: result.errors, items: result.deleted.slice(0, 50) };
954
+ }
955
+ function actionPreviewTraces(body) {
956
+ const operation = body.operation || "clean";
957
+ const options = body.options;
958
+ const preview = generateEnhancedPreview(undefined, {
959
+ operation,
960
+ exclusions: options?.exclusions,
961
+ days: options?.days,
962
+ categories: options?.categories,
963
+ });
964
+ return { success: true, ...preview };
965
+ }
966
+ function actionRedact(body) {
967
+ const file = body.file;
968
+ const lineNum = body.line;
969
+ const patternType = body.pattern;
970
+ if (!file || !lineNum)
971
+ return { success: false, error: "file and line required" };
972
+ if (!fs.existsSync(file))
973
+ return { success: false, error: "File not found" };
974
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
975
+ const backupPath = file.replace(".jsonl", `.backup.${timestamp}.jsonl`);
976
+ try {
977
+ fs.copyFileSync(file, backupPath);
978
+ }
979
+ catch {
980
+ return { success: false, error: "Failed to create backup" };
981
+ }
982
+ const content = fs.readFileSync(file, "utf-8");
983
+ const lines = content.split("\n");
984
+ if (lineNum < 1 || lineNum > lines.length)
985
+ return { success: false, error: "Line out of range" };
986
+ const line = lines[lineNum - 1];
987
+ if (!line.trim())
988
+ return { success: false, error: "Empty line" };
989
+ let redacted = line;
990
+ let count = 0;
991
+ const patterns = patternType
992
+ ? SECRET_PATTERNS.filter(p => p.type === patternType || p.name === patternType)
993
+ : SECRET_PATTERNS;
994
+ for (const pat of patterns) {
995
+ pat.regex.lastIndex = 0;
996
+ const before = redacted;
997
+ redacted = redacted.replace(pat.regex, "[REDACTED]");
998
+ if (redacted !== before)
999
+ count++;
1000
+ }
1001
+ if (count === 0)
1002
+ return { success: false, error: "No matching secrets found on this line" };
1003
+ lines[lineNum - 1] = redacted;
1004
+ fs.writeFileSync(file, lines.join("\n"), "utf-8");
1005
+ return { success: true, redactedCount: count, backupPath };
1006
+ }
1007
+ function actionRedactAll() {
1008
+ const scan = scanForSecrets();
1009
+ if (scan.totalFindings === 0)
1010
+ return { success: true, filesModified: 0, secretsRedacted: 0 };
1011
+ const fileGroups = new Map();
1012
+ for (const f of scan.findings) {
1013
+ const existing = fileGroups.get(f.file) || [];
1014
+ existing.push({ line: f.line, type: f.type });
1015
+ fileGroups.set(f.file, existing);
1016
+ }
1017
+ let filesModified = 0;
1018
+ let secretsRedacted = 0;
1019
+ const errors = [];
1020
+ for (const [file, findings] of fileGroups) {
1021
+ try {
1022
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1023
+ const backupPath = file.replace(".jsonl", `.backup.${timestamp}.jsonl`);
1024
+ fs.copyFileSync(file, backupPath);
1025
+ const content = fs.readFileSync(file, "utf-8");
1026
+ const lines = content.split("\n");
1027
+ let modified = false;
1028
+ const processedLines = new Set();
1029
+ for (const f of findings) {
1030
+ if (processedLines.has(f.line))
1031
+ continue;
1032
+ processedLines.add(f.line);
1033
+ if (f.line < 1 || f.line > lines.length)
1034
+ continue;
1035
+ let line = lines[f.line - 1];
1036
+ for (const pat of SECRET_PATTERNS) {
1037
+ pat.regex.lastIndex = 0;
1038
+ const before = line;
1039
+ line = line.replace(pat.regex, "[REDACTED]");
1040
+ if (line !== before) {
1041
+ secretsRedacted++;
1042
+ modified = true;
1043
+ }
1044
+ }
1045
+ lines[f.line - 1] = line;
1046
+ }
1047
+ if (modified) {
1048
+ fs.writeFileSync(file, lines.join("\n"), "utf-8");
1049
+ filesModified++;
1050
+ }
1051
+ }
1052
+ catch (err) {
1053
+ errors.push(`${file}: ${err instanceof Error ? err.message : String(err)}`);
1054
+ }
1055
+ }
1056
+ return { success: true, filesModified, secretsRedacted, errors, items: Array.from(fileGroups.keys()).slice(0, 50) };
1057
+ }
1058
+ function actionArchive(body) {
1059
+ const dryRun = body.dryRun !== false;
1060
+ const days = body.days || 30;
1061
+ const result = archiveConversations(PROJECTS_DIR, { minDaysInactive: days, dryRun });
1062
+ return { success: true, archived: result.archived.length, spaceFreed: result.totalSize, dryRun, error: result.error, items: result.archived.slice(0, 50) };
1063
+ }
1064
+ function actionMaintenanceRun(body) {
1065
+ const auto = body.auto === true;
1066
+ const report = runMaintenance(PROJECTS_DIR, { dryRun: !auto });
1067
+ return {
1068
+ success: true,
1069
+ status: report.status,
1070
+ actionsPerformed: report.actions.length,
1071
+ actions: report.actions.map(a => ({ type: a.type, description: a.description, sizeBytes: a.sizeBytes })),
1072
+ auto,
1073
+ };
1074
+ }
1075
+ function actionDeleteBackups(body) {
1076
+ const days = body.days || 7;
1077
+ const result = deleteOldBackups(PROJECTS_DIR, days);
1078
+ return { success: true, deleted: result.deleted.length, errors: result.errors, items: result.deleted.slice(0, 50) };
1079
+ }
1080
+ function actionRestore(body) {
1081
+ const backupPath = body.backupPath;
1082
+ if (!backupPath)
1083
+ return { success: false, error: "backupPath required" };
1084
+ const result = restoreFromBackup(backupPath);
1085
+ return { success: result.success, originalPath: result.originalPath, error: result.error };
1086
+ }
1087
+ function actionExport(body) {
1088
+ const filePath = body.file;
1089
+ const format = body.format === "json" ? "json" : "markdown";
1090
+ if (!filePath)
1091
+ return { success: false, error: "file path required" };
1092
+ if (!fs.existsSync(filePath))
1093
+ return { success: false, error: "File not found" };
1094
+ const result = exportConversation(filePath, {
1095
+ format: format,
1096
+ includeToolResults: body.includeTools === true,
1097
+ includeTimestamps: true,
1098
+ });
1099
+ return { success: true, messageCount: result.messageCount, format };
1100
+ }
1101
+ function actionWipeTraces(body) {
1102
+ if (body.confirm !== true)
1103
+ return { success: false, error: "Confirmation required: set confirm=true" };
1104
+ const exclusions = body.exclusions;
1105
+ const result = wipeAllTraces(undefined, {
1106
+ confirm: true,
1107
+ keepSettings: body.keepSettings === true,
1108
+ exclusions,
1109
+ });
1110
+ return { success: true, filesWiped: result.filesWiped, bytesFreed: result.bytesFreed, categoriesWiped: result.categoriesWiped, preserved: result.preserved };
1111
+ }
1112
+ async function actionTestMcp() {
1113
+ const report = await diagnoseMcpServers({ test: true });
1114
+ return {
1115
+ totalServers: report.totalServers,
1116
+ healthyServers: report.healthyServers,
1117
+ configs: report.configs.map(c => ({
1118
+ configPath: c.configPath,
1119
+ servers: c.servers.map(s => ({ name: s.name, command: s.command, type: s.type })),
1120
+ issues: c.issues,
1121
+ valid: c.valid,
1122
+ })),
1123
+ recommendations: report.recommendations,
1124
+ };
1125
+ }
1126
+ function actionAddMcpServer(body) {
1127
+ const name = body.name;
1128
+ const command = body.command;
1129
+ const args = body.args;
1130
+ const env = body.env;
1131
+ const target = body.target || "global";
1132
+ if (!name || !command) {
1133
+ return { success: false, error: "Name and command are required" };
1134
+ }
1135
+ const configPath = target === "project"
1136
+ ? path.join(process.cwd(), ".mcp.json")
1137
+ : path.join(os.homedir(), ".claude.json");
1138
+ try {
1139
+ let config = {};
1140
+ if (fs.existsSync(configPath)) {
1141
+ const content = fs.readFileSync(configPath, "utf-8");
1142
+ config = JSON.parse(content);
1143
+ }
1144
+ if (!config.mcpServers) {
1145
+ config.mcpServers = {};
1146
+ }
1147
+ const mcpServers = config.mcpServers;
1148
+ if (mcpServers[name]) {
1149
+ return { success: false, error: `Server "${name}" already exists` };
1150
+ }
1151
+ const serverConfig = { command };
1152
+ if (args && args.length > 0)
1153
+ serverConfig.args = args;
1154
+ if (env && Object.keys(env).length > 0)
1155
+ serverConfig.env = env;
1156
+ mcpServers[name] = serverConfig;
1157
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
1158
+ return { success: true, configPath, serverName: name };
1159
+ }
1160
+ catch (e) {
1161
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
1162
+ }
1163
+ }
1164
+ // ===== Route tables =====
1165
+ const getRoutes = {
1166
+ "/api/overview": () => getOverview(),
1167
+ "/api/storage": () => getStorage(),
1168
+ "/api/sessions": () => getSessions(),
1169
+ "/api/security": () => getSecurity(),
1170
+ "/api/compliance": () => getCompliance(),
1171
+ "/api/traces": () => getTraces(),
1172
+ "/api/mcp": () => getMcp(),
1173
+ "/api/logs": (params) => getLogs(params),
1174
+ "/api/config": () => getConfig(),
1175
+ "/api/analytics": () => getAnalytics(),
1176
+ "/api/duplicates": () => getDuplicates(),
1177
+ "/api/context": () => getContext(),
1178
+ "/api/backups": () => getBackups(),
1179
+ "/api/stats": () => getStats(),
1180
+ "/api/archive/candidates": () => getArchiveCandidates(),
1181
+ "/api/maintenance": () => getMaintenanceCheck(),
1182
+ "/api/scan": () => getScan(),
1183
+ "/api/search": (params) => getSearch(params),
1184
+ "/api/snapshots": () => ({ snapshots: listStorageSnapshots() }),
1185
+ "/api/snapshot": (params) => {
1186
+ const id = params?.id;
1187
+ if (!id)
1188
+ return { error: "Snapshot ID required" };
1189
+ const s = loadStorageSnapshot(id);
1190
+ return s ? { success: true, snapshot: s } : { success: false, error: "Not found" };
1191
+ },
1192
+ "/api/compare": (params) => {
1193
+ const id1 = params?.base;
1194
+ const id2 = params?.current;
1195
+ if (!id1 || !id2)
1196
+ return { error: "base and current IDs required" };
1197
+ const s1 = loadStorageSnapshot(id1);
1198
+ const s2 = loadStorageSnapshot(id2);
1199
+ if (!s1 || !s2)
1200
+ return { error: "Snapshots not found" };
1201
+ const diff = compareStorageSnapshots(s1.analysis, s2.analysis);
1202
+ return { success: true, diff, baseDate: s1.date, currentDate: s2.date };
1203
+ }
1204
+ };
1205
+ const postRoutes = {
1206
+ "/api/action/clean": (b) => actionClean(b),
1207
+ "/api/action/fix": (b) => actionFix(b),
1208
+ "/api/action/fix-all": () => actionFixAll(),
1209
+ "/api/action/repair": (b) => actionRepair(b),
1210
+ "/api/action/extract": (b) => actionExtract(b),
1211
+ "/api/action/retention": (b) => actionRetention(b),
1212
+ "/api/action/preview-traces": (b) => actionPreviewTraces(b),
1213
+ "/api/action/clean-traces": (b) => actionCleanTraces(b),
1214
+ "/api/action/redact": (b) => actionRedact(b),
1215
+ "/api/action/redact-all": () => actionRedactAll(),
1216
+ "/api/action/archive": (b) => actionArchive(b),
1217
+ "/api/action/maintenance": (b) => actionMaintenanceRun(b),
1218
+ "/api/action/delete-backups": (b) => actionDeleteBackups(b),
1219
+ "/api/action/restore": (b) => actionRestore(b),
1220
+ "/api/action/export": (b) => actionExport(b),
1221
+ "/api/action/wipe-traces": (b) => actionWipeTraces(b),
1222
+ "/api/action/test-mcp": () => actionTestMcp(),
1223
+ "/api/action/add-mcp-server": (b) => actionAddMcpServer(b),
1224
+ "/api/action/save-config": (b) => actionSaveConfig(b),
1225
+ "/api/action/snapshot": (b) => {
1226
+ const label = b.label || "Manual Snapshot";
1227
+ const analysis = analyzeClaudeStorage();
1228
+ const id = saveStorageSnapshot(analysis, label);
1229
+ return { success: true, id };
1230
+ },
1231
+ "/api/action/delete-snapshot": (b) => {
1232
+ const id = b.id;
1233
+ if (!id)
1234
+ return { success: false, error: "ID required" };
1235
+ return { success: deleteStorageSnapshot(id) };
1236
+ }
1237
+ };
1238
+ export function createDashboardServer() {
1239
+ const html = generateDashboardHTML();
1240
+ const server = http.createServer(async (req, res) => {
1241
+ const { pathname, params } = parseUrl(req.url || "/");
1242
+ if (req.method === "GET" && pathname === "/") {
1243
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1244
+ res.end(html);
1245
+ return;
1246
+ }
1247
+ if (req.method === "GET" && pathname.startsWith("/api/")) {
1248
+ const handler = getRoutes[pathname];
1249
+ if (handler) {
1250
+ try {
1251
+ const data = await handler(params);
1252
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
1253
+ res.end(JSON.stringify(data));
1254
+ }
1255
+ catch (err) {
1256
+ res.writeHead(500, { "Content-Type": "application/json" });
1257
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1258
+ }
1259
+ return;
1260
+ }
1261
+ const sessionMatch = matchRoute(pathname, "/api/session/:id");
1262
+ if (sessionMatch) {
1263
+ try {
1264
+ const data = getSessionDetail(sessionMatch.id);
1265
+ if (!data) {
1266
+ res.writeHead(404, { "Content-Type": "application/json" });
1267
+ res.end(JSON.stringify({ error: "Session not found" }));
1268
+ }
1269
+ else {
1270
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
1271
+ res.end(JSON.stringify(data));
1272
+ }
1273
+ }
1274
+ catch (err) {
1275
+ res.writeHead(500, { "Content-Type": "application/json" });
1276
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1277
+ }
1278
+ return;
1279
+ }
1280
+ const auditMatch = matchRoute(pathname, "/api/session/:id/audit");
1281
+ if (auditMatch) {
1282
+ try {
1283
+ const data = getSessionAudit(auditMatch.id);
1284
+ if (!data) {
1285
+ res.writeHead(404, { "Content-Type": "application/json" });
1286
+ res.end(JSON.stringify({ error: "Session not found" }));
1287
+ }
1288
+ else {
1289
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
1290
+ res.end(JSON.stringify(data));
1291
+ }
1292
+ }
1293
+ catch (err) {
1294
+ res.writeHead(500, { "Content-Type": "application/json" });
1295
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1296
+ }
1297
+ return;
1298
+ }
1299
+ const findingMatch = matchRoute(pathname, "/api/security/finding/:file/:line");
1300
+ if (findingMatch) {
1301
+ try {
1302
+ const data = getSecurityFindingPreview(decodeURIComponent(findingMatch.file), parseInt(findingMatch.line, 10));
1303
+ if (!data) {
1304
+ res.writeHead(404, { "Content-Type": "application/json" });
1305
+ res.end(JSON.stringify({ error: "Finding not found" }));
1306
+ }
1307
+ else {
1308
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
1309
+ res.end(JSON.stringify(data));
1310
+ }
1311
+ }
1312
+ catch (err) {
1313
+ res.writeHead(500, { "Content-Type": "application/json" });
1314
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1315
+ }
1316
+ return;
1317
+ }
1318
+ const mcpCapMatch = matchRoute(pathname, "/api/mcp/server/:name/capabilities");
1319
+ if (mcpCapMatch) {
1320
+ try {
1321
+ const data = await getMcpServerCapabilities(decodeURIComponent(mcpCapMatch.name));
1322
+ if (!data) {
1323
+ res.writeHead(404, { "Content-Type": "application/json" });
1324
+ res.end(JSON.stringify({ error: "Server not found" }));
1325
+ }
1326
+ else {
1327
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
1328
+ res.end(JSON.stringify(data));
1329
+ }
1330
+ }
1331
+ catch (err) {
1332
+ res.writeHead(500, { "Content-Type": "application/json" });
1333
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1334
+ }
1335
+ return;
1336
+ }
1337
+ }
1338
+ if (req.method === "POST" && pathname.startsWith("/api/action/")) {
1339
+ const handler = postRoutes[pathname];
1340
+ if (handler) {
1341
+ try {
1342
+ const body = await readBody(req);
1343
+ const data = await handler(body);
1344
+ res.writeHead(200, { "Content-Type": "application/json" });
1345
+ res.end(JSON.stringify(data));
1346
+ }
1347
+ catch (err) {
1348
+ res.writeHead(500, { "Content-Type": "application/json" });
1349
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1350
+ }
1351
+ return;
1352
+ }
1353
+ }
1354
+ res.writeHead(404, { "Content-Type": "text/plain" });
1355
+ res.end("Not Found");
1356
+ });
1357
+ return server;
1358
+ }
1359
+ export async function startDashboard(options) {
1360
+ const port = options?.port || 1405;
1361
+ const shouldOpen = options?.open !== false;
1362
+ const server = createDashboardServer();
1363
+ return new Promise((resolve, reject) => {
1364
+ server.on("error", (err) => {
1365
+ if (err.code === "EADDRINUSE") {
1366
+ console.error(`Port ${port} is already in use. Try a different port with --port.`);
1367
+ }
1368
+ reject(err);
1369
+ });
1370
+ server.listen(port, "127.0.0.1", () => {
1371
+ const url = `http://localhost:${port}`;
1372
+ console.log(`Dashboard running at ${url}`);
1373
+ if (options?.daemon) {
1374
+ try {
1375
+ fs.writeFileSync(PID_FILE, String(process.pid));
1376
+ }
1377
+ catch { /* skip */ }
1378
+ console.log(`Running as daemon (PID: ${process.pid})`);
1379
+ console.log(`Stop with: cct dashboard --stop`);
1380
+ }
1381
+ else {
1382
+ console.log("Press Ctrl+C to stop.\n");
1383
+ }
1384
+ if (shouldOpen) {
1385
+ const platform = os.platform();
1386
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
1387
+ execFile(cmd, [url], () => { });
1388
+ }
1389
+ resolve(server);
1390
+ });
1391
+ });
1392
+ }
1393
+ export function stopDashboard() {
1394
+ try {
1395
+ if (!fs.existsSync(PID_FILE))
1396
+ return false;
1397
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
1398
+ process.kill(pid, "SIGTERM");
1399
+ fs.unlinkSync(PID_FILE);
1400
+ return true;
1401
+ }
1402
+ catch {
1403
+ try {
1404
+ fs.unlinkSync(PID_FILE);
1405
+ }
1406
+ catch { /* skip */ }
1407
+ return false;
1408
+ }
1409
+ }
1410
+ export function isDashboardRunning() {
1411
+ try {
1412
+ if (!fs.existsSync(PID_FILE))
1413
+ return { running: false };
1414
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
1415
+ process.kill(pid, 0);
1416
+ return { running: true, pid };
1417
+ }
1418
+ catch {
1419
+ return { running: false };
1420
+ }
1421
+ }
1422
+ //# sourceMappingURL=dashboard.js.map