memstack-skill-loader 4.4.0rc1__tar.gz → 4.5.1__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.
Files changed (40) hide show
  1. {memstack_skill_loader-4.4.0rc1/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.5.1}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/README.md +19 -15
  3. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/pyproject.toml +1 -1
  4. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/__init__.py +1 -1
  5. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/agent_runner.py +1 -1
  6. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/dashboard.html +262 -13
  7. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/dashboard.py +212 -19
  8. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/license.py +326 -49
  9. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/server.py +2 -1
  10. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/stats_tracker.py +12 -3
  11. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/server.py +325 -7
  12. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  13. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader.egg-info/SOURCES.txt +4 -1
  14. memstack_skill_loader-4.5.1/tests/test_license_grace.py +604 -0
  15. memstack_skill_loader-4.5.1/tests/test_pro_skills_update.py +281 -0
  16. memstack_skill_loader-4.5.1/tests/test_skill_drift.py +36 -0
  17. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/MANIFEST.in +0 -0
  18. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/setup.cfg +0 -0
  19. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/__main__.py +0 -0
  20. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/categories.py +0 -0
  21. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/compression.py +0 -0
  22. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/config.py +0 -0
  23. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/indexer.py +0 -0
  24. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/memory_db.py +0 -0
  25. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/__init__.py +0 -0
  26. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/_diag.py +0 -0
  27. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
  28. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/compressor.py +0 -0
  29. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
  30. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
  31. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/search.py +0 -0
  32. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/skill_config.py +0 -0
  33. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/stats.py +0 -0
  34. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/tfidf_search.py +0 -0
  35. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader/version_check.py +0 -0
  36. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  37. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  38. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  39. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
  40. {memstack_skill_loader-4.4.0rc1 → memstack_skill_loader-4.5.1}/tests/test_pro_compressor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.4.0rc1
3
+ Version: 4.5.1
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -1,22 +1,26 @@
1
1
  # MemStack™ Skill Loader
2
2
 
3
- **127 skills for Claude Code** 84 free + 43 Pro exclusive. Vector-indexed so CC loads only the skill it needs, saving your context window.
3
+ **128 skills for Claude Code:** 85 free + 43 Pro exclusive. Vector-indexed so CC loads only the skill it needs, saving your context window.
4
4
 
5
5
  ## Quick Start (5 minutes)
6
6
 
7
- 1. **Install from PyPI:**
7
+ 1. **Get the free skills (in Claude Code):** Installs the plugin that provides the 85 free skills.
8
+ ```
9
+ /plugin marketplace add cwinvestments/memstack
10
+ /plugin install memstack@cwinvestments-memstack
11
+ ```
12
+
13
+ 2. **Install the engine (in your terminal):** Vector-indexes the skills so Claude Code loads only what it needs.
8
14
  ```bash
9
15
  pip install memstack-skill-loader
10
16
  ```
11
17
 
12
- 2. **Register with Claude Code:**
18
+ 3. **Register the engine with Claude Code (in your terminal):**
13
19
  ```bash
14
20
  claude mcp add --scope user memstack-skills -- python -m memstack_skill_loader
15
21
  ```
16
22
 
17
- 3. **Restart Claude Code**, then type `list skills` to verify.
18
-
19
- 4. **Activate Pro** (if purchased at [memstack.pro](https://memstack.pro)):
23
+ 4. **Restart Claude Code, then activate (in Claude Code):** Use `key="free"` for the free tier, or your Pro key from [memstack.pro](https://memstack.pro). Then type `list skills` to verify.
20
24
  ```
21
25
  activate_license(key="your-key-here", email="you@example.com")
22
26
  ```
@@ -27,11 +31,11 @@
27
31
 
28
32
  ## How It Works
29
33
 
30
- MCP server that vector-indexes all 127 MemStack™ skills so Claude Code can call `find_skill("deploy to Railway")` and load **only** the relevant skill on demand instead of all skills consuming context window.
34
+ MCP server that vector-indexes all 128 MemStack™ skills so Claude Code can call `find_skill("deploy to Railway")` and load **only** the relevant skill on demand, instead of all skills consuming context window.
31
35
 
32
- - **No API keys required** everything runs locally
33
- - **Pro skills auto-detected** set your license key and they appear automatically
34
- - **Auto-reindex on start** skills stay current without manual rebuilds
36
+ - **No API keys required:** everything runs locally
37
+ - **Pro skills auto-detected:** set your license key and they appear automatically
38
+ - **Auto-reindex on start:** skills stay current without manual rebuilds
35
39
 
36
40
  ### Environment Variable Override
37
41
 
@@ -99,7 +103,7 @@ The `config.json` file controls where skills are loaded from:
99
103
  }
100
104
  ```
101
105
 
102
- Pro skills are **auto-detected** when `MEMSTACK_PRO_LICENSE_KEY` is set no need to add them to `config.json`.
106
+ Pro skills are **auto-detected** when `MEMSTACK_PRO_LICENSE_KEY` is set, no need to add them to `config.json`.
103
107
 
104
108
  Add entries to `skill_sources` to index skills from multiple directories:
105
109
 
@@ -123,8 +127,8 @@ Add entries to `skill_sources` to index skills from multiple directories:
123
127
  ```
124
128
 
125
129
  The `pattern` field controls how skills are discovered:
126
- - `**/SKILL.md` Subdirectory structure (e.g., `category/skill-name/SKILL.md`)
127
- - `*.md` Flat directory (each `.md` file is a skill)
130
+ - `**/SKILL.md`: Subdirectory structure (e.g., `category/skill-name/SKILL.md`)
131
+ - `*.md`: Flat directory (each `.md` file is a skill)
128
132
 
129
133
  ## Release Notes
130
134
 
@@ -141,8 +145,8 @@ The `pattern` field controls how skills are discovered:
141
145
  - Token usage tracking with estimated costs
142
146
  - TokenStack™ proxy integration (~35-40% token savings)
143
147
 
144
- **[v3.4.0](https://github.com/cwinvestments/memstack-skill-loader/releases/tag/v3.4.0)** 100 Skills Milestone (18 new Pro skills, auto-detection, display name fixes)
148
+ **[v3.4.0](https://github.com/cwinvestments/memstack-skill-loader/releases/tag/v3.4.0)**: 100 Skills Milestone (18 new Pro skills, auto-detection, display name fixes)
145
149
 
146
150
  ## License
147
151
 
148
- Proprietary Part of MemStack™ Pro by CW Affiliate Investments LLC.
152
+ Proprietary. Part of MemStack™ Pro by CW Affiliate Investments LLC.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.4.0rc1"
7
+ version = "4.5.1"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.4.0rc1"
3
+ __version__ = "4.5.1"
@@ -67,7 +67,7 @@ MAX_ITERATIONS = 9999
67
67
  ANTHROPIC_BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "")
68
68
  API_KEY_FILE = Path.home() / ".memstack" / "api_key"
69
69
  OAUTH_TOKEN_FILE = Path.home() / ".memstack" / "oauth_token"
70
- API_DEFAULT_MODEL = "claude-sonnet-4-20250514"
70
+ API_DEFAULT_MODEL = "claude-sonnet-4-6"
71
71
  API_MAX_TOKENS = 16000
72
72
 
73
73
  # API key loading moved to _load_api_key() — called on-demand per agent invocation.
@@ -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 {
@@ -1310,6 +1343,7 @@
1310
1343
  <span id="proxy-indicator" class="health-indicator" title="TokenStack Proxy">
1311
1344
  <span class="health-dot" id="proxy-dot"></span>
1312
1345
  <span class="health-text" id="proxy-text">Proxy: Checking...</span>
1346
+ <span id="proxy-tier-badge" style="display:none;font-size:0.62rem;padding:1px 7px;border-radius:8px;margin-left:0.4rem;vertical-align:middle;font-weight:600;letter-spacing:0.03em;"></span>
1313
1347
  </span>
1314
1348
  </h2>
1315
1349
  <p class="meta">Auto-refreshes every 30s &middot; <a href="#" id="refresh-btn">Refresh Now</a></p>
@@ -1655,13 +1689,18 @@
1655
1689
 
1656
1690
  <!-- ═══════ Memory Browser ═══════ -->
1657
1691
  <div id="page-memory-browser" class="page">
1658
- <div class="placeholder-page">
1659
- <div class="placeholder-card">
1660
- <div class="icon">&#128218;</div>
1661
- <h3>Memory Browser</h3>
1662
- <div class="version-badge">Coming in v4.1</div>
1663
- <p>Search across sessions, lessons, and facts from your development history. Memory capture pipelines are being refined for the next release.</p>
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>
1664
1701
  </div>
1702
+ <div id="mb-list" class="diary-list"></div>
1703
+ <div id="mb-detail" class="mb-detail" style="display:none;"></div>
1665
1704
  </div>
1666
1705
  </div>
1667
1706
 
@@ -1917,8 +1956,8 @@
1917
1956
  <div class="settings-model-col">
1918
1957
  <label class="settings-model-label">Manager Model</label>
1919
1958
  <select id="settings-model-manager" class="settings-model-select" onchange="saveModelPrefs()">
1920
- <option value="claude-opus-4-8">claude-opus-4-8</option>
1921
- <option value="claude-opus-4-6" selected>claude-opus-4-6</option>
1959
+ <option value="claude-opus-4-8" selected>claude-opus-4-8</option>
1960
+ <option value="claude-opus-4-6">claude-opus-4-6</option>
1922
1961
  <option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
1923
1962
  <option value="claude-haiku-4-5">claude-haiku-4-5</option>
1924
1963
  </select>
@@ -2193,6 +2232,7 @@ document.querySelectorAll('.nav-item').forEach(item => {
2193
2232
  if (page === 'overview' && !projectsLoaded) loadProjects();
2194
2233
  if (page === 'overview') { updateWelcomeGreeting(); loadDiary(); }
2195
2234
  if (page === 'agent-monitor') loadAgentMonitor();
2235
+ if (page === 'memory-browser') loadMemoryBrowser();
2196
2236
  if (page === 'burn-report') loadBurnReport();
2197
2237
  if (page === 'settings') loadSettings();
2198
2238
  if (page !== 'agent-monitor') stopAgentRefresh();
@@ -2921,7 +2961,166 @@ async function loadProjects() {
2921
2961
  }
2922
2962
  }
2923
2963
 
2924
- /* Memory Browser — placeholder, full implementation coming in v4.1 */
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">&larr; 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
+ }
2925
3124
 
2926
3125
 
2927
3126
  /* ════════════════════════════════════════════
@@ -3206,7 +3405,7 @@ let busyPhraseIntervals = {};
3206
3405
  let busyPhraseIndices = {manager: 0, builder: 0, reviewer: 0};
3207
3406
  let elapsedIntervals = {};
3208
3407
 
3209
- const MODEL_DEFAULTS = {manager: 'claude-opus-4-6', builder: 'claude-sonnet-4-6', reviewer: 'claude-sonnet-4-6'};
3408
+ const MODEL_DEFAULTS = {manager: 'claude-opus-4-8', builder: 'claude-sonnet-4-6', reviewer: 'claude-sonnet-4-6'};
3210
3409
 
3211
3410
  function saveModelPrefs() {
3212
3411
  ['manager', 'builder', 'reviewer'].forEach(role => {
@@ -3284,6 +3483,28 @@ async function startAgentTask() {
3284
3483
  const btn = document.getElementById('agent-start-btn');
3285
3484
  btn.disabled = true; btn.textContent = 'Starting...';
3286
3485
  try {
3486
+ if (workDir) {
3487
+ let pf = null;
3488
+ try {
3489
+ const pfRes = await fetch('/api/git-branches?workdir=' + encodeURIComponent(workDir), {headers: AUTH_GET});
3490
+ pf = await pfRes.json();
3491
+ } catch(e) { pf = null; }
3492
+ if (pf && pf.is_repo === false) {
3493
+ const doInit = confirm("This folder isn't a git repository. The agents use git to track changes.\n\nInitialize a new git repository in this exact folder?\n\n" + workDir);
3494
+ if (!doInit) { return; }
3495
+ btn.textContent = 'Initializing git...';
3496
+ let initData = null;
3497
+ try {
3498
+ const initRes = await fetch('/api/git-init', {method:'POST', headers: AUTH_HEADERS, body: JSON.stringify({workdir: workDir})});
3499
+ initData = await initRes.json();
3500
+ } catch(e) { initData = {success: false, error: e.message}; }
3501
+ if (!initData || !initData.success) {
3502
+ alert("Could not initialize a git repository in:\n" + workDir + (initData && initData.error ? "\n\nDetails: " + initData.error : ""));
3503
+ return;
3504
+ }
3505
+ await fetchBranches(workDir);
3506
+ }
3507
+ }
3287
3508
  const branchSel = document.getElementById('agent-branch-select');
3288
3509
  const branchRow = document.getElementById('agent-branch-row');
3289
3510
  const selectedBranch = (branchRow && branchRow.style.display !== 'none' && branchSel && branchSel.value) ? branchSel.value : '';
@@ -3294,7 +3515,19 @@ async function startAgentTask() {
3294
3515
  btn.textContent = 'Checking out branch...';
3295
3516
  const coRes = await fetch('/api/git-checkout', {method:'POST', headers: AUTH_HEADERS, body: JSON.stringify({workdir: workDir, branch: selectedBranch})});
3296
3517
  const coData = await coRes.json();
3297
- if (!coData.success) { alert('Branch checkout failed: ' + (coData.error || 'Unknown error')); return; }
3518
+ if (!coData.success) {
3519
+ const raw = (coData.error || '').trim();
3520
+ let msg = "Could not switch to branch '" + selectedBranch + "'.";
3521
+ if (/not a git repository/i.test(raw)) {
3522
+ msg = "This folder isn't a git repository, so the branch could not be checked out.\n\nFolder: " + workDir;
3523
+ } else if (/local changes|would be overwritten|uncommitted/i.test(raw)) {
3524
+ msg = "Cannot switch to '" + selectedBranch + "' because there are uncommitted changes. Commit or stash them first.";
3525
+ } else if (raw) {
3526
+ msg += "\n\nDetails: " + raw;
3527
+ }
3528
+ alert(msg);
3529
+ return;
3530
+ }
3298
3531
  }
3299
3532
  }
3300
3533
  let finalTask = task;
@@ -3911,12 +4144,28 @@ async function checkProxyHealth() {
3911
4144
  dot.className = 'health-dot connected';
3912
4145
  text.className = 'health-text connected';
3913
4146
  const session = data.session_avg_savings_pct || data.total_savings_pct || 0;
3914
- const lifetime = data.lifetime_avg || 0;
3915
- text.textContent = 'Proxy: Connected (' + session.toFixed(0) + '% session | ' + lifetime.toFixed(0) + '% lifetime)';
4147
+ const last30d = data.lifetime_avg || 0;
4148
+ text.textContent = 'Proxy: Connected (' + session.toFixed(0) + '% session | ' + last30d.toFixed(0) + '% 30d)';
4149
+ const badge = document.getElementById('proxy-tier-badge');
4150
+ if (badge) {
4151
+ if (data.tier === 'pro') {
4152
+ badge.textContent = 'PRO';
4153
+ badge.style.cssText = badge.style.cssText.replace(/display:[^;]*;?/, '') +
4154
+ ';background:rgba(210,153,34,0.15);color:#d29922;border:1px solid rgba(210,153,34,0.3);display:inline-block;';
4155
+ } else if (data.tier === 'free') {
4156
+ badge.textContent = 'FREE';
4157
+ badge.style.cssText = badge.style.cssText.replace(/display:[^;]*;?/, '') +
4158
+ ';background:rgba(139,148,158,0.15);color:#8b949e;border:1px solid rgba(139,148,158,0.2);display:inline-block;';
4159
+ } else {
4160
+ badge.style.display = 'none';
4161
+ }
4162
+ }
3916
4163
  } catch(e) {
3917
4164
  dot.className = 'health-dot disconnected';
3918
4165
  text.className = 'health-text disconnected';
3919
4166
  text.textContent = 'Proxy: Disconnected';
4167
+ const badge = document.getElementById('proxy-tier-badge');
4168
+ if (badge) badge.style.display = 'none';
3920
4169
  }
3921
4170
  }
3922
4171