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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +212 -0
- velune/cli/autocomplete.py +76 -0
- velune/cli/banner.py +98 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +149 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +188 -0
- velune/cli/commands/config.py +182 -0
- velune/cli/commands/daemon.py +85 -0
- velune/cli/commands/doctor.py +373 -0
- velune/cli/commands/init.py +160 -0
- velune/cli/commands/mcp.py +80 -0
- velune/cli/commands/memory.py +269 -0
- velune/cli/commands/models.py +462 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +171 -0
- velune/cli/commands/setup.py +182 -0
- velune/cli/commands/workspace.py +217 -0
- velune/cli/context.py +37 -0
- velune/cli/councilmodel_ui.py +171 -0
- velune/cli/display/council_view.py +240 -0
- velune/cli/display/memory_view.py +93 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +21 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +44 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +118 -0
- velune/cli/registry.py +81 -0
- velune/cli/repl.py +1178 -0
- velune/cli/session_manager.py +69 -0
- velune/cli/slash_commands.py +37 -0
- velune/cognition/__init__.py +19 -0
- velune/cognition/arbitrator.py +216 -0
- velune/cognition/architecture.py +398 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +216 -0
- velune/cognition/council/challenger.py +70 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +39 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +44 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +53 -0
- velune/cognition/council/planner.py +119 -0
- velune/cognition/council/reviewer.py +72 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +181 -0
- velune/cognition/firewall.py +256 -0
- velune/cognition/module.py +38 -0
- velune/cognition/orchestrator.py +886 -0
- velune/cognition/personality.py +236 -0
- velune/cognition/style_resolver.py +62 -0
- velune/cognition/verification.py +201 -0
- velune/context/__init__.py +11 -0
- velune/context/extractive.py +94 -0
- velune/context/window.py +62 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +53 -0
- velune/core/errors/execution.py +26 -0
- velune/core/errors/memory.py +21 -0
- velune/core/errors/orchestration.py +26 -0
- velune/core/errors/provider.py +31 -0
- velune/core/event_loop.py +30 -0
- velune/core/logging.py +83 -0
- velune/core/runtime.py +106 -0
- velune/core/task_registry.py +120 -0
- velune/core/trace.py +80 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +49 -0
- velune/core/types/context.py +39 -0
- velune/core/types/inference.py +35 -0
- velune/core/types/memory.py +39 -0
- velune/core/types/model.py +64 -0
- velune/core/types/provider.py +35 -0
- velune/core/types/repository.py +35 -0
- velune/core/types/task.py +56 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +115 -0
- velune/daemon/transport.py +169 -0
- velune/events.py +194 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +311 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +128 -0
- velune/execution/command_spec.py +140 -0
- velune/execution/diff_preview.py +172 -0
- velune/execution/executor.py +173 -0
- velune/execution/module.py +16 -0
- velune/execution/multi_diff.py +70 -0
- velune/execution/path_guard.py +19 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +80 -0
- velune/execution/sandbox.py +257 -0
- velune/execution/validator.py +113 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +162 -0
- velune/kernel/__init__.py +58 -0
- velune/kernel/bootstrap.py +107 -0
- velune/kernel/config.py +252 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +102 -0
- velune/kernel/module.py +15 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +93 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +113 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +90 -0
- velune/memory/__init__.py +28 -0
- velune/memory/lifecycle.py +154 -0
- velune/memory/module.py +94 -0
- velune/memory/prioritizer.py +65 -0
- velune/memory/storage/sqlite_manager.py +368 -0
- velune/memory/tiers/episodic.py +156 -0
- velune/memory/tiers/graph.py +282 -0
- velune/memory/tiers/lineage.py +367 -0
- velune/memory/tiers/semantic.py +198 -0
- velune/memory/tiers/working.py +165 -0
- velune/models/__init__.py +16 -0
- velune/models/module.py +18 -0
- velune/models/probes.py +182 -0
- velune/models/profile_cache.py +82 -0
- velune/models/profiler.py +105 -0
- velune/models/registry.py +201 -0
- velune/models/scorer.py +225 -0
- velune/models/specializations.py +196 -0
- velune/orchestration/__init__.py +15 -0
- velune/orchestration/module.py +14 -0
- velune/orchestration/role_assignments.py +78 -0
- velune/orchestration/schemas.py +99 -0
- velune/plugins/__init__.py +13 -0
- velune/plugins/hooks.py +49 -0
- velune/plugins/loader.py +95 -0
- velune/plugins/registry.py +54 -0
- velune/plugins/schemas.py +19 -0
- velune/providers/__init__.py +18 -0
- velune/providers/adapters/anthropic.py +231 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +203 -0
- velune/providers/adapters/llamacpp.py +202 -0
- velune/providers/adapters/lmstudio.py +173 -0
- velune/providers/adapters/ollama.py +186 -0
- velune/providers/adapters/openai.py +207 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +135 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +77 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +87 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +109 -0
- velune/providers/discovery/groq.py +20 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +165 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +114 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/keystore.py +83 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +208 -0
- velune/providers/module.py +15 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +173 -0
- velune/providers/router.py +82 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +21 -0
- velune/repository/analyzer.py +123 -0
- velune/repository/cognition.py +172 -0
- velune/repository/grapher.py +182 -0
- velune/repository/indexer.py +229 -0
- velune/repository/module.py +15 -0
- velune/repository/parser.py +378 -0
- velune/repository/project_type.py +293 -0
- velune/repository/scanner.py +177 -0
- velune/repository/schemas.py +102 -0
- velune/repository/tracker.py +233 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/graph.py +117 -0
- velune/retrieval/hybrid.py +250 -0
- velune/retrieval/keyword.py +113 -0
- velune/retrieval/module.py +19 -0
- velune/retrieval/reranker.py +68 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/vector.py +163 -0
- velune/telemetry/__init__.py +7 -0
- velune/telemetry/cognition.py +260 -0
- velune/telemetry/token_tracker.py +133 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +84 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +107 -0
- velune/tools/code/search.py +112 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +123 -0
- velune/tools/filesystem/write.py +160 -0
- velune/tools/git/history.py +185 -0
- velune/tools/git/operations.py +132 -0
- velune/tools/git/state.py +134 -0
- velune/tools/module.py +65 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +96 -0
- velune_cli-1.0.0.dist-info/METADATA +497 -0
- velune_cli-1.0.0.dist-info/RECORD +229 -0
- velune_cli-1.0.0.dist-info/WHEEL +4 -0
- velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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]")
|