meshapi-code 0.3.2__tar.gz → 0.3.4__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.
- meshapi_code-0.3.4/NOTICE +9 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/PKG-INFO +3 -2
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/pyproject.toml +3 -3
- meshapi_code-0.3.4/src/meshapi/__init__.py +1 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/cli.py +42 -2
- meshapi_code-0.3.4/src/meshapi/config.py +79 -0
- meshapi_code-0.3.2/src/meshapi/__init__.py +0 -1
- meshapi_code-0.3.2/src/meshapi/config.py +0 -37
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/.github/workflows/publish.yml +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/.gitignore +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/CLAUDE.md +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/LICENSE +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/README.md +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/__main__.py +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/client.py +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/commands.py +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/permissions.py +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/render.py +0 -0
- {meshapi_code-0.3.2 → meshapi_code-0.3.4}/src/meshapi/tools.py +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshapi-code
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
7
7
|
Project-URL: Repository, https://github.com/aifiesta/meshapi-code
|
|
8
|
-
Author:
|
|
8
|
+
Author: Fiesta Labs Inc.
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
11
12
|
Keywords: anthropic,chat,cli,gateway,llm,mesh,openai
|
|
12
13
|
Classifier: Development Status :: 3 - Alpha
|
|
13
14
|
Classifier: Environment :: Console
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "meshapi-code"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "Terminal chat for Mesh API — OpenAI-compatible LLM gateway"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
|
-
license-files = ["LICENSE"]
|
|
7
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
|
-
authors = [{ name = "
|
|
9
|
+
authors = [{ name = "Fiesta Labs Inc." }]
|
|
10
10
|
keywords = ["cli", "llm", "openai", "anthropic", "chat", "mesh", "gateway"]
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 3 - Alpha",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.4"
|
|
@@ -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=
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|