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
@@ -0,0 +1,171 @@
1
+ """Run command — velune run <task> to trigger Reasoning Council deliberation and sandbox execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from velune.cli.context import CLIContext
11
+ from velune.cognition.firewall import CognitiveFirewall
12
+ from velune.repository.schemas import RepositorySnapshot
13
+
14
+ console = Console()
15
+ run_cmd = typer.Typer(help="Autonomous council run commands")
16
+
17
+
18
+ def run_command(
19
+ ctx: typer.Context,
20
+ task: str = typer.Argument(..., help="Natural-language task to execute"),
21
+ dry_run: bool = typer.Option(False, "--dry-run", "-d", help="Deliberate but do not write modifications or execute scripts"),
22
+ force: bool = typer.Option(False, "--force", "-f", help="Force execution without human confirm thresholds"),
23
+ ) -> None:
24
+ """Deliberate with the stateful LangGraph Reasoning Council and execute in the secured sandbox."""
25
+
26
+ cli_context = ctx.obj
27
+ if not isinstance(cli_context, CLIContext):
28
+ raise typer.BadParameter("CLI context was not properly initialized")
29
+
30
+ from velune.core.event_loop import submit
31
+ submit(_run_command_async(cli_context, task, dry_run, force))
32
+
33
+
34
+ async def _run_command_async(
35
+ cli_context: CLIContext,
36
+ task: str,
37
+ dry_run: bool,
38
+ force: bool,
39
+ ) -> None:
40
+ # 1. Access modern services from the DI container
41
+ container = cli_context.container
42
+ lifecycle = container.get("runtime.lifecycle")
43
+ model_registry = container.get("runtime.model_registry")
44
+ orchestration_engine = container.get("runtime.orchestration_engine")
45
+ # 2. Boot up Cognitive OS Subsystems
46
+ if not cli_context.json_mode:
47
+ console.print("[bold cyan]⠋[/bold cyan] Bootstrapping Cognitive Operating System kernel...")
48
+ await lifecycle.startup()
49
+
50
+ # 3. Refresh model catalog scan to assign specialized seats
51
+ if not cli_context.json_mode:
52
+ console.print("[bold cyan]⠋[/bold cyan] Probing system hardware and local/remote providers...")
53
+ await model_registry.refresh()
54
+
55
+ # Onboarding preflight check gate
56
+ from velune.cli.commands.preflight import run_preflight_check
57
+ if not await run_preflight_check(container, console if not cli_context.json_mode else None):
58
+ if cli_context.json_mode:
59
+ import json
60
+ print(json.dumps({"error": "Preflight check failed. Ensure workspace is initialized and models are scanned."}))
61
+ await lifecycle.shutdown()
62
+ return
63
+
64
+ if not cli_context.json_mode:
65
+ console.print()
66
+ console.print(Panel(f"[bold green]Task Auth:[/bold green] {task}", border_style="cyan", title="[bold cyan]Velune Stateful Execution Pipeline[/bold cyan]"))
67
+ # 4. Stream Multi-Agent Council Deliberation & Execution Graph
68
+ console.print("[bold magenta]🧠 Streaming LangGraph stateful execution & checkpoint pipeline...[/bold magenta]\n")
69
+
70
+ from velune.orchestration.schemas import ExecutionStatus
71
+
72
+ async def stream_runner():
73
+ milestones = []
74
+ async for milestone in orchestration_engine.stream(task):
75
+ if not cli_context.json_mode:
76
+ console.print(f" [bold cyan]•[/bold cyan] {milestone}")
77
+ milestones.append(milestone)
78
+
79
+ # Parse run_id from milestones (format: "[run_id] milestone_name")
80
+ run_id = None
81
+ for m in milestones:
82
+ if hasattr(m, "run_id"):
83
+ run_id = m.run_id
84
+ break
85
+ elif isinstance(m, str) and m.startswith("[") and "]" in m:
86
+ run_id = m.split("]")[0][1:]
87
+ break
88
+ return orchestration_engine.get_state(run_id) if run_id else None
89
+
90
+ state = await stream_runner()
91
+
92
+ # 5. Display Task Execution Results
93
+ if not cli_context.json_mode:
94
+ console.print()
95
+ if state is None:
96
+ if cli_context.json_mode:
97
+ import json
98
+ print(json.dumps({"error": "Pipeline failed to initialize state."}))
99
+ else:
100
+ console.print("[bold red]✗ Pipeline failed to initialize state.[/bold red]")
101
+ await lifecycle.shutdown()
102
+ return
103
+
104
+ success = state.status == ExecutionStatus.COMPLETED
105
+ attempts_count = len(state.attempts)
106
+ plan_steps = len(state.task_plan.steps) if state.task_plan else 0
107
+ checkpoints_count = len(state.checkpoints)
108
+
109
+ if cli_context.json_mode:
110
+ import json
111
+ print(json.dumps({
112
+ "success": success,
113
+ "run_id": state.run_id,
114
+ "plan_steps": plan_steps,
115
+ "retry_attempts": attempts_count,
116
+ "checkpoints_saved": checkpoints_count,
117
+ "output": state.output or "Execution completed successfully.",
118
+ "error": state.error,
119
+ "validation_issues": state.validation_issues or [],
120
+ }))
121
+ else:
122
+ if success:
123
+ console.print(
124
+ Panel(
125
+ Text.assemble(
126
+ ("[bold green]✓ STATEFUL AUTONOMOUS EXECUTION COMPLETED[/bold green]\n\n"),
127
+ (f"Run ID: [bold white]{state.run_id}[/bold white]\n"),
128
+ (f"Plan Steps: [bold white]{plan_steps}[/bold white] steps processed\n"),
129
+ (f"Retry Attempts: [bold white]{attempts_count}[/bold white]\n"),
130
+ (f"Checkpoints Saved: [bold white]{checkpoints_count}[/bold white]\n\n"),
131
+ ("[bold green]Synthesized Output:[/bold green]\n"),
132
+ (state.output or "Execution completed successfully.")
133
+ ),
134
+ border_style="green",
135
+ title="[bold green]Success Report[/bold green]"
136
+ )
137
+ )
138
+ else:
139
+ console.print(
140
+ Panel(
141
+ Text.assemble(
142
+ ("[bold red]✗ AUTONOMOUS PIPELINE BLOCKED & ROLLED BACK[/bold red]\n\n"),
143
+ (f"Run ID: [bold white]{state.run_id}[/bold white]\n"),
144
+ (f"Failure Reason: [bold red]{state.error or 'Validation/Execution mismatch'}[/bold red]\n"),
145
+ (f"Retry Attempts: [bold white]{attempts_count}[/bold white]\n"),
146
+ (f"Validation Issues: [bold yellow]{', '.join(state.validation_issues) if state.validation_issues else 'None'}[/bold yellow]\n\n"),
147
+ ("[yellow]State checkpointer stashed checkpoints, and Git workspace states have been preserved/rolled back.[/yellow]")
148
+ ),
149
+ border_style="red",
150
+ title="[bold red]Rollback Execution Report[/bold red]"
151
+ )
152
+ )
153
+
154
+ # 6. Graceful Shutdown
155
+ await lifecycle.shutdown()
156
+
157
+
158
+ def _format_snapshot_context_safe(snapshot: RepositorySnapshot, firewall: CognitiveFirewall) -> str:
159
+ """Format the RepositorySnapshot securely as a text summary context for the planner/coder agents."""
160
+ lines = [f"Repository Root: {snapshot.root_path}"]
161
+ lines.append("Codebase Files:")
162
+ for f in snapshot.files[:25]:
163
+ # Only expose path and language — no raw symbol names or content
164
+ risk_marker = " [⚠ injection-risk]" if f.metadata.get("injection_risk") else ""
165
+ lines.append(f" - {f.path} ({f.language.value}){risk_marker}")
166
+ # Symbol names are safe to expose (identifiers, not content)
167
+ if f.symbols:
168
+ safe_syms = [s.name for s in f.symbols[:3] if s.name.isidentifier()]
169
+ if safe_syms:
170
+ lines.append(f" Symbols: {', '.join(safe_syms)}")
171
+ return "\n".join(lines)
@@ -0,0 +1,182 @@
1
+ """Interactive provider setup wizard — stores keys in the OS keychain."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Confirm, Prompt
8
+ from rich.table import Table
9
+
10
+ from velune.providers.keystore import get_key, has_key, save_key
11
+
12
+ console = Console()
13
+
14
+ PROVIDER_METADATA: dict[str, dict] = {
15
+ "ollama": {
16
+ "label": "Ollama (local — free, no key needed)",
17
+ "requires_key": False,
18
+ "free": True,
19
+ "url": "https://ollama.com",
20
+ },
21
+ "groq": {
22
+ "label": "Groq (cloud — free tier, very fast)",
23
+ "requires_key": True,
24
+ "free": True,
25
+ "key_label": "Groq API key",
26
+ "get_key_url": "https://console.groq.com/keys",
27
+ },
28
+ "openai": {
29
+ "label": "OpenAI (cloud — GPT-4o, paid)",
30
+ "requires_key": True,
31
+ "free": False,
32
+ "key_label": "OpenAI API key",
33
+ "get_key_url": "https://platform.openai.com/api-keys",
34
+ },
35
+ "anthropic": {
36
+ "label": "Anthropic (cloud — Claude, paid)",
37
+ "requires_key": True,
38
+ "free": False,
39
+ "key_label": "Anthropic API key",
40
+ "get_key_url": "https://console.anthropic.com",
41
+ },
42
+ "xai": {
43
+ "label": "xAI (cloud — Grok, paid)",
44
+ "requires_key": True,
45
+ "free": False,
46
+ "key_label": "xAI API key",
47
+ "get_key_url": "https://console.x.ai",
48
+ },
49
+ "google": {
50
+ "label": "Google Gemini (cloud — 2.0 Flash free quota)",
51
+ "requires_key": True,
52
+ "free": True,
53
+ "key_label": "Google API key",
54
+ "get_key_url": "https://aistudio.google.com/app/apikey",
55
+ },
56
+ "openrouter": {
57
+ "label": "OpenRouter (cloud — one key, 100+ models)",
58
+ "requires_key": True,
59
+ "free": False,
60
+ "key_label": "OpenRouter API key",
61
+ "get_key_url": "https://openrouter.ai/keys",
62
+ },
63
+ "together": {
64
+ "label": "Together.AI (cloud — Llama, Qwen, DeepSeek, cheap)",
65
+ "requires_key": True,
66
+ "free": False,
67
+ "key_label": "Together.AI API key",
68
+ "get_key_url": "https://api.together.ai/settings/api-keys",
69
+ },
70
+ "fireworks": {
71
+ "label": "Fireworks.AI (cloud — fastest open-model inference)",
72
+ "requires_key": True,
73
+ "free": False,
74
+ "key_label": "Fireworks.AI API key",
75
+ "get_key_url": "https://fireworks.ai/account/api-keys",
76
+ },
77
+ }
78
+
79
+
80
+ def run_setup_wizard() -> None:
81
+ """Run the interactive API key setup wizard."""
82
+ console.print(Panel(
83
+ "[bold cyan]Velune Provider Setup[/bold cyan]\n"
84
+ "[dim]Configure which AI providers you want to use.[/dim]\n"
85
+ "[dim]Keys are stored securely in your OS keychain.[/dim]",
86
+ border_style="cyan", padding=(0, 1),
87
+ ))
88
+ console.print()
89
+
90
+ chosen = _select_providers()
91
+ if not chosen:
92
+ console.print("[yellow]No providers selected. Run `velune setup` any time.[/yellow]")
93
+ return
94
+
95
+ configured: list[str] = []
96
+ for pid in chosen:
97
+ meta = PROVIDER_METADATA[pid]
98
+ if not meta.get("requires_key"):
99
+ console.print(f"[green]✓[/green] {meta['label']} — no key required")
100
+ configured.append(pid)
101
+ continue
102
+
103
+ console.print(f"\n[cyan]{meta['label']}[/cyan]")
104
+ console.print(f" [dim]Get your key: {meta.get('get_key_url', '')}[/dim]")
105
+
106
+ if has_key(pid):
107
+ existing = get_key(pid)
108
+ masked = _mask_key(existing)
109
+ overwrite = Confirm.ask(
110
+ f" Key already configured ({masked}). Replace it?",
111
+ default=False,
112
+ )
113
+ if not overwrite:
114
+ configured.append(pid)
115
+ continue
116
+
117
+ key = Prompt.ask(
118
+ f" Enter {meta['key_label']}",
119
+ password=True,
120
+ )
121
+
122
+ if not key.strip():
123
+ console.print(" [yellow]Skipped — no key entered.[/yellow]")
124
+ continue
125
+
126
+ save_key(pid, key.strip())
127
+ console.print(" [green]✓ Saved to OS keychain[/green]")
128
+ configured.append(pid)
129
+
130
+ console.print()
131
+ if configured:
132
+ console.print(
133
+ f"[bold green]✓ Configured providers:[/bold green] {', '.join(configured)}"
134
+ )
135
+ console.print("[dim]Run `velune doctor` to verify connectivity.[/dim]")
136
+
137
+
138
+ def _select_providers() -> list[str]:
139
+ table = Table(border_style="dim", padding=(0, 1))
140
+ table.add_column("#", style="dim", width=3)
141
+ table.add_column("Provider", style="cyan")
142
+ table.add_column("Type", style="dim")
143
+ table.add_column("Cost", style="dim")
144
+ table.add_column("Status")
145
+
146
+ provider_ids = list(PROVIDER_METADATA.keys())
147
+ for i, pid in enumerate(provider_ids, 1):
148
+ meta = PROVIDER_METADATA[pid]
149
+ cost = "[green]free[/green]" if meta.get("free") else "[dim]paid[/dim]"
150
+ status = "[green]✓ configured[/green]" if has_key(pid) else "[dim]not set[/dim]"
151
+ ptype = "local" if not meta.get("requires_key") else "cloud"
152
+ table.add_row(str(i), meta["label"], ptype, cost, status)
153
+
154
+ console.print(table)
155
+ console.print()
156
+
157
+ raw = Prompt.ask(
158
+ "Select providers to configure [dim](comma-separated numbers, e.g. 1,2,3)[/dim]",
159
+ default="1",
160
+ )
161
+
162
+ selected: list[str] = []
163
+ for part in raw.split(","):
164
+ part = part.strip()
165
+ if part.isdigit():
166
+ idx = int(part) - 1
167
+ if 0 <= idx < len(provider_ids):
168
+ selected.append(provider_ids[idx])
169
+ return selected
170
+
171
+
172
+ def _mask_key(key: str | None) -> str:
173
+ if not key:
174
+ return "***"
175
+ if len(key) <= 12:
176
+ return "*" * len(key)
177
+ return key[:8] + "..." + key[-4:]
178
+
179
+
180
+ def setup_command() -> None:
181
+ """Configure AI provider API keys."""
182
+ run_setup_wizard()
@@ -0,0 +1,217 @@
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
+ veluneignore_path.write_text(DEFAULT_VELUNEIGNORE, encoding="utf-8")
49
+ if not cli_context.json_mode:
50
+ console.print("[green]✓[/green] Created default .veluneignore (edit to customise index exclusions).")
51
+
52
+ from velune.core.event_loop import submit
53
+ submit(_workspace_init_async(cli_context, path, velune_dir, force))
54
+
55
+
56
+ async def _workspace_init_async(
57
+ cli_context: CLIContext,
58
+ path: Path,
59
+ velune_dir: Path,
60
+ force: bool,
61
+ ) -> None:
62
+ container = cli_context.container
63
+ lifecycle = container.get("runtime.lifecycle")
64
+ repo_cognition = container.get("runtime.repository_cognition")
65
+
66
+ # 3. Boot subsystems and compile index
67
+ await lifecycle.startup()
68
+
69
+ if not cli_context.json_mode:
70
+ console.print("[bold cyan]⠋[/bold cyan] Building Tree-sitter compiler AST indices and scanning imports...")
71
+ with console.status("[bold magenta]⚡ Parsing symbols, dependencies, and git Authorship...[/bold magenta]"):
72
+ snapshot = repo_cognition.index(force=force)
73
+ else:
74
+ snapshot = repo_cognition.index(force=force)
75
+
76
+ # Calculate statistics
77
+ num_files = len(snapshot.files)
78
+ num_symbols = len(snapshot.symbols)
79
+ num_edges = len(snapshot.edges)
80
+ skipped_secrets = snapshot.summary.get("skipped_secrets", [])
81
+
82
+ languages = {}
83
+ for f in snapshot.files:
84
+ languages[f.language.value] = languages.get(f.language.value, 0) + 1
85
+
86
+ lang_summary = ", ".join(f"{count} {lang}" for lang, count in languages.items())
87
+ git_branch = snapshot.summary.get("git", {}).get("active_branch", "untracked")
88
+
89
+ # 4. Render gorgeous Success Summary Panel
90
+ if cli_context.json_mode:
91
+ import json
92
+ print(json.dumps({
93
+ "success": True,
94
+ "workspace_path": str(path),
95
+ "caches_directory": str(velune_dir),
96
+ "indexed_files": num_files,
97
+ "languages": languages,
98
+ "parsed_ast_symbols": num_symbols,
99
+ "dependency_edges": num_edges,
100
+ "active_branch": git_branch,
101
+ "skipped_secrets": skipped_secrets,
102
+ }))
103
+ else:
104
+ console.print()
105
+ console.print(
106
+ Panel(
107
+ Text.assemble(
108
+ ("[bold green]✓ VELUNE WORKSPACE SUCCESSFULLY INDEXED[/bold green]\n\n"),
109
+ (f"[bold]Workspace path:[/bold] {path}\n"),
110
+ (f"[bold]Caches directory:[/bold] {velune_dir}\n"),
111
+ (f"[bold]Indexed files:[/bold] {num_files} ({lang_summary or 'no code files found'})\n"),
112
+ (f"[bold]Parsed AST symbols:[/bold] {num_symbols} classes/functions/imports\n"),
113
+ (f"[bold]Dependency edges:[/bold] {num_edges} import link(s)\n"),
114
+ (f"[bold]Active branch:[/bold] [magenta]{git_branch}[/magenta]\n\n"),
115
+ ("[dim]Velune repository cognitive engine is primed. Use 'velune run' to start autonomous edits.[/dim]")
116
+ ),
117
+ border_style="green",
118
+ box=ROUNDED,
119
+ title="[bold green]Cognitive Priming Success[/bold green]"
120
+ )
121
+ )
122
+
123
+ if skipped_secrets:
124
+ secret_lines = "\n".join(f" [bold yellow]•[/bold yellow] {p}" for p in skipped_secrets)
125
+ console.print(
126
+ Panel(
127
+ Text.from_markup(
128
+ "[bold yellow]Velune detected and protected the following files from being indexed:[/bold yellow]\n\n"
129
+ + secret_lines
130
+ + "\n\n[dim]These files matched known secrets/credentials patterns. "
131
+ "Add them to [bold].veluneignore[/bold] to silence this notice, "
132
+ "or ensure they are listed in [bold].gitignore[/bold].[/dim]"
133
+ ),
134
+ title="[bold yellow]🔒 Secrets Protected[/bold yellow]",
135
+ border_style="yellow",
136
+ box=ROUNDED,
137
+ padding=(1, 2),
138
+ )
139
+ )
140
+
141
+ await lifecycle.shutdown()
142
+
143
+
144
+ @workspace_cmd.command("status")
145
+ def workspace_status(
146
+ ctx: typer.Context,
147
+ path: Path = typer.Option(Path.cwd(), "--path", "-p", help="Workspace path"),
148
+ ) -> None:
149
+ """Show active workspace structure and index summary."""
150
+ velune_dir = path / ".velune"
151
+
152
+ cli_context = ctx.obj
153
+ if not isinstance(cli_context, CLIContext):
154
+ raise typer.BadParameter("CLI context was not properly initialized")
155
+
156
+ if not velune_dir.exists():
157
+ if cli_context.json_mode:
158
+ import json
159
+ print(json.dumps({"error": "Not a Velune workspace (no .velune directory detected)"}))
160
+ else:
161
+ console.print("[bold red]✗ Not a Velune workspace (no .velune directory detected).[/bold red]")
162
+ console.print("[dim]Use 'velune workspace init' to initialize.[/dim]")
163
+ return
164
+
165
+ from velune.core.event_loop import submit
166
+ submit(_workspace_status_async(cli_context, path, velune_dir))
167
+
168
+
169
+ async def _workspace_status_async(
170
+ cli_context: CLIContext,
171
+ path: Path,
172
+ velune_dir: Path,
173
+ ) -> None:
174
+ container = cli_context.container
175
+ lifecycle = container.get("runtime.lifecycle")
176
+ repo_cognition = container.get("runtime.repository_cognition")
177
+
178
+ await lifecycle.startup()
179
+
180
+ if not cli_context.json_mode:
181
+ with console.status("[bold cyan]Querying cognitive index status...[/bold cyan]"):
182
+ snapshot = repo_cognition.index(force=False)
183
+ else:
184
+ snapshot = repo_cognition.index(force=False)
185
+
186
+ num_files = len(snapshot.files)
187
+ num_symbols = len(snapshot.symbols)
188
+ git_branch = snapshot.summary.get("git", {}).get("active_branch", "untracked")
189
+
190
+ if cli_context.json_mode:
191
+ import json
192
+ print(json.dumps({
193
+ "workspace_root": str(path),
194
+ "velune_cache": str(velune_dir),
195
+ "indexed_files_count": num_files,
196
+ "indexed_symbols_count": num_symbols,
197
+ "git_branch": git_branch,
198
+ "status": "Active & Fully Primed"
199
+ }))
200
+ else:
201
+ console.print(
202
+ Panel(
203
+ Text.assemble(
204
+ (f"[bold]Workspace root:[/bold] {path}\n"),
205
+ (f"[bold]Velune cache:[/bold] {velune_dir}\n"),
206
+ (f"[bold]Indexed files count:[/bold] {num_files}\n"),
207
+ (f"[bold]Indexed symbols count:[/bold] {num_symbols}\n"),
208
+ (f"[bold]Git branch:[/bold] [magenta]{git_branch}[/magenta]\n"),
209
+ ("[bold]Status:[/bold] [bold green]Active & Fully Primed[/bold green]")
210
+ ),
211
+ border_style="cyan",
212
+ box=ROUNDED,
213
+ title="[bold cyan]Workspace Status[/bold cyan]"
214
+ )
215
+ )
216
+
217
+ await lifecycle.shutdown()
velune/cli/context.py ADDED
@@ -0,0 +1,37 @@
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
37
+