@colbymchenry/cmem 0.2.36 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,212 +1,27 @@
1
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 existsSync9, mkdirSync as mkdirSync2 } from "fs";
21
- import { homedir as homedir2 } from "os";
22
- import { join as join5, dirname as dirname5 } 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 dirname5(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 (!existsSync9(LAUNCH_AGENTS_DIR)) {
88
- mkdirSync2(LAUNCH_AGENTS_DIR, { recursive: true });
89
- }
90
- const cmemDir = join5(homedir2(), ".cmem");
91
- if (!existsSync9(cmemDir)) {
92
- mkdirSync2(cmemDir, { recursive: true });
93
- }
94
- const cmemPath = getCmemPath();
95
- console.log(chalk9.dim(`Using cmem at: ${cmemPath}`));
96
- if (existsSync9(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 (!existsSync9(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 (!existsSync9(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
- }
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
192
7
  });
193
8
 
194
9
  // src/cli.ts
195
10
  import { program } from "commander";
196
11
  import { render } from "ink";
197
12
  import React2 from "react";
198
- import { existsSync as existsSync11, readFileSync as readFileSync4 } from "fs";
199
- import { join as join7, dirname as dirname7 } from "path";
200
- import { fileURLToPath as fileURLToPath2 } from "url";
13
+ import { existsSync as existsSync12, readFileSync as readFileSync4 } from "fs";
14
+ import { join as join8, dirname as dirname8 } from "path";
15
+ import { fileURLToPath as fileURLToPath3 } from "url";
201
16
  import { spawn as spawn2 } from "child_process";
202
17
 
203
18
  // src/ui/App.tsx
204
19
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
205
- import { Box as Box6, Text as Text6, useInput, useApp } from "ink";
20
+ import { Box as Box7, Text as Text7, useInput, useApp } from "ink";
206
21
  import TextInput2 from "ink-text-input";
207
22
  import Spinner from "ink-spinner";
208
- import { basename as basename5, dirname as dirname3 } from "path";
209
- import { existsSync as existsSync6 } from "fs";
23
+ import { basename as basename5, dirname as dirname4 } from "path";
24
+ import { existsSync as existsSync7 } from "fs";
210
25
 
211
26
  // src/utils/config.ts
212
27
  import { homedir } from "os";
@@ -224,6 +39,8 @@ var EMBEDDING_DIMENSIONS = 768;
224
39
  var MAX_EMBEDDING_CHARS = 8e3;
225
40
  var MAX_MESSAGE_PREVIEW_CHARS = 500;
226
41
  var MAX_MESSAGES_FOR_CONTEXT = 20;
42
+ var DB_SIZE_ALERT_THRESHOLD = 5 * 1024 * 1024 * 1024;
43
+ var DEFAULT_PURGE_DAYS = 30;
227
44
  function ensureCmemDir() {
228
45
  if (!existsSync(CMEM_DIR)) {
229
46
  mkdirSync(CMEM_DIR, { recursive: true });
@@ -280,403 +97,96 @@ function restoreFromBackup(sourceFile) {
280
97
  }
281
98
  }
282
99
 
283
- // src/ui/components/Header.tsx
284
- import { Box, Text } from "ink";
285
- import { basename as basename2 } from "path";
286
- import { jsx, jsxs } from "react/jsx-runtime";
287
- var Header = ({ embeddingsReady, projectFilter }) => {
288
- return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [
289
- /* @__PURE__ */ jsxs(Box, { children: [
290
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "cmem" }),
291
- projectFilter && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
292
- " \u{1F4C1} ",
293
- basename2(projectFilter)
294
- ] }),
295
- !embeddingsReady && /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: " (loading model...)" })
296
- ] }),
297
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[q] quit" })
298
- ] });
299
- };
300
-
301
- // src/ui/components/SearchInput.tsx
302
- import { Box as Box2, Text as Text2 } from "ink";
303
- import TextInput from "ink-text-input";
304
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
305
- var SearchInput = ({
306
- value,
307
- onChange,
308
- isFocused
309
- }) => {
310
- return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
311
- /* @__PURE__ */ jsx2(Text2, { children: "Search: " }),
312
- isFocused ? /* @__PURE__ */ jsx2(
313
- TextInput,
314
- {
315
- value,
316
- onChange,
317
- placeholder: "type to search...",
318
- focus: true
319
- }
320
- ) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: value || "press / to search" })
321
- ] });
322
- };
100
+ // src/db/maintenance.ts
101
+ import { statSync as statSync2, existsSync as existsSync4, unlinkSync, readdirSync as readdirSync2, rmdirSync } from "fs";
102
+ import { join as join3, dirname as dirname3 } from "path";
323
103
 
324
- // src/ui/components/SessionList.tsx
325
- import { Box as Box3, Text as Text3 } from "ink";
326
- import { basename as basename3 } from "path";
104
+ // src/db/index.ts
105
+ import Database from "better-sqlite3";
106
+ import * as sqliteVec from "sqlite-vec";
107
+ import { existsSync as existsSync3 } from "fs";
327
108
 
328
- // src/utils/format.ts
329
- function formatTimeAgo(timestamp) {
330
- const date = new Date(timestamp);
331
- const now = /* @__PURE__ */ new Date();
332
- const diffMs = now.getTime() - date.getTime();
333
- const diffSecs = Math.floor(diffMs / 1e3);
334
- const diffMins = Math.floor(diffSecs / 60);
335
- const diffHours = Math.floor(diffMins / 60);
336
- const diffDays = Math.floor(diffHours / 24);
337
- const diffWeeks = Math.floor(diffDays / 7);
338
- const diffMonths = Math.floor(diffDays / 30);
339
- if (diffSecs < 60) return "just now";
340
- if (diffMins < 60) return `${diffMins}m ago`;
341
- if (diffHours < 24) return `${diffHours}h ago`;
342
- if (diffDays < 7) return `${diffDays}d ago`;
343
- if (diffWeeks < 4) return `${diffWeeks}w ago`;
344
- return `${diffMonths}mo ago`;
345
- }
346
- function truncate(text, maxLength) {
347
- if (text.length <= maxLength) return text;
348
- return text.slice(0, maxLength - 3) + "...";
349
- }
350
- function shortId(id) {
351
- return id.slice(0, 8);
352
- }
353
- function formatNumber(num) {
354
- return num.toLocaleString();
355
- }
356
- function formatBytes(bytes) {
357
- if (bytes === 0) return "0 B";
358
- const k = 1024;
359
- const sizes = ["B", "KB", "MB", "GB"];
360
- const i = Math.floor(Math.log(bytes) / Math.log(k));
361
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
362
- }
363
- function generateTitle(content) {
364
- const firstLine = content.split("\n")[0].trim();
365
- const title = truncate(firstLine, 50);
366
- return title || "Untitled Session";
109
+ // src/parser/index.ts
110
+ import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
111
+ import { join as join2 } from "path";
112
+ var AUTOMATED_TITLE_PATTERNS = [
113
+ /^<[a-z-]+>/i,
114
+ // XML-like tags: <project-instructions>, <command-message>, etc.
115
+ /^<[A-Z_]+>/,
116
+ // Uppercase tags: <SYSTEM>, <TOOL_USE>, etc.
117
+ /^\[system\]/i,
118
+ // [system] prefix
119
+ /^\/[a-z]+$/i
120
+ // Slash commands: /init, /help, etc.
121
+ ];
122
+ function isAutomatedByContent(title) {
123
+ for (const pattern of AUTOMATED_TITLE_PATTERNS) {
124
+ if (pattern.test(title.trim())) {
125
+ return true;
126
+ }
127
+ }
128
+ return false;
367
129
  }
368
-
369
- // src/ui/components/SessionList.tsx
370
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
371
- var SessionList = ({
372
- sessions,
373
- selectedIndex
374
- }) => {
375
- if (sessions.length === 0) {
376
- return /* @__PURE__ */ jsxs3(
377
- Box3,
378
- {
379
- flexDirection: "column",
380
- borderStyle: "round",
381
- borderColor: "gray",
382
- paddingX: 1,
383
- paddingY: 0,
384
- children: [
385
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Sessions" }),
386
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No sessions found" }),
387
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Run: cmem save --latest" })
388
- ]
130
+ function extractSessionMetadata(filepath) {
131
+ const content = readFileSync(filepath, "utf-8");
132
+ const metadata = {
133
+ isSidechain: false,
134
+ isMeta: false
135
+ };
136
+ for (const line of content.split("\n")) {
137
+ if (!line.trim()) continue;
138
+ try {
139
+ const parsed = JSON.parse(line);
140
+ if (parsed.type === "user" && parsed.message) {
141
+ if (parsed.isSidechain === true) {
142
+ metadata.isSidechain = true;
143
+ }
144
+ if (parsed.agentId) {
145
+ metadata.isSidechain = true;
146
+ }
147
+ if (parsed.isMeta === true) {
148
+ metadata.isMeta = true;
149
+ }
150
+ break;
389
151
  }
390
- );
391
- }
392
- const visibleCount = 8;
393
- let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
394
- const endIndex = Math.min(sessions.length, startIndex + visibleCount);
395
- if (endIndex - startIndex < visibleCount) {
396
- startIndex = Math.max(0, endIndex - visibleCount);
152
+ } catch {
153
+ }
397
154
  }
398
- const visibleSessions = sessions.slice(startIndex, endIndex);
399
- return /* @__PURE__ */ jsxs3(
400
- Box3,
401
- {
402
- flexDirection: "column",
403
- borderStyle: "round",
404
- borderColor: "gray",
405
- paddingX: 1,
406
- paddingY: 0,
407
- children: [
408
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
409
- "Sessions (",
410
- sessions.length,
411
- ")"
412
- ] }),
413
- visibleSessions.map((session, i) => {
414
- const actualIndex = startIndex + i;
415
- const isSelected = actualIndex === selectedIndex;
416
- return /* @__PURE__ */ jsx3(
417
- SessionItem,
418
- {
419
- session,
420
- isSelected
421
- },
422
- session.id
423
- );
424
- }),
425
- sessions.length > visibleCount && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
426
- startIndex > 0 ? "\u2191 more above" : "",
427
- startIndex > 0 && endIndex < sessions.length ? " | " : "",
428
- endIndex < sessions.length ? "\u2193 more below" : ""
429
- ] })
430
- ]
431
- }
432
- );
433
- };
434
- var SessionItem = ({ session, isSelected }) => {
435
- const hasCustomTitle = !!session.customTitle;
436
- const displayTitle = truncate(session.customTitle || session.title, 38);
437
- const folderName = session.projectPath ? truncate(basename3(session.projectPath), 38) : "";
438
- const msgs = String(session.messageCount).padStart(3);
439
- const updated = formatTimeAgo(session.updatedAt);
440
- const getTitleColor = () => {
441
- if (isSelected) return "cyan";
442
- if (session.isFavorite) return "yellow";
443
- if (hasCustomTitle) return "magenta";
444
- return void 0;
445
- };
446
- return /* @__PURE__ */ jsxs3(Box3, { children: [
447
- /* @__PURE__ */ jsx3(Text3, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
448
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: session.isFavorite ? "\u2B50" : " " }),
449
- /* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(38) }),
450
- /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
451
- " ",
452
- folderName.padEnd(38)
453
- ] }),
454
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
455
- " ",
456
- msgs,
457
- " "
458
- ] }),
459
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: updated.padStart(8) })
460
- ] });
461
- };
462
-
463
- // src/ui/components/ProjectList.tsx
464
- import { Box as Box4, Text as Text4 } from "ink";
465
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
466
- var ProjectList = ({
467
- projects,
468
- selectedIndex
469
- }) => {
470
- if (projects.length === 0) {
471
- return /* @__PURE__ */ jsxs4(
472
- Box4,
473
- {
474
- flexDirection: "column",
475
- borderStyle: "round",
476
- borderColor: "gray",
477
- paddingX: 1,
478
- paddingY: 0,
479
- children: [
480
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Projects" }),
481
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No projects found" }),
482
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Start using Claude Code in a project directory" })
483
- ]
484
- }
485
- );
486
- }
487
- const visibleCount = 8;
488
- let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
489
- const endIndex = Math.min(projects.length, startIndex + visibleCount);
490
- if (endIndex - startIndex < visibleCount) {
491
- startIndex = Math.max(0, endIndex - visibleCount);
492
- }
493
- const visibleProjects = projects.slice(startIndex, endIndex);
494
- return /* @__PURE__ */ jsxs4(
495
- Box4,
496
- {
497
- flexDirection: "column",
498
- borderStyle: "round",
499
- borderColor: "gray",
500
- paddingX: 1,
501
- paddingY: 0,
502
- children: [
503
- /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
504
- "Projects (",
505
- projects.length,
506
- ")"
507
- ] }),
508
- visibleProjects.map((project, i) => {
509
- const actualIndex = startIndex + i;
510
- const isSelected = actualIndex === selectedIndex;
511
- return /* @__PURE__ */ jsx4(
512
- ProjectItem,
513
- {
514
- project,
515
- isSelected
516
- },
517
- project.path
518
- );
519
- }),
520
- projects.length > visibleCount && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
521
- startIndex > 0 ? "\u2191 more above" : "",
522
- startIndex > 0 && endIndex < projects.length ? " | " : "",
523
- endIndex < projects.length ? "\u2193 more below" : ""
524
- ] })
525
- ]
526
- }
527
- );
528
- };
529
- var ProjectItem = ({ project, isSelected }) => {
530
- const displayName = truncate(project.name, 40);
531
- const sessions = `${project.sessionCount} session${project.sessionCount !== 1 ? "s" : ""}`;
532
- const messages = `${project.totalMessages} msgs`;
533
- const updated = formatTimeAgo(project.lastUpdated);
534
- const orderBadge = project.sortOrder !== null ? `${project.sortOrder + 1}.` : " ";
535
- return /* @__PURE__ */ jsxs4(Box4, { children: [
536
- /* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
537
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
538
- orderBadge.padStart(3),
539
- " "
540
- ] }),
541
- /* @__PURE__ */ jsx4(Text4, { color: "blue", children: "\u{1F4C1} " }),
542
- /* @__PURE__ */ jsx4(Text4, { bold: isSelected, color: isSelected ? "cyan" : void 0, wrap: "truncate", children: displayName.padEnd(35) }),
543
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
544
- " ",
545
- sessions.padEnd(12)
546
- ] }),
547
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
548
- " ",
549
- messages.padEnd(10)
550
- ] }),
551
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: updated.padStart(10) })
552
- ] });
553
- };
554
-
555
- // src/ui/components/Preview.tsx
556
- import { Box as Box5, Text as Text5 } from "ink";
557
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
558
- var Preview = ({ session }) => {
559
- if (!session) {
560
- return null;
561
- }
562
- const summary = session.summary ? truncate(session.summary, 200) : "No summary available";
563
- return /* @__PURE__ */ jsxs5(
564
- Box5,
565
- {
566
- flexDirection: "column",
567
- borderStyle: "round",
568
- borderColor: "gray",
569
- paddingX: 1,
570
- paddingY: 0,
571
- marginTop: 1,
572
- children: [
573
- /* @__PURE__ */ jsxs5(Box5, { children: [
574
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Preview" }),
575
- session.isFavorite && /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: " \u2B50" }),
576
- session.projectPath && /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "blue", children: [
577
- " \u{1F4C1} ",
578
- session.projectPath
579
- ] })
580
- ] }),
581
- /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: summary }),
582
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
583
- "Messages: ",
584
- session.messageCount
585
- ] })
586
- ]
587
- }
588
- );
589
- };
590
-
591
- // src/ui/hooks/useSessions.ts
592
- import { useState, useEffect, useCallback } from "react";
593
-
594
- // src/db/index.ts
595
- import Database from "better-sqlite3";
596
- import * as sqliteVec from "sqlite-vec";
597
- import { existsSync as existsSync3 } from "fs";
598
-
599
- // src/parser/index.ts
600
- import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
601
- import { join as join2 } from "path";
602
- var AUTOMATED_TITLE_PATTERNS = [
603
- /^<[a-z-]+>/i,
604
- // XML-like tags: <project-instructions>, <command-message>, etc.
605
- /^<[A-Z_]+>/,
606
- // Uppercase tags: <SYSTEM>, <TOOL_USE>, etc.
607
- /^\[system\]/i,
608
- // [system] prefix
609
- /^\/[a-z]+$/i
610
- // Slash commands: /init, /help, etc.
611
- ];
612
- function isAutomatedByContent(title) {
613
- for (const pattern of AUTOMATED_TITLE_PATTERNS) {
614
- if (pattern.test(title.trim())) {
615
- return true;
616
- }
617
- }
618
- return false;
619
- }
620
- function extractSessionMetadata(filepath) {
621
- const content = readFileSync(filepath, "utf-8");
622
- const metadata = {
623
- isSidechain: false,
624
- isMeta: false
625
- };
626
- for (const line of content.split("\n")) {
627
- if (!line.trim()) continue;
628
- try {
629
- const parsed = JSON.parse(line);
630
- if (parsed.type === "user" && parsed.message) {
631
- if (parsed.isSidechain === true) {
632
- metadata.isSidechain = true;
633
- }
634
- if (parsed.agentId) {
635
- metadata.isSidechain = true;
636
- }
637
- if (parsed.isMeta === true) {
638
- metadata.isMeta = true;
639
- }
640
- break;
641
- }
642
- } catch {
643
- }
644
- }
645
- return metadata;
646
- }
647
- function parseSessionFile(filepath) {
648
- const content = readFileSync(filepath, "utf-8");
649
- const messages = [];
650
- for (const line of content.split("\n")) {
651
- if (!line.trim()) continue;
652
- try {
653
- const parsed = JSON.parse(line);
654
- if ((parsed.type === "user" || parsed.type === "assistant") && parsed.message) {
655
- const msg = parsed.message;
656
- if (msg.role && msg.content) {
657
- const content2 = Array.isArray(msg.content) ? msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n") : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
658
- if (content2) {
659
- messages.push({
660
- role: msg.role,
661
- content: content2,
662
- timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
663
- });
664
- }
665
- }
666
- } else if (parsed.type === "message" && parsed.role && parsed.content) {
667
- messages.push({
668
- role: parsed.role,
669
- content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
670
- timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
671
- });
672
- } else if (parsed.role && parsed.content && !parsed.type) {
673
- messages.push({
674
- role: parsed.role,
675
- content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
676
- timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
677
- });
678
- }
679
- } catch {
155
+ return metadata;
156
+ }
157
+ function parseSessionFile(filepath) {
158
+ const content = readFileSync(filepath, "utf-8");
159
+ const messages = [];
160
+ for (const line of content.split("\n")) {
161
+ if (!line.trim()) continue;
162
+ try {
163
+ const parsed = JSON.parse(line);
164
+ if ((parsed.type === "user" || parsed.type === "assistant") && parsed.message) {
165
+ const msg = parsed.message;
166
+ if (msg.role && msg.content) {
167
+ 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);
168
+ if (content2) {
169
+ messages.push({
170
+ role: msg.role,
171
+ content: content2,
172
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
173
+ });
174
+ }
175
+ }
176
+ } else if (parsed.type === "message" && parsed.role && parsed.content) {
177
+ messages.push({
178
+ role: parsed.role,
179
+ content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
180
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
181
+ });
182
+ } else if (parsed.role && parsed.content && !parsed.type) {
183
+ messages.push({
184
+ role: parsed.role,
185
+ content: typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content),
186
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString()
187
+ });
188
+ }
189
+ } catch {
680
190
  }
681
191
  }
682
192
  return messages;
@@ -888,33 +398,115 @@ function initSchema(database) {
888
398
  database.exec(`
889
399
  CREATE INDEX IF NOT EXISTS idx_favorites_type ON favorites(type);
890
400
  `);
891
- runMigrations(database);
892
- }
893
- function runMigrations(database) {
894
- const migrationName = "populate_session_metadata_v1";
895
- const existing = database.prepare(
896
- "SELECT 1 FROM migrations WHERE name = ?"
897
- ).get(migrationName);
898
- if (existing) return;
899
- const sessions = database.prepare(`
900
- SELECT id, source_file FROM sessions WHERE source_file IS NOT NULL
901
- `).all();
902
- const updateStmt = database.prepare(`
903
- UPDATE sessions SET is_sidechain = ?, is_automated = ? WHERE id = ?
401
+ database.exec(`
402
+ CREATE TABLE IF NOT EXISTS lessons (
403
+ id TEXT PRIMARY KEY,
404
+ project_path TEXT NOT NULL,
405
+ category TEXT NOT NULL CHECK (category IN (
406
+ 'architecture_decision', 'anti_pattern', 'bug_pattern',
407
+ 'project_convention', 'dependency_knowledge', 'domain_knowledge',
408
+ 'workflow', 'other'
409
+ )),
410
+ title TEXT NOT NULL,
411
+ trigger_context TEXT NOT NULL,
412
+ insight TEXT NOT NULL,
413
+ reasoning TEXT,
414
+ confidence REAL DEFAULT 0.5,
415
+ times_applied INTEGER DEFAULT 0,
416
+ times_validated INTEGER DEFAULT 0,
417
+ times_rejected INTEGER DEFAULT 0,
418
+ source_session_id TEXT,
419
+ source_type TEXT DEFAULT 'synthesized',
420
+ archived INTEGER DEFAULT 0,
421
+ created_at TEXT DEFAULT (datetime('now')),
422
+ updated_at TEXT DEFAULT (datetime('now')),
423
+ last_applied_at TEXT
424
+ );
904
425
  `);
905
- const transaction = database.transaction(() => {
906
- for (const session of sessions) {
907
- if (!existsSync3(session.source_file)) continue;
908
- try {
909
- const metadata = extractSessionMetadata(session.source_file);
910
- const isAutomated = metadata.isSidechain || metadata.isMeta;
911
- updateStmt.run(
912
- metadata.isSidechain ? 1 : 0,
913
- isAutomated ? 1 : 0,
914
- session.id
915
- );
916
- } catch {
917
- }
426
+ database.exec(`
427
+ CREATE VIRTUAL TABLE IF NOT EXISTS lesson_embeddings USING vec0(
428
+ lesson_id TEXT PRIMARY KEY,
429
+ embedding FLOAT[${EMBEDDING_DIMENSIONS}]
430
+ );
431
+ `);
432
+ database.exec(`
433
+ CREATE TABLE IF NOT EXISTS lesson_feedback (
434
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
435
+ lesson_id TEXT NOT NULL,
436
+ session_id TEXT,
437
+ feedback_type TEXT NOT NULL CHECK (feedback_type IN (
438
+ 'validated', 'rejected', 'modified'
439
+ )),
440
+ comment TEXT,
441
+ created_at TEXT DEFAULT (datetime('now')),
442
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE
443
+ );
444
+ `);
445
+ database.exec(`
446
+ CREATE TABLE IF NOT EXISTS synthesis_queue (
447
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
448
+ session_id TEXT NOT NULL UNIQUE,
449
+ project_path TEXT NOT NULL,
450
+ queued_at TEXT DEFAULT (datetime('now')),
451
+ status TEXT DEFAULT 'pending',
452
+ processed_at TEXT,
453
+ lessons_created INTEGER DEFAULT 0,
454
+ error TEXT
455
+ );
456
+ `);
457
+ database.exec(`
458
+ CREATE TABLE IF NOT EXISTS session_injections (
459
+ session_id TEXT NOT NULL,
460
+ lesson_id TEXT NOT NULL,
461
+ injected_at TEXT DEFAULT (datetime('now')),
462
+ PRIMARY KEY (session_id, lesson_id)
463
+ );
464
+ `);
465
+ database.exec(`
466
+ CREATE INDEX IF NOT EXISTS idx_lessons_project ON lessons(project_path);
467
+ `);
468
+ database.exec(`
469
+ CREATE INDEX IF NOT EXISTS idx_lessons_category ON lessons(category);
470
+ `);
471
+ database.exec(`
472
+ CREATE INDEX IF NOT EXISTS idx_lessons_confidence ON lessons(confidence DESC);
473
+ `);
474
+ database.exec(`
475
+ CREATE INDEX IF NOT EXISTS idx_lessons_archived ON lessons(archived);
476
+ `);
477
+ database.exec(`
478
+ CREATE INDEX IF NOT EXISTS idx_synthesis_queue_status ON synthesis_queue(status);
479
+ `);
480
+ database.exec(`
481
+ CREATE INDEX IF NOT EXISTS idx_session_injections_session ON session_injections(session_id);
482
+ `);
483
+ runMigrations(database);
484
+ }
485
+ function runMigrations(database) {
486
+ const migrationName = "populate_session_metadata_v1";
487
+ const existing = database.prepare(
488
+ "SELECT 1 FROM migrations WHERE name = ?"
489
+ ).get(migrationName);
490
+ if (existing) return;
491
+ const sessions = database.prepare(`
492
+ SELECT id, source_file FROM sessions WHERE source_file IS NOT NULL
493
+ `).all();
494
+ const updateStmt = database.prepare(`
495
+ UPDATE sessions SET is_sidechain = ?, is_automated = ? WHERE id = ?
496
+ `);
497
+ const transaction = database.transaction(() => {
498
+ for (const session of sessions) {
499
+ if (!existsSync3(session.source_file)) continue;
500
+ try {
501
+ const metadata = extractSessionMetadata(session.source_file);
502
+ const isAutomated = metadata.isSidechain || metadata.isMeta;
503
+ updateStmt.run(
504
+ metadata.isSidechain ? 1 : 0,
505
+ isAutomated ? 1 : 0,
506
+ session.id
507
+ );
508
+ } catch {
509
+ }
918
510
  }
919
511
  database.prepare(
920
512
  "INSERT INTO migrations (name, applied_at) VALUES (?, ?)"
@@ -923,6 +515,156 @@ function runMigrations(database) {
923
515
  transaction();
924
516
  }
925
517
 
518
+ // src/db/maintenance.ts
519
+ function getDatabaseSize() {
520
+ try {
521
+ const stats = statSync2(DB_PATH);
522
+ return stats.size;
523
+ } catch {
524
+ return 0;
525
+ }
526
+ }
527
+ function getBackupsDirSize() {
528
+ try {
529
+ if (!existsSync4(BACKUPS_DIR)) return 0;
530
+ return getDirSize(BACKUPS_DIR);
531
+ } catch {
532
+ return 0;
533
+ }
534
+ }
535
+ function getDirSize(dirPath) {
536
+ let size = 0;
537
+ try {
538
+ const entries = readdirSync2(dirPath, { withFileTypes: true });
539
+ for (const entry of entries) {
540
+ const fullPath = join3(dirPath, entry.name);
541
+ if (entry.isDirectory()) {
542
+ size += getDirSize(fullPath);
543
+ } else {
544
+ try {
545
+ size += statSync2(fullPath).size;
546
+ } catch {
547
+ }
548
+ }
549
+ }
550
+ } catch {
551
+ }
552
+ return size;
553
+ }
554
+ function getTotalStorageSize() {
555
+ return getDatabaseSize() + getBackupsDirSize();
556
+ }
557
+ function getPurgePreview(days) {
558
+ const db2 = getDatabase();
559
+ const result = db2.prepare(`
560
+ SELECT
561
+ COUNT(*) as sessionCount,
562
+ COALESCE(MIN(s.updated_at), '') as oldestDate,
563
+ COALESCE(MAX(s.updated_at), '') as newestDate
564
+ FROM sessions s
565
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
566
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
567
+ AND f.id IS NULL
568
+ `).get(days);
569
+ const messageResult = db2.prepare(`
570
+ SELECT COALESCE(SUM(s.message_count), 0) as messageCount
571
+ FROM sessions s
572
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
573
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
574
+ AND f.id IS NULL
575
+ `).get(days);
576
+ const sessionsWithSourceFiles = db2.prepare(`
577
+ SELECT s.source_file as sourceFile
578
+ FROM sessions s
579
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
580
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
581
+ AND f.id IS NULL
582
+ AND s.source_file IS NOT NULL
583
+ `).all(days);
584
+ let backupFilesToDelete = 0;
585
+ let backupBytesToFree = 0;
586
+ for (const session of sessionsWithSourceFiles) {
587
+ const backupPath = getBackupPath(session.sourceFile);
588
+ if (existsSync4(backupPath)) {
589
+ backupFilesToDelete++;
590
+ try {
591
+ backupBytesToFree += statSync2(backupPath).size;
592
+ } catch {
593
+ }
594
+ }
595
+ }
596
+ return {
597
+ sessionsToDelete: result.sessionCount,
598
+ messagesInvolved: messageResult.messageCount,
599
+ oldestSessionDate: result.oldestDate ? formatDate(result.oldestDate) : "",
600
+ newestSessionDate: result.newestDate ? formatDate(result.newestDate) : "",
601
+ backupFilesToDelete,
602
+ backupBytesToFree
603
+ };
604
+ }
605
+ function purgeOldSessions(days) {
606
+ const db2 = getDatabase();
607
+ const sessionsToDelete = db2.prepare(`
608
+ SELECT s.id, s.source_file as sourceFile FROM sessions s
609
+ LEFT JOIN favorites f ON f.type = 'session' AND f.value = s.id
610
+ WHERE s.updated_at < datetime('now', '-' || ? || ' days')
611
+ AND f.id IS NULL
612
+ `).all(days);
613
+ if (sessionsToDelete.length === 0) {
614
+ return { sessionsDeleted: 0, backupsDeleted: 0 };
615
+ }
616
+ let backupsDeleted = 0;
617
+ for (const session of sessionsToDelete) {
618
+ if (session.sourceFile) {
619
+ const backupPath = getBackupPath(session.sourceFile);
620
+ if (existsSync4(backupPath)) {
621
+ try {
622
+ unlinkSync(backupPath);
623
+ backupsDeleted++;
624
+ const parentDir = dirname3(backupPath);
625
+ try {
626
+ const remaining = readdirSync2(parentDir);
627
+ if (remaining.length === 0) {
628
+ rmdirSync(parentDir);
629
+ }
630
+ } catch {
631
+ }
632
+ } catch {
633
+ }
634
+ }
635
+ }
636
+ }
637
+ const transaction = db2.transaction(() => {
638
+ for (const session of sessionsToDelete) {
639
+ db2.prepare("DELETE FROM session_embeddings WHERE session_id = ?").run(session.id);
640
+ db2.prepare("DELETE FROM embedding_state WHERE session_id = ?").run(session.id);
641
+ db2.prepare("DELETE FROM messages WHERE session_id = ?").run(session.id);
642
+ db2.prepare("DELETE FROM sessions WHERE id = ?").run(session.id);
643
+ }
644
+ });
645
+ transaction();
646
+ return { sessionsDeleted: sessionsToDelete.length, backupsDeleted };
647
+ }
648
+ function formatDate(isoDate) {
649
+ try {
650
+ const date = new Date(isoDate);
651
+ return date.toLocaleDateString("en-US", {
652
+ year: "numeric",
653
+ month: "short",
654
+ day: "numeric"
655
+ });
656
+ } catch {
657
+ return isoDate;
658
+ }
659
+ }
660
+ function formatBytes(bytes) {
661
+ if (bytes === 0) return "0 B";
662
+ const k = 1024;
663
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
664
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
665
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
666
+ }
667
+
926
668
  // src/db/sessions.ts
927
669
  import { randomUUID } from "crypto";
928
670
  function createSession(input) {
@@ -1240,16 +982,432 @@ function updateProjectOrders(orders) {
1240
982
  for (const order of orders) {
1241
983
  stmt.run(order.path, order.sortOrder, now);
1242
984
  }
1243
- });
1244
- transaction();
1245
- }
1246
- function hasCustomProjectOrder() {
1247
- const db2 = getDatabase();
1248
- const row = db2.prepare(`
1249
- SELECT 1 FROM project_order LIMIT 1
1250
- `).get();
1251
- return !!row;
1252
- }
985
+ });
986
+ transaction();
987
+ }
988
+ function hasCustomProjectOrder() {
989
+ const db2 = getDatabase();
990
+ const row = db2.prepare(`
991
+ SELECT 1 FROM project_order LIMIT 1
992
+ `).get();
993
+ return !!row;
994
+ }
995
+
996
+ // src/ui/components/StorageScreen.tsx
997
+ import { Box, Text } from "ink";
998
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
999
+ var StorageScreen = ({
1000
+ stats,
1001
+ purgeOptions,
1002
+ selectedOption,
1003
+ mode
1004
+ }) => {
1005
+ const selectedPurge = selectedOption !== null ? purgeOptions[selectedOption] : null;
1006
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1007
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Storage Management" }) }),
1008
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1009
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Current Storage:" }),
1010
+ /* @__PURE__ */ jsxs(Text, { children: [
1011
+ " Database: ",
1012
+ formatBytes(stats.dbSize)
1013
+ ] }),
1014
+ /* @__PURE__ */ jsxs(Text, { children: [
1015
+ " Backups: ",
1016
+ formatBytes(stats.backupsSize)
1017
+ ] }),
1018
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1019
+ " Total: ",
1020
+ formatBytes(stats.totalSize)
1021
+ ] }),
1022
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1023
+ " (",
1024
+ stats.sessionCount,
1025
+ " sessions, ",
1026
+ stats.messageCount,
1027
+ " messages)"
1028
+ ] })
1029
+ ] }),
1030
+ mode === "view" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1031
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1032
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Purge Old Sessions:" }),
1033
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (Deletes both database entries and backup files)" }),
1034
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (Starred sessions are always preserved)" })
1035
+ ] }),
1036
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: purgeOptions.map((option, index) => {
1037
+ const hasData = option.preview.sessionsToDelete > 0;
1038
+ return /* @__PURE__ */ jsxs(Box, { children: [
1039
+ /* @__PURE__ */ jsxs(Text, { color: hasData ? "yellow" : "gray", children: [
1040
+ "[",
1041
+ index + 1,
1042
+ "] Older than ",
1043
+ option.days,
1044
+ " days: ",
1045
+ " "
1046
+ ] }),
1047
+ hasData ? /* @__PURE__ */ jsxs(Text, { children: [
1048
+ option.preview.sessionsToDelete,
1049
+ " sessions",
1050
+ option.preview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs(Text, { children: [
1051
+ ", ",
1052
+ option.preview.backupFilesToDelete,
1053
+ " backups"
1054
+ ] }),
1055
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1056
+ " (~",
1057
+ formatBytes(option.preview.backupBytesToFree),
1058
+ " freed)"
1059
+ ] })
1060
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No sessions to purge" })
1061
+ ] }, option.days);
1062
+ }) }),
1063
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[1/2/3] Select option [Esc] Back" }) })
1064
+ ] }) : (
1065
+ /* Confirm mode */
1066
+ selectedPurge && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1067
+ /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "Confirm Purge:" }),
1068
+ /* @__PURE__ */ jsxs(Text, { children: [
1069
+ " Period: Older than ",
1070
+ selectedPurge.days,
1071
+ " days"
1072
+ ] }),
1073
+ /* @__PURE__ */ jsxs(Text, { children: [
1074
+ " Sessions: ",
1075
+ selectedPurge.preview.sessionsToDelete
1076
+ ] }),
1077
+ /* @__PURE__ */ jsxs(Text, { children: [
1078
+ " Messages: ",
1079
+ selectedPurge.preview.messagesInvolved
1080
+ ] }),
1081
+ selectedPurge.preview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs(Text, { children: [
1082
+ " Backup files: ",
1083
+ selectedPurge.preview.backupFilesToDelete
1084
+ ] }),
1085
+ /* @__PURE__ */ jsxs(Text, { children: [
1086
+ " Date range: ",
1087
+ selectedPurge.preview.oldestSessionDate,
1088
+ " to ",
1089
+ selectedPurge.preview.newestSessionDate
1090
+ ] }),
1091
+ /* @__PURE__ */ jsxs(Text, { children: [
1092
+ " Space freed: ~",
1093
+ formatBytes(selectedPurge.preview.backupBytesToFree)
1094
+ ] }),
1095
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: "This cannot be undone. Proceed? [y/n]" }) })
1096
+ ] })
1097
+ )
1098
+ ] });
1099
+ };
1100
+
1101
+ // src/ui/components/Header.tsx
1102
+ import { Box as Box2, Text as Text2 } from "ink";
1103
+ import { basename as basename3 } from "path";
1104
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1105
+ var Header = ({ embeddingsReady, projectFilter }) => {
1106
+ return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, justifyContent: "space-between", children: [
1107
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1108
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "cmem" }),
1109
+ projectFilter && /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
1110
+ " \u{1F4C1} ",
1111
+ basename3(projectFilter)
1112
+ ] }),
1113
+ !embeddingsReady && /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: " (loading model...)" })
1114
+ ] }),
1115
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[q] quit" })
1116
+ ] });
1117
+ };
1118
+
1119
+ // src/ui/components/SearchInput.tsx
1120
+ import { Box as Box3, Text as Text3 } from "ink";
1121
+ import TextInput from "ink-text-input";
1122
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1123
+ var SearchInput = ({
1124
+ value,
1125
+ onChange,
1126
+ isFocused
1127
+ }) => {
1128
+ return /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
1129
+ /* @__PURE__ */ jsx3(Text3, { children: "Search: " }),
1130
+ isFocused ? /* @__PURE__ */ jsx3(
1131
+ TextInput,
1132
+ {
1133
+ value,
1134
+ onChange,
1135
+ placeholder: "type to search...",
1136
+ focus: true
1137
+ }
1138
+ ) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: value || "press / to search" })
1139
+ ] });
1140
+ };
1141
+
1142
+ // src/ui/components/SessionList.tsx
1143
+ import { Box as Box4, Text as Text4 } from "ink";
1144
+ import { basename as basename4 } from "path";
1145
+
1146
+ // src/utils/format.ts
1147
+ function formatTimeAgo(timestamp) {
1148
+ const date = new Date(timestamp);
1149
+ const now = /* @__PURE__ */ new Date();
1150
+ const diffMs = now.getTime() - date.getTime();
1151
+ const diffSecs = Math.floor(diffMs / 1e3);
1152
+ const diffMins = Math.floor(diffSecs / 60);
1153
+ const diffHours = Math.floor(diffMins / 60);
1154
+ const diffDays = Math.floor(diffHours / 24);
1155
+ const diffWeeks = Math.floor(diffDays / 7);
1156
+ const diffMonths = Math.floor(diffDays / 30);
1157
+ if (diffSecs < 60) return "just now";
1158
+ if (diffMins < 60) return `${diffMins}m ago`;
1159
+ if (diffHours < 24) return `${diffHours}h ago`;
1160
+ if (diffDays < 7) return `${diffDays}d ago`;
1161
+ if (diffWeeks < 4) return `${diffWeeks}w ago`;
1162
+ return `${diffMonths}mo ago`;
1163
+ }
1164
+ function truncate(text, maxLength) {
1165
+ if (text.length <= maxLength) return text;
1166
+ return text.slice(0, maxLength - 3) + "...";
1167
+ }
1168
+ function shortId(id) {
1169
+ return id.slice(0, 8);
1170
+ }
1171
+ function formatNumber(num) {
1172
+ return num.toLocaleString();
1173
+ }
1174
+ function formatBytes2(bytes) {
1175
+ if (bytes === 0) return "0 B";
1176
+ const k = 1024;
1177
+ const sizes = ["B", "KB", "MB", "GB"];
1178
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1179
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
1180
+ }
1181
+ function generateTitle(content) {
1182
+ const firstLine = content.split("\n")[0].trim();
1183
+ const title = truncate(firstLine, 50);
1184
+ return title || "Untitled Session";
1185
+ }
1186
+
1187
+ // src/ui/components/SessionList.tsx
1188
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1189
+ var SessionList = ({
1190
+ sessions,
1191
+ selectedIndex
1192
+ }) => {
1193
+ if (sessions.length === 0) {
1194
+ return /* @__PURE__ */ jsxs4(
1195
+ Box4,
1196
+ {
1197
+ flexDirection: "column",
1198
+ borderStyle: "round",
1199
+ borderColor: "gray",
1200
+ paddingX: 1,
1201
+ paddingY: 0,
1202
+ children: [
1203
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Sessions" }),
1204
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No sessions found" }),
1205
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Run: cmem save --latest" })
1206
+ ]
1207
+ }
1208
+ );
1209
+ }
1210
+ const visibleCount = 8;
1211
+ let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
1212
+ const endIndex = Math.min(sessions.length, startIndex + visibleCount);
1213
+ if (endIndex - startIndex < visibleCount) {
1214
+ startIndex = Math.max(0, endIndex - visibleCount);
1215
+ }
1216
+ const visibleSessions = sessions.slice(startIndex, endIndex);
1217
+ return /* @__PURE__ */ jsxs4(
1218
+ Box4,
1219
+ {
1220
+ flexDirection: "column",
1221
+ borderStyle: "round",
1222
+ borderColor: "gray",
1223
+ paddingX: 1,
1224
+ paddingY: 0,
1225
+ children: [
1226
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1227
+ "Sessions (",
1228
+ sessions.length,
1229
+ ")"
1230
+ ] }),
1231
+ visibleSessions.map((session, i) => {
1232
+ const actualIndex = startIndex + i;
1233
+ const isSelected = actualIndex === selectedIndex;
1234
+ return /* @__PURE__ */ jsx4(
1235
+ SessionItem,
1236
+ {
1237
+ session,
1238
+ isSelected
1239
+ },
1240
+ session.id
1241
+ );
1242
+ }),
1243
+ sessions.length > visibleCount && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1244
+ startIndex > 0 ? "\u2191 more above" : "",
1245
+ startIndex > 0 && endIndex < sessions.length ? " | " : "",
1246
+ endIndex < sessions.length ? "\u2193 more below" : ""
1247
+ ] })
1248
+ ]
1249
+ }
1250
+ );
1251
+ };
1252
+ var SessionItem = ({ session, isSelected }) => {
1253
+ const hasCustomTitle = !!session.customTitle;
1254
+ const displayTitle = truncate(session.customTitle || session.title, 38);
1255
+ const folderName = session.projectPath ? truncate(basename4(session.projectPath), 38) : "";
1256
+ const msgs = String(session.messageCount).padStart(3);
1257
+ const updated = formatTimeAgo(session.updatedAt);
1258
+ const getTitleColor = () => {
1259
+ if (isSelected) return "cyan";
1260
+ if (session.isFavorite) return "yellow";
1261
+ if (hasCustomTitle) return "magenta";
1262
+ return void 0;
1263
+ };
1264
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
1265
+ /* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
1266
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: session.isFavorite ? "\u2B50" : " " }),
1267
+ /* @__PURE__ */ jsx4(Text4, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(38) }),
1268
+ /* @__PURE__ */ jsxs4(Text4, { color: "blue", children: [
1269
+ " ",
1270
+ folderName.padEnd(38)
1271
+ ] }),
1272
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1273
+ " ",
1274
+ msgs,
1275
+ " "
1276
+ ] }),
1277
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: updated.padStart(8) })
1278
+ ] });
1279
+ };
1280
+
1281
+ // src/ui/components/ProjectList.tsx
1282
+ import { Box as Box5, Text as Text5 } from "ink";
1283
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1284
+ var ProjectList = ({
1285
+ projects,
1286
+ selectedIndex
1287
+ }) => {
1288
+ if (projects.length === 0) {
1289
+ return /* @__PURE__ */ jsxs5(
1290
+ Box5,
1291
+ {
1292
+ flexDirection: "column",
1293
+ borderStyle: "round",
1294
+ borderColor: "gray",
1295
+ paddingX: 1,
1296
+ paddingY: 0,
1297
+ children: [
1298
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Projects" }),
1299
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No projects found" }),
1300
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Start using Claude Code in a project directory" })
1301
+ ]
1302
+ }
1303
+ );
1304
+ }
1305
+ const visibleCount = 8;
1306
+ let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
1307
+ const endIndex = Math.min(projects.length, startIndex + visibleCount);
1308
+ if (endIndex - startIndex < visibleCount) {
1309
+ startIndex = Math.max(0, endIndex - visibleCount);
1310
+ }
1311
+ const visibleProjects = projects.slice(startIndex, endIndex);
1312
+ return /* @__PURE__ */ jsxs5(
1313
+ Box5,
1314
+ {
1315
+ flexDirection: "column",
1316
+ borderStyle: "round",
1317
+ borderColor: "gray",
1318
+ paddingX: 1,
1319
+ paddingY: 0,
1320
+ children: [
1321
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
1322
+ "Projects (",
1323
+ projects.length,
1324
+ ")"
1325
+ ] }),
1326
+ visibleProjects.map((project, i) => {
1327
+ const actualIndex = startIndex + i;
1328
+ const isSelected = actualIndex === selectedIndex;
1329
+ return /* @__PURE__ */ jsx5(
1330
+ ProjectItem,
1331
+ {
1332
+ project,
1333
+ isSelected
1334
+ },
1335
+ project.path
1336
+ );
1337
+ }),
1338
+ projects.length > visibleCount && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1339
+ startIndex > 0 ? "\u2191 more above" : "",
1340
+ startIndex > 0 && endIndex < projects.length ? " | " : "",
1341
+ endIndex < projects.length ? "\u2193 more below" : ""
1342
+ ] })
1343
+ ]
1344
+ }
1345
+ );
1346
+ };
1347
+ var ProjectItem = ({ project, isSelected }) => {
1348
+ const displayName = truncate(project.name, 40);
1349
+ const sessions = `${project.sessionCount} session${project.sessionCount !== 1 ? "s" : ""}`;
1350
+ const messages = `${project.totalMessages} msgs`;
1351
+ const updated = formatTimeAgo(project.lastUpdated);
1352
+ const orderBadge = project.sortOrder !== null ? `${project.sortOrder + 1}.` : " ";
1353
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
1354
+ /* @__PURE__ */ jsx5(Text5, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
1355
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1356
+ orderBadge.padStart(3),
1357
+ " "
1358
+ ] }),
1359
+ /* @__PURE__ */ jsx5(Text5, { color: "blue", children: "\u{1F4C1} " }),
1360
+ /* @__PURE__ */ jsx5(Text5, { bold: isSelected, color: isSelected ? "cyan" : void 0, wrap: "truncate", children: displayName.padEnd(35) }),
1361
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1362
+ " ",
1363
+ sessions.padEnd(12)
1364
+ ] }),
1365
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1366
+ " ",
1367
+ messages.padEnd(10)
1368
+ ] }),
1369
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: updated.padStart(10) })
1370
+ ] });
1371
+ };
1372
+
1373
+ // src/ui/components/Preview.tsx
1374
+ import { Box as Box6, Text as Text6 } from "ink";
1375
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1376
+ var Preview = ({ session }) => {
1377
+ if (!session) {
1378
+ return null;
1379
+ }
1380
+ const summary = session.summary ? truncate(session.summary, 200) : "No summary available";
1381
+ return /* @__PURE__ */ jsxs6(
1382
+ Box6,
1383
+ {
1384
+ flexDirection: "column",
1385
+ borderStyle: "round",
1386
+ borderColor: "gray",
1387
+ paddingX: 1,
1388
+ paddingY: 0,
1389
+ marginTop: 1,
1390
+ children: [
1391
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1392
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Preview" }),
1393
+ session.isFavorite && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: " \u2B50" }),
1394
+ session.projectPath && /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "blue", children: [
1395
+ " \u{1F4C1} ",
1396
+ session.projectPath
1397
+ ] })
1398
+ ] }),
1399
+ /* @__PURE__ */ jsx6(Text6, { wrap: "wrap", children: summary }),
1400
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1401
+ "Messages: ",
1402
+ session.messageCount
1403
+ ] })
1404
+ ]
1405
+ }
1406
+ );
1407
+ };
1408
+
1409
+ // src/ui/hooks/useSessions.ts
1410
+ import { useState, useEffect, useCallback } from "react";
1253
1411
 
1254
1412
  // src/db/vectors.ts
1255
1413
  function storeEmbedding(sessionId, embedding) {
@@ -1276,8 +1434,8 @@ function searchSessions(queryEmbedding, limit = 10) {
1276
1434
  }
1277
1435
 
1278
1436
  // src/embeddings/index.ts
1279
- import { existsSync as existsSync4 } from "fs";
1280
- import { join as join3 } from "path";
1437
+ import { existsSync as existsSync5 } from "fs";
1438
+ import { join as join4 } from "path";
1281
1439
  var transformersModule = null;
1282
1440
  var pipeline = null;
1283
1441
  var initialized = false;
@@ -1288,8 +1446,8 @@ async function getTransformers() {
1288
1446
  return transformersModule;
1289
1447
  }
1290
1448
  function isModelCached() {
1291
- const modelCachePath = join3(MODELS_DIR, EMBEDDING_MODEL);
1292
- return existsSync4(modelCachePath);
1449
+ const modelCachePath = join4(MODELS_DIR, EMBEDDING_MODEL);
1450
+ return existsSync5(modelCachePath);
1293
1451
  }
1294
1452
  async function initializeEmbeddings(onProgress) {
1295
1453
  if (initialized && pipeline) {
@@ -1353,16 +1511,16 @@ function createEmbeddingText(title, summary, messages) {
1353
1511
  }
1354
1512
 
1355
1513
  // src/ui/hooks/useSessions.ts
1356
- import { existsSync as existsSync5 } from "fs";
1514
+ import { existsSync as existsSync6 } from "fs";
1357
1515
  function isRecoverable(session) {
1358
1516
  if (!session.sourceFile) return false;
1359
- if (existsSync5(session.sourceFile)) return true;
1517
+ if (existsSync6(session.sourceFile)) return true;
1360
1518
  if (hasBackup(session.sourceFile)) return true;
1361
1519
  return false;
1362
1520
  }
1363
1521
  function ensureBackedUp(session) {
1364
1522
  if (!session.sourceFile) return;
1365
- if (existsSync5(session.sourceFile) && !hasBackup(session.sourceFile)) {
1523
+ if (existsSync6(session.sourceFile) && !hasBackup(session.sourceFile)) {
1366
1524
  backupSessionFile(session.sourceFile);
1367
1525
  }
1368
1526
  }
@@ -1570,7 +1728,7 @@ function useSessions(options = {}) {
1570
1728
  }
1571
1729
 
1572
1730
  // src/ui/App.tsx
1573
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1731
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1574
1732
  var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1575
1733
  const { exit } = useApp();
1576
1734
  const {
@@ -1595,6 +1753,12 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1595
1753
  const [currentTab, setCurrentTab] = useState2("global");
1596
1754
  const [selectedProjectPath, setSelectedProjectPath] = useState2(null);
1597
1755
  const [renderKey, setRenderKey] = useState2(0);
1756
+ const [dbSizeAlertShown, setDbSizeAlertShown] = useState2(false);
1757
+ const [purgePreview, setPurgePreview] = useState2(null);
1758
+ const [dbSize, setDbSize] = useState2(0);
1759
+ const [storageStats, setStorageStats] = useState2(null);
1760
+ const [purgeOptions, setPurgeOptions] = useState2([]);
1761
+ const [selectedPurgeOption, setSelectedPurgeOption] = useState2(null);
1598
1762
  const getCurrentView = useCallback2(() => {
1599
1763
  if (currentTab === "projects") {
1600
1764
  return selectedProjectPath ? "project-sessions" : "projects";
@@ -1606,6 +1770,20 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1606
1770
  useEffect2(() => {
1607
1771
  setSelectedIndex(0);
1608
1772
  }, [currentTab, selectedProjectPath]);
1773
+ useEffect2(() => {
1774
+ if (!loading && !dbSizeAlertShown) {
1775
+ const size = getTotalStorageSize();
1776
+ if (size >= DB_SIZE_ALERT_THRESHOLD) {
1777
+ const preview = getPurgePreview(DEFAULT_PURGE_DAYS);
1778
+ if (preview.sessionsToDelete > 0) {
1779
+ setDbSize(size);
1780
+ setPurgePreview(preview);
1781
+ setMode("db-size-alert");
1782
+ }
1783
+ }
1784
+ setDbSizeAlertShown(true);
1785
+ }
1786
+ }, [loading, dbSizeAlertShown]);
1609
1787
  useEffect2(() => {
1610
1788
  const maxIndex = currentView === "projects" ? projects.length - 1 : currentView === "project-sessions" ? projectSessions.length - 1 : sessions.length - 1;
1611
1789
  if (selectedIndex > maxIndex) {
@@ -1639,7 +1817,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1639
1817
  const session = currentSessions2[selectedIndex];
1640
1818
  if (!session) return;
1641
1819
  if (session.sourceFile) {
1642
- if (!existsSync6(session.sourceFile)) {
1820
+ if (!existsSync7(session.sourceFile)) {
1643
1821
  if (hasBackup(session.sourceFile)) {
1644
1822
  const restored = restoreFromBackup(session.sourceFile);
1645
1823
  if (restored) {
@@ -1657,7 +1835,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1657
1835
  const claudeSessionId = filename.replace(".jsonl", "");
1658
1836
  let projectPath = session.projectPath;
1659
1837
  if (!projectPath) {
1660
- const projectDirName = basename5(dirname3(session.sourceFile));
1838
+ const projectDirName = basename5(dirname4(session.sourceFile));
1661
1839
  if (projectDirName.startsWith("-")) {
1662
1840
  const segments = projectDirName.substring(1).split("-");
1663
1841
  let currentPath = "";
@@ -1667,7 +1845,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1667
1845
  for (let i = remainingSegments.length; i > 0; i--) {
1668
1846
  const testSegment = remainingSegments.slice(0, i).join("-");
1669
1847
  const testPath = currentPath + "/" + testSegment;
1670
- if (existsSync6(testPath)) {
1848
+ if (existsSync7(testPath)) {
1671
1849
  currentPath = testPath;
1672
1850
  remainingSegments = remainingSegments.slice(i);
1673
1851
  found = true;
@@ -1679,7 +1857,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1679
1857
  break;
1680
1858
  }
1681
1859
  }
1682
- if (currentPath && existsSync6(currentPath)) {
1860
+ if (currentPath && existsSync7(currentPath)) {
1683
1861
  projectPath = currentPath;
1684
1862
  }
1685
1863
  }
@@ -1736,7 +1914,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1736
1914
  setMode("list");
1737
1915
  }, [getCurrentSessions, selectedIndex, refresh]);
1738
1916
  useInput((input, key) => {
1739
- if (input === "q" && mode !== "search" && mode !== "rename" && mode !== "sort-project") {
1917
+ if (input === "q" && mode !== "search" && mode !== "rename" && mode !== "sort-project" && mode !== "db-size-alert" && mode !== "confirm-purge" && mode !== "storage" && mode !== "storage-confirm") {
1740
1918
  exit();
1741
1919
  return;
1742
1920
  }
@@ -1794,10 +1972,93 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1794
1972
  }
1795
1973
  return;
1796
1974
  }
1975
+ if (mode === "db-size-alert") {
1976
+ if (input === "y" || input === "Y") {
1977
+ setMode("confirm-purge");
1978
+ } else {
1979
+ setMode("list");
1980
+ }
1981
+ return;
1982
+ }
1983
+ if (mode === "confirm-purge") {
1984
+ if (input === "y" || input === "Y") {
1985
+ const result = purgeOldSessions(DEFAULT_PURGE_DAYS);
1986
+ const parts = [];
1987
+ if (result.sessionsDeleted > 0) {
1988
+ parts.push(`${result.sessionsDeleted} session${result.sessionsDeleted !== 1 ? "s" : ""}`);
1989
+ }
1990
+ if (result.backupsDeleted > 0) {
1991
+ parts.push(`${result.backupsDeleted} backup${result.backupsDeleted !== 1 ? "s" : ""}`);
1992
+ }
1993
+ setStatusMessage(`Purged ${parts.join(" and ")}`);
1994
+ refresh();
1995
+ setMode("list");
1996
+ } else {
1997
+ setMode("list");
1998
+ }
1999
+ return;
2000
+ }
2001
+ if (mode === "storage") {
2002
+ if (key.escape || input === "q") {
2003
+ setMode("list");
2004
+ return;
2005
+ }
2006
+ const optionIndex = parseInt(input, 10) - 1;
2007
+ if (optionIndex >= 0 && optionIndex < purgeOptions.length) {
2008
+ const option = purgeOptions[optionIndex];
2009
+ if (option && option.preview.sessionsToDelete > 0) {
2010
+ setSelectedPurgeOption(optionIndex);
2011
+ setMode("storage-confirm");
2012
+ }
2013
+ }
2014
+ return;
2015
+ }
2016
+ if (mode === "storage-confirm") {
2017
+ if (input === "y" || input === "Y") {
2018
+ if (selectedPurgeOption !== null && purgeOptions[selectedPurgeOption]) {
2019
+ const days = purgeOptions[selectedPurgeOption].days;
2020
+ const result = purgeOldSessions(days);
2021
+ const parts = [];
2022
+ if (result.sessionsDeleted > 0) {
2023
+ parts.push(`${result.sessionsDeleted} session${result.sessionsDeleted !== 1 ? "s" : ""}`);
2024
+ }
2025
+ if (result.backupsDeleted > 0) {
2026
+ parts.push(`${result.backupsDeleted} backup${result.backupsDeleted !== 1 ? "s" : ""}`);
2027
+ }
2028
+ setStatusMessage(`Purged ${parts.join(" and ")}`);
2029
+ refresh();
2030
+ setSelectedPurgeOption(null);
2031
+ setMode("list");
2032
+ }
2033
+ } else {
2034
+ setSelectedPurgeOption(null);
2035
+ setMode("storage");
2036
+ }
2037
+ return;
2038
+ }
1797
2039
  if (input === "/") {
1798
2040
  setMode("search");
1799
2041
  return;
1800
2042
  }
2043
+ if (input === "P") {
2044
+ const dbStats = getStats();
2045
+ const dbSizeVal = getDatabaseSize();
2046
+ const backupsSizeVal = getBackupsDirSize();
2047
+ setStorageStats({
2048
+ dbSize: dbSizeVal,
2049
+ backupsSize: backupsSizeVal,
2050
+ totalSize: dbSizeVal + backupsSizeVal,
2051
+ sessionCount: dbStats.sessionCount,
2052
+ messageCount: dbStats.messageCount
2053
+ });
2054
+ setPurgeOptions([
2055
+ { days: 15, preview: getPurgePreview(15) },
2056
+ { days: 30, preview: getPurgePreview(30) },
2057
+ { days: 60, preview: getPurgePreview(60) }
2058
+ ]);
2059
+ setMode("storage");
2060
+ return;
2061
+ }
1801
2062
  if ((key.escape || key.backspace || key.delete) && currentView === "project-sessions") {
1802
2063
  handleBackToProjects();
1803
2064
  return;
@@ -1876,35 +2137,46 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1876
2137
  setSelectedIndex(0);
1877
2138
  }, []);
1878
2139
  if (loading && sessions.length === 0) {
1879
- return /* @__PURE__ */ jsxs6(Box6, { padding: 1, children: [
1880
- /* @__PURE__ */ jsx6(Spinner, { type: "dots" }),
1881
- /* @__PURE__ */ jsx6(Text6, { children: " Loading sessions..." })
2140
+ return /* @__PURE__ */ jsxs7(Box7, { padding: 1, children: [
2141
+ /* @__PURE__ */ jsx7(Spinner, { type: "dots" }),
2142
+ /* @__PURE__ */ jsx7(Text7, { children: " Loading sessions..." })
1882
2143
  ] });
1883
2144
  }
1884
2145
  const currentSessions = getCurrentSessions();
1885
2146
  const selectedSession = currentView !== "projects" ? currentSessions[selectedIndex] || null : null;
1886
2147
  const selectedProject = currentView === "projects" ? projects[selectedIndex] || null : null;
1887
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
1888
- /* @__PURE__ */ jsx6(Header, { embeddingsReady, projectFilter: selectedProjectPath }),
1889
- currentTab === "global" ? /* @__PURE__ */ jsx6(
2148
+ if ((mode === "storage" || mode === "storage-confirm") && storageStats) {
2149
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx7(
2150
+ StorageScreen,
2151
+ {
2152
+ stats: storageStats,
2153
+ purgeOptions,
2154
+ selectedOption: selectedPurgeOption,
2155
+ mode: mode === "storage-confirm" ? "confirm" : "view"
2156
+ }
2157
+ ) }, renderKey);
2158
+ }
2159
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
2160
+ /* @__PURE__ */ jsx7(Header, { embeddingsReady, projectFilter: selectedProjectPath }),
2161
+ currentTab === "global" ? /* @__PURE__ */ jsx7(
1890
2162
  SearchInput,
1891
2163
  {
1892
2164
  value: searchQuery,
1893
2165
  onChange: handleSearchChange,
1894
2166
  isFocused: mode === "search"
1895
2167
  }
1896
- ) : currentView === "project-sessions" && selectedProjectPath ? /* @__PURE__ */ jsxs6(Box6, { children: [
1897
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Projects \u2192 " }),
1898
- /* @__PURE__ */ jsx6(Text6, { color: "blue", children: basename5(selectedProjectPath) })
1899
- ] }) : /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Browse projects below" }) }),
1900
- currentView === "projects" ? /* @__PURE__ */ jsx6(
2168
+ ) : currentView === "project-sessions" && selectedProjectPath ? /* @__PURE__ */ jsxs7(Box7, { children: [
2169
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Projects \u2192 " }),
2170
+ /* @__PURE__ */ jsx7(Text7, { color: "blue", children: basename5(selectedProjectPath) })
2171
+ ] }) : /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Browse projects below" }) }),
2172
+ currentView === "projects" ? /* @__PURE__ */ jsx7(
1901
2173
  ProjectList,
1902
2174
  {
1903
2175
  projects,
1904
2176
  selectedIndex,
1905
2177
  onSelect: setSelectedIndex
1906
2178
  }
1907
- ) : /* @__PURE__ */ jsx6(
2179
+ ) : /* @__PURE__ */ jsx7(
1908
2180
  SessionList,
1909
2181
  {
1910
2182
  sessions: currentSessions,
@@ -1912,9 +2184,9 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1912
2184
  onSelect: setSelectedIndex
1913
2185
  }
1914
2186
  ),
1915
- selectedSession && /* @__PURE__ */ jsx6(Preview, { session: selectedSession }),
1916
- selectedProject && /* @__PURE__ */ jsxs6(
1917
- Box6,
2187
+ selectedSession && /* @__PURE__ */ jsx7(Preview, { session: selectedSession }),
2188
+ selectedProject && /* @__PURE__ */ jsxs7(
2189
+ Box7,
1918
2190
  {
1919
2191
  flexDirection: "column",
1920
2192
  borderStyle: "round",
@@ -1923,19 +2195,19 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1923
2195
  paddingY: 0,
1924
2196
  marginTop: 1,
1925
2197
  children: [
1926
- /* @__PURE__ */ jsxs6(Box6, { children: [
1927
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Project Preview" }),
1928
- selectedProject.sortOrder !== null && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2198
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2199
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Project Preview" }),
2200
+ selectedProject.sortOrder !== null && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1929
2201
  " (#",
1930
2202
  selectedProject.sortOrder + 1,
1931
2203
  ")"
1932
2204
  ] })
1933
2205
  ] }),
1934
- /* @__PURE__ */ jsxs6(Text6, { color: "blue", children: [
2206
+ /* @__PURE__ */ jsxs7(Text7, { color: "blue", children: [
1935
2207
  "\u{1F4C1} ",
1936
2208
  selectedProject.path
1937
2209
  ] }),
1938
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2210
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1939
2211
  selectedProject.sessionCount,
1940
2212
  " session",
1941
2213
  selectedProject.sessionCount !== 1 ? "s" : "",
@@ -1946,13 +2218,57 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1946
2218
  ]
1947
2219
  }
1948
2220
  ),
1949
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
2221
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: mode === "db-size-alert" ? /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2222
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", bold: true, children: [
2223
+ "\u26A0 Storage size: ",
2224
+ formatBytes(dbSize),
2225
+ " (exceeds 5GB)"
2226
+ ] }),
2227
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
2228
+ "Purge ",
2229
+ purgePreview?.sessionsToDelete,
2230
+ " sessions older than ",
2231
+ DEFAULT_PURGE_DAYS,
2232
+ " days? [y/n]"
2233
+ ] }),
2234
+ purgePreview && purgePreview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2235
+ " (",
2236
+ purgePreview.backupFilesToDelete,
2237
+ " backup files, ~",
2238
+ formatBytes(purgePreview.backupBytesToFree),
2239
+ ")"
2240
+ ] }),
2241
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(Starred sessions will be kept)" })
2242
+ ] }) : mode === "confirm-purge" ? /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2243
+ /* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "Confirm purge:" }),
2244
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2245
+ " ",
2246
+ purgePreview?.sessionsToDelete,
2247
+ " sessions (",
2248
+ purgePreview?.messagesInvolved,
2249
+ " messages)"
2250
+ ] }),
2251
+ purgePreview && purgePreview.backupFilesToDelete > 0 && /* @__PURE__ */ jsxs7(Text7, { children: [
2252
+ " ",
2253
+ purgePreview.backupFilesToDelete,
2254
+ " backup files (~",
2255
+ formatBytes(purgePreview.backupBytesToFree),
2256
+ ")"
2257
+ ] }),
2258
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2259
+ " Range: ",
2260
+ purgePreview?.oldestSessionDate,
2261
+ " to ",
2262
+ purgePreview?.newestSessionDate
2263
+ ] }),
2264
+ /* @__PURE__ */ jsx7(Text7, { color: "red", children: "This cannot be undone. Proceed? [y/n]" })
2265
+ ] }) : mode === "confirm-delete" ? /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1950
2266
  'Delete "',
1951
2267
  selectedSession?.customTitle || selectedSession?.title,
1952
2268
  '"? [y/n]'
1953
- ] }) : mode === "rename" ? /* @__PURE__ */ jsxs6(Box6, { children: [
1954
- /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: "Rename: " }),
1955
- /* @__PURE__ */ jsx6(
2269
+ ] }) : mode === "rename" ? /* @__PURE__ */ jsxs7(Box7, { children: [
2270
+ /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: "Rename: " }),
2271
+ /* @__PURE__ */ jsx7(
1956
2272
  TextInput2,
1957
2273
  {
1958
2274
  value: renameValue,
@@ -1960,11 +2276,11 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1960
2276
  placeholder: "Enter new name..."
1961
2277
  }
1962
2278
  ),
1963
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1964
- ] }) : mode === "sort-project" ? /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "Sort mode: [\u2191\u2193] Move project [Enter/Esc] Done" }) : statusMessage ? /* @__PURE__ */ jsx6(Text6, { color: "green", children: statusMessage }) : currentView === "projects" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Open [s] Sort [\u2190\u2192] Switch tabs [q] Quit" }) : currentView === "project-sessions" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [Esc] Back [r] Rename [d] Delete [q] Quit" }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [r] Rename [d] Delete [/] Search [q] Quit" }) }),
1965
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1966
- /* @__PURE__ */ jsx6(
1967
- Text6,
2279
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
2280
+ ] }) : mode === "sort-project" ? /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "Sort mode: [\u2191\u2193] Move project [Enter/Esc] Done" }) : statusMessage ? /* @__PURE__ */ jsx7(Text7, { color: "green", children: statusMessage }) : currentView === "projects" ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Open [s] Sort [P] Storage [\u2190\u2192] Tabs [q] Quit" }) : currentView === "project-sessions" ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [Esc] Back [r] Rename [d] Delete [P] Storage [q] Quit" }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [r] Rename [d] Delete [/] Search [P] Storage [q] Quit" }) }),
2281
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
2282
+ /* @__PURE__ */ jsx7(
2283
+ Text7,
1968
2284
  {
1969
2285
  backgroundColor: currentTab === "global" ? "green" : void 0,
1970
2286
  color: currentTab === "global" ? "white" : void 0,
@@ -1973,9 +2289,9 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1973
2289
  children: currentTab === "global" ? " Global " : "Global"
1974
2290
  }
1975
2291
  ),
1976
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
1977
- /* @__PURE__ */ jsx6(
1978
- Text6,
2292
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
2293
+ /* @__PURE__ */ jsx7(
2294
+ Text7,
1979
2295
  {
1980
2296
  backgroundColor: currentTab === "projects" ? "magenta" : void 0,
1981
2297
  color: currentTab === "projects" ? "white" : void 0,
@@ -1984,7 +2300,7 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1984
2300
  children: currentTab === "projects" ? " Projects " : "Projects"
1985
2301
  }
1986
2302
  ),
1987
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [\u2190\u2192] switch" })
2303
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " [\u2190\u2192] switch" })
1988
2304
  ] })
1989
2305
  ] }, renderKey);
1990
2306
  };
@@ -2284,7 +2600,7 @@ async function deleteCommand(id) {
2284
2600
 
2285
2601
  // src/commands/stats.ts
2286
2602
  import chalk6 from "chalk";
2287
- import { statSync as statSync2, existsSync as existsSync7 } from "fs";
2603
+ import { statSync as statSync3, existsSync as existsSync8 } from "fs";
2288
2604
  async function statsCommand() {
2289
2605
  console.log(chalk6.cyan("cmem Storage Statistics\n"));
2290
2606
  const stats = getStats();
@@ -2292,9 +2608,19 @@ async function statsCommand() {
2292
2608
  console.log(` Sessions: ${formatNumber(stats.sessionCount)}`);
2293
2609
  console.log(` Messages: ${formatNumber(stats.messageCount)}`);
2294
2610
  console.log(` Embeddings: ${formatNumber(stats.embeddingCount)}`);
2295
- if (existsSync7(DB_PATH)) {
2296
- const dbStats = statSync2(DB_PATH);
2297
- console.log(` DB Size: ${formatBytes(dbStats.size)}`);
2611
+ let totalSize = 0;
2612
+ if (existsSync8(DB_PATH)) {
2613
+ const dbStats = statSync3(DB_PATH);
2614
+ console.log(` DB Size: ${formatBytes2(dbStats.size)}`);
2615
+ totalSize += dbStats.size;
2616
+ }
2617
+ const backupsSize = getBackupsDirSize();
2618
+ if (backupsSize > 0) {
2619
+ console.log(` Backups: ${formatBytes2(backupsSize)}`);
2620
+ totalSize += backupsSize;
2621
+ }
2622
+ if (totalSize > 0) {
2623
+ console.log(` Total: ${formatBytes2(totalSize)}`);
2298
2624
  }
2299
2625
  console.log(` Location: ${CMEM_DIR}`);
2300
2626
  console.log("");
@@ -2319,8 +2645,8 @@ async function statsCommand() {
2319
2645
  // src/commands/watch.ts
2320
2646
  import chalk7 from "chalk";
2321
2647
  import chokidar from "chokidar";
2322
- import { statSync as statSync3, existsSync as existsSync8, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
2323
- import { join as join4, dirname as dirname4, basename as basename6 } from "path";
2648
+ import { statSync as statSync4, existsSync as existsSync9, readFileSync as readFileSync2, readdirSync as readdirSync3 } from "fs";
2649
+ import { join as join5, dirname as dirname5, basename as basename6 } from "path";
2324
2650
  var spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2325
2651
  var Spinner2 = class {
2326
2652
  interval = null;
@@ -2454,13 +2780,13 @@ async function watchCommand(options) {
2454
2780
  }
2455
2781
  function findAllSessionFiles(dir) {
2456
2782
  const files = [];
2457
- if (!existsSync8(dir)) return files;
2783
+ if (!existsSync9(dir)) return files;
2458
2784
  function scanDir(currentDir, depth = 0) {
2459
2785
  if (depth > 10) return;
2460
2786
  try {
2461
- const entries = readdirSync2(currentDir, { withFileTypes: true });
2787
+ const entries = readdirSync3(currentDir, { withFileTypes: true });
2462
2788
  for (const entry of entries) {
2463
- const fullPath = join4(currentDir, entry.name);
2789
+ const fullPath = join5(currentDir, entry.name);
2464
2790
  if (entry.isDirectory()) {
2465
2791
  if (entry.name === "subagents") continue;
2466
2792
  scanDir(fullPath, depth + 1);
@@ -2492,11 +2818,11 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
2492
2818
  if (verbose) console.log(chalk7.dim(` Skipping empty session: ${filePath}`));
2493
2819
  return false;
2494
2820
  }
2495
- const stats = statSync3(filePath);
2821
+ const stats = statSync4(filePath);
2496
2822
  const fileMtime = stats.mtime.toISOString();
2497
2823
  const existingSession = getSessionBySourceFile(filePath);
2498
2824
  const sessionId_from_file = basename6(filePath, ".jsonl");
2499
- const agentMessages = loadSubagentMessages2(dirname4(filePath), sessionId_from_file);
2825
+ const agentMessages = loadSubagentMessages2(dirname5(filePath), sessionId_from_file);
2500
2826
  const allMessages = [...messages, ...agentMessages];
2501
2827
  const contentLength = allMessages.reduce((sum, m) => sum + m.content.length, 0);
2502
2828
  const firstUserMsg = messages.find((m) => m.role === "user");
@@ -2576,13 +2902,13 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
2576
2902
  }
2577
2903
  }
2578
2904
  function loadSubagentMessages2(projectDirPath, parentSessionId) {
2579
- const subagentsDir = join4(projectDirPath, parentSessionId, "subagents");
2580
- if (!existsSync8(subagentsDir)) return [];
2905
+ const subagentsDir = join5(projectDirPath, parentSessionId, "subagents");
2906
+ if (!existsSync9(subagentsDir)) return [];
2581
2907
  const messages = [];
2582
2908
  try {
2583
- const agentFiles = readdirSync2(subagentsDir).filter((f) => f.endsWith(".jsonl"));
2909
+ const agentFiles = readdirSync3(subagentsDir).filter((f) => f.endsWith(".jsonl"));
2584
2910
  for (const agentFile of agentFiles) {
2585
- const agentPath = join4(subagentsDir, agentFile);
2911
+ const agentPath = join5(subagentsDir, agentFile);
2586
2912
  const agentMessages = parseSessionFile(agentPath);
2587
2913
  messages.push(...agentMessages);
2588
2914
  }
@@ -2591,9 +2917,9 @@ function loadSubagentMessages2(projectDirPath, parentSessionId) {
2591
2917
  return messages;
2592
2918
  }
2593
2919
  function getProjectPathFromIndex(filePath, sessionId) {
2594
- const projectDir = dirname4(filePath);
2595
- const indexPath = join4(projectDir, "sessions-index.json");
2596
- if (!existsSync8(indexPath)) return null;
2920
+ const projectDir = dirname5(filePath);
2921
+ const indexPath = join5(projectDir, "sessions-index.json");
2922
+ if (!existsSync9(indexPath)) return null;
2597
2923
  try {
2598
2924
  const content = readFileSync2(indexPath, "utf-8");
2599
2925
  const index = JSON.parse(content);
@@ -2745,45 +3071,597 @@ async function runClaudePrompt(prompt, options = {}) {
2745
3071
  }
2746
3072
  }
2747
3073
  }
2748
- const content = textChunks.join("");
2749
- if (errorContent && !content) {
2750
- resolve({
2751
- success: false,
2752
- content: "",
2753
- error: errorContent
2754
- });
3074
+ const content = textChunks.join("");
3075
+ if (errorContent && !content) {
3076
+ resolve({
3077
+ success: false,
3078
+ content: "",
3079
+ error: errorContent
3080
+ });
3081
+ } else {
3082
+ resolve({
3083
+ success: code === 0 && content.length > 0,
3084
+ content,
3085
+ error: errorContent || void 0,
3086
+ inputTokens: finalResult?.inputTokens,
3087
+ outputTokens: finalResult?.outputTokens,
3088
+ costUsd: finalResult?.costUsd,
3089
+ durationMs: finalResult?.durationMs
3090
+ });
3091
+ }
3092
+ });
3093
+ childProcess.on("error", (err) => {
3094
+ resolve({
3095
+ success: false,
3096
+ content: "",
3097
+ error: err.message
3098
+ });
3099
+ });
3100
+ setTimeout(() => {
3101
+ childProcess.kill("SIGTERM");
3102
+ resolve({
3103
+ success: false,
3104
+ content: textChunks.join(""),
3105
+ error: "Request timed out after 60 seconds"
3106
+ });
3107
+ }, 6e4);
3108
+ });
3109
+ }
3110
+ function isClaudeCliAvailable() {
3111
+ return getClaudePath() !== null;
3112
+ }
3113
+
3114
+ // src/db/lessons.ts
3115
+ import { randomUUID as randomUUID2 } from "crypto";
3116
+ function mapLessonRow(row) {
3117
+ return {
3118
+ id: row.id,
3119
+ projectPath: row.project_path,
3120
+ category: row.category,
3121
+ title: row.title,
3122
+ triggerContext: row.trigger_context,
3123
+ insight: row.insight,
3124
+ reasoning: row.reasoning ?? void 0,
3125
+ confidence: row.confidence,
3126
+ timesApplied: row.times_applied,
3127
+ timesValidated: row.times_validated,
3128
+ timesRejected: row.times_rejected,
3129
+ sourceSessionId: row.source_session_id ?? void 0,
3130
+ sourceType: row.source_type,
3131
+ archived: row.archived === 1,
3132
+ createdAt: row.created_at,
3133
+ updatedAt: row.updated_at,
3134
+ lastAppliedAt: row.last_applied_at ?? void 0
3135
+ };
3136
+ }
3137
+ function createLesson(input) {
3138
+ const db2 = getDatabase();
3139
+ const id = randomUUID2();
3140
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3141
+ db2.prepare(`
3142
+ INSERT INTO lessons (
3143
+ id, project_path, category, title, trigger_context, insight,
3144
+ reasoning, confidence, source_session_id, source_type,
3145
+ created_at, updated_at
3146
+ )
3147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3148
+ `).run(
3149
+ id,
3150
+ input.projectPath,
3151
+ input.category,
3152
+ input.title,
3153
+ input.triggerContext,
3154
+ input.insight,
3155
+ input.reasoning ?? null,
3156
+ input.confidence ?? 0.5,
3157
+ input.sourceSessionId ?? null,
3158
+ input.sourceType ?? "synthesized",
3159
+ now,
3160
+ now
3161
+ );
3162
+ return getLesson(id);
3163
+ }
3164
+ function updateLesson(id, updates) {
3165
+ const db2 = getDatabase();
3166
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3167
+ const fields = ["updated_at = ?"];
3168
+ const values = [now];
3169
+ if (updates.category !== void 0) {
3170
+ fields.push("category = ?");
3171
+ values.push(updates.category);
3172
+ }
3173
+ if (updates.title !== void 0) {
3174
+ fields.push("title = ?");
3175
+ values.push(updates.title);
3176
+ }
3177
+ if (updates.triggerContext !== void 0) {
3178
+ fields.push("trigger_context = ?");
3179
+ values.push(updates.triggerContext);
3180
+ }
3181
+ if (updates.insight !== void 0) {
3182
+ fields.push("insight = ?");
3183
+ values.push(updates.insight);
3184
+ }
3185
+ if (updates.reasoning !== void 0) {
3186
+ fields.push("reasoning = ?");
3187
+ values.push(updates.reasoning);
3188
+ }
3189
+ if (updates.confidence !== void 0) {
3190
+ fields.push("confidence = ?");
3191
+ values.push(updates.confidence);
3192
+ }
3193
+ if (updates.archived !== void 0) {
3194
+ fields.push("archived = ?");
3195
+ values.push(updates.archived ? 1 : 0);
3196
+ }
3197
+ values.push(id);
3198
+ db2.prepare(`
3199
+ UPDATE lessons SET ${fields.join(", ")} WHERE id = ?
3200
+ `).run(...values);
3201
+ return getLesson(id);
3202
+ }
3203
+ function deleteLesson(id) {
3204
+ const db2 = getDatabase();
3205
+ const transaction = db2.transaction(() => {
3206
+ db2.prepare("DELETE FROM lesson_embeddings WHERE lesson_id = ?").run(id);
3207
+ db2.prepare("DELETE FROM lesson_feedback WHERE lesson_id = ?").run(id);
3208
+ const result = db2.prepare("DELETE FROM lessons WHERE id = ?").run(id);
3209
+ return result.changes > 0;
3210
+ });
3211
+ return transaction();
3212
+ }
3213
+ function archiveLesson(id) {
3214
+ const db2 = getDatabase();
3215
+ db2.prepare(`
3216
+ UPDATE lessons SET archived = 1, updated_at = ? WHERE id = ?
3217
+ `).run((/* @__PURE__ */ new Date()).toISOString(), id);
3218
+ }
3219
+ function unarchiveLesson(id) {
3220
+ const db2 = getDatabase();
3221
+ db2.prepare(`
3222
+ UPDATE lessons SET archived = 0, updated_at = ? WHERE id = ?
3223
+ `).run((/* @__PURE__ */ new Date()).toISOString(), id);
3224
+ }
3225
+ function getLesson(id) {
3226
+ const db2 = getDatabase();
3227
+ const row = db2.prepare(`
3228
+ SELECT * FROM lessons WHERE id = ?
3229
+ `).get(id);
3230
+ return row ? mapLessonRow(row) : null;
3231
+ }
3232
+ function getLessonsByProject(projectPath, options = {}) {
3233
+ const db2 = getDatabase();
3234
+ let query = "SELECT * FROM lessons WHERE project_path = ?";
3235
+ const params = [projectPath];
3236
+ if (options.category) {
3237
+ query += " AND category = ?";
3238
+ params.push(options.category);
3239
+ }
3240
+ if (options.archived !== void 0) {
3241
+ query += " AND archived = ?";
3242
+ params.push(options.archived ? 1 : 0);
3243
+ } else {
3244
+ query += " AND archived = 0";
3245
+ }
3246
+ if (options.minConfidence !== void 0) {
3247
+ query += " AND confidence >= ?";
3248
+ params.push(options.minConfidence);
3249
+ }
3250
+ query += " ORDER BY confidence DESC, times_applied DESC";
3251
+ if (options.limit) {
3252
+ query += " LIMIT ?";
3253
+ params.push(options.limit);
3254
+ }
3255
+ const rows = db2.prepare(query).all(...params);
3256
+ return rows.map(mapLessonRow);
3257
+ }
3258
+ function getCoreLessons(projectPath, limit = 3) {
3259
+ const db2 = getDatabase();
3260
+ const rows = db2.prepare(`
3261
+ SELECT * FROM lessons
3262
+ WHERE project_path = ?
3263
+ AND archived = 0
3264
+ AND confidence >= 0.7
3265
+ ORDER BY
3266
+ times_validated DESC,
3267
+ confidence DESC,
3268
+ times_applied DESC
3269
+ LIMIT ?
3270
+ `).all(projectPath, limit);
3271
+ return rows.map(mapLessonRow);
3272
+ }
3273
+ function getAllLessons(options = {}) {
3274
+ const db2 = getDatabase();
3275
+ let query = "SELECT * FROM lessons WHERE 1=1";
3276
+ const params = [];
3277
+ if (options.category) {
3278
+ query += " AND category = ?";
3279
+ params.push(options.category);
3280
+ }
3281
+ if (options.archived !== void 0) {
3282
+ query += " AND archived = ?";
3283
+ params.push(options.archived ? 1 : 0);
3284
+ }
3285
+ if (options.minConfidence !== void 0) {
3286
+ query += " AND confidence >= ?";
3287
+ params.push(options.minConfidence);
3288
+ }
3289
+ query += " ORDER BY updated_at DESC";
3290
+ if (options.limit) {
3291
+ query += " LIMIT ?";
3292
+ params.push(options.limit);
3293
+ }
3294
+ const rows = db2.prepare(query).all(...params);
3295
+ return rows.map(mapLessonRow);
3296
+ }
3297
+ function recordLessonApplication(id) {
3298
+ const db2 = getDatabase();
3299
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3300
+ db2.prepare(`
3301
+ UPDATE lessons
3302
+ SET times_applied = times_applied + 1,
3303
+ last_applied_at = ?,
3304
+ updated_at = ?
3305
+ WHERE id = ?
3306
+ `).run(now, now, id);
3307
+ }
3308
+ function recordLessonValidation(id, sessionId, comment) {
3309
+ const db2 = getDatabase();
3310
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3311
+ const transaction = db2.transaction(() => {
3312
+ db2.prepare(`
3313
+ UPDATE lessons
3314
+ SET times_validated = times_validated + 1, updated_at = ?
3315
+ WHERE id = ?
3316
+ `).run(now, id);
3317
+ db2.prepare(`
3318
+ INSERT INTO lesson_feedback (lesson_id, session_id, feedback_type, comment)
3319
+ VALUES (?, ?, 'validated', ?)
3320
+ `).run(id, sessionId ?? null, comment ?? null);
3321
+ });
3322
+ transaction();
3323
+ }
3324
+ function recordLessonRejection(id, sessionId, comment) {
3325
+ const db2 = getDatabase();
3326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3327
+ const transaction = db2.transaction(() => {
3328
+ db2.prepare(`
3329
+ UPDATE lessons
3330
+ SET times_rejected = times_rejected + 1, updated_at = ?
3331
+ WHERE id = ?
3332
+ `).run(now, id);
3333
+ db2.prepare(`
3334
+ INSERT INTO lesson_feedback (lesson_id, session_id, feedback_type, comment)
3335
+ VALUES (?, ?, 'rejected', ?)
3336
+ `).run(id, sessionId ?? null, comment ?? null);
3337
+ });
3338
+ transaction();
3339
+ }
3340
+ function storeLessonEmbedding(lessonId, embedding) {
3341
+ const db2 = getDatabase();
3342
+ db2.prepare("DELETE FROM lesson_embeddings WHERE lesson_id = ?").run(lessonId);
3343
+ db2.prepare(`
3344
+ INSERT INTO lesson_embeddings (lesson_id, embedding)
3345
+ VALUES (?, ?)
3346
+ `).run(lessonId, JSON.stringify(embedding));
3347
+ }
3348
+ function searchLessonsByEmbedding(embedding, projectPath, limit = 5) {
3349
+ return searchLessonsByEmbeddingWithDistance(embedding, projectPath, limit).map((r) => r.lesson);
3350
+ }
3351
+ function searchLessonsByEmbeddingWithDistance(embedding, projectPath, limit = 5) {
3352
+ const db2 = getDatabase();
3353
+ const rows = db2.prepare(`
3354
+ SELECT
3355
+ l.*,
3356
+ le.distance
3357
+ FROM lesson_embeddings le
3358
+ JOIN lessons l ON l.id = le.lesson_id
3359
+ WHERE l.project_path = ?
3360
+ AND l.archived = 0
3361
+ AND le.embedding MATCH ?
3362
+ ORDER BY le.distance ASC
3363
+ LIMIT ?
3364
+ `).all(projectPath, JSON.stringify(embedding), limit);
3365
+ return rows.map((row) => ({
3366
+ lesson: mapLessonRow(row),
3367
+ distance: row.distance
3368
+ }));
3369
+ }
3370
+ function getPendingSynthesis(limit = 5) {
3371
+ const db2 = getDatabase();
3372
+ const rows = db2.prepare(`
3373
+ SELECT
3374
+ id,
3375
+ session_id as sessionId,
3376
+ project_path as projectPath,
3377
+ queued_at as queuedAt,
3378
+ status,
3379
+ processed_at as processedAt,
3380
+ lessons_created as lessonsCreated,
3381
+ error
3382
+ FROM synthesis_queue
3383
+ WHERE status = 'pending'
3384
+ ORDER BY queued_at ASC
3385
+ LIMIT ?
3386
+ `).all(limit);
3387
+ return rows;
3388
+ }
3389
+ function markSynthesisProcessing(id) {
3390
+ const db2 = getDatabase();
3391
+ db2.prepare(`
3392
+ UPDATE synthesis_queue SET status = 'processing' WHERE id = ?
3393
+ `).run(id);
3394
+ }
3395
+ function markSynthesisComplete(id, lessonsCreated) {
3396
+ const db2 = getDatabase();
3397
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3398
+ db2.prepare(`
3399
+ UPDATE synthesis_queue
3400
+ SET status = 'completed', processed_at = ?, lessons_created = ?
3401
+ WHERE id = ?
3402
+ `).run(now, lessonsCreated, id);
3403
+ }
3404
+ function markSynthesisFailed(id, error) {
3405
+ const db2 = getDatabase();
3406
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3407
+ db2.prepare(`
3408
+ UPDATE synthesis_queue
3409
+ SET status = 'failed', processed_at = ?, error = ?
3410
+ WHERE id = ?
3411
+ `).run(now, error, id);
3412
+ }
3413
+ function getSynthesisStats() {
3414
+ const db2 = getDatabase();
3415
+ const pending = db2.prepare(`
3416
+ SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'pending'
3417
+ `).get().count;
3418
+ const processing = db2.prepare(`
3419
+ SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'processing'
3420
+ `).get().count;
3421
+ const completed = db2.prepare(`
3422
+ SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'completed'
3423
+ `).get().count;
3424
+ const failed = db2.prepare(`
3425
+ SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'failed'
3426
+ `).get().count;
3427
+ const totalLessonsCreated = db2.prepare(`
3428
+ SELECT COALESCE(SUM(lessons_created), 0) as total FROM synthesis_queue WHERE status = 'completed'
3429
+ `).get().total;
3430
+ return { pending, processing, completed, failed, totalLessonsCreated };
3431
+ }
3432
+ function getLessonStats() {
3433
+ const db2 = getDatabase();
3434
+ const totalLessons = db2.prepare(`
3435
+ SELECT COUNT(*) as count FROM lessons
3436
+ `).get().count;
3437
+ const activeLessons = db2.prepare(`
3438
+ SELECT COUNT(*) as count FROM lessons WHERE archived = 0
3439
+ `).get().count;
3440
+ const archivedLessons = totalLessons - activeLessons;
3441
+ const categoryRows = db2.prepare(`
3442
+ SELECT category, COUNT(*) as count
3443
+ FROM lessons
3444
+ WHERE archived = 0
3445
+ GROUP BY category
3446
+ `).all();
3447
+ const byCategory = {
3448
+ architecture_decision: 0,
3449
+ anti_pattern: 0,
3450
+ bug_pattern: 0,
3451
+ project_convention: 0,
3452
+ dependency_knowledge: 0,
3453
+ domain_knowledge: 0,
3454
+ workflow: 0,
3455
+ other: 0
3456
+ };
3457
+ for (const row of categoryRows) {
3458
+ byCategory[row.category] = row.count;
3459
+ }
3460
+ const avgConfidence = db2.prepare(`
3461
+ SELECT COALESCE(AVG(confidence), 0) as avg FROM lessons WHERE archived = 0
3462
+ `).get().avg;
3463
+ return {
3464
+ totalLessons,
3465
+ activeLessons,
3466
+ archivedLessons,
3467
+ byCategory,
3468
+ avgConfidence
3469
+ };
3470
+ }
3471
+ function getQueueStatus() {
3472
+ return getSynthesisStats();
3473
+ }
3474
+ function cleanupOldInjections(daysOld = 7) {
3475
+ const db2 = getDatabase();
3476
+ const cutoff = /* @__PURE__ */ new Date();
3477
+ cutoff.setDate(cutoff.getDate() - daysOld);
3478
+ const result = db2.prepare(`
3479
+ DELETE FROM session_injections WHERE injected_at < ?
3480
+ `).run(cutoff.toISOString());
3481
+ return result.changes;
3482
+ }
3483
+ function getDistinctProjects() {
3484
+ const db2 = getDatabase();
3485
+ const rows = db2.prepare(`
3486
+ SELECT DISTINCT project_path
3487
+ FROM lessons
3488
+ WHERE project_path IS NOT NULL AND project_path != ''
3489
+ ORDER BY project_path ASC
3490
+ `).all();
3491
+ return rows.map((row) => row.project_path);
3492
+ }
3493
+
3494
+ // src/learning/LessonManager.ts
3495
+ var VALIDATION_BOOST = 0.1;
3496
+ var REJECTION_PENALTY = 0.2;
3497
+ var MIN_CONFIDENCE = 0;
3498
+ var MAX_CONFIDENCE = 1;
3499
+ var AUTO_ARCHIVE_THRESHOLD = 0.1;
3500
+ var DECAY_RATE = 0.05;
3501
+ var LessonManager = class {
3502
+ /**
3503
+ * Create a new lesson and generate its embedding
3504
+ */
3505
+ async create(input) {
3506
+ const lesson = createLesson(input);
3507
+ try {
3508
+ const embeddingText = this.buildEmbeddingText(lesson);
3509
+ const embedding = await getEmbedding(embeddingText);
3510
+ storeLessonEmbedding(lesson.id, embedding);
3511
+ } catch {
3512
+ }
3513
+ return lesson;
3514
+ }
3515
+ /**
3516
+ * Update a lesson
3517
+ */
3518
+ update(id, updates) {
3519
+ return updateLesson(id, updates);
3520
+ }
3521
+ /**
3522
+ * Update a lesson and regenerate its embedding
3523
+ */
3524
+ async updateWithEmbedding(id, updates) {
3525
+ const lesson = updateLesson(id, updates);
3526
+ if (lesson) {
3527
+ if (updates.title || updates.triggerContext || updates.insight) {
3528
+ try {
3529
+ const embeddingText = this.buildEmbeddingText(lesson);
3530
+ const embedding = await getEmbedding(embeddingText);
3531
+ storeLessonEmbedding(lesson.id, embedding);
3532
+ } catch {
3533
+ }
3534
+ }
3535
+ }
3536
+ return lesson;
3537
+ }
3538
+ /**
3539
+ * Delete a lesson permanently
3540
+ */
3541
+ delete(id) {
3542
+ return deleteLesson(id);
3543
+ }
3544
+ /**
3545
+ * Archive a lesson (soft delete)
3546
+ */
3547
+ archive(id) {
3548
+ archiveLesson(id);
3549
+ }
3550
+ /**
3551
+ * Unarchive a lesson
3552
+ */
3553
+ unarchive(id) {
3554
+ unarchiveLesson(id);
3555
+ }
3556
+ /**
3557
+ * Get a lesson by ID
3558
+ */
3559
+ get(id) {
3560
+ return getLesson(id);
3561
+ }
3562
+ /**
3563
+ * Get lessons for a project
3564
+ */
3565
+ getByProject(projectPath, options) {
3566
+ return getLessonsByProject(projectPath, options);
3567
+ }
3568
+ /**
3569
+ * Get core lessons (high confidence, well-validated)
3570
+ */
3571
+ getCore(projectPath, limit) {
3572
+ return getCoreLessons(projectPath, limit);
3573
+ }
3574
+ /**
3575
+ * Get all lessons
3576
+ */
3577
+ getAll(options) {
3578
+ return getAllLessons(options);
3579
+ }
3580
+ /**
3581
+ * Record that a lesson was applied (shown to user)
3582
+ */
3583
+ recordApplication(id) {
3584
+ recordLessonApplication(id);
3585
+ }
3586
+ /**
3587
+ * Record validation feedback - boosts confidence
3588
+ */
3589
+ recordValidation(id, sessionId, comment) {
3590
+ recordLessonValidation(id, sessionId, comment);
3591
+ const lesson = getLesson(id);
3592
+ if (lesson) {
3593
+ const newConfidence = Math.min(MAX_CONFIDENCE, lesson.confidence + VALIDATION_BOOST);
3594
+ updateLesson(id, { confidence: newConfidence });
3595
+ }
3596
+ }
3597
+ /**
3598
+ * Record rejection feedback - reduces confidence
3599
+ * Auto-archives if confidence drops too low
3600
+ */
3601
+ recordRejection(id, sessionId, comment) {
3602
+ recordLessonRejection(id, sessionId, comment);
3603
+ const lesson = getLesson(id);
3604
+ if (lesson) {
3605
+ const newConfidence = Math.max(MIN_CONFIDENCE, lesson.confidence - REJECTION_PENALTY);
3606
+ if (newConfidence < AUTO_ARCHIVE_THRESHOLD) {
3607
+ archiveLesson(id);
2755
3608
  } else {
2756
- resolve({
2757
- success: code === 0 && content.length > 0,
2758
- content,
2759
- error: errorContent || void 0,
2760
- inputTokens: finalResult?.inputTokens,
2761
- outputTokens: finalResult?.outputTokens,
2762
- costUsd: finalResult?.costUsd,
2763
- durationMs: finalResult?.durationMs
2764
- });
3609
+ updateLesson(id, { confidence: newConfidence });
2765
3610
  }
2766
- });
2767
- childProcess.on("error", (err) => {
2768
- resolve({
2769
- success: false,
2770
- content: "",
2771
- error: err.message
2772
- });
2773
- });
2774
- setTimeout(() => {
2775
- childProcess.kill("SIGTERM");
2776
- resolve({
2777
- success: false,
2778
- content: textChunks.join(""),
2779
- error: "Request timed out after 60 seconds"
2780
- });
2781
- }, 6e4);
2782
- });
2783
- }
2784
- function isClaudeCliAvailable() {
2785
- return getClaudePath() !== null;
2786
- }
3611
+ }
3612
+ }
3613
+ /**
3614
+ * Decay confidence of unused lessons
3615
+ * Should be run periodically (e.g., weekly)
3616
+ */
3617
+ decayUnusedLessons(daysThreshold = 30) {
3618
+ const lessons = getAllLessons({ archived: false });
3619
+ const cutoffDate = /* @__PURE__ */ new Date();
3620
+ cutoffDate.setDate(cutoffDate.getDate() - daysThreshold);
3621
+ let decayedCount = 0;
3622
+ for (const lesson of lessons) {
3623
+ const lastUsed = lesson.lastAppliedAt ? new Date(lesson.lastAppliedAt) : new Date(lesson.createdAt);
3624
+ if (lastUsed < cutoffDate) {
3625
+ const newConfidence = Math.max(MIN_CONFIDENCE, lesson.confidence - DECAY_RATE);
3626
+ if (newConfidence < AUTO_ARCHIVE_THRESHOLD) {
3627
+ archiveLesson(lesson.id);
3628
+ } else {
3629
+ updateLesson(lesson.id, { confidence: newConfidence });
3630
+ }
3631
+ decayedCount++;
3632
+ }
3633
+ }
3634
+ return decayedCount;
3635
+ }
3636
+ /**
3637
+ * Build text for embedding generation
3638
+ */
3639
+ buildEmbeddingText(lesson) {
3640
+ const parts = [
3641
+ `Title: ${lesson.title}`,
3642
+ `Category: ${lesson.category}`,
3643
+ `When to apply: ${lesson.triggerContext}`,
3644
+ `Insight: ${lesson.insight}`
3645
+ ];
3646
+ if (lesson.reasoning) {
3647
+ parts.push(`Reasoning: ${lesson.reasoning}`);
3648
+ }
3649
+ return parts.join("\n\n");
3650
+ }
3651
+ };
3652
+ var lessonManager = new LessonManager();
3653
+
3654
+ // src/learning/types.ts
3655
+ var VALID_CATEGORIES = [
3656
+ "architecture_decision",
3657
+ "anti_pattern",
3658
+ "bug_pattern",
3659
+ "project_convention",
3660
+ "dependency_knowledge",
3661
+ "domain_knowledge",
3662
+ "workflow",
3663
+ "other"
3664
+ ];
2787
3665
 
2788
3666
  // src/mcp/server.ts
2789
3667
  function createMcpServer() {
@@ -2894,6 +3772,141 @@ function createMcpServer() {
2894
3772
  },
2895
3773
  required: ["query"]
2896
3774
  }
3775
+ },
3776
+ // ==================== LESSON TOOLS ====================
3777
+ {
3778
+ name: "search_lessons",
3779
+ description: "Search learned lessons for a project using semantic search. Returns lessons that match the query context.",
3780
+ inputSchema: {
3781
+ type: "object",
3782
+ properties: {
3783
+ query: {
3784
+ type: "string",
3785
+ description: "Search query describing what you want to find"
3786
+ },
3787
+ projectPath: {
3788
+ type: "string",
3789
+ description: "Project path to search lessons for (defaults to current directory)"
3790
+ },
3791
+ limit: {
3792
+ type: "number",
3793
+ description: "Maximum number of results (default: 5)"
3794
+ }
3795
+ },
3796
+ required: ["query"]
3797
+ }
3798
+ },
3799
+ {
3800
+ name: "get_lesson",
3801
+ description: "Get details of a specific learned lesson by ID.",
3802
+ inputSchema: {
3803
+ type: "object",
3804
+ properties: {
3805
+ lessonId: {
3806
+ type: "string",
3807
+ description: "The lesson ID to retrieve"
3808
+ }
3809
+ },
3810
+ required: ["lessonId"]
3811
+ }
3812
+ },
3813
+ {
3814
+ name: "save_lesson",
3815
+ description: "Manually save a new lesson. Use this when you learn something important about a project that should be remembered.",
3816
+ inputSchema: {
3817
+ type: "object",
3818
+ properties: {
3819
+ projectPath: {
3820
+ type: "string",
3821
+ description: "Project path this lesson applies to"
3822
+ },
3823
+ category: {
3824
+ type: "string",
3825
+ enum: VALID_CATEGORIES,
3826
+ description: "Category of the lesson"
3827
+ },
3828
+ title: {
3829
+ type: "string",
3830
+ description: "Short title (max 60 chars)"
3831
+ },
3832
+ triggerContext: {
3833
+ type: "string",
3834
+ description: "When this lesson should be surfaced"
3835
+ },
3836
+ insight: {
3837
+ type: "string",
3838
+ description: "The actual knowledge (specific, actionable)"
3839
+ },
3840
+ reasoning: {
3841
+ type: "string",
3842
+ description: "Why this is true (optional)"
3843
+ }
3844
+ },
3845
+ required: ["projectPath", "category", "title", "triggerContext", "insight"]
3846
+ }
3847
+ },
3848
+ {
3849
+ name: "validate_lesson",
3850
+ description: "Mark a lesson as helpful/correct. Boosts its confidence score.",
3851
+ inputSchema: {
3852
+ type: "object",
3853
+ properties: {
3854
+ lessonId: {
3855
+ type: "string",
3856
+ description: "The lesson ID to validate"
3857
+ },
3858
+ comment: {
3859
+ type: "string",
3860
+ description: "Optional comment about why it was helpful"
3861
+ }
3862
+ },
3863
+ required: ["lessonId"]
3864
+ }
3865
+ },
3866
+ {
3867
+ name: "reject_lesson",
3868
+ description: "Mark a lesson as unhelpful/incorrect. Reduces its confidence score.",
3869
+ inputSchema: {
3870
+ type: "object",
3871
+ properties: {
3872
+ lessonId: {
3873
+ type: "string",
3874
+ description: "The lesson ID to reject"
3875
+ },
3876
+ comment: {
3877
+ type: "string",
3878
+ description: "Optional comment about why it was wrong"
3879
+ }
3880
+ },
3881
+ required: ["lessonId"]
3882
+ }
3883
+ },
3884
+ {
3885
+ name: "list_lessons",
3886
+ description: "List all lessons for a project.",
3887
+ inputSchema: {
3888
+ type: "object",
3889
+ properties: {
3890
+ projectPath: {
3891
+ type: "string",
3892
+ description: "Project path to list lessons for"
3893
+ },
3894
+ category: {
3895
+ type: "string",
3896
+ enum: VALID_CATEGORIES,
3897
+ description: "Filter by category"
3898
+ },
3899
+ includeArchived: {
3900
+ type: "boolean",
3901
+ description: "Include archived lessons (default: false)"
3902
+ },
3903
+ limit: {
3904
+ type: "number",
3905
+ description: "Maximum number of results (default: 20)"
3906
+ }
3907
+ },
3908
+ required: ["projectPath"]
3909
+ }
2897
3910
  }
2898
3911
  ]
2899
3912
  };
@@ -2918,6 +3931,29 @@ function createMcpServer() {
2918
3931
  return await handleSearchAndSummarize(
2919
3932
  args
2920
3933
  );
3934
+ // ==================== LESSON HANDLERS ====================
3935
+ case "search_lessons":
3936
+ return await handleSearchLessons(
3937
+ args
3938
+ );
3939
+ case "get_lesson":
3940
+ return await handleGetLesson(args);
3941
+ case "save_lesson":
3942
+ return await handleSaveLesson(
3943
+ args
3944
+ );
3945
+ case "validate_lesson":
3946
+ return await handleValidateLesson(
3947
+ args
3948
+ );
3949
+ case "reject_lesson":
3950
+ return await handleRejectLesson(
3951
+ args
3952
+ );
3953
+ case "list_lessons":
3954
+ return await handleListLessons(
3955
+ args
3956
+ );
2921
3957
  default:
2922
3958
  return {
2923
3959
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
@@ -3180,6 +4216,210 @@ No sessions found.`;
3180
4216
  }
3181
4217
  return lines.join("\n");
3182
4218
  }
4219
+ async function handleSearchLessons(args) {
4220
+ const { query, projectPath = process.cwd(), limit = 5 } = args;
4221
+ if (!isReady()) {
4222
+ try {
4223
+ await initializeEmbeddings();
4224
+ } catch {
4225
+ return {
4226
+ content: [
4227
+ {
4228
+ type: "text",
4229
+ text: "Embedding model not available. Cannot perform semantic search."
4230
+ }
4231
+ ],
4232
+ isError: true
4233
+ };
4234
+ }
4235
+ }
4236
+ const queryEmbedding = await getEmbedding(query);
4237
+ const lessons = searchLessonsByEmbedding(queryEmbedding, projectPath, limit);
4238
+ if (lessons.length === 0) {
4239
+ return {
4240
+ content: [
4241
+ {
4242
+ type: "text",
4243
+ text: `No lessons found for query: "${query}" in project: ${projectPath}`
4244
+ }
4245
+ ]
4246
+ };
4247
+ }
4248
+ const lines = [`# Lessons matching "${query}"`, ""];
4249
+ for (const lesson of lessons) {
4250
+ lines.push(`### ${lesson.title}`);
4251
+ lines.push(`- **ID:** \`${lesson.id.slice(0, 8)}\``);
4252
+ lines.push(`- **Category:** ${lesson.category}`);
4253
+ lines.push(`- **Confidence:** ${(lesson.confidence * 100).toFixed(0)}%`);
4254
+ lines.push(`- **When to apply:** ${lesson.triggerContext}`);
4255
+ lines.push("");
4256
+ lines.push(`**Insight:** ${lesson.insight}`);
4257
+ if (lesson.reasoning) {
4258
+ lines.push(`**Reasoning:** ${lesson.reasoning}`);
4259
+ }
4260
+ lines.push("");
4261
+ }
4262
+ return {
4263
+ content: [{ type: "text", text: lines.join("\n") }]
4264
+ };
4265
+ }
4266
+ async function handleGetLesson(args) {
4267
+ const { lessonId } = args;
4268
+ const lesson = getLesson(lessonId);
4269
+ if (!lesson) {
4270
+ return {
4271
+ content: [{ type: "text", text: `Lesson not found: ${lessonId}` }],
4272
+ isError: true
4273
+ };
4274
+ }
4275
+ const lines = [
4276
+ `# ${lesson.title}`,
4277
+ "",
4278
+ `**ID:** ${lesson.id}`,
4279
+ `**Category:** ${lesson.category}`,
4280
+ `**Project:** ${lesson.projectPath}`,
4281
+ `**Confidence:** ${(lesson.confidence * 100).toFixed(0)}%`,
4282
+ `**Times Applied:** ${lesson.timesApplied}`,
4283
+ `**Times Validated:** ${lesson.timesValidated}`,
4284
+ `**Times Rejected:** ${lesson.timesRejected}`,
4285
+ `**Archived:** ${lesson.archived ? "Yes" : "No"}`,
4286
+ `**Created:** ${lesson.createdAt}`,
4287
+ `**Updated:** ${lesson.updatedAt}`,
4288
+ "",
4289
+ "## When to Apply",
4290
+ lesson.triggerContext,
4291
+ "",
4292
+ "## Insight",
4293
+ lesson.insight
4294
+ ];
4295
+ if (lesson.reasoning) {
4296
+ lines.push("", "## Reasoning", lesson.reasoning);
4297
+ }
4298
+ if (lesson.sourceSessionId) {
4299
+ lines.push("", `**Source Session:** ${lesson.sourceSessionId}`);
4300
+ }
4301
+ return {
4302
+ content: [{ type: "text", text: lines.join("\n") }]
4303
+ };
4304
+ }
4305
+ async function handleSaveLesson(args) {
4306
+ const input = {
4307
+ projectPath: args.projectPath,
4308
+ category: args.category,
4309
+ title: args.title,
4310
+ triggerContext: args.triggerContext,
4311
+ insight: args.insight,
4312
+ reasoning: args.reasoning,
4313
+ sourceType: "manual",
4314
+ confidence: 0.6
4315
+ // Manual lessons start with moderate confidence
4316
+ };
4317
+ try {
4318
+ const lesson = await lessonManager.create(input);
4319
+ return {
4320
+ content: [
4321
+ {
4322
+ type: "text",
4323
+ text: `Lesson saved successfully!
4324
+
4325
+ **ID:** ${lesson.id}
4326
+ **Title:** ${lesson.title}`
4327
+ }
4328
+ ]
4329
+ };
4330
+ } catch (error) {
4331
+ return {
4332
+ content: [
4333
+ {
4334
+ type: "text",
4335
+ text: `Failed to save lesson: ${String(error)}`
4336
+ }
4337
+ ],
4338
+ isError: true
4339
+ };
4340
+ }
4341
+ }
4342
+ async function handleValidateLesson(args) {
4343
+ const { lessonId, comment } = args;
4344
+ const lesson = getLesson(lessonId);
4345
+ if (!lesson) {
4346
+ return {
4347
+ content: [{ type: "text", text: `Lesson not found: ${lessonId}` }],
4348
+ isError: true
4349
+ };
4350
+ }
4351
+ lessonManager.recordValidation(lessonId, void 0, comment);
4352
+ const updated = getLesson(lessonId);
4353
+ return {
4354
+ content: [
4355
+ {
4356
+ type: "text",
4357
+ text: `Lesson validated: "${lesson.title}"
4358
+ New confidence: ${((updated?.confidence ?? 0) * 100).toFixed(0)}%`
4359
+ }
4360
+ ]
4361
+ };
4362
+ }
4363
+ async function handleRejectLesson(args) {
4364
+ const { lessonId, comment } = args;
4365
+ const lesson = getLesson(lessonId);
4366
+ if (!lesson) {
4367
+ return {
4368
+ content: [{ type: "text", text: `Lesson not found: ${lessonId}` }],
4369
+ isError: true
4370
+ };
4371
+ }
4372
+ lessonManager.recordRejection(lessonId, void 0, comment);
4373
+ const updated = getLesson(lessonId);
4374
+ let message = `Lesson rejected: "${lesson.title}"`;
4375
+ if (updated?.archived) {
4376
+ message += "\nLesson has been auto-archived due to low confidence.";
4377
+ } else if (updated) {
4378
+ message += `
4379
+ New confidence: ${(updated.confidence * 100).toFixed(0)}%`;
4380
+ }
4381
+ return {
4382
+ content: [{ type: "text", text: message }]
4383
+ };
4384
+ }
4385
+ async function handleListLessons(args) {
4386
+ const { projectPath, category, includeArchived = false, limit = 20 } = args;
4387
+ const lessons = getLessonsByProject(projectPath, {
4388
+ category,
4389
+ archived: includeArchived ? void 0 : false,
4390
+ limit
4391
+ });
4392
+ if (lessons.length === 0) {
4393
+ return {
4394
+ content: [
4395
+ {
4396
+ type: "text",
4397
+ text: `No lessons found for project: ${projectPath}`
4398
+ }
4399
+ ]
4400
+ };
4401
+ }
4402
+ const stats = getLessonStats();
4403
+ const lines = [
4404
+ `# Lessons for ${projectPath}`,
4405
+ "",
4406
+ `**Total:** ${lessons.length} | **Active:** ${stats.activeLessons} | **Archived:** ${stats.archivedLessons}`,
4407
+ ""
4408
+ ];
4409
+ for (const lesson of lessons) {
4410
+ const archived = lesson.archived ? " [ARCHIVED]" : "";
4411
+ lines.push(`### ${lesson.title}${archived}`);
4412
+ lines.push(`- **ID:** \`${lesson.id.slice(0, 8)}\``);
4413
+ lines.push(`- **Category:** ${lesson.category}`);
4414
+ lines.push(`- **Confidence:** ${(lesson.confidence * 100).toFixed(0)}%`);
4415
+ lines.push(`- **Applied:** ${lesson.timesApplied} | **Validated:** ${lesson.timesValidated} | **Rejected:** ${lesson.timesRejected}`);
4416
+ lines.push(`- **Insight:** ${truncate(lesson.insight, 100)}`);
4417
+ lines.push("");
4418
+ }
4419
+ return {
4420
+ content: [{ type: "text", text: lines.join("\n") }]
4421
+ };
4422
+ }
3183
4423
  async function startMcpServer() {
3184
4424
  const server = createMcpServer();
3185
4425
  const transport = new StdioServerTransport();
@@ -3197,29 +4437,32 @@ async function mcpCommand() {
3197
4437
  }
3198
4438
  }
3199
4439
 
3200
- // src/cli.ts
3201
- init_install();
3202
-
3203
4440
  // src/commands/setup.ts
3204
- import chalk10 from "chalk";
3205
- import { execSync as execSync3 } from "child_process";
3206
- import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3207
- import { homedir as homedir3 } from "os";
4441
+ import chalk9 from "chalk";
4442
+ import { execSync as execSync2 } from "child_process";
4443
+ import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4444
+ import { homedir as homedir2 } from "os";
3208
4445
  import { join as join6, dirname as dirname6, basename as basename7 } from "path";
3209
4446
  import { fileURLToPath } from "url";
3210
4447
  import { createInterface } from "readline";
3211
4448
  var __filename = fileURLToPath(import.meta.url);
3212
4449
  var __dirname = dirname6(__filename);
3213
- var CLAUDE_JSON_PATH = join6(homedir3(), ".claude.json");
3214
- var CLAUDE_SETTINGS_PATH = join6(homedir3(), ".claude", "settings.json");
3215
- var CMEM_DIR2 = join6(homedir3(), ".cmem");
4450
+ var CLAUDE_JSON_PATH = join6(homedir2(), ".claude.json");
4451
+ var CLAUDE_SETTINGS_PATH = join6(homedir2(), ".claude", "settings.json");
4452
+ var CMEM_DIR2 = join6(homedir2(), ".cmem");
3216
4453
  var SETUP_MARKER = join6(CMEM_DIR2, ".setup-complete");
3217
4454
  var CMEM_PERMISSIONS = [
3218
4455
  "mcp__cmem__search_sessions",
3219
4456
  "mcp__cmem__list_sessions",
3220
4457
  "mcp__cmem__get_session",
3221
4458
  "mcp__cmem__get_session_context",
3222
- "mcp__cmem__search_and_summarize"
4459
+ "mcp__cmem__search_and_summarize",
4460
+ "mcp__cmem__search_lessons",
4461
+ "mcp__cmem__get_lesson",
4462
+ "mcp__cmem__save_lesson",
4463
+ "mcp__cmem__validate_lesson",
4464
+ "mcp__cmem__reject_lesson",
4465
+ "mcp__cmem__list_lessons"
3223
4466
  ];
3224
4467
  var spinnerFrames2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3225
4468
  var Spinner3 = class {
@@ -3233,24 +4476,24 @@ var Spinner3 = class {
3233
4476
  process.stdout.write(` ${spinnerFrames2[0]} ${this.message}`);
3234
4477
  this.interval = setInterval(() => {
3235
4478
  this.frameIndex = (this.frameIndex + 1) % spinnerFrames2.length;
3236
- process.stdout.write(`\r ${chalk10.cyan(spinnerFrames2[this.frameIndex])} ${this.message}`);
4479
+ process.stdout.write(`\r ${chalk9.cyan(spinnerFrames2[this.frameIndex])} ${this.message}`);
3237
4480
  }, 80);
3238
4481
  }
3239
4482
  update(message) {
3240
4483
  this.message = message;
3241
- process.stdout.write(`\r ${chalk10.cyan(spinnerFrames2[this.frameIndex])} ${this.message} `);
4484
+ process.stdout.write(`\r ${chalk9.cyan(spinnerFrames2[this.frameIndex])} ${this.message} `);
3242
4485
  }
3243
4486
  succeed(message) {
3244
4487
  this.stop();
3245
- console.log(`\r ${chalk10.green("\u2713")} ${message || this.message} `);
4488
+ console.log(`\r ${chalk9.green("\u2713")} ${message || this.message} `);
3246
4489
  }
3247
4490
  fail(message) {
3248
4491
  this.stop();
3249
- console.log(`\r ${chalk10.red("\u2717")} ${message || this.message} `);
4492
+ console.log(`\r ${chalk9.red("\u2717")} ${message || this.message} `);
3250
4493
  }
3251
4494
  warn(message) {
3252
4495
  this.stop();
3253
- console.log(`\r ${chalk10.yellow("!")} ${message || this.message} `);
4496
+ console.log(`\r ${chalk9.yellow("!")} ${message || this.message} `);
3254
4497
  }
3255
4498
  stop() {
3256
4499
  if (this.interval) {
@@ -3260,9 +4503,9 @@ var Spinner3 = class {
3260
4503
  }
3261
4504
  };
3262
4505
  function printBanner(version) {
3263
- const magenta = chalk10.magenta;
3264
- const cyan = chalk10.cyan;
3265
- const dim = chalk10.dim;
4506
+ const magenta = chalk9.magenta;
4507
+ const cyan = chalk9.cyan;
4508
+ const dim = chalk9.dim;
3266
4509
  console.log("");
3267
4510
  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"));
3268
4511
  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"));
@@ -3282,11 +4525,11 @@ async function promptChoice(question, options, defaultChoice = 1) {
3282
4525
  output: process.stdout
3283
4526
  });
3284
4527
  options.forEach((opt, i) => {
3285
- console.log(chalk10.dim(` ${i + 1}) ${opt}`));
4528
+ console.log(chalk9.dim(` ${i + 1}) ${opt}`));
3286
4529
  });
3287
4530
  console.log("");
3288
4531
  return new Promise((resolve) => {
3289
- rl.question(chalk10.white(` Choice [${defaultChoice}]: `), (answer) => {
4532
+ rl.question(chalk9.white(` Choice [${defaultChoice}]: `), (answer) => {
3290
4533
  rl.close();
3291
4534
  const num = parseInt(answer.trim(), 10);
3292
4535
  if (isNaN(num) || num < 1 || num > options.length) {
@@ -3304,7 +4547,7 @@ async function promptYesNo(question, defaultYes = true) {
3304
4547
  });
3305
4548
  const hint = defaultYes ? "[Y/n]" : "[y/N]";
3306
4549
  return new Promise((resolve) => {
3307
- rl.question(chalk10.white(` ${question} ${chalk10.dim(hint)} `), (answer) => {
4550
+ rl.question(chalk9.white(` ${question} ${chalk9.dim(hint)} `), (answer) => {
3308
4551
  rl.close();
3309
4552
  const normalized = answer.toLowerCase().trim();
3310
4553
  if (normalized === "") {
@@ -3321,7 +4564,7 @@ function isRunningViaNpx() {
3321
4564
  }
3322
4565
  function isGloballyInstalled() {
3323
4566
  try {
3324
- const result = execSync3("which cmem 2>/dev/null || where cmem 2>/dev/null", {
4567
+ const result = execSync2("which cmem 2>/dev/null || where cmem 2>/dev/null", {
3325
4568
  encoding: "utf-8",
3326
4569
  stdio: ["pipe", "pipe", "pipe"]
3327
4570
  }).trim();
@@ -3343,7 +4586,7 @@ function getCmemVersion() {
3343
4586
  }
3344
4587
  function getInstalledVersion() {
3345
4588
  try {
3346
- const result = execSync3("cmem --version 2>/dev/null", {
4589
+ const result = execSync2("cmem --version 2>/dev/null", {
3347
4590
  encoding: "utf-8"
3348
4591
  }).trim();
3349
4592
  return result;
@@ -3351,49 +4594,86 @@ function getInstalledVersion() {
3351
4594
  return null;
3352
4595
  }
3353
4596
  }
4597
+ function getCmemDistPath() {
4598
+ try {
4599
+ const globalPath = execSync2("npm root -g 2>/dev/null", {
4600
+ encoding: "utf-8"
4601
+ }).trim();
4602
+ const cmemGlobalPath = join6(globalPath, "@colbymchenry", "cmem", "dist");
4603
+ if (existsSync10(cmemGlobalPath)) {
4604
+ return cmemGlobalPath;
4605
+ }
4606
+ const parentDist = dirname6(__dirname);
4607
+ if (existsSync10(join6(parentDist, "hooks"))) {
4608
+ return parentDist;
4609
+ }
4610
+ return null;
4611
+ } catch {
4612
+ const parentDist = dirname6(__dirname);
4613
+ if (existsSync10(parentDist)) {
4614
+ return parentDist;
4615
+ }
4616
+ return null;
4617
+ }
4618
+ }
4619
+ function cleanupOldDaemon() {
4620
+ const plistPath = join6(homedir2(), "Library", "LaunchAgents", "com.cmem.watch.plist");
4621
+ if (existsSync10(plistPath)) {
4622
+ try {
4623
+ execSync2(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "pipe" });
4624
+ } catch {
4625
+ }
4626
+ try {
4627
+ const { unlinkSync: unlinkSync2 } = __require("fs");
4628
+ unlinkSync2(plistPath);
4629
+ } catch {
4630
+ }
4631
+ }
4632
+ }
3354
4633
  async function setupCommand() {
3355
4634
  const currentVersion = getCmemVersion();
3356
4635
  const installedVersion = getInstalledVersion();
3357
4636
  const isGlobal = isGloballyInstalled();
3358
4637
  const isNpx = isRunningViaNpx();
3359
4638
  printBanner(currentVersion);
4639
+ cleanupOldDaemon();
3360
4640
  if (!isGlobal || isNpx) {
3361
- console.log(chalk10.yellow(" Install cmem globally?"));
3362
- console.log(chalk10.dim(" Makes the `cmem` command available everywhere\n"));
4641
+ console.log(chalk9.yellow(" Install cmem globally?"));
4642
+ console.log(chalk9.dim(" Makes the `cmem` command available everywhere\n"));
3363
4643
  const choice = await promptChoice("", [
3364
4644
  "Yes - install globally via npm",
3365
4645
  "No - I'll use npx each time"
3366
4646
  ], 1);
3367
4647
  if (choice === 1) {
3368
- console.log(chalk10.dim("\n Installing @colbymchenry/cmem globally...\n"));
4648
+ console.log(chalk9.dim("\n Installing @colbymchenry/cmem globally...\n"));
3369
4649
  try {
3370
- execSync3("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
3371
- console.log(chalk10.green("\n \u2713 Installed globally\n"));
4650
+ execSync2("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
4651
+ console.log(chalk9.green("\n \u2713 Installed globally\n"));
3372
4652
  } catch {
3373
- console.log(chalk10.red("\n \u2717 Failed to install. Try: sudo npm install -g @colbymchenry/cmem\n"));
4653
+ console.log(chalk9.red("\n \u2717 Failed to install. Try: sudo npm install -g @colbymchenry/cmem\n"));
3374
4654
  }
3375
4655
  } else {
3376
- console.log(chalk10.dim("\n Skipped. Use `npx @colbymchenry/cmem` to run.\n"));
4656
+ console.log(chalk9.dim("\n Skipped. Use `npx @colbymchenry/cmem` to run.\n"));
3377
4657
  }
3378
4658
  } else if (installedVersion && installedVersion !== currentVersion) {
3379
- console.log(chalk10.yellow(` Update available: ${installedVersion} \u2192 ${currentVersion}`));
4659
+ console.log(chalk9.yellow(` Update available: ${installedVersion} \u2192 ${currentVersion}`));
3380
4660
  const shouldUpdate = await promptYesNo("Update to latest version?");
3381
4661
  if (shouldUpdate) {
3382
- console.log(chalk10.dim("\n Updating @colbymchenry/cmem...\n"));
4662
+ console.log(chalk9.dim("\n Updating @colbymchenry/cmem...\n"));
3383
4663
  try {
3384
- execSync3("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
3385
- console.log(chalk10.green("\n \u2713 Updated\n"));
4664
+ execSync2("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
4665
+ console.log(chalk9.green("\n \u2713 Updated\n"));
3386
4666
  } catch {
3387
- console.log(chalk10.red("\n \u2717 Failed to update\n"));
4667
+ console.log(chalk9.red("\n \u2717 Failed to update\n"));
3388
4668
  }
3389
4669
  } else {
3390
4670
  console.log("");
3391
4671
  }
3392
4672
  } else {
3393
- console.log(chalk10.green(" \u2713 cmem is installed globally\n"));
4673
+ console.log(chalk9.green(" \u2713 cmem is installed globally\n"));
3394
4674
  }
3395
- console.log(chalk10.yellow(" Semantic search setup"));
3396
- console.log(chalk10.dim(" Enables searching conversations by meaning\n"));
4675
+ console.log(chalk9.yellow(" Semantic search setup"));
4676
+ console.log(chalk9.dim(" Enables searching conversations by meaning\n"));
3397
4677
  const modelSpinner = new Spinner3("Checking embedding model...");
3398
4678
  modelSpinner.start();
3399
4679
  const cached = isModelCached();
@@ -3414,97 +4694,83 @@ async function setupCommand() {
3414
4694
  modelSpinner.succeed("Embedding model ready");
3415
4695
  } catch (err) {
3416
4696
  modelSpinner.fail("Failed to download embedding model");
3417
- console.log(chalk10.dim(` Error: ${err}
4697
+ console.log(chalk9.dim(` Error: ${err}
3418
4698
  `));
3419
4699
  }
3420
4700
  }
3421
4701
  console.log("");
3422
- const daemonInstalled = existsSync10(
3423
- join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist")
3424
- );
3425
- const isUpdating = installedVersion && installedVersion !== currentVersion;
3426
- if (daemonInstalled && !isUpdating) {
3427
- console.log(chalk10.green(" \u2713 Auto-start daemon is installed"));
3428
- try {
3429
- execSync3("launchctl load ~/Library/LaunchAgents/com.cmem.watch.plist 2>/dev/null", { stdio: "ignore" });
3430
- } catch {
3431
- }
3432
- console.log("");
3433
- } else if (daemonInstalled && isUpdating && process.platform === "darwin") {
3434
- console.log(chalk10.yellow(" Updating daemon..."));
3435
- try {
3436
- const { installCommand: installCommand2 } = await Promise.resolve().then(() => (init_install(), install_exports));
3437
- await installCommand2();
3438
- } catch {
3439
- console.log(chalk10.red(" \u2717 Failed to update daemon"));
3440
- console.log(chalk10.dim(" Try manually: cmem install\n"));
3441
- }
3442
- } else if (process.platform === "darwin") {
3443
- console.log(chalk10.yellow(" Auto-start daemon?"));
3444
- console.log(chalk10.dim(" Syncs Claude Code sessions in the background\n"));
4702
+ const mcpConfigured = isMcpConfigured();
4703
+ if (mcpConfigured) {
4704
+ console.log(chalk9.green(" \u2713 MCP server configured in Claude Code\n"));
4705
+ } else {
4706
+ console.log(chalk9.yellow(" Add MCP server to Claude Code?"));
4707
+ console.log(chalk9.dim(" Lets Claude search your past conversations\n"));
3445
4708
  const choice = await promptChoice("", [
3446
- "Yes - start on login (recommended)",
3447
- "No - I'll run `cmem watch` manually"
4709
+ "Yes - configure automatically (recommended)",
4710
+ "No - I'll configure it manually"
3448
4711
  ], 1);
3449
4712
  if (choice === 1) {
3450
- try {
3451
- const { installCommand: installCommand2 } = await Promise.resolve().then(() => (init_install(), install_exports));
3452
- await installCommand2();
3453
- } catch (err) {
3454
- console.log(chalk10.red(" \u2717 Failed to install daemon"));
3455
- console.log(chalk10.dim(" Try manually: cmem install\n"));
4713
+ const success = configureMcpServer();
4714
+ if (success) {
4715
+ console.log(chalk9.green("\n \u2713 Added MCP server to ~/.claude.json"));
4716
+ console.log(chalk9.green(" \u2713 Added permissions to ~/.claude/settings.json"));
4717
+ console.log(chalk9.dim(" Restart Claude Code or run /mcp to connect\n"));
4718
+ } else {
4719
+ console.log(chalk9.red("\n \u2717 Failed to configure MCP server\n"));
3456
4720
  }
3457
4721
  } else {
3458
- console.log(chalk10.dim("\n Skipped. Run `cmem watch` when needed.\n"));
4722
+ console.log(chalk9.dim("\n Skipped.\n"));
3459
4723
  }
3460
4724
  }
3461
- const mcpConfigured = isMcpConfigured();
3462
- if (mcpConfigured) {
3463
- console.log(chalk10.green(" \u2713 MCP server configured in Claude Code\n"));
4725
+ const hooksConfigured = areHooksConfigured();
4726
+ if (hooksConfigured) {
4727
+ console.log(chalk9.green(" \u2713 Learning hooks configured\n"));
3464
4728
  } else {
3465
- console.log(chalk10.yellow(" Add MCP server to Claude Code?"));
3466
- console.log(chalk10.dim(" Lets Claude search your past conversations\n"));
4729
+ console.log(chalk9.yellow(" Enable automatic learning?"));
4730
+ console.log(chalk9.dim(" Hooks sync sessions and inject knowledge automatically\n"));
3467
4731
  const choice = await promptChoice("", [
3468
- "Yes - configure automatically (recommended)",
3469
- "No - I'll configure it manually"
4732
+ "Yes - enable hooks (recommended)",
4733
+ "No - I'll manage manually"
3470
4734
  ], 1);
3471
4735
  if (choice === 1) {
3472
- const success = configureMcpServer();
3473
- if (success) {
3474
- console.log(chalk10.green("\n \u2713 Added MCP server to ~/.claude.json"));
3475
- console.log(chalk10.green(" \u2713 Added permissions to ~/.claude/settings.json"));
3476
- console.log(chalk10.dim(" Restart Claude Code or run /mcp to connect\n"));
4736
+ const result = configureHooks();
4737
+ if (result.success) {
4738
+ console.log(chalk9.green("\n \u2713 Configured hooks in ~/.claude/settings.json"));
4739
+ console.log(chalk9.dim(" \u2022 UserPromptSubmit: Injects relevant lessons"));
4740
+ console.log(chalk9.dim(" \u2022 Stop/PreCompact: Syncs and backs up sessions\n"));
3477
4741
  } else {
3478
- console.log(chalk10.red("\n \u2717 Failed to configure MCP server\n"));
4742
+ console.log(chalk9.red(`
4743
+ \u2717 Failed to configure hooks: ${result.message}`));
4744
+ console.log(chalk9.dim(" You can configure hooks manually in ~/.claude/settings.json\n"));
3479
4745
  }
3480
4746
  } else {
3481
- console.log(chalk10.dim("\n Skipped.\n"));
4747
+ console.log(chalk9.dim("\n Skipped. Run `cmem watch` for manual sync.\n"));
3482
4748
  }
3483
4749
  }
3484
- console.log(chalk10.yellow(" Initial session indexing"));
3485
- console.log(chalk10.dim(" Scanning and indexing your existing Claude Code conversations\n"));
4750
+ console.log(chalk9.yellow(" Initial session indexing"));
4751
+ console.log(chalk9.dim(" Scanning and indexing your existing Claude Code conversations\n"));
3486
4752
  await indexExistingSessions();
3487
4753
  if (!existsSync10(CMEM_DIR2)) {
3488
- mkdirSync3(CMEM_DIR2, { recursive: true });
3489
- }
3490
- writeFileSync2(SETUP_MARKER, (/* @__PURE__ */ new Date()).toISOString());
3491
- 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"));
3492
- console.log(chalk10.green.bold("\n \u2713 Setup complete!\n"));
3493
- console.log(chalk10.white(" Commands:"));
3494
- console.log(chalk10.dim(" cmem Browse sessions (TUI)"));
3495
- console.log(chalk10.dim(' cmem search "X" Semantic search'));
3496
- console.log(chalk10.dim(" cmem watch Start sync daemon"));
3497
- console.log(chalk10.dim(" cmem stats Storage statistics"));
3498
- console.log(chalk10.dim(" cmem --help All commands"));
3499
- console.log(chalk10.white("\n MCP Tools (available to Claude):"));
3500
- console.log(chalk10.dim(" search_sessions Find past conversations"));
3501
- console.log(chalk10.dim(" get_session Retrieve full history"));
3502
- console.log(chalk10.dim(" list_sessions Browse recent sessions"));
3503
- if (process.platform === "darwin") {
3504
- const plistExists = existsSync10(join6(homedir3(), "Library", "LaunchAgents", "com.cmem.watch.plist"));
3505
- if (plistExists) {
3506
- console.log(chalk10.green("\n \u{1F680} Daemon is now syncing your sessions in the background!"));
3507
- }
4754
+ mkdirSync2(CMEM_DIR2, { recursive: true });
4755
+ }
4756
+ writeFileSync(SETUP_MARKER, (/* @__PURE__ */ new Date()).toISOString());
4757
+ console.log(chalk9.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"));
4758
+ console.log(chalk9.green.bold("\n \u2713 Setup complete!\n"));
4759
+ console.log(chalk9.white(" Commands:"));
4760
+ console.log(chalk9.dim(" cmem Browse sessions (TUI)"));
4761
+ console.log(chalk9.dim(' cmem search "X" Semantic search'));
4762
+ console.log(chalk9.dim(" cmem synthesize Extract lessons from sessions"));
4763
+ console.log(chalk9.dim(" cmem stats Storage statistics"));
4764
+ console.log(chalk9.dim(" cmem --help All commands"));
4765
+ console.log(chalk9.white("\n MCP Tools (available to Claude):"));
4766
+ console.log(chalk9.dim(" search_sessions Find past conversations"));
4767
+ console.log(chalk9.dim(" search_lessons Find project knowledge"));
4768
+ console.log(chalk9.dim(" save_lesson Save important insights"));
4769
+ if (areHooksConfigured()) {
4770
+ console.log(chalk9.green("\n \u{1F9E0} Learning is enabled!"));
4771
+ console.log(chalk9.dim(" \u2022 Lessons are injected before each prompt"));
4772
+ console.log(chalk9.dim(" \u2022 Sessions sync automatically on stop/compact"));
4773
+ console.log(chalk9.dim(" \u2022 Run `cmem synthesize` to extract lessons"));
3508
4774
  }
3509
4775
  console.log("");
3510
4776
  }
@@ -3537,10 +4803,10 @@ function configureMcpServer() {
3537
4803
  command: "cmem",
3538
4804
  args: ["mcp"]
3539
4805
  };
3540
- writeFileSync2(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2) + "\n");
4806
+ writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2) + "\n");
3541
4807
  const claudeDir = dirname6(CLAUDE_SETTINGS_PATH);
3542
4808
  if (!existsSync10(claudeDir)) {
3543
- mkdirSync3(claudeDir, { recursive: true });
4809
+ mkdirSync2(claudeDir, { recursive: true });
3544
4810
  }
3545
4811
  let settings = {};
3546
4812
  if (existsSync10(CLAUDE_SETTINGS_PATH)) {
@@ -3561,10 +4827,100 @@ function configureMcpServer() {
3561
4827
  settings.permissions.allow.push(perm);
3562
4828
  }
3563
4829
  }
3564
- writeFileSync2(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
4830
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
3565
4831
  return true;
3566
4832
  } catch (err) {
3567
- console.error(chalk10.red("Error configuring MCP:"), err);
4833
+ console.error(chalk9.red("Error configuring MCP:"), err);
4834
+ return false;
4835
+ }
4836
+ }
4837
+ function configureHooks() {
4838
+ try {
4839
+ const distPath = getCmemDistPath();
4840
+ if (!distPath) {
4841
+ return { success: false, message: "Could not find cmem installation path" };
4842
+ }
4843
+ const consultPath = join6(distPath, "hooks", "consult.js");
4844
+ const syncPath = join6(distPath, "hooks", "sync.js");
4845
+ if (!existsSync10(consultPath)) {
4846
+ return { success: false, message: `Hook file not found: ${consultPath}` };
4847
+ }
4848
+ const claudeDir = dirname6(CLAUDE_SETTINGS_PATH);
4849
+ if (!existsSync10(claudeDir)) {
4850
+ mkdirSync2(claudeDir, { recursive: true });
4851
+ }
4852
+ let settings = {};
4853
+ if (existsSync10(CLAUDE_SETTINGS_PATH)) {
4854
+ try {
4855
+ settings = JSON.parse(readFileSync3(CLAUDE_SETTINGS_PATH, "utf-8"));
4856
+ } catch {
4857
+ settings = {};
4858
+ }
4859
+ }
4860
+ if (!settings.hooks) {
4861
+ settings.hooks = {};
4862
+ }
4863
+ const hasCmemHook = (hookArray) => {
4864
+ if (!hookArray) return false;
4865
+ return hookArray.some((h) => h.hooks?.some((hk) => hk.command?.includes("cmem")));
4866
+ };
4867
+ if (!hasCmemHook(settings.hooks.UserPromptSubmit)) {
4868
+ if (!settings.hooks.UserPromptSubmit) {
4869
+ settings.hooks.UserPromptSubmit = [];
4870
+ }
4871
+ settings.hooks.UserPromptSubmit.push({
4872
+ matcher: ".*",
4873
+ hooks: [{
4874
+ type: "command",
4875
+ command: `node "${consultPath}"`,
4876
+ timeout: 5e3
4877
+ }]
4878
+ });
4879
+ }
4880
+ if (!hasCmemHook(settings.hooks.Stop)) {
4881
+ if (!settings.hooks.Stop) {
4882
+ settings.hooks.Stop = [];
4883
+ }
4884
+ settings.hooks.Stop.push({
4885
+ matcher: ".*",
4886
+ hooks: [{
4887
+ type: "command",
4888
+ command: `node "${syncPath}"`
4889
+ }]
4890
+ });
4891
+ }
4892
+ if (!hasCmemHook(settings.hooks.PreCompact)) {
4893
+ if (!settings.hooks.PreCompact) {
4894
+ settings.hooks.PreCompact = [];
4895
+ }
4896
+ settings.hooks.PreCompact.push({
4897
+ matcher: ".*",
4898
+ hooks: [{
4899
+ type: "command",
4900
+ command: `node "${syncPath}"`
4901
+ }]
4902
+ });
4903
+ }
4904
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
4905
+ return { success: true, message: "Hooks configured successfully" };
4906
+ } catch (err) {
4907
+ return { success: false, message: String(err) };
4908
+ }
4909
+ }
4910
+ function areHooksConfigured() {
4911
+ if (!existsSync10(CLAUDE_SETTINGS_PATH)) {
4912
+ return false;
4913
+ }
4914
+ try {
4915
+ const settings = JSON.parse(readFileSync3(CLAUDE_SETTINGS_PATH, "utf-8"));
4916
+ const hooks = settings.hooks;
4917
+ if (!hooks) return false;
4918
+ const hasCmemHook = (hookArray) => {
4919
+ if (!hookArray) return false;
4920
+ return hookArray.some((h) => h.hooks?.some((hk) => hk.command?.includes("cmem")));
4921
+ };
4922
+ return hasCmemHook(hooks.UserPromptSubmit) || hasCmemHook(hooks.Stop);
4923
+ } catch {
3568
4924
  return false;
3569
4925
  }
3570
4926
  }
@@ -3587,7 +4943,7 @@ function findAllSessionFiles2(dir) {
3587
4943
  function scanDir(currentDir, depth = 0) {
3588
4944
  if (depth > 10) return;
3589
4945
  try {
3590
- const entries = readdirSync3(currentDir, { withFileTypes: true });
4946
+ const entries = readdirSync4(currentDir, { withFileTypes: true });
3591
4947
  for (const entry of entries) {
3592
4948
  const fullPath = join6(currentDir, entry.name);
3593
4949
  if (entry.isDirectory()) {
@@ -3609,7 +4965,7 @@ async function indexSessionFile(filePath, embeddingsReady) {
3609
4965
  if (messages.length === 0) return false;
3610
4966
  const existing = getSessionBySourceFile(filePath);
3611
4967
  if (existing) return false;
3612
- const stats = statSync4(filePath);
4968
+ const stats = statSync5(filePath);
3613
4969
  const fileMtime = stats.mtime.toISOString();
3614
4970
  const firstUserMsg = messages.find((m) => m.role === "user");
3615
4971
  const title = firstUserMsg ? generateTitle(firstUserMsg.content) : "Untitled Session";
@@ -3649,7 +5005,7 @@ async function indexExistingSessions() {
3649
5005
  const totalFiles = sessionFiles.length;
3650
5006
  if (totalFiles === 0) {
3651
5007
  spinner.succeed("No Claude Code sessions found yet");
3652
- console.log(chalk10.dim(" Start using Claude Code, then run cmem to see your sessions\n"));
5008
+ console.log(chalk9.dim(" Start using Claude Code, then run cmem to see your sessions\n"));
3653
5009
  return;
3654
5010
  }
3655
5011
  const statsBefore = getStats();
@@ -3672,18 +5028,651 @@ async function indexExistingSessions() {
3672
5028
  }
3673
5029
  spinner.succeed(`Indexed ${totalFiles} sessions (${newlyIndexed} new)`);
3674
5030
  const statsAfter = getStats();
3675
- console.log(chalk10.dim(` ${statsAfter.sessionCount} sessions, ${statsAfter.embeddingCount} with embeddings
5031
+ console.log(chalk9.dim(` ${statsAfter.sessionCount} sessions, ${statsAfter.embeddingCount} with embeddings
3676
5032
  `));
3677
5033
  }
3678
5034
 
5035
+ // src/commands/purge.ts
5036
+ import chalk10 from "chalk";
5037
+ import { createInterface as createInterface2 } from "readline";
5038
+ async function purgeCommand(options) {
5039
+ const days = options.days ? parseInt(options.days, 10) : DEFAULT_PURGE_DAYS;
5040
+ if (isNaN(days) || days < 1) {
5041
+ console.log(chalk10.red("Invalid days value. Must be a positive number."));
5042
+ return;
5043
+ }
5044
+ const preview = getPurgePreview(days);
5045
+ if (preview.sessionsToDelete === 0) {
5046
+ console.log(chalk10.green("No sessions eligible for purge."));
5047
+ console.log(chalk10.dim("(Sessions must be older than " + days + " days and not starred)"));
5048
+ return;
5049
+ }
5050
+ console.log(chalk10.bold("\nPurge Preview:"));
5051
+ console.log(chalk10.dim("\u2500".repeat(50)));
5052
+ console.log(` Database size: ${chalk10.cyan(formatBytes(getDatabaseSize()))}`);
5053
+ console.log(` Backups size: ${chalk10.cyan(formatBytes(getBackupsDirSize()))}`);
5054
+ console.log(` Total storage: ${chalk10.cyan(formatBytes(getTotalStorageSize()))}`);
5055
+ console.log();
5056
+ console.log(` Sessions to delete: ${chalk10.yellow(preview.sessionsToDelete.toString())}`);
5057
+ console.log(` Messages involved: ${chalk10.yellow(preview.messagesInvolved.toString())}`);
5058
+ if (preview.backupFilesToDelete > 0) {
5059
+ console.log(` Backup files: ${chalk10.yellow(preview.backupFilesToDelete.toString())} (~${formatBytes(preview.backupBytesToFree)})`);
5060
+ }
5061
+ console.log(` Date range: ${chalk10.dim(preview.oldestSessionDate)} to ${chalk10.dim(preview.newestSessionDate)}`);
5062
+ console.log(chalk10.dim(" (Starred sessions will be preserved)"));
5063
+ console.log(chalk10.dim("\u2500".repeat(50)));
5064
+ if (options.dryRun) {
5065
+ console.log(chalk10.cyan("\n[Dry run] No changes made."));
5066
+ return;
5067
+ }
5068
+ if (!options.force) {
5069
+ const confirmed = await confirm(
5070
+ chalk10.red(`
5071
+ Delete ${preview.sessionsToDelete} sessions? This cannot be undone. [y/N] `)
5072
+ );
5073
+ if (!confirmed) {
5074
+ console.log(chalk10.dim("Cancelled."));
5075
+ return;
5076
+ }
5077
+ }
5078
+ const result = purgeOldSessions(days);
5079
+ console.log(chalk10.green(`
5080
+ Purged ${result.sessionsDeleted} session${result.sessionsDeleted !== 1 ? "s" : ""}.`));
5081
+ if (result.backupsDeleted > 0) {
5082
+ console.log(chalk10.green(`Deleted ${result.backupsDeleted} backup file${result.backupsDeleted !== 1 ? "s" : ""}.`));
5083
+ }
5084
+ console.log(`New storage size: ${chalk10.cyan(formatBytes(getTotalStorageSize()))}`);
5085
+ }
5086
+ function confirm(prompt) {
5087
+ const rl = createInterface2({
5088
+ input: process.stdin,
5089
+ output: process.stdout
5090
+ });
5091
+ return new Promise((resolve) => {
5092
+ rl.question(prompt, (answer) => {
5093
+ rl.close();
5094
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
5095
+ });
5096
+ });
5097
+ }
5098
+
5099
+ // src/commands/synthesize.ts
5100
+ import chalk11 from "chalk";
5101
+
5102
+ // src/learning/SynthesisEngine.ts
5103
+ var SIMILARITY_THRESHOLD = 0.85;
5104
+ var MAX_MESSAGES_FOR_SYNTHESIS = 50;
5105
+ var MAX_MESSAGE_LENGTH = 2e3;
5106
+ var SynthesisEngine = class {
5107
+ /**
5108
+ * Synthesize lessons from a session
5109
+ */
5110
+ async synthesize(session, messages) {
5111
+ const result = {
5112
+ lessonsCreated: 0,
5113
+ lessonsSkipped: 0,
5114
+ errors: []
5115
+ };
5116
+ if (!isClaudeCliAvailable()) {
5117
+ result.errors.push("Claude CLI not available for synthesis");
5118
+ return result;
5119
+ }
5120
+ if (messages.length < 3) {
5121
+ result.errors.push("Session too short for meaningful synthesis");
5122
+ return result;
5123
+ }
5124
+ try {
5125
+ const prompt = this.buildSynthesisPrompt(session, messages);
5126
+ const response = await runClaudePrompt(prompt, {
5127
+ model: "haiku",
5128
+ // Use haiku for speed and cost
5129
+ maxTokens: 2e3
5130
+ });
5131
+ if (!response.success) {
5132
+ result.errors.push(`Claude error: ${response.error}`);
5133
+ return result;
5134
+ }
5135
+ const rawLessons = this.parseResponse(response.content);
5136
+ if (rawLessons.length === 0) {
5137
+ return result;
5138
+ }
5139
+ const projectPath = session.projectPath || "";
5140
+ for (const raw of rawLessons) {
5141
+ try {
5142
+ const stored = await this.dedupeAndStore(raw, session, projectPath);
5143
+ if (stored) {
5144
+ result.lessonsCreated++;
5145
+ } else {
5146
+ result.lessonsSkipped++;
5147
+ }
5148
+ } catch (error) {
5149
+ result.errors.push(`Failed to store lesson: ${String(error)}`);
5150
+ }
5151
+ }
5152
+ return result;
5153
+ } catch (error) {
5154
+ result.errors.push(`Synthesis failed: ${String(error)}`);
5155
+ return result;
5156
+ }
5157
+ }
5158
+ /**
5159
+ * Build the synthesis prompt for Claude
5160
+ */
5161
+ buildSynthesisPrompt(session, messages) {
5162
+ const formattedMessages = this.formatMessages(messages);
5163
+ return `You are analyzing a Claude Code conversation session to extract reusable lessons.
5164
+
5165
+ ## Session Information
5166
+ - Project: ${session.projectPath || "Unknown"}
5167
+ - Title: ${session.title}
5168
+ - Messages: ${messages.length}
5169
+
5170
+ ## Conversation
5171
+ ${formattedMessages}
5172
+
5173
+ ## Task
5174
+ Extract reusable lessons from this conversation. Focus on:
5175
+
5176
+ 1. **Architecture Decisions** - Design choices and their rationale
5177
+ 2. **Anti-Patterns** - What NOT to do and why
5178
+ 3. **Bug Patterns** - Common bugs and their root causes
5179
+ 4. **Project Conventions** - Code style, naming, file organization
5180
+ 5. **Dependency Knowledge** - Library quirks, version issues, APIs
5181
+ 6. **Domain Knowledge** - Business logic, rules, requirements
5182
+ 7. **Workflows** - Processes, commands, deployment steps
5183
+
5184
+ ## Requirements
5185
+ - Only extract genuinely reusable lessons
5186
+ - Be specific and actionable, not generic
5187
+ - Include context for when the lesson applies
5188
+ - Skip trivial or one-off fixes
5189
+ - Focus on knowledge that would help future work
5190
+
5191
+ ## Output Format
5192
+ Return a JSON array of lessons. Each lesson must have:
5193
+ \`\`\`json
5194
+ [
5195
+ {
5196
+ "category": "architecture_decision|anti_pattern|bug_pattern|project_convention|dependency_knowledge|domain_knowledge|workflow|other",
5197
+ "title": "Short descriptive title (max 60 chars)",
5198
+ "trigger_context": "When this lesson should be surfaced (what kind of prompt/task)",
5199
+ "insight": "The actual knowledge - specific and actionable",
5200
+ "reasoning": "Why this is true or important (optional)",
5201
+ "confidence": 0.3-0.7
5202
+ }
5203
+ ]
5204
+ \`\`\`
5205
+
5206
+ Confidence guidelines:
5207
+ - 0.3-0.4: Observed once, might be specific to this case
5208
+ - 0.5: Reasonable general lesson
5209
+ - 0.6-0.7: Clear pattern with strong evidence
5210
+
5211
+ Return an empty array [] if no reusable lessons can be extracted.
5212
+
5213
+ Output only the JSON array, no other text.`;
5214
+ }
5215
+ /**
5216
+ * Format messages for the prompt
5217
+ */
5218
+ formatMessages(messages) {
5219
+ const relevantMessages = messages.slice(-MAX_MESSAGES_FOR_SYNTHESIS);
5220
+ return relevantMessages.map((m) => {
5221
+ const role = m.role === "user" ? "User" : "Assistant";
5222
+ const content = m.content.length > MAX_MESSAGE_LENGTH ? m.content.slice(0, MAX_MESSAGE_LENGTH) + "...[truncated]" : m.content;
5223
+ return `### ${role}
5224
+ ${content}`;
5225
+ }).join("\n\n");
5226
+ }
5227
+ /**
5228
+ * Parse Claude's response into raw lessons
5229
+ */
5230
+ parseResponse(content) {
5231
+ try {
5232
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
5233
+ if (!jsonMatch) {
5234
+ return [];
5235
+ }
5236
+ const parsed = JSON.parse(jsonMatch[0]);
5237
+ if (!Array.isArray(parsed)) {
5238
+ return [];
5239
+ }
5240
+ return parsed.filter((item) => this.isValidRawLesson(item)).map((item) => ({
5241
+ category: item.category,
5242
+ title: String(item.title).slice(0, 60),
5243
+ triggerContext: String(item.trigger_context),
5244
+ insight: String(item.insight),
5245
+ reasoning: item.reasoning ? String(item.reasoning) : void 0,
5246
+ confidence: Math.min(0.7, Math.max(0.3, Number(item.confidence) || 0.5))
5247
+ }));
5248
+ } catch {
5249
+ return [];
5250
+ }
5251
+ }
5252
+ /**
5253
+ * Validate a raw lesson object
5254
+ */
5255
+ isValidRawLesson(item) {
5256
+ if (typeof item !== "object" || item === null) {
5257
+ return false;
5258
+ }
5259
+ const obj = item;
5260
+ return typeof obj.category === "string" && VALID_CATEGORIES.includes(obj.category) && typeof obj.title === "string" && obj.title.length > 0 && typeof obj.trigger_context === "string" && obj.trigger_context.length > 0 && typeof obj.insight === "string" && obj.insight.length > 0;
5261
+ }
5262
+ /**
5263
+ * Check for duplicates and store if unique
5264
+ */
5265
+ async dedupeAndStore(raw, session, projectPath) {
5266
+ const embeddingText = `${raw.title} ${raw.triggerContext} ${raw.insight}`;
5267
+ try {
5268
+ const embedding = await getEmbedding(embeddingText);
5269
+ const similar = searchLessonsByEmbedding(embedding, projectPath, 1);
5270
+ if (similar.length > 0 && this.isTooSimilar(similar[0], raw)) {
5271
+ return null;
5272
+ }
5273
+ const input = {
5274
+ projectPath,
5275
+ category: raw.category,
5276
+ title: raw.title,
5277
+ triggerContext: raw.triggerContext,
5278
+ insight: raw.insight,
5279
+ reasoning: raw.reasoning,
5280
+ confidence: raw.confidence,
5281
+ sourceSessionId: session.id,
5282
+ sourceType: "synthesized"
5283
+ };
5284
+ const lesson = await lessonManager.create(input);
5285
+ return lesson;
5286
+ } catch {
5287
+ const input = {
5288
+ projectPath,
5289
+ category: raw.category,
5290
+ title: raw.title,
5291
+ triggerContext: raw.triggerContext,
5292
+ insight: raw.insight,
5293
+ reasoning: raw.reasoning,
5294
+ confidence: raw.confidence,
5295
+ sourceSessionId: session.id,
5296
+ sourceType: "synthesized"
5297
+ };
5298
+ return lessonManager.create(input);
5299
+ }
5300
+ }
5301
+ /**
5302
+ * Check if a raw lesson is too similar to an existing lesson
5303
+ */
5304
+ isTooSimilar(existing, raw) {
5305
+ const existingText = `${existing.title} ${existing.insight}`.toLowerCase();
5306
+ const rawText = `${raw.title} ${raw.insight}`.toLowerCase();
5307
+ const existingWords = new Set(existingText.split(/\s+/));
5308
+ const rawWords = rawText.split(/\s+/);
5309
+ let commonCount = 0;
5310
+ for (const word of rawWords) {
5311
+ if (existingWords.has(word)) {
5312
+ commonCount++;
5313
+ }
5314
+ }
5315
+ const similarity = commonCount / Math.max(existingWords.size, rawWords.length);
5316
+ return similarity > SIMILARITY_THRESHOLD;
5317
+ }
5318
+ };
5319
+ var synthesisEngine = new SynthesisEngine();
5320
+
5321
+ // src/learning/processQueue.ts
5322
+ async function processSynthesisQueue(limit = 5) {
5323
+ const result = {
5324
+ processed: 0,
5325
+ lessonsCreated: 0,
5326
+ failed: 0,
5327
+ errors: []
5328
+ };
5329
+ const pending = getPendingSynthesis(limit);
5330
+ if (pending.length === 0) {
5331
+ return result;
5332
+ }
5333
+ for (const item of pending) {
5334
+ try {
5335
+ markSynthesisProcessing(item.id);
5336
+ const session = getSession(item.sessionId);
5337
+ if (!session) {
5338
+ markSynthesisFailed(item.id, "Session not found");
5339
+ result.failed++;
5340
+ result.errors.push(`Session not found: ${item.sessionId}`);
5341
+ continue;
5342
+ }
5343
+ const messages = getSessionMessages(session.id);
5344
+ if (messages.length === 0) {
5345
+ markSynthesisFailed(item.id, "Session has no messages");
5346
+ result.failed++;
5347
+ continue;
5348
+ }
5349
+ const synthesisResult = await synthesisEngine.synthesize(session, messages);
5350
+ if (synthesisResult.errors.length > 0) {
5351
+ result.errors.push(...synthesisResult.errors);
5352
+ }
5353
+ markSynthesisComplete(item.id, synthesisResult.lessonsCreated);
5354
+ result.processed++;
5355
+ result.lessonsCreated += synthesisResult.lessonsCreated;
5356
+ } catch (error) {
5357
+ markSynthesisFailed(item.id, String(error));
5358
+ result.failed++;
5359
+ result.errors.push(`Failed to process ${item.sessionId}: ${String(error)}`);
5360
+ }
5361
+ }
5362
+ return result;
5363
+ }
5364
+ function getQueueStatus2() {
5365
+ return getSynthesisStats();
5366
+ }
5367
+
5368
+ // src/commands/synthesize.ts
5369
+ async function synthesizeCommand(options) {
5370
+ const status = getQueueStatus2();
5371
+ const lessonStats = getLessonStats();
5372
+ if (options.status) {
5373
+ console.log(chalk11.cyan("Synthesis Queue Status\n"));
5374
+ console.log(` Pending: ${chalk11.yellow(status.pending)}`);
5375
+ console.log(` Processing: ${chalk11.blue(status.processing)}`);
5376
+ console.log(` Completed: ${chalk11.green(status.completed)}`);
5377
+ console.log(` Failed: ${chalk11.red(status.failed)}`);
5378
+ console.log("");
5379
+ console.log(chalk11.cyan("Lesson Statistics\n"));
5380
+ console.log(` Total: ${lessonStats.totalLessons}`);
5381
+ console.log(` Active: ${lessonStats.activeLessons}`);
5382
+ console.log(` Archived: ${lessonStats.archivedLessons}`);
5383
+ console.log(` Avg Confidence: ${(lessonStats.avgConfidence * 100).toFixed(0)}%`);
5384
+ console.log("");
5385
+ console.log(chalk11.dim("Categories:"));
5386
+ for (const [category, count] of Object.entries(lessonStats.byCategory)) {
5387
+ if (count > 0) {
5388
+ console.log(` ${category}: ${count}`);
5389
+ }
5390
+ }
5391
+ return;
5392
+ }
5393
+ if (status.pending === 0) {
5394
+ console.log(chalk11.yellow("No sessions pending synthesis."));
5395
+ console.log(chalk11.dim(`
5396
+ Total lessons: ${lessonStats.totalLessons}`));
5397
+ return;
5398
+ }
5399
+ const limit = parseInt(options.limit || "5", 10);
5400
+ console.log(chalk11.cyan("Processing synthesis queue...\n"));
5401
+ console.log(` Queue: ${status.pending} pending`);
5402
+ console.log(` Processing up to ${limit} sessions
5403
+ `);
5404
+ const result = await processSynthesisQueue(limit);
5405
+ console.log(chalk11.cyan("\nResults:\n"));
5406
+ console.log(` Processed: ${chalk11.green(result.processed)}`);
5407
+ console.log(` Lessons created: ${chalk11.green(result.lessonsCreated)}`);
5408
+ console.log(` Failed: ${result.failed > 0 ? chalk11.red(result.failed) : "0"}`);
5409
+ if (result.errors.length > 0) {
5410
+ console.log(chalk11.yellow("\nWarnings:"));
5411
+ for (const error of result.errors.slice(0, 5)) {
5412
+ console.log(chalk11.dim(` - ${error}`));
5413
+ }
5414
+ if (result.errors.length > 5) {
5415
+ console.log(chalk11.dim(` ... and ${result.errors.length - 5} more`));
5416
+ }
5417
+ }
5418
+ const newStats = getLessonStats();
5419
+ const newStatus = getQueueStatus2();
5420
+ console.log("");
5421
+ console.log(`Remaining in queue: ${newStatus.pending}`);
5422
+ console.log(`Total lessons: ${newStats.totalLessons}`);
5423
+ try {
5424
+ const cleaned = cleanupOldInjections(7);
5425
+ if (cleaned > 0) {
5426
+ console.log(chalk11.dim(`
5427
+ Cleaned up ${cleaned} old injection cache records`));
5428
+ }
5429
+ } catch {
5430
+ }
5431
+ }
5432
+
5433
+ // src/commands/gui.ts
5434
+ import chalk12 from "chalk";
5435
+
5436
+ // src/api/server.ts
5437
+ import express from "express";
5438
+ import cors from "cors";
5439
+ import { join as join7, dirname as dirname7 } from "path";
5440
+ import { fileURLToPath as fileURLToPath2 } from "url";
5441
+ import { existsSync as existsSync11 } from "fs";
5442
+
5443
+ // src/api/routes.ts
5444
+ import { Router } from "express";
5445
+ var apiRouter = Router();
5446
+ apiRouter.get("/lessons", (req, res) => {
5447
+ try {
5448
+ const {
5449
+ projectPath,
5450
+ category,
5451
+ archived,
5452
+ minConfidence,
5453
+ limit
5454
+ } = req.query;
5455
+ let lessons;
5456
+ if (projectPath) {
5457
+ lessons = getLessonsByProject(projectPath, {
5458
+ category,
5459
+ archived: archived === "true",
5460
+ minConfidence: minConfidence ? parseFloat(minConfidence) : void 0,
5461
+ limit: limit ? parseInt(limit, 10) : void 0
5462
+ });
5463
+ } else {
5464
+ lessons = getAllLessons({
5465
+ category,
5466
+ archived: archived === "true",
5467
+ minConfidence: minConfidence ? parseFloat(minConfidence) : void 0,
5468
+ limit: limit ? parseInt(limit, 10) : void 0
5469
+ });
5470
+ }
5471
+ res.json(lessons);
5472
+ } catch (error) {
5473
+ console.error("Error fetching lessons:", error);
5474
+ res.status(500).json({ message: "Failed to fetch lessons" });
5475
+ }
5476
+ });
5477
+ apiRouter.get("/lessons/:id", (req, res) => {
5478
+ try {
5479
+ const lesson = getLesson(req.params.id);
5480
+ if (!lesson) {
5481
+ res.status(404).json({ message: "Lesson not found" });
5482
+ return;
5483
+ }
5484
+ res.json(lesson);
5485
+ } catch (error) {
5486
+ console.error("Error fetching lesson:", error);
5487
+ res.status(500).json({ message: "Failed to fetch lesson" });
5488
+ }
5489
+ });
5490
+ apiRouter.post("/lessons", (req, res) => {
5491
+ try {
5492
+ const input = req.body;
5493
+ if (!input.projectPath || !input.category || !input.title || !input.triggerContext || !input.insight) {
5494
+ res.status(400).json({ message: "Missing required fields" });
5495
+ return;
5496
+ }
5497
+ const lesson = createLesson(input);
5498
+ res.status(201).json(lesson);
5499
+ } catch (error) {
5500
+ console.error("Error creating lesson:", error);
5501
+ res.status(500).json({ message: "Failed to create lesson" });
5502
+ }
5503
+ });
5504
+ apiRouter.put("/lessons/:id", (req, res) => {
5505
+ try {
5506
+ const existing = getLesson(req.params.id);
5507
+ if (!existing) {
5508
+ res.status(404).json({ message: "Lesson not found" });
5509
+ return;
5510
+ }
5511
+ const updates = req.body;
5512
+ const lesson = updateLesson(req.params.id, updates);
5513
+ res.json(lesson);
5514
+ } catch (error) {
5515
+ console.error("Error updating lesson:", error);
5516
+ res.status(500).json({ message: "Failed to update lesson" });
5517
+ }
5518
+ });
5519
+ apiRouter.delete("/lessons/:id", (req, res) => {
5520
+ try {
5521
+ const existing = getLesson(req.params.id);
5522
+ if (!existing) {
5523
+ res.status(404).json({ message: "Lesson not found" });
5524
+ return;
5525
+ }
5526
+ deleteLesson(req.params.id);
5527
+ res.status(204).send();
5528
+ } catch (error) {
5529
+ console.error("Error deleting lesson:", error);
5530
+ res.status(500).json({ message: "Failed to delete lesson" });
5531
+ }
5532
+ });
5533
+ apiRouter.post("/lessons/:id/validate", (req, res) => {
5534
+ try {
5535
+ const existing = getLesson(req.params.id);
5536
+ if (!existing) {
5537
+ res.status(404).json({ message: "Lesson not found" });
5538
+ return;
5539
+ }
5540
+ const { comment } = req.body;
5541
+ recordLessonValidation(req.params.id, comment);
5542
+ const newConfidence = Math.min(1, existing.confidence + 0.1);
5543
+ const lesson = updateLesson(req.params.id, { confidence: newConfidence });
5544
+ res.json(lesson);
5545
+ } catch (error) {
5546
+ console.error("Error validating lesson:", error);
5547
+ res.status(500).json({ message: "Failed to validate lesson" });
5548
+ }
5549
+ });
5550
+ apiRouter.post("/lessons/:id/reject", (req, res) => {
5551
+ try {
5552
+ const existing = getLesson(req.params.id);
5553
+ if (!existing) {
5554
+ res.status(404).json({ message: "Lesson not found" });
5555
+ return;
5556
+ }
5557
+ const { comment } = req.body;
5558
+ recordLessonRejection(req.params.id, comment);
5559
+ const newConfidence = Math.max(0, existing.confidence - 0.2);
5560
+ if (newConfidence < 0.1) {
5561
+ archiveLesson(req.params.id);
5562
+ }
5563
+ const lesson = updateLesson(req.params.id, { confidence: newConfidence });
5564
+ res.json(lesson);
5565
+ } catch (error) {
5566
+ console.error("Error rejecting lesson:", error);
5567
+ res.status(500).json({ message: "Failed to reject lesson" });
5568
+ }
5569
+ });
5570
+ apiRouter.post("/lessons/:id/archive", (req, res) => {
5571
+ try {
5572
+ const existing = getLesson(req.params.id);
5573
+ if (!existing) {
5574
+ res.status(404).json({ message: "Lesson not found" });
5575
+ return;
5576
+ }
5577
+ archiveLesson(req.params.id);
5578
+ const lesson = getLesson(req.params.id);
5579
+ res.json(lesson);
5580
+ } catch (error) {
5581
+ console.error("Error archiving lesson:", error);
5582
+ res.status(500).json({ message: "Failed to archive lesson" });
5583
+ }
5584
+ });
5585
+ apiRouter.get("/stats", (_req, res) => {
5586
+ try {
5587
+ const lessonStats = getLessonStats();
5588
+ const queueStatus = getQueueStatus();
5589
+ res.json({
5590
+ ...lessonStats,
5591
+ synthesisQueue: queueStatus
5592
+ });
5593
+ } catch (error) {
5594
+ console.error("Error fetching stats:", error);
5595
+ res.status(500).json({ message: "Failed to fetch stats" });
5596
+ }
5597
+ });
5598
+ apiRouter.get("/projects", (_req, res) => {
5599
+ try {
5600
+ const projects = getDistinctProjects();
5601
+ res.json(projects);
5602
+ } catch (error) {
5603
+ console.error("Error fetching projects:", error);
5604
+ res.status(500).json({ message: "Failed to fetch projects" });
5605
+ }
5606
+ });
5607
+
5608
+ // src/api/server.ts
5609
+ var __filename2 = fileURLToPath2(import.meta.url);
5610
+ var __dirname2 = dirname7(__filename2);
5611
+ var DEFAULT_PORT = 3848;
5612
+ async function startServer(options = {}) {
5613
+ const port = options.port ?? DEFAULT_PORT;
5614
+ getDatabase();
5615
+ const app = express();
5616
+ app.use(cors());
5617
+ app.use(express.json());
5618
+ app.use("/api", (_req, res, next) => {
5619
+ res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
5620
+ res.set("Pragma", "no-cache");
5621
+ res.set("Expires", "0");
5622
+ next();
5623
+ });
5624
+ app.use("/api", apiRouter);
5625
+ const guiDistPath = join7(__dirname2, "../gui/dist");
5626
+ if (existsSync11(guiDistPath)) {
5627
+ app.use(express.static(guiDistPath));
5628
+ app.get("/{*path}", (_req, res) => {
5629
+ res.sendFile(join7(guiDistPath, "index.html"));
5630
+ });
5631
+ } else {
5632
+ app.get("/", (_req, res) => {
5633
+ res.json({
5634
+ message: "CMEM API Server",
5635
+ note: "GUI not built. Run: cd gui && npm run build",
5636
+ endpoints: {
5637
+ lessons: "/api/lessons",
5638
+ stats: "/api/stats",
5639
+ projects: "/api/projects"
5640
+ }
5641
+ });
5642
+ });
5643
+ }
5644
+ app.listen(port, () => {
5645
+ console.log(`CMEM GUI server running at http://localhost:${port}`);
5646
+ if (options.open) {
5647
+ const url = `http://localhost:${port}`;
5648
+ const openCommand = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
5649
+ import("child_process").then(({ exec }) => {
5650
+ exec(openCommand);
5651
+ });
5652
+ }
5653
+ });
5654
+ }
5655
+
5656
+ // src/commands/gui.ts
5657
+ async function guiCommand(options) {
5658
+ const port = options.port ? parseInt(options.port, 10) : 3848;
5659
+ console.log(chalk12.cyan("Starting CMEM GUI..."));
5660
+ console.log("");
5661
+ await startServer({
5662
+ port,
5663
+ open: options.open !== false
5664
+ // Default to opening browser
5665
+ });
5666
+ }
5667
+
3679
5668
  // src/cli.ts
3680
5669
  var sessionToResume = null;
3681
5670
  function getVersion() {
3682
5671
  try {
3683
- const __filename2 = fileURLToPath2(import.meta.url);
3684
- const __dirname2 = dirname7(__filename2);
3685
- const packagePath = join7(__dirname2, "..", "package.json");
3686
- if (existsSync11(packagePath)) {
5672
+ const __filename3 = fileURLToPath3(import.meta.url);
5673
+ const __dirname3 = dirname8(__filename3);
5674
+ const packagePath = join8(__dirname3, "..", "package.json");
5675
+ if (existsSync12(packagePath)) {
3687
5676
  const pkg = JSON.parse(readFileSync4(packagePath, "utf-8"));
3688
5677
  return pkg.version || "0.1.0";
3689
5678
  }
@@ -3740,6 +5729,9 @@ program.command("restore <id>").description("Restore a session").option("--copy"
3740
5729
  program.command("delete <id>").description("Delete a session").action(async (id) => {
3741
5730
  await deleteCommand(id);
3742
5731
  });
5732
+ program.command("purge").description("Delete old sessions to free up space").option("-d, --days <n>", "Delete sessions older than N days (default: 30)").option("--dry-run", "Preview what would be deleted without making changes").option("-f, --force", "Skip confirmation prompt").action(async (options) => {
5733
+ await purgeCommand(options);
5734
+ });
3743
5735
  program.command("stats").description("Show storage statistics").action(async () => {
3744
5736
  await statsCommand();
3745
5737
  });
@@ -3752,14 +5744,11 @@ program.command("watch").description("Watch for Claude Code session changes and
3752
5744
  program.command("mcp").description("Start MCP server for Claude Code integration").action(async () => {
3753
5745
  await mcpCommand();
3754
5746
  });
3755
- program.command("install").description("Install watch daemon to run at system startup (macOS)").action(async () => {
3756
- await installCommand();
3757
- });
3758
- program.command("uninstall").description("Remove watch daemon from system startup").action(async () => {
3759
- await uninstallCommand();
5747
+ program.command("synthesize").description("Extract lessons from sessions in the synthesis queue").option("-n, --limit <n>", "Maximum number of sessions to process", "5").option("-s, --status", "Show queue status only").action(async (options) => {
5748
+ await synthesizeCommand(options);
3760
5749
  });
3761
- program.command("status").description("Check watch daemon status").action(async () => {
3762
- await statusCommand();
5750
+ program.command("gui").description("Launch the web-based lesson management interface").option("-p, --port <port>", "Port to run server on", "3848").option("--no-open", "Do not open browser automatically").action(async (options) => {
5751
+ await guiCommand(options);
3763
5752
  });
3764
5753
  program.parse();
3765
5754
  //# sourceMappingURL=cli.js.map