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/ide_inject.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ide_inject.py — Auto-detect installed AI tools and inject MCP configuration.
|
|
3
|
+
|
|
4
|
+
Detects Claude Code, Claude Desktop, Cursor, Windsurf, and Google Antigravity,
|
|
5
|
+
then writes the correct MCP server config to each tool's settings file.
|
|
6
|
+
Non-destructive merge: only touches the 'codevira' entry, preserves everything else.
|
|
7
|
+
|
|
8
|
+
v1.6 additions:
|
|
9
|
+
- Claude Desktop support (stdio-only, requires full binary path + --project-dir)
|
|
10
|
+
- Global mode: inject once with no project path, works for every project
|
|
11
|
+
- HTTP URL injection for Claude Code CLI
|
|
12
|
+
- Windows cross-platform fix for sysconfig path resolution
|
|
13
|
+
- Antigravity server name sanitization (handles special chars)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# IDE detection
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def detect_installed_ides(project_root: Path) -> list[str]:
|
|
32
|
+
"""Detect which AI coding tools are installed."""
|
|
33
|
+
found: list[str] = []
|
|
34
|
+
|
|
35
|
+
# Claude Code: per-project .claude/ or claude binary in PATH
|
|
36
|
+
if (project_root / ".claude").is_dir() or shutil.which("claude"):
|
|
37
|
+
found.append("claude")
|
|
38
|
+
|
|
39
|
+
# Claude Desktop: check for its config directory
|
|
40
|
+
desktop_cfg_dir = _claude_desktop_config_path().parent
|
|
41
|
+
if desktop_cfg_dir.exists():
|
|
42
|
+
found.append("claude_desktop")
|
|
43
|
+
|
|
44
|
+
# Cursor: global ~/.cursor/ or cursor binary
|
|
45
|
+
if (Path.home() / ".cursor").is_dir() or shutil.which("cursor"):
|
|
46
|
+
found.append("cursor")
|
|
47
|
+
|
|
48
|
+
# Windsurf: global ~/.windsurf/ or ~/.codeium/windsurf/
|
|
49
|
+
if (Path.home() / ".windsurf").is_dir() or (Path.home() / ".codeium" / "windsurf").is_dir():
|
|
50
|
+
found.append("windsurf")
|
|
51
|
+
|
|
52
|
+
# Google Antigravity: global ~/.gemini/
|
|
53
|
+
if (Path.home() / ".gemini").is_dir():
|
|
54
|
+
found.append("antigravity")
|
|
55
|
+
|
|
56
|
+
return found
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Config file paths
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _claude_config_path(project_root: Path) -> Path:
|
|
64
|
+
return project_root / ".claude" / "settings.json"
|
|
65
|
+
|
|
66
|
+
def _claude_global_config_path() -> Path:
|
|
67
|
+
return Path.home() / ".claude" / "settings.json"
|
|
68
|
+
|
|
69
|
+
def _claude_desktop_config_path() -> Path:
|
|
70
|
+
"""Return the Claude Desktop config file path (platform-aware)."""
|
|
71
|
+
if sys.platform == "darwin":
|
|
72
|
+
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
73
|
+
elif sys.platform == "win32":
|
|
74
|
+
appdata = Path(sys.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
75
|
+
return appdata / "Claude" / "claude_desktop_config.json"
|
|
76
|
+
else:
|
|
77
|
+
# Linux / other
|
|
78
|
+
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
79
|
+
|
|
80
|
+
def _cursor_config_path(project_root: Path) -> Path:
|
|
81
|
+
return project_root / ".cursor" / "mcp.json"
|
|
82
|
+
|
|
83
|
+
def _cursor_global_config_path() -> Path:
|
|
84
|
+
return Path.home() / ".cursor" / "mcp.json"
|
|
85
|
+
|
|
86
|
+
def _windsurf_config_path(project_root: Path) -> Path:
|
|
87
|
+
return project_root / ".windsurf" / "mcp.json"
|
|
88
|
+
|
|
89
|
+
def _windsurf_global_config_path() -> Path:
|
|
90
|
+
"""Return global Windsurf MCP config path."""
|
|
91
|
+
if (Path.home() / ".codeium" / "windsurf").is_dir():
|
|
92
|
+
return Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
|
|
93
|
+
return Path.home() / ".windsurf" / "mcp_config.json"
|
|
94
|
+
|
|
95
|
+
def _antigravity_config_path() -> Path:
|
|
96
|
+
return Path.home() / ".gemini" / "settings" / "mcp_config.json"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# JSON helpers
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _read_json_safe(path: Path) -> dict:
|
|
104
|
+
"""Read a JSON file, returning {} if missing or corrupt."""
|
|
105
|
+
if not path.exists():
|
|
106
|
+
return {}
|
|
107
|
+
try:
|
|
108
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
109
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
110
|
+
logger.warning("Could not parse %s: %s (will create fresh)", path, e)
|
|
111
|
+
return {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _write_json_safe(path: Path, data: dict) -> None:
|
|
115
|
+
"""Atomic write: write to .tmp then rename."""
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
tmp = path.with_suffix(".tmp")
|
|
118
|
+
tmp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
119
|
+
tmp.replace(path) # Atomic on POSIX, best-effort on Windows
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _merge_mcp_config(existing: dict, server_name: str, server_config: dict) -> dict:
|
|
123
|
+
"""Non-destructive merge: only touch the server_name entry."""
|
|
124
|
+
result = json.loads(json.dumps(existing)) # deep copy
|
|
125
|
+
if "mcpServers" not in result:
|
|
126
|
+
result["mcpServers"] = {}
|
|
127
|
+
result["mcpServers"][server_name] = server_config
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Resolve the best command to run codevira
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def _resolve_command() -> tuple[str, str]:
|
|
136
|
+
"""
|
|
137
|
+
Returns (cmd_path, python_exe).
|
|
138
|
+
cmd_path is the absolute path to codevira binary.
|
|
139
|
+
python_exe is the Python interpreter that runs this process.
|
|
140
|
+
|
|
141
|
+
Search order for the binary:
|
|
142
|
+
1. shutil.which (works when ~/.local/bin is in PATH)
|
|
143
|
+
2. pipx default venv location ~/.local/pipx/venvs/codevira/bin/
|
|
144
|
+
3. pip --user install location ~/Library/Python/X.Y/bin/ (macOS) or %APPDATA% (Windows)
|
|
145
|
+
4. Same bin dir as current Python interpreter
|
|
146
|
+
5. Fallback: run as `python -m mcp_server` using current interpreter
|
|
147
|
+
"""
|
|
148
|
+
python_exe = sys.executable
|
|
149
|
+
|
|
150
|
+
# 1. Standard PATH lookup
|
|
151
|
+
exe = shutil.which("codevira")
|
|
152
|
+
if exe:
|
|
153
|
+
return exe, python_exe
|
|
154
|
+
|
|
155
|
+
# 2. pipx default venv
|
|
156
|
+
pipx_bin = Path.home() / ".local" / "pipx" / "venvs" / "codevira" / "bin" / "codevira"
|
|
157
|
+
if sys.platform == "win32":
|
|
158
|
+
pipx_bin = Path.home() / ".local" / "pipx" / "venvs" / "codevira" / "Scripts" / "codevira.exe"
|
|
159
|
+
if pipx_bin.exists():
|
|
160
|
+
return str(pipx_bin), python_exe
|
|
161
|
+
|
|
162
|
+
# 3. pip --user install location (cross-platform)
|
|
163
|
+
try:
|
|
164
|
+
import sysconfig
|
|
165
|
+
if sys.platform == "win32":
|
|
166
|
+
scripts_scheme = "nt_user"
|
|
167
|
+
else:
|
|
168
|
+
scripts_scheme = "posix_user"
|
|
169
|
+
user_scripts = sysconfig.get_path("scripts", scripts_scheme)
|
|
170
|
+
if user_scripts:
|
|
171
|
+
suffix = ".exe" if sys.platform == "win32" else ""
|
|
172
|
+
user_bin = Path(user_scripts) / f"codevira{suffix}"
|
|
173
|
+
if user_bin.exists():
|
|
174
|
+
return str(user_bin), python_exe
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
# 4. Same bin dir as current Python
|
|
179
|
+
suffix = ".exe" if sys.platform == "win32" else ""
|
|
180
|
+
sibling_bin = Path(python_exe).parent / f"codevira{suffix}"
|
|
181
|
+
if sibling_bin.exists():
|
|
182
|
+
return str(sibling_bin), python_exe
|
|
183
|
+
|
|
184
|
+
# 5. Fallback: use current interpreter with -m flag (always works)
|
|
185
|
+
return python_exe, python_exe
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Per-IDE injection (per-project mode)
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _build_server_config(cmd_path: str, python_exe: str, project_root: Path, use_cwd: bool = True) -> dict:
|
|
193
|
+
"""
|
|
194
|
+
Build the MCP server config dict for per-project mode.
|
|
195
|
+
|
|
196
|
+
If cmd_path is the Python interpreter (fallback), use `-m mcp_server --project-dir`.
|
|
197
|
+
If cmd_path is the codevira binary:
|
|
198
|
+
- use_cwd=True: {"command": ..., "args": [], "cwd": ...} (Claude / Cursor / Windsurf)
|
|
199
|
+
- use_cwd=False: {"command": ..., "args": ["--project-dir", ...]} (tools that ignore cwd)
|
|
200
|
+
"""
|
|
201
|
+
is_python_fallback = (cmd_path == python_exe)
|
|
202
|
+
|
|
203
|
+
if is_python_fallback:
|
|
204
|
+
return {
|
|
205
|
+
"command": cmd_path,
|
|
206
|
+
"args": ["-m", "mcp_server", "--project-dir", str(project_root)],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if use_cwd:
|
|
210
|
+
return {
|
|
211
|
+
"command": cmd_path,
|
|
212
|
+
"args": [],
|
|
213
|
+
"cwd": str(project_root),
|
|
214
|
+
}
|
|
215
|
+
else:
|
|
216
|
+
return {
|
|
217
|
+
"command": cmd_path,
|
|
218
|
+
"args": ["--project-dir", str(project_root)],
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _build_global_server_config(cmd_path: str, python_exe: str) -> dict:
|
|
223
|
+
"""
|
|
224
|
+
Build the MCP server config dict for global mode (v1.6).
|
|
225
|
+
|
|
226
|
+
Global mode: no project path — the server detects the project from cwd
|
|
227
|
+
when each AI tool opens a project. Works for every project automatically.
|
|
228
|
+
"""
|
|
229
|
+
is_python_fallback = (cmd_path == python_exe)
|
|
230
|
+
if is_python_fallback:
|
|
231
|
+
return {"command": cmd_path, "args": ["-m", "mcp_server"]}
|
|
232
|
+
return {"command": cmd_path, "args": []}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _inject_claude(project_root: Path, cmd_path: str, python_exe: str) -> str | None:
|
|
236
|
+
"""Inject MCP config into Claude Code per-project settings."""
|
|
237
|
+
config_path = _claude_config_path(project_root)
|
|
238
|
+
existing = _read_json_safe(config_path)
|
|
239
|
+
server_config = _build_server_config(cmd_path, python_exe, project_root, use_cwd=True)
|
|
240
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
241
|
+
_write_json_safe(config_path, merged)
|
|
242
|
+
return str(config_path)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _inject_claude_desktop(project_root: Path, cmd_path: str, python_exe: str) -> str | None:
|
|
246
|
+
"""Inject MCP config into Claude Desktop (stdio-only, requires full binary path).
|
|
247
|
+
|
|
248
|
+
Claude Desktop:
|
|
249
|
+
- Does NOT support the 'url' format — only 'command' + 'args'
|
|
250
|
+
- Does NOT support 'cwd' — must use '--project-dir' arg
|
|
251
|
+
- Requires the FULL absolute binary path (not just 'codevira')
|
|
252
|
+
"""
|
|
253
|
+
config_path = _claude_desktop_config_path()
|
|
254
|
+
existing = _read_json_safe(config_path)
|
|
255
|
+
|
|
256
|
+
# Always use --project-dir for Claude Desktop (no cwd support)
|
|
257
|
+
server_config = _build_server_config(cmd_path, python_exe, project_root, use_cwd=False)
|
|
258
|
+
|
|
259
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
260
|
+
_write_json_safe(config_path, merged)
|
|
261
|
+
return str(config_path)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _inject_cursor(project_root: Path, cmd_path: str, python_exe: str) -> str | None:
|
|
265
|
+
"""Inject MCP config into Cursor per-project settings."""
|
|
266
|
+
config_path = _cursor_config_path(project_root)
|
|
267
|
+
existing = _read_json_safe(config_path)
|
|
268
|
+
server_config = _build_server_config(cmd_path, python_exe, project_root, use_cwd=True)
|
|
269
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
270
|
+
_write_json_safe(config_path, merged)
|
|
271
|
+
return str(config_path)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _inject_windsurf(project_root: Path, cmd_path: str, python_exe: str) -> str | None:
|
|
275
|
+
"""Inject MCP config into Windsurf per-project settings."""
|
|
276
|
+
config_path = _windsurf_config_path(project_root)
|
|
277
|
+
existing = _read_json_safe(config_path)
|
|
278
|
+
server_config = _build_server_config(cmd_path, python_exe, project_root, use_cwd=True)
|
|
279
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
280
|
+
_write_json_safe(config_path, merged)
|
|
281
|
+
return str(config_path)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _inject_antigravity(project_root: Path, cmd_path: str, python_exe: str, project_name: str) -> str | None:
|
|
285
|
+
"""Inject MCP config into Google Antigravity settings (global file, unique server name per project).
|
|
286
|
+
|
|
287
|
+
Antigravity does not support 'cwd', so always use --project-dir args.
|
|
288
|
+
"""
|
|
289
|
+
config_path = _antigravity_config_path()
|
|
290
|
+
existing = _read_json_safe(config_path)
|
|
291
|
+
|
|
292
|
+
# Sanitize project name: lowercase, replace anything non-alphanumeric with hyphens
|
|
293
|
+
safe_name = re.sub(r"[^a-z0-9-]", "-", project_name.lower())
|
|
294
|
+
safe_name = re.sub(r"-{2,}", "-", safe_name).strip("-")
|
|
295
|
+
server_name = f"codevira-{safe_name}"
|
|
296
|
+
|
|
297
|
+
base_config = _build_server_config(cmd_path, python_exe, project_root, use_cwd=False)
|
|
298
|
+
server_config = {"$typeName": "exa.cascade_plugins_pb.CascadePluginCommandTemplate", **base_config}
|
|
299
|
+
|
|
300
|
+
merged = _merge_mcp_config(existing, server_name, server_config)
|
|
301
|
+
_write_json_safe(config_path, merged)
|
|
302
|
+
return str(config_path)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Global mode injection (v1.6) — one-time, no project path
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def inject_global_claude_code(cmd_path: str, python_exe: str) -> str | None:
|
|
310
|
+
"""Inject global codevira config into Claude Code (~/.claude/settings.json).
|
|
311
|
+
|
|
312
|
+
Global mode: no cwd, no --project-dir. The server auto-detects the project
|
|
313
|
+
from cwd when Claude Code opens each project directory.
|
|
314
|
+
"""
|
|
315
|
+
config_path = _claude_global_config_path()
|
|
316
|
+
existing = _read_json_safe(config_path)
|
|
317
|
+
server_config = _build_global_server_config(cmd_path, python_exe)
|
|
318
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
319
|
+
_write_json_safe(config_path, merged)
|
|
320
|
+
return str(config_path)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def inject_global_cursor(cmd_path: str, python_exe: str) -> str | None:
|
|
324
|
+
"""Inject global codevira config into Cursor (~/.cursor/mcp.json)."""
|
|
325
|
+
config_path = _cursor_global_config_path()
|
|
326
|
+
existing = _read_json_safe(config_path)
|
|
327
|
+
server_config = _build_global_server_config(cmd_path, python_exe)
|
|
328
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
329
|
+
_write_json_safe(config_path, merged)
|
|
330
|
+
return str(config_path)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def inject_global_windsurf(cmd_path: str, python_exe: str) -> str | None:
|
|
334
|
+
"""Inject global codevira config into Windsurf."""
|
|
335
|
+
config_path = _windsurf_global_config_path()
|
|
336
|
+
existing = _read_json_safe(config_path)
|
|
337
|
+
server_config = _build_global_server_config(cmd_path, python_exe)
|
|
338
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
339
|
+
_write_json_safe(config_path, merged)
|
|
340
|
+
return str(config_path)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def inject_claude_http_url(url: str) -> str | None:
|
|
344
|
+
"""Inject HTTP URL config into Claude Code global settings.
|
|
345
|
+
|
|
346
|
+
Only for Claude Code CLI — Cursor/Windsurf do not support URL format.
|
|
347
|
+
Claude Desktop does not support URL format either (stdio only).
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
url: Full MCP URL e.g. 'https://localhost:7443/mcp'
|
|
351
|
+
"""
|
|
352
|
+
config_path = _claude_global_config_path()
|
|
353
|
+
existing = _read_json_safe(config_path)
|
|
354
|
+
server_config = {"url": url}
|
|
355
|
+
merged = _merge_mcp_config(existing, "codevira", server_config)
|
|
356
|
+
_write_json_safe(config_path, merged)
|
|
357
|
+
return str(config_path)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Main orchestrators
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def inject_ide_config(
|
|
365
|
+
project_root: Path,
|
|
366
|
+
project_name: str = "",
|
|
367
|
+
global_mode: bool = False,
|
|
368
|
+
) -> dict[str, str]:
|
|
369
|
+
"""
|
|
370
|
+
Detect installed AI tools and auto-inject MCP configuration.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
project_root: Project directory (used in per-project mode).
|
|
374
|
+
project_name: Display name for the project.
|
|
375
|
+
global_mode: If True, inject global config (no project path) instead of
|
|
376
|
+
per-project config. Use for 'codevira register'.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Dict of {ide_name: config_path_written} for each configured tool.
|
|
380
|
+
"""
|
|
381
|
+
project_root = project_root.resolve()
|
|
382
|
+
if not project_name:
|
|
383
|
+
project_name = project_root.name
|
|
384
|
+
|
|
385
|
+
cmd_path, python_exe = _resolve_command()
|
|
386
|
+
ides = detect_installed_ides(project_root)
|
|
387
|
+
results: dict[str, str] = {}
|
|
388
|
+
|
|
389
|
+
for ide in ides:
|
|
390
|
+
try:
|
|
391
|
+
if global_mode:
|
|
392
|
+
# Global mode: register once, works for every project
|
|
393
|
+
if ide == "claude":
|
|
394
|
+
path = inject_global_claude_code(cmd_path, python_exe)
|
|
395
|
+
if path:
|
|
396
|
+
results["Claude Code (global)"] = path
|
|
397
|
+
elif ide == "cursor":
|
|
398
|
+
path = inject_global_cursor(cmd_path, python_exe)
|
|
399
|
+
if path:
|
|
400
|
+
results["Cursor (global)"] = path
|
|
401
|
+
elif ide == "windsurf":
|
|
402
|
+
path = inject_global_windsurf(cmd_path, python_exe)
|
|
403
|
+
if path:
|
|
404
|
+
results["Windsurf (global)"] = path
|
|
405
|
+
elif ide == "claude_desktop":
|
|
406
|
+
# Claude Desktop always needs --project-dir (no cwd + no url support)
|
|
407
|
+
# In global mode, skip Claude Desktop (it can't do project-agnostic config)
|
|
408
|
+
pass
|
|
409
|
+
elif ide == "antigravity":
|
|
410
|
+
# Antigravity always uses per-project keys; skip in global mode
|
|
411
|
+
pass
|
|
412
|
+
else:
|
|
413
|
+
# Per-project mode (existing behavior)
|
|
414
|
+
if ide == "claude":
|
|
415
|
+
path = _inject_claude(project_root, cmd_path, python_exe)
|
|
416
|
+
if path:
|
|
417
|
+
results["Claude Code"] = path
|
|
418
|
+
elif ide == "claude_desktop":
|
|
419
|
+
path = _inject_claude_desktop(project_root, cmd_path, python_exe)
|
|
420
|
+
if path:
|
|
421
|
+
results["Claude Desktop"] = path
|
|
422
|
+
elif ide == "cursor":
|
|
423
|
+
path = _inject_cursor(project_root, cmd_path, python_exe)
|
|
424
|
+
if path:
|
|
425
|
+
results["Cursor"] = path
|
|
426
|
+
elif ide == "windsurf":
|
|
427
|
+
path = _inject_windsurf(project_root, cmd_path, python_exe)
|
|
428
|
+
if path:
|
|
429
|
+
results["Windsurf"] = path
|
|
430
|
+
elif ide == "antigravity":
|
|
431
|
+
path = _inject_antigravity(project_root, cmd_path, python_exe, project_name)
|
|
432
|
+
if path:
|
|
433
|
+
results["Antigravity"] = path
|
|
434
|
+
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.warning("Failed to inject %s config: %s", ide, e)
|
|
437
|
+
try:
|
|
438
|
+
from mcp_server.crash_logger import log_crash
|
|
439
|
+
log_crash(e, context=f"IDE config inject: {ide}",
|
|
440
|
+
project_path=str(project_root))
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
return results
|
mcp_server/launchd.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
launchd.py — macOS launchd service management for Codevira MCP HTTP server.
|
|
3
|
+
|
|
4
|
+
Generates a launchd plist so that `codevira serve` starts automatically
|
|
5
|
+
on login and stays running as a background service.
|
|
6
|
+
|
|
7
|
+
Usage (via CLI):
|
|
8
|
+
codevira serve --install-service # install + load
|
|
9
|
+
codevira serve --uninstall-service # unload + remove
|
|
10
|
+
|
|
11
|
+
This is macOS-only. Windows and Linux service support is planned for v2.0.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import plistlib
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_PLIST_LABEL = "com.codevira.mcp-serve"
|
|
24
|
+
_PLIST_PATH = Path.home() / "Library" / "LaunchAgents" / f"{_PLIST_LABEL}.plist"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def install_launchd(
|
|
28
|
+
port: int = 7007,
|
|
29
|
+
use_https: bool = False,
|
|
30
|
+
host: str = "127.0.0.1",
|
|
31
|
+
project_dir: Path | None = None,
|
|
32
|
+
) -> Path:
|
|
33
|
+
"""Generate and load a launchd plist for the Codevira MCP HTTP server.
|
|
34
|
+
|
|
35
|
+
The plist starts `codevira serve` on login with the given options.
|
|
36
|
+
Logs go to ~/Library/Logs/codevira.log.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
port: TCP port for the server (default: 7007).
|
|
40
|
+
use_https: If True, adds --https flag (requires mkcert CA to be trusted).
|
|
41
|
+
host: Bind address (default: 127.0.0.1).
|
|
42
|
+
project_dir: If provided, adds --project-dir to ProgramArguments and
|
|
43
|
+
sets WorkingDirectory in the plist so the server resolves
|
|
44
|
+
the correct project root.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Path to the installed plist file.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError on non-macOS platforms or if launchctl fails.
|
|
51
|
+
"""
|
|
52
|
+
if sys.platform != "darwin":
|
|
53
|
+
raise RuntimeError("launchd auto-start is only supported on macOS.")
|
|
54
|
+
|
|
55
|
+
from mcp_server.ide_inject import _resolve_command
|
|
56
|
+
cmd_path, _ = _resolve_command()
|
|
57
|
+
|
|
58
|
+
args = [cmd_path, "serve", "--host", host, "--port", str(port)]
|
|
59
|
+
if project_dir is not None:
|
|
60
|
+
args.extend(["--project-dir", str(project_dir)])
|
|
61
|
+
if use_https:
|
|
62
|
+
args.append("--https")
|
|
63
|
+
|
|
64
|
+
log_dir = Path.home() / "Library" / "Logs"
|
|
65
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
log_path = str(log_dir / "codevira.log")
|
|
67
|
+
|
|
68
|
+
# Build plist via plistlib (safe from XML injection — values are properly
|
|
69
|
+
# escaped regardless of content in host, port, or cmd_path).
|
|
70
|
+
plist_data = {
|
|
71
|
+
"Label": _PLIST_LABEL,
|
|
72
|
+
"ProgramArguments": args,
|
|
73
|
+
"RunAtLoad": True,
|
|
74
|
+
"KeepAlive": True,
|
|
75
|
+
"StandardOutPath": log_path,
|
|
76
|
+
"StandardErrorPath": log_path,
|
|
77
|
+
}
|
|
78
|
+
if project_dir is not None:
|
|
79
|
+
plist_data["WorkingDirectory"] = str(project_dir)
|
|
80
|
+
|
|
81
|
+
_PLIST_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
# Unload existing service if present (ignore errors)
|
|
84
|
+
subprocess.run(
|
|
85
|
+
["launchctl", "unload", str(_PLIST_PATH)],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
timeout=10,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
with open(_PLIST_PATH, "wb") as f:
|
|
91
|
+
plistlib.dump(plist_data, f)
|
|
92
|
+
logger.info("Wrote launchd plist: %s", _PLIST_PATH)
|
|
93
|
+
|
|
94
|
+
# Load the new service
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
["launchctl", "load", str(_PLIST_PATH)],
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
timeout=10,
|
|
100
|
+
)
|
|
101
|
+
if result.returncode != 0:
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
f"launchctl load failed:\n{result.stderr or result.stdout}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
logger.info("Launchd service loaded: %s", _PLIST_LABEL)
|
|
107
|
+
return _PLIST_PATH
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def uninstall_launchd() -> bool:
|
|
111
|
+
"""Unload and remove the Codevira launchd plist.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if the service was removed, False if it wasn't installed.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
RuntimeError on non-macOS platforms.
|
|
118
|
+
"""
|
|
119
|
+
if sys.platform != "darwin":
|
|
120
|
+
raise RuntimeError("launchd management is only supported on macOS.")
|
|
121
|
+
|
|
122
|
+
if not _PLIST_PATH.exists():
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
subprocess.run(
|
|
126
|
+
["launchctl", "unload", str(_PLIST_PATH)],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
timeout=10,
|
|
129
|
+
)
|
|
130
|
+
_PLIST_PATH.unlink(missing_ok=True)
|
|
131
|
+
logger.info("Launchd service removed: %s", _PLIST_LABEL)
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def launchd_status() -> dict:
|
|
136
|
+
"""Return the current status of the launchd service."""
|
|
137
|
+
if sys.platform != "darwin":
|
|
138
|
+
return {"platform": "not_macos", "installed": False}
|
|
139
|
+
|
|
140
|
+
installed = _PLIST_PATH.exists()
|
|
141
|
+
running = False
|
|
142
|
+
if installed:
|
|
143
|
+
result = subprocess.run(
|
|
144
|
+
["launchctl", "list", _PLIST_LABEL],
|
|
145
|
+
capture_output=True,
|
|
146
|
+
text=True,
|
|
147
|
+
timeout=5,
|
|
148
|
+
)
|
|
149
|
+
running = result.returncode == 0
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"installed": installed,
|
|
153
|
+
"running": running,
|
|
154
|
+
"plist_path": str(_PLIST_PATH) if installed else None,
|
|
155
|
+
"label": _PLIST_LABEL,
|
|
156
|
+
}
|