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.
Files changed (100) hide show
  1. codepreproc_client/__init__.py +14 -0
  2. codepreproc_client/__main__.py +9 -0
  3. codepreproc_client/layer1_business/__init__.py +0 -0
  4. codepreproc_client/layer1_business/allowed_scope_cache.py +62 -0
  5. codepreproc_client/layer1_business/api_client/__init__.py +0 -0
  6. codepreproc_client/layer1_business/api_client/lease_client.py +34 -0
  7. codepreproc_client/layer1_business/api_client/net_guard.py +98 -0
  8. codepreproc_client/layer1_business/api_client/promote_client.py +59 -0
  9. codepreproc_client/layer1_business/api_client/reasoning_client.py +163 -0
  10. codepreproc_client/layer1_business/api_client/registry_client.py +119 -0
  11. codepreproc_client/layer1_business/api_client/superkg_client.py +327 -0
  12. codepreproc_client/layer1_business/cli/__init__.py +0 -0
  13. codepreproc_client/layer1_business/cli/init.py +578 -0
  14. codepreproc_client/layer1_business/cli/main.py +78 -0
  15. codepreproc_client/layer1_business/config.py +104 -0
  16. codepreproc_client/layer1_business/git_utils.py +67 -0
  17. codepreproc_client/layer1_business/license/__init__.py +0 -0
  18. codepreproc_client/layer1_business/license/fingerprint.py +61 -0
  19. codepreproc_client/layer1_business/license/jwt_client.py +60 -0
  20. codepreproc_client/layer1_business/license/project_lock.py +44 -0
  21. codepreproc_client/layer1_business/local_registry.py +40 -0
  22. codepreproc_client/layer1_business/manifest/__init__.py +0 -0
  23. codepreproc_client/layer1_business/manifest/credentials.py +35 -0
  24. codepreproc_client/layer1_business/mcp/__init__.py +0 -0
  25. codepreproc_client/layer1_business/mcp/action_monitor.py +246 -0
  26. codepreproc_client/layer1_business/mcp/lifecycle.py +649 -0
  27. codepreproc_client/layer1_business/mcp/server.py +656 -0
  28. codepreproc_client/layer1_business/mcp/tools.py +1665 -0
  29. codepreproc_client/layer1_business/project_registry.py +99 -0
  30. codepreproc_client/layer2_tooling/__init__.py +0 -0
  31. codepreproc_client/layer2_tooling/condensation/__init__.py +0 -0
  32. codepreproc_client/layer2_tooling/condensation/ast_graph.py +426 -0
  33. codepreproc_client/layer2_tooling/condensation/bm25_store.py +132 -0
  34. codepreproc_client/layer2_tooling/condensation/chunking/__init__.py +0 -0
  35. codepreproc_client/layer2_tooling/condensation/chunking/ast_chunker.py +313 -0
  36. codepreproc_client/layer2_tooling/condensation/chunking/languages.py +238 -0
  37. codepreproc_client/layer2_tooling/condensation/embed_worker.py +128 -0
  38. codepreproc_client/layer2_tooling/condensation/embeddings.py +452 -0
  39. codepreproc_client/layer2_tooling/condensation/extract/__init__.py +0 -0
  40. codepreproc_client/layer2_tooling/condensation/extract/compactor.py +103 -0
  41. codepreproc_client/layer2_tooling/condensation/extract/providers/__init__.py +0 -0
  42. codepreproc_client/layer2_tooling/condensation/extract/providers/base.py +38 -0
  43. codepreproc_client/layer2_tooling/condensation/indexer.py +945 -0
  44. codepreproc_client/layer2_tooling/condensation/ppl/__init__.py +0 -0
  45. codepreproc_client/layer2_tooling/condensation/ppl/emitter.py +162 -0
  46. codepreproc_client/layer2_tooling/condensation/ppl/parser.py +352 -0
  47. codepreproc_client/layer2_tooling/condensation/qdrant_store.py +387 -0
  48. codepreproc_client/layer2_tooling/condensation/scanning/__init__.py +0 -0
  49. codepreproc_client/layer2_tooling/condensation/scanning/manifest_scanner.py +72 -0
  50. codepreproc_client/layer2_tooling/condensation/scanning/md_scanner.py +97 -0
  51. codepreproc_client/layer2_tooling/condensation/scanning/repo_mapper.py +495 -0
  52. codepreproc_client/layer2_tooling/condensation/session_vector_store.py +69 -0
  53. codepreproc_client/layer2_tooling/coupling/__init__.py +0 -0
  54. codepreproc_client/layer2_tooling/coupling/domain.py +263 -0
  55. codepreproc_client/layer2_tooling/coupling/graph.py +255 -0
  56. codepreproc_client/layer2_tooling/coupling/layer2_gate.py +189 -0
  57. codepreproc_client/layer2_tooling/coupling/projector.py +139 -0
  58. codepreproc_client/layer2_tooling/coupling/resolver.py +150 -0
  59. codepreproc_client/layer2_tooling/materialization/__init__.py +0 -0
  60. codepreproc_client/layer2_tooling/materialization/pipeline/__init__.py +0 -0
  61. codepreproc_client/layer2_tooling/materialization/pipeline/execute_semantic.py +1249 -0
  62. codepreproc_client/layer2_tooling/materialization/pipeline/filesystem_reorg.py +575 -0
  63. codepreproc_client/layer2_tooling/materialization/pipeline/patch_generator.py +127 -0
  64. codepreproc_client/layer2_tooling/materialization/pipeline/patch_validator.py +101 -0
  65. codepreproc_client/layer2_tooling/materialization/semantic/__init__.py +19 -0
  66. codepreproc_client/layer2_tooling/materialization/semantic/anchors.py +61 -0
  67. codepreproc_client/layer2_tooling/materialization/semantic/applicability.py +75 -0
  68. codepreproc_client/layer2_tooling/materialization/semantic/edit_planner.py +264 -0
  69. codepreproc_client/layer2_tooling/materialization/semantic/materializer.py +121 -0
  70. codepreproc_client/layer2_tooling/materialization/semantic/merger.py +73 -0
  71. codepreproc_client/layer2_tooling/materialization/semantic/ops/__init__.py +0 -0
  72. codepreproc_client/layer2_tooling/materialization/semantic/ops/add_import.py +36 -0
  73. codepreproc_client/layer2_tooling/materialization/semantic/ops/append_argument.py +37 -0
  74. codepreproc_client/layer2_tooling/materialization/semantic/ops/insert_literal.py +24 -0
  75. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_import.py +33 -0
  76. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_statement_unique.py +14 -0
  77. codepreproc_client/layer2_tooling/materialization/semantic/ops/rename_symbol_local.py +16 -0
  78. codepreproc_client/layer2_tooling/materialization/semantic/region_resolver.py +229 -0
  79. codepreproc_client/layer2_tooling/materialization/semantic/synthesizer.py +186 -0
  80. codepreproc_client/layer2_tooling/materialization/semantic/target_locator.py +94 -0
  81. codepreproc_client/layer2_tooling/materialization/semantic/validator.py +322 -0
  82. codepreproc_client/layer2_tooling/materialization/snippet/__init__.py +0 -0
  83. codepreproc_client/layer2_tooling/materialization/snippet/assembler.py +503 -0
  84. codepreproc_client/layer2_tooling/materialization/snippet/loader.py +187 -0
  85. codepreproc_client/layer2_tooling/retrieval/__init__.py +0 -0
  86. codepreproc_client/layer2_tooling/retrieval/graph_walk.py +47 -0
  87. codepreproc_client/layer2_tooling/retrieval/hybrid.py +95 -0
  88. codepreproc_client/layer2_tooling/retrieval/rerank_worker.py +86 -0
  89. codepreproc_client/layer2_tooling/retrieval/reranker.py +213 -0
  90. codepreproc_client/layer2_tooling/watcher/__init__.py +0 -0
  91. codepreproc_client/layer2_tooling/watcher/fs_watcher.py +3 -0
  92. codrspot_processor_mcp-0.1.0.dist-info/METADATA +396 -0
  93. codrspot_processor_mcp-0.1.0.dist-info/RECORD +100 -0
  94. codrspot_processor_mcp-0.1.0.dist-info/WHEEL +5 -0
  95. codrspot_processor_mcp-0.1.0.dist-info/entry_points.txt +3 -0
  96. codrspot_processor_mcp-0.1.0.dist-info/licenses/LICENSE +23 -0
  97. codrspot_processor_mcp-0.1.0.dist-info/top_level.txt +2 -0
  98. contracts/__init__.py +0 -0
  99. contracts/dtos.py +1239 -0
  100. 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)