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,228 @@
1
+ """Interactive conversational chat entry point with low-latency streaming."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from velune.cli.context import CLIContext
9
+ from velune.cognition.firewall import CognitiveFirewall
10
+ from velune.models.specializations import CouncilRole
11
+ from velune.repository.schemas import RepositorySnapshot
12
+
13
+ console = Console()
14
+
15
+
16
+ def chat_command(ctx: typer.Context) -> None:
17
+ """Converses directly with the codebase using a fast, single-model streaming interface."""
18
+ cli_context = ctx.obj
19
+ if not isinstance(cli_context, CLIContext):
20
+ raise typer.BadParameter("CLI context was not properly initialized")
21
+
22
+ from velune.core.event_loop import submit
23
+
24
+ submit(_chat_command_async(cli_context))
25
+
26
+
27
+ async def _chat_command_async(cli_context: CLIContext) -> None:
28
+ # 1. Access services from DI container
29
+ container = cli_context.container
30
+ lifecycle = container.get("runtime.lifecycle")
31
+ model_registry = container.get("runtime.model_registry")
32
+ model_specialization = container.get("runtime.council_orchestrator").mapper
33
+ repo_cognition = container.get("runtime.repository_cognition")
34
+
35
+ # 2. Boot subsystems
36
+ console.print("[bold cyan]⠋[/bold cyan] Bootstrapping Cognitive Operating System kernel...")
37
+ await lifecycle.startup()
38
+ await model_registry.refresh()
39
+
40
+ # Onboarding preflight check gate
41
+ from velune.cli.commands.preflight import run_preflight_check
42
+
43
+ if not await run_preflight_check(container, console):
44
+ await lifecycle.shutdown()
45
+ return
46
+
47
+ # 3. Map Coder role to get fast, single-model access
48
+ roles = model_specialization.map_roles()
49
+ coder_model = roles.get(CouncilRole.CODER)
50
+ if not coder_model:
51
+ from velune.cli.rendering.error_panel import render_error
52
+ from velune.core.errors.catalog import NoModelsAvailableError
53
+
54
+ console.print(
55
+ render_error(
56
+ NoModelsAvailableError(
57
+ cause_override="No Coder model is assigned or found in the model catalog."
58
+ )
59
+ )
60
+ )
61
+ await lifecycle.shutdown()
62
+ return
63
+
64
+ provider_registry = container.get("runtime.provider_registry")
65
+ provider = provider_registry.get_or_raise(coder_model.provider_id)
66
+
67
+ # 4. Ingest and Scan AST Snapshot
68
+ with console.status("[bold magenta]⚡ Scanning codebase AST structure...[/bold magenta]"):
69
+ snapshot = repo_cognition.index()
70
+
71
+ firewall = CognitiveFirewall()
72
+ formatted_snap = _format_snapshot_context_safe(snapshot, firewall)
73
+
74
+ # 5. Initialize conversation history with codebase context
75
+ messages = [
76
+ {
77
+ "role": "system",
78
+ "content": (
79
+ "You are the Lead Coder for the Velune Reasoning Council, serving in low-latency conversational mode.\n"
80
+ "Your objective is to answer questions, explain code, and assist with natural language tasks concisely and directly.\n"
81
+ "You have access to the user's workspace context details below.\n"
82
+ "Keep responses focused, and do not use verbose pleasantries."
83
+ ),
84
+ },
85
+ {"role": "system", "content": f"User's workspace context summary:\n{formatted_snap}"},
86
+ ]
87
+
88
+ console.print()
89
+ console.print(
90
+ "[bold green]Velune Chat Mode[/bold green] (type [cyan]!exit[/cyan] to quit, [cyan]!run <task>[/cyan] to escalate to full council)"
91
+ )
92
+ console.print(
93
+ "--------------------------------------------------------------------------------"
94
+ )
95
+
96
+ while True:
97
+ try:
98
+ user_input = console.input("[bold green]You:[/bold green] ").strip()
99
+ except (KeyboardInterrupt, EOFError):
100
+ console.print("\n[yellow]Exiting chat session. Goodbye![/yellow]")
101
+ break
102
+
103
+ if not user_input:
104
+ continue
105
+
106
+ if user_input.lower() in ("!exit", "!quit"):
107
+ console.print("[yellow]Exiting chat session. Goodbye![/yellow]")
108
+ break
109
+
110
+ # 6. Escalation handler to full council
111
+ if user_input.startswith("!run "):
112
+ task = user_input[5:].strip()
113
+ if not task:
114
+ console.print(
115
+ "[yellow]Usage: !run <task> — please specify a task to execute[/yellow]"
116
+ )
117
+ continue
118
+ console.print(
119
+ f"[bold cyan]Escalating task to full Reasoning Council: '{task}'...[/bold cyan]"
120
+ )
121
+
122
+ orchestration_engine = container.get("runtime.orchestration_engine")
123
+ try:
124
+ milestones = []
125
+ async for milestone in orchestration_engine.stream(task):
126
+ console.print(f" [bold cyan]•[/bold cyan] {milestone}")
127
+ milestones.append(milestone)
128
+
129
+ run_id = None
130
+ for m in milestones:
131
+ if hasattr(m, "run_id"):
132
+ run_id = m.run_id
133
+ break
134
+ elif isinstance(m, str) and m.startswith("[") and "]" in m:
135
+ run_id = m.split("]")[0][1:]
136
+ break
137
+ state = orchestration_engine.get_state(run_id) if run_id else None
138
+ if state:
139
+ from velune.orchestration.schemas import ExecutionStatus
140
+
141
+ success = state.status == ExecutionStatus.COMPLETED
142
+ if success:
143
+ console.print(
144
+ f"[bold green]✓ Execution completed successfully: {state.output or 'Done'}[/bold green]"
145
+ )
146
+ else:
147
+ from velune.cli.rendering.error_panel import render_unexpected_error
148
+
149
+ console.print(
150
+ render_unexpected_error(
151
+ RuntimeError(state.error or "Unknown execution failure")
152
+ )
153
+ )
154
+ else:
155
+ console.print("[dim]Council run completed but returned no state.[/dim]")
156
+ except Exception as e:
157
+ from velune.cli.rendering.error_panel import render_error, render_unexpected_error
158
+ from velune.core.errors.catalog import VeluneError
159
+
160
+ if isinstance(e, VeluneError):
161
+ console.print(render_error(e))
162
+ else:
163
+ console.print(render_unexpected_error(e))
164
+ continue
165
+
166
+ # 7. Low-latency, streaming conversational execution
167
+ messages.append({"role": "user", "content": user_input})
168
+ from velune.core.types.inference import InferenceRequest
169
+
170
+ request = InferenceRequest(
171
+ model_id=coder_model.model_id,
172
+ messages=messages,
173
+ temperature=0.3,
174
+ )
175
+
176
+ console.print("[bold cyan]Velune:[/bold cyan] ", end="")
177
+ full_response_content = []
178
+
179
+ try:
180
+ capabilities = provider.get_capabilities()
181
+ supports_streaming = getattr(capabilities, "supports_streaming", False) and hasattr(
182
+ provider, "stream"
183
+ )
184
+
185
+ if supports_streaming:
186
+ try:
187
+ async for chunk in provider.stream(request):
188
+ print(chunk.content, end="", flush=True)
189
+ full_response_content.append(chunk.content)
190
+ print()
191
+ except KeyboardInterrupt:
192
+ print()
193
+ console.print("[yellow]\nGeneration cancelled by user.[/yellow]")
194
+ else:
195
+ response = await provider.infer(request)
196
+ console.print(response.content)
197
+ full_response_content.append(response.content)
198
+
199
+ if full_response_content:
200
+ assistant_response = "".join(full_response_content)
201
+ messages.append({"role": "assistant", "content": assistant_response})
202
+
203
+ except Exception as e:
204
+ print()
205
+ from velune.cli.rendering.error_panel import render_error, render_unexpected_error
206
+ from velune.core.errors.catalog import VeluneError
207
+
208
+ if isinstance(e, VeluneError):
209
+ console.print(render_error(e))
210
+ else:
211
+ console.print(render_unexpected_error(e))
212
+
213
+ # 8. Shutdown
214
+ await lifecycle.shutdown()
215
+
216
+
217
+ def _format_snapshot_context_safe(snapshot: RepositorySnapshot, firewall: CognitiveFirewall) -> str:
218
+ """Format snapshot metadata context for query prompt securely."""
219
+ lines = [f"Repository Root: {snapshot.root_path}"]
220
+ lines.append("Codebase Files:")
221
+ for f in snapshot.files[:25]:
222
+ risk_marker = " [⚠ injection-risk]" if f.metadata.get("injection_risk") else ""
223
+ lines.append(f" - {f.path} ({f.language.value}){risk_marker}")
224
+ if f.symbols:
225
+ safe_syms = [s.name for s in f.symbols[:3] if s.name.isidentifier()]
226
+ if safe_syms:
227
+ lines.append(f" Symbols: {', '.join(safe_syms)}")
228
+ return "\n".join(lines)
@@ -0,0 +1,224 @@
1
+ """Config command - velune config set/get/show."""
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.panel import Panel
10
+
11
+ from velune.cli.context import CLIContext
12
+
13
+ console = Console()
14
+
15
+ config_cmd = typer.Typer(help="Configuration management commands")
16
+
17
+
18
+ @config_cmd.command("set")
19
+ def config_set(
20
+ ctx: typer.Context,
21
+ key: str = typer.Argument(..., help="Configuration key (e.g. providers.default_provider)"),
22
+ value: str = typer.Argument(..., help="Configuration value"),
23
+ ) -> None:
24
+ """Set a configuration value in velune.toml."""
25
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
26
+ if not cli_context:
27
+ if ctx.obj and getattr(ctx.obj, "json_mode", False):
28
+ import json
29
+
30
+ print(json.dumps({"error": "CLI context is uninitialized"}))
31
+ else:
32
+ from velune.cli.rendering.error_panel import render_error
33
+ from velune.core.errors.catalog import WorkspaceNotInitializedError
34
+
35
+ console.print(
36
+ render_error(
37
+ WorkspaceNotInitializedError(
38
+ cause_override="CLI context was not properly initialized before this command."
39
+ )
40
+ )
41
+ )
42
+ raise typer.Exit(1)
43
+
44
+ config_path = cli_context.config_path or (cli_context.workspace / "velune.toml")
45
+
46
+ # Load raw TOML
47
+ import toml
48
+
49
+ try:
50
+ if config_path.exists():
51
+ data = toml.load(config_path)
52
+ else:
53
+ data = {}
54
+ except Exception as e:
55
+ if cli_context.json_mode:
56
+ import json
57
+
58
+ print(json.dumps({"error": f"Failed to load existing config: {e}"}))
59
+ else:
60
+ from velune.cli.rendering.error_panel import render_unexpected_error
61
+
62
+ console.print(render_unexpected_error(e))
63
+ data = {}
64
+
65
+ # Set the nested key
66
+ parts = key.split(".")
67
+ curr = data
68
+ for part in parts[:-1]:
69
+ if part not in curr or not isinstance(curr[part], dict):
70
+ curr[part] = {}
71
+ curr = curr[part]
72
+
73
+ # Convert value to correct type (bool, int, float, str)
74
+ typed_val: Any = value
75
+ if value.lower() == "true":
76
+ typed_val = True
77
+ elif value.lower() == "false":
78
+ typed_val = False
79
+ else:
80
+ try:
81
+ if "." in value:
82
+ typed_val = float(value)
83
+ else:
84
+ typed_val = int(value)
85
+ except ValueError:
86
+ pass
87
+
88
+ curr[parts[-1]] = typed_val
89
+
90
+ # Save back
91
+ try:
92
+ config_path.parent.mkdir(parents=True, exist_ok=True)
93
+ with open(config_path, "w") as f:
94
+ toml.dump(data, f)
95
+ if cli_context.json_mode:
96
+ import json
97
+
98
+ print(
99
+ json.dumps(
100
+ {"success": True, "key": key, "value": typed_val, "path": str(config_path)}
101
+ )
102
+ )
103
+ else:
104
+ console.print(
105
+ f"[green]✓ Successfully set [bold]{key}[/bold] to [bold]{typed_val}[/bold] in {config_path}[/green]"
106
+ )
107
+ except Exception as e:
108
+ if cli_context.json_mode:
109
+ import json
110
+
111
+ print(json.dumps({"error": f"Failed to save config: {e}"}))
112
+ else:
113
+ from velune.cli.rendering.error_panel import render_unexpected_error
114
+
115
+ console.print(render_unexpected_error(e))
116
+ raise typer.Exit(1)
117
+
118
+
119
+ @config_cmd.command("get")
120
+ def config_get(
121
+ ctx: typer.Context,
122
+ key: str = typer.Argument(..., help="Configuration key (e.g. providers.default_provider)"),
123
+ ) -> None:
124
+ """Get a configuration value."""
125
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
126
+ if not cli_context:
127
+ if ctx.obj and getattr(ctx.obj, "json_mode", False):
128
+ import json
129
+
130
+ print(json.dumps({"error": "CLI context is uninitialized"}))
131
+ else:
132
+ from velune.cli.rendering.error_panel import render_error
133
+ from velune.core.errors.catalog import WorkspaceNotInitializedError
134
+
135
+ console.print(
136
+ render_error(
137
+ WorkspaceNotInitializedError(
138
+ cause_override="CLI context was not properly initialized before this command."
139
+ )
140
+ )
141
+ )
142
+ raise typer.Exit(1)
143
+
144
+ # Fetch from the active loaded config object which is resolved and typed
145
+ config = cli_context.config
146
+ parts = key.split(".")
147
+ curr: Any = config
148
+
149
+ for part in parts:
150
+ if hasattr(curr, part):
151
+ curr = getattr(curr, part)
152
+ elif isinstance(curr, dict) and part in curr:
153
+ curr = curr[part]
154
+ else:
155
+ if cli_context.json_mode:
156
+ import json
157
+
158
+ print(json.dumps({"error": f"Key '{key}' not found in active configuration"}))
159
+ else:
160
+ console.print(f"[red]Key '{key}' not found in active configuration.[/red]")
161
+ raise typer.Exit(1)
162
+
163
+ if cli_context.json_mode:
164
+ import json
165
+
166
+ print(json.dumps({"key": key, "value": curr}))
167
+ else:
168
+ console.print(f"[bold]{key}[/bold] = {curr}")
169
+
170
+
171
+ @config_cmd.command("show")
172
+ def config_show(ctx: typer.Context) -> None:
173
+ """Show all configuration."""
174
+ cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
175
+
176
+ if cli_context is None:
177
+ if ctx.obj and getattr(ctx.obj, "json_mode", False):
178
+ import json
179
+
180
+ print(json.dumps({"error": "Configuration not yet loaded"}))
181
+ else:
182
+ console.print(Panel.fit("Configuration not yet loaded.", title="Configuration"))
183
+ return
184
+
185
+ config = cli_context.config
186
+ if cli_context.json_mode:
187
+ import json
188
+
189
+ print(
190
+ json.dumps(
191
+ {
192
+ "project": {
193
+ "name": config.project.name,
194
+ "version": config.project.version,
195
+ },
196
+ "providers": {
197
+ "default": config.providers.default_provider,
198
+ },
199
+ "workspace": {
200
+ "index_on_init": config.workspace.index_on_init,
201
+ "watch_files": config.workspace.watch_files,
202
+ "git_aware": config.workspace.git_aware,
203
+ },
204
+ "telemetry": {
205
+ "enabled": config.telemetry.enabled,
206
+ "log_level": config.telemetry.log_level,
207
+ },
208
+ }
209
+ )
210
+ )
211
+ else:
212
+ console.print(
213
+ Panel.fit(
214
+ f"project.name = {config.project.name}\n"
215
+ f"project.version = {config.project.version}\n"
216
+ f"providers.default = {config.providers.default_provider}\n"
217
+ f"workspace.index_on_init = {config.workspace.index_on_init}\n"
218
+ f"workspace.watch_files = {config.workspace.watch_files}\n"
219
+ f"workspace.git_aware = {config.workspace.git_aware}\n"
220
+ f"telemetry.enabled = {config.telemetry.enabled}\n"
221
+ f"telemetry.log_level = {config.telemetry.log_level}",
222
+ title="Configuration",
223
+ )
224
+ )
@@ -0,0 +1,88 @@
1
+ import os
2
+ import signal
3
+ import subprocess
4
+ import sys
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from velune.daemon.client import DaemonClient
12
+ from velune.daemon.transport import DAEMON_PID_FILE
13
+
14
+ daemon_cmd = typer.Typer(help="Velune daemon management")
15
+ console = Console()
16
+
17
+
18
+ @daemon_cmd.command("start")
19
+ def daemon_start(workspace: Path = typer.Option(Path.cwd(), help="Workspace root")):
20
+ """Start Velune daemon in background."""
21
+ if DaemonClient.is_running():
22
+ console.print("[yellow]Daemon is already running.[/yellow]")
23
+ return
24
+
25
+ workspace_abs = workspace.resolve()
26
+
27
+ # Detached background process spawn
28
+ if sys.platform == "win32":
29
+ subprocess.Popen(
30
+ [sys.executable, "-m", "velune.daemon.server", str(workspace_abs)],
31
+ start_new_session=True,
32
+ stdout=subprocess.DEVNULL,
33
+ stderr=subprocess.DEVNULL,
34
+ )
35
+ else:
36
+ subprocess.Popen(
37
+ [sys.executable, "-m", "velune.daemon.server", str(workspace_abs)],
38
+ start_new_session=True,
39
+ stdout=subprocess.DEVNULL,
40
+ stderr=subprocess.DEVNULL,
41
+ )
42
+
43
+ # Wait for daemon to become active
44
+ for _ in range(30):
45
+ time.sleep(0.1)
46
+ if DaemonClient.is_running():
47
+ console.print("[green]Daemon started.[/green]")
48
+ return
49
+
50
+ console.print("[red]Failed to start daemon.[/red]")
51
+
52
+
53
+ @daemon_cmd.command("stop")
54
+ def daemon_stop():
55
+ """Stop background Velune daemon process."""
56
+ if not DaemonClient.is_running():
57
+ console.print("[yellow]Daemon is not running.[/yellow]")
58
+ return
59
+
60
+ if DAEMON_PID_FILE.exists():
61
+ pid = int(DAEMON_PID_FILE.read_text())
62
+ try:
63
+ os.kill(pid, signal.SIGTERM)
64
+ console.print("[green]Daemon stopped.[/green]")
65
+ except Exception as e:
66
+ console.print(f"[red]Failed to stop daemon PID {pid}: {e}[/red]")
67
+ finally:
68
+ try:
69
+ DAEMON_PID_FILE.unlink()
70
+ except Exception:
71
+ pass
72
+ else:
73
+ console.print("[yellow]Daemon running but PID file missing.[/yellow]")
74
+
75
+
76
+ @daemon_cmd.command("status")
77
+ def daemon_status():
78
+ """Display daemon running status and PID."""
79
+ if DaemonClient.is_running():
80
+ try:
81
+ from velune.kernel.entrypoint import run_async
82
+
83
+ result = run_async(DaemonClient.send_command("ping"))
84
+ console.print(f"[green]Daemon running (PID: {result['pid']})[/green]")
85
+ except Exception as e:
86
+ console.print(f"[red]Daemon running but communication failed: {e}[/red]")
87
+ else:
88
+ console.print("[yellow]Daemon not running[/yellow]")