@colbymchenry/cmem 0.2.36 → 0.2.37

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 (3) hide show
  1. package/dist/cli.js +1189 -693
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17,9 +17,9 @@ __export(install_exports, {
17
17
  uninstallCommand: () => uninstallCommand
18
18
  });
19
19
  import chalk9 from "chalk";
20
- import { writeFileSync, unlinkSync, existsSync as existsSync9, mkdirSync as mkdirSync2 } from "fs";
20
+ import { writeFileSync, unlinkSync as unlinkSync2, existsSync as existsSync10, mkdirSync as mkdirSync2 } from "fs";
21
21
  import { homedir as homedir2 } from "os";
22
- import { join as join5, dirname as dirname5 } from "path";
22
+ import { join as join6, dirname as dirname6 } from "path";
23
23
  import { execSync as execSync2 } from "child_process";
24
24
  function getCmemPath() {
25
25
  try {
@@ -32,7 +32,7 @@ function getCmemPath() {
32
32
  function getNodeBinDir() {
33
33
  try {
34
34
  const nodePath = execSync2("which node", { encoding: "utf-8" }).trim();
35
- return dirname5(nodePath);
35
+ return dirname6(nodePath);
36
36
  } catch {
37
37
  return "/usr/local/bin";
38
38
  }
@@ -84,16 +84,16 @@ async function installCommand() {
84
84
  return;
85
85
  }
86
86
  console.log(chalk9.cyan("Installing cmem watch daemon...\n"));
87
- if (!existsSync9(LAUNCH_AGENTS_DIR)) {
87
+ if (!existsSync10(LAUNCH_AGENTS_DIR)) {
88
88
  mkdirSync2(LAUNCH_AGENTS_DIR, { recursive: true });
89
89
  }
90
- const cmemDir = join5(homedir2(), ".cmem");
91
- if (!existsSync9(cmemDir)) {
90
+ const cmemDir = join6(homedir2(), ".cmem");
91
+ if (!existsSync10(cmemDir)) {
92
92
  mkdirSync2(cmemDir, { recursive: true });
93
93
  }
94
94
  const cmemPath = getCmemPath();
95
95
  console.log(chalk9.dim(`Using cmem at: ${cmemPath}`));
96
- if (existsSync9(PLIST_PATH)) {
96
+ if (existsSync10(PLIST_PATH)) {
97
97
  console.log(chalk9.yellow("LaunchAgent already exists. Unloading first..."));
98
98
  try {
99
99
  execSync2(`launchctl unload "${PLIST_PATH}"`, { stdio: "ignore" });
@@ -126,7 +126,7 @@ async function uninstallCommand() {
126
126
  return;
127
127
  }
128
128
  console.log(chalk9.cyan("Uninstalling cmem watch daemon...\n"));
129
- if (!existsSync9(PLIST_PATH)) {
129
+ if (!existsSync10(PLIST_PATH)) {
130
130
  console.log(chalk9.yellow("LaunchAgent not found. Nothing to uninstall."));
131
131
  return;
132
132
  }
@@ -137,7 +137,7 @@ async function uninstallCommand() {
137
137
  console.log(chalk9.yellow("LaunchAgent was not loaded"));
138
138
  }
139
139
  try {
140
- unlinkSync(PLIST_PATH);
140
+ unlinkSync2(PLIST_PATH);
141
141
  console.log(chalk9.green(`\u2713 Removed ${PLIST_PATH}`));
142
142
  } catch (err) {
143
143
  console.log(chalk9.red("Failed to remove plist:"), err);
@@ -152,7 +152,7 @@ async function statusCommand() {
152
152
  return;
153
153
  }
154
154
  console.log(chalk9.cyan("cmem watch daemon status\n"));
155
- if (!existsSync9(PLIST_PATH)) {
155
+ if (!existsSync10(PLIST_PATH)) {
156
156
  console.log(chalk9.yellow("Status: Not installed"));
157
157
  console.log(chalk9.dim("Run: cmem install"));
158
158
  return;
@@ -185,9 +185,9 @@ var LAUNCH_AGENTS_DIR, PLIST_NAME, PLIST_PATH;
185
185
  var init_install = __esm({
186
186
  "src/commands/install.ts"() {
187
187
  "use strict";
188
- LAUNCH_AGENTS_DIR = join5(homedir2(), "Library", "LaunchAgents");
188
+ LAUNCH_AGENTS_DIR = join6(homedir2(), "Library", "LaunchAgents");
189
189
  PLIST_NAME = "com.cmem.watch.plist";
190
- PLIST_PATH = join5(LAUNCH_AGENTS_DIR, PLIST_NAME);
190
+ PLIST_PATH = join6(LAUNCH_AGENTS_DIR, PLIST_NAME);
191
191
  }
192
192
  });
193
193
 
@@ -195,18 +195,18 @@ var init_install = __esm({
195
195
  import { program } from "commander";
196
196
  import { render } from "ink";
197
197
  import React2 from "react";
198
- import { existsSync as existsSync11, readFileSync as readFileSync4 } from "fs";
199
- import { join as join7, dirname as dirname7 } from "path";
198
+ import { existsSync as existsSync12, readFileSync as readFileSync4 } from "fs";
199
+ import { join as join8, dirname as dirname8 } from "path";
200
200
  import { fileURLToPath as fileURLToPath2 } from "url";
201
201
  import { spawn as spawn2 } from "child_process";
202
202
 
203
203
  // src/ui/App.tsx
204
204
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
205
- import { Box as Box6, Text as Text6, useInput, useApp } from "ink";
205
+ import { Box as Box7, Text as Text7, useInput, useApp } from "ink";
206
206
  import TextInput2 from "ink-text-input";
207
207
  import Spinner from "ink-spinner";
208
- import { basename as basename5, dirname as dirname3 } from "path";
209
- import { existsSync as existsSync6 } from "fs";
208
+ import { basename as basename5, dirname as dirname4 } from "path";
209
+ import { existsSync as existsSync7 } from "fs";
210
210
 
211
211
  // src/utils/config.ts
212
212
  import { homedir } from "os";
@@ -224,6 +224,8 @@ var EMBEDDING_DIMENSIONS = 768;
224
224
  var MAX_EMBEDDING_CHARS = 8e3;
225
225
  var MAX_MESSAGE_PREVIEW_CHARS = 500;
226
226
  var MAX_MESSAGES_FOR_CONTEXT = 20;
227
+ var DB_SIZE_ALERT_THRESHOLD = 5 * 1024 * 1024 * 1024;
228
+ var DEFAULT_PURGE_DAYS = 30;
227
229
  function ensureCmemDir() {
228
230
  if (!existsSync(CMEM_DIR)) {
229
231
  mkdirSync(CMEM_DIR, { recursive: true });
@@ -280,491 +282,184 @@ function restoreFromBackup(sourceFile) {
280
282
  }
281
283
  }
282
284
 
283
- // src/ui/components/Header.tsx
284
- import { Box, Text } from "ink";
285
- import { basename as basename2 } from "path";
286
- import { jsx, jsxs } from "react/jsx-runtime";
287
- var Header = ({ embeddingsReady, projectFilter }) => {
288
- return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [
289
- /* @__PURE__ */ jsxs(Box, { children: [
290
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "cmem" }),
291
- projectFilter && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
292
- " \u{1F4C1} ",
293
- basename2(projectFilter)
294
- ] }),
295
- !embeddingsReady && /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: " (loading model...)" })
296
- ] }),
297
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[q] quit" })
298
- ] });
299
- };
300
-
301
- // src/ui/components/SearchInput.tsx
302
- import { Box as Box2, Text as Text2 } from "ink";
303
- import TextInput from "ink-text-input";
304
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
305
- var SearchInput = ({
306
- value,
307
- onChange,
308
- isFocused
309
- }) => {
310
- return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
311
- /* @__PURE__ */ jsx2(Text2, { children: "Search: " }),
312
- isFocused ? /* @__PURE__ */ jsx2(
313
- TextInput,
314
- {
315
- value,
316
- onChange,
317
- placeholder: "type to search...",
318
- focus: true
319
- }
320
- ) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: value || "press / to search" })
321
- ] });
322
- };
285
+ // src/db/maintenance.ts
286
+ import { statSync as statSync2, existsSync as existsSync4, unlinkSync, readdirSync as readdirSync2, rmdirSync } from "fs";
287
+ import { join as join3, dirname as dirname3 } from "path";
323
288
 
324
- // src/ui/components/SessionList.tsx
325
- import { Box as Box3, Text as Text3 } from "ink";
326
- import { basename as basename3 } from "path";
289
+ // src/db/index.ts
290
+ import Database from "better-sqlite3";
291
+ import * as sqliteVec from "sqlite-vec";
292
+ import { existsSync as existsSync3 } from "fs";
327
293
 
328
- // src/utils/format.ts
329
- function formatTimeAgo(timestamp) {
330
- const date = new Date(timestamp);
331
- const now = /* @__PURE__ */ new Date();
332
- const diffMs = now.getTime() - date.getTime();
333
- const diffSecs = Math.floor(diffMs / 1e3);
334
- const diffMins = Math.floor(diffSecs / 60);
335
- const diffHours = Math.floor(diffMins / 60);
336
- const diffDays = Math.floor(diffHours / 24);
337
- const diffWeeks = Math.floor(diffDays / 7);
338
- const diffMonths = Math.floor(diffDays / 30);
339
- if (diffSecs < 60) return "just now";
340
- if (diffMins < 60) return `${diffMins}m ago`;
341
- if (diffHours < 24) return `${diffHours}h ago`;
342
- if (diffDays < 7) return `${diffDays}d ago`;
343
- if (diffWeeks < 4) return `${diffWeeks}w ago`;
344
- return `${diffMonths}mo ago`;
345
- }
346
- function truncate(text, maxLength) {
347
- if (text.length <= maxLength) return text;
348
- return text.slice(0, maxLength - 3) + "...";
294
+ // src/parser/index.ts
295
+ import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
296
+ import { join as join2 } from "path";
297
+ var AUTOMATED_TITLE_PATTERNS = [
298
+ /^<[a-z-]+>/i,
299
+ // XML-like tags: <project-instructions>, <command-message>, etc.
300
+ /^<[A-Z_]+>/,
301
+ // Uppercase tags: <SYSTEM>, <TOOL_USE>, etc.
302
+ /^\[system\]/i,
303
+ // [system] prefix
304
+ /^\/[a-z]+$/i
305
+ // Slash commands: /init, /help, etc.
306
+ ];
307
+ function isAutomatedByContent(title) {
308
+ for (const pattern of AUTOMATED_TITLE_PATTERNS) {
309
+ if (pattern.test(title.trim())) {
310
+ return true;
311
+ }
312
+ }
313
+ return false;
349
314
  }
350
- function shortId(id) {
351
- return id.slice(0, 8);
315
+ function extractSessionMetadata(filepath) {
316
+ const content = readFileSync(filepath, "utf-8");
317
+ const metadata = {
318
+ isSidechain: false,
319
+ isMeta: false
320
+ };
321
+ for (const line of content.split("\n")) {
322
+ if (!line.trim()) continue;
323
+ try {
324
+ const parsed = JSON.parse(line);
325
+ if (parsed.type === "user" && parsed.message) {
326
+ if (parsed.isSidechain === true) {
327
+ metadata.isSidechain = true;
328
+ }
329
+ if (parsed.agentId) {
330
+ metadata.isSidechain = true;
331
+ }
332
+ if (parsed.isMeta === true) {
333
+ metadata.isMeta = true;
334
+ }
335
+ break;
336
+ }
337
+ } catch {
338
+ }
339
+ }
340
+ return metadata;
352
341
  }
353
- function formatNumber(num) {
354
- return num.toLocaleString();
342
+ function parseSessionFile(filepath) {
343
+ const content = readFileSync(filepath, "utf-8");
344
+ const messages = [];
345
+ for (const line of content.split("\n")) {
346
+ if (!line.trim()) continue;
347
+ try {
348
+ const parsed = JSON.parse(line);
349
+ if ((parsed.type === "user" || parsed.type === "assistant") && parsed.message) {
350
+ const msg = parsed.message;
351
+ if (msg.role && msg.content) {
352
+ const content2 = Array.isArray(msg.content) ? msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n") : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
353
+ if (content2) {
354
+ messages.push({
355
+ role: msg.role,
356
+ content: content2,
357
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
358
+ });
359
+ }
360
+ }
361
+ } else if (parsed.type === "message" && parsed.role && parsed.content) {
362
+ messages.push({
363
+ role: parsed.role,
364
+ content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
365
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
366
+ });
367
+ } else if (parsed.role && parsed.content && !parsed.type) {
368
+ messages.push({
369
+ role: parsed.role,
370
+ content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
371
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
372
+ });
373
+ }
374
+ } catch {
375
+ }
376
+ }
377
+ return messages;
355
378
  }
356
- function formatBytes(bytes) {
357
- if (bytes === 0) return "0 B";
358
- const k = 1024;
359
- const sizes = ["B", "KB", "MB", "GB"];
360
- const i = Math.floor(Math.log(bytes) / Math.log(k));
361
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
379
+ function findSessionFiles() {
380
+ const sessions = [];
381
+ if (existsSync2(CLAUDE_PROJECTS_DIR)) {
382
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR);
383
+ for (const projectDir of projectDirs) {
384
+ const projectDirPath = join2(CLAUDE_PROJECTS_DIR, projectDir);
385
+ const stat = statSync(projectDirPath);
386
+ if (!stat.isDirectory()) continue;
387
+ const indexPath = join2(projectDirPath, "sessions-index.json");
388
+ const sessionIndex = loadSessionIndex(indexPath);
389
+ const indexedSessions = /* @__PURE__ */ new Map();
390
+ if (sessionIndex) {
391
+ for (const entry of sessionIndex.entries) {
392
+ indexedSessions.set(entry.sessionId, entry);
393
+ }
394
+ }
395
+ const entries = readdirSync(projectDirPath, { withFileTypes: true });
396
+ for (const entry of entries) {
397
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
398
+ const filePath = join2(projectDirPath, entry.name);
399
+ const sessionId = entry.name.replace(".jsonl", "");
400
+ try {
401
+ const session = loadSession(filePath);
402
+ if (session) {
403
+ const indexEntry = indexedSessions.get(sessionId);
404
+ if (indexEntry) {
405
+ session.projectPath = indexEntry.projectPath;
406
+ }
407
+ const agentMessages = loadSubagentMessages(projectDirPath, sessionId);
408
+ if (agentMessages.length > 0) {
409
+ session.agentMessages = agentMessages;
410
+ }
411
+ sessions.push(session);
412
+ }
413
+ } catch {
414
+ }
415
+ }
416
+ }
417
+ }
418
+ if (existsSync2(CLAUDE_SESSIONS_DIR)) {
419
+ const sessionFiles = readdirSync(CLAUDE_SESSIONS_DIR).filter((f) => f.endsWith(".jsonl"));
420
+ for (const sessionFile of sessionFiles) {
421
+ const filePath = join2(CLAUDE_SESSIONS_DIR, sessionFile);
422
+ try {
423
+ const session = loadSession(filePath);
424
+ if (session) {
425
+ sessions.push(session);
426
+ }
427
+ } catch {
428
+ }
429
+ }
430
+ }
431
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
432
+ return sessions;
362
433
  }
363
- function generateTitle(content) {
364
- const firstLine = content.split("\n")[0].trim();
365
- const title = truncate(firstLine, 50);
366
- return title || "Untitled Session";
434
+ function loadSessionIndex(indexPath) {
435
+ if (!existsSync2(indexPath)) return null;
436
+ try {
437
+ const content = readFileSync(indexPath, "utf-8");
438
+ return JSON.parse(content);
439
+ } catch {
440
+ return null;
441
+ }
367
442
  }
368
-
369
- // src/ui/components/SessionList.tsx
370
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
371
- var SessionList = ({
372
- sessions,
373
- selectedIndex
374
- }) => {
375
- if (sessions.length === 0) {
376
- return /* @__PURE__ */ jsxs3(
377
- Box3,
378
- {
379
- flexDirection: "column",
380
- borderStyle: "round",
381
- borderColor: "gray",
382
- paddingX: 1,
383
- paddingY: 0,
384
- children: [
385
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Sessions" }),
386
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No sessions found" }),
387
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Run: cmem save --latest" })
388
- ]
389
- }
390
- );
443
+ function loadSubagentMessages(projectDirPath, parentSessionId) {
444
+ const subagentsDir = join2(projectDirPath, parentSessionId, "subagents");
445
+ if (!existsSync2(subagentsDir)) return [];
446
+ const messages = [];
447
+ try {
448
+ const agentFiles = readdirSync(subagentsDir).filter((f) => f.endsWith(".jsonl"));
449
+ for (const agentFile of agentFiles) {
450
+ const agentPath = join2(subagentsDir, agentFile);
451
+ const agentMessages = parseSessionFile(agentPath);
452
+ messages.push(...agentMessages);
453
+ }
454
+ } catch {
391
455
  }
392
- const visibleCount = 8;
393
- let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
394
- const endIndex = Math.min(sessions.length, startIndex + visibleCount);
395
- if (endIndex - startIndex < visibleCount) {
396
- startIndex = Math.max(0, endIndex - visibleCount);
397
- }
398
- const visibleSessions = sessions.slice(startIndex, endIndex);
399
- return /* @__PURE__ */ jsxs3(
400
- Box3,
401
- {
402
- flexDirection: "column",
403
- borderStyle: "round",
404
- borderColor: "gray",
405
- paddingX: 1,
406
- paddingY: 0,
407
- children: [
408
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
409
- "Sessions (",
410
- sessions.length,
411
- ")"
412
- ] }),
413
- visibleSessions.map((session, i) => {
414
- const actualIndex = startIndex + i;
415
- const isSelected = actualIndex === selectedIndex;
416
- return /* @__PURE__ */ jsx3(
417
- SessionItem,
418
- {
419
- session,
420
- isSelected
421
- },
422
- session.id
423
- );
424
- }),
425
- sessions.length > visibleCount && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
426
- startIndex > 0 ? "\u2191 more above" : "",
427
- startIndex > 0 && endIndex < sessions.length ? " | " : "",
428
- endIndex < sessions.length ? "\u2193 more below" : ""
429
- ] })
430
- ]
431
- }
432
- );
433
- };
434
- var SessionItem = ({ session, isSelected }) => {
435
- const hasCustomTitle = !!session.customTitle;
436
- const displayTitle = truncate(session.customTitle || session.title, 38);
437
- const folderName = session.projectPath ? truncate(basename3(session.projectPath), 38) : "";
438
- const msgs = String(session.messageCount).padStart(3);
439
- const updated = formatTimeAgo(session.updatedAt);
440
- const getTitleColor = () => {
441
- if (isSelected) return "cyan";
442
- if (session.isFavorite) return "yellow";
443
- if (hasCustomTitle) return "magenta";
444
- return void 0;
445
- };
446
- return /* @__PURE__ */ jsxs3(Box3, { children: [
447
- /* @__PURE__ */ jsx3(Text3, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
448
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: session.isFavorite ? "\u2B50" : " " }),
449
- /* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(38) }),
450
- /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
451
- " ",
452
- folderName.padEnd(38)
453
- ] }),
454
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
455
- " ",
456
- msgs,
457
- " "
458
- ] }),
459
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: updated.padStart(8) })
460
- ] });
461
- };
462
-
463
- // src/ui/components/ProjectList.tsx
464
- import { Box as Box4, Text as Text4 } from "ink";
465
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
466
- var ProjectList = ({
467
- projects,
468
- selectedIndex
469
- }) => {
470
- if (projects.length === 0) {
471
- return /* @__PURE__ */ jsxs4(
472
- Box4,
473
- {
474
- flexDirection: "column",
475
- borderStyle: "round",
476
- borderColor: "gray",
477
- paddingX: 1,
478
- paddingY: 0,
479
- children: [
480
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Projects" }),
481
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No projects found" }),
482
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Start using Claude Code in a project directory" })
483
- ]
484
- }
485
- );
486
- }
487
- const visibleCount = 8;
488
- let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
489
- const endIndex = Math.min(projects.length, startIndex + visibleCount);
490
- if (endIndex - startIndex < visibleCount) {
491
- startIndex = Math.max(0, endIndex - visibleCount);
492
- }
493
- const visibleProjects = projects.slice(startIndex, endIndex);
494
- return /* @__PURE__ */ jsxs4(
495
- Box4,
496
- {
497
- flexDirection: "column",
498
- borderStyle: "round",
499
- borderColor: "gray",
500
- paddingX: 1,
501
- paddingY: 0,
502
- children: [
503
- /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
504
- "Projects (",
505
- projects.length,
506
- ")"
507
- ] }),
508
- visibleProjects.map((project, i) => {
509
- const actualIndex = startIndex + i;
510
- const isSelected = actualIndex === selectedIndex;
511
- return /* @__PURE__ */ jsx4(
512
- ProjectItem,
513
- {
514
- project,
515
- isSelected
516
- },
517
- project.path
518
- );
519
- }),
520
- projects.length > visibleCount && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
521
- startIndex > 0 ? "\u2191 more above" : "",
522
- startIndex > 0 && endIndex < projects.length ? " | " : "",
523
- endIndex < projects.length ? "\u2193 more below" : ""
524
- ] })
525
- ]
526
- }
527
- );
528
- };
529
- var ProjectItem = ({ project, isSelected }) => {
530
- const displayName = truncate(project.name, 40);
531
- const sessions = `${project.sessionCount} session${project.sessionCount !== 1 ? "s" : ""}`;
532
- const messages = `${project.totalMessages} msgs`;
533
- const updated = formatTimeAgo(project.lastUpdated);
534
- const orderBadge = project.sortOrder !== null ? `${project.sortOrder + 1}.` : " ";
535
- return /* @__PURE__ */ jsxs4(Box4, { children: [
536
- /* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
537
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
538
- orderBadge.padStart(3),
539
- " "
540
- ] }),
541
- /* @__PURE__ */ jsx4(Text4, { color: "blue", children: "\u{1F4C1} " }),
542
- /* @__PURE__ */ jsx4(Text4, { bold: isSelected, color: isSelected ? "cyan" : void 0, wrap: "truncate", children: displayName.padEnd(35) }),
543
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
544
- " ",
545
- sessions.padEnd(12)
546
- ] }),
547
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
548
- " ",
549
- messages.padEnd(10)
550
- ] }),
551
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: updated.padStart(10) })
552
- ] });
553
- };
554
-
555
- // src/ui/components/Preview.tsx
556
- import { Box as Box5, Text as Text5 } from "ink";
557
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
558
- var Preview = ({ session }) => {
559
- if (!session) {
560
- return null;
561
- }
562
- const summary = session.summary ? truncate(session.summary, 200) : "No summary available";
563
- return /* @__PURE__ */ jsxs5(
564
- Box5,
565
- {
566
- flexDirection: "column",
567
- borderStyle: "round",
568
- borderColor: "gray",
569
- paddingX: 1,
570
- paddingY: 0,
571
- marginTop: 1,
572
- children: [
573
- /* @__PURE__ */ jsxs5(Box5, { children: [
574
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Preview" }),
575
- session.isFavorite && /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: " \u2B50" }),
576
- session.projectPath && /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "blue", children: [
577
- " \u{1F4C1} ",
578
- session.projectPath
579
- ] })
580
- ] }),
581
- /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: summary }),
582
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
583
- "Messages: ",
584
- session.messageCount
585
- ] })
586
- ]
587
- }
588
- );
589
- };
590
-
591
- // src/ui/hooks/useSessions.ts
592
- import { useState, useEffect, useCallback } from "react";
593
-
594
- // src/db/index.ts
595
- import Database from "better-sqlite3";
596
- import * as sqliteVec from "sqlite-vec";
597
- import { existsSync as existsSync3 } from "fs";
598
-
599
- // src/parser/index.ts
600
- import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
601
- import { join as join2 } from "path";
602
- var AUTOMATED_TITLE_PATTERNS = [
603
- /^<[a-z-]+>/i,
604
- // XML-like tags: <project-instructions>, <command-message>, etc.
605
- /^<[A-Z_]+>/,
606
- // Uppercase tags: <SYSTEM>, <TOOL_USE>, etc.
607
- /^\[system\]/i,
608
- // [system] prefix
609
- /^\/[a-z]+$/i
610
- // Slash commands: /init, /help, etc.
611
- ];
612
- function isAutomatedByContent(title) {
613
- for (const pattern of AUTOMATED_TITLE_PATTERNS) {
614
- if (pattern.test(title.trim())) {
615
- return true;
616
- }
617
- }
618
- return false;
619
- }
620
- function extractSessionMetadata(filepath) {
621
- const content = readFileSync(filepath, "utf-8");
622
- const metadata = {
623
- isSidechain: false,
624
- isMeta: false
625
- };
626
- for (const line of content.split("\n")) {
627
- if (!line.trim()) continue;
628
- try {
629
- const parsed = JSON.parse(line);
630
- if (parsed.type === "user" && parsed.message) {
631
- if (parsed.isSidechain === true) {
632
- metadata.isSidechain = true;
633
- }
634
- if (parsed.agentId) {
635
- metadata.isSidechain = true;
636
- }
637
- if (parsed.isMeta === true) {
638
- metadata.isMeta = true;
639
- }
640
- break;
641
- }
642
- } catch {
643
- }
644
- }
645
- return metadata;
646
- }
647
- function parseSessionFile(filepath) {
648
- const content = readFileSync(filepath, "utf-8");
649
- const messages = [];
650
- for (const line of content.split("\n")) {
651
- if (!line.trim()) continue;
652
- try {
653
- const parsed = JSON.parse(line);
654
- if ((parsed.type === "user" || parsed.type === "assistant") && parsed.message) {
655
- const msg = parsed.message;
656
- if (msg.role && msg.content) {
657
- const content2 = Array.isArray(msg.content) ? msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n") : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
658
- if (content2) {
659
- messages.push({
660
- role: msg.role,
661
- content: content2,
662
- timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
663
- });
664
- }
665
- }
666
- } else if (parsed.type === "message" && parsed.role && parsed.content) {
667
- messages.push({
668
- role: parsed.role,
669
- content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
670
- timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
671
- });
672
- } else if (parsed.role && parsed.content && !parsed.type) {
673
- messages.push({
674
- role: parsed.role,
675
- content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
676
- timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
677
- });
678
- }
679
- } catch {
680
- }
681
- }
682
- return messages;
683
- }
684
- function findSessionFiles() {
685
- const sessions = [];
686
- if (existsSync2(CLAUDE_PROJECTS_DIR)) {
687
- const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR);
688
- for (const projectDir of projectDirs) {
689
- const projectDirPath = join2(CLAUDE_PROJECTS_DIR, projectDir);
690
- const stat = statSync(projectDirPath);
691
- if (!stat.isDirectory()) continue;
692
- const indexPath = join2(projectDirPath, "sessions-index.json");
693
- const sessionIndex = loadSessionIndex(indexPath);
694
- const indexedSessions = /* @__PURE__ */ new Map();
695
- if (sessionIndex) {
696
- for (const entry of sessionIndex.entries) {
697
- indexedSessions.set(entry.sessionId, entry);
698
- }
699
- }
700
- const entries = readdirSync(projectDirPath, { withFileTypes: true });
701
- for (const entry of entries) {
702
- if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
703
- const filePath = join2(projectDirPath, entry.name);
704
- const sessionId = entry.name.replace(".jsonl", "");
705
- try {
706
- const session = loadSession(filePath);
707
- if (session) {
708
- const indexEntry = indexedSessions.get(sessionId);
709
- if (indexEntry) {
710
- session.projectPath = indexEntry.projectPath;
711
- }
712
- const agentMessages = loadSubagentMessages(projectDirPath, sessionId);
713
- if (agentMessages.length > 0) {
714
- session.agentMessages = agentMessages;
715
- }
716
- sessions.push(session);
717
- }
718
- } catch {
719
- }
720
- }
721
- }
722
- }
723
- if (existsSync2(CLAUDE_SESSIONS_DIR)) {
724
- const sessionFiles = readdirSync(CLAUDE_SESSIONS_DIR).filter((f) => f.endsWith(".jsonl"));
725
- for (const sessionFile of sessionFiles) {
726
- const filePath = join2(CLAUDE_SESSIONS_DIR, sessionFile);
727
- try {
728
- const session = loadSession(filePath);
729
- if (session) {
730
- sessions.push(session);
731
- }
732
- } catch {
733
- }
734
- }
735
- }
736
- sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
737
- return sessions;
738
- }
739
- function loadSessionIndex(indexPath) {
740
- if (!existsSync2(indexPath)) return null;
741
- try {
742
- const content = readFileSync(indexPath, "utf-8");
743
- return JSON.parse(content);
744
- } catch {
745
- return null;
746
- }
747
- }
748
- function loadSubagentMessages(projectDirPath, parentSessionId) {
749
- const subagentsDir = join2(projectDirPath, parentSessionId, "subagents");
750
- if (!existsSync2(subagentsDir)) return [];
751
- const messages = [];
752
- try {
753
- const agentFiles = readdirSync(subagentsDir).filter((f) => f.endsWith(".jsonl"));
754
- for (const agentFile of agentFiles) {
755
- const agentPath = join2(subagentsDir, agentFile);
756
- const agentMessages = parseSessionFile(agentPath);
757
- messages.push(...agentMessages);
758
- }
759
- } catch {
760
- }
761
- return messages;
762
- }
763
- function loadSession(filePath) {
764
- const rawData = readFileSync(filePath, "utf-8");
765
- const messages = parseSessionFile(filePath);
766
- if (messages.length === 0) {
767
- return null;
456
+ return messages;
457
+ }
458
+ function loadSession(filePath) {
459
+ const rawData = readFileSync(filePath, "utf-8");
460
+ const messages = parseSessionFile(filePath);
461
+ if (messages.length === 0) {
462
+ return null;
768
463
  }
769
464
  const stats = statSync(filePath);
770
465
  return {
@@ -923,6 +618,156 @@ function runMigrations(database) {
923
618
  transaction();
924
619
  }
925
620
 
621
+ // src/db/maintenance.ts
622
+ function getDatabaseSize() {
623
+ try {
624
+ const stats = statSync2(DB_PATH);
625
+ return stats.size;
626
+ } catch {
627
+ return 0;
628
+ }
629
+ }
630
+ function getBackupsDirSize() {
631
+ try {
632
+ if (!existsSync4(BACKUPS_DIR)) return 0;
633
+ return getDirSize(BACKUPS_DIR);
634
+ } catch {
635
+ return 0;
636
+ }
637
+ }
638
+ function getDirSize(dirPath) {
639
+ let size = 0;
640
+ try {
641
+ const entries = readdirSync2(dirPath, { withFileTypes: true });
642
+ for (const entry of entries) {
643
+ const fullPath = join3(dirPath, entry.name);
644
+ if (entry.isDirectory()) {
645
+ size += getDirSize(fullPath);
646
+ } else {
647
+ try {
648
+ size += statSync2(fullPath).size;
649
+ } catch {
650
+ }
651
+ }
652
+ }
653
+ } catch {
654
+ }
655
+ return size;
656
+ }
657
+ function getTotalStorageSize() {
658
+ return getDatabaseSize() + getBackupsDirSize();
659
+ }
660
+ function getPurgePreview(days) {
661
+ const db2 = getDatabase();
662
+ const result = db2.prepare(`
663
+ SELECT
664
+ COUNT(*) as sessionCount,
665
+ COALESCE(MIN(s.updated_at), '') as oldestDate,
666
+ COALESCE(MAX(s.updated_at), '') as newestDate
667
+ FROM sessions s
668
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
669
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
670
+ AND f.id IS NULL
671
+ `).get(days);
672
+ const messageResult = db2.prepare(`
673
+ SELECT COALESCE(SUM(s.message_count), 0) as messageCount
674
+ FROM sessions s
675
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
676
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
677
+ AND f.id IS NULL
678
+ `).get(days);
679
+ const sessionsWithSourceFiles = db2.prepare(`
680
+ SELECT s.source_file as sourceFile
681
+ FROM sessions s
682
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
683
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
684
+ AND f.id IS NULL
685
+ AND s.source_file IS NOT NULL
686
+ `).all(days);
687
+ let backupFilesToDelete = 0;
688
+ let backupBytesToFree = 0;
689
+ for (const session of sessionsWithSourceFiles) {
690
+ const backupPath = getBackupPath(session.sourceFile);
691
+ if (existsSync4(backupPath)) {
692
+ backupFilesToDelete++;
693
+ try {
694
+ backupBytesToFree += statSync2(backupPath).size;
695
+ } catch {
696
+ }
697
+ }
698
+ }
699
+ return {
700
+ sessionsToDelete: result.sessionCount,
701
+ messagesInvolved: messageResult.messageCount,
702
+ oldestSessionDate: result.oldestDate ? formatDate(result.oldestDate) : "",
703
+ newestSessionDate: result.newestDate ? formatDate(result.newestDate) : "",
704
+ backupFilesToDelete,
705
+ backupBytesToFree
706
+ };
707
+ }
708
+ function purgeOldSessions(days) {
709
+ const db2 = getDatabase();
710
+ const sessionsToDelete = db2.prepare(`
711
+ SELECT s.id, s.source_file as sourceFile FROM sessions s
712
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
713
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
714
+ AND f.id IS NULL
715
+ `).all(days);
716
+ if (sessionsToDelete.length === 0) {
717
+ return { sessionsDeleted: 0, backupsDeleted: 0 };
718
+ }
719
+ let backupsDeleted = 0;
720
+ for (const session of sessionsToDelete) {
721
+ if (session.sourceFile) {
722
+ const backupPath = getBackupPath(session.sourceFile);
723
+ if (existsSync4(backupPath)) {
724
+ try {
725
+ unlinkSync(backupPath);
726
+ backupsDeleted++;
727
+ const parentDir = dirname3(backupPath);
728
+ try {
729
+ const remaining = readdirSync2(parentDir);
730
+ if (remaining.length === 0) {
731
+ rmdirSync(parentDir);
732
+ }
733
+ } catch {
734
+ }
735
+ } catch {
736
+ }
737
+ }
738
+ }
739
+ }
740
+ const transaction = db2.transaction(() => {
741
+ for (const session of sessionsToDelete) {
742
+ db2.prepare("DELETE FROM session_embeddings WHERE session_id = ?").run(session.id);
743
+ db2.prepare("DELETE FROM embedding_state WHERE session_id = ?").run(session.id);
744
+ db2.prepare("DELETE FROM messages WHERE session_id = ?").run(session.id);
745
+ db2.prepare("DELETE FROM sessions WHERE id = ?").run(session.id);
746
+ }
747
+ });
748
+ transaction();
749
+ return { sessionsDeleted: sessionsToDelete.length, backupsDeleted };
750
+ }
751
+ function formatDate(isoDate) {
752
+ try {
753
+ const date = new Date(isoDate);
754
+ return date.toLocaleDateString("en-US", {
755
+ year: "numeric",
756
+ month: "short",
757
+ day: "numeric"
758
+ });
759
+ } catch {
760
+ return isoDate;
761
+ }
762
+ }
763
+ function formatBytes(bytes) {
764
+ if (bytes === 0) return "0 B";
765
+ const k = 1024;
766
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
767
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
768
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
769
+ }
770
+
926
771
  // src/db/sessions.ts
927
772
  import { randomUUID } from "crypto";
928
773
  function createSession(input) {
@@ -1126,130 +971,546 @@ function renameSession(id, customTitle) {
1126
971
  UPDATE sessions SET custom_title = ?, updated_at = ? WHERE id = ?
1127
972
  `).run(customTitle, (/* @__PURE__ */ new Date()).toISOString(), id);
1128
973
  }
1129
- function getStats() {
974
+ function getStats() {
975
+ const db2 = getDatabase();
976
+ const sessionCount = db2.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
977
+ const messageCount = db2.prepare("SELECT COUNT(*) as count FROM messages").get().count;
978
+ const embeddingCount = db2.prepare("SELECT COUNT(*) as count FROM session_embeddings").get().count;
979
+ return { sessionCount, messageCount, embeddingCount };
980
+ }
981
+ function sessionExists(rawData) {
982
+ const db2 = getDatabase();
983
+ const row = db2.prepare("SELECT 1 FROM sessions WHERE raw_data = ? LIMIT 1").get(rawData);
984
+ return !!row;
985
+ }
986
+ function getEmbeddingState(sessionId) {
987
+ const db2 = getDatabase();
988
+ const row = db2.prepare(`
989
+ SELECT session_id as sessionId, content_length as contentLength,
990
+ file_mtime as fileMtime, last_embedded_at as lastEmbeddedAt
991
+ FROM embedding_state WHERE session_id = ?
992
+ `).get(sessionId);
993
+ return row || null;
994
+ }
995
+ function updateEmbeddingState(sessionId, contentLength, fileMtime) {
996
+ const db2 = getDatabase();
997
+ const now = (/* @__PURE__ */ new Date()).toISOString();
998
+ db2.prepare(`
999
+ INSERT OR REPLACE INTO embedding_state (session_id, content_length, file_mtime, last_embedded_at)
1000
+ VALUES (?, ?, ?, ?)
1001
+ `).run(sessionId, contentLength, fileMtime || null, now);
1002
+ }
1003
+ function needsReembedding(sessionId, currentContentLength, threshold = 500) {
1004
+ const state = getEmbeddingState(sessionId);
1005
+ if (!state) return true;
1006
+ return currentContentLength - state.contentLength >= threshold;
1007
+ }
1008
+ function addFavorite(type, value) {
1009
+ const db2 = getDatabase();
1010
+ try {
1011
+ db2.prepare(`
1012
+ INSERT OR IGNORE INTO favorites (type, value, created_at)
1013
+ VALUES (?, ?, ?)
1014
+ `).run(type, value, (/* @__PURE__ */ new Date()).toISOString());
1015
+ return true;
1016
+ } catch {
1017
+ return false;
1018
+ }
1019
+ }
1020
+ function removeFavorite(type, value) {
1021
+ const db2 = getDatabase();
1022
+ const result = db2.prepare(`
1023
+ DELETE FROM favorites WHERE type = ? AND value = ?
1024
+ `).run(type, value);
1025
+ return result.changes > 0;
1026
+ }
1027
+ function toggleFavorite(type, value) {
1028
+ if (isFavorite(type, value)) {
1029
+ removeFavorite(type, value);
1030
+ return false;
1031
+ } else {
1032
+ addFavorite(type, value);
1033
+ return true;
1034
+ }
1035
+ }
1036
+ function isFavorite(type, value) {
1037
+ const db2 = getDatabase();
1038
+ const row = db2.prepare(`
1039
+ SELECT 1 FROM favorites WHERE type = ? AND value = ?
1040
+ `).get(type, value);
1041
+ return !!row;
1042
+ }
1043
+ function getFavorites(type) {
1130
1044
  const db2 = getDatabase();
1131
- const sessionCount = db2.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
1132
- const messageCount = db2.prepare("SELECT COUNT(*) as count FROM messages").get().count;
1133
- const embeddingCount = db2.prepare("SELECT COUNT(*) as count FROM session_embeddings").get().count;
1134
- return { sessionCount, messageCount, embeddingCount };
1045
+ const rows = db2.prepare(`
1046
+ SELECT id, type, value, created_at as createdAt
1047
+ FROM favorites
1048
+ WHERE type = ?
1049
+ ORDER BY created_at DESC
1050
+ `).all(type);
1051
+ return rows;
1135
1052
  }
1136
- function sessionExists(rawData) {
1053
+ function getFavoriteSessionIds() {
1054
+ const favorites = getFavorites("session");
1055
+ return new Set(favorites.map((f) => f.value));
1056
+ }
1057
+ function hasFavoriteSessions() {
1137
1058
  const db2 = getDatabase();
1138
- const row = db2.prepare("SELECT 1 FROM sessions WHERE raw_data = ? LIMIT 1").get(rawData);
1059
+ const row = db2.prepare(`
1060
+ SELECT 1 FROM favorites WHERE type = 'session' LIMIT 1
1061
+ `).get();
1139
1062
  return !!row;
1140
1063
  }
1141
- function getEmbeddingState(sessionId) {
1064
+ function getProjectOrders() {
1142
1065
  const db2 = getDatabase();
1143
- const row = db2.prepare(`
1144
- SELECT session_id as sessionId, content_length as contentLength,
1145
- file_mtime as fileMtime, last_embedded_at as lastEmbeddedAt
1146
- FROM embedding_state WHERE session_id = ?
1147
- `).get(sessionId);
1148
- return row || null;
1066
+ const rows = db2.prepare(`
1067
+ SELECT path, sort_order as sortOrder
1068
+ FROM project_order
1069
+ ORDER BY sort_order ASC
1070
+ `).all();
1071
+ const map = /* @__PURE__ */ new Map();
1072
+ for (const row of rows) {
1073
+ map.set(row.path, row.sortOrder);
1074
+ }
1075
+ return map;
1149
1076
  }
1150
- function updateEmbeddingState(sessionId, contentLength, fileMtime) {
1077
+ function updateProjectOrders(orders) {
1151
1078
  const db2 = getDatabase();
1152
1079
  const now = (/* @__PURE__ */ new Date()).toISOString();
1153
- db2.prepare(`
1154
- INSERT OR REPLACE INTO embedding_state (session_id, content_length, file_mtime, last_embedded_at)
1155
- VALUES (?, ?, ?, ?)
1156
- `).run(sessionId, contentLength, fileMtime || null, now);
1157
- }
1158
- function needsReembedding(sessionId, currentContentLength, threshold = 500) {
1159
- const state = getEmbeddingState(sessionId);
1160
- if (!state) return true;
1161
- return currentContentLength - state.contentLength >= threshold;
1080
+ const stmt = db2.prepare(`
1081
+ INSERT OR REPLACE INTO project_order (path, sort_order, updated_at)
1082
+ VALUES (?, ?, ?)
1083
+ `);
1084
+ const transaction = db2.transaction(() => {
1085
+ for (const order of orders) {
1086
+ stmt.run(order.path, order.sortOrder, now);
1087
+ }
1088
+ });
1089
+ transaction();
1162
1090
  }
1163
- function addFavorite(type, value) {
1091
+ function hasCustomProjectOrder() {
1164
1092
  const db2 = getDatabase();
1165
- try {
1166
- db2.prepare(`
1167
- INSERT OR IGNORE INTO favorites (type, value, created_at)
1168
- VALUES (?, ?, ?)
1169
- `).run(type, value, (/* @__PURE__ */ new Date()).toISOString());
1170
- return true;
1171
- } catch {
1172
- return false;
1093
+ const row = db2.prepare(`
1094
+ SELECT 1 FROM project_order LIMIT 1
1095
+ `).get();
1096
+ return !!row;
1097
+ }
1098
+
1099
+ // src/ui/components/StorageScreen.tsx
1100
+ import { Box, Text } from "ink";
1101
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1102
+ var StorageScreen = ({
1103
+ stats,
1104
+ purgeOptions,
1105
+ selectedOption,
1106
+ mode
1107
+ }) => {
1108
+ const selectedPurge = selectedOption !== null ? purgeOptions[selectedOption] : null;
1109
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1110
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Storage Management" }) }),
1111
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1112
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Current Storage:" }),
1113
+ /* @__PURE__ */ jsxs(Text, { children: [
1114
+ " Database: ",
1115
+ formatBytes(stats.dbSize)
1116
+ ] }),
1117
+ /* @__PURE__ */ jsxs(Text, { children: [
1118
+ " Backups: ",
1119
+ formatBytes(stats.backupsSize)
1120
+ ] }),
1121
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1122
+ " Total: ",
1123
+ formatBytes(stats.totalSize)
1124
+ ] }),
1125
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1126
+ " (",
1127
+ stats.sessionCount,
1128
+ " sessions, ",
1129
+ stats.messageCount,
1130
+ " messages)"
1131
+ ] })
1132
+ ] }),
1133
+ mode === "view" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1134
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1135
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Purge Old Sessions:" }),
1136
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (Deletes both database entries and backup files)" }),
1137
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (Starred sessions are always preserved)" })
1138
+ ] }),
1139
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: purgeOptions.map((option, index) => {
1140
+ const hasData = option.preview.sessionsToDelete > 0;
1141
+ return /* @__PURE__ */ jsxs(Box, { children: [
1142
+ /* @__PURE__ */ jsxs(Text, { color: hasData ? "yellow" : "gray", children: [
1143
+ "[",
1144
+ index + 1,
1145
+ "] Older than ",
1146
+ option.days,
1147
+ " days: ",
1148
+ " "
1149
+ ] }),
1150
+ hasData ? /* @__PURE__ */ jsxs(Text, { children: [
1151
+ option.preview.sessionsToDelete,
1152
+ " sessions",
1153
+ option.preview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs(Text, { children: [
1154
+ ", ",
1155
+ option.preview.backupFilesToDelete,
1156
+ " backups"
1157
+ ] }),
1158
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1159
+ " (~",
1160
+ formatBytes(option.preview.backupBytesToFree),
1161
+ " freed)"
1162
+ ] })
1163
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No sessions to purge" })
1164
+ ] }, option.days);
1165
+ }) }),
1166
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[1/2/3] Select option [Esc] Back" }) })
1167
+ ] }) : (
1168
+ /* Confirm mode */
1169
+ selectedPurge && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1170
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "Confirm Purge:" }),
1171
+ /* @__PURE__ */ jsxs(Text, { children: [
1172
+ " Period: Older than ",
1173
+ selectedPurge.days,
1174
+ " days"
1175
+ ] }),
1176
+ /* @__PURE__ */ jsxs(Text, { children: [
1177
+ " Sessions: ",
1178
+ selectedPurge.preview.sessionsToDelete
1179
+ ] }),
1180
+ /* @__PURE__ */ jsxs(Text, { children: [
1181
+ " Messages: ",
1182
+ selectedPurge.preview.messagesInvolved
1183
+ ] }),
1184
+ selectedPurge.preview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs(Text, { children: [
1185
+ " Backup files: ",
1186
+ selectedPurge.preview.backupFilesToDelete
1187
+ ] }),
1188
+ /* @__PURE__ */ jsxs(Text, { children: [
1189
+ " Date range: ",
1190
+ selectedPurge.preview.oldestSessionDate,
1191
+ " to ",
1192
+ selectedPurge.preview.newestSessionDate
1193
+ ] }),
1194
+ /* @__PURE__ */ jsxs(Text, { children: [
1195
+ " Space freed: ~",
1196
+ formatBytes(selectedPurge.preview.backupBytesToFree)
1197
+ ] }),
1198
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: "This cannot be undone. Proceed? [y/n]" }) })
1199
+ ] })
1200
+ )
1201
+ ] });
1202
+ };
1203
+
1204
+ // src/ui/components/Header.tsx
1205
+ import { Box as Box2, Text as Text2 } from "ink";
1206
+ import { basename as basename3 } from "path";
1207
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1208
+ var Header = ({ embeddingsReady, projectFilter }) => {
1209
+ return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, justifyContent: "space-between", children: [
1210
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1211
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "cmem" }),
1212
+ projectFilter && /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
1213
+ " \u{1F4C1} ",
1214
+ basename3(projectFilter)
1215
+ ] }),
1216
+ !embeddingsReady && /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: " (loading model...)" })
1217
+ ] }),
1218
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[q] quit" })
1219
+ ] });
1220
+ };
1221
+
1222
+ // src/ui/components/SearchInput.tsx
1223
+ import { Box as Box3, Text as Text3 } from "ink";
1224
+ import TextInput from "ink-text-input";
1225
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1226
+ var SearchInput = ({
1227
+ value,
1228
+ onChange,
1229
+ isFocused
1230
+ }) => {
1231
+ return /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
1232
+ /* @__PURE__ */ jsx3(Text3, { children: "Search: " }),
1233
+ isFocused ? /* @__PURE__ */ jsx3(
1234
+ TextInput,
1235
+ {
1236
+ value,
1237
+ onChange,
1238
+ placeholder: "type to search...",
1239
+ focus: true
1240
+ }
1241
+ ) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: value || "press / to search" })
1242
+ ] });
1243
+ };
1244
+
1245
+ // src/ui/components/SessionList.tsx
1246
+ import { Box as Box4, Text as Text4 } from "ink";
1247
+ import { basename as basename4 } from "path";
1248
+
1249
+ // src/utils/format.ts
1250
+ function formatTimeAgo(timestamp) {
1251
+ const date = new Date(timestamp);
1252
+ const now = /* @__PURE__ */ new Date();
1253
+ const diffMs = now.getTime() - date.getTime();
1254
+ const diffSecs = Math.floor(diffMs / 1e3);
1255
+ const diffMins = Math.floor(diffSecs / 60);
1256
+ const diffHours = Math.floor(diffMins / 60);
1257
+ const diffDays = Math.floor(diffHours / 24);
1258
+ const diffWeeks = Math.floor(diffDays / 7);
1259
+ const diffMonths = Math.floor(diffDays / 30);
1260
+ if (diffSecs < 60) return "just now";
1261
+ if (diffMins < 60) return `${diffMins}m ago`;
1262
+ if (diffHours < 24) return `${diffHours}h ago`;
1263
+ if (diffDays < 7) return `${diffDays}d ago`;
1264
+ if (diffWeeks < 4) return `${diffWeeks}w ago`;
1265
+ return `${diffMonths}mo ago`;
1266
+ }
1267
+ function truncate(text, maxLength) {
1268
+ if (text.length <= maxLength) return text;
1269
+ return text.slice(0, maxLength - 3) + "...";
1270
+ }
1271
+ function shortId(id) {
1272
+ return id.slice(0, 8);
1273
+ }
1274
+ function formatNumber(num) {
1275
+ return num.toLocaleString();
1276
+ }
1277
+ function formatBytes2(bytes) {
1278
+ if (bytes === 0) return "0 B";
1279
+ const k = 1024;
1280
+ const sizes = ["B", "KB", "MB", "GB"];
1281
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1282
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
1283
+ }
1284
+ function generateTitle(content) {
1285
+ const firstLine = content.split("\n")[0].trim();
1286
+ const title = truncate(firstLine, 50);
1287
+ return title || "Untitled Session";
1288
+ }
1289
+
1290
+ // src/ui/components/SessionList.tsx
1291
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1292
+ var SessionList = ({
1293
+ sessions,
1294
+ selectedIndex
1295
+ }) => {
1296
+ if (sessions.length === 0) {
1297
+ return /* @__PURE__ */ jsxs4(
1298
+ Box4,
1299
+ {
1300
+ flexDirection: "column",
1301
+ borderStyle: "round",
1302
+ borderColor: "gray",
1303
+ paddingX: 1,
1304
+ paddingY: 0,
1305
+ children: [
1306
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Sessions" }),
1307
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No sessions found" }),
1308
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Run: cmem save --latest" })
1309
+ ]
1310
+ }
1311
+ );
1312
+ }
1313
+ const visibleCount = 8;
1314
+ let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
1315
+ const endIndex = Math.min(sessions.length, startIndex + visibleCount);
1316
+ if (endIndex - startIndex < visibleCount) {
1317
+ startIndex = Math.max(0, endIndex - visibleCount);
1318
+ }
1319
+ const visibleSessions = sessions.slice(startIndex, endIndex);
1320
+ return /* @__PURE__ */ jsxs4(
1321
+ Box4,
1322
+ {
1323
+ flexDirection: "column",
1324
+ borderStyle: "round",
1325
+ borderColor: "gray",
1326
+ paddingX: 1,
1327
+ paddingY: 0,
1328
+ children: [
1329
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1330
+ "Sessions (",
1331
+ sessions.length,
1332
+ ")"
1333
+ ] }),
1334
+ visibleSessions.map((session, i) => {
1335
+ const actualIndex = startIndex + i;
1336
+ const isSelected = actualIndex === selectedIndex;
1337
+ return /* @__PURE__ */ jsx4(
1338
+ SessionItem,
1339
+ {
1340
+ session,
1341
+ isSelected
1342
+ },
1343
+ session.id
1344
+ );
1345
+ }),
1346
+ sessions.length > visibleCount && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1347
+ startIndex > 0 ? "\u2191 more above" : "",
1348
+ startIndex > 0 && endIndex < sessions.length ? " | " : "",
1349
+ endIndex < sessions.length ? "\u2193 more below" : ""
1350
+ ] })
1351
+ ]
1352
+ }
1353
+ );
1354
+ };
1355
+ var SessionItem = ({ session, isSelected }) => {
1356
+ const hasCustomTitle = !!session.customTitle;
1357
+ const displayTitle = truncate(session.customTitle || session.title, 38);
1358
+ const folderName = session.projectPath ? truncate(basename4(session.projectPath), 38) : "";
1359
+ const msgs = String(session.messageCount).padStart(3);
1360
+ const updated = formatTimeAgo(session.updatedAt);
1361
+ const getTitleColor = () => {
1362
+ if (isSelected) return "cyan";
1363
+ if (session.isFavorite) return "yellow";
1364
+ if (hasCustomTitle) return "magenta";
1365
+ return void 0;
1366
+ };
1367
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
1368
+ /* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
1369
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: session.isFavorite ? "\u2B50" : " " }),
1370
+ /* @__PURE__ */ jsx4(Text4, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(38) }),
1371
+ /* @__PURE__ */ jsxs4(Text4, { color: "blue", children: [
1372
+ " ",
1373
+ folderName.padEnd(38)
1374
+ ] }),
1375
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1376
+ " ",
1377
+ msgs,
1378
+ " "
1379
+ ] }),
1380
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: updated.padStart(8) })
1381
+ ] });
1382
+ };
1383
+
1384
+ // src/ui/components/ProjectList.tsx
1385
+ import { Box as Box5, Text as Text5 } from "ink";
1386
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1387
+ var ProjectList = ({
1388
+ projects,
1389
+ selectedIndex
1390
+ }) => {
1391
+ if (projects.length === 0) {
1392
+ return /* @__PURE__ */ jsxs5(
1393
+ Box5,
1394
+ {
1395
+ flexDirection: "column",
1396
+ borderStyle: "round",
1397
+ borderColor: "gray",
1398
+ paddingX: 1,
1399
+ paddingY: 0,
1400
+ children: [
1401
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Projects" }),
1402
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No projects found" }),
1403
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Start using Claude Code in a project directory" })
1404
+ ]
1405
+ }
1406
+ );
1173
1407
  }
1174
- }
1175
- function removeFavorite(type, value) {
1176
- const db2 = getDatabase();
1177
- const result = db2.prepare(`
1178
- DELETE FROM favorites WHERE type = ? AND value = ?
1179
- `).run(type, value);
1180
- return result.changes > 0;
1181
- }
1182
- function toggleFavorite(type, value) {
1183
- if (isFavorite(type, value)) {
1184
- removeFavorite(type, value);
1185
- return false;
1186
- } else {
1187
- addFavorite(type, value);
1188
- return true;
1408
+ const visibleCount = 8;
1409
+ let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
1410
+ const endIndex = Math.min(projects.length, startIndex + visibleCount);
1411
+ if (endIndex - startIndex < visibleCount) {
1412
+ startIndex = Math.max(0, endIndex - visibleCount);
1189
1413
  }
1190
- }
1191
- function isFavorite(type, value) {
1192
- const db2 = getDatabase();
1193
- const row = db2.prepare(`
1194
- SELECT 1 FROM favorites WHERE type = ? AND value = ?
1195
- `).get(type, value);
1196
- return !!row;
1197
- }
1198
- function getFavorites(type) {
1199
- const db2 = getDatabase();
1200
- const rows = db2.prepare(`
1201
- SELECT id, type, value, created_at as createdAt
1202
- FROM favorites
1203
- WHERE type = ?
1204
- ORDER BY created_at DESC
1205
- `).all(type);
1206
- return rows;
1207
- }
1208
- function getFavoriteSessionIds() {
1209
- const favorites = getFavorites("session");
1210
- return new Set(favorites.map((f) => f.value));
1211
- }
1212
- function hasFavoriteSessions() {
1213
- const db2 = getDatabase();
1214
- const row = db2.prepare(`
1215
- SELECT 1 FROM favorites WHERE type = 'session' LIMIT 1
1216
- `).get();
1217
- return !!row;
1218
- }
1219
- function getProjectOrders() {
1220
- const db2 = getDatabase();
1221
- const rows = db2.prepare(`
1222
- SELECT path, sort_order as sortOrder
1223
- FROM project_order
1224
- ORDER BY sort_order ASC
1225
- `).all();
1226
- const map = /* @__PURE__ */ new Map();
1227
- for (const row of rows) {
1228
- map.set(row.path, row.sortOrder);
1414
+ const visibleProjects = projects.slice(startIndex, endIndex);
1415
+ return /* @__PURE__ */ jsxs5(
1416
+ Box5,
1417
+ {
1418
+ flexDirection: "column",
1419
+ borderStyle: "round",
1420
+ borderColor: "gray",
1421
+ paddingX: 1,
1422
+ paddingY: 0,
1423
+ children: [
1424
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
1425
+ "Projects (",
1426
+ projects.length,
1427
+ ")"
1428
+ ] }),
1429
+ visibleProjects.map((project, i) => {
1430
+ const actualIndex = startIndex + i;
1431
+ const isSelected = actualIndex === selectedIndex;
1432
+ return /* @__PURE__ */ jsx5(
1433
+ ProjectItem,
1434
+ {
1435
+ project,
1436
+ isSelected
1437
+ },
1438
+ project.path
1439
+ );
1440
+ }),
1441
+ projects.length > visibleCount && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1442
+ startIndex > 0 ? "\u2191 more above" : "",
1443
+ startIndex > 0 && endIndex < projects.length ? " | " : "",
1444
+ endIndex < projects.length ? "\u2193 more below" : ""
1445
+ ] })
1446
+ ]
1447
+ }
1448
+ );
1449
+ };
1450
+ var ProjectItem = ({ project, isSelected }) => {
1451
+ const displayName = truncate(project.name, 40);
1452
+ const sessions = `${project.sessionCount} session${project.sessionCount !== 1 ? "s" : ""}`;
1453
+ const messages = `${project.totalMessages} msgs`;
1454
+ const updated = formatTimeAgo(project.lastUpdated);
1455
+ const orderBadge = project.sortOrder !== null ? `${project.sortOrder + 1}.` : " ";
1456
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
1457
+ /* @__PURE__ */ jsx5(Text5, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
1458
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1459
+ orderBadge.padStart(3),
1460
+ " "
1461
+ ] }),
1462
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u{1F4C1} " }),
1463
+ /* @__PURE__ */ jsx5(Text5, { bold: isSelected, color: isSelected ? "cyan" : void 0, wrap: "truncate", children: displayName.padEnd(35) }),
1464
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1465
+ " ",
1466
+ sessions.padEnd(12)
1467
+ ] }),
1468
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1469
+ " ",
1470
+ messages.padEnd(10)
1471
+ ] }),
1472
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: updated.padStart(10) })
1473
+ ] });
1474
+ };
1475
+
1476
+ // src/ui/components/Preview.tsx
1477
+ import { Box as Box6, Text as Text6 } from "ink";
1478
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1479
+ var Preview = ({ session }) => {
1480
+ if (!session) {
1481
+ return null;
1229
1482
  }
1230
- return map;
1231
- }
1232
- function updateProjectOrders(orders) {
1233
- const db2 = getDatabase();
1234
- const now = (/* @__PURE__ */ new Date()).toISOString();
1235
- const stmt = db2.prepare(`
1236
- INSERT OR REPLACE INTO project_order (path, sort_order, updated_at)
1237
- VALUES (?, ?, ?)
1238
- `);
1239
- const transaction = db2.transaction(() => {
1240
- for (const order of orders) {
1241
- stmt.run(order.path, order.sortOrder, now);
1483
+ const summary = session.summary ? truncate(session.summary, 200) : "No summary available";
1484
+ return /* @__PURE__ */ jsxs6(
1485
+ Box6,
1486
+ {
1487
+ flexDirection: "column",
1488
+ borderStyle: "round",
1489
+ borderColor: "gray",
1490
+ paddingX: 1,
1491
+ paddingY: 0,
1492
+ marginTop: 1,
1493
+ children: [
1494
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1495
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Preview" }),
1496
+ session.isFavorite && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: " \u2B50" }),
1497
+ session.projectPath && /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "blue", children: [
1498
+ " \u{1F4C1} ",
1499
+ session.projectPath
1500
+ ] })
1501
+ ] }),
1502
+ /* @__PURE__ */ jsx6(Text6, { wrap: "wrap", children: summary }),
1503
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1504
+ "Messages: ",
1505
+ session.messageCount
1506
+ ] })
1507
+ ]
1242
1508
  }
1243
- });
1244
- transaction();
1245
- }
1246
- function hasCustomProjectOrder() {
1247
- const db2 = getDatabase();
1248
- const row = db2.prepare(`
1249
- SELECT 1 FROM project_order LIMIT 1
1250
- `).get();
1251
- return !!row;
1252
- }
1509
+ );
1510
+ };
1511
+
1512
+ // src/ui/hooks/useSessions.ts
1513
+ import { useState, useEffect, useCallback } from "react";
1253
1514
 
1254
1515
  // src/db/vectors.ts
1255
1516
  function storeEmbedding(sessionId, embedding) {
@@ -1276,8 +1537,8 @@ function searchSessions(queryEmbedding, limit = 10) {
1276
1537
  }
1277
1538
 
1278
1539
  // src/embeddings/index.ts
1279
- import { existsSync as existsSync4 } from "fs";
1280
- import { join as join3 } from "path";
1540
+ import { existsSync as existsSync5 } from "fs";
1541
+ import { join as join4 } from "path";
1281
1542
  var transformersModule = null;
1282
1543
  var pipeline = null;
1283
1544
  var initialized = false;
@@ -1288,8 +1549,8 @@ async function getTransformers() {
1288
1549
  return transformersModule;
1289
1550
  }
1290
1551
  function isModelCached() {
1291
- const modelCachePath = join3(MODELS_DIR, EMBEDDING_MODEL);
1292
- return existsSync4(modelCachePath);
1552
+ const modelCachePath = join4(MODELS_DIR, EMBEDDING_MODEL);
1553
+ return existsSync5(modelCachePath);
1293
1554
  }
1294
1555
  async function initializeEmbeddings(onProgress) {
1295
1556
  if (initialized && pipeline) {
@@ -1353,16 +1614,16 @@ function createEmbeddingText(title, summary, messages) {
1353
1614
  }
1354
1615
 
1355
1616
  // src/ui/hooks/useSessions.ts
1356
- import { existsSync as existsSync5 } from "fs";
1617
+ import { existsSync as existsSync6 } from "fs";
1357
1618
  function isRecoverable(session) {
1358
1619
  if (!session.sourceFile) return false;
1359
- if (existsSync5(session.sourceFile)) return true;
1620
+ if (existsSync6(session.sourceFile)) return true;
1360
1621
  if (hasBackup(session.sourceFile)) return true;
1361
1622
  return false;
1362
1623
  }
1363
1624
  function ensureBackedUp(session) {
1364
1625
  if (!session.sourceFile) return;
1365
- if (existsSync5(session.sourceFile) && !hasBackup(session.sourceFile)) {
1626
+ if (existsSync6(session.sourceFile) && !hasBackup(session.sourceFile)) {
1366
1627
  backupSessionFile(session.sourceFile);
1367
1628
  }
1368
1629
  }
@@ -1570,7 +1831,7 @@ function useSessions(options = {}) {
1570
1831
  }
1571
1832
 
1572
1833
  // src/ui/App.tsx
1573
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1834
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1574
1835
  var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1575
1836
  const { exit } = useApp();
1576
1837
  const {
@@ -1595,6 +1856,12 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1595
1856
  const [currentTab, setCurrentTab] = useState2("global");
1596
1857
  const [selectedProjectPath, setSelectedProjectPath] = useState2(null);
1597
1858
  const [renderKey, setRenderKey] = useState2(0);
1859
+ const [dbSizeAlertShown, setDbSizeAlertShown] = useState2(false);
1860
+ const [purgePreview, setPurgePreview] = useState2(null);
1861
+ const [dbSize, setDbSize] = useState2(0);
1862
+ const [storageStats, setStorageStats] = useState2(null);
1863
+ const [purgeOptions, setPurgeOptions] = useState2([]);
1864
+ const [selectedPurgeOption, setSelectedPurgeOption] = useState2(null);
1598
1865
  const getCurrentView = useCallback2(() => {
1599
1866
  if (currentTab === "projects") {
1600
1867
  return selectedProjectPath ? "project-sessions" : "projects";
@@ -1606,6 +1873,20 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1606
1873
  useEffect2(() => {
1607
1874
  setSelectedIndex(0);
1608
1875
  }, [currentTab, selectedProjectPath]);
1876
+ useEffect2(() => {
1877
+ if (!loading && !dbSizeAlertShown) {
1878
+ const size = getTotalStorageSize();
1879
+ if (size >= DB_SIZE_ALERT_THRESHOLD) {
1880
+ const preview = getPurgePreview(DEFAULT_PURGE_DAYS);
1881
+ if (preview.sessionsToDelete > 0) {
1882
+ setDbSize(size);
1883
+ setPurgePreview(preview);
1884
+ setMode("db-size-alert");
1885
+ }
1886
+ }
1887
+ setDbSizeAlertShown(true);
1888
+ }
1889
+ }, [loading, dbSizeAlertShown]);
1609
1890
  useEffect2(() => {
1610
1891
  const maxIndex = currentView === "projects" ? projects.length - 1 : currentView === "project-sessions" ? projectSessions.length - 1 : sessions.length - 1;
1611
1892
  if (selectedIndex > maxIndex) {
@@ -1639,7 +1920,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1639
1920
  const session = currentSessions2[selectedIndex];
1640
1921
  if (!session) return;
1641
1922
  if (session.sourceFile) {
1642
- if (!existsSync6(session.sourceFile)) {
1923
+ if (!existsSync7(session.sourceFile)) {
1643
1924
  if (hasBackup(session.sourceFile)) {
1644
1925
  const restored = restoreFromBackup(session.sourceFile);
1645
1926
  if (restored) {
@@ -1657,7 +1938,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1657
1938
  const claudeSessionId = filename.replace(".jsonl", "");
1658
1939
  let projectPath = session.projectPath;
1659
1940
  if (!projectPath) {
1660
- const projectDirName = basename5(dirname3(session.sourceFile));
1941
+ const projectDirName = basename5(dirname4(session.sourceFile));
1661
1942
  if (projectDirName.startsWith("-")) {
1662
1943
  const segments = projectDirName.substring(1).split("-");
1663
1944
  let currentPath = "";
@@ -1667,7 +1948,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1667
1948
  for (let i = remainingSegments.length; i > 0; i--) {
1668
1949
  const testSegment = remainingSegments.slice(0, i).join("-");
1669
1950
  const testPath = currentPath + "/" + testSegment;
1670
- if (existsSync6(testPath)) {
1951
+ if (existsSync7(testPath)) {
1671
1952
  currentPath = testPath;
1672
1953
  remainingSegments = remainingSegments.slice(i);
1673
1954
  found = true;
@@ -1679,7 +1960,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1679
1960
  break;
1680
1961
  }
1681
1962
  }
1682
- if (currentPath && existsSync6(currentPath)) {
1963
+ if (currentPath && existsSync7(currentPath)) {
1683
1964
  projectPath = currentPath;
1684
1965
  }
1685
1966
  }
@@ -1736,7 +2017,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1736
2017
  setMode("list");
1737
2018
  }, [getCurrentSessions, selectedIndex, refresh]);
1738
2019
  useInput((input, key) => {
1739
- if (input === "q" && mode !== "search" && mode !== "rename" && mode !== "sort-project") {
2020
+ if (input === "q" && mode !== "search" && mode !== "rename" && mode !== "sort-project" && mode !== "db-size-alert" && mode !== "confirm-purge" && mode !== "storage" && mode !== "storage-confirm") {
1740
2021
  exit();
1741
2022
  return;
1742
2023
  }
@@ -1794,10 +2075,93 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1794
2075
  }
1795
2076
  return;
1796
2077
  }
2078
+ if (mode === "db-size-alert") {
2079
+ if (input === "y" || input === "Y") {
2080
+ setMode("confirm-purge");
2081
+ } else {
2082
+ setMode("list");
2083
+ }
2084
+ return;
2085
+ }
2086
+ if (mode === "confirm-purge") {
2087
+ if (input === "y" || input === "Y") {
2088
+ const result = purgeOldSessions(DEFAULT_PURGE_DAYS);
2089
+ const parts = [];
2090
+ if (result.sessionsDeleted > 0) {
2091
+ parts.push(`${result.sessionsDeleted} session${result.sessionsDeleted !== 1 ? "s" : ""}`);
2092
+ }
2093
+ if (result.backupsDeleted > 0) {
2094
+ parts.push(`${result.backupsDeleted} backup${result.backupsDeleted !== 1 ? "s" : ""}`);
2095
+ }
2096
+ setStatusMessage(`Purged ${parts.join(" and ")}`);
2097
+ refresh();
2098
+ setMode("list");
2099
+ } else {
2100
+ setMode("list");
2101
+ }
2102
+ return;
2103
+ }
2104
+ if (mode === "storage") {
2105
+ if (key.escape || input === "q") {
2106
+ setMode("list");
2107
+ return;
2108
+ }
2109
+ const optionIndex = parseInt(input, 10) - 1;
2110
+ if (optionIndex >= 0 && optionIndex < purgeOptions.length) {
2111
+ const option = purgeOptions[optionIndex];
2112
+ if (option && option.preview.sessionsToDelete > 0) {
2113
+ setSelectedPurgeOption(optionIndex);
2114
+ setMode("storage-confirm");
2115
+ }
2116
+ }
2117
+ return;
2118
+ }
2119
+ if (mode === "storage-confirm") {
2120
+ if (input === "y" || input === "Y") {
2121
+ if (selectedPurgeOption !== null && purgeOptions[selectedPurgeOption]) {
2122
+ const days = purgeOptions[selectedPurgeOption].days;
2123
+ const result = purgeOldSessions(days);
2124
+ const parts = [];
2125
+ if (result.sessionsDeleted > 0) {
2126
+ parts.push(`${result.sessionsDeleted} session${result.sessionsDeleted !== 1 ? "s" : ""}`);
2127
+ }
2128
+ if (result.backupsDeleted > 0) {
2129
+ parts.push(`${result.backupsDeleted} backup${result.backupsDeleted !== 1 ? "s" : ""}`);
2130
+ }
2131
+ setStatusMessage(`Purged ${parts.join(" and ")}`);
2132
+ refresh();
2133
+ setSelectedPurgeOption(null);
2134
+ setMode("list");
2135
+ }
2136
+ } else {
2137
+ setSelectedPurgeOption(null);
2138
+ setMode("storage");
2139
+ }
2140
+ return;
2141
+ }
1797
2142
  if (input === "/") {
1798
2143
  setMode("search");
1799
2144
  return;
1800
2145
  }
2146
+ if (input === "P") {
2147
+ const dbStats = getStats();
2148
+ const dbSizeVal = getDatabaseSize();
2149
+ const backupsSizeVal = getBackupsDirSize();
2150
+ setStorageStats({
2151
+ dbSize: dbSizeVal,
2152
+ backupsSize: backupsSizeVal,
2153
+ totalSize: dbSizeVal + backupsSizeVal,
2154
+ sessionCount: dbStats.sessionCount,
2155
+ messageCount: dbStats.messageCount
2156
+ });
2157
+ setPurgeOptions([
2158
+ { days: 15, preview: getPurgePreview(15) },
2159
+ { days: 30, preview: getPurgePreview(30) },
2160
+ { days: 60, preview: getPurgePreview(60) }
2161
+ ]);
2162
+ setMode("storage");
2163
+ return;
2164
+ }
1801
2165
  if ((key.escape || key.backspace || key.delete) && currentView === "project-sessions") {
1802
2166
  handleBackToProjects();
1803
2167
  return;
@@ -1876,35 +2240,46 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1876
2240
  setSelectedIndex(0);
1877
2241
  }, []);
1878
2242
  if (loading && sessions.length === 0) {
1879
- return /* @__PURE__ */ jsxs6(Box6, { padding: 1, children: [
1880
- /* @__PURE__ */ jsx6(Spinner, { type: "dots" }),
1881
- /* @__PURE__ */ jsx6(Text6, { children: " Loading sessions..." })
2243
+ return /* @__PURE__ */ jsxs7(Box7, { padding: 1, children: [
2244
+ /* @__PURE__ */ jsx7(Spinner, { type: "dots" }),
2245
+ /* @__PURE__ */ jsx7(Text7, { children: " Loading sessions..." })
1882
2246
  ] });
1883
2247
  }
1884
2248
  const currentSessions = getCurrentSessions();
1885
2249
  const selectedSession = currentView !== "projects" ? currentSessions[selectedIndex] || null : null;
1886
2250
  const selectedProject = currentView === "projects" ? projects[selectedIndex] || null : null;
1887
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
1888
- /* @__PURE__ */ jsx6(Header, { embeddingsReady, projectFilter: selectedProjectPath }),
1889
- currentTab === "global" ? /* @__PURE__ */ jsx6(
2251
+ if ((mode === "storage" || mode === "storage-confirm") && storageStats) {
2252
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx7(
2253
+ StorageScreen,
2254
+ {
2255
+ stats: storageStats,
2256
+ purgeOptions,
2257
+ selectedOption: selectedPurgeOption,
2258
+ mode: mode === "storage-confirm" ? "confirm" : "view"
2259
+ }
2260
+ ) }, renderKey);
2261
+ }
2262
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
2263
+ /* @__PURE__ */ jsx7(Header, { embeddingsReady, projectFilter: selectedProjectPath }),
2264
+ currentTab === "global" ? /* @__PURE__ */ jsx7(
1890
2265
  SearchInput,
1891
2266
  {
1892
2267
  value: searchQuery,
1893
2268
  onChange: handleSearchChange,
1894
2269
  isFocused: mode === "search"
1895
2270
  }
1896
- ) : currentView === "project-sessions" && selectedProjectPath ? /* @__PURE__ */ jsxs6(Box6, { children: [
1897
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Projects \u2192 " }),
1898
- /* @__PURE__ */ jsx6(Text6, { color: "blue", children: basename5(selectedProjectPath) })
1899
- ] }) : /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Browse projects below" }) }),
1900
- currentView === "projects" ? /* @__PURE__ */ jsx6(
2271
+ ) : currentView === "project-sessions" && selectedProjectPath ? /* @__PURE__ */ jsxs7(Box7, { children: [
2272
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Projects \u2192 " }),
2273
+ /* @__PURE__ */ jsx7(Text7, { color: "blue", children: basename5(selectedProjectPath) })
2274
+ ] }) : /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Browse projects below" }) }),
2275
+ currentView === "projects" ? /* @__PURE__ */ jsx7(
1901
2276
  ProjectList,
1902
2277
  {
1903
2278
  projects,
1904
2279
  selectedIndex,
1905
2280
  onSelect: setSelectedIndex
1906
2281
  }
1907
- ) : /* @__PURE__ */ jsx6(
2282
+ ) : /* @__PURE__ */ jsx7(
1908
2283
  SessionList,
1909
2284
  {
1910
2285
  sessions: currentSessions,
@@ -1912,9 +2287,9 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1912
2287
  onSelect: setSelectedIndex
1913
2288
  }
1914
2289
  ),
1915
- selectedSession && /* @__PURE__ */ jsx6(Preview, { session: selectedSession }),
1916
- selectedProject && /* @__PURE__ */ jsxs6(
1917
- Box6,
2290
+ selectedSession && /* @__PURE__ */ jsx7(Preview, { session: selectedSession }),
2291
+ selectedProject && /* @__PURE__ */ jsxs7(
2292
+ Box7,
1918
2293
  {
1919
2294
  flexDirection: "column",
1920
2295
  borderStyle: "round",
@@ -1923,19 +2298,19 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1923
2298
  paddingY: 0,
1924
2299
  marginTop: 1,
1925
2300
  children: [
1926
- /* @__PURE__ */ jsxs6(Box6, { children: [
1927
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Project Preview" }),
1928
- selectedProject.sortOrder !== null && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2301
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2302
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Project Preview" }),
2303
+ selectedProject.sortOrder !== null && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1929
2304
  " (#",
1930
2305
  selectedProject.sortOrder + 1,
1931
2306
  ")"
1932
2307
  ] })
1933
2308
  ] }),
1934
- /* @__PURE__ */ jsxs6(Text6, { color: "blue", children: [
2309
+ /* @__PURE__ */ jsxs7(Text7, { color: "blue", children: [
1935
2310
  "\u{1F4C1} ",
1936
2311
  selectedProject.path
1937
2312
  ] }),
1938
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2313
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1939
2314
  selectedProject.sessionCount,
1940
2315
  " session",
1941
2316
  selectedProject.sessionCount !== 1 ? "s" : "",
@@ -1946,13 +2321,57 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1946
2321
  ]
1947
2322
  }
1948
2323
  ),
1949
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
2324
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: mode === "db-size-alert" ? /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2325
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", bold: true, children: [
2326
+ "\u26A0 Storage size: ",
2327
+ formatBytes(dbSize),
2328
+ " (exceeds 5GB)"
2329
+ ] }),
2330
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
2331
+ "Purge ",
2332
+ purgePreview?.sessionsToDelete,
2333
+ " sessions older than ",
2334
+ DEFAULT_PURGE_DAYS,
2335
+ " days? [y/n]"
2336
+ ] }),
2337
+ purgePreview && purgePreview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2338
+ " (",
2339
+ purgePreview.backupFilesToDelete,
2340
+ " backup files, ~",
2341
+ formatBytes(purgePreview.backupBytesToFree),
2342
+ ")"
2343
+ ] }),
2344
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(Starred sessions will be kept)" })
2345
+ ] }) : mode === "confirm-purge" ? /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2346
+ /* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "Confirm purge:" }),
2347
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2348
+ " ",
2349
+ purgePreview?.sessionsToDelete,
2350
+ " sessions (",
2351
+ purgePreview?.messagesInvolved,
2352
+ " messages)"
2353
+ ] }),
2354
+ purgePreview && purgePreview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs7(Text7, { children: [
2355
+ " ",
2356
+ purgePreview.backupFilesToDelete,
2357
+ " backup files (~",
2358
+ formatBytes(purgePreview.backupBytesToFree),
2359
+ ")"
2360
+ ] }),
2361
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2362
+ " Range: ",
2363
+ purgePreview?.oldestSessionDate,
2364
+ " to ",
2365
+ purgePreview?.newestSessionDate
2366
+ ] }),
2367
+ /* @__PURE__ */ jsx7(Text7, { color: "red", children: "This cannot be undone. Proceed? [y/n]" })
2368
+ ] }) : mode === "confirm-delete" ? /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1950
2369
  'Delete "',
1951
2370
  selectedSession?.customTitle || selectedSession?.title,
1952
2371
  '"? [y/n]'
1953
- ] }) : mode === "rename" ? /* @__PURE__ */ jsxs6(Box6, { children: [
1954
- /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: "Rename: " }),
1955
- /* @__PURE__ */ jsx6(
2372
+ ] }) : mode === "rename" ? /* @__PURE__ */ jsxs7(Box7, { children: [
2373
+ /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: "Rename: " }),
2374
+ /* @__PURE__ */ jsx7(
1956
2375
  TextInput2,
1957
2376
  {
1958
2377
  value: renameValue,
@@ -1960,11 +2379,11 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1960
2379
  placeholder: "Enter new name..."
1961
2380
  }
1962
2381
  ),
1963
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1964
- ] }) : mode === "sort-project" ? /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "Sort mode: [\u2191\u2193] Move project [Enter/Esc] Done" }) : statusMessage ? /* @__PURE__ */ jsx6(Text6, { color: "green", children: statusMessage }) : currentView === "projects" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Open [s] Sort [\u2190\u2192] Switch tabs [q] Quit" }) : currentView === "project-sessions" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [Esc] Back [r] Rename [d] Delete [q] Quit" }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [r] Rename [d] Delete [/] Search [q] Quit" }) }),
1965
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1966
- /* @__PURE__ */ jsx6(
1967
- Text6,
2382
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
2383
+ ] }) : mode === "sort-project" ? /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "Sort mode: [\u2191\u2193] Move project [Enter/Esc] Done" }) : statusMessage ? /* @__PURE__ */ jsx7(Text7, { color: "green", children: statusMessage }) : currentView === "projects" ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Open [s] Sort [P] Storage [\u2190\u2192] Tabs [q] Quit" }) : currentView === "project-sessions" ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [Esc] Back [r] Rename [d] Delete [P] Storage [q] Quit" }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [r] Rename [d] Delete [/] Search [P] Storage [q] Quit" }) }),
2384
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
2385
+ /* @__PURE__ */ jsx7(
2386
+ Text7,
1968
2387
  {
1969
2388
  backgroundColor: currentTab === "global" ? "green" : void 0,
1970
2389
  color: currentTab === "global" ? "white" : void 0,
@@ -1973,9 +2392,9 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1973
2392
  children: currentTab === "global" ? " Global " : "Global"
1974
2393
  }
1975
2394
  ),
1976
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
1977
- /* @__PURE__ */ jsx6(
1978
- Text6,
2395
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
2396
+ /* @__PURE__ */ jsx7(
2397
+ Text7,
1979
2398
  {
1980
2399
  backgroundColor: currentTab === "projects" ? "magenta" : void 0,
1981
2400
  color: currentTab === "projects" ? "white" : void 0,
@@ -1984,7 +2403,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1984
2403
  children: currentTab === "projects" ? " Projects " : "Projects"
1985
2404
  }
1986
2405
  ),
1987
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [\u2190\u2192] switch" })
2406
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " [\u2190\u2192] switch" })
1988
2407
  ] })
1989
2408
  ] }, renderKey);
1990
2409
  };
@@ -2284,7 +2703,7 @@ async function deleteCommand(id) {
2284
2703
 
2285
2704
  // src/commands/stats.ts
2286
2705
  import chalk6 from "chalk";
2287
- import { statSync as statSync2, existsSync as existsSync7 } from "fs";
2706
+ import { statSync as statSync3, existsSync as existsSync8 } from "fs";
2288
2707
  async function statsCommand() {
2289
2708
  console.log(chalk6.cyan("cmem Storage Statistics\n"));
2290
2709
  const stats = getStats();
@@ -2292,9 +2711,19 @@ async function statsCommand() {
2292
2711
  console.log(` Sessions: ${formatNumber(stats.sessionCount)}`);
2293
2712
  console.log(` Messages: ${formatNumber(stats.messageCount)}`);
2294
2713
  console.log(` Embeddings: ${formatNumber(stats.embeddingCount)}`);
2295
- if (existsSync7(DB_PATH)) {
2296
- const dbStats = statSync2(DB_PATH);
2297
- console.log(` DB Size: ${formatBytes(dbStats.size)}`);
2714
+ let totalSize = 0;
2715
+ if (existsSync8(DB_PATH)) {
2716
+ const dbStats = statSync3(DB_PATH);
2717
+ console.log(` DB Size: ${formatBytes2(dbStats.size)}`);
2718
+ totalSize += dbStats.size;
2719
+ }
2720
+ const backupsSize = getBackupsDirSize();
2721
+ if (backupsSize > 0) {
2722
+ console.log(` Backups: ${formatBytes2(backupsSize)}`);
2723
+ totalSize += backupsSize;
2724
+ }
2725
+ if (totalSize > 0) {
2726
+ console.log(` Total: ${formatBytes2(totalSize)}`);
2298
2727
  }
2299
2728
  console.log(` Location: ${CMEM_DIR}`);
2300
2729
  console.log("");
@@ -2319,8 +2748,8 @@ async function statsCommand() {
2319
2748
  // src/commands/watch.ts
2320
2749
  import chalk7 from "chalk";
2321
2750
  import chokidar from "chokidar";
2322
- import { statSync as statSync3, existsSync as existsSync8, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
2323
- import { join as join4, dirname as dirname4, basename as basename6 } from "path";
2751
+ import { statSync as statSync4, existsSync as existsSync9, readFileSync as readFileSync2, readdirSync as readdirSync3 } from "fs";
2752
+ import { join as join5, dirname as dirname5, basename as basename6 } from "path";
2324
2753
  var spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2325
2754
  var Spinner2 = class {
2326
2755
  interval = null;
@@ -2454,13 +2883,13 @@ async function watchCommand(options) {
2454
2883
  }
2455
2884
  function findAllSessionFiles(dir) {
2456
2885
  const files = [];
2457
- if (!existsSync8(dir)) return files;
2886
+ if (!existsSync9(dir)) return files;
2458
2887
  function scanDir(currentDir, depth = 0) {
2459
2888
  if (depth > 10) return;
2460
2889
  try {
2461
- const entries = readdirSync2(currentDir, { withFileTypes: true });
2890
+ const entries = readdirSync3(currentDir, { withFileTypes: true });
2462
2891
  for (const entry of entries) {
2463
- const fullPath = join4(currentDir, entry.name);
2892
+ const fullPath = join5(currentDir, entry.name);
2464
2893
  if (entry.isDirectory()) {
2465
2894
  if (entry.name === "subagents") continue;
2466
2895
  scanDir(fullPath, depth + 1);
@@ -2492,11 +2921,11 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
2492
2921
  if (verbose) console.log(chalk7.dim(` Skipping empty session: ${filePath}`));
2493
2922
  return false;
2494
2923
  }
2495
- const stats = statSync3(filePath);
2924
+ const stats = statSync4(filePath);
2496
2925
  const fileMtime = stats.mtime.toISOString();
2497
2926
  const existingSession = getSessionBySourceFile(filePath);
2498
2927
  const sessionId_from_file = basename6(filePath, ".jsonl");
2499
- const agentMessages = loadSubagentMessages2(dirname4(filePath), sessionId_from_file);
2928
+ const agentMessages = loadSubagentMessages2(dirname5(filePath), sessionId_from_file);
2500
2929
  const allMessages = [...messages, ...agentMessages];
2501
2930
  const contentLength = allMessages.reduce((sum, m) => sum + m.content.length, 0);
2502
2931
  const firstUserMsg = messages.find((m) => m.role === "user");
@@ -2576,13 +3005,13 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
2576
3005
  }
2577
3006
  }
2578
3007
  function loadSubagentMessages2(projectDirPath, parentSessionId) {
2579
- const subagentsDir = join4(projectDirPath, parentSessionId, "subagents");
2580
- if (!existsSync8(subagentsDir)) return [];
3008
+ const subagentsDir = join5(projectDirPath, parentSessionId, "subagents");
3009
+ if (!existsSync9(subagentsDir)) return [];
2581
3010
  const messages = [];
2582
3011
  try {
2583
- const agentFiles = readdirSync2(subagentsDir).filter((f) => f.endsWith(".jsonl"));
3012
+ const agentFiles = readdirSync3(subagentsDir).filter((f) => f.endsWith(".jsonl"));
2584
3013
  for (const agentFile of agentFiles) {
2585
- const agentPath = join4(subagentsDir, agentFile);
3014
+ const agentPath = join5(subagentsDir, agentFile);
2586
3015
  const agentMessages = parseSessionFile(agentPath);
2587
3016
  messages.push(...agentMessages);
2588
3017
  }
@@ -2591,9 +3020,9 @@ function loadSubagentMessages2(projectDirPath, parentSessionId) {
2591
3020
  return messages;
2592
3021
  }
2593
3022
  function getProjectPathFromIndex(filePath, sessionId) {
2594
- const projectDir = dirname4(filePath);
2595
- const indexPath = join4(projectDir, "sessions-index.json");
2596
- if (!existsSync8(indexPath)) return null;
3023
+ const projectDir = dirname5(filePath);
3024
+ const indexPath = join5(projectDir, "sessions-index.json");
3025
+ if (!existsSync9(indexPath)) return null;
2597
3026
  try {
2598
3027
  const content = readFileSync2(indexPath, "utf-8");
2599
3028
  const index = JSON.parse(content);
@@ -3203,17 +3632,17 @@ init_install();
3203
3632
  // src/commands/setup.ts
3204
3633
  import chalk10 from "chalk";
3205
3634
  import { execSync as execSync3 } from "child_process";
3206
- import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3635
+ import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3207
3636
  import { homedir as homedir3 } from "os";
3208
- import { join as join6, dirname as dirname6, basename as basename7 } from "path";
3637
+ import { join as join7, dirname as dirname7, basename as basename7 } from "path";
3209
3638
  import { fileURLToPath } from "url";
3210
3639
  import { createInterface } from "readline";
3211
3640
  var __filename = fileURLToPath(import.meta.url);
3212
- var __dirname = dirname6(__filename);
3213
- var CLAUDE_JSON_PATH = join6(homedir3(), ".claude.json");
3214
- var CLAUDE_SETTINGS_PATH = join6(homedir3(), ".claude", "settings.json");
3215
- var CMEM_DIR2 = join6(homedir3(), ".cmem");
3216
- var SETUP_MARKER = join6(CMEM_DIR2, ".setup-complete");
3641
+ var __dirname = dirname7(__filename);
3642
+ var CLAUDE_JSON_PATH = join7(homedir3(), ".claude.json");
3643
+ var CLAUDE_SETTINGS_PATH = join7(homedir3(), ".claude", "settings.json");
3644
+ var CMEM_DIR2 = join7(homedir3(), ".cmem");
3645
+ var SETUP_MARKER = join7(CMEM_DIR2, ".setup-complete");
3217
3646
  var CMEM_PERMISSIONS = [
3218
3647
  "mcp__cmem__search_sessions",
3219
3648
  "mcp__cmem__list_sessions",
@@ -3332,8 +3761,8 @@ function isGloballyInstalled() {
3332
3761
  }
3333
3762
  function getCmemVersion() {
3334
3763
  try {
3335
- const packagePath = join6(__dirname, "..", "package.json");
3336
- if (existsSync10(packagePath)) {
3764
+ const packagePath = join7(__dirname, "..", "package.json");
3765
+ if (existsSync11(packagePath)) {
3337
3766
  const pkg = JSON.parse(readFileSync3(packagePath, "utf-8"));
3338
3767
  return pkg.version || "0.1.0";
3339
3768
  }
@@ -3419,8 +3848,8 @@ async function setupCommand() {
3419
3848
  }
3420
3849
  }
3421
3850
  console.log("");
3422
- const daemonInstalled = existsSync10(
3423
- join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist")
3851
+ const daemonInstalled = existsSync11(
3852
+ join7(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist")
3424
3853
  );
3425
3854
  const isUpdating = installedVersion && installedVersion !== currentVersion;
3426
3855
  if (daemonInstalled && !isUpdating) {
@@ -3484,7 +3913,7 @@ async function setupCommand() {
3484
3913
  console.log(chalk10.yellow(" Initial session indexing"));
3485
3914
  console.log(chalk10.dim(" Scanning and indexing your existing Claude Code conversations\n"));
3486
3915
  await indexExistingSessions();
3487
- if (!existsSync10(CMEM_DIR2)) {
3916
+ if (!existsSync11(CMEM_DIR2)) {
3488
3917
  mkdirSync3(CMEM_DIR2, { recursive: true });
3489
3918
  }
3490
3919
  writeFileSync2(SETUP_MARKER, (/* @__PURE__ */ new Date()).toISOString());
@@ -3501,7 +3930,7 @@ async function setupCommand() {
3501
3930
  console.log(chalk10.dim(" get_session Retrieve full history"));
3502
3931
  console.log(chalk10.dim(" list_sessions Browse recent sessions"));
3503
3932
  if (process.platform === "darwin") {
3504
- const plistExists = existsSync10(join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist"));
3933
+ const plistExists = existsSync11(join7(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist"));
3505
3934
  if (plistExists) {
3506
3935
  console.log(chalk10.green("\n \u{1F680} Daemon is now syncing your sessions in the background!"));
3507
3936
  }
@@ -3509,7 +3938,7 @@ async function setupCommand() {
3509
3938
  console.log("");
3510
3939
  }
3511
3940
  function isMcpConfigured() {
3512
- if (!existsSync10(CLAUDE_JSON_PATH)) {
3941
+ if (!existsSync11(CLAUDE_JSON_PATH)) {
3513
3942
  return false;
3514
3943
  }
3515
3944
  try {
@@ -3522,7 +3951,7 @@ function isMcpConfigured() {
3522
3951
  function configureMcpServer() {
3523
3952
  try {
3524
3953
  let claudeJson = {};
3525
- if (existsSync10(CLAUDE_JSON_PATH)) {
3954
+ if (existsSync11(CLAUDE_JSON_PATH)) {
3526
3955
  try {
3527
3956
  claudeJson = JSON.parse(readFileSync3(CLAUDE_JSON_PATH, "utf-8"));
3528
3957
  } catch {
@@ -3538,12 +3967,12 @@ function configureMcpServer() {
3538
3967
  args: ["mcp"]
3539
3968
  };
3540
3969
  writeFileSync2(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2) + "\n");
3541
- const claudeDir = dirname6(CLAUDE_SETTINGS_PATH);
3542
- if (!existsSync10(claudeDir)) {
3970
+ const claudeDir = dirname7(CLAUDE_SETTINGS_PATH);
3971
+ if (!existsSync11(claudeDir)) {
3543
3972
  mkdirSync3(claudeDir, { recursive: true });
3544
3973
  }
3545
3974
  let settings = {};
3546
- if (existsSync10(CLAUDE_SETTINGS_PATH)) {
3975
+ if (existsSync11(CLAUDE_SETTINGS_PATH)) {
3547
3976
  try {
3548
3977
  settings = JSON.parse(readFileSync3(CLAUDE_SETTINGS_PATH, "utf-8"));
3549
3978
  } catch {
@@ -3570,7 +3999,7 @@ function configureMcpServer() {
3570
3999
  }
3571
4000
  function shouldRunSetup() {
3572
4001
  const isNpx = isRunningViaNpx();
3573
- const setupComplete = existsSync10(SETUP_MARKER);
4002
+ const setupComplete = existsSync11(SETUP_MARKER);
3574
4003
  const isGlobal = isGloballyInstalled();
3575
4004
  if (isNpx) return true;
3576
4005
  if (!isGlobal && !setupComplete) return true;
@@ -3583,13 +4012,13 @@ function shouldRunSetup() {
3583
4012
  }
3584
4013
  function findAllSessionFiles2(dir) {
3585
4014
  const files = [];
3586
- if (!existsSync10(dir)) return files;
4015
+ if (!existsSync11(dir)) return files;
3587
4016
  function scanDir(currentDir, depth = 0) {
3588
4017
  if (depth > 10) return;
3589
4018
  try {
3590
- const entries = readdirSync3(currentDir, { withFileTypes: true });
4019
+ const entries = readdirSync4(currentDir, { withFileTypes: true });
3591
4020
  for (const entry of entries) {
3592
- const fullPath = join6(currentDir, entry.name);
4021
+ const fullPath = join7(currentDir, entry.name);
3593
4022
  if (entry.isDirectory()) {
3594
4023
  if (entry.name === "subagents") continue;
3595
4024
  scanDir(fullPath, depth + 1);
@@ -3609,7 +4038,7 @@ async function indexSessionFile(filePath, embeddingsReady) {
3609
4038
  if (messages.length === 0) return false;
3610
4039
  const existing = getSessionBySourceFile(filePath);
3611
4040
  if (existing) return false;
3612
- const stats = statSync4(filePath);
4041
+ const stats = statSync5(filePath);
3613
4042
  const fileMtime = stats.mtime.toISOString();
3614
4043
  const firstUserMsg = messages.find((m) => m.role === "user");
3615
4044
  const title = firstUserMsg ? generateTitle(firstUserMsg.content) : "Untitled Session";
@@ -3676,14 +4105,78 @@ async function indexExistingSessions() {
3676
4105
  `));
3677
4106
  }
3678
4107
 
4108
+ // src/commands/purge.ts
4109
+ import chalk11 from "chalk";
4110
+ import { createInterface as createInterface2 } from "readline";
4111
+ async function purgeCommand(options) {
4112
+ const days = options.days ? parseInt(options.days, 10) : DEFAULT_PURGE_DAYS;
4113
+ if (isNaN(days) || days < 1) {
4114
+ console.log(chalk11.red("Invalid days value. Must be a positive number."));
4115
+ return;
4116
+ }
4117
+ const preview = getPurgePreview(days);
4118
+ if (preview.sessionsToDelete === 0) {
4119
+ console.log(chalk11.green("No sessions eligible for purge."));
4120
+ console.log(chalk11.dim("(Sessions must be older than " + days + " days and not starred)"));
4121
+ return;
4122
+ }
4123
+ console.log(chalk11.bold("\nPurge Preview:"));
4124
+ console.log(chalk11.dim("\u2500".repeat(50)));
4125
+ console.log(` Database size: ${chalk11.cyan(formatBytes(getDatabaseSize()))}`);
4126
+ console.log(` Backups size: ${chalk11.cyan(formatBytes(getBackupsDirSize()))}`);
4127
+ console.log(` Total storage: ${chalk11.cyan(formatBytes(getTotalStorageSize()))}`);
4128
+ console.log();
4129
+ console.log(` Sessions to delete: ${chalk11.yellow(preview.sessionsToDelete.toString())}`);
4130
+ console.log(` Messages involved: ${chalk11.yellow(preview.messagesInvolved.toString())}`);
4131
+ if (preview.backupFilesToDelete > 0) {
4132
+ console.log(` Backup files: ${chalk11.yellow(preview.backupFilesToDelete.toString())} (~${formatBytes(preview.backupBytesToFree)})`);
4133
+ }
4134
+ console.log(` Date range: ${chalk11.dim(preview.oldestSessionDate)} to ${chalk11.dim(preview.newestSessionDate)}`);
4135
+ console.log(chalk11.dim(" (Starred sessions will be preserved)"));
4136
+ console.log(chalk11.dim("\u2500".repeat(50)));
4137
+ if (options.dryRun) {
4138
+ console.log(chalk11.cyan("\n[Dry run] No changes made."));
4139
+ return;
4140
+ }
4141
+ if (!options.force) {
4142
+ const confirmed = await confirm(
4143
+ chalk11.red(`
4144
+ Delete ${preview.sessionsToDelete} sessions? This cannot be undone. [y/N] `)
4145
+ );
4146
+ if (!confirmed) {
4147
+ console.log(chalk11.dim("Cancelled."));
4148
+ return;
4149
+ }
4150
+ }
4151
+ const result = purgeOldSessions(days);
4152
+ console.log(chalk11.green(`
4153
+ Purged ${result.sessionsDeleted} session${result.sessionsDeleted !== 1 ? "s" : ""}.`));
4154
+ if (result.backupsDeleted > 0) {
4155
+ console.log(chalk11.green(`Deleted ${result.backupsDeleted} backup file${result.backupsDeleted !== 1 ? "s" : ""}.`));
4156
+ }
4157
+ console.log(`New storage size: ${chalk11.cyan(formatBytes(getTotalStorageSize()))}`);
4158
+ }
4159
+ function confirm(prompt) {
4160
+ const rl = createInterface2({
4161
+ input: process.stdin,
4162
+ output: process.stdout
4163
+ });
4164
+ return new Promise((resolve) => {
4165
+ rl.question(prompt, (answer) => {
4166
+ rl.close();
4167
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
4168
+ });
4169
+ });
4170
+ }
4171
+
3679
4172
  // src/cli.ts
3680
4173
  var sessionToResume = null;
3681
4174
  function getVersion() {
3682
4175
  try {
3683
4176
  const __filename2 = fileURLToPath2(import.meta.url);
3684
- const __dirname2 = dirname7(__filename2);
3685
- const packagePath = join7(__dirname2, "..", "package.json");
3686
- if (existsSync11(packagePath)) {
4177
+ const __dirname2 = dirname8(__filename2);
4178
+ const packagePath = join8(__dirname2, "..", "package.json");
4179
+ if (existsSync12(packagePath)) {
3687
4180
  const pkg = JSON.parse(readFileSync4(packagePath, "utf-8"));
3688
4181
  return pkg.version || "0.1.0";
3689
4182
  }
@@ -3740,6 +4233,9 @@ program.command("restore <id>").description("Restore a session").option("--copy"
3740
4233
  program.command("delete <id>").description("Delete a session").action(async (id) => {
3741
4234
  await deleteCommand(id);
3742
4235
  });
4236
+ program.command("purge").description("Delete old sessions to free up space").option("-d, --days <n>", "Delete sessions older than N days (default: 30)").option("--dry-run", "Preview what would be deleted without making changes").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
4237
+ await purgeCommand(options);
4238
+ });
3743
4239
  program.command("stats").description("Show storage statistics").action(async () => {
3744
4240
  await statsCommand();
3745
4241
  });