velune-cli 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""velune init — workspace initialisation command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
app_init = typer.Typer()
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app_init.command("init")
|
|
17
|
+
def init_command(
|
|
18
|
+
path: Path = typer.Argument(
|
|
19
|
+
default=None,
|
|
20
|
+
help="Path to project root (defaults to current directory)",
|
|
21
|
+
),
|
|
22
|
+
provider: str = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--provider",
|
|
25
|
+
"-p",
|
|
26
|
+
help="Default provider: ollama | groq | openai | anthropic",
|
|
27
|
+
),
|
|
28
|
+
skip_hardware: bool = typer.Option(
|
|
29
|
+
False,
|
|
30
|
+
"--skip-hardware-check",
|
|
31
|
+
help="Skip hardware detection",
|
|
32
|
+
),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize Velune in a project.
|
|
35
|
+
|
|
36
|
+
Creates .velune/ config, detects hardware, checks providers,
|
|
37
|
+
and prepares the workspace for first use.
|
|
38
|
+
"""
|
|
39
|
+
workspace = (path or Path.cwd()).resolve()
|
|
40
|
+
|
|
41
|
+
console.print(
|
|
42
|
+
Panel(
|
|
43
|
+
f"[bold cyan]Velune Init[/bold cyan]\n[dim]Setting up workspace: {workspace}[/dim]",
|
|
44
|
+
border_style="cyan",
|
|
45
|
+
padding=(0, 1),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not skip_hardware:
|
|
50
|
+
_run_hardware_check()
|
|
51
|
+
|
|
52
|
+
# .velune/ directory tree
|
|
53
|
+
velune_dir = workspace / ".velune"
|
|
54
|
+
velune_dir.mkdir(exist_ok=True)
|
|
55
|
+
(velune_dir / "sessions").mkdir(exist_ok=True)
|
|
56
|
+
(velune_dir / "index").mkdir(exist_ok=True)
|
|
57
|
+
console.print("[green]✓[/green] Created .velune/ directory")
|
|
58
|
+
|
|
59
|
+
# Project type detection
|
|
60
|
+
try:
|
|
61
|
+
import json as _json
|
|
62
|
+
|
|
63
|
+
from velune.repository.project_type import ProjectTypeDetector
|
|
64
|
+
|
|
65
|
+
_profile = ProjectTypeDetector().detect(workspace)
|
|
66
|
+
(velune_dir / "project_profile.json").write_text(
|
|
67
|
+
_json.dumps(
|
|
68
|
+
{
|
|
69
|
+
"project_type": _profile.project_type.value,
|
|
70
|
+
"display_name": _profile.display_name,
|
|
71
|
+
"primary_language": _profile.primary_language,
|
|
72
|
+
"detected_frameworks": _profile.detected_frameworks,
|
|
73
|
+
"entry_points": _profile.entry_points,
|
|
74
|
+
"test_directories": _profile.test_directories,
|
|
75
|
+
"config_files": _profile.config_files,
|
|
76
|
+
},
|
|
77
|
+
indent=2,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
console.print(
|
|
81
|
+
f"[green]✓[/green] Detected project type: [cyan]{_profile.display_name}[/cyan]"
|
|
82
|
+
)
|
|
83
|
+
if _profile.detected_frameworks:
|
|
84
|
+
console.print(f" [dim]Frameworks: {', '.join(_profile.detected_frameworks)}[/dim]")
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# .veluneignore
|
|
89
|
+
ignore_file = workspace / ".veluneignore"
|
|
90
|
+
if not ignore_file.exists():
|
|
91
|
+
from velune.repository.scanner import DEFAULT_VELUNEIGNORE
|
|
92
|
+
|
|
93
|
+
ignore_file.write_text(DEFAULT_VELUNEIGNORE)
|
|
94
|
+
console.print("[green]✓[/green] Created .veluneignore")
|
|
95
|
+
|
|
96
|
+
# config.toml
|
|
97
|
+
config_path = velune_dir / "config.toml"
|
|
98
|
+
if not config_path.exists():
|
|
99
|
+
default_provider = provider or _suggest_provider()
|
|
100
|
+
config_content = f"""\
|
|
101
|
+
[project]
|
|
102
|
+
name = "{workspace.name}"
|
|
103
|
+
workspace = "."
|
|
104
|
+
|
|
105
|
+
[providers]
|
|
106
|
+
default_provider = "{default_provider}"
|
|
107
|
+
|
|
108
|
+
[council]
|
|
109
|
+
default_tier = "auto"
|
|
110
|
+
|
|
111
|
+
[memory]
|
|
112
|
+
enabled = true
|
|
113
|
+
"""
|
|
114
|
+
config_path.write_text(config_content)
|
|
115
|
+
console.print(
|
|
116
|
+
f"[green]✓[/green] Created .velune/config.toml (provider: {default_provider})"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# .gitignore
|
|
120
|
+
gitignore = workspace / ".gitignore"
|
|
121
|
+
if gitignore.exists():
|
|
122
|
+
content = gitignore.read_text()
|
|
123
|
+
if ".velune/" not in content:
|
|
124
|
+
with open(gitignore, "a") as f:
|
|
125
|
+
f.write("\n# Velune\n.velune/\n")
|
|
126
|
+
console.print("[green]✓[/green] Added .velune/ to .gitignore")
|
|
127
|
+
|
|
128
|
+
console.print()
|
|
129
|
+
console.print("[bold green]✓ Velune initialized.[/bold green]")
|
|
130
|
+
console.print("[dim]Next steps:[/dim]")
|
|
131
|
+
console.print(" [cyan]velune[/cyan] — start the REPL")
|
|
132
|
+
console.print(" [cyan]velune doctor[/cyan] — verify configuration")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _run_hardware_check() -> None:
|
|
136
|
+
from velune.hardware.detector import HardwareDetector
|
|
137
|
+
|
|
138
|
+
profile = HardwareDetector().detect()
|
|
139
|
+
|
|
140
|
+
table = Table(border_style="dim", padding=(0, 1), show_header=False)
|
|
141
|
+
table.add_column("Key", style="dim", width=20)
|
|
142
|
+
table.add_column("Value", style="white")
|
|
143
|
+
|
|
144
|
+
table.add_row("RAM", f"{profile.total_ram_gb:.0f} GB")
|
|
145
|
+
table.add_row("GPU", profile.gpu_name or "None (CPU only)")
|
|
146
|
+
table.add_row(
|
|
147
|
+
"VRAM",
|
|
148
|
+
f"{profile.vram_total_gb:.0f} GB" if profile.vram_total_gb else "—",
|
|
149
|
+
)
|
|
150
|
+
table.add_row("Tier", f"[cyan]{profile.tier.value}[/cyan]")
|
|
151
|
+
table.add_row("Best local model", profile.recommended_model_size)
|
|
152
|
+
|
|
153
|
+
console.print(table)
|
|
154
|
+
|
|
155
|
+
for w in profile.warnings:
|
|
156
|
+
console.print(f" [yellow]⚠[/yellow] {w}")
|
|
157
|
+
for s in profile.suggestions:
|
|
158
|
+
console.print(f" [dim]→ {s}[/dim]")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _suggest_provider() -> str:
|
|
162
|
+
try:
|
|
163
|
+
import httpx
|
|
164
|
+
|
|
165
|
+
r = httpx.get("http://localhost:11434/api/tags", timeout=2)
|
|
166
|
+
if r.status_code == 200:
|
|
167
|
+
return "ollama"
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
return "groq"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""MCP command - velune mcp-serve and velune mcp connect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from velune.cli.context import CLIContext
|
|
11
|
+
from velune.core.event_loop import submit
|
|
12
|
+
from velune.core.runtime import build_runtime
|
|
13
|
+
from velune.mcp.client import VeluneMCPClient
|
|
14
|
+
from velune.mcp.server import VeluneMCPServer
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
mcp_cmd = typer.Typer(help="Model Context Protocol (MCP) commands")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp_cmd.command("connect")
|
|
21
|
+
def mcp_connect(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
server_url: str = typer.Argument(..., help="SSE URL of the external MCP server"),
|
|
24
|
+
name: str = typer.Argument(..., help="Name of the MCP server"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Connect to an external MCP server and list its tools."""
|
|
27
|
+
console.print(f"[cyan]Connecting to external MCP server '{name}' at {server_url}...[/cyan]")
|
|
28
|
+
|
|
29
|
+
client = VeluneMCPClient(server_url, name)
|
|
30
|
+
|
|
31
|
+
async def _connect_and_list():
|
|
32
|
+
try:
|
|
33
|
+
tools = await client.connect()
|
|
34
|
+
console.print(f"[green]✓ Connected to {name} successfully![/green]")
|
|
35
|
+
console.print(f"[bold]Exposed Tools ({len(tools)}):[/bold]")
|
|
36
|
+
for tool in tools:
|
|
37
|
+
desc = tool.get("description", "No description")
|
|
38
|
+
if len(desc) > 80:
|
|
39
|
+
desc = desc[:77] + "..."
|
|
40
|
+
console.print(f" - [cyan]{name}_{tool['name']}[/cyan]: {desc}")
|
|
41
|
+
except Exception as e:
|
|
42
|
+
console.print(f"[red]Error connecting to MCP server: {e}[/red]")
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
finally:
|
|
45
|
+
await client.disconnect()
|
|
46
|
+
|
|
47
|
+
submit(_connect_and_list())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@mcp_cmd.command("serve")
|
|
51
|
+
def mcp_serve_subcmd(ctx: typer.Context) -> None:
|
|
52
|
+
"""Start Velune as an MCP server for Claude Desktop / VS Code."""
|
|
53
|
+
cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
|
|
54
|
+
workspace = cli_context.workspace if cli_context else Path.cwd()
|
|
55
|
+
config_path = cli_context.config_path if cli_context else None
|
|
56
|
+
|
|
57
|
+
container = build_runtime(workspace, config_path=config_path).container
|
|
58
|
+
tool_registry = container.get("runtime.tool_registry")
|
|
59
|
+
server = VeluneMCPServer(tool_registry)
|
|
60
|
+
|
|
61
|
+
import logging
|
|
62
|
+
|
|
63
|
+
logging.getLogger("velune").setLevel(logging.WARNING)
|
|
64
|
+
|
|
65
|
+
submit(server.run_stdio())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def mcp_serve(ctx: typer.Context) -> None:
|
|
69
|
+
"""Start Velune as an MCP server for Claude Desktop / VS Code."""
|
|
70
|
+
cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
|
|
71
|
+
workspace = cli_context.workspace if cli_context else Path.cwd()
|
|
72
|
+
config_path = cli_context.config_path if cli_context else None
|
|
73
|
+
|
|
74
|
+
container = build_runtime(workspace, config_path=config_path).container
|
|
75
|
+
tool_registry = container.get("runtime.tool_registry")
|
|
76
|
+
server = VeluneMCPServer(tool_registry)
|
|
77
|
+
|
|
78
|
+
import logging
|
|
79
|
+
|
|
80
|
+
logging.getLogger("velune").setLevel(logging.WARNING)
|
|
81
|
+
|
|
82
|
+
submit(server.run_stdio())
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Memory commands — velune memory inspect/stats/clear/compact.
|
|
2
|
+
|
|
3
|
+
Phase 1 Honesty Refactor
|
|
4
|
+
------------------------
|
|
5
|
+
All three commands previously returned hardcoded fake data or performed
|
|
6
|
+
no actual work. They now:
|
|
7
|
+
|
|
8
|
+
* ``inspect`` — reads real records from the episodic SQLite tier.
|
|
9
|
+
Falls back gracefully to an empty result set on cold start (no DB yet).
|
|
10
|
+
|
|
11
|
+
* ``clear`` — actually calls ``delete_session()`` (episodic) or
|
|
12
|
+
``clear()`` (working) to remove data. No longer a no-op.
|
|
13
|
+
|
|
14
|
+
* ``compact`` — emits an honest "not yet implemented" notice instead of
|
|
15
|
+
printing fabricated distillation counters. The command still succeeds
|
|
16
|
+
so callers can script against it safely.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
|
|
24
|
+
from velune.cli.context import CLIContext
|
|
25
|
+
from velune.cli.display.memory_view import MemoryDisplayView
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
memory_cmd = typer.Typer(help="Memory management commands")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@memory_cmd.command("stats")
|
|
32
|
+
def memory_stats(ctx: typer.Context) -> None:
|
|
33
|
+
"""Show visual memory map and registered policy statistics."""
|
|
34
|
+
cli_context = ctx.obj
|
|
35
|
+
if not isinstance(cli_context, CLIContext):
|
|
36
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
37
|
+
|
|
38
|
+
config = cli_context.config
|
|
39
|
+
|
|
40
|
+
# Compile memory statistics block
|
|
41
|
+
stats = {
|
|
42
|
+
"workspace": str(cli_context.workspace),
|
|
43
|
+
"working_memory_ttl": config.memory.working_memory_ttl,
|
|
44
|
+
"episodic_retention_days": config.memory.episodic_retention_days,
|
|
45
|
+
"semantic_threshold": config.memory.semantic_threshold,
|
|
46
|
+
"graph_enabled": config.memory.graph_enabled,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if cli_context.json_mode:
|
|
50
|
+
import json
|
|
51
|
+
|
|
52
|
+
print(json.dumps(stats))
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
display = MemoryDisplayView(console)
|
|
56
|
+
display.render_memory_architecture(stats)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@memory_cmd.command("inspect")
|
|
60
|
+
def memory_inspect(
|
|
61
|
+
ctx: typer.Context,
|
|
62
|
+
tier: str = typer.Option(
|
|
63
|
+
"all", "--tier", "-t", help="Memory tier to inspect (working, episodic, all)"
|
|
64
|
+
),
|
|
65
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Number of records to show"),
|
|
66
|
+
session_id: str = typer.Option(
|
|
67
|
+
"", "--session", "-s", help="Filter by session ID (empty = all)"
|
|
68
|
+
),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Inspect stored records across different memory tiers.
|
|
71
|
+
|
|
72
|
+
Reads *real* data from the episodic SQLite database. If no data has
|
|
73
|
+
been written yet (cold start), an empty table is shown rather than
|
|
74
|
+
fabricated placeholder records.
|
|
75
|
+
"""
|
|
76
|
+
cli_context = ctx.obj
|
|
77
|
+
if not isinstance(cli_context, CLIContext):
|
|
78
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
79
|
+
|
|
80
|
+
from velune.core.event_loop import submit
|
|
81
|
+
|
|
82
|
+
submit(_memory_inspect_async(cli_context, tier, limit, session_id))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _memory_inspect_async(
|
|
86
|
+
cli_context: CLIContext,
|
|
87
|
+
tier: str,
|
|
88
|
+
limit: int,
|
|
89
|
+
session_id: str,
|
|
90
|
+
) -> None:
|
|
91
|
+
container = cli_context.container
|
|
92
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
93
|
+
|
|
94
|
+
await lifecycle.startup()
|
|
95
|
+
|
|
96
|
+
records: list[dict] = []
|
|
97
|
+
|
|
98
|
+
# --- Working memory (always in-process) ---
|
|
99
|
+
if tier.lower() in ("working", "all"):
|
|
100
|
+
try:
|
|
101
|
+
working = container.get("runtime.working_memory")
|
|
102
|
+
if working:
|
|
103
|
+
for turn in working.get_recent_turns(limit=limit):
|
|
104
|
+
if session_id and getattr(turn, "session_id", "") != session_id:
|
|
105
|
+
continue
|
|
106
|
+
records.append(
|
|
107
|
+
{
|
|
108
|
+
"id": f"wrk-{turn.timestamp:.0f}",
|
|
109
|
+
"tier": "working",
|
|
110
|
+
"importance": 0.0, # working tier has no importance scores yet
|
|
111
|
+
"content_preview": (turn.content[:120] + "…")
|
|
112
|
+
if len(turn.content) > 120
|
|
113
|
+
else turn.content,
|
|
114
|
+
"status": turn.role,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
if not cli_context.json_mode:
|
|
119
|
+
console.print(f"[yellow]⚠ Could not read working memory: {exc}[/yellow]")
|
|
120
|
+
|
|
121
|
+
# --- Episodic memory (SQLite) ---
|
|
122
|
+
if tier.lower() in ("episodic", "all"):
|
|
123
|
+
try:
|
|
124
|
+
episodic = container.get("runtime.episodic_memory")
|
|
125
|
+
if episodic:
|
|
126
|
+
# If no session_id filter provided, scan recent turns from a
|
|
127
|
+
# heuristic default session so the table isn't always empty.
|
|
128
|
+
sid = session_id or "default"
|
|
129
|
+
turns = episodic.get_turns(sid)
|
|
130
|
+
for turn in turns[-limit:]:
|
|
131
|
+
records.append(
|
|
132
|
+
{
|
|
133
|
+
"id": f"eps-{turn.id or turn.timestamp:.0f}",
|
|
134
|
+
"tier": "episodic",
|
|
135
|
+
"importance": 0.0,
|
|
136
|
+
"content_preview": (turn.content[:120] + "…")
|
|
137
|
+
if len(turn.content) > 120
|
|
138
|
+
else turn.content,
|
|
139
|
+
"status": turn.role,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
if not cli_context.json_mode:
|
|
144
|
+
console.print(f"[yellow]⚠ Could not read episodic memory: {exc}[/yellow]")
|
|
145
|
+
|
|
146
|
+
if cli_context.json_mode:
|
|
147
|
+
import json
|
|
148
|
+
|
|
149
|
+
print(json.dumps({"records": records[:limit]}))
|
|
150
|
+
else:
|
|
151
|
+
display = MemoryDisplayView(console)
|
|
152
|
+
if records:
|
|
153
|
+
display.render_memory_records_table(records[:limit], tier)
|
|
154
|
+
else:
|
|
155
|
+
console.print(
|
|
156
|
+
"[dim]No memory records found"
|
|
157
|
+
+ (f" for session '{session_id}'" if session_id else "")
|
|
158
|
+
+ ". Run a task first to populate episodic memory.[/dim]"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
await lifecycle.shutdown()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@memory_cmd.command("clear")
|
|
165
|
+
def memory_clear(
|
|
166
|
+
ctx: typer.Context,
|
|
167
|
+
tier: str = typer.Argument(..., help="Memory tier to clear (working, episodic, all)"),
|
|
168
|
+
session_id: str = typer.Option("default", "--session", "-s", help="Session ID to clear"),
|
|
169
|
+
confirm: bool = typer.Option(False, "--confirm", "-y", help="Skip safety prompt"),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Clear memory records of a specific tier.
|
|
172
|
+
|
|
173
|
+
Actually removes data — no longer a no-op.
|
|
174
|
+
"""
|
|
175
|
+
cli_context = ctx.obj
|
|
176
|
+
if not isinstance(cli_context, CLIContext):
|
|
177
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
178
|
+
|
|
179
|
+
if not cli_context.json_mode and not confirm:
|
|
180
|
+
typer.confirm(
|
|
181
|
+
f"Are you sure you want to purge '{tier}' memory tier"
|
|
182
|
+
+ (f" for session '{session_id}'" if session_id else "")
|
|
183
|
+
+ "?",
|
|
184
|
+
abort=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
from velune.core.event_loop import submit
|
|
188
|
+
|
|
189
|
+
submit(_memory_clear_async(cli_context, tier, session_id))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def _memory_clear_async(cli_context: CLIContext, tier: str, session_id: str) -> None:
|
|
193
|
+
container = cli_context.container
|
|
194
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
195
|
+
await lifecycle.startup()
|
|
196
|
+
|
|
197
|
+
cleared: list[str] = []
|
|
198
|
+
errors: list[str] = []
|
|
199
|
+
|
|
200
|
+
if tier.lower() in ("working", "all"):
|
|
201
|
+
try:
|
|
202
|
+
working = container.get("runtime.working_memory")
|
|
203
|
+
if working:
|
|
204
|
+
working.clear()
|
|
205
|
+
cleared.append("working")
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
errors.append(f"working: {exc}")
|
|
208
|
+
|
|
209
|
+
if tier.lower() in ("episodic", "all"):
|
|
210
|
+
try:
|
|
211
|
+
episodic = container.get("runtime.episodic_memory")
|
|
212
|
+
if episodic and session_id:
|
|
213
|
+
episodic.delete_session(session_id)
|
|
214
|
+
cleared.append(f"episodic[session={session_id}]")
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
errors.append(f"episodic: {exc}")
|
|
217
|
+
|
|
218
|
+
await lifecycle.shutdown()
|
|
219
|
+
|
|
220
|
+
if cli_context.json_mode:
|
|
221
|
+
import json
|
|
222
|
+
|
|
223
|
+
print(json.dumps({"success": not errors, "cleared": cleared, "errors": errors}))
|
|
224
|
+
else:
|
|
225
|
+
for c in cleared:
|
|
226
|
+
console.print(f"[green]✓ Cleared {c} memory.[/green]")
|
|
227
|
+
for e in errors:
|
|
228
|
+
console.print(f"[red]✗ Error clearing {e}[/red]")
|
|
229
|
+
if not cleared and not errors:
|
|
230
|
+
console.print("[dim]Nothing to clear.[/dim]")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@memory_cmd.command("compact")
|
|
234
|
+
def memory_compact(ctx: typer.Context) -> None:
|
|
235
|
+
"""Trigger the memory consolidator to compress episodic history into vectors & graph facts.
|
|
236
|
+
|
|
237
|
+
Note: Semantic distillation (vector + graph consolidation) is not yet
|
|
238
|
+
implemented in Phase 1. This command will report honestly instead of
|
|
239
|
+
printing fabricated success counters.
|
|
240
|
+
"""
|
|
241
|
+
cli_context = ctx.obj
|
|
242
|
+
if not isinstance(cli_context, CLIContext):
|
|
243
|
+
raise typer.BadParameter("CLI context was not properly initialized")
|
|
244
|
+
|
|
245
|
+
from velune.core.event_loop import submit
|
|
246
|
+
|
|
247
|
+
submit(_memory_compact_async(cli_context))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def _memory_compact_async(cli_context: CLIContext) -> None:
|
|
251
|
+
container = cli_context.container
|
|
252
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
253
|
+
|
|
254
|
+
await lifecycle.startup()
|
|
255
|
+
|
|
256
|
+
# Compact only what Phase 1 actually implements:
|
|
257
|
+
# flush working memory turns → episodic SQLite.
|
|
258
|
+
# Semantic distillation and graph consolidation are planned for a later phase.
|
|
259
|
+
if not cli_context.json_mode:
|
|
260
|
+
console.print("[bold cyan]⠋[/bold cyan] Flushing working memory to episodic store...")
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
# shutdown() now handles the flush
|
|
264
|
+
await lifecycle.shutdown()
|
|
265
|
+
if not cli_context.json_mode:
|
|
266
|
+
console.print("[green]✓[/green] Working memory flushed to episodic SQLite.")
|
|
267
|
+
console.print(
|
|
268
|
+
"[dim]Note: semantic distillation (vector + graph consolidation) "
|
|
269
|
+
"is planned for a future phase and is not yet active.[/dim]"
|
|
270
|
+
)
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
await lifecycle.shutdown()
|
|
273
|
+
if not cli_context.json_mode:
|
|
274
|
+
console.print(f"[red]✗ Compaction error: {exc}[/red]")
|
|
275
|
+
if cli_context.json_mode:
|
|
276
|
+
import json
|
|
277
|
+
|
|
278
|
+
print(json.dumps({"success": False, "error": str(exc)}))
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
if cli_context.json_mode:
|
|
282
|
+
import json
|
|
283
|
+
|
|
284
|
+
print(
|
|
285
|
+
json.dumps(
|
|
286
|
+
{
|
|
287
|
+
"success": True,
|
|
288
|
+
"message": "Working memory flushed to episodic SQLite. Semantic compaction not yet implemented.",
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
console.print("[bold green]Memory flush complete.[/bold green]")
|