meshcode 1.2.5__tar.gz → 1.3.0__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.
Files changed (26) hide show
  1. {meshcode-1.2.5 → meshcode-1.3.0}/PKG-INFO +1 -1
  2. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/__init__.py +1 -1
  3. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/comms_v4.py +17 -9
  4. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/server.py +53 -9
  5. meshcode-1.3.0/meshcode/run_agent.py +151 -0
  6. meshcode-1.3.0/meshcode/setup_clients.py +297 -0
  7. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode.egg-info/SOURCES.txt +1 -0
  9. {meshcode-1.2.5 → meshcode-1.3.0}/pyproject.toml +1 -1
  10. meshcode-1.2.5/meshcode/setup_clients.py +0 -145
  11. {meshcode-1.2.5 → meshcode-1.3.0}/README.md +0 -0
  12. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/cli.py +0 -0
  13. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/launcher.py +0 -0
  14. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/launcher_install.py +0 -0
  15. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/backend.py +0 -0
  18. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/realtime.py +0 -0
  19. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode.egg-info/dependency_links.txt +0 -0
  23. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode.egg-info/entry_points.txt +0 -0
  24. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode.egg-info/requires.txt +0 -0
  25. {meshcode-1.2.5 → meshcode-1.3.0}/meshcode.egg-info/top_level.txt +0 -0
  26. {meshcode-1.2.5 → meshcode-1.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.2.5
3
+ Version: 1.3.0
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "1.2.5"
2
+ __version__ = "1.3.0"
@@ -1888,17 +1888,25 @@ if __name__ == "__main__":
1888
1888
  disconnect_terminal(proj, name)
1889
1889
 
1890
1890
  elif cmd == "setup":
1891
- if len(sys.argv) < 5:
1892
- print("Usage: meshcode setup <client> <project> <agent> [role]")
1893
- print("Clients: claude-code, cursor, cline, claude-desktop")
1891
+ # Two forms supported:
1892
+ # meshcode setup <project> <agent> [role] → workspace flow (1.3+)
1893
+ # meshcode setup <client> <project> <agent> [role] → legacy global flow
1894
+ # Dispatched by setup_clients.setup() which detects which form was used.
1895
+ import importlib
1896
+ _setup_dispatcher = importlib.import_module("meshcode.setup_clients").setup
1897
+ sys.exit(_setup_dispatcher(*sys.argv[2:]))
1898
+
1899
+ elif cmd == "run":
1900
+ # meshcode run <agent> [--project <name>] [--editor claude|cursor|code]
1901
+ if len(sys.argv) < 3:
1902
+ print("Usage: meshcode run <agent> [--project <name>] [--editor claude|cursor|code]")
1894
1903
  sys.exit(1)
1895
- client = sys.argv[2]
1896
- project = sys.argv[3]
1897
- agent = sys.argv[4]
1898
- role = sys.argv[5] if len(sys.argv) > 5 else ""
1904
+ agent = sys.argv[2]
1905
+ proj_override = flags.get("project")
1906
+ editor_override = flags.get("editor")
1899
1907
  import importlib
1900
- _setup_client = importlib.import_module("meshcode.setup_clients").setup
1901
- sys.exit(_setup_client(client, project, agent, role))
1908
+ _run = importlib.import_module("meshcode.run_agent").run
1909
+ sys.exit(_run(agent, project=proj_override, editor_override=editor_override))
1902
1910
 
1903
1911
  elif cmd == "login":
1904
1912
  key = sys.argv[2] if len(sys.argv) > 2 else ""
@@ -48,10 +48,29 @@ if not PROJECT_NAME or not AGENT_NAME:
48
48
  sys.exit(2)
49
49
 
50
50
 
51
- # Resolve project_id once at startup; auto-register the agent
52
- _PROJECT_ID: Optional[str] = be.get_project_id(PROJECT_NAME)
51
+ # Resolve project_id at startup. Try in order:
52
+ # 1. MESHCODE_PROJECT_ID env var (baked by `meshcode setup`, fastest)
53
+ # 2. mc_resolve_project RPC with the user's api_key (security definer, bypasses RLS)
54
+ # 3. Direct SELECT via get_project_id (only works if RLS is open / user is admin)
55
+ _PROJECT_ID: Optional[str] = os.environ.get("MESHCODE_PROJECT_ID") or None
53
56
  if not _PROJECT_ID:
54
- print(f"[meshcode-mcp] ERROR: project '{PROJECT_NAME}' not found", file=sys.stderr)
57
+ _api_key = os.environ.get("MESHCODE_API_KEY", "")
58
+ if _api_key:
59
+ try:
60
+ _r = be.sb_rpc("mc_resolve_project", {
61
+ "p_api_key": _api_key,
62
+ "p_project_name": PROJECT_NAME,
63
+ })
64
+ if isinstance(_r, dict) and _r.get("project_id"):
65
+ _PROJECT_ID = _r["project_id"]
66
+ elif isinstance(_r, dict) and _r.get("error"):
67
+ print(f"[meshcode-mcp] WARNING: mc_resolve_project: {_r['error']}", file=sys.stderr)
68
+ except Exception as _e:
69
+ print(f"[meshcode-mcp] WARNING: mc_resolve_project failed: {_e}", file=sys.stderr)
70
+ if not _PROJECT_ID:
71
+ _PROJECT_ID = be.get_project_id(PROJECT_NAME)
72
+ if not _PROJECT_ID:
73
+ print(f"[meshcode-mcp] ERROR: project '{PROJECT_NAME}' not found (check MESHCODE_API_KEY env var)", file=sys.stderr)
55
74
  sys.exit(2)
56
75
 
57
76
  _register_result = be.register_agent(PROJECT_NAME, AGENT_NAME, AGENT_ROLE or "MCP-connected agent")
@@ -59,12 +78,37 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
59
78
  print(f"[meshcode-mcp] WARNING: register failed: {_register_result['error']}", file=sys.stderr)
60
79
 
61
80
  # Flip to online so the dashboard reflects the live MCP session.
62
- # Without this the agent stays in 'needs_setup' / 'offline' even though
63
- # heartbeat is running. The set_status helper updates status + last_heartbeat.
64
- try:
65
- be.set_status(_PROJECT_ID, AGENT_NAME, "online", "MCP session active")
66
- except Exception as _e:
67
- print(f"[meshcode-mcp] WARNING: could not flip status to online: {_e}", file=sys.stderr)
81
+ # Use the SECURITY DEFINER RPC (mc_agent_set_status_by_api_key) so we
82
+ # bypass RLS the publishable key has no JWT context and cannot UPDATE
83
+ # mc_agents directly. The RPC validates ownership via api_key.
84
+ def _flip_status(status: str, task: str = "") -> bool:
85
+ api_key = os.environ.get("MESHCODE_API_KEY", "")
86
+ if not api_key:
87
+ # Last-resort fallback: try the direct PATCH (may be denied by RLS)
88
+ try:
89
+ be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
90
+ return True
91
+ except Exception:
92
+ return False
93
+ try:
94
+ r = be.sb_rpc("mc_agent_set_status_by_api_key", {
95
+ "p_api_key": api_key,
96
+ "p_project_id": _PROJECT_ID,
97
+ "p_agent_name": AGENT_NAME,
98
+ "p_status": status,
99
+ "p_task": task,
100
+ })
101
+ if isinstance(r, dict) and r.get("ok"):
102
+ return True
103
+ if isinstance(r, dict) and r.get("error"):
104
+ log.warning(f"set_status RPC: {r['error']}")
105
+ return False
106
+ except Exception as e:
107
+ log.warning(f"set_status RPC threw: {e}")
108
+ return False
109
+
110
+ if not _flip_status("online", "MCP session active"):
111
+ print(f"[meshcode-mcp] WARNING: could not flip status to online", file=sys.stderr)
68
112
 
69
113
 
70
114
  # ============================================================
@@ -0,0 +1,151 @@
1
+ """`meshcode run <agent>` — universal agent launcher.
2
+
3
+ Looks up the agent's workspace dir (created by `meshcode setup`) in the
4
+ registry at ~/meshcode/.registry.json, auto-detects the user's MCP-aware
5
+ editor, and launches it pointing at that workspace's isolated .mcp.json
6
+ so ONLY this one agent loads in the new window.
7
+
8
+ Editor detection order (first match wins):
9
+ 1. $MESHCODE_EDITOR env var (explicit override: claude / cursor / code)
10
+ 2. claude (Claude Code) — uses --mcp-config flag for per-instance config
11
+ 3. cursor — opens Cursor in the workspace dir (reads .cursor/mcp.json)
12
+ 4. code (VS Code, used by Cline) — opens VS Code in the workspace dir
13
+ (Cline reads .vscode/mcp.json)
14
+
15
+ The workspace dir contains all three config files (.mcp.json,
16
+ .cursor/mcp.json, .vscode/mcp.json) so any of these editors works
17
+ identically. Closing the editor window kills the MCP subprocess and
18
+ the agent flips to offline.
19
+ """
20
+ import json
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+ from typing import Optional, Tuple
27
+
28
+ WORKSPACES_ROOT = Path.home() / "meshcode"
29
+ REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
30
+
31
+
32
+ def _load_registry() -> dict:
33
+ if not REGISTRY_PATH.exists():
34
+ return {}
35
+ try:
36
+ return json.loads(REGISTRY_PATH.read_text())
37
+ except Exception:
38
+ return {}
39
+
40
+
41
+ def _find_agent_workspace(agent: str, project: Optional[str] = None) -> Optional[Tuple[Path, str]]:
42
+ """Look up the agent in the registry. Returns (workspace_path, project_name) or None.
43
+
44
+ If multiple agents share the same name across projects, requires the
45
+ user to disambiguate by passing project explicitly.
46
+ """
47
+ reg = _load_registry()
48
+ agents = reg.get("agents", {})
49
+
50
+ # Direct hit by agent name (current model: agent names unique within registry)
51
+ info = agents.get(agent)
52
+ if info:
53
+ ws = Path(info["workspace"])
54
+ if not ws.exists():
55
+ print(f"[meshcode] ERROR: workspace dir for '{agent}' is missing: {ws}", file=sys.stderr)
56
+ print(f"[meshcode] Re-run: meshcode setup {info.get('project','<project>')} {agent}", file=sys.stderr)
57
+ return None
58
+ if project and info.get("project") != project:
59
+ print(f"[meshcode] ERROR: agent '{agent}' belongs to project '{info.get('project')}', not '{project}'", file=sys.stderr)
60
+ return None
61
+ return ws, info.get("project", "")
62
+
63
+ # Fallback: scan ~/meshcode for any dir matching <project>-<agent>
64
+ if project:
65
+ ws = WORKSPACES_ROOT / f"{project}-{agent}"
66
+ if ws.exists():
67
+ return ws, project
68
+ else:
69
+ # Scan all workspaces for any matching <*>-<agent>
70
+ if WORKSPACES_ROOT.exists():
71
+ matches = [p for p in WORKSPACES_ROOT.iterdir() if p.is_dir() and p.name.endswith(f"-{agent}")]
72
+ if len(matches) == 1:
73
+ return matches[0], matches[0].name.rsplit(f"-{agent}", 1)[0]
74
+ if len(matches) > 1:
75
+ print(f"[meshcode] ERROR: agent '{agent}' exists in multiple projects:", file=sys.stderr)
76
+ for m in matches:
77
+ print(f"[meshcode] {m.name}", file=sys.stderr)
78
+ print(f"[meshcode] Disambiguate: meshcode run {agent} --project <name>", file=sys.stderr)
79
+ return None
80
+
81
+ print(f"[meshcode] ERROR: no workspace found for agent '{agent}'", file=sys.stderr)
82
+ print(f"[meshcode] Run `meshcode setup <project> {agent}` first.", file=sys.stderr)
83
+ return None
84
+
85
+
86
+ def _detect_editor() -> Optional[str]:
87
+ """Pick the user's preferred MCP-aware editor."""
88
+ override = os.environ.get("MESHCODE_EDITOR", "").strip().lower()
89
+ if override:
90
+ if shutil.which(override):
91
+ return override
92
+ print(f"[meshcode] WARNING: MESHCODE_EDITOR='{override}' not found in PATH", file=sys.stderr)
93
+
94
+ for cmd in ("claude", "cursor", "code"):
95
+ if shutil.which(cmd):
96
+ return cmd
97
+ return None
98
+
99
+
100
+ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None) -> int:
101
+ """Launch the user's editor with ONLY the named agent's MCP server loaded."""
102
+ found = _find_agent_workspace(agent, project)
103
+ if not found:
104
+ return 2
105
+ ws, resolved_project = found
106
+ server_id = f"meshcode-{resolved_project}-{agent}"
107
+
108
+ editor = editor_override or _detect_editor()
109
+ if not editor:
110
+ print("[meshcode] ERROR: no MCP-aware editor found in PATH (claude / cursor / code).", file=sys.stderr)
111
+ print(f"[meshcode] Workspace is ready at: {ws}", file=sys.stderr)
112
+ print("[meshcode] Open it manually with the editor of your choice.", file=sys.stderr)
113
+ return 2
114
+
115
+ print(f"[meshcode] Launching {editor} for agent '{agent}' (project: {resolved_project})")
116
+ print(f"[meshcode] Workspace: {ws}")
117
+ print(f"[meshcode] MCP server: {server_id}")
118
+ print(f"[meshcode] Closing the editor will flip this agent offline.")
119
+ print()
120
+
121
+ if editor == "claude":
122
+ # Claude Code: pass --mcp-config to point at the workspace's .mcp.json
123
+ # and --strict-mcp-config so it ignores ~/.claude.json's mcpServers.
124
+ cmd = [
125
+ editor,
126
+ "--mcp-config", str(ws / ".mcp.json"),
127
+ "--strict-mcp-config",
128
+ "--dangerously-skip-permissions",
129
+ ]
130
+ try:
131
+ os.chdir(ws)
132
+ except Exception:
133
+ pass
134
+ elif editor == "cursor":
135
+ # Cursor reads .cursor/mcp.json from the workspace cwd.
136
+ cmd = [editor, str(ws)]
137
+ elif editor == "code":
138
+ # VS Code (Cline reads .vscode/mcp.json from workspace cwd).
139
+ cmd = [editor, str(ws)]
140
+ else:
141
+ cmd = [editor, str(ws)]
142
+
143
+ try:
144
+ # Replace this process with the editor so the user gets a clean tab.
145
+ os.execvp(cmd[0], cmd)
146
+ except FileNotFoundError:
147
+ print(f"[meshcode] ERROR: '{editor}' not found in PATH", file=sys.stderr)
148
+ return 127
149
+ except Exception as e:
150
+ print(f"[meshcode] ERROR launching {editor}: {e}", file=sys.stderr)
151
+ return 1
@@ -0,0 +1,297 @@
1
+ """Per-agent workspace creator for `meshcode setup`.
2
+
3
+ NEW MODEL (1.3.0): instead of polluting the user's global MCP config
4
+ (~/.claude.json, ~/.cursor/mcp.json, ...), each `meshcode setup <project>
5
+ <agent>` creates an isolated workspace directory at:
6
+
7
+ ~/meshcode/<project>-<agent>/
8
+
9
+ containing a single .mcp.json with ONLY that agent's MCP server entry.
10
+ The user then runs `meshcode run <agent>` to launch their editor with
11
+ that workspace, and ONLY that one agent goes online. Closing the editor
12
+ window flips the agent offline. No cross-window status pollution.
13
+
14
+ Backward compatibility: the old `meshcode setup <client> <project>
15
+ <agent>` form still works for the global-config flow (kept for users
16
+ on Claude Desktop, which doesn't support per-instance MCP configs).
17
+ """
18
+ import json
19
+ import os
20
+ import platform
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Dict, Any, Optional
24
+
25
+
26
+ def _load_credentials() -> Dict[str, str]:
27
+ creds_path = Path.home() / ".meshcode" / "credentials.json"
28
+ if not creds_path.exists():
29
+ print("[meshcode] ERROR: No credentials found. Run `meshcode login <api_key>` first.", file=sys.stderr)
30
+ sys.exit(2)
31
+ return json.loads(creds_path.read_text())
32
+
33
+
34
+ def _load_supabase_env() -> Dict[str, str]:
35
+ """Read SUPABASE_URL/SUPABASE_KEY from env or ~/.meshcode/env, fall back to publishable defaults."""
36
+ url = os.environ.get("SUPABASE_URL", "")
37
+ key = os.environ.get("SUPABASE_KEY", "")
38
+ if not url or not key:
39
+ env_file = Path.home() / ".meshcode" / "env"
40
+ if env_file.exists():
41
+ for line in env_file.read_text().splitlines():
42
+ line = line.strip()
43
+ if line.startswith("export "):
44
+ line = line[7:]
45
+ if "=" in line:
46
+ k, v = line.split("=", 1)
47
+ v = v.strip().strip('"').strip("'")
48
+ if k == "SUPABASE_URL" and not url:
49
+ url = v
50
+ elif k == "SUPABASE_KEY" and not key:
51
+ key = v
52
+ if not url:
53
+ url = "https://wwgzzmydrwrjgaebspdo.supabase.co"
54
+ if not key:
55
+ key = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
56
+ return {"SUPABASE_URL": url, "SUPABASE_KEY": key}
57
+
58
+
59
+ def _resolve_project_id(api_key: str, project: str, sb: Dict[str, str]) -> str:
60
+ """Call mc_resolve_project SECURITY DEFINER RPC to get the project_id."""
61
+ try:
62
+ from urllib.request import Request as _Req, urlopen as _urlopen
63
+ body = json.dumps({"p_api_key": api_key, "p_project_name": project}).encode()
64
+ req = _Req(
65
+ f"{sb['SUPABASE_URL']}/rest/v1/rpc/mc_resolve_project",
66
+ data=body,
67
+ method="POST",
68
+ headers={
69
+ "apikey": sb["SUPABASE_KEY"],
70
+ "Authorization": f"Bearer {sb['SUPABASE_KEY']}",
71
+ "Content-Type": "application/json",
72
+ },
73
+ )
74
+ with _urlopen(req, timeout=10) as resp:
75
+ data = json.loads(resp.read().decode())
76
+ if isinstance(data, dict) and data.get("project_id"):
77
+ return data["project_id"]
78
+ if isinstance(data, dict) and data.get("error"):
79
+ print(f"[meshcode] ERROR: could not resolve project '{project}': {data['error']}", file=sys.stderr)
80
+ sys.exit(2)
81
+ except Exception as e:
82
+ print(f"[meshcode] WARNING: project_id resolution failed ({e}); MCP server will retry at boot", file=sys.stderr)
83
+ return ""
84
+
85
+
86
+ def _build_server_block(project: str, project_id: str, agent: str, role: str,
87
+ api_key: str, sb: Dict[str, str]) -> Dict[str, Any]:
88
+ return {
89
+ "command": sys.executable or "python3",
90
+ "args": ["-m", "meshcode.meshcode_mcp", "serve"],
91
+ "env": {
92
+ "MESHCODE_PROJECT": project,
93
+ "MESHCODE_PROJECT_ID": project_id,
94
+ "MESHCODE_AGENT": agent,
95
+ "MESHCODE_ROLE": role or "MCP-connected agent",
96
+ "MESHCODE_API_KEY": api_key,
97
+ "SUPABASE_URL": sb["SUPABASE_URL"],
98
+ "SUPABASE_KEY": sb["SUPABASE_KEY"],
99
+ },
100
+ }
101
+
102
+
103
+ def _atomic_write_json(path: Path, data: dict) -> None:
104
+ path.parent.mkdir(parents=True, exist_ok=True)
105
+ tmp = path.with_suffix(path.suffix + ".tmp")
106
+ tmp.write_text(json.dumps(data, indent=2))
107
+ tmp.replace(path)
108
+
109
+
110
+ # ============================================================
111
+ # WORKSPACE MODEL (1.3.0+) — one isolated dir per agent
112
+ # ============================================================
113
+
114
+ WORKSPACES_ROOT = Path.home() / "meshcode"
115
+
116
+
117
+ def _workspace_dir(project: str, agent: str) -> Path:
118
+ return WORKSPACES_ROOT / f"{project}-{agent}"
119
+
120
+
121
+ def setup_workspace(project: str, agent: str, role: str = "") -> int:
122
+ """Create an isolated workspace dir at ~/meshcode/<project>-<agent>/ with
123
+ .mcp.json containing ONLY this agent's server entry. Universal for any
124
+ MCP-compatible editor that supports per-directory configs (Claude Code via
125
+ --mcp-config flag, Cursor + Cline via .cursor/mcp.json + .vscode/mcp.json).
126
+ """
127
+ creds = _load_credentials()
128
+ sb = _load_supabase_env()
129
+ api_key = creds.get("api_key", "")
130
+ project_id = _resolve_project_id(api_key, project, sb)
131
+
132
+ server_id = f"meshcode-{project}-{agent}"
133
+ server_block = _build_server_block(project, project_id, agent, role, api_key, sb)
134
+
135
+ ws = _workspace_dir(project, agent)
136
+ ws.mkdir(parents=True, exist_ok=True)
137
+
138
+ # Write 3 config files inside the workspace so any MCP-aware editor
139
+ # opened in this dir picks up the meshcode server.
140
+ #
141
+ # .mcp.json — Claude Code project-scoped MCP config
142
+ # .cursor/mcp.json — Cursor workspace-scoped MCP config
143
+ # .vscode/mcp.json — Cline / VS Code workspace-scoped MCP config
144
+ #
145
+ # All three contain the SAME single server entry. Whichever editor the
146
+ # user opens here will see exactly one agent: this one.
147
+ server_doc = {"mcpServers": {server_id: server_block}}
148
+ _atomic_write_json(ws / ".mcp.json", server_doc)
149
+ _atomic_write_json(ws / ".cursor" / "mcp.json", server_doc)
150
+ _atomic_write_json(ws / ".vscode" / "mcp.json", server_doc)
151
+
152
+ # A tiny manifest the `meshcode run` command reads to know where this
153
+ # workspace lives. Stored at the root so `meshcode run <agent>` can find
154
+ # it without scanning every dir.
155
+ registry_path = WORKSPACES_ROOT / ".registry.json"
156
+ registry: Dict[str, Any] = {}
157
+ if registry_path.exists():
158
+ try:
159
+ registry = json.loads(registry_path.read_text())
160
+ except Exception:
161
+ registry = {}
162
+ registry.setdefault("agents", {})[agent] = {
163
+ "project": project,
164
+ "workspace": str(ws),
165
+ "server_id": server_id,
166
+ "role": role,
167
+ }
168
+ _atomic_write_json(registry_path, registry)
169
+
170
+ print(f"[meshcode] ✓ Workspace created for agent '{agent}' (project: {project})")
171
+ print(f"[meshcode] Path: {ws}")
172
+ print(f"[meshcode]")
173
+ print(f"[meshcode] To launch this agent in your editor:")
174
+ print(f"[meshcode] meshcode run {agent}")
175
+ print(f"[meshcode]")
176
+ print(f"[meshcode] Closing the editor window flips this agent offline.")
177
+ print(f"[meshcode] Other agents stay independent — no cross-window pollution.")
178
+ return 0
179
+
180
+
181
+ # ============================================================
182
+ # LEGACY GLOBAL-CONFIG MODEL (kept for Claude Desktop)
183
+ # ============================================================
184
+
185
+ CLIENT_CONFIG_PATHS = {
186
+ "claude-code": {
187
+ "Darwin": Path.home() / ".claude.json",
188
+ "Linux": Path.home() / ".claude.json",
189
+ "Windows": Path.home() / ".claude.json",
190
+ },
191
+ "cursor": {
192
+ "Darwin": Path.home() / ".cursor" / "mcp.json",
193
+ "Linux": Path.home() / ".cursor" / "mcp.json",
194
+ "Windows": Path.home() / ".cursor" / "mcp.json",
195
+ },
196
+ "cline": {
197
+ "Darwin": Path.home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
198
+ "Linux": Path.home() / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
199
+ "Windows": Path.home() / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
200
+ },
201
+ "claude-desktop": {
202
+ "Darwin": Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
203
+ "Linux": Path.home() / ".config" / "Claude" / "claude_desktop_config.json",
204
+ "Windows": Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json",
205
+ },
206
+ }
207
+
208
+ CLIENT_DISPLAY_NAMES = {
209
+ "claude-code": "Claude Code",
210
+ "cursor": "Cursor",
211
+ "cline": "Cline (VS Code)",
212
+ "claude-desktop": "Claude Desktop",
213
+ }
214
+
215
+
216
+ def setup_global(client: str, project: str, agent: str, role: str = "") -> int:
217
+ """LEGACY: write to the user's GLOBAL MCP config file. All agents that
218
+ setup this way will load in EVERY window of the editor — they all show
219
+ online whenever any window is open. Kept for Claude Desktop and other
220
+ clients that don't support per-instance configs."""
221
+ if client not in CLIENT_CONFIG_PATHS:
222
+ print(f"[meshcode] ERROR: Unknown client '{client}'. Supported: {', '.join(CLIENT_CONFIG_PATHS)}", file=sys.stderr)
223
+ return 2
224
+
225
+ creds = _load_credentials()
226
+ sb = _load_supabase_env()
227
+ api_key = creds.get("api_key", "")
228
+ project_id = _resolve_project_id(api_key, project, sb)
229
+
230
+ os_name = platform.system()
231
+ config_path = CLIENT_CONFIG_PATHS[client].get(os_name)
232
+ if config_path is None:
233
+ print(f"[meshcode] ERROR: '{client}' is not supported on {os_name}", file=sys.stderr)
234
+ return 2
235
+
236
+ existing: Dict[str, Any] = {}
237
+ if config_path.exists():
238
+ try:
239
+ existing = json.loads(config_path.read_text())
240
+ except json.JSONDecodeError:
241
+ print(f"[meshcode] WARNING: {config_path} exists but is not valid JSON.", file=sys.stderr)
242
+ return 2
243
+
244
+ if not isinstance(existing, dict):
245
+ existing = {}
246
+
247
+ servers = existing.setdefault("mcpServers", {})
248
+ if not isinstance(servers, dict):
249
+ print(f"[meshcode] ERROR: 'mcpServers' in {config_path} is not an object", file=sys.stderr)
250
+ return 2
251
+
252
+ server_id = f"meshcode-{project}-{agent}"
253
+ servers[server_id] = _build_server_block(project, project_id, agent, role, api_key, sb)
254
+ _atomic_write_json(config_path, existing)
255
+
256
+ display = CLIENT_DISPLAY_NAMES[client]
257
+ print(f"[meshcode] MCP server '{server_id}' added to {display} GLOBAL config")
258
+ print(f"[meshcode] Path: {config_path}")
259
+ print(f"[meshcode] ⚠️ Global mode: this agent will appear in EVERY window of {display}.")
260
+ print(f"[meshcode] For per-window agent isolation, use the workspace flow instead:")
261
+ print(f"[meshcode] meshcode setup {project} {agent}")
262
+ print(f"[meshcode] meshcode run {agent}")
263
+ return 0
264
+
265
+
266
+ # ============================================================
267
+ # DISPATCHER (called from comms_v4.py CLI)
268
+ # ============================================================
269
+
270
+ def setup(*args) -> int:
271
+ """Smart dispatcher.
272
+
273
+ Forms supported:
274
+ meshcode setup <project> <agent> [role] → workspace flow (NEW)
275
+ meshcode setup <client> <project> <agent> [role] → legacy global flow
276
+
277
+ Detection: if the first arg is one of the known client slugs, fall back
278
+ to the legacy global flow. Otherwise treat the first arg as a project
279
+ name and create a workspace.
280
+ """
281
+ if not args:
282
+ print("Usage:", file=sys.stderr)
283
+ print(" meshcode setup <project> <agent> [role] # workspace flow (recommended)", file=sys.stderr)
284
+ print(" meshcode setup <client> <project> <agent> [role] # legacy global flow", file=sys.stderr)
285
+ print(" Clients: claude-code, cursor, cline, claude-desktop", file=sys.stderr)
286
+ return 1
287
+
288
+ if args[0] in CLIENT_CONFIG_PATHS:
289
+ if len(args) < 3:
290
+ print("Usage: meshcode setup <client> <project> <agent> [role]", file=sys.stderr)
291
+ return 1
292
+ return setup_global(args[0], args[1], args[2], args[3] if len(args) > 3 else "")
293
+
294
+ if len(args) < 2:
295
+ print("Usage: meshcode setup <project> <agent> [role]", file=sys.stderr)
296
+ return 1
297
+ return setup_workspace(args[0], args[1], args[2] if len(args) > 2 else "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.2.5
3
+ Version: 1.3.0
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -6,6 +6,7 @@ meshcode/comms_v4.py
6
6
  meshcode/launcher.py
7
7
  meshcode/launcher_install.py
8
8
  meshcode/protocol_v2.py
9
+ meshcode/run_agent.py
9
10
  meshcode/setup_clients.py
10
11
  meshcode.egg-info/PKG-INFO
11
12
  meshcode.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "1.2.5"
7
+ version = "1.3.0"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,145 +0,0 @@
1
- """Per-client MCP server config writers for `meshcode setup <client>`.
2
-
3
- Supported clients: claude-code, cursor, cline, claude-desktop.
4
-
5
- Each writer:
6
- 1. Resolves the right config file path for the current OS.
7
- 2. Loads existing JSON (or empty dict) and merges the meshcode MCP server entry
8
- under "mcpServers". Never clobbers other entries.
9
- 3. Writes back atomically (temp file + rename).
10
- 4. Prints a success message with the path and "restart {client}" instructions.
11
- """
12
- import json
13
- import os
14
- import platform
15
- import sys
16
- from pathlib import Path
17
- from typing import Dict, Any
18
-
19
-
20
- def _load_credentials() -> Dict[str, str]:
21
- creds_path = Path.home() / ".meshcode" / "credentials.json"
22
- if not creds_path.exists():
23
- print("[meshcode] ERROR: No credentials found. Run `meshcode login <api_key>` first.", file=sys.stderr)
24
- sys.exit(2)
25
- return json.loads(creds_path.read_text())
26
-
27
-
28
- def _load_supabase_env() -> Dict[str, str]:
29
- """Read SUPABASE_URL/SUPABASE_KEY from env or ~/.meshcode/env."""
30
- url = os.environ.get("SUPABASE_URL", "")
31
- key = os.environ.get("SUPABASE_KEY", "")
32
- if not url or not key:
33
- env_file = Path.home() / ".meshcode" / "env"
34
- if env_file.exists():
35
- for line in env_file.read_text().splitlines():
36
- line = line.strip()
37
- if line.startswith("export "):
38
- line = line[7:]
39
- if "=" in line:
40
- k, v = line.split("=", 1)
41
- v = v.strip().strip('"').strip("'")
42
- if k == "SUPABASE_URL" and not url:
43
- url = v
44
- elif k == "SUPABASE_KEY" and not key:
45
- key = v
46
- if not url:
47
- url = "https://wwgzzmydrwrjgaebspdo.supabase.co"
48
- if not key:
49
- key = "sb_publishable_0qf0U1GURopPIxLR8Vu7eQ_5grflPP4"
50
- return {"SUPABASE_URL": url, "SUPABASE_KEY": key}
51
-
52
-
53
- CLIENT_CONFIG_PATHS = {
54
- "claude-code": {
55
- "Darwin": Path.home() / ".claude.json",
56
- "Linux": Path.home() / ".claude.json",
57
- "Windows": Path.home() / ".claude.json",
58
- },
59
- "cursor": {
60
- "Darwin": Path.home() / ".cursor" / "mcp.json",
61
- "Linux": Path.home() / ".cursor" / "mcp.json",
62
- "Windows": Path.home() / ".cursor" / "mcp.json",
63
- },
64
- "cline": {
65
- "Darwin": Path.home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
66
- "Linux": Path.home() / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
67
- "Windows": Path.home() / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
68
- },
69
- "claude-desktop": {
70
- "Darwin": Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
71
- "Linux": Path.home() / ".config" / "Claude" / "claude_desktop_config.json",
72
- "Windows": Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json",
73
- },
74
- }
75
-
76
- CLIENT_DISPLAY_NAMES = {
77
- "claude-code": "Claude Code",
78
- "cursor": "Cursor",
79
- "cline": "Cline (VS Code)",
80
- "claude-desktop": "Claude Desktop",
81
- }
82
-
83
-
84
- def _atomic_write_json(path: Path, data: dict) -> None:
85
- path.parent.mkdir(parents=True, exist_ok=True)
86
- tmp = path.with_suffix(path.suffix + ".tmp")
87
- tmp.write_text(json.dumps(data, indent=2))
88
- tmp.replace(path)
89
-
90
-
91
- def setup(client: str, project: str, agent: str, role: str = "") -> int:
92
- """Write MCP server config block for the given client. Returns 0 on success."""
93
- if client not in CLIENT_CONFIG_PATHS:
94
- print(f"[meshcode] ERROR: Unknown client '{client}'. Supported: {', '.join(CLIENT_CONFIG_PATHS)}", file=sys.stderr)
95
- return 2
96
-
97
- creds = _load_credentials()
98
- sb = _load_supabase_env()
99
- api_key = creds.get("api_key", "")
100
-
101
- os_name = platform.system()
102
- config_path = CLIENT_CONFIG_PATHS[client].get(os_name)
103
- if config_path is None:
104
- print(f"[meshcode] ERROR: '{client}' is not supported on {os_name}", file=sys.stderr)
105
- return 2
106
-
107
- existing: Dict[str, Any] = {}
108
- if config_path.exists():
109
- try:
110
- existing = json.loads(config_path.read_text())
111
- except json.JSONDecodeError:
112
- print(f"[meshcode] WARNING: {config_path} exists but is not valid JSON. Overwriting may break {CLIENT_DISPLAY_NAMES[client]}.", file=sys.stderr)
113
- return 2
114
-
115
- if not isinstance(existing, dict):
116
- existing = {}
117
-
118
- servers = existing.setdefault("mcpServers", {})
119
- if not isinstance(servers, dict):
120
- print(f"[meshcode] ERROR: 'mcpServers' in {config_path} is not an object", file=sys.stderr)
121
- return 2
122
-
123
- server_id = f"meshcode-{project}-{agent}"
124
- servers[server_id] = {
125
- "command": sys.executable or "python3",
126
- "args": ["-m", "meshcode.meshcode_mcp", "serve"],
127
- "env": {
128
- "MESHCODE_PROJECT": project,
129
- "MESHCODE_AGENT": agent,
130
- "MESHCODE_ROLE": role or "MCP-connected agent",
131
- "MESHCODE_API_KEY": api_key,
132
- "SUPABASE_URL": sb["SUPABASE_URL"],
133
- "SUPABASE_KEY": sb["SUPABASE_KEY"],
134
- },
135
- }
136
-
137
- _atomic_write_json(config_path, existing)
138
-
139
- display = CLIENT_DISPLAY_NAMES[client]
140
- print(f"[meshcode] MCP server '{server_id}' added to {display} config")
141
- print(f"[meshcode] Path: {config_path}")
142
- print(f"[meshcode] Restart {display} to load the server.")
143
- print(f"[meshcode] Inside {display}, verify with /mcp — you should see '{server_id}' connected.")
144
- print(f"[meshcode] Tools: meshcode_send, meshcode_check, meshcode_read, meshcode_status, meshcode_set_status, ...")
145
- return 0
File without changes
File without changes
File without changes
File without changes