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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Workspace commands — velune workspace init/status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.box import ROUNDED
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from velune.cli.context import CLIContext
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
workspace_cmd = typer.Typer(help="Workspace management commands")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@workspace_cmd.command("init")
|
|
20
|
+
def workspace_init(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
path: Path = typer.Argument(Path.cwd(), help="Workspace path"),
|
|
23
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force reinitialization and re-index"),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initialize a Velune workspace and build initial Tree-sitter AST parser indices."""
|
|
26
|
+
cli_context = ctx.obj
|
|
27
|
+
if not isinstance(cli_context, CLIContext):
|
|
28
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
29
|
+
|
|
30
|
+
if not cli_context.json_mode:
|
|
31
|
+
console.print(f"[bold cyan]Initializing Velune Cognitive Workspace at:[/bold cyan] {path}")
|
|
32
|
+
|
|
33
|
+
# 1. Create .velune directory structure
|
|
34
|
+
velune_dir = path / ".velune"
|
|
35
|
+
velune_dir.mkdir(exist_ok=True)
|
|
36
|
+
(velune_dir / "memory").mkdir(exist_ok=True)
|
|
37
|
+
(velune_dir / "retrieval").mkdir(exist_ok=True)
|
|
38
|
+
(velune_dir / "index").mkdir(exist_ok=True)
|
|
39
|
+
(velune_dir / "snapshots").mkdir(exist_ok=True)
|
|
40
|
+
|
|
41
|
+
if not cli_context.json_mode:
|
|
42
|
+
console.print("[green]✓[/green] Created .velune configuration directory structure.")
|
|
43
|
+
|
|
44
|
+
# 2. Write default .veluneignore if one doesn't already exist
|
|
45
|
+
veluneignore_path = path / ".veluneignore"
|
|
46
|
+
if not veluneignore_path.exists():
|
|
47
|
+
from velune.repository.scanner import DEFAULT_VELUNEIGNORE
|
|
48
|
+
|
|
49
|
+
veluneignore_path.write_text(DEFAULT_VELUNEIGNORE, encoding="utf-8")
|
|
50
|
+
if not cli_context.json_mode:
|
|
51
|
+
console.print(
|
|
52
|
+
"[green]✓[/green] Created default .veluneignore (edit to customise index exclusions)."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
from velune.core.event_loop import submit
|
|
56
|
+
|
|
57
|
+
submit(_workspace_init_async(cli_context, path, velune_dir, force))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _workspace_init_async(
|
|
61
|
+
cli_context: CLIContext,
|
|
62
|
+
path: Path,
|
|
63
|
+
velune_dir: Path,
|
|
64
|
+
force: bool,
|
|
65
|
+
) -> None:
|
|
66
|
+
container = cli_context.container
|
|
67
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
68
|
+
repo_cognition = container.get("runtime.repository_cognition")
|
|
69
|
+
|
|
70
|
+
# 3. Boot subsystems and compile index
|
|
71
|
+
await lifecycle.startup()
|
|
72
|
+
|
|
73
|
+
if not cli_context.json_mode:
|
|
74
|
+
console.print(
|
|
75
|
+
"[bold cyan]⠋[/bold cyan] Building Tree-sitter compiler AST indices and scanning imports..."
|
|
76
|
+
)
|
|
77
|
+
with console.status(
|
|
78
|
+
"[bold magenta]⚡ Parsing symbols, dependencies, and git Authorship...[/bold magenta]"
|
|
79
|
+
):
|
|
80
|
+
snapshot = repo_cognition.index(force=force)
|
|
81
|
+
else:
|
|
82
|
+
snapshot = repo_cognition.index(force=force)
|
|
83
|
+
|
|
84
|
+
# Calculate statistics
|
|
85
|
+
num_files = len(snapshot.files)
|
|
86
|
+
num_symbols = len(snapshot.symbols)
|
|
87
|
+
num_edges = len(snapshot.edges)
|
|
88
|
+
skipped_secrets = snapshot.summary.get("skipped_secrets", [])
|
|
89
|
+
|
|
90
|
+
languages = {}
|
|
91
|
+
for f in snapshot.files:
|
|
92
|
+
languages[f.language.value] = languages.get(f.language.value, 0) + 1
|
|
93
|
+
|
|
94
|
+
lang_summary = ", ".join(f"{count} {lang}" for lang, count in languages.items())
|
|
95
|
+
git_branch = snapshot.summary.get("git", {}).get("active_branch", "untracked")
|
|
96
|
+
|
|
97
|
+
# 4. Render gorgeous Success Summary Panel
|
|
98
|
+
if cli_context.json_mode:
|
|
99
|
+
import json
|
|
100
|
+
|
|
101
|
+
print(
|
|
102
|
+
json.dumps(
|
|
103
|
+
{
|
|
104
|
+
"success": True,
|
|
105
|
+
"workspace_path": str(path),
|
|
106
|
+
"caches_directory": str(velune_dir),
|
|
107
|
+
"indexed_files": num_files,
|
|
108
|
+
"languages": languages,
|
|
109
|
+
"parsed_ast_symbols": num_symbols,
|
|
110
|
+
"dependency_edges": num_edges,
|
|
111
|
+
"active_branch": git_branch,
|
|
112
|
+
"skipped_secrets": skipped_secrets,
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
console.print()
|
|
118
|
+
console.print(
|
|
119
|
+
Panel(
|
|
120
|
+
Text.assemble(
|
|
121
|
+
("[bold green]✓ VELUNE WORKSPACE SUCCESSFULLY INDEXED[/bold green]\n\n"),
|
|
122
|
+
(f"[bold]Workspace path:[/bold] {path}\n"),
|
|
123
|
+
(f"[bold]Caches directory:[/bold] {velune_dir}\n"),
|
|
124
|
+
(
|
|
125
|
+
f"[bold]Indexed files:[/bold] {num_files} ({lang_summary or 'no code files found'})\n"
|
|
126
|
+
),
|
|
127
|
+
(f"[bold]Parsed AST symbols:[/bold] {num_symbols} classes/functions/imports\n"),
|
|
128
|
+
(f"[bold]Dependency edges:[/bold] {num_edges} import link(s)\n"),
|
|
129
|
+
(f"[bold]Active branch:[/bold] [magenta]{git_branch}[/magenta]\n\n"),
|
|
130
|
+
(
|
|
131
|
+
"[dim]Velune repository cognitive engine is primed. Use 'velune run' to start autonomous edits.[/dim]"
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
border_style="green",
|
|
135
|
+
box=ROUNDED,
|
|
136
|
+
title="[bold green]Cognitive Priming Success[/bold green]",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if skipped_secrets:
|
|
141
|
+
secret_lines = "\n".join(f" [bold yellow]•[/bold yellow] {p}" for p in skipped_secrets)
|
|
142
|
+
console.print(
|
|
143
|
+
Panel(
|
|
144
|
+
Text.from_markup(
|
|
145
|
+
"[bold yellow]Velune detected and protected the following files from being indexed:[/bold yellow]\n\n"
|
|
146
|
+
+ secret_lines
|
|
147
|
+
+ "\n\n[dim]These files matched known secrets/credentials patterns. "
|
|
148
|
+
"Add them to [bold].veluneignore[/bold] to silence this notice, "
|
|
149
|
+
"or ensure they are listed in [bold].gitignore[/bold].[/dim]"
|
|
150
|
+
),
|
|
151
|
+
title="[bold yellow]🔒 Secrets Protected[/bold yellow]",
|
|
152
|
+
border_style="yellow",
|
|
153
|
+
box=ROUNDED,
|
|
154
|
+
padding=(1, 2),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
await lifecycle.shutdown()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@workspace_cmd.command("status")
|
|
162
|
+
def workspace_status(
|
|
163
|
+
ctx: typer.Context,
|
|
164
|
+
path: Path = typer.Option(Path.cwd(), "--path", "-p", help="Workspace path"),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Show active workspace structure and index summary."""
|
|
167
|
+
velune_dir = path / ".velune"
|
|
168
|
+
|
|
169
|
+
cli_context = ctx.obj
|
|
170
|
+
if not isinstance(cli_context, CLIContext):
|
|
171
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
172
|
+
|
|
173
|
+
if not velune_dir.exists():
|
|
174
|
+
if cli_context.json_mode:
|
|
175
|
+
import json
|
|
176
|
+
|
|
177
|
+
print(json.dumps({"error": "Not a Velune workspace (no .velune directory detected)"}))
|
|
178
|
+
else:
|
|
179
|
+
from velune.cli.rendering.error_panel import render_error
|
|
180
|
+
from velune.core.errors.catalog import WorkspaceNotInitializedError
|
|
181
|
+
|
|
182
|
+
console.print(
|
|
183
|
+
render_error(
|
|
184
|
+
WorkspaceNotInitializedError(
|
|
185
|
+
cause_override=f"No .velune directory found in {path}."
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
from velune.core.event_loop import submit
|
|
192
|
+
|
|
193
|
+
submit(_workspace_status_async(cli_context, path, velune_dir))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def _workspace_status_async(
|
|
197
|
+
cli_context: CLIContext,
|
|
198
|
+
path: Path,
|
|
199
|
+
velune_dir: Path,
|
|
200
|
+
) -> None:
|
|
201
|
+
container = cli_context.container
|
|
202
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
203
|
+
repo_cognition = container.get("runtime.repository_cognition")
|
|
204
|
+
|
|
205
|
+
await lifecycle.startup()
|
|
206
|
+
|
|
207
|
+
if not cli_context.json_mode:
|
|
208
|
+
with console.status("[bold cyan]Querying cognitive index status...[/bold cyan]"):
|
|
209
|
+
snapshot = repo_cognition.index(force=False)
|
|
210
|
+
else:
|
|
211
|
+
snapshot = repo_cognition.index(force=False)
|
|
212
|
+
|
|
213
|
+
num_files = len(snapshot.files)
|
|
214
|
+
num_symbols = len(snapshot.symbols)
|
|
215
|
+
git_branch = snapshot.summary.get("git", {}).get("active_branch", "untracked")
|
|
216
|
+
|
|
217
|
+
if cli_context.json_mode:
|
|
218
|
+
import json
|
|
219
|
+
|
|
220
|
+
print(
|
|
221
|
+
json.dumps(
|
|
222
|
+
{
|
|
223
|
+
"workspace_root": str(path),
|
|
224
|
+
"velune_cache": str(velune_dir),
|
|
225
|
+
"indexed_files_count": num_files,
|
|
226
|
+
"indexed_symbols_count": num_symbols,
|
|
227
|
+
"git_branch": git_branch,
|
|
228
|
+
"status": "Active & Fully Primed",
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
console.print(
|
|
234
|
+
Panel(
|
|
235
|
+
Text.assemble(
|
|
236
|
+
(f"[bold]Workspace root:[/bold] {path}\n"),
|
|
237
|
+
(f"[bold]Velune cache:[/bold] {velune_dir}\n"),
|
|
238
|
+
(f"[bold]Indexed files count:[/bold] {num_files}\n"),
|
|
239
|
+
(f"[bold]Indexed symbols count:[/bold] {num_symbols}\n"),
|
|
240
|
+
(f"[bold]Git branch:[/bold] [magenta]{git_branch}[/magenta]\n"),
|
|
241
|
+
("[bold]Status:[/bold] [bold green]Active & Fully Primed[/bold green]"),
|
|
242
|
+
),
|
|
243
|
+
border_style="cyan",
|
|
244
|
+
box=ROUNDED,
|
|
245
|
+
title="[bold cyan]Workspace Status[/bold cyan]",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
await lifecycle.shutdown()
|
velune/cli/context.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""CLI runtime context passed through Typer commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from velune.core.runtime import RuntimeContext
|
|
11
|
+
from velune.kernel.config import VeluneConfig
|
|
12
|
+
from velune.kernel.registry import ServiceContainer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class CLIContext:
|
|
17
|
+
"""Shared CLI state for the current process."""
|
|
18
|
+
|
|
19
|
+
workspace: Path
|
|
20
|
+
config_path: Path | None
|
|
21
|
+
verbose: bool
|
|
22
|
+
runtime: RuntimeContext
|
|
23
|
+
json_mode: bool = False
|
|
24
|
+
yes: bool = False
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def console(self) -> Console:
|
|
28
|
+
return self.runtime.console
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def config(self) -> VeluneConfig:
|
|
32
|
+
return self.runtime.config
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def container(self) -> ServiceContainer:
|
|
36
|
+
return self.runtime.container
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Interactive two-stage TUI for assigning models to council agent roles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.application import Application
|
|
8
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
9
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
10
|
+
from prompt_toolkit.layout import Layout
|
|
11
|
+
from prompt_toolkit.layout.containers import Window
|
|
12
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
13
|
+
|
|
14
|
+
from velune.orchestration.role_assignments import (
|
|
15
|
+
COUNCIL_ROLES,
|
|
16
|
+
ROLE_DESCRIPTIONS,
|
|
17
|
+
CouncilRoleMap,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from velune.core.types.model import ModelDescriptor
|
|
24
|
+
|
|
25
|
+
# Sentinel to distinguish "user pressed Escape" from "user selected clear"
|
|
26
|
+
_CANCELLED = object()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def run_councilmodel_ui(
|
|
30
|
+
role_map: CouncilRoleMap,
|
|
31
|
+
available_models: list[ModelDescriptor],
|
|
32
|
+
console: Console,
|
|
33
|
+
) -> CouncilRoleMap | None:
|
|
34
|
+
"""Two-stage interactive UI: select a role, then select a model for it.
|
|
35
|
+
|
|
36
|
+
Returns the updated role_map, or None if cancelled at the role-select stage.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# ── Stage 1: Role selection ────────────────────────────────────────
|
|
40
|
+
disabled_roles = {"architect", "security", "challenger", "synthesizer"}
|
|
41
|
+
active_indices = [i for i, r in enumerate(COUNCIL_ROLES) if r not in disabled_roles]
|
|
42
|
+
selected_role_idx = [active_indices[0] if active_indices else 0]
|
|
43
|
+
role_result: list[str | None] = [None]
|
|
44
|
+
|
|
45
|
+
def render_role_list() -> FormattedText:
|
|
46
|
+
lines: list[tuple[str, str]] = []
|
|
47
|
+
lines.append(("bold", " Assign model to council role\n"))
|
|
48
|
+
lines.append(("fg:ansibrightblack", " ↑↓ navigate · Enter select · Esc cancel\n\n"))
|
|
49
|
+
for i, role in enumerate(COUNCIL_ROLES):
|
|
50
|
+
is_active = i == selected_role_idx[0]
|
|
51
|
+
prefix = "❯ " if is_active else " "
|
|
52
|
+
|
|
53
|
+
if role in disabled_roles:
|
|
54
|
+
row_style = "fg:ansibrightblack"
|
|
55
|
+
tag = " [disabled]"
|
|
56
|
+
prefix = " "
|
|
57
|
+
desc = ROLE_DESCRIPTIONS.get(role, "")
|
|
58
|
+
lines.append((row_style, f" {prefix}{role:<14} {desc}{tag}\n"))
|
|
59
|
+
else:
|
|
60
|
+
row_style = "bold fg:cyan" if is_active else ""
|
|
61
|
+
desc = ROLE_DESCRIPTIONS.get(role, "")
|
|
62
|
+
lines.append((row_style, f" {prefix}{role:<14} {desc}\n"))
|
|
63
|
+
current = role_map.get(role)
|
|
64
|
+
if current:
|
|
65
|
+
lines.append(
|
|
66
|
+
("fg:ansibrightblack", f" currently: {current.model_id}\n")
|
|
67
|
+
)
|
|
68
|
+
return FormattedText(lines)
|
|
69
|
+
|
|
70
|
+
kb1 = KeyBindings()
|
|
71
|
+
|
|
72
|
+
@kb1.add("up")
|
|
73
|
+
def _up(event) -> None:
|
|
74
|
+
curr = selected_role_idx[0]
|
|
75
|
+
while True:
|
|
76
|
+
curr = (curr - 1) % len(COUNCIL_ROLES)
|
|
77
|
+
if curr in active_indices:
|
|
78
|
+
selected_role_idx[0] = curr
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
@kb1.add("down")
|
|
82
|
+
def _down(event) -> None:
|
|
83
|
+
curr = selected_role_idx[0]
|
|
84
|
+
while True:
|
|
85
|
+
curr = (curr + 1) % len(COUNCIL_ROLES)
|
|
86
|
+
if curr in active_indices:
|
|
87
|
+
selected_role_idx[0] = curr
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
@kb1.add("enter")
|
|
91
|
+
def _select_role(event) -> None:
|
|
92
|
+
role_result[0] = COUNCIL_ROLES[selected_role_idx[0]]
|
|
93
|
+
event.app.exit()
|
|
94
|
+
|
|
95
|
+
@kb1.add("escape")
|
|
96
|
+
@kb1.add("c-c")
|
|
97
|
+
def _cancel_role(event) -> None:
|
|
98
|
+
event.app.exit() # role_result stays None
|
|
99
|
+
|
|
100
|
+
app1 = Application(
|
|
101
|
+
layout=Layout(
|
|
102
|
+
Window(
|
|
103
|
+
content=FormattedTextControl(render_role_list, focusable=True),
|
|
104
|
+
)
|
|
105
|
+
),
|
|
106
|
+
key_bindings=kb1,
|
|
107
|
+
full_screen=False,
|
|
108
|
+
mouse_support=False,
|
|
109
|
+
)
|
|
110
|
+
await app1.run_async()
|
|
111
|
+
|
|
112
|
+
selected_role = role_result[0]
|
|
113
|
+
if selected_role is None:
|
|
114
|
+
return None # cancelled at role stage
|
|
115
|
+
|
|
116
|
+
# ── Stage 2: Model selection for chosen role ───────────────────────
|
|
117
|
+
# First entry is None = "clear assignment"
|
|
118
|
+
model_options: list[ModelDescriptor | None] = [None] + list(available_models)
|
|
119
|
+
selected_model_idx = [0]
|
|
120
|
+
model_result: list[object] = [_CANCELLED]
|
|
121
|
+
|
|
122
|
+
def render_model_list() -> FormattedText:
|
|
123
|
+
lines: list[tuple[str, str]] = []
|
|
124
|
+
lines.append(("bold", f" Select model for [{selected_role}]\n"))
|
|
125
|
+
lines.append(("fg:ansibrightblack", " ↑↓ navigate · Enter select · Esc back\n\n"))
|
|
126
|
+
|
|
127
|
+
for i, model in enumerate(model_options):
|
|
128
|
+
is_active = i == selected_model_idx[0]
|
|
129
|
+
prefix = "❯ " if is_active else " "
|
|
130
|
+
row_style = "bold fg:cyan" if is_active else ""
|
|
131
|
+
|
|
132
|
+
if model is None:
|
|
133
|
+
lines.append((row_style, f" {prefix}(clear — use default routing)\n"))
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
current = role_map.get(selected_role)
|
|
137
|
+
is_current = current is not None and current.model_id == model.model_id
|
|
138
|
+
current_marker = " ← current" if is_current else ""
|
|
139
|
+
local_cloud = "local" if model.is_local else "cloud"
|
|
140
|
+
cost = getattr(model, "cost_per_1k_tokens", None)
|
|
141
|
+
free_str = " free" if cost == 0.0 else ""
|
|
142
|
+
lines.append(
|
|
143
|
+
(
|
|
144
|
+
row_style,
|
|
145
|
+
f" {prefix}{model.model_id:<42}"
|
|
146
|
+
f" [{local_cloud}{free_str} · {model.speed_tier}]"
|
|
147
|
+
f"{current_marker}\n",
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
return FormattedText(lines)
|
|
151
|
+
|
|
152
|
+
kb2 = KeyBindings()
|
|
153
|
+
|
|
154
|
+
@kb2.add("up")
|
|
155
|
+
def _up2(event) -> None:
|
|
156
|
+
selected_model_idx[0] = (selected_model_idx[0] - 1) % len(model_options)
|
|
157
|
+
|
|
158
|
+
@kb2.add("down")
|
|
159
|
+
def _down2(event) -> None:
|
|
160
|
+
selected_model_idx[0] = (selected_model_idx[0] + 1) % len(model_options)
|
|
161
|
+
|
|
162
|
+
@kb2.add("enter")
|
|
163
|
+
def _select_model(event) -> None:
|
|
164
|
+
model_result[0] = model_options[selected_model_idx[0]]
|
|
165
|
+
event.app.exit()
|
|
166
|
+
|
|
167
|
+
@kb2.add("escape")
|
|
168
|
+
@kb2.add("c-c")
|
|
169
|
+
def _cancel_model(event) -> None:
|
|
170
|
+
event.app.exit() # model_result stays _CANCELLED
|
|
171
|
+
|
|
172
|
+
app2 = Application(
|
|
173
|
+
layout=Layout(
|
|
174
|
+
Window(
|
|
175
|
+
content=FormattedTextControl(render_model_list, focusable=True),
|
|
176
|
+
)
|
|
177
|
+
),
|
|
178
|
+
key_bindings=kb2,
|
|
179
|
+
full_screen=False,
|
|
180
|
+
mouse_support=False,
|
|
181
|
+
)
|
|
182
|
+
await app2.run_async()
|
|
183
|
+
|
|
184
|
+
if model_result[0] is _CANCELLED:
|
|
185
|
+
return role_map # user backed out — return map unchanged
|
|
186
|
+
|
|
187
|
+
chosen_model = model_result[0]
|
|
188
|
+
if chosen_model is None:
|
|
189
|
+
role_map.clear_role(selected_role)
|
|
190
|
+
console.print(f"[yellow]✓ Cleared assignment for [{selected_role}][/yellow]")
|
|
191
|
+
else:
|
|
192
|
+
role_map.assign(selected_role, chosen_model.model_id, chosen_model.provider_id)
|
|
193
|
+
console.print(
|
|
194
|
+
f"[green]✓ [{selected_role}][/green] → "
|
|
195
|
+
f"[cyan]{chosen_model.model_id}[/cyan] "
|
|
196
|
+
f"[dim]({chosen_model.provider_id})[/dim]"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return role_map
|