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/cli/pull_ui.py ADDED
@@ -0,0 +1,123 @@
1
+ """Interactive model browser TUI for /pull — select a model to download."""
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.providers.ollama_manager import RECOMMENDED_MODELS
15
+
16
+ if TYPE_CHECKING:
17
+ from rich.console import Console
18
+
19
+ _SKILL_STYLES: dict[str, str] = {
20
+ "coding": "fg:ansigreen",
21
+ "reasoning": "fg:ansimagenta",
22
+ "embedding": "fg:ansicyan",
23
+ "general": "fg:ansibrightblack",
24
+ }
25
+
26
+
27
+ async def run_pull_ui(
28
+ local_models: list[str],
29
+ hardware_ram_gb: float,
30
+ console: Console,
31
+ ) -> str | None:
32
+ """Show the interactive model browser and return the chosen model_id, or None."""
33
+
34
+ selected_idx = [0]
35
+ result: list[str | None] = [None]
36
+
37
+ def _fits(model: dict) -> bool:
38
+ try:
39
+ needed = float(model["ram_needed"].replace(" GB", "").strip())
40
+ return hardware_ram_gb >= needed
41
+ except Exception:
42
+ return True
43
+
44
+ def render_list() -> FormattedText:
45
+ lines: list[tuple[str, str]] = []
46
+ lines.append(("bold", " Available models to pull\n"))
47
+ lines.append(("fg:ansibrightblack", " ↑↓ navigate · Enter pull · Esc cancel\n\n"))
48
+ lines.append(("fg:ansibrightblack", f" Your RAM: {hardware_ram_gb:.0f} GB\n\n"))
49
+
50
+ for i, model in enumerate(RECOMMENDED_MODELS):
51
+ is_active = i == selected_idx[0]
52
+ prefix = "❯ " if is_active else " "
53
+ row_style = "bold fg:cyan" if is_active else ""
54
+ model_id = model["model_id"]
55
+ is_local = any(
56
+ m == model_id or m.split(":")[0] == model_id.split(":")[0] and m == model_id
57
+ for m in local_models
58
+ )
59
+ fits = _fits(model)
60
+ skill = model.get("skill", "general")
61
+ skill_style = _SKILL_STYLES.get(skill, "fg:ansibrightblack")
62
+
63
+ if is_local:
64
+ status = " ✓ installed"
65
+ status_style = "fg:ansigreen"
66
+ elif not fits:
67
+ status = " needs more RAM"
68
+ status_style = "fg:ansibrightblack"
69
+ else:
70
+ status = ""
71
+ status_style = ""
72
+
73
+ lines.append(
74
+ (
75
+ row_style,
76
+ f" {prefix}{model_id:<34} {model['size_gb']:4.1f} GB ",
77
+ )
78
+ )
79
+ lines.append((skill_style, f"[{skill}]"))
80
+ if status:
81
+ lines.append((status_style, status))
82
+ lines.append(("", "\n"))
83
+
84
+ if is_active:
85
+ lines.append(("fg:ansibrightblack", f" {model['description']}\n"))
86
+ lines.append(
87
+ ("fg:ansibrightblack", f" RAM needed: {model['ram_needed']}\n")
88
+ )
89
+
90
+ return FormattedText(lines)
91
+
92
+ kb = KeyBindings()
93
+
94
+ @kb.add("up")
95
+ def _up(event) -> None:
96
+ selected_idx[0] = (selected_idx[0] - 1) % len(RECOMMENDED_MODELS)
97
+
98
+ @kb.add("down")
99
+ def _down(event) -> None:
100
+ selected_idx[0] = (selected_idx[0] + 1) % len(RECOMMENDED_MODELS)
101
+
102
+ @kb.add("enter")
103
+ def _select(event) -> None:
104
+ result[0] = RECOMMENDED_MODELS[selected_idx[0]]["model_id"]
105
+ event.app.exit()
106
+
107
+ @kb.add("escape")
108
+ @kb.add("c-c")
109
+ def _cancel(event) -> None:
110
+ event.app.exit()
111
+
112
+ app = Application(
113
+ layout=Layout(
114
+ Window(
115
+ content=FormattedTextControl(render_list, focusable=True),
116
+ )
117
+ ),
118
+ key_bindings=kb,
119
+ full_screen=False,
120
+ mouse_support=False,
121
+ )
122
+ await app.run_async()
123
+ return result[0]
velune/cli/registry.py ADDED
@@ -0,0 +1,80 @@
1
+ """CLI command discovery and registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from collections.abc import Sequence
7
+ from importlib.metadata import EntryPoint, entry_points
8
+ from types import ModuleType
9
+
10
+ import typer
11
+
12
+ from velune.cli.commands import (
13
+ ask_command,
14
+ chat_command,
15
+ config_cmd,
16
+ daemon_cmd,
17
+ doctor_cmd,
18
+ init_command,
19
+ mcp_cmd,
20
+ mcp_serve,
21
+ memory_cmd,
22
+ models_cmd,
23
+ run_command,
24
+ setup_command,
25
+ workspace_cmd,
26
+ )
27
+ from velune.kernel.registry import ServiceContainer
28
+
29
+ BUILTIN_COMMAND_MODULES: Sequence[str] = (
30
+ "velune.cli.commands.ask",
31
+ "velune.cli.commands.chat",
32
+ "velune.cli.commands.run",
33
+ "velune.cli.commands.models",
34
+ "velune.cli.commands.workspace",
35
+ "velune.cli.commands.memory",
36
+ "velune.cli.commands.config",
37
+ "velune.cli.commands.daemon",
38
+ "velune.cli.commands.doctor",
39
+ "velune.cli.commands.mcp",
40
+ )
41
+
42
+
43
+ def discover_builtin_modules() -> list[ModuleType]:
44
+ """Import and return built-in command modules."""
45
+
46
+ modules: list[ModuleType] = []
47
+ for module_name in BUILTIN_COMMAND_MODULES:
48
+ modules.append(importlib.import_module(module_name))
49
+ return modules
50
+
51
+
52
+ def discover_plugin_entry_points(group: str = "velune.commands") -> list[EntryPoint]:
53
+ """Return third-party command extension entry points."""
54
+
55
+ return list(entry_points(group=group))
56
+
57
+
58
+ def register_commands(app: typer.Typer, container: ServiceContainer) -> None:
59
+ """Attach all built-in and plugin command groups to the app."""
60
+
61
+ app.command(name="ask")(ask_command)
62
+ app.command(name="chat")(chat_command)
63
+ app.command(name="init")(init_command)
64
+ app.command(name="run")(run_command)
65
+ app.command(name="setup")(setup_command)
66
+ app.command(name="mcp-serve")(mcp_serve)
67
+ app.add_typer(models_cmd, name="models")
68
+ app.add_typer(workspace_cmd, name="workspace")
69
+ app.add_typer(memory_cmd, name="memory")
70
+ app.add_typer(config_cmd, name="config")
71
+ app.add_typer(daemon_cmd, name="daemon")
72
+ app.add_typer(doctor_cmd, name="doctor")
73
+ app.add_typer(mcp_cmd, name="mcp")
74
+
75
+ for entry_point in discover_plugin_entry_points():
76
+ loaded = entry_point.load()
77
+ if hasattr(loaded, "register"):
78
+ loaded.register(app=app, container=container)
79
+ elif callable(loaded):
80
+ loaded(app=app, container=container)
@@ -0,0 +1,5 @@
1
+ """CLI rendering helpers."""
2
+
3
+ from velune.cli.rendering.markdown import CustomMarkdown, MarkdownStreamBuffer
4
+
5
+ __all__ = ["CustomMarkdown", "MarkdownStreamBuffer"]
@@ -0,0 +1,79 @@
1
+ """Rich error panel renderer for structured VeluneError display."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ if TYPE_CHECKING:
12
+ from velune.core.errors.catalog import VeluneError
13
+
14
+ _BUG_REPORT_URL = "https://github.com/velune-ai/velune/issues"
15
+
16
+
17
+ def render_error(error: VeluneError) -> Panel:
18
+ """Build a Rich Panel for a VeluneError with cause and fix sections."""
19
+ body = Text()
20
+
21
+ cause = error.get_cause()
22
+ if cause:
23
+ body.append("Cause\n", style="bold white")
24
+ body.append(f" {cause}\n", style="dim white")
25
+
26
+ if error.fix:
27
+ body.append("\nFix\n", style="bold white")
28
+ for step in error.fix:
29
+ body.append(f" • {step}\n", style="dim white")
30
+
31
+ if error.docs_url:
32
+ body.append(f"\n docs → {error.docs_url}", style="blue underline")
33
+
34
+ detail = error.get_detail()
35
+ if detail and detail != error.title:
36
+ body.append("\n\n Detail: ", style="dim")
37
+ body.append(detail, style="dim white")
38
+
39
+ body.append("\n\n Use --verbose for full stack trace.", style="dim")
40
+
41
+ return Panel(
42
+ body,
43
+ title=f"[bold red]Error:[/bold red] {error.title}",
44
+ border_style="red",
45
+ padding=(1, 2),
46
+ )
47
+
48
+
49
+ def render_unexpected_error(exc: Exception) -> Panel:
50
+ """Build a Rich Panel for an unrecognised exception."""
51
+ body = Text()
52
+ body.append(
53
+ "An unexpected error occurred.\n\n",
54
+ style="dim white",
55
+ )
56
+ body.append(f" {type(exc).__name__}: ", style="dim white")
57
+ body.append(f"{exc}\n", style="white")
58
+
59
+ body.append("\nWhat to do\n", style="bold white")
60
+ body.append(" • Use --verbose to see the full stack trace\n", style="dim white")
61
+ body.append(f" • Report the issue at {_BUG_REPORT_URL}\n", style="dim white")
62
+ body.append(" • Include the --verbose output in your report\n", style="dim white")
63
+
64
+ return Panel(
65
+ body,
66
+ title="[bold red]Unexpected Error[/bold red]",
67
+ border_style="red",
68
+ padding=(1, 2),
69
+ )
70
+
71
+
72
+ def print_error(error: VeluneError, console: Console | None = None) -> None:
73
+ """Convenience wrapper: render and print a VeluneError to the given console."""
74
+ (console or Console()).print(render_error(error))
75
+
76
+
77
+ def print_unexpected_error(exc: Exception, console: Console | None = None) -> None:
78
+ """Convenience wrapper: render and print an unexpected exception."""
79
+ (console or Console()).print(render_unexpected_error(exc))
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from rich.console import Console, ConsoleOptions, RenderResult
4
+ from rich.markdown import CodeBlock, Markdown
5
+ from rich.syntax import Syntax
6
+
7
+
8
+ class CustomCodeBlock(CodeBlock):
9
+ """A code block with syntax highlighting and line numbers for blocks >5 lines."""
10
+
11
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
12
+ code = str(self.text).rstrip()
13
+ line_count = len(code.splitlines())
14
+ line_numbers = line_count > 5
15
+ syntax = Syntax(
16
+ code,
17
+ self.lexer_name,
18
+ theme="monokai",
19
+ word_wrap=True,
20
+ line_numbers=line_numbers,
21
+ padding=1,
22
+ )
23
+ yield syntax
24
+
25
+
26
+ class CustomMarkdown(Markdown):
27
+ """Custom Markdown renderer that overrides CodeBlock element handling."""
28
+
29
+ elements = Markdown.elements.copy()
30
+ elements["fence"] = CustomCodeBlock
31
+ elements["code_block"] = CustomCodeBlock
32
+
33
+ def __init__(self, markup: str, code_theme: str = "monokai", **kwargs) -> None:
34
+ super().__init__(markup, code_theme=code_theme, **kwargs)
35
+
36
+
37
+ class MarkdownStreamBuffer:
38
+ """Buffer that accumulates chunks of Markdown, handles split fences, and returns clean renderables."""
39
+
40
+ def __init__(self) -> None:
41
+ self._buffer = ""
42
+
43
+ def append(self, text: str) -> None:
44
+ self._buffer += text
45
+
46
+ @property
47
+ def raw_content(self) -> str:
48
+ return self._buffer
49
+
50
+ def get_renderable(self) -> CustomMarkdown:
51
+ content = self._buffer
52
+
53
+ # Clean up split code fences at the end of the buffer to prevent flicker/incorrect rendering.
54
+ # If it ends with incomplete backticks on a new line (e.g. \n` or \n``)
55
+ # or just starts with incomplete backticks if buffer is very short
56
+ if content.endswith("\n`"):
57
+ content = content[:-2]
58
+ elif content.endswith("\n``"):
59
+ content = content[:-3]
60
+ elif content == "`" or content == "``":
61
+ content = ""
62
+
63
+ return CustomMarkdown(content)