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
velune/__init__.py
ADDED
velune/__main__.py
ADDED
velune/cli/__init__.py
ADDED
velune/cli/app.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Typer application factory for Velune."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
if sys.platform == "win32":
|
|
9
|
+
try:
|
|
10
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
11
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
12
|
+
except Exception:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
# Suppress all internal Velune logs from showing in terminal.
|
|
18
|
+
# Users see Rich output only — not raw Python logs. This MUST run before
|
|
19
|
+
# any velune.* modules are imported so their module-level loggers inherit
|
|
20
|
+
# these levels before producing any output.
|
|
21
|
+
logging.getLogger("velune").setLevel(logging.WARNING)
|
|
22
|
+
logging.getLogger("qdrant_client").setLevel(logging.WARNING)
|
|
23
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
24
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
25
|
+
logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
|
26
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
27
|
+
|
|
28
|
+
# Suppress the root logger from printing INFO/DEBUG to stderr.
|
|
29
|
+
logging.getLogger().setLevel(logging.WARNING)
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import typer
|
|
34
|
+
from rich.console import Console
|
|
35
|
+
from rich.live import Live
|
|
36
|
+
from rich.panel import Panel
|
|
37
|
+
from rich.text import Text
|
|
38
|
+
|
|
39
|
+
from velune import __version__
|
|
40
|
+
from velune.cli.context import CLIContext
|
|
41
|
+
from velune.cli.registry import register_commands
|
|
42
|
+
from velune.core.runtime import build_runtime
|
|
43
|
+
from velune.kernel.registry import ServiceContainer
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _startup_frames(workspace: Path, config_path: Path | None) -> list[Panel]:
|
|
47
|
+
banner = """
|
|
48
|
+
██╗ ██╗███████╗██╗ ██╗ ██╗███╗ ██╗███████╗
|
|
49
|
+
██║ ██║██╔════╝██║ ██║ ██║████╗ ██║██╔════╝
|
|
50
|
+
██║ ██║█████╗ ██║ ██║ ██║██╔██╗ ██║█████╗
|
|
51
|
+
╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║╚██╗██║██╔══╝
|
|
52
|
+
╚████╔╝ ███████╗███████╗╚██████╔╝██║ ╚████║███████╗
|
|
53
|
+
╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
|
|
54
|
+
""".strip("\n")
|
|
55
|
+
|
|
56
|
+
frames: list[Panel] = []
|
|
57
|
+
lines = banner.splitlines()
|
|
58
|
+
for index in range(1, len(lines) + 1):
|
|
59
|
+
body = "\n".join(lines[:index])
|
|
60
|
+
if index == len(lines):
|
|
61
|
+
body += "\n\n[bold cyan]Welcome to Velune CLI![/bold cyan]\n[dim]v" + __version__ + "[/dim]\n\n[bold]What would you like to build today?[/bold]"
|
|
62
|
+
frames.append(
|
|
63
|
+
Panel(
|
|
64
|
+
Text.from_markup(body),
|
|
65
|
+
title="Velune",
|
|
66
|
+
border_style="cyan",
|
|
67
|
+
padding=(1, 2),
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
frames.append(
|
|
72
|
+
Panel(
|
|
73
|
+
Text.from_markup(
|
|
74
|
+
"[bold cyan]Welcome to Velune CLI![/bold cyan]\n"
|
|
75
|
+
f"[dim]v{__version__}[/dim]\n\n"
|
|
76
|
+
"[bold]What would you like to build today?[/bold]"
|
|
77
|
+
),
|
|
78
|
+
title="Velune",
|
|
79
|
+
border_style="cyan",
|
|
80
|
+
padding=(1, 2),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return frames
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_ollama_live() -> bool:
|
|
87
|
+
try:
|
|
88
|
+
import httpx
|
|
89
|
+
r = httpx.get("http://localhost:11434/api/tags", timeout=2)
|
|
90
|
+
return r.status_code == 200
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _show_startup_animation(console: Console, workspace: Path, config_path: Path | None) -> None:
|
|
96
|
+
"""Show startup animation only in interactive TTY sessions."""
|
|
97
|
+
import sys
|
|
98
|
+
if not sys.stdout.isatty():
|
|
99
|
+
return # Skip animation in CI, piped output, --quiet mode
|
|
100
|
+
|
|
101
|
+
frames = _startup_frames(workspace, config_path)
|
|
102
|
+
with Live(frames[0], console=console, refresh_per_second=12, transient=True) as live:
|
|
103
|
+
for frame in frames[1:]:
|
|
104
|
+
live.update(frame)
|
|
105
|
+
time.sleep(0.08) # Acceptable: sync context, interactive only
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_app() -> typer.Typer:
|
|
109
|
+
"""Create the root Typer application."""
|
|
110
|
+
|
|
111
|
+
app = typer.Typer(
|
|
112
|
+
name="velune",
|
|
113
|
+
help="Terminal-first cognitive AI orchestration system",
|
|
114
|
+
no_args_is_help=False,
|
|
115
|
+
add_completion=True,
|
|
116
|
+
rich_markup_mode="rich",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@app.callback(invoke_without_command=True)
|
|
120
|
+
def main(
|
|
121
|
+
ctx: typer.Context,
|
|
122
|
+
workspace: Path = typer.Option(Path.cwd(), "--workspace", "-w", help="Workspace root"),
|
|
123
|
+
config_path: Path | None = typer.Option(None, "--config", "-c", help="Explicit velune.toml path"),
|
|
124
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging"),
|
|
125
|
+
version: bool = typer.Option(False, "--version", help="Show version and exit"),
|
|
126
|
+
json_mode: bool = typer.Option(False, "--json", help="Enable machine-readable JSON output mode"),
|
|
127
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Auto-accept all file changes without prompting"),
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Initialize process-wide runtime state for every CLI invocation."""
|
|
130
|
+
|
|
131
|
+
if version:
|
|
132
|
+
if json_mode:
|
|
133
|
+
import json
|
|
134
|
+
print(json.dumps({"version": __version__}))
|
|
135
|
+
else:
|
|
136
|
+
Console().print(f"Velune v{__version__}")
|
|
137
|
+
raise typer.Exit()
|
|
138
|
+
|
|
139
|
+
# Developers can opt into full internal logs with --verbose/-v.
|
|
140
|
+
if verbose:
|
|
141
|
+
logging.getLogger("velune").setLevel(logging.DEBUG)
|
|
142
|
+
else:
|
|
143
|
+
logging.getLogger("velune").setLevel(logging.WARNING)
|
|
144
|
+
|
|
145
|
+
if yes:
|
|
146
|
+
from velune.execution.diff_preview import configure as _configure_diff
|
|
147
|
+
_configure_diff(auto_accept=True)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
runtime = build_runtime(workspace=workspace, config_path=config_path, verbose=verbose)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if json_mode:
|
|
153
|
+
import json
|
|
154
|
+
print(json.dumps({"error": f"Velune failed to start: {e}"}))
|
|
155
|
+
else:
|
|
156
|
+
Console().print(
|
|
157
|
+
f"[bold red]Velune failed to start:[/bold red] {e}\n"
|
|
158
|
+
"Run [bold cyan]`velune doctor check`[/bold cyan] to diagnose the issue."
|
|
159
|
+
)
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
runtime.container.register_instance("runtime.auto_accept", yes)
|
|
163
|
+
|
|
164
|
+
ctx.obj = CLIContext(
|
|
165
|
+
workspace=workspace,
|
|
166
|
+
config_path=config_path,
|
|
167
|
+
verbose=verbose,
|
|
168
|
+
runtime=runtime,
|
|
169
|
+
json_mode=json_mode,
|
|
170
|
+
yes=yes,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if ctx.invoked_subcommand is None:
|
|
174
|
+
if json_mode:
|
|
175
|
+
import json
|
|
176
|
+
print(json.dumps({
|
|
177
|
+
"status": "ready",
|
|
178
|
+
"workspace": str(workspace),
|
|
179
|
+
"config_path": str(config_path) if config_path else None,
|
|
180
|
+
"version": __version__
|
|
181
|
+
}))
|
|
182
|
+
else:
|
|
183
|
+
from velune.providers.keystore import list_configured_providers
|
|
184
|
+
configured = list_configured_providers()
|
|
185
|
+
ollama_live = _check_ollama_live()
|
|
186
|
+
|
|
187
|
+
if not configured and not ollama_live:
|
|
188
|
+
runtime.console.print(Panel(
|
|
189
|
+
"[yellow]No AI providers configured.[/yellow]\n"
|
|
190
|
+
"[dim]Velune needs at least one provider to work.[/dim]",
|
|
191
|
+
border_style="yellow",
|
|
192
|
+
))
|
|
193
|
+
run_now = typer.confirm("Run setup now?", default=True)
|
|
194
|
+
if run_now:
|
|
195
|
+
from velune.cli.commands.setup import run_setup_wizard
|
|
196
|
+
run_setup_wizard()
|
|
197
|
+
else:
|
|
198
|
+
runtime.console.print(
|
|
199
|
+
"[dim]Run `velune setup` any time to configure providers.[/dim]"
|
|
200
|
+
)
|
|
201
|
+
# NO EXIT HERE — fall through to REPL regardless. The only
|
|
202
|
+
# ways out of Velune are /exit, /quit, or Ctrl+C twice.
|
|
203
|
+
|
|
204
|
+
_show_startup_animation(runtime.console, workspace, config_path)
|
|
205
|
+
from velune.cli.repl import run_repl
|
|
206
|
+
run_repl(runtime)
|
|
207
|
+
|
|
208
|
+
register_commands(app, ServiceContainer())
|
|
209
|
+
return app
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
app = create_app()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
2
|
+
from prompt_toolkit.document import Document
|
|
3
|
+
|
|
4
|
+
SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
5
|
+
("ask", "Send a prompt to the active model"),
|
|
6
|
+
("run", "Execute a task through the council"),
|
|
7
|
+
("council", "Force full council on a task"),
|
|
8
|
+
("model", "Switch the active model interactively"),
|
|
9
|
+
("models", "List all available models"),
|
|
10
|
+
("mode", "Show current session mode and settings"),
|
|
11
|
+
("optimus", "Switch to speed mode — smallest model, instant tier"),
|
|
12
|
+
("godly", "Switch to max power mode — largest model, full council"),
|
|
13
|
+
("normal", "Return to balanced normal mode"),
|
|
14
|
+
("setup", "Configure API provider keys"),
|
|
15
|
+
("memory", "Inspect memory tiers and session stats"),
|
|
16
|
+
("session", "Save, list, resume, or export sessions"),
|
|
17
|
+
("usage", "Show token usage and cost for this session"),
|
|
18
|
+
("context", "Show context window usage"),
|
|
19
|
+
("diff", "Show pending file changes as unified diff"),
|
|
20
|
+
("doctor", "Run environment health checks"),
|
|
21
|
+
("help", "Show all available commands"),
|
|
22
|
+
("clear", "Clear screen and conversation context"),
|
|
23
|
+
("exit", "Exit the Velune session"),
|
|
24
|
+
("councilmodel","Assign specific models to council agent roles"),
|
|
25
|
+
("cm", "Assign specific models to council agent roles"),
|
|
26
|
+
("roles", "Show council role assignments table"),
|
|
27
|
+
("pull", "Download an Ollama model interactively"),
|
|
28
|
+
("download", "Download an Ollama model interactively"),
|
|
29
|
+
("get", "Download an Ollama model interactively"),
|
|
30
|
+
("delete", "Delete a locally installed Ollama model"),
|
|
31
|
+
("remove", "Delete a locally installed Ollama model"),
|
|
32
|
+
("rm", "Delete a locally installed Ollama model"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_MODEL_PREFIX = "/model "
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SlashCompleter(Completer):
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
extra_commands: list[tuple[str, str]] | None = None,
|
|
42
|
+
model_ids: list[str] | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._commands = SLASH_COMMANDS.copy()
|
|
45
|
+
if extra_commands:
|
|
46
|
+
self._commands.extend(extra_commands)
|
|
47
|
+
self._model_ids: list[str] = model_ids or []
|
|
48
|
+
|
|
49
|
+
def get_completions(self, document: Document, complete_event):
|
|
50
|
+
text = document.text_before_cursor
|
|
51
|
+
|
|
52
|
+
if not text.startswith("/"):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# "/model <partial>" → complete model IDs
|
|
56
|
+
if text.startswith(_MODEL_PREFIX):
|
|
57
|
+
partial = text[len(_MODEL_PREFIX):]
|
|
58
|
+
for mid in self._model_ids:
|
|
59
|
+
if mid.startswith(partial):
|
|
60
|
+
yield Completion(
|
|
61
|
+
text=mid,
|
|
62
|
+
start_position=-len(partial),
|
|
63
|
+
display=mid,
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# "/<partial>" → complete command names
|
|
68
|
+
word = text[1:]
|
|
69
|
+
for cmd_name, description in self._commands:
|
|
70
|
+
if cmd_name.startswith(word.lower()):
|
|
71
|
+
yield Completion(
|
|
72
|
+
text=cmd_name,
|
|
73
|
+
start_position=-len(word),
|
|
74
|
+
display=f"/{cmd_name}",
|
|
75
|
+
display_meta=description,
|
|
76
|
+
)
|
velune/cli/banner.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from rich.console import Console, Group
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
_TIER_COLORS: dict[str, str] = {
|
|
6
|
+
"critical": "red",
|
|
7
|
+
"low": "red",
|
|
8
|
+
"marginal": "yellow",
|
|
9
|
+
"capable": "green",
|
|
10
|
+
"powerful": "green",
|
|
11
|
+
"elite": "cyan",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_startup_banner(
|
|
16
|
+
console: Console,
|
|
17
|
+
hardware_profile,
|
|
18
|
+
configured_providers: list[str],
|
|
19
|
+
ollama_live: bool,
|
|
20
|
+
workspace_name: str,
|
|
21
|
+
active_model_id: str | None,
|
|
22
|
+
version: str = "0.1.0",
|
|
23
|
+
project_type_name: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
# Header
|
|
26
|
+
header = Text()
|
|
27
|
+
header.append("velune ", style="bold cyan")
|
|
28
|
+
header.append(f"v{version}", style="dim")
|
|
29
|
+
header.append(" · ", style="dim")
|
|
30
|
+
header.append(workspace_name, style="bold white")
|
|
31
|
+
|
|
32
|
+
# Hardware
|
|
33
|
+
tier = hardware_profile.tier.value
|
|
34
|
+
color = _TIER_COLORS.get(tier, "white")
|
|
35
|
+
vram_gb = hardware_profile.vram_total_gb
|
|
36
|
+
gpu_part = (
|
|
37
|
+
f"{hardware_profile.gpu_name} ({vram_gb:.0f} GB)"
|
|
38
|
+
if hardware_profile.gpu_name and vram_gb is not None
|
|
39
|
+
else (hardware_profile.gpu_name if hardware_profile.gpu_name else "CPU only")
|
|
40
|
+
)
|
|
41
|
+
hw_line = Text()
|
|
42
|
+
hw_line.append("hardware ", style="dim")
|
|
43
|
+
hw_line.append(f"{hardware_profile.total_ram_gb:.0f} GB RAM", style="white")
|
|
44
|
+
hw_line.append(" · ", style="dim")
|
|
45
|
+
hw_line.append(gpu_part, style="white")
|
|
46
|
+
hw_line.append(" · ", style="dim")
|
|
47
|
+
hw_line.append(f"tier: {tier}", style=color)
|
|
48
|
+
|
|
49
|
+
# Providers
|
|
50
|
+
parts: list[str] = []
|
|
51
|
+
if ollama_live:
|
|
52
|
+
parts.append("[green]● ollama[/green]")
|
|
53
|
+
for pid in configured_providers:
|
|
54
|
+
if pid != "ollama":
|
|
55
|
+
parts.append(f"[green]● {pid}[/green]")
|
|
56
|
+
if not parts:
|
|
57
|
+
parts.append("[red]✗ no providers[/red]")
|
|
58
|
+
providers_text = Text.from_markup("providers " + " ".join(parts))
|
|
59
|
+
|
|
60
|
+
# Model
|
|
61
|
+
if active_model_id:
|
|
62
|
+
model_text = Text.from_markup(
|
|
63
|
+
f"[dim]model[/dim] [cyan]{active_model_id}[/cyan]"
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
model_text = Text.from_markup(
|
|
67
|
+
"[dim]model[/dim] [yellow]none — type /model to select[/yellow]"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Project type (optional)
|
|
71
|
+
project_text = None
|
|
72
|
+
if project_type_name and project_type_name != "Unknown":
|
|
73
|
+
project_text = Text.from_markup(
|
|
74
|
+
f"[dim]project[/dim] [green]{project_type_name}[/green]"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Hint
|
|
78
|
+
hint = Text("type a prompt or ", style="dim")
|
|
79
|
+
hint.append("/help", style="cyan")
|
|
80
|
+
hint.append(" for commands", style="dim")
|
|
81
|
+
|
|
82
|
+
body_items = [header, hw_line, providers_text, model_text]
|
|
83
|
+
if project_text:
|
|
84
|
+
body_items.append(project_text)
|
|
85
|
+
body_items.append(hint)
|
|
86
|
+
|
|
87
|
+
console.print(Panel(
|
|
88
|
+
Group(*body_items),
|
|
89
|
+
border_style="cyan",
|
|
90
|
+
padding=(0, 1),
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
for warning in hardware_profile.warnings:
|
|
94
|
+
console.print(f" [yellow]⚠[/yellow] [dim]{warning}[/dim]")
|
|
95
|
+
for suggestion in hardware_profile.suggestions:
|
|
96
|
+
console.print(f" [dim]→ {suggestion}[/dim]")
|
|
97
|
+
if hardware_profile.warnings or hardware_profile.suggestions:
|
|
98
|
+
console.print()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Built-in Velune CLI command modules."""
|
|
2
|
+
|
|
3
|
+
from velune.cli.commands.ask import ask_cmd, ask_command
|
|
4
|
+
from velune.cli.commands.chat import chat_command
|
|
5
|
+
from velune.cli.commands.config import config_cmd
|
|
6
|
+
from velune.cli.commands.daemon import daemon_cmd
|
|
7
|
+
from velune.cli.commands.doctor import doctor_cmd
|
|
8
|
+
from velune.cli.commands.init import init_command
|
|
9
|
+
from velune.cli.commands.mcp import mcp_cmd, mcp_serve
|
|
10
|
+
from velune.cli.commands.memory import memory_cmd
|
|
11
|
+
from velune.cli.commands.models import models_cmd
|
|
12
|
+
from velune.cli.commands.run import run_cmd, run_command
|
|
13
|
+
from velune.cli.commands.setup import setup_command
|
|
14
|
+
from velune.cli.commands.workspace import workspace_cmd
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ask_cmd",
|
|
18
|
+
"ask_command",
|
|
19
|
+
"chat_command",
|
|
20
|
+
"config_cmd",
|
|
21
|
+
"init_command",
|
|
22
|
+
"memory_cmd",
|
|
23
|
+
"setup_command",
|
|
24
|
+
"models_cmd",
|
|
25
|
+
"run_cmd",
|
|
26
|
+
"run_command",
|
|
27
|
+
"workspace_cmd",
|
|
28
|
+
"daemon_cmd",
|
|
29
|
+
"doctor_cmd",
|
|
30
|
+
"mcp_cmd",
|
|
31
|
+
"mcp_serve",
|
|
32
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Interactive ask command boundary — routes natural language questions to the Council."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from velune.cognition.firewall import CognitiveFirewall
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from velune.cli.context import CLIContext
|
|
14
|
+
from velune.cli.display.council_view import CouncilDisplayView
|
|
15
|
+
from velune.repository.schemas import RepositorySnapshot
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
ask_cmd = typer.Typer(help="Interactive prompt entry point")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ask_command(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
prompt: str | None = typer.Argument(None, help="Question or task to route through Velune"),
|
|
24
|
+
council_tier: str | None = typer.Option(None, "--council-tier", help="Override council execution tier (instant, standard, full)"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Deliberates with the Reasoning Council for conceptual answers and code reviews without execution."""
|
|
27
|
+
|
|
28
|
+
if ctx.invoked_subcommand is not None:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
cli_context = ctx.obj
|
|
32
|
+
if not isinstance(cli_context, CLIContext):
|
|
33
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
34
|
+
|
|
35
|
+
if not prompt:
|
|
36
|
+
if cli_context.json_mode:
|
|
37
|
+
import json
|
|
38
|
+
print(json.dumps({"error": "Prompt argument is required in JSON mode"}))
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
# Prompt user interactively if no prompt argument is given
|
|
41
|
+
prompt = typer.prompt("What would you like to ask Velune?")
|
|
42
|
+
if not prompt:
|
|
43
|
+
console.print("[yellow]Empty query. Exiting.[/yellow]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
from velune.core.event_loop import submit
|
|
47
|
+
submit(_ask_command_async(cli_context, prompt, council_tier))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _ask_command_async(cli_context: CLIContext, prompt: str, council_tier: str | None = None) -> None:
|
|
51
|
+
# 1. Access services from DI container
|
|
52
|
+
container = cli_context.container
|
|
53
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
54
|
+
model_registry = container.get("runtime.model_registry")
|
|
55
|
+
model_specialization = container.get("runtime.council_orchestrator").mapper
|
|
56
|
+
repo_cognition = container.get("runtime.repository_cognition")
|
|
57
|
+
orchestrator = container.get("runtime.council_orchestrator")
|
|
58
|
+
|
|
59
|
+
# 2. Boot 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
|
+
await model_registry.refresh()
|
|
64
|
+
|
|
65
|
+
# Onboarding preflight check gate
|
|
66
|
+
from velune.cli.commands.preflight import run_preflight_check
|
|
67
|
+
if not await run_preflight_check(container, console if not cli_context.json_mode else None):
|
|
68
|
+
if cli_context.json_mode:
|
|
69
|
+
import json
|
|
70
|
+
print(json.dumps({"error": "Preflight check failed. Ensure workspace is initialized and models are scanned."}))
|
|
71
|
+
await lifecycle.shutdown()
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if not cli_context.json_mode:
|
|
75
|
+
display = CouncilDisplayView(console)
|
|
76
|
+
display.render_header(prompt)
|
|
77
|
+
|
|
78
|
+
# 3. Map role specializations
|
|
79
|
+
roles = model_specialization.map_roles()
|
|
80
|
+
if not cli_context.json_mode:
|
|
81
|
+
display.render_role_assignments(roles)
|
|
82
|
+
|
|
83
|
+
from velune.cognition.firewall import CognitiveFirewall
|
|
84
|
+
firewall = CognitiveFirewall()
|
|
85
|
+
|
|
86
|
+
# 4. Ingest and Scan AST Snapshot
|
|
87
|
+
snapshot: RepositorySnapshot | None = None
|
|
88
|
+
if not cli_context.json_mode:
|
|
89
|
+
with console.status("[bold magenta]⚡ Scanning codebase AST structure...[/bold magenta]"):
|
|
90
|
+
snapshot = repo_cognition.index()
|
|
91
|
+
else:
|
|
92
|
+
snapshot = repo_cognition.index()
|
|
93
|
+
|
|
94
|
+
formatted_snap = _format_snapshot_context_safe(snapshot, firewall)
|
|
95
|
+
|
|
96
|
+
# 5. deliberating debate loop
|
|
97
|
+
council_res = None
|
|
98
|
+
if not cli_context.json_mode:
|
|
99
|
+
with console.status("[bold magenta]🧠 Deliberating Reasoning Council debate...[/bold magenta]"):
|
|
100
|
+
council_res = await orchestrator.execute_task(prompt, formatted_snap, council_tier=council_tier)
|
|
101
|
+
else:
|
|
102
|
+
council_res = await orchestrator.execute_task(prompt, formatted_snap, council_tier=council_tier)
|
|
103
|
+
|
|
104
|
+
arbitration = council_res["arbitration"]
|
|
105
|
+
final_summary = council_res["final_summary"]
|
|
106
|
+
|
|
107
|
+
if cli_context.json_mode:
|
|
108
|
+
import json
|
|
109
|
+
roles_dict = {role.value: model_id for role, model_id in roles.items()}
|
|
110
|
+
print(json.dumps({
|
|
111
|
+
"prompt": prompt,
|
|
112
|
+
"roles": roles_dict,
|
|
113
|
+
"reviewer_report": council_res["reviewer_report"],
|
|
114
|
+
"challenger_report": council_res["challenger_report"],
|
|
115
|
+
"arbitration": arbitration,
|
|
116
|
+
"final_summary": final_summary,
|
|
117
|
+
}))
|
|
118
|
+
else:
|
|
119
|
+
# 6. Render reports
|
|
120
|
+
display.render_step_header("Council Reviewer", "🔍")
|
|
121
|
+
display.render_reviewer_report(council_res["reviewer_report"])
|
|
122
|
+
|
|
123
|
+
display.render_step_header("Council Challenger", "⚡")
|
|
124
|
+
display.render_challenger_report(council_res["challenger_report"])
|
|
125
|
+
|
|
126
|
+
display.render_step_header("Arbitration Engine", "⚖️")
|
|
127
|
+
display.render_arbitration_result(arbitration)
|
|
128
|
+
|
|
129
|
+
display.render_step_header("Council Synthesizer", "🚀")
|
|
130
|
+
display.render_synthesized_response(final_summary)
|
|
131
|
+
|
|
132
|
+
# 7. Shutdown
|
|
133
|
+
await lifecycle.shutdown()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _format_snapshot_context_safe(snapshot: RepositorySnapshot, firewall: CognitiveFirewall) -> str:
|
|
137
|
+
"""Format snapshot metadata context for query prompt securely."""
|
|
138
|
+
lines = [f"Repository Root: {snapshot.root_path}"]
|
|
139
|
+
lines.append("Codebase Files:")
|
|
140
|
+
for f in snapshot.files[:25]:
|
|
141
|
+
# Only expose path and language — no raw symbol names or content
|
|
142
|
+
risk_marker = " [⚠ injection-risk]" if f.metadata.get("injection_risk") else ""
|
|
143
|
+
lines.append(f" - {f.path} ({f.language.value}){risk_marker}")
|
|
144
|
+
# Symbol names are safe to expose (identifiers, not content)
|
|
145
|
+
if f.symbols:
|
|
146
|
+
safe_syms = [s.name for s in f.symbols[:3] if s.name.isidentifier()]
|
|
147
|
+
if safe_syms:
|
|
148
|
+
lines.append(f" Symbols: {', '.join(safe_syms)}")
|
|
149
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Command registration contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from velune.kernel.registry import ServiceContainer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommandRegistrar(Protocol):
|
|
13
|
+
"""A command module that can attach itself to a Typer app."""
|
|
14
|
+
|
|
15
|
+
def register(self, app: typer.Typer, container: ServiceContainer) -> None:
|
|
16
|
+
"""Register the command group or command callbacks."""
|