codemesh 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {codemesh-0.1.4 → codemesh-0.1.6}/PKG-INFO +1 -1
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/__init__.py +1 -1
- codemesh-0.1.6/codemesh/cli/install_cmd.py +634 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/cli/main.py +74 -42
- {codemesh-0.1.4 → codemesh-0.1.6}/pyproject.toml +1 -1
- codemesh-0.1.4/codemesh/cli/install_cmd.py +0 -346
- {codemesh-0.1.4 → codemesh-0.1.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/.github/workflows/ci.yml +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/.github/workflows/publish.yml +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/.gitignore +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/CHANGELOG.md +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/CONTRIBUTING.md +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/LICENSE +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/Makefile +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/README.md +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/__main__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/cli/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/cli/init.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/context/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/context/builder.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/db/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/db/connection.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/db/queries.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/db/schema.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/embedding/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/c_family.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/go.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/java.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/python.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/rust.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/swift.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/languages/typescript.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/extraction/orchestrator.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/graph/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/graph/query_manager.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/graph/traverser.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/indexer.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/mcp/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/mcp/server.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/mcp/tools.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/querier.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/frameworks/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/frameworks/django.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/frameworks/fastapi.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/import_resolver.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/name_matcher.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/resolution/resolver.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/retrieval/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/search/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/sync/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/sync/watcher.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/types.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/viz/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/viz/graph_builder.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/viz/server.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/codemesh/viz/templates/index.html +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/conftest.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/fixtures/__init__.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_adversarial.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_benchmark_repoqa.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_embedding_e2e.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_extraction.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_integration.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_llm_judge.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_performance.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/tests/test_viz.py +0 -0
- {codemesh-0.1.4 → codemesh-0.1.6}/uv.lock +0 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
"""Install/uninstall commands — configure CodeMesh MCP server for AI coding agents."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from codemesh.cli.init import _CLAUDE_MD_TEMPLATE, _CODEX_TEMPLATE, _CURSOR_RULES_TEMPLATE
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── MCP server config templates ──────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
_CLAUDE_MCP_CONFIG = {
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"codemesh": {
|
|
24
|
+
"type": "stdio",
|
|
25
|
+
"command": "codemesh",
|
|
26
|
+
"args": ["serve", "--transport", "stdio"],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_CLAUDE_PERMISSIONS = {
|
|
32
|
+
"permissions": {
|
|
33
|
+
"allow": [
|
|
34
|
+
"mcp__codemesh__codemesh_search",
|
|
35
|
+
"mcp__codemesh__codemesh_context",
|
|
36
|
+
"mcp__codemesh__codemesh_callers",
|
|
37
|
+
"mcp__codemesh__codemesh_callees",
|
|
38
|
+
"mcp__codemesh__codemesh_impact",
|
|
39
|
+
"mcp__codemesh__codemesh_node",
|
|
40
|
+
"mcp__codemesh__codemesh_status",
|
|
41
|
+
"mcp__codemesh__codemesh_files",
|
|
42
|
+
"mcp__codemesh__codemesh_explore",
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Agent metadata ───────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class AgentInfo:
|
|
52
|
+
name: str # canonical key: "claude", "cursor", etc.
|
|
53
|
+
display: str # human name: "Claude Code"
|
|
54
|
+
detected: bool = False # is the agent installed on this machine?
|
|
55
|
+
configured: bool = False # is codemesh already configured for this agent?
|
|
56
|
+
scope: str = "project" # "project" or "global"
|
|
57
|
+
detail: str = "" # extra info for the UI, e.g. path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def detect_agents(root: Path | None = None) -> list[AgentInfo]:
|
|
61
|
+
"""Detect all supported agents and their codemesh configuration status.
|
|
62
|
+
|
|
63
|
+
Returns a list of AgentInfo for every known agent, with detected/configured
|
|
64
|
+
flags set accordingly.
|
|
65
|
+
"""
|
|
66
|
+
if root is None:
|
|
67
|
+
root = Path.cwd()
|
|
68
|
+
|
|
69
|
+
agents: list[AgentInfo] = []
|
|
70
|
+
|
|
71
|
+
# ── Claude Code ─────────────────────────────────────────────────────────
|
|
72
|
+
claude_dir = _find_claude_json_dir()
|
|
73
|
+
claude_json = claude_dir / "claude.json" if claude_dir else None
|
|
74
|
+
claude_configured = (
|
|
75
|
+
claude_json is not None
|
|
76
|
+
and claude_json.exists()
|
|
77
|
+
and "codemesh" in json.loads(claude_json.read_text()).get("mcpServers", {})
|
|
78
|
+
)
|
|
79
|
+
agents.append(AgentInfo(
|
|
80
|
+
name="claude",
|
|
81
|
+
display="Claude Code",
|
|
82
|
+
detected=(claude_dir is not None and claude_dir.exists()),
|
|
83
|
+
configured=claude_configured,
|
|
84
|
+
scope="global",
|
|
85
|
+
detail=str(claude_json) if claude_json else "",
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
# ── Cursor ──────────────────────────────────────────────────────────────
|
|
89
|
+
cursor_mcp = root / ".cursor" / "mcp.json"
|
|
90
|
+
cursor_configured = (
|
|
91
|
+
cursor_mcp.exists()
|
|
92
|
+
and "codemesh" in json.loads(cursor_mcp.read_text()).get("mcpServers", {})
|
|
93
|
+
)
|
|
94
|
+
agents.append(AgentInfo(
|
|
95
|
+
name="cursor",
|
|
96
|
+
display="Cursor",
|
|
97
|
+
detected=(root / ".cursor").exists(),
|
|
98
|
+
configured=cursor_configured,
|
|
99
|
+
scope="project",
|
|
100
|
+
detail=str(cursor_mcp),
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
# ── Codex CLI ───────────────────────────────────────────────────────────
|
|
104
|
+
codex_dir = Path.home() / ".codex"
|
|
105
|
+
codex_config = codex_dir / "config.json"
|
|
106
|
+
codex_configured = (
|
|
107
|
+
codex_config.exists()
|
|
108
|
+
and "codemesh" in json.loads(codex_config.read_text()).get("mcpServers", {})
|
|
109
|
+
)
|
|
110
|
+
agents.append(AgentInfo(
|
|
111
|
+
name="codex",
|
|
112
|
+
display="Codex CLI",
|
|
113
|
+
detected=shutil.which("codex") is not None,
|
|
114
|
+
configured=codex_configured,
|
|
115
|
+
scope="global",
|
|
116
|
+
detail=str(codex_config),
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
# ── Hermes Agent ────────────────────────────────────────────────────────
|
|
120
|
+
hermes_config = Path.home() / ".hermes" / "config.yaml"
|
|
121
|
+
hermes_configured = False
|
|
122
|
+
if hermes_config.exists():
|
|
123
|
+
try:
|
|
124
|
+
import yaml
|
|
125
|
+
hermes_data = yaml.safe_load(hermes_config.read_text()) or {}
|
|
126
|
+
mcp_servers = hermes_data.get("mcp_servers", {})
|
|
127
|
+
hermes_configured = "codemesh" in mcp_servers
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
agents.append(AgentInfo(
|
|
131
|
+
name="hermes",
|
|
132
|
+
display="Hermes Agent",
|
|
133
|
+
detected=hermes_config.exists() or shutil.which("hermes") is not None,
|
|
134
|
+
configured=hermes_configured,
|
|
135
|
+
scope="global",
|
|
136
|
+
detail=str(hermes_config),
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
return agents
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── Interactive agent selection ──────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def select_agents_interactive(
|
|
145
|
+
agents: list[AgentInfo],
|
|
146
|
+
mode: str = "install", # "install" or "uninstall"
|
|
147
|
+
) -> list[str]:
|
|
148
|
+
"""Present an interactive checklist and return the selected agent names.
|
|
149
|
+
|
|
150
|
+
Uses a simple numbered input — works in any terminal without requiring
|
|
151
|
+
an interactive TUI library.
|
|
152
|
+
|
|
153
|
+
For install: pre-selects detected agents that are NOT yet configured.
|
|
154
|
+
For uninstall: pre-selects agents that ARE configured.
|
|
155
|
+
"""
|
|
156
|
+
typer.echo("")
|
|
157
|
+
if mode == "install":
|
|
158
|
+
typer.echo(" Which agents should codemesh configure?")
|
|
159
|
+
else:
|
|
160
|
+
typer.echo(" Which agents should codemesh uninstall from?")
|
|
161
|
+
typer.echo("")
|
|
162
|
+
|
|
163
|
+
pre_selected: set[str] = set()
|
|
164
|
+
for i, a in enumerate(agents, 1):
|
|
165
|
+
if mode == "install" and a.detected and not a.configured:
|
|
166
|
+
pre_selected.add(a.name)
|
|
167
|
+
|
|
168
|
+
# Build annotation
|
|
169
|
+
annotations: list[str] = []
|
|
170
|
+
if a.configured:
|
|
171
|
+
annotations.append("already configured")
|
|
172
|
+
elif not a.detected:
|
|
173
|
+
annotations.append("not found")
|
|
174
|
+
if a.scope == "global" and a.detected:
|
|
175
|
+
annotations.append("global only")
|
|
176
|
+
|
|
177
|
+
ann_str = f" — {', '.join(annotations)}" if annotations else ""
|
|
178
|
+
marker = "◼" if a.name in pre_selected else "◻"
|
|
179
|
+
|
|
180
|
+
typer.echo(f" {i}. {marker} {a.display}{ann_str}")
|
|
181
|
+
|
|
182
|
+
typer.echo("")
|
|
183
|
+
typer.echo(" Enter numbers to toggle (e.g. 1,3,4), 'all', or 'none'.")
|
|
184
|
+
typer.echo(" Press Enter with no input to accept the pre-selection.")
|
|
185
|
+
typer.echo("")
|
|
186
|
+
|
|
187
|
+
selected: set[str] = set(pre_selected)
|
|
188
|
+
|
|
189
|
+
while True:
|
|
190
|
+
raw = typer.prompt(" Select", default="", show_default=False).strip()
|
|
191
|
+
|
|
192
|
+
if raw == "":
|
|
193
|
+
break
|
|
194
|
+
elif raw.lower() == "all":
|
|
195
|
+
selected = {a.name for a in agents if a.detected}
|
|
196
|
+
break
|
|
197
|
+
elif raw.lower() == "none":
|
|
198
|
+
selected = set()
|
|
199
|
+
break
|
|
200
|
+
else:
|
|
201
|
+
# Parse comma/space-separated numbers
|
|
202
|
+
parts = raw.replace(",", " ").split()
|
|
203
|
+
for p in parts:
|
|
204
|
+
try:
|
|
205
|
+
idx = int(p) - 1
|
|
206
|
+
if 0 <= idx < len(agents):
|
|
207
|
+
name = agents[idx].name
|
|
208
|
+
if name in selected:
|
|
209
|
+
selected.discard(name)
|
|
210
|
+
else:
|
|
211
|
+
selected.add(name)
|
|
212
|
+
else:
|
|
213
|
+
typer.echo(f" Ignoring out-of-range number: {p}")
|
|
214
|
+
except ValueError:
|
|
215
|
+
# Match by name
|
|
216
|
+
matched = False
|
|
217
|
+
for a in agents:
|
|
218
|
+
if a.name == p.lower() or a.display.lower() == p.lower():
|
|
219
|
+
if a.name in selected:
|
|
220
|
+
selected.discard(a.name)
|
|
221
|
+
else:
|
|
222
|
+
selected.add(a.name)
|
|
223
|
+
matched = True
|
|
224
|
+
break
|
|
225
|
+
if not matched:
|
|
226
|
+
typer.echo(f" Unknown agent: {p}")
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if not selected:
|
|
230
|
+
typer.echo(" No agents selected.")
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
|
|
233
|
+
return list(selected)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ── Install helpers ──────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
def _find_claude_json_dir() -> Path | None:
|
|
239
|
+
"""Find the Claude Code configuration directory."""
|
|
240
|
+
candidates = [
|
|
241
|
+
Path.home() / ".claude",
|
|
242
|
+
Path.home() / ".config" / "claude",
|
|
243
|
+
]
|
|
244
|
+
for c in candidates:
|
|
245
|
+
if c.exists():
|
|
246
|
+
return c
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _merge_json_file(path: Path, new_data: dict) -> dict:
|
|
251
|
+
"""Merge new data into an existing JSON file."""
|
|
252
|
+
existing: dict = {}
|
|
253
|
+
if path.exists():
|
|
254
|
+
try:
|
|
255
|
+
existing = json.loads(path.read_text())
|
|
256
|
+
except (json.JSONDecodeError, OSError):
|
|
257
|
+
existing = {}
|
|
258
|
+
|
|
259
|
+
if "mcpServers" in new_data:
|
|
260
|
+
existing.setdefault("mcpServers", {})
|
|
261
|
+
existing["mcpServers"].update(new_data["mcpServers"])
|
|
262
|
+
|
|
263
|
+
if "permissions" in new_data and "allow" in new_data["permissions"]:
|
|
264
|
+
existing.setdefault("permissions", {"allow": []})
|
|
265
|
+
perms_allow = existing["permissions"].get("allow", [])
|
|
266
|
+
for item in new_data["permissions"]["allow"]:
|
|
267
|
+
if item not in perms_allow:
|
|
268
|
+
perms_allow.append(item)
|
|
269
|
+
existing["permissions"]["allow"] = perms_allow
|
|
270
|
+
|
|
271
|
+
return existing
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def install_claude(root: Path, global_config: bool = True) -> dict:
|
|
275
|
+
"""Configure Claude Code to use CodeMesh MCP server."""
|
|
276
|
+
result = {"claude_json": None, "claude_settings": None}
|
|
277
|
+
|
|
278
|
+
if global_config:
|
|
279
|
+
claude_dir = _find_claude_json_dir()
|
|
280
|
+
if claude_dir is None:
|
|
281
|
+
claude_dir = Path.home() / ".claude"
|
|
282
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
283
|
+
claude_json = claude_dir / "claude.json"
|
|
284
|
+
claude_settings = claude_dir / "settings.json"
|
|
285
|
+
else:
|
|
286
|
+
claude_json = root / ".claude.json"
|
|
287
|
+
claude_settings = root / ".claude_settings.json"
|
|
288
|
+
|
|
289
|
+
# Check if already configured
|
|
290
|
+
if claude_json.exists():
|
|
291
|
+
existing = json.loads(claude_json.read_text()) if claude_json.exists() else {}
|
|
292
|
+
if "codemesh" in existing.get("mcpServers", {}):
|
|
293
|
+
result["claude_json"] = str(claude_json) + " (already configured)"
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
merged = _merge_json_file(claude_json, _CLAUDE_MCP_CONFIG)
|
|
297
|
+
claude_json.write_text(json.dumps(merged, indent=2))
|
|
298
|
+
result["claude_json"] = str(claude_json)
|
|
299
|
+
|
|
300
|
+
merged_settings = _merge_json_file(claude_settings, _CLAUDE_PERMISSIONS)
|
|
301
|
+
claude_settings.write_text(json.dumps(merged_settings, indent=2))
|
|
302
|
+
result["claude_settings"] = str(claude_settings)
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def install_cursor(root: Path) -> dict:
|
|
308
|
+
"""Configure Cursor to use CodeMesh MCP server."""
|
|
309
|
+
result = {"cursor_mcp": None}
|
|
310
|
+
|
|
311
|
+
cursor_dir = root / ".cursor"
|
|
312
|
+
cursor_dir.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
mcp_json = cursor_dir / "mcp.json"
|
|
314
|
+
|
|
315
|
+
config = {}
|
|
316
|
+
if mcp_json.exists():
|
|
317
|
+
try:
|
|
318
|
+
config = json.loads(mcp_json.read_text())
|
|
319
|
+
except (json.JSONDecodeError, OSError):
|
|
320
|
+
config = {}
|
|
321
|
+
|
|
322
|
+
config.setdefault("mcpServers", {})
|
|
323
|
+
if "codemesh" in config.get("mcpServers", {}):
|
|
324
|
+
result["cursor_mcp"] = str(mcp_json) + " (already configured)"
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
config["mcpServers"]["codemesh"] = {
|
|
328
|
+
"type": "stdio",
|
|
329
|
+
"command": "codemesh",
|
|
330
|
+
"args": ["serve", "--transport", "stdio"],
|
|
331
|
+
}
|
|
332
|
+
mcp_json.write_text(json.dumps(config, indent=2))
|
|
333
|
+
result["cursor_mcp"] = str(mcp_json)
|
|
334
|
+
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def install_codex(root: Path) -> dict:
|
|
339
|
+
"""Configure Codex CLI to use CodeMesh MCP server."""
|
|
340
|
+
result = {"codex_config": None}
|
|
341
|
+
|
|
342
|
+
codex_dir = Path.home() / ".codex"
|
|
343
|
+
codex_dir.mkdir(parents=True, exist_ok=True)
|
|
344
|
+
config_file = codex_dir / "config.json"
|
|
345
|
+
|
|
346
|
+
config: dict = {}
|
|
347
|
+
if config_file.exists():
|
|
348
|
+
try:
|
|
349
|
+
config = json.loads(config_file.read_text())
|
|
350
|
+
except (json.JSONDecodeError, OSError):
|
|
351
|
+
config = {}
|
|
352
|
+
|
|
353
|
+
config.setdefault("mcpServers", {})
|
|
354
|
+
if "codemesh" in config.get("mcpServers", {}):
|
|
355
|
+
result["codex_config"] = str(config_file) + " (already configured)"
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
config["mcpServers"]["codemesh"] = {
|
|
359
|
+
"type": "stdio",
|
|
360
|
+
"command": "codemesh",
|
|
361
|
+
"args": ["serve", "--transport", "stdio"],
|
|
362
|
+
}
|
|
363
|
+
config_file.write_text(json.dumps(config, indent=2))
|
|
364
|
+
result["codex_config"] = str(config_file)
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def install_hermes(_root: Path) -> dict:
|
|
370
|
+
"""Configure Hermes Agent to use CodeMesh MCP server.
|
|
371
|
+
|
|
372
|
+
Hermes uses ~/.hermes/config.yaml with an mcp_servers section.
|
|
373
|
+
Silently returns an empty result if PyYAML is not installed.
|
|
374
|
+
"""
|
|
375
|
+
result: dict = {}
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
import yaml # noqa: F401
|
|
379
|
+
except ImportError:
|
|
380
|
+
return result # PyYAML not installed — skip silently
|
|
381
|
+
|
|
382
|
+
hermes_config_path = Path.home() / ".hermes" / "config.yaml"
|
|
383
|
+
if not hermes_config_path.parent.exists():
|
|
384
|
+
hermes_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
|
|
386
|
+
config: dict = {}
|
|
387
|
+
if hermes_config_path.exists():
|
|
388
|
+
try:
|
|
389
|
+
config = yaml.safe_load(hermes_config_path.read_text()) or {}
|
|
390
|
+
except Exception:
|
|
391
|
+
config = {}
|
|
392
|
+
|
|
393
|
+
config.setdefault("mcp_servers", {})
|
|
394
|
+
if "codemesh" in config.get("mcp_servers", {}):
|
|
395
|
+
return result # already configured
|
|
396
|
+
|
|
397
|
+
config["mcp_servers"]["codemesh"] = {
|
|
398
|
+
"command": "codemesh",
|
|
399
|
+
"args": ["serve", "--transport", "stdio"],
|
|
400
|
+
"enabled": True,
|
|
401
|
+
}
|
|
402
|
+
hermes_config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
403
|
+
result["hermes_config"] = str(hermes_config_path)
|
|
404
|
+
|
|
405
|
+
return result
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ── Uninstall helpers ────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
def uninstall_claude(root: Path, global_config: bool = True) -> dict:
|
|
411
|
+
"""Remove CodeMesh MCP server configuration from Claude Code."""
|
|
412
|
+
result = {"claude_json": None, "claude_settings": None}
|
|
413
|
+
|
|
414
|
+
if global_config:
|
|
415
|
+
claude_dir = _find_claude_json_dir()
|
|
416
|
+
if claude_dir is None:
|
|
417
|
+
return result
|
|
418
|
+
claude_json = claude_dir / "claude.json"
|
|
419
|
+
claude_settings = claude_dir / "settings.json"
|
|
420
|
+
else:
|
|
421
|
+
claude_json = root / ".claude.json"
|
|
422
|
+
claude_settings = root / ".claude_settings.json"
|
|
423
|
+
|
|
424
|
+
if claude_json.exists():
|
|
425
|
+
try:
|
|
426
|
+
data = json.loads(claude_json.read_text())
|
|
427
|
+
except (json.JSONDecodeError, OSError):
|
|
428
|
+
data = {}
|
|
429
|
+
if "codemesh" in data.get("mcpServers", {}):
|
|
430
|
+
del data["mcpServers"]["codemesh"]
|
|
431
|
+
if not data["mcpServers"]:
|
|
432
|
+
del data["mcpServers"]
|
|
433
|
+
claude_json.write_text(json.dumps(data, indent=2))
|
|
434
|
+
result["claude_json"] = str(claude_json)
|
|
435
|
+
else:
|
|
436
|
+
result["claude_json"] = "not configured"
|
|
437
|
+
|
|
438
|
+
if claude_settings.exists():
|
|
439
|
+
try:
|
|
440
|
+
settings = json.loads(claude_settings.read_text())
|
|
441
|
+
except (json.JSONDecodeError, OSError):
|
|
442
|
+
settings = {}
|
|
443
|
+
perms = settings.get("permissions", {}).get("allow", [])
|
|
444
|
+
codemesh_perms = [p for p in perms if p.startswith("mcp__codemesh__")]
|
|
445
|
+
if codemesh_perms:
|
|
446
|
+
settings.setdefault("permissions", {})["allow"] = [
|
|
447
|
+
p for p in perms if not p.startswith("mcp__codemesh__")
|
|
448
|
+
]
|
|
449
|
+
claude_settings.write_text(json.dumps(settings, indent=2))
|
|
450
|
+
result["claude_settings"] = str(claude_settings)
|
|
451
|
+
else:
|
|
452
|
+
result["claude_settings"] = "not configured"
|
|
453
|
+
|
|
454
|
+
return result
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def uninstall_cursor(root: Path) -> dict:
|
|
458
|
+
"""Remove CodeMesh MCP server configuration from Cursor."""
|
|
459
|
+
result = {"cursor_mcp": None}
|
|
460
|
+
|
|
461
|
+
mcp_json = root / ".cursor" / "mcp.json"
|
|
462
|
+
if not mcp_json.exists():
|
|
463
|
+
result["cursor_mcp"] = "not configured"
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
config = json.loads(mcp_json.read_text())
|
|
468
|
+
except (json.JSONDecodeError, OSError):
|
|
469
|
+
return result
|
|
470
|
+
|
|
471
|
+
if "codemesh" in config.get("mcpServers", {}):
|
|
472
|
+
del config["mcpServers"]["codemesh"]
|
|
473
|
+
if not config["mcpServers"]:
|
|
474
|
+
del config["mcpServers"]
|
|
475
|
+
mcp_json.write_text(json.dumps(config, indent=2))
|
|
476
|
+
result["cursor_mcp"] = str(mcp_json)
|
|
477
|
+
else:
|
|
478
|
+
result["cursor_mcp"] = "not configured"
|
|
479
|
+
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def uninstall_codex(_root: Path) -> dict:
|
|
484
|
+
"""Remove CodeMesh MCP server configuration from Codex CLI."""
|
|
485
|
+
result = {"codex_config": None}
|
|
486
|
+
|
|
487
|
+
codex_dir = Path.home() / ".codex"
|
|
488
|
+
config_file = codex_dir / "config.json"
|
|
489
|
+
if not config_file.exists():
|
|
490
|
+
result["codex_config"] = "not configured"
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
config = json.loads(config_file.read_text())
|
|
495
|
+
except (json.JSONDecodeError, OSError):
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
if "codemesh" in config.get("mcpServers", {}):
|
|
499
|
+
del config["mcpServers"]["codemesh"]
|
|
500
|
+
if not config["mcpServers"]:
|
|
501
|
+
del config["mcpServers"]
|
|
502
|
+
config_file.write_text(json.dumps(config, indent=2))
|
|
503
|
+
result["codex_config"] = str(config_file)
|
|
504
|
+
else:
|
|
505
|
+
result["codex_config"] = "not configured"
|
|
506
|
+
|
|
507
|
+
return result
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def uninstall_hermes(_root: Path) -> dict:
|
|
511
|
+
"""Remove CodeMesh MCP server configuration from Hermes Agent.
|
|
512
|
+
|
|
513
|
+
Silently returns an empty result if PyYAML is not installed.
|
|
514
|
+
"""
|
|
515
|
+
result: dict = {}
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
import yaml # noqa: F401
|
|
519
|
+
except ImportError:
|
|
520
|
+
return result
|
|
521
|
+
|
|
522
|
+
hermes_config_path = Path.home() / ".hermes" / "config.yaml"
|
|
523
|
+
if not hermes_config_path.exists():
|
|
524
|
+
return result
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
config = yaml.safe_load(hermes_config_path.read_text()) or {}
|
|
528
|
+
except Exception:
|
|
529
|
+
return result
|
|
530
|
+
|
|
531
|
+
if "codemesh" in config.get("mcp_servers", {}):
|
|
532
|
+
del config["mcp_servers"]["codemesh"]
|
|
533
|
+
if not config["mcp_servers"]:
|
|
534
|
+
del config["mcp_servers"]
|
|
535
|
+
hermes_config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
536
|
+
result["hermes_config"] = str(hermes_config_path)
|
|
537
|
+
|
|
538
|
+
return result
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ── Project artifact cleanup (surgical) ──────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
def _remove_codemesh_section(content: str, heading: str = "## CodeMesh") -> tuple[str, bool]:
|
|
544
|
+
"""Remove the CodeMesh section from a markdown file.
|
|
545
|
+
|
|
546
|
+
Returns (new_content, was_modified).
|
|
547
|
+
"""
|
|
548
|
+
if heading not in content:
|
|
549
|
+
return content, False
|
|
550
|
+
|
|
551
|
+
parts = content.split(heading, 1)
|
|
552
|
+
before = parts[0]
|
|
553
|
+
after = parts[1] if len(parts) > 1 else ""
|
|
554
|
+
|
|
555
|
+
next_section_idx = -1
|
|
556
|
+
if "\n## " in after:
|
|
557
|
+
next_section_idx = after.index("\n## ")
|
|
558
|
+
|
|
559
|
+
if next_section_idx >= 0:
|
|
560
|
+
after = after[next_section_idx:]
|
|
561
|
+
new_content = (before.rstrip("\n") + "\n\n" + after.lstrip("\n")).strip("\n") + "\n"
|
|
562
|
+
if not new_content.strip():
|
|
563
|
+
return "", True
|
|
564
|
+
return new_content, True
|
|
565
|
+
else:
|
|
566
|
+
if before.strip():
|
|
567
|
+
return before.rstrip("\n") + "\n", True
|
|
568
|
+
else:
|
|
569
|
+
return "", True
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def clean_project(root: Path, force: bool = False) -> dict:
|
|
573
|
+
"""Remove CodeMesh project artifacts (.codemesh/, CLAUDE.md, AGENTS.md, .cursor/rules/).
|
|
574
|
+
|
|
575
|
+
Uses surgical removal for shared files (CLAUDE.md, AGENTS.md):
|
|
576
|
+
- If the file is EXACTLY our template, it's deleted
|
|
577
|
+
- If the file contains CodeMesh section mixed with user content, only the
|
|
578
|
+
CodeMesh section is extracted and the rest is preserved
|
|
579
|
+
- If the file doesn't contain CodeMesh content, it's left untouched
|
|
580
|
+
|
|
581
|
+
Returns a dict with paths removed or modified.
|
|
582
|
+
"""
|
|
583
|
+
import shutil as _shutil
|
|
584
|
+
|
|
585
|
+
removed: list[str] = []
|
|
586
|
+
modified: list[str] = []
|
|
587
|
+
|
|
588
|
+
# --- .codemesh/ directory: always safe to remove entirely ---
|
|
589
|
+
codemesh_dir = root / ".codemesh"
|
|
590
|
+
if codemesh_dir.exists():
|
|
591
|
+
_shutil.rmtree(codemesh_dir)
|
|
592
|
+
removed.append(str(codemesh_dir))
|
|
593
|
+
|
|
594
|
+
# --- CLAUDE.md: surgical removal ---
|
|
595
|
+
claude_md = root / "CLAUDE.md"
|
|
596
|
+
if claude_md.exists():
|
|
597
|
+
content = claude_md.read_text()
|
|
598
|
+
if _CLAUDE_MD_TEMPLATE.strip() == content.strip():
|
|
599
|
+
claude_md.unlink()
|
|
600
|
+
removed.append(str(claude_md))
|
|
601
|
+
elif "## CodeMesh" in content:
|
|
602
|
+
new_content, changed = _remove_codemesh_section(content)
|
|
603
|
+
if changed:
|
|
604
|
+
if new_content.strip():
|
|
605
|
+
claude_md.write_text(new_content)
|
|
606
|
+
modified.append(str(claude_md))
|
|
607
|
+
else:
|
|
608
|
+
claude_md.unlink()
|
|
609
|
+
removed.append(str(claude_md))
|
|
610
|
+
|
|
611
|
+
# --- AGENTS.md: surgical removal ---
|
|
612
|
+
agents_md = root / "AGENTS.md"
|
|
613
|
+
if agents_md.exists():
|
|
614
|
+
content = agents_md.read_text()
|
|
615
|
+
if _CODEX_TEMPLATE.strip() == content.strip():
|
|
616
|
+
agents_md.unlink()
|
|
617
|
+
removed.append(str(agents_md))
|
|
618
|
+
elif "## CodeMesh" in content:
|
|
619
|
+
new_content, changed = _remove_codemesh_section(content)
|
|
620
|
+
if changed:
|
|
621
|
+
if new_content.strip():
|
|
622
|
+
agents_md.write_text(new_content)
|
|
623
|
+
modified.append(str(agents_md))
|
|
624
|
+
else:
|
|
625
|
+
agents_md.unlink()
|
|
626
|
+
removed.append(str(agents_md))
|
|
627
|
+
|
|
628
|
+
# --- .cursor/rules/codemesh.mdc: dedicated file, safe to delete ---
|
|
629
|
+
cursor_rules = root / ".cursor" / "rules" / "codemesh.mdc"
|
|
630
|
+
if cursor_rules.exists():
|
|
631
|
+
cursor_rules.unlink()
|
|
632
|
+
removed.append(str(cursor_rules))
|
|
633
|
+
|
|
634
|
+
return {"removed": removed, "modified": modified}
|