@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.
- package/dist/cli.js +1189 -693
- package/dist/cli.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
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 (!
|
|
87
|
+
if (!existsSync10(LAUNCH_AGENTS_DIR)) {
|
|
88
88
|
mkdirSync2(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
89
89
|
}
|
|
90
|
-
const cmemDir =
|
|
91
|
-
if (!
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
188
|
+
LAUNCH_AGENTS_DIR = join6(homedir2(), "Library", "LaunchAgents");
|
|
189
189
|
PLIST_NAME = "com.cmem.watch.plist";
|
|
190
|
-
PLIST_PATH =
|
|
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
|
|
199
|
-
import { join as
|
|
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
|
|
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
|
|
209
|
-
import { existsSync as
|
|
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/
|
|
284
|
-
import {
|
|
285
|
-
import {
|
|
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/
|
|
325
|
-
import
|
|
326
|
-
import
|
|
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/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
351
|
-
|
|
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
|
|
354
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
|
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(
|
|
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
|
|
1064
|
+
function getProjectOrders() {
|
|
1142
1065
|
const db2 = getDatabase();
|
|
1143
|
-
const
|
|
1144
|
-
SELECT
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
`).
|
|
1148
|
-
|
|
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
|
|
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
|
|
1155
|
-
VALUES (?, ?,
|
|
1156
|
-
`)
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
|
1091
|
+
function hasCustomProjectOrder() {
|
|
1164
1092
|
const db2 = getDatabase();
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
|
1280
|
-
import { join as
|
|
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 =
|
|
1292
|
-
return
|
|
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
|
|
1617
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1357
1618
|
function isRecoverable(session) {
|
|
1358
1619
|
if (!session.sourceFile) return false;
|
|
1359
|
-
if (
|
|
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 (
|
|
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
|
|
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 (!
|
|
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(
|
|
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 (
|
|
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 &&
|
|
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__ */
|
|
1880
|
-
/* @__PURE__ */
|
|
1881
|
-
/* @__PURE__ */
|
|
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
|
-
|
|
1888
|
-
/* @__PURE__ */
|
|
1889
|
-
|
|
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__ */
|
|
1897
|
-
/* @__PURE__ */
|
|
1898
|
-
/* @__PURE__ */
|
|
1899
|
-
] }) : /* @__PURE__ */
|
|
1900
|
-
currentView === "projects" ? /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1916
|
-
selectedProject && /* @__PURE__ */
|
|
1917
|
-
|
|
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__ */
|
|
1927
|
-
/* @__PURE__ */
|
|
1928
|
-
selectedProject.sortOrder !== null && /* @__PURE__ */
|
|
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__ */
|
|
2309
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "blue", children: [
|
|
1935
2310
|
"\u{1F4C1} ",
|
|
1936
2311
|
selectedProject.path
|
|
1937
2312
|
] }),
|
|
1938
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1954
|
-
/* @__PURE__ */
|
|
1955
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1964
|
-
] }) : mode === "sort-project" ? /* @__PURE__ */
|
|
1965
|
-
/* @__PURE__ */
|
|
1966
|
-
/* @__PURE__ */
|
|
1967
|
-
|
|
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__ */
|
|
1977
|
-
/* @__PURE__ */
|
|
1978
|
-
|
|
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__ */
|
|
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
|
|
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
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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
|
|
2323
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
2890
|
+
const entries = readdirSync3(currentDir, { withFileTypes: true });
|
|
2462
2891
|
for (const entry of entries) {
|
|
2463
|
-
const fullPath =
|
|
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 =
|
|
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(
|
|
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 =
|
|
2580
|
-
if (!
|
|
3008
|
+
const subagentsDir = join5(projectDirPath, parentSessionId, "subagents");
|
|
3009
|
+
if (!existsSync9(subagentsDir)) return [];
|
|
2581
3010
|
const messages = [];
|
|
2582
3011
|
try {
|
|
2583
|
-
const agentFiles =
|
|
3012
|
+
const agentFiles = readdirSync3(subagentsDir).filter((f) => f.endsWith(".jsonl"));
|
|
2584
3013
|
for (const agentFile of agentFiles) {
|
|
2585
|
-
const agentPath =
|
|
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 =
|
|
2595
|
-
const indexPath =
|
|
2596
|
-
if (!
|
|
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
|
|
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
|
|
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 =
|
|
3213
|
-
var CLAUDE_JSON_PATH =
|
|
3214
|
-
var CLAUDE_SETTINGS_PATH =
|
|
3215
|
-
var CMEM_DIR2 =
|
|
3216
|
-
var SETUP_MARKER =
|
|
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 =
|
|
3336
|
-
if (
|
|
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 =
|
|
3423
|
-
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
3542
|
-
if (!
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
4019
|
+
const entries = readdirSync4(currentDir, { withFileTypes: true });
|
|
3591
4020
|
for (const entry of entries) {
|
|
3592
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
3685
|
-
const packagePath =
|
|
3686
|
-
if (
|
|
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
|
});
|