claude-memory-agent 2.1.0 → 2.2.0
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/bin/cli.js +11 -1
- package/bin/lib/banner.js +39 -0
- package/bin/lib/environment.js +166 -0
- package/bin/lib/installer.js +291 -0
- package/bin/lib/models.js +95 -0
- package/bin/lib/steps/advanced.js +101 -0
- package/bin/lib/steps/confirm.js +87 -0
- package/bin/lib/steps/model.js +57 -0
- package/bin/lib/steps/provider.js +65 -0
- package/bin/lib/steps/scope.js +59 -0
- package/bin/lib/steps/server.js +74 -0
- package/bin/lib/ui.js +75 -0
- package/bin/onboarding.js +164 -0
- package/bin/postinstall.js +22 -257
- package/config.py +103 -4
- package/dashboard.html +697 -27
- package/hooks/extract_memories.py +439 -0
- package/hooks/pre_compact_hook.py +76 -0
- package/hooks/session_end_hook.py +149 -0
- package/hooks/stop_hook.py +372 -0
- package/install.py +85 -32
- package/main.py +1636 -892
- package/mcp_server.py +451 -0
- package/package.json +14 -3
- package/requirements.txt +12 -8
- package/services/adaptive_ranker.py +272 -0
- package/services/agent_catalog.json +153 -0
- package/services/agent_registry.py +245 -730
- package/services/claude_md_sync.py +320 -4
- package/services/consolidation.py +417 -0
- package/services/database.py +586 -105
- package/services/embedding_pipeline.py +262 -0
- package/services/embeddings.py +493 -85
- package/services/memory_decay.py +408 -0
- package/services/native_memory_paths.py +86 -0
- package/services/native_memory_sync.py +496 -0
- package/services/response_manager.py +183 -0
- package/services/terminal_ui.py +199 -0
- package/services/tier_manager.py +235 -0
- package/services/websocket.py +26 -6
- package/skills/search.py +136 -61
- package/skills/session_review.py +210 -23
- package/skills/store.py +125 -18
- package/terminal_dashboard.py +474 -0
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/grounding-hook.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/curator.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/confidence_tracker.cpython-312.pyc +0 -0
- package/skills/__pycache__/context.cpython-312.pyc +0 -0
- package/skills/__pycache__/curator.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/session_review.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/test_automation.py +0 -221
- package/test_complete.py +0 -338
- package/test_full.py +0 -322
- package/verify_db.py +0 -134
package/dashboard.html
CHANGED
|
@@ -204,6 +204,27 @@
|
|
|
204
204
|
border-radius: 50%;
|
|
205
205
|
animation: pulse 2s infinite;
|
|
206
206
|
}
|
|
207
|
+
.retry-btn {
|
|
208
|
+
background: transparent;
|
|
209
|
+
border: 1px solid currentColor;
|
|
210
|
+
color: inherit;
|
|
211
|
+
padding: 4px 8px;
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
font-size: 11px;
|
|
215
|
+
margin-left: 8px;
|
|
216
|
+
transition: all 0.2s ease;
|
|
217
|
+
}
|
|
218
|
+
.retry-btn:hover {
|
|
219
|
+
background: rgba(255,255,255,0.1);
|
|
220
|
+
}
|
|
221
|
+
.retry-btn.spinning i {
|
|
222
|
+
animation: spin 1s linear infinite;
|
|
223
|
+
}
|
|
224
|
+
@keyframes spin {
|
|
225
|
+
from { transform: rotate(0deg); }
|
|
226
|
+
to { transform: rotate(360deg); }
|
|
227
|
+
}
|
|
207
228
|
/* Main Content */
|
|
208
229
|
.main-content {
|
|
209
230
|
max-width: 1800px;
|
|
@@ -1489,6 +1510,243 @@
|
|
|
1489
1510
|
.toast.success { border-left: 4px solid var(--accent-green); }
|
|
1490
1511
|
.toast.error { border-left: 4px solid var(--accent-red); }
|
|
1491
1512
|
.toast.info { border-left: 4px solid var(--accent-blue); }
|
|
1513
|
+
/* Maintenance Tab */
|
|
1514
|
+
.maintenance-grid {
|
|
1515
|
+
display: grid;
|
|
1516
|
+
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
|
1517
|
+
gap: 24px;
|
|
1518
|
+
}
|
|
1519
|
+
.maintenance-card {
|
|
1520
|
+
background: var(--bg-card);
|
|
1521
|
+
border: 1px solid var(--border-color);
|
|
1522
|
+
border-radius: 16px;
|
|
1523
|
+
padding: 24px;
|
|
1524
|
+
position: relative;
|
|
1525
|
+
overflow: hidden;
|
|
1526
|
+
transition: all 0.3s;
|
|
1527
|
+
}
|
|
1528
|
+
.maintenance-card:hover {
|
|
1529
|
+
border-color: var(--accent-blue);
|
|
1530
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
|
1531
|
+
}
|
|
1532
|
+
.maintenance-card::before {
|
|
1533
|
+
content: '';
|
|
1534
|
+
position: absolute;
|
|
1535
|
+
top: 0; left: 0; right: 0;
|
|
1536
|
+
height: 3px;
|
|
1537
|
+
}
|
|
1538
|
+
.maintenance-card.decay-card::before { background: var(--gradient-4); }
|
|
1539
|
+
.maintenance-card.dedup-card::before { background: var(--gradient-3); }
|
|
1540
|
+
.maintenance-card.tier1-card::before { background: var(--gradient-1); }
|
|
1541
|
+
.maintenance-card-header {
|
|
1542
|
+
display: flex;
|
|
1543
|
+
align-items: center;
|
|
1544
|
+
gap: 14px;
|
|
1545
|
+
margin-bottom: 20px;
|
|
1546
|
+
}
|
|
1547
|
+
.maintenance-card-icon {
|
|
1548
|
+
width: 48px; height: 48px;
|
|
1549
|
+
border-radius: 12px;
|
|
1550
|
+
display: flex;
|
|
1551
|
+
align-items: center;
|
|
1552
|
+
justify-content: center;
|
|
1553
|
+
font-size: 20px;
|
|
1554
|
+
flex-shrink: 0;
|
|
1555
|
+
}
|
|
1556
|
+
.maintenance-card-title {
|
|
1557
|
+
font-size: 17px;
|
|
1558
|
+
font-weight: 600;
|
|
1559
|
+
}
|
|
1560
|
+
.maintenance-card-subtitle {
|
|
1561
|
+
font-size: 12px;
|
|
1562
|
+
color: var(--text-secondary);
|
|
1563
|
+
margin-top: 2px;
|
|
1564
|
+
}
|
|
1565
|
+
.maintenance-stats {
|
|
1566
|
+
display: grid;
|
|
1567
|
+
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
|
1568
|
+
gap: 12px;
|
|
1569
|
+
margin-bottom: 20px;
|
|
1570
|
+
}
|
|
1571
|
+
.maintenance-stat {
|
|
1572
|
+
text-align: center;
|
|
1573
|
+
padding: 12px 8px;
|
|
1574
|
+
background: var(--bg-tertiary);
|
|
1575
|
+
border-radius: 10px;
|
|
1576
|
+
transition: all 0.2s;
|
|
1577
|
+
}
|
|
1578
|
+
.maintenance-stat:hover {
|
|
1579
|
+
background: var(--bg-secondary);
|
|
1580
|
+
}
|
|
1581
|
+
.maintenance-stat-value {
|
|
1582
|
+
font-size: 22px;
|
|
1583
|
+
font-weight: 700;
|
|
1584
|
+
color: var(--text-primary);
|
|
1585
|
+
}
|
|
1586
|
+
.maintenance-stat-value.green { color: var(--accent-green); }
|
|
1587
|
+
.maintenance-stat-value.yellow { color: var(--accent-yellow); }
|
|
1588
|
+
.maintenance-stat-value.red { color: var(--accent-red); }
|
|
1589
|
+
.maintenance-stat-value.blue { color: var(--accent-blue); }
|
|
1590
|
+
.maintenance-stat-value.purple { color: var(--accent-purple); }
|
|
1591
|
+
.maintenance-stat-value.cyan { color: var(--accent-cyan); }
|
|
1592
|
+
.maintenance-stat-label {
|
|
1593
|
+
font-size: 11px;
|
|
1594
|
+
color: var(--text-secondary);
|
|
1595
|
+
margin-top: 4px;
|
|
1596
|
+
text-transform: uppercase;
|
|
1597
|
+
letter-spacing: 0.5px;
|
|
1598
|
+
}
|
|
1599
|
+
.maintenance-actions {
|
|
1600
|
+
display: flex;
|
|
1601
|
+
gap: 10px;
|
|
1602
|
+
flex-wrap: wrap;
|
|
1603
|
+
}
|
|
1604
|
+
.maintenance-btn {
|
|
1605
|
+
padding: 10px 18px;
|
|
1606
|
+
border-radius: 10px;
|
|
1607
|
+
font-size: 13px;
|
|
1608
|
+
font-weight: 500;
|
|
1609
|
+
cursor: pointer;
|
|
1610
|
+
border: 1px solid var(--border-color);
|
|
1611
|
+
background: var(--bg-tertiary);
|
|
1612
|
+
color: var(--text-secondary);
|
|
1613
|
+
transition: all 0.2s;
|
|
1614
|
+
display: flex;
|
|
1615
|
+
align-items: center;
|
|
1616
|
+
gap: 8px;
|
|
1617
|
+
}
|
|
1618
|
+
.maintenance-btn:hover {
|
|
1619
|
+
color: var(--text-primary);
|
|
1620
|
+
border-color: var(--accent-blue);
|
|
1621
|
+
background: rgba(88, 166, 255, 0.1);
|
|
1622
|
+
}
|
|
1623
|
+
.maintenance-btn:disabled {
|
|
1624
|
+
opacity: 0.5;
|
|
1625
|
+
cursor: not-allowed;
|
|
1626
|
+
}
|
|
1627
|
+
.maintenance-btn.primary {
|
|
1628
|
+
background: var(--accent-blue);
|
|
1629
|
+
color: white;
|
|
1630
|
+
border-color: var(--accent-blue);
|
|
1631
|
+
}
|
|
1632
|
+
.maintenance-btn.primary:hover {
|
|
1633
|
+
background: #4a9aef;
|
|
1634
|
+
box-shadow: 0 4px 15px rgba(88, 166, 255, 0.3);
|
|
1635
|
+
}
|
|
1636
|
+
.maintenance-btn.success {
|
|
1637
|
+
background: var(--accent-green);
|
|
1638
|
+
color: white;
|
|
1639
|
+
border-color: var(--accent-green);
|
|
1640
|
+
}
|
|
1641
|
+
.maintenance-btn.success:hover {
|
|
1642
|
+
background: #36a446;
|
|
1643
|
+
box-shadow: 0 4px 15px rgba(63, 185, 80, 0.3);
|
|
1644
|
+
}
|
|
1645
|
+
.maintenance-btn .spinner-sm {
|
|
1646
|
+
width: 14px; height: 14px;
|
|
1647
|
+
border: 2px solid rgba(255,255,255,0.3);
|
|
1648
|
+
border-top: 2px solid currentColor;
|
|
1649
|
+
border-radius: 50%;
|
|
1650
|
+
animation: spin 0.8s linear infinite;
|
|
1651
|
+
display: none;
|
|
1652
|
+
}
|
|
1653
|
+
.maintenance-btn.loading .spinner-sm { display: inline-block; }
|
|
1654
|
+
.maintenance-btn.loading { pointer-events: none; opacity: 0.8; }
|
|
1655
|
+
.maintenance-last-run {
|
|
1656
|
+
margin-top: 16px;
|
|
1657
|
+
padding-top: 12px;
|
|
1658
|
+
border-top: 1px solid var(--border-color);
|
|
1659
|
+
font-size: 12px;
|
|
1660
|
+
color: var(--text-muted);
|
|
1661
|
+
display: flex;
|
|
1662
|
+
align-items: center;
|
|
1663
|
+
gap: 6px;
|
|
1664
|
+
}
|
|
1665
|
+
.maintenance-last-run i { font-size: 11px; }
|
|
1666
|
+
.dedup-results {
|
|
1667
|
+
margin-top: 16px;
|
|
1668
|
+
max-height: 250px;
|
|
1669
|
+
overflow-y: auto;
|
|
1670
|
+
}
|
|
1671
|
+
.dedup-cluster {
|
|
1672
|
+
background: var(--bg-tertiary);
|
|
1673
|
+
border-radius: 10px;
|
|
1674
|
+
padding: 12px 14px;
|
|
1675
|
+
margin-bottom: 10px;
|
|
1676
|
+
border: 1px solid transparent;
|
|
1677
|
+
transition: all 0.2s;
|
|
1678
|
+
}
|
|
1679
|
+
.dedup-cluster:hover {
|
|
1680
|
+
border-color: var(--border-color);
|
|
1681
|
+
}
|
|
1682
|
+
.dedup-cluster-header {
|
|
1683
|
+
display: flex;
|
|
1684
|
+
justify-content: space-between;
|
|
1685
|
+
align-items: center;
|
|
1686
|
+
margin-bottom: 8px;
|
|
1687
|
+
}
|
|
1688
|
+
.dedup-cluster-score {
|
|
1689
|
+
font-size: 12px;
|
|
1690
|
+
font-weight: 600;
|
|
1691
|
+
padding: 3px 10px;
|
|
1692
|
+
border-radius: 12px;
|
|
1693
|
+
}
|
|
1694
|
+
.dedup-cluster-score.high {
|
|
1695
|
+
background: rgba(248, 81, 73, 0.15);
|
|
1696
|
+
color: var(--accent-red);
|
|
1697
|
+
}
|
|
1698
|
+
.dedup-cluster-score.medium {
|
|
1699
|
+
background: rgba(210, 153, 34, 0.15);
|
|
1700
|
+
color: var(--accent-yellow);
|
|
1701
|
+
}
|
|
1702
|
+
.dedup-cluster-item {
|
|
1703
|
+
font-size: 12px;
|
|
1704
|
+
color: var(--text-secondary);
|
|
1705
|
+
padding: 4px 0;
|
|
1706
|
+
white-space: nowrap;
|
|
1707
|
+
overflow: hidden;
|
|
1708
|
+
text-overflow: ellipsis;
|
|
1709
|
+
}
|
|
1710
|
+
.dedup-cluster-item i {
|
|
1711
|
+
margin-right: 6px;
|
|
1712
|
+
font-size: 10px;
|
|
1713
|
+
}
|
|
1714
|
+
.tier1-preview {
|
|
1715
|
+
margin-top: 16px;
|
|
1716
|
+
background: var(--bg-tertiary);
|
|
1717
|
+
border-radius: 10px;
|
|
1718
|
+
padding: 16px;
|
|
1719
|
+
max-height: 300px;
|
|
1720
|
+
overflow-y: auto;
|
|
1721
|
+
font-size: 13px;
|
|
1722
|
+
line-height: 1.6;
|
|
1723
|
+
white-space: pre-wrap;
|
|
1724
|
+
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
|
1725
|
+
color: var(--text-secondary);
|
|
1726
|
+
display: none;
|
|
1727
|
+
}
|
|
1728
|
+
.tier1-preview.visible { display: block; }
|
|
1729
|
+
.tier1-preview h4 {
|
|
1730
|
+
color: var(--text-primary);
|
|
1731
|
+
margin-bottom: 8px;
|
|
1732
|
+
font-family: inherit;
|
|
1733
|
+
}
|
|
1734
|
+
.health-ring {
|
|
1735
|
+
position: relative;
|
|
1736
|
+
width: 80px;
|
|
1737
|
+
height: 80px;
|
|
1738
|
+
margin: 0 auto 12px;
|
|
1739
|
+
}
|
|
1740
|
+
.health-ring svg {
|
|
1741
|
+
transform: rotate(-90deg);
|
|
1742
|
+
}
|
|
1743
|
+
.health-ring-text {
|
|
1744
|
+
position: absolute;
|
|
1745
|
+
top: 50%; left: 50%;
|
|
1746
|
+
transform: translate(-50%, -50%);
|
|
1747
|
+
font-size: 16px;
|
|
1748
|
+
font-weight: 700;
|
|
1749
|
+
}
|
|
1492
1750
|
/* Responsive */
|
|
1493
1751
|
@media (max-width: 768px) {
|
|
1494
1752
|
.header-content { flex-direction: column; gap: 16px; }
|
|
@@ -1851,6 +2109,9 @@
|
|
|
1851
2109
|
<div class="status-indicator" id="statusIndicator">
|
|
1852
2110
|
<span class="status-dot"></span>
|
|
1853
2111
|
<span id="statusText">Connected</span>
|
|
2112
|
+
<button id="retryBtn" class="retry-btn" style="display: none;" onclick="retryConnection()" title="Click to retry Ollama connection">
|
|
2113
|
+
<i class="fas fa-sync-alt"></i>
|
|
2114
|
+
</button>
|
|
1854
2115
|
</div>
|
|
1855
2116
|
<div class="ws-status disconnected" id="wsStatus" title="Connecting...">
|
|
1856
2117
|
<i class="fas fa-bolt"></i>
|
|
@@ -1897,6 +2158,12 @@
|
|
|
1897
2158
|
<div class="stat-value" id="eventsCount">0</div>
|
|
1898
2159
|
<div class="stat-label">Timeline Events</div>
|
|
1899
2160
|
</div>
|
|
2161
|
+
<div class="stat-card" style="--card-accent: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)">
|
|
2162
|
+
<div class="stat-icon" style="background: rgba(63, 185, 80, 0.15); color: var(--accent-green);"><i class="fas fa-heartbeat"></i></div>
|
|
2163
|
+
<div class="stat-value" id="healthScore">--</div>
|
|
2164
|
+
<div class="stat-label">Health Score</div>
|
|
2165
|
+
<div class="stat-change positive" id="healthAtRisk">0 at risk</div>
|
|
2166
|
+
</div>
|
|
1900
2167
|
</div>
|
|
1901
2168
|
|
|
1902
2169
|
<!-- Tabs -->
|
|
@@ -1931,6 +2198,9 @@
|
|
|
1931
2198
|
<div class="tab" data-tab="graph" onclick="switchTab('graph')">
|
|
1932
2199
|
<i class="fas fa-project-diagram"></i> Graph
|
|
1933
2200
|
</div>
|
|
2201
|
+
<div class="tab" data-tab="maintenance" onclick="switchTab('maintenance')">
|
|
2202
|
+
<i class="fas fa-tools"></i> Maintenance
|
|
2203
|
+
</div>
|
|
1934
2204
|
</div>
|
|
1935
2205
|
|
|
1936
2206
|
<!-- Agents Tab -->
|
|
@@ -2275,6 +2545,81 @@
|
|
|
2275
2545
|
</div>
|
|
2276
2546
|
</div>
|
|
2277
2547
|
</div>
|
|
2548
|
+
|
|
2549
|
+
<!-- Maintenance Tab -->
|
|
2550
|
+
<div class="tab-content" id="maintenance-tab">
|
|
2551
|
+
<div class="section-header">
|
|
2552
|
+
<div class="section-title"><i class="fas fa-tools"></i> System Maintenance</div>
|
|
2553
|
+
<div class="section-actions">
|
|
2554
|
+
<button class="btn btn-secondary" onclick="loadMaintenanceData()"><i class="fas fa-sync"></i> Refresh All</button>
|
|
2555
|
+
</div>
|
|
2556
|
+
</div>
|
|
2557
|
+
<div class="maintenance-grid">
|
|
2558
|
+
<div class="maintenance-card decay-card">
|
|
2559
|
+
<div class="maintenance-card-header">
|
|
2560
|
+
<div class="maintenance-card-icon" style="background: rgba(63, 185, 80, 0.15); color: var(--accent-green);"><i class="fas fa-hourglass-half"></i></div>
|
|
2561
|
+
<div>
|
|
2562
|
+
<div class="maintenance-card-title">Memory Decay</div>
|
|
2563
|
+
<div class="maintenance-card-subtitle">Type-based memory lifecycle management</div>
|
|
2564
|
+
</div>
|
|
2565
|
+
</div>
|
|
2566
|
+
<div class="maintenance-stats" id="decayStats">
|
|
2567
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value green" id="decayPermanent">--</div><div class="maintenance-stat-label">Permanent</div></div>
|
|
2568
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value blue" id="decayDecayable">--</div><div class="maintenance-stat-label">Decayable</div></div>
|
|
2569
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value yellow" id="decayAtRisk">--</div><div class="maintenance-stat-label">At Risk</div></div>
|
|
2570
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value red" id="decayArchived">--</div><div class="maintenance-stat-label">Archived</div></div>
|
|
2571
|
+
</div>
|
|
2572
|
+
<div id="decayTypeBreakdown" style="margin-bottom: 16px;"></div>
|
|
2573
|
+
<div class="maintenance-actions">
|
|
2574
|
+
<button class="maintenance-btn primary" id="runDecayBtn" onclick="runDecayMaintenance()"><span class="spinner-sm"></span><i class="fas fa-play"></i> Run Decay Maintenance</button>
|
|
2575
|
+
</div>
|
|
2576
|
+
<div class="maintenance-last-run" id="decayLastRun"><i class="fas fa-clock"></i> <span>Last run: Never</span></div>
|
|
2577
|
+
</div>
|
|
2578
|
+
<div class="maintenance-card dedup-card">
|
|
2579
|
+
<div class="maintenance-card-header">
|
|
2580
|
+
<div class="maintenance-card-icon" style="background: rgba(88, 166, 255, 0.15); color: var(--accent-blue);"><i class="fas fa-clone"></i></div>
|
|
2581
|
+
<div>
|
|
2582
|
+
<div class="maintenance-card-title">Deduplication</div>
|
|
2583
|
+
<div class="maintenance-card-subtitle">Find and merge duplicate memories</div>
|
|
2584
|
+
</div>
|
|
2585
|
+
</div>
|
|
2586
|
+
<div class="maintenance-stats" id="dedupStats">
|
|
2587
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value cyan" id="dedupClusters">--</div><div class="maintenance-stat-label">Clusters</div></div>
|
|
2588
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value purple" id="dedupPairs">--</div><div class="maintenance-stat-label">Pairs Found</div></div>
|
|
2589
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value green" id="dedupAutoMerged">--</div><div class="maintenance-stat-label">Auto-Merged</div></div>
|
|
2590
|
+
</div>
|
|
2591
|
+
<div class="maintenance-actions">
|
|
2592
|
+
<button class="maintenance-btn primary" id="scanDedupBtn" onclick="scanDuplicates()"><span class="spinner-sm"></span><i class="fas fa-search"></i> Scan Duplicates</button>
|
|
2593
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-left: auto;">
|
|
2594
|
+
<label style="font-size: 12px; color: var(--text-secondary);">Threshold:</label>
|
|
2595
|
+
<input type="range" id="dedupThreshold" min="85" max="99" value="92" style="width: 80px; accent-color: var(--accent-blue);" oninput="document.getElementById('dedupThresholdVal').textContent = this.value + '%'">
|
|
2596
|
+
<span id="dedupThresholdVal" style="font-size: 12px; color: var(--accent-blue); font-weight: 600; min-width: 32px;">92%</span>
|
|
2597
|
+
</div>
|
|
2598
|
+
</div>
|
|
2599
|
+
<div class="dedup-results" id="dedupResults"></div>
|
|
2600
|
+
</div>
|
|
2601
|
+
<div class="maintenance-card tier1-card">
|
|
2602
|
+
<div class="maintenance-card-header">
|
|
2603
|
+
<div class="maintenance-card-icon" style="background: rgba(163, 113, 247, 0.15); color: var(--accent-purple);"><i class="fas fa-file-code"></i></div>
|
|
2604
|
+
<div>
|
|
2605
|
+
<div class="maintenance-card-title">Tier 1 Context (CLAUDE.md)</div>
|
|
2606
|
+
<div class="maintenance-card-subtitle">Auto-generate startup context from top memories</div>
|
|
2607
|
+
</div>
|
|
2608
|
+
</div>
|
|
2609
|
+
<div class="maintenance-stats" id="tier1Stats">
|
|
2610
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value purple" id="tier1Entries">--</div><div class="maintenance-stat-label">Entries</div></div>
|
|
2611
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value blue" id="tier1Categories">--</div><div class="maintenance-stat-label">Categories</div></div>
|
|
2612
|
+
<div class="maintenance-stat"><div class="maintenance-stat-value cyan" id="tier1CharCount">--</div><div class="maintenance-stat-label">Characters</div></div>
|
|
2613
|
+
</div>
|
|
2614
|
+
<div class="maintenance-actions">
|
|
2615
|
+
<button class="maintenance-btn" id="previewTier1Btn" onclick="previewTier1()"><span class="spinner-sm"></span><i class="fas fa-eye"></i> Preview</button>
|
|
2616
|
+
<button class="maintenance-btn success" id="generateTier1Btn" onclick="generateTier1()"><span class="spinner-sm"></span><i class="fas fa-file-export"></i> Generate & Write</button>
|
|
2617
|
+
</div>
|
|
2618
|
+
<div class="tier1-preview" id="tier1Preview"></div>
|
|
2619
|
+
<div class="maintenance-last-run" id="tier1LastRun"><i class="fas fa-clock"></i> <span>Last generated: Never</span></div>
|
|
2620
|
+
</div>
|
|
2621
|
+
</div>
|
|
2622
|
+
</div>
|
|
2278
2623
|
</main>
|
|
2279
2624
|
|
|
2280
2625
|
<div class="toast-container" id="toastContainer"></div>
|
|
@@ -2433,10 +2778,84 @@
|
|
|
2433
2778
|
}
|
|
2434
2779
|
|
|
2435
2780
|
function getActivityIcon(type) {
|
|
2436
|
-
const icons = { context: 'clock', embed: 'microchip', store: 'save', search: 'search' };
|
|
2781
|
+
const icons = { context: 'clock', embed: 'microchip', store: 'save', search: 'search', timeline: 'stream', decision: 'lightbulb', observation: 'eye', action: 'cog', error: 'exclamation-triangle', user_request: 'user', checkpoint: 'flag' };
|
|
2437
2782
|
return icons[type] || 'circle';
|
|
2438
2783
|
}
|
|
2439
2784
|
|
|
2785
|
+
let activityLoaded = false;
|
|
2786
|
+
|
|
2787
|
+
async function loadActivity() {
|
|
2788
|
+
// Only load historical data once; after that, WebSocket keeps it updated
|
|
2789
|
+
if (activityLoaded && activityItems.length > 0) {
|
|
2790
|
+
renderActivityFeed();
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
const container = document.getElementById('activityItems');
|
|
2794
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
2795
|
+
try {
|
|
2796
|
+
let url = `${API_URL}/api/timeline?limit=30`;
|
|
2797
|
+
if (currentProject) {
|
|
2798
|
+
url += `&project_path=${encodeURIComponent(currentProject)}`;
|
|
2799
|
+
}
|
|
2800
|
+
const response = await fetch(url);
|
|
2801
|
+
const data = await response.json();
|
|
2802
|
+
if (data.success && data.events && data.events.length > 0) {
|
|
2803
|
+
// Map timeline events to activity items (without duplicating existing live items)
|
|
2804
|
+
const existingTitles = new Set(activityItems.map(a => a.title + a.detail));
|
|
2805
|
+
const historicalItems = data.events
|
|
2806
|
+
.filter(e => !existingTitles.has((e.event_type === 'decision' ? 'Decision' : e.event_type === 'observation' ? 'Observation' : e.event_type === 'action' ? 'Action' : e.event_type === 'error' ? 'Error' : 'Timeline Event') + (e.summary || '')))
|
|
2807
|
+
.map(e => {
|
|
2808
|
+
const typeMap = {
|
|
2809
|
+
'decision': { actType: 'context', title: 'Decision' },
|
|
2810
|
+
'observation': { actType: 'search', title: 'Observation' },
|
|
2811
|
+
'action': { actType: 'store', title: 'Action' },
|
|
2812
|
+
'user_request': { actType: 'context', title: 'User Request' },
|
|
2813
|
+
'error': { actType: 'embed', title: 'Error' },
|
|
2814
|
+
'checkpoint': { actType: 'context', title: 'Checkpoint' },
|
|
2815
|
+
'outcome': { actType: 'store', title: 'Outcome' },
|
|
2816
|
+
'thinking': { actType: 'search', title: 'Thinking' }
|
|
2817
|
+
};
|
|
2818
|
+
const mapped = typeMap[e.event_type] || { actType: 'context', title: e.event_type || 'Event' };
|
|
2819
|
+
return {
|
|
2820
|
+
type: mapped.actType,
|
|
2821
|
+
title: mapped.title,
|
|
2822
|
+
detail: (e.summary || '').substring(0, 100),
|
|
2823
|
+
time: new Date(e.created_at)
|
|
2824
|
+
};
|
|
2825
|
+
});
|
|
2826
|
+
// Merge: live items first, then historical (avoid duplicates)
|
|
2827
|
+
const merged = [...activityItems, ...historicalItems];
|
|
2828
|
+
// Sort by time descending
|
|
2829
|
+
merged.sort((a, b) => b.time - a.time);
|
|
2830
|
+
// Deduplicate by title+detail
|
|
2831
|
+
const seen = new Set();
|
|
2832
|
+
activityItems = merged.filter(item => {
|
|
2833
|
+
const key = item.title + item.detail;
|
|
2834
|
+
if (seen.has(key)) return false;
|
|
2835
|
+
seen.add(key);
|
|
2836
|
+
return true;
|
|
2837
|
+
}).slice(0, 50);
|
|
2838
|
+
activityLoaded = true;
|
|
2839
|
+
renderActivityFeed();
|
|
2840
|
+
} else {
|
|
2841
|
+
activityLoaded = true;
|
|
2842
|
+
renderActivityFeed();
|
|
2843
|
+
}
|
|
2844
|
+
} catch (e) {
|
|
2845
|
+
console.error('Error loading activity:', e);
|
|
2846
|
+
container.innerHTML = `<div style="padding: 40px; text-align: center; color: var(--text-secondary);">
|
|
2847
|
+
<i class="fas fa-exclamation-triangle" style="font-size: 32px; opacity: 0.3; margin-bottom: 12px;"></i>
|
|
2848
|
+
<div>Failed to load activity</div>
|
|
2849
|
+
</div>`;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
async function loadActivityIfActive() {
|
|
2854
|
+
if (document.getElementById('activity-tab').classList.contains('active')) {
|
|
2855
|
+
await loadActivity();
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2440
2859
|
function formatRelativeTime(date) {
|
|
2441
2860
|
const seconds = Math.floor((new Date() - date) / 1000);
|
|
2442
2861
|
if (seconds < 60) return 'just now';
|
|
@@ -2493,6 +2912,8 @@
|
|
|
2493
2912
|
initWebSocket();
|
|
2494
2913
|
setInterval(sendHeartbeat, 30000);
|
|
2495
2914
|
renderVectorPreview(Array.from({ length: 100 }, () => Math.random() * 0.5));
|
|
2915
|
+
// Load health stats for top bar (non-blocking)
|
|
2916
|
+
loadDecayStats().catch(() => {});
|
|
2496
2917
|
});
|
|
2497
2918
|
|
|
2498
2919
|
// Show/hide loading overlay
|
|
@@ -2583,12 +3004,15 @@
|
|
|
2583
3004
|
|
|
2584
3005
|
// Hot reload all data for the project
|
|
2585
3006
|
try {
|
|
3007
|
+
// Reset activity cache when project changes so it reloads
|
|
3008
|
+
activityLoaded = false;
|
|
2586
3009
|
await Promise.all([
|
|
2587
3010
|
loadProjectConfig(),
|
|
2588
3011
|
loadStats(),
|
|
2589
3012
|
loadMemoriesIfActive(),
|
|
2590
3013
|
loadPatternsIfActive(),
|
|
2591
|
-
loadTimelineIfActive()
|
|
3014
|
+
loadTimelineIfActive(),
|
|
3015
|
+
loadActivityIfActive()
|
|
2592
3016
|
]);
|
|
2593
3017
|
renderAgents();
|
|
2594
3018
|
renderMcps();
|
|
@@ -2870,6 +3294,7 @@
|
|
|
2870
3294
|
if (tabId === 'timeline') loadTimeline();
|
|
2871
3295
|
if (tabId === 'memories') loadMemories();
|
|
2872
3296
|
if (tabId === 'patterns') loadPatterns();
|
|
3297
|
+
if (tabId === 'activity') loadActivity();
|
|
2873
3298
|
}
|
|
2874
3299
|
|
|
2875
3300
|
// Timeline
|
|
@@ -2880,30 +3305,31 @@
|
|
|
2880
3305
|
const container = document.getElementById('timelineContainer');
|
|
2881
3306
|
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
2882
3307
|
try {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3308
|
+
// Use /api/timeline directly - queries the DB with optional filters.
|
|
3309
|
+
// This replaces the broken session-lookup -> A2A -> skill chain which
|
|
3310
|
+
// failed when currentProject was null ("All Projects") or when
|
|
3311
|
+
// session_state rows had no matching project_path.
|
|
3312
|
+
let url = `${API_URL}/api/timeline?limit=100`;
|
|
3313
|
+
if (currentProject) {
|
|
3314
|
+
url += `&project_path=${encodeURIComponent(currentProject)}`;
|
|
2888
3315
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
body: JSON.stringify({
|
|
2894
|
-
jsonrpc: '2.0',
|
|
2895
|
-
id: Date.now(),
|
|
2896
|
-
method: 'tasks/send',
|
|
2897
|
-
params: { metadata: { skill_id: 'timeline_get', params: { session_id: sessionId, limit: 50 } } }
|
|
2898
|
-
})
|
|
2899
|
-
});
|
|
3316
|
+
if (timelineFilter && timelineFilter !== 'all') {
|
|
3317
|
+
url += `&event_type=${encodeURIComponent(timelineFilter)}`;
|
|
3318
|
+
}
|
|
3319
|
+
const response = await fetch(url);
|
|
2900
3320
|
const data = await response.json();
|
|
2901
|
-
const
|
|
2902
|
-
if (!
|
|
2903
|
-
container.innerHTML = `<div class="timeline-empty"><i class="fas fa-stream"></i><div>No timeline events yet</div></div>`;
|
|
3321
|
+
const events = data.events || [];
|
|
3322
|
+
if (!events.length) {
|
|
3323
|
+
container.innerHTML = `<div class="timeline-empty"><i class="fas fa-stream"></i><div>No timeline events yet</div><div style="font-size: 12px; margin-top: 8px;">Events will appear here as you work</div></div>`;
|
|
2904
3324
|
return;
|
|
2905
3325
|
}
|
|
2906
|
-
|
|
3326
|
+
// Parse details field if stored as JSON string
|
|
3327
|
+
events.forEach(e => {
|
|
3328
|
+
if (typeof e.details === 'string') {
|
|
3329
|
+
try { e.details = JSON.parse(e.details); } catch (_) {}
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
const chains = buildCausalChains(events);
|
|
2907
3333
|
const filterHtml = `
|
|
2908
3334
|
<div class="timeline-filters">
|
|
2909
3335
|
<div class="timeline-filter-chip ${timelineFilter === 'all' ? 'active' : ''}" onclick="filterTimeline('all')"><i class="fas fa-layer-group"></i> All</div>
|
|
@@ -3222,32 +3648,81 @@
|
|
|
3222
3648
|
|
|
3223
3649
|
// Health check
|
|
3224
3650
|
async function checkHealth() {
|
|
3651
|
+
const indicator = document.getElementById('statusIndicator');
|
|
3652
|
+
const text = document.getElementById('statusText');
|
|
3653
|
+
const retryBtn = document.getElementById('retryBtn');
|
|
3654
|
+
|
|
3225
3655
|
try {
|
|
3226
3656
|
const response = await fetch(`${API_URL}/health`);
|
|
3227
3657
|
const data = await response.json();
|
|
3228
|
-
|
|
3229
|
-
const text = document.getElementById('statusText');
|
|
3658
|
+
|
|
3230
3659
|
if (data.status === 'healthy') {
|
|
3231
3660
|
indicator.style.borderColor = 'rgba(63, 185, 80, 0.3)';
|
|
3232
3661
|
indicator.style.background = 'rgba(63, 185, 80, 0.1)';
|
|
3233
3662
|
indicator.style.color = 'var(--accent-green)';
|
|
3234
3663
|
text.textContent = 'Connected';
|
|
3664
|
+
retryBtn.style.display = 'none';
|
|
3665
|
+
} else if (data.status === 'degraded') {
|
|
3666
|
+
// Degraded mode - server is connected but some features limited
|
|
3667
|
+
indicator.style.borderColor = 'rgba(227, 179, 65, 0.3)';
|
|
3668
|
+
indicator.style.background = 'rgba(227, 179, 65, 0.1)';
|
|
3669
|
+
indicator.style.color = 'var(--accent-yellow)';
|
|
3670
|
+
text.textContent = 'Degraded';
|
|
3671
|
+
text.title = 'Server connected but Ollama is unavailable. Click retry to reconnect.';
|
|
3672
|
+
retryBtn.style.display = 'inline-block';
|
|
3235
3673
|
} else {
|
|
3236
3674
|
throw new Error('Unhealthy');
|
|
3237
3675
|
}
|
|
3238
3676
|
} catch (e) {
|
|
3239
|
-
const indicator = document.getElementById('statusIndicator');
|
|
3240
|
-
const text = document.getElementById('statusText');
|
|
3241
3677
|
indicator.style.borderColor = 'rgba(248, 81, 73, 0.3)';
|
|
3242
3678
|
indicator.style.background = 'rgba(248, 81, 73, 0.1)';
|
|
3243
3679
|
indicator.style.color = 'var(--accent-red)';
|
|
3244
3680
|
text.textContent = 'Disconnected';
|
|
3681
|
+
retryBtn.style.display = 'inline-block';
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
async function retryConnection() {
|
|
3686
|
+
const retryBtn = document.getElementById('retryBtn');
|
|
3687
|
+
const text = document.getElementById('statusText');
|
|
3688
|
+
|
|
3689
|
+
// Show spinning animation
|
|
3690
|
+
retryBtn.classList.add('spinning');
|
|
3691
|
+
text.textContent = 'Retrying...';
|
|
3692
|
+
|
|
3693
|
+
try {
|
|
3694
|
+
const response = await fetch(`${API_URL}/health/retry`, { method: 'POST' });
|
|
3695
|
+
const data = await response.json();
|
|
3696
|
+
|
|
3697
|
+
if (data.success) {
|
|
3698
|
+
showToast('Connection restored!', 'success');
|
|
3699
|
+
} else {
|
|
3700
|
+
showToast(data.message || 'Still degraded', 'warning');
|
|
3701
|
+
}
|
|
3702
|
+
} catch (e) {
|
|
3703
|
+
showToast('Retry failed: ' + e.message, 'error');
|
|
3245
3704
|
}
|
|
3705
|
+
|
|
3706
|
+
retryBtn.classList.remove('spinning');
|
|
3707
|
+
// Refresh health status
|
|
3708
|
+
await checkHealth();
|
|
3246
3709
|
}
|
|
3247
3710
|
|
|
3248
3711
|
setInterval(checkHealth, 10000);
|
|
3249
3712
|
checkHealth();
|
|
3250
3713
|
|
|
3714
|
+
// Auto-retry in background every 30 seconds when degraded
|
|
3715
|
+
setInterval(async () => {
|
|
3716
|
+
const text = document.getElementById('statusText');
|
|
3717
|
+
if (text && (text.textContent === 'Degraded' || text.textContent === 'Disconnected')) {
|
|
3718
|
+
// Silent background retry
|
|
3719
|
+
try {
|
|
3720
|
+
await fetch(`${API_URL}/health/retry`, { method: 'POST' });
|
|
3721
|
+
await checkHealth();
|
|
3722
|
+
} catch (e) { /* ignore */ }
|
|
3723
|
+
}
|
|
3724
|
+
}, 30000);
|
|
3725
|
+
|
|
3251
3726
|
// Memory Detail Modal
|
|
3252
3727
|
function showMemoryDetail(index) {
|
|
3253
3728
|
const memory = currentMemories[index];
|
|
@@ -3309,7 +3784,10 @@
|
|
|
3309
3784
|
const time = new Date(s.ended_at || s.started_at).toLocaleTimeString();
|
|
3310
3785
|
const pending = s.pending_count || 0;
|
|
3311
3786
|
const project = s.project_name || s.project_path?.split(/[/\\]/).pop() || 'Unknown';
|
|
3312
|
-
|
|
3787
|
+
const rawGoal = (s.current_goal || '').replace(/<[^>]*>/g, '').trim();
|
|
3788
|
+
const goal = rawGoal ? ' - ' + rawGoal.substring(0, 60) + (rawGoal.length > 60 ? '...' : '') : '';
|
|
3789
|
+
const memLabel = s.memory_count > 0 ? `${s.memory_count} memories` : 'no memories yet';
|
|
3790
|
+
return `<option value="${s.session_id}">[${date} ${time}] ${project} - ${memLabel}${pending > 0 ? ` (${pending} pending)` : ''}${goal}</option>`;
|
|
3313
3791
|
}).join('');
|
|
3314
3792
|
} catch (e) {
|
|
3315
3793
|
console.error('Error loading sessions:', e);
|
|
@@ -4168,6 +4646,195 @@
|
|
|
4168
4646
|
|
|
4169
4647
|
// ===== End Knowledge Graph =====
|
|
4170
4648
|
|
|
4649
|
+
// ===== Maintenance Tab Functions =====
|
|
4650
|
+
let maintenanceDataLoaded = false;
|
|
4651
|
+
|
|
4652
|
+
async function loadMaintenanceData() {
|
|
4653
|
+
await Promise.all([loadDecayStats()]);
|
|
4654
|
+
maintenanceDataLoaded = true;
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
async function loadDecayStats() {
|
|
4658
|
+
try {
|
|
4659
|
+
const response = await fetch(`${API_URL}/api/decay/stats`);
|
|
4660
|
+
const data = await response.json();
|
|
4661
|
+
if (data.success || data.permanent !== undefined) {
|
|
4662
|
+
const stats = data.data || data;
|
|
4663
|
+
document.getElementById('decayPermanent').textContent = stats.permanent ?? 0;
|
|
4664
|
+
document.getElementById('decayDecayable').textContent = stats.decayable ?? 0;
|
|
4665
|
+
document.getElementById('decayAtRisk').textContent = stats.at_risk ?? 0;
|
|
4666
|
+
document.getElementById('decayArchived').textContent = stats.archived ?? 0;
|
|
4667
|
+
const total = (stats.permanent || 0) + (stats.decayable || 0);
|
|
4668
|
+
const atRisk = stats.at_risk || 0;
|
|
4669
|
+
if (total > 0) {
|
|
4670
|
+
const healthPct = Math.round(((total - atRisk) / total) * 100);
|
|
4671
|
+
document.getElementById('healthScore').textContent = healthPct + '%';
|
|
4672
|
+
const healthEl = document.getElementById('healthAtRisk');
|
|
4673
|
+
healthEl.textContent = atRisk + ' at risk';
|
|
4674
|
+
if (atRisk > 0) {
|
|
4675
|
+
healthEl.className = 'stat-change';
|
|
4676
|
+
healthEl.style.background = 'rgba(210, 153, 34, 0.15)';
|
|
4677
|
+
healthEl.style.color = 'var(--accent-yellow)';
|
|
4678
|
+
} else {
|
|
4679
|
+
healthEl.className = 'stat-change positive';
|
|
4680
|
+
healthEl.style.background = '';
|
|
4681
|
+
healthEl.style.color = '';
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
if (stats.by_type) { renderDecayTypeBreakdown(stats.by_type); }
|
|
4685
|
+
if (stats.last_run) {
|
|
4686
|
+
document.getElementById('decayLastRun').querySelector('span').textContent = 'Last run: ' + formatTimeAgo(stats.last_run);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
} catch (e) { console.error('Failed to load decay stats:', e); }
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
function renderDecayTypeBreakdown(byType) {
|
|
4693
|
+
const container = document.getElementById('decayTypeBreakdown');
|
|
4694
|
+
if (!byType || Object.keys(byType).length === 0) { container.innerHTML = ''; return; }
|
|
4695
|
+
const typeColors = { decision: 'var(--accent-blue)', preference: 'var(--accent-pink)', code: 'var(--accent-green)', error: 'var(--accent-red)', session: 'var(--accent-cyan)', chunk: 'var(--accent-yellow)' };
|
|
4696
|
+
const typeIcons = { decision: 'lightbulb', preference: 'sliders-h', code: 'code', error: 'bug', session: 'clock', chunk: 'puzzle-piece' };
|
|
4697
|
+
let html = '<div style="display: flex; flex-wrap: wrap; gap: 8px;">';
|
|
4698
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
4699
|
+
const color = typeColors[type] || 'var(--text-secondary)';
|
|
4700
|
+
const icon = typeIcons[type] || 'circle';
|
|
4701
|
+
html += `<div style="display: flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--bg-tertiary); border-radius: 8px; font-size: 12px;">
|
|
4702
|
+
<i class="fas fa-${icon}" style="color: ${color}; font-size: 10px;"></i>
|
|
4703
|
+
<span style="color: var(--text-secondary);">${type}</span>
|
|
4704
|
+
<span style="color: ${color}; font-weight: 600;">${count}</span>
|
|
4705
|
+
</div>`;
|
|
4706
|
+
}
|
|
4707
|
+
container.innerHTML = html + '</div>';
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4710
|
+
async function runDecayMaintenance() {
|
|
4711
|
+
const btn = document.getElementById('runDecayBtn');
|
|
4712
|
+
btn.classList.add('loading');
|
|
4713
|
+
try {
|
|
4714
|
+
const response = await fetch(`${API_URL}/api/decay/run`, { method: 'POST' });
|
|
4715
|
+
const data = await response.json();
|
|
4716
|
+
if (response.ok) {
|
|
4717
|
+
const result = data.data || data;
|
|
4718
|
+
showToast(`Decay complete: ${result.updated_count || result.updated || 0} updated, ${result.archived_count || result.archived || 0} archived`, 'success');
|
|
4719
|
+
await loadDecayStats();
|
|
4720
|
+
} else { showToast('Decay failed: ' + (data.error || 'Unknown error'), 'error'); }
|
|
4721
|
+
} catch (e) {
|
|
4722
|
+
console.error('Decay maintenance failed:', e);
|
|
4723
|
+
showToast('Failed to run decay maintenance', 'error');
|
|
4724
|
+
} finally { btn.classList.remove('loading'); }
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
async function scanDuplicates() {
|
|
4728
|
+
const btn = document.getElementById('scanDedupBtn');
|
|
4729
|
+
btn.classList.add('loading');
|
|
4730
|
+
const threshold = (document.getElementById('dedupThreshold').value / 100).toFixed(2);
|
|
4731
|
+
try {
|
|
4732
|
+
let url = `${API_URL}/api/curator/duplicates?similarity_threshold=${threshold}`;
|
|
4733
|
+
if (currentProject) { url += `&project_path=${encodeURIComponent(currentProject)}`; }
|
|
4734
|
+
const response = await fetch(url);
|
|
4735
|
+
const data = await response.json();
|
|
4736
|
+
const results = data.data || data;
|
|
4737
|
+
const duplicates = results.duplicates || results.clusters || results || [];
|
|
4738
|
+
const container = document.getElementById('dedupResults');
|
|
4739
|
+
document.getElementById('dedupClusters').textContent = duplicates.length || 0;
|
|
4740
|
+
let totalPairs = 0;
|
|
4741
|
+
if (Array.isArray(duplicates)) { duplicates.forEach(d => { totalPairs += (d.pairs || d.memories || []).length || 1; }); }
|
|
4742
|
+
document.getElementById('dedupPairs').textContent = totalPairs;
|
|
4743
|
+
if (!duplicates.length) {
|
|
4744
|
+
container.innerHTML = `<div style="text-align: center; padding: 24px; color: var(--text-secondary);"><i class="fas fa-check-circle" style="color: var(--accent-green); font-size: 24px; margin-bottom: 8px;"></i><div>No duplicates found at ${Math.round(threshold * 100)}% threshold</div></div>`;
|
|
4745
|
+
showToast('No duplicates found', 'success');
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
let html = '';
|
|
4749
|
+
duplicates.forEach((cluster, i) => {
|
|
4750
|
+
const similarity = cluster.similarity || cluster.score || 0;
|
|
4751
|
+
const simPct = Math.round(similarity * 100);
|
|
4752
|
+
const scoreClass = simPct >= 95 ? 'high' : 'medium';
|
|
4753
|
+
const memories = cluster.memories || cluster.pairs || [cluster];
|
|
4754
|
+
const content1 = (cluster.content_1 || memories[0]?.content || '').substring(0, 80);
|
|
4755
|
+
const content2 = (cluster.content_2 || memories[1]?.content || '').substring(0, 80);
|
|
4756
|
+
html += `<div class="dedup-cluster"><div class="dedup-cluster-header"><span style="font-size: 12px; color: var(--text-secondary);">Cluster ${i + 1}</span><span class="dedup-cluster-score ${scoreClass}">${simPct}% similar</span></div><div class="dedup-cluster-item"><i class="fas fa-file-alt" style="color: var(--accent-blue);"></i> ${escapeHtml(content1)}...</div><div class="dedup-cluster-item"><i class="fas fa-copy" style="color: var(--accent-yellow);"></i> ${escapeHtml(content2)}...</div></div>`;
|
|
4757
|
+
});
|
|
4758
|
+
container.innerHTML = html;
|
|
4759
|
+
showToast(`Found ${duplicates.length} duplicate cluster(s)`, 'info');
|
|
4760
|
+
} catch (e) {
|
|
4761
|
+
console.error('Duplicate scan failed:', e);
|
|
4762
|
+
showToast('Failed to scan duplicates', 'error');
|
|
4763
|
+
} finally { btn.classList.remove('loading'); }
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
|
|
4767
|
+
|
|
4768
|
+
async function previewTier1() {
|
|
4769
|
+
const btn = document.getElementById('previewTier1Btn');
|
|
4770
|
+
btn.classList.add('loading');
|
|
4771
|
+
try {
|
|
4772
|
+
const body = { dry_run: true };
|
|
4773
|
+
if (currentProject) body.project_path = currentProject;
|
|
4774
|
+
const response = await fetch(`${API_URL}/api/tier1/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
4775
|
+
const data = await response.json();
|
|
4776
|
+
const result = data.data || data;
|
|
4777
|
+
const preview = document.getElementById('tier1Preview');
|
|
4778
|
+
if (result.content || result.tier1_content) {
|
|
4779
|
+
const content = result.content || result.tier1_content || '';
|
|
4780
|
+
preview.textContent = content;
|
|
4781
|
+
preview.classList.add('visible');
|
|
4782
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
4783
|
+
const categories = content.match(/^## /gm) || content.match(/^### /gm) || [];
|
|
4784
|
+
document.getElementById('tier1Entries').textContent = result.entry_count || lines.filter(l => l.startsWith('- ')).length || 0;
|
|
4785
|
+
document.getElementById('tier1Categories').textContent = result.category_count || categories.length || 0;
|
|
4786
|
+
document.getElementById('tier1CharCount').textContent = content.length;
|
|
4787
|
+
showToast('Preview generated', 'success');
|
|
4788
|
+
} else {
|
|
4789
|
+
preview.textContent = 'No content generated. Store more high-importance memories first.';
|
|
4790
|
+
preview.classList.add('visible');
|
|
4791
|
+
showToast('No tier 1 content available', 'info');
|
|
4792
|
+
}
|
|
4793
|
+
} catch (e) {
|
|
4794
|
+
console.error('Tier 1 preview failed:', e);
|
|
4795
|
+
showToast('Failed to generate preview', 'error');
|
|
4796
|
+
} finally { btn.classList.remove('loading'); }
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
async function generateTier1() {
|
|
4800
|
+
const btn = document.getElementById('generateTier1Btn');
|
|
4801
|
+
btn.classList.add('loading');
|
|
4802
|
+
try {
|
|
4803
|
+
const body = { dry_run: false };
|
|
4804
|
+
if (currentProject) body.project_path = currentProject;
|
|
4805
|
+
const response = await fetch(`${API_URL}/api/tier1/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
4806
|
+
const data = await response.json();
|
|
4807
|
+
const result = data.data || data;
|
|
4808
|
+
if (result.success !== false) {
|
|
4809
|
+
const content = result.content || result.tier1_content || '';
|
|
4810
|
+
const entries = content.split('\n').filter(l => l.startsWith('- ')).length;
|
|
4811
|
+
document.getElementById('tier1Entries').textContent = result.entry_count || entries;
|
|
4812
|
+
document.getElementById('tier1CharCount').textContent = content.length;
|
|
4813
|
+
document.getElementById('tier1LastRun').querySelector('span').textContent = 'Last generated: Just now';
|
|
4814
|
+
showToast(`CLAUDE.md updated with ${entries} entries`, 'success');
|
|
4815
|
+
} else { showToast('Generation failed: ' + (result.error || 'Unknown error'), 'error'); }
|
|
4816
|
+
} catch (e) {
|
|
4817
|
+
console.error('Tier 1 generation failed:', e);
|
|
4818
|
+
showToast('Failed to generate CLAUDE.md content', 'error');
|
|
4819
|
+
} finally { btn.classList.remove('loading'); }
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
function formatTimeAgo(dateStr) {
|
|
4823
|
+
if (!dateStr) return 'Never';
|
|
4824
|
+
try {
|
|
4825
|
+
const date = new Date(dateStr);
|
|
4826
|
+
const now = new Date();
|
|
4827
|
+
const diffMs = now - date;
|
|
4828
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
4829
|
+
if (diffMins < 1) return 'Just now';
|
|
4830
|
+
if (diffMins < 60) return diffMins + 'm ago';
|
|
4831
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
4832
|
+
if (diffHours < 24) return diffHours + 'h ago';
|
|
4833
|
+
return Math.floor(diffHours / 24) + 'd ago';
|
|
4834
|
+
} catch (e) { return dateStr; }
|
|
4835
|
+
}
|
|
4836
|
+
// ===== End Maintenance Tab =====
|
|
4837
|
+
|
|
4171
4838
|
// Hook into tab switching
|
|
4172
4839
|
const originalSwitchTab = window.switchTab;
|
|
4173
4840
|
window.switchTab = function(tab) {
|
|
@@ -4182,6 +4849,9 @@
|
|
|
4182
4849
|
loadCuratorStatus();
|
|
4183
4850
|
}, 100);
|
|
4184
4851
|
}
|
|
4852
|
+
if (tab === 'maintenance') {
|
|
4853
|
+
loadMaintenanceData();
|
|
4854
|
+
}
|
|
4185
4855
|
};
|
|
4186
4856
|
|
|
4187
4857
|
// Check URL hash for deep linking to review
|