claude-memory-layer 1.0.18 → 1.0.19

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 (35) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +372 -74
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/hooks/post-tool-use.js +6 -0
  5. package/dist/hooks/post-tool-use.js.map +2 -2
  6. package/dist/hooks/session-end.js +6 -0
  7. package/dist/hooks/session-end.js.map +2 -2
  8. package/dist/hooks/session-start.js +6 -0
  9. package/dist/hooks/session-start.js.map +2 -2
  10. package/dist/hooks/stop.js +6 -0
  11. package/dist/hooks/stop.js.map +2 -2
  12. package/dist/hooks/user-prompt-submit.js +245 -31
  13. package/dist/hooks/user-prompt-submit.js.map +3 -3
  14. package/dist/server/api/index.js +329 -31
  15. package/dist/server/api/index.js.map +3 -3
  16. package/dist/server/index.js +336 -38
  17. package/dist/server/index.js.map +3 -3
  18. package/dist/services/memory-service.js +6 -0
  19. package/dist/services/memory-service.js.map +2 -2
  20. package/dist/ui/app.js +236 -4
  21. package/dist/ui/index.html +51 -0
  22. package/dist/ui/style.css +34 -0
  23. package/memory/_index.md +3 -0
  24. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  25. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  26. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  27. package/package.json +3 -2
  28. package/scripts/delete-unknown-projects.js +154 -0
  29. package/src/hooks/user-prompt-submit.ts +225 -29
  30. package/src/server/api/events.ts +1 -0
  31. package/src/server/api/stats.ts +346 -0
  32. package/src/services/memory-service.ts +7 -0
  33. package/src/ui/app.js +236 -4
  34. package/src/ui/index.html +51 -0
  35. package/src/ui/style.css +34 -0
package/dist/ui/app.js CHANGED
@@ -12,6 +12,8 @@ const state = {
12
12
  mostAccessed: null,
13
13
  helpfulness: null,
14
14
  retrievalTraces: null,
15
+ adherenceSummary: null,
16
+ adherenceWindow: '24h',
15
17
  currentLevel: 'L0',
16
18
  currentSort: 'recent',
17
19
  currentView: 'overview',
@@ -20,6 +22,9 @@ const state = {
20
22
  events: [],
21
23
  isLoading: false,
22
24
  chartInstance: null,
25
+ kpiChartInstance: null,
26
+ kpiWindow: '7d',
27
+ kpi: null,
23
28
  chatMessages: [],
24
29
  isChatOpen: false,
25
30
  isChatStreaming: false,
@@ -102,13 +107,42 @@ function setupEventListeners() {
102
107
  });
103
108
 
104
109
  // Sort buttons
105
- document.querySelectorAll('.sort-btn').forEach(btn => {
110
+ document.querySelectorAll('.sort-btn[data-sort]').forEach(btn => {
106
111
  btn.addEventListener('click', (e) => {
107
112
  const sort = e.currentTarget.dataset.sort;
108
113
  if (sort) selectSort(sort);
109
114
  });
110
115
  });
111
116
 
117
+ // Adherence window controls
118
+ document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(btn => {
119
+ btn.addEventListener('click', async (e) => {
120
+ const window = e.currentTarget.dataset.adhWindow;
121
+ if (!window || state.adherenceWindow === window) return;
122
+ state.adherenceWindow = window;
123
+ document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(b => {
124
+ b.classList.toggle('active', b.dataset.adhWindow === window);
125
+ });
126
+ state.adherenceSummary = await fetchAdherenceSummary().catch(() => null);
127
+ updateAdherenceSummaryUI();
128
+ });
129
+ });
130
+
131
+ // KPI window controls
132
+ document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(btn => {
133
+ btn.addEventListener('click', async (e) => {
134
+ const window = e.currentTarget.dataset.kpiWindow;
135
+ if (!window || state.kpiWindow === window) return;
136
+ state.kpiWindow = window;
137
+ document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(b => {
138
+ b.classList.toggle('active', b.dataset.kpiWindow === window);
139
+ });
140
+ await loadKpiData();
141
+ updateKpiCardsUI();
142
+ renderKpiTrendChart();
143
+ });
144
+ });
145
+
112
146
  // Search
113
147
  const searchInput = document.getElementById('search-input');
114
148
  if (searchInput) {
@@ -125,6 +159,10 @@ function setupEventListeners() {
125
159
  state.chartInstance.destroy();
126
160
  state.chartInstance = null;
127
161
  }
162
+ if (state.kpiChartInstance) {
163
+ state.kpiChartInstance.destroy();
164
+ state.kpiChartInstance = null;
165
+ }
128
166
  await initActivityChart();
129
167
  // Reload current view if not overview
130
168
  if (state.currentView !== 'overview') {
@@ -231,17 +269,24 @@ function setupEventListeners() {
231
269
 
232
270
  // --- Data Fetching ---
233
271
 
272
+ async function loadKpiData() {
273
+ state.kpi = await fetch(apiUrl(`${API_BASE}/stats/kpi`, { window: state.kpiWindow }))
274
+ .then(r => r.json())
275
+ .catch(() => null);
276
+ }
277
+
234
278
  async function refreshData() {
235
279
  const btn = document.getElementById('refresh-btn');
236
280
  if(btn) btn.classList.add('loading');
237
281
 
238
282
  try {
239
- const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
283
+ const [stats, shared, mostAccessed, helpfulness, retrievalTraces, adherenceSummary] = await Promise.all([
240
284
  fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
241
285
  fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
242
286
  fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
243
287
  fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
244
- fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
288
+ fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null),
289
+ fetchAdherenceSummary().catch(() => null)
245
290
  ]);
246
291
 
247
292
  state.stats = stats;
@@ -249,10 +294,15 @@ async function refreshData() {
249
294
  state.mostAccessed = mostAccessed;
250
295
  state.helpfulness = helpfulness;
251
296
  state.retrievalTraces = retrievalTraces;
297
+ state.adherenceSummary = adherenceSummary;
298
+
299
+ await loadKpiData();
252
300
 
253
301
  updateStatsUI();
254
302
  updateSharedUI();
255
303
  updateMemoryUsageUI();
304
+ updateKpiCardsUI();
305
+ renderKpiTrendChart();
256
306
  await loadLevelEvents(state.currentLevel);
257
307
 
258
308
  checkEndlessStatus();
@@ -327,6 +377,98 @@ function updateSharedUI() {
327
377
  document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
328
378
  }
329
379
 
380
+ function percentText(v) {
381
+ return `${((v || 0) * 100).toFixed(1)}%`;
382
+ }
383
+
384
+ function renderDelta(id, value, lowerIsBetter = false, asPercent = true) {
385
+ const el = document.getElementById(id);
386
+ if (!el) return;
387
+ const v = Number(value || 0);
388
+ const sign = v > 0 ? '+' : '';
389
+ const text = asPercent ? `${sign}${(v * 100).toFixed(1)}%` : `${sign}${v.toFixed(2)}`;
390
+
391
+ let positive = v > 0;
392
+ if (lowerIsBetter) positive = v < 0;
393
+ const cls = v === 0 ? 'neutral' : (positive ? 'good' : 'bad');
394
+ const arrow = v === 0 ? '→' : (positive ? '▲' : '▼');
395
+
396
+ el.className = `kpi-delta ${cls}`;
397
+ el.textContent = `${arrow} ${text} vs prev`;
398
+ }
399
+
400
+ function updateKpiCardsUI() {
401
+ const m = state.kpi?.metrics;
402
+ const d = state.kpi?.deltas;
403
+ if (!m) return;
404
+ const set = (id, value) => {
405
+ const el = document.getElementById(id);
406
+ if (el) el.textContent = value;
407
+ };
408
+ set('kpi-useful-recall', percentText(m.usefulRecallRate));
409
+ set('kpi-completion-turns', Number(m.avgCompletionTurns || 0).toFixed(2));
410
+ set('kpi-rework-rate', percentText(m.reworkRate));
411
+ set('kpi-failure-rate', percentText(m.postChangeFailureRate));
412
+
413
+ if (d) {
414
+ renderDelta('kpi-useful-recall-delta', d.usefulRecallRate, false, true);
415
+ renderDelta('kpi-completion-turns-delta', d.avgCompletionTurns, true, false);
416
+ renderDelta('kpi-rework-rate-delta', d.reworkRate, true, true);
417
+ renderDelta('kpi-failure-rate-delta', d.postChangeFailureRate, true, true);
418
+ }
419
+
420
+ const alertsEl = document.getElementById('kpi-alerts');
421
+ if (alertsEl) {
422
+ const alerts = state.kpi?.alerts || [];
423
+ if (alerts.length === 0) {
424
+ alertsEl.innerHTML = '<span style="color:var(--success);">No KPI alerts in current window.</span>';
425
+ } else {
426
+ alertsEl.innerHTML = alerts.slice(0, 3).map(a => `⚠️ ${escapeHtml(a.message)} (${a.metric})`).join(' · ');
427
+ }
428
+ }
429
+ }
430
+
431
+ function renderKpiTrendChart() {
432
+ const chartEl = document.querySelector('#kpi-trend-chart');
433
+ if (!chartEl) return;
434
+
435
+ const daily = state.kpi?.trend?.daily || [];
436
+ const categories = daily.map(d => d.date);
437
+ const useful = daily.map(d => Number(d.usefulRecallRate || 0) * 100);
438
+ const rework = daily.map(d => Number(d.reworkRate || 0) * 100);
439
+ const fail = daily.map(d => Number(d.postChangeFailureRate || 0) * 100);
440
+
441
+ if (state.kpiChartInstance) {
442
+ state.kpiChartInstance.destroy();
443
+ state.kpiChartInstance = null;
444
+ }
445
+
446
+ const options = {
447
+ series: [
448
+ { name: 'Useful Recall %', data: useful },
449
+ { name: 'Rework %', data: rework },
450
+ { name: 'Failure %', data: fail }
451
+ ],
452
+ chart: {
453
+ type: 'line',
454
+ height: 240,
455
+ background: 'transparent',
456
+ toolbar: { show: false },
457
+ fontFamily: 'Outfit, sans-serif'
458
+ },
459
+ stroke: { curve: 'smooth', width: 2 },
460
+ dataLabels: { enabled: false },
461
+ xaxis: { categories, labels: { style: { colors: '#8B9BB4' } } },
462
+ yaxis: { labels: { formatter: (v) => `${v.toFixed(0)}%`, style: { colors: '#8B9BB4' } } },
463
+ theme: { mode: 'dark' },
464
+ grid: { borderColor: 'rgba(255,255,255,0.05)', strokeDashArray: 4 },
465
+ colors: ['#34D399', '#FEB019', '#FF4560']
466
+ };
467
+
468
+ state.kpiChartInstance = new ApexCharts(chartEl, options);
469
+ state.kpiChartInstance.render();
470
+ }
471
+
330
472
  function selectLevel(level) {
331
473
  state.currentLevel = level;
332
474
 
@@ -347,6 +489,23 @@ function selectSort(sort) {
347
489
  loadLevelEvents(state.currentLevel, sort);
348
490
  }
349
491
 
492
+ function getAdherenceInfo(event) {
493
+ const adherence = event?.metadata?.adherence || event?.meta?.adherence || null;
494
+ if (!adherence || typeof adherence !== 'object') return null;
495
+ const reason = adherence.reason || 'unknown';
496
+ const checked = Boolean(adherence.checked);
497
+ const turn = adherence.turn;
498
+ return { reason, checked, turn };
499
+ }
500
+
501
+ function renderAdherenceBadge(event) {
502
+ const info = getAdherenceInfo(event);
503
+ if (!info) return '';
504
+ const modeClass = info.checked ? 'adherence-checked' : 'adherence-skipped';
505
+ const turnText = Number.isFinite(info.turn) ? ` · T${info.turn}` : '';
506
+ return `<span class="adherence-badge ${modeClass}" title="adherence ${info.checked ? 'checked' : 'skipped'}${turnText}">adh:${escapeHtml(info.reason)}</span>`;
507
+ }
508
+
350
509
  function updateEventsListUI() {
351
510
  const container = document.getElementById('event-list-container');
352
511
  container.innerHTML = '';
@@ -374,13 +533,17 @@ function updateEventsListUI() {
374
533
  const accessBadge = event.accessCount > 0
375
534
  ? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
376
535
  : '';
536
+ const adherenceBadge = renderAdherenceBadge(event);
377
537
  const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
378
538
  ? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
379
539
  : '';
380
540
 
381
541
  el.innerHTML = `
382
542
  <div class="event-header">
383
- <span class="event-type-badge ${typeClass}">${eventType}</span>
543
+ <div style="display:flex; gap:8px; align-items:center;">
544
+ <span class="event-type-badge ${typeClass}">${eventType}</span>
545
+ ${adherenceBadge}
546
+ </div>
384
547
  <div style="display:flex; gap:8px; align-items:center;">
385
548
  ${accessBadge}
386
549
  ${lastUsed}
@@ -400,9 +563,72 @@ function updateMemoryUsageUI() {
400
563
  updateGraduationBars();
401
564
  updateHelpfulnessUI();
402
565
  updateMostHelpfulList();
566
+ updateAdherenceSummaryUI();
403
567
  updateRetrievalTraceUI();
404
568
  }
405
569
 
570
+ function adherenceWindowToMs(window) {
571
+ if (window === '24h') return 24 * 60 * 60 * 1000;
572
+ if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
573
+ if (window === '30d') return 30 * 24 * 60 * 60 * 1000;
574
+ return 24 * 60 * 60 * 1000;
575
+ }
576
+
577
+ async function fetchAdherenceSummary() {
578
+ const res = await fetch(apiUrl(`${API_BASE}/events`, { level: 'L0', limit: 500, sort: 'recent' }));
579
+ if (!res.ok) return null;
580
+ const data = await res.json();
581
+ const events = data.events || [];
582
+
583
+ const counts = {};
584
+ let checked = 0;
585
+ let skipped = 0;
586
+ let total = 0;
587
+
588
+ const now = Date.now();
589
+ const windowMs = adherenceWindowToMs(state.adherenceWindow);
590
+
591
+ for (const e of events) {
592
+ const ts = e?.timestamp ? new Date(e.timestamp).getTime() : 0;
593
+ if (!ts || now - ts > windowMs) continue;
594
+
595
+ const adherence = e?.metadata?.adherence || e?.meta?.adherence;
596
+ if (!adherence) continue;
597
+ total++;
598
+ const reason = adherence.reason || 'unknown';
599
+ counts[reason] = (counts[reason] || 0) + 1;
600
+ if (adherence.checked) checked++; else skipped++;
601
+ }
602
+
603
+ return { total, checked, skipped, counts, window: state.adherenceWindow };
604
+ }
605
+
606
+ function updateAdherenceSummaryUI() {
607
+ const el = document.getElementById('adherence-summary');
608
+ if (!el) return;
609
+
610
+ const s = state.adherenceSummary;
611
+ if (!s || !s.total) {
612
+ el.innerHTML = '<span style="color:var(--text-muted);">No adherence metadata yet.</span>';
613
+ return;
614
+ }
615
+
616
+ const top = Object.entries(s.counts || {})
617
+ .sort((a, b) => b[1] - a[1])
618
+ .slice(0, 5)
619
+ .map(([reason, count]) => `<span class="adherence-badge adherence-checked" style="margin-right:6px;">${escapeHtml(reason)}: ${count}</span>`)
620
+ .join('');
621
+
622
+ el.innerHTML = `
623
+ <div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:8px;">
624
+ <span><strong>${s.total}</strong> tagged prompts (${escapeHtml(s.window || state.adherenceWindow)})</span>
625
+ <span style="color:var(--success);"><strong>${s.checked}</strong> checked</span>
626
+ <span style="color:var(--text-muted);"><strong>${s.skipped}</strong> skipped</span>
627
+ </div>
628
+ <div>${top}</div>
629
+ `;
630
+ }
631
+
406
632
  function updateGraduationBars() {
407
633
  const container = document.getElementById('graduation-bars');
408
634
  if (!container || !state.stats?.levelStats) return;
@@ -691,6 +917,7 @@ async function openDetailModal(eventId) {
691
917
  const eventType = evt.eventType || 'unknown';
692
918
  const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
693
919
  const time = new Date(evt.timestamp).toLocaleString();
920
+ const adherenceBadge = renderAdherenceBadge(evt);
694
921
 
695
922
  let contextHtml = '';
696
923
  if (ctx.length > 0) {
@@ -716,6 +943,7 @@ async function openDetailModal(eventId) {
716
943
  <i class="ri-price-tag-3-line"></i>
717
944
  <span class="event-type-badge ${typeClass}">${eventType}</span>
718
945
  </div>
946
+ ${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
719
947
  <div class="modal-meta-item">
720
948
  <i class="ri-time-line"></i>
721
949
  ${time}
@@ -763,11 +991,13 @@ async function showEventsListModal() {
763
991
 
764
992
  body.innerHTML = events.map(e => {
765
993
  const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
994
+ const adherenceBadge = renderAdherenceBadge(e);
766
995
  return `
767
996
  <div class="modal-list-item" onclick="openDetailModal('${e.id}')">
768
997
  <div class="modal-list-info">
769
998
  <div class="title">
770
999
  <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1000
+ ${adherenceBadge}
771
1001
  ${escapeHtml((e.preview || '').slice(0, 80))}
772
1002
  </div>
773
1003
  <div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
@@ -847,11 +1077,13 @@ async function showSessionDetailInModal(sessionId) {
847
1077
  <div class="modal-section-title">Events</div>
848
1078
  ${events.map(e => {
849
1079
  const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1080
+ const adherenceBadge = renderAdherenceBadge(e);
850
1081
  return `
851
1082
  <div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
852
1083
  <div class="modal-list-info">
853
1084
  <div class="title">
854
1085
  <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1086
+ ${adherenceBadge}
855
1087
  ${escapeHtml((e.preview || '').slice(0, 80))}
856
1088
  </div>
857
1089
  <div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
@@ -120,6 +120,45 @@
120
120
  </div>
121
121
  </div>
122
122
 
123
+ <!-- KPI Cards -->
124
+ <div class="stats-grid kpi-grid" style="margin-top:-4px;">
125
+ <div class="stat-card kpi-card">
126
+ <div class="stat-value" id="kpi-useful-recall">-</div>
127
+ <div class="kpi-delta" id="kpi-useful-recall-delta">-</div>
128
+ <div class="stat-label"><i class="ri-thumb-up-line"></i> Useful Recall Rate</div>
129
+ </div>
130
+ <div class="stat-card kpi-card">
131
+ <div class="stat-value" id="kpi-completion-turns">-</div>
132
+ <div class="kpi-delta" id="kpi-completion-turns-delta">-</div>
133
+ <div class="stat-label"><i class="ri-repeat-line"></i> Avg Completion Turns</div>
134
+ </div>
135
+ <div class="stat-card kpi-card">
136
+ <div class="stat-value" id="kpi-rework-rate">-</div>
137
+ <div class="kpi-delta" id="kpi-rework-rate-delta">-</div>
138
+ <div class="stat-label"><i class="ri-hammer-line"></i> Rework Rate</div>
139
+ </div>
140
+ <div class="stat-card kpi-card">
141
+ <div class="stat-value" id="kpi-failure-rate">-</div>
142
+ <div class="kpi-delta" id="kpi-failure-rate-delta">-</div>
143
+ <div class="stat-label"><i class="ri-error-warning-line"></i> Post-change Failure</div>
144
+ </div>
145
+ </div>
146
+
147
+ <div class="card" style="margin-bottom:24px;">
148
+ <div class="card-header">
149
+ <div class="card-title">
150
+ <i class="ri-line-chart-line"></i>
151
+ <span>KPI Trend</span>
152
+ </div>
153
+ <div style="display:flex; gap:8px;">
154
+ <button class="sort-btn active" data-kpi-window="7d">7d</button>
155
+ <button class="sort-btn" data-kpi-window="30d">30d</button>
156
+ </div>
157
+ </div>
158
+ <div id="kpi-alerts" style="font-size:12px; color:var(--text-muted); margin-bottom:10px;">Loading...</div>
159
+ <div id="kpi-trend-chart" style="height:240px;"></div>
160
+ </div>
161
+
123
162
  <!-- Main Grid -->
124
163
  <div class="dashboard-grid">
125
164
 
@@ -272,6 +311,18 @@
272
311
  </div>
273
312
  </div>
274
313
 
314
+ <div style="margin-top:20px;">
315
+ <div class="section-label" style="display:flex; justify-content:space-between; align-items:center;">
316
+ <span>Adherence Reasons</span>
317
+ <span id="adherence-window-controls" style="display:flex; gap:6px;">
318
+ <button class="sort-btn active" data-adh-window="24h">24h</button>
319
+ <button class="sort-btn" data-adh-window="7d">7d</button>
320
+ <button class="sort-btn" data-adh-window="30d">30d</button>
321
+ </span>
322
+ </div>
323
+ <div id="adherence-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
324
+ </div>
325
+
275
326
  <div style="margin-top:20px;">
276
327
  <div class="section-label">Retrieval Trace (1:1)</div>
277
328
  <div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
package/dist/ui/style.css CHANGED
@@ -329,6 +329,23 @@ body {
329
329
  opacity: 0.5;
330
330
  }
331
331
 
332
+ .kpi-grid .stat-card .stat-value {
333
+ font-size: 28px;
334
+ }
335
+ .kpi-grid .stat-card .stat-label {
336
+ font-size: 12px;
337
+ }
338
+ .kpi-delta {
339
+ font-size: 11px;
340
+ margin-top: -4px;
341
+ margin-bottom: 8px;
342
+ color: var(--text-muted);
343
+ font-weight: 600;
344
+ }
345
+ .kpi-delta.good { color: var(--success); }
346
+ .kpi-delta.bad { color: var(--error); }
347
+ .kpi-delta.neutral { color: var(--text-muted); }
348
+
332
349
  .stat-value {
333
350
  font-size: 36px;
334
351
  font-weight: 700;
@@ -594,6 +611,23 @@ body {
594
611
  font-weight: 600;
595
612
  }
596
613
 
614
+ .adherence-badge {
615
+ font-size: 10px;
616
+ padding: 3px 7px;
617
+ border-radius: 6px;
618
+ font-weight: 700;
619
+ text-transform: uppercase;
620
+ letter-spacing: 0.2px;
621
+ }
622
+ .adherence-checked {
623
+ background: rgba(52, 211, 153, 0.14);
624
+ color: #34D399;
625
+ }
626
+ .adherence-skipped {
627
+ background: rgba(148, 163, 184, 0.14);
628
+ color: #94A3B8;
629
+ }
630
+
597
631
  /* Section Label */
598
632
  .section-label {
599
633
  font-size: 12px;
package/memory/_index.md CHANGED
@@ -4,6 +4,7 @@ Generated automatically by MarkdownMirror.
4
4
 
5
5
  - memory/.claude-plugin/commands/2026-02-25.md
6
6
  - memory/agent_response/uncategorized/2026-02-26.md
7
+ - memory/agent_response/uncategorized/2026-03-03.md
7
8
  - memory/default/uncategorized/2026-02-25.md
8
9
  - memory/node_modules/abbrev/2026-02-25.md
9
10
  - memory/node_modules/abort-controller/2026-02-25.md
@@ -393,6 +394,7 @@ Generated automatically by MarkdownMirror.
393
394
  - memory/node_modules/yocto-queue/2026-02-25.md
394
395
  - memory/node_modules/zod/2026-02-25.md
395
396
  - memory/session_summary/uncategorized/2026-02-26.md
397
+ - memory/session_summary/uncategorized/2026-03-03.md
396
398
  - memory/specs/20260207-dashboard-upgrade/2026-02-25.md
397
399
  - memory/specs/citations-system/2026-02-25.md
398
400
  - memory/specs/endless-mode/2026-02-25.md
@@ -406,4 +408,5 @@ Generated automatically by MarkdownMirror.
406
408
  - memory/specs/vector-outbox-v2/2026-02-25.md
407
409
  - memory/specs/web-viewer-ui/2026-02-25.md
408
410
  - memory/tool_observation/uncategorized/2026-02-26.md
411
+ - memory/tool_observation/uncategorized/2026-03-03.md
409
412
  - memory/user_prompt/uncategorized/2026-02-26.md
@@ -0,0 +1,14 @@
1
+
2
+ ## 2026-03-03T14:04:37.701Z | 55805b7d-13d2-4e6e-9c1d-bcdf1f1259f9
3
+ - type: agent_response
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ 커밋 완료했습니다.
6
+
7
+ **커밋 `b37aa13`** - `feat(cli): expand hook coverage to all lifecycle events`
8
+
9
+ 변경 내용:
10
+ - `src/cli/index.ts`: `SessionStart`, `Stop`, `SessionEnd` 훅 추가, `makeHook` 헬퍼로 리팩토링, `REQUIRED_HOOK_FILES` 상수 및 `hasHook` 헬퍼 추가, install/uninstall/status 명령 업데이트
11
+ - `scripts/delete-unknown-projects.js`: 세션 레지스트리에 없는 프로젝트 디렉토리 정리 스크립트 (신규)
12
+ - `package.json` / `package-lock.json`: 버전 1.0.18, `ops:projects:clean-unknown` 스크립트 추가
13
+
14
+ 제외된 파일 (커밋 불포함): `.npm-cache/`, `bootstrap-kb/`, `claude-memory-layer-1.0.14.tgz`, `d.sh`, `deploy.sh`, `memory/`, `yarn.lock` (빌드 아티팩트 및 로컬 파일)
@@ -0,0 +1,5 @@
1
+
2
+ ## 2026-03-03T14:04:47.970Z | 7dfbcdd9-f49f-482d-a0c6-9779331cc5cf
3
+ - type: session_summary
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ Session with 0 user prompts and 1 responses.
@@ -0,0 +1,21 @@
1
+
2
+ ## 2026-03-03T14:03:38.204Z | 46d8dc3c-a7ce-4602-ae18-daf4ed0266f1
3
+ - type: tool_observation
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ {"toolName":"Skill","toolInput":{"skill":"commit"},"toolOutput":"{\"success\":true,\"commandName\":\"commit\"}","durationMs":0,"success":true,"metadata":{"turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
6
+ ## 2026-03-03T14:03:43.897Z | ae933ad8-5f37-4a0b-9028-85bb29bdb6fc
7
+ - type: tool_observation
8
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
9
+ {"toolName":"Bash","toolInput":{"command":"git status && git diff && git log --oneline -5","description":"Check git status, diff, and recent commits"},"toolOutput":"On branch main\nYour branch is up to date with 'origin/main'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\tmodified: package-lock.json\n\tmodified: package.json\n\tmodified: src/cli/index.ts\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\t.npm-cache/\n\tbootstrap-kb/\n\tclaude-memory-layer-1.0.14.tgz\n\td.sh\n\tdeploy.sh\n\tmemory/\n\tscripts/delete-unknown-projects.js\n\tyarn.lock\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\ndiff --git package-lock.json package-lock.json\nindex c01b2e9..1181958 100644\n--- package-lock.json\n+++ package-lock.json\n@@ -1,12 +1,12 @@\n {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n \"lockfileVersion\": 3,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\ndiff --git package.json package.json\nindex bd41ac1..453d235 100644\n--- package.json\n+++ package.json\n@@ -1,6 +1,6 @@\n {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n\n... [121 lines truncated] ...\n\n delete settings.hooks.PostToolUse;\n+ delete settings.hooks.Stop;\n+ delete settings.hooks.SessionEnd;\n \n // Clean up empty hooks object\n if (Object.keys(settings.hooks).length === 0) {\n@@ -219,19 +238,22 @@ program\n console.log('\\n🧠 Claude Memory Layer Status\\n');\n \n // Check hooks\n- const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(h =>\n- h.hooks?.some(hook => hook.command?.includes('user-prompt-submit'))\n- );\n- const hasPostToolHook = settings.hooks?.PostToolUse?.some(h =>\n- h.hooks?.some(hook => hook.command?.includes('post-tool-use'))\n- );\n+ const hasSessionStartHook = hasHook(settings, 'SessionStart', 'session-start');\n+ const hasUserPromptHook = hasHook(settings, 'UserPromptSubmit', 'user-prompt-submit');\n+ const hasPostToolHook = hasHook(settings, 'PostToolUse', 'post-tool-use');\n+ const hasStopHook = hasHook(settings, 'Stop', 'stop');\n+ const hasSessionEndHook = hasHook(settings, 'SessionEnd', 'session-end');\n \n console.log('Hooks:');\n+ console.log(` SessionStart: ${hasSessionStartHook ? '✅ Installed' : '❌ Not installed'}`);\n console.log(` UserPromptSubmit: ${hasUserPromptHook ? '✅ Installed' : '❌ Not installed'}`);\n console.log(` PostToolUse: ${hasPostToolHook ? '✅ Installed' : '❌ Not installed'}`);\n+ console.log(` Stop: ${hasStopHook ? '✅ Installed' : '❌ Not installed'}`);\n+ console.log(` SessionEnd: ${hasSessionEndHook ? '✅ Installed' : '❌ Not installed'}`);\n \n // Check plugin files\n- const hooksExist = fs.existsSync(path.join(pluginPath, 'hooks', 'user-prompt-submit.js'));\n+ const hooksExist = REQUIRED_HOOK_FILES\n+ .every((file) => fs.existsSync(path.join(pluginPath, 'hooks', file)));\n console.log(`\\nPlugin files: ${hooksExist ? '✅ Found' : '❌ Not found'}`);\n console.log(` Path: ${pluginPath}`);\n \n@@ -239,7 +261,7 @@ program\n const dashboardRunning = await isServerRunning(37777);\n console.log(`\\nDashboard: ${dashboardRunning ? '✅ Running at http://localhost:37777' : '⏹️ Not running'}`);\n \n- if (!hasUserPromptHook || !hasPostToolHook) {\n+ if (!hasSessionStartHook || !hasUserPromptHook || !hasPostToolHook || !hasStopHook || !hasSessionEndHook) {\n console.log('\\n💡 Run \"claude-memory-layer install\" to set up hooks.\\n');\n } else {\n console.log('\\n✅ Plugin is fully installed and configured.\\n');\n257241b fix(hook): use lightweight service in session-start\n60d59c6 fix(api): handle missing retrieval_traces table in read-only mode\n1ab58f2 feat(build): inject version from package.json at build time\n4d48779 tune(hook): raise default injection threshold with fallback retry for recall\n3c618db fix(import): normalize underscore/hyphen path variants for project folder matching","durationMs":0,"success":true,"metadata":{"command":"git status","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
10
+ ## 2026-03-03T14:03:55.698Z | 101afa33-2fd1-4008-88b8-c824465ccd69
11
+ - type: tool_observation
12
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
13
+ {"toolName":"Bash","toolInput":{"command":"cat scripts/delete-unknown-projects.js","description":"Check new script content"},"toolOutput":"#!/usr/bin/env node\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nfunction parseArgs(argv) {\n const result = {\n apply: false,\n hashes: []\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === '--apply') {\n result.apply = true;\n continue;\n }\n if (arg === '--hash' && i + 1 < argv.length) {\n result.hashes.push(String(argv[i + 1]).trim().toLowerCase());\n i += 1;\n continue;\n }\n if (arg.startsWith('--hash=')) {\n result.hashes.push(arg.slice('--hash='.length).trim().toLowerCase());\n }\n }\n\n return result;\n}\n\nfunction loadRegistryHashes(registryPath) {\n if (!fs.existsSync(registryPath)) return new Set();\n try {\n const raw = JSON.parse(fs.readFileSync(registryPath, 'utf8'));\n const sessions = raw && typeof raw === 'object' ? raw.sessions : null;\n if (!sessions || typeof sessions !== 'object') return new Set();\n\n const hashes = new Set();\n for (const entry of Object.values(sessions)) {\n if (!entry || typeof entry !== 'object') continue;\n const hash = entry.projectHash;\n if (typeof hash === 'string' && /^[a-f0-9]{8}$/.test(hash)) {\n hashes.add(hash);\n }\n }\n return hashes;\n } catch {\n return new Set();\n }\n}\n\n... [54 lines truncated] ...\n\n .map((entry) => entry.name)\n .filter((name) => /^[a-f0-9]{8}$/.test(name))\n .filter((hash) => !knownHashes.has(hash))\n .filter((hash) => filterHashes.size === 0 || filterHashes.has(hash));\n\n const projectInfo = candidates.map((hash) => {\n const fullPath = path.join(projectsDir, hash);\n const sizeBytes = getDirSizeBytes(fullPath);\n return {\n hash,\n path: fullPath,\n sizeBytes,\n sizeHuman: formatBytes(sizeBytes)\n };\n });\n\n const totalBytes = projectInfo.reduce((sum, item) => sum + item.sizeBytes, 0);\n\n if (!args.apply) {\n console.log(JSON.stringify({\n status: 'dry-run',\n message: 'Run with --apply to delete the directories below.',\n memoryRoot,\n projectsDir,\n registryPath,\n unknownCount: projectInfo.length,\n totalSizeBytes: totalBytes,\n totalSizeHuman: formatBytes(totalBytes),\n projects: projectInfo\n }, null, 2));\n process.exit(0);\n }\n\n let deleted = 0;\n for (const item of projectInfo) {\n fs.rmSync(item.path, { recursive: true, force: true });\n deleted += 1;\n }\n\n console.log(JSON.stringify({\n status: 'ok',\n mode: 'apply',\n deletedCount: deleted,\n deletedSizeBytes: totalBytes,\n deletedSizeHuman: formatBytes(totalBytes),\n deletedHashes: projectInfo.map((item) => item.hash)\n }, null, 2));\n}\n\nmain();","durationMs":0,"success":true,"metadata":{"command":"cat","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
14
+ ## 2026-03-03T14:04:06.818Z | e92edb69-bd60-41e4-a490-08a7c50deadd
15
+ - type: tool_observation
16
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
17
+ {"toolName":"Bash","toolInput":{"command":"git add package.json package-lock.json src/cli/index.ts scripts/delete-unknown-projects.js","description":"Stage relevant changed and new files"},"toolOutput":"","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
18
+ ## 2026-03-03T14:04:29.483Z | bff186d2-1eba-4fb3-bbb4-98a5dccbc508
19
+ - type: tool_observation
20
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
21
+ {"toolName":"Bash","toolInput":{"command":"git commit -m \"$(cat <<'EOF'\nfeat(cli): expand hook coverage to all lifecycle events\n\n- Add SessionStart, Stop, SessionEnd hooks alongside existing UserPromptSubmit and PostToolUse\n- Refactor getHooksConfig with makeHook helper to reduce repetition\n- Add REQUIRED_HOOK_FILES constant and hasHook helper for validation\n- Update install/uninstall/status commands to handle all 5 hooks\n- Add scripts/delete-unknown-projects.js for cleaning orphaned project dirs\n- Add ops:projects:clean-unknown npm script\n- Bump version to 1.0.18\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit all staged changes"},"toolOutput":"[main b37aa13] feat(cli): expand hook coverage to all lifecycle events\n 4 files changed, 214 insertions(+), 37 deletions(-)\n create mode 100755 scripts/delete-unknown-projects.js","durationMs":0,"success":true,"metadata":{"command":"git commit","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-layer",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Claude Code plugin that learns from conversations to provide personalized assistance",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "ops:sync-gap:heal": "bash scripts/sync-gap-auto-heal.sh",
20
20
  "ops:review:resolve": "node scripts/review-queue-auto-resolve.js",
21
21
  "ops:heartbeat": "bash scripts/heartbeat-memory-orchestrator.sh",
22
- "ops:health": "npm run ops:sync-gap:report && npm run ops:sync-gap:heal && npm run ops:review:resolve"
22
+ "ops:health": "npm run ops:sync-gap:report && npm run ops:sync-gap:heal && npm run ops:review:resolve",
23
+ "ops:projects:clean-unknown": "node scripts/delete-unknown-projects.js"
23
24
  },
24
25
  "keywords": [
25
26
  "claude-code",