@colbymchenry/cmem 0.2.32 → 0.2.36
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/README.md +3 -0
- package/dist/cli.js +180 -102
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,8 @@ Ever closed your terminal and lost that perfect Claude conversation? The one whe
|
|
|
62
62
|
|
|
63
63
|
🔄 **Automatic Sync** — A background daemon watches your Claude Code sessions and keeps everything indexed in real-time.
|
|
64
64
|
|
|
65
|
+
💾 **Never Lost** — Every conversation is automatically backed up. Even if Claude Code clears its storage, cmem restores your sessions instantly when you resume them.
|
|
66
|
+
|
|
65
67
|
📦 **100% Local & Private** — Everything runs on your machine. No cloud services, no API keys, no external servers. Just SQLite + local AI embeddings.
|
|
66
68
|
|
|
67
69
|
## Quick Start
|
|
@@ -215,6 +217,7 @@ Browse sessions organized by project folder.
|
|
|
215
217
|
All data is stored locally in `~/.cmem/`:
|
|
216
218
|
- `sessions.db` — SQLite database with vector embeddings
|
|
217
219
|
- `models/` — Downloaded embedding model (~130MB)
|
|
220
|
+
- `backups/` — Full copies of all conversation JSONLs (auto-restored if Claude deletes them)
|
|
218
221
|
|
|
219
222
|
Claude Code sessions are read from `~/.claude/`.
|
|
220
223
|
|
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, existsSync as existsSync9, mkdirSync as mkdirSync2 } from "fs";
|
|
21
21
|
import { homedir as homedir2 } from "os";
|
|
22
|
-
import { join as join5, dirname as
|
|
22
|
+
import { join as join5, dirname as dirname5 } 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 dirname5(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 (!existsSync9(LAUNCH_AGENTS_DIR)) {
|
|
88
88
|
mkdirSync2(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
89
89
|
}
|
|
90
90
|
const cmemDir = join5(homedir2(), ".cmem");
|
|
91
|
-
if (!
|
|
91
|
+
if (!existsSync9(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 (existsSync9(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 (!existsSync9(PLIST_PATH)) {
|
|
130
130
|
console.log(chalk9.yellow("LaunchAgent not found. Nothing to uninstall."));
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
@@ -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 (!existsSync9(PLIST_PATH)) {
|
|
156
156
|
console.log(chalk9.yellow("Status: Not installed"));
|
|
157
157
|
console.log(chalk9.dim("Run: cmem install"));
|
|
158
158
|
return;
|
|
@@ -195,8 +195,8 @@ 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 join7, dirname as
|
|
198
|
+
import { existsSync as existsSync11, readFileSync as readFileSync4 } from "fs";
|
|
199
|
+
import { join as join7, dirname as dirname7 } from "path";
|
|
200
200
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
201
201
|
import { spawn as spawn2 } from "child_process";
|
|
202
202
|
|
|
@@ -205,12 +205,84 @@ import { useState as useState2, useEffect as useEffect2, useCallback as useCallb
|
|
|
205
205
|
import { Box as Box6, Text as Text6, useInput, useApp } from "ink";
|
|
206
206
|
import TextInput2 from "ink-text-input";
|
|
207
207
|
import Spinner from "ink-spinner";
|
|
208
|
-
import { basename as
|
|
209
|
-
import { existsSync as
|
|
208
|
+
import { basename as basename5, dirname as dirname3 } from "path";
|
|
209
|
+
import { existsSync as existsSync6 } from "fs";
|
|
210
|
+
|
|
211
|
+
// src/utils/config.ts
|
|
212
|
+
import { homedir } from "os";
|
|
213
|
+
import { join, basename, dirname } from "path";
|
|
214
|
+
import { mkdirSync, existsSync, copyFileSync } from "fs";
|
|
215
|
+
var CMEM_DIR = join(homedir(), ".cmem");
|
|
216
|
+
var DB_PATH = join(CMEM_DIR, "sessions.db");
|
|
217
|
+
var MODELS_DIR = join(CMEM_DIR, "models");
|
|
218
|
+
var BACKUPS_DIR = join(CMEM_DIR, "backups");
|
|
219
|
+
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
220
|
+
var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
221
|
+
var CLAUDE_SESSIONS_DIR = join(CLAUDE_DIR, "sessions");
|
|
222
|
+
var EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
223
|
+
var EMBEDDING_DIMENSIONS = 768;
|
|
224
|
+
var MAX_EMBEDDING_CHARS = 8e3;
|
|
225
|
+
var MAX_MESSAGE_PREVIEW_CHARS = 500;
|
|
226
|
+
var MAX_MESSAGES_FOR_CONTEXT = 20;
|
|
227
|
+
function ensureCmemDir() {
|
|
228
|
+
if (!existsSync(CMEM_DIR)) {
|
|
229
|
+
mkdirSync(CMEM_DIR, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function ensureModelsDir() {
|
|
233
|
+
ensureCmemDir();
|
|
234
|
+
if (!existsSync(MODELS_DIR)) {
|
|
235
|
+
mkdirSync(MODELS_DIR, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function ensureBackupsDir() {
|
|
239
|
+
ensureCmemDir();
|
|
240
|
+
if (!existsSync(BACKUPS_DIR)) {
|
|
241
|
+
mkdirSync(BACKUPS_DIR, { recursive: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function getBackupPath(sourceFile) {
|
|
245
|
+
const projectDir = basename(dirname(sourceFile));
|
|
246
|
+
const sessionFile = basename(sourceFile);
|
|
247
|
+
return join(BACKUPS_DIR, projectDir, sessionFile);
|
|
248
|
+
}
|
|
249
|
+
function backupSessionFile(sourceFile) {
|
|
250
|
+
try {
|
|
251
|
+
if (!existsSync(sourceFile)) return false;
|
|
252
|
+
ensureBackupsDir();
|
|
253
|
+
const backupPath = getBackupPath(sourceFile);
|
|
254
|
+
const backupDir = dirname(backupPath);
|
|
255
|
+
if (!existsSync(backupDir)) {
|
|
256
|
+
mkdirSync(backupDir, { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
copyFileSync(sourceFile, backupPath);
|
|
259
|
+
return true;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function hasBackup(sourceFile) {
|
|
265
|
+
const backupPath = getBackupPath(sourceFile);
|
|
266
|
+
return existsSync(backupPath);
|
|
267
|
+
}
|
|
268
|
+
function restoreFromBackup(sourceFile) {
|
|
269
|
+
try {
|
|
270
|
+
const backupPath = getBackupPath(sourceFile);
|
|
271
|
+
if (!existsSync(backupPath)) return false;
|
|
272
|
+
const sourceDir = dirname(sourceFile);
|
|
273
|
+
if (!existsSync(sourceDir)) {
|
|
274
|
+
mkdirSync(sourceDir, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
copyFileSync(backupPath, sourceFile);
|
|
277
|
+
return true;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
210
282
|
|
|
211
283
|
// src/ui/components/Header.tsx
|
|
212
284
|
import { Box, Text } from "ink";
|
|
213
|
-
import { basename } from "path";
|
|
285
|
+
import { basename as basename2 } from "path";
|
|
214
286
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
215
287
|
var Header = ({ embeddingsReady, projectFilter }) => {
|
|
216
288
|
return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
@@ -218,7 +290,7 @@ var Header = ({ embeddingsReady, projectFilter }) => {
|
|
|
218
290
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "cmem" }),
|
|
219
291
|
projectFilter && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
220
292
|
" \u{1F4C1} ",
|
|
221
|
-
|
|
293
|
+
basename2(projectFilter)
|
|
222
294
|
] }),
|
|
223
295
|
!embeddingsReady && /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: " (loading model...)" })
|
|
224
296
|
] }),
|
|
@@ -251,7 +323,7 @@ var SearchInput = ({
|
|
|
251
323
|
|
|
252
324
|
// src/ui/components/SessionList.tsx
|
|
253
325
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
254
|
-
import { basename as
|
|
326
|
+
import { basename as basename3 } from "path";
|
|
255
327
|
|
|
256
328
|
// src/utils/format.ts
|
|
257
329
|
function formatTimeAgo(timestamp) {
|
|
@@ -362,7 +434,7 @@ var SessionList = ({
|
|
|
362
434
|
var SessionItem = ({ session, isSelected }) => {
|
|
363
435
|
const hasCustomTitle = !!session.customTitle;
|
|
364
436
|
const displayTitle = truncate(session.customTitle || session.title, 38);
|
|
365
|
-
const folderName = session.projectPath ? truncate(
|
|
437
|
+
const folderName = session.projectPath ? truncate(basename3(session.projectPath), 38) : "";
|
|
366
438
|
const msgs = String(session.messageCount).padStart(3);
|
|
367
439
|
const updated = formatTimeAgo(session.updatedAt);
|
|
368
440
|
const getTitleColor = () => {
|
|
@@ -524,33 +596,6 @@ import Database from "better-sqlite3";
|
|
|
524
596
|
import * as sqliteVec from "sqlite-vec";
|
|
525
597
|
import { existsSync as existsSync3 } from "fs";
|
|
526
598
|
|
|
527
|
-
// src/utils/config.ts
|
|
528
|
-
import { homedir } from "os";
|
|
529
|
-
import { join } from "path";
|
|
530
|
-
import { mkdirSync, existsSync } from "fs";
|
|
531
|
-
var CMEM_DIR = join(homedir(), ".cmem");
|
|
532
|
-
var DB_PATH = join(CMEM_DIR, "sessions.db");
|
|
533
|
-
var MODELS_DIR = join(CMEM_DIR, "models");
|
|
534
|
-
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
535
|
-
var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
536
|
-
var CLAUDE_SESSIONS_DIR = join(CLAUDE_DIR, "sessions");
|
|
537
|
-
var EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
538
|
-
var EMBEDDING_DIMENSIONS = 768;
|
|
539
|
-
var MAX_EMBEDDING_CHARS = 8e3;
|
|
540
|
-
var MAX_MESSAGE_PREVIEW_CHARS = 500;
|
|
541
|
-
var MAX_MESSAGES_FOR_CONTEXT = 20;
|
|
542
|
-
function ensureCmemDir() {
|
|
543
|
-
if (!existsSync(CMEM_DIR)) {
|
|
544
|
-
mkdirSync(CMEM_DIR, { recursive: true });
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
function ensureModelsDir() {
|
|
548
|
-
ensureCmemDir();
|
|
549
|
-
if (!existsSync(MODELS_DIR)) {
|
|
550
|
-
mkdirSync(MODELS_DIR, { recursive: true });
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
599
|
// src/parser/index.ts
|
|
555
600
|
import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
|
|
556
601
|
import { join as join2 } from "path";
|
|
@@ -1308,6 +1353,19 @@ function createEmbeddingText(title, summary, messages) {
|
|
|
1308
1353
|
}
|
|
1309
1354
|
|
|
1310
1355
|
// src/ui/hooks/useSessions.ts
|
|
1356
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1357
|
+
function isRecoverable(session) {
|
|
1358
|
+
if (!session.sourceFile) return false;
|
|
1359
|
+
if (existsSync5(session.sourceFile)) return true;
|
|
1360
|
+
if (hasBackup(session.sourceFile)) return true;
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
function ensureBackedUp(session) {
|
|
1364
|
+
if (!session.sourceFile) return;
|
|
1365
|
+
if (existsSync5(session.sourceFile) && !hasBackup(session.sourceFile)) {
|
|
1366
|
+
backupSessionFile(session.sourceFile);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1311
1369
|
function useSessions(options = {}) {
|
|
1312
1370
|
const [sessions, setSessions] = useState([]);
|
|
1313
1371
|
const [allSessions, setAllSessions] = useState([]);
|
|
@@ -1372,13 +1430,15 @@ function useSessions(options = {}) {
|
|
|
1372
1430
|
const loadSessions = useCallback(() => {
|
|
1373
1431
|
try {
|
|
1374
1432
|
const loaded = projectFilter ? listHumanSessionsByProject(projectFilter) : listHumanSessions();
|
|
1433
|
+
loaded.forEach(ensureBackedUp);
|
|
1434
|
+
const recoverable = loaded.filter(isRecoverable);
|
|
1375
1435
|
const favIds = getFavoriteSessionIds();
|
|
1376
1436
|
const orderMap = getProjectOrders();
|
|
1377
1437
|
setFavoriteSessionIds(favIds);
|
|
1378
1438
|
setProjectOrderMap(orderMap);
|
|
1379
1439
|
setHasFavSessions(hasFavoriteSessions());
|
|
1380
1440
|
setHasCustomOrder(hasCustomProjectOrder());
|
|
1381
|
-
const sorted = smartSort(
|
|
1441
|
+
const sorted = smartSort(recoverable, favIds);
|
|
1382
1442
|
setAllSessions(sorted);
|
|
1383
1443
|
setSessions(sorted);
|
|
1384
1444
|
if (!projectFilter) {
|
|
@@ -1511,9 +1571,6 @@ function useSessions(options = {}) {
|
|
|
1511
1571
|
|
|
1512
1572
|
// src/ui/App.tsx
|
|
1513
1573
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1514
|
-
var clearScreen = () => {
|
|
1515
|
-
process.stdout.write("\x1B[2J\x1B[0f");
|
|
1516
|
-
};
|
|
1517
1574
|
var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
1518
1575
|
const { exit } = useApp();
|
|
1519
1576
|
const {
|
|
@@ -1537,6 +1594,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1537
1594
|
const [renameValue, setRenameValue] = useState2("");
|
|
1538
1595
|
const [currentTab, setCurrentTab] = useState2("global");
|
|
1539
1596
|
const [selectedProjectPath, setSelectedProjectPath] = useState2(null);
|
|
1597
|
+
const [renderKey, setRenderKey] = useState2(0);
|
|
1540
1598
|
const getCurrentView = useCallback2(() => {
|
|
1541
1599
|
if (currentTab === "projects") {
|
|
1542
1600
|
return selectedProjectPath ? "project-sessions" : "projects";
|
|
@@ -1581,34 +1639,50 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1581
1639
|
const session = currentSessions2[selectedIndex];
|
|
1582
1640
|
if (!session) return;
|
|
1583
1641
|
if (session.sourceFile) {
|
|
1584
|
-
|
|
1642
|
+
if (!existsSync6(session.sourceFile)) {
|
|
1643
|
+
if (hasBackup(session.sourceFile)) {
|
|
1644
|
+
const restored = restoreFromBackup(session.sourceFile);
|
|
1645
|
+
if (restored) {
|
|
1646
|
+
setStatusMessage("Restored session from backup");
|
|
1647
|
+
} else {
|
|
1648
|
+
setStatusMessage("Failed to restore session from backup");
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
} else {
|
|
1652
|
+
setStatusMessage("Session file deleted and no backup - cannot resume");
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const filename = basename5(session.sourceFile);
|
|
1585
1657
|
const claudeSessionId = filename.replace(".jsonl", "");
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1658
|
+
let projectPath = session.projectPath;
|
|
1659
|
+
if (!projectPath) {
|
|
1660
|
+
const projectDirName = basename5(dirname3(session.sourceFile));
|
|
1661
|
+
if (projectDirName.startsWith("-")) {
|
|
1662
|
+
const segments = projectDirName.substring(1).split("-");
|
|
1663
|
+
let currentPath = "";
|
|
1664
|
+
let remainingSegments = [...segments];
|
|
1665
|
+
while (remainingSegments.length > 0) {
|
|
1666
|
+
let found = false;
|
|
1667
|
+
for (let i = remainingSegments.length; i > 0; i--) {
|
|
1668
|
+
const testSegment = remainingSegments.slice(0, i).join("-");
|
|
1669
|
+
const testPath = currentPath + "/" + testSegment;
|
|
1670
|
+
if (existsSync6(testPath)) {
|
|
1671
|
+
currentPath = testPath;
|
|
1672
|
+
remainingSegments = remainingSegments.slice(i);
|
|
1673
|
+
found = true;
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (!found) {
|
|
1678
|
+
currentPath = currentPath + "/" + remainingSegments.join("-");
|
|
1601
1679
|
break;
|
|
1602
1680
|
}
|
|
1603
1681
|
}
|
|
1604
|
-
if (
|
|
1605
|
-
|
|
1606
|
-
break;
|
|
1682
|
+
if (currentPath && existsSync6(currentPath)) {
|
|
1683
|
+
projectPath = currentPath;
|
|
1607
1684
|
}
|
|
1608
1685
|
}
|
|
1609
|
-
if (currentPath && existsSync5(currentPath)) {
|
|
1610
|
-
projectPath = currentPath;
|
|
1611
|
-
}
|
|
1612
1686
|
}
|
|
1613
1687
|
if (onResume) {
|
|
1614
1688
|
onResume(claudeSessionId, projectPath);
|
|
@@ -1621,13 +1695,13 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1621
1695
|
const handleEnterProject = useCallback2(() => {
|
|
1622
1696
|
const project = projects[selectedIndex];
|
|
1623
1697
|
if (project) {
|
|
1624
|
-
|
|
1698
|
+
setRenderKey((k) => k + 1);
|
|
1625
1699
|
setSelectedProjectPath(project.path);
|
|
1626
1700
|
setSelectedIndex(0);
|
|
1627
1701
|
}
|
|
1628
1702
|
}, [projects, selectedIndex]);
|
|
1629
1703
|
const handleBackToProjects = useCallback2(() => {
|
|
1630
|
-
|
|
1704
|
+
setRenderKey((k) => k + 1);
|
|
1631
1705
|
setSelectedProjectPath(null);
|
|
1632
1706
|
setSelectedIndex(0);
|
|
1633
1707
|
}, []);
|
|
@@ -1743,7 +1817,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1743
1817
|
}
|
|
1744
1818
|
if (key.leftArrow || input === "h") {
|
|
1745
1819
|
if (currentTab === "projects") {
|
|
1746
|
-
|
|
1820
|
+
setRenderKey((k) => k + 1);
|
|
1747
1821
|
setCurrentTab("global");
|
|
1748
1822
|
setSelectedProjectPath(null);
|
|
1749
1823
|
}
|
|
@@ -1751,7 +1825,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1751
1825
|
}
|
|
1752
1826
|
if (key.rightArrow || input === "l") {
|
|
1753
1827
|
if (currentTab === "global") {
|
|
1754
|
-
|
|
1828
|
+
setRenderKey((k) => k + 1);
|
|
1755
1829
|
setCurrentTab("projects");
|
|
1756
1830
|
}
|
|
1757
1831
|
return;
|
|
@@ -1821,7 +1895,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1821
1895
|
}
|
|
1822
1896
|
) : currentView === "project-sessions" && selectedProjectPath ? /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1823
1897
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Projects \u2192 " }),
|
|
1824
|
-
/* @__PURE__ */ jsx6(Text6, { color: "blue", children:
|
|
1898
|
+
/* @__PURE__ */ jsx6(Text6, { color: "blue", children: basename5(selectedProjectPath) })
|
|
1825
1899
|
] }) : /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Browse projects below" }) }),
|
|
1826
1900
|
currentView === "projects" ? /* @__PURE__ */ jsx6(
|
|
1827
1901
|
ProjectList,
|
|
@@ -1912,7 +1986,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
|
|
|
1912
1986
|
),
|
|
1913
1987
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [\u2190\u2192] switch" })
|
|
1914
1988
|
] })
|
|
1915
|
-
] });
|
|
1989
|
+
] }, renderKey);
|
|
1916
1990
|
};
|
|
1917
1991
|
|
|
1918
1992
|
// src/commands/save.ts
|
|
@@ -2210,7 +2284,7 @@ async function deleteCommand(id) {
|
|
|
2210
2284
|
|
|
2211
2285
|
// src/commands/stats.ts
|
|
2212
2286
|
import chalk6 from "chalk";
|
|
2213
|
-
import { statSync as statSync2, existsSync as
|
|
2287
|
+
import { statSync as statSync2, existsSync as existsSync7 } from "fs";
|
|
2214
2288
|
async function statsCommand() {
|
|
2215
2289
|
console.log(chalk6.cyan("cmem Storage Statistics\n"));
|
|
2216
2290
|
const stats = getStats();
|
|
@@ -2218,7 +2292,7 @@ async function statsCommand() {
|
|
|
2218
2292
|
console.log(` Sessions: ${formatNumber(stats.sessionCount)}`);
|
|
2219
2293
|
console.log(` Messages: ${formatNumber(stats.messageCount)}`);
|
|
2220
2294
|
console.log(` Embeddings: ${formatNumber(stats.embeddingCount)}`);
|
|
2221
|
-
if (
|
|
2295
|
+
if (existsSync7(DB_PATH)) {
|
|
2222
2296
|
const dbStats = statSync2(DB_PATH);
|
|
2223
2297
|
console.log(` DB Size: ${formatBytes(dbStats.size)}`);
|
|
2224
2298
|
}
|
|
@@ -2245,8 +2319,8 @@ async function statsCommand() {
|
|
|
2245
2319
|
// src/commands/watch.ts
|
|
2246
2320
|
import chalk7 from "chalk";
|
|
2247
2321
|
import chokidar from "chokidar";
|
|
2248
|
-
import { statSync as statSync3, existsSync as
|
|
2249
|
-
import { join as join4, dirname as
|
|
2322
|
+
import { statSync as statSync3, existsSync as existsSync8, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
2323
|
+
import { join as join4, dirname as dirname4, basename as basename6 } from "path";
|
|
2250
2324
|
var spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2251
2325
|
var Spinner2 = class {
|
|
2252
2326
|
interval = null;
|
|
@@ -2380,7 +2454,7 @@ async function watchCommand(options) {
|
|
|
2380
2454
|
}
|
|
2381
2455
|
function findAllSessionFiles(dir) {
|
|
2382
2456
|
const files = [];
|
|
2383
|
-
if (!
|
|
2457
|
+
if (!existsSync8(dir)) return files;
|
|
2384
2458
|
function scanDir(currentDir, depth = 0) {
|
|
2385
2459
|
if (depth > 10) return;
|
|
2386
2460
|
try {
|
|
@@ -2421,8 +2495,8 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
|
|
|
2421
2495
|
const stats = statSync3(filePath);
|
|
2422
2496
|
const fileMtime = stats.mtime.toISOString();
|
|
2423
2497
|
const existingSession = getSessionBySourceFile(filePath);
|
|
2424
|
-
const sessionId_from_file =
|
|
2425
|
-
const agentMessages = loadSubagentMessages2(
|
|
2498
|
+
const sessionId_from_file = basename6(filePath, ".jsonl");
|
|
2499
|
+
const agentMessages = loadSubagentMessages2(dirname4(filePath), sessionId_from_file);
|
|
2426
2500
|
const allMessages = [...messages, ...agentMessages];
|
|
2427
2501
|
const contentLength = allMessages.reduce((sum, m) => sum + m.content.length, 0);
|
|
2428
2502
|
const firstUserMsg = messages.find((m) => m.role === "user");
|
|
@@ -2491,6 +2565,10 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
|
|
|
2491
2565
|
}
|
|
2492
2566
|
}
|
|
2493
2567
|
}
|
|
2568
|
+
const backedUp = backupSessionFile(filePath);
|
|
2569
|
+
if (verbose && backedUp) {
|
|
2570
|
+
console.log(chalk7.dim(` Backed up: ${basename6(filePath)}`));
|
|
2571
|
+
}
|
|
2494
2572
|
return isNew;
|
|
2495
2573
|
} catch (err) {
|
|
2496
2574
|
if (verbose) console.log(chalk7.red(`Error processing ${filePath}:`), err);
|
|
@@ -2499,7 +2577,7 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
|
|
|
2499
2577
|
}
|
|
2500
2578
|
function loadSubagentMessages2(projectDirPath, parentSessionId) {
|
|
2501
2579
|
const subagentsDir = join4(projectDirPath, parentSessionId, "subagents");
|
|
2502
|
-
if (!
|
|
2580
|
+
if (!existsSync8(subagentsDir)) return [];
|
|
2503
2581
|
const messages = [];
|
|
2504
2582
|
try {
|
|
2505
2583
|
const agentFiles = readdirSync2(subagentsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -2513,9 +2591,9 @@ function loadSubagentMessages2(projectDirPath, parentSessionId) {
|
|
|
2513
2591
|
return messages;
|
|
2514
2592
|
}
|
|
2515
2593
|
function getProjectPathFromIndex(filePath, sessionId) {
|
|
2516
|
-
const projectDir =
|
|
2594
|
+
const projectDir = dirname4(filePath);
|
|
2517
2595
|
const indexPath = join4(projectDir, "sessions-index.json");
|
|
2518
|
-
if (!
|
|
2596
|
+
if (!existsSync8(indexPath)) return null;
|
|
2519
2597
|
try {
|
|
2520
2598
|
const content = readFileSync2(indexPath, "utf-8");
|
|
2521
2599
|
const index = JSON.parse(content);
|
|
@@ -3125,13 +3203,13 @@ init_install();
|
|
|
3125
3203
|
// src/commands/setup.ts
|
|
3126
3204
|
import chalk10 from "chalk";
|
|
3127
3205
|
import { execSync as execSync3 } from "child_process";
|
|
3128
|
-
import { existsSync as
|
|
3206
|
+
import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
3129
3207
|
import { homedir as homedir3 } from "os";
|
|
3130
|
-
import { join as join6, dirname as
|
|
3208
|
+
import { join as join6, dirname as dirname6, basename as basename7 } from "path";
|
|
3131
3209
|
import { fileURLToPath } from "url";
|
|
3132
3210
|
import { createInterface } from "readline";
|
|
3133
3211
|
var __filename = fileURLToPath(import.meta.url);
|
|
3134
|
-
var __dirname =
|
|
3212
|
+
var __dirname = dirname6(__filename);
|
|
3135
3213
|
var CLAUDE_JSON_PATH = join6(homedir3(), ".claude.json");
|
|
3136
3214
|
var CLAUDE_SETTINGS_PATH = join6(homedir3(), ".claude", "settings.json");
|
|
3137
3215
|
var CMEM_DIR2 = join6(homedir3(), ".cmem");
|
|
@@ -3255,7 +3333,7 @@ function isGloballyInstalled() {
|
|
|
3255
3333
|
function getCmemVersion() {
|
|
3256
3334
|
try {
|
|
3257
3335
|
const packagePath = join6(__dirname, "..", "package.json");
|
|
3258
|
-
if (
|
|
3336
|
+
if (existsSync10(packagePath)) {
|
|
3259
3337
|
const pkg = JSON.parse(readFileSync3(packagePath, "utf-8"));
|
|
3260
3338
|
return pkg.version || "0.1.0";
|
|
3261
3339
|
}
|
|
@@ -3341,7 +3419,7 @@ async function setupCommand() {
|
|
|
3341
3419
|
}
|
|
3342
3420
|
}
|
|
3343
3421
|
console.log("");
|
|
3344
|
-
const daemonInstalled =
|
|
3422
|
+
const daemonInstalled = existsSync10(
|
|
3345
3423
|
join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist")
|
|
3346
3424
|
);
|
|
3347
3425
|
const isUpdating = installedVersion && installedVersion !== currentVersion;
|
|
@@ -3406,7 +3484,7 @@ async function setupCommand() {
|
|
|
3406
3484
|
console.log(chalk10.yellow(" Initial session indexing"));
|
|
3407
3485
|
console.log(chalk10.dim(" Scanning and indexing your existing Claude Code conversations\n"));
|
|
3408
3486
|
await indexExistingSessions();
|
|
3409
|
-
if (!
|
|
3487
|
+
if (!existsSync10(CMEM_DIR2)) {
|
|
3410
3488
|
mkdirSync3(CMEM_DIR2, { recursive: true });
|
|
3411
3489
|
}
|
|
3412
3490
|
writeFileSync2(SETUP_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -3423,7 +3501,7 @@ async function setupCommand() {
|
|
|
3423
3501
|
console.log(chalk10.dim(" get_session Retrieve full history"));
|
|
3424
3502
|
console.log(chalk10.dim(" list_sessions Browse recent sessions"));
|
|
3425
3503
|
if (process.platform === "darwin") {
|
|
3426
|
-
const plistExists =
|
|
3504
|
+
const plistExists = existsSync10(join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist"));
|
|
3427
3505
|
if (plistExists) {
|
|
3428
3506
|
console.log(chalk10.green("\n \u{1F680} Daemon is now syncing your sessions in the background!"));
|
|
3429
3507
|
}
|
|
@@ -3431,7 +3509,7 @@ async function setupCommand() {
|
|
|
3431
3509
|
console.log("");
|
|
3432
3510
|
}
|
|
3433
3511
|
function isMcpConfigured() {
|
|
3434
|
-
if (!
|
|
3512
|
+
if (!existsSync10(CLAUDE_JSON_PATH)) {
|
|
3435
3513
|
return false;
|
|
3436
3514
|
}
|
|
3437
3515
|
try {
|
|
@@ -3444,7 +3522,7 @@ function isMcpConfigured() {
|
|
|
3444
3522
|
function configureMcpServer() {
|
|
3445
3523
|
try {
|
|
3446
3524
|
let claudeJson = {};
|
|
3447
|
-
if (
|
|
3525
|
+
if (existsSync10(CLAUDE_JSON_PATH)) {
|
|
3448
3526
|
try {
|
|
3449
3527
|
claudeJson = JSON.parse(readFileSync3(CLAUDE_JSON_PATH, "utf-8"));
|
|
3450
3528
|
} catch {
|
|
@@ -3460,12 +3538,12 @@ function configureMcpServer() {
|
|
|
3460
3538
|
args: ["mcp"]
|
|
3461
3539
|
};
|
|
3462
3540
|
writeFileSync2(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3463
|
-
const claudeDir =
|
|
3464
|
-
if (!
|
|
3541
|
+
const claudeDir = dirname6(CLAUDE_SETTINGS_PATH);
|
|
3542
|
+
if (!existsSync10(claudeDir)) {
|
|
3465
3543
|
mkdirSync3(claudeDir, { recursive: true });
|
|
3466
3544
|
}
|
|
3467
3545
|
let settings = {};
|
|
3468
|
-
if (
|
|
3546
|
+
if (existsSync10(CLAUDE_SETTINGS_PATH)) {
|
|
3469
3547
|
try {
|
|
3470
3548
|
settings = JSON.parse(readFileSync3(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
3471
3549
|
} catch {
|
|
@@ -3492,7 +3570,7 @@ function configureMcpServer() {
|
|
|
3492
3570
|
}
|
|
3493
3571
|
function shouldRunSetup() {
|
|
3494
3572
|
const isNpx = isRunningViaNpx();
|
|
3495
|
-
const setupComplete =
|
|
3573
|
+
const setupComplete = existsSync10(SETUP_MARKER);
|
|
3496
3574
|
const isGlobal = isGloballyInstalled();
|
|
3497
3575
|
if (isNpx) return true;
|
|
3498
3576
|
if (!isGlobal && !setupComplete) return true;
|
|
@@ -3505,7 +3583,7 @@ function shouldRunSetup() {
|
|
|
3505
3583
|
}
|
|
3506
3584
|
function findAllSessionFiles2(dir) {
|
|
3507
3585
|
const files = [];
|
|
3508
|
-
if (!
|
|
3586
|
+
if (!existsSync10(dir)) return files;
|
|
3509
3587
|
function scanDir(currentDir, depth = 0) {
|
|
3510
3588
|
if (depth > 10) return;
|
|
3511
3589
|
try {
|
|
@@ -3537,7 +3615,7 @@ async function indexSessionFile(filePath, embeddingsReady) {
|
|
|
3537
3615
|
const title = firstUserMsg ? generateTitle(firstUserMsg.content) : "Untitled Session";
|
|
3538
3616
|
const summary = generateSummary(messages);
|
|
3539
3617
|
const rawData = JSON.stringify({ filePath, messages, mtime: fileMtime });
|
|
3540
|
-
const sessionId_from_file =
|
|
3618
|
+
const sessionId_from_file = basename7(filePath, ".jsonl");
|
|
3541
3619
|
const sessionId = createSession({
|
|
3542
3620
|
title,
|
|
3543
3621
|
summary,
|
|
@@ -3603,9 +3681,9 @@ var sessionToResume = null;
|
|
|
3603
3681
|
function getVersion() {
|
|
3604
3682
|
try {
|
|
3605
3683
|
const __filename2 = fileURLToPath2(import.meta.url);
|
|
3606
|
-
const __dirname2 =
|
|
3684
|
+
const __dirname2 = dirname7(__filename2);
|
|
3607
3685
|
const packagePath = join7(__dirname2, "..", "package.json");
|
|
3608
|
-
if (
|
|
3686
|
+
if (existsSync11(packagePath)) {
|
|
3609
3687
|
const pkg = JSON.parse(readFileSync4(packagePath, "utf-8"));
|
|
3610
3688
|
return pkg.version || "0.1.0";
|
|
3611
3689
|
}
|