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
velune/mcp/server.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""MCP server exposing Velune's council and memory as remote tools.
|
|
2
|
+
|
|
3
|
+
Serves two transports:
|
|
4
|
+
- stdio: for Claude Desktop and local clients
|
|
5
|
+
- HTTP/SSE on localhost:7777: for VS Code and browser clients
|
|
6
|
+
|
|
7
|
+
Security: Validates workspace_path against allowed_workspaces, read-only by default.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import mcp.server.stdio
|
|
21
|
+
from mcp.server import Server
|
|
22
|
+
from mcp.server.models import InitializationOptions
|
|
23
|
+
from mcp.types import TextContent, Tool
|
|
24
|
+
|
|
25
|
+
from velune.tools.base.registry import ToolRegistry
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from velune.kernel.config import VeluneConfig
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("velune.mcp.server")
|
|
31
|
+
|
|
32
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
33
|
+
DEFAULT_PORT = 7777
|
|
34
|
+
MAX_REQUEST_BYTES = 1 * 1024 * 1024 # 1 MB per request
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RateLimiter:
|
|
38
|
+
"""Token-bucket rate limiter keyed by client ID.
|
|
39
|
+
|
|
40
|
+
Each client starts with a full bucket. Tokens refill at *calls_per_minute*
|
|
41
|
+
tokens per minute. Once the bucket empties, calls are rejected until
|
|
42
|
+
enough time passes to accumulate another token.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, calls_per_minute: int = 60) -> None:
|
|
46
|
+
self._limit = calls_per_minute
|
|
47
|
+
self._tokens: dict[str, float] = {}
|
|
48
|
+
self._last_check: dict[str, float] = {}
|
|
49
|
+
|
|
50
|
+
def is_allowed(self, client_id: str = "default") -> bool:
|
|
51
|
+
now = time.monotonic()
|
|
52
|
+
if client_id not in self._tokens:
|
|
53
|
+
# First call — bucket starts full so the client isn't immediately blocked.
|
|
54
|
+
self._tokens[client_id] = float(self._limit)
|
|
55
|
+
self._last_check[client_id] = now
|
|
56
|
+
elapsed = now - self._last_check[client_id]
|
|
57
|
+
self._last_check[client_id] = now
|
|
58
|
+
self._tokens[client_id] = min(
|
|
59
|
+
float(self._limit),
|
|
60
|
+
self._tokens[client_id] + elapsed * (self._limit / 60.0),
|
|
61
|
+
)
|
|
62
|
+
if self._tokens[client_id] >= 1.0:
|
|
63
|
+
self._tokens[client_id] -= 1.0
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WorkspaceValidator:
|
|
69
|
+
"""Validates workspace paths against allowed list."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self, allowed_workspaces: list[Path] | None = None, current_dir: Path | None = None
|
|
73
|
+
) -> None:
|
|
74
|
+
self.allowed_workspaces = allowed_workspaces or [current_dir or Path.cwd()]
|
|
75
|
+
|
|
76
|
+
def is_valid(self, workspace_path: str) -> bool:
|
|
77
|
+
"""Check if workspace_path is in allowed list."""
|
|
78
|
+
try:
|
|
79
|
+
path = Path(workspace_path).resolve()
|
|
80
|
+
for allowed in self.allowed_workspaces:
|
|
81
|
+
allowed_resolved = Path(allowed).resolve()
|
|
82
|
+
try:
|
|
83
|
+
path.relative_to(allowed_resolved)
|
|
84
|
+
return True
|
|
85
|
+
except ValueError:
|
|
86
|
+
continue
|
|
87
|
+
return False
|
|
88
|
+
except Exception:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def validate(self, workspace_path: str) -> Path:
|
|
92
|
+
"""Validate and return resolved path, or raise ValueError."""
|
|
93
|
+
if not self.is_valid(workspace_path):
|
|
94
|
+
allowed_str = ", ".join(str(p) for p in self.allowed_workspaces)
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"workspace_path '{workspace_path}' not in allowed list: {allowed_str}"
|
|
97
|
+
)
|
|
98
|
+
return Path(workspace_path).resolve()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class VeluneMCPServer:
|
|
102
|
+
"""Exposes Velune's council, memory, and code analysis as MCP tools.
|
|
103
|
+
|
|
104
|
+
Supports two transports:
|
|
105
|
+
- stdio: for Claude Desktop
|
|
106
|
+
- HTTP/SSE: for VS Code and other clients
|
|
107
|
+
|
|
108
|
+
Security: Workspace paths validated against allowed list.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
tool_registry: ToolRegistry | None = None,
|
|
114
|
+
workspace_path: str | Path | None = None,
|
|
115
|
+
allowed_workspaces: list[str | Path] | None = None,
|
|
116
|
+
config: VeluneConfig | None = None,
|
|
117
|
+
calls_per_minute: int = 60,
|
|
118
|
+
):
|
|
119
|
+
self.tool_registry = tool_registry
|
|
120
|
+
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
|
121
|
+
self.allowed_workspaces = [Path(p) for p in (allowed_workspaces or [self.workspace_path])]
|
|
122
|
+
self.config = config
|
|
123
|
+
self.server = Server("velune")
|
|
124
|
+
self._rate_limiter = RateLimiter(calls_per_minute=calls_per_minute)
|
|
125
|
+
self._validator = WorkspaceValidator(self.allowed_workspaces)
|
|
126
|
+
|
|
127
|
+
# Lazy-load Velune components
|
|
128
|
+
self._council_orchestrator = None
|
|
129
|
+
self._memory_manager = None
|
|
130
|
+
self._repository_cognition = None
|
|
131
|
+
|
|
132
|
+
self._register_handlers()
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def council_orchestrator(self):
|
|
136
|
+
"""Lazy-load council orchestrator."""
|
|
137
|
+
if self._council_orchestrator is None:
|
|
138
|
+
try:
|
|
139
|
+
from velune.cognition.council_orchestrator import CouncilOrchestrator
|
|
140
|
+
from velune.models.specializations import ModelSpecializationMapper
|
|
141
|
+
from velune.providers.registry import ProviderRegistry
|
|
142
|
+
|
|
143
|
+
registry = ProviderRegistry()
|
|
144
|
+
mapper = ModelSpecializationMapper()
|
|
145
|
+
self._council_orchestrator = CouncilOrchestrator(
|
|
146
|
+
provider_registry=registry,
|
|
147
|
+
mapper=mapper,
|
|
148
|
+
)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning("Could not load council orchestrator: %s", e)
|
|
151
|
+
return self._council_orchestrator
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def repository_cognition(self):
|
|
155
|
+
"""Lazy-load repository cognition."""
|
|
156
|
+
if self._repository_cognition is None:
|
|
157
|
+
try:
|
|
158
|
+
from velune.kernel.registry import get_container
|
|
159
|
+
|
|
160
|
+
container = get_container()
|
|
161
|
+
if container.has("runtime.repository_cognition"):
|
|
162
|
+
self._repository_cognition = container.get("runtime.repository_cognition")
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
return self._repository_cognition
|
|
166
|
+
|
|
167
|
+
def _register_handlers(self) -> None:
|
|
168
|
+
@self.server.list_tools()
|
|
169
|
+
async def list_tools() -> list[Tool]:
|
|
170
|
+
tools = []
|
|
171
|
+
|
|
172
|
+
# Add Velune-native tools
|
|
173
|
+
tools.extend(
|
|
174
|
+
[
|
|
175
|
+
Tool(
|
|
176
|
+
name="velune_ask",
|
|
177
|
+
description="Ask Velune about your repository",
|
|
178
|
+
inputSchema={
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"prompt": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"description": "Question about the repository",
|
|
184
|
+
},
|
|
185
|
+
"workspace_path": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"description": "Path to repository (optional)",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
"required": ["prompt"],
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
Tool(
|
|
194
|
+
name="velune_search_memory",
|
|
195
|
+
description="Search Velune's memory for relevant past interactions",
|
|
196
|
+
inputSchema={
|
|
197
|
+
"type": "object",
|
|
198
|
+
"properties": {
|
|
199
|
+
"query": {"type": "string"},
|
|
200
|
+
"workspace_path": {"type": "string"},
|
|
201
|
+
"limit": {"type": "integer", "default": 5},
|
|
202
|
+
},
|
|
203
|
+
"required": ["query"],
|
|
204
|
+
},
|
|
205
|
+
),
|
|
206
|
+
Tool(
|
|
207
|
+
name="velune_get_symbols",
|
|
208
|
+
description="Get code symbols (functions, classes) from the repository",
|
|
209
|
+
inputSchema={
|
|
210
|
+
"type": "object",
|
|
211
|
+
"properties": {
|
|
212
|
+
"workspace_path": {"type": "string"},
|
|
213
|
+
"name_pattern": {"type": "string", "description": "Optional regex"},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
),
|
|
217
|
+
Tool(
|
|
218
|
+
name="velune_estimate_blast_radius",
|
|
219
|
+
description="Estimate impact of changing a file on the codebase",
|
|
220
|
+
inputSchema={
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {
|
|
223
|
+
"workspace_path": {"type": "string"},
|
|
224
|
+
"file_path": {"type": "string"},
|
|
225
|
+
},
|
|
226
|
+
"required": ["file_path"],
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Add tools from registry if available
|
|
233
|
+
if self.tool_registry:
|
|
234
|
+
tools.extend(
|
|
235
|
+
[
|
|
236
|
+
Tool(
|
|
237
|
+
name=schema["name"],
|
|
238
|
+
description=schema["description"],
|
|
239
|
+
inputSchema=schema["schema"],
|
|
240
|
+
)
|
|
241
|
+
for schema in self.tool_registry.list_tool_schemas()
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return tools
|
|
246
|
+
|
|
247
|
+
@self.server.call_tool()
|
|
248
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
249
|
+
if not self._rate_limiter.is_allowed():
|
|
250
|
+
raise ValueError("Rate limit exceeded — too many tool calls per minute.")
|
|
251
|
+
|
|
252
|
+
# Handle Velune-native tools
|
|
253
|
+
if name == "velune_ask":
|
|
254
|
+
result = await self._velune_ask(
|
|
255
|
+
arguments.get("prompt", ""),
|
|
256
|
+
arguments.get("workspace_path"),
|
|
257
|
+
)
|
|
258
|
+
elif name == "velune_search_memory":
|
|
259
|
+
result = await self._velune_search_memory(
|
|
260
|
+
arguments.get("query", ""),
|
|
261
|
+
arguments.get("workspace_path"),
|
|
262
|
+
arguments.get("limit", 5),
|
|
263
|
+
)
|
|
264
|
+
elif name == "velune_get_symbols":
|
|
265
|
+
result = await self._velune_get_symbols(
|
|
266
|
+
arguments.get("workspace_path"),
|
|
267
|
+
arguments.get("name_pattern"),
|
|
268
|
+
)
|
|
269
|
+
elif name == "velune_estimate_blast_radius":
|
|
270
|
+
result = await self._velune_estimate_blast_radius(
|
|
271
|
+
arguments.get("workspace_path"),
|
|
272
|
+
arguments.get("file_path"),
|
|
273
|
+
)
|
|
274
|
+
elif self.tool_registry:
|
|
275
|
+
# Fall back to registry
|
|
276
|
+
tool = self.tool_registry.get(name)
|
|
277
|
+
if not tool:
|
|
278
|
+
raise ValueError(f"Tool not found: {name}")
|
|
279
|
+
result = await tool.execute(**arguments)
|
|
280
|
+
else:
|
|
281
|
+
raise ValueError(f"Tool not found: {name}")
|
|
282
|
+
|
|
283
|
+
return [
|
|
284
|
+
TextContent(
|
|
285
|
+
type="text",
|
|
286
|
+
text=json.dumps(result) if isinstance(result, dict) else str(result),
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
# =========================================================================
|
|
291
|
+
# Tool implementations
|
|
292
|
+
# =========================================================================
|
|
293
|
+
|
|
294
|
+
async def _velune_ask(self, prompt: str, workspace_path: str | None = None) -> dict[str, Any]:
|
|
295
|
+
"""Ask Velune about the repository."""
|
|
296
|
+
try:
|
|
297
|
+
workspace = self._validator.validate(workspace_path or str(self.workspace_path))
|
|
298
|
+
except ValueError as e:
|
|
299
|
+
return {"error": str(e)}
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
if not self.council_orchestrator:
|
|
303
|
+
return {"error": "Council not available"}
|
|
304
|
+
|
|
305
|
+
from velune.cognition.budget import CouncilExecutionBudget
|
|
306
|
+
|
|
307
|
+
budget = CouncilExecutionBudget(
|
|
308
|
+
max_wall_time_seconds=30,
|
|
309
|
+
max_review_cycles=1,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
repo_context = "Repository: " + str(workspace)
|
|
313
|
+
state = await self.council_orchestrator.run(
|
|
314
|
+
task=prompt,
|
|
315
|
+
retrieved_context=repo_context,
|
|
316
|
+
budget=budget,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
response = (
|
|
320
|
+
state.pending_diffs[0].get("proposed", "")
|
|
321
|
+
if state.pending_diffs
|
|
322
|
+
else state.final_output or "No response"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return {"response": response, "model": "velune-council"}
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error("velune_ask failed: %s", e)
|
|
328
|
+
return {"error": str(e)}
|
|
329
|
+
|
|
330
|
+
async def _velune_search_memory(
|
|
331
|
+
self,
|
|
332
|
+
query: str,
|
|
333
|
+
workspace_path: str | None = None,
|
|
334
|
+
limit: int = 5,
|
|
335
|
+
) -> dict[str, Any]:
|
|
336
|
+
"""Search memory for relevant interactions."""
|
|
337
|
+
try:
|
|
338
|
+
self._validator.validate(workspace_path or str(self.workspace_path))
|
|
339
|
+
except ValueError as e:
|
|
340
|
+
return {"error": str(e), "results": []}
|
|
341
|
+
|
|
342
|
+
# TODO: Integrate with actual memory manager
|
|
343
|
+
return {"results": []}
|
|
344
|
+
|
|
345
|
+
async def _velune_get_symbols(
|
|
346
|
+
self,
|
|
347
|
+
workspace_path: str | None = None,
|
|
348
|
+
name_pattern: str | None = None,
|
|
349
|
+
) -> dict[str, Any]:
|
|
350
|
+
"""Get code symbols from repository."""
|
|
351
|
+
try:
|
|
352
|
+
self._validator.validate(workspace_path or str(self.workspace_path))
|
|
353
|
+
except ValueError as e:
|
|
354
|
+
return {"error": str(e), "symbols": []}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
if not self.repository_cognition:
|
|
358
|
+
return {"symbols": []}
|
|
359
|
+
|
|
360
|
+
snapshot = self.repository_cognition.index(force=False)
|
|
361
|
+
if not snapshot:
|
|
362
|
+
return {"symbols": []}
|
|
363
|
+
|
|
364
|
+
symbols = []
|
|
365
|
+
pattern = re.compile(name_pattern) if name_pattern else None
|
|
366
|
+
|
|
367
|
+
for file in snapshot.files:
|
|
368
|
+
if file.language.value not in ("python", "typescript", "javascript"):
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
content = Path(file.path).read_text(errors="ignore")
|
|
373
|
+
for i, line in enumerate(content.split("\n"), 1):
|
|
374
|
+
if match := re.match(r"^\s*(async\s+)?(def|class)\s+(\w+)", line):
|
|
375
|
+
kind = "function" if "def" in match.group(2) else "class"
|
|
376
|
+
name = match.group(3)
|
|
377
|
+
if pattern and not pattern.search(name):
|
|
378
|
+
continue
|
|
379
|
+
symbols.append(
|
|
380
|
+
{
|
|
381
|
+
"name": name,
|
|
382
|
+
"kind": kind,
|
|
383
|
+
"file": str(file.path),
|
|
384
|
+
"line": i,
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
except Exception:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
return {"symbols": symbols[:100]}
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error("velune_get_symbols failed: %s", e)
|
|
393
|
+
return {"error": str(e), "symbols": []}
|
|
394
|
+
|
|
395
|
+
async def _velune_estimate_blast_radius(
|
|
396
|
+
self,
|
|
397
|
+
workspace_path: str | None = None,
|
|
398
|
+
file_path: str | None = None,
|
|
399
|
+
) -> dict[str, Any]:
|
|
400
|
+
"""Estimate impact of changing a file."""
|
|
401
|
+
try:
|
|
402
|
+
self._validator.validate(workspace_path or str(self.workspace_path))
|
|
403
|
+
except ValueError as e:
|
|
404
|
+
return {"error": str(e)}
|
|
405
|
+
|
|
406
|
+
if not file_path:
|
|
407
|
+
return {"error": "file_path required"}
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
if not self.repository_cognition:
|
|
411
|
+
return {"score": 0.5, "fan_in": 0, "fan_out": 0}
|
|
412
|
+
|
|
413
|
+
grapher = self.repository_cognition.grapher
|
|
414
|
+
rel_file = grapher._to_rel_path(file_path)
|
|
415
|
+
|
|
416
|
+
if rel_file not in grapher.graph:
|
|
417
|
+
return {"score": 0.2, "fan_in": 0, "fan_out": 0}
|
|
418
|
+
|
|
419
|
+
dependents = len(grapher.get_dependents(rel_file))
|
|
420
|
+
dependencies = len(grapher.get_dependencies(rel_file))
|
|
421
|
+
|
|
422
|
+
import math
|
|
423
|
+
|
|
424
|
+
raw_score = 1.0 * dependents + 0.5 * dependencies
|
|
425
|
+
score = 0.1 + 0.8 * (1.0 - math.exp(-raw_score / 5.0))
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
"score": round(score, 3),
|
|
429
|
+
"fan_in": dependencies,
|
|
430
|
+
"fan_out": dependents,
|
|
431
|
+
}
|
|
432
|
+
except Exception as e:
|
|
433
|
+
logger.error("velune_estimate_blast_radius failed: %s", e)
|
|
434
|
+
return {"error": str(e)}
|
|
435
|
+
|
|
436
|
+
def get_tools_list(self) -> list[dict[str, Any]]:
|
|
437
|
+
"""Return a list of all registered tools with their schemas (for testing/APIs)."""
|
|
438
|
+
tools = [
|
|
439
|
+
{
|
|
440
|
+
"name": "velune_ask",
|
|
441
|
+
"description": "Ask Velune about your repository",
|
|
442
|
+
"inputSchema": {
|
|
443
|
+
"type": "object",
|
|
444
|
+
"properties": {
|
|
445
|
+
"prompt": {
|
|
446
|
+
"type": "string",
|
|
447
|
+
"description": "Question about the repository",
|
|
448
|
+
},
|
|
449
|
+
"workspace_path": {
|
|
450
|
+
"type": "string",
|
|
451
|
+
"description": "Path to repository (optional)",
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
"required": ["prompt"],
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"name": "velune_search_memory",
|
|
459
|
+
"description": "Search Velune's memory for relevant past interactions",
|
|
460
|
+
"inputSchema": {
|
|
461
|
+
"type": "object",
|
|
462
|
+
"properties": {
|
|
463
|
+
"query": {"type": "string"},
|
|
464
|
+
"workspace_path": {"type": "string"},
|
|
465
|
+
"limit": {"type": "integer", "default": 5},
|
|
466
|
+
},
|
|
467
|
+
"required": ["query"],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
"name": "velune_get_symbols",
|
|
472
|
+
"description": "Get code symbols (functions, classes) from the repository",
|
|
473
|
+
"inputSchema": {
|
|
474
|
+
"type": "object",
|
|
475
|
+
"properties": {
|
|
476
|
+
"workspace_path": {"type": "string"},
|
|
477
|
+
"name_pattern": {"type": "string", "description": "Optional regex"},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
"name": "velune_estimate_blast_radius",
|
|
483
|
+
"description": "Estimate impact of changing a file on the codebase",
|
|
484
|
+
"inputSchema": {
|
|
485
|
+
"type": "object",
|
|
486
|
+
"properties": {
|
|
487
|
+
"workspace_path": {"type": "string"},
|
|
488
|
+
"file_path": {"type": "string"},
|
|
489
|
+
},
|
|
490
|
+
"required": ["file_path"],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
]
|
|
494
|
+
if self.tool_registry:
|
|
495
|
+
for schema in self.tool_registry.list_tool_schemas():
|
|
496
|
+
tools.append(
|
|
497
|
+
{
|
|
498
|
+
"name": schema["name"],
|
|
499
|
+
"description": schema["description"],
|
|
500
|
+
"inputSchema": schema["schema"],
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
return tools
|
|
504
|
+
|
|
505
|
+
async def handle_json_rpc_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
506
|
+
"""Process a JSON-RPC request (for testing/APIs)."""
|
|
507
|
+
method = request.get("method")
|
|
508
|
+
req_id = request.get("id")
|
|
509
|
+
params = request.get("params", {})
|
|
510
|
+
|
|
511
|
+
if not self._rate_limiter.is_allowed():
|
|
512
|
+
return {
|
|
513
|
+
"jsonrpc": "2.0",
|
|
514
|
+
"id": req_id,
|
|
515
|
+
"error": {
|
|
516
|
+
"code": -32000,
|
|
517
|
+
"message": "Rate limit exceeded — too many tool calls per minute.",
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if method == "velune_ask":
|
|
522
|
+
res = await self._velune_ask(
|
|
523
|
+
params.get("prompt", ""),
|
|
524
|
+
params.get("workspace_path"),
|
|
525
|
+
)
|
|
526
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": res}
|
|
527
|
+
elif method == "velune_search_memory":
|
|
528
|
+
res = await self._velune_search_memory(
|
|
529
|
+
params.get("query", ""),
|
|
530
|
+
params.get("workspace_path"),
|
|
531
|
+
params.get("limit", 5),
|
|
532
|
+
)
|
|
533
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": res}
|
|
534
|
+
elif method == "velune_get_symbols":
|
|
535
|
+
res = await self._velune_get_symbols(
|
|
536
|
+
params.get("workspace_path"),
|
|
537
|
+
params.get("name_pattern"),
|
|
538
|
+
)
|
|
539
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": res}
|
|
540
|
+
elif method == "velune_estimate_blast_radius":
|
|
541
|
+
res = await self._velune_estimate_blast_radius(
|
|
542
|
+
params.get("workspace_path"),
|
|
543
|
+
params.get("file_path"),
|
|
544
|
+
)
|
|
545
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": res}
|
|
546
|
+
elif self.tool_registry and self.tool_registry.get(method):
|
|
547
|
+
tool = self.tool_registry.get(method)
|
|
548
|
+
try:
|
|
549
|
+
res = await tool.execute(**params)
|
|
550
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": res}
|
|
551
|
+
except Exception as e:
|
|
552
|
+
return {
|
|
553
|
+
"jsonrpc": "2.0",
|
|
554
|
+
"id": req_id,
|
|
555
|
+
"error": {
|
|
556
|
+
"code": -32603,
|
|
557
|
+
"message": str(e),
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
else:
|
|
561
|
+
return {
|
|
562
|
+
"jsonrpc": "2.0",
|
|
563
|
+
"id": req_id,
|
|
564
|
+
"error": {
|
|
565
|
+
"code": -32601,
|
|
566
|
+
"message": f"Method not found: {method}",
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async def run_stdio(self) -> None:
|
|
571
|
+
"""Run MCP server over stdio (for Claude Desktop)."""
|
|
572
|
+
logger.info("Starting Velune MCP server (stdio)")
|
|
573
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
574
|
+
await self.server.run(
|
|
575
|
+
read_stream,
|
|
576
|
+
write_stream,
|
|
577
|
+
InitializationOptions(
|
|
578
|
+
server_name="velune", server_version="0.1.0", capabilities={"tools": {}}
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
async def run_http(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
|
|
583
|
+
"""Run MCP server over HTTP/SSE (for VS Code, browsers)."""
|
|
584
|
+
logger.info(f"Starting Velune MCP server (HTTP on {host}:{port})")
|
|
585
|
+
try:
|
|
586
|
+
from aiohttp import web
|
|
587
|
+
|
|
588
|
+
async def handle_sse(request):
|
|
589
|
+
"""Handle SSE transport."""
|
|
590
|
+
response = web.StreamResponse()
|
|
591
|
+
response.headers["Content-Type"] = "text/event-stream"
|
|
592
|
+
response.headers["Cache-Control"] = "no-cache"
|
|
593
|
+
response.headers["Connection"] = "keep-alive"
|
|
594
|
+
await response.prepare(request)
|
|
595
|
+
|
|
596
|
+
# Simplified: echo back a message
|
|
597
|
+
msg = json.dumps({"result": "Velune MCP Server ready"})
|
|
598
|
+
await response.write(f"data: {msg}\n\n".encode())
|
|
599
|
+
await response.write_eof()
|
|
600
|
+
return response
|
|
601
|
+
|
|
602
|
+
async def handle_tools(request):
|
|
603
|
+
"""Handle tool listing."""
|
|
604
|
+
tools = await self.server.list_tools()
|
|
605
|
+
return web.json_response(
|
|
606
|
+
[{"name": t.name, "description": t.description} for t in tools]
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
app = web.Application()
|
|
610
|
+
app.router.add_get("/sse", handle_sse)
|
|
611
|
+
app.router.add_get("/tools", handle_tools)
|
|
612
|
+
|
|
613
|
+
runner = web.AppRunner(app)
|
|
614
|
+
await runner.setup()
|
|
615
|
+
site = web.TCPSite(runner, host, port)
|
|
616
|
+
await site.start()
|
|
617
|
+
|
|
618
|
+
logger.info(f"Velune MCP Server listening on http://{host}:{port}")
|
|
619
|
+
# Keep running until interrupted
|
|
620
|
+
await asyncio.sleep(3600 * 24)
|
|
621
|
+
except ImportError:
|
|
622
|
+
logger.error("aiohttp not installed for HTTP transport")
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error(f"HTTP server failed: {e}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Hierarchical Memory Subsystem for Velune Cognitive OS.
|
|
2
|
+
|
|
3
|
+
Includes working (Tier 1), episodic (Tier 2), semantic (Tier 3), graph (Tier 4)
|
|
4
|
+
and lineage memory systems along with prioritized lifecycle managers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from velune.memory.lifecycle import MemoryLifecycleCoordinator, MemoryLifecycleManager
|
|
8
|
+
from velune.memory.prioritizer import MemoryPrioritizer
|
|
9
|
+
from velune.memory.tiers.episodic import EpisodicMemoryTier, EpisodicStep, EpisodicTurn
|
|
10
|
+
from velune.memory.tiers.graph import GraphEdge, GraphMemoryTier, GraphNode
|
|
11
|
+
from velune.memory.tiers.lineage import LineageMemoryTier
|
|
12
|
+
from velune.memory.tiers.semantic import SemanticMemoryTier
|
|
13
|
+
from velune.memory.tiers.working import MemoryTurn, WorkingMemoryTier
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"WorkingMemoryTier",
|
|
17
|
+
"MemoryTurn",
|
|
18
|
+
"EpisodicMemoryTier",
|
|
19
|
+
"EpisodicTurn",
|
|
20
|
+
"EpisodicStep",
|
|
21
|
+
"SemanticMemoryTier",
|
|
22
|
+
"GraphMemoryTier",
|
|
23
|
+
"GraphNode",
|
|
24
|
+
"GraphEdge",
|
|
25
|
+
"LineageMemoryTier",
|
|
26
|
+
"MemoryPrioritizer",
|
|
27
|
+
# Lifecycle managers — MemoryLifecycleCoordinator is the simple coordinator;
|
|
28
|
+
# MemoryLifecycleManager is the full Phase 2a production class with multi-tier
|
|
29
|
+
# retrieval, vitality-based filtering, and health reporting.
|
|
30
|
+
"MemoryLifecycleCoordinator",
|
|
31
|
+
"MemoryLifecycleManager",
|
|
32
|
+
]
|