memstack-skill-loader 4.4.0__tar.gz → 4.5.0__tar.gz
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.
- {memstack_skill_loader-4.4.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.5.0}/PKG-INFO +1 -1
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/pyproject.toml +1 -1
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/dashboard.html +205 -7
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/dashboard.py +129 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/license.py +326 -49
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +1 -0
- memstack_skill_loader-4.5.0/tests/test_license_grace.py +604 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/tests/test_pro_skills_update.py +281 -281
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/tests/test_skill_drift.py +36 -36
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/README.md +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/setup.cfg +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/agent_runner.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/categories.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/memory_db.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/server.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
- {memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/tests/test_pro_compressor.py +0 -0
{memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/dashboard.html
RENAMED
|
@@ -546,6 +546,39 @@
|
|
|
546
546
|
.diary-pagination .diary-pg-btn { font-size: 0.78rem; color: #58a6ff; cursor: pointer; background: none; border: 1px solid #30363d; border-radius: 4px; padding: 0.25rem 0.7rem; }
|
|
547
547
|
.diary-pagination .diary-pg-btn:hover { border-color: #58a6ff; }
|
|
548
548
|
|
|
549
|
+
/* ── Memory Browser ── */
|
|
550
|
+
.mb-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.9rem; flex-wrap: wrap; }
|
|
551
|
+
.mb-select, .mb-search {
|
|
552
|
+
background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
|
|
553
|
+
padding: 0.4rem 0.6rem; border-radius: 6px; font-size: 0.82rem;
|
|
554
|
+
}
|
|
555
|
+
.mb-search { flex: 1; min-width: 200px; }
|
|
556
|
+
.mb-search:focus, .mb-select:focus { outline: none; border-color: #58a6ff; }
|
|
557
|
+
.mb-count { font-size: 0.75rem; color: #484f58; white-space: nowrap; }
|
|
558
|
+
.mb-entry { cursor: pointer; transition: border-color 0.15s; }
|
|
559
|
+
.mb-entry:hover { border-color: #58a6ff; }
|
|
560
|
+
.mb-badges { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
|
|
561
|
+
.mb-badge {
|
|
562
|
+
font-size: 0.66rem; padding: 0.1rem 0.45rem; border-radius: 10px;
|
|
563
|
+
border: 1px solid #30363d; color: #8b949e; white-space: nowrap;
|
|
564
|
+
}
|
|
565
|
+
.mb-badge.project { color: #58a6ff; border-color: #1f4068; }
|
|
566
|
+
.mb-badge.agent { color: #d29922; border-color: #5a4413; }
|
|
567
|
+
.mb-badge.direct { color: #3fb950; border-color: #1b4721; }
|
|
568
|
+
.mb-detail-head { display: flex; justify-content: space-between; align-items: center; gap: 0.6rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
|
|
569
|
+
.mb-back {
|
|
570
|
+
background: #21262d; border: 1px solid #30363d; color: #c9d1d9;
|
|
571
|
+
padding: 0.3rem 0.7rem; border-radius: 6px; cursor: pointer; font-size: 0.78rem;
|
|
572
|
+
}
|
|
573
|
+
.mb-back:hover { border-color: #58a6ff; }
|
|
574
|
+
.mb-detail-actions { display: flex; gap: 0.5rem; align-items: center; }
|
|
575
|
+
.mb-detail-title { color: #e6edf3; font-size: 1rem; margin-bottom: 0.6rem; }
|
|
576
|
+
.mb-detail-content {
|
|
577
|
+
background: #0d1117; border: 1px solid #21262d; border-radius: 6px;
|
|
578
|
+
padding: 1rem; font-size: 0.8rem; color: #c9d1d9; line-height: 1.5;
|
|
579
|
+
white-space: pre-wrap; word-break: break-word; max-height: 65vh; overflow-y: auto;
|
|
580
|
+
}
|
|
581
|
+
|
|
549
582
|
/* ── Projects ── */
|
|
550
583
|
.projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
|
551
584
|
.project-card {
|
|
@@ -1656,13 +1689,18 @@
|
|
|
1656
1689
|
|
|
1657
1690
|
<!-- ═══════ Memory Browser ═══════ -->
|
|
1658
1691
|
<div id="page-memory-browser" class="page">
|
|
1659
|
-
<div class="
|
|
1660
|
-
<
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1692
|
+
<div class="page-header">
|
|
1693
|
+
<h2>Memory Browser</h2>
|
|
1694
|
+
<p class="meta">Search session diaries across all projects (agent runs and direct sessions).</p>
|
|
1695
|
+
</div>
|
|
1696
|
+
<div class="panel">
|
|
1697
|
+
<div class="mb-controls">
|
|
1698
|
+
<select id="mb-project" class="mb-select" aria-label="Project filter"></select>
|
|
1699
|
+
<input id="mb-search" class="mb-search" type="text" placeholder="Search title and content..." autocomplete="off">
|
|
1700
|
+
<span id="mb-count" class="mb-count"></span>
|
|
1665
1701
|
</div>
|
|
1702
|
+
<div id="mb-list" class="diary-list"></div>
|
|
1703
|
+
<div id="mb-detail" class="mb-detail" style="display:none;"></div>
|
|
1666
1704
|
</div>
|
|
1667
1705
|
</div>
|
|
1668
1706
|
|
|
@@ -2194,6 +2232,7 @@ document.querySelectorAll('.nav-item').forEach(item => {
|
|
|
2194
2232
|
if (page === 'overview' && !projectsLoaded) loadProjects();
|
|
2195
2233
|
if (page === 'overview') { updateWelcomeGreeting(); loadDiary(); }
|
|
2196
2234
|
if (page === 'agent-monitor') loadAgentMonitor();
|
|
2235
|
+
if (page === 'memory-browser') loadMemoryBrowser();
|
|
2197
2236
|
if (page === 'burn-report') loadBurnReport();
|
|
2198
2237
|
if (page === 'settings') loadSettings();
|
|
2199
2238
|
if (page !== 'agent-monitor') stopAgentRefresh();
|
|
@@ -2922,7 +2961,166 @@ async function loadProjects() {
|
|
|
2922
2961
|
}
|
|
2923
2962
|
}
|
|
2924
2963
|
|
|
2925
|
-
/*
|
|
2964
|
+
/* ════════════════════════════════════════════
|
|
2965
|
+
Memory Browser
|
|
2966
|
+
════════════════════════════════════════════ */
|
|
2967
|
+
let _mbEntries = [];
|
|
2968
|
+
let _mbLoaded = false;
|
|
2969
|
+
|
|
2970
|
+
// Mask secrets/PII so they are never rendered in plain sight.
|
|
2971
|
+
function maskSecrets(text) {
|
|
2972
|
+
if (!text) return '';
|
|
2973
|
+
return text
|
|
2974
|
+
.replace(/MSPRO-[A-Z0-9-]{4,}/gi, 'MSPRO-****')
|
|
2975
|
+
.replace(/sk-[A-Za-z0-9-]{8,}/g, 'sk-****')
|
|
2976
|
+
.replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, function (m) {
|
|
2977
|
+
const at = m.indexOf('@');
|
|
2978
|
+
return m[0] + '****' + m.slice(at);
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// Clean seam: a future semantic search can replace this body without touching the UI.
|
|
2983
|
+
function searchEntries(entries, query) {
|
|
2984
|
+
const q = (query || '').trim().toLowerCase();
|
|
2985
|
+
if (!q) return entries;
|
|
2986
|
+
const terms = q.split(/\s+/);
|
|
2987
|
+
return entries.filter(function (e) {
|
|
2988
|
+
const hay = ((e.title || '') + '\n' + (e.content || '')).toLowerCase();
|
|
2989
|
+
return terms.every(function (t) { return hay.includes(t); });
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function _mbCleanBody(raw, title) {
|
|
2994
|
+
const titleLower = (title || '').toLowerCase().trim();
|
|
2995
|
+
return (raw || '').split('\n')
|
|
2996
|
+
.map(function (l) { return l.replace(/^#{1,6}\s+/, ''); })
|
|
2997
|
+
.filter(function (l, i) {
|
|
2998
|
+
const t = l.trim();
|
|
2999
|
+
if (!t) return true;
|
|
3000
|
+
if (t.startsWith('Working directory:')) return false;
|
|
3001
|
+
if (i === 0 && (t.toLowerCase() === titleLower || /^\d{4}-/.test(t))) return false;
|
|
3002
|
+
return true;
|
|
3003
|
+
})
|
|
3004
|
+
.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
function _mbCleanTitle(e) {
|
|
3008
|
+
return (e.title || e.filename || '').replace(/^\d{4}-\d{2}-\d{2}[\s\-_]*/, '').trim() || e.filename || 'Untitled';
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function _mbFormatDate(raw) {
|
|
3012
|
+
if (!raw) return '';
|
|
3013
|
+
const d = new Date(raw + 'T00:00:00');
|
|
3014
|
+
return isNaN(d.getTime()) ? raw : d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function _mbSourceClass(e) { return e.source_type === 'agent' ? 'agent' : 'direct'; }
|
|
3018
|
+
|
|
3019
|
+
function renderMemoryList() {
|
|
3020
|
+
const listEl = document.getElementById('mb-list');
|
|
3021
|
+
const detailEl = document.getElementById('mb-detail');
|
|
3022
|
+
const countEl = document.getElementById('mb-count');
|
|
3023
|
+
detailEl.style.display = 'none';
|
|
3024
|
+
listEl.style.display = 'flex';
|
|
3025
|
+
const query = document.getElementById('mb-search').value;
|
|
3026
|
+
const filtered = searchEntries(_mbEntries, query);
|
|
3027
|
+
countEl.textContent = filtered.length + ' of ' + _mbEntries.length + ' entries';
|
|
3028
|
+
if (!_mbEntries.length) {
|
|
3029
|
+
listEl.innerHTML = '<span class="empty-state">No diary entries found.</span>';
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
if (!filtered.length) {
|
|
3033
|
+
listEl.innerHTML = '<span class="empty-state">No entries match your search.</span>';
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
listEl.innerHTML = filtered.map(function (e, i) {
|
|
3037
|
+
const body = _mbCleanBody(e.content, e.title);
|
|
3038
|
+
const snippet = escapeHtml(maskSecrets(body.substring(0, 220)));
|
|
3039
|
+
const more = body.length > 220 ? '...' : '';
|
|
3040
|
+
return '<div class="diary-entry mb-entry" data-idx="' + i + '">'
|
|
3041
|
+
+ '<div class="diary-header">'
|
|
3042
|
+
+ '<span class="diary-title">' + escapeHtml(_mbCleanTitle(e)) + '</span>'
|
|
3043
|
+
+ '<span class="diary-date">' + _mbFormatDate(e.date) + '</span>'
|
|
3044
|
+
+ '</div>'
|
|
3045
|
+
+ '<div class="mb-badges">'
|
|
3046
|
+
+ '<span class="mb-badge project">' + escapeHtml(e.project || 'Unknown') + '</span>'
|
|
3047
|
+
+ '<span class="mb-badge ' + _mbSourceClass(e) + '">' + _mbSourceClass(e) + '</span>'
|
|
3048
|
+
+ '</div>'
|
|
3049
|
+
+ '<div class="diary-body" style="margin-top:0.4rem">' + snippet + more + '</div>'
|
|
3050
|
+
+ '</div>';
|
|
3051
|
+
}).join('');
|
|
3052
|
+
listEl.querySelectorAll('.mb-entry').forEach(function (el) {
|
|
3053
|
+
el.addEventListener('click', function () {
|
|
3054
|
+
showMemoryDetail(filtered[parseInt(el.dataset.idx, 10)]);
|
|
3055
|
+
});
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
function showMemoryDetail(e) {
|
|
3060
|
+
if (!e) return;
|
|
3061
|
+
const listEl = document.getElementById('mb-list');
|
|
3062
|
+
const detailEl = document.getElementById('mb-detail');
|
|
3063
|
+
listEl.style.display = 'none';
|
|
3064
|
+
detailEl.style.display = 'block';
|
|
3065
|
+
const body = maskSecrets(_mbCleanBody(e.content, e.title));
|
|
3066
|
+
detailEl.innerHTML = ''
|
|
3067
|
+
+ '<div class="mb-detail-head">'
|
|
3068
|
+
+ '<div class="mb-detail-actions">'
|
|
3069
|
+
+ '<button class="mb-back" id="mb-back-btn">← Back to list</button>'
|
|
3070
|
+
+ '<button class="mb-back" id="mb-copy-btn" title="Copy to clipboard">Copy</button>'
|
|
3071
|
+
+ '</div>'
|
|
3072
|
+
+ '<div class="mb-badges">'
|
|
3073
|
+
+ '<span class="mb-badge project">' + escapeHtml(e.project || 'Unknown') + '</span>'
|
|
3074
|
+
+ '<span class="mb-badge ' + _mbSourceClass(e) + '">' + _mbSourceClass(e) + '</span>'
|
|
3075
|
+
+ '<span class="diary-date">' + _mbFormatDate(e.date) + '</span>'
|
|
3076
|
+
+ '</div></div>'
|
|
3077
|
+
+ '<h3 class="mb-detail-title">' + escapeHtml(_mbCleanTitle(e)) + '</h3>'
|
|
3078
|
+
+ '<div class="mb-detail-content">' + escapeHtml(body) + '</div>';
|
|
3079
|
+
document.getElementById('mb-back-btn').addEventListener('click', renderMemoryList);
|
|
3080
|
+
document.getElementById('mb-copy-btn').addEventListener('click', function() {
|
|
3081
|
+
const btn = this;
|
|
3082
|
+
// Copy the masked body (what is displayed), never the raw e.content
|
|
3083
|
+
navigator.clipboard.writeText(body).then(function() {
|
|
3084
|
+
btn.textContent = 'Copied!';
|
|
3085
|
+
btn.style.color = '#3fb950';
|
|
3086
|
+
btn.style.borderColor = '#3fb950';
|
|
3087
|
+
setTimeout(function() { btn.textContent = 'Copy'; btn.style.color = ''; btn.style.borderColor = ''; }, 2000);
|
|
3088
|
+
}).catch(function() { /* fallback: ignore */ });
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
async function fetchMemoryEntries(project) {
|
|
3093
|
+
const listEl = document.getElementById('mb-list');
|
|
3094
|
+
listEl.innerHTML = '<span class="empty-state">Loading...</span>';
|
|
3095
|
+
try {
|
|
3096
|
+
const res = await fetch('/api/memory-browser/entries?project=' + encodeURIComponent(project), {headers: AUTH_GET});
|
|
3097
|
+
_mbEntries = await res.json();
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
_mbEntries = [];
|
|
3100
|
+
}
|
|
3101
|
+
renderMemoryList();
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
async function loadMemoryBrowser() {
|
|
3105
|
+
if (_mbLoaded) return;
|
|
3106
|
+
_mbLoaded = true;
|
|
3107
|
+
const sel = document.getElementById('mb-project');
|
|
3108
|
+
const searchEl = document.getElementById('mb-search');
|
|
3109
|
+
searchEl.addEventListener('input', renderMemoryList);
|
|
3110
|
+
sel.addEventListener('change', function () { fetchMemoryEntries(sel.value); });
|
|
3111
|
+
try {
|
|
3112
|
+
const res = await fetch('/api/memory-browser/projects', {headers: AUTH_GET});
|
|
3113
|
+
const projects = await res.json();
|
|
3114
|
+
const total = projects.reduce(function (s, p) { return s + p.count; }, 0);
|
|
3115
|
+
sel.innerHTML = '<option value="__all__">All projects (' + total + ')</option>'
|
|
3116
|
+
+ projects.map(function (p) {
|
|
3117
|
+
return '<option value="' + escapeHtml(p.project) + '">' + escapeHtml(p.project) + ' (' + p.count + ')</option>';
|
|
3118
|
+
}).join('');
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
sel.innerHTML = '<option value="__all__">All projects</option>';
|
|
3121
|
+
}
|
|
3122
|
+
await fetchMemoryEntries('__all__');
|
|
3123
|
+
}
|
|
2926
3124
|
|
|
2927
3125
|
|
|
2928
3126
|
/* ════════════════════════════════════════════
|
{memstack_skill_loader-4.4.0 → memstack_skill_loader-4.5.0}/src/memstack_skill_loader/dashboard.py
RENAMED
|
@@ -427,6 +427,118 @@ def _get_diary_entries() -> list[dict]:
|
|
|
427
427
|
return entries
|
|
428
428
|
|
|
429
429
|
|
|
430
|
+
# ── Memory Browser (project-aware diary reader) ──────────────────────────────
|
|
431
|
+
# Reuses the markdown diary record (agent autosaves in ~/.memstack/diary + each
|
|
432
|
+
# project's memory/sessions) but, unlike _get_diary_entries above, resolves
|
|
433
|
+
# projects EXPLICITLY instead of inheriting the launch cwd.
|
|
434
|
+
|
|
435
|
+
_MB_WORKDIR_RE = re.compile(r"(?:Working directory|Project)\s*[:=]\s*(.+)", re.IGNORECASE)
|
|
436
|
+
_MB_PROJECTS_PATH_RE = re.compile(r"([A-Za-z]:[\\/]+Projects[\\/]+[A-Za-z0-9_.+\- ]+)")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _mb_basename(path_str: str) -> str | None:
|
|
440
|
+
"""Last non-empty path segment of a filesystem path."""
|
|
441
|
+
path_str = (path_str or "").strip().strip('"').strip()
|
|
442
|
+
if not path_str:
|
|
443
|
+
return None
|
|
444
|
+
parts = [p for p in re.split(r"[\\/]+", path_str.rstrip("\\/")) if p]
|
|
445
|
+
return parts[-1] if parts else None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _mb_extract_workdir(content: str) -> str | None:
|
|
449
|
+
"""Best-effort absolute working dir for an agent diary (no structured field exists)."""
|
|
450
|
+
head = content[:1500]
|
|
451
|
+
m = _MB_WORKDIR_RE.search(head)
|
|
452
|
+
if m:
|
|
453
|
+
cand = m.group(1).strip().strip('"').strip()
|
|
454
|
+
if cand and re.match(r"^[A-Za-z]:[\\/]", cand):
|
|
455
|
+
return cand
|
|
456
|
+
m2 = _MB_PROJECTS_PATH_RE.search(head)
|
|
457
|
+
if m2:
|
|
458
|
+
return m2.group(1).strip()
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _mb_read_md_dir(diary_dir: Path, source_type: str, project_override: str | None = None) -> list[dict]:
|
|
463
|
+
"""Read *.md from one directory into browser entries."""
|
|
464
|
+
out = []
|
|
465
|
+
if not diary_dir.exists():
|
|
466
|
+
return out
|
|
467
|
+
for f in diary_dir.glob("*.md"):
|
|
468
|
+
try:
|
|
469
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
470
|
+
except Exception:
|
|
471
|
+
continue
|
|
472
|
+
name = f.stem
|
|
473
|
+
date_part = name[:10] if len(name) >= 10 else name
|
|
474
|
+
first_line = content.split("\n", 1)[0].strip("#").strip()
|
|
475
|
+
if project_override is not None:
|
|
476
|
+
project = project_override
|
|
477
|
+
else:
|
|
478
|
+
wd = _mb_extract_workdir(content)
|
|
479
|
+
project = (_mb_basename(wd) if wd else None) or "Agent Runs"
|
|
480
|
+
try:
|
|
481
|
+
mtime = f.stat().st_mtime
|
|
482
|
+
except OSError:
|
|
483
|
+
mtime = 0.0
|
|
484
|
+
out.append({
|
|
485
|
+
"date": date_part,
|
|
486
|
+
"filename": f.name,
|
|
487
|
+
"title": first_line or name,
|
|
488
|
+
"project": project,
|
|
489
|
+
"source_type": source_type,
|
|
490
|
+
"content": content,
|
|
491
|
+
"mtime": mtime,
|
|
492
|
+
})
|
|
493
|
+
return out
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _resolve_diary_project_roots() -> dict[str, Path]:
|
|
497
|
+
"""Resolve project roots that hold a memory/sessions dir, WITHOUT inheriting cwd.
|
|
498
|
+
|
|
499
|
+
Candidates: recent project dirs (Store B telemetry), working dirs harvested from
|
|
500
|
+
agent diaries, and the launch cwd. Only roots with memory/sessions are kept.
|
|
501
|
+
"""
|
|
502
|
+
candidates: list[str] = []
|
|
503
|
+
try:
|
|
504
|
+
candidates.extend(memory_db.get_recent_project_dirs(limit=25))
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
candidates.append(os.getcwd())
|
|
508
|
+
agent_dir = Path.home() / ".memstack" / "diary"
|
|
509
|
+
if agent_dir.exists():
|
|
510
|
+
for f in agent_dir.glob("*.md"):
|
|
511
|
+
try:
|
|
512
|
+
wd = _mb_extract_workdir(f.read_text(encoding="utf-8", errors="replace"))
|
|
513
|
+
except Exception:
|
|
514
|
+
wd = None
|
|
515
|
+
if wd:
|
|
516
|
+
candidates.append(wd)
|
|
517
|
+
roots: dict[str, Path] = {}
|
|
518
|
+
for c in candidates:
|
|
519
|
+
if not c:
|
|
520
|
+
continue
|
|
521
|
+
try:
|
|
522
|
+
root = Path(c)
|
|
523
|
+
except (TypeError, ValueError):
|
|
524
|
+
continue
|
|
525
|
+
if (root / "memory" / "sessions").is_dir():
|
|
526
|
+
roots[root.name] = root
|
|
527
|
+
return roots
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _collect_browser_entries(project: str | None = None) -> list[dict]:
|
|
531
|
+
"""All diary entries across agent autosaves + every resolved project's sessions."""
|
|
532
|
+
entries: list[dict] = []
|
|
533
|
+
entries.extend(_mb_read_md_dir(Path.home() / ".memstack" / "diary", "agent"))
|
|
534
|
+
for proj_name, root in _resolve_diary_project_roots().items():
|
|
535
|
+
entries.extend(_mb_read_md_dir(root / "memory" / "sessions", "direct", project_override=proj_name))
|
|
536
|
+
entries.sort(key=lambda e: e["mtime"], reverse=True)
|
|
537
|
+
if project and project != "__all__":
|
|
538
|
+
entries = [e for e in entries if e["project"] == project]
|
|
539
|
+
return entries
|
|
540
|
+
|
|
541
|
+
|
|
430
542
|
class _DashboardServer(HTTPServer):
|
|
431
543
|
allow_reuse_address = False
|
|
432
544
|
|
|
@@ -486,6 +598,23 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
486
598
|
elif self.path == "/api/diary":
|
|
487
599
|
body = json.dumps(_get_diary_entries()).encode()
|
|
488
600
|
self._respond(200, "application/json", body)
|
|
601
|
+
elif self.path.startswith("/api/memory-browser/projects"):
|
|
602
|
+
entries = _collect_browser_entries()
|
|
603
|
+
counts: dict[str, int] = {}
|
|
604
|
+
for e in entries:
|
|
605
|
+
counts[e["project"]] = counts.get(e["project"], 0) + 1
|
|
606
|
+
data = sorted(
|
|
607
|
+
({"project": p, "count": n} for p, n in counts.items()),
|
|
608
|
+
key=lambda d: (-d["count"], d["project"].lower()),
|
|
609
|
+
)
|
|
610
|
+
body = json.dumps(data).encode()
|
|
611
|
+
self._respond(200, "application/json", body)
|
|
612
|
+
elif self.path.startswith("/api/memory-browser/entries"):
|
|
613
|
+
parsed = urlparse(self.path)
|
|
614
|
+
qs = parse_qs(parsed.query)
|
|
615
|
+
project = qs.get("project", ["__all__"])[0]
|
|
616
|
+
body = json.dumps(_collect_browser_entries(project)).encode()
|
|
617
|
+
self._respond(200, "application/json", body)
|
|
489
618
|
elif self.path == "/api/projects":
|
|
490
619
|
body = json.dumps(get_project_details()).encode()
|
|
491
620
|
self._respond(200, "application/json", body)
|