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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -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 +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -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 +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -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 +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -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 +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -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 +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.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,208 @@
|
|
|
1
|
+
"""Typer application factory for Velune."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
if sys.platform == "win32":
|
|
8
|
+
try:
|
|
9
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
10
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
11
|
+
except Exception:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
# Suppress all internal Velune logs from showing in terminal.
|
|
17
|
+
# Users see Rich output only — not raw Python logs. This MUST run before
|
|
18
|
+
# any velune.* modules are imported so their module-level loggers inherit
|
|
19
|
+
# these levels before producing any output.
|
|
20
|
+
logging.getLogger("velune").setLevel(logging.WARNING)
|
|
21
|
+
logging.getLogger("qdrant_client").setLevel(logging.WARNING)
|
|
22
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
23
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
24
|
+
logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
|
25
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
26
|
+
|
|
27
|
+
# Suppress the root logger from printing INFO/DEBUG to stderr.
|
|
28
|
+
logging.getLogger().setLevel(logging.WARNING)
|
|
29
|
+
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import typer
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.panel import Panel
|
|
35
|
+
from rich.text import Text
|
|
36
|
+
|
|
37
|
+
from velune import __version__
|
|
38
|
+
from velune.cli.context import CLIContext
|
|
39
|
+
from velune.cli.registry import register_commands
|
|
40
|
+
from velune.core.runtime import build_runtime
|
|
41
|
+
from velune.core.startup_profiler import mark as _startup_mark
|
|
42
|
+
from velune.kernel.registry import ServiceContainer
|
|
43
|
+
|
|
44
|
+
_startup_mark("cli.app imported (typer/rich/runtime ready)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _startup_frames(workspace: Path, config_path: Path | None) -> list[Panel]:
|
|
48
|
+
body = (
|
|
49
|
+
f"[bold cyan]Velune[/bold cyan] [dim]v{__version__}[/dim] · "
|
|
50
|
+
"[green]Local-first AI Orchestrator[/green]"
|
|
51
|
+
)
|
|
52
|
+
frame = Panel(
|
|
53
|
+
Text.from_markup(body),
|
|
54
|
+
border_style="cyan",
|
|
55
|
+
padding=(0, 2),
|
|
56
|
+
)
|
|
57
|
+
return [frame]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _show_startup_banner(console: Console, workspace: Path, config_path: Path | None) -> None:
|
|
61
|
+
"""Render the welcome banner once, instantly.
|
|
62
|
+
|
|
63
|
+
The previous implementation animated the banner frame-by-frame with a
|
|
64
|
+
``time.sleep(0.08)`` per line — ~0.6-1.2s of pure blocking delay on every
|
|
65
|
+
interactive launch. Modern AI terminals show their surface immediately;
|
|
66
|
+
perceived speed comes from an instant prompt, not a reveal animation.
|
|
67
|
+
"""
|
|
68
|
+
import sys
|
|
69
|
+
|
|
70
|
+
if not sys.stdout.isatty():
|
|
71
|
+
return # Skip banner in CI, piped output, --quiet mode
|
|
72
|
+
|
|
73
|
+
console.print(_startup_frames(workspace, config_path)[-1])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_app() -> typer.Typer:
|
|
77
|
+
"""Create the root Typer application."""
|
|
78
|
+
|
|
79
|
+
app = typer.Typer(
|
|
80
|
+
name="velune",
|
|
81
|
+
help="Terminal-first cognitive AI orchestration system",
|
|
82
|
+
no_args_is_help=False,
|
|
83
|
+
add_completion=True,
|
|
84
|
+
rich_markup_mode="rich",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@app.callback(invoke_without_command=True)
|
|
88
|
+
def main(
|
|
89
|
+
ctx: typer.Context,
|
|
90
|
+
workspace: Path = typer.Option(Path.cwd(), "--workspace", "-w", help="Workspace root"),
|
|
91
|
+
config_path: Path | None = typer.Option(
|
|
92
|
+
None, "--config", "-c", help="Explicit velune.toml path"
|
|
93
|
+
),
|
|
94
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging"),
|
|
95
|
+
version: bool = typer.Option(False, "--version", help="Show version and exit"),
|
|
96
|
+
json_mode: bool = typer.Option(
|
|
97
|
+
False, "--json", help="Enable machine-readable JSON output mode"
|
|
98
|
+
),
|
|
99
|
+
yes: bool = typer.Option(
|
|
100
|
+
False, "--yes", "-y", help="Auto-accept all file changes without prompting"
|
|
101
|
+
),
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Initialize process-wide runtime state for every CLI invocation."""
|
|
104
|
+
|
|
105
|
+
if version:
|
|
106
|
+
if json_mode:
|
|
107
|
+
import json
|
|
108
|
+
|
|
109
|
+
print(json.dumps({"version": __version__}))
|
|
110
|
+
else:
|
|
111
|
+
Console().print(
|
|
112
|
+
f"[bold cyan]velune[/bold cyan] version [green]{__version__}[/green]"
|
|
113
|
+
)
|
|
114
|
+
raise typer.Exit()
|
|
115
|
+
|
|
116
|
+
# Developers can opt into full internal logs with --verbose/-v.
|
|
117
|
+
if verbose:
|
|
118
|
+
logging.getLogger("velune").setLevel(logging.DEBUG)
|
|
119
|
+
else:
|
|
120
|
+
logging.getLogger("velune").setLevel(logging.WARNING)
|
|
121
|
+
|
|
122
|
+
if yes:
|
|
123
|
+
from velune.execution.diff_preview import configure as _configure_diff
|
|
124
|
+
|
|
125
|
+
_configure_diff(auto_accept=True)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
runtime = build_runtime(workspace=workspace, config_path=config_path, verbose=verbose)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
if json_mode:
|
|
131
|
+
import json
|
|
132
|
+
|
|
133
|
+
print(json.dumps({"error": f"Velune failed to start: {e}"}))
|
|
134
|
+
else:
|
|
135
|
+
from velune.cli.rendering.error_panel import render_error, render_unexpected_error
|
|
136
|
+
from velune.core.errors.catalog import VeluneError, WorkspaceNotInitializedError
|
|
137
|
+
|
|
138
|
+
if isinstance(e, VeluneError):
|
|
139
|
+
Console().print(render_error(e))
|
|
140
|
+
elif "velune.toml" in str(e).lower() or "workspace" in str(e).lower():
|
|
141
|
+
Console().print(render_error(WorkspaceNotInitializedError(str(e))))
|
|
142
|
+
else:
|
|
143
|
+
Console().print(render_unexpected_error(e))
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
runtime.container.register_instance("runtime.auto_accept", yes)
|
|
147
|
+
|
|
148
|
+
ctx.obj = CLIContext(
|
|
149
|
+
workspace=workspace,
|
|
150
|
+
config_path=config_path,
|
|
151
|
+
verbose=verbose,
|
|
152
|
+
runtime=runtime,
|
|
153
|
+
json_mode=json_mode,
|
|
154
|
+
yes=yes,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if ctx.invoked_subcommand is None:
|
|
158
|
+
if json_mode:
|
|
159
|
+
import json
|
|
160
|
+
|
|
161
|
+
print(
|
|
162
|
+
json.dumps(
|
|
163
|
+
{
|
|
164
|
+
"status": "ready",
|
|
165
|
+
"workspace": str(workspace),
|
|
166
|
+
"config_path": str(config_path) if config_path else None,
|
|
167
|
+
"version": __version__,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
from velune.providers.keystore import list_configured_providers
|
|
173
|
+
|
|
174
|
+
# list_configured_providers already includes a (short, deduped)
|
|
175
|
+
# Ollama reachability probe, so a single call suffices.
|
|
176
|
+
configured = list_configured_providers()
|
|
177
|
+
|
|
178
|
+
if not configured:
|
|
179
|
+
runtime.console.print(
|
|
180
|
+
Panel(
|
|
181
|
+
"[yellow]No AI providers configured.[/yellow]\n"
|
|
182
|
+
"[dim]Velune needs at least one provider to work.[/dim]",
|
|
183
|
+
border_style="yellow",
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
run_now = typer.confirm("Run setup now?", default=True)
|
|
187
|
+
if run_now:
|
|
188
|
+
from velune.cli.commands.setup import run_setup_wizard
|
|
189
|
+
|
|
190
|
+
run_setup_wizard()
|
|
191
|
+
else:
|
|
192
|
+
runtime.console.print(
|
|
193
|
+
"[dim]Run `velune setup` any time to configure providers.[/dim]"
|
|
194
|
+
)
|
|
195
|
+
# NO EXIT HERE — fall through to REPL regardless. The only
|
|
196
|
+
# ways out of Velune are /exit, /quit, or Ctrl+C twice.
|
|
197
|
+
|
|
198
|
+
_show_startup_banner(runtime.console, workspace, config_path)
|
|
199
|
+
_startup_mark("REPL handoff (prompt visible)")
|
|
200
|
+
from velune.kernel.entrypoint import launch
|
|
201
|
+
|
|
202
|
+
launch(runtime)
|
|
203
|
+
|
|
204
|
+
register_commands(app, ServiceContainer())
|
|
205
|
+
return app
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
app = create_app()
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
("graph", "Render a hierarchical tree of knowledge graph entities"),
|
|
34
|
+
("bench", "View or run empirical model capability benchmarks"),
|
|
35
|
+
("config", "Show current system configuration settings"),
|
|
36
|
+
("history", "Show REPL command execution history"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
_MODEL_PREFIX = "/model "
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SlashCompleter(Completer):
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
extra_commands: list[tuple[str, str]] | None = None,
|
|
46
|
+
model_ids: list[str] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._commands = SLASH_COMMANDS.copy()
|
|
49
|
+
if extra_commands:
|
|
50
|
+
self._commands.extend(extra_commands)
|
|
51
|
+
self._model_ids: list[str] = model_ids or []
|
|
52
|
+
|
|
53
|
+
def get_completions(self, document: Document, complete_event):
|
|
54
|
+
text = document.text_before_cursor
|
|
55
|
+
|
|
56
|
+
if not text.startswith("/"):
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# "/model <partial>" → complete model IDs
|
|
60
|
+
if text.startswith(_MODEL_PREFIX):
|
|
61
|
+
partial = text[len(_MODEL_PREFIX) :]
|
|
62
|
+
for mid in self._model_ids:
|
|
63
|
+
if mid.startswith(partial):
|
|
64
|
+
yield Completion(
|
|
65
|
+
text=mid,
|
|
66
|
+
start_position=-len(partial),
|
|
67
|
+
display=mid,
|
|
68
|
+
)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# "/<partial>" → complete command names
|
|
72
|
+
word = text[1:]
|
|
73
|
+
for cmd_name, description in self._commands:
|
|
74
|
+
if cmd_name.startswith(word.lower()):
|
|
75
|
+
yield Completion(
|
|
76
|
+
text=cmd_name,
|
|
77
|
+
start_position=-len(word),
|
|
78
|
+
display=f"/{cmd_name}",
|
|
79
|
+
display_meta=description,
|
|
80
|
+
)
|
velune/cli/banner.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def render_startup_banner(
|
|
5
|
+
console: Console,
|
|
6
|
+
hardware_profile,
|
|
7
|
+
configured_providers: list[str],
|
|
8
|
+
ollama_live: bool,
|
|
9
|
+
workspace_path: str,
|
|
10
|
+
active_model_id: str | None,
|
|
11
|
+
version: str = "0.1.0",
|
|
12
|
+
project_type_name: str | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
# Title header
|
|
15
|
+
console.print(f"[bold white]Velune {version}[/bold white]")
|
|
16
|
+
console.print("[dim]Keys: /help for commands, /exit to exit[/dim]")
|
|
17
|
+
|
|
18
|
+
# Linked workspace path
|
|
19
|
+
pt_suffix = (
|
|
20
|
+
f" ({project_type_name})" if project_type_name and project_type_name != "Unknown" else ""
|
|
21
|
+
)
|
|
22
|
+
console.print(f"Linked to: [white]{workspace_path}[/white]{pt_suffix}")
|
|
23
|
+
|
|
24
|
+
# Hardware/System info
|
|
25
|
+
vram_gb = hardware_profile.vram_total_gb
|
|
26
|
+
gpu_part = (
|
|
27
|
+
f"{hardware_profile.gpu_name} ({vram_gb:.0f}GB)"
|
|
28
|
+
if hardware_profile.gpu_name and vram_gb is not None
|
|
29
|
+
else (hardware_profile.gpu_name or "CPU only")
|
|
30
|
+
)
|
|
31
|
+
ram_part = f"{hardware_profile.total_ram_gb:.0f}GB RAM"
|
|
32
|
+
|
|
33
|
+
provider_list = []
|
|
34
|
+
if ollama_live:
|
|
35
|
+
provider_list.append("ollama")
|
|
36
|
+
for pid in configured_providers:
|
|
37
|
+
if pid != "ollama":
|
|
38
|
+
provider_list.append(pid)
|
|
39
|
+
|
|
40
|
+
providers_str = ", ".join(provider_list) if provider_list else "none"
|
|
41
|
+
console.print(f"System: {ram_part} • {gpu_part} • providers: {providers_str}")
|
|
42
|
+
|
|
43
|
+
# Model
|
|
44
|
+
if active_model_id:
|
|
45
|
+
console.print(f"Model: [cyan]{active_model_id}[/cyan]")
|
|
46
|
+
else:
|
|
47
|
+
console.print("Model: [yellow]none — type /model to select[/yellow]")
|
|
48
|
+
console.print()
|
|
49
|
+
|
|
50
|
+
# Warnings/suggestions
|
|
51
|
+
if hardware_profile.warnings:
|
|
52
|
+
for warning in hardware_profile.warnings:
|
|
53
|
+
console.print(f"[yellow]⚠[/yellow] [yellow]{warning}[/yellow]")
|
|
54
|
+
|
|
55
|
+
if hardware_profile.suggestions:
|
|
56
|
+
for suggestion in hardware_profile.suggestions:
|
|
57
|
+
console.print(f"[cyan]→[/cyan] {suggestion}")
|
|
58
|
+
|
|
59
|
+
if hardware_profile.warnings or hardware_profile.suggestions:
|
|
60
|
+
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,175 @@
|
|
|
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(
|
|
25
|
+
None, "--council-tier", help="Override council execution tier (instant, standard, full)"
|
|
26
|
+
),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Deliberates with the Reasoning Council for conceptual answers and code reviews without execution."""
|
|
29
|
+
|
|
30
|
+
if ctx.invoked_subcommand is not None:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
cli_context = ctx.obj
|
|
34
|
+
if not isinstance(cli_context, CLIContext):
|
|
35
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
36
|
+
|
|
37
|
+
if not prompt:
|
|
38
|
+
if cli_context.json_mode:
|
|
39
|
+
import json
|
|
40
|
+
|
|
41
|
+
print(json.dumps({"error": "Prompt argument is required in JSON mode"}))
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
# Prompt user interactively if no prompt argument is given
|
|
44
|
+
prompt = typer.prompt("What would you like to ask Velune?")
|
|
45
|
+
if not prompt:
|
|
46
|
+
console.print("[yellow]Empty query. Exiting.[/yellow]")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
from velune.core.event_loop import submit
|
|
50
|
+
|
|
51
|
+
submit(_ask_command_async(cli_context, prompt, council_tier))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def _ask_command_async(
|
|
55
|
+
cli_context: CLIContext, prompt: str, council_tier: str | None = None
|
|
56
|
+
) -> None:
|
|
57
|
+
# 1. Access services from DI container
|
|
58
|
+
container = cli_context.container
|
|
59
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
60
|
+
model_registry = container.get("runtime.model_registry")
|
|
61
|
+
model_specialization = container.get("runtime.council_orchestrator").mapper
|
|
62
|
+
repo_cognition = container.get("runtime.repository_cognition")
|
|
63
|
+
orchestrator = container.get("runtime.council_orchestrator")
|
|
64
|
+
|
|
65
|
+
# 2. Boot subsystems
|
|
66
|
+
if not cli_context.json_mode:
|
|
67
|
+
console.print("[bold cyan]⠋[/bold cyan] Bootstrapping Cognitive Operating System kernel...")
|
|
68
|
+
await lifecycle.startup()
|
|
69
|
+
await model_registry.refresh()
|
|
70
|
+
|
|
71
|
+
# Onboarding preflight check gate
|
|
72
|
+
from velune.cli.commands.preflight import run_preflight_check
|
|
73
|
+
|
|
74
|
+
if not await run_preflight_check(container, console if not cli_context.json_mode else None):
|
|
75
|
+
if cli_context.json_mode:
|
|
76
|
+
import json
|
|
77
|
+
|
|
78
|
+
print(
|
|
79
|
+
json.dumps(
|
|
80
|
+
{
|
|
81
|
+
"error": "Preflight check failed. Ensure workspace is initialized and models are scanned."
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
await lifecycle.shutdown()
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if not cli_context.json_mode:
|
|
89
|
+
display = CouncilDisplayView(console)
|
|
90
|
+
display.render_header(prompt)
|
|
91
|
+
|
|
92
|
+
# 3. Map role specializations
|
|
93
|
+
roles = model_specialization.map_roles()
|
|
94
|
+
if not cli_context.json_mode:
|
|
95
|
+
display.render_role_assignments(roles)
|
|
96
|
+
|
|
97
|
+
from velune.cognition.firewall import CognitiveFirewall
|
|
98
|
+
|
|
99
|
+
firewall = CognitiveFirewall()
|
|
100
|
+
|
|
101
|
+
# 4. Ingest and Scan AST Snapshot
|
|
102
|
+
snapshot: RepositorySnapshot | None = None
|
|
103
|
+
if not cli_context.json_mode:
|
|
104
|
+
with console.status("[bold magenta]⚡ Scanning codebase AST structure...[/bold magenta]"):
|
|
105
|
+
snapshot = repo_cognition.index()
|
|
106
|
+
else:
|
|
107
|
+
snapshot = repo_cognition.index()
|
|
108
|
+
|
|
109
|
+
formatted_snap = _format_snapshot_context_safe(snapshot, firewall)
|
|
110
|
+
|
|
111
|
+
# 5. deliberating debate loop
|
|
112
|
+
council_res = None
|
|
113
|
+
if not cli_context.json_mode:
|
|
114
|
+
with console.status(
|
|
115
|
+
"[bold magenta]🧠 Deliberating Reasoning Council debate...[/bold magenta]"
|
|
116
|
+
):
|
|
117
|
+
council_res = await orchestrator.execute_task(
|
|
118
|
+
prompt, formatted_snap, council_tier=council_tier
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
council_res = await orchestrator.execute_task(
|
|
122
|
+
prompt, formatted_snap, council_tier=council_tier
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
arbitration = council_res["arbitration"]
|
|
126
|
+
final_summary = council_res["final_summary"]
|
|
127
|
+
|
|
128
|
+
if cli_context.json_mode:
|
|
129
|
+
import json
|
|
130
|
+
|
|
131
|
+
roles_dict = {role.value: model_id for role, model_id in roles.items()}
|
|
132
|
+
print(
|
|
133
|
+
json.dumps(
|
|
134
|
+
{
|
|
135
|
+
"prompt": prompt,
|
|
136
|
+
"roles": roles_dict,
|
|
137
|
+
"reviewer_report": council_res["reviewer_report"],
|
|
138
|
+
"challenger_report": council_res["challenger_report"],
|
|
139
|
+
"arbitration": arbitration,
|
|
140
|
+
"final_summary": final_summary,
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
# 6. Render reports
|
|
146
|
+
display.render_step_header("Council Reviewer", "🔍")
|
|
147
|
+
display.render_reviewer_report(council_res["reviewer_report"])
|
|
148
|
+
|
|
149
|
+
display.render_step_header("Council Challenger", "⚡")
|
|
150
|
+
display.render_challenger_report(council_res["challenger_report"])
|
|
151
|
+
|
|
152
|
+
display.render_step_header("Arbitration Engine", "⚖️")
|
|
153
|
+
display.render_arbitration_result(arbitration)
|
|
154
|
+
|
|
155
|
+
display.render_step_header("Council Synthesizer", "🚀")
|
|
156
|
+
display.render_synthesized_response(final_summary)
|
|
157
|
+
|
|
158
|
+
# 7. Shutdown
|
|
159
|
+
await lifecycle.shutdown()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _format_snapshot_context_safe(snapshot: RepositorySnapshot, firewall: CognitiveFirewall) -> str:
|
|
163
|
+
"""Format snapshot metadata context for query prompt securely."""
|
|
164
|
+
lines = [f"Repository Root: {snapshot.root_path}"]
|
|
165
|
+
lines.append("Codebase Files:")
|
|
166
|
+
for f in snapshot.files[:25]:
|
|
167
|
+
# Only expose path and language — no raw symbol names or content
|
|
168
|
+
risk_marker = " [⚠ injection-risk]" if f.metadata.get("injection_risk") else ""
|
|
169
|
+
lines.append(f" - {f.path} ({f.language.value}){risk_marker}")
|
|
170
|
+
# Symbol names are safe to expose (identifiers, not content)
|
|
171
|
+
if f.symbols:
|
|
172
|
+
safe_syms = [s.name for s in f.symbols[:3] if s.name.isidentifier()]
|
|
173
|
+
if safe_syms:
|
|
174
|
+
lines.append(f" Symbols: {', '.join(safe_syms)}")
|
|
175
|
+
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."""
|