velune-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +212 -0
  5. velune/cli/autocomplete.py +76 -0
  6. velune/cli/banner.py +98 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +149 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +188 -0
  11. velune/cli/commands/config.py +182 -0
  12. velune/cli/commands/daemon.py +85 -0
  13. velune/cli/commands/doctor.py +373 -0
  14. velune/cli/commands/init.py +160 -0
  15. velune/cli/commands/mcp.py +80 -0
  16. velune/cli/commands/memory.py +269 -0
  17. velune/cli/commands/models.py +462 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +171 -0
  20. velune/cli/commands/setup.py +182 -0
  21. velune/cli/commands/workspace.py +217 -0
  22. velune/cli/context.py +37 -0
  23. velune/cli/councilmodel_ui.py +171 -0
  24. velune/cli/display/council_view.py +240 -0
  25. velune/cli/display/memory_view.py +93 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +21 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +44 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +118 -0
  33. velune/cli/registry.py +81 -0
  34. velune/cli/repl.py +1178 -0
  35. velune/cli/session_manager.py +69 -0
  36. velune/cli/slash_commands.py +37 -0
  37. velune/cognition/__init__.py +19 -0
  38. velune/cognition/arbitrator.py +216 -0
  39. velune/cognition/architecture.py +398 -0
  40. velune/cognition/council/__init__.py +47 -0
  41. velune/cognition/council/base.py +216 -0
  42. velune/cognition/council/challenger.py +70 -0
  43. velune/cognition/council/coder.py +79 -0
  44. velune/cognition/council/critic_agent.py +39 -0
  45. velune/cognition/council/critic_configs.py +111 -0
  46. velune/cognition/council/critics.py +41 -0
  47. velune/cognition/council/debate.py +44 -0
  48. velune/cognition/council/factory.py +140 -0
  49. velune/cognition/council/messages.py +53 -0
  50. velune/cognition/council/planner.py +119 -0
  51. velune/cognition/council/reviewer.py +72 -0
  52. velune/cognition/council/synthesizer.py +67 -0
  53. velune/cognition/council/tiers.py +181 -0
  54. velune/cognition/firewall.py +256 -0
  55. velune/cognition/module.py +38 -0
  56. velune/cognition/orchestrator.py +886 -0
  57. velune/cognition/personality.py +236 -0
  58. velune/cognition/style_resolver.py +62 -0
  59. velune/cognition/verification.py +201 -0
  60. velune/context/__init__.py +11 -0
  61. velune/context/extractive.py +94 -0
  62. velune/context/window.py +62 -0
  63. velune/core/__init__.py +89 -0
  64. velune/core/background.py +5 -0
  65. velune/core/config/__init__.py +37 -0
  66. velune/core/errors/__init__.py +53 -0
  67. velune/core/errors/execution.py +26 -0
  68. velune/core/errors/memory.py +21 -0
  69. velune/core/errors/orchestration.py +26 -0
  70. velune/core/errors/provider.py +31 -0
  71. velune/core/event_loop.py +30 -0
  72. velune/core/logging.py +83 -0
  73. velune/core/runtime.py +106 -0
  74. velune/core/task_registry.py +120 -0
  75. velune/core/trace.py +80 -0
  76. velune/core/types/__init__.py +48 -0
  77. velune/core/types/agent.py +49 -0
  78. velune/core/types/context.py +39 -0
  79. velune/core/types/inference.py +35 -0
  80. velune/core/types/memory.py +39 -0
  81. velune/core/types/model.py +64 -0
  82. velune/core/types/provider.py +35 -0
  83. velune/core/types/repository.py +35 -0
  84. velune/core/types/task.py +56 -0
  85. velune/core/types/workspace.py +28 -0
  86. velune/daemon/client.py +13 -0
  87. velune/daemon/server.py +115 -0
  88. velune/daemon/transport.py +169 -0
  89. velune/events.py +194 -0
  90. velune/execution/__init__.py +22 -0
  91. velune/execution/benchmarker.py +311 -0
  92. velune/execution/cancellation.py +53 -0
  93. velune/execution/checkpointer.py +128 -0
  94. velune/execution/command_spec.py +140 -0
  95. velune/execution/diff_preview.py +172 -0
  96. velune/execution/executor.py +173 -0
  97. velune/execution/module.py +16 -0
  98. velune/execution/multi_diff.py +70 -0
  99. velune/execution/path_guard.py +19 -0
  100. velune/execution/planner.py +91 -0
  101. velune/execution/rollback.py +80 -0
  102. velune/execution/sandbox.py +257 -0
  103. velune/execution/validator.py +113 -0
  104. velune/hardware/__init__.py +1 -0
  105. velune/hardware/detector.py +162 -0
  106. velune/kernel/__init__.py +58 -0
  107. velune/kernel/bootstrap.py +107 -0
  108. velune/kernel/config.py +252 -0
  109. velune/kernel/health.py +54 -0
  110. velune/kernel/lifecycle.py +102 -0
  111. velune/kernel/module.py +15 -0
  112. velune/kernel/modules.py +23 -0
  113. velune/kernel/registry.py +93 -0
  114. velune/kernel/schemas.py +28 -0
  115. velune/main.py +9 -0
  116. velune/mcp/__init__.py +9 -0
  117. velune/mcp/client.py +113 -0
  118. velune/mcp/config.py +19 -0
  119. velune/mcp/server.py +90 -0
  120. velune/memory/__init__.py +28 -0
  121. velune/memory/lifecycle.py +154 -0
  122. velune/memory/module.py +94 -0
  123. velune/memory/prioritizer.py +65 -0
  124. velune/memory/storage/sqlite_manager.py +368 -0
  125. velune/memory/tiers/episodic.py +156 -0
  126. velune/memory/tiers/graph.py +282 -0
  127. velune/memory/tiers/lineage.py +367 -0
  128. velune/memory/tiers/semantic.py +198 -0
  129. velune/memory/tiers/working.py +165 -0
  130. velune/models/__init__.py +16 -0
  131. velune/models/module.py +18 -0
  132. velune/models/probes.py +182 -0
  133. velune/models/profile_cache.py +82 -0
  134. velune/models/profiler.py +105 -0
  135. velune/models/registry.py +201 -0
  136. velune/models/scorer.py +225 -0
  137. velune/models/specializations.py +196 -0
  138. velune/orchestration/__init__.py +15 -0
  139. velune/orchestration/module.py +14 -0
  140. velune/orchestration/role_assignments.py +78 -0
  141. velune/orchestration/schemas.py +99 -0
  142. velune/plugins/__init__.py +13 -0
  143. velune/plugins/hooks.py +49 -0
  144. velune/plugins/loader.py +95 -0
  145. velune/plugins/registry.py +54 -0
  146. velune/plugins/schemas.py +19 -0
  147. velune/providers/__init__.py +18 -0
  148. velune/providers/adapters/anthropic.py +231 -0
  149. velune/providers/adapters/fireworks.py +115 -0
  150. velune/providers/adapters/google.py +234 -0
  151. velune/providers/adapters/groq.py +151 -0
  152. velune/providers/adapters/huggingface.py +203 -0
  153. velune/providers/adapters/llamacpp.py +202 -0
  154. velune/providers/adapters/lmstudio.py +173 -0
  155. velune/providers/adapters/ollama.py +186 -0
  156. velune/providers/adapters/openai.py +207 -0
  157. velune/providers/adapters/openrouter.py +81 -0
  158. velune/providers/adapters/together.py +134 -0
  159. velune/providers/adapters/xai.py +60 -0
  160. velune/providers/base.py +86 -0
  161. velune/providers/benchmarker.py +135 -0
  162. velune/providers/discovery/__init__.py +33 -0
  163. velune/providers/discovery/anthropic.py +77 -0
  164. velune/providers/discovery/benchmarks.py +44 -0
  165. velune/providers/discovery/classifier.py +69 -0
  166. velune/providers/discovery/fireworks.py +95 -0
  167. velune/providers/discovery/gguf.py +87 -0
  168. velune/providers/discovery/google.py +95 -0
  169. velune/providers/discovery/gpu.py +109 -0
  170. velune/providers/discovery/groq.py +20 -0
  171. velune/providers/discovery/huggingface.py +67 -0
  172. velune/providers/discovery/lmstudio.py +80 -0
  173. velune/providers/discovery/ollama.py +165 -0
  174. velune/providers/discovery/openai.py +96 -0
  175. velune/providers/discovery/openrouter.py +113 -0
  176. velune/providers/discovery/scanner.py +114 -0
  177. velune/providers/discovery/together.py +114 -0
  178. velune/providers/discovery/xai.py +57 -0
  179. velune/providers/keystore.py +83 -0
  180. velune/providers/local_paths.py +49 -0
  181. velune/providers/local_resolver.py +208 -0
  182. velune/providers/module.py +15 -0
  183. velune/providers/ollama_manager.py +193 -0
  184. velune/providers/registry.py +173 -0
  185. velune/providers/router.py +82 -0
  186. velune/py.typed +0 -0
  187. velune/repository/__init__.py +21 -0
  188. velune/repository/analyzer.py +123 -0
  189. velune/repository/cognition.py +172 -0
  190. velune/repository/grapher.py +182 -0
  191. velune/repository/indexer.py +229 -0
  192. velune/repository/module.py +15 -0
  193. velune/repository/parser.py +378 -0
  194. velune/repository/project_type.py +293 -0
  195. velune/repository/scanner.py +177 -0
  196. velune/repository/schemas.py +102 -0
  197. velune/repository/tracker.py +233 -0
  198. velune/retrieval/__init__.py +27 -0
  199. velune/retrieval/graph.py +117 -0
  200. velune/retrieval/hybrid.py +250 -0
  201. velune/retrieval/keyword.py +113 -0
  202. velune/retrieval/module.py +19 -0
  203. velune/retrieval/reranker.py +68 -0
  204. velune/retrieval/schemas.py +59 -0
  205. velune/retrieval/vector.py +163 -0
  206. velune/telemetry/__init__.py +7 -0
  207. velune/telemetry/cognition.py +260 -0
  208. velune/telemetry/token_tracker.py +133 -0
  209. velune/tools/__init__.py +41 -0
  210. velune/tools/base/registry.py +84 -0
  211. velune/tools/base/tool.py +63 -0
  212. velune/tools/code/navigate.py +107 -0
  213. velune/tools/code/search.py +112 -0
  214. velune/tools/filesystem/read.py +75 -0
  215. velune/tools/filesystem/search.py +123 -0
  216. velune/tools/filesystem/write.py +160 -0
  217. velune/tools/git/history.py +185 -0
  218. velune/tools/git/operations.py +132 -0
  219. velune/tools/git/state.py +134 -0
  220. velune/tools/module.py +65 -0
  221. velune/tools/terminal/execute.py +72 -0
  222. velune/tools/terminal/history.py +47 -0
  223. velune/tools/web/fetch.py +55 -0
  224. velune/tools/web/validator.py +96 -0
  225. velune_cli-1.0.0.dist-info/METADATA +497 -0
  226. velune_cli-1.0.0.dist-info/RECORD +229 -0
  227. velune_cli-1.0.0.dist-info/WHEEL +4 -0
  228. velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
  229. velune_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
velune/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Velune package metadata."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ __all__ = ["__version__"]
velune/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Module entry point for `python -m velune`."""
2
+
3
+ from velune.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
velune/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CLI entry points."""
2
+
3
+ from velune.cli.main import app
4
+
5
+ __all__ = ["app"]
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."""