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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +212 -0
- velune/cli/autocomplete.py +76 -0
- velune/cli/banner.py +98 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +149 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +188 -0
- velune/cli/commands/config.py +182 -0
- velune/cli/commands/daemon.py +85 -0
- velune/cli/commands/doctor.py +373 -0
- velune/cli/commands/init.py +160 -0
- velune/cli/commands/mcp.py +80 -0
- velune/cli/commands/memory.py +269 -0
- velune/cli/commands/models.py +462 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +171 -0
- velune/cli/commands/setup.py +182 -0
- velune/cli/commands/workspace.py +217 -0
- velune/cli/context.py +37 -0
- velune/cli/councilmodel_ui.py +171 -0
- velune/cli/display/council_view.py +240 -0
- velune/cli/display/memory_view.py +93 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +21 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +44 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +118 -0
- velune/cli/registry.py +81 -0
- velune/cli/repl.py +1178 -0
- velune/cli/session_manager.py +69 -0
- velune/cli/slash_commands.py +37 -0
- velune/cognition/__init__.py +19 -0
- velune/cognition/arbitrator.py +216 -0
- velune/cognition/architecture.py +398 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +216 -0
- velune/cognition/council/challenger.py +70 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +39 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +44 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +53 -0
- velune/cognition/council/planner.py +119 -0
- velune/cognition/council/reviewer.py +72 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +181 -0
- velune/cognition/firewall.py +256 -0
- velune/cognition/module.py +38 -0
- velune/cognition/orchestrator.py +886 -0
- velune/cognition/personality.py +236 -0
- velune/cognition/style_resolver.py +62 -0
- velune/cognition/verification.py +201 -0
- velune/context/__init__.py +11 -0
- velune/context/extractive.py +94 -0
- velune/context/window.py +62 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +53 -0
- velune/core/errors/execution.py +26 -0
- velune/core/errors/memory.py +21 -0
- velune/core/errors/orchestration.py +26 -0
- velune/core/errors/provider.py +31 -0
- velune/core/event_loop.py +30 -0
- velune/core/logging.py +83 -0
- velune/core/runtime.py +106 -0
- velune/core/task_registry.py +120 -0
- velune/core/trace.py +80 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +49 -0
- velune/core/types/context.py +39 -0
- velune/core/types/inference.py +35 -0
- velune/core/types/memory.py +39 -0
- velune/core/types/model.py +64 -0
- velune/core/types/provider.py +35 -0
- velune/core/types/repository.py +35 -0
- velune/core/types/task.py +56 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +115 -0
- velune/daemon/transport.py +169 -0
- velune/events.py +194 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +311 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +128 -0
- velune/execution/command_spec.py +140 -0
- velune/execution/diff_preview.py +172 -0
- velune/execution/executor.py +173 -0
- velune/execution/module.py +16 -0
- velune/execution/multi_diff.py +70 -0
- velune/execution/path_guard.py +19 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +80 -0
- velune/execution/sandbox.py +257 -0
- velune/execution/validator.py +113 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +162 -0
- velune/kernel/__init__.py +58 -0
- velune/kernel/bootstrap.py +107 -0
- velune/kernel/config.py +252 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +102 -0
- velune/kernel/module.py +15 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +93 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +113 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +90 -0
- velune/memory/__init__.py +28 -0
- velune/memory/lifecycle.py +154 -0
- velune/memory/module.py +94 -0
- velune/memory/prioritizer.py +65 -0
- velune/memory/storage/sqlite_manager.py +368 -0
- velune/memory/tiers/episodic.py +156 -0
- velune/memory/tiers/graph.py +282 -0
- velune/memory/tiers/lineage.py +367 -0
- velune/memory/tiers/semantic.py +198 -0
- velune/memory/tiers/working.py +165 -0
- velune/models/__init__.py +16 -0
- velune/models/module.py +18 -0
- velune/models/probes.py +182 -0
- velune/models/profile_cache.py +82 -0
- velune/models/profiler.py +105 -0
- velune/models/registry.py +201 -0
- velune/models/scorer.py +225 -0
- velune/models/specializations.py +196 -0
- velune/orchestration/__init__.py +15 -0
- velune/orchestration/module.py +14 -0
- velune/orchestration/role_assignments.py +78 -0
- velune/orchestration/schemas.py +99 -0
- velune/plugins/__init__.py +13 -0
- velune/plugins/hooks.py +49 -0
- velune/plugins/loader.py +95 -0
- velune/plugins/registry.py +54 -0
- velune/plugins/schemas.py +19 -0
- velune/providers/__init__.py +18 -0
- velune/providers/adapters/anthropic.py +231 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +203 -0
- velune/providers/adapters/llamacpp.py +202 -0
- velune/providers/adapters/lmstudio.py +173 -0
- velune/providers/adapters/ollama.py +186 -0
- velune/providers/adapters/openai.py +207 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +135 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +77 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +87 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +109 -0
- velune/providers/discovery/groq.py +20 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +165 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +114 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/keystore.py +83 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +208 -0
- velune/providers/module.py +15 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +173 -0
- velune/providers/router.py +82 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +21 -0
- velune/repository/analyzer.py +123 -0
- velune/repository/cognition.py +172 -0
- velune/repository/grapher.py +182 -0
- velune/repository/indexer.py +229 -0
- velune/repository/module.py +15 -0
- velune/repository/parser.py +378 -0
- velune/repository/project_type.py +293 -0
- velune/repository/scanner.py +177 -0
- velune/repository/schemas.py +102 -0
- velune/repository/tracker.py +233 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/graph.py +117 -0
- velune/retrieval/hybrid.py +250 -0
- velune/retrieval/keyword.py +113 -0
- velune/retrieval/module.py +19 -0
- velune/retrieval/reranker.py +68 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/vector.py +163 -0
- velune/telemetry/__init__.py +7 -0
- velune/telemetry/cognition.py +260 -0
- velune/telemetry/token_tracker.py +133 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +84 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +107 -0
- velune/tools/code/search.py +112 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +123 -0
- velune/tools/filesystem/write.py +160 -0
- velune/tools/git/history.py +185 -0
- velune/tools/git/operations.py +132 -0
- velune/tools/git/state.py +134 -0
- velune/tools/module.py +65 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +96 -0
- velune_cli-1.0.0.dist-info/METADATA +497 -0
- velune_cli-1.0.0.dist-info/RECORD +229 -0
- velune_cli-1.0.0.dist-info/WHEEL +4 -0
- velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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)
|