@colbymchenry/cmem 0.2.20

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.

Potentially problematic release.


This version of @colbymchenry/cmem might be problematic. Click here for more details.

package/dist/cli.js ADDED
@@ -0,0 +1,3197 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/commands/install.ts
13
+ var install_exports = {};
14
+ __export(install_exports, {
15
+ installCommand: () => installCommand,
16
+ statusCommand: () => statusCommand,
17
+ uninstallCommand: () => uninstallCommand
18
+ });
19
+ import chalk9 from "chalk";
20
+ import { writeFileSync, unlinkSync, existsSync as existsSync8, mkdirSync as mkdirSync2 } from "fs";
21
+ import { homedir as homedir2 } from "os";
22
+ import { join as join5, dirname as dirname4 } from "path";
23
+ import { execSync as execSync2 } from "child_process";
24
+ function getCmemPath() {
25
+ try {
26
+ const result = execSync2("which cmem", { encoding: "utf-8" }).trim();
27
+ return result;
28
+ } catch {
29
+ return "/usr/local/bin/cmem";
30
+ }
31
+ }
32
+ function getNodeBinDir() {
33
+ try {
34
+ const nodePath = execSync2("which node", { encoding: "utf-8" }).trim();
35
+ return dirname4(nodePath);
36
+ } catch {
37
+ return "/usr/local/bin";
38
+ }
39
+ }
40
+ function generatePlist(cmemPath) {
41
+ const nodeBinDir = getNodeBinDir();
42
+ const pathValue = `${nodeBinDir}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin`;
43
+ return `<?xml version="1.0" encoding="UTF-8"?>
44
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
45
+ <plist version="1.0">
46
+ <dict>
47
+ <key>Label</key>
48
+ <string>com.cmem.watch</string>
49
+
50
+ <key>ProgramArguments</key>
51
+ <array>
52
+ <string>${cmemPath}</string>
53
+ <string>watch</string>
54
+ </array>
55
+
56
+ <key>RunAtLoad</key>
57
+ <true/>
58
+
59
+ <key>KeepAlive</key>
60
+ <true/>
61
+
62
+ <key>StandardOutPath</key>
63
+ <string>${homedir2()}/.cmem/watch.log</string>
64
+
65
+ <key>StandardErrorPath</key>
66
+ <string>${homedir2()}/.cmem/watch.error.log</string>
67
+
68
+ <key>EnvironmentVariables</key>
69
+ <dict>
70
+ <key>PATH</key>
71
+ <string>${pathValue}</string>
72
+ </dict>
73
+ </dict>
74
+ </plist>`;
75
+ }
76
+ async function installCommand() {
77
+ const platform = process.platform;
78
+ if (platform !== "darwin") {
79
+ console.log(chalk9.yellow("Auto-install is currently only supported on macOS."));
80
+ console.log(chalk9.dim("\nFor Linux, create a systemd service:"));
81
+ console.log(chalk9.dim(" ~/.config/systemd/user/cmem-watch.service"));
82
+ console.log(chalk9.dim("\nFor manual background run:"));
83
+ console.log(chalk9.dim(" nohup cmem watch > ~/.cmem/watch.log 2>&1 &"));
84
+ return;
85
+ }
86
+ console.log(chalk9.cyan("Installing cmem watch daemon...\n"));
87
+ if (!existsSync8(LAUNCH_AGENTS_DIR)) {
88
+ mkdirSync2(LAUNCH_AGENTS_DIR, { recursive: true });
89
+ }
90
+ const cmemDir = join5(homedir2(), ".cmem");
91
+ if (!existsSync8(cmemDir)) {
92
+ mkdirSync2(cmemDir, { recursive: true });
93
+ }
94
+ const cmemPath = getCmemPath();
95
+ console.log(chalk9.dim(`Using cmem at: ${cmemPath}`));
96
+ if (existsSync8(PLIST_PATH)) {
97
+ console.log(chalk9.yellow("LaunchAgent already exists. Unloading first..."));
98
+ try {
99
+ execSync2(`launchctl unload "${PLIST_PATH}"`, { stdio: "ignore" });
100
+ } catch {
101
+ }
102
+ }
103
+ const plistContent = generatePlist(cmemPath);
104
+ writeFileSync(PLIST_PATH, plistContent);
105
+ console.log(chalk9.green(`\u2713 Created ${PLIST_PATH}`));
106
+ try {
107
+ execSync2(`launchctl load "${PLIST_PATH}"`);
108
+ console.log(chalk9.green("\u2713 LaunchAgent loaded"));
109
+ } catch (err) {
110
+ console.log(chalk9.red("Failed to load LaunchAgent:"), err);
111
+ return;
112
+ }
113
+ console.log(chalk9.green("\n\u2713 cmem watch daemon installed and running!"));
114
+ console.log(chalk9.dim("\nThe daemon will:"));
115
+ console.log(chalk9.dim(" \u2022 Start automatically on login"));
116
+ console.log(chalk9.dim(" \u2022 Restart if it crashes"));
117
+ console.log(chalk9.dim(` \u2022 Log to ~/.cmem/watch.log`));
118
+ console.log(chalk9.dim("\nCommands:"));
119
+ console.log(chalk9.dim(" cmem uninstall Stop and remove the daemon"));
120
+ console.log(chalk9.dim(" cmem status Check daemon status"));
121
+ }
122
+ async function uninstallCommand() {
123
+ const platform = process.platform;
124
+ if (platform !== "darwin") {
125
+ console.log(chalk9.yellow("Auto-uninstall is currently only supported on macOS."));
126
+ return;
127
+ }
128
+ console.log(chalk9.cyan("Uninstalling cmem watch daemon...\n"));
129
+ if (!existsSync8(PLIST_PATH)) {
130
+ console.log(chalk9.yellow("LaunchAgent not found. Nothing to uninstall."));
131
+ return;
132
+ }
133
+ try {
134
+ execSync2(`launchctl unload "${PLIST_PATH}"`);
135
+ console.log(chalk9.green("\u2713 LaunchAgent unloaded"));
136
+ } catch {
137
+ console.log(chalk9.yellow("LaunchAgent was not loaded"));
138
+ }
139
+ try {
140
+ unlinkSync(PLIST_PATH);
141
+ console.log(chalk9.green(`\u2713 Removed ${PLIST_PATH}`));
142
+ } catch (err) {
143
+ console.log(chalk9.red("Failed to remove plist:"), err);
144
+ return;
145
+ }
146
+ console.log(chalk9.green("\n\u2713 cmem watch daemon uninstalled"));
147
+ }
148
+ async function statusCommand() {
149
+ const platform = process.platform;
150
+ if (platform !== "darwin") {
151
+ console.log(chalk9.yellow("Status check is currently only supported on macOS."));
152
+ return;
153
+ }
154
+ console.log(chalk9.cyan("cmem watch daemon status\n"));
155
+ if (!existsSync8(PLIST_PATH)) {
156
+ console.log(chalk9.yellow("Status: Not installed"));
157
+ console.log(chalk9.dim("Run: cmem install"));
158
+ return;
159
+ }
160
+ try {
161
+ const result = execSync2(`launchctl list | grep com.cmem.watch`, {
162
+ encoding: "utf-8"
163
+ });
164
+ const parts = result.trim().split(/\s+/);
165
+ const pid = parts[0];
166
+ const exitCode = parts[1];
167
+ if (pid && pid !== "-") {
168
+ console.log(chalk9.green(`Status: Running (PID ${pid})`));
169
+ } else if (exitCode === "0") {
170
+ console.log(chalk9.yellow("Status: Stopped (exit code 0)"));
171
+ } else {
172
+ console.log(chalk9.red(`Status: Crashed (exit code ${exitCode})`));
173
+ console.log(chalk9.dim("Check ~/.cmem/watch.error.log for details"));
174
+ }
175
+ } catch {
176
+ console.log(chalk9.yellow("Status: Not running"));
177
+ console.log(chalk9.dim("The daemon is installed but not currently running."));
178
+ console.log(chalk9.dim("It will start on next login, or run: launchctl load ~/Library/LaunchAgents/com.cmem.watch.plist"));
179
+ }
180
+ console.log(chalk9.dim(`
181
+ Plist: ${PLIST_PATH}`));
182
+ console.log(chalk9.dim("Logs: ~/.cmem/watch.log"));
183
+ }
184
+ var LAUNCH_AGENTS_DIR, PLIST_NAME, PLIST_PATH;
185
+ var init_install = __esm({
186
+ "src/commands/install.ts"() {
187
+ "use strict";
188
+ LAUNCH_AGENTS_DIR = join5(homedir2(), "Library", "LaunchAgents");
189
+ PLIST_NAME = "com.cmem.watch.plist";
190
+ PLIST_PATH = join5(LAUNCH_AGENTS_DIR, PLIST_NAME);
191
+ }
192
+ });
193
+
194
+ // src/cli.ts
195
+ import { program } from "commander";
196
+ import { render } from "ink";
197
+ import React2 from "react";
198
+ import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
199
+ import { join as join7, dirname as dirname6 } from "path";
200
+ import { fileURLToPath as fileURLToPath2 } from "url";
201
+ import { spawn as spawn2 } from "child_process";
202
+
203
+ // src/ui/App.tsx
204
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
205
+ import { Box as Box5, Text as Text5, useInput, useApp } from "ink";
206
+ import TextInput2 from "ink-text-input";
207
+ import Spinner from "ink-spinner";
208
+ import { basename as basename4, dirname as dirname2 } from "path";
209
+ import { existsSync as existsSync5 } from "fs";
210
+
211
+ // src/ui/components/Header.tsx
212
+ import { Box, Text } from "ink";
213
+ import { basename } from "path";
214
+ import { jsx, jsxs } from "react/jsx-runtime";
215
+ var Header = ({ embeddingsReady, projectFilter }) => {
216
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [
217
+ /* @__PURE__ */ jsxs(Box, { children: [
218
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "cmem" }),
219
+ projectFilter && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
220
+ " \u{1F4C1} ",
221
+ basename(projectFilter)
222
+ ] }),
223
+ !embeddingsReady && /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: " (loading model...)" })
224
+ ] }),
225
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[q] quit" })
226
+ ] });
227
+ };
228
+
229
+ // src/ui/components/SearchInput.tsx
230
+ import { Box as Box2, Text as Text2 } from "ink";
231
+ import TextInput from "ink-text-input";
232
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
233
+ var SearchInput = ({
234
+ value,
235
+ onChange,
236
+ isFocused
237
+ }) => {
238
+ return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
239
+ /* @__PURE__ */ jsx2(Text2, { children: "Search: " }),
240
+ isFocused ? /* @__PURE__ */ jsx2(
241
+ TextInput,
242
+ {
243
+ value,
244
+ onChange,
245
+ placeholder: "type to search...",
246
+ focus: true
247
+ }
248
+ ) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: value || "press / to search" })
249
+ ] });
250
+ };
251
+
252
+ // src/ui/components/SessionList.tsx
253
+ import { Box as Box3, Text as Text3 } from "ink";
254
+ import { basename as basename2 } from "path";
255
+
256
+ // src/utils/format.ts
257
+ function formatTimeAgo(timestamp) {
258
+ const date = new Date(timestamp);
259
+ const now = /* @__PURE__ */ new Date();
260
+ const diffMs = now.getTime() - date.getTime();
261
+ const diffSecs = Math.floor(diffMs / 1e3);
262
+ const diffMins = Math.floor(diffSecs / 60);
263
+ const diffHours = Math.floor(diffMins / 60);
264
+ const diffDays = Math.floor(diffHours / 24);
265
+ const diffWeeks = Math.floor(diffDays / 7);
266
+ const diffMonths = Math.floor(diffDays / 30);
267
+ if (diffSecs < 60) return "just now";
268
+ if (diffMins < 60) return `${diffMins}m ago`;
269
+ if (diffHours < 24) return `${diffHours}h ago`;
270
+ if (diffDays < 7) return `${diffDays}d ago`;
271
+ if (diffWeeks < 4) return `${diffWeeks}w ago`;
272
+ return `${diffMonths}mo ago`;
273
+ }
274
+ function truncate(text, maxLength) {
275
+ if (text.length <= maxLength) return text;
276
+ return text.slice(0, maxLength - 3) + "...";
277
+ }
278
+ function shortId(id) {
279
+ return id.slice(0, 8);
280
+ }
281
+ function formatNumber(num) {
282
+ return num.toLocaleString();
283
+ }
284
+ function formatBytes(bytes) {
285
+ if (bytes === 0) return "0 B";
286
+ const k = 1024;
287
+ const sizes = ["B", "KB", "MB", "GB"];
288
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
289
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
290
+ }
291
+ function generateTitle(content) {
292
+ const firstLine = content.split("\n")[0].trim();
293
+ const title = truncate(firstLine, 50);
294
+ return title || "Untitled Session";
295
+ }
296
+
297
+ // src/ui/components/SessionList.tsx
298
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
299
+ var SessionList = ({
300
+ sessions,
301
+ selectedIndex
302
+ }) => {
303
+ if (sessions.length === 0) {
304
+ return /* @__PURE__ */ jsxs3(
305
+ Box3,
306
+ {
307
+ flexDirection: "column",
308
+ borderStyle: "round",
309
+ borderColor: "gray",
310
+ paddingX: 1,
311
+ paddingY: 0,
312
+ children: [
313
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Sessions" }),
314
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No sessions found" }),
315
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Run: cmem save --latest" })
316
+ ]
317
+ }
318
+ );
319
+ }
320
+ const visibleCount = 8;
321
+ let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
322
+ const endIndex = Math.min(sessions.length, startIndex + visibleCount);
323
+ if (endIndex - startIndex < visibleCount) {
324
+ startIndex = Math.max(0, endIndex - visibleCount);
325
+ }
326
+ const visibleSessions = sessions.slice(startIndex, endIndex);
327
+ return /* @__PURE__ */ jsxs3(
328
+ Box3,
329
+ {
330
+ flexDirection: "column",
331
+ borderStyle: "round",
332
+ borderColor: "gray",
333
+ paddingX: 1,
334
+ paddingY: 0,
335
+ children: [
336
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
337
+ "Sessions (",
338
+ sessions.length,
339
+ ")"
340
+ ] }),
341
+ visibleSessions.map((session, i) => {
342
+ const actualIndex = startIndex + i;
343
+ const isSelected = actualIndex === selectedIndex;
344
+ return /* @__PURE__ */ jsx3(
345
+ SessionItem,
346
+ {
347
+ session,
348
+ isSelected
349
+ },
350
+ session.id
351
+ );
352
+ }),
353
+ sessions.length > visibleCount && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
354
+ startIndex > 0 ? "\u2191 more above" : "",
355
+ startIndex > 0 && endIndex < sessions.length ? " | " : "",
356
+ endIndex < sessions.length ? "\u2193 more below" : ""
357
+ ] })
358
+ ]
359
+ }
360
+ );
361
+ };
362
+ var SessionItem = ({ session, isSelected }) => {
363
+ const hasCustomTitle = !!session.customTitle;
364
+ const displayTitle = truncate(session.customTitle || session.title, 40);
365
+ const folderName = session.projectPath ? truncate(basename2(session.projectPath), 40) : "";
366
+ const msgs = String(session.messageCount).padStart(3);
367
+ const updated = formatTimeAgo(session.updatedAt);
368
+ const getTitleColor = () => {
369
+ if (isSelected) return "cyan";
370
+ if (hasCustomTitle) return "magenta";
371
+ return void 0;
372
+ };
373
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
374
+ /* @__PURE__ */ jsx3(Text3, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
375
+ /* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(40) }),
376
+ /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
377
+ " ",
378
+ folderName.padEnd(40)
379
+ ] }),
380
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
381
+ " ",
382
+ msgs,
383
+ " "
384
+ ] }),
385
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: updated.padStart(8) })
386
+ ] });
387
+ };
388
+
389
+ // src/ui/components/Preview.tsx
390
+ import { Box as Box4, Text as Text4 } from "ink";
391
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
392
+ var Preview = ({ session }) => {
393
+ if (!session) {
394
+ return null;
395
+ }
396
+ const summary = session.summary ? truncate(session.summary, 200) : "No summary available";
397
+ return /* @__PURE__ */ jsxs4(
398
+ Box4,
399
+ {
400
+ flexDirection: "column",
401
+ borderStyle: "round",
402
+ borderColor: "gray",
403
+ paddingX: 1,
404
+ paddingY: 0,
405
+ marginTop: 1,
406
+ children: [
407
+ /* @__PURE__ */ jsxs4(Box4, { children: [
408
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Preview" }),
409
+ session.projectPath && /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "blue", children: [
410
+ " \u{1F4C1} ",
411
+ session.projectPath
412
+ ] })
413
+ ] }),
414
+ /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: summary }),
415
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
416
+ "Messages: ",
417
+ session.messageCount
418
+ ] })
419
+ ]
420
+ }
421
+ );
422
+ };
423
+
424
+ // src/ui/hooks/useSessions.ts
425
+ import { useState, useEffect, useCallback } from "react";
426
+
427
+ // src/db/index.ts
428
+ import Database from "better-sqlite3";
429
+ import * as sqliteVec from "sqlite-vec";
430
+ import { existsSync as existsSync3 } from "fs";
431
+
432
+ // src/utils/config.ts
433
+ import { homedir } from "os";
434
+ import { join } from "path";
435
+ import { mkdirSync, existsSync } from "fs";
436
+ var CMEM_DIR = join(homedir(), ".cmem");
437
+ var DB_PATH = join(CMEM_DIR, "sessions.db");
438
+ var MODELS_DIR = join(CMEM_DIR, "models");
439
+ var CLAUDE_DIR = join(homedir(), ".claude");
440
+ var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
441
+ var CLAUDE_SESSIONS_DIR = join(CLAUDE_DIR, "sessions");
442
+ var EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
443
+ var EMBEDDING_DIMENSIONS = 768;
444
+ var MAX_EMBEDDING_CHARS = 8e3;
445
+ var MAX_MESSAGE_PREVIEW_CHARS = 500;
446
+ var MAX_MESSAGES_FOR_CONTEXT = 20;
447
+ function ensureCmemDir() {
448
+ if (!existsSync(CMEM_DIR)) {
449
+ mkdirSync(CMEM_DIR, { recursive: true });
450
+ }
451
+ }
452
+ function ensureModelsDir() {
453
+ ensureCmemDir();
454
+ if (!existsSync(MODELS_DIR)) {
455
+ mkdirSync(MODELS_DIR, { recursive: true });
456
+ }
457
+ }
458
+
459
+ // src/parser/index.ts
460
+ import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
461
+ import { join as join2 } from "path";
462
+ function extractSessionMetadata(filepath) {
463
+ const content = readFileSync(filepath, "utf-8");
464
+ const metadata = {
465
+ isSidechain: false,
466
+ isMeta: false
467
+ };
468
+ for (const line of content.split("\n")) {
469
+ if (!line.trim()) continue;
470
+ try {
471
+ const parsed = JSON.parse(line);
472
+ if (parsed.type === "user" && parsed.message) {
473
+ if (parsed.isSidechain === true) {
474
+ metadata.isSidechain = true;
475
+ }
476
+ if (parsed.agentId) {
477
+ metadata.isSidechain = true;
478
+ }
479
+ if (parsed.isMeta === true) {
480
+ metadata.isMeta = true;
481
+ }
482
+ break;
483
+ }
484
+ } catch {
485
+ }
486
+ }
487
+ return metadata;
488
+ }
489
+ function parseSessionFile(filepath) {
490
+ const content = readFileSync(filepath, "utf-8");
491
+ const messages = [];
492
+ for (const line of content.split("\n")) {
493
+ if (!line.trim()) continue;
494
+ try {
495
+ const parsed = JSON.parse(line);
496
+ if ((parsed.type === "user" || parsed.type === "assistant") && parsed.message) {
497
+ const msg = parsed.message;
498
+ if (msg.role && msg.content) {
499
+ 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);
500
+ if (content2) {
501
+ messages.push({
502
+ role: msg.role,
503
+ content: content2,
504
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
505
+ });
506
+ }
507
+ }
508
+ } else if (parsed.type === "message" && parsed.role && parsed.content) {
509
+ messages.push({
510
+ role: parsed.role,
511
+ content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
512
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
513
+ });
514
+ } else if (parsed.role && parsed.content && !parsed.type) {
515
+ messages.push({
516
+ role: parsed.role,
517
+ content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
518
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
519
+ });
520
+ }
521
+ } catch {
522
+ }
523
+ }
524
+ return messages;
525
+ }
526
+ function findSessionFiles() {
527
+ const sessions = [];
528
+ if (existsSync2(CLAUDE_PROJECTS_DIR)) {
529
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR);
530
+ for (const projectDir of projectDirs) {
531
+ const projectDirPath = join2(CLAUDE_PROJECTS_DIR, projectDir);
532
+ const stat = statSync(projectDirPath);
533
+ if (!stat.isDirectory()) continue;
534
+ const indexPath = join2(projectDirPath, "sessions-index.json");
535
+ const sessionIndex = loadSessionIndex(indexPath);
536
+ const indexedSessions = /* @__PURE__ */ new Map();
537
+ if (sessionIndex) {
538
+ for (const entry of sessionIndex.entries) {
539
+ indexedSessions.set(entry.sessionId, entry);
540
+ }
541
+ }
542
+ const entries = readdirSync(projectDirPath, { withFileTypes: true });
543
+ for (const entry of entries) {
544
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
545
+ const filePath = join2(projectDirPath, entry.name);
546
+ const sessionId = entry.name.replace(".jsonl", "");
547
+ try {
548
+ const session = loadSession(filePath);
549
+ if (session) {
550
+ const indexEntry = indexedSessions.get(sessionId);
551
+ if (indexEntry) {
552
+ session.projectPath = indexEntry.projectPath;
553
+ }
554
+ const agentMessages = loadSubagentMessages(projectDirPath, sessionId);
555
+ if (agentMessages.length > 0) {
556
+ session.agentMessages = agentMessages;
557
+ }
558
+ sessions.push(session);
559
+ }
560
+ } catch {
561
+ }
562
+ }
563
+ }
564
+ }
565
+ if (existsSync2(CLAUDE_SESSIONS_DIR)) {
566
+ const sessionFiles = readdirSync(CLAUDE_SESSIONS_DIR).filter((f) => f.endsWith(".jsonl"));
567
+ for (const sessionFile of sessionFiles) {
568
+ const filePath = join2(CLAUDE_SESSIONS_DIR, sessionFile);
569
+ try {
570
+ const session = loadSession(filePath);
571
+ if (session) {
572
+ sessions.push(session);
573
+ }
574
+ } catch {
575
+ }
576
+ }
577
+ }
578
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
579
+ return sessions;
580
+ }
581
+ function loadSessionIndex(indexPath) {
582
+ if (!existsSync2(indexPath)) return null;
583
+ try {
584
+ const content = readFileSync(indexPath, "utf-8");
585
+ return JSON.parse(content);
586
+ } catch {
587
+ return null;
588
+ }
589
+ }
590
+ function loadSubagentMessages(projectDirPath, parentSessionId) {
591
+ const subagentsDir = join2(projectDirPath, parentSessionId, "subagents");
592
+ if (!existsSync2(subagentsDir)) return [];
593
+ const messages = [];
594
+ try {
595
+ const agentFiles = readdirSync(subagentsDir).filter((f) => f.endsWith(".jsonl"));
596
+ for (const agentFile of agentFiles) {
597
+ const agentPath = join2(subagentsDir, agentFile);
598
+ const agentMessages = parseSessionFile(agentPath);
599
+ messages.push(...agentMessages);
600
+ }
601
+ } catch {
602
+ }
603
+ return messages;
604
+ }
605
+ function loadSession(filePath) {
606
+ const rawData = readFileSync(filePath, "utf-8");
607
+ const messages = parseSessionFile(filePath);
608
+ if (messages.length === 0) {
609
+ return null;
610
+ }
611
+ const stats = statSync(filePath);
612
+ return {
613
+ filePath,
614
+ projectPath: null,
615
+ messages,
616
+ rawData,
617
+ modifiedAt: stats.mtime
618
+ };
619
+ }
620
+ function getMostRecentSession() {
621
+ const sessions = findSessionFiles();
622
+ return sessions.length > 0 ? sessions[0] : null;
623
+ }
624
+ function generateSummary(messages) {
625
+ const firstUserMessage = messages.find((m) => m.role === "user");
626
+ if (!firstUserMessage) {
627
+ return "No user messages found";
628
+ }
629
+ const summary = firstUserMessage.content.slice(0, 300);
630
+ return summary.length < firstUserMessage.content.length ? summary + "..." : summary;
631
+ }
632
+
633
+ // src/db/index.ts
634
+ var db = null;
635
+ function getDatabase() {
636
+ if (db) return db;
637
+ ensureCmemDir();
638
+ db = new Database(DB_PATH);
639
+ db.pragma("journal_mode = WAL");
640
+ sqliteVec.load(db);
641
+ initSchema(db);
642
+ return db;
643
+ }
644
+ function initSchema(database) {
645
+ database.exec(`
646
+ CREATE TABLE IF NOT EXISTS migrations (
647
+ name TEXT PRIMARY KEY,
648
+ applied_at TEXT NOT NULL
649
+ );
650
+ `);
651
+ database.exec(`
652
+ CREATE TABLE IF NOT EXISTS sessions (
653
+ id TEXT PRIMARY KEY,
654
+ title TEXT NOT NULL,
655
+ summary TEXT,
656
+ created_at TEXT NOT NULL,
657
+ updated_at TEXT NOT NULL,
658
+ message_count INTEGER DEFAULT 0,
659
+ project_path TEXT,
660
+ source_file TEXT,
661
+ raw_data TEXT NOT NULL
662
+ );
663
+ `);
664
+ try {
665
+ database.exec(`ALTER TABLE sessions ADD COLUMN source_file TEXT`);
666
+ } catch {
667
+ }
668
+ try {
669
+ database.exec(`ALTER TABLE sessions ADD COLUMN is_sidechain INTEGER DEFAULT 0`);
670
+ } catch {
671
+ }
672
+ try {
673
+ database.exec(`ALTER TABLE sessions ADD COLUMN is_automated INTEGER DEFAULT 0`);
674
+ } catch {
675
+ }
676
+ try {
677
+ database.exec(`ALTER TABLE sessions ADD COLUMN custom_title TEXT`);
678
+ } catch {
679
+ }
680
+ database.exec(`
681
+ CREATE TABLE IF NOT EXISTS messages (
682
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
683
+ session_id TEXT NOT NULL,
684
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
685
+ content TEXT NOT NULL,
686
+ timestamp TEXT NOT NULL,
687
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
688
+ );
689
+ `);
690
+ database.exec(`
691
+ CREATE TABLE IF NOT EXISTS embedding_state (
692
+ session_id TEXT PRIMARY KEY,
693
+ content_length INTEGER NOT NULL,
694
+ file_mtime TEXT,
695
+ last_embedded_at TEXT NOT NULL,
696
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
697
+ );
698
+ `);
699
+ database.exec(`
700
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_embeddings USING vec0(
701
+ session_id TEXT PRIMARY KEY,
702
+ embedding FLOAT[${EMBEDDING_DIMENSIONS}]
703
+ );
704
+ `);
705
+ database.exec(`
706
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
707
+ `);
708
+ database.exec(`
709
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
710
+ `);
711
+ database.exec(`
712
+ CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_file);
713
+ `);
714
+ runMigrations(database);
715
+ }
716
+ function runMigrations(database) {
717
+ const migrationName = "populate_session_metadata_v1";
718
+ const existing = database.prepare(
719
+ "SELECT 1 FROM migrations WHERE name = ?"
720
+ ).get(migrationName);
721
+ if (existing) return;
722
+ const sessions = database.prepare(`
723
+ SELECT id, source_file FROM sessions WHERE source_file IS NOT NULL
724
+ `).all();
725
+ const updateStmt = database.prepare(`
726
+ UPDATE sessions SET is_sidechain = ?, is_automated = ? WHERE id = ?
727
+ `);
728
+ const transaction = database.transaction(() => {
729
+ for (const session of sessions) {
730
+ if (!existsSync3(session.source_file)) continue;
731
+ try {
732
+ const metadata = extractSessionMetadata(session.source_file);
733
+ const isAutomated = metadata.isSidechain || metadata.isMeta;
734
+ updateStmt.run(
735
+ metadata.isSidechain ? 1 : 0,
736
+ isAutomated ? 1 : 0,
737
+ session.id
738
+ );
739
+ } catch {
740
+ }
741
+ }
742
+ database.prepare(
743
+ "INSERT INTO migrations (name, applied_at) VALUES (?, ?)"
744
+ ).run(migrationName, (/* @__PURE__ */ new Date()).toISOString());
745
+ });
746
+ transaction();
747
+ }
748
+
749
+ // src/db/sessions.ts
750
+ import { randomUUID } from "crypto";
751
+ function createSession(input) {
752
+ const db2 = getDatabase();
753
+ const id = randomUUID();
754
+ const now = (/* @__PURE__ */ new Date()).toISOString();
755
+ const insertSession = db2.prepare(`
756
+ INSERT INTO sessions (id, title, summary, created_at, updated_at, message_count, project_path, source_file, raw_data, is_sidechain, is_automated)
757
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
758
+ `);
759
+ const insertMessage = db2.prepare(`
760
+ INSERT INTO messages (session_id, role, content, timestamp)
761
+ VALUES (?, ?, ?, ?)
762
+ `);
763
+ const transaction = db2.transaction(() => {
764
+ insertSession.run(
765
+ id,
766
+ input.title,
767
+ input.summary || null,
768
+ now,
769
+ now,
770
+ input.messages.length,
771
+ input.projectPath || null,
772
+ input.sourceFile || null,
773
+ input.rawData,
774
+ input.isSidechain ? 1 : 0,
775
+ input.isAutomated ? 1 : 0
776
+ );
777
+ for (const msg of input.messages) {
778
+ insertMessage.run(id, msg.role, msg.content, msg.timestamp);
779
+ }
780
+ });
781
+ transaction();
782
+ return id;
783
+ }
784
+ function updateSession(id, input) {
785
+ const db2 = getDatabase();
786
+ const now = (/* @__PURE__ */ new Date()).toISOString();
787
+ const updates = ["updated_at = ?"];
788
+ const values = [now];
789
+ if (input.title !== void 0) {
790
+ updates.push("title = ?");
791
+ values.push(input.title);
792
+ }
793
+ if (input.summary !== void 0) {
794
+ updates.push("summary = ?");
795
+ values.push(input.summary);
796
+ }
797
+ if (input.rawData !== void 0) {
798
+ updates.push("raw_data = ?");
799
+ values.push(input.rawData);
800
+ }
801
+ if (input.messages !== void 0) {
802
+ updates.push("message_count = ?");
803
+ values.push(input.messages.length);
804
+ }
805
+ if (input.isSidechain !== void 0) {
806
+ updates.push("is_sidechain = ?");
807
+ values.push(input.isSidechain ? 1 : 0);
808
+ }
809
+ if (input.isAutomated !== void 0) {
810
+ updates.push("is_automated = ?");
811
+ values.push(input.isAutomated ? 1 : 0);
812
+ }
813
+ values.push(id);
814
+ const transaction = db2.transaction(() => {
815
+ db2.prepare(`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`).run(...values);
816
+ if (input.messages !== void 0) {
817
+ db2.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
818
+ const insertMessage = db2.prepare(`
819
+ INSERT INTO messages (session_id, role, content, timestamp)
820
+ VALUES (?, ?, ?, ?)
821
+ `);
822
+ for (const msg of input.messages) {
823
+ insertMessage.run(id, msg.role, msg.content, msg.timestamp);
824
+ }
825
+ }
826
+ });
827
+ transaction();
828
+ }
829
+ function getSession(id) {
830
+ const db2 = getDatabase();
831
+ const row = db2.prepare(`
832
+ SELECT id, title, custom_title as customTitle, summary,
833
+ created_at as createdAt, updated_at as updatedAt,
834
+ message_count as messageCount, project_path as projectPath,
835
+ source_file as sourceFile, raw_data as rawData,
836
+ is_sidechain as isSidechain, is_automated as isAutomated
837
+ FROM sessions WHERE id = ?
838
+ `).get(id);
839
+ if (!row) return null;
840
+ return mapSessionRow(row);
841
+ }
842
+ function getSessionByIdPrefix(idPrefix) {
843
+ const db2 = getDatabase();
844
+ const row = db2.prepare(`
845
+ SELECT id, title, custom_title as customTitle, summary,
846
+ created_at as createdAt, updated_at as updatedAt,
847
+ message_count as messageCount, project_path as projectPath,
848
+ source_file as sourceFile, raw_data as rawData,
849
+ is_sidechain as isSidechain, is_automated as isAutomated
850
+ FROM sessions WHERE id LIKE ? || '%'
851
+ LIMIT 1
852
+ `).get(idPrefix);
853
+ if (!row) return null;
854
+ return mapSessionRow(row);
855
+ }
856
+ function getSessionBySourceFile(sourceFile) {
857
+ const db2 = getDatabase();
858
+ const row = db2.prepare(`
859
+ SELECT id, title, custom_title as customTitle, summary,
860
+ created_at as createdAt, updated_at as updatedAt,
861
+ message_count as messageCount, project_path as projectPath,
862
+ source_file as sourceFile, raw_data as rawData,
863
+ is_sidechain as isSidechain, is_automated as isAutomated
864
+ FROM sessions WHERE source_file = ?
865
+ `).get(sourceFile);
866
+ if (!row) return null;
867
+ return mapSessionRow(row);
868
+ }
869
+ function getSessionMessages(sessionId) {
870
+ const db2 = getDatabase();
871
+ const rows = db2.prepare(`
872
+ SELECT id, session_id as sessionId, role, content, timestamp
873
+ FROM messages WHERE session_id = ?
874
+ ORDER BY timestamp ASC
875
+ `).all(sessionId);
876
+ return rows;
877
+ }
878
+ function mapSessionRow(row) {
879
+ return {
880
+ ...row,
881
+ customTitle: row.customTitle,
882
+ isSidechain: row.isSidechain === 1,
883
+ isAutomated: row.isAutomated === 1
884
+ };
885
+ }
886
+ function listSessions(limit = 100) {
887
+ const db2 = getDatabase();
888
+ const rows = db2.prepare(`
889
+ SELECT id, title, custom_title as customTitle, summary,
890
+ created_at as createdAt, updated_at as updatedAt,
891
+ message_count as messageCount, project_path as projectPath,
892
+ source_file as sourceFile, raw_data as rawData,
893
+ is_sidechain as isSidechain, is_automated as isAutomated
894
+ FROM sessions
895
+ ORDER BY updated_at DESC
896
+ LIMIT ?
897
+ `).all(limit);
898
+ return rows.map(mapSessionRow);
899
+ }
900
+ function listHumanSessions(limit = 100) {
901
+ const db2 = getDatabase();
902
+ const rows = db2.prepare(`
903
+ SELECT id, title, custom_title as customTitle, summary,
904
+ created_at as createdAt, updated_at as updatedAt,
905
+ message_count as messageCount, project_path as projectPath,
906
+ source_file as sourceFile, raw_data as rawData,
907
+ is_sidechain as isSidechain, is_automated as isAutomated
908
+ FROM sessions
909
+ WHERE is_sidechain = 0 AND is_automated = 0
910
+ ORDER BY updated_at DESC
911
+ LIMIT ?
912
+ `).all(limit);
913
+ return rows.map(mapSessionRow);
914
+ }
915
+ function listHumanSessionsByProject(projectPath, limit = 100) {
916
+ const db2 = getDatabase();
917
+ const rows = db2.prepare(`
918
+ SELECT id, title, custom_title as customTitle, summary,
919
+ created_at as createdAt, updated_at as updatedAt,
920
+ message_count as messageCount, project_path as projectPath,
921
+ source_file as sourceFile, raw_data as rawData,
922
+ is_sidechain as isSidechain, is_automated as isAutomated
923
+ FROM sessions
924
+ WHERE is_sidechain = 0 AND is_automated = 0
925
+ AND project_path LIKE ? || '%'
926
+ ORDER BY updated_at DESC
927
+ LIMIT ?
928
+ `).all(projectPath, limit);
929
+ return rows.map(mapSessionRow);
930
+ }
931
+ function deleteSession(id) {
932
+ const db2 = getDatabase();
933
+ const transaction = db2.transaction(() => {
934
+ db2.prepare("DELETE FROM session_embeddings WHERE session_id = ?").run(id);
935
+ db2.prepare("DELETE FROM embedding_state WHERE session_id = ?").run(id);
936
+ db2.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
937
+ const result = db2.prepare("DELETE FROM sessions WHERE id = ?").run(id);
938
+ return result.changes > 0;
939
+ });
940
+ return transaction();
941
+ }
942
+ function renameSession(id, customTitle) {
943
+ const db2 = getDatabase();
944
+ db2.prepare(`
945
+ UPDATE sessions SET custom_title = ?, updated_at = ? WHERE id = ?
946
+ `).run(customTitle, (/* @__PURE__ */ new Date()).toISOString(), id);
947
+ }
948
+ function getStats() {
949
+ const db2 = getDatabase();
950
+ const sessionCount = db2.prepare("SELECT COUNT(*) as count FROM sessions").get().count;
951
+ const messageCount = db2.prepare("SELECT COUNT(*) as count FROM messages").get().count;
952
+ const embeddingCount = db2.prepare("SELECT COUNT(*) as count FROM session_embeddings").get().count;
953
+ return { sessionCount, messageCount, embeddingCount };
954
+ }
955
+ function sessionExists(rawData) {
956
+ const db2 = getDatabase();
957
+ const row = db2.prepare("SELECT 1 FROM sessions WHERE raw_data = ? LIMIT 1").get(rawData);
958
+ return !!row;
959
+ }
960
+ function getEmbeddingState(sessionId) {
961
+ const db2 = getDatabase();
962
+ const row = db2.prepare(`
963
+ SELECT session_id as sessionId, content_length as contentLength,
964
+ file_mtime as fileMtime, last_embedded_at as lastEmbeddedAt
965
+ FROM embedding_state WHERE session_id = ?
966
+ `).get(sessionId);
967
+ return row || null;
968
+ }
969
+ function updateEmbeddingState(sessionId, contentLength, fileMtime) {
970
+ const db2 = getDatabase();
971
+ const now = (/* @__PURE__ */ new Date()).toISOString();
972
+ db2.prepare(`
973
+ INSERT OR REPLACE INTO embedding_state (session_id, content_length, file_mtime, last_embedded_at)
974
+ VALUES (?, ?, ?, ?)
975
+ `).run(sessionId, contentLength, fileMtime || null, now);
976
+ }
977
+ function needsReembedding(sessionId, currentContentLength, threshold = 500) {
978
+ const state = getEmbeddingState(sessionId);
979
+ if (!state) return true;
980
+ return currentContentLength - state.contentLength >= threshold;
981
+ }
982
+
983
+ // src/db/vectors.ts
984
+ function storeEmbedding(sessionId, embedding) {
985
+ const db2 = getDatabase();
986
+ const embeddingJson = JSON.stringify(embedding);
987
+ db2.prepare(`
988
+ INSERT OR REPLACE INTO session_embeddings (session_id, embedding)
989
+ VALUES (?, ?)
990
+ `).run(sessionId, embeddingJson);
991
+ }
992
+ function searchSessions(queryEmbedding, limit = 10) {
993
+ const db2 = getDatabase();
994
+ const embeddingJson = JSON.stringify(queryEmbedding);
995
+ const rows = db2.prepare(`
996
+ SELECT s.id, s.title, s.summary, s.created_at as createdAt, s.updated_at as updatedAt,
997
+ s.message_count as messageCount, s.project_path as projectPath, s.raw_data as rawData,
998
+ vec_distance_L2(e.embedding, ?) as distance
999
+ FROM session_embeddings e
1000
+ JOIN sessions s ON e.session_id = s.id
1001
+ ORDER BY distance ASC
1002
+ LIMIT ?
1003
+ `).all(embeddingJson, limit);
1004
+ return rows;
1005
+ }
1006
+
1007
+ // src/embeddings/index.ts
1008
+ import { existsSync as existsSync4 } from "fs";
1009
+ import { join as join3 } from "path";
1010
+ var transformersModule = null;
1011
+ var pipeline = null;
1012
+ var initialized = false;
1013
+ async function getTransformers() {
1014
+ if (!transformersModule) {
1015
+ transformersModule = await import("@xenova/transformers");
1016
+ }
1017
+ return transformersModule;
1018
+ }
1019
+ function isModelCached() {
1020
+ const modelCachePath = join3(MODELS_DIR, EMBEDDING_MODEL);
1021
+ return existsSync4(modelCachePath);
1022
+ }
1023
+ async function initializeEmbeddings(onProgress) {
1024
+ if (initialized && pipeline) {
1025
+ return;
1026
+ }
1027
+ ensureModelsDir();
1028
+ const { pipeline: createPipeline, env } = await getTransformers();
1029
+ env.cacheDir = MODELS_DIR;
1030
+ const cached = isModelCached();
1031
+ if (cached) {
1032
+ env.allowRemoteModels = false;
1033
+ onProgress?.({ status: "loading" });
1034
+ } else {
1035
+ onProgress?.({ status: "downloading" });
1036
+ }
1037
+ pipeline = await createPipeline("feature-extraction", EMBEDDING_MODEL, {
1038
+ progress_callback: onProgress ? (progress) => {
1039
+ if (progress.status === "progress" && progress.file && progress.progress !== void 0) {
1040
+ onProgress({
1041
+ status: "downloading",
1042
+ file: progress.file,
1043
+ progress: progress.progress
1044
+ });
1045
+ }
1046
+ } : void 0
1047
+ });
1048
+ initialized = true;
1049
+ onProgress?.({ status: "ready" });
1050
+ }
1051
+ function isReady() {
1052
+ return initialized && pipeline !== null;
1053
+ }
1054
+ async function getEmbedding(text) {
1055
+ if (!initialized) {
1056
+ await initializeEmbeddings();
1057
+ }
1058
+ const truncated = text.slice(0, MAX_EMBEDDING_CHARS);
1059
+ const output = await pipeline(truncated, {
1060
+ pooling: "mean",
1061
+ normalize: true
1062
+ });
1063
+ return Array.from(output.data);
1064
+ }
1065
+ function createEmbeddingText(title, summary, messages) {
1066
+ const parts = [];
1067
+ parts.push(`Title: ${title}`);
1068
+ if (summary) {
1069
+ parts.push(`Summary: ${summary}`);
1070
+ }
1071
+ const userMessages = messages.filter((m) => m.role === "user").slice(0, 5).map((m) => m.content.slice(0, MAX_MESSAGE_PREVIEW_CHARS));
1072
+ if (userMessages.length > 0) {
1073
+ parts.push("User messages:");
1074
+ parts.push(...userMessages);
1075
+ }
1076
+ const assistantMessages = messages.filter((m) => m.role === "assistant").slice(0, 3).map((m) => m.content.slice(0, MAX_MESSAGE_PREVIEW_CHARS));
1077
+ if (assistantMessages.length > 0) {
1078
+ parts.push("Assistant responses:");
1079
+ parts.push(...assistantMessages);
1080
+ }
1081
+ return parts.join("\n\n");
1082
+ }
1083
+
1084
+ // src/ui/hooks/useSessions.ts
1085
+ function useSessions(options = {}) {
1086
+ const [sessions, setSessions] = useState([]);
1087
+ const [allSessions, setAllSessions] = useState([]);
1088
+ const [loading, setLoading] = useState(true);
1089
+ const [error, setError] = useState(null);
1090
+ const [embeddingsReady, setEmbeddingsReady] = useState(false);
1091
+ const [isSearching, setIsSearching] = useState(false);
1092
+ const [projectFilter, setProjectFilter] = useState(options.projectFilter ?? null);
1093
+ const loadSessions = useCallback(() => {
1094
+ try {
1095
+ const loaded = projectFilter ? listHumanSessionsByProject(projectFilter) : listHumanSessions();
1096
+ setAllSessions(loaded);
1097
+ setSessions(loaded);
1098
+ setError(null);
1099
+ } catch (err) {
1100
+ setError(String(err));
1101
+ } finally {
1102
+ setLoading(false);
1103
+ }
1104
+ }, [projectFilter]);
1105
+ const initEmbeddings = useCallback(async () => {
1106
+ if (isReady()) {
1107
+ setEmbeddingsReady(true);
1108
+ return;
1109
+ }
1110
+ try {
1111
+ await initializeEmbeddings();
1112
+ setEmbeddingsReady(true);
1113
+ } catch {
1114
+ setEmbeddingsReady(false);
1115
+ }
1116
+ }, []);
1117
+ useEffect(() => {
1118
+ loadSessions();
1119
+ initEmbeddings();
1120
+ }, [loadSessions, initEmbeddings]);
1121
+ useEffect(() => {
1122
+ loadSessions();
1123
+ }, [projectFilter, loadSessions]);
1124
+ const search = useCallback(async (query) => {
1125
+ if (!query.trim()) {
1126
+ setSessions(allSessions);
1127
+ setIsSearching(false);
1128
+ return;
1129
+ }
1130
+ if (!embeddingsReady) {
1131
+ const filtered = allSessions.filter(
1132
+ (s) => s.title.toLowerCase().includes(query.toLowerCase()) || s.summary && s.summary.toLowerCase().includes(query.toLowerCase())
1133
+ );
1134
+ setSessions(filtered);
1135
+ setIsSearching(true);
1136
+ return;
1137
+ }
1138
+ try {
1139
+ setLoading(true);
1140
+ const queryEmbedding = await getEmbedding(query);
1141
+ const results = searchSessions(queryEmbedding, 20);
1142
+ setSessions(results);
1143
+ setIsSearching(true);
1144
+ } catch (err) {
1145
+ setError(String(err));
1146
+ const filtered = allSessions.filter(
1147
+ (s) => s.title.toLowerCase().includes(query.toLowerCase()) || s.summary && s.summary.toLowerCase().includes(query.toLowerCase())
1148
+ );
1149
+ setSessions(filtered);
1150
+ } finally {
1151
+ setLoading(false);
1152
+ }
1153
+ }, [allSessions, embeddingsReady]);
1154
+ const clearSearch = useCallback(() => {
1155
+ setSessions(allSessions);
1156
+ setIsSearching(false);
1157
+ }, [allSessions]);
1158
+ const deleteSessionHandler = useCallback((id) => {
1159
+ const deleted = deleteSession(id);
1160
+ if (deleted) {
1161
+ setAllSessions((prev) => prev.filter((s) => s.id !== id));
1162
+ setSessions((prev) => prev.filter((s) => s.id !== id));
1163
+ }
1164
+ }, []);
1165
+ const getSessionById = useCallback((id) => {
1166
+ return getSession(id);
1167
+ }, []);
1168
+ return {
1169
+ sessions,
1170
+ loading,
1171
+ error,
1172
+ embeddingsReady,
1173
+ projectFilter,
1174
+ setProjectFilter,
1175
+ refresh: loadSessions,
1176
+ search,
1177
+ clearSearch,
1178
+ deleteSession: deleteSessionHandler,
1179
+ getSessionById
1180
+ };
1181
+ }
1182
+
1183
+ // src/ui/App.tsx
1184
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1185
+ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1186
+ const { exit } = useApp();
1187
+ const {
1188
+ sessions,
1189
+ loading,
1190
+ embeddingsReady,
1191
+ projectFilter,
1192
+ setProjectFilter,
1193
+ search,
1194
+ clearSearch,
1195
+ deleteSession: deleteSession2,
1196
+ refresh
1197
+ } = useSessions({ projectFilter: initialProjectFilter });
1198
+ const [selectedIndex, setSelectedIndex] = useState2(0);
1199
+ const [searchQuery, setSearchQuery] = useState2("");
1200
+ const [mode, setMode] = useState2("list");
1201
+ const [statusMessage, setStatusMessage] = useState2(null);
1202
+ const [renameValue, setRenameValue] = useState2("");
1203
+ useEffect2(() => {
1204
+ if (selectedIndex >= sessions.length) {
1205
+ setSelectedIndex(Math.max(0, sessions.length - 1));
1206
+ }
1207
+ }, [sessions, selectedIndex]);
1208
+ useEffect2(() => {
1209
+ if (mode === "search" && searchQuery.length > 2) {
1210
+ const timer = setTimeout(() => {
1211
+ search(searchQuery);
1212
+ }, 300);
1213
+ return () => clearTimeout(timer);
1214
+ } else if (mode === "search" && searchQuery.length === 0) {
1215
+ clearSearch();
1216
+ }
1217
+ }, [searchQuery, mode, search, clearSearch]);
1218
+ useEffect2(() => {
1219
+ if (statusMessage) {
1220
+ const timer = setTimeout(() => setStatusMessage(null), 3e3);
1221
+ return () => clearTimeout(timer);
1222
+ }
1223
+ }, [statusMessage]);
1224
+ const handleRestore = useCallback2(() => {
1225
+ const session = sessions[selectedIndex];
1226
+ if (!session) return;
1227
+ if (session.sourceFile) {
1228
+ const filename = basename4(session.sourceFile);
1229
+ const claudeSessionId = filename.replace(".jsonl", "");
1230
+ const projectDirName = basename4(dirname2(session.sourceFile));
1231
+ let projectPath = null;
1232
+ if (projectDirName.startsWith("-")) {
1233
+ const segments = projectDirName.substring(1).split("-");
1234
+ let currentPath = "";
1235
+ let remainingSegments = [...segments];
1236
+ while (remainingSegments.length > 0) {
1237
+ let found = false;
1238
+ for (let i = remainingSegments.length; i > 0; i--) {
1239
+ const testSegment = remainingSegments.slice(0, i).join("-");
1240
+ const testPath = currentPath + "/" + testSegment;
1241
+ if (existsSync5(testPath)) {
1242
+ currentPath = testPath;
1243
+ remainingSegments = remainingSegments.slice(i);
1244
+ found = true;
1245
+ break;
1246
+ }
1247
+ }
1248
+ if (!found) {
1249
+ currentPath = currentPath + "/" + remainingSegments.join("-");
1250
+ break;
1251
+ }
1252
+ }
1253
+ if (currentPath && existsSync5(currentPath)) {
1254
+ projectPath = currentPath;
1255
+ }
1256
+ }
1257
+ if (onResume) {
1258
+ onResume(claudeSessionId, projectPath);
1259
+ exit();
1260
+ }
1261
+ } else {
1262
+ setStatusMessage("No source file - cannot resume this session");
1263
+ }
1264
+ }, [sessions, selectedIndex, onResume, exit]);
1265
+ const handleDelete = useCallback2(() => {
1266
+ const session = sessions[selectedIndex];
1267
+ if (session) {
1268
+ deleteSession2(session.id);
1269
+ setStatusMessage(`Deleted: ${session.customTitle || session.title}`);
1270
+ setMode("list");
1271
+ }
1272
+ }, [sessions, selectedIndex, deleteSession2]);
1273
+ const handleRename = useCallback2(() => {
1274
+ const session = sessions[selectedIndex];
1275
+ if (session && renameValue.trim()) {
1276
+ renameSession(session.id, renameValue.trim());
1277
+ setStatusMessage(`Renamed to: ${renameValue.trim()}`);
1278
+ refresh();
1279
+ }
1280
+ setMode("list");
1281
+ setRenameValue("");
1282
+ }, [sessions, selectedIndex, renameValue, refresh]);
1283
+ const handleClearRename = useCallback2(() => {
1284
+ const session = sessions[selectedIndex];
1285
+ if (session && session.customTitle) {
1286
+ renameSession(session.id, null);
1287
+ setStatusMessage(`Cleared custom name`);
1288
+ refresh();
1289
+ }
1290
+ setMode("list");
1291
+ }, [sessions, selectedIndex, refresh]);
1292
+ useInput((input, key) => {
1293
+ if (input === "q" && mode !== "search" && mode !== "rename") {
1294
+ exit();
1295
+ return;
1296
+ }
1297
+ if (mode === "confirm-delete") {
1298
+ if (input === "y" || input === "Y") {
1299
+ handleDelete();
1300
+ } else {
1301
+ setMode("list");
1302
+ }
1303
+ return;
1304
+ }
1305
+ if (mode === "rename") {
1306
+ if (key.escape) {
1307
+ setMode("list");
1308
+ setRenameValue("");
1309
+ return;
1310
+ }
1311
+ if (key.return) {
1312
+ handleRename();
1313
+ return;
1314
+ }
1315
+ return;
1316
+ }
1317
+ if (mode === "search") {
1318
+ if (key.escape) {
1319
+ setMode("list");
1320
+ setSearchQuery("");
1321
+ clearSearch();
1322
+ return;
1323
+ }
1324
+ if (key.return) {
1325
+ setMode("list");
1326
+ return;
1327
+ }
1328
+ return;
1329
+ }
1330
+ if (input === "/") {
1331
+ setMode("search");
1332
+ return;
1333
+ }
1334
+ if (input === "r") {
1335
+ if (sessions.length > 0) {
1336
+ const session = sessions[selectedIndex];
1337
+ setRenameValue(session?.customTitle || "");
1338
+ setMode("rename");
1339
+ }
1340
+ return;
1341
+ }
1342
+ if (input === "R") {
1343
+ handleClearRename();
1344
+ return;
1345
+ }
1346
+ if (key.upArrow || input === "k") {
1347
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
1348
+ return;
1349
+ }
1350
+ if (key.downArrow || input === "j") {
1351
+ setSelectedIndex((prev) => Math.min(sessions.length - 1, prev + 1));
1352
+ return;
1353
+ }
1354
+ if (key.return) {
1355
+ handleRestore();
1356
+ return;
1357
+ }
1358
+ if (input === "d") {
1359
+ if (sessions.length > 0) {
1360
+ setMode("confirm-delete");
1361
+ }
1362
+ return;
1363
+ }
1364
+ if (input === "f") {
1365
+ if (projectFilter) {
1366
+ setProjectFilter(null);
1367
+ setStatusMessage("Filter cleared - showing all sessions");
1368
+ } else {
1369
+ const session = sessions[selectedIndex];
1370
+ if (session?.projectPath) {
1371
+ setProjectFilter(session.projectPath);
1372
+ setStatusMessage(`Filtering to: ${basename4(session.projectPath)}`);
1373
+ } else {
1374
+ setStatusMessage("No folder for this session");
1375
+ }
1376
+ }
1377
+ setSelectedIndex(0);
1378
+ return;
1379
+ }
1380
+ });
1381
+ const handleSearchChange = useCallback2((value) => {
1382
+ setSearchQuery(value);
1383
+ setSelectedIndex(0);
1384
+ }, []);
1385
+ if (loading && sessions.length === 0) {
1386
+ return /* @__PURE__ */ jsxs5(Box5, { padding: 1, children: [
1387
+ /* @__PURE__ */ jsx5(Spinner, { type: "dots" }),
1388
+ /* @__PURE__ */ jsx5(Text5, { children: " Loading sessions..." })
1389
+ ] });
1390
+ }
1391
+ const selectedSession = sessions[selectedIndex] || null;
1392
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", padding: 1, children: [
1393
+ /* @__PURE__ */ jsx5(Header, { embeddingsReady, projectFilter }),
1394
+ /* @__PURE__ */ jsx5(
1395
+ SearchInput,
1396
+ {
1397
+ value: searchQuery,
1398
+ onChange: handleSearchChange,
1399
+ isFocused: mode === "search"
1400
+ }
1401
+ ),
1402
+ /* @__PURE__ */ jsx5(
1403
+ SessionList,
1404
+ {
1405
+ sessions,
1406
+ selectedIndex,
1407
+ onSelect: setSelectedIndex
1408
+ }
1409
+ ),
1410
+ /* @__PURE__ */ jsx5(Preview, { session: selectedSession }),
1411
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
1412
+ 'Delete "',
1413
+ selectedSession?.customTitle || selectedSession?.title,
1414
+ '"? [y/n]'
1415
+ ] }) : mode === "rename" ? /* @__PURE__ */ jsxs5(Box5, { children: [
1416
+ /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: "Rename: " }),
1417
+ /* @__PURE__ */ jsx5(
1418
+ TextInput2,
1419
+ {
1420
+ value: renameValue,
1421
+ onChange: setRenameValue,
1422
+ placeholder: "Enter new name..."
1423
+ }
1424
+ ),
1425
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1426
+ ] }) : statusMessage ? /* @__PURE__ */ jsx5(Text5, { color: "green", children: statusMessage }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u2191\u2193/jk] Navigate [Enter] Resume [f] Filter folder [r] Rename [d] Delete [/] Search [q] Quit" }) })
1427
+ ] });
1428
+ };
1429
+
1430
+ // src/commands/save.ts
1431
+ import chalk from "chalk";
1432
+ async function saveCommand(options) {
1433
+ console.log(chalk.cyan("Scanning for Claude Code sessions..."));
1434
+ let embeddingsReady = isReady();
1435
+ if (!embeddingsReady) {
1436
+ console.log(chalk.dim("Initializing embedding model..."));
1437
+ try {
1438
+ await initializeEmbeddings();
1439
+ embeddingsReady = true;
1440
+ } catch (err) {
1441
+ console.log(chalk.yellow("Warning: Could not initialize embeddings."));
1442
+ console.log(chalk.yellow("Semantic search will not be available."));
1443
+ }
1444
+ }
1445
+ const sessions = findSessionFiles();
1446
+ if (sessions.length === 0) {
1447
+ console.log(chalk.red("No Claude Code sessions found."));
1448
+ console.log(chalk.dim("Looking in:"));
1449
+ console.log(chalk.dim(" ~/.claude/projects/"));
1450
+ console.log(chalk.dim(" ~/.claude/sessions/"));
1451
+ console.log(chalk.dim("\nUse Claude Code first, then run: cmem save --latest"));
1452
+ return;
1453
+ }
1454
+ console.log(chalk.green(`Found ${sessions.length} session(s)`));
1455
+ let sessionToSave = options.latest ? getMostRecentSession() : sessions[0];
1456
+ if (!sessionToSave) {
1457
+ console.log(chalk.red("No valid session found."));
1458
+ return;
1459
+ }
1460
+ if (sessionExists(sessionToSave.rawData)) {
1461
+ console.log(chalk.yellow("This session has already been saved."));
1462
+ return;
1463
+ }
1464
+ const firstUserMsg = sessionToSave.messages.find((m) => m.role === "user");
1465
+ const title = options.title || (firstUserMsg ? generateTitle(firstUserMsg.content) : "Untitled Session");
1466
+ const summary = generateSummary(sessionToSave.messages);
1467
+ console.log(chalk.dim(`Title: ${title}`));
1468
+ console.log(chalk.dim(`Messages: ${sessionToSave.messages.length}`));
1469
+ console.log(chalk.dim(`Project: ${sessionToSave.projectPath || "Unknown"}`));
1470
+ const sessionId = createSession({
1471
+ title,
1472
+ summary,
1473
+ projectPath: sessionToSave.projectPath || void 0,
1474
+ rawData: sessionToSave.rawData,
1475
+ messages: sessionToSave.messages
1476
+ });
1477
+ console.log(chalk.green(`Session saved with ID: ${sessionId.slice(0, 8)}`));
1478
+ if (embeddingsReady) {
1479
+ console.log(chalk.dim("Generating embedding..."));
1480
+ try {
1481
+ const embeddingText = createEmbeddingText(title, summary, sessionToSave.messages);
1482
+ const embedding = await getEmbedding(embeddingText);
1483
+ storeEmbedding(sessionId, embedding);
1484
+ console.log(chalk.green("Embedding stored for semantic search."));
1485
+ } catch (err) {
1486
+ console.log(chalk.yellow("Failed to generate embedding. Semantic search may not work for this session."));
1487
+ }
1488
+ }
1489
+ console.log(chalk.green("\nSession saved successfully!"));
1490
+ }
1491
+
1492
+ // src/commands/list.ts
1493
+ import chalk2 from "chalk";
1494
+ async function listCommand(options = {}) {
1495
+ const sessions = options.all ? listSessions() : listHumanSessions();
1496
+ if (sessions.length === 0) {
1497
+ console.log(chalk2.yellow("No saved sessions."));
1498
+ console.log(chalk2.dim("Run: cmem save --latest"));
1499
+ return;
1500
+ }
1501
+ console.log(chalk2.cyan(`Saved Sessions (${sessions.length})
1502
+ `));
1503
+ console.log(
1504
+ chalk2.dim(
1505
+ "ID".padEnd(10) + "Title".padEnd(40) + "Msgs".padEnd(6) + "Updated".padEnd(10) + "Project"
1506
+ )
1507
+ );
1508
+ console.log(chalk2.dim("\u2500".repeat(90)));
1509
+ for (const session of sessions) {
1510
+ const id = shortId(session.id);
1511
+ const title = truncate(session.title, 38);
1512
+ const msgs = String(session.messageCount).padStart(4);
1513
+ const updated = formatTimeAgo(session.updatedAt);
1514
+ const project = session.projectPath ? truncate(session.projectPath, 25) : chalk2.dim("\u2014");
1515
+ console.log(
1516
+ chalk2.white(id.padEnd(10)) + title.padEnd(40) + chalk2.dim(msgs.padEnd(6)) + chalk2.dim(updated.padEnd(10)) + chalk2.dim(project)
1517
+ );
1518
+ }
1519
+ console.log(chalk2.dim("\n\u2500".repeat(90)));
1520
+ console.log(chalk2.dim("Use: cmem restore <id> to restore a session"));
1521
+ }
1522
+
1523
+ // src/commands/search.ts
1524
+ import chalk3 from "chalk";
1525
+ async function searchCommand(query) {
1526
+ console.log(chalk3.cyan(`Searching for: "${query}"...
1527
+ `));
1528
+ if (!isReady()) {
1529
+ console.log(chalk3.dim("Initializing embedding model..."));
1530
+ try {
1531
+ await initializeEmbeddings((progress) => {
1532
+ if (progress.status === "downloading" && progress.progress !== void 0) {
1533
+ process.stdout.write(`\r${chalk3.dim(`Downloading model... ${Math.round(progress.progress)}%`)}`);
1534
+ }
1535
+ });
1536
+ console.log(chalk3.green("\r\u2713 Model ready \n"));
1537
+ } catch (err) {
1538
+ console.log(chalk3.red("Failed to initialize embedding model."));
1539
+ console.log(chalk3.dim(String(err)));
1540
+ return;
1541
+ }
1542
+ }
1543
+ try {
1544
+ const queryEmbedding = await getEmbedding(query);
1545
+ const results = searchSessions(queryEmbedding, 10);
1546
+ if (results.length === 0) {
1547
+ console.log(chalk3.yellow("No matching sessions found."));
1548
+ console.log(chalk3.dim("Try a different search query or save more sessions."));
1549
+ return;
1550
+ }
1551
+ console.log(chalk3.green(`Found ${results.length} matching session(s)
1552
+ `));
1553
+ console.log(
1554
+ chalk3.dim(
1555
+ "ID".padEnd(10) + "Title".padEnd(40) + "Msgs".padEnd(6) + "Updated"
1556
+ )
1557
+ );
1558
+ console.log(chalk3.dim("\u2500".repeat(70)));
1559
+ for (const session of results) {
1560
+ const id = shortId(session.id);
1561
+ const title = truncate(session.title, 38);
1562
+ const msgs = String(session.messageCount).padStart(4);
1563
+ const updated = formatTimeAgo(session.updatedAt);
1564
+ console.log(
1565
+ chalk3.white(id.padEnd(10)) + title.padEnd(40) + chalk3.dim(msgs.padEnd(6)) + chalk3.dim(updated)
1566
+ );
1567
+ if (session.summary) {
1568
+ console.log(chalk3.dim(" " + truncate(session.summary, 65)));
1569
+ }
1570
+ }
1571
+ console.log(chalk3.dim("\n\u2500".repeat(70)));
1572
+ console.log(chalk3.dim("Use: cmem restore <id> to restore a session"));
1573
+ } catch (err) {
1574
+ console.log(chalk3.red("Search failed."));
1575
+ console.log(chalk3.dim(String(err)));
1576
+ }
1577
+ }
1578
+
1579
+ // src/commands/restore.ts
1580
+ import chalk4 from "chalk";
1581
+
1582
+ // src/utils/clipboard.ts
1583
+ import clipboard from "clipboardy";
1584
+ async function copyToClipboard(text) {
1585
+ await clipboard.write(text);
1586
+ }
1587
+
1588
+ // src/commands/restore.ts
1589
+ async function restoreCommand(id, options) {
1590
+ let session = getSession(id);
1591
+ if (!session) {
1592
+ const sessions = listSessions();
1593
+ const match = sessions.find((s) => s.id.startsWith(id));
1594
+ if (match) {
1595
+ session = match;
1596
+ }
1597
+ }
1598
+ if (!session) {
1599
+ console.log(chalk4.red(`Session not found: ${id}`));
1600
+ console.log(chalk4.dim("Run: cmem list to see available sessions"));
1601
+ return;
1602
+ }
1603
+ const messages = getSessionMessages(session.id);
1604
+ const format = options.format || "context";
1605
+ let output;
1606
+ switch (format) {
1607
+ case "json":
1608
+ output = formatAsJson(session, messages);
1609
+ break;
1610
+ case "markdown":
1611
+ output = formatAsMarkdown(session, messages);
1612
+ break;
1613
+ case "context":
1614
+ default:
1615
+ output = formatAsContext(session, messages);
1616
+ break;
1617
+ }
1618
+ if (options.copy) {
1619
+ await copyToClipboard(output);
1620
+ console.log(chalk4.green("Session context copied to clipboard!"));
1621
+ console.log(chalk4.dim(`Session: ${session.title}`));
1622
+ console.log(chalk4.dim(`Messages: ${messages.length}`));
1623
+ } else {
1624
+ console.log(output);
1625
+ }
1626
+ }
1627
+ function formatAsContext(session, messages) {
1628
+ const lines = [];
1629
+ lines.push("# Previous Session Context");
1630
+ lines.push("");
1631
+ lines.push(`**Session:** ${session.title}`);
1632
+ if (session.projectPath) {
1633
+ lines.push(`**Project:** ${session.projectPath}`);
1634
+ }
1635
+ lines.push(`**Messages:** ${messages.length} total (showing last ${Math.min(messages.length, MAX_MESSAGES_FOR_CONTEXT)})`);
1636
+ lines.push("");
1637
+ lines.push("---");
1638
+ lines.push("");
1639
+ lines.push("## Conversation History");
1640
+ lines.push("");
1641
+ const recentMessages = messages.slice(-MAX_MESSAGES_FOR_CONTEXT);
1642
+ for (const msg of recentMessages) {
1643
+ const roleLabel = msg.role === "user" ? "**User:**" : "**Claude:**";
1644
+ lines.push(roleLabel);
1645
+ lines.push(msg.content);
1646
+ lines.push("");
1647
+ }
1648
+ lines.push("---");
1649
+ lines.push("");
1650
+ lines.push("*Continue this conversation in Claude Code*");
1651
+ return lines.join("\n");
1652
+ }
1653
+ function formatAsMarkdown(session, messages) {
1654
+ const lines = [];
1655
+ lines.push(`# ${session.title}`);
1656
+ lines.push("");
1657
+ if (session.projectPath) {
1658
+ lines.push(`**Project:** ${session.projectPath}`);
1659
+ lines.push("");
1660
+ }
1661
+ if (session.summary) {
1662
+ lines.push("## Summary");
1663
+ lines.push(session.summary);
1664
+ lines.push("");
1665
+ }
1666
+ lines.push("## Conversation");
1667
+ lines.push("");
1668
+ for (const msg of messages) {
1669
+ lines.push(`### ${msg.role === "user" ? "User" : "Claude"}`);
1670
+ lines.push(`*${msg.timestamp}*`);
1671
+ lines.push("");
1672
+ lines.push(msg.content);
1673
+ lines.push("");
1674
+ }
1675
+ return lines.join("\n");
1676
+ }
1677
+ function formatAsJson(session, messages) {
1678
+ return JSON.stringify(
1679
+ {
1680
+ session: {
1681
+ id: session.id,
1682
+ title: session.title,
1683
+ projectPath: session.projectPath,
1684
+ summary: session.summary,
1685
+ createdAt: session.createdAt,
1686
+ updatedAt: session.updatedAt
1687
+ },
1688
+ messages: messages.map((m) => ({
1689
+ role: m.role,
1690
+ content: m.content,
1691
+ timestamp: m.timestamp
1692
+ }))
1693
+ },
1694
+ null,
1695
+ 2
1696
+ );
1697
+ }
1698
+
1699
+ // src/commands/delete.ts
1700
+ import chalk5 from "chalk";
1701
+ async function deleteCommand(id) {
1702
+ let session = getSession(id);
1703
+ if (!session) {
1704
+ const sessions = listSessions();
1705
+ const match = sessions.find((s) => s.id.startsWith(id));
1706
+ if (match) {
1707
+ session = match;
1708
+ }
1709
+ }
1710
+ if (!session) {
1711
+ console.log(chalk5.red(`Session not found: ${id}`));
1712
+ console.log(chalk5.dim("Run: cmem list to see available sessions"));
1713
+ return;
1714
+ }
1715
+ const deleted = deleteSession(session.id);
1716
+ if (deleted) {
1717
+ console.log(chalk5.green(`Deleted session: ${shortId(session.id)} - ${session.title}`));
1718
+ } else {
1719
+ console.log(chalk5.red("Failed to delete session."));
1720
+ }
1721
+ }
1722
+
1723
+ // src/commands/stats.ts
1724
+ import chalk6 from "chalk";
1725
+ import { statSync as statSync2, existsSync as existsSync6 } from "fs";
1726
+ async function statsCommand() {
1727
+ console.log(chalk6.cyan("cmem Storage Statistics\n"));
1728
+ const stats = getStats();
1729
+ console.log(chalk6.white("Database:"));
1730
+ console.log(` Sessions: ${formatNumber(stats.sessionCount)}`);
1731
+ console.log(` Messages: ${formatNumber(stats.messageCount)}`);
1732
+ console.log(` Embeddings: ${formatNumber(stats.embeddingCount)}`);
1733
+ if (existsSync6(DB_PATH)) {
1734
+ const dbStats = statSync2(DB_PATH);
1735
+ console.log(` DB Size: ${formatBytes(dbStats.size)}`);
1736
+ }
1737
+ console.log(` Location: ${CMEM_DIR}`);
1738
+ console.log("");
1739
+ console.log(chalk6.white("Embeddings:"));
1740
+ const modelCached = isModelCached();
1741
+ if (modelCached) {
1742
+ console.log(` Model: ${chalk6.green(EMBEDDING_MODEL)}`);
1743
+ console.log(` Location: ${MODELS_DIR}`);
1744
+ } else {
1745
+ console.log(` Model: ${chalk6.yellow("Not downloaded")}`);
1746
+ console.log(chalk6.dim(" Run cmem setup to download the embedding model"));
1747
+ }
1748
+ console.log("");
1749
+ console.log(chalk6.white("Coverage:"));
1750
+ const coveragePercent = stats.sessionCount > 0 ? Math.round(stats.embeddingCount / stats.sessionCount * 100) : 0;
1751
+ console.log(` Semantic: ${coveragePercent}% of sessions have embeddings`);
1752
+ if (coveragePercent < 100 && stats.sessionCount > 0) {
1753
+ console.log(chalk6.dim(" Run cmem watch to generate missing embeddings"));
1754
+ }
1755
+ }
1756
+
1757
+ // src/commands/watch.ts
1758
+ import chalk7 from "chalk";
1759
+ import chokidar from "chokidar";
1760
+ import { statSync as statSync3, existsSync as existsSync7, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1761
+ import { join as join4, dirname as dirname3, basename as basename5 } from "path";
1762
+ var spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1763
+ var Spinner2 = class {
1764
+ interval = null;
1765
+ frameIndex = 0;
1766
+ message;
1767
+ constructor(message) {
1768
+ this.message = message;
1769
+ }
1770
+ start() {
1771
+ process.stdout.write(` ${spinnerFrames[0]} ${this.message}`);
1772
+ this.interval = setInterval(() => {
1773
+ this.frameIndex = (this.frameIndex + 1) % spinnerFrames.length;
1774
+ process.stdout.write(`\r ${chalk7.cyan(spinnerFrames[this.frameIndex])} ${this.message}`);
1775
+ }, 80);
1776
+ }
1777
+ update(message) {
1778
+ this.message = message;
1779
+ process.stdout.write(`\r ${chalk7.cyan(spinnerFrames[this.frameIndex])} ${this.message} `);
1780
+ }
1781
+ succeed(message) {
1782
+ this.stop();
1783
+ console.log(`\r ${chalk7.green("\u2713")} ${message || this.message} `);
1784
+ }
1785
+ fail(message) {
1786
+ this.stop();
1787
+ console.log(`\r ${chalk7.red("\u2717")} ${message || this.message} `);
1788
+ }
1789
+ stop() {
1790
+ if (this.interval) {
1791
+ clearInterval(this.interval);
1792
+ this.interval = null;
1793
+ }
1794
+ }
1795
+ };
1796
+ var processingDebounce = /* @__PURE__ */ new Map();
1797
+ var DEBOUNCE_MS = 2e3;
1798
+ async function watchCommand(options) {
1799
+ const verbose = options.verbose ?? false;
1800
+ const embedThreshold = options.embedThreshold ?? 500;
1801
+ console.log(chalk7.cyan("\u{1F50D} cmem watch - Monitoring Claude Code sessions\n"));
1802
+ const modelSpinner = new Spinner2("Initializing embedding model...");
1803
+ modelSpinner.start();
1804
+ try {
1805
+ await initializeEmbeddings((progress) => {
1806
+ if (progress.status === "downloading" && progress.progress !== void 0) {
1807
+ const fileName = progress.file ? progress.file.split("/").pop() : "model";
1808
+ modelSpinner.update(`Downloading ${fileName}... ${Math.round(progress.progress)}%`);
1809
+ } else if (progress.status === "loading") {
1810
+ modelSpinner.update("Loading model...");
1811
+ }
1812
+ });
1813
+ modelSpinner.succeed("Embedding model ready");
1814
+ } catch (err) {
1815
+ modelSpinner.fail("Could not initialize embeddings");
1816
+ console.log(chalk7.yellow(" Sessions will be saved but not vectorized.\n"));
1817
+ }
1818
+ const embeddingsReady = isReady();
1819
+ console.log("");
1820
+ const scanSpinner = new Spinner2("Scanning for existing sessions...");
1821
+ scanSpinner.start();
1822
+ const existingFiles = findAllSessionFiles(CLAUDE_DIR);
1823
+ const totalFiles = existingFiles.length;
1824
+ const statsBefore = getStats();
1825
+ const alreadyIndexed = statsBefore.sessionCount;
1826
+ const needsIndexing = totalFiles - alreadyIndexed;
1827
+ if (totalFiles === 0) {
1828
+ scanSpinner.succeed("No Claude Code sessions found yet");
1829
+ } else if (needsIndexing <= 0) {
1830
+ scanSpinner.succeed(`Found ${totalFiles} sessions (all indexed)`);
1831
+ } else {
1832
+ scanSpinner.update(`Found ${totalFiles} sessions, indexing ${needsIndexing} new...`);
1833
+ let processed = 0;
1834
+ let newlyIndexed = 0;
1835
+ for (const filePath of existingFiles) {
1836
+ processed++;
1837
+ const wasNew = await processSessionFile(filePath, embeddingsReady, embedThreshold, false, true);
1838
+ if (wasNew) newlyIndexed++;
1839
+ const percent = Math.round(processed / totalFiles * 100);
1840
+ scanSpinner.update(`Indexing sessions... ${percent}% (${processed}/${totalFiles})`);
1841
+ }
1842
+ scanSpinner.succeed(`Indexed ${totalFiles} sessions (${newlyIndexed} new, ${totalFiles - newlyIndexed} updated)`);
1843
+ }
1844
+ console.log("");
1845
+ console.log(chalk7.dim(`Watching: ${CLAUDE_DIR}`));
1846
+ console.log(chalk7.dim(`Embed threshold: ${embedThreshold} chars
1847
+ `));
1848
+ const watcher = chokidar.watch(CLAUDE_DIR, {
1849
+ persistent: true,
1850
+ ignoreInitial: true,
1851
+ // We already processed existing files above
1852
+ depth: 10,
1853
+ awaitWriteFinish: {
1854
+ stabilityThreshold: 1e3,
1855
+ pollInterval: 100
1856
+ }
1857
+ });
1858
+ watcher.on("add", (filePath) => {
1859
+ if (!filePath.endsWith(".jsonl")) return;
1860
+ if (filePath.includes("/subagents/")) {
1861
+ if (verbose) console.log(chalk7.dim(`[skip agent] ${filePath}`));
1862
+ return;
1863
+ }
1864
+ if (verbose) console.log(chalk7.dim(`[new] ${filePath}`));
1865
+ debouncedProcess(filePath, embeddingsReady, embedThreshold, verbose);
1866
+ });
1867
+ watcher.on("change", (filePath) => {
1868
+ if (!filePath.endsWith(".jsonl")) return;
1869
+ if (filePath.includes("/subagents/")) {
1870
+ if (verbose) console.log(chalk7.dim(`[skip agent] ${filePath}`));
1871
+ return;
1872
+ }
1873
+ if (verbose) console.log(chalk7.dim(`[changed] ${filePath}`));
1874
+ debouncedProcess(filePath, embeddingsReady, embedThreshold, verbose);
1875
+ });
1876
+ watcher.on("error", (error) => {
1877
+ console.error(chalk7.red("Watcher error:"), error);
1878
+ });
1879
+ watcher.on("ready", () => {
1880
+ console.log(chalk7.green("\u2713 Watching for session changes..."));
1881
+ console.log(chalk7.dim("Press Ctrl+C to stop\n"));
1882
+ });
1883
+ process.on("SIGINT", () => {
1884
+ console.log(chalk7.dim("\nShutting down watcher..."));
1885
+ watcher.close();
1886
+ process.exit(0);
1887
+ });
1888
+ process.on("SIGTERM", () => {
1889
+ watcher.close();
1890
+ process.exit(0);
1891
+ });
1892
+ }
1893
+ function findAllSessionFiles(dir) {
1894
+ const files = [];
1895
+ if (!existsSync7(dir)) return files;
1896
+ function scanDir(currentDir, depth = 0) {
1897
+ if (depth > 10) return;
1898
+ try {
1899
+ const entries = readdirSync2(currentDir, { withFileTypes: true });
1900
+ for (const entry of entries) {
1901
+ const fullPath = join4(currentDir, entry.name);
1902
+ if (entry.isDirectory()) {
1903
+ if (entry.name === "subagents") continue;
1904
+ scanDir(fullPath, depth + 1);
1905
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
1906
+ files.push(fullPath);
1907
+ }
1908
+ }
1909
+ } catch {
1910
+ }
1911
+ }
1912
+ scanDir(dir);
1913
+ return files;
1914
+ }
1915
+ function debouncedProcess(filePath, embeddingsReady, embedThreshold, verbose) {
1916
+ const existing = processingDebounce.get(filePath);
1917
+ if (existing) {
1918
+ clearTimeout(existing);
1919
+ }
1920
+ const timer = setTimeout(async () => {
1921
+ processingDebounce.delete(filePath);
1922
+ await processSessionFile(filePath, embeddingsReady, embedThreshold, verbose);
1923
+ }, DEBOUNCE_MS);
1924
+ processingDebounce.set(filePath, timer);
1925
+ }
1926
+ async function processSessionFile(filePath, embeddingsReady, embedThreshold, verbose, forceMetadataUpdate = false) {
1927
+ try {
1928
+ const messages = parseSessionFile(filePath);
1929
+ if (messages.length === 0) {
1930
+ if (verbose) console.log(chalk7.dim(` Skipping empty session: ${filePath}`));
1931
+ return false;
1932
+ }
1933
+ const stats = statSync3(filePath);
1934
+ const fileMtime = stats.mtime.toISOString();
1935
+ const existingSession = getSessionBySourceFile(filePath);
1936
+ const sessionId_from_file = basename5(filePath, ".jsonl");
1937
+ const agentMessages = loadSubagentMessages2(dirname3(filePath), sessionId_from_file);
1938
+ const allMessages = [...messages, ...agentMessages];
1939
+ const contentLength = allMessages.reduce((sum, m) => sum + m.content.length, 0);
1940
+ const firstUserMsg = messages.find((m) => m.role === "user");
1941
+ const title = firstUserMsg ? generateTitle(firstUserMsg.content) : "Untitled Session";
1942
+ const summary = generateSummary(messages);
1943
+ const rawData = JSON.stringify({ filePath, messages, mtime: fileMtime });
1944
+ const projectPath = getProjectPathFromIndex(filePath, sessionId_from_file);
1945
+ const metadata = extractSessionMetadata(filePath);
1946
+ const isAutomated = metadata.isSidechain || metadata.isMeta;
1947
+ let sessionId;
1948
+ let isNew = false;
1949
+ if (existingSession) {
1950
+ sessionId = existingSession.id;
1951
+ const needsMetadataUpdate = forceMetadataUpdate || !existingSession.isSidechain && !existingSession.isAutomated && isAutomated;
1952
+ if (existingSession.messageCount !== messages.length || needsMetadataUpdate) {
1953
+ updateSession(sessionId, {
1954
+ title,
1955
+ summary,
1956
+ rawData,
1957
+ messages,
1958
+ isSidechain: metadata.isSidechain,
1959
+ isAutomated
1960
+ });
1961
+ if (verbose) {
1962
+ const automatedTag = isAutomated ? chalk7.dim(" [auto]") : "";
1963
+ console.log(chalk7.blue(`\u21BB Updated: ${title} (${messages.length} msgs)${automatedTag}`));
1964
+ }
1965
+ }
1966
+ } else {
1967
+ isNew = true;
1968
+ sessionId = createSession({
1969
+ title,
1970
+ summary,
1971
+ projectPath,
1972
+ sourceFile: filePath,
1973
+ rawData,
1974
+ messages,
1975
+ isSidechain: metadata.isSidechain,
1976
+ isAutomated
1977
+ });
1978
+ if (verbose) {
1979
+ const automatedTag = isAutomated ? chalk7.dim(" [auto]") : "";
1980
+ console.log(chalk7.green(`\u2713 Saved: ${title} (${messages.length} msgs)${automatedTag}`));
1981
+ }
1982
+ }
1983
+ if (embeddingsReady) {
1984
+ const shouldEmbed = needsReembedding(sessionId, contentLength, embedThreshold);
1985
+ if (shouldEmbed) {
1986
+ try {
1987
+ const embeddingText = createEmbeddingText(title, summary, allMessages);
1988
+ const embedding = await getEmbedding(embeddingText);
1989
+ storeEmbedding(sessionId, embedding);
1990
+ updateEmbeddingState(sessionId, contentLength, fileMtime);
1991
+ if (verbose) {
1992
+ const agentCount = agentMessages.length;
1993
+ const agentInfo = agentCount > 0 ? ` (+${agentCount} agent msgs)` : "";
1994
+ console.log(chalk7.dim(` Embedded: ${title}${agentInfo}`));
1995
+ }
1996
+ } catch (err) {
1997
+ if (verbose) {
1998
+ console.log(chalk7.yellow(` Failed to embed: ${title}`));
1999
+ console.log(chalk7.dim(` ${err}`));
2000
+ }
2001
+ }
2002
+ }
2003
+ }
2004
+ return isNew;
2005
+ } catch (err) {
2006
+ if (verbose) console.log(chalk7.red(`Error processing ${filePath}:`), err);
2007
+ return false;
2008
+ }
2009
+ }
2010
+ function loadSubagentMessages2(projectDirPath, parentSessionId) {
2011
+ const subagentsDir = join4(projectDirPath, parentSessionId, "subagents");
2012
+ if (!existsSync7(subagentsDir)) return [];
2013
+ const messages = [];
2014
+ try {
2015
+ const agentFiles = readdirSync2(subagentsDir).filter((f) => f.endsWith(".jsonl"));
2016
+ for (const agentFile of agentFiles) {
2017
+ const agentPath = join4(subagentsDir, agentFile);
2018
+ const agentMessages = parseSessionFile(agentPath);
2019
+ messages.push(...agentMessages);
2020
+ }
2021
+ } catch {
2022
+ }
2023
+ return messages;
2024
+ }
2025
+ function getProjectPathFromIndex(filePath, sessionId) {
2026
+ const projectDir = dirname3(filePath);
2027
+ const indexPath = join4(projectDir, "sessions-index.json");
2028
+ if (!existsSync7(indexPath)) return null;
2029
+ try {
2030
+ const content = readFileSync2(indexPath, "utf-8");
2031
+ const index = JSON.parse(content);
2032
+ const entry = index.entries.find((e) => e.sessionId === sessionId);
2033
+ return entry?.projectPath || null;
2034
+ } catch {
2035
+ return null;
2036
+ }
2037
+ }
2038
+
2039
+ // src/commands/mcp.ts
2040
+ import chalk8 from "chalk";
2041
+
2042
+ // src/mcp/server.ts
2043
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2044
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2045
+ import {
2046
+ CallToolRequestSchema,
2047
+ ListToolsRequestSchema
2048
+ } from "@modelcontextprotocol/sdk/types.js";
2049
+
2050
+ // src/utils/claude-cli.ts
2051
+ import { spawn } from "child_process";
2052
+ import { execSync } from "child_process";
2053
+ function getClaudePath() {
2054
+ try {
2055
+ const result = execSync("which claude", { encoding: "utf-8" }).trim();
2056
+ return result || null;
2057
+ } catch {
2058
+ return null;
2059
+ }
2060
+ }
2061
+ function parseStreamJsonLine(line) {
2062
+ if (!line.trim()) return null;
2063
+ try {
2064
+ const data = JSON.parse(line);
2065
+ if (data.type === "assistant") {
2066
+ if (data.message?.content && Array.isArray(data.message.content)) {
2067
+ const textBlocks = [];
2068
+ for (const block of data.message.content) {
2069
+ if (block.type === "text" && block.text) {
2070
+ textBlocks.push(block.text);
2071
+ }
2072
+ }
2073
+ if (textBlocks.length > 0) {
2074
+ return { type: "text", content: textBlocks.join("\n") };
2075
+ }
2076
+ }
2077
+ } else if (data.type === "content_block_delta") {
2078
+ if (data.delta?.type === "text_delta" && data.delta.text) {
2079
+ return { type: "text", content: data.delta.text };
2080
+ }
2081
+ } else if (data.type === "result") {
2082
+ if (data.is_error) {
2083
+ return { type: "error", content: data.result || "Unknown error" };
2084
+ }
2085
+ const usage = data.usage || {};
2086
+ const inputTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
2087
+ const outputTokens = usage.output_tokens || 0;
2088
+ return {
2089
+ type: "done",
2090
+ inputTokens,
2091
+ outputTokens,
2092
+ costUsd: data.total_cost_usd,
2093
+ durationMs: data.duration_ms
2094
+ };
2095
+ } else if (data.type === "error") {
2096
+ return {
2097
+ type: "error",
2098
+ content: data.error?.message || JSON.stringify(data.error) || "Unknown error"
2099
+ };
2100
+ }
2101
+ return { type: "skip" };
2102
+ } catch {
2103
+ return null;
2104
+ }
2105
+ }
2106
+ async function runClaudePrompt(prompt, options = {}) {
2107
+ const claudePath = getClaudePath();
2108
+ if (!claudePath) {
2109
+ return {
2110
+ success: false,
2111
+ content: "",
2112
+ error: "Claude CLI not found. Please install Claude Code CLI."
2113
+ };
2114
+ }
2115
+ const { model = "haiku" } = options;
2116
+ const args = [
2117
+ "-p",
2118
+ prompt,
2119
+ "--output-format",
2120
+ "stream-json",
2121
+ "--verbose",
2122
+ // Required when using stream-json with -p
2123
+ "--model",
2124
+ model,
2125
+ "--permission-mode",
2126
+ "plan"
2127
+ // Read-only, no tools needed for summarization
2128
+ ];
2129
+ return new Promise((resolve) => {
2130
+ const childProcess = spawn(claudePath, args, {
2131
+ env: {
2132
+ ...process.env,
2133
+ CI: "true"
2134
+ // Prevent interactive prompts
2135
+ },
2136
+ stdio: ["pipe", "pipe", "pipe"]
2137
+ });
2138
+ childProcess.stdin?.end();
2139
+ let buffer = "";
2140
+ const textChunks = [];
2141
+ let finalResult = null;
2142
+ let errorContent = "";
2143
+ childProcess.stdout?.on("data", (data) => {
2144
+ buffer += data.toString();
2145
+ const lines = buffer.split("\n");
2146
+ buffer = lines.pop() || "";
2147
+ for (const line of lines) {
2148
+ if (!line.trim()) continue;
2149
+ const chunk = parseStreamJsonLine(line);
2150
+ if (chunk) {
2151
+ if (chunk.type === "text" && chunk.content) {
2152
+ textChunks.push(chunk.content);
2153
+ } else if (chunk.type === "done") {
2154
+ finalResult = chunk;
2155
+ } else if (chunk.type === "error" && chunk.content) {
2156
+ errorContent = chunk.content;
2157
+ }
2158
+ }
2159
+ }
2160
+ });
2161
+ childProcess.stderr?.on("data", (data) => {
2162
+ const text = data.toString().toLowerCase();
2163
+ if (text.includes("error") || text.includes("failed")) {
2164
+ errorContent = data.toString().trim();
2165
+ }
2166
+ });
2167
+ childProcess.on("close", (code) => {
2168
+ if (buffer.trim()) {
2169
+ const chunk = parseStreamJsonLine(buffer);
2170
+ if (chunk) {
2171
+ if (chunk.type === "text" && chunk.content) {
2172
+ textChunks.push(chunk.content);
2173
+ } else if (chunk.type === "done") {
2174
+ finalResult = chunk;
2175
+ } else if (chunk.type === "error" && chunk.content) {
2176
+ errorContent = chunk.content;
2177
+ }
2178
+ }
2179
+ }
2180
+ const content = textChunks.join("");
2181
+ if (errorContent && !content) {
2182
+ resolve({
2183
+ success: false,
2184
+ content: "",
2185
+ error: errorContent
2186
+ });
2187
+ } else {
2188
+ resolve({
2189
+ success: code === 0 && content.length > 0,
2190
+ content,
2191
+ error: errorContent || void 0,
2192
+ inputTokens: finalResult?.inputTokens,
2193
+ outputTokens: finalResult?.outputTokens,
2194
+ costUsd: finalResult?.costUsd,
2195
+ durationMs: finalResult?.durationMs
2196
+ });
2197
+ }
2198
+ });
2199
+ childProcess.on("error", (err) => {
2200
+ resolve({
2201
+ success: false,
2202
+ content: "",
2203
+ error: err.message
2204
+ });
2205
+ });
2206
+ setTimeout(() => {
2207
+ childProcess.kill("SIGTERM");
2208
+ resolve({
2209
+ success: false,
2210
+ content: textChunks.join(""),
2211
+ error: "Request timed out after 60 seconds"
2212
+ });
2213
+ }, 6e4);
2214
+ });
2215
+ }
2216
+ function isClaudeCliAvailable() {
2217
+ return getClaudePath() !== null;
2218
+ }
2219
+
2220
+ // src/mcp/server.ts
2221
+ function createMcpServer() {
2222
+ const server = new Server(
2223
+ {
2224
+ name: "cmem",
2225
+ version: "0.1.0"
2226
+ },
2227
+ {
2228
+ capabilities: {
2229
+ tools: {}
2230
+ }
2231
+ }
2232
+ );
2233
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2234
+ return {
2235
+ tools: [
2236
+ {
2237
+ name: "search_sessions",
2238
+ description: "Semantic search across all saved Claude Code conversation sessions. Use this to find past conversations about specific topics, projects, or problems. Returns matching sessions ranked by relevance.",
2239
+ inputSchema: {
2240
+ type: "object",
2241
+ properties: {
2242
+ query: {
2243
+ type: "string",
2244
+ description: 'Natural language search query. Examples: "React hooks discussion", "database migration", "authentication implementation"'
2245
+ },
2246
+ limit: {
2247
+ type: "number",
2248
+ description: "Maximum number of results to return (default: 5)"
2249
+ }
2250
+ },
2251
+ required: ["query"]
2252
+ }
2253
+ },
2254
+ {
2255
+ name: "list_sessions",
2256
+ description: "List all saved Claude Code conversation sessions, ordered by most recently updated. Use this to browse available sessions or find recent conversations.",
2257
+ inputSchema: {
2258
+ type: "object",
2259
+ properties: {
2260
+ limit: {
2261
+ type: "number",
2262
+ description: "Maximum number of sessions to return (default: 10)"
2263
+ }
2264
+ }
2265
+ }
2266
+ },
2267
+ {
2268
+ name: "get_session",
2269
+ description: "Get detailed information about a specific conversation session, including its full message history. Use this after finding a session via search or list.",
2270
+ inputSchema: {
2271
+ type: "object",
2272
+ properties: {
2273
+ sessionId: {
2274
+ type: "string",
2275
+ description: "The session ID to retrieve"
2276
+ },
2277
+ includeMessages: {
2278
+ type: "boolean",
2279
+ description: "Whether to include the full message history (default: true)"
2280
+ },
2281
+ messageLimit: {
2282
+ type: "number",
2283
+ description: "Maximum number of messages to include (default: 50, from most recent)"
2284
+ }
2285
+ },
2286
+ required: ["sessionId"]
2287
+ }
2288
+ },
2289
+ {
2290
+ name: "get_session_context",
2291
+ description: "Get a formatted context summary from a past session that can be used to continue or reference that conversation. Returns key information and recent messages in a readable format.",
2292
+ inputSchema: {
2293
+ type: "object",
2294
+ properties: {
2295
+ sessionId: {
2296
+ type: "string",
2297
+ description: "The session ID to get context from"
2298
+ },
2299
+ messageCount: {
2300
+ type: "number",
2301
+ description: "Number of recent messages to include (default: 10)"
2302
+ }
2303
+ },
2304
+ required: ["sessionId"]
2305
+ }
2306
+ },
2307
+ {
2308
+ name: "search_and_summarize",
2309
+ description: "Search past Claude Code sessions and get an AI-generated summary tailored to your specific question. This spawns a separate Claude instance to read and synthesize the relevant sessions, keeping your main context clean. Use this when you need insights from past conversations.",
2310
+ inputSchema: {
2311
+ type: "object",
2312
+ properties: {
2313
+ query: {
2314
+ type: "string",
2315
+ description: 'Your question or topic to search for. Examples: "What did we decide about the database schema?", "How did we implement authentication?", "What was the bug fix for the login issue?"'
2316
+ },
2317
+ sessionLimit: {
2318
+ type: "number",
2319
+ description: "Maximum number of sessions to analyze (default: 3)"
2320
+ },
2321
+ model: {
2322
+ type: "string",
2323
+ enum: ["haiku", "sonnet", "opus"],
2324
+ description: "Model to use for summarization (default: haiku for speed)"
2325
+ }
2326
+ },
2327
+ required: ["query"]
2328
+ }
2329
+ }
2330
+ ]
2331
+ };
2332
+ });
2333
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2334
+ const { name, arguments: args } = request.params;
2335
+ try {
2336
+ switch (name) {
2337
+ case "search_sessions":
2338
+ return await handleSearchSessions(args);
2339
+ case "list_sessions":
2340
+ return await handleListSessions(args);
2341
+ case "get_session":
2342
+ return await handleGetSession(
2343
+ args
2344
+ );
2345
+ case "get_session_context":
2346
+ return await handleGetSessionContext(
2347
+ args
2348
+ );
2349
+ case "search_and_summarize":
2350
+ return await handleSearchAndSummarize(
2351
+ args
2352
+ );
2353
+ default:
2354
+ return {
2355
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
2356
+ isError: true
2357
+ };
2358
+ }
2359
+ } catch (error) {
2360
+ return {
2361
+ content: [{ type: "text", text: `Error: ${String(error)}` }],
2362
+ isError: true
2363
+ };
2364
+ }
2365
+ });
2366
+ return server;
2367
+ }
2368
+ async function handleSearchSessions(args) {
2369
+ const { query, limit = 5 } = args;
2370
+ if (!isReady()) {
2371
+ try {
2372
+ await initializeEmbeddings();
2373
+ } catch {
2374
+ const allSessions = listSessions(100);
2375
+ const queryLower = query.toLowerCase();
2376
+ const filtered = allSessions.filter(
2377
+ (s) => s.title.toLowerCase().includes(queryLower) || s.summary && s.summary.toLowerCase().includes(queryLower)
2378
+ ).slice(0, limit);
2379
+ return {
2380
+ content: [
2381
+ {
2382
+ type: "text",
2383
+ text: formatSessionList(filtered, `Text search results for "${query}" (embeddings unavailable)`)
2384
+ }
2385
+ ]
2386
+ };
2387
+ }
2388
+ }
2389
+ const queryEmbedding = await getEmbedding(query);
2390
+ const results = searchSessions(queryEmbedding, limit);
2391
+ return {
2392
+ content: [
2393
+ {
2394
+ type: "text",
2395
+ text: formatSessionList(results, `Semantic search results for "${query}"`)
2396
+ }
2397
+ ]
2398
+ };
2399
+ }
2400
+ async function handleListSessions(args) {
2401
+ const { limit = 10 } = args;
2402
+ const sessions = listSessions(limit);
2403
+ return {
2404
+ content: [
2405
+ {
2406
+ type: "text",
2407
+ text: formatSessionList(sessions, "Recent sessions")
2408
+ }
2409
+ ]
2410
+ };
2411
+ }
2412
+ async function handleGetSession(args) {
2413
+ const { sessionId, includeMessages = true, messageLimit = 50 } = args;
2414
+ let session = getSession(sessionId) || getSessionByIdPrefix(sessionId);
2415
+ if (!session) {
2416
+ return {
2417
+ content: [{ type: "text", text: `Session not found: ${sessionId}` }],
2418
+ isError: true
2419
+ };
2420
+ }
2421
+ const lines = [
2422
+ `# Session: ${session.title}`,
2423
+ "",
2424
+ `**ID:** ${session.id}`,
2425
+ `**Created:** ${session.createdAt}`,
2426
+ `**Updated:** ${session.updatedAt} (${formatTimeAgo(session.updatedAt)})`,
2427
+ `**Messages:** ${session.messageCount}`
2428
+ ];
2429
+ if (session.projectPath) {
2430
+ lines.push(`**Project:** ${session.projectPath}`);
2431
+ }
2432
+ if (session.summary) {
2433
+ lines.push("", "## Summary", session.summary);
2434
+ }
2435
+ if (includeMessages) {
2436
+ const messages = getSessionMessages(session.id);
2437
+ const recentMessages = messages.slice(-messageLimit);
2438
+ lines.push("", "## Messages", "");
2439
+ for (const msg of recentMessages) {
2440
+ lines.push(`### ${msg.role === "user" ? "User" : "Assistant"}`);
2441
+ lines.push(msg.content);
2442
+ lines.push("");
2443
+ }
2444
+ if (messages.length > messageLimit) {
2445
+ lines.push(`_...${messages.length - messageLimit} earlier messages omitted_`);
2446
+ }
2447
+ }
2448
+ return {
2449
+ content: [{ type: "text", text: lines.join("\n") }]
2450
+ };
2451
+ }
2452
+ async function handleGetSessionContext(args) {
2453
+ const { sessionId, messageCount = 10 } = args;
2454
+ let session = getSession(sessionId) || getSessionByIdPrefix(sessionId);
2455
+ if (!session) {
2456
+ return {
2457
+ content: [{ type: "text", text: `Session not found: ${sessionId}` }],
2458
+ isError: true
2459
+ };
2460
+ }
2461
+ const messages = getSessionMessages(session.id);
2462
+ const recentMessages = messages.slice(-messageCount);
2463
+ const lines = [
2464
+ "# Context from Previous Session",
2465
+ "",
2466
+ `**Topic:** ${session.title}`
2467
+ ];
2468
+ if (session.projectPath) {
2469
+ lines.push(`**Project:** ${session.projectPath}`);
2470
+ }
2471
+ if (session.summary) {
2472
+ lines.push("", "## Summary", session.summary);
2473
+ }
2474
+ lines.push(
2475
+ "",
2476
+ "## Recent Conversation",
2477
+ `_Last ${recentMessages.length} of ${messages.length} messages_`,
2478
+ ""
2479
+ );
2480
+ for (const msg of recentMessages) {
2481
+ const role = msg.role === "user" ? "**User:**" : "**Assistant:**";
2482
+ lines.push(role);
2483
+ lines.push(truncate(msg.content, 2e3));
2484
+ lines.push("");
2485
+ }
2486
+ return {
2487
+ content: [{ type: "text", text: lines.join("\n") }]
2488
+ };
2489
+ }
2490
+ async function handleSearchAndSummarize(args) {
2491
+ const { query, sessionLimit = 3, model = "haiku" } = args;
2492
+ if (!isClaudeCliAvailable()) {
2493
+ return {
2494
+ content: [
2495
+ {
2496
+ type: "text",
2497
+ text: "Claude CLI not found. This tool requires Claude Code CLI to be installed."
2498
+ }
2499
+ ],
2500
+ isError: true
2501
+ };
2502
+ }
2503
+ if (!isReady()) {
2504
+ try {
2505
+ await initializeEmbeddings();
2506
+ } catch {
2507
+ return {
2508
+ content: [
2509
+ {
2510
+ type: "text",
2511
+ text: "Embedding model not available. Please run `cmem setup` first."
2512
+ }
2513
+ ],
2514
+ isError: true
2515
+ };
2516
+ }
2517
+ }
2518
+ const queryEmbedding = await getEmbedding(query);
2519
+ const sessions = searchSessions(queryEmbedding, sessionLimit);
2520
+ if (sessions.length === 0) {
2521
+ return {
2522
+ content: [
2523
+ {
2524
+ type: "text",
2525
+ text: `No relevant sessions found for: "${query}"`
2526
+ }
2527
+ ]
2528
+ };
2529
+ }
2530
+ const sessionContents = [];
2531
+ for (const session of sessions) {
2532
+ const messages = getSessionMessages(session.id);
2533
+ if (messages.length === 0) continue;
2534
+ const sessionText = [
2535
+ `## Session: ${session.title}`,
2536
+ `Project: ${session.projectPath || "Unknown"}`,
2537
+ `Date: ${session.updatedAt}`,
2538
+ "",
2539
+ ...messages.slice(-20).map((m) => `**${m.role}:** ${truncate(m.content, 1500)}`)
2540
+ ].join("\n");
2541
+ sessionContents.push(sessionText);
2542
+ }
2543
+ if (sessionContents.length === 0) {
2544
+ return {
2545
+ content: [
2546
+ {
2547
+ type: "text",
2548
+ text: `Found ${sessions.length} sessions but they appear to be empty.`
2549
+ }
2550
+ ]
2551
+ };
2552
+ }
2553
+ const prompt = `You are analyzing past Claude Code conversation sessions to answer a user's question.
2554
+
2555
+ <user_question>
2556
+ ${query}
2557
+ </user_question>
2558
+
2559
+ <past_sessions>
2560
+ ${sessionContents.join("\n\n---\n\n")}
2561
+ </past_sessions>
2562
+
2563
+ Based on the past sessions above, provide a concise and helpful answer to the user's question. Focus on:
2564
+ 1. Directly answering their question with specific details from the conversations
2565
+ 2. Mentioning which session(s) the information came from
2566
+ 3. Highlighting any relevant decisions, code snippets, or conclusions
2567
+
2568
+ If the sessions don't contain relevant information to answer the question, say so clearly.
2569
+
2570
+ Keep your response concise but complete.`;
2571
+ const response = await runClaudePrompt(prompt, { model });
2572
+ if (!response.success) {
2573
+ return {
2574
+ content: [
2575
+ {
2576
+ type: "text",
2577
+ text: `Error generating summary: ${response.error || "Unknown error"}`
2578
+ }
2579
+ ],
2580
+ isError: true
2581
+ };
2582
+ }
2583
+ const resultLines = [
2584
+ response.content,
2585
+ "",
2586
+ "---",
2587
+ `*Analyzed ${sessions.length} session(s) | Model: ${model}${response.durationMs ? ` | ${(response.durationMs / 1e3).toFixed(1)}s` : ""}${response.outputTokens ? ` | ${response.outputTokens} tokens` : ""}*`
2588
+ ];
2589
+ return {
2590
+ content: [{ type: "text", text: resultLines.join("\n") }]
2591
+ };
2592
+ }
2593
+ function formatSessionList(sessions, header) {
2594
+ if (sessions.length === 0) {
2595
+ return `${header}
2596
+
2597
+ No sessions found.`;
2598
+ }
2599
+ const lines = [header, ""];
2600
+ for (const session of sessions) {
2601
+ lines.push(`### ${session.title}`);
2602
+ lines.push(`- **ID:** \`${session.id.slice(0, 8)}\` (use this to get full session)`);
2603
+ lines.push(`- **Messages:** ${session.messageCount}`);
2604
+ lines.push(`- **Updated:** ${formatTimeAgo(session.updatedAt)}`);
2605
+ if (session.projectPath) {
2606
+ lines.push(`- **Project:** ${session.projectPath}`);
2607
+ }
2608
+ if (session.summary) {
2609
+ lines.push(`- **Summary:** ${truncate(session.summary, 150)}`);
2610
+ }
2611
+ lines.push("");
2612
+ }
2613
+ return lines.join("\n");
2614
+ }
2615
+ async function startMcpServer() {
2616
+ const server = createMcpServer();
2617
+ const transport = new StdioServerTransport();
2618
+ await server.connect(transport);
2619
+ }
2620
+
2621
+ // src/commands/mcp.ts
2622
+ async function mcpCommand() {
2623
+ console.error(chalk8.cyan("Starting cmem MCP server..."));
2624
+ try {
2625
+ await startMcpServer();
2626
+ } catch (err) {
2627
+ console.error(chalk8.red("MCP server error:"), err);
2628
+ process.exit(1);
2629
+ }
2630
+ }
2631
+
2632
+ // src/cli.ts
2633
+ init_install();
2634
+
2635
+ // src/commands/setup.ts
2636
+ import chalk10 from "chalk";
2637
+ import { execSync as execSync3 } from "child_process";
2638
+ import { existsSync as existsSync9, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
2639
+ import { homedir as homedir3 } from "os";
2640
+ import { join as join6, dirname as dirname5, basename as basename6 } from "path";
2641
+ import { fileURLToPath } from "url";
2642
+ import { createInterface } from "readline";
2643
+ var __filename = fileURLToPath(import.meta.url);
2644
+ var __dirname = dirname5(__filename);
2645
+ var CLAUDE_JSON_PATH = join6(homedir3(), ".claude.json");
2646
+ var CLAUDE_SETTINGS_PATH = join6(homedir3(), ".claude", "settings.json");
2647
+ var CMEM_DIR2 = join6(homedir3(), ".cmem");
2648
+ var SETUP_MARKER = join6(CMEM_DIR2, ".setup-complete");
2649
+ var CMEM_PERMISSIONS = [
2650
+ "mcp__cmem__search_sessions",
2651
+ "mcp__cmem__list_sessions",
2652
+ "mcp__cmem__get_session",
2653
+ "mcp__cmem__get_session_context",
2654
+ "mcp__cmem__search_and_summarize"
2655
+ ];
2656
+ var spinnerFrames2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2657
+ var Spinner3 = class {
2658
+ interval = null;
2659
+ frameIndex = 0;
2660
+ message;
2661
+ constructor(message) {
2662
+ this.message = message;
2663
+ }
2664
+ start() {
2665
+ process.stdout.write(` ${spinnerFrames2[0]} ${this.message}`);
2666
+ this.interval = setInterval(() => {
2667
+ this.frameIndex = (this.frameIndex + 1) % spinnerFrames2.length;
2668
+ process.stdout.write(`\r ${chalk10.cyan(spinnerFrames2[this.frameIndex])} ${this.message}`);
2669
+ }, 80);
2670
+ }
2671
+ update(message) {
2672
+ this.message = message;
2673
+ process.stdout.write(`\r ${chalk10.cyan(spinnerFrames2[this.frameIndex])} ${this.message} `);
2674
+ }
2675
+ succeed(message) {
2676
+ this.stop();
2677
+ console.log(`\r ${chalk10.green("\u2713")} ${message || this.message} `);
2678
+ }
2679
+ fail(message) {
2680
+ this.stop();
2681
+ console.log(`\r ${chalk10.red("\u2717")} ${message || this.message} `);
2682
+ }
2683
+ warn(message) {
2684
+ this.stop();
2685
+ console.log(`\r ${chalk10.yellow("!")} ${message || this.message} `);
2686
+ }
2687
+ stop() {
2688
+ if (this.interval) {
2689
+ clearInterval(this.interval);
2690
+ this.interval = null;
2691
+ }
2692
+ }
2693
+ };
2694
+ function printBanner(version) {
2695
+ const magenta = chalk10.magenta;
2696
+ const cyan = chalk10.cyan;
2697
+ const dim = chalk10.dim;
2698
+ console.log("");
2699
+ console.log(magenta(" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557"));
2700
+ console.log(magenta(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551"));
2701
+ console.log(magenta(" \u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551"));
2702
+ console.log(magenta(" \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551"));
2703
+ console.log(magenta(" \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551"));
2704
+ console.log(magenta(" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"));
2705
+ console.log("");
2706
+ console.log(cyan(` cmem v${version}`));
2707
+ console.log(dim(" Persistent memory & semantic search for Claude Code"));
2708
+ console.log(dim(" Created by Colby McHenry"));
2709
+ console.log("");
2710
+ }
2711
+ async function promptChoice(question, options, defaultChoice = 1) {
2712
+ const rl = createInterface({
2713
+ input: process.stdin,
2714
+ output: process.stdout
2715
+ });
2716
+ options.forEach((opt, i) => {
2717
+ console.log(chalk10.dim(` ${i + 1}) ${opt}`));
2718
+ });
2719
+ console.log("");
2720
+ return new Promise((resolve) => {
2721
+ rl.question(chalk10.white(` Choice [${defaultChoice}]: `), (answer) => {
2722
+ rl.close();
2723
+ const num = parseInt(answer.trim(), 10);
2724
+ if (isNaN(num) || num < 1 || num > options.length) {
2725
+ resolve(defaultChoice);
2726
+ } else {
2727
+ resolve(num);
2728
+ }
2729
+ });
2730
+ });
2731
+ }
2732
+ async function promptYesNo(question, defaultYes = true) {
2733
+ const rl = createInterface({
2734
+ input: process.stdin,
2735
+ output: process.stdout
2736
+ });
2737
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
2738
+ return new Promise((resolve) => {
2739
+ rl.question(chalk10.white(` ${question} ${chalk10.dim(hint)} `), (answer) => {
2740
+ rl.close();
2741
+ const normalized = answer.toLowerCase().trim();
2742
+ if (normalized === "") {
2743
+ resolve(defaultYes);
2744
+ } else {
2745
+ resolve(normalized === "y" || normalized === "yes");
2746
+ }
2747
+ });
2748
+ });
2749
+ }
2750
+ function isRunningViaNpx() {
2751
+ const execPath = process.argv[1] || "";
2752
+ return execPath.includes("_npx") || execPath.includes(".npm/_cacache");
2753
+ }
2754
+ function isGloballyInstalled() {
2755
+ try {
2756
+ const result = execSync3("which cmem 2>/dev/null || where cmem 2>/dev/null", {
2757
+ encoding: "utf-8",
2758
+ stdio: ["pipe", "pipe", "pipe"]
2759
+ }).trim();
2760
+ return result.length > 0 && !result.includes("_npx");
2761
+ } catch {
2762
+ return false;
2763
+ }
2764
+ }
2765
+ function getCmemVersion() {
2766
+ try {
2767
+ const packagePath = join6(__dirname, "..", "package.json");
2768
+ if (existsSync9(packagePath)) {
2769
+ const pkg = JSON.parse(readFileSync3(packagePath, "utf-8"));
2770
+ return pkg.version || "0.1.0";
2771
+ }
2772
+ } catch {
2773
+ }
2774
+ return "0.1.0";
2775
+ }
2776
+ function getInstalledVersion() {
2777
+ try {
2778
+ const result = execSync3("cmem --version 2>/dev/null", {
2779
+ encoding: "utf-8"
2780
+ }).trim();
2781
+ return result;
2782
+ } catch {
2783
+ return null;
2784
+ }
2785
+ }
2786
+ async function setupCommand() {
2787
+ const currentVersion = getCmemVersion();
2788
+ const installedVersion = getInstalledVersion();
2789
+ const isGlobal = isGloballyInstalled();
2790
+ const isNpx = isRunningViaNpx();
2791
+ printBanner(currentVersion);
2792
+ if (!isGlobal || isNpx) {
2793
+ console.log(chalk10.yellow(" Install cmem globally?"));
2794
+ console.log(chalk10.dim(" Makes the `cmem` command available everywhere\n"));
2795
+ const choice = await promptChoice("", [
2796
+ "Yes - install globally via npm",
2797
+ "No - I'll use npx each time"
2798
+ ], 1);
2799
+ if (choice === 1) {
2800
+ console.log(chalk10.dim("\n Installing @colbymchenry/cmem globally...\n"));
2801
+ try {
2802
+ execSync3("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
2803
+ console.log(chalk10.green("\n \u2713 Installed globally\n"));
2804
+ } catch {
2805
+ console.log(chalk10.red("\n \u2717 Failed to install. Try: sudo npm install -g @colbymchenry/cmem\n"));
2806
+ }
2807
+ } else {
2808
+ console.log(chalk10.dim("\n Skipped. Use `npx @colbymchenry/cmem` to run.\n"));
2809
+ }
2810
+ } else if (installedVersion && installedVersion !== currentVersion) {
2811
+ console.log(chalk10.yellow(` Update available: ${installedVersion} \u2192 ${currentVersion}`));
2812
+ const shouldUpdate = await promptYesNo("Update to latest version?");
2813
+ if (shouldUpdate) {
2814
+ console.log(chalk10.dim("\n Updating @colbymchenry/cmem...\n"));
2815
+ try {
2816
+ execSync3("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
2817
+ console.log(chalk10.green("\n \u2713 Updated\n"));
2818
+ } catch {
2819
+ console.log(chalk10.red("\n \u2717 Failed to update\n"));
2820
+ }
2821
+ } else {
2822
+ console.log("");
2823
+ }
2824
+ } else {
2825
+ console.log(chalk10.green(" \u2713 cmem is installed globally\n"));
2826
+ }
2827
+ console.log(chalk10.yellow(" Semantic search setup"));
2828
+ console.log(chalk10.dim(" Enables searching conversations by meaning\n"));
2829
+ const modelSpinner = new Spinner3("Checking embedding model...");
2830
+ modelSpinner.start();
2831
+ const cached = isModelCached();
2832
+ if (cached) {
2833
+ modelSpinner.succeed("Embedding model ready");
2834
+ } else {
2835
+ modelSpinner.update("Downloading embedding model (~130MB)...");
2836
+ try {
2837
+ await initializeEmbeddings((progress) => {
2838
+ if (progress.status === "downloading" && progress.progress !== void 0) {
2839
+ const percent = Math.round(progress.progress);
2840
+ const fileName = progress.file ? progress.file.split("/").pop() : "";
2841
+ modelSpinner.update(`Downloading ${fileName}... ${percent}%`);
2842
+ } else if (progress.status === "loading") {
2843
+ modelSpinner.update("Loading model...");
2844
+ }
2845
+ });
2846
+ modelSpinner.succeed("Embedding model ready");
2847
+ } catch (err) {
2848
+ modelSpinner.fail("Failed to download embedding model");
2849
+ console.log(chalk10.dim(` Error: ${err}
2850
+ `));
2851
+ }
2852
+ }
2853
+ console.log("");
2854
+ const daemonInstalled = existsSync9(
2855
+ join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist")
2856
+ );
2857
+ const isUpdating = installedVersion && installedVersion !== currentVersion;
2858
+ if (daemonInstalled && !isUpdating) {
2859
+ console.log(chalk10.green(" \u2713 Auto-start daemon is installed"));
2860
+ try {
2861
+ execSync3("launchctl load ~/Library/LaunchAgents/com.cmem.watch.plist 2>/dev/null", { stdio: "ignore" });
2862
+ } catch {
2863
+ }
2864
+ console.log("");
2865
+ } else if (daemonInstalled && isUpdating && process.platform === "darwin") {
2866
+ console.log(chalk10.yellow(" Updating daemon..."));
2867
+ try {
2868
+ const { installCommand: installCommand2 } = await Promise.resolve().then(() => (init_install(), install_exports));
2869
+ await installCommand2();
2870
+ } catch {
2871
+ console.log(chalk10.red(" \u2717 Failed to update daemon"));
2872
+ console.log(chalk10.dim(" Try manually: cmem install\n"));
2873
+ }
2874
+ } else if (process.platform === "darwin") {
2875
+ console.log(chalk10.yellow(" Auto-start daemon?"));
2876
+ console.log(chalk10.dim(" Syncs Claude Code sessions in the background\n"));
2877
+ const choice = await promptChoice("", [
2878
+ "Yes - start on login (recommended)",
2879
+ "No - I'll run `cmem watch` manually"
2880
+ ], 1);
2881
+ if (choice === 1) {
2882
+ try {
2883
+ const { installCommand: installCommand2 } = await Promise.resolve().then(() => (init_install(), install_exports));
2884
+ await installCommand2();
2885
+ } catch (err) {
2886
+ console.log(chalk10.red(" \u2717 Failed to install daemon"));
2887
+ console.log(chalk10.dim(" Try manually: cmem install\n"));
2888
+ }
2889
+ } else {
2890
+ console.log(chalk10.dim("\n Skipped. Run `cmem watch` when needed.\n"));
2891
+ }
2892
+ }
2893
+ const mcpConfigured = isMcpConfigured();
2894
+ if (mcpConfigured) {
2895
+ console.log(chalk10.green(" \u2713 MCP server configured in Claude Code\n"));
2896
+ } else {
2897
+ console.log(chalk10.yellow(" Add MCP server to Claude Code?"));
2898
+ console.log(chalk10.dim(" Lets Claude search your past conversations\n"));
2899
+ const choice = await promptChoice("", [
2900
+ "Yes - configure automatically (recommended)",
2901
+ "No - I'll configure it manually"
2902
+ ], 1);
2903
+ if (choice === 1) {
2904
+ const success = configureMcpServer();
2905
+ if (success) {
2906
+ console.log(chalk10.green("\n \u2713 Added MCP server to ~/.claude.json"));
2907
+ console.log(chalk10.green(" \u2713 Added permissions to ~/.claude/settings.json"));
2908
+ console.log(chalk10.dim(" Restart Claude Code or run /mcp to connect\n"));
2909
+ } else {
2910
+ console.log(chalk10.red("\n \u2717 Failed to configure MCP server\n"));
2911
+ }
2912
+ } else {
2913
+ console.log(chalk10.dim("\n Skipped.\n"));
2914
+ }
2915
+ }
2916
+ console.log(chalk10.yellow(" Initial session indexing"));
2917
+ console.log(chalk10.dim(" Scanning and indexing your existing Claude Code conversations\n"));
2918
+ await indexExistingSessions();
2919
+ if (!existsSync9(CMEM_DIR2)) {
2920
+ mkdirSync3(CMEM_DIR2, { recursive: true });
2921
+ }
2922
+ writeFileSync2(SETUP_MARKER, (/* @__PURE__ */ new Date()).toISOString());
2923
+ console.log(chalk10.magenta(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
2924
+ console.log(chalk10.green.bold("\n \u2713 Setup complete!\n"));
2925
+ console.log(chalk10.white(" Commands:"));
2926
+ console.log(chalk10.dim(" cmem Browse sessions (TUI)"));
2927
+ console.log(chalk10.dim(' cmem search "X" Semantic search'));
2928
+ console.log(chalk10.dim(" cmem watch Start sync daemon"));
2929
+ console.log(chalk10.dim(" cmem stats Storage statistics"));
2930
+ console.log(chalk10.dim(" cmem --help All commands"));
2931
+ console.log(chalk10.white("\n MCP Tools (available to Claude):"));
2932
+ console.log(chalk10.dim(" search_sessions Find past conversations"));
2933
+ console.log(chalk10.dim(" get_session Retrieve full history"));
2934
+ console.log(chalk10.dim(" list_sessions Browse recent sessions"));
2935
+ if (process.platform === "darwin") {
2936
+ const plistExists = existsSync9(join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist"));
2937
+ if (plistExists) {
2938
+ console.log(chalk10.green("\n \u{1F680} Daemon is now syncing your sessions in the background!"));
2939
+ }
2940
+ }
2941
+ console.log("");
2942
+ }
2943
+ function isMcpConfigured() {
2944
+ if (!existsSync9(CLAUDE_JSON_PATH)) {
2945
+ return false;
2946
+ }
2947
+ try {
2948
+ const config = JSON.parse(readFileSync3(CLAUDE_JSON_PATH, "utf-8"));
2949
+ return !!config.mcpServers?.cmem;
2950
+ } catch {
2951
+ return false;
2952
+ }
2953
+ }
2954
+ function configureMcpServer() {
2955
+ try {
2956
+ let claudeJson = {};
2957
+ if (existsSync9(CLAUDE_JSON_PATH)) {
2958
+ try {
2959
+ claudeJson = JSON.parse(readFileSync3(CLAUDE_JSON_PATH, "utf-8"));
2960
+ } catch {
2961
+ claudeJson = {};
2962
+ }
2963
+ }
2964
+ if (!claudeJson.mcpServers) {
2965
+ claudeJson.mcpServers = {};
2966
+ }
2967
+ claudeJson.mcpServers.cmem = {
2968
+ type: "stdio",
2969
+ command: "cmem",
2970
+ args: ["mcp"]
2971
+ };
2972
+ writeFileSync2(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2) + "\n");
2973
+ const claudeDir = dirname5(CLAUDE_SETTINGS_PATH);
2974
+ if (!existsSync9(claudeDir)) {
2975
+ mkdirSync3(claudeDir, { recursive: true });
2976
+ }
2977
+ let settings = {};
2978
+ if (existsSync9(CLAUDE_SETTINGS_PATH)) {
2979
+ try {
2980
+ settings = JSON.parse(readFileSync3(CLAUDE_SETTINGS_PATH, "utf-8"));
2981
+ } catch {
2982
+ settings = {};
2983
+ }
2984
+ }
2985
+ if (!settings.permissions) {
2986
+ settings.permissions = {};
2987
+ }
2988
+ if (!settings.permissions.allow) {
2989
+ settings.permissions.allow = [];
2990
+ }
2991
+ for (const perm of CMEM_PERMISSIONS) {
2992
+ if (!settings.permissions.allow.includes(perm)) {
2993
+ settings.permissions.allow.push(perm);
2994
+ }
2995
+ }
2996
+ writeFileSync2(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
2997
+ return true;
2998
+ } catch (err) {
2999
+ console.error(chalk10.red("Error configuring MCP:"), err);
3000
+ return false;
3001
+ }
3002
+ }
3003
+ function shouldRunSetup() {
3004
+ const isNpx = isRunningViaNpx();
3005
+ const setupComplete = existsSync9(SETUP_MARKER);
3006
+ const isGlobal = isGloballyInstalled();
3007
+ if (isNpx) return true;
3008
+ if (!isGlobal && !setupComplete) return true;
3009
+ const currentVersion = getCmemVersion();
3010
+ const installedVersion = getInstalledVersion();
3011
+ if (installedVersion && installedVersion !== currentVersion) {
3012
+ return true;
3013
+ }
3014
+ return false;
3015
+ }
3016
+ function findAllSessionFiles2(dir) {
3017
+ const files = [];
3018
+ if (!existsSync9(dir)) return files;
3019
+ function scanDir(currentDir, depth = 0) {
3020
+ if (depth > 10) return;
3021
+ try {
3022
+ const entries = readdirSync3(currentDir, { withFileTypes: true });
3023
+ for (const entry of entries) {
3024
+ const fullPath = join6(currentDir, entry.name);
3025
+ if (entry.isDirectory()) {
3026
+ if (entry.name === "subagents") continue;
3027
+ scanDir(fullPath, depth + 1);
3028
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
3029
+ files.push(fullPath);
3030
+ }
3031
+ }
3032
+ } catch {
3033
+ }
3034
+ }
3035
+ scanDir(dir);
3036
+ return files;
3037
+ }
3038
+ async function indexSessionFile(filePath, embeddingsReady) {
3039
+ try {
3040
+ const messages = parseSessionFile(filePath);
3041
+ if (messages.length === 0) return false;
3042
+ const existing = getSessionBySourceFile(filePath);
3043
+ if (existing) return false;
3044
+ const stats = statSync4(filePath);
3045
+ const fileMtime = stats.mtime.toISOString();
3046
+ const firstUserMsg = messages.find((m) => m.role === "user");
3047
+ const title = firstUserMsg ? generateTitle(firstUserMsg.content) : "Untitled Session";
3048
+ const summary = generateSummary(messages);
3049
+ const rawData = JSON.stringify({ filePath, messages, mtime: fileMtime });
3050
+ const sessionId_from_file = basename6(filePath, ".jsonl");
3051
+ const sessionId = createSession({
3052
+ title,
3053
+ summary,
3054
+ projectPath: null,
3055
+ sourceFile: filePath,
3056
+ rawData,
3057
+ messages
3058
+ });
3059
+ if (embeddingsReady) {
3060
+ const contentLength = messages.reduce((sum, m) => sum + m.content.length, 0);
3061
+ const shouldEmbed = needsReembedding(sessionId, contentLength, 500);
3062
+ if (shouldEmbed) {
3063
+ try {
3064
+ const embeddingText = createEmbeddingText(title, summary, messages);
3065
+ const embedding = await getEmbedding(embeddingText);
3066
+ storeEmbedding(sessionId, embedding);
3067
+ updateEmbeddingState(sessionId, contentLength, fileMtime);
3068
+ } catch {
3069
+ }
3070
+ }
3071
+ }
3072
+ return true;
3073
+ } catch {
3074
+ return false;
3075
+ }
3076
+ }
3077
+ async function indexExistingSessions() {
3078
+ const spinner = new Spinner3("Scanning for sessions...");
3079
+ spinner.start();
3080
+ const sessionFiles = findAllSessionFiles2(CLAUDE_DIR);
3081
+ const totalFiles = sessionFiles.length;
3082
+ if (totalFiles === 0) {
3083
+ spinner.succeed("No Claude Code sessions found yet");
3084
+ console.log(chalk10.dim(" Start using Claude Code, then run cmem to see your sessions\n"));
3085
+ return;
3086
+ }
3087
+ const statsBefore = getStats();
3088
+ const alreadyIndexed = statsBefore.sessionCount;
3089
+ if (alreadyIndexed >= totalFiles) {
3090
+ spinner.succeed(`Found ${totalFiles} sessions (all indexed)`);
3091
+ console.log("");
3092
+ return;
3093
+ }
3094
+ spinner.update(`Found ${totalFiles} sessions, indexing...`);
3095
+ const embeddingsReady = isReady();
3096
+ let processed = 0;
3097
+ let newlyIndexed = 0;
3098
+ for (const filePath of sessionFiles) {
3099
+ processed++;
3100
+ const wasNew = await indexSessionFile(filePath, embeddingsReady);
3101
+ if (wasNew) newlyIndexed++;
3102
+ const percent = Math.round(processed / totalFiles * 100);
3103
+ spinner.update(`Indexing sessions... ${percent}% (${processed}/${totalFiles})`);
3104
+ }
3105
+ spinner.succeed(`Indexed ${totalFiles} sessions (${newlyIndexed} new)`);
3106
+ const statsAfter = getStats();
3107
+ console.log(chalk10.dim(` ${statsAfter.sessionCount} sessions, ${statsAfter.embeddingCount} with embeddings
3108
+ `));
3109
+ }
3110
+
3111
+ // src/cli.ts
3112
+ var sessionToResume = null;
3113
+ function getVersion() {
3114
+ try {
3115
+ const __filename2 = fileURLToPath2(import.meta.url);
3116
+ const __dirname2 = dirname6(__filename2);
3117
+ const packagePath = join7(__dirname2, "..", "package.json");
3118
+ if (existsSync10(packagePath)) {
3119
+ const pkg = JSON.parse(readFileSync4(packagePath, "utf-8"));
3120
+ return pkg.version || "0.1.0";
3121
+ }
3122
+ } catch {
3123
+ }
3124
+ return "0.1.0";
3125
+ }
3126
+ program.name("cmem").description("Persistent session storage for Claude Code CLI").version(getVersion()).option("-l, --local", "Filter to sessions from current directory").action(async (options) => {
3127
+ if (shouldRunSetup()) {
3128
+ await setupCommand();
3129
+ return;
3130
+ }
3131
+ const handleResume = (sessionId, projectPath) => {
3132
+ sessionToResume = { sessionId, projectPath };
3133
+ };
3134
+ const projectFilter = options.local ? process.cwd() : null;
3135
+ const { waitUntilExit } = render(React2.createElement(App, { onResume: handleResume, projectFilter }));
3136
+ await waitUntilExit();
3137
+ if (sessionToResume) {
3138
+ const args = ["--resume", sessionToResume.sessionId];
3139
+ const cwd = sessionToResume.projectPath || process.cwd();
3140
+ console.log(`
3141
+ Resuming session in: ${cwd}
3142
+ `);
3143
+ const child = spawn2("claude", args, {
3144
+ cwd,
3145
+ stdio: "inherit",
3146
+ shell: true
3147
+ });
3148
+ child.on("error", (err) => {
3149
+ console.error("Failed to start Claude:", err.message);
3150
+ process.exit(1);
3151
+ });
3152
+ child.on("exit", (code) => {
3153
+ process.exit(code || 0);
3154
+ });
3155
+ }
3156
+ });
3157
+ program.command("setup").description("Run interactive setup wizard").action(async () => {
3158
+ await setupCommand();
3159
+ });
3160
+ program.command("save").description("Save a Claude Code session").option("-t, --title <title>", "Custom title").option("--latest", "Auto-save most recent session").action(async (options) => {
3161
+ await saveCommand(options);
3162
+ });
3163
+ program.command("list").description("List saved sessions (human sessions only by default)").option("-a, --all", "Show all sessions including automated ones").action(async (options) => {
3164
+ await listCommand(options);
3165
+ });
3166
+ program.command("search <query>").description("Semantic search across sessions").action(async (query) => {
3167
+ await searchCommand(query);
3168
+ });
3169
+ program.command("restore <id>").description("Restore a session").option("--copy", "Copy to clipboard").option("--format <format>", "Output format: context|json|markdown", "context").action(async (id, options) => {
3170
+ await restoreCommand(id, options);
3171
+ });
3172
+ program.command("delete <id>").description("Delete a session").action(async (id) => {
3173
+ await deleteCommand(id);
3174
+ });
3175
+ program.command("stats").description("Show storage statistics").action(async () => {
3176
+ await statsCommand();
3177
+ });
3178
+ program.command("watch").description("Watch for Claude Code session changes and auto-sync").option("-v, --verbose", "Show detailed output").option("--embed-threshold <chars>", "Re-embed after this many new chars", "500").action(async (options) => {
3179
+ await watchCommand({
3180
+ verbose: options.verbose,
3181
+ embedThreshold: parseInt(options.embedThreshold, 10)
3182
+ });
3183
+ });
3184
+ program.command("mcp").description("Start MCP server for Claude Code integration").action(async () => {
3185
+ await mcpCommand();
3186
+ });
3187
+ program.command("install").description("Install watch daemon to run at system startup (macOS)").action(async () => {
3188
+ await installCommand();
3189
+ });
3190
+ program.command("uninstall").description("Remove watch daemon from system startup").action(async () => {
3191
+ await uninstallCommand();
3192
+ });
3193
+ program.command("status").description("Check watch daemon status").action(async () => {
3194
+ await statusCommand();
3195
+ });
3196
+ program.parse();
3197
+ //# sourceMappingURL=cli.js.map