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.
- package/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +372 -74
- package/dist/cli/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +6 -0
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +6 -0
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +6 -0
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +6 -0
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +245 -31
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +329 -31
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +336 -38
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +6 -0
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +236 -4
- package/dist/ui/index.html +51 -0
- package/dist/ui/style.css +34 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +1 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +7 -0
- package/src/ui/app.js +236 -4
- package/src/ui/index.html +51 -0
- 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
|
-
<
|
|
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>
|
package/dist/ui/index.html
CHANGED
|
@@ -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,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.
|
|
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",
|