velune-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +212 -0
  5. velune/cli/autocomplete.py +76 -0
  6. velune/cli/banner.py +98 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +149 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +188 -0
  11. velune/cli/commands/config.py +182 -0
  12. velune/cli/commands/daemon.py +85 -0
  13. velune/cli/commands/doctor.py +373 -0
  14. velune/cli/commands/init.py +160 -0
  15. velune/cli/commands/mcp.py +80 -0
  16. velune/cli/commands/memory.py +269 -0
  17. velune/cli/commands/models.py +462 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +171 -0
  20. velune/cli/commands/setup.py +182 -0
  21. velune/cli/commands/workspace.py +217 -0
  22. velune/cli/context.py +37 -0
  23. velune/cli/councilmodel_ui.py +171 -0
  24. velune/cli/display/council_view.py +240 -0
  25. velune/cli/display/memory_view.py +93 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +21 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +44 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +118 -0
  33. velune/cli/registry.py +81 -0
  34. velune/cli/repl.py +1178 -0
  35. velune/cli/session_manager.py +69 -0
  36. velune/cli/slash_commands.py +37 -0
  37. velune/cognition/__init__.py +19 -0
  38. velune/cognition/arbitrator.py +216 -0
  39. velune/cognition/architecture.py +398 -0
  40. velune/cognition/council/__init__.py +47 -0
  41. velune/cognition/council/base.py +216 -0
  42. velune/cognition/council/challenger.py +70 -0
  43. velune/cognition/council/coder.py +79 -0
  44. velune/cognition/council/critic_agent.py +39 -0
  45. velune/cognition/council/critic_configs.py +111 -0
  46. velune/cognition/council/critics.py +41 -0
  47. velune/cognition/council/debate.py +44 -0
  48. velune/cognition/council/factory.py +140 -0
  49. velune/cognition/council/messages.py +53 -0
  50. velune/cognition/council/planner.py +119 -0
  51. velune/cognition/council/reviewer.py +72 -0
  52. velune/cognition/council/synthesizer.py +67 -0
  53. velune/cognition/council/tiers.py +181 -0
  54. velune/cognition/firewall.py +256 -0
  55. velune/cognition/module.py +38 -0
  56. velune/cognition/orchestrator.py +886 -0
  57. velune/cognition/personality.py +236 -0
  58. velune/cognition/style_resolver.py +62 -0
  59. velune/cognition/verification.py +201 -0
  60. velune/context/__init__.py +11 -0
  61. velune/context/extractive.py +94 -0
  62. velune/context/window.py +62 -0
  63. velune/core/__init__.py +89 -0
  64. velune/core/background.py +5 -0
  65. velune/core/config/__init__.py +37 -0
  66. velune/core/errors/__init__.py +53 -0
  67. velune/core/errors/execution.py +26 -0
  68. velune/core/errors/memory.py +21 -0
  69. velune/core/errors/orchestration.py +26 -0
  70. velune/core/errors/provider.py +31 -0
  71. velune/core/event_loop.py +30 -0
  72. velune/core/logging.py +83 -0
  73. velune/core/runtime.py +106 -0
  74. velune/core/task_registry.py +120 -0
  75. velune/core/trace.py +80 -0
  76. velune/core/types/__init__.py +48 -0
  77. velune/core/types/agent.py +49 -0
  78. velune/core/types/context.py +39 -0
  79. velune/core/types/inference.py +35 -0
  80. velune/core/types/memory.py +39 -0
  81. velune/core/types/model.py +64 -0
  82. velune/core/types/provider.py +35 -0
  83. velune/core/types/repository.py +35 -0
  84. velune/core/types/task.py +56 -0
  85. velune/core/types/workspace.py +28 -0
  86. velune/daemon/client.py +13 -0
  87. velune/daemon/server.py +115 -0
  88. velune/daemon/transport.py +169 -0
  89. velune/events.py +194 -0
  90. velune/execution/__init__.py +22 -0
  91. velune/execution/benchmarker.py +311 -0
  92. velune/execution/cancellation.py +53 -0
  93. velune/execution/checkpointer.py +128 -0
  94. velune/execution/command_spec.py +140 -0
  95. velune/execution/diff_preview.py +172 -0
  96. velune/execution/executor.py +173 -0
  97. velune/execution/module.py +16 -0
  98. velune/execution/multi_diff.py +70 -0
  99. velune/execution/path_guard.py +19 -0
  100. velune/execution/planner.py +91 -0
  101. velune/execution/rollback.py +80 -0
  102. velune/execution/sandbox.py +257 -0
  103. velune/execution/validator.py +113 -0
  104. velune/hardware/__init__.py +1 -0
  105. velune/hardware/detector.py +162 -0
  106. velune/kernel/__init__.py +58 -0
  107. velune/kernel/bootstrap.py +107 -0
  108. velune/kernel/config.py +252 -0
  109. velune/kernel/health.py +54 -0
  110. velune/kernel/lifecycle.py +102 -0
  111. velune/kernel/module.py +15 -0
  112. velune/kernel/modules.py +23 -0
  113. velune/kernel/registry.py +93 -0
  114. velune/kernel/schemas.py +28 -0
  115. velune/main.py +9 -0
  116. velune/mcp/__init__.py +9 -0
  117. velune/mcp/client.py +113 -0
  118. velune/mcp/config.py +19 -0
  119. velune/mcp/server.py +90 -0
  120. velune/memory/__init__.py +28 -0
  121. velune/memory/lifecycle.py +154 -0
  122. velune/memory/module.py +94 -0
  123. velune/memory/prioritizer.py +65 -0
  124. velune/memory/storage/sqlite_manager.py +368 -0
  125. velune/memory/tiers/episodic.py +156 -0
  126. velune/memory/tiers/graph.py +282 -0
  127. velune/memory/tiers/lineage.py +367 -0
  128. velune/memory/tiers/semantic.py +198 -0
  129. velune/memory/tiers/working.py +165 -0
  130. velune/models/__init__.py +16 -0
  131. velune/models/module.py +18 -0
  132. velune/models/probes.py +182 -0
  133. velune/models/profile_cache.py +82 -0
  134. velune/models/profiler.py +105 -0
  135. velune/models/registry.py +201 -0
  136. velune/models/scorer.py +225 -0
  137. velune/models/specializations.py +196 -0
  138. velune/orchestration/__init__.py +15 -0
  139. velune/orchestration/module.py +14 -0
  140. velune/orchestration/role_assignments.py +78 -0
  141. velune/orchestration/schemas.py +99 -0
  142. velune/plugins/__init__.py +13 -0
  143. velune/plugins/hooks.py +49 -0
  144. velune/plugins/loader.py +95 -0
  145. velune/plugins/registry.py +54 -0
  146. velune/plugins/schemas.py +19 -0
  147. velune/providers/__init__.py +18 -0
  148. velune/providers/adapters/anthropic.py +231 -0
  149. velune/providers/adapters/fireworks.py +115 -0
  150. velune/providers/adapters/google.py +234 -0
  151. velune/providers/adapters/groq.py +151 -0
  152. velune/providers/adapters/huggingface.py +203 -0
  153. velune/providers/adapters/llamacpp.py +202 -0
  154. velune/providers/adapters/lmstudio.py +173 -0
  155. velune/providers/adapters/ollama.py +186 -0
  156. velune/providers/adapters/openai.py +207 -0
  157. velune/providers/adapters/openrouter.py +81 -0
  158. velune/providers/adapters/together.py +134 -0
  159. velune/providers/adapters/xai.py +60 -0
  160. velune/providers/base.py +86 -0
  161. velune/providers/benchmarker.py +135 -0
  162. velune/providers/discovery/__init__.py +33 -0
  163. velune/providers/discovery/anthropic.py +77 -0
  164. velune/providers/discovery/benchmarks.py +44 -0
  165. velune/providers/discovery/classifier.py +69 -0
  166. velune/providers/discovery/fireworks.py +95 -0
  167. velune/providers/discovery/gguf.py +87 -0
  168. velune/providers/discovery/google.py +95 -0
  169. velune/providers/discovery/gpu.py +109 -0
  170. velune/providers/discovery/groq.py +20 -0
  171. velune/providers/discovery/huggingface.py +67 -0
  172. velune/providers/discovery/lmstudio.py +80 -0
  173. velune/providers/discovery/ollama.py +165 -0
  174. velune/providers/discovery/openai.py +96 -0
  175. velune/providers/discovery/openrouter.py +113 -0
  176. velune/providers/discovery/scanner.py +114 -0
  177. velune/providers/discovery/together.py +114 -0
  178. velune/providers/discovery/xai.py +57 -0
  179. velune/providers/keystore.py +83 -0
  180. velune/providers/local_paths.py +49 -0
  181. velune/providers/local_resolver.py +208 -0
  182. velune/providers/module.py +15 -0
  183. velune/providers/ollama_manager.py +193 -0
  184. velune/providers/registry.py +173 -0
  185. velune/providers/router.py +82 -0
  186. velune/py.typed +0 -0
  187. velune/repository/__init__.py +21 -0
  188. velune/repository/analyzer.py +123 -0
  189. velune/repository/cognition.py +172 -0
  190. velune/repository/grapher.py +182 -0
  191. velune/repository/indexer.py +229 -0
  192. velune/repository/module.py +15 -0
  193. velune/repository/parser.py +378 -0
  194. velune/repository/project_type.py +293 -0
  195. velune/repository/scanner.py +177 -0
  196. velune/repository/schemas.py +102 -0
  197. velune/repository/tracker.py +233 -0
  198. velune/retrieval/__init__.py +27 -0
  199. velune/retrieval/graph.py +117 -0
  200. velune/retrieval/hybrid.py +250 -0
  201. velune/retrieval/keyword.py +113 -0
  202. velune/retrieval/module.py +19 -0
  203. velune/retrieval/reranker.py +68 -0
  204. velune/retrieval/schemas.py +59 -0
  205. velune/retrieval/vector.py +163 -0
  206. velune/telemetry/__init__.py +7 -0
  207. velune/telemetry/cognition.py +260 -0
  208. velune/telemetry/token_tracker.py +133 -0
  209. velune/tools/__init__.py +41 -0
  210. velune/tools/base/registry.py +84 -0
  211. velune/tools/base/tool.py +63 -0
  212. velune/tools/code/navigate.py +107 -0
  213. velune/tools/code/search.py +112 -0
  214. velune/tools/filesystem/read.py +75 -0
  215. velune/tools/filesystem/search.py +123 -0
  216. velune/tools/filesystem/write.py +160 -0
  217. velune/tools/git/history.py +185 -0
  218. velune/tools/git/operations.py +132 -0
  219. velune/tools/git/state.py +134 -0
  220. velune/tools/module.py +65 -0
  221. velune/tools/terminal/execute.py +72 -0
  222. velune/tools/terminal/history.py +47 -0
  223. velune/tools/web/fetch.py +55 -0
  224. velune/tools/web/validator.py +96 -0
  225. velune_cli-1.0.0.dist-info/METADATA +497 -0
  226. velune_cli-1.0.0.dist-info/RECORD +229 -0
  227. velune_cli-1.0.0.dist-info/WHEEL +4 -0
  228. velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
  229. velune_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,293 @@
1
+ """Project type detector — classifies workspaces and builds context profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from pathlib import Path
8
+
9
+
10
+ class ProjectType(Enum):
11
+ PYTHON_FASTAPI = "python-fastapi"
12
+ PYTHON_DJANGO = "python-django"
13
+ PYTHON_FLASK = "python-flask"
14
+ PYTHON_CLI = "python-cli"
15
+ PYTHON_GENERIC = "python"
16
+ NODE_REACT = "node-react"
17
+ NODE_NEXTJS = "node-nextjs"
18
+ NODE_EXPRESS = "node-express"
19
+ NODE_GENERIC = "node"
20
+ RUST = "rust"
21
+ GO = "go"
22
+ JAVA_SPRING = "java-spring"
23
+ JAVA_GENERIC = "java"
24
+ DOTNET = "dotnet"
25
+ FLUTTER = "flutter"
26
+ UNKNOWN = "unknown"
27
+
28
+
29
+ @dataclass
30
+ class ProjectProfile:
31
+ project_type: ProjectType
32
+ display_name: str
33
+ primary_language: str
34
+ detected_frameworks: list[str]
35
+ entry_points: list[str]
36
+ test_directories: list[str]
37
+ config_files: list[str]
38
+ suggested_model_skill: str # "coding" | "reasoning" | "balanced"
39
+ context_hints: list[str]
40
+ system_prompt_addon: str
41
+
42
+
43
+ PROJECT_SYSTEM_PROMPTS: dict[ProjectType, str] = {
44
+ ProjectType.PYTHON_FASTAPI: (
45
+ "This is a Python FastAPI project. When suggesting code changes, "
46
+ "prefer async/await patterns, Pydantic models for validation, "
47
+ "and dependency injection. Routes are in routers/. Models in models/."
48
+ ),
49
+ ProjectType.PYTHON_DJANGO: (
50
+ "This is a Django project. Follow Django conventions: fat models, "
51
+ "thin views, use Django ORM patterns, signals, and class-based views "
52
+ "where appropriate. Settings are in settings.py or settings/."
53
+ ),
54
+ ProjectType.PYTHON_FLASK: (
55
+ "This is a Flask project. Follow Flask patterns: blueprints for "
56
+ "routing, application factory pattern, SQLAlchemy for ORM if present."
57
+ ),
58
+ ProjectType.NODE_REACT: (
59
+ "This is a React frontend project. Prefer functional components "
60
+ "with hooks, TypeScript if tsconfig.json exists, and modern React "
61
+ "patterns. Components in src/components/."
62
+ ),
63
+ ProjectType.NODE_NEXTJS: (
64
+ "This is a Next.js project. Use App Router patterns if app/ exists, "
65
+ "Pages Router if pages/ exists. Server components by default, "
66
+ "client components only when needed."
67
+ ),
68
+ ProjectType.RUST: (
69
+ "This is a Rust project. Follow Rust idioms: ownership semantics, "
70
+ "Result/Option for error handling, no unwrap() in library code. "
71
+ "Check Cargo.toml for edition and features."
72
+ ),
73
+ ProjectType.GO: (
74
+ "This is a Go project. Follow Go conventions: simple interfaces, "
75
+ "error returns, goroutines and channels for concurrency. "
76
+ "Entry point is main.go or cmd/."
77
+ ),
78
+ }
79
+
80
+ _DISPLAY_NAMES: dict[ProjectType, str] = {
81
+ ProjectType.PYTHON_FASTAPI: "Python / FastAPI",
82
+ ProjectType.PYTHON_DJANGO: "Python / Django",
83
+ ProjectType.PYTHON_FLASK: "Python / Flask",
84
+ ProjectType.PYTHON_CLI: "Python / CLI",
85
+ ProjectType.PYTHON_GENERIC: "Python",
86
+ ProjectType.NODE_REACT: "Node.js / React",
87
+ ProjectType.NODE_NEXTJS: "Node.js / Next.js",
88
+ ProjectType.NODE_EXPRESS: "Node.js / Express",
89
+ ProjectType.NODE_GENERIC: "Node.js",
90
+ ProjectType.RUST: "Rust",
91
+ ProjectType.GO: "Go",
92
+ ProjectType.JAVA_SPRING: "Java / Spring",
93
+ ProjectType.JAVA_GENERIC: "Java",
94
+ ProjectType.DOTNET: ".NET",
95
+ ProjectType.FLUTTER: "Flutter / Dart",
96
+ ProjectType.UNKNOWN: "Unknown",
97
+ }
98
+
99
+ _LANGUAGE_MAP: dict[ProjectType, str] = {
100
+ ProjectType.PYTHON_FASTAPI: "python",
101
+ ProjectType.PYTHON_DJANGO: "python",
102
+ ProjectType.PYTHON_FLASK: "python",
103
+ ProjectType.PYTHON_CLI: "python",
104
+ ProjectType.PYTHON_GENERIC: "python",
105
+ ProjectType.NODE_REACT: "typescript",
106
+ ProjectType.NODE_NEXTJS: "typescript",
107
+ ProjectType.NODE_EXPRESS: "javascript",
108
+ ProjectType.NODE_GENERIC: "javascript",
109
+ ProjectType.RUST: "rust",
110
+ ProjectType.GO: "go",
111
+ ProjectType.JAVA_SPRING: "java",
112
+ ProjectType.JAVA_GENERIC: "java",
113
+ ProjectType.DOTNET: "csharp",
114
+ ProjectType.FLUTTER: "dart",
115
+ ProjectType.UNKNOWN: "unknown",
116
+ }
117
+
118
+
119
+ class ProjectTypeDetector:
120
+ """Detects the project type of a workspace by inspecting root-level files."""
121
+
122
+ def detect(self, workspace: Path) -> ProjectProfile:
123
+ files = self._list_root_files(workspace)
124
+ project_type, frameworks = self._classify(workspace, files)
125
+ entry_points = self._find_entry_points(workspace, project_type)
126
+ test_dirs = self._find_test_dirs(workspace)
127
+ config_files = self._find_config_files(workspace, files)
128
+ context_hints = self._build_context_hints(project_type)
129
+ system_prompt = PROJECT_SYSTEM_PROMPTS.get(project_type, "")
130
+
131
+ return ProjectProfile(
132
+ project_type=project_type,
133
+ display_name=_DISPLAY_NAMES.get(project_type, "Unknown"),
134
+ primary_language=_LANGUAGE_MAP.get(project_type, "unknown"),
135
+ detected_frameworks=frameworks,
136
+ entry_points=entry_points,
137
+ test_directories=test_dirs,
138
+ config_files=config_files,
139
+ suggested_model_skill="coding",
140
+ context_hints=context_hints,
141
+ system_prompt_addon=system_prompt,
142
+ )
143
+
144
+ def _list_root_files(self, workspace: Path) -> set[str]:
145
+ try:
146
+ return {f.name for f in workspace.iterdir() if not f.name.startswith(".")}
147
+ except Exception:
148
+ return set()
149
+
150
+ def _classify(
151
+ self, workspace: Path, files: set[str]
152
+ ) -> tuple[ProjectType, list[str]]:
153
+ # ── Rust ──────────────────────────────────────────────────────
154
+ if "Cargo.toml" in files:
155
+ return ProjectType.RUST, ["cargo"]
156
+
157
+ # ── Go ────────────────────────────────────────────────────────
158
+ if "go.mod" in files:
159
+ return ProjectType.GO, ["go modules"]
160
+
161
+ # ── Flutter / Dart ────────────────────────────────────────────
162
+ if "pubspec.yaml" in files:
163
+ return ProjectType.FLUTTER, ["flutter", "dart"]
164
+
165
+ # ── .NET ──────────────────────────────────────────────────────
166
+ if any(f.endswith(".csproj") or f.endswith(".sln") for f in files):
167
+ return ProjectType.DOTNET, ["dotnet"]
168
+
169
+ # ── Node / JavaScript / TypeScript ────────────────────────────
170
+ if "package.json" in files:
171
+ try:
172
+ import json
173
+ pkg = json.loads((workspace / "package.json").read_text())
174
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
175
+ if "next" in deps:
176
+ return ProjectType.NODE_NEXTJS, ["nextjs", "react"]
177
+ if "react" in deps or "react-dom" in deps:
178
+ frameworks = ["react"]
179
+ if "typescript" in deps or (workspace / "tsconfig.json").exists():
180
+ frameworks.append("typescript")
181
+ return ProjectType.NODE_REACT, frameworks
182
+ if "express" in deps:
183
+ return ProjectType.NODE_EXPRESS, ["express"]
184
+ except Exception:
185
+ pass
186
+ return ProjectType.NODE_GENERIC, []
187
+
188
+ # ── Python ────────────────────────────────────────────────────
189
+ has_python = (
190
+ "pyproject.toml" in files
191
+ or "setup.py" in files
192
+ or "requirements.txt" in files
193
+ or any(f.endswith(".py") for f in files)
194
+ )
195
+ if has_python:
196
+ combined = self._read_requirement_sources(workspace)
197
+ if "fastapi" in combined:
198
+ frameworks = ["fastapi", "uvicorn"]
199
+ if "sqlalchemy" in combined:
200
+ frameworks.append("sqlalchemy")
201
+ if "pydantic" in combined:
202
+ frameworks.append("pydantic")
203
+ return ProjectType.PYTHON_FASTAPI, frameworks
204
+ if "django" in combined:
205
+ frameworks: list[str] = ["django"]
206
+ if "drf" in combined or "rest_framework" in combined:
207
+ frameworks.append("drf")
208
+ return ProjectType.PYTHON_DJANGO, frameworks
209
+ if "flask" in combined:
210
+ return ProjectType.PYTHON_FLASK, ["flask"]
211
+ if "typer" in combined or "click" in combined or "argparse" in combined:
212
+ return ProjectType.PYTHON_CLI, ["cli"]
213
+ return ProjectType.PYTHON_GENERIC, []
214
+
215
+ # ── Java ──────────────────────────────────────────────────────
216
+ if "pom.xml" in files or "build.gradle" in files:
217
+ combined = ""
218
+ for fname in ("pom.xml", "build.gradle"):
219
+ fp = workspace / fname
220
+ if fp.exists():
221
+ try:
222
+ combined += fp.read_text().lower()
223
+ except Exception:
224
+ pass
225
+ if "spring" in combined:
226
+ return ProjectType.JAVA_SPRING, ["spring"]
227
+ return ProjectType.JAVA_GENERIC, []
228
+
229
+ return ProjectType.UNKNOWN, []
230
+
231
+ def _read_requirement_sources(self, workspace: Path) -> str:
232
+ parts: list[str] = []
233
+ for fname in ("requirements.txt", "requirements.in", "pyproject.toml", "setup.py"):
234
+ fp = workspace / fname
235
+ if fp.exists():
236
+ try:
237
+ parts.append(fp.read_text().lower())
238
+ except Exception:
239
+ pass
240
+ return " ".join(parts)
241
+
242
+ def _find_entry_points(self, workspace: Path, pt: ProjectType) -> list[str]:
243
+ candidates: dict[ProjectType, list[str]] = {
244
+ ProjectType.PYTHON_FASTAPI: ["main.py", "app/main.py", "src/main.py"],
245
+ ProjectType.PYTHON_DJANGO: ["manage.py"],
246
+ ProjectType.PYTHON_FLASK: ["app.py", "run.py", "main.py"],
247
+ ProjectType.PYTHON_CLI: ["main.py", "cli.py", "__main__.py"],
248
+ ProjectType.PYTHON_GENERIC: ["main.py", "__main__.py"],
249
+ ProjectType.NODE_REACT: ["src/main.tsx", "src/App.tsx", "src/index.tsx"],
250
+ ProjectType.NODE_NEXTJS: ["app/page.tsx", "pages/index.tsx"],
251
+ ProjectType.NODE_EXPRESS: ["index.js", "server.js", "app.js"],
252
+ ProjectType.RUST: ["src/main.rs", "src/lib.rs"],
253
+ ProjectType.GO: ["main.go", "cmd/main.go"],
254
+ }
255
+ found = []
256
+ for rel in candidates.get(pt, []):
257
+ if (workspace / rel).exists():
258
+ found.append(rel)
259
+ return found[:3]
260
+
261
+ def _find_test_dirs(self, workspace: Path) -> list[str]:
262
+ names = ["tests", "test", "__tests__", "spec", "specs", "e2e"]
263
+ return [d for d in names if (workspace / d).is_dir()]
264
+
265
+ def _find_config_files(self, workspace: Path, files: set[str]) -> list[str]:
266
+ candidates = [
267
+ "pyproject.toml", "package.json", "Cargo.toml", "go.mod",
268
+ "tsconfig.json", ".eslintrc.json", "webpack.config.js",
269
+ "vite.config.ts", "next.config.js", "docker-compose.yml",
270
+ "Dockerfile", ".env.example",
271
+ ]
272
+ return [f for f in candidates if f in files]
273
+
274
+ def _build_context_hints(self, pt: ProjectType) -> list[str]:
275
+ hints: list[str] = []
276
+ if pt in (
277
+ ProjectType.PYTHON_FASTAPI,
278
+ ProjectType.PYTHON_DJANGO,
279
+ ProjectType.PYTHON_FLASK,
280
+ ProjectType.PYTHON_GENERIC,
281
+ ):
282
+ hints.append("Always check imports and type hints in Python files")
283
+ hints.append("Prefer pathlib over os.path for file operations")
284
+ if pt == ProjectType.NODE_REACT:
285
+ hints.append("Check for TypeScript types in *.d.ts files")
286
+ hints.append("Components use named exports, not default where possible")
287
+ if pt == ProjectType.RUST:
288
+ hints.append("Check Cargo.toml features before suggesting dependencies")
289
+ hints.append("Use ? operator for error propagation")
290
+ if pt == ProjectType.GO:
291
+ hints.append("Follow Go error handling: always check returned errors")
292
+ hints.append("Check go.sum when suggesting new imports")
293
+ return hints
@@ -0,0 +1,177 @@
1
+ """Gitignore-aware file scanner for workspace discovery."""
2
+
3
+ import fnmatch
4
+ from pathlib import Path
5
+
6
+ DEFAULT_VELUNEIGNORE = """\
7
+ # Velune index exclusions
8
+ # Secrets and credentials
9
+ .env
10
+ .env.*
11
+ *.pem
12
+ *.key
13
+ *.p12
14
+ *.pfx
15
+ *.crt
16
+ *.jks
17
+ *.keystore
18
+ id_rsa
19
+ id_dsa
20
+ id_ed25519
21
+ id_ecdsa
22
+ .netrc
23
+ .npmrc
24
+ .pypirc
25
+ .aws/
26
+ credentials.json
27
+ service-account.json
28
+ gcp-credentials.json
29
+ secrets/
30
+ credentials/
31
+
32
+ # Large generated files
33
+ *.min.js
34
+ *.min.css
35
+ dist/
36
+ build/
37
+ __pycache__/
38
+ *.pyc
39
+ .mypy_cache/
40
+ .ruff_cache/
41
+
42
+ # Data and media
43
+ *.sqlite
44
+ *.db
45
+ *.csv
46
+ *.parquet
47
+ data/
48
+ datasets/
49
+ *.jpg
50
+ *.jpeg
51
+ *.png
52
+ *.gif
53
+ *.mp4
54
+ *.zip
55
+ *.tar.gz
56
+
57
+ # IDE
58
+ .idea/
59
+ .vscode/settings.json
60
+ *.swp
61
+ """
62
+
63
+
64
+ class FilesystemScanner:
65
+ """Discovers source files inside a workspace, strictly adhering to .gitignore rules."""
66
+
67
+ def __init__(self, root_path: Path) -> None:
68
+ self.root_path = root_path.resolve()
69
+ self.gitignore_patterns = self._load_gitignore() + self._load_veluneignore()
70
+
71
+ def _load_gitignore(self) -> list[str]:
72
+ """Loads and parses .gitignore rules along with core default exclusions."""
73
+ patterns = [
74
+ # Default exclusions
75
+ ".git",
76
+ ".github",
77
+ "__pycache__",
78
+ "*.pyc",
79
+ "*.pyo",
80
+ "*.pyd",
81
+ ".venv",
82
+ "venv",
83
+ "env",
84
+ "node_modules",
85
+ "dist",
86
+ "build",
87
+ ".velune",
88
+ "*.egg-info",
89
+ ".pytest_cache",
90
+ ".mypy_cache",
91
+ "CVS",
92
+ ".DS_Store",
93
+ ]
94
+
95
+ gitignore_path = self.root_path / ".gitignore"
96
+ if gitignore_path.exists():
97
+ try:
98
+ with open(gitignore_path, encoding="utf-8", errors="ignore") as f:
99
+ for line in f:
100
+ line = line.strip()
101
+ if line and not line.startswith("#"):
102
+ # Normalize trailing slash to match standard glob behavior
103
+ if line.endswith("/"):
104
+ line = line[:-1]
105
+ patterns.append(line)
106
+ except Exception:
107
+ pass
108
+
109
+ return patterns
110
+
111
+ def _load_veluneignore(self) -> list[str]:
112
+ """Loads and parses .veluneignore rules from the workspace root."""
113
+ patterns: list[str] = []
114
+ veluneignore_path = self.root_path / ".veluneignore"
115
+ if not veluneignore_path.exists():
116
+ return patterns
117
+ try:
118
+ with open(veluneignore_path, encoding="utf-8", errors="ignore") as f:
119
+ for line in f:
120
+ line = line.strip()
121
+ if line and not line.startswith("#"):
122
+ if line.endswith("/"):
123
+ line = line[:-1]
124
+ patterns.append(line)
125
+ except Exception:
126
+ pass
127
+ return patterns
128
+
129
+ def is_ignored(self, path: Path) -> bool:
130
+ """Determines if a path is excluded by .gitignore or default ignore rules."""
131
+ try:
132
+ rel_path = path.resolve().relative_to(self.root_path)
133
+ except ValueError:
134
+ # Not under root
135
+ return True
136
+
137
+ path_parts = rel_path.parts
138
+ path_str = str(rel_path).replace("\\", "/")
139
+
140
+ for pattern in self.gitignore_patterns:
141
+ # Check direct match or directory component match
142
+ if fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(path_str, f"*/{pattern}"):
143
+ return True
144
+ for part in path_parts:
145
+ if fnmatch.fnmatch(part, pattern):
146
+ return True
147
+
148
+ return False
149
+
150
+ def scan(self, extensions: list[str] | None = None) -> list[Path]:
151
+ """Scans the repository recursively and lists all valid files."""
152
+ files: list[Path] = []
153
+ self._recursive_scan(self.root_path, extensions, files)
154
+ return files
155
+
156
+ def _recursive_scan(self, current_dir: Path, extensions: list[str] | None, accumulator: list[Path]) -> None:
157
+ """Recurses through directory structure, skipping ignored directories entirely to optimize speed."""
158
+ try:
159
+ for item in current_dir.iterdir():
160
+ if self.is_ignored(item):
161
+ continue
162
+
163
+ if item.is_dir():
164
+ self._recursive_scan(item, extensions, accumulator)
165
+ elif item.is_file():
166
+ if extensions is None or item.suffix.lower() in extensions:
167
+ accumulator.append(item)
168
+ except PermissionError:
169
+ pass # Fail silently for protected folders
170
+
171
+ def scan_code_files(self) -> list[Path]:
172
+ """Convenience method to scan for common source code extensions."""
173
+ code_extensions = [
174
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java",
175
+ ".c", ".cpp", ".h", ".cs", ".php", ".rb", ".swift", ".kt"
176
+ ]
177
+ return self.scan(code_extensions)
@@ -0,0 +1,102 @@
1
+ """Strictly-typed schemas for repository cognition."""
2
+
3
+ import hashlib
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+
10
+ def build_qualified_name(file_path: str, name: str, parent: str | None = None) -> str:
11
+ """Builds a dotted qualified name for a symbol from its file path and parent scope."""
12
+ p = file_path.replace("\\", "/")
13
+
14
+ # Extract package path relative to 'velune/' if absolute
15
+ if "/velune/" in p:
16
+ p = p.split("/velune/", 1)[1]
17
+ p = "velune/" + p
18
+ elif p.startswith("c:") or p.startswith("C:") or ":" in p:
19
+ p = p.split(":", 1)[1].lstrip("/")
20
+
21
+ p = p.rsplit(".", 1)[0]
22
+ dotted = p.replace("/", ".").strip(".")
23
+
24
+ if parent:
25
+ return f"{dotted}.{parent}.{name}"
26
+ return f"{dotted}.{name}"
27
+
28
+
29
+ def compute_symbol_id(file_path: str, qualified_name: str, kind: str) -> str:
30
+ """Computes a stable, deterministic, line-independent SHA256 identity for a symbol."""
31
+ p = file_path.replace("\\", "/")
32
+ if "/velune/" in p:
33
+ p = p.split("/velune/", 1)[1]
34
+ p = "velune/" + p
35
+ elif p.startswith("c:") or p.startswith("C:") or ":" in p:
36
+ p = p.split(":", 1)[1].lstrip("/")
37
+
38
+ payload = f"{p}:{qualified_name}:{kind.lower()}"
39
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
40
+
41
+
42
+ class RepositoryLanguage(StrEnum):
43
+ PYTHON = "python"
44
+ JAVASCRIPT = "javascript"
45
+ TYPESCRIPT = "typescript"
46
+ GO = "go"
47
+ RUST = "rust"
48
+ UNKNOWN = "unknown"
49
+
50
+
51
+ class RepositorySymbolKind(StrEnum):
52
+ CLASS = "class"
53
+ FUNCTION = "function"
54
+ METHOD = "method"
55
+ IMPORT = "import"
56
+ UNKNOWN = "unknown"
57
+
58
+
59
+ class RepositorySymbol(BaseModel):
60
+ name: str
61
+ kind: RepositorySymbolKind
62
+ file_path: str
63
+ line_start: int = 1
64
+ line_end: int = 1
65
+ docstring: str | None = None
66
+ parent: str | None = None
67
+ metadata: dict[str, Any] = Field(default_factory=dict)
68
+ symbol_id: str | None = None
69
+ qualified_name: str | None = None
70
+
71
+ @model_validator(mode="after")
72
+ def populate_identity(self) -> "RepositorySymbol":
73
+ """Ensures stable symbol_id and qualified_name are automatically calculated if missing."""
74
+ if not self.qualified_name:
75
+ self.qualified_name = build_qualified_name(self.file_path, self.name, self.parent)
76
+ if not self.symbol_id:
77
+ self.symbol_id = compute_symbol_id(self.file_path, self.qualified_name, self.kind.value)
78
+ return self
79
+
80
+
81
+ class RepositoryFile(BaseModel):
82
+ path: str
83
+ language: RepositoryLanguage
84
+ size_bytes: int
85
+ sha256: str
86
+ symbols: list[RepositorySymbol] = Field(default_factory=list)
87
+ metadata: dict[str, Any] = Field(default_factory=dict)
88
+
89
+
90
+ class RepositoryEdge(BaseModel):
91
+ source: str
92
+ target: str
93
+ edge_type: str # e.g., "imports", "calls", "contains"
94
+ weight: float = 1.0
95
+
96
+
97
+ class RepositorySnapshot(BaseModel):
98
+ root_path: str
99
+ files: list[RepositoryFile] = Field(default_factory=list)
100
+ symbols: list[RepositorySymbol] = Field(default_factory=list)
101
+ edges: list[RepositoryEdge] = Field(default_factory=list)
102
+ summary: dict[str, Any] = Field(default_factory=dict)