claude-memory-hub 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.5.2] - 2026-04-01
9
+
10
+ ### Fixed
11
+ - **Viewer JS broken after bundle** — inline `onclick` handlers lost reference when Bun bundled template literal into `cli.js`. Rewrote all JS to IIFE + `addEventListener` pattern
12
+ - **Escaped quotes in template literal** — `this.classList.toggle('expanded')` caused `SyntaxError: Unexpected identifier` after bundle. Switched to double quotes and event delegation
13
+ - **push-private.sh deletes source** — `git checkout main` removed untracked `src/` directory. Added backup/restore of source dirs around branch switch
14
+
15
+ ### Changed
16
+ - **push-public.sh** — fixed version extraction in commit message (`node -p` with proper quoting)
17
+
18
+ ---
19
+
20
+ ## [0.5.1] - 2026-04-01
21
+
22
+ ### Fixed
23
+ - **Viewer API crash** — all `db.query()` calls in viewer replaced with `db.prepare().all()` to fix bun:sqlite parameter binding (`SQLITE_MISMATCH` errors on sessions, summaries, entities endpoints)
24
+ - **Error handling** — viewer server now catches errors at fetch level with `error()` handler, preventing Bun's default error page from leaking
25
+
26
+ ### Changed
27
+ - **UI redesign** — dark gradient theme, stat cards with gradient text, pill-shaped type badges, expandable card content, SVG search icon, improved typography and spacing, responsive grid layout
28
+
29
+ ---
30
+
8
31
  ## [0.5.0] - 2026-04-01
9
32
 
10
33
  Major release: production hardening, hybrid search, browser UI, claude-mem migration.
package/README.md CHANGED
@@ -235,6 +235,21 @@ One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, Jet
235
235
 
236
236
  **Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically. No manual steps needed.
237
237
 
238
+ ### Update
239
+
240
+ ```bash
241
+ bunx claude-memory-hub@latest install
242
+ ```
243
+
244
+ Or if installed globally:
245
+
246
+ ```bash
247
+ bun install -g claude-memory-hub@latest
248
+ claude-memory-hub install
249
+ ```
250
+
251
+ Your data at `~/.claude-memory-hub/` is preserved across updates. Schema migrations run automatically.
252
+
238
253
  ### From source
239
254
 
240
255
  ```bash
package/dist/cli.js CHANGED
@@ -737,49 +737,52 @@ __export(exports_viewer, {
737
737
  function handleApi(url) {
738
738
  const db = getDatabase();
739
739
  const path = url.pathname;
740
- if (path === "/api/health") {
741
- const report = runHealthCheck(db);
742
- return json(report);
743
- }
744
- if (path === "/api/stats") {
745
- const sessions = db.query("SELECT COUNT(*) as c FROM sessions").get()?.c ?? 0;
746
- const entities = db.query("SELECT COUNT(*) as c FROM entities").get()?.c ?? 0;
747
- const summaries = db.query("SELECT COUNT(*) as c FROM long_term_summaries").get()?.c ?? 0;
748
- const notes = db.query("SELECT COUNT(*) as c FROM session_notes").get()?.c ?? 0;
749
- return json({ sessions, entities, summaries, notes });
750
- }
751
- if (path === "/api/search") {
752
- const query = url.searchParams.get("q") || "";
753
- const limit = parseInt(url.searchParams.get("limit") || "20");
754
- const offset = parseInt(url.searchParams.get("offset") || "0");
755
- const project = url.searchParams.get("project") || undefined;
756
- const results = searchIndex(query, { limit, offset, project }, db);
757
- return json(results);
758
- }
759
- if (path === "/api/sessions") {
760
- const limit = parseInt(url.searchParams.get("limit") || "50");
761
- const offset = parseInt(url.searchParams.get("offset") || "0");
762
- const rows = db.query("SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?", limit, offset).all();
763
- return json(rows);
764
- }
765
- if (path === "/api/summaries") {
766
- const limit = parseInt(url.searchParams.get("limit") || "50");
767
- const offset = parseInt(url.searchParams.get("offset") || "0");
768
- const rows = db.query("SELECT * FROM long_term_summaries ORDER BY created_at DESC LIMIT ? OFFSET ?", limit, offset).all();
769
- return json(rows);
770
- }
771
- if (path === "/api/entities") {
772
- const sessionId = url.searchParams.get("session_id");
773
- const limit = parseInt(url.searchParams.get("limit") || "100");
774
- const offset = parseInt(url.searchParams.get("offset") || "0");
775
- if (sessionId) {
776
- const rows2 = db.query("SELECT * FROM entities WHERE session_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?", sessionId, limit, offset).all();
777
- return json(rows2);
740
+ try {
741
+ if (path === "/api/health") {
742
+ return json(runHealthCheck(db));
743
+ }
744
+ if (path === "/api/stats") {
745
+ const sessions = db.prepare("SELECT COUNT(*) as c FROM sessions").get()?.c ?? 0;
746
+ const entities = db.prepare("SELECT COUNT(*) as c FROM entities").get()?.c ?? 0;
747
+ const summaries = db.prepare("SELECT COUNT(*) as c FROM long_term_summaries").get()?.c ?? 0;
748
+ const notes = db.prepare("SELECT COUNT(*) as c FROM session_notes").get()?.c ?? 0;
749
+ return json({ sessions, entities, summaries, notes });
750
+ }
751
+ if (path === "/api/search") {
752
+ const query = url.searchParams.get("q") || "";
753
+ const limit = parseInt(url.searchParams.get("limit") || "20");
754
+ const offset = parseInt(url.searchParams.get("offset") || "0");
755
+ const project = url.searchParams.get("project");
756
+ return json(searchIndex(query, { limit, offset, ...project ? { project } : {} }, db));
778
757
  }
779
- const rows = db.query("SELECT * FROM entities ORDER BY created_at DESC LIMIT ? OFFSET ?", limit, offset).all();
780
- return json(rows);
758
+ if (path === "/api/sessions") {
759
+ const limit = parseInt(url.searchParams.get("limit") || "50");
760
+ const offset = parseInt(url.searchParams.get("offset") || "0");
761
+ const rows = db.prepare("SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?").all(limit, offset);
762
+ return json(rows);
763
+ }
764
+ if (path === "/api/summaries") {
765
+ const limit = parseInt(url.searchParams.get("limit") || "50");
766
+ const offset = parseInt(url.searchParams.get("offset") || "0");
767
+ const rows = db.prepare("SELECT * FROM long_term_summaries ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
768
+ return json(rows);
769
+ }
770
+ if (path === "/api/entities") {
771
+ const sessionId = url.searchParams.get("session_id");
772
+ const limit = parseInt(url.searchParams.get("limit") || "100");
773
+ const offset = parseInt(url.searchParams.get("offset") || "0");
774
+ if (sessionId) {
775
+ const rows2 = db.prepare("SELECT * FROM entities WHERE session_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(sessionId, limit, offset);
776
+ return json(rows2);
777
+ }
778
+ const rows = db.prepare("SELECT * FROM entities ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
779
+ return json(rows);
780
+ }
781
+ return json({ error: "Not found" }, 404);
782
+ } catch (e) {
783
+ log5.error("API error", { path, error: String(e) });
784
+ return json({ error: String(e) }, 500);
781
785
  }
782
- return json({ error: "Not found" }, 404);
783
786
  }
784
787
  function json(data, status = 200) {
785
788
  return new Response(JSON.stringify(data), {
@@ -791,10 +794,25 @@ function startViewer() {
791
794
  const server = Bun.serve({
792
795
  port: PORT,
793
796
  fetch(req) {
794
- const url = new URL(req.url);
795
- if (url.pathname.startsWith("/api/"))
796
- return handleApi(url);
797
- return new Response(HTML, { headers: { "Content-Type": "text/html" } });
797
+ try {
798
+ const url = new URL(req.url);
799
+ if (url.pathname.startsWith("/api/"))
800
+ return handleApi(url);
801
+ return new Response(HTML, { headers: { "Content-Type": "text/html" } });
802
+ } catch (e) {
803
+ log5.error("Server fetch error", { error: String(e) });
804
+ return new Response(JSON.stringify({ error: String(e) }), {
805
+ status: 500,
806
+ headers: { "Content-Type": "application/json" }
807
+ });
808
+ }
809
+ },
810
+ error(err) {
811
+ log5.error("Server error", { error: String(err) });
812
+ return new Response(JSON.stringify({ error: String(err) }), {
813
+ status: 500,
814
+ headers: { "Content-Type": "application/json" }
815
+ });
798
816
  }
799
817
  });
800
818
  console.log(`claude-memory-hub viewer running at http://localhost:${server.port}`);
@@ -805,177 +823,253 @@ var log5, PORT = 37888, HTML = `<!DOCTYPE html>
805
823
  <head>
806
824
  <meta charset="utf-8">
807
825
  <meta name="viewport" content="width=device-width, initial-scale=1">
808
- <title>claude-memory-hub viewer</title>
826
+ <title>claude-memory-hub</title>
809
827
  <style>
810
- :root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #c9d1d9; --muted: #8b949e; --accent: #58a6ff; --green: #3fb950; --yellow: #d29922; --red: #f85149; }
811
- * { margin: 0; padding: 0; box-sizing: border-box; }
812
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
813
- .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
814
- header { display: flex; align-items: center; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
815
- header h1 { font-size: 20px; font-weight: 600; }
816
- .stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
817
- .stat { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px 20px; min-width: 140px; }
818
- .stat .value { font-size: 28px; font-weight: 700; color: var(--accent); }
819
- .stat .label { font-size: 12px; color: var(--muted); text-transform: uppercase; }
820
- .search-bar { display: flex; gap: 8px; margin-bottom: 24px; }
821
- .search-bar input { flex: 1; background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; color: var(--text); font-size: 14px; outline: none; }
822
- .search-bar input:focus { border-color: var(--accent); }
823
- .search-bar button { background: var(--accent); color: #fff; border: none; border-radius: 6px; padding: 10px 20px; cursor: pointer; font-weight: 600; }
824
- .tabs { display: flex; gap: 4px; margin-bottom: 16px; }
825
- .tabs button { background: transparent; border: 1px solid var(--border); color: var(--muted); border-radius: 6px; padding: 6px 14px; cursor: pointer; font-size: 13px; }
826
- .tabs button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
827
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 8px; }
828
- .card .meta { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
829
- .card .type { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; }
830
- .type-summary { background: rgba(88,166,255,0.15); color: var(--accent); }
831
- .type-entity { background: rgba(63,185,80,0.15); color: var(--green); }
832
- .type-session { background: rgba(210,153,34,0.15); color: var(--yellow); }
833
- .card .content { font-size: 14px; white-space: pre-wrap; word-break: break-word; }
834
- .health { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
835
- .health .check { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
836
- .check-ok { background: rgba(63,185,80,0.15); color: var(--green); }
837
- .check-degraded { background: rgba(210,153,34,0.15); color: var(--yellow); }
838
- .check-error { background: rgba(248,81,73,0.15); color: var(--red); }
839
- .pagination { display: flex; justify-content: center; gap: 8px; margin-top: 16px; }
840
- .pagination button { background: var(--card); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: 6px 14px; cursor: pointer; }
841
- .pagination button:disabled { opacity: 0.4; cursor: default; }
842
- .empty { text-align: center; color: var(--muted); padding: 40px; }
843
- #results { min-height: 200px; }
828
+ :root {
829
+ --bg: #0a0a0f;
830
+ --surface: #12121a;
831
+ --card: #1a1a26;
832
+ --card-hover: #22222f;
833
+ --border: #2a2a3a;
834
+ --border-light: #3a3a4f;
835
+ --text: #e4e4ef;
836
+ --text-secondary: #9494a8;
837
+ --text-muted: #6a6a80;
838
+ --accent: #7c6bf5;
839
+ --accent-light: #9d8fff;
840
+ --accent-bg: rgba(124,107,245,0.1);
841
+ --green: #4ade80;
842
+ --green-bg: rgba(74,222,128,0.08);
843
+ --yellow: #facc15;
844
+ --yellow-bg: rgba(250,204,21,0.08);
845
+ --red: #f87171;
846
+ --red-bg: rgba(248,113,113,0.08);
847
+ --blue: #60a5fa;
848
+ --blue-bg: rgba(96,165,250,0.08);
849
+ --radius: 12px;
850
+ --radius-sm: 8px;
851
+ --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
852
+ }
853
+ * { margin: 0; padding: 0; box-sizing: border-box; }
854
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
855
+
856
+ /* Layout */
857
+ .app { max-width: 1100px; margin: 0 auto; padding: 32px 24px; }
858
+
859
+ /* Header */
860
+ .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }
861
+ .header-left { display: flex; align-items: center; gap: 14px; }
862
+ .logo { width: 36px; height: 36px; background: linear-gradient(135deg, var(--accent), #a78bfa); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
863
+ .header h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
864
+ .header h1 span { color: var(--text-muted); font-weight: 400; }
865
+ .health-badges { display: flex; gap: 6px; }
866
+ .badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; letter-spacing: 0.02em; }
867
+ .badge-ok { background: var(--green-bg); color: var(--green); }
868
+ .badge-degraded { background: var(--yellow-bg); color: var(--yellow); }
869
+ .badge-error { background: var(--red-bg); color: var(--red); }
870
+
871
+ /* Stats */
872
+ .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 28px; }
873
+ .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; transition: border-color var(--transition); }
874
+ .stat-card:hover { border-color: var(--border-light); }
875
+ .stat-value { font-size: 32px; font-weight: 700; background: linear-gradient(135deg, var(--accent-light), var(--blue)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.2; }
876
+ .stat-label { font-size: 11px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 4px; }
877
+
878
+ /* Search */
879
+ .search-wrap { position: relative; margin-bottom: 24px; }
880
+ .search-wrap input { width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px 14px 44px; color: var(--text); font-size: 14px; outline: none; transition: all var(--transition); }
881
+ .search-wrap input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
882
+ .search-wrap input::placeholder { color: var(--text-muted); }
883
+ .search-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; }
884
+
885
+ /* Tabs */
886
+ .tabs { display: flex; gap: 4px; margin-bottom: 20px; background: var(--surface); border-radius: var(--radius); padding: 4px; border: 1px solid var(--border); }
887
+ .tab { flex: 1; background: transparent; border: none; color: var(--text-muted); border-radius: var(--radius-sm); padding: 10px 16px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all var(--transition); }
888
+ .tab:hover { color: var(--text-secondary); background: var(--card); }
889
+ .tab.active { background: var(--accent); color: #fff; }
890
+ .tab .count { font-size: 11px; opacity: 0.7; margin-left: 4px; }
891
+
892
+ /* Cards */
893
+ #results { display: flex; flex-direction: column; gap: 8px; min-height: 200px; }
894
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; transition: all var(--transition); cursor: default; }
895
+ .card:hover { background: var(--card-hover); border-color: var(--border-light); }
896
+ .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
897
+ .card-type { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; }
898
+ .type-summary { background: var(--accent-bg); color: var(--accent-light); }
899
+ .type-entity, .type-file_read { background: var(--green-bg); color: var(--green); }
900
+ .type-file_modified, .type-file_created { background: var(--blue-bg); color: var(--blue); }
901
+ .type-error { background: var(--red-bg); color: var(--red); }
902
+ .type-decision { background: var(--yellow-bg); color: var(--yellow); }
903
+ .type-session { background: var(--yellow-bg); color: var(--yellow); }
904
+ .card-meta { font-size: 12px; color: var(--text-muted); display: flex; gap: 12px; flex-wrap: wrap; }
905
+ .card-content { font-size: 13.5px; color: var(--text-secondary); white-space: pre-wrap; word-break: break-word; line-height: 1.65; max-height: 200px; overflow: hidden; position: relative; }
906
+ .card-content.expanded { max-height: none; }
907
+ .card-expand { background: none; border: none; color: var(--accent); font-size: 12px; cursor: pointer; margin-top: 6px; padding: 0; }
908
+
909
+ /* Pagination */
910
+ .pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 24px; }
911
+ .pg-btn { background: var(--surface); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius-sm); padding: 8px 18px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all var(--transition); }
912
+ .pg-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
913
+ .pg-btn:disabled { opacity: 0.3; cursor: default; }
914
+ .pg-info { color: var(--text-muted); font-size: 13px; min-width: 80px; text-align: center; }
915
+
916
+ /* Empty state */
917
+ .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: var(--text-muted); }
918
+ .empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.3; }
919
+ .empty-text { font-size: 14px; }
920
+
921
+ /* Responsive */
922
+ @media (max-width: 768px) {
923
+ .stats { grid-template-columns: repeat(2, 1fr); }
924
+ .app { padding: 16px; }
925
+ .header { flex-direction: column; align-items: flex-start; gap: 12px; }
926
+ }
844
927
  </style>
845
928
  </head>
846
929
  <body>
847
- <div class="container">
848
- <header>
849
- <h1>claude-memory-hub</h1>
850
- <div id="health"></div>
851
- </header>
930
+ <div class="app">
931
+ <div class="header">
932
+ <div class="header-left">
933
+ <div class="logo">M</div>
934
+ <h1>memory-hub <span>viewer</span></h1>
935
+ </div>
936
+ <div class="health-badges" id="health"></div>
937
+ </div>
852
938
 
853
939
  <div class="stats" id="stats"></div>
854
940
 
855
- <div class="search-bar">
856
- <input id="searchInput" type="text" placeholder="Search memories..." />
857
- <button onclick="doSearch()">Search</button>
941
+ <div class="search-wrap">
942
+ <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
943
+ <input id="searchInput" type="text" placeholder="Search memories, files, decisions..." />
858
944
  </div>
859
945
 
860
- <div class="tabs">
861
- <button class="active" onclick="switchTab('summaries',this)">Summaries</button>
862
- <button onclick="switchTab('sessions',this)">Sessions</button>
863
- <button onclick="switchTab('entities',this)">Entities</button>
946
+ <div class="tabs" id="tabsContainer">
947
+ <button class="tab active" data-tab="summaries">Summaries <span class="count" id="cnt-summaries"></span></button>
948
+ <button class="tab" data-tab="sessions">Sessions <span class="count" id="cnt-sessions"></span></button>
949
+ <button class="tab" data-tab="entities">Entities <span class="count" id="cnt-entities"></span></button>
864
950
  </div>
865
951
 
866
952
  <div id="results"></div>
867
953
 
868
954
  <div class="pagination">
869
- <button id="prevBtn" onclick="paginate(-1)" disabled>&larr; Previous</button>
870
- <span id="pageInfo" style="color:var(--muted);font-size:13px;padding:6px;"></span>
871
- <button id="nextBtn" onclick="paginate(1)">Next &rarr;</button>
955
+ <button class="pg-btn" id="prevBtn" disabled>Previous</button>
956
+ <span class="pg-info" id="pageInfo"></span>
957
+ <button class="pg-btn" id="nextBtn">Next</button>
872
958
  </div>
873
959
  </div>
874
960
 
875
961
  <script>
876
- let currentTab = 'summaries';
877
- let currentOffset = 0;
878
- const PAGE_SIZE = 20;
962
+ (function(){
963
+ var currentTab = "summaries";
964
+ var currentOffset = 0;
965
+ var PAGE_SIZE = 15;
879
966
 
880
- async function api(path) {
881
- const res = await fetch(path);
882
- return res.json();
883
- }
884
-
885
- async function init() {
886
- const [stats, health] = await Promise.all([api('/api/stats'), api('/api/health')]);
887
-
888
- document.getElementById('stats').innerHTML =
889
- ['sessions','entities','summaries','notes'].map(k =>
890
- '<div class="stat"><div class="value">'+stats[k]+'</div><div class="label">'+k+'</div></div>'
891
- ).join('');
892
-
893
- document.getElementById('health').innerHTML = health.checks.map(c => {
894
- const cls = 'check-' + c.status;
895
- return '<span class="check '+cls+'">'+c.component+': '+c.status+'</span>';
896
- }).join('');
967
+ function api(path) {
968
+ return fetch(path).then(function(r){ return r.ok ? r.json() : []; }).catch(function(){ return []; });
969
+ }
897
970
 
898
- loadTab();
899
- }
971
+ function fmtDate(epoch) {
972
+ if (!epoch) return "N/A";
973
+ var d = new Date(epoch);
974
+ return d.toLocaleDateString("en-US", {month:"short",day:"numeric"}) + " " + d.toLocaleTimeString("en-US", {hour:"2-digit",minute:"2-digit"});
975
+ }
900
976
 
901
- function switchTab(tab, btn) {
902
- currentTab = tab;
903
- currentOffset = 0;
904
- document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
905
- btn.classList.add('active');
906
- loadTab();
907
- }
977
+ function esc(s) { var d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; }
908
978
 
909
- async function loadTab() {
910
- const results = document.getElementById('results');
911
- results.innerHTML = '<div class="empty">Loading...</div>';
979
+ function updatePagination(count) {
980
+ document.getElementById("prevBtn").disabled = currentOffset === 0;
981
+ document.getElementById("nextBtn").disabled = count < PAGE_SIZE;
982
+ document.getElementById("pageInfo").textContent = "Page " + (Math.floor(currentOffset / PAGE_SIZE) + 1);
983
+ }
912
984
 
913
- const data = await api('/api/' + currentTab + '?limit=' + PAGE_SIZE + '&offset=' + currentOffset);
985
+ function renderCards(data) {
986
+ if (currentTab === "summaries") {
987
+ return data.map(function(s) {
988
+ var preview = (s.summary || "").slice(0, 400);
989
+ return '<div class="card"><div class="card-header"><span class="card-type type-summary">summary</span><div class="card-meta"><span>' + fmtDate(s.created_at) + '</span><span>' + esc(s.project) + '</span><span>' + esc(s.session_id || "").slice(0,8) + '</span></div></div><div class="card-content">' + esc(preview) + '</div></div>';
990
+ }).join("");
991
+ }
992
+ if (currentTab === "sessions") {
993
+ return data.map(function(s) {
994
+ var cls = s.status === "completed" ? "type-summary" : s.status === "failed" ? "type-error" : "type-session";
995
+ return '<div class="card"><div class="card-header"><span class="card-type ' + cls + '">' + esc(s.status) + '</span><div class="card-meta"><span>' + fmtDate(s.started_at) + '</span><span>' + esc(s.project) + '</span><span>' + esc(s.id || "").slice(0,12) + '</span></div></div><div class="card-content">' + esc(s.user_prompt || "(no prompt)") + '</div></div>';
996
+ }).join("");
997
+ }
998
+ return data.map(function(e) {
999
+ return '<div class="card"><div class="card-header"><span class="card-type type-' + (e.entity_type || "entity") + '">' + esc(e.entity_type) + '</span><div class="card-meta"><span>' + fmtDate(e.created_at) + '</span><span>' + esc(e.tool_name) + '</span><span>imp: ' + e.importance + '</span></div></div><div class="card-content">' + esc(e.entity_value) + (e.context ? "\\n" + esc(e.context) : "") + '</div></div>';
1000
+ }).join("");
1001
+ }
914
1002
 
915
- if (data.length === 0) {
916
- results.innerHTML = '<div class="empty">No data yet.</div>';
917
- updatePagination(0);
918
- return;
1003
+ function loadTab() {
1004
+ var el = document.getElementById("results");
1005
+ el.innerHTML = '<div class="empty"><div class="empty-text">Loading...</div></div>';
1006
+ api("/api/" + currentTab + "?limit=" + PAGE_SIZE + "&offset=" + currentOffset).then(function(data) {
1007
+ if (!data || data.length === 0 || data.error) {
1008
+ el.innerHTML = '<div class="empty"><div class="empty-text">' + (data && data.error ? esc(data.error) : "No data yet.") + '</div></div>';
1009
+ updatePagination(0);
1010
+ return;
1011
+ }
1012
+ el.innerHTML = renderCards(data);
1013
+ updatePagination(data.length);
1014
+ el.querySelectorAll(".card-content").forEach(function(c){ c.addEventListener("click", function(){ this.classList.toggle("expanded"); }); });
1015
+ });
919
1016
  }
920
1017
 
921
- if (currentTab === 'summaries') {
922
- results.innerHTML = data.map(s =>
923
- '<div class="card"><div class="meta"><span class="type type-summary">summary</span> ' +
924
- fmtDate(s.created_at) + ' | ' + esc(s.project) + ' | session: ' + esc(s.session_id).slice(0,8) + '...</div>' +
925
- '<div class="content">' + esc(s.summary) + '</div></div>'
926
- ).join('');
927
- } else if (currentTab === 'sessions') {
928
- results.innerHTML = data.map(s =>
929
- '<div class="card"><div class="meta"><span class="type type-session">session</span> ' +
930
- fmtDate(s.started_at) + ' | ' + esc(s.project) + ' | ' + esc(s.status) + '</div>' +
931
- '<div class="content">' + esc(s.user_prompt || '(no prompt)') + '</div></div>'
932
- ).join('');
933
- } else {
934
- results.innerHTML = data.map(e =>
935
- '<div class="card"><div class="meta"><span class="type type-entity">' + esc(e.entity_type) + '</span> ' +
936
- fmtDate(e.created_at) + ' | ' + esc(e.tool_name) + ' | importance: ' + e.importance + '</div>' +
937
- '<div class="content">' + esc(e.entity_value) + (e.context ? '\\n' + esc(e.context) : '') + '</div></div>'
938
- ).join('');
1018
+ function doSearch() {
1019
+ var q = document.getElementById("searchInput").value.trim();
1020
+ if (!q) { currentOffset = 0; loadTab(); return; }
1021
+ var el = document.getElementById("results");
1022
+ el.innerHTML = '<div class="empty"><div class="empty-text">Searching...</div></div>';
1023
+ api("/api/search?q=" + encodeURIComponent(q) + "&limit=" + PAGE_SIZE).then(function(data) {
1024
+ if (!data || data.length === 0) {
1025
+ el.innerHTML = '<div class="empty"><div class="empty-text">No results for "' + esc(q) + '"</div></div>';
1026
+ return;
1027
+ }
1028
+ el.innerHTML = data.map(function(r) {
1029
+ return '<div class="card"><div class="card-header"><span class="card-type type-' + r.type + '">' + esc(r.type) + "#" + r.id + '</span><div class="card-meta"><span>' + fmtDate(r.created_at) + '</span><span>' + esc(r.project) + '</span><span>score: ' + (r.score || 0).toFixed(2) + '</span></div></div><div class="card-content">' + esc(r.title) + '</div></div>';
1030
+ }).join("");
1031
+ });
939
1032
  }
940
- updatePagination(data.length);
941
- }
942
1033
 
943
- async function doSearch() {
944
- const q = document.getElementById('searchInput').value.trim();
945
- if (!q) { loadTab(); return; }
1034
+ // Tab click handlers
1035
+ document.querySelectorAll("[data-tab]").forEach(function(btn) {
1036
+ btn.addEventListener("click", function() {
1037
+ currentTab = this.getAttribute("data-tab");
1038
+ currentOffset = 0;
1039
+ document.querySelectorAll(".tab").forEach(function(b){ b.classList.remove("active"); });
1040
+ this.classList.add("active");
1041
+ loadTab();
1042
+ });
1043
+ });
946
1044
 
947
- const results = document.getElementById('results');
948
- results.innerHTML = '<div class="empty">Searching...</div>';
1045
+ // Pagination
1046
+ document.getElementById("prevBtn").addEventListener("click", function(){ currentOffset = Math.max(0, currentOffset - PAGE_SIZE); loadTab(); });
1047
+ document.getElementById("nextBtn").addEventListener("click", function(){ currentOffset += PAGE_SIZE; loadTab(); });
949
1048
 
950
- const data = await api('/api/search?q=' + encodeURIComponent(q) + '&limit=' + PAGE_SIZE);
951
- if (data.length === 0) {
952
- results.innerHTML = '<div class="empty">No results for "' + esc(q) + '"</div>';
953
- return;
954
- }
955
- results.innerHTML = data.map(r =>
956
- '<div class="card"><div class="meta"><span class="type type-' + r.type + '">' + esc(r.type) + '#' + r.id + '</span> ' +
957
- fmtDate(r.created_at) + ' | ' + esc(r.project) + ' | score: ' + (r.score||0).toFixed(2) + '</div>' +
958
- '<div class="content">' + esc(r.title) + '</div></div>'
959
- ).join('');
960
- }
1049
+ // Search
1050
+ document.getElementById("searchInput").addEventListener("keydown", function(e){ if (e.key === "Enter") doSearch(); });
961
1051
 
962
- function paginate(dir) {
963
- currentOffset = Math.max(0, currentOffset + dir * PAGE_SIZE);
964
- loadTab();
965
- }
1052
+ // Init
1053
+ Promise.all([api("/api/stats"), api("/api/health")]).then(function(res) {
1054
+ var stats = res[0], health = res[1];
966
1055
 
967
- function updatePagination(count) {
968
- document.getElementById('prevBtn').disabled = currentOffset === 0;
969
- document.getElementById('nextBtn').disabled = count < PAGE_SIZE;
970
- const page = Math.floor(currentOffset / PAGE_SIZE) + 1;
971
- document.getElementById('pageInfo').textContent = 'Page ' + page;
972
- }
1056
+ document.getElementById("stats").innerHTML = ["sessions","entities","summaries","notes"].map(function(k) {
1057
+ return '<div class="stat-card"><div class="stat-value">' + (stats[k] || 0) + '</div><div class="stat-label">' + k + '</div></div>';
1058
+ }).join("");
973
1059
 
974
- function fmtDate(epoch) { return epoch ? new Date(epoch).toLocaleString() : 'N/A'; }
975
- function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
1060
+ var cntS = document.getElementById("cnt-summaries"); if(cntS) cntS.textContent = stats.summaries || "";
1061
+ var cntSe = document.getElementById("cnt-sessions"); if(cntSe) cntSe.textContent = stats.sessions || "";
1062
+ var cntE = document.getElementById("cnt-entities"); if(cntE) cntE.textContent = stats.entities || "";
976
1063
 
977
- document.getElementById('searchInput').addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
978
- init();
1064
+ if (health && health.checks) {
1065
+ document.getElementById("health").innerHTML = health.checks.map(function(c) {
1066
+ return '<span class="badge badge-' + c.status + '">' + c.component + '</span>';
1067
+ }).join("");
1068
+ }
1069
+
1070
+ loadTab();
1071
+ });
1072
+ })();
979
1073
  </script>
980
1074
  </body>
981
1075
  </html>`;
@@ -1489,8 +1583,8 @@ switch (command) {
1489
1583
  Promise.resolve().then(() => (init_viewer(), exports_viewer)).then((m) => m.startViewer());
1490
1584
  break;
1491
1585
  case "health": {
1492
- const { runHealthCheck: runHealthCheck2, formatHealthReport: formatHealthReport3 } = (init_monitor(), __toCommonJS(exports_monitor));
1493
- console.log(formatHealthReport3(runHealthCheck2()));
1586
+ const { runHealthCheck: runHealthCheck2, formatHealthReport: formatHealthReport2 } = (init_monitor(), __toCommonJS(exports_monitor));
1587
+ console.log(formatHealthReport2(runHealthCheck2()));
1494
1588
  break;
1495
1589
  }
1496
1590
  case "reindex": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-hub",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Persistent memory system for Claude Code. Zero API key. Zero Python. 5 hooks + MCP server + SQLite FTS5.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",