memstack-skill-loader 4.6.0__tar.gz → 4.8.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.
Files changed (43) hide show
  1. {memstack_skill_loader-4.6.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.8.0}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/README.md +15 -4
  3. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/pyproject.toml +1 -1
  4. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/__init__.py +1 -1
  5. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/categories.py +4 -3
  6. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/license.py +50 -3
  7. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/server.py +13 -2
  8. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  9. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +1 -0
  10. memstack_skill_loader-4.8.0/tests/test_license_environment.py +86 -0
  11. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/MANIFEST.in +0 -0
  12. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/setup.cfg +0 -0
  13. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/__main__.py +0 -0
  14. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/agent_runner.py +0 -0
  15. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/compression.py +0 -0
  16. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/config.py +0 -0
  17. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/dashboard.html +0 -0
  18. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/dashboard.py +0 -0
  19. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/indexer.py +0 -0
  20. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/memory_db.py +0 -0
  21. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
  22. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
  23. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
  24. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
  25. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
  26. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
  27. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/server.py +0 -0
  28. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
  29. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/search.py +0 -0
  30. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/skill_config.py +0 -0
  31. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/stats.py +0 -0
  32. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
  33. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader/version_check.py +0 -0
  34. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  35. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  36. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  37. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
  38. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/tests/test_body_parser_metric.py +0 -0
  39. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/tests/test_code_read_savings.py +0 -0
  40. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/tests/test_license_grace.py +0 -0
  41. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/tests/test_pro_compressor.py +0 -0
  42. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.0}/tests/test_pro_skills_update.py +0 -0
  43. {memstack_skill_loader-4.6.0 → memstack_skill_loader-4.8.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.6.0
3
+ Version: 4.8.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
@@ -1,6 +1,6 @@
1
1
  # MemStack™ Skill Loader
2
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.
3
+ **129 skills for Claude Code:** 85 free + 44 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
 
@@ -53,7 +53,7 @@ If this doesn't match Step 2's interpreter, the server won't launch (you'll see
53
53
  ```
54
54
  activate_license(key="your-key", email="you@example.com")
55
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.
56
+ Use `key="free"` for the free tier, or your Pro key from [memstack.pro](https://memstack.pro) to unlock all 44 Pro skills (85 free + 44 Pro = 129 total). Then type `list skills` to verify.
57
57
 
58
58
  > To override the skills path, set `MEMSTACK_SKILLS_DIR=/path/to/your/memstack/skills`.
59
59
 
@@ -86,12 +86,23 @@ sudo apt install python3-pip
86
86
 
87
87
  ## How It Works
88
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.
89
+ MCP server that vector-indexes all 129 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
90
 
91
- - **No API keys required:** everything runs locally
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
92
  - **Pro skills auto-detected:** set your license key and they appear automatically
93
93
  - **Auto-reindex on start:** skills stay current without manual rebuilds
94
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
+
95
106
  ### Environment Variable Override
96
107
 
97
108
  Set `MEMSTACK_SKILLS_DIR` to override the skills path in `config.json`:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.6.0"
7
+ version = "4.8.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.6.0"
3
+ __version__ = "4.8.0"
@@ -50,7 +50,7 @@ CATEGORY_MAP = {
50
50
  "owasp-top10": "Security", "owasp-top-10": "Security",
51
51
  "rls-checker": "Security", "rls-guardian": "Security",
52
52
  "secrets-scanner": "Security", "git-guard": "Security",
53
- # ── Pro-exclusive skills (43) — CANONICAL category map ─────────────────
53
+ # ── Pro-exclusive skills (44) — CANONICAL category map ─────────────────
54
54
  # Must cover every slug in PRO_EXCLUSIVE_SKILLS (license.py) exactly; no Pro
55
55
  # slug may fall through to "Other". Enforced by scripts/check_skill_drift.py
56
56
  # (Check C / F-3). Core = MemStack's own session/memory/agent/efficiency
@@ -75,9 +75,10 @@ CATEGORY_MAP = {
75
75
  # Security (4)
76
76
  "advanced-security": "Security", "config-audit": "Security",
77
77
  "dependency-auditor": "Security", "env-manager-pro": "Security",
78
- # Automation (4)
78
+ # Automation (5)
79
79
  "browser-use": "Automation", "hooks-integration": "Automation",
80
- "video-pipeline": "Automation", "web-scraper": "Automation",
80
+ "video-pipeline": "Automation", "video-review": "Automation",
81
+ "web-scraper": "Automation",
81
82
  # Business (2)
82
83
  "meeting-insights-analyzer": "Business", "us-privacy-compliance": "Business",
83
84
  # Content (1)
@@ -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
@@ -62,7 +63,7 @@ TRANSIENT_REASONS = frozenset({"rate_limited", "server_error"})
62
63
  MACHINE_MISMATCH_REASON = "machine_mismatch"
63
64
 
64
65
  # Skills that require a Pro license. Free-tier users see a locked message.
65
- PRO_EXCLUSIVE_SKILLS = frozenset({"advanced-security", "api-docs", "api-load-tester", "branching", "browser-use", "burn", "checkpoint", "claude-api-helper", "codebase-index", "config-audit", "consolidate", "context-db", "council", "database-architect", "database-migration", "dependency-auditor", "developer-growth-analysis", "diagram-generator", "doc-index", "drift-detection", "env-manager-pro", "error-handler", "frontend-design", "git-worktrees", "governor-pro", "gtm-validator", "hooks-integration", "ios-app-store", "log-analyzer", "mcp-builder", "meeting-insights-analyzer", "model-router", "multi-agent", "nextjs-conventions", "performance-profiler", "python-conventions", "rag-builder", "session-restore", "social-media", "test-generator", "us-privacy-compliance", "video-pipeline", "web-scraper"})
66
+ PRO_EXCLUSIVE_SKILLS = frozenset({"advanced-security", "api-docs", "api-load-tester", "branching", "browser-use", "burn", "checkpoint", "claude-api-helper", "codebase-index", "config-audit", "consolidate", "context-db", "council", "database-architect", "database-migration", "dependency-auditor", "developer-growth-analysis", "diagram-generator", "doc-index", "drift-detection", "env-manager-pro", "error-handler", "frontend-design", "git-worktrees", "governor-pro", "gtm-validator", "hooks-integration", "ios-app-store", "log-analyzer", "mcp-builder", "meeting-insights-analyzer", "model-router", "multi-agent", "nextjs-conventions", "performance-profiler", "python-conventions", "rag-builder", "session-restore", "social-media", "test-generator", "us-privacy-compliance", "video-pipeline", "video-review", "web-scraper"})
66
67
 
67
68
 
68
69
  @dataclass
@@ -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:
@@ -434,6 +434,15 @@ _TAMPERED_MSG = (
434
434
  "\U0001f512 MemStack\u2122 Pro: License file integrity check failed. "
435
435
  "Get your key at https://memstack.pro"
436
436
  )
437
+ # Appended to the activate_license success message (both tiers) so activation is
438
+ # informed consent: a plain, non-alarming disclosure of exactly what the
439
+ # on-session-start validation POST sends. Kept in one place so both branches
440
+ # stay in sync.
441
+ _VALIDATION_DISCLOSURE = (
442
+ "License validation sends your license key, a machine ID, your email, and "
443
+ "your environment (OS, Python version, MemStack version). No code, file "
444
+ "paths, usernames, or project data is sent."
445
+ )
437
446
 
438
447
 
439
448
  def _grace_banner(status) -> str:
@@ -1090,7 +1099,8 @@ async def _handle_tool(name: str, arguments: dict) -> list[TextContent]:
1090
1099
  total = len(skills) if skills else 0
1091
1100
  msg = (
1092
1101
  f"\u2705 License activated! Tier: **Pro**. "
1093
- f"All {total} skills unlocked."
1102
+ f"All {total} skills unlocked.\n\n"
1103
+ f"{_VALIDATION_DISCLOSURE}"
1094
1104
  )
1095
1105
  else:
1096
1106
  skills = await asyncio.to_thread(list_all_skills, config)
@@ -1101,7 +1111,8 @@ async def _handle_tool(name: str, arguments: dict) -> list[TextContent]:
1101
1111
  msg = (
1102
1112
  f"\u2705 License activated! Tier: **Free**. "
1103
1113
  f"{free_count} skills unlocked. "
1104
- f"Upgrade to Pro at https://memstack.pro to unlock all {total}."
1114
+ f"Upgrade to Pro at https://memstack.pro to unlock all {total}.\n\n"
1115
+ f"{_VALIDATION_DISCLOSURE}"
1105
1116
  )
1106
1117
  return [TextContent(type="text", text=msg)]
1107
1118
  return [TextContent(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.6.0
3
+ Version: 4.8.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
@@ -34,6 +34,7 @@ src/memstack_skill_loader/proxy/server.py
34
34
  src/memstack_skill_loader/proxy/stats_tracker.py
35
35
  tests/test_body_parser_metric.py
36
36
  tests/test_code_read_savings.py
37
+ tests/test_license_environment.py
37
38
  tests/test_license_grace.py
38
39
  tests/test_pro_compressor.py
39
40
  tests/test_pro_skills_update.py
@@ -0,0 +1,86 @@
1
+ """Environment info attached to the license-validation POST payload.
2
+
3
+ Covers ``license._environment_info`` — the coarse OS / Python / loader-version
4
+ block added to every validate call so a platform-specific breakage is visible in
5
+ aggregate. Two properties matter and are tested here:
6
+
7
+ 1. SHAPE — exactly the three expected keys, no more; every value is str|None.
8
+ 2. FAIL-SAFE — if any single detector raises, the function still returns the
9
+ full dict with ``None`` for the failed field and NEVER propagates the
10
+ exception. License validation must not break because env detection failed.
11
+
12
+ Detection is patched at the module boundary (``L.platform`` and
13
+ ``importlib.metadata.version``, which the function imports lazily) so a raising
14
+ detector is exercised without touching the real interpreter.
15
+ """
16
+
17
+ import pytest
18
+
19
+ from memstack_skill_loader import license as L
20
+
21
+ EXPECTED_KEYS = {"os", "python_version", "loader_version"}
22
+
23
+
24
+ def _raise(*args, **kwargs):
25
+ raise RuntimeError("boom")
26
+
27
+
28
+ # ── 1. SHAPE ──────────────────────────────────────────────────────────────────
29
+
30
+
31
+ def test_returns_exactly_the_expected_keys():
32
+ info = L._environment_info()
33
+ assert set(info) == EXPECTED_KEYS # no extra keys (e.g. plugin_version is omitted)
34
+
35
+
36
+ def test_values_are_str_or_none():
37
+ info = L._environment_info()
38
+ for key, value in info.items():
39
+ assert value is None or isinstance(value, str), f"{key}={value!r}"
40
+
41
+
42
+ def test_happy_path_populates_all_fields():
43
+ # On a real interpreter every field is detectable, so none should be None.
44
+ info = L._environment_info()
45
+ assert all(v is not None for v in info.values()), info
46
+
47
+
48
+ # ── 2. FAIL-SAFE ──────────────────────────────────────────────────────────────
49
+
50
+
51
+ def test_os_detection_failure_yields_null(monkeypatch):
52
+ monkeypatch.setattr(L.platform, "system", _raise)
53
+ info = L._environment_info()
54
+ assert info["os"] is None
55
+ # other fields still detected; no exception propagated
56
+ assert set(info) == EXPECTED_KEYS
57
+ assert info["python_version"] is not None
58
+ assert info["loader_version"] is not None
59
+
60
+
61
+ def test_python_version_detection_failure_yields_null(monkeypatch):
62
+ monkeypatch.setattr(L.platform, "python_version", _raise)
63
+ info = L._environment_info()
64
+ assert info["python_version"] is None
65
+ assert set(info) == EXPECTED_KEYS
66
+ assert info["os"] is not None
67
+ assert info["loader_version"] is not None
68
+
69
+
70
+ def test_loader_version_lookup_failure_yields_null(monkeypatch):
71
+ # _environment_info imports importlib.metadata.version lazily, so patching
72
+ # the source attribute makes the in-function lookup raise.
73
+ monkeypatch.setattr("importlib.metadata.version", _raise)
74
+ info = L._environment_info()
75
+ assert info["loader_version"] is None
76
+ assert set(info) == EXPECTED_KEYS
77
+ assert info["os"] is not None
78
+ assert info["python_version"] is not None
79
+
80
+
81
+ def test_all_detectors_failing_returns_full_null_dict_without_raising(monkeypatch):
82
+ monkeypatch.setattr(L.platform, "system", _raise)
83
+ monkeypatch.setattr(L.platform, "python_version", _raise)
84
+ monkeypatch.setattr("importlib.metadata.version", _raise)
85
+ info = L._environment_info() # must not raise
86
+ assert info == {"os": None, "python_version": None, "loader_version": None}