claude-memory-layer 1.0.48 → 1.0.50

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.
@@ -32,6 +32,88 @@ async function loadVectorHealthData() {
32
32
  .catch(() => null);
33
33
  }
34
34
 
35
+ async function loadProjectDetailData() {
36
+ if (!state.currentProject) {
37
+ state.projectDetail = null;
38
+ state.projectDetailProject = null;
39
+ return;
40
+ }
41
+ const projectHash = state.currentProject;
42
+ state.projectDetail = await fetch(apiUrl(`${API_BASE}/projects/${encodeURIComponent(projectHash)}/detail`))
43
+ .then(r => r.ok !== false ? r.json() : null)
44
+ .catch(() => null);
45
+ state.projectDetailProject = projectHash;
46
+ }
47
+
48
+ function topEntries(record, limit = 3) {
49
+ return Object.entries(record || {})
50
+ .sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0))
51
+ .slice(0, limit);
52
+ }
53
+
54
+ function updateProjectDetailUI() {
55
+ const el = document.getElementById('project-detail-card');
56
+ if (!el) return;
57
+
58
+ if (!state.currentProject) {
59
+ el.hidden = true;
60
+ el.innerHTML = '';
61
+ return;
62
+ }
63
+
64
+ el.hidden = false;
65
+ const detail = state.projectDetailProject === state.currentProject ? state.projectDetail : null;
66
+ if (!detail) {
67
+ el.innerHTML = `
68
+ <div class="card-header">
69
+ <div class="card-title"><i class="ri-folder-chart-line"></i><span>Project Detail</span></div>
70
+ </div>
71
+ <div class="disclosure-empty">Project detail is unavailable for the selected scope.</div>
72
+ `;
73
+ return;
74
+ }
75
+
76
+ const project = detail.project || {};
77
+ const storage = detail.storage || {};
78
+ const sessions = detail.sessions || {};
79
+ const retrieval = detail.retrieval || {};
80
+ const outbox = detail.outbox || {};
81
+ const eventTypeChips = topEntries(detail.eventTypes, 4)
82
+ .map(([label, count]) => `<span class="event-type-badge ${eventTypeBadgeClass(label)}">${escapeHtml(label)} · ${formatNumber(count)}</span>`)
83
+ .join('') || '<span class="session-muted">No event types</span>';
84
+ const sourceChips = topEntries(detail.sources, 3)
85
+ .map(([label, count]) => `<span class="disclosure-scope-pill">${escapeHtml(label)} · ${formatNumber(count)}</span>`)
86
+ .join('') || '<span class="session-muted">No source metadata</span>';
87
+ const selectionRate = `${((retrieval.selectionRate || 0) * 100).toFixed(1)}% selection`;
88
+
89
+ el.innerHTML = `
90
+ <div class="card-header" style="align-items:flex-start;">
91
+ <div>
92
+ <div class="card-title"><i class="ri-folder-chart-line"></i><span>Project Detail</span></div>
93
+ <div class="session-muted">${escapeHtml(project.projectName || state.currentProject)} · ${escapeHtml(project.registered ? 'registered' : 'unregistered')}</div>
94
+ </div>
95
+ <span class="disclosure-scope-pill">${escapeHtml(project.hash || state.currentProject)}</span>
96
+ </div>
97
+ <div class="stats-grid kpi-grid" style="margin-top:0; margin-bottom:14px;">
98
+ <div class="stat-card kpi-card"><div class="stat-value">${formatNumber(storage.eventCount || 0)} events</div><div class="stat-label"><i class="ri-file-list-3-line"></i> total</div></div>
99
+ <div class="stat-card kpi-card"><div class="stat-value">${formatNumber(sessions.total || 0)} sessions</div><div class="stat-label"><i class="ri-discuss-line"></i> active</div></div>
100
+ <div class="stat-card kpi-card"><div class="stat-value">${formatNumber(storage.vectorCount || 0)} vectors</div><div class="stat-label"><i class="ri-node-tree"></i> indexed</div></div>
101
+ <div class="stat-card kpi-card"><div class="stat-value">${formatNumber(retrieval.totalQueries || 0)}</div><div class="stat-label"><i class="ri-search-eye-line"></i> ${selectionRate}</div></div>
102
+ </div>
103
+ <div class="cfg-grid">
104
+ <div class="cfg-section">
105
+ <div class="cfg-section-title"><i class="ri-price-tag-3-line"></i>Event types</div>
106
+ <div style="display:flex; gap:6px; flex-wrap:wrap;">${eventTypeChips}</div>
107
+ </div>
108
+ <div class="cfg-section">
109
+ <div class="cfg-section-title"><i class="ri-router-line"></i>Sources & outbox</div>
110
+ <div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px;">${sourceChips}</div>
111
+ <div class="session-muted">${formatNumber(outbox.pending || 0)} pending · ${formatNumber(outbox.processing || 0)} processing · ${formatNumber(outbox.failed || 0)} failed · ${formatNumber(outbox.stuckProcessing || 0)} stuck</div>
112
+ </div>
113
+ </div>
114
+ `;
115
+ }
116
+
35
117
  async function refreshData() {
36
118
  const btn = document.getElementById('refresh-btn');
37
119
  if(btn) btn.classList.add('loading');
@@ -41,7 +123,7 @@ async function refreshData() {
41
123
  const kpiWindowAtStart = state.kpiWindow;
42
124
 
43
125
  try {
44
- const [stats, shared, mostAccessed, helpfulness, memoryUsefulness, retrievalTraces, retrievalReviewQueue, operationsStats, perspectiveStats, adherenceSummary, vectorHealth] = await Promise.all([
126
+ const [stats, shared, mostAccessed, helpfulness, memoryUsefulness, retrievalTraces, retrievalReviewQueue, operationsStats, perspectiveStats, adherenceSummary, vectorHealth, projectDetail] = await Promise.all([
45
127
  fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
46
128
  fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
47
129
  fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
@@ -52,7 +134,10 @@ async function refreshData() {
52
134
  fetch(apiUrl(`${API_BASE}/stats/operations`, { windowDays: operationStatsWindowDays() })).then(r => r.ok ? r.json() : null).catch(() => null),
53
135
  fetch(apiUrl(`${API_BASE}/stats/perspective`, { windowDays: operationStatsWindowDays() })).then(r => r.ok ? r.json() : null).catch(() => null),
54
136
  fetchAdherenceSummary().catch(() => null),
55
- fetch(apiUrl(`${API_BASE}/health`)).then(r => r.ok ? r.json() : null).catch(() => null)
137
+ fetch(apiUrl(`${API_BASE}/health`)).then(r => r.ok ? r.json() : null).catch(() => null),
138
+ state.currentProject
139
+ ? fetch(apiUrl(`${API_BASE}/projects/${encodeURIComponent(state.currentProject)}/detail`)).then(r => r.ok ? r.json() : null).catch(() => null)
140
+ : Promise.resolve(null)
56
141
  ]);
57
142
 
58
143
  if (
@@ -74,11 +159,14 @@ async function refreshData() {
74
159
  state.perspectiveStats = perspectiveStats;
75
160
  state.adherenceSummary = adherenceSummary;
76
161
  state.vectorHealth = vectorHealth;
162
+ state.projectDetail = projectDetail;
163
+ state.projectDetailProject = state.currentProject || null;
77
164
 
78
165
  await loadKpiData();
79
166
  if (refreshRequestId !== state.refreshRequestId) return;
80
167
 
81
168
  updateStatsUI();
169
+ updateProjectDetailUI();
82
170
  updateSharedUI();
83
171
  updateMemoryUsageUI();
84
172
  updateKpiCardsUI();
@@ -120,6 +208,7 @@ async function loadLevelEvents(level, sort) {
120
208
 
121
209
  function updateStatsUI() {
122
210
  if (!state.stats) return;
211
+ if (typeof updateProjectScopeUI === 'function') updateProjectScopeUI();
123
212
 
124
213
  const eventCount = state.stats.storage?.eventCount || 0;
125
214
  const sessionCount = state.stats.sessions?.total || 0;
@@ -319,7 +408,7 @@ function updateEventsListUI() {
319
408
 
320
409
  const time = new Date(event.timestamp).toLocaleString();
321
410
  const eventType = event.eventType || event.type || 'unknown';
322
- const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
411
+ const typeClass = eventTypeBadgeClass(eventType);
323
412
  const preview = event.preview || event.content || '';
324
413
  const accessBadge = event.accessCount > 0
325
414
  ? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
@@ -332,7 +421,7 @@ function updateEventsListUI() {
332
421
  el.innerHTML = `
333
422
  <div class="event-header">
334
423
  <div style="display:flex; gap:8px; align-items:center;">
335
- <span class="event-type-badge ${typeClass}">${eventType}</span>
424
+ <span class="event-type-badge ${typeClass}">${escapeHtml(eventType)}</span>
336
425
  ${adherenceBadge}
337
426
  </div>
338
427
  <div style="display:flex; gap:8px; align-items:center;">
@@ -367,11 +456,12 @@ function updateTopAccessedEventsUI() {
367
456
  const preview = (m.summary || m.preview || m.content || '').replace(/<[^>]*>/g, '').slice(0, 80);
368
457
  const lastAccessed = m.lastAccessedAt ? new Date(m.lastAccessedAt).toLocaleDateString() : (m.lastAccessed ? new Date(m.lastAccessed).toLocaleDateString() : '-');
369
458
  const id = m.id || m.memoryId || '';
459
+ const typeClass = eventTypeBadgeClass(type);
370
460
  return `
371
- <div class="shared-item" style="cursor:pointer;" ${id ? `onclick="openDetailModal('${id}')"` : ''}>
461
+ <div class="shared-item" style="cursor:pointer;" ${id ? `onclick="openDetailModal(${jsAttrArg(id)})"` : ''}>
372
462
  <div class="shared-info" style="flex-direction:column; align-items:flex-start; gap:2px;">
373
463
  <div style="display:flex; gap:6px; align-items:center;">
374
- <span class="event-type-badge type-${type.replace('_','-')}">${type}</span>
464
+ <span class="event-type-badge ${typeClass}">${escapeHtml(type)}</span>
375
465
  <span style="font-size:10px; color:var(--text-muted);">last: ${lastAccessed}</span>
376
466
  </div>
377
467
  <span style="font-size:12px; color:var(--text-secondary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:200px;" title="${escapeHtml(preview)}">${escapeHtml(preview) || '(no preview)'}</span>
@@ -1233,9 +1323,9 @@ function updateRetrievalTraceUI() {
1233
1323
  const selectedIdsHtml = selectedDetails.length > 0
1234
1324
  ? selectedDetails.map((d) => {
1235
1325
  const breakdown = `score=${Number(d.score || 0).toFixed(3)} · s=${Number(d.semanticScore || 0).toFixed(3)} · l=${Number(d.lexicalScore || 0).toFixed(3)} · r=${Number(d.recencyScore || 0).toFixed(3)}`;
1236
- return `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${d.eventId}')" title="${escapeHtml(breakdown)}">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`;
1326
+ return `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal(${jsAttrArg(d.eventId || '')})" title="${escapeHtml(breakdown)}">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`;
1237
1327
  }).join(' ')
1238
- : ((t.selectedEventIds || []).slice(0, 2).map((id) => `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${id}')">${escapeHtml((id || '').slice(0, 8))}...</span>`).join(' ') || '-');
1328
+ : ((t.selectedEventIds || []).slice(0, 2).map((id) => `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal(${jsAttrArg(id || '')})">${escapeHtml((id || '').slice(0, 8))}...</span>`).join(' ') || '-');
1239
1329
 
1240
1330
  const scoreBreakdownHtml = selectedDetails.length > 0
1241
1331
  ? selectedDetails.map((d) => `<div style="font-size:10px; color:var(--text-muted);">${escapeHtml((d.eventId || '').slice(0, 8))}... → score ${Number(d.score || 0).toFixed(3)} (s ${Number(d.semanticScore || 0).toFixed(3)}, l ${Number(d.lexicalScore || 0).toFixed(3)}, r ${Number(d.recencyScore || 0).toFixed(3)})</div>`).join('')
@@ -1247,7 +1337,7 @@ function updateRetrievalTraceUI() {
1247
1337
  <span style="font-size:12px; color:var(--text-secondary);"><strong>Trace:</strong> ${escapeHtml((t.traceId || '').slice(0, 12)) || '-'}</span>
1248
1338
  <span style="font-size:11px; color:var(--text-muted);">${ts} · strategy=${escapeHtml(t.strategy || 'auto')} · conf=${escapeHtml(confidence)} ${rewriteBadge}</span>
1249
1339
  <span style="font-size:11px; color:var(--text-muted);">selected IDs: ${selectedIdsHtml}</span>
1250
- <span style="font-size:11px; color:var(--text-muted);">candidates: ${candidateDetails.map((d) => `<span class=\"event-type-badge\" style=\"cursor:pointer;\" onclick=\"openDetailModal('${d.eventId}')\">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`).join(' ') || '-'}</span>
1340
+ <span style="font-size:11px; color:var(--text-muted);">candidates: ${candidateDetails.map((d) => `<span class=\"event-type-badge\" style=\"cursor:pointer;\" onclick=\"openDetailModal(${jsAttrArg(d.eventId || '')})\">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`).join(' ') || '-'}</span>
1251
1341
  ${scoreBreakdownHtml}
1252
1342
  </div>
1253
1343
  <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:68px;">
@@ -17,6 +17,8 @@ const state = {
17
17
  operationsStats: null,
18
18
  perspectiveStats: null,
19
19
  vectorHealth: null,
20
+ projectDetail: null,
21
+ projectDetailProject: null,
20
22
  vectorHealthRecovery: null,
21
23
  vectorHealthRecoveryProject: null,
22
24
  isVectorRecoveryRunning: false,
@@ -26,6 +28,19 @@ const state = {
26
28
  userPromptItems: [],
27
29
  userPromptPage: 1,
28
30
  userPromptPageSize: 30,
31
+ sessionInspectorSessions: [],
32
+ sessionInspectorPage: 1,
33
+ sessionInspectorPageSize: 50,
34
+ selectedSession: null,
35
+ selectedSessionTurns: [],
36
+ selectedSessionEvents: [],
37
+ sessionSnapshotTab: 'overview',
38
+ sessionJumpEventId: null,
39
+ pendingSessionJump: null,
40
+ sessionInspectorRequestId: 0,
41
+ sessionDetailRequestId: 0,
42
+ isSessionInspectorLoading: false,
43
+ isSessionDetailLoading: false,
29
44
  currentLevel: 'L0',
30
45
  currentSort: 'recent',
31
46
  currentView: 'overview',
@@ -46,15 +61,27 @@ const state = {
46
61
  chatCurrentTab: 'chat',
47
62
  disclosureResults: [],
48
63
  disclosureMeta: null,
64
+ disclosureQuery: '',
49
65
  disclosureSelectedId: null,
50
66
  disclosureExpansion: null,
51
67
  disclosureSource: null,
52
- isDisclosureLoading: false
68
+ isDisclosureLoading: false,
69
+ playgroundLastRun: null,
70
+ isPlaygroundLoading: false
53
71
  };
54
72
 
55
73
  // Utils
56
74
  const formatNumber = (num) => new Intl.NumberFormat().format(num || 0);
57
75
 
76
+ function eventTypeBadgeClass(type) {
77
+ const token = String(type || 'unknown')
78
+ .toLowerCase()
79
+ .replace(/_/g, '-')
80
+ .replace(/[^a-z0-9-]+/g, '-')
81
+ .replace(/^-+|-+$/g, '') || 'unknown';
82
+ return `type-${token}`;
83
+ }
84
+
58
85
  // Colors for Chart
59
86
  const CHART_COLORS = {
60
87
  L0: '#7B61FF',