claude-memory-layer 1.0.10 → 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 +1266 -181
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +367 -7
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +598 -40
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +486 -49
- package/dist/hooks/session-end.js.map +3 -3
- package/dist/hooks/session-start.js +474 -22
- package/dist/hooks/session-start.js.map +3 -3
- package/dist/hooks/stop.js +586 -70
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +537 -27
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +938 -39
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +947 -48
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +475 -22
- 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 +444 -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 +44 -5
- 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 +137 -5
- 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/.history/package_20260202121115.json +0 -49
- package/test_access.js +0 -49
package/dist/server/api/index.js
CHANGED
|
@@ -4,17 +4,28 @@ import { dirname } from 'path';
|
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined")
|
|
11
|
+
return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
7
14
|
|
|
8
15
|
// src/server/api/index.ts
|
|
9
|
-
import { Hono as
|
|
16
|
+
import { Hono as Hono9 } from "hono";
|
|
10
17
|
|
|
11
18
|
// src/server/api/sessions.ts
|
|
12
19
|
import { Hono } from "hono";
|
|
13
20
|
|
|
21
|
+
// src/server/api/utils.ts
|
|
22
|
+
import * as path2 from "path";
|
|
23
|
+
import * as os2 from "os";
|
|
24
|
+
|
|
14
25
|
// src/services/memory-service.ts
|
|
15
26
|
import * as path from "path";
|
|
16
27
|
import * as os from "os";
|
|
17
|
-
import * as
|
|
28
|
+
import * as fs2 from "fs";
|
|
18
29
|
import * as crypto2 from "crypto";
|
|
19
30
|
|
|
20
31
|
// src/core/event-store.ts
|
|
@@ -72,11 +83,11 @@ function toDate(value) {
|
|
|
72
83
|
return new Date(value);
|
|
73
84
|
return new Date(String(value));
|
|
74
85
|
}
|
|
75
|
-
function createDatabase(
|
|
86
|
+
function createDatabase(path4, options) {
|
|
76
87
|
if (options?.readOnly) {
|
|
77
|
-
return new duckdb.Database(
|
|
88
|
+
return new duckdb.Database(path4, { access_mode: "READ_ONLY" });
|
|
78
89
|
}
|
|
79
|
-
return new duckdb.Database(
|
|
90
|
+
return new duckdb.Database(path4);
|
|
80
91
|
}
|
|
81
92
|
function dbRun(db, sql, params = []) {
|
|
82
93
|
return new Promise((resolve2, reject) => {
|
|
@@ -746,8 +757,14 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
746
757
|
|
|
747
758
|
// src/core/sqlite-wrapper.ts
|
|
748
759
|
import Database from "better-sqlite3";
|
|
749
|
-
|
|
750
|
-
|
|
760
|
+
import * as fs from "fs";
|
|
761
|
+
import * as nodePath from "path";
|
|
762
|
+
function createSQLiteDatabase(path4, options) {
|
|
763
|
+
const dir = nodePath.dirname(path4);
|
|
764
|
+
if (!fs.existsSync(dir)) {
|
|
765
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
766
|
+
}
|
|
767
|
+
const db = new Database(path4, {
|
|
751
768
|
readonly: options?.readonly ?? false
|
|
752
769
|
});
|
|
753
770
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -1014,6 +1031,23 @@ var SQLiteEventStore = class {
|
|
|
1014
1031
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
1015
1032
|
);
|
|
1016
1033
|
|
|
1034
|
+
-- Memory Helpfulness tracking
|
|
1035
|
+
CREATE TABLE IF NOT EXISTS memory_helpfulness (
|
|
1036
|
+
id TEXT PRIMARY KEY,
|
|
1037
|
+
event_id TEXT NOT NULL,
|
|
1038
|
+
session_id TEXT NOT NULL,
|
|
1039
|
+
retrieval_score REAL DEFAULT 0,
|
|
1040
|
+
query_preview TEXT,
|
|
1041
|
+
session_continued INTEGER DEFAULT 0,
|
|
1042
|
+
prompt_count_after INTEGER DEFAULT 0,
|
|
1043
|
+
tool_success_count INTEGER DEFAULT 0,
|
|
1044
|
+
tool_total_count INTEGER DEFAULT 0,
|
|
1045
|
+
was_reasked INTEGER DEFAULT 0,
|
|
1046
|
+
helpfulness_score REAL DEFAULT 0.5,
|
|
1047
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1048
|
+
measured_at TEXT
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1017
1051
|
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
1018
1052
|
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
1019
1053
|
target_name TEXT PRIMARY KEY,
|
|
@@ -1039,6 +1073,9 @@ var SQLiteEventStore = class {
|
|
|
1039
1073
|
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
1040
1074
|
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
1041
1075
|
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
1076
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
|
|
1077
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
|
|
1078
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
|
|
1042
1079
|
|
|
1043
1080
|
-- FTS5 Full-Text Search for fast keyword search
|
|
1044
1081
|
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
@@ -1082,6 +1119,15 @@ var SQLiteEventStore = class {
|
|
|
1082
1119
|
console.error("Error adding last_accessed_at column:", err);
|
|
1083
1120
|
}
|
|
1084
1121
|
}
|
|
1122
|
+
if (!columnNames.includes("turn_id")) {
|
|
1123
|
+
try {
|
|
1124
|
+
sqliteExec(this.db, `
|
|
1125
|
+
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
1126
|
+
`);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.error("Error adding turn_id column:", err);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1085
1131
|
try {
|
|
1086
1132
|
sqliteExec(this.db, `
|
|
1087
1133
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -1094,6 +1140,12 @@ var SQLiteEventStore = class {
|
|
|
1094
1140
|
`);
|
|
1095
1141
|
} catch (err) {
|
|
1096
1142
|
}
|
|
1143
|
+
try {
|
|
1144
|
+
sqliteExec(this.db, `
|
|
1145
|
+
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
1146
|
+
`);
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
}
|
|
1097
1149
|
this.initialized = true;
|
|
1098
1150
|
}
|
|
1099
1151
|
/**
|
|
@@ -1118,9 +1170,11 @@ var SQLiteEventStore = class {
|
|
|
1118
1170
|
const id = randomUUID2();
|
|
1119
1171
|
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1120
1172
|
try {
|
|
1173
|
+
const metadata = input.metadata || {};
|
|
1174
|
+
const turnId = metadata.turnId || null;
|
|
1121
1175
|
const insertEvent = this.db.prepare(`
|
|
1122
|
-
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1123
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1176
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1124
1178
|
`);
|
|
1125
1179
|
const insertDedup = this.db.prepare(`
|
|
1126
1180
|
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
@@ -1137,7 +1191,8 @@ var SQLiteEventStore = class {
|
|
|
1137
1191
|
input.content,
|
|
1138
1192
|
canonicalKey,
|
|
1139
1193
|
dedupeKey,
|
|
1140
|
-
JSON.stringify(
|
|
1194
|
+
JSON.stringify(metadata),
|
|
1195
|
+
turnId
|
|
1141
1196
|
);
|
|
1142
1197
|
insertDedup.run(dedupeKey, id);
|
|
1143
1198
|
insertLevel.run(id);
|
|
@@ -1487,11 +1542,11 @@ var SQLiteEventStore = class {
|
|
|
1487
1542
|
);
|
|
1488
1543
|
}
|
|
1489
1544
|
/**
|
|
1490
|
-
* Get most accessed memories
|
|
1545
|
+
* Get most accessed memories (falls back to recent events if none accessed)
|
|
1491
1546
|
*/
|
|
1492
1547
|
async getMostAccessed(limit = 10) {
|
|
1493
1548
|
await this.initialize();
|
|
1494
|
-
|
|
1549
|
+
let rows = sqliteAll(
|
|
1495
1550
|
this.db,
|
|
1496
1551
|
`SELECT * FROM events
|
|
1497
1552
|
WHERE access_count > 0
|
|
@@ -1499,8 +1554,166 @@ var SQLiteEventStore = class {
|
|
|
1499
1554
|
LIMIT ?`,
|
|
1500
1555
|
[limit]
|
|
1501
1556
|
);
|
|
1557
|
+
if (rows.length === 0) {
|
|
1558
|
+
rows = sqliteAll(
|
|
1559
|
+
this.db,
|
|
1560
|
+
`SELECT * FROM events
|
|
1561
|
+
ORDER BY timestamp DESC
|
|
1562
|
+
LIMIT ?`,
|
|
1563
|
+
[limit]
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1502
1566
|
return rows.map((row) => this.rowToEvent(row));
|
|
1503
1567
|
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Record a memory retrieval for helpfulness tracking
|
|
1570
|
+
*/
|
|
1571
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
1572
|
+
if (this.readOnly)
|
|
1573
|
+
return;
|
|
1574
|
+
await this.initialize();
|
|
1575
|
+
const id = randomUUID2();
|
|
1576
|
+
sqliteRun(
|
|
1577
|
+
this.db,
|
|
1578
|
+
`INSERT INTO memory_helpfulness (id, event_id, session_id, retrieval_score, query_preview, created_at)
|
|
1579
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
1580
|
+
[id, eventId, sessionId, score, query.slice(0, 100)]
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Evaluate helpfulness for all retrievals in a session
|
|
1585
|
+
* Called at session end - uses behavioral signals to compute score
|
|
1586
|
+
*/
|
|
1587
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
1588
|
+
if (this.readOnly)
|
|
1589
|
+
return;
|
|
1590
|
+
await this.initialize();
|
|
1591
|
+
const retrievals = sqliteAll(
|
|
1592
|
+
this.db,
|
|
1593
|
+
`SELECT * FROM memory_helpfulness WHERE session_id = ? AND measured_at IS NULL`,
|
|
1594
|
+
[sessionId]
|
|
1595
|
+
);
|
|
1596
|
+
if (retrievals.length === 0)
|
|
1597
|
+
return;
|
|
1598
|
+
const sessionEvents = sqliteAll(
|
|
1599
|
+
this.db,
|
|
1600
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
1601
|
+
[sessionId]
|
|
1602
|
+
);
|
|
1603
|
+
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
1604
|
+
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
1605
|
+
let toolSuccessCount = 0;
|
|
1606
|
+
let toolTotalCount = toolEvents.length;
|
|
1607
|
+
for (const t of toolEvents) {
|
|
1608
|
+
try {
|
|
1609
|
+
const content = JSON.parse(t.content);
|
|
1610
|
+
if (content.success !== false)
|
|
1611
|
+
toolSuccessCount++;
|
|
1612
|
+
} catch {
|
|
1613
|
+
toolSuccessCount++;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const toolSuccessRatio = toolTotalCount > 0 ? toolSuccessCount / toolTotalCount : 0.5;
|
|
1617
|
+
for (const retrieval of retrievals) {
|
|
1618
|
+
const retrievalTime = retrieval.created_at;
|
|
1619
|
+
const eventsAfter = sessionEvents.filter((e) => e.timestamp > retrievalTime);
|
|
1620
|
+
const sessionContinued = eventsAfter.length > 0 ? 1 : 0;
|
|
1621
|
+
const promptsAfter = promptEvents.filter((e) => e.timestamp > retrievalTime);
|
|
1622
|
+
const promptCountAfter = promptsAfter.length;
|
|
1623
|
+
const queryWords = new Set((retrieval.query_preview || "").toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1624
|
+
let wasReasked = 0;
|
|
1625
|
+
for (const p of promptsAfter) {
|
|
1626
|
+
const pWords = new Set(p.content.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1627
|
+
let overlap = 0;
|
|
1628
|
+
for (const w of queryWords) {
|
|
1629
|
+
if (pWords.has(w))
|
|
1630
|
+
overlap++;
|
|
1631
|
+
}
|
|
1632
|
+
if (queryWords.size > 0 && overlap / queryWords.size > 0.5) {
|
|
1633
|
+
wasReasked = 1;
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const retrievalScore = retrieval.retrieval_score || 0;
|
|
1638
|
+
const helpfulnessScore = 0.3 * Math.min(retrievalScore, 1) + 0.25 * (sessionContinued ? 1 : 0) + 0.25 * toolSuccessRatio + 0.2 * (wasReasked ? 0 : 1);
|
|
1639
|
+
sqliteRun(
|
|
1640
|
+
this.db,
|
|
1641
|
+
`UPDATE memory_helpfulness
|
|
1642
|
+
SET session_continued = ?, prompt_count_after = ?,
|
|
1643
|
+
tool_success_count = ?, tool_total_count = ?,
|
|
1644
|
+
was_reasked = ?, helpfulness_score = ?,
|
|
1645
|
+
measured_at = datetime('now')
|
|
1646
|
+
WHERE id = ?`,
|
|
1647
|
+
[
|
|
1648
|
+
sessionContinued,
|
|
1649
|
+
promptCountAfter,
|
|
1650
|
+
toolSuccessCount,
|
|
1651
|
+
toolTotalCount,
|
|
1652
|
+
wasReasked,
|
|
1653
|
+
helpfulnessScore,
|
|
1654
|
+
retrieval.id
|
|
1655
|
+
]
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Get most helpful memories ranked by helpfulness score
|
|
1661
|
+
*/
|
|
1662
|
+
async getHelpfulMemories(limit = 10) {
|
|
1663
|
+
await this.initialize();
|
|
1664
|
+
const rows = sqliteAll(
|
|
1665
|
+
this.db,
|
|
1666
|
+
`SELECT
|
|
1667
|
+
mh.event_id,
|
|
1668
|
+
AVG(mh.helpfulness_score) as avg_score,
|
|
1669
|
+
COUNT(*) as eval_count,
|
|
1670
|
+
e.content,
|
|
1671
|
+
e.access_count
|
|
1672
|
+
FROM memory_helpfulness mh
|
|
1673
|
+
JOIN events e ON e.id = mh.event_id
|
|
1674
|
+
WHERE mh.measured_at IS NOT NULL
|
|
1675
|
+
GROUP BY mh.event_id
|
|
1676
|
+
ORDER BY avg_score DESC
|
|
1677
|
+
LIMIT ?`,
|
|
1678
|
+
[limit]
|
|
1679
|
+
);
|
|
1680
|
+
return rows.map((r) => ({
|
|
1681
|
+
eventId: r.event_id,
|
|
1682
|
+
summary: r.content.substring(0, 200) + (r.content.length > 200 ? "..." : ""),
|
|
1683
|
+
helpfulnessScore: Math.round(r.avg_score * 100) / 100,
|
|
1684
|
+
accessCount: r.access_count || 0,
|
|
1685
|
+
evaluationCount: r.eval_count
|
|
1686
|
+
}));
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Get helpfulness statistics for dashboard
|
|
1690
|
+
*/
|
|
1691
|
+
async getHelpfulnessStats() {
|
|
1692
|
+
await this.initialize();
|
|
1693
|
+
const stats = sqliteGet(
|
|
1694
|
+
this.db,
|
|
1695
|
+
`SELECT
|
|
1696
|
+
AVG(helpfulness_score) as avg_score,
|
|
1697
|
+
COUNT(*) as total_evaluated,
|
|
1698
|
+
SUM(CASE WHEN helpfulness_score >= 0.7 THEN 1 ELSE 0 END) as helpful,
|
|
1699
|
+
SUM(CASE WHEN helpfulness_score >= 0.4 AND helpfulness_score < 0.7 THEN 1 ELSE 0 END) as neutral,
|
|
1700
|
+
SUM(CASE WHEN helpfulness_score < 0.4 THEN 1 ELSE 0 END) as unhelpful
|
|
1701
|
+
FROM memory_helpfulness
|
|
1702
|
+
WHERE measured_at IS NOT NULL`
|
|
1703
|
+
);
|
|
1704
|
+
const totalRow = sqliteGet(
|
|
1705
|
+
this.db,
|
|
1706
|
+
`SELECT COUNT(*) as total FROM memory_helpfulness`
|
|
1707
|
+
);
|
|
1708
|
+
return {
|
|
1709
|
+
avgScore: Math.round((stats?.avg_score || 0) * 100) / 100,
|
|
1710
|
+
totalEvaluated: stats?.total_evaluated || 0,
|
|
1711
|
+
totalRetrievals: totalRow?.total || 0,
|
|
1712
|
+
helpful: stats?.helpful || 0,
|
|
1713
|
+
neutral: stats?.neutral || 0,
|
|
1714
|
+
unhelpful: stats?.unhelpful || 0
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1504
1717
|
/**
|
|
1505
1718
|
* Fast keyword search using FTS5
|
|
1506
1719
|
* Returns events matching the search query, ranked by relevance
|
|
@@ -1569,6 +1782,143 @@ var SQLiteEventStore = class {
|
|
|
1569
1782
|
async close() {
|
|
1570
1783
|
sqliteClose(this.db);
|
|
1571
1784
|
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Get events grouped by turn_id for a session
|
|
1787
|
+
* Returns turns ordered by first event timestamp (newest first)
|
|
1788
|
+
*/
|
|
1789
|
+
async getSessionTurns(sessionId, options) {
|
|
1790
|
+
await this.initialize();
|
|
1791
|
+
const limit = options?.limit || 20;
|
|
1792
|
+
const offset = options?.offset || 0;
|
|
1793
|
+
const turnRows = sqliteAll(
|
|
1794
|
+
this.db,
|
|
1795
|
+
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
1796
|
+
FROM events
|
|
1797
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
1798
|
+
GROUP BY turn_id
|
|
1799
|
+
ORDER BY min_ts DESC
|
|
1800
|
+
LIMIT ? OFFSET ?`,
|
|
1801
|
+
[sessionId, limit, offset]
|
|
1802
|
+
);
|
|
1803
|
+
const turns = [];
|
|
1804
|
+
for (const turnRow of turnRows) {
|
|
1805
|
+
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
1806
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
1807
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
1808
|
+
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
1809
|
+
turns.push({
|
|
1810
|
+
turnId: turnRow.turn_id,
|
|
1811
|
+
events,
|
|
1812
|
+
startedAt: toDateFromSQLite(turnRow.min_ts),
|
|
1813
|
+
promptPreview: promptEvent ? promptEvent.content.slice(0, 200) + (promptEvent.content.length > 200 ? "..." : "") : "(no prompt)",
|
|
1814
|
+
eventCount: events.length,
|
|
1815
|
+
toolCount: toolEvents.length,
|
|
1816
|
+
hasResponse
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return turns;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Get all events for a specific turn_id
|
|
1823
|
+
*/
|
|
1824
|
+
async getEventsByTurn(turnId) {
|
|
1825
|
+
await this.initialize();
|
|
1826
|
+
const rows = sqliteAll(
|
|
1827
|
+
this.db,
|
|
1828
|
+
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
1829
|
+
[turnId]
|
|
1830
|
+
);
|
|
1831
|
+
return rows.map(this.rowToEvent);
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Count total turns for a session
|
|
1835
|
+
*/
|
|
1836
|
+
async countSessionTurns(sessionId) {
|
|
1837
|
+
await this.initialize();
|
|
1838
|
+
const row = sqliteGet(
|
|
1839
|
+
this.db,
|
|
1840
|
+
`SELECT COUNT(DISTINCT turn_id) as count
|
|
1841
|
+
FROM events
|
|
1842
|
+
WHERE session_id = ? AND turn_id IS NOT NULL`,
|
|
1843
|
+
[sessionId]
|
|
1844
|
+
);
|
|
1845
|
+
return row?.count || 0;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Migrate existing events: backfill turn_id for events that have turnId in metadata
|
|
1849
|
+
* but no turn_id column value (for events stored before this migration)
|
|
1850
|
+
*/
|
|
1851
|
+
async backfillTurnIds() {
|
|
1852
|
+
await this.initialize();
|
|
1853
|
+
const rows = sqliteAll(
|
|
1854
|
+
this.db,
|
|
1855
|
+
`SELECT id, metadata FROM events
|
|
1856
|
+
WHERE turn_id IS NULL AND metadata IS NOT NULL AND metadata LIKE '%turnId%'`
|
|
1857
|
+
);
|
|
1858
|
+
let updated = 0;
|
|
1859
|
+
for (const row of rows) {
|
|
1860
|
+
try {
|
|
1861
|
+
const metadata = JSON.parse(row.metadata);
|
|
1862
|
+
if (metadata.turnId) {
|
|
1863
|
+
sqliteRun(
|
|
1864
|
+
this.db,
|
|
1865
|
+
`UPDATE events SET turn_id = ? WHERE id = ?`,
|
|
1866
|
+
[metadata.turnId, row.id]
|
|
1867
|
+
);
|
|
1868
|
+
updated++;
|
|
1869
|
+
}
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return updated;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Delete all events for a session (for force reimport)
|
|
1877
|
+
*/
|
|
1878
|
+
async deleteSessionEvents(sessionId) {
|
|
1879
|
+
await this.initialize();
|
|
1880
|
+
const events = sqliteAll(
|
|
1881
|
+
this.db,
|
|
1882
|
+
`SELECT id FROM events WHERE session_id = ?`,
|
|
1883
|
+
[sessionId]
|
|
1884
|
+
);
|
|
1885
|
+
if (events.length === 0)
|
|
1886
|
+
return 0;
|
|
1887
|
+
const eventIds = events.map((e) => e.id);
|
|
1888
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
1889
|
+
const ftsTriggersDropped = [];
|
|
1890
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
1891
|
+
try {
|
|
1892
|
+
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
1893
|
+
ftsTriggersDropped.push(triggerName);
|
|
1894
|
+
} catch {
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
1898
|
+
try {
|
|
1899
|
+
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
1904
|
+
if (ftsTriggersDropped.length > 0) {
|
|
1905
|
+
try {
|
|
1906
|
+
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
1907
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
1908
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
1909
|
+
END`);
|
|
1910
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
1911
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
1912
|
+
END`);
|
|
1913
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
1914
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
1915
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
1916
|
+
END`);
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return result.changes || 0;
|
|
1921
|
+
}
|
|
1572
1922
|
/**
|
|
1573
1923
|
* Convert database row to MemoryEvent
|
|
1574
1924
|
*/
|
|
@@ -1589,6 +1939,9 @@ var SQLiteEventStore = class {
|
|
|
1589
1939
|
if (row.last_accessed_at !== void 0) {
|
|
1590
1940
|
event.last_accessed_at = row.last_accessed_at;
|
|
1591
1941
|
}
|
|
1942
|
+
if (row.turn_id !== void 0 && row.turn_id !== null) {
|
|
1943
|
+
event.turn_id = row.turn_id;
|
|
1944
|
+
}
|
|
1592
1945
|
return event;
|
|
1593
1946
|
}
|
|
1594
1947
|
};
|
|
@@ -1800,7 +2153,16 @@ var VectorStore = class {
|
|
|
1800
2153
|
metadata: JSON.stringify(record.metadata || {})
|
|
1801
2154
|
};
|
|
1802
2155
|
if (!this.table) {
|
|
1803
|
-
|
|
2156
|
+
try {
|
|
2157
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
2158
|
+
} catch (e) {
|
|
2159
|
+
if (e?.message?.includes("already exists")) {
|
|
2160
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2161
|
+
await this.table.add([data]);
|
|
2162
|
+
} else {
|
|
2163
|
+
throw e;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
1804
2166
|
} else {
|
|
1805
2167
|
await this.table.add([data]);
|
|
1806
2168
|
}
|
|
@@ -1826,7 +2188,16 @@ var VectorStore = class {
|
|
|
1826
2188
|
metadata: JSON.stringify(record.metadata || {})
|
|
1827
2189
|
}));
|
|
1828
2190
|
if (!this.table) {
|
|
1829
|
-
|
|
2191
|
+
try {
|
|
2192
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
2193
|
+
} catch (e) {
|
|
2194
|
+
if (e?.message?.includes("already exists")) {
|
|
2195
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2196
|
+
await this.table.add(data);
|
|
2197
|
+
} else {
|
|
2198
|
+
throw e;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
1830
2201
|
} else {
|
|
1831
2202
|
await this.table.add(data);
|
|
1832
2203
|
}
|
|
@@ -4572,7 +4943,7 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
4572
4943
|
function normalizePath(projectPath) {
|
|
4573
4944
|
const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
4574
4945
|
try {
|
|
4575
|
-
return
|
|
4946
|
+
return fs2.realpathSync(expanded);
|
|
4576
4947
|
} catch {
|
|
4577
4948
|
return path.resolve(expanded);
|
|
4578
4949
|
}
|
|
@@ -4587,6 +4958,17 @@ function getProjectStoragePath(projectPath) {
|
|
|
4587
4958
|
}
|
|
4588
4959
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
4589
4960
|
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
4961
|
+
function loadSessionRegistry() {
|
|
4962
|
+
try {
|
|
4963
|
+
if (fs2.existsSync(REGISTRY_PATH)) {
|
|
4964
|
+
const data = fs2.readFileSync(REGISTRY_PATH, "utf-8");
|
|
4965
|
+
return JSON.parse(data);
|
|
4966
|
+
}
|
|
4967
|
+
} catch (error) {
|
|
4968
|
+
console.error("Failed to load session registry:", error);
|
|
4969
|
+
}
|
|
4970
|
+
return { version: 1, sessions: {} };
|
|
4971
|
+
}
|
|
4590
4972
|
var MemoryService = class {
|
|
4591
4973
|
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4592
4974
|
sqliteStore;
|
|
@@ -4620,8 +5002,8 @@ var MemoryService = class {
|
|
|
4620
5002
|
const storagePath = this.expandPath(config.storagePath);
|
|
4621
5003
|
this.readOnly = config.readOnly ?? false;
|
|
4622
5004
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
4623
|
-
if (!this.readOnly && !
|
|
4624
|
-
|
|
5005
|
+
if (!this.readOnly && !fs2.existsSync(storagePath)) {
|
|
5006
|
+
fs2.mkdirSync(storagePath, { recursive: true });
|
|
4625
5007
|
}
|
|
4626
5008
|
this.projectHash = config.projectHash || null;
|
|
4627
5009
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
@@ -4716,8 +5098,8 @@ var MemoryService = class {
|
|
|
4716
5098
|
*/
|
|
4717
5099
|
async initializeSharedStore() {
|
|
4718
5100
|
const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
|
|
4719
|
-
if (!
|
|
4720
|
-
|
|
5101
|
+
if (!fs2.existsSync(sharedPath)) {
|
|
5102
|
+
fs2.mkdirSync(sharedPath, { recursive: true });
|
|
4721
5103
|
}
|
|
4722
5104
|
this.sharedEventStore = createSharedEventStore(
|
|
4723
5105
|
path.join(sharedPath, "shared.duckdb")
|
|
@@ -4814,6 +5196,7 @@ var MemoryService = class {
|
|
|
4814
5196
|
async storeToolObservation(sessionId, payload) {
|
|
4815
5197
|
await this.initialize();
|
|
4816
5198
|
const content = JSON.stringify(payload);
|
|
5199
|
+
const turnId = payload.metadata?.turnId;
|
|
4817
5200
|
const result = await this.sqliteStore.append({
|
|
4818
5201
|
eventType: "tool_observation",
|
|
4819
5202
|
sessionId,
|
|
@@ -4821,7 +5204,8 @@ var MemoryService = class {
|
|
|
4821
5204
|
content,
|
|
4822
5205
|
metadata: {
|
|
4823
5206
|
toolName: payload.toolName,
|
|
4824
|
-
success: payload.success
|
|
5207
|
+
success: payload.success,
|
|
5208
|
+
...turnId ? { turnId } : {}
|
|
4825
5209
|
}
|
|
4826
5210
|
});
|
|
4827
5211
|
if (result.success && !result.isDuplicate) {
|
|
@@ -5104,6 +5488,31 @@ var MemoryService = class {
|
|
|
5104
5488
|
return [];
|
|
5105
5489
|
return this.consolidatedStore.getAll({ limit });
|
|
5106
5490
|
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Extract topic keywords from event content (markdown headings and key terms)
|
|
5493
|
+
*/
|
|
5494
|
+
extractTopicsFromContent(content) {
|
|
5495
|
+
const topics = /* @__PURE__ */ new Set();
|
|
5496
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm);
|
|
5497
|
+
if (headings) {
|
|
5498
|
+
for (const h of headings.slice(0, 5)) {
|
|
5499
|
+
const text = h.replace(/^#+\s+/, "").replace(/[*_`#]/g, "").trim();
|
|
5500
|
+
if (text.length > 2 && text.length < 50) {
|
|
5501
|
+
topics.add(text);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
|
|
5506
|
+
if (boldTerms) {
|
|
5507
|
+
for (const b of boldTerms.slice(0, 5)) {
|
|
5508
|
+
const text = b.replace(/\*\*/g, "").trim();
|
|
5509
|
+
if (text.length > 2 && text.length < 30) {
|
|
5510
|
+
topics.add(text);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
return Array.from(topics).slice(0, 5);
|
|
5515
|
+
}
|
|
5107
5516
|
/**
|
|
5108
5517
|
* Increment access count for memories that were used in prompts
|
|
5109
5518
|
*/
|
|
@@ -5127,8 +5536,7 @@ var MemoryService = class {
|
|
|
5127
5536
|
return events.map((event) => ({
|
|
5128
5537
|
memoryId: event.id,
|
|
5129
5538
|
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5130
|
-
topics:
|
|
5131
|
-
// Could extract topics from content if needed
|
|
5539
|
+
topics: this.extractTopicsFromContent(event.content),
|
|
5132
5540
|
accessCount: event.access_count || 0,
|
|
5133
5541
|
lastAccessed: event.last_accessed_at || null,
|
|
5134
5542
|
confidence: 1,
|
|
@@ -5149,6 +5557,34 @@ var MemoryService = class {
|
|
|
5149
5557
|
}
|
|
5150
5558
|
return [];
|
|
5151
5559
|
}
|
|
5560
|
+
/**
|
|
5561
|
+
* Record a memory retrieval for helpfulness tracking
|
|
5562
|
+
*/
|
|
5563
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
5564
|
+
await this.initialize();
|
|
5565
|
+
await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
|
|
5566
|
+
}
|
|
5567
|
+
/**
|
|
5568
|
+
* Evaluate helpfulness of retrievals in a session (called at session end)
|
|
5569
|
+
*/
|
|
5570
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
5571
|
+
await this.initialize();
|
|
5572
|
+
await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
|
|
5573
|
+
}
|
|
5574
|
+
/**
|
|
5575
|
+
* Get most helpful memories ranked by helpfulness score
|
|
5576
|
+
*/
|
|
5577
|
+
async getHelpfulMemories(limit = 10) {
|
|
5578
|
+
await this.initialize();
|
|
5579
|
+
return this.sqliteStore.getHelpfulMemories(limit);
|
|
5580
|
+
}
|
|
5581
|
+
/**
|
|
5582
|
+
* Get helpfulness statistics for dashboard
|
|
5583
|
+
*/
|
|
5584
|
+
async getHelpfulnessStats() {
|
|
5585
|
+
await this.initialize();
|
|
5586
|
+
return this.sqliteStore.getHelpfulnessStats();
|
|
5587
|
+
}
|
|
5152
5588
|
/**
|
|
5153
5589
|
* Mark a consolidated memory as accessed
|
|
5154
5590
|
*/
|
|
@@ -5212,6 +5648,44 @@ var MemoryService = class {
|
|
|
5212
5648
|
lastConsolidation
|
|
5213
5649
|
};
|
|
5214
5650
|
}
|
|
5651
|
+
// ============================================================
|
|
5652
|
+
// Turn Grouping Methods
|
|
5653
|
+
// ============================================================
|
|
5654
|
+
/**
|
|
5655
|
+
* Get events grouped by turn for a session
|
|
5656
|
+
*/
|
|
5657
|
+
async getSessionTurns(sessionId, options) {
|
|
5658
|
+
await this.initialize();
|
|
5659
|
+
return this.sqliteStore.getSessionTurns(sessionId, options);
|
|
5660
|
+
}
|
|
5661
|
+
/**
|
|
5662
|
+
* Get all events for a specific turn
|
|
5663
|
+
*/
|
|
5664
|
+
async getEventsByTurn(turnId) {
|
|
5665
|
+
await this.initialize();
|
|
5666
|
+
return this.sqliteStore.getEventsByTurn(turnId);
|
|
5667
|
+
}
|
|
5668
|
+
/**
|
|
5669
|
+
* Count total turns for a session
|
|
5670
|
+
*/
|
|
5671
|
+
async countSessionTurns(sessionId) {
|
|
5672
|
+
await this.initialize();
|
|
5673
|
+
return this.sqliteStore.countSessionTurns(sessionId);
|
|
5674
|
+
}
|
|
5675
|
+
/**
|
|
5676
|
+
* Backfill turn_ids from metadata for events stored before the migration
|
|
5677
|
+
*/
|
|
5678
|
+
async backfillTurnIds() {
|
|
5679
|
+
await this.initialize();
|
|
5680
|
+
return this.sqliteStore.backfillTurnIds();
|
|
5681
|
+
}
|
|
5682
|
+
/**
|
|
5683
|
+
* Delete all events for a session (for force reimport)
|
|
5684
|
+
*/
|
|
5685
|
+
async deleteSessionEvents(sessionId) {
|
|
5686
|
+
await this.initialize();
|
|
5687
|
+
return this.sqliteStore.deleteSessionEvents(sessionId);
|
|
5688
|
+
}
|
|
5215
5689
|
/**
|
|
5216
5690
|
* Format Endless Mode context for Claude
|
|
5217
5691
|
*/
|
|
@@ -5320,12 +5794,36 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
5320
5794
|
return serviceCache.get(hash);
|
|
5321
5795
|
}
|
|
5322
5796
|
|
|
5797
|
+
// src/server/api/utils.ts
|
|
5798
|
+
function getServiceFromQuery(c) {
|
|
5799
|
+
const project = c.req.query("project");
|
|
5800
|
+
if (project) {
|
|
5801
|
+
const isHash = /^[a-f0-9]{8}$/.test(project);
|
|
5802
|
+
let storagePath;
|
|
5803
|
+
if (isHash) {
|
|
5804
|
+
storagePath = path2.join(os2.homedir(), ".claude-code", "memory", "projects", project);
|
|
5805
|
+
} else {
|
|
5806
|
+
const crypto3 = __require("crypto");
|
|
5807
|
+
const normalized = project.replace(/\/+$/, "") || "/";
|
|
5808
|
+
const hash = crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
5809
|
+
storagePath = path2.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
|
|
5810
|
+
}
|
|
5811
|
+
return new MemoryService({
|
|
5812
|
+
storagePath,
|
|
5813
|
+
readOnly: true,
|
|
5814
|
+
analyticsEnabled: false,
|
|
5815
|
+
sharedStoreConfig: { enabled: false }
|
|
5816
|
+
});
|
|
5817
|
+
}
|
|
5818
|
+
return getReadOnlyMemoryService();
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5323
5821
|
// src/server/api/sessions.ts
|
|
5324
5822
|
var sessionsRouter = new Hono();
|
|
5325
5823
|
sessionsRouter.get("/", async (c) => {
|
|
5326
5824
|
const page = parseInt(c.req.query("page") || "1", 10);
|
|
5327
5825
|
const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
|
|
5328
|
-
const memoryService =
|
|
5826
|
+
const memoryService = getServiceFromQuery(c);
|
|
5329
5827
|
try {
|
|
5330
5828
|
await memoryService.initialize();
|
|
5331
5829
|
const recentEvents = await memoryService.getRecentEvents(1e3);
|
|
@@ -5369,7 +5867,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
5369
5867
|
});
|
|
5370
5868
|
sessionsRouter.get("/:id", async (c) => {
|
|
5371
5869
|
const { id } = c.req.param();
|
|
5372
|
-
const memoryService =
|
|
5870
|
+
const memoryService = getServiceFromQuery(c);
|
|
5373
5871
|
try {
|
|
5374
5872
|
await memoryService.initialize();
|
|
5375
5873
|
const events = await memoryService.getSessionHistory(id);
|
|
@@ -5410,18 +5908,36 @@ var eventsRouter = new Hono2();
|
|
|
5410
5908
|
eventsRouter.get("/", async (c) => {
|
|
5411
5909
|
const sessionId = c.req.query("sessionId");
|
|
5412
5910
|
const eventType = c.req.query("type");
|
|
5911
|
+
const level = c.req.query("level");
|
|
5912
|
+
const sort = c.req.query("sort") || "recent";
|
|
5413
5913
|
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
5414
5914
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5415
|
-
const memoryService =
|
|
5915
|
+
const memoryService = getServiceFromQuery(c);
|
|
5416
5916
|
try {
|
|
5417
5917
|
await memoryService.initialize();
|
|
5418
|
-
let events
|
|
5918
|
+
let events;
|
|
5919
|
+
if (level) {
|
|
5920
|
+
events = await memoryService.getEventsByLevel(level, { limit: limit + offset + 1e3, offset: 0 });
|
|
5921
|
+
} else {
|
|
5922
|
+
events = await memoryService.getRecentEvents(limit + offset + 1e3);
|
|
5923
|
+
}
|
|
5419
5924
|
if (sessionId) {
|
|
5420
5925
|
events = events.filter((e) => e.sessionId === sessionId);
|
|
5421
5926
|
}
|
|
5422
5927
|
if (eventType) {
|
|
5423
5928
|
events = events.filter((e) => e.eventType === eventType);
|
|
5424
5929
|
}
|
|
5930
|
+
if (sort === "accessed") {
|
|
5931
|
+
events.sort((a, b) => {
|
|
5932
|
+
const aTime = a.last_accessed_at || "";
|
|
5933
|
+
const bTime = b.last_accessed_at || "";
|
|
5934
|
+
return bTime.localeCompare(aTime);
|
|
5935
|
+
});
|
|
5936
|
+
} else if (sort === "most-accessed") {
|
|
5937
|
+
events.sort((a, b) => (b.access_count || 0) - (a.access_count || 0));
|
|
5938
|
+
} else if (sort === "oldest") {
|
|
5939
|
+
events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
5940
|
+
}
|
|
5425
5941
|
const total = events.length;
|
|
5426
5942
|
events = events.slice(offset, offset + limit);
|
|
5427
5943
|
return c.json({
|
|
@@ -5431,7 +5947,9 @@ eventsRouter.get("/", async (c) => {
|
|
|
5431
5947
|
timestamp: e.timestamp,
|
|
5432
5948
|
sessionId: e.sessionId,
|
|
5433
5949
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
5434
|
-
contentLength: e.content.length
|
|
5950
|
+
contentLength: e.content.length,
|
|
5951
|
+
accessCount: e.access_count || 0,
|
|
5952
|
+
lastAccessedAt: e.last_accessed_at || null
|
|
5435
5953
|
})),
|
|
5436
5954
|
total,
|
|
5437
5955
|
limit,
|
|
@@ -5446,7 +5964,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
5446
5964
|
});
|
|
5447
5965
|
eventsRouter.get("/:id", async (c) => {
|
|
5448
5966
|
const { id } = c.req.param();
|
|
5449
|
-
const memoryService =
|
|
5967
|
+
const memoryService = getServiceFromQuery(c);
|
|
5450
5968
|
try {
|
|
5451
5969
|
await memoryService.initialize();
|
|
5452
5970
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5486,7 +6004,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
5486
6004
|
import { Hono as Hono3 } from "hono";
|
|
5487
6005
|
var searchRouter = new Hono3();
|
|
5488
6006
|
searchRouter.post("/", async (c) => {
|
|
5489
|
-
const memoryService =
|
|
6007
|
+
const memoryService = getServiceFromQuery(c);
|
|
5490
6008
|
try {
|
|
5491
6009
|
const body = await c.req.json();
|
|
5492
6010
|
if (!body.query) {
|
|
@@ -5530,7 +6048,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5530
6048
|
return c.json({ error: 'Query parameter "q" is required' }, 400);
|
|
5531
6049
|
}
|
|
5532
6050
|
const topK = parseInt(c.req.query("topK") || "5", 10);
|
|
5533
|
-
const memoryService =
|
|
6051
|
+
const memoryService = getServiceFromQuery(c);
|
|
5534
6052
|
try {
|
|
5535
6053
|
await memoryService.initialize();
|
|
5536
6054
|
const result = await memoryService.retrieveMemories(query, { topK });
|
|
@@ -5558,7 +6076,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5558
6076
|
import { Hono as Hono4 } from "hono";
|
|
5559
6077
|
var statsRouter = new Hono4();
|
|
5560
6078
|
statsRouter.get("/shared", async (c) => {
|
|
5561
|
-
const memoryService =
|
|
6079
|
+
const memoryService = getServiceFromQuery(c);
|
|
5562
6080
|
try {
|
|
5563
6081
|
await memoryService.initialize();
|
|
5564
6082
|
const sharedStats = await memoryService.getSharedStoreStats();
|
|
@@ -5615,7 +6133,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5615
6133
|
if (!validLevels.includes(level)) {
|
|
5616
6134
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
5617
6135
|
}
|
|
5618
|
-
const memoryService =
|
|
6136
|
+
const memoryService = getServiceFromQuery(c);
|
|
5619
6137
|
try {
|
|
5620
6138
|
await memoryService.initialize();
|
|
5621
6139
|
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
@@ -5662,7 +6180,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5662
6180
|
}
|
|
5663
6181
|
});
|
|
5664
6182
|
statsRouter.get("/", async (c) => {
|
|
5665
|
-
const memoryService =
|
|
6183
|
+
const memoryService = getServiceFromQuery(c);
|
|
5666
6184
|
try {
|
|
5667
6185
|
await memoryService.initialize();
|
|
5668
6186
|
const stats = await memoryService.getStats();
|
|
@@ -5706,7 +6224,7 @@ statsRouter.get("/", async (c) => {
|
|
|
5706
6224
|
});
|
|
5707
6225
|
statsRouter.get("/most-accessed", async (c) => {
|
|
5708
6226
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
5709
|
-
const memoryService =
|
|
6227
|
+
const memoryService = getServiceFromQuery(c);
|
|
5710
6228
|
try {
|
|
5711
6229
|
await memoryService.initialize();
|
|
5712
6230
|
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
@@ -5737,7 +6255,7 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
5737
6255
|
});
|
|
5738
6256
|
statsRouter.get("/timeline", async (c) => {
|
|
5739
6257
|
const days = parseInt(c.req.query("days") || "7", 10);
|
|
5740
|
-
const memoryService =
|
|
6258
|
+
const memoryService = getServiceFromQuery(c);
|
|
5741
6259
|
try {
|
|
5742
6260
|
await memoryService.initialize();
|
|
5743
6261
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5767,8 +6285,39 @@ statsRouter.get("/timeline", async (c) => {
|
|
|
5767
6285
|
await memoryService.shutdown();
|
|
5768
6286
|
}
|
|
5769
6287
|
});
|
|
6288
|
+
statsRouter.get("/helpfulness", async (c) => {
|
|
6289
|
+
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
6290
|
+
const memoryService = getServiceFromQuery(c);
|
|
6291
|
+
try {
|
|
6292
|
+
await memoryService.initialize();
|
|
6293
|
+
const stats = await memoryService.getHelpfulnessStats();
|
|
6294
|
+
const topMemories = await memoryService.getHelpfulMemories(limit);
|
|
6295
|
+
return c.json({
|
|
6296
|
+
...stats,
|
|
6297
|
+
topMemories: topMemories.map((m) => ({
|
|
6298
|
+
eventId: m.eventId,
|
|
6299
|
+
summary: m.summary,
|
|
6300
|
+
helpfulnessScore: m.helpfulnessScore,
|
|
6301
|
+
accessCount: m.accessCount,
|
|
6302
|
+
evaluationCount: m.evaluationCount
|
|
6303
|
+
}))
|
|
6304
|
+
});
|
|
6305
|
+
} catch (error) {
|
|
6306
|
+
return c.json({
|
|
6307
|
+
avgScore: 0,
|
|
6308
|
+
totalEvaluated: 0,
|
|
6309
|
+
totalRetrievals: 0,
|
|
6310
|
+
helpful: 0,
|
|
6311
|
+
neutral: 0,
|
|
6312
|
+
unhelpful: 0,
|
|
6313
|
+
topMemories: []
|
|
6314
|
+
});
|
|
6315
|
+
} finally {
|
|
6316
|
+
await memoryService.shutdown();
|
|
6317
|
+
}
|
|
6318
|
+
});
|
|
5770
6319
|
statsRouter.post("/graduation/run", async (c) => {
|
|
5771
|
-
const memoryService =
|
|
6320
|
+
const memoryService = getServiceFromQuery(c);
|
|
5772
6321
|
try {
|
|
5773
6322
|
await memoryService.initialize();
|
|
5774
6323
|
const result = await memoryService.forceGraduation();
|
|
@@ -5829,7 +6378,7 @@ var citationsRouter = new Hono5();
|
|
|
5829
6378
|
citationsRouter.get("/:id", async (c) => {
|
|
5830
6379
|
const { id } = c.req.param();
|
|
5831
6380
|
const citationId = parseCitationId(id) || id;
|
|
5832
|
-
const memoryService =
|
|
6381
|
+
const memoryService = getServiceFromQuery(c);
|
|
5833
6382
|
try {
|
|
5834
6383
|
await memoryService.initialize();
|
|
5835
6384
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5863,7 +6412,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
5863
6412
|
citationsRouter.get("/:id/related", async (c) => {
|
|
5864
6413
|
const { id } = c.req.param();
|
|
5865
6414
|
const citationId = parseCitationId(id) || id;
|
|
5866
|
-
const memoryService =
|
|
6415
|
+
const memoryService = getServiceFromQuery(c);
|
|
5867
6416
|
try {
|
|
5868
6417
|
await memoryService.initialize();
|
|
5869
6418
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5899,8 +6448,358 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
5899
6448
|
}
|
|
5900
6449
|
});
|
|
5901
6450
|
|
|
6451
|
+
// src/server/api/turns.ts
|
|
6452
|
+
import { Hono as Hono6 } from "hono";
|
|
6453
|
+
var turnsRouter = new Hono6();
|
|
6454
|
+
turnsRouter.get("/", async (c) => {
|
|
6455
|
+
const sessionId = c.req.query("sessionId");
|
|
6456
|
+
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
6457
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
6458
|
+
if (!sessionId) {
|
|
6459
|
+
return c.json({ error: "sessionId is required" }, 400);
|
|
6460
|
+
}
|
|
6461
|
+
const memoryService = getServiceFromQuery(c);
|
|
6462
|
+
try {
|
|
6463
|
+
await memoryService.initialize();
|
|
6464
|
+
const turns = await memoryService.getSessionTurns(sessionId, { limit, offset });
|
|
6465
|
+
const totalTurns = await memoryService.countSessionTurns(sessionId);
|
|
6466
|
+
return c.json({
|
|
6467
|
+
turns: turns.map((t) => ({
|
|
6468
|
+
turnId: t.turnId,
|
|
6469
|
+
startedAt: t.startedAt.toISOString(),
|
|
6470
|
+
promptPreview: t.promptPreview,
|
|
6471
|
+
eventCount: t.eventCount,
|
|
6472
|
+
toolCount: t.toolCount,
|
|
6473
|
+
hasResponse: t.hasResponse,
|
|
6474
|
+
events: t.events.map((e) => ({
|
|
6475
|
+
id: e.id,
|
|
6476
|
+
eventType: e.eventType,
|
|
6477
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
|
|
6478
|
+
preview: e.content.slice(0, 300) + (e.content.length > 300 ? "..." : ""),
|
|
6479
|
+
contentLength: e.content.length
|
|
6480
|
+
}))
|
|
6481
|
+
})),
|
|
6482
|
+
total: totalTurns,
|
|
6483
|
+
limit,
|
|
6484
|
+
offset,
|
|
6485
|
+
hasMore: offset + limit < totalTurns
|
|
6486
|
+
});
|
|
6487
|
+
} catch (error) {
|
|
6488
|
+
return c.json({ error: error.message }, 500);
|
|
6489
|
+
} finally {
|
|
6490
|
+
await memoryService.shutdown();
|
|
6491
|
+
}
|
|
6492
|
+
});
|
|
6493
|
+
turnsRouter.get("/:turnId", async (c) => {
|
|
6494
|
+
const { turnId } = c.req.param();
|
|
6495
|
+
const memoryService = getServiceFromQuery(c);
|
|
6496
|
+
try {
|
|
6497
|
+
await memoryService.initialize();
|
|
6498
|
+
const events = await memoryService.getEventsByTurn(turnId);
|
|
6499
|
+
if (events.length === 0) {
|
|
6500
|
+
return c.json({ error: "Turn not found" }, 404);
|
|
6501
|
+
}
|
|
6502
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
6503
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
6504
|
+
const responseEvents = events.filter((e) => e.eventType === "agent_response");
|
|
6505
|
+
return c.json({
|
|
6506
|
+
turnId,
|
|
6507
|
+
sessionId: events[0].sessionId,
|
|
6508
|
+
startedAt: events[0].timestamp instanceof Date ? events[0].timestamp.toISOString() : events[0].timestamp,
|
|
6509
|
+
prompt: promptEvent ? {
|
|
6510
|
+
id: promptEvent.id,
|
|
6511
|
+
content: promptEvent.content,
|
|
6512
|
+
timestamp: promptEvent.timestamp instanceof Date ? promptEvent.timestamp.toISOString() : promptEvent.timestamp
|
|
6513
|
+
} : null,
|
|
6514
|
+
tools: toolEvents.map((e) => {
|
|
6515
|
+
let toolName = "";
|
|
6516
|
+
let success = true;
|
|
6517
|
+
try {
|
|
6518
|
+
const parsed = JSON.parse(e.content);
|
|
6519
|
+
toolName = parsed.toolName || "";
|
|
6520
|
+
success = parsed.success !== false;
|
|
6521
|
+
} catch {
|
|
6522
|
+
}
|
|
6523
|
+
return {
|
|
6524
|
+
id: e.id,
|
|
6525
|
+
toolName,
|
|
6526
|
+
success,
|
|
6527
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
|
|
6528
|
+
preview: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : "")
|
|
6529
|
+
};
|
|
6530
|
+
}),
|
|
6531
|
+
responses: responseEvents.map((e) => ({
|
|
6532
|
+
id: e.id,
|
|
6533
|
+
content: e.content,
|
|
6534
|
+
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp
|
|
6535
|
+
})),
|
|
6536
|
+
totalEvents: events.length
|
|
6537
|
+
});
|
|
6538
|
+
} catch (error) {
|
|
6539
|
+
return c.json({ error: error.message }, 500);
|
|
6540
|
+
} finally {
|
|
6541
|
+
await memoryService.shutdown();
|
|
6542
|
+
}
|
|
6543
|
+
});
|
|
6544
|
+
turnsRouter.post("/backfill", async (c) => {
|
|
6545
|
+
const memoryService = getServiceFromQuery(c);
|
|
6546
|
+
try {
|
|
6547
|
+
await memoryService.initialize();
|
|
6548
|
+
const updated = await memoryService.backfillTurnIds();
|
|
6549
|
+
return c.json({
|
|
6550
|
+
success: true,
|
|
6551
|
+
updated,
|
|
6552
|
+
message: `Backfilled turn_id for ${updated} events`
|
|
6553
|
+
});
|
|
6554
|
+
} catch (error) {
|
|
6555
|
+
return c.json({
|
|
6556
|
+
success: false,
|
|
6557
|
+
error: error.message
|
|
6558
|
+
}, 500);
|
|
6559
|
+
} finally {
|
|
6560
|
+
await memoryService.shutdown();
|
|
6561
|
+
}
|
|
6562
|
+
});
|
|
6563
|
+
|
|
6564
|
+
// src/server/api/projects.ts
|
|
6565
|
+
import { Hono as Hono7 } from "hono";
|
|
6566
|
+
import * as fs3 from "fs";
|
|
6567
|
+
import * as path3 from "path";
|
|
6568
|
+
import * as os3 from "os";
|
|
6569
|
+
var projectsRouter = new Hono7();
|
|
6570
|
+
projectsRouter.get("/", async (c) => {
|
|
6571
|
+
try {
|
|
6572
|
+
const projectsDir = path3.join(os3.homedir(), ".claude-code", "memory", "projects");
|
|
6573
|
+
if (!fs3.existsSync(projectsDir)) {
|
|
6574
|
+
return c.json({ projects: [] });
|
|
6575
|
+
}
|
|
6576
|
+
const projectHashes = fs3.readdirSync(projectsDir).filter((name) => {
|
|
6577
|
+
const fullPath = path3.join(projectsDir, name);
|
|
6578
|
+
return fs3.statSync(fullPath).isDirectory();
|
|
6579
|
+
});
|
|
6580
|
+
const registry = loadSessionRegistry();
|
|
6581
|
+
const hashToPath = /* @__PURE__ */ new Map();
|
|
6582
|
+
for (const entry of Object.values(registry.sessions)) {
|
|
6583
|
+
if (!hashToPath.has(entry.projectHash)) {
|
|
6584
|
+
hashToPath.set(entry.projectHash, entry.projectPath);
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
const projects = projectHashes.map((hash) => {
|
|
6588
|
+
const dirPath = path3.join(projectsDir, hash);
|
|
6589
|
+
const dbPath = path3.join(dirPath, "events.sqlite");
|
|
6590
|
+
let dbSize = 0;
|
|
6591
|
+
if (fs3.existsSync(dbPath)) {
|
|
6592
|
+
dbSize = fs3.statSync(dbPath).size;
|
|
6593
|
+
}
|
|
6594
|
+
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
6595
|
+
return {
|
|
6596
|
+
hash,
|
|
6597
|
+
projectPath,
|
|
6598
|
+
projectName: path3.basename(projectPath),
|
|
6599
|
+
dbSize,
|
|
6600
|
+
dbSizeHuman: formatBytes(dbSize)
|
|
6601
|
+
};
|
|
6602
|
+
});
|
|
6603
|
+
projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
|
|
6604
|
+
return c.json({ projects });
|
|
6605
|
+
} catch (error) {
|
|
6606
|
+
return c.json({ projects: [], error: error.message }, 500);
|
|
6607
|
+
}
|
|
6608
|
+
});
|
|
6609
|
+
function formatBytes(bytes) {
|
|
6610
|
+
if (bytes === 0)
|
|
6611
|
+
return "0 B";
|
|
6612
|
+
const k = 1024;
|
|
6613
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
6614
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
6615
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
6616
|
+
}
|
|
6617
|
+
|
|
6618
|
+
// src/server/api/chat.ts
|
|
6619
|
+
import { Hono as Hono8 } from "hono";
|
|
6620
|
+
import { streamSSE } from "hono/streaming";
|
|
6621
|
+
import { spawn } from "child_process";
|
|
6622
|
+
var chatRouter = new Hono8();
|
|
6623
|
+
var CLAUDE_TIMEOUT_MS = 12e4;
|
|
6624
|
+
chatRouter.post("/", async (c) => {
|
|
6625
|
+
let body;
|
|
6626
|
+
try {
|
|
6627
|
+
body = await c.req.json();
|
|
6628
|
+
} catch {
|
|
6629
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
6630
|
+
}
|
|
6631
|
+
if (!body.message?.trim()) {
|
|
6632
|
+
return c.json({ error: "Message is required" }, 400);
|
|
6633
|
+
}
|
|
6634
|
+
const memoryService = getServiceFromQuery(c);
|
|
6635
|
+
try {
|
|
6636
|
+
await memoryService.initialize();
|
|
6637
|
+
let memoryContext = "";
|
|
6638
|
+
let statsContext = "";
|
|
6639
|
+
try {
|
|
6640
|
+
const result = await memoryService.retrieveMemories(body.message, {
|
|
6641
|
+
topK: 8,
|
|
6642
|
+
minScore: 0.5
|
|
6643
|
+
});
|
|
6644
|
+
if (result.memories.length > 0) {
|
|
6645
|
+
const parts = ["## Relevant Memories\n"];
|
|
6646
|
+
for (const m of result.memories) {
|
|
6647
|
+
const date = new Date(m.event.timestamp).toISOString().split("T")[0];
|
|
6648
|
+
const content = m.event.content.slice(0, 500);
|
|
6649
|
+
parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
6650
|
+
parts.push(content);
|
|
6651
|
+
if (m.sessionContext) {
|
|
6652
|
+
parts.push(`_Context: ${m.sessionContext}_`);
|
|
6653
|
+
}
|
|
6654
|
+
parts.push("");
|
|
6655
|
+
}
|
|
6656
|
+
memoryContext = parts.join("\n");
|
|
6657
|
+
}
|
|
6658
|
+
} catch {
|
|
6659
|
+
}
|
|
6660
|
+
try {
|
|
6661
|
+
const stats = await memoryService.getStats();
|
|
6662
|
+
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
6663
|
+
statsContext = [
|
|
6664
|
+
"## Memory Stats",
|
|
6665
|
+
`- Total events: ${stats.totalEvents}`,
|
|
6666
|
+
`- Vector nodes: ${stats.vectorCount}`,
|
|
6667
|
+
`- By level: ${levels}`
|
|
6668
|
+
].join("\n");
|
|
6669
|
+
} catch {
|
|
6670
|
+
}
|
|
6671
|
+
const fullPrompt = buildPrompt(
|
|
6672
|
+
statsContext,
|
|
6673
|
+
memoryContext,
|
|
6674
|
+
body.history || [],
|
|
6675
|
+
body.message
|
|
6676
|
+
);
|
|
6677
|
+
return streamSSE(c, async (stream) => {
|
|
6678
|
+
try {
|
|
6679
|
+
await streamClaudeResponse(fullPrompt, stream);
|
|
6680
|
+
} catch (err) {
|
|
6681
|
+
await stream.writeSSE({
|
|
6682
|
+
event: "error",
|
|
6683
|
+
data: JSON.stringify({ error: err.message })
|
|
6684
|
+
});
|
|
6685
|
+
}
|
|
6686
|
+
});
|
|
6687
|
+
} catch (error) {
|
|
6688
|
+
return c.json({ error: error.message }, 500);
|
|
6689
|
+
} finally {
|
|
6690
|
+
await memoryService.shutdown();
|
|
6691
|
+
}
|
|
6692
|
+
});
|
|
6693
|
+
function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
6694
|
+
const parts = [];
|
|
6695
|
+
parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
|
|
6696
|
+
parts.push("The memory system tracks coding sessions, tool usage, prompts, and responses.");
|
|
6697
|
+
parts.push("Answer concisely based on the memory context below. If you don't have enough data, say so.");
|
|
6698
|
+
parts.push("Use markdown formatting in your responses.\n");
|
|
6699
|
+
if (statsContext) {
|
|
6700
|
+
parts.push(statsContext);
|
|
6701
|
+
parts.push("");
|
|
6702
|
+
}
|
|
6703
|
+
if (memoryContext) {
|
|
6704
|
+
parts.push(memoryContext);
|
|
6705
|
+
} else {
|
|
6706
|
+
parts.push("No directly relevant memories found for this query.");
|
|
6707
|
+
parts.push("Answer based on general knowledge or suggest the user rephrase.\n");
|
|
6708
|
+
}
|
|
6709
|
+
parts.push("---\n");
|
|
6710
|
+
const recentHistory = history.slice(-10);
|
|
6711
|
+
if (recentHistory.length > 0) {
|
|
6712
|
+
parts.push("## Conversation History\n");
|
|
6713
|
+
for (const msg of recentHistory) {
|
|
6714
|
+
const prefix = msg.role === "user" ? "User" : "Assistant";
|
|
6715
|
+
parts.push(`**${prefix}:** ${msg.content}
|
|
6716
|
+
`);
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
parts.push(`**User:** ${currentMessage}`);
|
|
6720
|
+
return parts.join("\n");
|
|
6721
|
+
}
|
|
6722
|
+
function streamClaudeResponse(prompt, stream) {
|
|
6723
|
+
return new Promise((resolve2, reject) => {
|
|
6724
|
+
const proc = spawn("claude", [
|
|
6725
|
+
"-p",
|
|
6726
|
+
"--output-format",
|
|
6727
|
+
"stream-json",
|
|
6728
|
+
"--verbose"
|
|
6729
|
+
], {
|
|
6730
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6731
|
+
env: { ...process.env }
|
|
6732
|
+
});
|
|
6733
|
+
const timeout = setTimeout(() => {
|
|
6734
|
+
proc.kill("SIGTERM");
|
|
6735
|
+
reject(new Error("Chat response timed out after 2 minutes"));
|
|
6736
|
+
}, CLAUDE_TIMEOUT_MS);
|
|
6737
|
+
proc.stdin.write(prompt);
|
|
6738
|
+
proc.stdin.end();
|
|
6739
|
+
let buffer = "";
|
|
6740
|
+
let lastSentText = "";
|
|
6741
|
+
proc.stdout.on("data", async (chunk) => {
|
|
6742
|
+
buffer += chunk.toString();
|
|
6743
|
+
const lines = buffer.split("\n");
|
|
6744
|
+
buffer = lines.pop() || "";
|
|
6745
|
+
for (const line of lines) {
|
|
6746
|
+
if (!line.trim())
|
|
6747
|
+
continue;
|
|
6748
|
+
try {
|
|
6749
|
+
const parsed = JSON.parse(line);
|
|
6750
|
+
if (parsed.type === "assistant" && parsed.message?.content) {
|
|
6751
|
+
const textBlocks = parsed.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
6752
|
+
if (textBlocks.length > lastSentText.length) {
|
|
6753
|
+
const delta = textBlocks.slice(lastSentText.length);
|
|
6754
|
+
lastSentText = textBlocks;
|
|
6755
|
+
await stream.writeSSE({
|
|
6756
|
+
event: "message",
|
|
6757
|
+
data: JSON.stringify({ content: delta })
|
|
6758
|
+
});
|
|
6759
|
+
}
|
|
6760
|
+
}
|
|
6761
|
+
if (parsed.type === "result") {
|
|
6762
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
6763
|
+
}
|
|
6764
|
+
} catch {
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
});
|
|
6768
|
+
proc.stderr.on("data", (chunk) => {
|
|
6769
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
6770
|
+
console.error("[chat] claude stderr:", chunk.toString());
|
|
6771
|
+
}
|
|
6772
|
+
});
|
|
6773
|
+
proc.on("error", (err) => {
|
|
6774
|
+
clearTimeout(timeout);
|
|
6775
|
+
if (err.code === "ENOENT") {
|
|
6776
|
+
reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
|
|
6777
|
+
} else {
|
|
6778
|
+
reject(err);
|
|
6779
|
+
}
|
|
6780
|
+
});
|
|
6781
|
+
proc.on("close", async (code) => {
|
|
6782
|
+
clearTimeout(timeout);
|
|
6783
|
+
if (buffer.trim()) {
|
|
6784
|
+
try {
|
|
6785
|
+
const parsed = JSON.parse(buffer);
|
|
6786
|
+
if (parsed.type === "result") {
|
|
6787
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
6788
|
+
}
|
|
6789
|
+
} catch {
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6792
|
+
if (code !== 0 && code !== null) {
|
|
6793
|
+
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
6794
|
+
} else {
|
|
6795
|
+
resolve2();
|
|
6796
|
+
}
|
|
6797
|
+
});
|
|
6798
|
+
});
|
|
6799
|
+
}
|
|
6800
|
+
|
|
5902
6801
|
// src/server/api/index.ts
|
|
5903
|
-
var apiRouter = new
|
|
6802
|
+
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);
|
|
5904
6803
|
export {
|
|
5905
6804
|
apiRouter
|
|
5906
6805
|
};
|