claude-memory-layer 1.0.9 → 1.0.11

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