velune-cli 0.9.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 (279) 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 +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -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
@@ -0,0 +1,270 @@
1
+ """Run command — velune run <task> to trigger Reasoning Council deliberation and sandbox execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from velune.cli.context import CLIContext
11
+ from velune.cognition.firewall import CognitiveFirewall
12
+ from velune.repository.schemas import RepositorySnapshot
13
+
14
+ console = Console()
15
+ run_cmd = typer.Typer(help="Autonomous council run commands")
16
+
17
+
18
+ def run_command(
19
+ ctx: typer.Context,
20
+ task: str = typer.Argument(..., help="Natural-language task to execute"),
21
+ dry_run: bool = typer.Option(
22
+ False,
23
+ "--dry-run",
24
+ "-d",
25
+ help="Deliberate but do not write modifications or execute scripts",
26
+ ),
27
+ force: bool = typer.Option(
28
+ False, "--force", "-f", help="Force execution without human confirm thresholds"
29
+ ),
30
+ yes: bool = typer.Option(
31
+ False, "--yes", "-y", help="Skip cost confirmation prompts (for scripting)"
32
+ ),
33
+ ) -> None:
34
+ """Deliberate with the stateful LangGraph Reasoning Council and execute in the secured sandbox."""
35
+
36
+ cli_context = ctx.obj
37
+ if not isinstance(cli_context, CLIContext):
38
+ raise typer.BadParameter("CLI context was not properly initialized")
39
+
40
+ # Propagate --yes into shared context so async helpers can read it
41
+ cli_context.yes = yes or cli_context.yes
42
+
43
+ from velune.core.event_loop import submit
44
+
45
+ submit(_run_command_async(cli_context, task, dry_run, force))
46
+
47
+
48
+ async def _run_command_async(
49
+ cli_context: CLIContext,
50
+ task: str,
51
+ dry_run: bool,
52
+ force: bool,
53
+ ) -> None:
54
+ # 1. Access modern services from the DI container
55
+ container = cli_context.container
56
+ lifecycle = container.get("runtime.lifecycle")
57
+ model_registry = container.get("runtime.model_registry")
58
+ orchestration_engine = container.get("runtime.orchestration_engine")
59
+ # 2. Boot up Cognitive OS Subsystems
60
+ if not cli_context.json_mode:
61
+ console.print("[bold cyan]⠋[/bold cyan] Bootstrapping Cognitive Operating System kernel...")
62
+ await lifecycle.startup()
63
+
64
+ # 3. Refresh model catalog scan to assign specialized seats
65
+ if not cli_context.json_mode:
66
+ console.print(
67
+ "[bold cyan]⠋[/bold cyan] Probing system hardware and local/remote providers..."
68
+ )
69
+ await model_registry.refresh()
70
+
71
+ # Onboarding preflight check gate
72
+ from velune.cli.commands.preflight import run_preflight_check
73
+
74
+ if not await run_preflight_check(container, console if not cli_context.json_mode else None):
75
+ if cli_context.json_mode:
76
+ import json
77
+
78
+ print(
79
+ json.dumps(
80
+ {
81
+ "error": "Preflight check failed. Ensure workspace is initialized and models are scanned."
82
+ }
83
+ )
84
+ )
85
+ await lifecycle.shutdown()
86
+ return
87
+
88
+ if not cli_context.json_mode:
89
+ console.print()
90
+ console.print(
91
+ Panel(
92
+ f"[bold green]Task Auth:[/bold green] {task}",
93
+ border_style="cyan",
94
+ title="[bold cyan]Velune Stateful Execution Pipeline[/bold cyan]",
95
+ )
96
+ )
97
+ # 4. Stream Multi-Agent Council Deliberation & Execution Graph
98
+ console.print(
99
+ "[bold magenta]🧠 Streaming LangGraph stateful execution & checkpoint pipeline...[/bold magenta]\n"
100
+ )
101
+
102
+ # --- Pre-operation cost estimation gate ---
103
+ if not cli_context.json_mode:
104
+ _maybe_confirm_cost(cli_context, task)
105
+
106
+ from velune.orchestration.schemas import ExecutionStatus
107
+
108
+ async def stream_runner():
109
+ milestones = []
110
+ async for milestone in orchestration_engine.stream(task):
111
+ if not cli_context.json_mode:
112
+ console.print(f" [bold cyan]•[/bold cyan] {milestone}")
113
+ milestones.append(milestone)
114
+
115
+ # Parse run_id from milestones (format: "[run_id] milestone_name")
116
+ run_id = None
117
+ for m in milestones:
118
+ if hasattr(m, "run_id"):
119
+ run_id = m.run_id
120
+ break
121
+ elif isinstance(m, str) and m.startswith("[") and "]" in m:
122
+ run_id = m.split("]")[0][1:]
123
+ break
124
+ return orchestration_engine.get_state(run_id) if run_id else None
125
+
126
+ state = await stream_runner()
127
+
128
+ # 5. Display Task Execution Results
129
+ if not cli_context.json_mode:
130
+ console.print()
131
+ if state is None:
132
+ if cli_context.json_mode:
133
+ import json
134
+
135
+ print(json.dumps({"error": "Pipeline failed to initialize state."}))
136
+ else:
137
+ console.print("[bold red]✗ Pipeline failed to initialize state.[/bold red]")
138
+ await lifecycle.shutdown()
139
+ return
140
+
141
+ success = state.status == ExecutionStatus.COMPLETED
142
+ attempts_count = len(state.attempts)
143
+ plan_steps = len(state.task_plan.steps) if state.task_plan else 0
144
+ checkpoints_count = len(state.checkpoints)
145
+
146
+ if cli_context.json_mode:
147
+ import json
148
+
149
+ print(
150
+ json.dumps(
151
+ {
152
+ "success": success,
153
+ "run_id": state.run_id,
154
+ "plan_steps": plan_steps,
155
+ "retry_attempts": attempts_count,
156
+ "checkpoints_saved": checkpoints_count,
157
+ "output": state.output or "Execution completed successfully.",
158
+ "error": state.error,
159
+ "validation_issues": state.validation_issues or [],
160
+ }
161
+ )
162
+ )
163
+ else:
164
+ if success:
165
+ console.print(
166
+ Panel(
167
+ Text.assemble(
168
+ ("[bold green]✓ STATEFUL AUTONOMOUS EXECUTION COMPLETED[/bold green]\n\n"),
169
+ (f"Run ID: [bold white]{state.run_id}[/bold white]\n"),
170
+ (f"Plan Steps: [bold white]{plan_steps}[/bold white] steps processed\n"),
171
+ (f"Retry Attempts: [bold white]{attempts_count}[/bold white]\n"),
172
+ (f"Checkpoints Saved: [bold white]{checkpoints_count}[/bold white]\n\n"),
173
+ ("[bold green]Synthesized Output:[/bold green]\n"),
174
+ (state.output or "Execution completed successfully."),
175
+ ),
176
+ border_style="green",
177
+ title="[bold green]Success Report[/bold green]",
178
+ )
179
+ )
180
+ else:
181
+ console.print(
182
+ Panel(
183
+ Text.assemble(
184
+ ("[bold red]✗ AUTONOMOUS PIPELINE BLOCKED & ROLLED BACK[/bold red]\n\n"),
185
+ (f"Run ID: [bold white]{state.run_id}[/bold white]\n"),
186
+ (
187
+ f"Failure Reason: [bold red]{state.error or 'Validation/Execution mismatch'}[/bold red]\n"
188
+ ),
189
+ (f"Retry Attempts: [bold white]{attempts_count}[/bold white]\n"),
190
+ (
191
+ f"Validation Issues: [bold yellow]{', '.join(state.validation_issues) if state.validation_issues else 'None'}[/bold yellow]\n\n"
192
+ ),
193
+ (
194
+ "[yellow]State checkpointer stashed checkpoints, and Git workspace states have been preserved/rolled back.[/yellow]"
195
+ ),
196
+ ),
197
+ border_style="red",
198
+ title="[bold red]Rollback Execution Report[/bold red]",
199
+ )
200
+ )
201
+
202
+ # 6. Graceful Shutdown
203
+ await lifecycle.shutdown()
204
+
205
+
206
+ def _maybe_confirm_cost(cli_context: CLIContext, task: str) -> None:
207
+ """Estimate cost of the upcoming run and prompt for confirmation if above threshold.
208
+
209
+ Raises SystemExit if the user declines. Skipped when --yes is set.
210
+ """
211
+ from velune.telemetry.cost_estimator import CostEstimator
212
+
213
+ # Heuristic: estimate from task text alone as lower-bound; council adds far more tokens.
214
+ # We use a conservative 8× multiplier to account for repo context + multi-agent turns.
215
+ task_messages = [{"role": "user", "content": task}]
216
+
217
+ try:
218
+ model_registry = cli_context.container.get("runtime.model_registry")
219
+ models = model_registry.list_all() if model_registry else []
220
+ except Exception:
221
+ models = []
222
+
223
+ # Pick the first non-local model as the representative for cost estimation
224
+ cloud_model = next((m for m in models if not getattr(m, "is_local", False)), None)
225
+ if cloud_model is None:
226
+ return # All local — no cost to warn about
227
+
228
+ estimator = CostEstimator()
229
+ base_tokens = estimator.estimate_tokens(task_messages, cloud_model)
230
+ estimated_tokens = base_tokens * 8 # council overhead multiplier
231
+ cost = estimator.estimate_cost(estimated_tokens, cloud_model)
232
+
233
+ if cost is None:
234
+ return
235
+
236
+ threshold = 0.01
237
+ try:
238
+ threshold = cli_context.config.providers.cost_threshold_usd
239
+ except Exception:
240
+ pass
241
+
242
+ if cost <= threshold:
243
+ return # Below threshold — proceed silently
244
+
245
+ estimate_str = estimator.format_estimate(estimated_tokens, cost, cloud_model)
246
+ console.print(f"\n[yellow]Estimated cost:[/yellow] {estimate_str}")
247
+
248
+ if cli_context.yes:
249
+ return
250
+
251
+ answer = console.input("Proceed? [Y/n] ").strip().lower()
252
+ if answer in ("n", "no"):
253
+ console.print("[red]Aborted.[/red]")
254
+ raise typer.Exit(0)
255
+
256
+
257
+ def _format_snapshot_context_safe(snapshot: RepositorySnapshot, firewall: CognitiveFirewall) -> str:
258
+ """Format the RepositorySnapshot securely as a text summary context for the planner/coder agents."""
259
+ lines = [f"Repository Root: {snapshot.root_path}"]
260
+ lines.append("Codebase Files:")
261
+ for f in snapshot.files[:25]:
262
+ # Only expose path and language — no raw symbol names or content
263
+ risk_marker = " [⚠ injection-risk]" if f.metadata.get("injection_risk") else ""
264
+ lines.append(f" - {f.path} ({f.language.value}){risk_marker}")
265
+ # Symbol names are safe to expose (identifiers, not content)
266
+ if f.symbols:
267
+ safe_syms = [s.name for s in f.symbols[:3] if s.name.isidentifier()]
268
+ if safe_syms:
269
+ lines.append(f" Symbols: {', '.join(safe_syms)}")
270
+ return "\n".join(lines)
@@ -0,0 +1,184 @@
1
+ """Interactive provider setup wizard — stores keys in the OS keychain."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Confirm, Prompt
8
+ from rich.table import Table
9
+
10
+ from velune.providers.keystore import get_key, has_key, save_key
11
+
12
+ console = Console()
13
+
14
+ PROVIDER_METADATA: dict[str, dict] = {
15
+ "ollama": {
16
+ "label": "Ollama (local — free, no key needed)",
17
+ "requires_key": False,
18
+ "free": True,
19
+ "url": "https://ollama.com",
20
+ },
21
+ "groq": {
22
+ "label": "Groq (cloud — free tier, very fast)",
23
+ "requires_key": True,
24
+ "free": True,
25
+ "key_label": "Groq API key",
26
+ "get_key_url": "https://console.groq.com/keys",
27
+ },
28
+ "openai": {
29
+ "label": "OpenAI (cloud — GPT-4o, paid)",
30
+ "requires_key": True,
31
+ "free": False,
32
+ "key_label": "OpenAI API key",
33
+ "get_key_url": "https://platform.openai.com/api-keys",
34
+ },
35
+ "anthropic": {
36
+ "label": "Anthropic (cloud — Claude, paid)",
37
+ "requires_key": True,
38
+ "free": False,
39
+ "key_label": "Anthropic API key",
40
+ "get_key_url": "https://console.anthropic.com",
41
+ },
42
+ "xai": {
43
+ "label": "xAI (cloud — Grok, paid)",
44
+ "requires_key": True,
45
+ "free": False,
46
+ "key_label": "xAI API key",
47
+ "get_key_url": "https://console.x.ai",
48
+ },
49
+ "google": {
50
+ "label": "Google Gemini (cloud — 2.0 Flash free quota)",
51
+ "requires_key": True,
52
+ "free": True,
53
+ "key_label": "Google API key",
54
+ "get_key_url": "https://aistudio.google.com/app/apikey",
55
+ },
56
+ "openrouter": {
57
+ "label": "OpenRouter (cloud — one key, 100+ models)",
58
+ "requires_key": True,
59
+ "free": False,
60
+ "key_label": "OpenRouter API key",
61
+ "get_key_url": "https://openrouter.ai/keys",
62
+ },
63
+ "together": {
64
+ "label": "Together.AI (cloud — Llama, Qwen, DeepSeek, cheap)",
65
+ "requires_key": True,
66
+ "free": False,
67
+ "key_label": "Together.AI API key",
68
+ "get_key_url": "https://api.together.ai/settings/api-keys",
69
+ },
70
+ "fireworks": {
71
+ "label": "Fireworks.AI (cloud — fastest open-model inference)",
72
+ "requires_key": True,
73
+ "free": False,
74
+ "key_label": "Fireworks.AI API key",
75
+ "get_key_url": "https://fireworks.ai/account/api-keys",
76
+ },
77
+ }
78
+
79
+
80
+ def run_setup_wizard() -> None:
81
+ """Run the interactive API key setup wizard."""
82
+ console.print(
83
+ Panel(
84
+ "[bold cyan]Velune Provider Setup[/bold cyan]\n"
85
+ "[dim]Configure which AI providers you want to use.[/dim]\n"
86
+ "[dim]Keys are stored securely in your OS keychain.[/dim]\n\n"
87
+ "🔒 [bold green]Privacy Notice:[/bold green] [dim]Your code and conversations stay on this machine unless you configure a cloud provider.[/dim]",
88
+ border_style="cyan",
89
+ padding=(0, 1),
90
+ )
91
+ )
92
+ console.print()
93
+
94
+ chosen = _select_providers()
95
+ if not chosen:
96
+ console.print("[yellow]No providers selected. Run `velune setup` any time.[/yellow]")
97
+ return
98
+
99
+ configured: list[str] = []
100
+ for pid in chosen:
101
+ meta = PROVIDER_METADATA[pid]
102
+ if not meta.get("requires_key"):
103
+ console.print(f"[green]✓[/green] {meta['label']} — no key required")
104
+ configured.append(pid)
105
+ continue
106
+
107
+ console.print(f"\n[cyan]{meta['label']}[/cyan]")
108
+ console.print(f" [dim]Get your key: {meta.get('get_key_url', '')}[/dim]")
109
+
110
+ if has_key(pid):
111
+ existing = get_key(pid)
112
+ masked = _mask_key(existing)
113
+ overwrite = Confirm.ask(
114
+ f" Key already configured ({masked}). Replace it?",
115
+ default=False,
116
+ )
117
+ if not overwrite:
118
+ configured.append(pid)
119
+ continue
120
+
121
+ key = Prompt.ask(
122
+ f" Enter {meta['key_label']}",
123
+ password=True,
124
+ )
125
+
126
+ if not key.strip():
127
+ console.print(" [yellow]Skipped — no key entered.[/yellow]")
128
+ continue
129
+
130
+ save_key(pid, key.strip())
131
+ console.print(" [green]✓ Saved to OS keychain[/green]")
132
+ configured.append(pid)
133
+
134
+ console.print()
135
+ if configured:
136
+ console.print(f"[bold green]✓ Configured providers:[/bold green] {', '.join(configured)}")
137
+ console.print("[dim]Run `velune doctor` to verify connectivity.[/dim]")
138
+
139
+
140
+ def _select_providers() -> list[str]:
141
+ table = Table(border_style="dim", padding=(0, 1))
142
+ table.add_column("#", style="dim", width=3)
143
+ table.add_column("Provider", style="cyan")
144
+ table.add_column("Type", style="dim")
145
+ table.add_column("Cost", style="dim")
146
+ table.add_column("Status")
147
+
148
+ provider_ids = list(PROVIDER_METADATA.keys())
149
+ for i, pid in enumerate(provider_ids, 1):
150
+ meta = PROVIDER_METADATA[pid]
151
+ cost = "[green]free[/green]" if meta.get("free") else "[dim]paid[/dim]"
152
+ status = "[green]✓ configured[/green]" if has_key(pid) else "[dim]not set[/dim]"
153
+ ptype = "local" if not meta.get("requires_key") else "cloud"
154
+ table.add_row(str(i), meta["label"], ptype, cost, status)
155
+
156
+ console.print(table)
157
+ console.print()
158
+
159
+ raw = Prompt.ask(
160
+ "Select providers to configure [dim](comma-separated numbers, e.g. 1,2,3)[/dim]",
161
+ default="1",
162
+ )
163
+
164
+ selected: list[str] = []
165
+ for part in raw.split(","):
166
+ part = part.strip()
167
+ if part.isdigit():
168
+ idx = int(part) - 1
169
+ if 0 <= idx < len(provider_ids):
170
+ selected.append(provider_ids[idx])
171
+ return selected
172
+
173
+
174
+ def _mask_key(key: str | None) -> str:
175
+ if not key:
176
+ return "***"
177
+ if len(key) <= 12:
178
+ return "*" * len(key)
179
+ return key[:8] + "..." + key[-4:]
180
+
181
+
182
+ def setup_command() -> None:
183
+ """Configure AI provider API keys."""
184
+ run_setup_wizard()