velune-cli 1.0.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 (229) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +212 -0
  5. velune/cli/autocomplete.py +76 -0
  6. velune/cli/banner.py +98 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +149 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +188 -0
  11. velune/cli/commands/config.py +182 -0
  12. velune/cli/commands/daemon.py +85 -0
  13. velune/cli/commands/doctor.py +373 -0
  14. velune/cli/commands/init.py +160 -0
  15. velune/cli/commands/mcp.py +80 -0
  16. velune/cli/commands/memory.py +269 -0
  17. velune/cli/commands/models.py +462 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +171 -0
  20. velune/cli/commands/setup.py +182 -0
  21. velune/cli/commands/workspace.py +217 -0
  22. velune/cli/context.py +37 -0
  23. velune/cli/councilmodel_ui.py +171 -0
  24. velune/cli/display/council_view.py +240 -0
  25. velune/cli/display/memory_view.py +93 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +21 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +44 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +118 -0
  33. velune/cli/registry.py +81 -0
  34. velune/cli/repl.py +1178 -0
  35. velune/cli/session_manager.py +69 -0
  36. velune/cli/slash_commands.py +37 -0
  37. velune/cognition/__init__.py +19 -0
  38. velune/cognition/arbitrator.py +216 -0
  39. velune/cognition/architecture.py +398 -0
  40. velune/cognition/council/__init__.py +47 -0
  41. velune/cognition/council/base.py +216 -0
  42. velune/cognition/council/challenger.py +70 -0
  43. velune/cognition/council/coder.py +79 -0
  44. velune/cognition/council/critic_agent.py +39 -0
  45. velune/cognition/council/critic_configs.py +111 -0
  46. velune/cognition/council/critics.py +41 -0
  47. velune/cognition/council/debate.py +44 -0
  48. velune/cognition/council/factory.py +140 -0
  49. velune/cognition/council/messages.py +53 -0
  50. velune/cognition/council/planner.py +119 -0
  51. velune/cognition/council/reviewer.py +72 -0
  52. velune/cognition/council/synthesizer.py +67 -0
  53. velune/cognition/council/tiers.py +181 -0
  54. velune/cognition/firewall.py +256 -0
  55. velune/cognition/module.py +38 -0
  56. velune/cognition/orchestrator.py +886 -0
  57. velune/cognition/personality.py +236 -0
  58. velune/cognition/style_resolver.py +62 -0
  59. velune/cognition/verification.py +201 -0
  60. velune/context/__init__.py +11 -0
  61. velune/context/extractive.py +94 -0
  62. velune/context/window.py +62 -0
  63. velune/core/__init__.py +89 -0
  64. velune/core/background.py +5 -0
  65. velune/core/config/__init__.py +37 -0
  66. velune/core/errors/__init__.py +53 -0
  67. velune/core/errors/execution.py +26 -0
  68. velune/core/errors/memory.py +21 -0
  69. velune/core/errors/orchestration.py +26 -0
  70. velune/core/errors/provider.py +31 -0
  71. velune/core/event_loop.py +30 -0
  72. velune/core/logging.py +83 -0
  73. velune/core/runtime.py +106 -0
  74. velune/core/task_registry.py +120 -0
  75. velune/core/trace.py +80 -0
  76. velune/core/types/__init__.py +48 -0
  77. velune/core/types/agent.py +49 -0
  78. velune/core/types/context.py +39 -0
  79. velune/core/types/inference.py +35 -0
  80. velune/core/types/memory.py +39 -0
  81. velune/core/types/model.py +64 -0
  82. velune/core/types/provider.py +35 -0
  83. velune/core/types/repository.py +35 -0
  84. velune/core/types/task.py +56 -0
  85. velune/core/types/workspace.py +28 -0
  86. velune/daemon/client.py +13 -0
  87. velune/daemon/server.py +115 -0
  88. velune/daemon/transport.py +169 -0
  89. velune/events.py +194 -0
  90. velune/execution/__init__.py +22 -0
  91. velune/execution/benchmarker.py +311 -0
  92. velune/execution/cancellation.py +53 -0
  93. velune/execution/checkpointer.py +128 -0
  94. velune/execution/command_spec.py +140 -0
  95. velune/execution/diff_preview.py +172 -0
  96. velune/execution/executor.py +173 -0
  97. velune/execution/module.py +16 -0
  98. velune/execution/multi_diff.py +70 -0
  99. velune/execution/path_guard.py +19 -0
  100. velune/execution/planner.py +91 -0
  101. velune/execution/rollback.py +80 -0
  102. velune/execution/sandbox.py +257 -0
  103. velune/execution/validator.py +113 -0
  104. velune/hardware/__init__.py +1 -0
  105. velune/hardware/detector.py +162 -0
  106. velune/kernel/__init__.py +58 -0
  107. velune/kernel/bootstrap.py +107 -0
  108. velune/kernel/config.py +252 -0
  109. velune/kernel/health.py +54 -0
  110. velune/kernel/lifecycle.py +102 -0
  111. velune/kernel/module.py +15 -0
  112. velune/kernel/modules.py +23 -0
  113. velune/kernel/registry.py +93 -0
  114. velune/kernel/schemas.py +28 -0
  115. velune/main.py +9 -0
  116. velune/mcp/__init__.py +9 -0
  117. velune/mcp/client.py +113 -0
  118. velune/mcp/config.py +19 -0
  119. velune/mcp/server.py +90 -0
  120. velune/memory/__init__.py +28 -0
  121. velune/memory/lifecycle.py +154 -0
  122. velune/memory/module.py +94 -0
  123. velune/memory/prioritizer.py +65 -0
  124. velune/memory/storage/sqlite_manager.py +368 -0
  125. velune/memory/tiers/episodic.py +156 -0
  126. velune/memory/tiers/graph.py +282 -0
  127. velune/memory/tiers/lineage.py +367 -0
  128. velune/memory/tiers/semantic.py +198 -0
  129. velune/memory/tiers/working.py +165 -0
  130. velune/models/__init__.py +16 -0
  131. velune/models/module.py +18 -0
  132. velune/models/probes.py +182 -0
  133. velune/models/profile_cache.py +82 -0
  134. velune/models/profiler.py +105 -0
  135. velune/models/registry.py +201 -0
  136. velune/models/scorer.py +225 -0
  137. velune/models/specializations.py +196 -0
  138. velune/orchestration/__init__.py +15 -0
  139. velune/orchestration/module.py +14 -0
  140. velune/orchestration/role_assignments.py +78 -0
  141. velune/orchestration/schemas.py +99 -0
  142. velune/plugins/__init__.py +13 -0
  143. velune/plugins/hooks.py +49 -0
  144. velune/plugins/loader.py +95 -0
  145. velune/plugins/registry.py +54 -0
  146. velune/plugins/schemas.py +19 -0
  147. velune/providers/__init__.py +18 -0
  148. velune/providers/adapters/anthropic.py +231 -0
  149. velune/providers/adapters/fireworks.py +115 -0
  150. velune/providers/adapters/google.py +234 -0
  151. velune/providers/adapters/groq.py +151 -0
  152. velune/providers/adapters/huggingface.py +203 -0
  153. velune/providers/adapters/llamacpp.py +202 -0
  154. velune/providers/adapters/lmstudio.py +173 -0
  155. velune/providers/adapters/ollama.py +186 -0
  156. velune/providers/adapters/openai.py +207 -0
  157. velune/providers/adapters/openrouter.py +81 -0
  158. velune/providers/adapters/together.py +134 -0
  159. velune/providers/adapters/xai.py +60 -0
  160. velune/providers/base.py +86 -0
  161. velune/providers/benchmarker.py +135 -0
  162. velune/providers/discovery/__init__.py +33 -0
  163. velune/providers/discovery/anthropic.py +77 -0
  164. velune/providers/discovery/benchmarks.py +44 -0
  165. velune/providers/discovery/classifier.py +69 -0
  166. velune/providers/discovery/fireworks.py +95 -0
  167. velune/providers/discovery/gguf.py +87 -0
  168. velune/providers/discovery/google.py +95 -0
  169. velune/providers/discovery/gpu.py +109 -0
  170. velune/providers/discovery/groq.py +20 -0
  171. velune/providers/discovery/huggingface.py +67 -0
  172. velune/providers/discovery/lmstudio.py +80 -0
  173. velune/providers/discovery/ollama.py +165 -0
  174. velune/providers/discovery/openai.py +96 -0
  175. velune/providers/discovery/openrouter.py +113 -0
  176. velune/providers/discovery/scanner.py +114 -0
  177. velune/providers/discovery/together.py +114 -0
  178. velune/providers/discovery/xai.py +57 -0
  179. velune/providers/keystore.py +83 -0
  180. velune/providers/local_paths.py +49 -0
  181. velune/providers/local_resolver.py +208 -0
  182. velune/providers/module.py +15 -0
  183. velune/providers/ollama_manager.py +193 -0
  184. velune/providers/registry.py +173 -0
  185. velune/providers/router.py +82 -0
  186. velune/py.typed +0 -0
  187. velune/repository/__init__.py +21 -0
  188. velune/repository/analyzer.py +123 -0
  189. velune/repository/cognition.py +172 -0
  190. velune/repository/grapher.py +182 -0
  191. velune/repository/indexer.py +229 -0
  192. velune/repository/module.py +15 -0
  193. velune/repository/parser.py +378 -0
  194. velune/repository/project_type.py +293 -0
  195. velune/repository/scanner.py +177 -0
  196. velune/repository/schemas.py +102 -0
  197. velune/repository/tracker.py +233 -0
  198. velune/retrieval/__init__.py +27 -0
  199. velune/retrieval/graph.py +117 -0
  200. velune/retrieval/hybrid.py +250 -0
  201. velune/retrieval/keyword.py +113 -0
  202. velune/retrieval/module.py +19 -0
  203. velune/retrieval/reranker.py +68 -0
  204. velune/retrieval/schemas.py +59 -0
  205. velune/retrieval/vector.py +163 -0
  206. velune/telemetry/__init__.py +7 -0
  207. velune/telemetry/cognition.py +260 -0
  208. velune/telemetry/token_tracker.py +133 -0
  209. velune/tools/__init__.py +41 -0
  210. velune/tools/base/registry.py +84 -0
  211. velune/tools/base/tool.py +63 -0
  212. velune/tools/code/navigate.py +107 -0
  213. velune/tools/code/search.py +112 -0
  214. velune/tools/filesystem/read.py +75 -0
  215. velune/tools/filesystem/search.py +123 -0
  216. velune/tools/filesystem/write.py +160 -0
  217. velune/tools/git/history.py +185 -0
  218. velune/tools/git/operations.py +132 -0
  219. velune/tools/git/state.py +134 -0
  220. velune/tools/module.py +65 -0
  221. velune/tools/terminal/execute.py +72 -0
  222. velune/tools/terminal/history.py +47 -0
  223. velune/tools/web/fetch.py +55 -0
  224. velune/tools/web/validator.py +96 -0
  225. velune_cli-1.0.0.dist-info/METADATA +497 -0
  226. velune_cli-1.0.0.dist-info/RECORD +229 -0
  227. velune_cli-1.0.0.dist-info/WHEEL +4 -0
  228. velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
  229. velune_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,462 @@
1
+ """Models command - velune models scan/list/assign."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from velune.cli.context import CLIContext
12
+
13
+ console = Console()
14
+
15
+ models_cmd = typer.Typer(help="Model management commands")
16
+
17
+
18
+ @models_cmd.command("scan")
19
+ def models_scan(
20
+ ctx: typer.Context,
21
+ provider: str = typer.Option(None, "--provider", "-p", help="Specific provider to scan"),
22
+ probe: bool = typer.Option(False, "--probe", help="Run empirical capability probes synchronously and cache results"),
23
+ ) -> None:
24
+ """Scan for available models."""
25
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
26
+ if cli_context is None:
27
+ if ctx.obj and getattr(ctx.obj, "json_mode", False):
28
+ import json
29
+ print(json.dumps({"error": "Model discovery service is unavailable"}))
30
+ else:
31
+ console.print("[red]Model discovery service is unavailable.[/red]")
32
+ raise typer.Exit(code=1)
33
+
34
+ from velune.core.event_loop import submit
35
+ records = submit(_models_scan_async(cli_context, provider, probe))
36
+
37
+ from velune.core.types.model import CapabilityLevel
38
+
39
+ if cli_context.json_mode:
40
+ import json
41
+ out = []
42
+ for record in records:
43
+ capabilities_map = {
44
+ "coding": record.capabilities.coding,
45
+ "reasoning": record.capabilities.reasoning,
46
+ "planning": record.capabilities.planning,
47
+ "summarization": record.capabilities.summarization,
48
+ "tool_use": record.capabilities.tool_use,
49
+ }
50
+ highest_cap = "general"
51
+ highest_level = CapabilityLevel.NONE
52
+ for cap_name, level in capabilities_map.items():
53
+ if level > highest_level:
54
+ highest_level = level
55
+ highest_cap = cap_name
56
+ specialization = highest_cap if highest_level > CapabilityLevel.NONE else "general"
57
+ embedding_supported = record.capabilities.embedding > CapabilityLevel.NONE
58
+ validated = record.metadata.get("validated")
59
+ status = "cached" if validated is None else ("online" if validated else "offline")
60
+
61
+ out.append({
62
+ "provider_id": record.provider_id,
63
+ "model_id": record.model_id,
64
+ "specialization": specialization,
65
+ "speed_tier": record.speed_tier,
66
+ "context_length": record.context_length,
67
+ "embedding_supported": embedding_supported,
68
+ "status": status,
69
+ })
70
+ print(json.dumps(out))
71
+ return
72
+
73
+ table = Table(title="Discovered Models")
74
+ table.add_column("Provider", style="cyan")
75
+ table.add_column("Model", style="green")
76
+ table.add_column("Specialization", style="magenta")
77
+ table.add_column("Speed", style="blue")
78
+ table.add_column("Context", style="yellow")
79
+ table.add_column("Embedding", style="white")
80
+ table.add_column("Status", style="bold")
81
+
82
+ for record in records:
83
+ capabilities_map = {
84
+ "coding": record.capabilities.coding,
85
+ "reasoning": record.capabilities.reasoning,
86
+ "planning": record.capabilities.planning,
87
+ "summarization": record.capabilities.summarization,
88
+ "tool_use": record.capabilities.tool_use,
89
+ }
90
+
91
+ highest_cap = "general"
92
+ highest_level = CapabilityLevel.NONE
93
+ for cap_name, level in capabilities_map.items():
94
+ if level > highest_level:
95
+ highest_level = level
96
+ highest_cap = cap_name
97
+
98
+ specialization = highest_cap if highest_level > CapabilityLevel.NONE else "general"
99
+ embedding_supported = "yes" if record.capabilities.embedding > CapabilityLevel.NONE else "no"
100
+
101
+ validated = record.metadata.get("validated")
102
+ if validated is None:
103
+ status = "[dim]cached[/dim]"
104
+ elif validated:
105
+ status = "[green]●[/green]"
106
+ else:
107
+ status = "[red]✗ offline[/red]"
108
+
109
+ table.add_row(
110
+ record.provider_id,
111
+ record.model_id,
112
+ specialization,
113
+ record.speed_tier,
114
+ str(record.context_length),
115
+ embedding_supported,
116
+ status,
117
+ )
118
+
119
+ console.print(table)
120
+
121
+ total = len(records)
122
+ providers = set(r.provider_id for r in records)
123
+ console.print(
124
+ f"[dim]Discovered {total} model(s) across {len(providers)} provider(s).[/dim]"
125
+ )
126
+
127
+
128
+ async def _models_scan_async(
129
+ cli_context: CLIContext,
130
+ provider_id: str | None,
131
+ probe: bool
132
+ ) -> Any:
133
+ container = cli_context.container
134
+ lifecycle = container.get("runtime.lifecycle")
135
+ discovery = container.get("runtime.model_discovery")
136
+ provider_registry = container.get("runtime.provider_registry")
137
+
138
+ if probe:
139
+ await lifecycle.startup()
140
+
141
+ try:
142
+ if provider_id:
143
+ records = await discovery.scan_provider(provider_id=provider_id)
144
+ else:
145
+ records = await discovery.scan_all()
146
+
147
+ if probe:
148
+ from pathlib import Path
149
+
150
+ from velune.models.probes import FastProbe, ModelProber
151
+ from velune.models.profile_cache import ModelProfileCache
152
+
153
+ profile_cache = ModelProfileCache(Path(".velune") / "model_profiles.json")
154
+ fast_probe = FastProbe()
155
+
156
+ if not cli_context.json_mode:
157
+ console.print("[bold cyan]⠋[/bold cyan] Probing discovered models synchronously...")
158
+
159
+ probe_tasks = []
160
+ valid_records = []
161
+
162
+ for record in records:
163
+ provider = provider_registry.get(record.provider_id)
164
+ if provider:
165
+ valid_records.append(record)
166
+ probe_tasks.append(fast_probe.ping(provider, record.model_id))
167
+
168
+ if valid_records:
169
+ import asyncio
170
+ responsiveness = await asyncio.gather(*probe_tasks, return_exceptions=True)
171
+
172
+ empirical_probe_tasks = []
173
+ probing_models = []
174
+
175
+ for record, is_responsive in zip(valid_records, responsiveness):
176
+ if is_responsive is True:
177
+ provider = provider_registry.get(record.provider_id)
178
+ prober = ModelProber(provider, record.model_id)
179
+ probing_models.append((record, prober))
180
+ empirical_probe_tasks.append(prober.run_all_probes())
181
+ record.metadata["validated"] = True
182
+ else:
183
+ record.metadata["validated"] = False
184
+
185
+ if empirical_probe_tasks:
186
+ if not cli_context.json_mode:
187
+ console.print(f"[bold magenta]⚡ Running empirical capability probes for {len(empirical_probe_tasks)} active model(s)...[/bold magenta]")
188
+ results = await asyncio.gather(*empirical_probe_tasks, return_exceptions=True)
189
+
190
+ for (record, prober), result in zip(probing_models, results):
191
+ # gather(return_exceptions=True) can also surface BaseException
192
+ # subclasses (e.g. asyncio.CancelledError); treat any of them
193
+ # as a failed probe so they are never cached as valid results.
194
+ if isinstance(result, BaseException):
195
+ if not cli_context.json_mode:
196
+ console.print(f"[red]✗[/red] Probe failed for {record.model_id}: {result}")
197
+ continue
198
+
199
+ profile_cache.set(record.model_id, record.provider_id, result)
200
+
201
+ registry = container.get("runtime.model_registry")
202
+ if registry:
203
+ registry._apply_probe_results(record, result)
204
+ registry.register(record)
205
+
206
+ if not cli_context.json_mode:
207
+ console.print("[bold green]✓[/bold green] Empirical benchmarks completed and cached.")
208
+
209
+ return records
210
+ finally:
211
+ if probe:
212
+ await lifecycle.shutdown()
213
+
214
+
215
+ @models_cmd.command("list")
216
+ def models_list(ctx: typer.Context) -> None:
217
+ """List registered models."""
218
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
219
+ registry = cli_context.container.get("runtime.model_registry") if cli_context else None
220
+
221
+ if cli_context and cli_context.json_mode:
222
+ import json
223
+ out = []
224
+ if registry is not None:
225
+ from velune.core.types.model import CapabilityLevel
226
+ records = registry.list_all()
227
+ for record in records:
228
+ capabilities = []
229
+ for cap_name in ["coding", "reasoning", "planning", "summarization", "tool_use", "long_context"]:
230
+ level = getattr(record.capabilities, cap_name, None)
231
+ if level and level > CapabilityLevel.NONE:
232
+ capabilities.append(cap_name)
233
+ out.append({
234
+ "model_id": record.model_id,
235
+ "display_name": record.display_name,
236
+ "provider_id": record.provider_id,
237
+ "capabilities": capabilities,
238
+ })
239
+ print(json.dumps(out))
240
+ return
241
+
242
+ table = Table(title="Registered Models")
243
+ table.add_column("ID", style="cyan")
244
+ table.add_column("Name", style="green")
245
+ table.add_column("Provider", style="magenta")
246
+ table.add_column("Capabilities", style="blue")
247
+
248
+ records = []
249
+ if registry is None:
250
+ table.add_row("<uninitialized>", "Velune", "system", "bootstrap only")
251
+ else:
252
+ from velune.core.types.model import CapabilityLevel
253
+ records = registry.list_all()
254
+ for record in records:
255
+ capabilities = []
256
+ for cap_name in ["coding", "reasoning", "planning", "summarization", "tool_use", "long_context"]:
257
+ level = getattr(record.capabilities, cap_name, None)
258
+ if level and level > CapabilityLevel.NONE:
259
+ capabilities.append(f"{cap_name} ({level.name})")
260
+
261
+ table.add_row(
262
+ record.model_id,
263
+ record.display_name,
264
+ record.provider_id,
265
+ ", ".join(capabilities) or "none",
266
+ )
267
+
268
+ console.print(table)
269
+
270
+ # Get GPU info and show VRAM details
271
+ gpu_info = None
272
+ if cli_context:
273
+ try:
274
+ gpu_info = cli_context.container.get("runtime.gpu_info")
275
+ except Exception:
276
+ pass
277
+
278
+ if gpu_info and gpu_info.get("has_gpu"):
279
+ free_gb = gpu_info.get("vram_free_gb")
280
+ if free_gb is not None:
281
+ console.print(f"[dim]Available VRAM: {free_gb:.1f}GB[/dim]")
282
+
283
+ over_budget = [m for m in records if
284
+ m.vram_required_gb and m.vram_required_gb > free_gb]
285
+ if over_budget:
286
+ console.print(f"[yellow]⚠ {len(over_budget)} models exceed available VRAM[/yellow]")
287
+
288
+
289
+ @models_cmd.command("assign")
290
+ def models_assign(
291
+ ctx: typer.Context,
292
+ role: str = typer.Argument(..., help="Agent role (planner, coder, reviewer, challenger, synthesizer)"),
293
+ model_id: str = typer.Argument(..., help="Model ID to assign"),
294
+ ) -> None:
295
+ """Assign a model to an agent role."""
296
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
297
+ orchestrator = cli_context.container.get("runtime.council_orchestrator") if cli_context else None
298
+
299
+ if orchestrator is None:
300
+ if cli_context and cli_context.json_mode:
301
+ import json
302
+ print(json.dumps({"error": "Council orchestrator is unavailable"}))
303
+ else:
304
+ console.print("[red]Council orchestrator is unavailable.[/red]")
305
+ raise typer.Exit(code=1)
306
+
307
+ mapper = orchestrator.mapper
308
+ try:
309
+ from velune.models.specializations import CouncilRole
310
+ council_role = CouncilRole(role.lower())
311
+ except ValueError:
312
+ if cli_context and cli_context.json_mode:
313
+ import json
314
+ print(json.dumps({"error": f"Invalid role '{role}'"}))
315
+ else:
316
+ console.print(f"[red]Invalid role '{role}'. Must be one of: planner, coder, reviewer, challenger, synthesizer[/red]")
317
+ raise typer.Exit(code=1)
318
+
319
+ # Check if model exists
320
+ registry = cli_context.container.get("runtime.model_registry") if cli_context else None
321
+ if registry:
322
+ descriptor = registry.get(model_id)
323
+ if not descriptor and not (cli_context and cli_context.json_mode):
324
+ console.print(f"[yellow]Warning: Model '{model_id}' is not currently registered/discovered.[/yellow]")
325
+
326
+ mapper.overrides[council_role] = model_id
327
+ if cli_context and cli_context.json_mode:
328
+ import json
329
+ print(json.dumps({"success": True, "role": council_role.value, "model_id": model_id}))
330
+ else:
331
+ console.print(f"[green]Successfully assigned role '{council_role.value}' to model '{model_id}' for the current runtime context.[/green]")
332
+
333
+
334
+ @models_cmd.command("benchmark")
335
+ def models_benchmark(
336
+ ctx: typer.Context,
337
+ model_id: str = typer.Argument(None, help="Specific model ID to benchmark. If omitted, benchmarks all registered models."),
338
+ ) -> None:
339
+ """Run capability probes on a specific model or all registered models."""
340
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
341
+ if not cli_context:
342
+ if ctx.obj and getattr(ctx.obj, "json_mode", False):
343
+ import json
344
+ print(json.dumps({"error": "CLI context is unavailable"}))
345
+ else:
346
+ console.print("[red]CLI context is unavailable.[/red]")
347
+ raise typer.Exit(code=1)
348
+
349
+ registry = cli_context.container.get("runtime.model_registry")
350
+ provider_registry = cli_context.container.get("runtime.provider_registry")
351
+
352
+ if registry is None or provider_registry is None:
353
+ if cli_context.json_mode:
354
+ import json
355
+ print(json.dumps({"error": "Model registry or provider registry is unavailable"}))
356
+ else:
357
+ console.print("[red]Model registry or provider registry is unavailable.[/red]")
358
+ raise typer.Exit(code=1)
359
+
360
+ # Get the models to benchmark
361
+ models_to_probe = []
362
+ if model_id:
363
+ model = registry.get(model_id)
364
+ if not model:
365
+ if cli_context.json_mode:
366
+ import json
367
+ print(json.dumps({"error": f"Model '{model_id}' is not registered"}))
368
+ else:
369
+ console.print(f"[red]Model '{model_id}' is not registered.[/red]")
370
+ raise typer.Exit(code=1)
371
+ models_to_probe.append(model)
372
+ else:
373
+ models_to_probe = registry.list_all()
374
+
375
+ if not models_to_probe:
376
+ if cli_context.json_mode:
377
+ import json
378
+ print(json.dumps([]))
379
+ else:
380
+ console.print("[yellow]No models registered for benchmarking.[/yellow]")
381
+ return
382
+
383
+ from velune.core.event_loop import submit
384
+ submit(_models_benchmark_async(cli_context, registry, provider_registry, models_to_probe))
385
+
386
+
387
+ async def _models_benchmark_async(
388
+ cli_context: CLIContext,
389
+ registry: Any,
390
+ provider_registry: Any,
391
+ models_to_probe: list[Any],
392
+ ) -> None:
393
+ import json
394
+ from pathlib import Path
395
+
396
+ from velune.models.probes import ModelProber
397
+ from velune.models.profile_cache import ModelProfileCache
398
+
399
+ profile_cache = ModelProfileCache(Path(".velune") / "model_profiles.json")
400
+
401
+ table = Table(title="Model Empirical Benchmark Results")
402
+ table.add_column("Model ID", style="cyan")
403
+ table.add_column("Provider", style="magenta")
404
+ table.add_column("Coding Score (Lat)", style="green")
405
+ table.add_column("Reasoning Score (Lat)", style="blue")
406
+ table.add_column("Instruction Score (Lat)", style="yellow")
407
+ table.add_column("Source", style="white")
408
+
409
+ json_results = []
410
+
411
+ for model in models_to_probe:
412
+ provider = provider_registry.get(model.provider_id)
413
+ if not provider:
414
+ if not cli_context.json_mode:
415
+ console.print(f"[yellow]Skipping model '{model.model_id}': Provider '{model.provider_id}' is not active or available.[/yellow]")
416
+ continue
417
+
418
+ if not cli_context.json_mode:
419
+ console.print(f"Benchmarking model [bold cyan]{model.model_id}[/bold cyan] via [bold magenta]{model.provider_id}[/bold magenta]...")
420
+
421
+ prober = ModelProber(provider, model.model_id)
422
+ results = await prober.run_all_probes()
423
+
424
+ # Save to cache
425
+ profile_cache.set(model.model_id, model.provider_id, results)
426
+ # Apply to registry in-memory
427
+ registry._apply_probe_results(model, results)
428
+
429
+ coding = results["coding"]
430
+ reasoning = results["reasoning"]
431
+ instruction = results["instruction"]
432
+
433
+ if cli_context.json_mode:
434
+ json_results.append({
435
+ "model_id": model.model_id,
436
+ "provider_id": model.provider_id,
437
+ "results": {
438
+ "coding": {"score": coding.score, "latency_ms": coding.latency_ms, "passed": coding.passed},
439
+ "reasoning": {"score": reasoning.score, "latency_ms": reasoning.latency_ms, "passed": reasoning.passed},
440
+ "instruction": {"score": instruction.score, "latency_ms": instruction.latency_ms, "passed": instruction.passed}
441
+ }
442
+ })
443
+ else:
444
+ def format_result(res) -> str:
445
+ if res.latency_ms < 0:
446
+ return "[red]Failed[/red]"
447
+ color = "green" if res.passed else "yellow"
448
+ return f"[{color}]{res.score:.2f}[/{color}] ({res.latency_ms:.0f}ms)"
449
+
450
+ table.add_row(
451
+ model.model_id,
452
+ model.provider_id,
453
+ format_result(coding),
454
+ format_result(reasoning),
455
+ format_result(instruction),
456
+ "empirical (forced)",
457
+ )
458
+
459
+ if cli_context.json_mode:
460
+ print(json.dumps(json_results))
461
+ else:
462
+ console.print(table)
@@ -0,0 +1,95 @@
1
+ """Preflight check validation for model availability and workspace initialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from rich.box import ROUNDED
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+
12
+ from velune.kernel.registry import ServiceContainer
13
+
14
+
15
+ async def run_preflight_check(container: ServiceContainer, console: Console | None = None) -> bool:
16
+ """Runs preflight checks for models and workspace state.
17
+
18
+ If checks fail, displays a gorgeous error panel with copy-pasteable fix commands and returns False.
19
+ Otherwise returns True.
20
+ """
21
+ issues = []
22
+
23
+ # 1. Check workspace initialization
24
+ workspace = container.get("runtime.workspace")
25
+ if not isinstance(workspace, Path):
26
+ workspace = Path(workspace)
27
+
28
+ # Guard: workspace must exist and be a git repository. On a brand-new
29
+ # install neither will be true — show a single targeted message and bail
30
+ # early rather than cascading through checks that will all fail.
31
+ if not workspace.exists() or not (workspace / ".git").exists():
32
+ if console:
33
+ console.print()
34
+ console.print(
35
+ Panel(
36
+ Text.from_markup(
37
+ "This doesn't look like a code project yet. Navigate to your project\n"
38
+ "folder and run [bold green]velune workspace init[/bold green] first."
39
+ ),
40
+ title="[bold yellow]⚠️ Not a Project Directory[/bold yellow]",
41
+ border_style="yellow",
42
+ box=ROUNDED,
43
+ padding=(1, 2),
44
+ )
45
+ )
46
+ console.print()
47
+ return False
48
+
49
+ # We check for the presence of the Tree-sitter AST index folder or `.velune` directory structure
50
+ if not (workspace / ".velune" / "index").exists():
51
+ issues.append(
52
+ "Workspace has not been initialized yet.\n"
53
+ " [bold white]Fix:[/bold white] Run the initialization command to parse the codebase:\n"
54
+ " [bold green]velune workspace init[/bold green]"
55
+ )
56
+
57
+ # 2. Check models registry
58
+ registry = container.get("runtime.model_registry")
59
+ models = registry.list_all()
60
+ if not models:
61
+ issues.append(
62
+ "No model providers or local LLM instances were detected.\n"
63
+ " [bold white]Fix:[/bold white] Make sure Ollama/LM-Studio is running, or check API keys, and run:\n"
64
+ " [bold green]velune models scan --probe[/bold green]"
65
+ )
66
+
67
+ if issues:
68
+ if console:
69
+ console.print()
70
+
71
+ body_elements = [
72
+ "[bold red]Velune preflight check failed with the following blocking issues:[/bold red]\n"
73
+ ]
74
+ for i, issue in enumerate(issues, 1):
75
+ body_elements.append(f"\n[bold red]{i}.[/bold red] {issue}\n")
76
+
77
+ body_elements.append(
78
+ "\n[dim]Ensure these preflight requirements are satisfied before running Reasoning Council tasks.[/dim]"
79
+ )
80
+
81
+ panel_content = Text.from_markup("".join(body_elements))
82
+
83
+ console.print(
84
+ Panel(
85
+ panel_content,
86
+ title="[bold red]⚠️ Preflight Check Blocked[/bold red]",
87
+ border_style="red",
88
+ box=ROUNDED,
89
+ padding=(1, 2),
90
+ )
91
+ )
92
+ console.print()
93
+ return False
94
+
95
+ return True