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