superlocalmemory 3.5.8 → 3.6.1

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 (72) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +142 -35
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/src/superlocalmemory/__init__.py +1 -1
  7. package/src/superlocalmemory/cli/cache_cmd.py +198 -0
  8. package/src/superlocalmemory/cli/commands.py +80 -2
  9. package/src/superlocalmemory/cli/compress_cmd.py +179 -0
  10. package/src/superlocalmemory/cli/help_cmd.py +197 -0
  11. package/src/superlocalmemory/cli/main.py +122 -0
  12. package/src/superlocalmemory/cli/optimize_cmd.py +178 -0
  13. package/src/superlocalmemory/cli/optimize_constants.py +31 -0
  14. package/src/superlocalmemory/cli/proxy_cmd.py +104 -0
  15. package/src/superlocalmemory/core/config.py +5 -0
  16. package/src/superlocalmemory/core/engine.py +23 -0
  17. package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
  18. package/src/superlocalmemory/llm/backbone.py +10 -4
  19. package/src/superlocalmemory/mcp/server.py +34 -0
  20. package/src/superlocalmemory/mcp/tools_v3.py +6 -2
  21. package/src/superlocalmemory/optimize/NOTICE +11 -0
  22. package/src/superlocalmemory/optimize/__init__.py +0 -0
  23. package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
  24. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
  25. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
  26. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
  27. package/src/superlocalmemory/optimize/adapters/wrap.py +218 -0
  28. package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
  29. package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
  30. package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
  31. package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
  32. package/src/superlocalmemory/optimize/cache/exact.py +85 -0
  33. package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
  34. package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
  35. package/src/superlocalmemory/optimize/cache/manager.py +452 -0
  36. package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
  37. package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
  38. package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
  39. package/src/superlocalmemory/optimize/compress/align.py +153 -0
  40. package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
  41. package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
  42. package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
  43. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
  44. package/src/superlocalmemory/optimize/compress/router.py +548 -0
  45. package/src/superlocalmemory/optimize/config/__init__.py +35 -0
  46. package/src/superlocalmemory/optimize/config/defaults.py +48 -0
  47. package/src/superlocalmemory/optimize/config/schema.py +255 -0
  48. package/src/superlocalmemory/optimize/config/store.py +209 -0
  49. package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
  50. package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
  51. package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
  52. package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
  53. package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
  54. package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
  55. package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
  56. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
  57. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
  58. package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
  59. package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
  60. package/src/superlocalmemory/optimize/proxy/server.py +151 -0
  61. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  62. package/src/superlocalmemory/optimize/storage/db.py +1016 -0
  63. package/src/superlocalmemory/optimize/storage/schema.py +184 -0
  64. package/src/superlocalmemory/server/routes/optimize.py +167 -0
  65. package/src/superlocalmemory/server/routes/v3_api.py +63 -1
  66. package/src/superlocalmemory/server/unified_daemon.py +105 -0
  67. package/src/superlocalmemory/ui/index.html +98 -0
  68. package/src/superlocalmemory/ui/js/ng-shell.js +5 -1
  69. package/src/superlocalmemory/ui/js/optimize.js +173 -0
  70. package/src/superlocalmemory.egg-info/PKG-INFO +144 -36
  71. package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
  72. package/src/superlocalmemory.egg-info/requires.txt +1 -0
@@ -0,0 +1,120 @@
1
+ """Agent redirect registry — DATA ONLY, no logic.
2
+
3
+ PORT: 8765 (INTERFACE-CONTRACT §0). There is NO port 52415 anywhere.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ AGENT_REGISTRY: dict[str, dict[str, Any]] = {
11
+ "claude": {
12
+ "binary": "claude",
13
+ "mechanism": "env",
14
+ "env_vars": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8765"},
15
+ "settings_path": None,
16
+ "protocol": "anthropic",
17
+ "print_only": False,
18
+ "help_text": (
19
+ "Launches `claude` with ANTHROPIC_BASE_URL pointing at the SLM proxy.\n"
20
+ "All Claude Code calls are intercepted: cache checked, response stored."
21
+ ),
22
+ },
23
+ "claude-settings": {
24
+ "binary": None,
25
+ "mechanism": "settings-file",
26
+ "settings_path": "~/.claude/settings.json",
27
+ "env_vars": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8765"},
28
+ "protocol": "anthropic",
29
+ "print_only": False,
30
+ "help_text": (
31
+ "Writes ANTHROPIC_BASE_URL into ~/.claude/settings.json env block."
32
+ ),
33
+ },
34
+ "antigravity": {
35
+ "binary": "agy",
36
+ "mechanism": "print-only",
37
+ "protocol": "anthropic",
38
+ "print_only": True,
39
+ "help_text": (
40
+ "Antigravity (agy) redirect must be verified at runtime.\n"
41
+ "Run: agy --help | grep -i base\n"
42
+ "Then export the appropriate env var manually."
43
+ ),
44
+ },
45
+ "cline": {
46
+ "binary": None,
47
+ "mechanism": "config-file",
48
+ "config_path": "{vscode_user_dir}/settings.json",
49
+ "config_key": "cline.openAiApiBase",
50
+ "config_value": "http://127.0.0.1:8765/v1",
51
+ "protocol": "openai",
52
+ "print_only": False,
53
+ "help_text": (
54
+ "Edits VS Code user settings to set cline.openAiApiBase."
55
+ ),
56
+ },
57
+ "opencode": {
58
+ "binary": "opencode",
59
+ "mechanism": "print-only",
60
+ "protocol": "openai",
61
+ "print_only": True,
62
+ "help_text": (
63
+ "OpenCode redirect must be verified at runtime.\n"
64
+ "Run: opencode --print-config"
65
+ ),
66
+ },
67
+ "cursor": {
68
+ "binary": "cursor",
69
+ "mechanism": "print-only",
70
+ "protocol": "openai",
71
+ "print_only": True,
72
+ "help_text": (
73
+ "Cursor settings cannot be automated via env vars.\n"
74
+ "Manual: Settings → Models → Override OpenAI Base URL → http://127.0.0.1:8765/v1"
75
+ ),
76
+ },
77
+ "aider": {
78
+ "binary": "aider",
79
+ "mechanism": "env",
80
+ "env_vars": {
81
+ "OPENAI_API_BASE": "http://127.0.0.1:8765/v1",
82
+ "ANTHROPIC_BASE_URL": "http://127.0.0.1:8765",
83
+ },
84
+ "protocol": "both",
85
+ "print_only": False,
86
+ "help_text": (
87
+ "Launches `aider` with both OpenAI and Anthropic base URLs set."
88
+ ),
89
+ },
90
+ "codex": {
91
+ "binary": "codex",
92
+ "mechanism": "env",
93
+ "env_vars": {"OPENAI_BASE_URL": "http://127.0.0.1:8765/v1"},
94
+ "protocol": "openai",
95
+ "print_only": False,
96
+ "help_text": (
97
+ "Launches `codex` with OPENAI_BASE_URL pointing at the SLM proxy."
98
+ ),
99
+ },
100
+ "copilot": {
101
+ "binary": "copilot",
102
+ "mechanism": "print-only",
103
+ "protocol": "anthropic",
104
+ "print_only": True,
105
+ "help_text": (
106
+ "Copilot redirect must be verified at runtime.\n"
107
+ "Run: gh copilot --help | grep -i provider"
108
+ ),
109
+ },
110
+ "generic": {
111
+ "binary": None,
112
+ "mechanism": "print-only",
113
+ "protocol": "openai",
114
+ "print_only": True,
115
+ "help_text": (
116
+ "Generic OpenAI-compatible client:\n"
117
+ " export OPENAI_BASE_URL=http://127.0.0.1:8765/v1"
118
+ ),
119
+ },
120
+ }
@@ -0,0 +1,115 @@
1
+ """SLM Anthropic SDK adapter.
2
+
3
+ Adapted from OmniCache (MIT). See ATTRIBUTION.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from typing import Any, TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from superlocalmemory.optimize.cache.manager import CacheManager
14
+ from superlocalmemory.optimize.config.schema import OptimizeConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _detect_async_anthropic(client: Any) -> bool:
20
+ try:
21
+ from anthropic import AsyncAnthropic
22
+ return isinstance(client, AsyncAnthropic)
23
+ except ImportError:
24
+ return False
25
+
26
+
27
+ class SLMAnthropicAdapter:
28
+ """Wraps anthropic.Anthropic (or AsyncAnthropic) messages with SLM cache."""
29
+
30
+ def __init__(
31
+ self,
32
+ client: Any,
33
+ cache_manager: "CacheManager",
34
+ config: "OptimizeConfig",
35
+ tenant_id: str = "default",
36
+ ) -> None:
37
+ self._original = client
38
+ self._cache_manager = cache_manager
39
+ self._cache_view = cache_manager.for_tenant(tenant_id)
40
+ self._config = config
41
+ self._tenant_id = tenant_id
42
+ self._is_async: bool = _detect_async_anthropic(client)
43
+ self.messages = _MessagesProxy(self)
44
+
45
+ def _build_cache_key(self, kwargs: dict) -> str:
46
+ model = kwargs.get("model", "unknown")
47
+ messages = kwargs.get("messages", [])
48
+ system = kwargs.get("system", "")
49
+ exclude = {"model", "messages", "system", "stream", "metadata"}
50
+ params = {k: v for k, v in kwargs.items() if k not in exclude}
51
+ return self._cache_manager.build_key(
52
+ {"model": model, "messages": messages, "system": system, "params": params},
53
+ self._tenant_id,
54
+ )
55
+
56
+ def _should_cache(self, kwargs: dict) -> bool:
57
+ if kwargs.get("stream", False):
58
+ return False
59
+ if kwargs.get("tools"):
60
+ return False
61
+ return True
62
+
63
+ def _messages_create_sync(self, **kwargs: Any) -> Any:
64
+ if not self._config.cache_enabled or not self._should_cache(kwargs):
65
+ return self._original.messages.create(**kwargs)
66
+ key = self._build_cache_key(kwargs)
67
+ if key is None:
68
+ return self._original.messages.create(**kwargs)
69
+ hit = self._cache_view.get(key)
70
+ if hit is not None:
71
+ try:
72
+ import json as _json
73
+ return _json.loads(hit.decode("utf-8"))
74
+ except Exception:
75
+ return self._original.messages.create(**kwargs)
76
+ response = self._original.messages.create(**kwargs)
77
+ try:
78
+ import json as _json
79
+ encoded = _json.dumps(response, default=str).encode("utf-8")
80
+ self._cache_view.set(key, encoded)
81
+ except Exception as exc:
82
+ logger.warning("SLMAnthropicAdapter: failed to cache response: %s", exc)
83
+ return response
84
+
85
+ async def _messages_create_async(self, **kwargs: Any) -> Any:
86
+ if not self._config.cache_enabled or not self._should_cache(kwargs):
87
+ return await self._original.messages.create(**kwargs)
88
+ key = self._build_cache_key(kwargs)
89
+ if key is None:
90
+ return await self._original.messages.create(**kwargs)
91
+ hit = self._cache_view.get(key)
92
+ if hit is not None:
93
+ try:
94
+ import json as _json
95
+ return _json.loads(hit.decode("utf-8"))
96
+ except Exception:
97
+ return await self._original.messages.create(**kwargs)
98
+ response = await self._original.messages.create(**kwargs)
99
+ try:
100
+ import json as _json
101
+ encoded = _json.dumps(response, default=str).encode("utf-8")
102
+ self._cache_view.set(key, encoded)
103
+ except Exception as exc:
104
+ logger.warning("SLMAnthropicAdapter: failed to cache response: %s", exc)
105
+ return response
106
+
107
+
108
+ class _MessagesProxy:
109
+ def __init__(self, adapter: SLMAnthropicAdapter) -> None:
110
+ self._adapter = adapter
111
+
112
+ def create(self, **kwargs: Any) -> Any:
113
+ if self._adapter._is_async:
114
+ return self._adapter._messages_create_async(**kwargs)
115
+ return self._adapter._messages_create_sync(**kwargs)
@@ -0,0 +1,125 @@
1
+ """SLM OpenAI SDK adapter.
2
+
3
+ Adapted from OmniCache (MIT). See ATTRIBUTION.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from typing import Any, TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from superlocalmemory.optimize.cache.manager import CacheManager
14
+ from superlocalmemory.optimize.config.schema import OptimizeConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _detect_async_openai(client: Any) -> bool:
20
+ try:
21
+ from openai import AsyncOpenAI
22
+ return isinstance(client, AsyncOpenAI)
23
+ except ImportError:
24
+ return False
25
+
26
+
27
+ class SLMOpenAIAdapter:
28
+ """Wraps openai.OpenAI (or AsyncOpenAI) with SLM cache hooks."""
29
+
30
+ def __init__(
31
+ self,
32
+ client: Any,
33
+ cache_manager: "CacheManager",
34
+ config: "OptimizeConfig",
35
+ tenant_id: str = "default",
36
+ ) -> None:
37
+ self._original = client
38
+ self._cache_manager = cache_manager
39
+ self._cache_view = cache_manager.for_tenant(tenant_id)
40
+ self._config = config
41
+ self._tenant_id = tenant_id
42
+ self._is_async: bool = _detect_async_openai(client)
43
+ self.chat = _ChatCompletionsProxy(self)
44
+
45
+ def _build_cache_key(self, kwargs: dict) -> str:
46
+ model = kwargs.get("model", "unknown")
47
+ messages = kwargs.get("messages", [])
48
+ exclude = {"model", "messages", "stream", "user", "request_id"}
49
+ params = {k: v for k, v in kwargs.items() if k not in exclude}
50
+ canonical = json.dumps(
51
+ {"messages": messages, "params": params},
52
+ sort_keys=True, default=str,
53
+ )
54
+ return self._cache_manager.build_key(
55
+ {"model": model, "messages": messages, "params": params},
56
+ self._tenant_id,
57
+ )
58
+
59
+ def _should_cache(self, kwargs: dict) -> bool:
60
+ if kwargs.get("stream", False):
61
+ return False
62
+ if kwargs.get("tools") or kwargs.get("functions"):
63
+ return False
64
+ return True
65
+
66
+ def _chat_create_sync(self, **kwargs: Any) -> Any:
67
+ if not self._config.cache_enabled or not self._should_cache(kwargs):
68
+ return self._original.chat.completions.create(**kwargs)
69
+ key = self._build_cache_key(kwargs)
70
+ if key is None:
71
+ return self._original.chat.completions.create(**kwargs)
72
+ hit = self._cache_view.get(key)
73
+ if hit is not None:
74
+ try:
75
+ import json as _json
76
+ return _json.loads(hit.decode("utf-8"))
77
+ except Exception:
78
+ return self._original.chat.completions.create(**kwargs)
79
+ response = self._original.chat.completions.create(**kwargs)
80
+ try:
81
+ import json as _json
82
+ encoded = _json.dumps(response, default=str).encode("utf-8")
83
+ self._cache_view.set(key, encoded)
84
+ except Exception as exc:
85
+ logger.warning("SLMOpenAIAdapter: failed to cache response: %s", exc)
86
+ return response
87
+
88
+ async def _chat_create_async(self, **kwargs: Any) -> Any:
89
+ if not self._config.cache_enabled or not self._should_cache(kwargs):
90
+ return await self._original.chat.completions.create(**kwargs)
91
+ key = self._build_cache_key(kwargs)
92
+ if key is None:
93
+ return await self._original.chat.completions.create(**kwargs)
94
+ hit = self._cache_view.get(key)
95
+ if hit is not None:
96
+ try:
97
+ import json as _json
98
+ return _json.loads(hit.decode("utf-8"))
99
+ except Exception:
100
+ return await self._original.chat.completions.create(**kwargs)
101
+ response = await self._original.chat.completions.create(**kwargs)
102
+ try:
103
+ import json as _json
104
+ encoded = _json.dumps(response, default=str).encode("utf-8")
105
+ self._cache_view.set(key, encoded)
106
+ except Exception as exc:
107
+ logger.warning("SLMOpenAIAdapter: failed to cache response: %s", exc)
108
+ return response
109
+
110
+
111
+ class _ChatCompletionsProxy:
112
+ """Mimics openai.resources.chat.Chat interface: `client.chat.completions.create`."""
113
+
114
+ def __init__(self, adapter: SLMOpenAIAdapter) -> None:
115
+ self._adapter = adapter
116
+ self.completions = self._Completions(adapter)
117
+
118
+ class _Completions:
119
+ def __init__(self, adapter: SLMOpenAIAdapter) -> None:
120
+ self._adapter = adapter
121
+
122
+ def create(self, **kwargs: Any) -> Any:
123
+ if self._adapter._is_async:
124
+ return self._adapter._chat_create_async(**kwargs)
125
+ return self._adapter._chat_create_sync(**kwargs)
@@ -0,0 +1,218 @@
1
+ """slm wrap <agent> — table-driven per-agent proxy activation.
2
+
3
+ PORT: 8765 (INTERFACE-CONTRACT §0). No 52415 anywhere in this file.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from superlocalmemory.optimize.adapters._agent_registry import AGENT_REGISTRY
17
+ from superlocalmemory.optimize.proxy.lifecycle import ensure_proxy_running, proxy_port
18
+
19
+ # Mechanisms that write static config files — need proxy *configured*, not alive.
20
+ _STATIC_MECHANISMS = {"settings-file", "config-file", "print-only"}
21
+
22
+
23
+ def _proxy_configured() -> bool:
24
+ """Return True if proxy_enabled=True in optimize.json (no liveness check)."""
25
+ try:
26
+ from superlocalmemory.optimize.config import get_optimize_config
27
+ return get_optimize_config().proxy_enabled
28
+ except Exception:
29
+ return False
30
+
31
+
32
+ def list_agents() -> list[str]:
33
+ """Return all registered agent keys."""
34
+ return list(AGENT_REGISTRY.keys())
35
+
36
+
37
+ def wrap_agent(
38
+ agent_key: str,
39
+ agent_args: list[str],
40
+ *,
41
+ persistent: bool = False,
42
+ dry_run: bool = False,
43
+ ) -> int:
44
+ """Configure proxy redirection for <agent_key> and optionally launch it.
45
+
46
+ Returns:
47
+ 0 on success, 1 on error, agent's exit code when subprocess launched.
48
+ """
49
+ if agent_key not in AGENT_REGISTRY:
50
+ known = ", ".join(AGENT_REGISTRY.keys())
51
+ print(
52
+ f"[slm wrap] Unknown agent '{agent_key}'. Known: {known}",
53
+ file=sys.stderr,
54
+ )
55
+ return 1
56
+
57
+ port = proxy_port()
58
+ spec = AGENT_REGISTRY[agent_key]
59
+ mechanism = spec.get("mechanism", "print-only")
60
+
61
+ # Static mechanisms (settings-file, config-file) only write JSON — proxy
62
+ # doesn't need to be alive yet. env/subprocess mechanisms inject the proxy
63
+ # URL into a live process, so full liveness is required there.
64
+ # Static mechanisms (settings-file, config-file, print-only) write JSON and
65
+ # dry-run modes only print — neither needs the proxy to be alive right now.
66
+ # Only live subprocess launches (mechanism="env", dry_run=False) require the
67
+ # proxy to be running so the subprocess can actually connect.
68
+ needs_liveness = (mechanism not in _STATIC_MECHANISMS) and not dry_run
69
+ if needs_liveness:
70
+ if not ensure_proxy_running():
71
+ print(
72
+ f"[slm wrap] proxy is not enabled or not running — run "
73
+ f"`slm proxy` to start it, or `slm optimize on` first.",
74
+ file=sys.stderr,
75
+ )
76
+ return 1
77
+ else:
78
+ if not _proxy_configured():
79
+ print(
80
+ f"[slm wrap] proxy is not enabled in optimize.json — set "
81
+ f"`proxy_enabled: true` (port 8765) and re-run, or run "
82
+ f"`slm optimize on` first.",
83
+ file=sys.stderr,
84
+ )
85
+ return 1
86
+
87
+ if mechanism == "print-only":
88
+ print(f"[slm wrap] {agent_key}: manual instructions")
89
+ print(spec.get("help_text", ""))
90
+ return 0
91
+
92
+ if mechanism == "settings-file":
93
+ # Persist env var to a settings file
94
+ settings_path_str = spec.get("settings_path", "")
95
+ env_vars = spec.get("env_vars", {})
96
+ if not env_vars:
97
+ print(f"[slm wrap] {agent_key}: no env vars to set", file=sys.stderr)
98
+ return 1
99
+ expanded = str(Path(settings_path_str).expanduser())
100
+ path = Path(expanded)
101
+ if dry_run:
102
+ print(f"[slm wrap] would write {path} with env={env_vars}")
103
+ return 0
104
+ existing: dict = {}
105
+ if path.exists():
106
+ try:
107
+ existing = json.loads(path.read_text(encoding="utf-8"))
108
+ except (json.JSONDecodeError, OSError, ValueError):
109
+ existing = {}
110
+ existing.setdefault("env", {})
111
+ for k, v in env_vars.items():
112
+ existing["env"][k] = v
113
+ try:
114
+ path.parent.mkdir(parents=True, exist_ok=True)
115
+ _atomic_write_text(path, json.dumps(existing, indent=2))
116
+ except (OSError, ValueError) as exc:
117
+ print(f"[slm wrap] could not write {path}: {exc}", file=sys.stderr)
118
+ return 1
119
+ print(f"[slm wrap] wrote {path}")
120
+ return 0
121
+
122
+ if mechanism == "config-file":
123
+ # VS Code settings.json edit. config_path may be:
124
+ # - absolute path → use directly
125
+ # - template with {vscode_user_dir} → expand via _vscode_user_dir()
126
+ # - relative path → resolve against current working directory
127
+ config_value = spec.get("config_value", "")
128
+ config_key = spec.get("config_key", "")
129
+ config_path_str = spec.get("config_path", "")
130
+ if not config_path_str:
131
+ print("[slm wrap] no config_path specified", file=sys.stderr)
132
+ return 1
133
+ if "{vscode_user_dir}" in config_path_str:
134
+ vscode_dir = _vscode_user_dir()
135
+ if vscode_dir is None:
136
+ print("[slm wrap] VS Code user dir not found", file=sys.stderr)
137
+ return 1
138
+ path = Path(config_path_str.replace("{vscode_user_dir}", str(vscode_dir)))
139
+ else:
140
+ path = Path(config_path_str).expanduser()
141
+ if dry_run:
142
+ print(f"[slm wrap] would write {path} key={config_key} value={config_value}")
143
+ return 0
144
+ existing = {}
145
+ if path.exists():
146
+ try:
147
+ existing = json.loads(path.read_text(encoding="utf-8"))
148
+ except (json.JSONDecodeError, OSError):
149
+ existing = {}
150
+ existing[config_key] = config_value
151
+ try:
152
+ path.parent.mkdir(parents=True, exist_ok=True)
153
+ _atomic_write_text(path, json.dumps(existing, indent=2))
154
+ except OSError as exc:
155
+ print(f"[slm wrap] could not write {path}: {exc}", file=sys.stderr)
156
+ return 1
157
+ print(f"[slm wrap] wrote {path}")
158
+ return 0
159
+
160
+ if mechanism == "env":
161
+ # Launch the binary with env vars injected
162
+ binary = spec.get("binary")
163
+ env_vars = spec.get("env_vars", {})
164
+ if not binary:
165
+ print(f"[slm wrap] {agent_key}: no binary specified", file=sys.stderr)
166
+ return 1
167
+ # dry_run: show intent without requiring the binary to be installed
168
+ if dry_run:
169
+ print(f"[slm wrap] would exec: {binary} {' '.join(agent_args)}")
170
+ print(f"[slm wrap] env: {env_vars}")
171
+ return 0
172
+ if shutil.which(binary) is None:
173
+ print(
174
+ f"[slm wrap] binary '{binary}' not found in PATH. "
175
+ f"Install {agent_key} or set PATH.",
176
+ file=sys.stderr,
177
+ )
178
+ return 1
179
+ full_env = os.environ.copy()
180
+ for k, v in env_vars.items():
181
+ full_env[k] = v.replace("{port}", str(port))
182
+ try:
183
+ return subprocess.call([binary, *agent_args], env=full_env)
184
+ except FileNotFoundError as exc:
185
+ print(f"[slm wrap] could not launch {binary}: {exc}", file=sys.stderr)
186
+ return 1
187
+
188
+ print(f"[slm wrap] {agent_key}: unknown mechanism {mechanism!r}", file=sys.stderr)
189
+ return 1
190
+
191
+
192
+ def _atomic_write_text(path: Path, content: str) -> None:
193
+ tmp = path.with_suffix(path.suffix + ".tmp")
194
+ with open(tmp, "w", encoding="utf-8") as f:
195
+ f.write(content)
196
+ f.flush()
197
+ os.fsync(f.fileno())
198
+ os.replace(tmp, path)
199
+ try:
200
+ os.chmod(path, 0o600)
201
+ except OSError:
202
+ pass
203
+
204
+
205
+ def _vscode_user_dir() -> Path | None:
206
+ """Return the VS Code user settings directory for the current OS."""
207
+ if sys.platform == "darwin":
208
+ p = Path.home() / "Library" / "Application Support" / "Code" / "User"
209
+ return p if p.exists() or p.parent.exists() else None
210
+ if sys.platform.startswith("win"):
211
+ appdata = os.environ.get("APPDATA")
212
+ if appdata:
213
+ p = Path(appdata) / "Code" / "User"
214
+ return p
215
+ return None
216
+ # Linux
217
+ p = Path.home() / ".config" / "Code" / "User"
218
+ return p
@@ -0,0 +1,31 @@
1
+ """optimize/cache — exact-response cache (Phase 1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from superlocalmemory.optimize.cache.exact import ExactCache
6
+ from superlocalmemory.optimize.cache.invalidation import InvalidationEngine
7
+ from superlocalmemory.optimize.cache.key_builder import CacheConfig, KeyBuilder
8
+ from superlocalmemory.optimize.cache.manager import (
9
+ CacheManager,
10
+ NoOpSemantic,
11
+ SemanticTier,
12
+ _TenantScopedManager,
13
+ )
14
+ from superlocalmemory.optimize.cache.stampede import StampedeShield
15
+
16
+ __all__ = [
17
+ "CacheManager",
18
+ "CacheView",
19
+ "ExactCache",
20
+ "InvalidationEngine",
21
+ "KeyBuilder",
22
+ "CacheConfig",
23
+ "StampedeShield",
24
+ "SemanticTier",
25
+ "NoOpSemantic",
26
+ "_TenantScopedManager",
27
+ ]
28
+
29
+
30
+ # Lazy alias so import order doesn't matter
31
+ CacheView = _TenantScopedManager