meshapi-code 0.3.2__tar.gz → 0.3.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshapi-code
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Terminal chat for Mesh API — OpenAI-compatible LLM gateway
5
5
  Project-URL: Homepage, https://meshapi.ai
6
6
  Project-URL: Documentation, https://docs.meshapi.ai
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "meshapi-code"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "Terminal chat for Mesh API — OpenAI-compatible LLM gateway"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3"
@@ -1,6 +1,7 @@
1
1
  """meshapi — terminal chat REPL for Mesh API."""
2
2
  import argparse
3
3
  import json
4
+ import re
4
5
  import sys
5
6
  from pathlib import Path
6
7
 
@@ -15,7 +16,7 @@ from rich.text import Text
15
16
  from . import __version__
16
17
  from .client import stream_chat
17
18
  from .commands import handle_command
18
- from .config import CONFIG_FILE, HISTORY_FILE, load_config
19
+ from .config import CONFIG_FILE, HISTORY_FILE, load_config, secure_file
19
20
  from .permissions import HINTS, LABELS, Mode, from_str, next_mode
20
21
  from .render import (
21
22
  BRAND, BRAND_BG, BRAND_BG_FG, BRAND_DIM, console, fmt_usd, pretty_cwd, render_stream,
@@ -34,6 +35,23 @@ MESH_LOGO_LINES = [
34
35
  LOGO_WIDTH = 35 # chars per line
35
36
  LOGO_GUTTER = 3 # spaces between logo and info column
36
37
 
38
+ # Mesh data-plane keys are `rsk_` followed by an opaque token. Prevent these
39
+ # strings from being persisted to the prompt-toolkit history file in case a
40
+ # user pastes one at the prompt by accident.
41
+ _API_KEY_RE = re.compile(r"\brsk_[A-Za-z0-9_-]{8,}\b")
42
+
43
+
44
+ class ScrubbedFileHistory(FileHistory):
45
+ """FileHistory that drops entries containing API-key-shaped strings
46
+ and tightens file perms to 0600 after every write."""
47
+
48
+ def store_string(self, string: str) -> None:
49
+ if _API_KEY_RE.search(string):
50
+ return
51
+ super().store_string(string)
52
+ secure_file(Path(self.filename))
53
+
54
+
37
55
  def parse_args(argv=None) -> argparse.Namespace:
38
56
  p = argparse.ArgumentParser(prog="meshapi", description="Terminal chat for Mesh API")
39
57
  p.add_argument("--version", action="version", version=f"meshapi {__version__}")
@@ -72,10 +90,28 @@ def render_banner(cfg: dict) -> None:
72
90
  console.print()
73
91
 
74
92
 
93
+ def _resolved_path_line(raw: str) -> str:
94
+ """Render `→ /abs/path` and flag if the path escapes the launch cwd."""
95
+ try:
96
+ resolved = Path(raw).expanduser().resolve()
97
+ except Exception:
98
+ return f"[dim]→ {raw}[/dim]"
99
+ cwd = Path.cwd().resolve()
100
+ try:
101
+ outside = not resolved.is_relative_to(cwd)
102
+ except AttributeError: # is_relative_to is 3.9+, but pyproject pins 3.10+
103
+ outside = not str(resolved).startswith(str(cwd))
104
+ if outside:
105
+ return f"[dim]→[/dim] [bold yellow]{resolved}[/bold yellow] [bold yellow](outside cwd)[/bold yellow]"
106
+ return f"[dim]→ {resolved}[/dim]"
107
+
108
+
75
109
  def confirm_tool_call(name: str, args: dict) -> bool:
76
110
  """ASK-mode prompt for a single tool call. Returns True if approved."""
77
111
  summary = summarize_call(name, args)
78
112
  console.print(f"[bold {BRAND}]⚙ approve tool call?[/bold {BRAND}] [dim]{summary}[/dim]")
113
+ if name in ("read_file", "write_file"):
114
+ console.print(_resolved_path_line(args.get("path") or ""))
79
115
  if name == "write_file":
80
116
  preview = (args.get("content") or "")[:300]
81
117
  console.print(f"[dim]──[/dim]\n{preview}{'…' if len(args.get('content') or '') > 300 else ''}\n[dim]──[/dim]")
@@ -163,8 +199,12 @@ def main() -> None:
163
199
  ("ansibrightblack", "shift+tab to cycle"),
164
200
  ])
165
201
 
202
+ # Touch the history file with 0600 up front so prompt_toolkit doesn't
203
+ # create it world-readable on first write.
204
+ HISTORY_FILE.touch(mode=0o600, exist_ok=True)
205
+ secure_file(HISTORY_FILE)
166
206
  session = PromptSession(
167
- history=FileHistory(str(HISTORY_FILE)),
207
+ history=ScrubbedFileHistory(str(HISTORY_FILE)),
168
208
  key_bindings=kb,
169
209
  bottom_toolbar=bottom_toolbar,
170
210
  )
@@ -0,0 +1,79 @@
1
+ """Config storage at ~/.meshapi/config.json."""
2
+ import json
3
+ import os
4
+ import stat
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ CONFIG_DIR = Path.home() / ".meshapi"
9
+ CONFIG_FILE = CONFIG_DIR / "config.json"
10
+ HISTORY_FILE = CONFIG_DIR / "history"
11
+
12
+ DEFAULT_CONFIG = {
13
+ "base_url": "https://api.meshapi.ai/v1",
14
+ "api_key": "",
15
+ "model": "anthropic/claude-sonnet-4.5",
16
+ "system": "You are a helpful coding assistant. Be concise.",
17
+ "route": None,
18
+ }
19
+
20
+ _DIR_MODE = stat.S_IRWXU # 0700
21
+ _FILE_MODE = stat.S_IRUSR | stat.S_IWUSR # 0600
22
+
23
+
24
+ def _secure_dir(path: Path) -> None:
25
+ path.mkdir(exist_ok=True)
26
+ try:
27
+ path.chmod(_DIR_MODE)
28
+ except OSError:
29
+ pass # best-effort on non-POSIX or weird filesystems
30
+
31
+
32
+ def secure_file(path: Path) -> None:
33
+ """Tighten an existing file's permissions to 0600. Public so cli.py
34
+ can apply it to the prompt_toolkit history file."""
35
+ try:
36
+ if path.exists():
37
+ path.chmod(_FILE_MODE)
38
+ except OSError:
39
+ pass
40
+
41
+
42
+ def _validate_base_url(url: str) -> str:
43
+ u = (url or "").strip().rstrip("/")
44
+ if u.startswith("https://"):
45
+ return u
46
+ if u.startswith(("http://localhost", "http://127.0.0.1")):
47
+ return u # local dev/proxy is the only http:// allowed
48
+ print(
49
+ f"meshapi: refusing to use base_url {url!r} — must be https:// "
50
+ "(or http://localhost for local dev). The Authorization header "
51
+ "carries your API key in cleartext otherwise.",
52
+ file=sys.stderr,
53
+ )
54
+ sys.exit(2)
55
+
56
+
57
+ def load_config() -> dict:
58
+ _secure_dir(CONFIG_DIR)
59
+ if not CONFIG_FILE.exists():
60
+ CONFIG_FILE.write_text(json.dumps(DEFAULT_CONFIG, indent=2))
61
+ secure_file(CONFIG_FILE)
62
+ cfg = {**DEFAULT_CONFIG, **json.loads(CONFIG_FILE.read_text())}
63
+ # MESH_API_KEY kept as fallback for one release; prefer MESHAPI_API_KEY.
64
+ cfg["api_key"] = (
65
+ os.getenv("MESHAPI_API_KEY")
66
+ or os.getenv("MESH_API_KEY")
67
+ or cfg.get("api_key", "")
68
+ )
69
+ cfg["base_url"] = _validate_base_url(
70
+ os.getenv("MESHAPI_BASE_URL", cfg["base_url"])
71
+ )
72
+ return cfg
73
+
74
+
75
+ def save_config(cfg: dict) -> None:
76
+ _secure_dir(CONFIG_DIR)
77
+ persisted = {k: v for k, v in cfg.items() if k != "api_key"}
78
+ CONFIG_FILE.write_text(json.dumps(persisted, indent=2))
79
+ secure_file(CONFIG_FILE)
@@ -1 +0,0 @@
1
- __version__ = "0.3.2"
@@ -1,37 +0,0 @@
1
- """Config storage at ~/.meshapi/config.json."""
2
- import json
3
- import os
4
- from pathlib import Path
5
-
6
- CONFIG_DIR = Path.home() / ".meshapi"
7
- CONFIG_FILE = CONFIG_DIR / "config.json"
8
- HISTORY_FILE = CONFIG_DIR / "history"
9
-
10
- DEFAULT_CONFIG = {
11
- "base_url": "https://api.meshapi.ai/v1",
12
- "api_key": "",
13
- "model": "anthropic/claude-sonnet-4.5",
14
- "system": "You are a helpful coding assistant. Be concise.",
15
- "route": None,
16
- }
17
-
18
-
19
- def load_config() -> dict:
20
- CONFIG_DIR.mkdir(exist_ok=True)
21
- if not CONFIG_FILE.exists():
22
- CONFIG_FILE.write_text(json.dumps(DEFAULT_CONFIG, indent=2))
23
- cfg = {**DEFAULT_CONFIG, **json.loads(CONFIG_FILE.read_text())}
24
- # MESH_API_KEY kept as fallback for one release; prefer MESHAPI_API_KEY.
25
- cfg["api_key"] = (
26
- os.getenv("MESHAPI_API_KEY")
27
- or os.getenv("MESH_API_KEY")
28
- or cfg.get("api_key", "")
29
- )
30
- cfg["base_url"] = os.getenv("MESHAPI_BASE_URL", cfg["base_url"])
31
- return cfg
32
-
33
-
34
- def save_config(cfg: dict) -> None:
35
- CONFIG_DIR.mkdir(exist_ok=True)
36
- persisted = {k: v for k, v in cfg.items() if k != "api_key"}
37
- CONFIG_FILE.write_text(json.dumps(persisted, indent=2))
File without changes
File without changes
File without changes
File without changes