@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/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +11 -0
- package/README.md +157 -111
- package/commands/cmem.md +8 -0
- package/dist/cli.js +2841 -852
- package/dist/cli.js.map +1 -1
- package/dist/hooks/consult.js +1002 -0
- package/dist/hooks/consult.js.map +1 -0
- package/dist/hooks/sync.js +804 -0
- package/dist/hooks/sync.js.map +1 -0
- package/dist/hooks/synthesize.js +1329 -0
- package/dist/hooks/synthesize.js.map +1 -0
- package/dist/mcp/server.js +1850 -0
- package/dist/mcp/server.js.map +1 -0
- package/hooks/hooks.json +38 -0
- package/package.json +14 -7
- package/skills/memory-search/SKILL.md +12 -0
- package/scripts/postinstall.js +0 -46
package/dist/cli.js
CHANGED
|
@@ -1,212 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
199
|
-
import { join as
|
|
200
|
-
import { fileURLToPath as
|
|
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
|
|
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
|
|
209
|
-
import { existsSync as
|
|
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/
|
|
284
|
-
import {
|
|
285
|
-
import {
|
|
286
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
287
|
-
var Header = ({ embeddingsReady, projectFilter }) => {
|
|
288
|
-
return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
289
|
-
/* @__PURE__ */ jsxs(Box, { children: [
|
|
290
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "cmem" }),
|
|
291
|
-
projectFilter && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
292
|
-
" \u{1F4C1} ",
|
|
293
|
-
basename2(projectFilter)
|
|
294
|
-
] }),
|
|
295
|
-
!embeddingsReady && /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: " (loading model...)" })
|
|
296
|
-
] }),
|
|
297
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[q] quit" })
|
|
298
|
-
] });
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
// src/ui/components/SearchInput.tsx
|
|
302
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
303
|
-
import TextInput from "ink-text-input";
|
|
304
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
305
|
-
var SearchInput = ({
|
|
306
|
-
value,
|
|
307
|
-
onChange,
|
|
308
|
-
isFocused
|
|
309
|
-
}) => {
|
|
310
|
-
return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
|
|
311
|
-
/* @__PURE__ */ jsx2(Text2, { children: "Search: " }),
|
|
312
|
-
isFocused ? /* @__PURE__ */ jsx2(
|
|
313
|
-
TextInput,
|
|
314
|
-
{
|
|
315
|
-
value,
|
|
316
|
-
onChange,
|
|
317
|
-
placeholder: "type to search...",
|
|
318
|
-
focus: true
|
|
319
|
-
}
|
|
320
|
-
) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: value || "press / to search" })
|
|
321
|
-
] });
|
|
322
|
-
};
|
|
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/
|
|
325
|
-
import
|
|
326
|
-
import
|
|
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/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return text.slice(0, maxLength - 3) + "...";
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
)
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
1280
|
-
import { join as
|
|
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 =
|
|
1292
|
-
return
|
|
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
|
|
1514
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1357
1515
|
function isRecoverable(session) {
|
|
1358
1516
|
if (!session.sourceFile) return false;
|
|
1359
|
-
if (
|
|
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 (
|
|
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
|
|
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 (!
|
|
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(
|
|
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 (
|
|
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 &&
|
|
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__ */
|
|
1880
|
-
/* @__PURE__ */
|
|
1881
|
-
/* @__PURE__ */
|
|
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
|
-
|
|
1888
|
-
/* @__PURE__ */
|
|
1889
|
-
|
|
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__ */
|
|
1897
|
-
/* @__PURE__ */
|
|
1898
|
-
/* @__PURE__ */
|
|
1899
|
-
] }) : /* @__PURE__ */
|
|
1900
|
-
currentView === "projects" ? /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1916
|
-
selectedProject && /* @__PURE__ */
|
|
1917
|
-
|
|
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__ */
|
|
1927
|
-
/* @__PURE__ */
|
|
1928
|
-
selectedProject.sortOrder !== null && /* @__PURE__ */
|
|
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__ */
|
|
2206
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "blue", children: [
|
|
1935
2207
|
"\u{1F4C1} ",
|
|
1936
2208
|
selectedProject.path
|
|
1937
2209
|
] }),
|
|
1938
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1954
|
-
/* @__PURE__ */
|
|
1955
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1964
|
-
] }) : mode === "sort-project" ? /* @__PURE__ */
|
|
1965
|
-
/* @__PURE__ */
|
|
1966
|
-
/* @__PURE__ */
|
|
1967
|
-
|
|
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__ */
|
|
1977
|
-
/* @__PURE__ */
|
|
1978
|
-
|
|
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__ */
|
|
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
|
|
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
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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
|
|
2323
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
2787
|
+
const entries = readdirSync3(currentDir, { withFileTypes: true });
|
|
2462
2788
|
for (const entry of entries) {
|
|
2463
|
-
const fullPath =
|
|
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 =
|
|
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(
|
|
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 =
|
|
2580
|
-
if (!
|
|
2905
|
+
const subagentsDir = join5(projectDirPath, parentSessionId, "subagents");
|
|
2906
|
+
if (!existsSync9(subagentsDir)) return [];
|
|
2581
2907
|
const messages = [];
|
|
2582
2908
|
try {
|
|
2583
|
-
const agentFiles =
|
|
2909
|
+
const agentFiles = readdirSync3(subagentsDir).filter((f) => f.endsWith(".jsonl"));
|
|
2584
2910
|
for (const agentFile of agentFiles) {
|
|
2585
|
-
const agentPath =
|
|
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 =
|
|
2595
|
-
const indexPath =
|
|
2596
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
});
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
}
|
|
2784
|
-
|
|
2785
|
-
|
|
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
|
|
3205
|
-
import { execSync as
|
|
3206
|
-
import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync
|
|
3207
|
-
import { homedir as
|
|
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(
|
|
3214
|
-
var CLAUDE_SETTINGS_PATH = join6(
|
|
3215
|
-
var CMEM_DIR2 = join6(
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
4488
|
+
console.log(`\r ${chalk9.green("\u2713")} ${message || this.message} `);
|
|
3246
4489
|
}
|
|
3247
4490
|
fail(message) {
|
|
3248
4491
|
this.stop();
|
|
3249
|
-
console.log(`\r ${
|
|
4492
|
+
console.log(`\r ${chalk9.red("\u2717")} ${message || this.message} `);
|
|
3250
4493
|
}
|
|
3251
4494
|
warn(message) {
|
|
3252
4495
|
this.stop();
|
|
3253
|
-
console.log(`\r ${
|
|
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 =
|
|
3264
|
-
const cyan =
|
|
3265
|
-
const 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(
|
|
4528
|
+
console.log(chalk9.dim(` ${i + 1}) ${opt}`));
|
|
3286
4529
|
});
|
|
3287
4530
|
console.log("");
|
|
3288
4531
|
return new Promise((resolve) => {
|
|
3289
|
-
rl.question(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
3362
|
-
console.log(
|
|
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(
|
|
4648
|
+
console.log(chalk9.dim("\n Installing @colbymchenry/cmem globally...\n"));
|
|
3369
4649
|
try {
|
|
3370
|
-
|
|
3371
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
4662
|
+
console.log(chalk9.dim("\n Updating @colbymchenry/cmem...\n"));
|
|
3383
4663
|
try {
|
|
3384
|
-
|
|
3385
|
-
console.log(
|
|
4664
|
+
execSync2("npm install -g @colbymchenry/cmem", { stdio: "inherit" });
|
|
4665
|
+
console.log(chalk9.green("\n \u2713 Updated\n"));
|
|
3386
4666
|
} catch {
|
|
3387
|
-
console.log(
|
|
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(
|
|
4673
|
+
console.log(chalk9.green(" \u2713 cmem is installed globally\n"));
|
|
3394
4674
|
}
|
|
3395
|
-
console.log(
|
|
3396
|
-
console.log(
|
|
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(
|
|
4697
|
+
console.log(chalk9.dim(` Error: ${err}
|
|
3418
4698
|
`));
|
|
3419
4699
|
}
|
|
3420
4700
|
}
|
|
3421
4701
|
console.log("");
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
);
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
console.log(
|
|
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 -
|
|
3447
|
-
"No - I'll
|
|
4709
|
+
"Yes - configure automatically (recommended)",
|
|
4710
|
+
"No - I'll configure it manually"
|
|
3448
4711
|
], 1);
|
|
3449
4712
|
if (choice === 1) {
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
console.log(
|
|
3455
|
-
|
|
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(
|
|
4722
|
+
console.log(chalk9.dim("\n Skipped.\n"));
|
|
3459
4723
|
}
|
|
3460
4724
|
}
|
|
3461
|
-
const
|
|
3462
|
-
if (
|
|
3463
|
-
console.log(
|
|
4725
|
+
const hooksConfigured = areHooksConfigured();
|
|
4726
|
+
if (hooksConfigured) {
|
|
4727
|
+
console.log(chalk9.green(" \u2713 Learning hooks configured\n"));
|
|
3464
4728
|
} else {
|
|
3465
|
-
console.log(
|
|
3466
|
-
console.log(
|
|
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 -
|
|
3469
|
-
"No - I'll
|
|
4732
|
+
"Yes - enable hooks (recommended)",
|
|
4733
|
+
"No - I'll manage manually"
|
|
3470
4734
|
], 1);
|
|
3471
4735
|
if (choice === 1) {
|
|
3472
|
-
const
|
|
3473
|
-
if (success) {
|
|
3474
|
-
console.log(
|
|
3475
|
-
console.log(
|
|
3476
|
-
console.log(
|
|
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(
|
|
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(
|
|
4747
|
+
console.log(chalk9.dim("\n Skipped. Run `cmem watch` for manual sync.\n"));
|
|
3482
4748
|
}
|
|
3483
4749
|
}
|
|
3484
|
-
console.log(
|
|
3485
|
-
console.log(
|
|
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
|
-
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
console.log(
|
|
3492
|
-
console.log(
|
|
3493
|
-
console.log(
|
|
3494
|
-
console.log(
|
|
3495
|
-
console.log(
|
|
3496
|
-
console.log(
|
|
3497
|
-
console.log(
|
|
3498
|
-
console.log(
|
|
3499
|
-
console.log(
|
|
3500
|
-
console.log(
|
|
3501
|
-
console.log(
|
|
3502
|
-
console.log(
|
|
3503
|
-
if (
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4830
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
3565
4831
|
return true;
|
|
3566
4832
|
} catch (err) {
|
|
3567
|
-
console.error(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
3684
|
-
const
|
|
3685
|
-
const packagePath =
|
|
3686
|
-
if (
|
|
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("
|
|
3756
|
-
await
|
|
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("
|
|
3762
|
-
await
|
|
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
|