velune-cli 0.9.0__py3-none-any.whl

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