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
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from velune.execution.path_guard import PathGuard
6
+ from velune.tools.base.tool import BaseTool
7
+
8
+
9
+ class SemanticCodeSearch(BaseTool):
10
+ """Tool for semantic code search."""
11
+
12
+ def __init__(self, workspace: Path | None = None) -> None:
13
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
14
+
15
+ def get_name(self) -> str:
16
+ return "semantic_code_search"
17
+
18
+ def get_description(self) -> str:
19
+ return "Search code semantically"
20
+
21
+ async def execute(
22
+ self,
23
+ query: str,
24
+ directory: str = ".",
25
+ limit: int = 10,
26
+ ) -> list[dict]:
27
+ """Search code semantically."""
28
+ from velune.tools.filesystem.search import GrepFiles
29
+
30
+ grep = GrepFiles(workspace=self.workspace)
31
+ results = await grep.execute(pattern=query, directory=directory)
32
+
33
+ return results[:limit]
34
+
35
+ def get_schema(self) -> dict:
36
+ return {
37
+ "type": "object",
38
+ "properties": {
39
+ "query": {
40
+ "type": "string",
41
+ "description": "Search query",
42
+ },
43
+ "directory": {
44
+ "type": "string",
45
+ "description": "Directory to search in",
46
+ },
47
+ "limit": {
48
+ "type": "integer",
49
+ "description": "Number of results to return",
50
+ },
51
+ },
52
+ "required": ["query"],
53
+ }
54
+
55
+
56
+ class SymbolSearch(BaseTool):
57
+ """Tool for searching symbols."""
58
+
59
+ def __init__(self, workspace: Path | None = None) -> None:
60
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
61
+
62
+ def get_name(self) -> str:
63
+ return "symbol_search"
64
+
65
+ def get_description(self) -> str:
66
+ return "Search for symbols in code"
67
+
68
+ async def execute(
69
+ self,
70
+ symbol_name: str,
71
+ directory: str = ".",
72
+ ) -> list[dict]:
73
+ """Search for symbols."""
74
+ from velune.repository.parser import ASTParser
75
+ from velune.repository.scanner import FilesystemScanner
76
+
77
+ guard = PathGuard(self.workspace)
78
+ root_path = guard.validate(directory)
79
+
80
+ scanner = FilesystemScanner(root_path)
81
+ files = scanner.scan([".py"])
82
+
83
+ parser = ASTParser()
84
+
85
+ results = []
86
+ for file_path in files:
87
+ try:
88
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
89
+ code = f.read()
90
+ except Exception:
91
+ continue
92
+
93
+ symbols, _ = parser.parse(file_path, code)
94
+ for symbol in symbols:
95
+ if symbol.name == symbol_name:
96
+ results.append(
97
+ {
98
+ "name": symbol.name,
99
+ "kind": symbol.kind.value
100
+ if hasattr(symbol.kind, "value")
101
+ else symbol.kind,
102
+ "file": str(file_path),
103
+ "line": symbol.line_start,
104
+ }
105
+ )
106
+
107
+ return results
108
+
109
+ def get_schema(self) -> dict:
110
+ return {
111
+ "type": "object",
112
+ "properties": {
113
+ "symbol_name": {
114
+ "type": "string",
115
+ "description": "Symbol name to search for",
116
+ },
117
+ "directory": {
118
+ "type": "string",
119
+ "description": "Directory to search in",
120
+ },
121
+ },
122
+ "required": ["symbol_name"],
123
+ }
@@ -0,0 +1,75 @@
1
+ """Filesystem read tools."""
2
+
3
+ from pathlib import Path
4
+
5
+ from velune.execution.path_guard import validate_workspace_path
6
+ from velune.tools.base.tool import BaseTool
7
+
8
+
9
+ class ReadFile(BaseTool):
10
+ """Tool for reading file contents."""
11
+
12
+ def __init__(self, workspace: Path | None = None) -> None:
13
+ self.workspace = workspace or Path.cwd()
14
+
15
+ def get_name(self) -> str:
16
+ return "read_file"
17
+
18
+ def get_description(self) -> str:
19
+ return "Read the contents of a file"
20
+
21
+ async def execute(self, file_path: str) -> str:
22
+ """Read file contents."""
23
+ path = Path(file_path)
24
+ validate_workspace_path(path, self.workspace, label="ReadFile")
25
+ if not path.exists():
26
+ raise FileNotFoundError(f"File not found: {file_path}")
27
+
28
+ with open(path, encoding="utf-8") as f:
29
+ return f.read()
30
+
31
+ def get_schema(self) -> dict:
32
+ return {
33
+ "type": "object",
34
+ "properties": {
35
+ "file_path": {
36
+ "type": "string",
37
+ "description": "Path to the file to read",
38
+ }
39
+ },
40
+ "required": ["file_path"],
41
+ }
42
+
43
+
44
+ class ReadDirectory(BaseTool):
45
+ """Tool for reading directory contents."""
46
+
47
+ def __init__(self, workspace: Path | None = None) -> None:
48
+ self.workspace = workspace or Path.cwd()
49
+
50
+ def get_name(self) -> str:
51
+ return "read_directory"
52
+
53
+ def get_description(self) -> str:
54
+ return "List the contents of a directory"
55
+
56
+ async def execute(self, directory_path: str) -> list[str]:
57
+ """List directory contents."""
58
+ path = Path(directory_path)
59
+ validate_workspace_path(path, self.workspace, label="ReadDirectory")
60
+ if not path.exists() or not path.is_dir():
61
+ raise NotADirectoryError(f"Directory not found: {directory_path}")
62
+
63
+ return [item.name for item in path.iterdir()]
64
+
65
+ def get_schema(self) -> dict:
66
+ return {
67
+ "type": "object",
68
+ "properties": {
69
+ "directory_path": {
70
+ "type": "string",
71
+ "description": "Path to the directory to list",
72
+ }
73
+ },
74
+ "required": ["directory_path"],
75
+ }
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from velune.execution.path_guard import PathGuard
6
+ from velune.tools.base.tool import BaseTool
7
+
8
+
9
+ class GrepFiles(BaseTool):
10
+ """Tool for searching file contents."""
11
+
12
+ def __init__(self, workspace: Path | None = None) -> None:
13
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
14
+
15
+ def get_name(self) -> str:
16
+ return "grep_files"
17
+
18
+ def get_description(self) -> str:
19
+ return "Search for text in files"
20
+
21
+ async def execute(
22
+ self,
23
+ pattern: str,
24
+ directory: str = ".",
25
+ file_pattern: str = "*",
26
+ ) -> list[dict]:
27
+ """Search for pattern in files."""
28
+ import re
29
+
30
+ from velune.repository.scanner import FilesystemScanner
31
+
32
+ guard = PathGuard(self.workspace)
33
+ root_path = guard.validate(directory)
34
+
35
+ scanner = FilesystemScanner(root_path)
36
+
37
+ extensions = None
38
+ if file_pattern and file_pattern != "*":
39
+ if file_pattern.startswith("*.") and not any(
40
+ c in file_pattern[2:] for c in ["*", "?", "[", "]"]
41
+ ):
42
+ extensions = [file_pattern[1:]]
43
+
44
+ files = scanner.scan(extensions)
45
+
46
+ results = []
47
+ regex = re.compile(pattern, re.IGNORECASE)
48
+
49
+ for file_path in files:
50
+ if file_pattern and not file_path.match(file_pattern):
51
+ continue
52
+
53
+ try:
54
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
55
+ content = f.read()
56
+
57
+ if re.search(regex, content):
58
+ for match in regex.finditer(content):
59
+ results.append(
60
+ {
61
+ "file": str(file_path),
62
+ "match": match.group(),
63
+ "line": content[: match.start()].count("\n") + 1,
64
+ }
65
+ )
66
+ except Exception:
67
+ pass
68
+
69
+ return results
70
+
71
+ def get_schema(self) -> dict:
72
+ return {
73
+ "type": "object",
74
+ "properties": {
75
+ "pattern": {
76
+ "type": "string",
77
+ "description": "Pattern to search for",
78
+ },
79
+ "directory": {
80
+ "type": "string",
81
+ "description": "Directory to search in",
82
+ },
83
+ "file_pattern": {
84
+ "type": "string",
85
+ "description": "File pattern to match",
86
+ },
87
+ },
88
+ "required": ["pattern"],
89
+ }
90
+
91
+
92
+ class FindFiles(BaseTool):
93
+ """Tool for finding files by name."""
94
+
95
+ def __init__(self, workspace: Path | None = None) -> None:
96
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
97
+
98
+ def get_name(self) -> str:
99
+ return "find_files"
100
+
101
+ def get_description(self) -> str:
102
+ return "Find files by name pattern"
103
+
104
+ async def execute(
105
+ self,
106
+ pattern: str,
107
+ directory: str = ".",
108
+ ) -> list[str]:
109
+ """Find files by pattern."""
110
+ import fnmatch
111
+
112
+ guard = PathGuard(self.workspace)
113
+ root_path = guard.validate(directory)
114
+
115
+ matches = []
116
+ for file_path in root_path.rglob("*"):
117
+ if file_path.is_file() and fnmatch.fnmatch(file_path.name, pattern):
118
+ matches.append(str(file_path))
119
+
120
+ return matches
121
+
122
+ def get_schema(self) -> dict:
123
+ return {
124
+ "type": "object",
125
+ "properties": {
126
+ "pattern": {
127
+ "type": "string",
128
+ "description": "File name pattern to match",
129
+ },
130
+ "directory": {
131
+ "type": "string",
132
+ "description": "Directory to search in",
133
+ },
134
+ },
135
+ "required": ["pattern"],
136
+ }
@@ -0,0 +1,163 @@
1
+ """Filesystem write tools — all writes pass through DiffPreview for user
2
+ approval before touching disk."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+
10
+ from velune.execution.path_guard import validate_workspace_path
11
+ from velune.tools.base.tool import BaseTool
12
+
13
+
14
+ class WriteFile(BaseTool):
15
+ """Write content to a file, showing a diff preview first."""
16
+
17
+ def __init__(
18
+ self,
19
+ workspace: Path | None = None,
20
+ console: Console | None = None,
21
+ ) -> None:
22
+ self.workspace = workspace or Path.cwd()
23
+ self.console = console or Console()
24
+
25
+ def get_name(self) -> str:
26
+ return "write_file"
27
+
28
+ def get_description(self) -> str:
29
+ return "Write content to a file (shows diff preview before writing)"
30
+
31
+ async def execute(self, file_path: str, content: str) -> str:
32
+ path = Path(file_path)
33
+ validate_workspace_path(path, self.workspace, label="WriteFile")
34
+ return await self._write_file_with_preview(path, content)
35
+
36
+ async def _write_file_with_preview(
37
+ self,
38
+ path: Path,
39
+ content: str,
40
+ auto_accept: bool = False,
41
+ ) -> str:
42
+ from velune.execution.diff_preview import DiffDecision, DiffPreview
43
+
44
+ preview = DiffPreview(self.console)
45
+ decision = await preview.preview_and_confirm(path, content, auto_accept=auto_accept)
46
+ if decision == DiffDecision.ACCEPT:
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ path.write_text(content, encoding="utf-8")
49
+ self.console.print(f"[green]✓ Written:[/green] {path}")
50
+ return f"Successfully wrote to {path}"
51
+ self.console.print(f"[yellow]Skipped:[/yellow] {path}")
52
+ return f"Skipped (rejected by user): {path}"
53
+
54
+ def get_schema(self) -> dict:
55
+ return {
56
+ "type": "object",
57
+ "properties": {
58
+ "file_path": {
59
+ "type": "string",
60
+ "description": "Path to the file to write",
61
+ },
62
+ "content": {
63
+ "type": "string",
64
+ "description": "Content to write to the file",
65
+ },
66
+ },
67
+ "required": ["file_path", "content"],
68
+ }
69
+
70
+
71
+ class CreateFile(BaseTool):
72
+ """Create an empty file, showing a diff preview first."""
73
+
74
+ def __init__(
75
+ self,
76
+ workspace: Path | None = None,
77
+ console: Console | None = None,
78
+ ) -> None:
79
+ self.workspace = workspace or Path.cwd()
80
+ self.console = console or Console()
81
+
82
+ def get_name(self) -> str:
83
+ return "create_file"
84
+
85
+ def get_description(self) -> str:
86
+ return "Create an empty file (shows preview before creating)"
87
+
88
+ async def execute(self, file_path: str) -> str:
89
+ path = Path(file_path)
90
+ validate_workspace_path(path, self.workspace, label="CreateFile")
91
+
92
+ from velune.execution.diff_preview import DiffDecision, DiffPreview
93
+
94
+ preview = DiffPreview(self.console)
95
+ # Treat create-empty as a write of empty content so the diff shows "NEW FILE"
96
+ decision = await preview.preview_and_confirm(path, "", auto_accept=False)
97
+ if decision == DiffDecision.ACCEPT:
98
+ path.parent.mkdir(parents=True, exist_ok=True)
99
+ path.touch()
100
+ self.console.print(f"[green]✓ Created:[/green] {path}")
101
+ return f"Successfully created {file_path}"
102
+ self.console.print(f"[yellow]Skipped:[/yellow] {path}")
103
+ return f"Skipped (rejected by user): {path}"
104
+
105
+ def get_schema(self) -> dict:
106
+ return {
107
+ "type": "object",
108
+ "properties": {
109
+ "file_path": {
110
+ "type": "string",
111
+ "description": "Path to the file to create",
112
+ },
113
+ },
114
+ "required": ["file_path"],
115
+ }
116
+
117
+
118
+ class DeleteFile(BaseTool):
119
+ """Delete a file, showing a deletion preview first."""
120
+
121
+ def __init__(
122
+ self,
123
+ workspace: Path | None = None,
124
+ console: Console | None = None,
125
+ ) -> None:
126
+ self.workspace = workspace or Path.cwd()
127
+ self.console = console or Console()
128
+
129
+ def get_name(self) -> str:
130
+ return "delete_file"
131
+
132
+ def get_description(self) -> str:
133
+ return "Delete a file (shows preview before deleting)"
134
+
135
+ async def execute(self, file_path: str) -> str:
136
+ path = Path(file_path)
137
+ validate_workspace_path(path, self.workspace, label="DeleteFile")
138
+ if not path.exists():
139
+ raise FileNotFoundError(f"File not found: {file_path}")
140
+
141
+ from velune.execution.diff_preview import DiffDecision, DiffPreview
142
+
143
+ # proposed="" marks this as a deletion in FileDiff
144
+ preview = DiffPreview(self.console)
145
+ decision = await preview.preview_and_confirm(path, "", auto_accept=False)
146
+ if decision == DiffDecision.ACCEPT:
147
+ path.unlink()
148
+ self.console.print(f"[red]✓ Deleted:[/red] {path}")
149
+ return f"Successfully deleted {file_path}"
150
+ self.console.print(f"[yellow]Skipped:[/yellow] {path}")
151
+ return f"Skipped (rejected by user): {path}"
152
+
153
+ def get_schema(self) -> dict:
154
+ return {
155
+ "type": "object",
156
+ "properties": {
157
+ "file_path": {
158
+ "type": "string",
159
+ "description": "Path to the file to delete",
160
+ },
161
+ },
162
+ "required": ["file_path"],
163
+ }
@@ -0,0 +1,177 @@
1
+ """Git history tools — GitLog, GitDiff, GitBlame.
2
+
3
+ All three tools use gitpython (``import git``) rather than raw subprocess so
4
+ that:
5
+ - No shell is ever invoked.
6
+ - File-path arguments are validated by ``PathGuard`` before being passed to
7
+ git, preventing path-traversal escapes.
8
+ - The ``--`` path separator is always used in commands that accept file paths,
9
+ preventing user-supplied names from being misinterpreted as git flags or
10
+ refs.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ from pathlib import Path
17
+
18
+ from velune.execution.path_guard import PathGuard
19
+ from velune.tools.base.tool import BaseTool
20
+
21
+
22
+ def _open_repo(path: Path): # type: ignore[return]
23
+ """Open a gitpython Repo, searching parent directories for .git."""
24
+ try:
25
+ import git
26
+
27
+ return git.Repo(str(path), search_parent_directories=True)
28
+ except Exception as exc:
29
+ raise ValueError(f"Not a git repository: {path}") from exc
30
+
31
+
32
+ class GitLog(BaseTool):
33
+ """Tool for viewing git commit history."""
34
+
35
+ def __init__(self, workspace: Path | None = None) -> None:
36
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
37
+
38
+ def get_name(self) -> str:
39
+ return "git_log"
40
+
41
+ def get_description(self) -> str:
42
+ return "View git commit history"
43
+
44
+ async def execute(
45
+ self,
46
+ directory: str = ".",
47
+ limit: int = 10,
48
+ ) -> list[dict]:
49
+ guard = PathGuard(self.workspace)
50
+ safe_root = guard.validate(directory)
51
+ repo = _open_repo(safe_root)
52
+
53
+ def _fetch() -> list[dict]:
54
+ commits = []
55
+ for commit in repo.iter_commits(max_count=max(1, int(limit))):
56
+ commits.append(
57
+ {
58
+ "hash": commit.hexsha,
59
+ "author": commit.author.name,
60
+ "date": commit.authored_datetime.isoformat(),
61
+ "message": commit.message.strip(),
62
+ }
63
+ )
64
+ return commits
65
+
66
+ return await asyncio.to_thread(_fetch)
67
+
68
+ def get_schema(self) -> dict:
69
+ return {
70
+ "type": "object",
71
+ "properties": {
72
+ "directory": {"type": "string", "description": "Git repository directory"},
73
+ "limit": {"type": "integer", "description": "Number of commits to show"},
74
+ },
75
+ }
76
+
77
+
78
+ class GitDiff(BaseTool):
79
+ """Tool for viewing git diff."""
80
+
81
+ def __init__(self, workspace: Path | None = None) -> None:
82
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
83
+
84
+ def get_name(self) -> str:
85
+ return "git_diff"
86
+
87
+ def get_description(self) -> str:
88
+ return "View git diff"
89
+
90
+ async def execute(
91
+ self,
92
+ directory: str = ".",
93
+ file_path: str | None = None,
94
+ ) -> str:
95
+ guard = PathGuard(self.workspace)
96
+ safe_root = guard.validate(directory)
97
+ repo = _open_repo(safe_root)
98
+
99
+ def _fetch() -> str:
100
+ if file_path:
101
+ # Resolve and validate the file path, then make it relative to
102
+ # the repo working directory so git can locate it.
103
+ raw = Path(file_path)
104
+ if not raw.is_absolute():
105
+ raw = safe_root / raw
106
+ safe_file = guard.validate(raw)
107
+ rel = str(safe_file.relative_to(Path(repo.working_dir).resolve()))
108
+ # "--" prevents any path from being interpreted as a git ref.
109
+ return repo.git.diff("--", rel)
110
+ return repo.git.diff()
111
+
112
+ return await asyncio.to_thread(_fetch)
113
+
114
+ def get_schema(self) -> dict:
115
+ return {
116
+ "type": "object",
117
+ "properties": {
118
+ "directory": {"type": "string", "description": "Git repository directory"},
119
+ "file_path": {"type": "string", "description": "Specific file to diff"},
120
+ },
121
+ }
122
+
123
+
124
+ class GitBlame(BaseTool):
125
+ """Tool for viewing git blame."""
126
+
127
+ def __init__(self, workspace: Path | None = None) -> None:
128
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
129
+
130
+ def get_name(self) -> str:
131
+ return "git_blame"
132
+
133
+ def get_description(self) -> str:
134
+ return "View git blame for a file"
135
+
136
+ async def execute(
137
+ self,
138
+ file_path: str,
139
+ directory: str = ".",
140
+ ) -> list[dict]:
141
+ guard = PathGuard(self.workspace)
142
+ safe_root = guard.validate(directory)
143
+ repo = _open_repo(safe_root)
144
+
145
+ raw = Path(file_path)
146
+ if not raw.is_absolute():
147
+ raw = safe_root / raw
148
+ safe_file = guard.validate(raw)
149
+ rel = str(safe_file.relative_to(Path(repo.working_dir).resolve()))
150
+
151
+ def _fetch() -> list[dict]:
152
+ lines: list[dict] = []
153
+ for commit, line_group in repo.blame("HEAD", rel):
154
+ for content in line_group:
155
+ lines.append(
156
+ {
157
+ "commit": commit.hexsha,
158
+ "author": commit.author.name,
159
+ "date": commit.authored_datetime.isoformat(),
160
+ "content": content
161
+ if isinstance(content, str)
162
+ else content.decode("utf-8", errors="replace"),
163
+ }
164
+ )
165
+ return lines
166
+
167
+ return await asyncio.to_thread(_fetch)
168
+
169
+ def get_schema(self) -> dict:
170
+ return {
171
+ "type": "object",
172
+ "properties": {
173
+ "file_path": {"type": "string", "description": "File to blame"},
174
+ "directory": {"type": "string", "description": "Git repository directory"},
175
+ },
176
+ "required": ["file_path"],
177
+ }