claude-memory-layer 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1373 -184
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +445 -7
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +705 -43
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +593 -52
- package/dist/hooks/session-end.js.map +3 -3
- package/dist/hooks/session-start.js +581 -25
- package/dist/hooks/session-start.js.map +3 -3
- package/dist/hooks/stop.js +693 -73
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +674 -94
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1045 -42
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1054 -51
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +599 -25
- package/dist/services/memory-service.js.map +3 -3
- package/dist/ui/app.js +1380 -55
- package/dist/ui/index.html +311 -148
- package/dist/ui/style.css +892 -0
- package/docs/OPERATIONS.md +18 -0
- package/package.json +8 -2
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +110 -58
- package/src/core/sqlite-event-store.ts +542 -6
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +23 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +78 -65
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/index.ts +7 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +43 -7
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/memory-service.ts +208 -9
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1380 -55
- package/src/ui/index.html +311 -148
- package/src/ui/style.css +892 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/test_access.js +0 -49
package/dist/cli/index.js
CHANGED
|
@@ -5,18 +5,25 @@ import { dirname } from 'path';
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined")
|
|
12
|
+
return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
8
15
|
|
|
9
16
|
// src/cli/index.ts
|
|
10
17
|
import { Command } from "commander";
|
|
11
18
|
import { exec } from "child_process";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
14
|
-
import * as
|
|
19
|
+
import * as fs6 from "fs";
|
|
20
|
+
import * as path6 from "path";
|
|
21
|
+
import * as os5 from "os";
|
|
15
22
|
|
|
16
23
|
// src/services/memory-service.ts
|
|
17
24
|
import * as path from "path";
|
|
18
25
|
import * as os from "os";
|
|
19
|
-
import * as
|
|
26
|
+
import * as fs2 from "fs";
|
|
20
27
|
import * as crypto2 from "crypto";
|
|
21
28
|
|
|
22
29
|
// src/core/event-store.ts
|
|
@@ -74,11 +81,11 @@ function toDate(value) {
|
|
|
74
81
|
return new Date(value);
|
|
75
82
|
return new Date(String(value));
|
|
76
83
|
}
|
|
77
|
-
function createDatabase(
|
|
84
|
+
function createDatabase(path7, options) {
|
|
78
85
|
if (options?.readOnly) {
|
|
79
|
-
return new duckdb.Database(
|
|
86
|
+
return new duckdb.Database(path7, { access_mode: "READ_ONLY" });
|
|
80
87
|
}
|
|
81
|
-
return new duckdb.Database(
|
|
88
|
+
return new duckdb.Database(path7);
|
|
82
89
|
}
|
|
83
90
|
function dbRun(db, sql, params = []) {
|
|
84
91
|
return new Promise((resolve2, reject) => {
|
|
@@ -748,8 +755,14 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
748
755
|
|
|
749
756
|
// src/core/sqlite-wrapper.ts
|
|
750
757
|
import Database from "better-sqlite3";
|
|
751
|
-
|
|
752
|
-
|
|
758
|
+
import * as fs from "fs";
|
|
759
|
+
import * as nodePath from "path";
|
|
760
|
+
function createSQLiteDatabase(path7, options) {
|
|
761
|
+
const dir = nodePath.dirname(path7);
|
|
762
|
+
if (!fs.existsSync(dir)) {
|
|
763
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
764
|
+
}
|
|
765
|
+
const db = new Database(path7, {
|
|
753
766
|
readonly: options?.readonly ?? false
|
|
754
767
|
});
|
|
755
768
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -1016,6 +1029,23 @@ var SQLiteEventStore = class {
|
|
|
1016
1029
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
1017
1030
|
);
|
|
1018
1031
|
|
|
1032
|
+
-- Memory Helpfulness tracking
|
|
1033
|
+
CREATE TABLE IF NOT EXISTS memory_helpfulness (
|
|
1034
|
+
id TEXT PRIMARY KEY,
|
|
1035
|
+
event_id TEXT NOT NULL,
|
|
1036
|
+
session_id TEXT NOT NULL,
|
|
1037
|
+
retrieval_score REAL DEFAULT 0,
|
|
1038
|
+
query_preview TEXT,
|
|
1039
|
+
session_continued INTEGER DEFAULT 0,
|
|
1040
|
+
prompt_count_after INTEGER DEFAULT 0,
|
|
1041
|
+
tool_success_count INTEGER DEFAULT 0,
|
|
1042
|
+
tool_total_count INTEGER DEFAULT 0,
|
|
1043
|
+
was_reasked INTEGER DEFAULT 0,
|
|
1044
|
+
helpfulness_score REAL DEFAULT 0.5,
|
|
1045
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1046
|
+
measured_at TEXT
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1019
1049
|
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
1020
1050
|
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
1021
1051
|
target_name TEXT PRIMARY KEY,
|
|
@@ -1041,6 +1071,31 @@ var SQLiteEventStore = class {
|
|
|
1041
1071
|
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
1042
1072
|
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
1043
1073
|
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
1074
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
|
|
1075
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
|
|
1076
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
|
|
1077
|
+
|
|
1078
|
+
-- FTS5 Full-Text Search for fast keyword search
|
|
1079
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
1080
|
+
content,
|
|
1081
|
+
event_id UNINDEXED,
|
|
1082
|
+
content='events',
|
|
1083
|
+
content_rowid='rowid'
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
-- Triggers to keep FTS in sync with events table
|
|
1087
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
1088
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1089
|
+
END;
|
|
1090
|
+
|
|
1091
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
1092
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
1093
|
+
END;
|
|
1094
|
+
|
|
1095
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
1096
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
1097
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1098
|
+
END;
|
|
1044
1099
|
`);
|
|
1045
1100
|
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
1046
1101
|
const columnNames = tableInfo.map((col) => col.name);
|
|
@@ -1062,6 +1117,15 @@ var SQLiteEventStore = class {
|
|
|
1062
1117
|
console.error("Error adding last_accessed_at column:", err);
|
|
1063
1118
|
}
|
|
1064
1119
|
}
|
|
1120
|
+
if (!columnNames.includes("turn_id")) {
|
|
1121
|
+
try {
|
|
1122
|
+
sqliteExec(this.db, `
|
|
1123
|
+
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
1124
|
+
`);
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
console.error("Error adding turn_id column:", err);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1065
1129
|
try {
|
|
1066
1130
|
sqliteExec(this.db, `
|
|
1067
1131
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -1074,6 +1138,12 @@ var SQLiteEventStore = class {
|
|
|
1074
1138
|
`);
|
|
1075
1139
|
} catch (err) {
|
|
1076
1140
|
}
|
|
1141
|
+
try {
|
|
1142
|
+
sqliteExec(this.db, `
|
|
1143
|
+
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
1144
|
+
`);
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
}
|
|
1077
1147
|
this.initialized = true;
|
|
1078
1148
|
}
|
|
1079
1149
|
/**
|
|
@@ -1098,9 +1168,11 @@ var SQLiteEventStore = class {
|
|
|
1098
1168
|
const id = randomUUID2();
|
|
1099
1169
|
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1100
1170
|
try {
|
|
1171
|
+
const metadata = input.metadata || {};
|
|
1172
|
+
const turnId = metadata.turnId || null;
|
|
1101
1173
|
const insertEvent = this.db.prepare(`
|
|
1102
|
-
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1103
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1174
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1175
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1104
1176
|
`);
|
|
1105
1177
|
const insertDedup = this.db.prepare(`
|
|
1106
1178
|
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
@@ -1117,7 +1189,8 @@ var SQLiteEventStore = class {
|
|
|
1117
1189
|
input.content,
|
|
1118
1190
|
canonicalKey,
|
|
1119
1191
|
dedupeKey,
|
|
1120
|
-
JSON.stringify(
|
|
1192
|
+
JSON.stringify(metadata),
|
|
1193
|
+
turnId
|
|
1121
1194
|
);
|
|
1122
1195
|
insertDedup.run(dedupeKey, id);
|
|
1123
1196
|
insertLevel.run(id);
|
|
@@ -1467,11 +1540,11 @@ var SQLiteEventStore = class {
|
|
|
1467
1540
|
);
|
|
1468
1541
|
}
|
|
1469
1542
|
/**
|
|
1470
|
-
* Get most accessed memories
|
|
1543
|
+
* Get most accessed memories (falls back to recent events if none accessed)
|
|
1471
1544
|
*/
|
|
1472
1545
|
async getMostAccessed(limit = 10) {
|
|
1473
1546
|
await this.initialize();
|
|
1474
|
-
|
|
1547
|
+
let rows = sqliteAll(
|
|
1475
1548
|
this.db,
|
|
1476
1549
|
`SELECT * FROM events
|
|
1477
1550
|
WHERE access_count > 0
|
|
@@ -1479,8 +1552,222 @@ var SQLiteEventStore = class {
|
|
|
1479
1552
|
LIMIT ?`,
|
|
1480
1553
|
[limit]
|
|
1481
1554
|
);
|
|
1555
|
+
if (rows.length === 0) {
|
|
1556
|
+
rows = sqliteAll(
|
|
1557
|
+
this.db,
|
|
1558
|
+
`SELECT * FROM events
|
|
1559
|
+
ORDER BY timestamp DESC
|
|
1560
|
+
LIMIT ?`,
|
|
1561
|
+
[limit]
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1482
1564
|
return rows.map((row) => this.rowToEvent(row));
|
|
1483
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Record a memory retrieval for helpfulness tracking
|
|
1568
|
+
*/
|
|
1569
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
1570
|
+
if (this.readOnly)
|
|
1571
|
+
return;
|
|
1572
|
+
await this.initialize();
|
|
1573
|
+
const id = randomUUID2();
|
|
1574
|
+
sqliteRun(
|
|
1575
|
+
this.db,
|
|
1576
|
+
`INSERT INTO memory_helpfulness (id, event_id, session_id, retrieval_score, query_preview, created_at)
|
|
1577
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
1578
|
+
[id, eventId, sessionId, score, query.slice(0, 100)]
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Evaluate helpfulness for all retrievals in a session
|
|
1583
|
+
* Called at session end - uses behavioral signals to compute score
|
|
1584
|
+
*/
|
|
1585
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
1586
|
+
if (this.readOnly)
|
|
1587
|
+
return;
|
|
1588
|
+
await this.initialize();
|
|
1589
|
+
const retrievals = sqliteAll(
|
|
1590
|
+
this.db,
|
|
1591
|
+
`SELECT * FROM memory_helpfulness WHERE session_id = ? AND measured_at IS NULL`,
|
|
1592
|
+
[sessionId]
|
|
1593
|
+
);
|
|
1594
|
+
if (retrievals.length === 0)
|
|
1595
|
+
return;
|
|
1596
|
+
const sessionEvents = sqliteAll(
|
|
1597
|
+
this.db,
|
|
1598
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
1599
|
+
[sessionId]
|
|
1600
|
+
);
|
|
1601
|
+
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
1602
|
+
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
1603
|
+
let toolSuccessCount = 0;
|
|
1604
|
+
let toolTotalCount = toolEvents.length;
|
|
1605
|
+
for (const t of toolEvents) {
|
|
1606
|
+
try {
|
|
1607
|
+
const content = JSON.parse(t.content);
|
|
1608
|
+
if (content.success !== false)
|
|
1609
|
+
toolSuccessCount++;
|
|
1610
|
+
} catch {
|
|
1611
|
+
toolSuccessCount++;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const toolSuccessRatio = toolTotalCount > 0 ? toolSuccessCount / toolTotalCount : 0.5;
|
|
1615
|
+
for (const retrieval of retrievals) {
|
|
1616
|
+
const retrievalTime = retrieval.created_at;
|
|
1617
|
+
const eventsAfter = sessionEvents.filter((e) => e.timestamp > retrievalTime);
|
|
1618
|
+
const sessionContinued = eventsAfter.length > 0 ? 1 : 0;
|
|
1619
|
+
const promptsAfter = promptEvents.filter((e) => e.timestamp > retrievalTime);
|
|
1620
|
+
const promptCountAfter = promptsAfter.length;
|
|
1621
|
+
const queryWords = new Set((retrieval.query_preview || "").toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1622
|
+
let wasReasked = 0;
|
|
1623
|
+
for (const p of promptsAfter) {
|
|
1624
|
+
const pWords = new Set(p.content.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1625
|
+
let overlap = 0;
|
|
1626
|
+
for (const w of queryWords) {
|
|
1627
|
+
if (pWords.has(w))
|
|
1628
|
+
overlap++;
|
|
1629
|
+
}
|
|
1630
|
+
if (queryWords.size > 0 && overlap / queryWords.size > 0.5) {
|
|
1631
|
+
wasReasked = 1;
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const retrievalScore = retrieval.retrieval_score || 0;
|
|
1636
|
+
const helpfulnessScore = 0.3 * Math.min(retrievalScore, 1) + 0.25 * (sessionContinued ? 1 : 0) + 0.25 * toolSuccessRatio + 0.2 * (wasReasked ? 0 : 1);
|
|
1637
|
+
sqliteRun(
|
|
1638
|
+
this.db,
|
|
1639
|
+
`UPDATE memory_helpfulness
|
|
1640
|
+
SET session_continued = ?, prompt_count_after = ?,
|
|
1641
|
+
tool_success_count = ?, tool_total_count = ?,
|
|
1642
|
+
was_reasked = ?, helpfulness_score = ?,
|
|
1643
|
+
measured_at = datetime('now')
|
|
1644
|
+
WHERE id = ?`,
|
|
1645
|
+
[
|
|
1646
|
+
sessionContinued,
|
|
1647
|
+
promptCountAfter,
|
|
1648
|
+
toolSuccessCount,
|
|
1649
|
+
toolTotalCount,
|
|
1650
|
+
wasReasked,
|
|
1651
|
+
helpfulnessScore,
|
|
1652
|
+
retrieval.id
|
|
1653
|
+
]
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Get most helpful memories ranked by helpfulness score
|
|
1659
|
+
*/
|
|
1660
|
+
async getHelpfulMemories(limit = 10) {
|
|
1661
|
+
await this.initialize();
|
|
1662
|
+
const rows = sqliteAll(
|
|
1663
|
+
this.db,
|
|
1664
|
+
`SELECT
|
|
1665
|
+
mh.event_id,
|
|
1666
|
+
AVG(mh.helpfulness_score) as avg_score,
|
|
1667
|
+
COUNT(*) as eval_count,
|
|
1668
|
+
e.content,
|
|
1669
|
+
e.access_count
|
|
1670
|
+
FROM memory_helpfulness mh
|
|
1671
|
+
JOIN events e ON e.id = mh.event_id
|
|
1672
|
+
WHERE mh.measured_at IS NOT NULL
|
|
1673
|
+
GROUP BY mh.event_id
|
|
1674
|
+
ORDER BY avg_score DESC
|
|
1675
|
+
LIMIT ?`,
|
|
1676
|
+
[limit]
|
|
1677
|
+
);
|
|
1678
|
+
return rows.map((r) => ({
|
|
1679
|
+
eventId: r.event_id,
|
|
1680
|
+
summary: r.content.substring(0, 200) + (r.content.length > 200 ? "..." : ""),
|
|
1681
|
+
helpfulnessScore: Math.round(r.avg_score * 100) / 100,
|
|
1682
|
+
accessCount: r.access_count || 0,
|
|
1683
|
+
evaluationCount: r.eval_count
|
|
1684
|
+
}));
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Get helpfulness statistics for dashboard
|
|
1688
|
+
*/
|
|
1689
|
+
async getHelpfulnessStats() {
|
|
1690
|
+
await this.initialize();
|
|
1691
|
+
const stats = sqliteGet(
|
|
1692
|
+
this.db,
|
|
1693
|
+
`SELECT
|
|
1694
|
+
AVG(helpfulness_score) as avg_score,
|
|
1695
|
+
COUNT(*) as total_evaluated,
|
|
1696
|
+
SUM(CASE WHEN helpfulness_score >= 0.7 THEN 1 ELSE 0 END) as helpful,
|
|
1697
|
+
SUM(CASE WHEN helpfulness_score >= 0.4 AND helpfulness_score < 0.7 THEN 1 ELSE 0 END) as neutral,
|
|
1698
|
+
SUM(CASE WHEN helpfulness_score < 0.4 THEN 1 ELSE 0 END) as unhelpful
|
|
1699
|
+
FROM memory_helpfulness
|
|
1700
|
+
WHERE measured_at IS NOT NULL`
|
|
1701
|
+
);
|
|
1702
|
+
const totalRow = sqliteGet(
|
|
1703
|
+
this.db,
|
|
1704
|
+
`SELECT COUNT(*) as total FROM memory_helpfulness`
|
|
1705
|
+
);
|
|
1706
|
+
return {
|
|
1707
|
+
avgScore: Math.round((stats?.avg_score || 0) * 100) / 100,
|
|
1708
|
+
totalEvaluated: stats?.total_evaluated || 0,
|
|
1709
|
+
totalRetrievals: totalRow?.total || 0,
|
|
1710
|
+
helpful: stats?.helpful || 0,
|
|
1711
|
+
neutral: stats?.neutral || 0,
|
|
1712
|
+
unhelpful: stats?.unhelpful || 0
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Fast keyword search using FTS5
|
|
1717
|
+
* Returns events matching the search query, ranked by relevance
|
|
1718
|
+
*/
|
|
1719
|
+
async keywordSearch(query, limit = 10) {
|
|
1720
|
+
await this.initialize();
|
|
1721
|
+
const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
|
|
1722
|
+
if (!searchTerms) {
|
|
1723
|
+
return [];
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
const rows = sqliteAll(
|
|
1727
|
+
this.db,
|
|
1728
|
+
`SELECT e.*, fts.rank
|
|
1729
|
+
FROM events_fts fts
|
|
1730
|
+
JOIN events e ON e.id = fts.event_id
|
|
1731
|
+
WHERE events_fts MATCH ?
|
|
1732
|
+
ORDER BY fts.rank
|
|
1733
|
+
LIMIT ?`,
|
|
1734
|
+
[searchTerms, limit]
|
|
1735
|
+
);
|
|
1736
|
+
return rows.map((row) => ({
|
|
1737
|
+
event: this.rowToEvent(row),
|
|
1738
|
+
rank: row.rank
|
|
1739
|
+
}));
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
const likePattern = `%${query}%`;
|
|
1742
|
+
const rows = sqliteAll(
|
|
1743
|
+
this.db,
|
|
1744
|
+
`SELECT *, 0 as rank FROM events
|
|
1745
|
+
WHERE content LIKE ?
|
|
1746
|
+
ORDER BY timestamp DESC
|
|
1747
|
+
LIMIT ?`,
|
|
1748
|
+
[likePattern, limit]
|
|
1749
|
+
);
|
|
1750
|
+
return rows.map((row) => ({
|
|
1751
|
+
event: this.rowToEvent(row),
|
|
1752
|
+
rank: 0
|
|
1753
|
+
}));
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Rebuild FTS index from existing events
|
|
1758
|
+
* Call this once after upgrading to FTS5
|
|
1759
|
+
*/
|
|
1760
|
+
async rebuildFtsIndex() {
|
|
1761
|
+
await this.initialize();
|
|
1762
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
1763
|
+
const totalEvents = countRow?.count ?? 0;
|
|
1764
|
+
sqliteExec(this.db, `
|
|
1765
|
+
DELETE FROM events_fts;
|
|
1766
|
+
INSERT INTO events_fts(rowid, content, event_id)
|
|
1767
|
+
SELECT rowid, content, id FROM events;
|
|
1768
|
+
`);
|
|
1769
|
+
return totalEvents;
|
|
1770
|
+
}
|
|
1484
1771
|
/**
|
|
1485
1772
|
* Get database instance for direct access
|
|
1486
1773
|
*/
|
|
@@ -1493,6 +1780,143 @@ var SQLiteEventStore = class {
|
|
|
1493
1780
|
async close() {
|
|
1494
1781
|
sqliteClose(this.db);
|
|
1495
1782
|
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Get events grouped by turn_id for a session
|
|
1785
|
+
* Returns turns ordered by first event timestamp (newest first)
|
|
1786
|
+
*/
|
|
1787
|
+
async getSessionTurns(sessionId, options) {
|
|
1788
|
+
await this.initialize();
|
|
1789
|
+
const limit = options?.limit || 20;
|
|
1790
|
+
const offset = options?.offset || 0;
|
|
1791
|
+
const turnRows = sqliteAll(
|
|
1792
|
+
this.db,
|
|
1793
|
+
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
1794
|
+
FROM events
|
|
1795
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
1796
|
+
GROUP BY turn_id
|
|
1797
|
+
ORDER BY min_ts DESC
|
|
1798
|
+
LIMIT ? OFFSET ?`,
|
|
1799
|
+
[sessionId, limit, offset]
|
|
1800
|
+
);
|
|
1801
|
+
const turns = [];
|
|
1802
|
+
for (const turnRow of turnRows) {
|
|
1803
|
+
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
1804
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
1805
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
1806
|
+
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
1807
|
+
turns.push({
|
|
1808
|
+
turnId: turnRow.turn_id,
|
|
1809
|
+
events,
|
|
1810
|
+
startedAt: toDateFromSQLite(turnRow.min_ts),
|
|
1811
|
+
promptPreview: promptEvent ? promptEvent.content.slice(0, 200) + (promptEvent.content.length > 200 ? "..." : "") : "(no prompt)",
|
|
1812
|
+
eventCount: events.length,
|
|
1813
|
+
toolCount: toolEvents.length,
|
|
1814
|
+
hasResponse
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
return turns;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Get all events for a specific turn_id
|
|
1821
|
+
*/
|
|
1822
|
+
async getEventsByTurn(turnId) {
|
|
1823
|
+
await this.initialize();
|
|
1824
|
+
const rows = sqliteAll(
|
|
1825
|
+
this.db,
|
|
1826
|
+
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
1827
|
+
[turnId]
|
|
1828
|
+
);
|
|
1829
|
+
return rows.map(this.rowToEvent);
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Count total turns for a session
|
|
1833
|
+
*/
|
|
1834
|
+
async countSessionTurns(sessionId) {
|
|
1835
|
+
await this.initialize();
|
|
1836
|
+
const row = sqliteGet(
|
|
1837
|
+
this.db,
|
|
1838
|
+
`SELECT COUNT(DISTINCT turn_id) as count
|
|
1839
|
+
FROM events
|
|
1840
|
+
WHERE session_id = ? AND turn_id IS NOT NULL`,
|
|
1841
|
+
[sessionId]
|
|
1842
|
+
);
|
|
1843
|
+
return row?.count || 0;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Migrate existing events: backfill turn_id for events that have turnId in metadata
|
|
1847
|
+
* but no turn_id column value (for events stored before this migration)
|
|
1848
|
+
*/
|
|
1849
|
+
async backfillTurnIds() {
|
|
1850
|
+
await this.initialize();
|
|
1851
|
+
const rows = sqliteAll(
|
|
1852
|
+
this.db,
|
|
1853
|
+
`SELECT id, metadata FROM events
|
|
1854
|
+
WHERE turn_id IS NULL AND metadata IS NOT NULL AND metadata LIKE '%turnId%'`
|
|
1855
|
+
);
|
|
1856
|
+
let updated = 0;
|
|
1857
|
+
for (const row of rows) {
|
|
1858
|
+
try {
|
|
1859
|
+
const metadata = JSON.parse(row.metadata);
|
|
1860
|
+
if (metadata.turnId) {
|
|
1861
|
+
sqliteRun(
|
|
1862
|
+
this.db,
|
|
1863
|
+
`UPDATE events SET turn_id = ? WHERE id = ?`,
|
|
1864
|
+
[metadata.turnId, row.id]
|
|
1865
|
+
);
|
|
1866
|
+
updated++;
|
|
1867
|
+
}
|
|
1868
|
+
} catch {
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return updated;
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Delete all events for a session (for force reimport)
|
|
1875
|
+
*/
|
|
1876
|
+
async deleteSessionEvents(sessionId) {
|
|
1877
|
+
await this.initialize();
|
|
1878
|
+
const events = sqliteAll(
|
|
1879
|
+
this.db,
|
|
1880
|
+
`SELECT id FROM events WHERE session_id = ?`,
|
|
1881
|
+
[sessionId]
|
|
1882
|
+
);
|
|
1883
|
+
if (events.length === 0)
|
|
1884
|
+
return 0;
|
|
1885
|
+
const eventIds = events.map((e) => e.id);
|
|
1886
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
1887
|
+
const ftsTriggersDropped = [];
|
|
1888
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
1889
|
+
try {
|
|
1890
|
+
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
1891
|
+
ftsTriggersDropped.push(triggerName);
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
1896
|
+
try {
|
|
1897
|
+
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
1898
|
+
} catch {
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
1902
|
+
if (ftsTriggersDropped.length > 0) {
|
|
1903
|
+
try {
|
|
1904
|
+
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
1905
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
1906
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
1907
|
+
END`);
|
|
1908
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
1909
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
1910
|
+
END`);
|
|
1911
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
1912
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
1913
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
1914
|
+
END`);
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return result.changes || 0;
|
|
1919
|
+
}
|
|
1496
1920
|
/**
|
|
1497
1921
|
* Convert database row to MemoryEvent
|
|
1498
1922
|
*/
|
|
@@ -1513,6 +1937,9 @@ var SQLiteEventStore = class {
|
|
|
1513
1937
|
if (row.last_accessed_at !== void 0) {
|
|
1514
1938
|
event.last_accessed_at = row.last_accessed_at;
|
|
1515
1939
|
}
|
|
1940
|
+
if (row.turn_id !== void 0 && row.turn_id !== null) {
|
|
1941
|
+
event.turn_id = row.turn_id;
|
|
1942
|
+
}
|
|
1516
1943
|
return event;
|
|
1517
1944
|
}
|
|
1518
1945
|
};
|
|
@@ -1724,7 +2151,16 @@ var VectorStore = class {
|
|
|
1724
2151
|
metadata: JSON.stringify(record.metadata || {})
|
|
1725
2152
|
};
|
|
1726
2153
|
if (!this.table) {
|
|
1727
|
-
|
|
2154
|
+
try {
|
|
2155
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
2156
|
+
} catch (e) {
|
|
2157
|
+
if (e?.message?.includes("already exists")) {
|
|
2158
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2159
|
+
await this.table.add([data]);
|
|
2160
|
+
} else {
|
|
2161
|
+
throw e;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
1728
2164
|
} else {
|
|
1729
2165
|
await this.table.add([data]);
|
|
1730
2166
|
}
|
|
@@ -1750,7 +2186,16 @@ var VectorStore = class {
|
|
|
1750
2186
|
metadata: JSON.stringify(record.metadata || {})
|
|
1751
2187
|
}));
|
|
1752
2188
|
if (!this.table) {
|
|
1753
|
-
|
|
2189
|
+
try {
|
|
2190
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
2191
|
+
} catch (e) {
|
|
2192
|
+
if (e?.message?.includes("already exists")) {
|
|
2193
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2194
|
+
await this.table.add(data);
|
|
2195
|
+
} else {
|
|
2196
|
+
throw e;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1754
2199
|
} else {
|
|
1755
2200
|
await this.table.add(data);
|
|
1756
2201
|
}
|
|
@@ -4496,7 +4941,7 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
4496
4941
|
function normalizePath(projectPath) {
|
|
4497
4942
|
const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
4498
4943
|
try {
|
|
4499
|
-
return
|
|
4944
|
+
return fs2.realpathSync(expanded);
|
|
4500
4945
|
} catch {
|
|
4501
4946
|
return path.resolve(expanded);
|
|
4502
4947
|
}
|
|
@@ -4511,6 +4956,42 @@ function getProjectStoragePath(projectPath) {
|
|
|
4511
4956
|
}
|
|
4512
4957
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
4513
4958
|
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
4959
|
+
function loadSessionRegistry() {
|
|
4960
|
+
try {
|
|
4961
|
+
if (fs2.existsSync(REGISTRY_PATH)) {
|
|
4962
|
+
const data = fs2.readFileSync(REGISTRY_PATH, "utf-8");
|
|
4963
|
+
return JSON.parse(data);
|
|
4964
|
+
}
|
|
4965
|
+
} catch (error) {
|
|
4966
|
+
console.error("Failed to load session registry:", error);
|
|
4967
|
+
}
|
|
4968
|
+
return { version: 1, sessions: {} };
|
|
4969
|
+
}
|
|
4970
|
+
function saveSessionRegistry(registry) {
|
|
4971
|
+
const dir = path.dirname(REGISTRY_PATH);
|
|
4972
|
+
if (!fs2.existsSync(dir)) {
|
|
4973
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
4974
|
+
}
|
|
4975
|
+
const tempPath = REGISTRY_PATH + ".tmp";
|
|
4976
|
+
fs2.writeFileSync(tempPath, JSON.stringify(registry, null, 2));
|
|
4977
|
+
fs2.renameSync(tempPath, REGISTRY_PATH);
|
|
4978
|
+
}
|
|
4979
|
+
function registerSession(sessionId, projectPath) {
|
|
4980
|
+
const registry = loadSessionRegistry();
|
|
4981
|
+
registry.sessions[sessionId] = {
|
|
4982
|
+
projectPath: normalizePath(projectPath),
|
|
4983
|
+
projectHash: hashProjectPath(projectPath),
|
|
4984
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4985
|
+
};
|
|
4986
|
+
const entries = Object.entries(registry.sessions);
|
|
4987
|
+
if (entries.length > 1e3) {
|
|
4988
|
+
const sorted = entries.sort(
|
|
4989
|
+
(a, b) => new Date(b[1].registeredAt).getTime() - new Date(a[1].registeredAt).getTime()
|
|
4990
|
+
);
|
|
4991
|
+
registry.sessions = Object.fromEntries(sorted.slice(0, 1e3));
|
|
4992
|
+
}
|
|
4993
|
+
saveSessionRegistry(registry);
|
|
4994
|
+
}
|
|
4514
4995
|
var MemoryService = class {
|
|
4515
4996
|
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4516
4997
|
sqliteStore;
|
|
@@ -4539,11 +5020,13 @@ var MemoryService = class {
|
|
|
4539
5020
|
sharedStoreConfig = null;
|
|
4540
5021
|
projectHash = null;
|
|
4541
5022
|
readOnly;
|
|
5023
|
+
lightweightMode;
|
|
4542
5024
|
constructor(config) {
|
|
4543
5025
|
const storagePath = this.expandPath(config.storagePath);
|
|
4544
5026
|
this.readOnly = config.readOnly ?? false;
|
|
4545
|
-
|
|
4546
|
-
|
|
5027
|
+
this.lightweightMode = config.lightweightMode ?? false;
|
|
5028
|
+
if (!this.readOnly && !fs2.existsSync(storagePath)) {
|
|
5029
|
+
fs2.mkdirSync(storagePath, { recursive: true });
|
|
4547
5030
|
}
|
|
4548
5031
|
this.projectHash = config.projectHash || null;
|
|
4549
5032
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
@@ -4588,6 +5071,10 @@ var MemoryService = class {
|
|
|
4588
5071
|
if (this.initialized)
|
|
4589
5072
|
return;
|
|
4590
5073
|
await this.sqliteStore.initialize();
|
|
5074
|
+
if (this.lightweightMode) {
|
|
5075
|
+
this.initialized = true;
|
|
5076
|
+
return;
|
|
5077
|
+
}
|
|
4591
5078
|
if (this.analyticsStore) {
|
|
4592
5079
|
try {
|
|
4593
5080
|
await this.analyticsStore.initialize();
|
|
@@ -4634,8 +5121,8 @@ var MemoryService = class {
|
|
|
4634
5121
|
*/
|
|
4635
5122
|
async initializeSharedStore() {
|
|
4636
5123
|
const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
|
|
4637
|
-
if (!
|
|
4638
|
-
|
|
5124
|
+
if (!fs2.existsSync(sharedPath)) {
|
|
5125
|
+
fs2.mkdirSync(sharedPath, { recursive: true });
|
|
4639
5126
|
}
|
|
4640
5127
|
this.sharedEventStore = createSharedEventStore(
|
|
4641
5128
|
path.join(sharedPath, "shared.duckdb")
|
|
@@ -4732,6 +5219,7 @@ var MemoryService = class {
|
|
|
4732
5219
|
async storeToolObservation(sessionId, payload) {
|
|
4733
5220
|
await this.initialize();
|
|
4734
5221
|
const content = JSON.stringify(payload);
|
|
5222
|
+
const turnId = payload.metadata?.turnId;
|
|
4735
5223
|
const result = await this.sqliteStore.append({
|
|
4736
5224
|
eventType: "tool_observation",
|
|
4737
5225
|
sessionId,
|
|
@@ -4739,7 +5227,8 @@ var MemoryService = class {
|
|
|
4739
5227
|
content,
|
|
4740
5228
|
metadata: {
|
|
4741
5229
|
toolName: payload.toolName,
|
|
4742
|
-
success: payload.success
|
|
5230
|
+
success: payload.success,
|
|
5231
|
+
...turnId ? { turnId } : {}
|
|
4743
5232
|
}
|
|
4744
5233
|
});
|
|
4745
5234
|
if (result.success && !result.isDuplicate) {
|
|
@@ -4757,9 +5246,6 @@ var MemoryService = class {
|
|
|
4757
5246
|
*/
|
|
4758
5247
|
async retrieveMemories(query, options) {
|
|
4759
5248
|
await this.initialize();
|
|
4760
|
-
if (this.vectorWorker) {
|
|
4761
|
-
await this.vectorWorker.processAll();
|
|
4762
|
-
}
|
|
4763
5249
|
if (options?.includeShared && this.sharedStore) {
|
|
4764
5250
|
return this.retriever.retrieveUnified(query, {
|
|
4765
5251
|
...options,
|
|
@@ -4769,6 +5255,29 @@ var MemoryService = class {
|
|
|
4769
5255
|
}
|
|
4770
5256
|
return this.retriever.retrieve(query, options);
|
|
4771
5257
|
}
|
|
5258
|
+
/**
|
|
5259
|
+
* Fast keyword search using SQLite FTS5
|
|
5260
|
+
* Much faster than vector search - no embedding model needed
|
|
5261
|
+
*/
|
|
5262
|
+
async keywordSearch(query, options) {
|
|
5263
|
+
await this.initialize();
|
|
5264
|
+
const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
|
|
5265
|
+
const maxRank = Math.min(...results.map((r) => r.rank), -1e-3);
|
|
5266
|
+
const minRank = Math.max(...results.map((r) => r.rank), -1e3);
|
|
5267
|
+
const rankRange = maxRank - minRank || 1;
|
|
5268
|
+
return results.map((r) => ({
|
|
5269
|
+
event: r.event,
|
|
5270
|
+
score: 1 - (r.rank - minRank) / rankRange
|
|
5271
|
+
// Normalize to 0-1
|
|
5272
|
+
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
5273
|
+
}
|
|
5274
|
+
/**
|
|
5275
|
+
* Rebuild FTS index (call after database upgrade)
|
|
5276
|
+
*/
|
|
5277
|
+
async rebuildFtsIndex() {
|
|
5278
|
+
await this.initialize();
|
|
5279
|
+
return this.sqliteStore.rebuildFtsIndex();
|
|
5280
|
+
}
|
|
4772
5281
|
/**
|
|
4773
5282
|
* Get session history
|
|
4774
5283
|
*/
|
|
@@ -5002,6 +5511,31 @@ var MemoryService = class {
|
|
|
5002
5511
|
return [];
|
|
5003
5512
|
return this.consolidatedStore.getAll({ limit });
|
|
5004
5513
|
}
|
|
5514
|
+
/**
|
|
5515
|
+
* Extract topic keywords from event content (markdown headings and key terms)
|
|
5516
|
+
*/
|
|
5517
|
+
extractTopicsFromContent(content) {
|
|
5518
|
+
const topics = /* @__PURE__ */ new Set();
|
|
5519
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm);
|
|
5520
|
+
if (headings) {
|
|
5521
|
+
for (const h of headings.slice(0, 5)) {
|
|
5522
|
+
const text = h.replace(/^#+\s+/, "").replace(/[*_`#]/g, "").trim();
|
|
5523
|
+
if (text.length > 2 && text.length < 50) {
|
|
5524
|
+
topics.add(text);
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
|
|
5529
|
+
if (boldTerms) {
|
|
5530
|
+
for (const b of boldTerms.slice(0, 5)) {
|
|
5531
|
+
const text = b.replace(/\*\*/g, "").trim();
|
|
5532
|
+
if (text.length > 2 && text.length < 30) {
|
|
5533
|
+
topics.add(text);
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
return Array.from(topics).slice(0, 5);
|
|
5538
|
+
}
|
|
5005
5539
|
/**
|
|
5006
5540
|
* Increment access count for memories that were used in prompts
|
|
5007
5541
|
*/
|
|
@@ -5025,8 +5559,7 @@ var MemoryService = class {
|
|
|
5025
5559
|
return events.map((event) => ({
|
|
5026
5560
|
memoryId: event.id,
|
|
5027
5561
|
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5028
|
-
topics:
|
|
5029
|
-
// Could extract topics from content if needed
|
|
5562
|
+
topics: this.extractTopicsFromContent(event.content),
|
|
5030
5563
|
accessCount: event.access_count || 0,
|
|
5031
5564
|
lastAccessed: event.last_accessed_at || null,
|
|
5032
5565
|
confidence: 1,
|
|
@@ -5047,6 +5580,34 @@ var MemoryService = class {
|
|
|
5047
5580
|
}
|
|
5048
5581
|
return [];
|
|
5049
5582
|
}
|
|
5583
|
+
/**
|
|
5584
|
+
* Record a memory retrieval for helpfulness tracking
|
|
5585
|
+
*/
|
|
5586
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
5587
|
+
await this.initialize();
|
|
5588
|
+
await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
|
|
5589
|
+
}
|
|
5590
|
+
/**
|
|
5591
|
+
* Evaluate helpfulness of retrievals in a session (called at session end)
|
|
5592
|
+
*/
|
|
5593
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
5594
|
+
await this.initialize();
|
|
5595
|
+
await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
|
|
5596
|
+
}
|
|
5597
|
+
/**
|
|
5598
|
+
* Get most helpful memories ranked by helpfulness score
|
|
5599
|
+
*/
|
|
5600
|
+
async getHelpfulMemories(limit = 10) {
|
|
5601
|
+
await this.initialize();
|
|
5602
|
+
return this.sqliteStore.getHelpfulMemories(limit);
|
|
5603
|
+
}
|
|
5604
|
+
/**
|
|
5605
|
+
* Get helpfulness statistics for dashboard
|
|
5606
|
+
*/
|
|
5607
|
+
async getHelpfulnessStats() {
|
|
5608
|
+
await this.initialize();
|
|
5609
|
+
return this.sqliteStore.getHelpfulnessStats();
|
|
5610
|
+
}
|
|
5050
5611
|
/**
|
|
5051
5612
|
* Mark a consolidated memory as accessed
|
|
5052
5613
|
*/
|
|
@@ -5110,6 +5671,44 @@ var MemoryService = class {
|
|
|
5110
5671
|
lastConsolidation
|
|
5111
5672
|
};
|
|
5112
5673
|
}
|
|
5674
|
+
// ============================================================
|
|
5675
|
+
// Turn Grouping Methods
|
|
5676
|
+
// ============================================================
|
|
5677
|
+
/**
|
|
5678
|
+
* Get events grouped by turn for a session
|
|
5679
|
+
*/
|
|
5680
|
+
async getSessionTurns(sessionId, options) {
|
|
5681
|
+
await this.initialize();
|
|
5682
|
+
return this.sqliteStore.getSessionTurns(sessionId, options);
|
|
5683
|
+
}
|
|
5684
|
+
/**
|
|
5685
|
+
* Get all events for a specific turn
|
|
5686
|
+
*/
|
|
5687
|
+
async getEventsByTurn(turnId) {
|
|
5688
|
+
await this.initialize();
|
|
5689
|
+
return this.sqliteStore.getEventsByTurn(turnId);
|
|
5690
|
+
}
|
|
5691
|
+
/**
|
|
5692
|
+
* Count total turns for a session
|
|
5693
|
+
*/
|
|
5694
|
+
async countSessionTurns(sessionId) {
|
|
5695
|
+
await this.initialize();
|
|
5696
|
+
return this.sqliteStore.countSessionTurns(sessionId);
|
|
5697
|
+
}
|
|
5698
|
+
/**
|
|
5699
|
+
* Backfill turn_ids from metadata for events stored before the migration
|
|
5700
|
+
*/
|
|
5701
|
+
async backfillTurnIds() {
|
|
5702
|
+
await this.initialize();
|
|
5703
|
+
return this.sqliteStore.backfillTurnIds();
|
|
5704
|
+
}
|
|
5705
|
+
/**
|
|
5706
|
+
* Delete all events for a session (for force reimport)
|
|
5707
|
+
*/
|
|
5708
|
+
async deleteSessionEvents(sessionId) {
|
|
5709
|
+
await this.initialize();
|
|
5710
|
+
return this.sqliteStore.deleteSessionEvents(sessionId);
|
|
5711
|
+
}
|
|
5113
5712
|
/**
|
|
5114
5713
|
* Format Endless Mode context for Claude
|
|
5115
5714
|
*/
|
|
@@ -5232,10 +5831,46 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
5232
5831
|
}
|
|
5233
5832
|
|
|
5234
5833
|
// src/services/session-history-importer.ts
|
|
5235
|
-
import * as
|
|
5834
|
+
import * as fs3 from "fs";
|
|
5236
5835
|
import * as path2 from "path";
|
|
5237
5836
|
import * as os2 from "os";
|
|
5238
5837
|
import * as readline from "readline";
|
|
5838
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
5839
|
+
function classifyEntry(entry) {
|
|
5840
|
+
if (entry.type !== "user" && entry.type !== "assistant") {
|
|
5841
|
+
return "skip";
|
|
5842
|
+
}
|
|
5843
|
+
const content = entry.message?.content;
|
|
5844
|
+
if (!content)
|
|
5845
|
+
return "skip";
|
|
5846
|
+
if (entry.type === "user") {
|
|
5847
|
+
if (typeof content === "string")
|
|
5848
|
+
return "user_prompt";
|
|
5849
|
+
if (Array.isArray(content)) {
|
|
5850
|
+
const hasToolResult = content.some((b) => b.type === "tool_result");
|
|
5851
|
+
if (hasToolResult)
|
|
5852
|
+
return "tool_result";
|
|
5853
|
+
const hasText = content.some((b) => b.type === "text" && b.text);
|
|
5854
|
+
if (hasText)
|
|
5855
|
+
return "user_prompt";
|
|
5856
|
+
}
|
|
5857
|
+
return "skip";
|
|
5858
|
+
}
|
|
5859
|
+
if (Array.isArray(content)) {
|
|
5860
|
+
const hasToolUse = content.some((b) => b.type === "tool_use");
|
|
5861
|
+
if (hasToolUse)
|
|
5862
|
+
return "tool_use";
|
|
5863
|
+
const hasText = content.some((b) => b.type === "text" && b.text);
|
|
5864
|
+
if (hasText)
|
|
5865
|
+
return "agent_text";
|
|
5866
|
+
const hasThinking = content.some((b) => b.type === "thinking");
|
|
5867
|
+
if (hasThinking)
|
|
5868
|
+
return "thinking";
|
|
5869
|
+
} else if (typeof content === "string" && content.length > 0) {
|
|
5870
|
+
return "agent_text";
|
|
5871
|
+
}
|
|
5872
|
+
return "skip";
|
|
5873
|
+
}
|
|
5239
5874
|
var SessionHistoryImporter = class {
|
|
5240
5875
|
memoryService;
|
|
5241
5876
|
claudeDir;
|
|
@@ -5255,6 +5890,8 @@ var SessionHistoryImporter = class {
|
|
|
5255
5890
|
skippedDuplicates: 0,
|
|
5256
5891
|
errors: []
|
|
5257
5892
|
};
|
|
5893
|
+
const onProgress = options.onProgress;
|
|
5894
|
+
onProgress?.({ phase: "scan", message: "Scanning for session files..." });
|
|
5258
5895
|
const projectDir = await this.findProjectDir(projectPath);
|
|
5259
5896
|
if (!projectDir) {
|
|
5260
5897
|
result.errors.push(`Project directory not found for: ${projectPath}`);
|
|
@@ -5262,16 +5899,29 @@ var SessionHistoryImporter = class {
|
|
|
5262
5899
|
}
|
|
5263
5900
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
5264
5901
|
result.totalSessions = sessionFiles.length;
|
|
5902
|
+
onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} sessions in ${path2.basename(projectDir)}` });
|
|
5265
5903
|
if (options.verbose) {
|
|
5266
5904
|
console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
|
|
5267
5905
|
}
|
|
5268
|
-
for (
|
|
5906
|
+
for (let i = 0; i < sessionFiles.length; i++) {
|
|
5907
|
+
const sessionFile = sessionFiles[i];
|
|
5269
5908
|
try {
|
|
5270
|
-
|
|
5909
|
+
onProgress?.({ phase: "session-start", sessionIndex: i, totalSessions: sessionFiles.length, filePath: sessionFile });
|
|
5910
|
+
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
5911
|
+
...options,
|
|
5912
|
+
_sessionIndex: i
|
|
5913
|
+
});
|
|
5271
5914
|
result.totalMessages += sessionResult.totalMessages;
|
|
5272
5915
|
result.importedPrompts += sessionResult.importedPrompts;
|
|
5273
5916
|
result.importedResponses += sessionResult.importedResponses;
|
|
5274
5917
|
result.skippedDuplicates += sessionResult.skippedDuplicates;
|
|
5918
|
+
onProgress?.({
|
|
5919
|
+
phase: "session-done",
|
|
5920
|
+
sessionIndex: i,
|
|
5921
|
+
importedPrompts: sessionResult.importedPrompts,
|
|
5922
|
+
importedResponses: sessionResult.importedResponses,
|
|
5923
|
+
skipped: sessionResult.skippedDuplicates
|
|
5924
|
+
});
|
|
5275
5925
|
} catch (error) {
|
|
5276
5926
|
result.errors.push(`Failed to import ${sessionFile}: ${error}`);
|
|
5277
5927
|
}
|
|
@@ -5290,60 +5940,105 @@ var SessionHistoryImporter = class {
|
|
|
5290
5940
|
skippedDuplicates: 0,
|
|
5291
5941
|
errors: []
|
|
5292
5942
|
};
|
|
5293
|
-
if (!
|
|
5943
|
+
if (!fs3.existsSync(filePath)) {
|
|
5294
5944
|
result.errors.push(`File not found: ${filePath}`);
|
|
5295
5945
|
return result;
|
|
5296
5946
|
}
|
|
5297
5947
|
const sessionId = path2.basename(filePath, ".jsonl");
|
|
5948
|
+
if (options.force) {
|
|
5949
|
+
const deleted = await this.memoryService.deleteSessionEvents(sessionId);
|
|
5950
|
+
if (options.verbose && deleted > 0) {
|
|
5951
|
+
console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5298
5954
|
await this.memoryService.startSession(sessionId, options.projectPath);
|
|
5299
|
-
const fileStream =
|
|
5955
|
+
const fileStream = fs3.createReadStream(filePath);
|
|
5300
5956
|
const rl = readline.createInterface({
|
|
5301
5957
|
input: fileStream,
|
|
5302
5958
|
crlfDelay: Infinity
|
|
5303
5959
|
});
|
|
5304
5960
|
let lineCount = 0;
|
|
5305
5961
|
const limit = options.limit || Infinity;
|
|
5962
|
+
const onProgress = options.onProgress;
|
|
5963
|
+
const sessionIndex = options._sessionIndex ?? 0;
|
|
5964
|
+
let lastProgressAt = 0;
|
|
5965
|
+
let currentTurnId = null;
|
|
5966
|
+
let textBuffer = [];
|
|
5967
|
+
let lastTimestamp;
|
|
5968
|
+
const flushTextBuffer = async () => {
|
|
5969
|
+
if (textBuffer.length === 0 || !currentTurnId)
|
|
5970
|
+
return;
|
|
5971
|
+
const substantive = textBuffer.filter((t) => t.length >= 100);
|
|
5972
|
+
const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
|
|
5973
|
+
if (!merged) {
|
|
5974
|
+
textBuffer = [];
|
|
5975
|
+
return;
|
|
5976
|
+
}
|
|
5977
|
+
const truncated = merged.length > 1e4 ? merged.slice(0, 1e4) + "...[truncated]" : merged;
|
|
5978
|
+
const appendResult = await this.memoryService.storeAgentResponse(
|
|
5979
|
+
sessionId,
|
|
5980
|
+
truncated,
|
|
5981
|
+
{ importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
|
|
5982
|
+
);
|
|
5983
|
+
if (appendResult.isDuplicate) {
|
|
5984
|
+
result.skippedDuplicates++;
|
|
5985
|
+
} else {
|
|
5986
|
+
result.importedResponses++;
|
|
5987
|
+
}
|
|
5988
|
+
lineCount++;
|
|
5989
|
+
textBuffer = [];
|
|
5990
|
+
};
|
|
5306
5991
|
for await (const line of rl) {
|
|
5307
5992
|
if (lineCount >= limit)
|
|
5308
5993
|
break;
|
|
5309
5994
|
try {
|
|
5310
5995
|
const entry = JSON.parse(line);
|
|
5311
5996
|
result.totalMessages++;
|
|
5312
|
-
|
|
5997
|
+
const msgClass = classifyEntry(entry);
|
|
5998
|
+
if (msgClass === "user_prompt") {
|
|
5999
|
+
await flushTextBuffer();
|
|
5313
6000
|
const content = this.extractContent(entry);
|
|
5314
6001
|
if (!content)
|
|
5315
6002
|
continue;
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
}
|
|
5327
|
-
} else if (entry.type === "assistant") {
|
|
5328
|
-
const truncatedContent = content.length > 5e3 ? content.slice(0, 5e3) + "...[truncated]" : content;
|
|
5329
|
-
const appendResult = await this.memoryService.storeAgentResponse(
|
|
5330
|
-
sessionId,
|
|
5331
|
-
truncatedContent,
|
|
5332
|
-
{ importedFrom: filePath, originalTimestamp: entry.timestamp }
|
|
5333
|
-
);
|
|
5334
|
-
if (appendResult.isDuplicate) {
|
|
5335
|
-
result.skippedDuplicates++;
|
|
5336
|
-
} else {
|
|
5337
|
-
result.importedResponses++;
|
|
5338
|
-
}
|
|
6003
|
+
currentTurnId = randomUUID9();
|
|
6004
|
+
const appendResult = await this.memoryService.storeUserPrompt(
|
|
6005
|
+
sessionId,
|
|
6006
|
+
content,
|
|
6007
|
+
{ importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
|
|
6008
|
+
);
|
|
6009
|
+
if (appendResult.isDuplicate) {
|
|
6010
|
+
result.skippedDuplicates++;
|
|
6011
|
+
} else {
|
|
6012
|
+
result.importedPrompts++;
|
|
5339
6013
|
}
|
|
5340
6014
|
lineCount++;
|
|
6015
|
+
} else if (msgClass === "agent_text") {
|
|
6016
|
+
const content = this.extractContent(entry);
|
|
6017
|
+
if (content) {
|
|
6018
|
+
textBuffer.push(content);
|
|
6019
|
+
lastTimestamp = entry.timestamp;
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
const now = Date.now();
|
|
6023
|
+
if (now - lastProgressAt > 200) {
|
|
6024
|
+
lastProgressAt = now;
|
|
6025
|
+
onProgress?.({
|
|
6026
|
+
phase: "session-progress",
|
|
6027
|
+
sessionIndex,
|
|
6028
|
+
messagesProcessed: result.totalMessages,
|
|
6029
|
+
imported: result.importedPrompts + result.importedResponses,
|
|
6030
|
+
skipped: result.skippedDuplicates
|
|
6031
|
+
});
|
|
5341
6032
|
}
|
|
5342
6033
|
} catch (parseError) {
|
|
5343
6034
|
result.errors.push(`Parse error on line: ${parseError}`);
|
|
5344
6035
|
}
|
|
5345
6036
|
}
|
|
6037
|
+
await flushTextBuffer();
|
|
5346
6038
|
await this.memoryService.endSession(sessionId);
|
|
6039
|
+
if (options.projectPath) {
|
|
6040
|
+
registerSession(sessionId, options.projectPath);
|
|
6041
|
+
}
|
|
5347
6042
|
if (options.verbose) {
|
|
5348
6043
|
console.log(`Imported ${result.importedPrompts} prompts, ${result.importedResponses} responses from ${filePath}`);
|
|
5349
6044
|
}
|
|
@@ -5361,29 +6056,46 @@ var SessionHistoryImporter = class {
|
|
|
5361
6056
|
skippedDuplicates: 0,
|
|
5362
6057
|
errors: []
|
|
5363
6058
|
};
|
|
6059
|
+
const onProgress = options.onProgress;
|
|
5364
6060
|
const projectsDir = path2.join(this.claudeDir, "projects");
|
|
5365
|
-
if (!
|
|
6061
|
+
if (!fs3.existsSync(projectsDir)) {
|
|
5366
6062
|
result.errors.push(`Projects directory not found: ${projectsDir}`);
|
|
5367
6063
|
return result;
|
|
5368
6064
|
}
|
|
5369
|
-
|
|
6065
|
+
onProgress?.({ phase: "scan", message: "Scanning all projects..." });
|
|
6066
|
+
const projectDirs = fs3.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs3.statSync(p).isDirectory());
|
|
6067
|
+
const allSessionFiles = [];
|
|
6068
|
+
for (const projectDir of projectDirs) {
|
|
6069
|
+
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
6070
|
+
allSessionFiles.push(...sessionFiles);
|
|
6071
|
+
}
|
|
6072
|
+
onProgress?.({ phase: "scan", message: `Found ${allSessionFiles.length} sessions across ${projectDirs.length} projects` });
|
|
5370
6073
|
if (options.verbose) {
|
|
5371
|
-
console.log(`Found ${projectDirs.length} project directories`);
|
|
6074
|
+
console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
|
|
5372
6075
|
}
|
|
5373
|
-
for (
|
|
6076
|
+
for (let i = 0; i < allSessionFiles.length; i++) {
|
|
6077
|
+
const sessionFile = allSessionFiles[i];
|
|
5374
6078
|
try {
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
6079
|
+
onProgress?.({ phase: "session-start", sessionIndex: i, totalSessions: allSessionFiles.length, filePath: sessionFile });
|
|
6080
|
+
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
6081
|
+
...options,
|
|
6082
|
+
_sessionIndex: i
|
|
6083
|
+
});
|
|
6084
|
+
result.totalSessions++;
|
|
6085
|
+
result.totalMessages += sessionResult.totalMessages;
|
|
6086
|
+
result.importedPrompts += sessionResult.importedPrompts;
|
|
6087
|
+
result.importedResponses += sessionResult.importedResponses;
|
|
6088
|
+
result.skippedDuplicates += sessionResult.skippedDuplicates;
|
|
6089
|
+
result.errors.push(...sessionResult.errors);
|
|
6090
|
+
onProgress?.({
|
|
6091
|
+
phase: "session-done",
|
|
6092
|
+
sessionIndex: i,
|
|
6093
|
+
importedPrompts: sessionResult.importedPrompts,
|
|
6094
|
+
importedResponses: sessionResult.importedResponses,
|
|
6095
|
+
skipped: sessionResult.skippedDuplicates
|
|
6096
|
+
});
|
|
5385
6097
|
} catch (error) {
|
|
5386
|
-
result.errors.push(`Failed to process ${
|
|
6098
|
+
result.errors.push(`Failed to process ${sessionFile}: ${error}`);
|
|
5387
6099
|
}
|
|
5388
6100
|
}
|
|
5389
6101
|
return result;
|
|
@@ -5393,10 +6105,10 @@ var SessionHistoryImporter = class {
|
|
|
5393
6105
|
*/
|
|
5394
6106
|
async findProjectDir(projectPath) {
|
|
5395
6107
|
const projectsDir = path2.join(this.claudeDir, "projects");
|
|
5396
|
-
if (!
|
|
6108
|
+
if (!fs3.existsSync(projectsDir)) {
|
|
5397
6109
|
return null;
|
|
5398
6110
|
}
|
|
5399
|
-
const projectDirs =
|
|
6111
|
+
const projectDirs = fs3.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs3.statSync(p).isDirectory());
|
|
5400
6112
|
const normalizedPath = projectPath.replace(/\//g, "-").replace(/^-/, "");
|
|
5401
6113
|
for (const dir of projectDirs) {
|
|
5402
6114
|
const dirName = path2.basename(dir);
|
|
@@ -5410,10 +6122,10 @@ var SessionHistoryImporter = class {
|
|
|
5410
6122
|
* Find all JSONL session files in a directory
|
|
5411
6123
|
*/
|
|
5412
6124
|
async findSessionFiles(dir) {
|
|
5413
|
-
if (!
|
|
6125
|
+
if (!fs3.existsSync(dir)) {
|
|
5414
6126
|
return [];
|
|
5415
6127
|
}
|
|
5416
|
-
return
|
|
6128
|
+
return fs3.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path2.join(dir, name)).filter((p) => fs3.statSync(p).isFile());
|
|
5417
6129
|
}
|
|
5418
6130
|
/**
|
|
5419
6131
|
* Extract text content from Claude message
|
|
@@ -5445,14 +6157,14 @@ var SessionHistoryImporter = class {
|
|
|
5445
6157
|
}
|
|
5446
6158
|
} else {
|
|
5447
6159
|
const projectsDir = path2.join(this.claudeDir, "projects");
|
|
5448
|
-
if (
|
|
5449
|
-
projectDirs =
|
|
6160
|
+
if (fs3.existsSync(projectsDir)) {
|
|
6161
|
+
projectDirs = fs3.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs3.statSync(p).isDirectory());
|
|
5450
6162
|
}
|
|
5451
6163
|
}
|
|
5452
6164
|
for (const projectDir of projectDirs) {
|
|
5453
6165
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
5454
6166
|
for (const filePath of sessionFiles) {
|
|
5455
|
-
const stats =
|
|
6167
|
+
const stats = fs3.statSync(filePath);
|
|
5456
6168
|
sessions.push({
|
|
5457
6169
|
sessionId: path2.basename(filePath, ".jsonl"),
|
|
5458
6170
|
filePath,
|
|
@@ -5470,24 +6182,52 @@ function createSessionHistoryImporter(memoryService) {
|
|
|
5470
6182
|
}
|
|
5471
6183
|
|
|
5472
6184
|
// src/server/index.ts
|
|
5473
|
-
import { Hono as
|
|
6185
|
+
import { Hono as Hono10 } from "hono";
|
|
5474
6186
|
import { cors } from "hono/cors";
|
|
5475
6187
|
import { logger } from "hono/logger";
|
|
5476
6188
|
import { serve } from "@hono/node-server";
|
|
5477
6189
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5478
|
-
import * as
|
|
5479
|
-
import * as
|
|
6190
|
+
import * as path5 from "path";
|
|
6191
|
+
import * as fs5 from "fs";
|
|
5480
6192
|
|
|
5481
6193
|
// src/server/api/index.ts
|
|
5482
|
-
import { Hono as
|
|
6194
|
+
import { Hono as Hono9 } from "hono";
|
|
5483
6195
|
|
|
5484
6196
|
// src/server/api/sessions.ts
|
|
5485
6197
|
import { Hono } from "hono";
|
|
6198
|
+
|
|
6199
|
+
// src/server/api/utils.ts
|
|
6200
|
+
import * as path3 from "path";
|
|
6201
|
+
import * as os3 from "os";
|
|
6202
|
+
function getServiceFromQuery(c) {
|
|
6203
|
+
const project = c.req.query("project");
|
|
6204
|
+
if (project) {
|
|
6205
|
+
const isHash = /^[a-f0-9]{8}$/.test(project);
|
|
6206
|
+
let storagePath;
|
|
6207
|
+
if (isHash) {
|
|
6208
|
+
storagePath = path3.join(os3.homedir(), ".claude-code", "memory", "projects", project);
|
|
6209
|
+
} else {
|
|
6210
|
+
const crypto3 = __require("crypto");
|
|
6211
|
+
const normalized = project.replace(/\/+$/, "") || "/";
|
|
6212
|
+
const hash = crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
6213
|
+
storagePath = path3.join(os3.homedir(), ".claude-code", "memory", "projects", hash);
|
|
6214
|
+
}
|
|
6215
|
+
return new MemoryService({
|
|
6216
|
+
storagePath,
|
|
6217
|
+
readOnly: true,
|
|
6218
|
+
analyticsEnabled: false,
|
|
6219
|
+
sharedStoreConfig: { enabled: false }
|
|
6220
|
+
});
|
|
6221
|
+
}
|
|
6222
|
+
return getReadOnlyMemoryService();
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
// src/server/api/sessions.ts
|
|
5486
6226
|
var sessionsRouter = new Hono();
|
|
5487
6227
|
sessionsRouter.get("/", async (c) => {
|
|
5488
6228
|
const page = parseInt(c.req.query("page") || "1", 10);
|
|
5489
6229
|
const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
|
|
5490
|
-
const memoryService =
|
|
6230
|
+
const memoryService = getServiceFromQuery(c);
|
|
5491
6231
|
try {
|
|
5492
6232
|
await memoryService.initialize();
|
|
5493
6233
|
const recentEvents = await memoryService.getRecentEvents(1e3);
|
|
@@ -5531,7 +6271,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
5531
6271
|
});
|
|
5532
6272
|
sessionsRouter.get("/:id", async (c) => {
|
|
5533
6273
|
const { id } = c.req.param();
|
|
5534
|
-
const memoryService =
|
|
6274
|
+
const memoryService = getServiceFromQuery(c);
|
|
5535
6275
|
try {
|
|
5536
6276
|
await memoryService.initialize();
|
|
5537
6277
|
const events = await memoryService.getSessionHistory(id);
|
|
@@ -5572,18 +6312,36 @@ var eventsRouter = new Hono2();
|
|
|
5572
6312
|
eventsRouter.get("/", async (c) => {
|
|
5573
6313
|
const sessionId = c.req.query("sessionId");
|
|
5574
6314
|
const eventType = c.req.query("type");
|
|
6315
|
+
const level = c.req.query("level");
|
|
6316
|
+
const sort = c.req.query("sort") || "recent";
|
|
5575
6317
|
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
5576
6318
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5577
|
-
const memoryService =
|
|
6319
|
+
const memoryService = getServiceFromQuery(c);
|
|
5578
6320
|
try {
|
|
5579
6321
|
await memoryService.initialize();
|
|
5580
|
-
let events
|
|
6322
|
+
let events;
|
|
6323
|
+
if (level) {
|
|
6324
|
+
events = await memoryService.getEventsByLevel(level, { limit: limit + offset + 1e3, offset: 0 });
|
|
6325
|
+
} else {
|
|
6326
|
+
events = await memoryService.getRecentEvents(limit + offset + 1e3);
|
|
6327
|
+
}
|
|
5581
6328
|
if (sessionId) {
|
|
5582
6329
|
events = events.filter((e) => e.sessionId === sessionId);
|
|
5583
6330
|
}
|
|
5584
6331
|
if (eventType) {
|
|
5585
6332
|
events = events.filter((e) => e.eventType === eventType);
|
|
5586
6333
|
}
|
|
6334
|
+
if (sort === "accessed") {
|
|
6335
|
+
events.sort((a, b) => {
|
|
6336
|
+
const aTime = a.last_accessed_at || "";
|
|
6337
|
+
const bTime = b.last_accessed_at || "";
|
|
6338
|
+
return bTime.localeCompare(aTime);
|
|
6339
|
+
});
|
|
6340
|
+
} else if (sort === "most-accessed") {
|
|
6341
|
+
events.sort((a, b) => (b.access_count || 0) - (a.access_count || 0));
|
|
6342
|
+
} else if (sort === "oldest") {
|
|
6343
|
+
events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
6344
|
+
}
|
|
5587
6345
|
const total = events.length;
|
|
5588
6346
|
events = events.slice(offset, offset + limit);
|
|
5589
6347
|
return c.json({
|
|
@@ -5593,7 +6351,9 @@ eventsRouter.get("/", async (c) => {
|
|
|
5593
6351
|
timestamp: e.timestamp,
|
|
5594
6352
|
sessionId: e.sessionId,
|
|
5595
6353
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
5596
|
-
contentLength: e.content.length
|
|
6354
|
+
contentLength: e.content.length,
|
|
6355
|
+
accessCount: e.access_count || 0,
|
|
6356
|
+
lastAccessedAt: e.last_accessed_at || null
|
|
5597
6357
|
})),
|
|
5598
6358
|
total,
|
|
5599
6359
|
limit,
|
|
@@ -5608,7 +6368,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
5608
6368
|
});
|
|
5609
6369
|
eventsRouter.get("/:id", async (c) => {
|
|
5610
6370
|
const { id } = c.req.param();
|
|
5611
|
-
const memoryService =
|
|
6371
|
+
const memoryService = getServiceFromQuery(c);
|
|
5612
6372
|
try {
|
|
5613
6373
|
await memoryService.initialize();
|
|
5614
6374
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5648,7 +6408,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
5648
6408
|
import { Hono as Hono3 } from "hono";
|
|
5649
6409
|
var searchRouter = new Hono3();
|
|
5650
6410
|
searchRouter.post("/", async (c) => {
|
|
5651
|
-
const memoryService =
|
|
6411
|
+
const memoryService = getServiceFromQuery(c);
|
|
5652
6412
|
try {
|
|
5653
6413
|
const body = await c.req.json();
|
|
5654
6414
|
if (!body.query) {
|
|
@@ -5692,7 +6452,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5692
6452
|
return c.json({ error: 'Query parameter "q" is required' }, 400);
|
|
5693
6453
|
}
|
|
5694
6454
|
const topK = parseInt(c.req.query("topK") || "5", 10);
|
|
5695
|
-
const memoryService =
|
|
6455
|
+
const memoryService = getServiceFromQuery(c);
|
|
5696
6456
|
try {
|
|
5697
6457
|
await memoryService.initialize();
|
|
5698
6458
|
const result = await memoryService.retrieveMemories(query, { topK });
|
|
@@ -5720,7 +6480,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5720
6480
|
import { Hono as Hono4 } from "hono";
|
|
5721
6481
|
var statsRouter = new Hono4();
|
|
5722
6482
|
statsRouter.get("/shared", async (c) => {
|
|
5723
|
-
const memoryService =
|
|
6483
|
+
const memoryService = getServiceFromQuery(c);
|
|
5724
6484
|
try {
|
|
5725
6485
|
await memoryService.initialize();
|
|
5726
6486
|
const sharedStats = await memoryService.getSharedStoreStats();
|
|
@@ -5777,7 +6537,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5777
6537
|
if (!validLevels.includes(level)) {
|
|
5778
6538
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
5779
6539
|
}
|
|
5780
|
-
const memoryService =
|
|
6540
|
+
const memoryService = getServiceFromQuery(c);
|
|
5781
6541
|
try {
|
|
5782
6542
|
await memoryService.initialize();
|
|
5783
6543
|
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
@@ -5824,7 +6584,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5824
6584
|
}
|
|
5825
6585
|
});
|
|
5826
6586
|
statsRouter.get("/", async (c) => {
|
|
5827
|
-
const memoryService =
|
|
6587
|
+
const memoryService = getServiceFromQuery(c);
|
|
5828
6588
|
try {
|
|
5829
6589
|
await memoryService.initialize();
|
|
5830
6590
|
const stats = await memoryService.getStats();
|
|
@@ -5868,7 +6628,7 @@ statsRouter.get("/", async (c) => {
|
|
|
5868
6628
|
});
|
|
5869
6629
|
statsRouter.get("/most-accessed", async (c) => {
|
|
5870
6630
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
5871
|
-
const memoryService =
|
|
6631
|
+
const memoryService = getServiceFromQuery(c);
|
|
5872
6632
|
try {
|
|
5873
6633
|
await memoryService.initialize();
|
|
5874
6634
|
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
@@ -5899,7 +6659,7 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
5899
6659
|
});
|
|
5900
6660
|
statsRouter.get("/timeline", async (c) => {
|
|
5901
6661
|
const days = parseInt(c.req.query("days") || "7", 10);
|
|
5902
|
-
const memoryService =
|
|
6662
|
+
const memoryService = getServiceFromQuery(c);
|
|
5903
6663
|
try {
|
|
5904
6664
|
await memoryService.initialize();
|
|
5905
6665
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5929,8 +6689,39 @@ statsRouter.get("/timeline", async (c) => {
|
|
|
5929
6689
|
await memoryService.shutdown();
|
|
5930
6690
|
}
|
|
5931
6691
|
});
|
|
6692
|
+
statsRouter.get("/helpfulness", async (c) => {
|
|
6693
|
+
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
6694
|
+
const memoryService = getServiceFromQuery(c);
|
|
6695
|
+
try {
|
|
6696
|
+
await memoryService.initialize();
|
|
6697
|
+
const stats = await memoryService.getHelpfulnessStats();
|
|
6698
|
+
const topMemories = await memoryService.getHelpfulMemories(limit);
|
|
6699
|
+
return c.json({
|
|
6700
|
+
...stats,
|
|
6701
|
+
topMemories: topMemories.map((m) => ({
|
|
6702
|
+
eventId: m.eventId,
|
|
6703
|
+
summary: m.summary,
|
|
6704
|
+
helpfulnessScore: m.helpfulnessScore,
|
|
6705
|
+
accessCount: m.accessCount,
|
|
6706
|
+
evaluationCount: m.evaluationCount
|
|
6707
|
+
}))
|
|
6708
|
+
});
|
|
6709
|
+
} catch (error) {
|
|
6710
|
+
return c.json({
|
|
6711
|
+
avgScore: 0,
|
|
6712
|
+
totalEvaluated: 0,
|
|
6713
|
+
totalRetrievals: 0,
|
|
6714
|
+
helpful: 0,
|
|
6715
|
+
neutral: 0,
|
|
6716
|
+
unhelpful: 0,
|
|
6717
|
+
topMemories: []
|
|
6718
|
+
});
|
|
6719
|
+
} finally {
|
|
6720
|
+
await memoryService.shutdown();
|
|
6721
|
+
}
|
|
6722
|
+
});
|
|
5932
6723
|
statsRouter.post("/graduation/run", async (c) => {
|
|
5933
|
-
const memoryService =
|
|
6724
|
+
const memoryService = getServiceFromQuery(c);
|
|
5934
6725
|
try {
|
|
5935
6726
|
await memoryService.initialize();
|
|
5936
6727
|
const result = await memoryService.forceGraduation();
|
|
@@ -5991,7 +6782,7 @@ var citationsRouter = new Hono5();
|
|
|
5991
6782
|
citationsRouter.get("/:id", async (c) => {
|
|
5992
6783
|
const { id } = c.req.param();
|
|
5993
6784
|
const citationId = parseCitationId(id) || id;
|
|
5994
|
-
const memoryService =
|
|
6785
|
+
const memoryService = getServiceFromQuery(c);
|
|
5995
6786
|
try {
|
|
5996
6787
|
await memoryService.initialize();
|
|
5997
6788
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6025,7 +6816,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
6025
6816
|
citationsRouter.get("/:id/related", async (c) => {
|
|
6026
6817
|
const { id } = c.req.param();
|
|
6027
6818
|
const citationId = parseCitationId(id) || id;
|
|
6028
|
-
const memoryService =
|
|
6819
|
+
const memoryService = getServiceFromQuery(c);
|
|
6029
6820
|
try {
|
|
6030
6821
|
await memoryService.initialize();
|
|
6031
6822
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6061,23 +6852,373 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
6061
6852
|
}
|
|
6062
6853
|
});
|
|
6063
6854
|
|
|
6855
|
+
// src/server/api/turns.ts
|
|
6856
|
+
import { Hono as Hono6 } from "hono";
|
|
6857
|
+
var turnsRouter = new Hono6();
|
|
6858
|
+
turnsRouter.get("/", async (c) => {
|
|
6859
|
+
const sessionId = c.req.query("sessionId");
|
|
6860
|
+
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
6861
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
6862
|
+
if (!sessionId) {
|
|
6863
|
+
return c.json({ error: "sessionId is required" }, 400);
|
|
6864
|
+
}
|
|
6865
|
+
const memoryService = getServiceFromQuery(c);
|
|
6866
|
+
try {
|
|
6867
|
+
await memoryService.initialize();
|
|
6868
|
+
const turns = await memoryService.getSessionTurns(sessionId, { limit, offset });
|
|
6869
|
+
const totalTurns = await memoryService.countSessionTurns(sessionId);
|
|
6870
|
+
return c.json({
|
|
6871
|
+
turns: turns.map((t) => ({
|
|
6872
|
+
turnId: t.turnId,
|
|
6873
|
+
startedAt: t.startedAt.toISOString(),
|
|
6874
|
+
promptPreview: t.promptPreview,
|
|
6875
|
+
eventCount: t.eventCount,
|
|
6876
|
+
toolCount: t.toolCount,
|
|
6877
|
+
hasResponse: t.hasResponse,
|
|
6878
|
+
events: t.events.map((e) => ({
|
|
6879
|
+
id: e.id,
|
|
6880
|
+
eventType: e.eventType,
|
|
6881
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
|
|
6882
|
+
preview: e.content.slice(0, 300) + (e.content.length > 300 ? "..." : ""),
|
|
6883
|
+
contentLength: e.content.length
|
|
6884
|
+
}))
|
|
6885
|
+
})),
|
|
6886
|
+
total: totalTurns,
|
|
6887
|
+
limit,
|
|
6888
|
+
offset,
|
|
6889
|
+
hasMore: offset + limit < totalTurns
|
|
6890
|
+
});
|
|
6891
|
+
} catch (error) {
|
|
6892
|
+
return c.json({ error: error.message }, 500);
|
|
6893
|
+
} finally {
|
|
6894
|
+
await memoryService.shutdown();
|
|
6895
|
+
}
|
|
6896
|
+
});
|
|
6897
|
+
turnsRouter.get("/:turnId", async (c) => {
|
|
6898
|
+
const { turnId } = c.req.param();
|
|
6899
|
+
const memoryService = getServiceFromQuery(c);
|
|
6900
|
+
try {
|
|
6901
|
+
await memoryService.initialize();
|
|
6902
|
+
const events = await memoryService.getEventsByTurn(turnId);
|
|
6903
|
+
if (events.length === 0) {
|
|
6904
|
+
return c.json({ error: "Turn not found" }, 404);
|
|
6905
|
+
}
|
|
6906
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
6907
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
6908
|
+
const responseEvents = events.filter((e) => e.eventType === "agent_response");
|
|
6909
|
+
return c.json({
|
|
6910
|
+
turnId,
|
|
6911
|
+
sessionId: events[0].sessionId,
|
|
6912
|
+
startedAt: events[0].timestamp instanceof Date ? events[0].timestamp.toISOString() : events[0].timestamp,
|
|
6913
|
+
prompt: promptEvent ? {
|
|
6914
|
+
id: promptEvent.id,
|
|
6915
|
+
content: promptEvent.content,
|
|
6916
|
+
timestamp: promptEvent.timestamp instanceof Date ? promptEvent.timestamp.toISOString() : promptEvent.timestamp
|
|
6917
|
+
} : null,
|
|
6918
|
+
tools: toolEvents.map((e) => {
|
|
6919
|
+
let toolName = "";
|
|
6920
|
+
let success = true;
|
|
6921
|
+
try {
|
|
6922
|
+
const parsed = JSON.parse(e.content);
|
|
6923
|
+
toolName = parsed.toolName || "";
|
|
6924
|
+
success = parsed.success !== false;
|
|
6925
|
+
} catch {
|
|
6926
|
+
}
|
|
6927
|
+
return {
|
|
6928
|
+
id: e.id,
|
|
6929
|
+
toolName,
|
|
6930
|
+
success,
|
|
6931
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
|
|
6932
|
+
preview: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : "")
|
|
6933
|
+
};
|
|
6934
|
+
}),
|
|
6935
|
+
responses: responseEvents.map((e) => ({
|
|
6936
|
+
id: e.id,
|
|
6937
|
+
content: e.content,
|
|
6938
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp
|
|
6939
|
+
})),
|
|
6940
|
+
totalEvents: events.length
|
|
6941
|
+
});
|
|
6942
|
+
} catch (error) {
|
|
6943
|
+
return c.json({ error: error.message }, 500);
|
|
6944
|
+
} finally {
|
|
6945
|
+
await memoryService.shutdown();
|
|
6946
|
+
}
|
|
6947
|
+
});
|
|
6948
|
+
turnsRouter.post("/backfill", async (c) => {
|
|
6949
|
+
const memoryService = getServiceFromQuery(c);
|
|
6950
|
+
try {
|
|
6951
|
+
await memoryService.initialize();
|
|
6952
|
+
const updated = await memoryService.backfillTurnIds();
|
|
6953
|
+
return c.json({
|
|
6954
|
+
success: true,
|
|
6955
|
+
updated,
|
|
6956
|
+
message: `Backfilled turn_id for ${updated} events`
|
|
6957
|
+
});
|
|
6958
|
+
} catch (error) {
|
|
6959
|
+
return c.json({
|
|
6960
|
+
success: false,
|
|
6961
|
+
error: error.message
|
|
6962
|
+
}, 500);
|
|
6963
|
+
} finally {
|
|
6964
|
+
await memoryService.shutdown();
|
|
6965
|
+
}
|
|
6966
|
+
});
|
|
6967
|
+
|
|
6968
|
+
// src/server/api/projects.ts
|
|
6969
|
+
import { Hono as Hono7 } from "hono";
|
|
6970
|
+
import * as fs4 from "fs";
|
|
6971
|
+
import * as path4 from "path";
|
|
6972
|
+
import * as os4 from "os";
|
|
6973
|
+
var projectsRouter = new Hono7();
|
|
6974
|
+
projectsRouter.get("/", async (c) => {
|
|
6975
|
+
try {
|
|
6976
|
+
const projectsDir = path4.join(os4.homedir(), ".claude-code", "memory", "projects");
|
|
6977
|
+
if (!fs4.existsSync(projectsDir)) {
|
|
6978
|
+
return c.json({ projects: [] });
|
|
6979
|
+
}
|
|
6980
|
+
const projectHashes = fs4.readdirSync(projectsDir).filter((name) => {
|
|
6981
|
+
const fullPath = path4.join(projectsDir, name);
|
|
6982
|
+
return fs4.statSync(fullPath).isDirectory();
|
|
6983
|
+
});
|
|
6984
|
+
const registry = loadSessionRegistry();
|
|
6985
|
+
const hashToPath = /* @__PURE__ */ new Map();
|
|
6986
|
+
for (const entry of Object.values(registry.sessions)) {
|
|
6987
|
+
if (!hashToPath.has(entry.projectHash)) {
|
|
6988
|
+
hashToPath.set(entry.projectHash, entry.projectPath);
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
const projects = projectHashes.map((hash) => {
|
|
6992
|
+
const dirPath = path4.join(projectsDir, hash);
|
|
6993
|
+
const dbPath = path4.join(dirPath, "events.sqlite");
|
|
6994
|
+
let dbSize = 0;
|
|
6995
|
+
if (fs4.existsSync(dbPath)) {
|
|
6996
|
+
dbSize = fs4.statSync(dbPath).size;
|
|
6997
|
+
}
|
|
6998
|
+
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
6999
|
+
return {
|
|
7000
|
+
hash,
|
|
7001
|
+
projectPath,
|
|
7002
|
+
projectName: path4.basename(projectPath),
|
|
7003
|
+
dbSize,
|
|
7004
|
+
dbSizeHuman: formatBytes(dbSize)
|
|
7005
|
+
};
|
|
7006
|
+
});
|
|
7007
|
+
projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
|
|
7008
|
+
return c.json({ projects });
|
|
7009
|
+
} catch (error) {
|
|
7010
|
+
return c.json({ projects: [], error: error.message }, 500);
|
|
7011
|
+
}
|
|
7012
|
+
});
|
|
7013
|
+
function formatBytes(bytes) {
|
|
7014
|
+
if (bytes === 0)
|
|
7015
|
+
return "0 B";
|
|
7016
|
+
const k = 1024;
|
|
7017
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
7018
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
7019
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
7020
|
+
}
|
|
7021
|
+
|
|
7022
|
+
// src/server/api/chat.ts
|
|
7023
|
+
import { Hono as Hono8 } from "hono";
|
|
7024
|
+
import { streamSSE } from "hono/streaming";
|
|
7025
|
+
import { spawn } from "child_process";
|
|
7026
|
+
var chatRouter = new Hono8();
|
|
7027
|
+
var CLAUDE_TIMEOUT_MS = 12e4;
|
|
7028
|
+
chatRouter.post("/", async (c) => {
|
|
7029
|
+
let body;
|
|
7030
|
+
try {
|
|
7031
|
+
body = await c.req.json();
|
|
7032
|
+
} catch {
|
|
7033
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
7034
|
+
}
|
|
7035
|
+
if (!body.message?.trim()) {
|
|
7036
|
+
return c.json({ error: "Message is required" }, 400);
|
|
7037
|
+
}
|
|
7038
|
+
const memoryService = getServiceFromQuery(c);
|
|
7039
|
+
try {
|
|
7040
|
+
await memoryService.initialize();
|
|
7041
|
+
let memoryContext = "";
|
|
7042
|
+
let statsContext = "";
|
|
7043
|
+
try {
|
|
7044
|
+
const result = await memoryService.retrieveMemories(body.message, {
|
|
7045
|
+
topK: 8,
|
|
7046
|
+
minScore: 0.5
|
|
7047
|
+
});
|
|
7048
|
+
if (result.memories.length > 0) {
|
|
7049
|
+
const parts = ["## Relevant Memories\n"];
|
|
7050
|
+
for (const m of result.memories) {
|
|
7051
|
+
const date = new Date(m.event.timestamp).toISOString().split("T")[0];
|
|
7052
|
+
const content = m.event.content.slice(0, 500);
|
|
7053
|
+
parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
7054
|
+
parts.push(content);
|
|
7055
|
+
if (m.sessionContext) {
|
|
7056
|
+
parts.push(`_Context: ${m.sessionContext}_`);
|
|
7057
|
+
}
|
|
7058
|
+
parts.push("");
|
|
7059
|
+
}
|
|
7060
|
+
memoryContext = parts.join("\n");
|
|
7061
|
+
}
|
|
7062
|
+
} catch {
|
|
7063
|
+
}
|
|
7064
|
+
try {
|
|
7065
|
+
const stats = await memoryService.getStats();
|
|
7066
|
+
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
7067
|
+
statsContext = [
|
|
7068
|
+
"## Memory Stats",
|
|
7069
|
+
`- Total events: ${stats.totalEvents}`,
|
|
7070
|
+
`- Vector nodes: ${stats.vectorCount}`,
|
|
7071
|
+
`- By level: ${levels}`
|
|
7072
|
+
].join("\n");
|
|
7073
|
+
} catch {
|
|
7074
|
+
}
|
|
7075
|
+
const fullPrompt = buildPrompt(
|
|
7076
|
+
statsContext,
|
|
7077
|
+
memoryContext,
|
|
7078
|
+
body.history || [],
|
|
7079
|
+
body.message
|
|
7080
|
+
);
|
|
7081
|
+
return streamSSE(c, async (stream) => {
|
|
7082
|
+
try {
|
|
7083
|
+
await streamClaudeResponse(fullPrompt, stream);
|
|
7084
|
+
} catch (err) {
|
|
7085
|
+
await stream.writeSSE({
|
|
7086
|
+
event: "error",
|
|
7087
|
+
data: JSON.stringify({ error: err.message })
|
|
7088
|
+
});
|
|
7089
|
+
}
|
|
7090
|
+
});
|
|
7091
|
+
} catch (error) {
|
|
7092
|
+
return c.json({ error: error.message }, 500);
|
|
7093
|
+
} finally {
|
|
7094
|
+
await memoryService.shutdown();
|
|
7095
|
+
}
|
|
7096
|
+
});
|
|
7097
|
+
function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
7098
|
+
const parts = [];
|
|
7099
|
+
parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
|
|
7100
|
+
parts.push("The memory system tracks coding sessions, tool usage, prompts, and responses.");
|
|
7101
|
+
parts.push("Answer concisely based on the memory context below. If you don't have enough data, say so.");
|
|
7102
|
+
parts.push("Use markdown formatting in your responses.\n");
|
|
7103
|
+
if (statsContext) {
|
|
7104
|
+
parts.push(statsContext);
|
|
7105
|
+
parts.push("");
|
|
7106
|
+
}
|
|
7107
|
+
if (memoryContext) {
|
|
7108
|
+
parts.push(memoryContext);
|
|
7109
|
+
} else {
|
|
7110
|
+
parts.push("No directly relevant memories found for this query.");
|
|
7111
|
+
parts.push("Answer based on general knowledge or suggest the user rephrase.\n");
|
|
7112
|
+
}
|
|
7113
|
+
parts.push("---\n");
|
|
7114
|
+
const recentHistory = history.slice(-10);
|
|
7115
|
+
if (recentHistory.length > 0) {
|
|
7116
|
+
parts.push("## Conversation History\n");
|
|
7117
|
+
for (const msg of recentHistory) {
|
|
7118
|
+
const prefix = msg.role === "user" ? "User" : "Assistant";
|
|
7119
|
+
parts.push(`**${prefix}:** ${msg.content}
|
|
7120
|
+
`);
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
parts.push(`**User:** ${currentMessage}`);
|
|
7124
|
+
return parts.join("\n");
|
|
7125
|
+
}
|
|
7126
|
+
function streamClaudeResponse(prompt, stream) {
|
|
7127
|
+
return new Promise((resolve2, reject) => {
|
|
7128
|
+
const proc = spawn("claude", [
|
|
7129
|
+
"-p",
|
|
7130
|
+
"--output-format",
|
|
7131
|
+
"stream-json",
|
|
7132
|
+
"--verbose"
|
|
7133
|
+
], {
|
|
7134
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7135
|
+
env: { ...process.env }
|
|
7136
|
+
});
|
|
7137
|
+
const timeout = setTimeout(() => {
|
|
7138
|
+
proc.kill("SIGTERM");
|
|
7139
|
+
reject(new Error("Chat response timed out after 2 minutes"));
|
|
7140
|
+
}, CLAUDE_TIMEOUT_MS);
|
|
7141
|
+
proc.stdin.write(prompt);
|
|
7142
|
+
proc.stdin.end();
|
|
7143
|
+
let buffer = "";
|
|
7144
|
+
let lastSentText = "";
|
|
7145
|
+
proc.stdout.on("data", async (chunk) => {
|
|
7146
|
+
buffer += chunk.toString();
|
|
7147
|
+
const lines = buffer.split("\n");
|
|
7148
|
+
buffer = lines.pop() || "";
|
|
7149
|
+
for (const line of lines) {
|
|
7150
|
+
if (!line.trim())
|
|
7151
|
+
continue;
|
|
7152
|
+
try {
|
|
7153
|
+
const parsed = JSON.parse(line);
|
|
7154
|
+
if (parsed.type === "assistant" && parsed.message?.content) {
|
|
7155
|
+
const textBlocks = parsed.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
7156
|
+
if (textBlocks.length > lastSentText.length) {
|
|
7157
|
+
const delta = textBlocks.slice(lastSentText.length);
|
|
7158
|
+
lastSentText = textBlocks;
|
|
7159
|
+
await stream.writeSSE({
|
|
7160
|
+
event: "message",
|
|
7161
|
+
data: JSON.stringify({ content: delta })
|
|
7162
|
+
});
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
if (parsed.type === "result") {
|
|
7166
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
7167
|
+
}
|
|
7168
|
+
} catch {
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
});
|
|
7172
|
+
proc.stderr.on("data", (chunk) => {
|
|
7173
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
7174
|
+
console.error("[chat] claude stderr:", chunk.toString());
|
|
7175
|
+
}
|
|
7176
|
+
});
|
|
7177
|
+
proc.on("error", (err) => {
|
|
7178
|
+
clearTimeout(timeout);
|
|
7179
|
+
if (err.code === "ENOENT") {
|
|
7180
|
+
reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
|
|
7181
|
+
} else {
|
|
7182
|
+
reject(err);
|
|
7183
|
+
}
|
|
7184
|
+
});
|
|
7185
|
+
proc.on("close", async (code) => {
|
|
7186
|
+
clearTimeout(timeout);
|
|
7187
|
+
if (buffer.trim()) {
|
|
7188
|
+
try {
|
|
7189
|
+
const parsed = JSON.parse(buffer);
|
|
7190
|
+
if (parsed.type === "result") {
|
|
7191
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
7192
|
+
}
|
|
7193
|
+
} catch {
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
if (code !== 0 && code !== null) {
|
|
7197
|
+
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
7198
|
+
} else {
|
|
7199
|
+
resolve2();
|
|
7200
|
+
}
|
|
7201
|
+
});
|
|
7202
|
+
});
|
|
7203
|
+
}
|
|
7204
|
+
|
|
6064
7205
|
// src/server/api/index.ts
|
|
6065
|
-
var apiRouter = new
|
|
7206
|
+
var apiRouter = new Hono9().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter);
|
|
6066
7207
|
|
|
6067
7208
|
// src/server/index.ts
|
|
6068
|
-
var app = new
|
|
7209
|
+
var app = new Hono10();
|
|
6069
7210
|
app.use("/*", cors());
|
|
6070
7211
|
app.use("/*", logger());
|
|
6071
7212
|
app.route("/api", apiRouter);
|
|
6072
7213
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
6073
|
-
var uiPath =
|
|
6074
|
-
if (
|
|
7214
|
+
var uiPath = path5.join(__dirname, "../../dist/ui");
|
|
7215
|
+
if (fs5.existsSync(uiPath)) {
|
|
6075
7216
|
app.use("/*", serveStatic({ root: uiPath }));
|
|
6076
7217
|
}
|
|
6077
7218
|
app.get("*", (c) => {
|
|
6078
|
-
const indexPath =
|
|
6079
|
-
if (
|
|
6080
|
-
return c.html(
|
|
7219
|
+
const indexPath = path5.join(uiPath, "index.html");
|
|
7220
|
+
if (fs5.existsSync(indexPath)) {
|
|
7221
|
+
return c.html(fs5.readFileSync(indexPath, "utf-8"));
|
|
6081
7222
|
}
|
|
6082
7223
|
return c.text('UI not built. Run "npm run build:ui" first.', 404);
|
|
6083
7224
|
});
|
|
@@ -6115,28 +7256,28 @@ if (isMainModule) {
|
|
|
6115
7256
|
}
|
|
6116
7257
|
|
|
6117
7258
|
// src/cli/index.ts
|
|
6118
|
-
var CLAUDE_SETTINGS_PATH =
|
|
7259
|
+
var CLAUDE_SETTINGS_PATH = path6.join(os5.homedir(), ".claude", "settings.json");
|
|
6119
7260
|
function getPluginPath() {
|
|
6120
7261
|
const possiblePaths = [
|
|
6121
|
-
|
|
7262
|
+
path6.join(__dirname, ".."),
|
|
6122
7263
|
// When running from dist/cli
|
|
6123
|
-
|
|
7264
|
+
path6.join(__dirname, "../..", "dist"),
|
|
6124
7265
|
// When running from src
|
|
6125
|
-
|
|
7266
|
+
path6.join(process.cwd(), "dist")
|
|
6126
7267
|
// Current working directory
|
|
6127
7268
|
];
|
|
6128
7269
|
for (const p of possiblePaths) {
|
|
6129
|
-
const hooksPath =
|
|
6130
|
-
if (
|
|
7270
|
+
const hooksPath = path6.join(p, "hooks", "user-prompt-submit.js");
|
|
7271
|
+
if (fs6.existsSync(hooksPath)) {
|
|
6131
7272
|
return p;
|
|
6132
7273
|
}
|
|
6133
7274
|
}
|
|
6134
|
-
return
|
|
7275
|
+
return path6.join(os5.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
|
|
6135
7276
|
}
|
|
6136
7277
|
function loadClaudeSettings() {
|
|
6137
7278
|
try {
|
|
6138
|
-
if (
|
|
6139
|
-
const content =
|
|
7279
|
+
if (fs6.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
7280
|
+
const content = fs6.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
6140
7281
|
return JSON.parse(content);
|
|
6141
7282
|
}
|
|
6142
7283
|
} catch (error) {
|
|
@@ -6145,13 +7286,13 @@ function loadClaudeSettings() {
|
|
|
6145
7286
|
return {};
|
|
6146
7287
|
}
|
|
6147
7288
|
function saveClaudeSettings(settings) {
|
|
6148
|
-
const dir =
|
|
6149
|
-
if (!
|
|
6150
|
-
|
|
7289
|
+
const dir = path6.dirname(CLAUDE_SETTINGS_PATH);
|
|
7290
|
+
if (!fs6.existsSync(dir)) {
|
|
7291
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
6151
7292
|
}
|
|
6152
7293
|
const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
|
|
6153
|
-
|
|
6154
|
-
|
|
7294
|
+
fs6.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
|
|
7295
|
+
fs6.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
6155
7296
|
}
|
|
6156
7297
|
function getHooksConfig(pluginPath) {
|
|
6157
7298
|
return {
|
|
@@ -6161,7 +7302,7 @@ function getHooksConfig(pluginPath) {
|
|
|
6161
7302
|
hooks: [
|
|
6162
7303
|
{
|
|
6163
7304
|
type: "command",
|
|
6164
|
-
command: `node ${
|
|
7305
|
+
command: `node ${path6.join(pluginPath, "hooks", "user-prompt-submit.js")}`
|
|
6165
7306
|
}
|
|
6166
7307
|
]
|
|
6167
7308
|
}
|
|
@@ -6172,7 +7313,7 @@ function getHooksConfig(pluginPath) {
|
|
|
6172
7313
|
hooks: [
|
|
6173
7314
|
{
|
|
6174
7315
|
type: "command",
|
|
6175
|
-
command: `node ${
|
|
7316
|
+
command: `node ${path6.join(pluginPath, "hooks", "post-tool-use.js")}`
|
|
6176
7317
|
}
|
|
6177
7318
|
]
|
|
6178
7319
|
}
|
|
@@ -6184,8 +7325,8 @@ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI")
|
|
|
6184
7325
|
program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
|
|
6185
7326
|
try {
|
|
6186
7327
|
const pluginPath = options.path || getPluginPath();
|
|
6187
|
-
const userPromptHook =
|
|
6188
|
-
if (!
|
|
7328
|
+
const userPromptHook = path6.join(pluginPath, "hooks", "user-prompt-submit.js");
|
|
7329
|
+
if (!fs6.existsSync(userPromptHook)) {
|
|
6189
7330
|
console.error(`
|
|
6190
7331
|
\u274C Hook files not found at: ${pluginPath}`);
|
|
6191
7332
|
console.error(' Make sure you have built the plugin with "npm run build"');
|
|
@@ -6251,7 +7392,7 @@ program.command("status").description("Check plugin installation status").action
|
|
|
6251
7392
|
console.log("Hooks:");
|
|
6252
7393
|
console.log(` UserPromptSubmit: ${hasUserPromptHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6253
7394
|
console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6254
|
-
const hooksExist =
|
|
7395
|
+
const hooksExist = fs6.existsSync(path6.join(pluginPath, "hooks", "user-prompt-submit.js"));
|
|
6255
7396
|
console.log(`
|
|
6256
7397
|
Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
|
|
6257
7398
|
console.log(` Path: ${pluginPath}`);
|
|
@@ -6379,95 +7520,143 @@ program.command("process").description("Process pending embeddings").option("-p,
|
|
|
6379
7520
|
process.exit(1);
|
|
6380
7521
|
}
|
|
6381
7522
|
});
|
|
6382
|
-
|
|
7523
|
+
function renderProgress(event) {
|
|
7524
|
+
switch (event.phase) {
|
|
7525
|
+
case "scan":
|
|
7526
|
+
console.log(` \u{1F50D} ${event.message}`);
|
|
7527
|
+
break;
|
|
7528
|
+
case "session-start": {
|
|
7529
|
+
const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
|
|
7530
|
+
const sessionName = path6.basename(event.filePath, ".jsonl").slice(0, 8);
|
|
7531
|
+
process.stdout.write(
|
|
7532
|
+
`\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
|
|
7533
|
+
);
|
|
7534
|
+
break;
|
|
7535
|
+
}
|
|
7536
|
+
case "session-progress": {
|
|
7537
|
+
process.stdout.write(
|
|
7538
|
+
`\r \u{1F4C4} [${event.sessionIndex + 1}/...] ${event.messagesProcessed} msgs | +${event.imported} imported, ~${event.skipped} skipped `
|
|
7539
|
+
);
|
|
7540
|
+
break;
|
|
7541
|
+
}
|
|
7542
|
+
case "session-done": {
|
|
7543
|
+
const imported = event.importedPrompts + event.importedResponses;
|
|
7544
|
+
if (imported > 0) {
|
|
7545
|
+
process.stdout.write(
|
|
7546
|
+
`\r \u2705 [${event.sessionIndex + 1}] +${event.importedPrompts} prompts, +${event.importedResponses} responses${event.skipped > 0 ? `, ~${event.skipped} skipped` : ""}
|
|
7547
|
+
`
|
|
7548
|
+
);
|
|
7549
|
+
} else if (event.skipped > 0) {
|
|
7550
|
+
process.stdout.write(
|
|
7551
|
+
`\r \u23ED\uFE0F [${event.sessionIndex + 1}] All ${event.skipped} already imported
|
|
7552
|
+
`
|
|
7553
|
+
);
|
|
7554
|
+
} else {
|
|
7555
|
+
process.stdout.write(
|
|
7556
|
+
`\r \u23ED\uFE0F [${event.sessionIndex + 1}] Empty session
|
|
7557
|
+
`
|
|
7558
|
+
);
|
|
7559
|
+
}
|
|
7560
|
+
break;
|
|
7561
|
+
}
|
|
7562
|
+
case "embedding":
|
|
7563
|
+
process.stdout.write(
|
|
7564
|
+
`\r \u{1F9E0} Embeddings: ${event.processed}/${event.total} processed `
|
|
7565
|
+
);
|
|
7566
|
+
if (event.processed >= event.total) {
|
|
7567
|
+
process.stdout.write("\n");
|
|
7568
|
+
}
|
|
7569
|
+
break;
|
|
7570
|
+
case "done":
|
|
7571
|
+
break;
|
|
7572
|
+
}
|
|
7573
|
+
}
|
|
7574
|
+
function printImportSummary(result, embedCount) {
|
|
7575
|
+
console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
7576
|
+
console.log("\u2502 \u2705 Import Complete \u2502");
|
|
7577
|
+
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
7578
|
+
console.log(`\u2502 Sessions processed: ${String(result.totalSessions).padStart(8)} \u2502`);
|
|
7579
|
+
console.log(`\u2502 Total messages: ${String(result.totalMessages).padStart(8)} \u2502`);
|
|
7580
|
+
console.log(`\u2502 Imported prompts: ${String(result.importedPrompts).padStart(8)} \u2502`);
|
|
7581
|
+
console.log(`\u2502 Imported responses: ${String(result.importedResponses).padStart(8)} \u2502`);
|
|
7582
|
+
console.log(`\u2502 Skipped duplicates: ${String(result.skippedDuplicates).padStart(8)} \u2502`);
|
|
7583
|
+
console.log(`\u2502 Embeddings queued: ${String(embedCount).padStart(8)} \u2502`);
|
|
7584
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
7585
|
+
if (result.errors.length > 0) {
|
|
7586
|
+
console.log(`
|
|
7587
|
+
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
7588
|
+
for (const error of result.errors.slice(0, 5)) {
|
|
7589
|
+
console.log(` - ${error}`);
|
|
7590
|
+
}
|
|
7591
|
+
if (result.errors.length > 5) {
|
|
7592
|
+
console.log(` ... and ${result.errors.length - 5} more`);
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
program.command("import").description("Import existing Claude Code conversation history").option("-p, --project <path>", "Import from specific project path").option("-s, --session <file>", "Import specific session file (JSONL)").option("-a, --all", "Import all sessions from all projects").option("-l, --limit <number>", "Limit messages per session").option("-f, --force", "Force reimport: delete existing events and reimport with turn_id grouping").option("-v, --verbose", "Show detailed progress").action(async (options) => {
|
|
7597
|
+
const startTime = Date.now();
|
|
6383
7598
|
const targetProjectPath = options.project || process.cwd();
|
|
6384
7599
|
const service = getMemoryServiceForProject(targetProjectPath);
|
|
6385
7600
|
const importer = createSessionHistoryImporter(service);
|
|
7601
|
+
const importOpts = {
|
|
7602
|
+
limit: options.limit ? parseInt(options.limit) : void 0,
|
|
7603
|
+
force: options.force,
|
|
7604
|
+
verbose: options.verbose,
|
|
7605
|
+
onProgress: renderProgress
|
|
7606
|
+
};
|
|
6386
7607
|
try {
|
|
7608
|
+
console.log("\n\u23F3 Initializing memory service...");
|
|
6387
7609
|
await service.initialize();
|
|
7610
|
+
console.log(" \u2705 Ready\n");
|
|
7611
|
+
if (options.force) {
|
|
7612
|
+
console.log("\u{1F504} Force mode: existing events will be deleted and reimported with turn_id grouping\n");
|
|
7613
|
+
}
|
|
6388
7614
|
let result;
|
|
6389
7615
|
if (options.session) {
|
|
6390
|
-
console.log(`
|
|
6391
|
-
|
|
6392
|
-
console.log(` Target project: ${targetProjectPath}
|
|
7616
|
+
console.log(`\u{1F4E5} Importing session: ${options.session}`);
|
|
7617
|
+
console.log(` Target: ${targetProjectPath}
|
|
6393
7618
|
`);
|
|
6394
7619
|
result = await importer.importSessionFile(options.session, {
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
verbose: options.verbose
|
|
7620
|
+
...importOpts,
|
|
7621
|
+
projectPath: targetProjectPath
|
|
6398
7622
|
});
|
|
6399
7623
|
} else if (options.project) {
|
|
6400
|
-
console.log(
|
|
6401
|
-
\u{1F4E5} Importing project: ${options.project}
|
|
7624
|
+
console.log(`\u{1F4E5} Importing project: ${options.project}
|
|
6402
7625
|
`);
|
|
6403
|
-
result = await importer.importProject(options.project,
|
|
6404
|
-
limit: options.limit ? parseInt(options.limit) : void 0,
|
|
6405
|
-
verbose: options.verbose
|
|
6406
|
-
});
|
|
7626
|
+
result = await importer.importProject(options.project, importOpts);
|
|
6407
7627
|
} else if (options.all) {
|
|
6408
|
-
console.log("\
|
|
7628
|
+
console.log("\u{1F4E5} Importing all sessions from all projects");
|
|
6409
7629
|
console.log(" \u26A0\uFE0F Using global storage (use -p for project-specific)\n");
|
|
6410
7630
|
const globalService = getDefaultMemoryService();
|
|
6411
7631
|
const globalImporter = createSessionHistoryImporter(globalService);
|
|
6412
7632
|
await globalService.initialize();
|
|
6413
|
-
result = await globalImporter.importAll(
|
|
6414
|
-
|
|
6415
|
-
verbose: options.verbose
|
|
6416
|
-
});
|
|
6417
|
-
console.log("\n\u23F3 Processing embeddings...");
|
|
7633
|
+
result = await globalImporter.importAll(importOpts);
|
|
7634
|
+
console.log("\n\u{1F9E0} Processing embeddings...");
|
|
6418
7635
|
const embedCount2 = await globalService.processPendingEmbeddings();
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
console.log(`
|
|
6422
|
-
|
|
6423
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
6424
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
6425
|
-
console.log(`Embeddings processed: ${embedCount2}`);
|
|
6426
|
-
if (result.errors.length > 0) {
|
|
6427
|
-
console.log(`
|
|
6428
|
-
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
6429
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
6430
|
-
console.log(` - ${error}`);
|
|
6431
|
-
}
|
|
6432
|
-
if (result.errors.length > 5) {
|
|
6433
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
6434
|
-
}
|
|
6435
|
-
}
|
|
7636
|
+
const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
7637
|
+
printImportSummary(result, embedCount2);
|
|
7638
|
+
console.log(`
|
|
7639
|
+
\u23F1\uFE0F Completed in ${elapsed2}s`);
|
|
6436
7640
|
await globalService.shutdown();
|
|
6437
7641
|
return;
|
|
6438
7642
|
} else {
|
|
6439
7643
|
const cwd = process.cwd();
|
|
6440
|
-
console.log(
|
|
6441
|
-
\u{1F4E5} Importing sessions for current project: ${cwd}
|
|
7644
|
+
console.log(`\u{1F4E5} Importing sessions for: ${cwd}
|
|
6442
7645
|
`);
|
|
6443
7646
|
result = await importer.importProject(cwd, {
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
verbose: options.verbose
|
|
7647
|
+
...importOpts,
|
|
7648
|
+
projectPath: cwd
|
|
6447
7649
|
});
|
|
6448
7650
|
}
|
|
6449
|
-
console.log("\n\
|
|
7651
|
+
console.log("\n\u{1F9E0} Processing embeddings...");
|
|
6450
7652
|
const embedCount = await service.processPendingEmbeddings();
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
console.log(`
|
|
6454
|
-
|
|
6455
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
6456
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
6457
|
-
console.log(`Embeddings processed: ${embedCount}`);
|
|
6458
|
-
if (result.errors.length > 0) {
|
|
6459
|
-
console.log(`
|
|
6460
|
-
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
6461
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
6462
|
-
console.log(` - ${error}`);
|
|
6463
|
-
}
|
|
6464
|
-
if (result.errors.length > 5) {
|
|
6465
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
6466
|
-
}
|
|
6467
|
-
}
|
|
7653
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
7654
|
+
printImportSummary(result, embedCount);
|
|
7655
|
+
console.log(`
|
|
7656
|
+
\u23F1\uFE0F Completed in ${elapsed}s`);
|
|
6468
7657
|
await service.shutdown();
|
|
6469
7658
|
} catch (error) {
|
|
6470
|
-
console.error("Import failed:", error);
|
|
7659
|
+
console.error("\n\u274C Import failed:", error);
|
|
6471
7660
|
process.exit(1);
|
|
6472
7661
|
}
|
|
6473
7662
|
});
|