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/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,9 @@ 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);
|
|
1044
1077
|
|
|
1045
1078
|
-- FTS5 Full-Text Search for fast keyword search
|
|
1046
1079
|
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
@@ -1084,6 +1117,15 @@ var SQLiteEventStore = class {
|
|
|
1084
1117
|
console.error("Error adding last_accessed_at column:", err);
|
|
1085
1118
|
}
|
|
1086
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
|
+
}
|
|
1087
1129
|
try {
|
|
1088
1130
|
sqliteExec(this.db, `
|
|
1089
1131
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -1096,6 +1138,12 @@ var SQLiteEventStore = class {
|
|
|
1096
1138
|
`);
|
|
1097
1139
|
} catch (err) {
|
|
1098
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
|
+
}
|
|
1099
1147
|
this.initialized = true;
|
|
1100
1148
|
}
|
|
1101
1149
|
/**
|
|
@@ -1120,9 +1168,11 @@ var SQLiteEventStore = class {
|
|
|
1120
1168
|
const id = randomUUID2();
|
|
1121
1169
|
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1122
1170
|
try {
|
|
1171
|
+
const metadata = input.metadata || {};
|
|
1172
|
+
const turnId = metadata.turnId || null;
|
|
1123
1173
|
const insertEvent = this.db.prepare(`
|
|
1124
|
-
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1125
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1174
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1175
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1126
1176
|
`);
|
|
1127
1177
|
const insertDedup = this.db.prepare(`
|
|
1128
1178
|
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
@@ -1139,7 +1189,8 @@ var SQLiteEventStore = class {
|
|
|
1139
1189
|
input.content,
|
|
1140
1190
|
canonicalKey,
|
|
1141
1191
|
dedupeKey,
|
|
1142
|
-
JSON.stringify(
|
|
1192
|
+
JSON.stringify(metadata),
|
|
1193
|
+
turnId
|
|
1143
1194
|
);
|
|
1144
1195
|
insertDedup.run(dedupeKey, id);
|
|
1145
1196
|
insertLevel.run(id);
|
|
@@ -1489,11 +1540,11 @@ var SQLiteEventStore = class {
|
|
|
1489
1540
|
);
|
|
1490
1541
|
}
|
|
1491
1542
|
/**
|
|
1492
|
-
* Get most accessed memories
|
|
1543
|
+
* Get most accessed memories (falls back to recent events if none accessed)
|
|
1493
1544
|
*/
|
|
1494
1545
|
async getMostAccessed(limit = 10) {
|
|
1495
1546
|
await this.initialize();
|
|
1496
|
-
|
|
1547
|
+
let rows = sqliteAll(
|
|
1497
1548
|
this.db,
|
|
1498
1549
|
`SELECT * FROM events
|
|
1499
1550
|
WHERE access_count > 0
|
|
@@ -1501,8 +1552,166 @@ var SQLiteEventStore = class {
|
|
|
1501
1552
|
LIMIT ?`,
|
|
1502
1553
|
[limit]
|
|
1503
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
|
+
}
|
|
1504
1564
|
return rows.map((row) => this.rowToEvent(row));
|
|
1505
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
|
+
}
|
|
1506
1715
|
/**
|
|
1507
1716
|
* Fast keyword search using FTS5
|
|
1508
1717
|
* Returns events matching the search query, ranked by relevance
|
|
@@ -1571,6 +1780,143 @@ var SQLiteEventStore = class {
|
|
|
1571
1780
|
async close() {
|
|
1572
1781
|
sqliteClose(this.db);
|
|
1573
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
|
+
}
|
|
1574
1920
|
/**
|
|
1575
1921
|
* Convert database row to MemoryEvent
|
|
1576
1922
|
*/
|
|
@@ -1591,6 +1937,9 @@ var SQLiteEventStore = class {
|
|
|
1591
1937
|
if (row.last_accessed_at !== void 0) {
|
|
1592
1938
|
event.last_accessed_at = row.last_accessed_at;
|
|
1593
1939
|
}
|
|
1940
|
+
if (row.turn_id !== void 0 && row.turn_id !== null) {
|
|
1941
|
+
event.turn_id = row.turn_id;
|
|
1942
|
+
}
|
|
1594
1943
|
return event;
|
|
1595
1944
|
}
|
|
1596
1945
|
};
|
|
@@ -1802,7 +2151,16 @@ var VectorStore = class {
|
|
|
1802
2151
|
metadata: JSON.stringify(record.metadata || {})
|
|
1803
2152
|
};
|
|
1804
2153
|
if (!this.table) {
|
|
1805
|
-
|
|
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
|
+
}
|
|
1806
2164
|
} else {
|
|
1807
2165
|
await this.table.add([data]);
|
|
1808
2166
|
}
|
|
@@ -1828,7 +2186,16 @@ var VectorStore = class {
|
|
|
1828
2186
|
metadata: JSON.stringify(record.metadata || {})
|
|
1829
2187
|
}));
|
|
1830
2188
|
if (!this.table) {
|
|
1831
|
-
|
|
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
|
+
}
|
|
1832
2199
|
} else {
|
|
1833
2200
|
await this.table.add(data);
|
|
1834
2201
|
}
|
|
@@ -4574,7 +4941,7 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
4574
4941
|
function normalizePath(projectPath) {
|
|
4575
4942
|
const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
4576
4943
|
try {
|
|
4577
|
-
return
|
|
4944
|
+
return fs2.realpathSync(expanded);
|
|
4578
4945
|
} catch {
|
|
4579
4946
|
return path.resolve(expanded);
|
|
4580
4947
|
}
|
|
@@ -4589,6 +4956,42 @@ function getProjectStoragePath(projectPath) {
|
|
|
4589
4956
|
}
|
|
4590
4957
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
4591
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
|
+
}
|
|
4592
4995
|
var MemoryService = class {
|
|
4593
4996
|
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4594
4997
|
sqliteStore;
|
|
@@ -4622,8 +5025,8 @@ var MemoryService = class {
|
|
|
4622
5025
|
const storagePath = this.expandPath(config.storagePath);
|
|
4623
5026
|
this.readOnly = config.readOnly ?? false;
|
|
4624
5027
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
4625
|
-
if (!this.readOnly && !
|
|
4626
|
-
|
|
5028
|
+
if (!this.readOnly && !fs2.existsSync(storagePath)) {
|
|
5029
|
+
fs2.mkdirSync(storagePath, { recursive: true });
|
|
4627
5030
|
}
|
|
4628
5031
|
this.projectHash = config.projectHash || null;
|
|
4629
5032
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
@@ -4718,8 +5121,8 @@ var MemoryService = class {
|
|
|
4718
5121
|
*/
|
|
4719
5122
|
async initializeSharedStore() {
|
|
4720
5123
|
const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
|
|
4721
|
-
if (!
|
|
4722
|
-
|
|
5124
|
+
if (!fs2.existsSync(sharedPath)) {
|
|
5125
|
+
fs2.mkdirSync(sharedPath, { recursive: true });
|
|
4723
5126
|
}
|
|
4724
5127
|
this.sharedEventStore = createSharedEventStore(
|
|
4725
5128
|
path.join(sharedPath, "shared.duckdb")
|
|
@@ -4816,6 +5219,7 @@ var MemoryService = class {
|
|
|
4816
5219
|
async storeToolObservation(sessionId, payload) {
|
|
4817
5220
|
await this.initialize();
|
|
4818
5221
|
const content = JSON.stringify(payload);
|
|
5222
|
+
const turnId = payload.metadata?.turnId;
|
|
4819
5223
|
const result = await this.sqliteStore.append({
|
|
4820
5224
|
eventType: "tool_observation",
|
|
4821
5225
|
sessionId,
|
|
@@ -4823,7 +5227,8 @@ var MemoryService = class {
|
|
|
4823
5227
|
content,
|
|
4824
5228
|
metadata: {
|
|
4825
5229
|
toolName: payload.toolName,
|
|
4826
|
-
success: payload.success
|
|
5230
|
+
success: payload.success,
|
|
5231
|
+
...turnId ? { turnId } : {}
|
|
4827
5232
|
}
|
|
4828
5233
|
});
|
|
4829
5234
|
if (result.success && !result.isDuplicate) {
|
|
@@ -5106,6 +5511,31 @@ var MemoryService = class {
|
|
|
5106
5511
|
return [];
|
|
5107
5512
|
return this.consolidatedStore.getAll({ limit });
|
|
5108
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
|
+
}
|
|
5109
5539
|
/**
|
|
5110
5540
|
* Increment access count for memories that were used in prompts
|
|
5111
5541
|
*/
|
|
@@ -5129,8 +5559,7 @@ var MemoryService = class {
|
|
|
5129
5559
|
return events.map((event) => ({
|
|
5130
5560
|
memoryId: event.id,
|
|
5131
5561
|
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5132
|
-
topics:
|
|
5133
|
-
// Could extract topics from content if needed
|
|
5562
|
+
topics: this.extractTopicsFromContent(event.content),
|
|
5134
5563
|
accessCount: event.access_count || 0,
|
|
5135
5564
|
lastAccessed: event.last_accessed_at || null,
|
|
5136
5565
|
confidence: 1,
|
|
@@ -5151,6 +5580,34 @@ var MemoryService = class {
|
|
|
5151
5580
|
}
|
|
5152
5581
|
return [];
|
|
5153
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
|
+
}
|
|
5154
5611
|
/**
|
|
5155
5612
|
* Mark a consolidated memory as accessed
|
|
5156
5613
|
*/
|
|
@@ -5214,6 +5671,44 @@ var MemoryService = class {
|
|
|
5214
5671
|
lastConsolidation
|
|
5215
5672
|
};
|
|
5216
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
|
+
}
|
|
5217
5712
|
/**
|
|
5218
5713
|
* Format Endless Mode context for Claude
|
|
5219
5714
|
*/
|
|
@@ -5336,10 +5831,46 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
5336
5831
|
}
|
|
5337
5832
|
|
|
5338
5833
|
// src/services/session-history-importer.ts
|
|
5339
|
-
import * as
|
|
5834
|
+
import * as fs3 from "fs";
|
|
5340
5835
|
import * as path2 from "path";
|
|
5341
5836
|
import * as os2 from "os";
|
|
5342
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
|
+
}
|
|
5343
5874
|
var SessionHistoryImporter = class {
|
|
5344
5875
|
memoryService;
|
|
5345
5876
|
claudeDir;
|
|
@@ -5359,6 +5890,8 @@ var SessionHistoryImporter = class {
|
|
|
5359
5890
|
skippedDuplicates: 0,
|
|
5360
5891
|
errors: []
|
|
5361
5892
|
};
|
|
5893
|
+
const onProgress = options.onProgress;
|
|
5894
|
+
onProgress?.({ phase: "scan", message: "Scanning for session files..." });
|
|
5362
5895
|
const projectDir = await this.findProjectDir(projectPath);
|
|
5363
5896
|
if (!projectDir) {
|
|
5364
5897
|
result.errors.push(`Project directory not found for: ${projectPath}`);
|
|
@@ -5366,16 +5899,29 @@ var SessionHistoryImporter = class {
|
|
|
5366
5899
|
}
|
|
5367
5900
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
5368
5901
|
result.totalSessions = sessionFiles.length;
|
|
5902
|
+
onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} sessions in ${path2.basename(projectDir)}` });
|
|
5369
5903
|
if (options.verbose) {
|
|
5370
5904
|
console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
|
|
5371
5905
|
}
|
|
5372
|
-
for (
|
|
5906
|
+
for (let i = 0; i < sessionFiles.length; i++) {
|
|
5907
|
+
const sessionFile = sessionFiles[i];
|
|
5373
5908
|
try {
|
|
5374
|
-
|
|
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
|
+
});
|
|
5375
5914
|
result.totalMessages += sessionResult.totalMessages;
|
|
5376
5915
|
result.importedPrompts += sessionResult.importedPrompts;
|
|
5377
5916
|
result.importedResponses += sessionResult.importedResponses;
|
|
5378
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
|
+
});
|
|
5379
5925
|
} catch (error) {
|
|
5380
5926
|
result.errors.push(`Failed to import ${sessionFile}: ${error}`);
|
|
5381
5927
|
}
|
|
@@ -5394,60 +5940,105 @@ var SessionHistoryImporter = class {
|
|
|
5394
5940
|
skippedDuplicates: 0,
|
|
5395
5941
|
errors: []
|
|
5396
5942
|
};
|
|
5397
|
-
if (!
|
|
5943
|
+
if (!fs3.existsSync(filePath)) {
|
|
5398
5944
|
result.errors.push(`File not found: ${filePath}`);
|
|
5399
5945
|
return result;
|
|
5400
5946
|
}
|
|
5401
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
|
+
}
|
|
5402
5954
|
await this.memoryService.startSession(sessionId, options.projectPath);
|
|
5403
|
-
const fileStream =
|
|
5955
|
+
const fileStream = fs3.createReadStream(filePath);
|
|
5404
5956
|
const rl = readline.createInterface({
|
|
5405
5957
|
input: fileStream,
|
|
5406
5958
|
crlfDelay: Infinity
|
|
5407
5959
|
});
|
|
5408
5960
|
let lineCount = 0;
|
|
5409
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
|
+
};
|
|
5410
5991
|
for await (const line of rl) {
|
|
5411
5992
|
if (lineCount >= limit)
|
|
5412
5993
|
break;
|
|
5413
5994
|
try {
|
|
5414
5995
|
const entry = JSON.parse(line);
|
|
5415
5996
|
result.totalMessages++;
|
|
5416
|
-
|
|
5997
|
+
const msgClass = classifyEntry(entry);
|
|
5998
|
+
if (msgClass === "user_prompt") {
|
|
5999
|
+
await flushTextBuffer();
|
|
5417
6000
|
const content = this.extractContent(entry);
|
|
5418
6001
|
if (!content)
|
|
5419
6002
|
continue;
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
}
|
|
5431
|
-
} else if (entry.type === "assistant") {
|
|
5432
|
-
const truncatedContent = content.length > 5e3 ? content.slice(0, 5e3) + "...[truncated]" : content;
|
|
5433
|
-
const appendResult = await this.memoryService.storeAgentResponse(
|
|
5434
|
-
sessionId,
|
|
5435
|
-
truncatedContent,
|
|
5436
|
-
{ importedFrom: filePath, originalTimestamp: entry.timestamp }
|
|
5437
|
-
);
|
|
5438
|
-
if (appendResult.isDuplicate) {
|
|
5439
|
-
result.skippedDuplicates++;
|
|
5440
|
-
} else {
|
|
5441
|
-
result.importedResponses++;
|
|
5442
|
-
}
|
|
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++;
|
|
5443
6013
|
}
|
|
5444
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
|
+
});
|
|
5445
6032
|
}
|
|
5446
6033
|
} catch (parseError) {
|
|
5447
6034
|
result.errors.push(`Parse error on line: ${parseError}`);
|
|
5448
6035
|
}
|
|
5449
6036
|
}
|
|
6037
|
+
await flushTextBuffer();
|
|
5450
6038
|
await this.memoryService.endSession(sessionId);
|
|
6039
|
+
if (options.projectPath) {
|
|
6040
|
+
registerSession(sessionId, options.projectPath);
|
|
6041
|
+
}
|
|
5451
6042
|
if (options.verbose) {
|
|
5452
6043
|
console.log(`Imported ${result.importedPrompts} prompts, ${result.importedResponses} responses from ${filePath}`);
|
|
5453
6044
|
}
|
|
@@ -5465,29 +6056,46 @@ var SessionHistoryImporter = class {
|
|
|
5465
6056
|
skippedDuplicates: 0,
|
|
5466
6057
|
errors: []
|
|
5467
6058
|
};
|
|
6059
|
+
const onProgress = options.onProgress;
|
|
5468
6060
|
const projectsDir = path2.join(this.claudeDir, "projects");
|
|
5469
|
-
if (!
|
|
6061
|
+
if (!fs3.existsSync(projectsDir)) {
|
|
5470
6062
|
result.errors.push(`Projects directory not found: ${projectsDir}`);
|
|
5471
6063
|
return result;
|
|
5472
6064
|
}
|
|
5473
|
-
|
|
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` });
|
|
5474
6073
|
if (options.verbose) {
|
|
5475
|
-
console.log(`Found ${projectDirs.length} project directories`);
|
|
6074
|
+
console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
|
|
5476
6075
|
}
|
|
5477
|
-
for (
|
|
6076
|
+
for (let i = 0; i < allSessionFiles.length; i++) {
|
|
6077
|
+
const sessionFile = allSessionFiles[i];
|
|
5478
6078
|
try {
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
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
|
+
});
|
|
5489
6097
|
} catch (error) {
|
|
5490
|
-
result.errors.push(`Failed to process ${
|
|
6098
|
+
result.errors.push(`Failed to process ${sessionFile}: ${error}`);
|
|
5491
6099
|
}
|
|
5492
6100
|
}
|
|
5493
6101
|
return result;
|
|
@@ -5497,10 +6105,10 @@ var SessionHistoryImporter = class {
|
|
|
5497
6105
|
*/
|
|
5498
6106
|
async findProjectDir(projectPath) {
|
|
5499
6107
|
const projectsDir = path2.join(this.claudeDir, "projects");
|
|
5500
|
-
if (!
|
|
6108
|
+
if (!fs3.existsSync(projectsDir)) {
|
|
5501
6109
|
return null;
|
|
5502
6110
|
}
|
|
5503
|
-
const projectDirs =
|
|
6111
|
+
const projectDirs = fs3.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs3.statSync(p).isDirectory());
|
|
5504
6112
|
const normalizedPath = projectPath.replace(/\//g, "-").replace(/^-/, "");
|
|
5505
6113
|
for (const dir of projectDirs) {
|
|
5506
6114
|
const dirName = path2.basename(dir);
|
|
@@ -5514,10 +6122,10 @@ var SessionHistoryImporter = class {
|
|
|
5514
6122
|
* Find all JSONL session files in a directory
|
|
5515
6123
|
*/
|
|
5516
6124
|
async findSessionFiles(dir) {
|
|
5517
|
-
if (!
|
|
6125
|
+
if (!fs3.existsSync(dir)) {
|
|
5518
6126
|
return [];
|
|
5519
6127
|
}
|
|
5520
|
-
return
|
|
6128
|
+
return fs3.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path2.join(dir, name)).filter((p) => fs3.statSync(p).isFile());
|
|
5521
6129
|
}
|
|
5522
6130
|
/**
|
|
5523
6131
|
* Extract text content from Claude message
|
|
@@ -5549,14 +6157,14 @@ var SessionHistoryImporter = class {
|
|
|
5549
6157
|
}
|
|
5550
6158
|
} else {
|
|
5551
6159
|
const projectsDir = path2.join(this.claudeDir, "projects");
|
|
5552
|
-
if (
|
|
5553
|
-
projectDirs =
|
|
6160
|
+
if (fs3.existsSync(projectsDir)) {
|
|
6161
|
+
projectDirs = fs3.readdirSync(projectsDir).map((name) => path2.join(projectsDir, name)).filter((p) => fs3.statSync(p).isDirectory());
|
|
5554
6162
|
}
|
|
5555
6163
|
}
|
|
5556
6164
|
for (const projectDir of projectDirs) {
|
|
5557
6165
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
5558
6166
|
for (const filePath of sessionFiles) {
|
|
5559
|
-
const stats =
|
|
6167
|
+
const stats = fs3.statSync(filePath);
|
|
5560
6168
|
sessions.push({
|
|
5561
6169
|
sessionId: path2.basename(filePath, ".jsonl"),
|
|
5562
6170
|
filePath,
|
|
@@ -5574,24 +6182,52 @@ function createSessionHistoryImporter(memoryService) {
|
|
|
5574
6182
|
}
|
|
5575
6183
|
|
|
5576
6184
|
// src/server/index.ts
|
|
5577
|
-
import { Hono as
|
|
6185
|
+
import { Hono as Hono10 } from "hono";
|
|
5578
6186
|
import { cors } from "hono/cors";
|
|
5579
6187
|
import { logger } from "hono/logger";
|
|
5580
6188
|
import { serve } from "@hono/node-server";
|
|
5581
6189
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5582
|
-
import * as
|
|
5583
|
-
import * as
|
|
6190
|
+
import * as path5 from "path";
|
|
6191
|
+
import * as fs5 from "fs";
|
|
5584
6192
|
|
|
5585
6193
|
// src/server/api/index.ts
|
|
5586
|
-
import { Hono as
|
|
6194
|
+
import { Hono as Hono9 } from "hono";
|
|
5587
6195
|
|
|
5588
6196
|
// src/server/api/sessions.ts
|
|
5589
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
|
|
5590
6226
|
var sessionsRouter = new Hono();
|
|
5591
6227
|
sessionsRouter.get("/", async (c) => {
|
|
5592
6228
|
const page = parseInt(c.req.query("page") || "1", 10);
|
|
5593
6229
|
const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
|
|
5594
|
-
const memoryService =
|
|
6230
|
+
const memoryService = getServiceFromQuery(c);
|
|
5595
6231
|
try {
|
|
5596
6232
|
await memoryService.initialize();
|
|
5597
6233
|
const recentEvents = await memoryService.getRecentEvents(1e3);
|
|
@@ -5635,7 +6271,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
5635
6271
|
});
|
|
5636
6272
|
sessionsRouter.get("/:id", async (c) => {
|
|
5637
6273
|
const { id } = c.req.param();
|
|
5638
|
-
const memoryService =
|
|
6274
|
+
const memoryService = getServiceFromQuery(c);
|
|
5639
6275
|
try {
|
|
5640
6276
|
await memoryService.initialize();
|
|
5641
6277
|
const events = await memoryService.getSessionHistory(id);
|
|
@@ -5676,18 +6312,36 @@ var eventsRouter = new Hono2();
|
|
|
5676
6312
|
eventsRouter.get("/", async (c) => {
|
|
5677
6313
|
const sessionId = c.req.query("sessionId");
|
|
5678
6314
|
const eventType = c.req.query("type");
|
|
6315
|
+
const level = c.req.query("level");
|
|
6316
|
+
const sort = c.req.query("sort") || "recent";
|
|
5679
6317
|
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
5680
6318
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5681
|
-
const memoryService =
|
|
6319
|
+
const memoryService = getServiceFromQuery(c);
|
|
5682
6320
|
try {
|
|
5683
6321
|
await memoryService.initialize();
|
|
5684
|
-
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
|
+
}
|
|
5685
6328
|
if (sessionId) {
|
|
5686
6329
|
events = events.filter((e) => e.sessionId === sessionId);
|
|
5687
6330
|
}
|
|
5688
6331
|
if (eventType) {
|
|
5689
6332
|
events = events.filter((e) => e.eventType === eventType);
|
|
5690
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
|
+
}
|
|
5691
6345
|
const total = events.length;
|
|
5692
6346
|
events = events.slice(offset, offset + limit);
|
|
5693
6347
|
return c.json({
|
|
@@ -5697,7 +6351,9 @@ eventsRouter.get("/", async (c) => {
|
|
|
5697
6351
|
timestamp: e.timestamp,
|
|
5698
6352
|
sessionId: e.sessionId,
|
|
5699
6353
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
5700
|
-
contentLength: e.content.length
|
|
6354
|
+
contentLength: e.content.length,
|
|
6355
|
+
accessCount: e.access_count || 0,
|
|
6356
|
+
lastAccessedAt: e.last_accessed_at || null
|
|
5701
6357
|
})),
|
|
5702
6358
|
total,
|
|
5703
6359
|
limit,
|
|
@@ -5712,7 +6368,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
5712
6368
|
});
|
|
5713
6369
|
eventsRouter.get("/:id", async (c) => {
|
|
5714
6370
|
const { id } = c.req.param();
|
|
5715
|
-
const memoryService =
|
|
6371
|
+
const memoryService = getServiceFromQuery(c);
|
|
5716
6372
|
try {
|
|
5717
6373
|
await memoryService.initialize();
|
|
5718
6374
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -5752,7 +6408,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
5752
6408
|
import { Hono as Hono3 } from "hono";
|
|
5753
6409
|
var searchRouter = new Hono3();
|
|
5754
6410
|
searchRouter.post("/", async (c) => {
|
|
5755
|
-
const memoryService =
|
|
6411
|
+
const memoryService = getServiceFromQuery(c);
|
|
5756
6412
|
try {
|
|
5757
6413
|
const body = await c.req.json();
|
|
5758
6414
|
if (!body.query) {
|
|
@@ -5796,7 +6452,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5796
6452
|
return c.json({ error: 'Query parameter "q" is required' }, 400);
|
|
5797
6453
|
}
|
|
5798
6454
|
const topK = parseInt(c.req.query("topK") || "5", 10);
|
|
5799
|
-
const memoryService =
|
|
6455
|
+
const memoryService = getServiceFromQuery(c);
|
|
5800
6456
|
try {
|
|
5801
6457
|
await memoryService.initialize();
|
|
5802
6458
|
const result = await memoryService.retrieveMemories(query, { topK });
|
|
@@ -5824,7 +6480,7 @@ searchRouter.get("/", async (c) => {
|
|
|
5824
6480
|
import { Hono as Hono4 } from "hono";
|
|
5825
6481
|
var statsRouter = new Hono4();
|
|
5826
6482
|
statsRouter.get("/shared", async (c) => {
|
|
5827
|
-
const memoryService =
|
|
6483
|
+
const memoryService = getServiceFromQuery(c);
|
|
5828
6484
|
try {
|
|
5829
6485
|
await memoryService.initialize();
|
|
5830
6486
|
const sharedStats = await memoryService.getSharedStoreStats();
|
|
@@ -5881,7 +6537,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5881
6537
|
if (!validLevels.includes(level)) {
|
|
5882
6538
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
5883
6539
|
}
|
|
5884
|
-
const memoryService =
|
|
6540
|
+
const memoryService = getServiceFromQuery(c);
|
|
5885
6541
|
try {
|
|
5886
6542
|
await memoryService.initialize();
|
|
5887
6543
|
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
@@ -5928,7 +6584,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
5928
6584
|
}
|
|
5929
6585
|
});
|
|
5930
6586
|
statsRouter.get("/", async (c) => {
|
|
5931
|
-
const memoryService =
|
|
6587
|
+
const memoryService = getServiceFromQuery(c);
|
|
5932
6588
|
try {
|
|
5933
6589
|
await memoryService.initialize();
|
|
5934
6590
|
const stats = await memoryService.getStats();
|
|
@@ -5972,7 +6628,7 @@ statsRouter.get("/", async (c) => {
|
|
|
5972
6628
|
});
|
|
5973
6629
|
statsRouter.get("/most-accessed", async (c) => {
|
|
5974
6630
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
5975
|
-
const memoryService =
|
|
6631
|
+
const memoryService = getServiceFromQuery(c);
|
|
5976
6632
|
try {
|
|
5977
6633
|
await memoryService.initialize();
|
|
5978
6634
|
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
@@ -6003,7 +6659,7 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
6003
6659
|
});
|
|
6004
6660
|
statsRouter.get("/timeline", async (c) => {
|
|
6005
6661
|
const days = parseInt(c.req.query("days") || "7", 10);
|
|
6006
|
-
const memoryService =
|
|
6662
|
+
const memoryService = getServiceFromQuery(c);
|
|
6007
6663
|
try {
|
|
6008
6664
|
await memoryService.initialize();
|
|
6009
6665
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6033,8 +6689,39 @@ statsRouter.get("/timeline", async (c) => {
|
|
|
6033
6689
|
await memoryService.shutdown();
|
|
6034
6690
|
}
|
|
6035
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
|
+
});
|
|
6036
6723
|
statsRouter.post("/graduation/run", async (c) => {
|
|
6037
|
-
const memoryService =
|
|
6724
|
+
const memoryService = getServiceFromQuery(c);
|
|
6038
6725
|
try {
|
|
6039
6726
|
await memoryService.initialize();
|
|
6040
6727
|
const result = await memoryService.forceGraduation();
|
|
@@ -6095,7 +6782,7 @@ var citationsRouter = new Hono5();
|
|
|
6095
6782
|
citationsRouter.get("/:id", async (c) => {
|
|
6096
6783
|
const { id } = c.req.param();
|
|
6097
6784
|
const citationId = parseCitationId(id) || id;
|
|
6098
|
-
const memoryService =
|
|
6785
|
+
const memoryService = getServiceFromQuery(c);
|
|
6099
6786
|
try {
|
|
6100
6787
|
await memoryService.initialize();
|
|
6101
6788
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6129,7 +6816,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
6129
6816
|
citationsRouter.get("/:id/related", async (c) => {
|
|
6130
6817
|
const { id } = c.req.param();
|
|
6131
6818
|
const citationId = parseCitationId(id) || id;
|
|
6132
|
-
const memoryService =
|
|
6819
|
+
const memoryService = getServiceFromQuery(c);
|
|
6133
6820
|
try {
|
|
6134
6821
|
await memoryService.initialize();
|
|
6135
6822
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -6165,23 +6852,373 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
6165
6852
|
}
|
|
6166
6853
|
});
|
|
6167
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
|
+
|
|
6168
7205
|
// src/server/api/index.ts
|
|
6169
|
-
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);
|
|
6170
7207
|
|
|
6171
7208
|
// src/server/index.ts
|
|
6172
|
-
var app = new
|
|
7209
|
+
var app = new Hono10();
|
|
6173
7210
|
app.use("/*", cors());
|
|
6174
7211
|
app.use("/*", logger());
|
|
6175
7212
|
app.route("/api", apiRouter);
|
|
6176
7213
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
6177
|
-
var uiPath =
|
|
6178
|
-
if (
|
|
7214
|
+
var uiPath = path5.join(__dirname, "../../dist/ui");
|
|
7215
|
+
if (fs5.existsSync(uiPath)) {
|
|
6179
7216
|
app.use("/*", serveStatic({ root: uiPath }));
|
|
6180
7217
|
}
|
|
6181
7218
|
app.get("*", (c) => {
|
|
6182
|
-
const indexPath =
|
|
6183
|
-
if (
|
|
6184
|
-
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"));
|
|
6185
7222
|
}
|
|
6186
7223
|
return c.text('UI not built. Run "npm run build:ui" first.', 404);
|
|
6187
7224
|
});
|
|
@@ -6219,28 +7256,28 @@ if (isMainModule) {
|
|
|
6219
7256
|
}
|
|
6220
7257
|
|
|
6221
7258
|
// src/cli/index.ts
|
|
6222
|
-
var CLAUDE_SETTINGS_PATH =
|
|
7259
|
+
var CLAUDE_SETTINGS_PATH = path6.join(os5.homedir(), ".claude", "settings.json");
|
|
6223
7260
|
function getPluginPath() {
|
|
6224
7261
|
const possiblePaths = [
|
|
6225
|
-
|
|
7262
|
+
path6.join(__dirname, ".."),
|
|
6226
7263
|
// When running from dist/cli
|
|
6227
|
-
|
|
7264
|
+
path6.join(__dirname, "../..", "dist"),
|
|
6228
7265
|
// When running from src
|
|
6229
|
-
|
|
7266
|
+
path6.join(process.cwd(), "dist")
|
|
6230
7267
|
// Current working directory
|
|
6231
7268
|
];
|
|
6232
7269
|
for (const p of possiblePaths) {
|
|
6233
|
-
const hooksPath =
|
|
6234
|
-
if (
|
|
7270
|
+
const hooksPath = path6.join(p, "hooks", "user-prompt-submit.js");
|
|
7271
|
+
if (fs6.existsSync(hooksPath)) {
|
|
6235
7272
|
return p;
|
|
6236
7273
|
}
|
|
6237
7274
|
}
|
|
6238
|
-
return
|
|
7275
|
+
return path6.join(os5.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
|
|
6239
7276
|
}
|
|
6240
7277
|
function loadClaudeSettings() {
|
|
6241
7278
|
try {
|
|
6242
|
-
if (
|
|
6243
|
-
const content =
|
|
7279
|
+
if (fs6.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
7280
|
+
const content = fs6.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
6244
7281
|
return JSON.parse(content);
|
|
6245
7282
|
}
|
|
6246
7283
|
} catch (error) {
|
|
@@ -6249,13 +7286,13 @@ function loadClaudeSettings() {
|
|
|
6249
7286
|
return {};
|
|
6250
7287
|
}
|
|
6251
7288
|
function saveClaudeSettings(settings) {
|
|
6252
|
-
const dir =
|
|
6253
|
-
if (!
|
|
6254
|
-
|
|
7289
|
+
const dir = path6.dirname(CLAUDE_SETTINGS_PATH);
|
|
7290
|
+
if (!fs6.existsSync(dir)) {
|
|
7291
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
6255
7292
|
}
|
|
6256
7293
|
const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
|
|
6257
|
-
|
|
6258
|
-
|
|
7294
|
+
fs6.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
|
|
7295
|
+
fs6.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
6259
7296
|
}
|
|
6260
7297
|
function getHooksConfig(pluginPath) {
|
|
6261
7298
|
return {
|
|
@@ -6265,7 +7302,7 @@ function getHooksConfig(pluginPath) {
|
|
|
6265
7302
|
hooks: [
|
|
6266
7303
|
{
|
|
6267
7304
|
type: "command",
|
|
6268
|
-
command: `node ${
|
|
7305
|
+
command: `node ${path6.join(pluginPath, "hooks", "user-prompt-submit.js")}`
|
|
6269
7306
|
}
|
|
6270
7307
|
]
|
|
6271
7308
|
}
|
|
@@ -6276,7 +7313,7 @@ function getHooksConfig(pluginPath) {
|
|
|
6276
7313
|
hooks: [
|
|
6277
7314
|
{
|
|
6278
7315
|
type: "command",
|
|
6279
|
-
command: `node ${
|
|
7316
|
+
command: `node ${path6.join(pluginPath, "hooks", "post-tool-use.js")}`
|
|
6280
7317
|
}
|
|
6281
7318
|
]
|
|
6282
7319
|
}
|
|
@@ -6288,8 +7325,8 @@ program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI")
|
|
|
6288
7325
|
program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
|
|
6289
7326
|
try {
|
|
6290
7327
|
const pluginPath = options.path || getPluginPath();
|
|
6291
|
-
const userPromptHook =
|
|
6292
|
-
if (!
|
|
7328
|
+
const userPromptHook = path6.join(pluginPath, "hooks", "user-prompt-submit.js");
|
|
7329
|
+
if (!fs6.existsSync(userPromptHook)) {
|
|
6293
7330
|
console.error(`
|
|
6294
7331
|
\u274C Hook files not found at: ${pluginPath}`);
|
|
6295
7332
|
console.error(' Make sure you have built the plugin with "npm run build"');
|
|
@@ -6355,7 +7392,7 @@ program.command("status").description("Check plugin installation status").action
|
|
|
6355
7392
|
console.log("Hooks:");
|
|
6356
7393
|
console.log(` UserPromptSubmit: ${hasUserPromptHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6357
7394
|
console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6358
|
-
const hooksExist =
|
|
7395
|
+
const hooksExist = fs6.existsSync(path6.join(pluginPath, "hooks", "user-prompt-submit.js"));
|
|
6359
7396
|
console.log(`
|
|
6360
7397
|
Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
|
|
6361
7398
|
console.log(` Path: ${pluginPath}`);
|
|
@@ -6483,95 +7520,143 @@ program.command("process").description("Process pending embeddings").option("-p,
|
|
|
6483
7520
|
process.exit(1);
|
|
6484
7521
|
}
|
|
6485
7522
|
});
|
|
6486
|
-
|
|
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();
|
|
6487
7598
|
const targetProjectPath = options.project || process.cwd();
|
|
6488
7599
|
const service = getMemoryServiceForProject(targetProjectPath);
|
|
6489
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
|
+
};
|
|
6490
7607
|
try {
|
|
7608
|
+
console.log("\n\u23F3 Initializing memory service...");
|
|
6491
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
|
+
}
|
|
6492
7614
|
let result;
|
|
6493
7615
|
if (options.session) {
|
|
6494
|
-
console.log(`
|
|
6495
|
-
|
|
6496
|
-
console.log(` Target project: ${targetProjectPath}
|
|
7616
|
+
console.log(`\u{1F4E5} Importing session: ${options.session}`);
|
|
7617
|
+
console.log(` Target: ${targetProjectPath}
|
|
6497
7618
|
`);
|
|
6498
7619
|
result = await importer.importSessionFile(options.session, {
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
verbose: options.verbose
|
|
7620
|
+
...importOpts,
|
|
7621
|
+
projectPath: targetProjectPath
|
|
6502
7622
|
});
|
|
6503
7623
|
} else if (options.project) {
|
|
6504
|
-
console.log(
|
|
6505
|
-
\u{1F4E5} Importing project: ${options.project}
|
|
7624
|
+
console.log(`\u{1F4E5} Importing project: ${options.project}
|
|
6506
7625
|
`);
|
|
6507
|
-
result = await importer.importProject(options.project,
|
|
6508
|
-
limit: options.limit ? parseInt(options.limit) : void 0,
|
|
6509
|
-
verbose: options.verbose
|
|
6510
|
-
});
|
|
7626
|
+
result = await importer.importProject(options.project, importOpts);
|
|
6511
7627
|
} else if (options.all) {
|
|
6512
|
-
console.log("\
|
|
7628
|
+
console.log("\u{1F4E5} Importing all sessions from all projects");
|
|
6513
7629
|
console.log(" \u26A0\uFE0F Using global storage (use -p for project-specific)\n");
|
|
6514
7630
|
const globalService = getDefaultMemoryService();
|
|
6515
7631
|
const globalImporter = createSessionHistoryImporter(globalService);
|
|
6516
7632
|
await globalService.initialize();
|
|
6517
|
-
result = await globalImporter.importAll(
|
|
6518
|
-
|
|
6519
|
-
verbose: options.verbose
|
|
6520
|
-
});
|
|
6521
|
-
console.log("\n\u23F3 Processing embeddings...");
|
|
7633
|
+
result = await globalImporter.importAll(importOpts);
|
|
7634
|
+
console.log("\n\u{1F9E0} Processing embeddings...");
|
|
6522
7635
|
const embedCount2 = await globalService.processPendingEmbeddings();
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
console.log(`
|
|
6526
|
-
|
|
6527
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
6528
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
6529
|
-
console.log(`Embeddings processed: ${embedCount2}`);
|
|
6530
|
-
if (result.errors.length > 0) {
|
|
6531
|
-
console.log(`
|
|
6532
|
-
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
6533
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
6534
|
-
console.log(` - ${error}`);
|
|
6535
|
-
}
|
|
6536
|
-
if (result.errors.length > 5) {
|
|
6537
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
6538
|
-
}
|
|
6539
|
-
}
|
|
7636
|
+
const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
7637
|
+
printImportSummary(result, embedCount2);
|
|
7638
|
+
console.log(`
|
|
7639
|
+
\u23F1\uFE0F Completed in ${elapsed2}s`);
|
|
6540
7640
|
await globalService.shutdown();
|
|
6541
7641
|
return;
|
|
6542
7642
|
} else {
|
|
6543
7643
|
const cwd = process.cwd();
|
|
6544
|
-
console.log(
|
|
6545
|
-
\u{1F4E5} Importing sessions for current project: ${cwd}
|
|
7644
|
+
console.log(`\u{1F4E5} Importing sessions for: ${cwd}
|
|
6546
7645
|
`);
|
|
6547
7646
|
result = await importer.importProject(cwd, {
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
verbose: options.verbose
|
|
7647
|
+
...importOpts,
|
|
7648
|
+
projectPath: cwd
|
|
6551
7649
|
});
|
|
6552
7650
|
}
|
|
6553
|
-
console.log("\n\
|
|
7651
|
+
console.log("\n\u{1F9E0} Processing embeddings...");
|
|
6554
7652
|
const embedCount = await service.processPendingEmbeddings();
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
console.log(`
|
|
6558
|
-
|
|
6559
|
-
console.log(`Imported responses: ${result.importedResponses}`);
|
|
6560
|
-
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
6561
|
-
console.log(`Embeddings processed: ${embedCount}`);
|
|
6562
|
-
if (result.errors.length > 0) {
|
|
6563
|
-
console.log(`
|
|
6564
|
-
\u26A0\uFE0F Errors (${result.errors.length}):`);
|
|
6565
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
6566
|
-
console.log(` - ${error}`);
|
|
6567
|
-
}
|
|
6568
|
-
if (result.errors.length > 5) {
|
|
6569
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
6570
|
-
}
|
|
6571
|
-
}
|
|
7653
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
7654
|
+
printImportSummary(result, embedCount);
|
|
7655
|
+
console.log(`
|
|
7656
|
+
\u23F1\uFE0F Completed in ${elapsed}s`);
|
|
6572
7657
|
await service.shutdown();
|
|
6573
7658
|
} catch (error) {
|
|
6574
|
-
console.error("Import failed:", error);
|
|
7659
|
+
console.error("\n\u274C Import failed:", error);
|
|
6575
7660
|
process.exit(1);
|
|
6576
7661
|
}
|
|
6577
7662
|
});
|