minima-cli 0.4.9__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 (161) hide show
  1. minima/__init__.py +5 -0
  2. minima/api/__init__.py +1 -0
  3. minima/api/auth.py +39 -0
  4. minima/api/errors.py +40 -0
  5. minima/api/routers/__init__.py +1 -0
  6. minima/api/routers/calibration.py +50 -0
  7. minima/api/routers/feedback.py +279 -0
  8. minima/api/routers/health.py +50 -0
  9. minima/api/routers/models.py +42 -0
  10. minima/api/routers/recommend.py +66 -0
  11. minima/api/routers/savings.py +55 -0
  12. minima/api/routers/strategies.py +33 -0
  13. minima/catalog/__init__.py +1 -0
  14. minima/catalog/data/capability_priors.json +210 -0
  15. minima/catalog/data/model_aliases.json +12 -0
  16. minima/catalog/merge.py +69 -0
  17. minima/catalog/refresh.py +54 -0
  18. minima/catalog/sources/__init__.py +1 -0
  19. minima/catalog/sources/litellm.py +19 -0
  20. minima/catalog/sources/openrouter.py +25 -0
  21. minima/catalog/store.py +86 -0
  22. minima/config.py +288 -0
  23. minima/deps.py +35 -0
  24. minima/llm/__init__.py +1 -0
  25. minima/llm/anthropic.py +106 -0
  26. minima/llm/base.py +196 -0
  27. minima/llm/gemini.py +124 -0
  28. minima/llm/registry.py +54 -0
  29. minima/logging.py +28 -0
  30. minima/main.py +109 -0
  31. minima/memory/__init__.py +1 -0
  32. minima/memory/adapter.py +572 -0
  33. minima/memory/keys.py +83 -0
  34. minima/memory/records.py +190 -0
  35. minima/memory/threadpool.py +41 -0
  36. minima/metrics/__init__.py +1 -0
  37. minima/metrics/calibration.py +415 -0
  38. minima/metrics/report.py +116 -0
  39. minima/metrics/savings.py +98 -0
  40. minima/recommender/__init__.py +1 -0
  41. minima/recommender/_pg_pool.py +38 -0
  42. minima/recommender/_redis_client.py +32 -0
  43. minima/recommender/aggregate.py +157 -0
  44. minima/recommender/classify.py +165 -0
  45. minima/recommender/decisionlog.py +505 -0
  46. minima/recommender/durablerefs.py +312 -0
  47. minima/recommender/engine.py +997 -0
  48. minima/recommender/escalation.py +83 -0
  49. minima/recommender/propensity.py +189 -0
  50. minima/recommender/recstore.py +368 -0
  51. minima/recommender/score.py +318 -0
  52. minima/recommender/types.py +166 -0
  53. minima/schemas/__init__.py +1 -0
  54. minima/schemas/common.py +73 -0
  55. minima/schemas/feedback.py +34 -0
  56. minima/schemas/models_catalog.py +36 -0
  57. minima/schemas/recommend.py +104 -0
  58. minima/schemas/savings.py +39 -0
  59. minima/schemas/strategies.py +57 -0
  60. minima/schemas/workflow.py +43 -0
  61. minima/seeding/__init__.py +1 -0
  62. minima/seeding/items.py +42 -0
  63. minima/seeding/llmrouterbench.py +232 -0
  64. minima/seeding/routerbench.py +141 -0
  65. minima/seeding/run_seed.py +56 -0
  66. minima/seeding/synthetic.py +70 -0
  67. minima/tenancy/__init__.py +8 -0
  68. minima/tenancy/context.py +37 -0
  69. minima/tenancy/passthrough.py +110 -0
  70. minima/version.py +3 -0
  71. minima_cli-0.4.9.dist-info/METADATA +275 -0
  72. minima_cli-0.4.9.dist-info/RECORD +161 -0
  73. minima_cli-0.4.9.dist-info/WHEEL +4 -0
  74. minima_cli-0.4.9.dist-info/entry_points.txt +5 -0
  75. minima_cli-0.4.9.dist-info/licenses/LICENSE +295 -0
  76. minima_client/__init__.py +19 -0
  77. minima_client/autocapture.py +101 -0
  78. minima_client/client.py +301 -0
  79. minima_client/errors.py +23 -0
  80. minima_harness/LICENSE_PI +32 -0
  81. minima_harness/__init__.py +16 -0
  82. minima_harness/agent/__init__.py +72 -0
  83. minima_harness/agent/agent.py +276 -0
  84. minima_harness/agent/events.py +124 -0
  85. minima_harness/agent/loop.py +311 -0
  86. minima_harness/agent/state.py +79 -0
  87. minima_harness/agent/tools.py +97 -0
  88. minima_harness/ai/__init__.py +66 -0
  89. minima_harness/ai/compat.py +71 -0
  90. minima_harness/ai/errors.py +96 -0
  91. minima_harness/ai/events.py +117 -0
  92. minima_harness/ai/openrouter_catalog.py +153 -0
  93. minima_harness/ai/provider_catalog.py +299 -0
  94. minima_harness/ai/provider_quirks.py +37 -0
  95. minima_harness/ai/providers/__init__.py +75 -0
  96. minima_harness/ai/providers/_common.py +48 -0
  97. minima_harness/ai/providers/anthropic.py +290 -0
  98. minima_harness/ai/providers/base.py +65 -0
  99. minima_harness/ai/providers/faux.py +173 -0
  100. minima_harness/ai/providers/google.py +221 -0
  101. minima_harness/ai/providers/openai_compat.py +278 -0
  102. minima_harness/ai/registry.py +184 -0
  103. minima_harness/ai/stream.py +82 -0
  104. minima_harness/ai/tools.py +51 -0
  105. minima_harness/ai/types.py +204 -0
  106. minima_harness/ai/usage.py +41 -0
  107. minima_harness/minima/__init__.py +40 -0
  108. minima_harness/minima/cache.py +102 -0
  109. minima_harness/minima/config.py +85 -0
  110. minima_harness/minima/goals.py +226 -0
  111. minima_harness/minima/judge.py +144 -0
  112. minima_harness/minima/mapping.py +147 -0
  113. minima_harness/minima/meter.py +143 -0
  114. minima_harness/minima/router.py +220 -0
  115. minima_harness/minima/runtime.py +544 -0
  116. minima_harness/minima/signals.py +195 -0
  117. minima_harness/session/__init__.py +14 -0
  118. minima_harness/session/format.py +35 -0
  119. minima_harness/session/store.py +236 -0
  120. minima_harness/tasks/__init__.py +17 -0
  121. minima_harness/tasks/task_set.py +78 -0
  122. minima_harness/tools/__init__.py +7 -0
  123. minima_harness/tools/_io.py +34 -0
  124. minima_harness/tools/bash.py +70 -0
  125. minima_harness/tools/builtin.py +23 -0
  126. minima_harness/tools/edit.py +50 -0
  127. minima_harness/tools/find.py +38 -0
  128. minima_harness/tools/grep.py +73 -0
  129. minima_harness/tools/ls.py +35 -0
  130. minima_harness/tools/read.py +38 -0
  131. minima_harness/tools/tasks.py +75 -0
  132. minima_harness/tools/write.py +36 -0
  133. minima_harness/tui/__init__.py +3 -0
  134. minima_harness/tui/analytics.py +111 -0
  135. minima_harness/tui/app.py +1927 -0
  136. minima_harness/tui/bridge.py +103 -0
  137. minima_harness/tui/cli.py +227 -0
  138. minima_harness/tui/clipboard.py +60 -0
  139. minima_harness/tui/commands.py +49 -0
  140. minima_harness/tui/compaction.py +17 -0
  141. minima_harness/tui/config_cli.py +141 -0
  142. minima_harness/tui/config_store.py +237 -0
  143. minima_harness/tui/context.py +93 -0
  144. minima_harness/tui/customize.py +95 -0
  145. minima_harness/tui/diff.py +53 -0
  146. minima_harness/tui/editor.py +43 -0
  147. minima_harness/tui/extensions.py +84 -0
  148. minima_harness/tui/extra_models.py +52 -0
  149. minima_harness/tui/history.py +71 -0
  150. minima_harness/tui/mubit.py +295 -0
  151. minima_harness/tui/overlays.py +593 -0
  152. minima_harness/tui/packages.py +59 -0
  153. minima_harness/tui/run_modes.py +66 -0
  154. minima_harness/tui/theme.py +77 -0
  155. minima_harness/tui/welcome.py +83 -0
  156. minima_harness/tui/widgets/__init__.py +3 -0
  157. minima_harness/tui/widgets/banner.py +38 -0
  158. minima_harness/tui/widgets/editor.py +83 -0
  159. minima_harness/tui/widgets/footer.py +73 -0
  160. minima_harness/tui/widgets/messages.py +151 -0
  161. minima_harness/tui/widgets/status.py +57 -0
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from minima_harness.session import SessionManager
7
+ from minima_harness.tui.customize import GLOBAL_DIR
8
+
9
+
10
+ class History:
11
+ """Shell-style prompt history with prev/next cursor (None cursor = the 'new' position)."""
12
+
13
+ def __init__(self, entries: list[str] | None = None) -> None:
14
+ self.entries: list[str] = list(entries or [])
15
+ self._i: int | None = None # None = new (after the last); index = browsing position
16
+
17
+ def add(self, text: str) -> None:
18
+ text = text.strip()
19
+ if text and (not self.entries or self.entries[-1] != text):
20
+ self.entries.append(text)
21
+ self._i = None
22
+
23
+ def prev(self) -> str | None:
24
+ """Move toward older; returns the entry (or None if there's no history)."""
25
+ if not self.entries:
26
+ return None
27
+ if self._i is None:
28
+ self._i = len(self.entries) - 1
29
+ elif self._i > 0:
30
+ self._i -= 1
31
+ return self.entries[self._i]
32
+
33
+ def next(self) -> str | None:
34
+ """Move toward newer; returns the entry, '' when back at new, or None if already new."""
35
+ if self._i is None:
36
+ return None
37
+ self._i += 1
38
+ if self._i >= len(self.entries):
39
+ self._i = None
40
+ return ""
41
+ return self.entries[self._i]
42
+
43
+
44
+ def _path(cwd: Path) -> Path:
45
+ slug = SessionManager().slug_for(cwd)
46
+ return GLOBAL_DIR / "history" / f"{slug}.jsonl"
47
+
48
+
49
+ def load_history(cwd: Path) -> list[str]:
50
+ path = _path(cwd)
51
+ if not path.is_file():
52
+ return []
53
+ out: list[str] = []
54
+ for line in path.read_text(encoding="utf-8").splitlines():
55
+ if not line.strip():
56
+ continue
57
+ try:
58
+ out.append(json.loads(line))
59
+ except Exception: # noqa: BLE001
60
+ continue
61
+ return out
62
+
63
+
64
+ def append_history(cwd: Path, text: str) -> None:
65
+ path = _path(cwd)
66
+ path.parent.mkdir(parents=True, exist_ok=True)
67
+ try:
68
+ with path.open("a", encoding="utf-8") as fh:
69
+ fh.write(json.dumps(text) + "\n")
70
+ except OSError: # noqa: BLE001
71
+ pass
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ _log = logging.getLogger("minima_harness.tui.mubit")
10
+
11
+ _DEFAULT_ENDPOINT = "https://api.mubit.ai"
12
+ _AGENT_ID = "minima-harness"
13
+ _initialized = False
14
+
15
+
16
+ def _slug(cwd: Path) -> str:
17
+ from minima_harness.session import SessionManager
18
+
19
+ return SessionManager().slug_for(cwd)
20
+
21
+
22
+ def init_mubit(cwd: Path) -> bool:
23
+ """Initialize the Mubit SDK for this project (idempotent). Returns True if available."""
24
+ global _initialized
25
+ if _initialized:
26
+ return True
27
+ key = os.environ.get("MUBIT_API_KEY")
28
+ if not key:
29
+ _log.warning("mubit_no_api_key")
30
+ return False
31
+ # `or` (not get-with-default) so a blank MUBIT_ENDPOINT="" — e.g. a stray empty line in a
32
+ # copied .env — falls back to the hosted default instead of passing "" to mubit.init().
33
+ endpoint = os.environ.get("MUBIT_ENDPOINT") or _DEFAULT_ENDPOINT
34
+ try:
35
+ import mubit
36
+
37
+ mubit.init(
38
+ api_key=key,
39
+ endpoint=endpoint,
40
+ agent_id=_AGENT_ID,
41
+ project_id=_slug(cwd),
42
+ auto_instrument=False,
43
+ auto_learn=False,
44
+ inject_lessons=True,
45
+ )
46
+ _initialized = True
47
+ return True
48
+ except Exception: # noqa: BLE001 - Mubit must never block the TUI
49
+ _log.warning("mubit_init_failed", exc_info=True)
50
+ return False
51
+
52
+
53
+ def available() -> bool:
54
+ return _initialized
55
+
56
+
57
+ def get_prompt() -> str:
58
+ if not _initialized:
59
+ return ""
60
+ try:
61
+ import mubit
62
+
63
+ return mubit.get_prompt(agent_id=_AGENT_ID) or ""
64
+ except Exception: # noqa: BLE001
65
+ _log.warning("mubit_get_prompt_failed", exc_info=True)
66
+ return ""
67
+
68
+
69
+ def set_prompt(content: str) -> bool:
70
+ try:
71
+ import mubit
72
+
73
+ mubit.set_prompt(content, agent_id=_AGENT_ID, activate=True)
74
+ return True
75
+ except Exception: # noqa: BLE001
76
+ _log.warning("mubit_set_prompt_failed", exc_info=True)
77
+ return False
78
+
79
+
80
+ def optimize_prompt() -> dict[str, Any] | None:
81
+ """Ask Mubit to optimize this agent's system prompt from accumulated lessons + outcomes.
82
+
83
+ Returns the raw response ``{success, activated, candidate, confidence,
84
+ optimization_summary}`` (the candidate is a non-activated suggestion), or None on any
85
+ failure. Verified live: ``activated`` is False, so this never changes the active prompt.
86
+ """
87
+ if not _initialized:
88
+ return None
89
+ try:
90
+ from mubit._helpers import require_context
91
+
92
+ return require_context().client.optimize_prompt({"agent_id": _AGENT_ID})
93
+ except Exception: # noqa: BLE001 - Mubit must never block the TUI
94
+ _log.warning("mubit_optimize_prompt_failed", exc_info=True)
95
+ return None
96
+
97
+
98
+ @dataclass(frozen=True, slots=True)
99
+ class Optimization:
100
+ """A proposed system-prompt optimization, for the /optimize preview."""
101
+
102
+ new_prompt: str
103
+ current_tokens: int
104
+ new_tokens: int
105
+ est_savings: int # current - new; negative means the prompt grew (quality over size)
106
+ rationale: str
107
+ source: str # "mubit" | "local"
108
+
109
+
110
+ def _local_optimization(cwd: Path) -> Optimization | None:
111
+ """Fallback when Mubit is unreachable: conservatively drop exact-duplicate lines from the
112
+ current Mubit prompt. Suggestion only; returns None when there's nothing safe to remove."""
113
+ current = get_prompt().strip()
114
+ if not current:
115
+ return None
116
+ lines = current.splitlines()
117
+ seen: set[str] = set()
118
+ deduped: list[str] = []
119
+ for ln in lines:
120
+ key = ln.strip()
121
+ if key and key in seen:
122
+ continue
123
+ if key:
124
+ seen.add(key)
125
+ deduped.append(ln)
126
+ new_prompt = "\n".join(deduped)
127
+ savings = estimate_tokens(current) - estimate_tokens(new_prompt)
128
+ if savings <= 0:
129
+ return None
130
+ removed = len(lines) - len(deduped)
131
+ return Optimization(
132
+ new_prompt=new_prompt,
133
+ current_tokens=estimate_tokens(current),
134
+ new_tokens=estimate_tokens(new_prompt),
135
+ est_savings=savings,
136
+ rationale=f"removed {removed} duplicate line(s)",
137
+ source="local",
138
+ )
139
+
140
+
141
+ def propose_prompt_optimization(cwd: Path, n_sessions: int = 10) -> Optimization | None:
142
+ """Propose a system-prompt optimization: Mubit's lesson-grounded candidate (Path A) when
143
+ available, else a local dedup (Path B). Never auto-applies — the caller previews + confirms."""
144
+ current = get_prompt()
145
+ resp = optimize_prompt()
146
+ if isinstance(resp, dict) and resp.get("success"):
147
+ cand = resp.get("candidate")
148
+ new_prompt = cand.get("content", "") if isinstance(cand, dict) else ""
149
+ if new_prompt.strip():
150
+ summary = (resp.get("optimization_summary") or "").strip()
151
+ return Optimization(
152
+ new_prompt=new_prompt.strip(),
153
+ current_tokens=estimate_tokens(current),
154
+ new_tokens=estimate_tokens(new_prompt),
155
+ est_savings=estimate_tokens(current) - estimate_tokens(new_prompt),
156
+ rationale=summary or "Mubit consolidated lessons + outcomes into the prompt.",
157
+ source="mubit",
158
+ )
159
+ return _local_optimization(cwd)
160
+
161
+
162
+ def get_skills(cwd: Path) -> list[dict[str, Any]]:
163
+ if not _initialized:
164
+ return []
165
+ try:
166
+ import mubit
167
+
168
+ return mubit.get_skills(project_id=_slug(cwd)) or []
169
+ except Exception: # noqa: BLE001
170
+ return []
171
+
172
+
173
+ def set_skill(cwd: Path, name: str, description: str, instructions: str = "") -> bool:
174
+ try:
175
+ import mubit
176
+
177
+ mubit.set_skill(name, description, instructions=instructions, project_id=_slug(cwd))
178
+ return True
179
+ except Exception: # noqa: BLE001
180
+ _log.warning("mubit_set_skill_failed", exc_info=True)
181
+ return False
182
+
183
+
184
+ def recall(query: str, session_id: str | None = None, limit: int = 5) -> list[Any]:
185
+ if not _initialized:
186
+ return []
187
+ try:
188
+ import mubit
189
+
190
+ return mubit.recall(query, session_id=session_id, limit=limit) or []
191
+ except Exception: # noqa: BLE001
192
+ return []
193
+
194
+
195
+ def learned() -> str:
196
+ if not _initialized:
197
+ return ""
198
+ try:
199
+ import mubit
200
+
201
+ return mubit.learned() or ""
202
+ except Exception: # noqa: BLE001
203
+ return ""
204
+
205
+
206
+ def estimate_tokens(text: str) -> int:
207
+ return max(1, len(text) // 4)
208
+
209
+
210
+ @dataclass(frozen=True, slots=True)
211
+ class PromptLayer:
212
+ """One layer of the assembled system prompt, for transparent display + control.
213
+
214
+ ``header`` is the section prefix used in the joined prompt (empty for the leading
215
+ layer); ``rendered`` reproduces exactly how the layer appears in ``effective_prompt``.
216
+ ``editable_target`` is ``"project"`` (→ Mubit), ``"session"`` (→ override), or ``None``.
217
+ """
218
+
219
+ name: str
220
+ text: str
221
+ header: str = ""
222
+ source: str = ""
223
+ editable_target: str | None = None
224
+
225
+ @property
226
+ def rendered(self) -> str:
227
+ return f"{self.header}\n{self.text}" if self.header else self.text
228
+
229
+ @property
230
+ def tokens(self) -> int:
231
+ return estimate_tokens(self.rendered)
232
+
233
+
234
+ def prompt_layers(cwd: Path, session_override: str = "") -> list[PromptLayer]:
235
+ """The ordered layers that compose the system prompt. Single source of truth —
236
+ ``effective_prompt`` is a thin join over this, so the inspector can never drift."""
237
+ from minima_harness.tui.context import build_system_prompt_parts, load_agents_md
238
+
239
+ layers: list[PromptLayer] = []
240
+ mubit_prompt = get_prompt().strip()
241
+ if mubit_prompt:
242
+ layers.append(
243
+ PromptLayer("system prompt", mubit_prompt, source="mubit", editable_target="project")
244
+ )
245
+ agents = load_agents_md(cwd)
246
+ if agents:
247
+ layers.append(
248
+ PromptLayer("project context", agents, "# Project context", "agents.md")
249
+ )
250
+ else:
251
+ for name, text in build_system_prompt_parts(cwd):
252
+ if name == "base":
253
+ layers.append(PromptLayer("base prompt", text, source="local"))
254
+ else: # agents.md
255
+ layers.append(
256
+ PromptLayer("project context", text, "# Project context", "agents.md")
257
+ )
258
+ override = session_override.strip()
259
+ if override:
260
+ layers.append(
261
+ PromptLayer(
262
+ "session override", override, "# Session override", "session", "session"
263
+ )
264
+ )
265
+ lessons = learned().strip()
266
+ if lessons:
267
+ layers.append(PromptLayer("lessons (Mubit)", lessons, "# Lessons (Mubit)", "mubit"))
268
+ return layers
269
+
270
+
271
+ def effective_prompt(cwd: Path, session_override: str = "") -> str:
272
+ """The system prompt sent to the model: the rendered join of :func:`prompt_layers`."""
273
+ return "\n\n".join(layer.rendered for layer in prompt_layers(cwd, session_override))
274
+
275
+
276
+ def token_breakdown(cwd: Path, messages: list) -> dict[str, int]:
277
+ """Approximate token counts per section of the context that goes to the model."""
278
+ system = effective_prompt(cwd)
279
+ history = "\n".join(getattr(m, "text", "") for m in messages)
280
+ return {
281
+ "system": estimate_tokens(system),
282
+ "history": estimate_tokens(history),
283
+ "total": estimate_tokens(system) + estimate_tokens(history),
284
+ }
285
+
286
+
287
+ def layer_token_breakdown(
288
+ cwd: Path, messages: list, session_override: str = ""
289
+ ) -> dict[str, Any]:
290
+ """Per-layer token counts + history + total, for the layered prompt inspector."""
291
+ layers = prompt_layers(cwd, session_override)
292
+ history = estimate_tokens("\n".join(getattr(m, "text", "") for m in messages))
293
+ layer_tokens = [(layer.name, layer.tokens) for layer in layers]
294
+ system = sum(t for _, t in layer_tokens)
295
+ return {"layers": layer_tokens, "system": system, "history": history, "total": system + history}