codrspot-processor-mcp 0.1.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.
- codepreproc_client/__init__.py +14 -0
- codepreproc_client/__main__.py +9 -0
- codepreproc_client/layer1_business/__init__.py +0 -0
- codepreproc_client/layer1_business/allowed_scope_cache.py +62 -0
- codepreproc_client/layer1_business/api_client/__init__.py +0 -0
- codepreproc_client/layer1_business/api_client/lease_client.py +34 -0
- codepreproc_client/layer1_business/api_client/net_guard.py +98 -0
- codepreproc_client/layer1_business/api_client/promote_client.py +59 -0
- codepreproc_client/layer1_business/api_client/reasoning_client.py +163 -0
- codepreproc_client/layer1_business/api_client/registry_client.py +119 -0
- codepreproc_client/layer1_business/api_client/superkg_client.py +327 -0
- codepreproc_client/layer1_business/cli/__init__.py +0 -0
- codepreproc_client/layer1_business/cli/init.py +578 -0
- codepreproc_client/layer1_business/cli/main.py +78 -0
- codepreproc_client/layer1_business/config.py +104 -0
- codepreproc_client/layer1_business/git_utils.py +67 -0
- codepreproc_client/layer1_business/license/__init__.py +0 -0
- codepreproc_client/layer1_business/license/fingerprint.py +61 -0
- codepreproc_client/layer1_business/license/jwt_client.py +60 -0
- codepreproc_client/layer1_business/license/project_lock.py +44 -0
- codepreproc_client/layer1_business/local_registry.py +40 -0
- codepreproc_client/layer1_business/manifest/__init__.py +0 -0
- codepreproc_client/layer1_business/manifest/credentials.py +35 -0
- codepreproc_client/layer1_business/mcp/__init__.py +0 -0
- codepreproc_client/layer1_business/mcp/action_monitor.py +246 -0
- codepreproc_client/layer1_business/mcp/lifecycle.py +649 -0
- codepreproc_client/layer1_business/mcp/server.py +656 -0
- codepreproc_client/layer1_business/mcp/tools.py +1665 -0
- codepreproc_client/layer1_business/project_registry.py +99 -0
- codepreproc_client/layer2_tooling/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/ast_graph.py +426 -0
- codepreproc_client/layer2_tooling/condensation/bm25_store.py +132 -0
- codepreproc_client/layer2_tooling/condensation/chunking/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/chunking/ast_chunker.py +313 -0
- codepreproc_client/layer2_tooling/condensation/chunking/languages.py +238 -0
- codepreproc_client/layer2_tooling/condensation/embed_worker.py +128 -0
- codepreproc_client/layer2_tooling/condensation/embeddings.py +452 -0
- codepreproc_client/layer2_tooling/condensation/extract/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/extract/compactor.py +103 -0
- codepreproc_client/layer2_tooling/condensation/extract/providers/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/extract/providers/base.py +38 -0
- codepreproc_client/layer2_tooling/condensation/indexer.py +945 -0
- codepreproc_client/layer2_tooling/condensation/ppl/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/ppl/emitter.py +162 -0
- codepreproc_client/layer2_tooling/condensation/ppl/parser.py +352 -0
- codepreproc_client/layer2_tooling/condensation/qdrant_store.py +387 -0
- codepreproc_client/layer2_tooling/condensation/scanning/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/scanning/manifest_scanner.py +72 -0
- codepreproc_client/layer2_tooling/condensation/scanning/md_scanner.py +97 -0
- codepreproc_client/layer2_tooling/condensation/scanning/repo_mapper.py +495 -0
- codepreproc_client/layer2_tooling/condensation/session_vector_store.py +69 -0
- codepreproc_client/layer2_tooling/coupling/__init__.py +0 -0
- codepreproc_client/layer2_tooling/coupling/domain.py +263 -0
- codepreproc_client/layer2_tooling/coupling/graph.py +255 -0
- codepreproc_client/layer2_tooling/coupling/layer2_gate.py +189 -0
- codepreproc_client/layer2_tooling/coupling/projector.py +139 -0
- codepreproc_client/layer2_tooling/coupling/resolver.py +150 -0
- codepreproc_client/layer2_tooling/materialization/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/execute_semantic.py +1249 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/filesystem_reorg.py +575 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/patch_generator.py +127 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/patch_validator.py +101 -0
- codepreproc_client/layer2_tooling/materialization/semantic/__init__.py +19 -0
- codepreproc_client/layer2_tooling/materialization/semantic/anchors.py +61 -0
- codepreproc_client/layer2_tooling/materialization/semantic/applicability.py +75 -0
- codepreproc_client/layer2_tooling/materialization/semantic/edit_planner.py +264 -0
- codepreproc_client/layer2_tooling/materialization/semantic/materializer.py +121 -0
- codepreproc_client/layer2_tooling/materialization/semantic/merger.py +73 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/add_import.py +36 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/append_argument.py +37 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/insert_literal.py +24 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_import.py +33 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_statement_unique.py +14 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/rename_symbol_local.py +16 -0
- codepreproc_client/layer2_tooling/materialization/semantic/region_resolver.py +229 -0
- codepreproc_client/layer2_tooling/materialization/semantic/synthesizer.py +186 -0
- codepreproc_client/layer2_tooling/materialization/semantic/target_locator.py +94 -0
- codepreproc_client/layer2_tooling/materialization/semantic/validator.py +322 -0
- codepreproc_client/layer2_tooling/materialization/snippet/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/snippet/assembler.py +503 -0
- codepreproc_client/layer2_tooling/materialization/snippet/loader.py +187 -0
- codepreproc_client/layer2_tooling/retrieval/__init__.py +0 -0
- codepreproc_client/layer2_tooling/retrieval/graph_walk.py +47 -0
- codepreproc_client/layer2_tooling/retrieval/hybrid.py +95 -0
- codepreproc_client/layer2_tooling/retrieval/rerank_worker.py +86 -0
- codepreproc_client/layer2_tooling/retrieval/reranker.py +213 -0
- codepreproc_client/layer2_tooling/watcher/__init__.py +0 -0
- codepreproc_client/layer2_tooling/watcher/fs_watcher.py +3 -0
- codrspot_processor_mcp-0.1.0.dist-info/METADATA +396 -0
- codrspot_processor_mcp-0.1.0.dist-info/RECORD +100 -0
- codrspot_processor_mcp-0.1.0.dist-info/WHEEL +5 -0
- codrspot_processor_mcp-0.1.0.dist-info/entry_points.txt +3 -0
- codrspot_processor_mcp-0.1.0.dist-info/licenses/LICENSE +23 -0
- codrspot_processor_mcp-0.1.0.dist-info/top_level.txt +2 -0
- contracts/__init__.py +0 -0
- contracts/dtos.py +1239 -0
- contracts/ir.py +102 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""codepreproc init: register a project into PostgreSQL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Language detection
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
EXTENSION_TO_LANGUAGE: dict[str, str] = {
|
|
19
|
+
".py": "python",
|
|
20
|
+
".ts": "typescript",
|
|
21
|
+
".tsx": "typescript",
|
|
22
|
+
".js": "javascript",
|
|
23
|
+
".mjs": "javascript",
|
|
24
|
+
".jsx": "javascript",
|
|
25
|
+
".go": "go",
|
|
26
|
+
".rs": "rust",
|
|
27
|
+
".dart": "dart",
|
|
28
|
+
".php": "php",
|
|
29
|
+
".rb": "ruby",
|
|
30
|
+
".java": "java",
|
|
31
|
+
".kt": "kotlin",
|
|
32
|
+
".cs": "csharp",
|
|
33
|
+
".cpp": "cpp",
|
|
34
|
+
".c": "c",
|
|
35
|
+
".swift": "swift",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Directories always skipped during scanning
|
|
39
|
+
_ALWAYS_SKIP_DIRS = frozenset({
|
|
40
|
+
".git", "node_modules", "__pycache__", ".venv", "venv", "env",
|
|
41
|
+
"dist", "build", ".next", ".nuxt", "target", "vendor",
|
|
42
|
+
"coverage", ".cache", ".pytest_cache", ".mypy_cache",
|
|
43
|
+
".dart_tool", ".flutter-plugins", "storage",
|
|
44
|
+
".idea", ".vscode",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# File suffixes that are binary / not source
|
|
48
|
+
_SKIP_SUFFIXES = frozenset({".pyc", ".pyo", ".class", ".o", ".a", ".so", ".dll", ".exe"})
|
|
49
|
+
|
|
50
|
+
# Default ignore list written into every new project entry
|
|
51
|
+
_DEFAULT_IGNORE = [
|
|
52
|
+
".git",
|
|
53
|
+
"node_modules",
|
|
54
|
+
"vendor",
|
|
55
|
+
"__pycache__",
|
|
56
|
+
"dist",
|
|
57
|
+
"build",
|
|
58
|
+
".venv",
|
|
59
|
+
"venv",
|
|
60
|
+
"*.pyc",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Helpers
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _slugify(name: str) -> str:
|
|
69
|
+
"""Convert a directory name to a safe project_id slug."""
|
|
70
|
+
slug = re.sub(r"[^a-z0-9_-]", "_", name.lower())
|
|
71
|
+
slug = re.sub(r"_+", "_", slug).strip("_")
|
|
72
|
+
return slug or "project"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_safe(path: Path, max_bytes: int = 8192) -> str:
|
|
76
|
+
try:
|
|
77
|
+
return path.read_text(encoding="utf-8", errors="replace")[:max_bytes]
|
|
78
|
+
except Exception:
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _walk_extensions(root: Path) -> Counter:
|
|
83
|
+
"""Count file extensions across the project tree, skipping ignored dirs."""
|
|
84
|
+
counts: Counter = Counter()
|
|
85
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
86
|
+
# Prune directories in-place so os.walk skips them entirely
|
|
87
|
+
dirnames[:] = [d for d in dirnames if d not in _ALWAYS_SKIP_DIRS]
|
|
88
|
+
for fname in filenames:
|
|
89
|
+
fpath = Path(fname)
|
|
90
|
+
if fpath.suffix in _SKIP_SUFFIXES:
|
|
91
|
+
continue
|
|
92
|
+
if fpath.suffix:
|
|
93
|
+
counts[fpath.suffix.lower()] += 1
|
|
94
|
+
return counts
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _detect_languages(ext_counts: Counter) -> list[str]:
|
|
98
|
+
"""Return the primary languages detected from extension counts.
|
|
99
|
+
|
|
100
|
+
A language is included if its files account for ≥5% of all matched
|
|
101
|
+
source files. At least one language is always returned.
|
|
102
|
+
"""
|
|
103
|
+
lang_counts: Counter = Counter()
|
|
104
|
+
for ext, count in ext_counts.items():
|
|
105
|
+
lang = EXTENSION_TO_LANGUAGE.get(ext)
|
|
106
|
+
if lang:
|
|
107
|
+
lang_counts[lang] += count
|
|
108
|
+
|
|
109
|
+
if not lang_counts:
|
|
110
|
+
return ["unknown"]
|
|
111
|
+
|
|
112
|
+
total = sum(lang_counts.values())
|
|
113
|
+
threshold = max(1, int(total * 0.05))
|
|
114
|
+
langs = [lang for lang, cnt in lang_counts.most_common() if cnt >= threshold]
|
|
115
|
+
return langs or [lang_counts.most_common(1)[0][0]]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Framework / tech-stack detection
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def _detect_framework(root: Path) -> dict[str, Any]:
|
|
123
|
+
"""Probe well-known marker files and return framework metadata."""
|
|
124
|
+
info: dict[str, Any] = {
|
|
125
|
+
"framework": None,
|
|
126
|
+
"tech_stack": [],
|
|
127
|
+
"extra_ignore": [],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ── Node.js / TypeScript ──────────────────────────────────────────────
|
|
131
|
+
pkg_json = root / "package.json"
|
|
132
|
+
if pkg_json.exists():
|
|
133
|
+
try:
|
|
134
|
+
pkg = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
135
|
+
all_deps: dict = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
136
|
+
if "@nestjs/core" in all_deps:
|
|
137
|
+
info["framework"] = "nestjs"
|
|
138
|
+
info["tech_stack"].append("NestJS")
|
|
139
|
+
elif "next" in all_deps:
|
|
140
|
+
info["framework"] = "nextjs"
|
|
141
|
+
info["tech_stack"].append("Next.js")
|
|
142
|
+
info["extra_ignore"].append(".next")
|
|
143
|
+
elif "react" in all_deps:
|
|
144
|
+
info["framework"] = "react"
|
|
145
|
+
info["tech_stack"].append("React")
|
|
146
|
+
elif "express" in all_deps:
|
|
147
|
+
info["framework"] = "express"
|
|
148
|
+
info["tech_stack"].append("Express")
|
|
149
|
+
else:
|
|
150
|
+
info["tech_stack"].append("Node.js")
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# ── Flutter / Dart ────────────────────────────────────────────────────
|
|
155
|
+
if (root / "pubspec.yaml").exists():
|
|
156
|
+
info["framework"] = "flutter"
|
|
157
|
+
info["tech_stack"].append("Flutter")
|
|
158
|
+
info["extra_ignore"].extend([".dart_tool", ".flutter-plugins"])
|
|
159
|
+
|
|
160
|
+
# ── PHP / Laravel ─────────────────────────────────────────────────────
|
|
161
|
+
composer_json = root / "composer.json"
|
|
162
|
+
if composer_json.exists():
|
|
163
|
+
try:
|
|
164
|
+
comp = json.loads(composer_json.read_text(encoding="utf-8"))
|
|
165
|
+
reqs: dict = {**comp.get("require", {}), **comp.get("require-dev", {})}
|
|
166
|
+
if "laravel/framework" in reqs:
|
|
167
|
+
info["framework"] = "laravel"
|
|
168
|
+
info["tech_stack"].append("Laravel")
|
|
169
|
+
info["extra_ignore"].extend(["vendor", "storage/logs", "storage/framework"])
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
# ── Python web frameworks ─────────────────────────────────────────────
|
|
174
|
+
if not info["framework"]:
|
|
175
|
+
req_files = [
|
|
176
|
+
root / "requirements.txt",
|
|
177
|
+
root / "requirements-dev.txt",
|
|
178
|
+
root / "requirements-prod.txt",
|
|
179
|
+
]
|
|
180
|
+
for req_file in req_files:
|
|
181
|
+
if req_file.exists():
|
|
182
|
+
content = _read_safe(req_file).lower()
|
|
183
|
+
if "fastapi" in content:
|
|
184
|
+
info["framework"] = "fastapi"
|
|
185
|
+
info["tech_stack"].append("FastAPI")
|
|
186
|
+
break
|
|
187
|
+
elif "django" in content:
|
|
188
|
+
info["framework"] = "django"
|
|
189
|
+
info["tech_stack"].append("Django")
|
|
190
|
+
break
|
|
191
|
+
elif "flask" in content:
|
|
192
|
+
info["framework"] = "flask"
|
|
193
|
+
info["tech_stack"].append("Flask")
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
pyproject = root / "pyproject.toml"
|
|
197
|
+
if not info["framework"] and pyproject.exists():
|
|
198
|
+
content = _read_safe(pyproject).lower()
|
|
199
|
+
for fw, label in [("fastapi", "FastAPI"), ("django", "Django"), ("flask", "Flask")]:
|
|
200
|
+
if fw in content:
|
|
201
|
+
info["framework"] = fw
|
|
202
|
+
info["tech_stack"].append(label)
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
# ── Rust ──────────────────────────────────────────────────────────────
|
|
206
|
+
if (root / "Cargo.toml").exists():
|
|
207
|
+
if not info["framework"]:
|
|
208
|
+
info["framework"] = "rust"
|
|
209
|
+
info["tech_stack"].append("Rust/Cargo")
|
|
210
|
+
if "target" not in info["extra_ignore"]:
|
|
211
|
+
info["extra_ignore"].append("target")
|
|
212
|
+
|
|
213
|
+
# ── Go ────────────────────────────────────────────────────────────────
|
|
214
|
+
if (root / "go.mod").exists():
|
|
215
|
+
if not info["framework"]:
|
|
216
|
+
info["framework"] = "go"
|
|
217
|
+
info["tech_stack"].append("Go modules")
|
|
218
|
+
|
|
219
|
+
# ── Java / Spring Boot ────────────────────────────────────────────────
|
|
220
|
+
pom = root / "pom.xml"
|
|
221
|
+
if pom.exists():
|
|
222
|
+
pom_text = pom.read_text(encoding="utf-8", errors="ignore").lower()
|
|
223
|
+
if "spring-boot" in pom_text:
|
|
224
|
+
if not info["framework"]:
|
|
225
|
+
info["framework"] = "spring"
|
|
226
|
+
info["tech_stack"].append("Spring Boot")
|
|
227
|
+
elif not info["framework"]:
|
|
228
|
+
info["framework"] = "java"
|
|
229
|
+
info["tech_stack"].append("Java/Maven")
|
|
230
|
+
if "target" not in info["extra_ignore"]:
|
|
231
|
+
info["extra_ignore"].append("target")
|
|
232
|
+
else:
|
|
233
|
+
for gf in ("build.gradle", "build.gradle.kts"):
|
|
234
|
+
gradle = root / gf
|
|
235
|
+
if gradle.exists():
|
|
236
|
+
gradle_text = gradle.read_text(encoding="utf-8", errors="ignore").lower()
|
|
237
|
+
if "spring-boot" in gradle_text:
|
|
238
|
+
if not info["framework"]:
|
|
239
|
+
info["framework"] = "spring"
|
|
240
|
+
info["tech_stack"].append("Spring Boot")
|
|
241
|
+
elif not info["framework"]:
|
|
242
|
+
info["framework"] = "java"
|
|
243
|
+
info["tech_stack"].append("Java/Gradle")
|
|
244
|
+
if "build" not in info["extra_ignore"]:
|
|
245
|
+
info["extra_ignore"].append("build")
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
return info
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _build_ignore_list(extra: list[str]) -> list[str]:
|
|
252
|
+
"""Combine default ignores with framework-specific extras (deduped, order-stable)."""
|
|
253
|
+
seen: set[str] = set()
|
|
254
|
+
result: list[str] = []
|
|
255
|
+
for item in _DEFAULT_IGNORE + extra:
|
|
256
|
+
if item not in seen:
|
|
257
|
+
seen.add(item)
|
|
258
|
+
result.append(item)
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# Project context generation
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def _gather_summary(root: Path, framework_info: dict[str, Any]) -> str:
|
|
267
|
+
"""Build a short textual description of the project for LLM input."""
|
|
268
|
+
parts: list[str] = []
|
|
269
|
+
|
|
270
|
+
# README (first ~1500 chars)
|
|
271
|
+
for readme_name in ("README.md", "README.rst", "README.txt", "readme.md"):
|
|
272
|
+
readme = root / readme_name
|
|
273
|
+
if readme.exists():
|
|
274
|
+
content = _read_safe(readme, max_bytes=2000).strip()
|
|
275
|
+
if content:
|
|
276
|
+
parts.append(f"README:\n{content[:1500]}")
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
# Top-level directory / file listing
|
|
280
|
+
try:
|
|
281
|
+
top_dirs = sorted(p.name for p in root.iterdir() if p.is_dir() and p.name not in _ALWAYS_SKIP_DIRS)
|
|
282
|
+
top_files = sorted(p.name for p in root.iterdir() if p.is_file())
|
|
283
|
+
if top_dirs:
|
|
284
|
+
parts.append(f"Top-level directories: {', '.join(top_dirs[:20])}")
|
|
285
|
+
if top_files:
|
|
286
|
+
parts.append(f"Top-level files: {', '.join(top_files[:20])}")
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
if framework_info.get("tech_stack"):
|
|
291
|
+
parts.append(f"Detected tech stack: {', '.join(framework_info['tech_stack'])}")
|
|
292
|
+
|
|
293
|
+
return "\n\n".join(parts)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _call_llm(endpoint: str, model: str, api_key: str, prompt_system: str, prompt_user: str) -> str | None:
|
|
297
|
+
"""Make a single chat-completion call. Returns None on any failure."""
|
|
298
|
+
try:
|
|
299
|
+
import httpx
|
|
300
|
+
|
|
301
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
302
|
+
if api_key:
|
|
303
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
304
|
+
|
|
305
|
+
payload = {
|
|
306
|
+
"model": model,
|
|
307
|
+
"messages": [
|
|
308
|
+
{"role": "system", "content": prompt_system},
|
|
309
|
+
{"role": "user", "content": prompt_user},
|
|
310
|
+
],
|
|
311
|
+
"max_tokens": 200,
|
|
312
|
+
"temperature": 0.3,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
with httpx.Client(timeout=15.0) as client:
|
|
316
|
+
resp = client.post(
|
|
317
|
+
f"{endpoint.rstrip('/')}/chat/completions",
|
|
318
|
+
headers=headers,
|
|
319
|
+
json=payload,
|
|
320
|
+
)
|
|
321
|
+
resp.raise_for_status()
|
|
322
|
+
return resp.json()["choices"][0]["message"]["content"].strip()
|
|
323
|
+
except Exception:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _generate_context(
|
|
328
|
+
project_name: str,
|
|
329
|
+
summary: str,
|
|
330
|
+
defaults: dict[str, Any],
|
|
331
|
+
) -> str | None:
|
|
332
|
+
"""Try configured LLM providers in order of preference, return None on failure."""
|
|
333
|
+
system_prompt = (
|
|
334
|
+
"You are a technical assistant. Based on the project summary, write a concise "
|
|
335
|
+
"2–3 sentence project_context. Use the same language as the README (Spanish if "
|
|
336
|
+
"the README is in Spanish). Describe what the project does, its main technologies, "
|
|
337
|
+
"and the architectural pattern if visible. Output ONLY the description — no quotes, "
|
|
338
|
+
"no markdown formatting."
|
|
339
|
+
)
|
|
340
|
+
user_prompt = f"Project name: {project_name}\n\n{summary[:3000]}"
|
|
341
|
+
|
|
342
|
+
llm_providers: dict = defaults.get("llm_providers", {})
|
|
343
|
+
|
|
344
|
+
# 1. Prefer OpenAI provider when a valid API key is available
|
|
345
|
+
for _pname, pdata in llm_providers.items():
|
|
346
|
+
if pdata.get("type") == "openai":
|
|
347
|
+
api_key = os.getenv(pdata.get("api_key_env", ""), "")
|
|
348
|
+
if api_key:
|
|
349
|
+
result = _call_llm(
|
|
350
|
+
endpoint="https://api.openai.com/v1",
|
|
351
|
+
model=pdata.get("model", "gpt-4o-mini"),
|
|
352
|
+
api_key=api_key,
|
|
353
|
+
prompt_system=system_prompt,
|
|
354
|
+
prompt_user=user_prompt,
|
|
355
|
+
)
|
|
356
|
+
if result:
|
|
357
|
+
return result
|
|
358
|
+
|
|
359
|
+
# 2. Fall back to the local LLM endpoint
|
|
360
|
+
local_endpoint = defaults.get("llm_endpoint", "http://127.0.0.1:8080/v1")
|
|
361
|
+
local_model = defaults.get("llm_model", "qwen2.5-coder-7b")
|
|
362
|
+
return _call_llm(
|
|
363
|
+
endpoint=local_endpoint,
|
|
364
|
+
model=local_model,
|
|
365
|
+
api_key="",
|
|
366
|
+
prompt_system=system_prompt,
|
|
367
|
+
prompt_user=user_prompt,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _fallback_context(project_name: str, languages: list[str], framework_info: dict[str, Any]) -> str:
|
|
372
|
+
"""Template-based project context when no LLM is available."""
|
|
373
|
+
tech = framework_info.get("tech_stack")
|
|
374
|
+
tech_str = f" using {', '.join(tech)}" if tech else ""
|
|
375
|
+
lang_str = ", ".join(languages)
|
|
376
|
+
return f"Project {project_name}{tech_str}. Primary languages: {lang_str}."
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# Registry helpers
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
def _is_already_registered(stubs: list[tuple[str, str]], root: Path) -> tuple[bool, str | None]:
|
|
384
|
+
"""Return (True, project_id) if root is already registered, else (False, None)."""
|
|
385
|
+
root_resolved = str(root.resolve())
|
|
386
|
+
for pid, existing in stubs:
|
|
387
|
+
try:
|
|
388
|
+
existing_resolved = str(Path(existing).expanduser().resolve())
|
|
389
|
+
except Exception:
|
|
390
|
+
existing_resolved = existing
|
|
391
|
+
if existing_resolved == root_resolved:
|
|
392
|
+
return True, pid
|
|
393
|
+
return False, None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _unique_project_id(base: str, existing_ids: set[str]) -> str:
|
|
397
|
+
"""Return base or base_N (first N≥2 not already taken)."""
|
|
398
|
+
if base not in existing_ids:
|
|
399
|
+
return base
|
|
400
|
+
counter = 2
|
|
401
|
+
while f"{base}_{counter}" in existing_ids:
|
|
402
|
+
counter += 1
|
|
403
|
+
return f"{base}_{counter}"
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _mint_jwt_for_project(project_id: str, repo_root: Path) -> tuple[str, Any]:
|
|
409
|
+
"""Derive this project's fingerprint and mint a JWT naming it in claims."""
|
|
410
|
+
from codepreproc_client.layer1_business.config import get_credentials_path, get_server_url
|
|
411
|
+
from codepreproc_client.layer1_business.license.fingerprint import derive_fingerprint
|
|
412
|
+
from codepreproc_client.layer1_business.license.jwt_client import JWTClient
|
|
413
|
+
from codepreproc_client.layer1_business.manifest.credentials import load_manifest
|
|
414
|
+
|
|
415
|
+
manifest = load_manifest(get_credentials_path())
|
|
416
|
+
attestation = derive_fingerprint(project_id, repo_root)
|
|
417
|
+
jwt_client = JWTClient(base_url=get_server_url(), manifest=manifest, fingerprint_attestation=attestation)
|
|
418
|
+
jwt = asyncio.run(jwt_client.get_bearer_token())
|
|
419
|
+
return jwt, attestation
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _persist_project(project: Any, jwt: str, fingerprint_attestation: Any) -> None:
|
|
423
|
+
"""Register/update a ProjectConfig via the server API, then mirror it locally."""
|
|
424
|
+
from codepreproc_client.layer1_business.allowed_scope_cache import get_allowed_projects
|
|
425
|
+
from codepreproc_client.layer1_business.api_client.registry_client import register_project
|
|
426
|
+
from codepreproc_client.layer1_business.config import get_home_dir, get_server_url
|
|
427
|
+
from codepreproc_client.layer1_business.local_registry import save_local_project
|
|
428
|
+
|
|
429
|
+
stored = asyncio.run(
|
|
430
|
+
register_project(
|
|
431
|
+
base_url=get_server_url(),
|
|
432
|
+
jwt=jwt,
|
|
433
|
+
project=project,
|
|
434
|
+
fingerprint_attestation=fingerprint_attestation,
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
save_local_project(get_home_dir(), stored)
|
|
438
|
+
# Refresh the tenant's allowed-project cache now that the tenant/project set may
|
|
439
|
+
# have changed - subsequent calls read this cache instead of hitting the server.
|
|
440
|
+
asyncio.run(
|
|
441
|
+
get_allowed_projects(home_dir=get_home_dir(), base_url=get_server_url(), jwt=jwt, force_refresh=True)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _set_superkg_id(project_id: str, system_id: str, jwt: str) -> None:
|
|
446
|
+
from codepreproc_client.layer1_business.api_client.registry_client import set_superkg_id
|
|
447
|
+
from codepreproc_client.layer1_business.config import get_home_dir, get_server_url
|
|
448
|
+
from codepreproc_client.layer1_business.local_registry import load_local_projects, save_local_project
|
|
449
|
+
|
|
450
|
+
asyncio.run(set_superkg_id(base_url=get_server_url(), jwt=jwt, project_id=project_id, system_id=system_id))
|
|
451
|
+
local = load_local_projects(get_home_dir())
|
|
452
|
+
project = local.get(project_id)
|
|
453
|
+
if project is not None:
|
|
454
|
+
save_local_project(get_home_dir(), project.model_copy(update={"superkg_system_id": system_id}))
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
# Public entry point
|
|
459
|
+
# ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
def init_project(path: Path | None = None, verbose: bool = True) -> int:
|
|
462
|
+
"""Register a project in the codepreproc PostgreSQL registry.
|
|
463
|
+
|
|
464
|
+
Returns an exit code (0 = success / already registered, 1 = error).
|
|
465
|
+
"""
|
|
466
|
+
from codepreproc_client.layer1_business.config import get_home_dir
|
|
467
|
+
from codepreproc_client.layer1_business.local_registry import load_local_projects
|
|
468
|
+
|
|
469
|
+
target = (path or Path.cwd()).resolve()
|
|
470
|
+
|
|
471
|
+
if not target.exists() or not target.is_dir():
|
|
472
|
+
print(f"Error: '{target}' is not a directory.", file=sys.stderr)
|
|
473
|
+
return 1
|
|
474
|
+
|
|
475
|
+
# ── Load existing project stubs from the local registry mirror ────────
|
|
476
|
+
local_projects = load_local_projects(get_home_dir())
|
|
477
|
+
stubs = [(pid, str(p.root)) for pid, p in local_projects.items()]
|
|
478
|
+
|
|
479
|
+
# ── Idempotency check ─────────────────────────────────────────────────
|
|
480
|
+
already, existing_id = _is_already_registered(stubs, target)
|
|
481
|
+
if already:
|
|
482
|
+
if verbose:
|
|
483
|
+
print(f"Project already registered as '{existing_id}' — nothing to do.")
|
|
484
|
+
return 0
|
|
485
|
+
|
|
486
|
+
# ── Derive project_id ─────────────────────────────────────────────────
|
|
487
|
+
existing_ids = {pid for pid, _ in stubs}
|
|
488
|
+
project_id = _unique_project_id(_slugify(target.name), existing_ids)
|
|
489
|
+
|
|
490
|
+
if verbose:
|
|
491
|
+
print(f"Initializing '{project_id}' from {target} …")
|
|
492
|
+
|
|
493
|
+
# ── Detect languages ──────────────────────────────────────────────────
|
|
494
|
+
if verbose:
|
|
495
|
+
print(" Scanning source files …")
|
|
496
|
+
ext_counts = _walk_extensions(target)
|
|
497
|
+
languages = _detect_languages(ext_counts)
|
|
498
|
+
if verbose:
|
|
499
|
+
print(f" Languages detected : {', '.join(languages)}")
|
|
500
|
+
|
|
501
|
+
# ── Detect framework ──────────────────────────────────────────────────
|
|
502
|
+
framework_info = _detect_framework(target)
|
|
503
|
+
if verbose and framework_info.get("tech_stack"):
|
|
504
|
+
print(f" Tech stack detected: {', '.join(framework_info['tech_stack'])}")
|
|
505
|
+
|
|
506
|
+
# ── Build ignore list ─────────────────────────────────────────────────
|
|
507
|
+
ignore = _build_ignore_list(framework_info.get("extra_ignore", []))
|
|
508
|
+
|
|
509
|
+
# ── Mint a JWT naming this project, then fetch shared defaults ────────
|
|
510
|
+
jwt, fingerprint_attestation = _mint_jwt_for_project(project_id, target)
|
|
511
|
+
|
|
512
|
+
async def _load_defaults() -> dict:
|
|
513
|
+
from codepreproc_client.layer1_business.api_client.registry_client import get_defaults
|
|
514
|
+
from codepreproc_client.layer1_business.config import get_server_url
|
|
515
|
+
|
|
516
|
+
try:
|
|
517
|
+
config = await get_defaults(base_url=get_server_url(), jwt=jwt)
|
|
518
|
+
return config.model_dump(mode="python")
|
|
519
|
+
except Exception:
|
|
520
|
+
return {}
|
|
521
|
+
|
|
522
|
+
defaults: dict = asyncio.run(_load_defaults())
|
|
523
|
+
|
|
524
|
+
# ── Generate project_context via LLM (optional) ───────────────────────
|
|
525
|
+
if verbose:
|
|
526
|
+
print(" Generating project_context …")
|
|
527
|
+
summary = _gather_summary(target, framework_info)
|
|
528
|
+
context = _generate_context(target.name, summary, defaults)
|
|
529
|
+
if context is None:
|
|
530
|
+
if verbose:
|
|
531
|
+
print(" LLM unavailable — using template fallback.")
|
|
532
|
+
context = _fallback_context(target.name, languages, framework_info)
|
|
533
|
+
|
|
534
|
+
# ── Persist project to PostgreSQL ─────────────────────────────────────
|
|
535
|
+
qdrant_collection = f"{project_id}_code"
|
|
536
|
+
from contracts.dtos import ProjectConfig, ValidationConfig
|
|
537
|
+
|
|
538
|
+
project_obj = ProjectConfig(
|
|
539
|
+
project_id=project_id,
|
|
540
|
+
root=target,
|
|
541
|
+
languages=languages,
|
|
542
|
+
qdrant_collection=qdrant_collection,
|
|
543
|
+
branch_aware=False,
|
|
544
|
+
project_context=context,
|
|
545
|
+
ignore=ignore,
|
|
546
|
+
llm_policy=defaults.get("default_llm_policy", "local_only"),
|
|
547
|
+
validation=ValidationConfig(),
|
|
548
|
+
)
|
|
549
|
+
_persist_project(project_obj, jwt, fingerprint_attestation)
|
|
550
|
+
|
|
551
|
+
if verbose:
|
|
552
|
+
print(f"\nProject '{project_id}' registered")
|
|
553
|
+
print(f" root : {target}")
|
|
554
|
+
print(f" languages : {', '.join(languages)}")
|
|
555
|
+
print(f" qdrant_collection : {qdrant_collection}")
|
|
556
|
+
|
|
557
|
+
# ── Bootstrap in SuperKG via the codepreproc-server proxy (optional, fire-and-forget) ─
|
|
558
|
+
superkg_enabled = os.getenv("SUPERKG_ENABLED", "1").strip().lower() not in {"0", "false", "no", "off"}
|
|
559
|
+
if superkg_enabled:
|
|
560
|
+
from codepreproc_client.layer1_business.api_client.superkg_client import SuperKGClient
|
|
561
|
+
from codepreproc_client.layer1_business.config import get_server_url
|
|
562
|
+
|
|
563
|
+
async def _jwt_provider(_project_id: str) -> str:
|
|
564
|
+
return jwt
|
|
565
|
+
|
|
566
|
+
superkg_client = SuperKGClient(base_url=get_server_url(), jwt_provider=_jwt_provider, enabled=True)
|
|
567
|
+
system_id = asyncio.run(superkg_client.bootstrap(project_id, target.name, context))
|
|
568
|
+
if system_id:
|
|
569
|
+
_set_superkg_id(project_id, system_id, jwt)
|
|
570
|
+
if verbose:
|
|
571
|
+
print(f" superkg_system_id : {system_id}")
|
|
572
|
+
elif verbose:
|
|
573
|
+
print(" superkg : unavailable (skipped)")
|
|
574
|
+
|
|
575
|
+
if verbose:
|
|
576
|
+
print("\nRun 'codepreproc-mcp' to start the server, then use the 'reindex' tool to index the project.")
|
|
577
|
+
|
|
578
|
+
return 0
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""CLI entrypoint for codepreproc.
|
|
2
|
+
|
|
3
|
+
Usage
|
|
4
|
+
-----
|
|
5
|
+
codepreproc init [PATH]
|
|
6
|
+
Register a project in the codepreproc registry.
|
|
7
|
+
PATH defaults to the current working directory.
|
|
8
|
+
Exits immediately with no changes if the project is already registered.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
|
|
18
|
+
load_dotenv(Path(__file__).parents[4] / ".env", override=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_USAGE = """\
|
|
22
|
+
Usage: codepreproc <command> [options]
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
init [PATH] Scan PATH (default: CWD), mint a JWT for it, and register it
|
|
26
|
+
with codepreproc-server over the API.
|
|
27
|
+
Requires credentials.yaml and CODEPREPROC_SERVER_URL to be set.
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-h, --help Show this message and exit.
|
|
31
|
+
|
|
32
|
+
Note: db-init, db-seed, and license-add are codepreproc-server-admin operator
|
|
33
|
+
commands now (they touch the shared Postgres instance directly) - run
|
|
34
|
+
`codepreproc-server-admin` on the server install, not this client.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run() -> None:
|
|
39
|
+
"""Console script entry point: codepreproc."""
|
|
40
|
+
args = sys.argv[1:]
|
|
41
|
+
|
|
42
|
+
if not args or args[0] in ("-h", "--help"):
|
|
43
|
+
print(_USAGE, end="")
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
|
|
46
|
+
command = args[0]
|
|
47
|
+
|
|
48
|
+
if command == "init":
|
|
49
|
+
from codepreproc_client.layer1_business.cli.init import init_project
|
|
50
|
+
|
|
51
|
+
path: Path | None = None
|
|
52
|
+
remaining = [a for a in args[1:] if not a.startswith("-")]
|
|
53
|
+
flags = [a for a in args[1:] if a.startswith("-")]
|
|
54
|
+
|
|
55
|
+
if "-h" in flags or "--help" in flags:
|
|
56
|
+
print("Usage: codepreproc init [PATH]\n")
|
|
57
|
+
print(" Scan PATH (default: CWD) and register it in PostgreSQL.")
|
|
58
|
+
print(" Does nothing if the project root is already registered.")
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
|
|
61
|
+
if remaining:
|
|
62
|
+
path = Path(remaining[0])
|
|
63
|
+
|
|
64
|
+
sys.exit(init_project(path=path))
|
|
65
|
+
|
|
66
|
+
elif command in ("db-init", "db-seed", "license-add"):
|
|
67
|
+
print(
|
|
68
|
+
f"codepreproc: '{command}' is now a codepreproc-server-admin operator command "
|
|
69
|
+
"(it touches the shared Postgres instance directly). Run "
|
|
70
|
+
f"`codepreproc-server-admin` on the server install, not this client.",
|
|
71
|
+
file=sys.stderr,
|
|
72
|
+
)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
print(f"codepreproc: unknown command '{command}'", file=sys.stderr)
|
|
77
|
+
print("Run 'codepreproc --help' for usage.", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|