snapdec 0.2.0__py3-none-any.whl

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. snapdec/_brand.py +8 -0
  2. snapdec/backends/__init__.py +5 -0
  3. snapdec/backends/base.py +31 -0
  4. snapdec/backends/keydetect.py +67 -0
  5. snapdec/backends/mock.py +72 -0
  6. snapdec/backends/remote_systemone.py +79 -0
  7. snapdec/cli/__init__.py +3 -0
  8. snapdec/cli/__main__.py +3 -0
  9. snapdec/cli/main.py +756 -0
  10. snapdec/config.py +119 -0
  11. snapdec/decisions/__init__.py +14 -0
  12. snapdec/decisions/envelope.py +98 -0
  13. snapdec/decisions/policy.py +51 -0
  14. snapdec/decisions/tier0/__init__.py +3 -0
  15. snapdec/decisions/tier0/project_facts.py +216 -0
  16. snapdec/hardware/__init__.py +4 -0
  17. snapdec/hardware/catalog.py +91 -0
  18. snapdec/hardware/detect.py +181 -0
  19. snapdec/hardware/profile.py +62 -0
  20. snapdec/integrations/__init__.py +4 -0
  21. snapdec/integrations/agents_json.py +162 -0
  22. snapdec/integrations/base.py +181 -0
  23. snapdec/integrations/claude_code.py +142 -0
  24. snapdec/integrations/codex.py +126 -0
  25. snapdec/integrations/json_family.py +126 -0
  26. snapdec/integrations/patchers.py +137 -0
  27. snapdec/integrations/registry.py +29 -0
  28. snapdec/mcp/__init__.py +3 -0
  29. snapdec/mcp/server.py +218 -0
  30. snapdec/runtime/__init__.py +8 -0
  31. snapdec/runtime/daemon.py +200 -0
  32. snapdec/runtime/ipc.py +148 -0
  33. snapdec/runtime/lifecycle.py +138 -0
  34. snapdec/runtime/probe.py +69 -0
  35. snapdec/runtime/provision.py +232 -0
  36. snapdec/skills/snapdec/SKILL.md +19 -0
  37. snapdec/skills/snapdec/references/recipes.md +21 -0
  38. snapdec/skills/snapdec/references/tools.md +26 -0
  39. snapdec-0.2.0.dist-info/METADATA +181 -0
  40. snapdec-0.2.0.dist-info/RECORD +43 -0
  41. snapdec-0.2.0.dist-info/WHEEL +4 -0
  42. snapdec-0.2.0.dist-info/entry_points.txt +2 -0
  43. snapdec-0.2.0.dist-info/licenses/LICENSE +202 -0
snapdec/_brand.py ADDED
@@ -0,0 +1,8 @@
1
+ """Single source of truth for the project name (rename gate §1.4).
2
+
3
+ A rename must be a single commit: this constant + pyproject.toml.
4
+ Do not hardcode the name anywhere else.
5
+ """
6
+
7
+ __version__ = "0.2.0"
8
+ NAME = "snapdec" # final — rename gate cleared 2026-10-03, see docs/adr/0007
@@ -0,0 +1,5 @@
1
+ from .base import Backend, Health
2
+ from .mock import MockBackend
3
+ from .remote_systemone import RemoteSystemOne
4
+
5
+ __all__ = ["Backend", "Health", "MockBackend", "RemoteSystemOne"]
@@ -0,0 +1,31 @@
1
+ """Backend interface (§6).
2
+
3
+ Backends are interchangeable behind one interface; anything that already
4
+ speaks /v1/systemone (hosted Jev, kev.serve, laya-serve, OpenRouter) is
5
+ just a manifest/URL away — no adapter code needed (remote_systemone).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Protocol, runtime_checkable
12
+
13
+ from ..decisions.envelope import SystemOneRequest, SystemOneResponse
14
+
15
+
16
+ @dataclass
17
+ class Health:
18
+ status: str # ready | warming | degraded | failed
19
+ device: str = ""
20
+ detail: str = ""
21
+
22
+
23
+ @runtime_checkable
24
+ class Backend(Protocol):
25
+ id: str
26
+
27
+ def load(self) -> None: ...
28
+ def system_one(self, req: SystemOneRequest) -> SystemOneResponse: ...
29
+ def health(self) -> Health: ...
30
+ def close(self) -> None: ...
31
+ def describe(self) -> dict[str, Any]: ...
@@ -0,0 +1,67 @@
1
+ """Auto-detect the provider from a pasted API key (§9.1 'all auto').
2
+
3
+ Only System One-compatible endpoints are usable by the remote backend.
4
+ OpenRouter is the universal bridge (hosts Jev/Kev models); OpenAI/Groq/
5
+ Anthropic keys do NOT speak /v1/systemone and are reported as such with
6
+ a hint instead of being silently misrouted.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ProviderGuess:
16
+ provider: str # openrouter | typesafe | unsupported:<id> | unknown
17
+ url: str = ""
18
+ default_models: tuple[str, ...] = ()
19
+ note: str = ""
20
+
21
+
22
+ _OPENROUTER = ProviderGuess(
23
+ "openrouter", "https://openrouter.ai/api/v1",
24
+ # free variants first — "paid or free judgement": try :free, fall back
25
+ ("jaredpalmer/kev-4b:free", "jaredpalmer/kev-4b", "typesafe/jev-latest"),
26
+ "OpenRouter — universal bridge, hosts Jev/Kev",
27
+ )
28
+ _TYPESAFE = ProviderGuess(
29
+ "typesafe", "https://api.typesafe.ai",
30
+ ("jev-latest",),
31
+ "TypeSafe Jev — native System One",
32
+ )
33
+
34
+
35
+ def detect_provider(key: str) -> ProviderGuess:
36
+ k = key.strip()
37
+ if k.startswith("sk-or-"):
38
+ return _OPENROUTER
39
+ if k.startswith("ts-") or k.startswith("tsk_"):
40
+ return _TYPESAFE
41
+ if k.startswith("sk-ant-"):
42
+ return ProviderGuess("unsupported:anthropic", note=(
43
+ "Anthropic key detected — Anthropic speaks /v1/messages, not "
44
+ "/v1/systemone. Create a free OpenRouter key (openrouter.ai/keys) "
45
+ "and paste that instead."))
46
+ if k.startswith("gsk_"):
47
+ return ProviderGuess("unsupported:groq", note=(
48
+ "Groq key detected — not System One compatible. "
49
+ "Use an OpenRouter key (openrouter.ai/keys)."))
50
+ if k.startswith("sk-proj-") or k.startswith("sk-"):
51
+ return ProviderGuess("unsupported:openai", note=(
52
+ "OpenAI key detected — not System One compatible. "
53
+ "Use an OpenRouter key (openrouter.ai/keys) or a TypeSafe key."))
54
+ return ProviderGuess("unknown")
55
+
56
+
57
+ def provider_by_id(pid: str) -> ProviderGuess:
58
+ return {"openrouter": _OPENROUTER, "typesafe": _TYPESAFE}.get(pid, ProviderGuess(pid))
59
+
60
+
61
+ def pick_model(guess: ProviderGuess, *, low_power: bool = False) -> str:
62
+ """Device-aware default: hosted models are small enough that the wire
63
+ cost dominates; low-power devices still prefer the cheapest first."""
64
+ models = guess.default_models
65
+ if not models:
66
+ return ""
67
+ return models[0]
@@ -0,0 +1,72 @@
1
+ """Deterministic mock backend — tests, demos, and `--backend mock`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import random
7
+ from typing import Any
8
+
9
+ from ..decisions.envelope import (
10
+ Answer,
11
+ SystemOneRequest,
12
+ SystemOneResponse,
13
+ )
14
+ from .base import Health
15
+
16
+
17
+ class MockBackend:
18
+ id = "mock"
19
+
20
+ def load(self) -> None:
21
+ pass
22
+
23
+ def _dist(self, seed_text: str, options: list[str]) -> dict[str, float]:
24
+ """Stable pseudo-distribution: same input → same numbers."""
25
+ rng = random.Random(int(hashlib.sha256(seed_text.encode("utf-8")).hexdigest()[:8], 16))
26
+ raw = [rng.random() + 0.35 for _ in options] or [1.0]
27
+ total = sum(raw)
28
+ probs = [r / total for r in raw]
29
+ # sharpen the top so `auto` is reachable with default thresholds
30
+ top = probs.index(max(probs))
31
+ probs[top] += 0.30
32
+ total = sum(probs)
33
+ return {o: p / total for o, p in zip(options, probs, strict=False)}
34
+
35
+ def system_one(self, req: SystemOneRequest) -> SystemOneResponse:
36
+ state = req.state if isinstance(req.state, str) else str(req.state)
37
+ answers: dict[str, Answer] = {}
38
+ for name, q in req.questions.items():
39
+ if q.type == "noul":
40
+ probs = self._dist(f"{state}|{name}", ["yes", "no"])
41
+ answers[name] = Answer(
42
+ type="noul", noul=probs["yes"] > probs["no"], probabilities=probs,
43
+ confidence=max(probs.values()),
44
+ )
45
+ elif q.type == "score":
46
+ levels = list(q.criteria) if isinstance(q.criteria, (list, dict)) else \
47
+ [f"L{i}" for i in range(1, 6)]
48
+ probs = self._dist(f"{state}|{name}", levels)
49
+ best = max(probs, key=probs.get)
50
+ idx = levels.index(best)
51
+ answers[name] = Answer(
52
+ type="score", score=idx + 1, nearest_level=best,
53
+ probabilities=probs, confidence=probs[best],
54
+ )
55
+ else: # choice
56
+ options = list(q.criteria) if q.criteria else ["a", "b"]
57
+ probs = self._dist(f"{state}|{name}", options)
58
+ best = max(probs, key=probs.get)
59
+ answers[name] = Answer(
60
+ type="choice", choice=best, probabilities=probs,
61
+ confidence=probs[best],
62
+ )
63
+ return SystemOneResponse(answers=answers, model="mock-1")
64
+
65
+ def health(self) -> Health:
66
+ return Health(status="ready", device="cpu", detail="deterministic mock")
67
+
68
+ def close(self) -> None:
69
+ pass
70
+
71
+ def describe(self) -> dict[str, Any]:
72
+ return {"name": self.id, "model": "mock-1", "remote": False, "ready": True}
@@ -0,0 +1,79 @@
1
+ """Remote /v1/systemone backend (§6).
2
+
3
+ Works unchanged with: hosted TypeSafe Jev, OpenRouter-hosted models,
4
+ kev.serve, laya-serve, team servers, Telnyx/Opper/LLM Gateway.
5
+ Fail-closed: any transport error raises; the daemon converts it into a
6
+ `review` envelope — never into an exception toward the host agent.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import httpx
14
+
15
+ from ..decisions.envelope import (
16
+ Answer,
17
+ SystemOneRequest,
18
+ SystemOneResponse,
19
+ )
20
+ from .base import Health
21
+
22
+
23
+ class RemoteSystemOne:
24
+ id = "remote"
25
+
26
+ def __init__(self, base_url: str, model: str = "", api_key: str | None = None,
27
+ timeout: float = 5.0) -> None:
28
+ self.base_url = base_url.rstrip("/")
29
+ self.model = model
30
+ self.api_key = api_key
31
+ self.timeout = timeout
32
+
33
+ # ---------------------------------------------------------------- http
34
+ def _headers(self) -> dict[str, str]:
35
+ h = {"content-type": "application/json"}
36
+ if self.api_key:
37
+ h["authorization"] = f"Bearer {self.api_key}"
38
+ h["x-api-key"] = self.api_key # kev.serve / laya-serve convention
39
+ return h
40
+
41
+ def load(self) -> None:
42
+ pass # no local state; readiness checked via health()
43
+
44
+ def system_one(self, req: SystemOneRequest) -> SystemOneResponse:
45
+ payload = req.model_dump(exclude_none=True)
46
+ if self.model:
47
+ payload["model"] = self.model
48
+ r = httpx.post(
49
+ f"{self.base_url}/v1/systemone",
50
+ json=payload, headers=self._headers(), timeout=self.timeout,
51
+ )
52
+ r.raise_for_status()
53
+ data = r.json()
54
+ answers = {k: Answer(**v) for k, v in (data.get("answers") or {}).items()}
55
+ return SystemOneResponse(answers=answers, usage=data.get("usage"),
56
+ model=data.get("model"))
57
+
58
+ def health(self) -> Health:
59
+ try:
60
+ with httpx.Client(timeout=self.timeout) as c:
61
+ r = c.get(f"{self.base_url}/v1/models", headers=self._headers())
62
+ if r.status_code == 404: # laya-serve exposes /models, not /v1/models
63
+ r = c.get(f"{self.base_url}/models", headers=self._headers())
64
+ if r.status_code in (200, 401, 403):
65
+ # 401/403 = server is up, key is wrong — still "reachable"
66
+ status = "ready" if r.status_code == 200 else "degraded"
67
+ return Health(status=status, device="remote",
68
+ detail=f"HTTP {r.status_code}")
69
+ return Health(status="degraded", device="remote",
70
+ detail=f"HTTP {r.status_code}")
71
+ except httpx.HTTPError as e:
72
+ return Health(status="failed", device="remote", detail=str(e))
73
+
74
+ def close(self) -> None:
75
+ pass
76
+
77
+ def describe(self) -> dict[str, Any]:
78
+ return {"name": self.id, "url": self.base_url, "model": self.model,
79
+ "remote": True}
@@ -0,0 +1,3 @@
1
+ from .main import app
2
+
3
+ __all__ = ["app"]
@@ -0,0 +1,3 @@
1
+ from .main import app
2
+
3
+ app()