meshcode 1.8.0__tar.gz → 1.8.2__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 (28) hide show
  1. {meshcode-1.8.0 → meshcode-1.8.2}/PKG-INFO +1 -1
  2. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/__init__.py +1 -1
  3. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/comms_v4.py +36 -1
  4. meshcode-1.8.2/meshcode/preferences.py +171 -0
  5. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/run_agent.py +10 -2
  6. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode.egg-info/SOURCES.txt +1 -0
  8. {meshcode-1.8.0 → meshcode-1.8.2}/pyproject.toml +1 -1
  9. {meshcode-1.8.0 → meshcode-1.8.2}/README.md +0 -0
  10. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/cli.py +0 -0
  11. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/invites.py +0 -0
  12. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/launcher.py +0 -0
  13. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/launcher_install.py +0 -0
  14. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/__init__.py +0 -0
  15. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/__main__.py +0 -0
  16. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/backend.py +0 -0
  17. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/realtime.py +0 -0
  18. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/server.py +0 -0
  19. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/secrets.py +0 -0
  23. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode/setup_clients.py +0 -0
  24. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode.egg-info/dependency_links.txt +0 -0
  25. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode.egg-info/entry_points.txt +0 -0
  26. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode.egg-info/requires.txt +0 -0
  27. {meshcode-1.8.0 → meshcode-1.8.2}/meshcode.egg-info/top_level.txt +0 -0
  28. {meshcode-1.8.0 → meshcode-1.8.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.8.0
3
+ Version: 1.8.2
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.8.0"
2
+ __version__ = "1.8.2"
@@ -1946,9 +1946,14 @@ if __name__ == "__main__":
1946
1946
  agent = sys.argv[2]
1947
1947
  proj_override = flags.get("project")
1948
1948
  editor_override = flags.get("editor")
1949
+ perm_override = flags.get("permission") # bypass | safe | ask
1950
+ if "--bypass" in sys.argv:
1951
+ perm_override = "bypass"
1952
+ elif "--safe" in sys.argv:
1953
+ perm_override = "safe"
1949
1954
  import importlib
1950
1955
  _run = importlib.import_module("meshcode.run_agent").run
1951
- sys.exit(_run(agent, project=proj_override, editor_override=editor_override))
1956
+ sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override))
1952
1957
 
1953
1958
  elif cmd == "invite":
1954
1959
  # meshcode invite <project> <agent> [--role "..."] [--days N]
@@ -2017,6 +2022,36 @@ if __name__ == "__main__":
2017
2022
  sys.exit(1)
2018
2023
  login(key)
2019
2024
 
2025
+ elif cmd == "prefs":
2026
+ # meshcode prefs permission-mode [bypass|safe|ask] (no arg = show)
2027
+ # meshcode prefs reset
2028
+ from meshcode.preferences import (
2029
+ get_permission_mode, set_permission_mode, reset_permission_mode,
2030
+ VALID_PERMISSION_MODES,
2031
+ )
2032
+ sub = sys.argv[2] if len(sys.argv) > 2 else ""
2033
+ if sub == "permission-mode":
2034
+ if len(sys.argv) > 3:
2035
+ mode = sys.argv[3].lower()
2036
+ if mode not in VALID_PERMISSION_MODES:
2037
+ print(f"[ERROR] mode must be one of: {', '.join(sorted(VALID_PERMISSION_MODES))}")
2038
+ sys.exit(1)
2039
+ ok = set_permission_mode(mode)
2040
+ print(f"[meshcode] permission_mode = {mode}" if ok else "[ERROR] failed to save")
2041
+ sys.exit(0 if ok else 1)
2042
+ else:
2043
+ cur = get_permission_mode()
2044
+ print(f"permission_mode: {cur or '(unset — will prompt on next run)'}")
2045
+ sys.exit(0)
2046
+ elif sub == "reset":
2047
+ reset_permission_mode()
2048
+ print("[meshcode] preferences reset")
2049
+ sys.exit(0)
2050
+ else:
2051
+ print("Usage: meshcode prefs permission-mode [bypass|safe|ask]")
2052
+ print(" meshcode prefs reset")
2053
+ sys.exit(1)
2054
+
2020
2055
  elif cmd == "launcher":
2021
2056
  # meshcode launcher {install|uninstall|start|stop|restart|status|logs|test}
2022
2057
  try:
@@ -0,0 +1,171 @@
1
+ """Per-user CLI preferences for MeshCode.
2
+
3
+ Stores small non-secret settings (like permission_mode for `meshcode run`)
4
+ in ~/.meshcode/preferences.json with mode 600. Not the same as the API key
5
+ storage in secrets.py — that goes to the OS keychain. This file is for
6
+ opinion/UX preferences only, no credentials.
7
+
8
+ Currently tracked:
9
+ permission_mode one of "bypass" | "safe" | "ask" | None (unset)
10
+ - "bypass": meshcode run passes --dangerously-skip-permissions
11
+ to Claude Code so the autonomous loop runs without prompts.
12
+ - "safe": meshcode run does NOT pass that flag, so Claude
13
+ prompts the user to approve every tool call. Kills the
14
+ autonomous loop in practice — exists for paranoid users.
15
+ - "ask": prompt every launch.
16
+ - None: never set; meshcode run prompts on first use,
17
+ saves the choice, and uses it from then on.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Any, Dict, Optional
27
+
28
+ PREFS_PATH = Path.home() / ".meshcode" / "preferences.json"
29
+
30
+ VALID_PERMISSION_MODES = {"bypass", "safe", "ask"}
31
+ DEFAULT_PERMISSION_MODE_FOR_NON_TTY = "bypass"
32
+
33
+
34
+ def load_prefs() -> Dict[str, Any]:
35
+ """Load the prefs file. Returns {} if missing or unparseable."""
36
+ if not PREFS_PATH.exists():
37
+ return {}
38
+ try:
39
+ return json.loads(PREFS_PATH.read_text())
40
+ except Exception:
41
+ return {}
42
+
43
+
44
+ def save_prefs(prefs: Dict[str, Any]) -> bool:
45
+ """Atomic write of the prefs file with mode 600."""
46
+ try:
47
+ PREFS_PATH.parent.mkdir(parents=True, exist_ok=True)
48
+ tmp = PREFS_PATH.with_suffix(".tmp")
49
+ tmp.write_text(json.dumps(prefs, indent=2))
50
+ try:
51
+ os.chmod(tmp, 0o600)
52
+ except Exception:
53
+ pass
54
+ tmp.replace(PREFS_PATH)
55
+ return True
56
+ except Exception as e:
57
+ print(f"[meshcode] WARNING: could not save prefs: {e}", file=sys.stderr)
58
+ return False
59
+
60
+
61
+ def get_permission_mode() -> Optional[str]:
62
+ """Returns 'bypass' / 'safe' / 'ask' / None."""
63
+ val = load_prefs().get("permission_mode")
64
+ if isinstance(val, str) and val in VALID_PERMISSION_MODES:
65
+ return val
66
+ return None
67
+
68
+
69
+ def set_permission_mode(mode: str) -> bool:
70
+ """Persist a new permission mode. Returns True on success."""
71
+ if mode not in VALID_PERMISSION_MODES:
72
+ print(f"[meshcode] ERROR: invalid permission mode '{mode}'. Use: {', '.join(sorted(VALID_PERMISSION_MODES))}", file=sys.stderr)
73
+ return False
74
+ prefs = load_prefs()
75
+ prefs["permission_mode"] = mode
76
+ prefs["permission_mode_set_at"] = int(time.time())
77
+ return save_prefs(prefs)
78
+
79
+
80
+ def reset_permission_mode() -> bool:
81
+ """Forget the permission mode so the next launch prompts again."""
82
+ prefs = load_prefs()
83
+ prefs.pop("permission_mode", None)
84
+ prefs.pop("permission_mode_set_at", None)
85
+ return save_prefs(prefs)
86
+
87
+
88
+ def prompt_permission_mode() -> str:
89
+ """Interactive first-run prompt. Returns the chosen mode and saves it.
90
+
91
+ If stdin is not a TTY (CI, automation, no terminal), silently picks
92
+ DEFAULT_PERMISSION_MODE_FOR_NON_TTY ('bypass') and saves it. The user
93
+ can change it later with `meshcode prefs permission-mode <mode>`.
94
+ """
95
+ if not sys.stdin.isatty():
96
+ set_permission_mode(DEFAULT_PERMISSION_MODE_FOR_NON_TTY)
97
+ return DEFAULT_PERMISSION_MODE_FOR_NON_TTY
98
+
99
+ print("", file=sys.stderr)
100
+ print("[meshcode] PERMISSION MODE — choose once (saved for future runs)", file=sys.stderr)
101
+ print("[meshcode]", file=sys.stderr)
102
+ print("[meshcode] Mesh agents work best with bypass mode: tools run without", file=sys.stderr)
103
+ print("[meshcode] confirmation prompts, so the autonomous loop is not interrupted.", file=sys.stderr)
104
+ print("[meshcode] Equivalent to running `claude --dangerously-skip-permissions`.", file=sys.stderr)
105
+ print("[meshcode]", file=sys.stderr)
106
+ print("[meshcode] In bypass mode, your agents can run shell commands, edit files,", file=sys.stderr)
107
+ print("[meshcode] and access the network without asking. Only use bypass in", file=sys.stderr)
108
+ print("[meshcode] directories where you're OK with the agent acting as you.", file=sys.stderr)
109
+ print("[meshcode]", file=sys.stderr)
110
+ print("[meshcode] [B] Bypass mode (recommended for autonomous mesh agents)", file=sys.stderr)
111
+ print("[meshcode] [s] Safe mode (you approve every tool call — kills the loop)", file=sys.stderr)
112
+ print("[meshcode] [a] Ask me each launch", file=sys.stderr)
113
+ print("[meshcode]", file=sys.stderr)
114
+ try:
115
+ ans = input("[meshcode] Pick [B/s/a] (default B): ").strip().lower()
116
+ except (EOFError, KeyboardInterrupt):
117
+ ans = ""
118
+
119
+ if ans in ("", "b", "bypass"):
120
+ chosen = "bypass"
121
+ elif ans in ("s", "safe"):
122
+ chosen = "safe"
123
+ elif ans in ("a", "ask"):
124
+ chosen = "ask"
125
+ else:
126
+ print(f"[meshcode] '{ans}' not recognized — defaulting to bypass.", file=sys.stderr)
127
+ chosen = "bypass"
128
+
129
+ set_permission_mode(chosen)
130
+ print(f"[meshcode] ✓ Saved as '{chosen}'. Change later with: meshcode prefs permission-mode <mode>", file=sys.stderr)
131
+ print("", file=sys.stderr)
132
+ return chosen
133
+
134
+
135
+ def resolve_permission_mode(per_launch_override: Optional[str] = None) -> str:
136
+ """Resolve the effective permission mode for a single launch.
137
+
138
+ Order:
139
+ 1. per-launch override (--bypass / --safe flag) wins
140
+ 2. saved preference
141
+ 3. interactive prompt (saves choice if user is at a TTY)
142
+ 4. fallback to 'bypass' for non-TTY environments
143
+ """
144
+ if per_launch_override in VALID_PERMISSION_MODES:
145
+ return per_launch_override
146
+
147
+ saved = get_permission_mode()
148
+ if saved == "ask":
149
+ # The user opted into being prompted every launch
150
+ return prompt_permission_mode_quick()
151
+ if saved in ("bypass", "safe"):
152
+ return saved
153
+
154
+ # Unset → first-run prompt + save
155
+ return prompt_permission_mode()
156
+
157
+
158
+ def prompt_permission_mode_quick() -> str:
159
+ """One-line prompt for users who picked 'ask each launch'.
160
+
161
+ Does NOT save — the saved preference stays as 'ask'.
162
+ """
163
+ if not sys.stdin.isatty():
164
+ return DEFAULT_PERMISSION_MODE_FOR_NON_TTY
165
+ try:
166
+ ans = input("[meshcode] Permission mode for this launch [B/s] (default B): ").strip().lower()
167
+ except (EOFError, KeyboardInterrupt):
168
+ ans = ""
169
+ if ans in ("s", "safe"):
170
+ return "safe"
171
+ return "bypass"
@@ -25,6 +25,8 @@ import sys
25
25
  from pathlib import Path
26
26
  from typing import Optional, Tuple
27
27
 
28
+ from .preferences import resolve_permission_mode
29
+
28
30
  WORKSPACES_ROOT = Path.home() / "meshcode"
29
31
  REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
30
32
 
@@ -97,7 +99,7 @@ def _detect_editor() -> Optional[str]:
97
99
  return None
98
100
 
99
101
 
100
- def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None) -> int:
102
+ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None, permission_override: Optional[str] = None) -> int:
101
103
  """Launch the user's editor with ONLY the named agent's MCP server loaded."""
102
104
  found = _find_agent_workspace(agent, project)
103
105
  if not found:
@@ -121,12 +123,18 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
121
123
  if editor == "claude":
122
124
  # Claude Code: pass --mcp-config to point at the workspace's .mcp.json
123
125
  # and --strict-mcp-config so it ignores ~/.claude.json's mcpServers.
126
+ mode = resolve_permission_mode(permission_override)
124
127
  cmd = [
125
128
  editor,
126
129
  "--mcp-config", str(ws / ".mcp.json"),
127
130
  "--strict-mcp-config",
128
- "--dangerously-skip-permissions",
129
131
  ]
132
+ if mode == "bypass":
133
+ cmd.append("--dangerously-skip-permissions")
134
+ print(f"[meshcode] Permission mode: bypass (autonomous loop, no prompts)")
135
+ else:
136
+ print(f"[meshcode] Permission mode: safe (Claude will prompt for every tool call)")
137
+ print(f"[meshcode] Tip: change with `meshcode prefs permission-mode bypass`")
130
138
  try:
131
139
  os.chdir(ws)
132
140
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.8.0
3
+ Version: 1.8.2
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/invites.py
7
7
  meshcode/launcher.py
8
8
  meshcode/launcher_install.py
9
+ meshcode/preferences.py
9
10
  meshcode/protocol_v2.py
10
11
  meshcode/run_agent.py
11
12
  meshcode/secrets.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "1.8.0"
7
+ version = "1.8.2"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes