memstack-skill-loader 4.5.1__tar.gz → 4.7.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.

Potentially problematic release.


This version of memstack-skill-loader might be problematic. Click here for more details.

Files changed (45) hide show
  1. {memstack_skill_loader-4.5.1/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.7.0}/PKG-INFO +1 -1
  2. memstack_skill_loader-4.7.0/README.md +218 -0
  3. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/pyproject.toml +1 -1
  4. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/__init__.py +1 -1
  5. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/categories.py +40 -13
  6. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/dashboard.html +21 -0
  7. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/dashboard.py +9 -1
  8. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/license.py +49 -2
  9. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/body_parser.py +24 -1
  10. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/pro_compressor.py +174 -45
  11. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/stats_tracker.py +58 -3
  12. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/server.py +13 -2
  13. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  14. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +3 -0
  15. memstack_skill_loader-4.7.0/tests/test_body_parser_metric.py +138 -0
  16. memstack_skill_loader-4.7.0/tests/test_code_read_savings.py +64 -0
  17. memstack_skill_loader-4.7.0/tests/test_license_environment.py +86 -0
  18. memstack_skill_loader-4.7.0/tests/test_pro_compressor.py +688 -0
  19. memstack_skill_loader-4.5.1/README.md +0 -152
  20. memstack_skill_loader-4.5.1/tests/test_pro_compressor.py +0 -384
  21. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/MANIFEST.in +0 -0
  22. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/setup.cfg +0 -0
  23. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/__main__.py +0 -0
  24. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/agent_runner.py +0 -0
  25. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/compression.py +0 -0
  26. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/config.py +0 -0
  27. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/indexer.py +0 -0
  28. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/memory_db.py +0 -0
  29. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
  30. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
  31. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
  32. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
  33. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/proxy/server.py +0 -0
  34. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/search.py +0 -0
  35. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/skill_config.py +0 -0
  36. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/stats.py +0 -0
  37. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
  38. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader/version_check.py +0 -0
  39. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  40. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  41. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  42. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
  43. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/tests/test_license_grace.py +0 -0
  44. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/tests/test_pro_skills_update.py +0 -0
  45. {memstack_skill_loader-4.5.1 → memstack_skill_loader-4.7.0}/tests/test_skill_drift.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.5.1
3
+ Version: 4.7.0
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
@@ -0,0 +1,218 @@
1
+ # MemStack™ Skill Loader
2
+
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
+
5
+ ## Quick Start (5 minutes)
6
+
7
+ MemStack installs in two parts: the **skills** (via the Claude Code plugin marketplace) and the **engine** (this loader, via PyPI). You need both. Every command below is labeled **(in Claude Code)** or **(in terminal)** — running one in the wrong place is the most common setup mistake.
8
+
9
+ **Step 1 — Install the free skills · (in Claude Code):**
10
+ ```
11
+ /plugin marketplace add cwinvestments/memstack
12
+ /plugin install memstack@cwinvestments-memstack
13
+ ```
14
+ Run both commands. This unlocks the 85 free skills right away.
15
+
16
+ > **SSH error?** ("Host key verification failed" on a fresh server that's never used GitHub over SSH.)
17
+ > **Default fix · (in terminal)** — rewrite GitHub to HTTPS, then retry Step 1:
18
+ > ```bash
19
+ > git config --global url."https://github.com/".insteadOf "git@github.com:"
20
+ > ```
21
+ > **Backup fix · (in terminal)** — add GitHub's host key, then retry Step 1:
22
+ > ```bash
23
+ > mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
24
+ > ```
25
+
26
+ **Step 2 — Install the engine · (in terminal):**
27
+
28
+ Windows:
29
+ ```bash
30
+ pip install memstack-skill-loader
31
+ ```
32
+
33
+ Linux / Mac — use the explicit interpreter (the same one you register in Step 3):
34
+ ```bash
35
+ /usr/bin/python3 -m pip install memstack-skill-loader --break-system-packages
36
+ ```
37
+ Find yours with `which python3` and substitute it if it isn't `/usr/bin/python3`. The `--break-system-packages` flag is required on newer externally-managed Python. No pip for that interpreter? Run `sudo apt install python3-pip` first (Debian/Ubuntu).
38
+
39
+ **Step 3 — Register the MCP server · (in terminal):** register against the **same interpreter you installed onto in Step 2.**
40
+
41
+ Windows:
42
+ ```bash
43
+ claude mcp add --scope user memstack-skills -- python -m memstack_skill_loader
44
+ ```
45
+
46
+ Linux / Mac:
47
+ ```bash
48
+ claude mcp add --scope user memstack-skills -- /usr/bin/python3 -m memstack_skill_loader
49
+ ```
50
+ If this doesn't match Step 2's interpreter, the server won't launch (you'll see a "failed to reconnect" error and `activate_license` will be missing).
51
+
52
+ **Step 4 — Activate your license · (in Claude Code, after a full restart):** fully quit and reopen Claude Code first so it picks up the new MCP server, then run:
53
+ ```
54
+ activate_license(key="your-key", email="you@example.com")
55
+ ```
56
+ Use `key="free"` for the free tier, or your Pro key from [memstack.pro](https://memstack.pro) to unlock all 43 Pro skills (85 free + 43 Pro = 128 total). Then type `list skills` to verify.
57
+
58
+ > To override the skills path, set `MEMSTACK_SKILLS_DIR=/path/to/your/memstack/skills`.
59
+
60
+ > See [QUICKSTART.md](QUICKSTART.md) for detailed setup, [QUICK-REFERENCE.md](QUICK-REFERENCE.md) for the full skill catalog, and [TROUBLESHOOTING.md](TROUBLESHOOTING.md) if you hit issues.
61
+
62
+ ## Troubleshooting
63
+
64
+ **`error: externally-managed-environment` (during Step 2)** — pip refuses to install on a system-managed Python. Add `--break-system-packages` · (in terminal):
65
+ ```bash
66
+ /usr/bin/python3 -m pip install memstack-skill-loader --break-system-packages
67
+ ```
68
+
69
+ **Plugin clone fails: "Host key verification failed" (during Step 1)** — a fresh server that's never connected to GitHub over SSH. **Default fix · (in terminal):** `git config --global url."https://github.com/".insteadOf "git@github.com:"` then retry Step 1. **Backup fix · (in terminal):** `mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts` then retry Step 1.
70
+
71
+ **`activate_license` not found, or MCP "failed to reconnect (-32000)"** — the loader was installed onto a **different Python interpreter** than the one Claude Code launches (the classic bare-`python` mismatch on Linux/Mac). Check · (in terminal):
72
+ ```bash
73
+ /usr/bin/python3 -c "import memstack_skill_loader; print('ok')"
74
+ ```
75
+ If it prints `ok`, make sure Step 3 registered that exact path, then fully restart Claude Code. If it errors with `ModuleNotFoundError`, reinstall onto the explicit interpreter and re-register against that same path · (in terminal):
76
+ ```bash
77
+ /usr/bin/python3 -m pip install memstack-skill-loader --break-system-packages
78
+ claude mcp add --scope user memstack-skills -- /usr/bin/python3 -m memstack_skill_loader
79
+ ```
80
+ Then fully restart Claude Code. Rule of thumb: whatever interpreter you `import`-check as `ok` is the path that must appear in your `claude mcp add` command.
81
+
82
+ **`No module named pip` (during Step 2)** — that interpreter ships without pip (common on minimal Debian/Ubuntu server images). Install it, then retry the Step 2 install · (in terminal):
83
+ ```bash
84
+ sudo apt install python3-pip
85
+ ```
86
+
87
+ ## How It Works
88
+
89
+ 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.
90
+
91
+ - **No Anthropic API key required:** skill search and indexing run locally — the only outbound network call is license validation, which contacts the MemStack license server on session start ([Data and privacy](#data-and-privacy))
92
+ - **Pro skills auto-detected:** set your license key and they appear automatically
93
+ - **Auto-reindex on start:** skills stay current without manual rebuilds
94
+
95
+ ### Data and privacy
96
+
97
+ License validation is the loader's only outbound network call. On session start it POSTs to the MemStack license server (`admin.cwaffiliateinvestments.com/api/licenses/validate`) with:
98
+
99
+ - your **license key**
100
+ - a **machine ID** — a stable, hashed fingerprint that binds the license to your machine
101
+ - your **email**, if you provided one at activation
102
+ - your **environment** — OS name, Python version, and MemStack loader version
103
+
104
+ That is the complete payload. **No code, file contents, file paths, usernames, project names, or prompts are ever sent.** The environment fields exist only so platform-specific breakage (for example, a bug that affects only Linux) is visible to us in aggregate.
105
+
106
+ ### Environment Variable Override
107
+
108
+ Set `MEMSTACK_SKILLS_DIR` to override the skills path in `config.json`:
109
+
110
+ ```bash
111
+ export MEMSTACK_SKILLS_DIR=/path/to/your/memstack/skills
112
+ ```
113
+
114
+ ## Stack
115
+
116
+ - Python 3.12+
117
+ - [MCP SDK](https://pypi.org/project/mcp/) (stdio transport)
118
+ - [LanceDB](https://lancedb.com/) (vector storage, zero external dependencies)
119
+ - [sentence-transformers](https://www.sbert.net/) with `all-MiniLM-L6-v2` (384-dim local embeddings)
120
+
121
+ ## Available Tools
122
+
123
+ ### `find_skill`
124
+ Search skills by describing what you need. Returns the most relevant skill(s) with full instructions.
125
+
126
+ ```
127
+ find_skill(query="deploy to Railway", top_k=3)
128
+ ```
129
+
130
+ ### `list_skills`
131
+ Browse the full skill catalog with names and descriptions.
132
+
133
+ ```
134
+ list_skills()
135
+ ```
136
+
137
+ ### `get_skill`
138
+ Load a specific skill by exact name.
139
+
140
+ ```
141
+ get_skill(name="railway deploy")
142
+ ```
143
+
144
+ ### `reindex_skills`
145
+ Rebuild the vector index after adding or modifying skills.
146
+
147
+ ```
148
+ reindex_skills()
149
+ ```
150
+
151
+ ## Configuration
152
+
153
+ The `config.json` file controls where skills are loaded from:
154
+
155
+ ```json
156
+ {
157
+ "skill_sources": [
158
+ {
159
+ "type": "local",
160
+ "path": "C:\\Projects\\memstack\\skills",
161
+ "pattern": "**/SKILL.md",
162
+ "label": "MemStack"
163
+ }
164
+ ],
165
+ "embedding_model": "all-MiniLM-L6-v2",
166
+ "default_top_k": 3,
167
+ "vector_db_path": "./vectors",
168
+ "auto_reindex_on_start": true
169
+ }
170
+ ```
171
+
172
+ Pro skills are **auto-detected** when `MEMSTACK_PRO_LICENSE_KEY` is set, no need to add them to `config.json`.
173
+
174
+ Add entries to `skill_sources` to index skills from multiple directories:
175
+
176
+ ```json
177
+ {
178
+ "skill_sources": [
179
+ {
180
+ "type": "local",
181
+ "path": "C:\\Projects\\memstack\\skills",
182
+ "pattern": "**/SKILL.md",
183
+ "label": "MemStack"
184
+ },
185
+ {
186
+ "type": "local",
187
+ "path": "/home/user/custom-skills",
188
+ "pattern": "*.md",
189
+ "label": "My Custom Skills"
190
+ }
191
+ ]
192
+ }
193
+ ```
194
+
195
+ The `pattern` field controls how skills are discovered:
196
+ - `**/SKILL.md`: Subdirectory structure (e.g., `category/skill-name/SKILL.md`)
197
+ - `*.md`: Flat directory (each `.md` file is a skill)
198
+
199
+ ## Release Notes
200
+
201
+ ### v4.0.0 (May 2026)
202
+
203
+ - Dashboard: 6-page localhost dashboard (Overview, Skills Manager, Burn Report, Memory Browser, Agent Monitor, Settings)
204
+ - Agent Runner: 3-agent orchestration (Manager/Builder/Reviewer) with per-agent model selection
205
+ - 17 MCP tools
206
+ - 127 skills (84 free + 43 Pro)
207
+ - Real-time context window monitoring per agent
208
+ - Session diary with AI-authored markdown narratives
209
+ - Safe git staging (prevents accidental commits of secrets/runtime data)
210
+ - Task completion notifications (browser, tab flash, audio)
211
+ - Token usage tracking with estimated costs
212
+ - TokenStack™ proxy integration (~35-40% token savings)
213
+
214
+ **[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)
215
+
216
+ ## License
217
+
218
+ 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.5.1"
7
+ version = "4.7.0"
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.5.1"
3
+ __version__ = "4.7.0"
@@ -12,7 +12,8 @@ CATEGORY_MAP = {
12
12
  "quill": "Business", "scan": "Business", "governor": "Business",
13
13
  "client-onboarding": "Business", "contract-template": "Business", "financial-model": "Business",
14
14
  "freelancer-toolkit": "Business", "invoice-generator": "Business", "scope-of-work": "Business",
15
- "sop-builder": "Business", "proposal-writer": "Business",
15
+ "sop-builder": "Business", "proposal-writer": "Business", "gdpr": "Business",
16
+ "licensing": "Business",
16
17
  # Content
17
18
  "humanize": "Content", "blog-post": "Content", "email-sequence": "Content",
18
19
  "landing-page-copy": "Content", "newsletter": "Content", "product-description": "Content",
@@ -26,7 +27,7 @@ CATEGORY_MAP = {
26
27
  "forge": "Development", "shard": "Development",
27
28
  "state": "Development", "work": "Development", "verify": "Development",
28
29
  "project": "Development", "familiar": "Development", "api-designer": "Development",
29
- "code-reviewer": "Development", "database-architect": "Development", "migration-planner": "Development",
30
+ "code-reviewer": "Development", "migration-planner": "Development",
30
31
  "performance-audit": "Development", "refactor-planner": "Development", "test-writer": "Development",
31
32
  "changelog-generator": "Development", "mentor": "Development", "webapp-testing": "Development",
32
33
  # Core
@@ -45,20 +46,46 @@ CATEGORY_MAP = {
45
46
  "user-story-generator": "Product", "mvp-scoper": "Product", "roadmap-builder": "Product",
46
47
  "feedback-analyzer": "Product",
47
48
  # Security
48
- "advanced-security": "Security", "env-manager-pro": "Security",
49
49
  "api-audit": "Security", "csp-headers": "Security", "dependency-audit": "Security",
50
50
  "owasp-top10": "Security", "owasp-top-10": "Security",
51
51
  "rls-checker": "Security", "rls-guardian": "Security",
52
- "secrets-scanner": "Security",
53
- # Pro skills (for backfill matching)
54
- "api-docs": "Core", "branching": "Core", "consolidate": "Core", "context-db": "Core",
55
- "multi-agent": "Development", "codebase-index": "Development", "doc-index": "Development",
56
- "diagram-generator": "Development", "browser-use": "Development", "session-restore": "Core",
57
- "drift-detection": "Security", "mcp-builder": "Development", "claude-api-helper": "Development",
58
- "test-generator": "Development", "log-analyzer": "Development",
59
- "performance-profiler": "Development", "dependency-auditor": "Security",
60
- "git-worktrees": "Development", "error-handler": "Development", "web-scraper": "Automation",
61
- "hooks-integration": "Development",
52
+ "secrets-scanner": "Security", "git-guard": "Security",
53
+ # ── Pro-exclusive skills (43) CANONICAL category map ─────────────────
54
+ # Must cover every slug in PRO_EXCLUSIVE_SKILLS (license.py) exactly; no Pro
55
+ # slug may fall through to "Other". Enforced by scripts/check_skill_drift.py
56
+ # (Check C / F-3). Core = MemStack's own session/memory/agent/efficiency
57
+ # meta-skills ONLY; anything that operates on the user's code/docs is
58
+ # Development, not Core.
59
+ # Core (9)
60
+ "burn": "Core", "checkpoint": "Core", "consolidate": "Core",
61
+ "context-db": "Core", "council": "Core", "governor-pro": "Core",
62
+ "model-router": "Core", "multi-agent": "Core", "session-restore": "Core",
63
+ # Development (21)
64
+ "api-docs": "Development", "api-load-tester": "Development",
65
+ "branching": "Development", "claude-api-helper": "Development",
66
+ "codebase-index": "Development", "database-architect": "Development",
67
+ "database-migration": "Development", "developer-growth-analysis": "Development",
68
+ "diagram-generator": "Development", "doc-index": "Development",
69
+ "drift-detection": "Development", "error-handler": "Development",
70
+ "frontend-design": "Development", "git-worktrees": "Development",
71
+ "log-analyzer": "Development", "mcp-builder": "Development",
72
+ "nextjs-conventions": "Development", "performance-profiler": "Development",
73
+ "python-conventions": "Development", "rag-builder": "Development",
74
+ "test-generator": "Development",
75
+ # Security (4)
76
+ "advanced-security": "Security", "config-audit": "Security",
77
+ "dependency-auditor": "Security", "env-manager-pro": "Security",
78
+ # Automation (4)
79
+ "browser-use": "Automation", "hooks-integration": "Automation",
80
+ "video-pipeline": "Automation", "web-scraper": "Automation",
81
+ # Business (2)
82
+ "meeting-insights-analyzer": "Business", "us-privacy-compliance": "Business",
83
+ # Content (1)
84
+ "social-media": "Content",
85
+ # Deployment (1)
86
+ "ios-app-store": "Deployment",
87
+ # Marketing (1)
88
+ "gtm-validator": "Marketing",
62
89
  }
63
90
 
64
91
 
@@ -1564,6 +1564,10 @@
1564
1564
  <div class="card">
1565
1565
  <div class="value stat-green" id="ts-avg-savings">-</div>
1566
1566
  <div class="label">Avg Savings</div>
1567
+ <div id="ts-code-read" style="display:none;font-size:0.62rem;color:#6e7681;margin-top:0.35rem;line-height:1.3">
1568
+ <span id="ts-code-read-text"></span>
1569
+ <span id="ts-code-read-info" style="cursor:help;border-bottom:1px dotted #6e7681">&#9432;</span>
1570
+ </div>
1567
1571
  </div>
1568
1572
  <div class="card">
1569
1573
  <div class="value" style="color:#58a6ff" id="ts-requests">-</div>
@@ -5216,6 +5220,21 @@ async function loadTokenStackSavings() {
5216
5220
  avgEl.textContent = (d.avg_savings_pct || 0).toFixed(1) + '%';
5217
5221
  reqEl.textContent = fmt(d.total_requests || 0);
5218
5222
 
5223
+ // Secondary, clearly-subordinate stat: all-time p95 of the AST-fired
5224
+ // (code-read) slice. Hidden unless the slice is big enough to be meaningful.
5225
+ const crEl = document.getElementById('ts-code-read');
5226
+ const crText = document.getElementById('ts-code-read-text');
5227
+ const crInfo = document.getElementById('ts-code-read-info');
5228
+ if (d.code_read_display_pct != null && (d.code_read_sample_size || 0) >= 50) {
5229
+ crText.textContent = 'up to ~' + d.code_read_display_pct + '% on code-heavy reads';
5230
+ crInfo.title = 'Code-heavy = requests where AST truncation fires (file reads of '
5231
+ + 'source code). p95 of that slice (n=' + fmt(d.code_read_sample_size)
5232
+ + ') — not the typical case.';
5233
+ crEl.style.display = '';
5234
+ } else {
5235
+ crEl.style.display = 'none';
5236
+ }
5237
+
5219
5238
  const labels = {free: 'Free', ast: 'AST', json: 'JSON', log: 'Log', path: 'Path',
5220
5239
  system: 'System Prompt', dedup: 'Conversation Dedup', markdown: 'Markdown'};
5221
5240
  const order = ['free', 'ast', 'json', 'log', 'path', 'system', 'dedup', 'markdown'];
@@ -5230,6 +5249,8 @@ async function loadTokenStackSavings() {
5230
5249
  savedEl.textContent = '-';
5231
5250
  avgEl.textContent = '-';
5232
5251
  reqEl.textContent = '-';
5252
+ const crEl = document.getElementById('ts-code-read');
5253
+ if (crEl) crEl.style.display = 'none';
5233
5254
  tbody.innerHTML = '<tr><td colspan="2"><span class="empty-state">No compression data yet. Start the dashboard with --with-proxy.</span></td></tr>';
5234
5255
  }
5235
5256
  }
@@ -30,7 +30,11 @@ from . import agent_runner
30
30
  import subprocess
31
31
  from .skill_config import load_skill_config, save_skill_config, toggle_skill, set_mode, SkillConfig
32
32
  from .stats import DB_PATH, get_dashboard_data, get_project_details, get_skill_fire_counts, get_burn_report_data, get_api_costs
33
- from .proxy.stats_tracker import get_tokenstack_stats, get_lifetime_avg_savings
33
+ from .proxy.stats_tracker import (
34
+ get_tokenstack_stats,
35
+ get_lifetime_avg_savings,
36
+ get_code_read_savings,
37
+ )
34
38
 
35
39
  _HTML_PATH = Path(__file__).parent / "dashboard.html"
36
40
  _LAST_WORKDIR_FILE = Path.home() / ".memstack" / "agent-runner" / "last-workdir.txt"
@@ -795,6 +799,10 @@ class _Handler(BaseHTTPRequestHandler):
795
799
  rng = "all"
796
800
  try:
797
801
  data = get_tokenstack_stats(rng)
802
+ # All-time code-read (AST-fired) p95 capability stat. Merged here
803
+ # rather than inside the range-scoped aggregate so it stays a
804
+ # stable claim independent of the selected time window.
805
+ data.update(get_code_read_savings())
798
806
  except Exception as exc:
799
807
  data = {"error": str(exc)}
800
808
  body = json.dumps(data).encode()
@@ -9,6 +9,7 @@ import hmac as hmac_mod
9
9
  import json
10
10
  import math
11
11
  import os
12
+ import platform
12
13
  import socket
13
14
  import sys
14
15
  import threading
@@ -406,6 +407,50 @@ def save_license_key(key: str) -> None:
406
407
  # ---------------------------------------------------------------------------
407
408
 
408
409
 
410
+ def _environment_info() -> dict[str, str | None]:
411
+ """Coarse environment info attached to every license-validation POST.
412
+
413
+ Purpose: let us see which OS / Python / loader versions our users run, so a
414
+ bug that silently breaks one platform (e.g. the hooks bug that killed skill
415
+ auto-loading on Linux/macOS) is visible in aggregate.
416
+
417
+ Collects ONLY three fields: OS name, running Python version, installed
418
+ loader version. It deliberately sends NO hostname, username, file path,
419
+ project name, or any identifier beyond the machine_id already in the
420
+ payload. Each field is detected independently and fail-safe: any failure
421
+ yields ``None`` (JSON null), never an exception — environment detection
422
+ must NEVER be able to fail a license validation.
423
+
424
+ ``plugin_version`` is intentionally absent. The Claude Code skills-bundle
425
+ plugin is a separate marketplace artifact; its version lives in
426
+ ``.claude-plugin/plugin.json`` inside Claude Code's plugin cache. This
427
+ loader's MCP server is registered standalone
428
+ (``claude mcp add ... python -m memstack_skill_loader``), so the process
429
+ never receives ``CLAUDE_PLUGIN_ROOT`` or any handle to that cache, and the
430
+ two artifacts share no version coupling. There is no reliable value to
431
+ send, so the field is omitted rather than guessed.
432
+ """
433
+ info: dict[str, str | None] = {"os": None, "python_version": None, "loader_version": None}
434
+
435
+ try:
436
+ info["os"] = platform.system() or None
437
+ except Exception:
438
+ pass
439
+
440
+ try:
441
+ info["python_version"] = platform.python_version()
442
+ except Exception:
443
+ pass
444
+
445
+ try:
446
+ from importlib.metadata import version as _pkg_version
447
+ info["loader_version"] = _pkg_version("memstack-skill-loader")
448
+ except Exception:
449
+ pass
450
+
451
+ return info
452
+
453
+
409
454
  async def validate_license(
410
455
  license_key: str | None = None,
411
456
  email: str | None = None,
@@ -644,9 +689,10 @@ async def _validate_locked(key: str, email: str | None) -> LicenseStatus:
644
689
  return cached_status
645
690
 
646
691
  # --- build request body ---
647
- body_payload: dict[str, str] = {"license_key": key, "machine_id": _machine_id()}
692
+ body_payload: dict[str, str | None] = {"license_key": key, "machine_id": _machine_id()}
648
693
  if email:
649
694
  body_payload["email"] = email
695
+ body_payload.update(_environment_info()) # os / python_version / loader_version (fail-safe)
650
696
 
651
697
  # --- always call API on session start ---
652
698
  try:
@@ -710,7 +756,8 @@ def refresh_license_cache(key: str, timeout: float = HTTP_TIMEOUT) -> LicenseSta
710
756
  Returns the new LicenseStatus on success, or None if the server is unreachable
711
757
  or returns an error. All network exceptions are caught internally.
712
758
  """
713
- body_payload: dict[str, str] = {"license_key": key, "machine_id": _machine_id()}
759
+ body_payload: dict[str, str | None] = {"license_key": key, "machine_id": _machine_id()}
760
+ body_payload.update(_environment_info()) # os / python_version / loader_version (fail-safe)
714
761
  # Uses VALIDATE_URL directly via httpx — not routed through ANTHROPIC_BASE_URL (Anthropic SDK only).
715
762
  try:
716
763
  with httpx.Client(timeout=timeout) as client:
@@ -154,8 +154,31 @@ def parse_and_compress(body_dict: dict, tier: str = "free", force_pro: bool = Fa
154
154
  _diag.body_transform("dedup", dedup_saved,
155
155
  note="(cross-message repeated-block removal)")
156
156
 
157
+ # Body-level transforms (system-prompt compression + conversation dedup) remove
158
+ # tokens from content that is already inside total_compressed (the post-block
159
+ # forwarded text), but they run *after* total_compressed was tallied in the
160
+ # message loop. Credit their savings in savings_pct by computing it against the
161
+ # reduced "forwarded" size — but DO NOT mutate total_compressed itself: the
162
+ # stored compressed_tokens must stay a BLOCK-ONLY base so historical (backfilled)
163
+ # and new rows share exactly one convention. The read path (get_tokenstack_stats
164
+ # / get_proxy_stats) reconstructs the body-inclusive tokens-saved total from this
165
+ # same block-only base plus the dedup/system columns, so the headline % and the
166
+ # absolute token count stay consistent. The removals are disjoint from the
167
+ # block-transform deltas (sequential) and from each other, so no double-count;
168
+ # clamp >=0 for the rare sub-threshold case. total_original stays block-scope —
169
+ # folding the system prompt's original size into the denominator is
170
+ # unreproducible by the column-only backfill (sys_orig is not stored) and would
171
+ # dilute the headline. See scripts/verify_metric_fix.py for the live-data proof.
172
+ body_saved = 0
173
+ if pro:
174
+ if sys_t is not None and sys_t.enabled:
175
+ body_saved += savings.get(sys_t.stat_key, 0)
176
+ if dedup_t is not None and dedup_t.enabled:
177
+ body_saved += savings.get(dedup_t.stat_key, 0)
178
+ forwarded = max(0, total_compressed - body_saved)
179
+
157
180
  if total_original > 0:
158
- savings_pct = round((1 - total_compressed / total_original) * 100, 1)
181
+ savings_pct = round((1 - forwarded / total_original) * 100, 1)
159
182
  else:
160
183
  savings_pct = 0.0
161
184