codevira 1.6.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.
- codevira-1.6.0.dist-info/LICENSE +21 -0
- codevira-1.6.0.dist-info/METADATA +477 -0
- codevira-1.6.0.dist-info/RECORD +58 -0
- codevira-1.6.0.dist-info/WHEEL +5 -0
- codevira-1.6.0.dist-info/entry_points.txt +2 -0
- codevira-1.6.0.dist-info/top_level.txt +2 -0
- indexer/__init__.py +1 -0
- indexer/chunker.py +428 -0
- indexer/global_db.py +197 -0
- indexer/graph_generator.py +380 -0
- indexer/index_codebase.py +588 -0
- indexer/outcome_tracker.py +172 -0
- indexer/rule_learner.py +186 -0
- indexer/sqlite_graph.py +640 -0
- indexer/treesitter_parser.py +423 -0
- mcp_server/__init__.py +1 -0
- mcp_server/__main__.py +20 -0
- mcp_server/auto_init.py +257 -0
- mcp_server/cli.py +622 -0
- mcp_server/crash_logger.py +236 -0
- mcp_server/data/__init__.py +1 -0
- mcp_server/data/agents/builder.md +84 -0
- mcp_server/data/agents/developer.md +111 -0
- mcp_server/data/agents/documenter.md +138 -0
- mcp_server/data/agents/orchestrator.md +96 -0
- mcp_server/data/agents/planner.md +106 -0
- mcp_server/data/agents/reviewer.md +82 -0
- mcp_server/data/agents/tester.md +83 -0
- mcp_server/data/config.example.yaml +33 -0
- mcp_server/data/rules/coding-standards.md +48 -0
- mcp_server/data/rules/engineering-excellence.md +28 -0
- mcp_server/data/rules/git-cicd-governance.md +32 -0
- mcp_server/data/rules/git_commits.md +130 -0
- mcp_server/data/rules/incremental-updates.md +5 -0
- mcp_server/data/rules/master_rule.md +187 -0
- mcp_server/data/rules/multi-language.md +19 -0
- mcp_server/data/rules/persistence.md +21 -0
- mcp_server/data/rules/resilience-observability.md +17 -0
- mcp_server/data/rules/smoke-testing.md +48 -0
- mcp_server/data/rules/testing-standards.md +23 -0
- mcp_server/detect.py +284 -0
- mcp_server/gitignore.py +284 -0
- mcp_server/global_sync.py +187 -0
- mcp_server/http_server.py +341 -0
- mcp_server/ide_inject.py +444 -0
- mcp_server/launchd.py +156 -0
- mcp_server/migrate.py +215 -0
- mcp_server/paths.py +256 -0
- mcp_server/prompts.py +136 -0
- mcp_server/server.py +1049 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/changesets.py +223 -0
- mcp_server/tools/code_reader.py +335 -0
- mcp_server/tools/graph.py +637 -0
- mcp_server/tools/learning.py +238 -0
- mcp_server/tools/playbook.py +89 -0
- mcp_server/tools/roadmap.py +599 -0
- mcp_server/tools/search.py +145 -0
mcp_server/migrate.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
migrate.py — Legacy → Centralized storage migration for Codevira v1.6.
|
|
3
|
+
|
|
4
|
+
Migrates per-project .codevira/ directories into ~/.codevira/projects/<key>/
|
|
5
|
+
so that data lives centrally and no longer pollutes the project tree.
|
|
6
|
+
|
|
7
|
+
Key behaviors:
|
|
8
|
+
- Idempotent: safe to call multiple times; second call is a no-op.
|
|
9
|
+
- Non-destructive: renames old .codevira/ to .codevira.migrated/ (not deleted).
|
|
10
|
+
- SQLite-safe: uses sqlite3.Connection.backup() to copy WAL-mode databases.
|
|
11
|
+
- Partial-migration recovery: if metadata.json is missing from centralized dir,
|
|
12
|
+
migration is re-run from scratch.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import shutil
|
|
19
|
+
import sqlite3
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_CODEVIRA_VERSION = "1.6.0"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect_migration_needed(project_root: Path) -> bool:
|
|
29
|
+
"""Return True if the project has a legacy .codevira/ that needs migration.
|
|
30
|
+
|
|
31
|
+
Conditions:
|
|
32
|
+
- <project_root>/.codevira/config.yaml exists (legacy initialized project)
|
|
33
|
+
- No corresponding centralized dir with metadata.json exists yet
|
|
34
|
+
"""
|
|
35
|
+
from mcp_server.paths import _sanitize_path_key, get_global_home
|
|
36
|
+
|
|
37
|
+
legacy = project_root / ".codevira"
|
|
38
|
+
if not (legacy / "config.yaml").is_file():
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
key = _sanitize_path_key(project_root)
|
|
42
|
+
centralized = get_global_home() / "projects" / key
|
|
43
|
+
# Migration complete only if metadata.json is present
|
|
44
|
+
if (centralized / "metadata.json").is_file():
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def migrate_to_centralized(project_root: Path) -> dict:
|
|
51
|
+
"""Migrate <project_root>/.codevira/ to ~/.codevira/projects/<key>/.
|
|
52
|
+
|
|
53
|
+
Returns a summary dict:
|
|
54
|
+
{migrated: True, files_copied: N, old_path: str, new_path: str}
|
|
55
|
+
or
|
|
56
|
+
{migrated: False, reason: str}
|
|
57
|
+
|
|
58
|
+
Migration steps:
|
|
59
|
+
1. Create centralized directory structure
|
|
60
|
+
2. Copy config.yaml and roadmap.yaml
|
|
61
|
+
3. Copy graph.db via sqlite3 backup API (WAL-safe)
|
|
62
|
+
4. Copy codeindex/ directory (ChromaDB persistent storage)
|
|
63
|
+
5. Write metadata.json
|
|
64
|
+
6. Update global.db project registry
|
|
65
|
+
7. Rename old .codevira/ → .codevira.migrated/ (safety net)
|
|
66
|
+
"""
|
|
67
|
+
from mcp_server.paths import _sanitize_path_key, _get_git_remote_url, get_global_home, get_global_db_path
|
|
68
|
+
|
|
69
|
+
legacy = project_root / ".codevira"
|
|
70
|
+
if not (legacy / "config.yaml").is_file():
|
|
71
|
+
return {"migrated": False, "reason": "No legacy .codevira/config.yaml found"}
|
|
72
|
+
|
|
73
|
+
key = _sanitize_path_key(project_root)
|
|
74
|
+
centralized = get_global_home() / "projects" / key
|
|
75
|
+
|
|
76
|
+
# Already migrated?
|
|
77
|
+
if (centralized / "metadata.json").is_file():
|
|
78
|
+
return {"migrated": False, "reason": "Already migrated"}
|
|
79
|
+
|
|
80
|
+
logger.info("Migrating %s → %s", legacy, centralized)
|
|
81
|
+
|
|
82
|
+
# Create directory structure
|
|
83
|
+
(centralized / "graph" / "changesets").mkdir(parents=True, exist_ok=True)
|
|
84
|
+
(centralized / "codeindex").mkdir(parents=True, exist_ok=True)
|
|
85
|
+
(centralized / "logs").mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
files_copied = 0
|
|
88
|
+
|
|
89
|
+
# 1. Copy config.yaml
|
|
90
|
+
src_config = legacy / "config.yaml"
|
|
91
|
+
dst_config = centralized / "config.yaml"
|
|
92
|
+
shutil.copy2(src_config, dst_config)
|
|
93
|
+
files_copied += 1
|
|
94
|
+
|
|
95
|
+
# 2. Copy roadmap.yaml (may not exist)
|
|
96
|
+
src_roadmap = legacy / "roadmap.yaml"
|
|
97
|
+
if src_roadmap.exists():
|
|
98
|
+
shutil.copy2(src_roadmap, centralized / "roadmap.yaml")
|
|
99
|
+
files_copied += 1
|
|
100
|
+
|
|
101
|
+
# 3. Copy graph.db via sqlite3 backup API (safe under WAL mode)
|
|
102
|
+
src_db = legacy / "graph" / "graph.db"
|
|
103
|
+
dst_db = centralized / "graph" / "graph.db"
|
|
104
|
+
if src_db.exists():
|
|
105
|
+
src_conn = None
|
|
106
|
+
dst_conn = None
|
|
107
|
+
try:
|
|
108
|
+
src_conn = sqlite3.connect(str(src_db))
|
|
109
|
+
dst_conn = sqlite3.connect(str(dst_db))
|
|
110
|
+
src_conn.backup(dst_conn)
|
|
111
|
+
files_copied += 1
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning("Could not backup graph.db via API, falling back to copy: %s", e)
|
|
114
|
+
# Fallback: copy main db + WAL/SHM files if present
|
|
115
|
+
shutil.copy2(src_db, dst_db)
|
|
116
|
+
for suffix in ("-wal", "-shm"):
|
|
117
|
+
wal_file = src_db.parent / (src_db.name + suffix)
|
|
118
|
+
if wal_file.exists():
|
|
119
|
+
shutil.copy2(wal_file, dst_db.parent / (dst_db.name + suffix))
|
|
120
|
+
files_copied += 1
|
|
121
|
+
finally:
|
|
122
|
+
if src_conn:
|
|
123
|
+
src_conn.close()
|
|
124
|
+
if dst_conn:
|
|
125
|
+
dst_conn.close()
|
|
126
|
+
|
|
127
|
+
# 4. Copy codeindex/ (ChromaDB directory)
|
|
128
|
+
src_index = legacy / "codeindex"
|
|
129
|
+
dst_index = centralized / "codeindex"
|
|
130
|
+
if src_index.exists() and src_index.is_dir():
|
|
131
|
+
if dst_index.exists():
|
|
132
|
+
shutil.rmtree(dst_index)
|
|
133
|
+
shutil.copytree(src_index, dst_index)
|
|
134
|
+
files_copied += 1
|
|
135
|
+
|
|
136
|
+
# 5. Write metadata.json
|
|
137
|
+
git_remote = _get_git_remote_url(project_root)
|
|
138
|
+
metadata = {
|
|
139
|
+
"path_key": key,
|
|
140
|
+
"git_remote": git_remote,
|
|
141
|
+
"original_path": str(project_root),
|
|
142
|
+
"migrated_at": datetime.now(timezone.utc).isoformat(),
|
|
143
|
+
"version": _CODEVIRA_VERSION,
|
|
144
|
+
}
|
|
145
|
+
(centralized / "metadata.json").write_text(json.dumps(metadata, indent=2))
|
|
146
|
+
|
|
147
|
+
# 6. Update global.db project registry
|
|
148
|
+
gdb = None
|
|
149
|
+
try:
|
|
150
|
+
from indexer.global_db import GlobalDB
|
|
151
|
+
gdb = GlobalDB(get_global_db_path())
|
|
152
|
+
_ensure_git_remote_column(gdb)
|
|
153
|
+
gdb.register_project(
|
|
154
|
+
path=str(centralized),
|
|
155
|
+
name=project_root.name,
|
|
156
|
+
language="unknown",
|
|
157
|
+
git_remote=git_remote,
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.warning("Could not update global.db during migration: %s", e)
|
|
161
|
+
finally:
|
|
162
|
+
if gdb is not None:
|
|
163
|
+
gdb.close()
|
|
164
|
+
|
|
165
|
+
# Invalidate the data-dir cache so get_data_dir() now returns the newly
|
|
166
|
+
# populated centralized directory, not the old legacy path.
|
|
167
|
+
try:
|
|
168
|
+
from mcp_server.paths import invalidate_data_dir_cache
|
|
169
|
+
invalidate_data_dir_cache(project_root)
|
|
170
|
+
except Exception:
|
|
171
|
+
pass # Cache invalidation is best-effort; migration proceeds regardless
|
|
172
|
+
|
|
173
|
+
# 7. Rename old .codevira/ → .codevira.migrated/ (safety net, not deleted)
|
|
174
|
+
migrated_backup = project_root / ".codevira.migrated"
|
|
175
|
+
if migrated_backup.exists():
|
|
176
|
+
shutil.rmtree(migrated_backup)
|
|
177
|
+
legacy.rename(migrated_backup)
|
|
178
|
+
|
|
179
|
+
logger.info(
|
|
180
|
+
"Migration complete: %d files copied. Legacy dir renamed to %s",
|
|
181
|
+
files_copied,
|
|
182
|
+
migrated_backup,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"migrated": True,
|
|
187
|
+
"files_copied": files_copied,
|
|
188
|
+
"old_path": str(legacy),
|
|
189
|
+
"new_path": str(centralized),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def cleanup_legacy_dir(project_root: Path) -> bool:
|
|
194
|
+
"""Remove the .codevira.migrated/ safety-net directory after confirmation.
|
|
195
|
+
|
|
196
|
+
Returns True if the directory was removed, False if it didn't exist.
|
|
197
|
+
Call only after verifying the migration was successful.
|
|
198
|
+
"""
|
|
199
|
+
backup = project_root / ".codevira.migrated"
|
|
200
|
+
if backup.exists():
|
|
201
|
+
shutil.rmtree(backup)
|
|
202
|
+
logger.info("Removed legacy backup dir: %s", backup)
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _ensure_git_remote_column(gdb) -> None:
|
|
208
|
+
"""Add git_remote column to global_db.projects if not present (v1.6 schema upgrade)."""
|
|
209
|
+
try:
|
|
210
|
+
cols = [row[1] for row in gdb.conn.execute("PRAGMA table_info(projects)").fetchall()]
|
|
211
|
+
if "git_remote" not in cols:
|
|
212
|
+
gdb.conn.execute("ALTER TABLE projects ADD COLUMN git_remote TEXT")
|
|
213
|
+
gdb.conn.commit()
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
mcp_server/paths.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
paths.py — Centralized path resolution for Codevira.
|
|
3
|
+
|
|
4
|
+
Resolution priority for get_data_dir():
|
|
5
|
+
1. Centralized ~/.codevira/projects/<key>/ — new in v1.6 (keyed by project path)
|
|
6
|
+
2. Git remote lookup — survives directory renames
|
|
7
|
+
3. Legacy <project_root>/.codevira/ — backward compat for existing projects
|
|
8
|
+
4. Default to centralized path for brand-new projects
|
|
9
|
+
|
|
10
|
+
Project root discovery (get_project_root()):
|
|
11
|
+
- Uses --project-dir CLI override if set
|
|
12
|
+
- Walks upward from cwd looking for project markers:
|
|
13
|
+
.git, pyproject.toml, package.json, go.mod, Cargo.toml, .codevira/
|
|
14
|
+
- Falls back to cwd if no marker found
|
|
15
|
+
|
|
16
|
+
Performance notes:
|
|
17
|
+
- get_data_dir() result is cached per project root (_data_dir_cache).
|
|
18
|
+
First call may spawn one `git remote` subprocess and scan metadata files;
|
|
19
|
+
every subsequent call for the same root is a dict lookup (~0µs).
|
|
20
|
+
- Call invalidate_data_dir_cache() after init/migration to force re-resolution.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import subprocess
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
# Allow overriding project directory via CLI flag (e.g. for Google Antigravity
|
|
30
|
+
# which doesn't support the `cwd` option in its MCP config).
|
|
31
|
+
_project_dir_override: Path | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_project_dir(path: str | Path) -> None:
|
|
35
|
+
"""Override the project directory (called by CLI when --project-dir is passed).
|
|
36
|
+
|
|
37
|
+
Also clears the data-dir cache so subsequent get_data_dir() calls
|
|
38
|
+
resolve against the new project root, not a stale cached entry.
|
|
39
|
+
"""
|
|
40
|
+
global _project_dir_override
|
|
41
|
+
_project_dir_override = Path(path).resolve()
|
|
42
|
+
invalidate_data_dir_cache()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Path-key helpers (for centralized storage)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
#: Markers that identify a project root when walking upward.
|
|
50
|
+
_PROJECT_MARKERS = frozenset({
|
|
51
|
+
".git",
|
|
52
|
+
"pyproject.toml",
|
|
53
|
+
"package.json",
|
|
54
|
+
"go.mod",
|
|
55
|
+
"Cargo.toml",
|
|
56
|
+
".codevira",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sanitize_path_key(abs_path: str | Path) -> str:
|
|
61
|
+
"""Convert an absolute path to a filesystem-safe key string.
|
|
62
|
+
|
|
63
|
+
Uses a short hash suffix to prevent collisions between paths that
|
|
64
|
+
differ only in separator characters (e.g. /foo-bar vs /foo/bar) or
|
|
65
|
+
drive letters (D:\\Projects\\Foo vs C:\\Projects\\Foo).
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
/Users/sachin/Projects/Foo → Users_sachin_Projects_Foo_a1b2c3d4
|
|
69
|
+
C:\\Users\\sachin\\Projects → Users_sachin_Projects_a1b2c3d4
|
|
70
|
+
"""
|
|
71
|
+
import hashlib
|
|
72
|
+
resolved = str(Path(abs_path).resolve())
|
|
73
|
+
# Hash the FULL resolved path (before any lossy transforms) for uniqueness
|
|
74
|
+
path_hash = hashlib.sha256(resolved.encode()).hexdigest()[:8]
|
|
75
|
+
# Strip drive letter and leading separators for the human-readable part
|
|
76
|
+
stripped = re.sub(r"^[A-Za-z]:", "", resolved) # Windows drive letter
|
|
77
|
+
stripped = stripped.lstrip("/\\")
|
|
78
|
+
# Replace path separators with underscores (not hyphens — preserves
|
|
79
|
+
# literal hyphens in directory names as distinct from separators)
|
|
80
|
+
key = re.sub(r"[/\\]", "_", stripped)
|
|
81
|
+
# Replace any remaining non-safe chars with hyphens
|
|
82
|
+
key = re.sub(r"[^a-zA-Z0-9._-]", "-", key)
|
|
83
|
+
# Collapse consecutive underscores/hyphens
|
|
84
|
+
key = re.sub(r"[_-]{2,}", "_", key)
|
|
85
|
+
key = key.strip("_-")
|
|
86
|
+
return f"{key}_{path_hash}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_git_remote_url(project_root: Path) -> str | None:
|
|
90
|
+
"""Return the git remote 'origin' URL for project_root, or None."""
|
|
91
|
+
try:
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
["git", "-C", str(project_root), "remote", "get-url", "origin"],
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
timeout=3,
|
|
97
|
+
)
|
|
98
|
+
if result.returncode == 0:
|
|
99
|
+
url = result.stdout.strip()
|
|
100
|
+
return url if url else None
|
|
101
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
102
|
+
pass
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _find_project_by_git_remote(remote_url: str) -> Path | None:
|
|
107
|
+
"""Scan ~/.codevira/projects/ for a project whose metadata.json matches remote_url."""
|
|
108
|
+
projects_dir = get_global_home() / "projects"
|
|
109
|
+
if not projects_dir.exists():
|
|
110
|
+
return None
|
|
111
|
+
for meta_file in projects_dir.glob("*/metadata.json"):
|
|
112
|
+
try:
|
|
113
|
+
meta = json.loads(meta_file.read_text())
|
|
114
|
+
if meta.get("git_remote") == remote_url:
|
|
115
|
+
# Return the centralized data dir (the directory containing metadata.json)
|
|
116
|
+
return meta_file.parent
|
|
117
|
+
except (json.JSONDecodeError, OSError):
|
|
118
|
+
continue
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Project root discovery
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def _discover_project_root(start: Path) -> Path:
|
|
127
|
+
"""Walk upward from *start* to find the nearest project root.
|
|
128
|
+
|
|
129
|
+
A project root is any ancestor directory that contains at least one
|
|
130
|
+
of: .git, pyproject.toml, package.json, go.mod, Cargo.toml, .codevira/
|
|
131
|
+
|
|
132
|
+
Stops at the first match so that nested repos return the inner root.
|
|
133
|
+
Falls back to *start* if no marker is found.
|
|
134
|
+
"""
|
|
135
|
+
start = start.resolve()
|
|
136
|
+
for candidate in (start, *start.parents):
|
|
137
|
+
for marker in _PROJECT_MARKERS:
|
|
138
|
+
if (candidate / marker).exists():
|
|
139
|
+
return candidate
|
|
140
|
+
return start
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_project_root() -> Path:
|
|
144
|
+
"""Return the project root directory.
|
|
145
|
+
|
|
146
|
+
Uses --project-dir override if set (for Google Antigravity),
|
|
147
|
+
otherwise falls back to the current working directory.
|
|
148
|
+
"""
|
|
149
|
+
if _project_dir_override is not None:
|
|
150
|
+
return _discover_project_root(_project_dir_override)
|
|
151
|
+
return _discover_project_root(Path.cwd())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Data directory resolution (v1.6 centralized + legacy fallback)
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Data directory cache — avoids re-running subprocess + glob on every tool call.
|
|
160
|
+
# Keyed by resolved project root Path. Invalidated after init/migration.
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
_data_dir_cache: dict[Path, Path] = {}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def invalidate_data_dir_cache(project_root: Path | None = None) -> None:
|
|
166
|
+
"""Clear the data-dir cache so the next call re-resolves from disk.
|
|
167
|
+
|
|
168
|
+
Call this after codevira init or after a migration completes, when the
|
|
169
|
+
centralized directory has just been created and the cache entry would
|
|
170
|
+
still point to the old (non-existent) default path.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
project_root: If given, only invalidate that project's entry.
|
|
174
|
+
If None, clear the entire cache.
|
|
175
|
+
"""
|
|
176
|
+
if project_root is None:
|
|
177
|
+
_data_dir_cache.clear()
|
|
178
|
+
else:
|
|
179
|
+
_data_dir_cache.pop(Path(project_root).resolve(), None)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_data_dir() -> Path:
|
|
183
|
+
"""Return the Codevira data directory for the current project.
|
|
184
|
+
|
|
185
|
+
Resolution chain (run once per project root, then cached):
|
|
186
|
+
1. Centralized ~/.codevira/projects/<key>/ if config.yaml exists there
|
|
187
|
+
2. Git remote lookup — finds centralized dir even after directory rename
|
|
188
|
+
3. Legacy <project_root>/.codevira/ if config.yaml exists there
|
|
189
|
+
4. Default to centralized path (new projects land here automatically)
|
|
190
|
+
|
|
191
|
+
After the first call for a given project root the result is cached in
|
|
192
|
+
_data_dir_cache. Subsequent calls are O(1) dict lookups with no I/O.
|
|
193
|
+
Call invalidate_data_dir_cache() after init or migration to force refresh.
|
|
194
|
+
"""
|
|
195
|
+
project_root = get_project_root()
|
|
196
|
+
|
|
197
|
+
# Fast path — already resolved for this root
|
|
198
|
+
if project_root in _data_dir_cache:
|
|
199
|
+
return _data_dir_cache[project_root]
|
|
200
|
+
|
|
201
|
+
result = _resolve_data_dir(project_root)
|
|
202
|
+
_data_dir_cache[project_root] = result
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _resolve_data_dir(project_root: Path) -> Path:
|
|
207
|
+
"""Perform the actual (potentially slow) data-dir resolution.
|
|
208
|
+
|
|
209
|
+
This is the only place that spawns subprocesses or reads metadata files.
|
|
210
|
+
Always call get_data_dir() in production code — it caches this result.
|
|
211
|
+
"""
|
|
212
|
+
key = _sanitize_path_key(project_root)
|
|
213
|
+
centralized = get_global_home() / "projects" / key
|
|
214
|
+
|
|
215
|
+
# 1. Centralized dir already initialized?
|
|
216
|
+
if (centralized / "config.yaml").is_file():
|
|
217
|
+
return centralized
|
|
218
|
+
|
|
219
|
+
# 2. Try git remote lookup (survives directory renames).
|
|
220
|
+
# _get_git_remote_url() and _find_project_by_git_remote() are the
|
|
221
|
+
# potentially expensive operations — subprocess + metadata file scan.
|
|
222
|
+
# They only run once per project root thanks to the cache above.
|
|
223
|
+
remote_url = _get_git_remote_url(project_root)
|
|
224
|
+
if remote_url:
|
|
225
|
+
found = _find_project_by_git_remote(remote_url)
|
|
226
|
+
if found is not None:
|
|
227
|
+
return found
|
|
228
|
+
|
|
229
|
+
# 3. Legacy in-project .codevira/ (backward compat for v1.5 and earlier)
|
|
230
|
+
legacy = project_root / ".codevira"
|
|
231
|
+
if (legacy / "config.yaml").is_file():
|
|
232
|
+
return legacy
|
|
233
|
+
|
|
234
|
+
# 4. Default to centralized path — new project, will be created on init
|
|
235
|
+
return centralized
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_package_data_dir() -> Path:
|
|
239
|
+
"""Return the bundled data directory that ships with the pip package.
|
|
240
|
+
|
|
241
|
+
Contains: rules/, agents/, config.example.yaml
|
|
242
|
+
These are read-only assets installed alongside the package.
|
|
243
|
+
"""
|
|
244
|
+
return Path(__file__).parent / "data"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_global_home() -> Path:
|
|
248
|
+
"""Return ~/.codevira/ global data directory. Creates it if needed."""
|
|
249
|
+
home = Path.home() / ".codevira"
|
|
250
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
return home
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_global_db_path() -> Path:
|
|
255
|
+
"""Return path to the global SQLite database for cross-project intelligence."""
|
|
256
|
+
return get_global_home() / "global.db"
|
mcp_server/prompts.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
prompts.py — MCP workflow prompt templates.
|
|
3
|
+
|
|
4
|
+
Five pre-built prompts that give AI agents structured starting points
|
|
5
|
+
for common workflows: review, debug, onboard, pre-commit, and architecture.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
PROMPTS = {
|
|
10
|
+
"review_changes": {
|
|
11
|
+
"name": "review_changes",
|
|
12
|
+
"description": "Review current changes with risk analysis, test coverage gaps, and learned rules.",
|
|
13
|
+
"arguments": [
|
|
14
|
+
{"name": "base_ref", "description": "Base git ref to diff against (default: main)", "required": False},
|
|
15
|
+
],
|
|
16
|
+
"template": (
|
|
17
|
+
"Review the current code changes for quality, risks, and missed tests.\n\n"
|
|
18
|
+
"Steps:\n"
|
|
19
|
+
"1. Call analyze_changes() to get function-level risk scores and test coverage gaps.\n"
|
|
20
|
+
"2. Call get_learned_rules() to check if any changes violate learned patterns.\n"
|
|
21
|
+
"3. Call get_preferences() to verify the changes match the developer's coding style.\n"
|
|
22
|
+
"4. For each HIGH risk change, call query_graph() to understand callers and downstream impact.\n\n"
|
|
23
|
+
"Present a structured review with:\n"
|
|
24
|
+
"- Risk summary (high/medium/low changes)\n"
|
|
25
|
+
"- Test coverage gaps\n"
|
|
26
|
+
"- Style violations\n"
|
|
27
|
+
"- Suggested improvements"
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
"debug_issue": {
|
|
31
|
+
"name": "debug_issue",
|
|
32
|
+
"description": "Debug an issue by tracing code paths, searching past decisions, and checking confidence.",
|
|
33
|
+
"arguments": [
|
|
34
|
+
{"name": "description", "description": "Description of the issue to debug", "required": True},
|
|
35
|
+
],
|
|
36
|
+
"template": (
|
|
37
|
+
"Debug the following issue: {description}\n\n"
|
|
38
|
+
"Steps:\n"
|
|
39
|
+
"1. Call search_codebase() with the issue description to find relevant code.\n"
|
|
40
|
+
"2. Call query_graph() on the most relevant files to trace callers and callees.\n"
|
|
41
|
+
"3. Call search_decisions() to check if similar issues were addressed before.\n"
|
|
42
|
+
"4. Call get_decision_confidence() on affected files to check if this is an ambiguous area.\n"
|
|
43
|
+
"5. Call get_impact() on the likely source file to understand blast radius.\n\n"
|
|
44
|
+
"Present:\n"
|
|
45
|
+
"- Most likely root cause with evidence\n"
|
|
46
|
+
"- Affected code paths\n"
|
|
47
|
+
"- Past decisions that relate to this area\n"
|
|
48
|
+
"- Recommended fix approach"
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
"onboard_session": {
|
|
52
|
+
"name": "onboard_session",
|
|
53
|
+
"description": "Start a new coding session with full context — roadmap, open work, rules, and recent history.",
|
|
54
|
+
"arguments": [],
|
|
55
|
+
"template": (
|
|
56
|
+
"Orient to this project and prepare for a productive coding session.\n\n"
|
|
57
|
+
"Steps:\n"
|
|
58
|
+
"1. Call get_session_context() for a complete catch-up: roadmap phase, open changesets, "
|
|
59
|
+
"recent decisions, confidence scores, preferences, and learned rules.\n"
|
|
60
|
+
"2. Call list_open_changesets() to check for interrupted multi-file work.\n"
|
|
61
|
+
"3. Call get_project_maturity() to understand overall project intelligence level.\n\n"
|
|
62
|
+
"Present a concise session briefing:\n"
|
|
63
|
+
"- Current project phase and next action\n"
|
|
64
|
+
"- Any open changesets that need resuming\n"
|
|
65
|
+
"- Key rules and preferences to follow\n"
|
|
66
|
+
"- Areas of low confidence that need extra care"
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
"pre_commit_check": {
|
|
70
|
+
"name": "pre_commit_check",
|
|
71
|
+
"description": "Pre-commit review — check staged changes for risks and missing tests.",
|
|
72
|
+
"arguments": [],
|
|
73
|
+
"template": (
|
|
74
|
+
"Review the staged changes before committing.\n\n"
|
|
75
|
+
"Steps:\n"
|
|
76
|
+
"1. Call analyze_changes(base_ref='HEAD') to review staged changes at function level.\n"
|
|
77
|
+
"2. Call find_hotspots() to check if any modified functions are complexity hotspots.\n"
|
|
78
|
+
"3. Call get_learned_rules() to verify no patterns are violated.\n\n"
|
|
79
|
+
"Present:\n"
|
|
80
|
+
"- Changes summary with risk scores\n"
|
|
81
|
+
"- Missing test coverage\n"
|
|
82
|
+
"- Hotspot warnings (large functions, high fan-in)\n"
|
|
83
|
+
"- Go/no-go recommendation"
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
"architecture_overview": {
|
|
87
|
+
"name": "architecture_overview",
|
|
88
|
+
"description": "Show project architecture — dependency graph, layers, hotspots, and maturity.",
|
|
89
|
+
"arguments": [],
|
|
90
|
+
"template": (
|
|
91
|
+
"Generate an architecture overview of this project.\n\n"
|
|
92
|
+
"Steps:\n"
|
|
93
|
+
"1. Call export_graph(format='mermaid') to generate a dependency diagram.\n"
|
|
94
|
+
"2. Call list_nodes() to see all files organized by layer.\n"
|
|
95
|
+
"3. Call find_hotspots() to identify complexity and risk areas.\n"
|
|
96
|
+
"4. Call get_project_maturity() for overall intelligence metrics.\n\n"
|
|
97
|
+
"Present:\n"
|
|
98
|
+
"- Dependency diagram (Mermaid)\n"
|
|
99
|
+
"- Layer breakdown (API, service, database, utility, test)\n"
|
|
100
|
+
"- Hotspots and risk areas\n"
|
|
101
|
+
"- Maturity score and what it means"
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def list_prompts() -> list[dict]:
|
|
108
|
+
"""Return all available prompts for MCP list_prompts()."""
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
"name": p["name"],
|
|
112
|
+
"description": p["description"],
|
|
113
|
+
"arguments": p.get("arguments", []),
|
|
114
|
+
}
|
|
115
|
+
for p in PROMPTS.values()
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_prompt(name: str, arguments: dict | None = None) -> dict | None:
|
|
120
|
+
"""Return a prompt template with arguments substituted."""
|
|
121
|
+
prompt = PROMPTS.get(name)
|
|
122
|
+
if not prompt:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
template = prompt["template"]
|
|
126
|
+
if arguments:
|
|
127
|
+
for key, value in arguments.items():
|
|
128
|
+
template = template.replace(f"{{{key}}}", str(value))
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"name": prompt["name"],
|
|
132
|
+
"description": prompt["description"],
|
|
133
|
+
"messages": [
|
|
134
|
+
{"role": "user", "content": {"type": "text", "text": template}},
|
|
135
|
+
],
|
|
136
|
+
}
|