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.
Files changed (74) hide show
  1. package/dist/cli/index.js +1266 -181
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/core/index.js +367 -7
  4. package/dist/core/index.js.map +2 -2
  5. package/dist/hooks/post-tool-use.js +598 -40
  6. package/dist/hooks/post-tool-use.js.map +4 -4
  7. package/dist/hooks/session-end.js +486 -49
  8. package/dist/hooks/session-end.js.map +3 -3
  9. package/dist/hooks/session-start.js +474 -22
  10. package/dist/hooks/session-start.js.map +3 -3
  11. package/dist/hooks/stop.js +586 -70
  12. package/dist/hooks/stop.js.map +4 -4
  13. package/dist/hooks/user-prompt-submit.js +537 -27
  14. package/dist/hooks/user-prompt-submit.js.map +4 -4
  15. package/dist/server/api/index.js +938 -39
  16. package/dist/server/api/index.js.map +4 -4
  17. package/dist/server/index.js +947 -48
  18. package/dist/server/index.js.map +4 -4
  19. package/dist/services/memory-service.js +475 -22
  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 +444 -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 +44 -5
  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 +137 -5
  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/.history/package_20260202121115.json +0 -49
  74. 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,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(input.metadata || {})
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
- const rows = sqliteAll(
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
- 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
+ }
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
- 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
+ }
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 fs.realpathSync(expanded);
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 && !fs.existsSync(storagePath)) {
4626
- fs.mkdirSync(storagePath, { recursive: true });
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 (!fs.existsSync(sharedPath)) {
4722
- fs.mkdirSync(sharedPath, { recursive: true });
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 fs2 from "fs";
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 (const sessionFile of sessionFiles) {
5906
+ for (let i = 0; i < sessionFiles.length; i++) {
5907
+ const sessionFile = sessionFiles[i];
5373
5908
  try {
5374
- 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
+ });
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 (!fs2.existsSync(filePath)) {
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 = fs2.createReadStream(filePath);
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
- if (entry.type === "user" || entry.type === "assistant") {
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
- if (entry.type === "user") {
5421
- const appendResult = await this.memoryService.storeUserPrompt(
5422
- sessionId,
5423
- content,
5424
- { importedFrom: filePath, originalTimestamp: entry.timestamp }
5425
- );
5426
- if (appendResult.isDuplicate) {
5427
- result.skippedDuplicates++;
5428
- } else {
5429
- result.importedPrompts++;
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 (!fs2.existsSync(projectsDir)) {
6061
+ if (!fs3.existsSync(projectsDir)) {
5470
6062
  result.errors.push(`Projects directory not found: ${projectsDir}`);
5471
6063
  return result;
5472
6064
  }
5473
- 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` });
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 (const projectDir of projectDirs) {
6076
+ for (let i = 0; i < allSessionFiles.length; i++) {
6077
+ const sessionFile = allSessionFiles[i];
5478
6078
  try {
5479
- const sessionFiles = await this.findSessionFiles(projectDir);
5480
- for (const sessionFile of sessionFiles) {
5481
- const sessionResult = await this.importSessionFile(sessionFile, options);
5482
- result.totalSessions++;
5483
- result.totalMessages += sessionResult.totalMessages;
5484
- result.importedPrompts += sessionResult.importedPrompts;
5485
- result.importedResponses += sessionResult.importedResponses;
5486
- result.skippedDuplicates += sessionResult.skippedDuplicates;
5487
- result.errors.push(...sessionResult.errors);
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 ${projectDir}: ${error}`);
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 (!fs2.existsSync(projectsDir)) {
6108
+ if (!fs3.existsSync(projectsDir)) {
5501
6109
  return null;
5502
6110
  }
5503
- 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());
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 (!fs2.existsSync(dir)) {
6125
+ if (!fs3.existsSync(dir)) {
5518
6126
  return [];
5519
6127
  }
5520
- 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());
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 (fs2.existsSync(projectsDir)) {
5553
- 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());
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 = fs2.statSync(filePath);
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 Hono7 } from "hono";
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 path3 from "path";
5583
- import * as fs3 from "fs";
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 Hono6 } from "hono";
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
6319
+ const memoryService = getServiceFromQuery(c);
5682
6320
  try {
5683
6321
  await memoryService.initialize();
5684
- 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
+ }
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 = getReadOnlyMemoryService();
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 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);
6170
7207
 
6171
7208
  // src/server/index.ts
6172
- var app = new Hono7();
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 = path3.join(__dirname, "../../dist/ui");
6178
- if (fs3.existsSync(uiPath)) {
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 = path3.join(uiPath, "index.html");
6183
- if (fs3.existsSync(indexPath)) {
6184
- 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"));
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 = path4.join(os3.homedir(), ".claude", "settings.json");
7259
+ var CLAUDE_SETTINGS_PATH = path6.join(os5.homedir(), ".claude", "settings.json");
6223
7260
  function getPluginPath() {
6224
7261
  const possiblePaths = [
6225
- path4.join(__dirname, ".."),
7262
+ path6.join(__dirname, ".."),
6226
7263
  // When running from dist/cli
6227
- path4.join(__dirname, "../..", "dist"),
7264
+ path6.join(__dirname, "../..", "dist"),
6228
7265
  // When running from src
6229
- path4.join(process.cwd(), "dist")
7266
+ path6.join(process.cwd(), "dist")
6230
7267
  // Current working directory
6231
7268
  ];
6232
7269
  for (const p of possiblePaths) {
6233
- const hooksPath = path4.join(p, "hooks", "user-prompt-submit.js");
6234
- if (fs4.existsSync(hooksPath)) {
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 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");
6239
7276
  }
6240
7277
  function loadClaudeSettings() {
6241
7278
  try {
6242
- if (fs4.existsSync(CLAUDE_SETTINGS_PATH)) {
6243
- 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");
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 = path4.dirname(CLAUDE_SETTINGS_PATH);
6253
- if (!fs4.existsSync(dir)) {
6254
- fs4.mkdirSync(dir, { recursive: true });
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
- fs4.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
6258
- fs4.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
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 ${path4.join(pluginPath, "hooks", "user-prompt-submit.js")}`
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 ${path4.join(pluginPath, "hooks", "post-tool-use.js")}`
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 = path4.join(pluginPath, "hooks", "user-prompt-submit.js");
6292
- if (!fs4.existsSync(userPromptHook)) {
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 = fs4.existsSync(path4.join(pluginPath, "hooks", "user-prompt-submit.js"));
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
- 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();
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
- \u{1F4E5} Importing session: ${options.session}`);
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
- projectPath: targetProjectPath,
6500
- limit: options.limit ? parseInt(options.limit) : void 0,
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("\n\u{1F4E5} Importing all sessions from all projects");
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
- limit: options.limit ? parseInt(options.limit) : void 0,
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
- console.log("\n\u2705 Import Complete\n");
6524
- console.log(`Sessions processed: ${result.totalSessions}`);
6525
- console.log(`Total messages: ${result.totalMessages}`);
6526
- console.log(`Imported prompts: ${result.importedPrompts}`);
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
- projectPath: cwd,
6549
- limit: options.limit ? parseInt(options.limit) : void 0,
6550
- verbose: options.verbose
7647
+ ...importOpts,
7648
+ projectPath: cwd
6551
7649
  });
6552
7650
  }
6553
- console.log("\n\u23F3 Processing embeddings...");
7651
+ console.log("\n\u{1F9E0} Processing embeddings...");
6554
7652
  const embedCount = await service.processPendingEmbeddings();
6555
- console.log("\n\u2705 Import Complete\n");
6556
- console.log(`Sessions processed: ${result.totalSessions}`);
6557
- console.log(`Total messages: ${result.totalMessages}`);
6558
- console.log(`Imported prompts: ${result.importedPrompts}`);
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
  });