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.
Files changed (279) 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 +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
velune/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Velune package metadata."""
2
+
3
+ __version__ = "0.9.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,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."""