@plumpslabs/kuma 2.3.30 → 2.3.32

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.
@@ -252,6 +252,8 @@ header .status .dot.off { background: var(--red); }
252
252
  <div class="tab active" data-tab="features"><i data-lucide="star" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Features</div>
253
253
  <div class="tab" data-tab="gotchas"><i data-lucide="alert-triangle" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Gotchas</div>
254
254
  <div class="tab" data-tab="health"><i data-lucide="heart-pulse" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Health</div>
255
+ <div class="tab" data-tab="efficiency"><i data-lucide="zap" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Efficiency</div>
256
+ <div class="tab" data-tab="staleness"><i data-lucide="activity" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Staleness</div>
255
257
  </div>
256
258
  <div class="tab-content" id="tab-content"></div>
257
259
  </div>
@@ -273,7 +275,7 @@ async function resetKumaDb() {
273
275
  var confirmation = prompt("⚠️ TYPE 'DELETE' TO CONFIRM RESETTING KUMA DB\nThis will permanently wipe all knowledge:");
274
276
  if (confirmation !== "DELETE") { if (confirmation !== null) alert("Reset cancelled."); return; }
275
277
  try {
276
- var res = await fetch(API + "/reset-db", { method: "POST" }).then(function(r){return r.json()});
278
+ var res = await fetch(API + "/reset-db?confirm=1", { method: "POST" }).then(function(r){return r.json()});
277
279
  if (res.ok) { alert("Kuma DB reset."); refetch(); }
278
280
  } catch (e) { alert("Reset failed: " + e.message); }
279
281
  }
@@ -281,7 +283,7 @@ async function resetKumaDb() {
281
283
  async function stopKumaStudio() {
282
284
  if (!confirm("Stop Kuma Studio?")) return;
283
285
  try {
284
- await fetch(API + "/stop-server", { method: "POST" });
286
+ await fetch(API + "/stop-server?confirm=1", { method: "POST" });
285
287
  document.body.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center;color:#94a3b8;font-size:16px;">Server stopped. Close this tab.</div>';
286
288
  } catch (e) { alert("Stop failed: " + e.message); }
287
289
  }
@@ -336,6 +338,8 @@ async function init() {
336
338
  renderFeatures(data);
337
339
  renderGotchas(data);
338
340
  renderHealth(data);
341
+ renderEfficiency(data);
342
+ renderStaleness(data);
339
343
  setupSearch();
340
344
  lucide.createIcons();
341
345
  }
@@ -658,13 +662,49 @@ async function openNodeModal(nodeId) {
658
662
  html += '<div class="modal-section">';
659
663
  html += '<h3>Info</h3>';
660
664
  html += '<div class="modal-kv"><span class="key">Type</span><span class="val"><span style="color:var(--accent)">'+escapeHtml(n.type)+'</span></span></div>';
665
+ html += '<div class="modal-kv"><span class="key">ID</span><span class="val" style="font-family:monospace;font-size:10px;color:var(--muted)">'+escapeHtml(n.id)+'</span></div>';
661
666
  if (n.file_path) html += '<div class="modal-kv"><span class="key">Path</span><span class="val" style="color:var(--accent2)">'+escapeHtml(n.file_path)+'</span></div>';
662
- if (meta.description) html += '<div class="modal-kv"><span class="key">Description</span><span class="val">'+escapeHtml(meta.description)+'</span></div>';
663
- if (meta.tags && meta.tags.length) html += '<div class="modal-kv"><span class="key">Tags</span><span class="val">'+meta.tags.map(function(t){return '<span class="tag">'+escapeHtml(t)+'</span>';}).join(' ')+'</span></div>';
664
- if (meta.risk) html += '<div class="modal-kv"><span class="key">Risk</span><span class="val" style="color:'+(meta.risk==='critical'?'var(--red)':meta.risk==='high'?'var(--orange)':meta.risk==='medium'?'var(--yellow)':'var(--green)')+'">'+escapeHtml(meta.risk)+'</span></div>';
667
+ if (n.severity) html += '<div class="modal-kv"><span class="key">Severity</span><span class="val" style="color:'+(n.severity==='critical'?'var(--red)':n.severity==='high'?'var(--orange)':n.severity==='medium'?'var(--yellow)':'var(--green)')+'">'+escapeHtml(n.severity)+'</span></div>';
668
+ if (n.confidence !== undefined && n.confidence !== null) html += '<div class="modal-kv"><span class="key">Confidence</span><span class="val" style="color:var(--accent)">'+escapeHtml(String(n.confidence))+'</span></div>';
669
+ if (n.last_verified_at) html += '<div class="modal-kv"><span class="key">Last Verified</span><span class="val">'+new Date(n.last_verified_at*1000).toLocaleString()+'</span></div>';
670
+ if (n.created_at) html += '<div class="modal-kv"><span class="key">Created</span><span class="val">'+new Date(n.created_at*1000).toLocaleString()+'</span></div>';
665
671
  if (n.updated_at) html += '<div class="modal-kv"><span class="key">Updated</span><span class="val">'+new Date(n.updated_at*1000).toLocaleString()+'</span></div>';
666
672
  html += '</div>';
667
673
 
674
+ // ALL metadata fields — render every key dynamically
675
+ var metaKeys = Object.keys(meta);
676
+ if (metaKeys.length > 0) {
677
+ html += '<div class="modal-section">';
678
+ html += '<h3>Metadata ('+metaKeys.length+' fields)</h3>';
679
+ metaKeys.forEach(function(key) {
680
+ var val = meta[key];
681
+ if (val === null || val === undefined || val === '') return;
682
+ var display = '';
683
+ var color = 'var(--text)';
684
+ if (Array.isArray(val)) {
685
+ if (val.length === 0) return;
686
+ display = val.join(' → ');
687
+ color = 'var(--accent2)';
688
+ } else if (typeof val === 'object') {
689
+ display = JSON.stringify(val, null, 2);
690
+ color = 'var(--muted)';
691
+ } else if (typeof val === 'number') {
692
+ display = String(val);
693
+ color = 'var(--accent)';
694
+ } else if (key === 'severity' || key === 'risk') {
695
+ display = String(val);
696
+ color = val === 'critical' ? 'var(--red)' : val === 'high' ? 'var(--orange)' : val === 'medium' ? 'var(--yellow)' : 'var(--green)';
697
+ } else if (key === 'stale') {
698
+ display = val === 1 ? 'YES — needs heal' : 'No';
699
+ color = val === 1 ? 'var(--red)' : 'var(--green)';
700
+ } else {
701
+ display = String(val);
702
+ }
703
+ html += '<div class="modal-kv"><span class="key">'+escapeHtml(key)+'</span><span class="val" style="color:'+color+'">'+escapeHtml(display)+'</span></div>';
704
+ });
705
+ html += '</div>';
706
+ }
707
+
668
708
  // Outgoing relations
669
709
  if (outgoing.length) {
670
710
  html += '<div class="modal-section"><h3>Outgoing ('+outgoing.length+')</h3>';
@@ -704,8 +744,12 @@ async function openNodeModal(nodeId) {
704
744
  var got = typeof g === 'string' ? JSON.parse(g) : g;
705
745
  html += '<div class="gotcha-item" style="border-color:var(--'+(got.severity==='critical'?'red':got.severity==='high'?'orange':got.severity==='medium'?'yellow':'green')+')">';
706
746
  html += '<div class="desc">'+escapeHtml(got.description)+'</div>';
707
- html += '<span class="sev-tag sev-'+got.severity+'">'+got.severity+'</span>';
708
- html += '</div>';
747
+ html += '<div style="display:flex;gap:8px;align-items:center;margin-top:4px">';
748
+ html += '<span class="sev-tag sev-'+got.severity+'">'+escapeHtml(got.severity)+'</span>';
749
+ if (got.workaround) html += '<span style="color:var(--accent2);font-size:10px">Workaround: '+escapeHtml(got.workaround)+'</span>';
750
+ if (got.added_by) html += '<span style="color:var(--muted);font-size:10px">by: '+escapeHtml(got.added_by)+'</span>';
751
+ if (got.created_at) html += '<span style="color:var(--muted);font-size:10px">'+new Date(got.created_at*1000).toLocaleDateString()+'</span>';
752
+ html += '</div></div>';
709
753
  });
710
754
  html += '</div>';
711
755
  }
@@ -744,7 +788,7 @@ async function refetch() {
744
788
  if (status.ok) { dot.className = 'dot on'; txt.textContent = status.project || 'connected'; } else { dot.className = 'dot off'; txt.textContent = 'no kuma.db found'; }
745
789
  var data = await fetch(API+'/dashboard').then(function(r){return r.json()});
746
790
  if (data.error) { showError(data.error); return; }
747
- renderStats(data); renderGraph(data); renderFeatures(data); renderGotchas(data); renderHealth(data);
791
+ renderStats(data); renderGraph(data); renderFeatures(data); renderGotchas(data); renderHealth(data); renderEfficiency(data); renderStaleness(data);
748
792
  lucide.createIcons();
749
793
  } catch(e) { showError('Refetch failed: ' + e.message); }
750
794
  btn.innerHTML = '<i data-lucide="refresh-cw" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Refetch';
@@ -762,7 +806,9 @@ function setupTabs(){
762
806
  content.innerHTML =
763
807
  '<div class="panel active" id="panel-features"><div class="empty">Loading...</div></div>' +
764
808
  '<div class="panel" id="panel-gotchas"><div class="empty">Loading...</div></div>' +
765
- '<div class="panel" id="panel-health"><div class="empty">Loading...</div></div>';
809
+ '<div class="panel" id="panel-health"><div class="empty">Loading...</div></div>' +
810
+ '<div class="panel" id="panel-efficiency"><div class="empty">Loading...</div></div>' +
811
+ '<div class="panel" id="panel-staleness"><div class="empty">Loading...</div></div>';
766
812
  document.querySelectorAll('.tab').forEach(function(tab){
767
813
  tab.onclick = function(){
768
814
  document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('active')});
@@ -827,6 +873,137 @@ function renderHealth(data){
827
873
  }).join('');
828
874
  }
829
875
 
876
+ // ============ EFFICIENCY PANEL (GAP 1) ============
877
+ // Proves the philosophy: "the more you use Kuma, the more efficient it gets."
878
+ // Every saved decision / record is an asset — here is the evidence.
879
+
880
+ function fmtDuration(sec) {
881
+ if (!sec || sec <= 0) return "0s";
882
+ if (sec >= 3600) return (sec/3600).toFixed(1)+"h";
883
+ if (sec >= 60) return Math.round(sec/60)+"m "+(sec%60)+"s";
884
+ return sec+"s";
885
+ }
886
+
887
+ function renderEfficiency(data){
888
+ var el = document.getElementById("panel-efficiency");
889
+ if(!el) return;
890
+ var eff = data.efficiency || {};
891
+ var m = eff.metrics || {};
892
+ var r = eff.recordings || {};
893
+ var savedSec = m.researchTimeSaved ? Math.round(m.researchTimeSaved/1000) : 0;
894
+ var passRate = eff.verificationPassRate;
895
+ var passColor = passRate == null ? 'var(--muted)' : passRate >= 80 ? 'var(--green)' : passRate >= 50 ? 'var(--yellow)' : 'var(--red)';
896
+
897
+ var stat = function(num, lbl, color){
898
+ return '<div style="flex:1;min-width:110px;padding:10px 12px;background:var(--bg);border-radius:8px;border:1px solid var(--border)"><div style="font-size:20px;font-weight:700;color:'+(color||'var(--accent2)')+'">'+num+'</div><div style="font-size:11px;color:var(--muted);margin-top:2px">'+lbl+'</div></div>';
899
+ };
900
+
901
+ var html = '';
902
+ html += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">' +
903
+ stat(eff.sessions||0, 'sessions') +
904
+ stat(eff.toolCalls||0, 'tool calls') +
905
+ stat(eff.totalEdits||0, 'file edits') +
906
+ stat(eff.researchCacheScopes||0, 'research cached', 'var(--green)') +
907
+ stat(eff.decisions||0, 'decisions', 'var(--accent)') +
908
+ stat(eff.verifications||0, 'verifications', passRate==null?'var(--muted)':'var(--green)') +
909
+ '</div>';
910
+
911
+ // The headline: time saved from cache — the compounding asset
912
+ html += '<div class="health-item" style="border-left:3px solid var(--green);margin-bottom:12px">' +
913
+ '<div style="font-size:13px;color:var(--muted)">⏱ Estimated time saved from research cache</div>' +
914
+ '<div class="score good" style="font-size:28px">'+fmtDuration(savedSec)+'</div>' +
915
+ '<div class="meta">'+ (m.filesRead||0) +' files read &middot; '+ (m.filesEdited||0) +' files edited (current session)</div>' +
916
+ '</div>';
917
+
918
+ // Recording assets — what we've banked
919
+ var recTotal = r.total||0;
920
+ html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">📦 Recorded Assets</div>';
921
+ html += '<div class="health-item" style="margin-bottom:12px">';
922
+ html += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
923
+ html += '<span class="sev-tag" style="background:rgba(124,58,237,0.15);color:var(--accent)">arch_flow: '+(r.archFlows||0)+'</span>';
924
+ html += '<span class="sev-tag" style="background:#f9731620;color:var(--orange)">gotcha: '+(r.gotchas||0)+'</span>';
925
+ html += '<span class="sev-tag" style="background:#eab30820;color:var(--yellow)">decision: '+(r.decisions||0)+'</span>';
926
+ html += '<span class="sev-tag" style="background:#06b6d420;color:var(--accent2)">research_save: '+(r.researchSaves||0)+'</span>';
927
+ html += '</div>';
928
+ html += '<div class="meta" style="margin-top:6px">Total assets banked: <b style="color:var(--accent2)">'+recTotal+'</b> — each one saves re-research next session</div>';
929
+ html += '</div>';
930
+
931
+ // Verification pass rate
932
+ html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">🧪 Verification</div>';
933
+ html += '<div class="health-item" style="margin-bottom:12px">' +
934
+ '<div class="score" style="color:'+passColor+'">'+(passRate==null?'—':passRate+'%')+'</div>' +
935
+ '<div class="meta">'+(eff.verifications||0)+' runs &middot; pass rate</div>' +
936
+ '</div>';
937
+
938
+ // Recent sessions breakdown
939
+ var sess = eff.recentSessions || [];
940
+ if (sess.length) {
941
+ html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">🕐 Recent Sessions</div>';
942
+ html += sess.map(function(s){
943
+ var d = s.startedAt ? new Date(s.startedAt*1000) : null;
944
+ var dateStr = d ? d.toLocaleDateString()+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}) : '-';
945
+ return '<div class="health-item">' +
946
+ '<div style="font-size:12px;color:var(--text)">'+escapeHtml(s.goal || 'untitled goal')+'</div>' +
947
+ '<div class="meta">'+dateStr+' &middot; '+s.toolCalls+' calls &middot; '+s.edits+' edits'+(s.failures?' &middot; <span style="color:var(--red)">'+s.failures+' failures</span>':'')+'</div>' +
948
+ '</div>';
949
+ }).join('');
950
+ }
951
+
952
+ el.innerHTML = html;
953
+ lucide.createIcons();
954
+ }
955
+
956
+ // ============ STALENESS PANEL (GAP 4) ============
957
+ // Assets can become liabilities when stale. Surface them before they bite.
958
+
959
+ function renderStaleness(data){
960
+ var el = document.getElementById("panel-staleness");
961
+ if(!el) return;
962
+ var st = data.staleness || { checked: 0, staleNodes: 0, missing: [] };
963
+ var html = '';
964
+
965
+ if (!st.staleNodes) {
966
+ html += '<div class="health-item" style="border-left:3px solid var(--green)">' +
967
+ '<div class="score good" style="font-size:22px">✅ All fresh</div>' +
968
+ '<div class="meta">'+st.checked+' file references checked — no stale assets found.</div>' +
969
+ '</div>';
970
+ } else {
971
+ html += '<div class="health-item" style="border-left:3px solid var(--red)">' +
972
+ '<div class="score bad" style="font-size:22px">'+st.staleNodes+' stale node(s)</div>' +
973
+ '<div class="meta">'+st.missing.length+' file(s) referenced but missing on disk ('+st.checked+' checked).</div>' +
974
+ '</div>';
975
+ html += '<div style="display:flex;gap:6px;margin:10px 0">' +
976
+ '<button onclick="copyHealCommand(this)" style="background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:6px;color:var(--accent);padding:6px 12px;font-size:11px;cursor:pointer">🛠 Copy heal command</button>' +
977
+ '</div>';
978
+ html += st.missing.map(function(m){
979
+ return '<div class="gotcha-item" style="border-color:var(--red)">' +
980
+ '<div class="file">'+escapeHtml(m.filePath)+'</div>' +
981
+ '<div class="desc">Referenced by <b style="color:var(--red)">'+m.nodeCount+'</b> node(s) but the file no longer exists.</div>' +
982
+ '</div>';
983
+ }).join('');
984
+ }
985
+
986
+ el.innerHTML = html;
987
+ lucide.createIcons();
988
+ }
989
+
990
+ function copyHealCommand(btn) {
991
+ var cmd = "kuma_memory({ action: 'heal' })";
992
+ if (navigator.clipboard && navigator.clipboard.writeText) {
993
+ navigator.clipboard.writeText(cmd).then(function() {
994
+ if (btn) { btn.textContent = "✅ Copied!"; setTimeout(function(){ btn.textContent = "🛠 Copy heal command"; }, 1500); }
995
+ }).catch(function() {
996
+ if (btn) { btn.textContent = "⚠️ Copy failed"; setTimeout(function(){ btn.textContent = "🛠 Copy heal command"; }, 1500); }
997
+ });
998
+ } else {
999
+ // Fallback for non-secure contexts
1000
+ var ta = document.createElement("textarea");
1001
+ ta.value = cmd; document.body.appendChild(ta); ta.select();
1002
+ try { document.execCommand("copy"); if (btn) { btn.textContent = "✅ Copied!"; setTimeout(function(){ btn.textContent = "🛠 Copy heal command"; }, 1500); } } catch(e) {}
1003
+ document.body.removeChild(ta);
1004
+ }
1005
+ }
1006
+
830
1007
  function setupSearch() {
831
1008
  var searchTimeout = null;
832
1009
  document.getElementById("node-search").oninput = function() {