evo-cli 0.13.0__tar.gz → 0.14.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.
- {evo_cli-0.13.0 → evo_cli-0.14.0}/PKG-INFO +14 -3
- {evo_cli-0.13.0 → evo_cli-0.14.0}/README.md +13 -2
- evo_cli-0.14.0/evo_cli/VERSION +1 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/mcp.py +27 -11
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/opencode.py +10 -5
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/tts.py +1 -1
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/__init__.py +2 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/core.py +15 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli.egg-info/PKG-INFO +14 -3
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_mcp.py +45 -1
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_opencode.py +32 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_tts.py +69 -0
- evo_cli-0.13.0/evo_cli/VERSION +0 -1
- {evo_cli-0.13.0 → evo_cli-0.14.0}/Containerfile +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/HISTORY.md +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/LICENSE +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/MANIFEST.in +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/base.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/cli.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/harness.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/console.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli.egg-info/SOURCES.txt +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/pyproject.toml +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/setup.cfg +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/__init__.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_cli.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_console.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_cred.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_download.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_gh.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_harness.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_update.py +0 -0
- {evo_cli-0.13.0 → evo_cli-0.14.0}/tests/test_wifi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Evolution CLI - a developer toolbox for setting up dev machines
|
|
5
5
|
Author: maycuatroi
|
|
6
6
|
Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
|
|
@@ -214,8 +214,19 @@ evo cred add vbee.token --from-stdin # JWT from the same app page
|
|
|
214
214
|
evo cred add openai_api_key --from-stdin
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
`VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
|
|
218
|
-
|
|
217
|
+
`VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
|
|
218
|
+
|
|
219
|
+
`--provider auto` (the default) resolves to `EVO_TTS_PROVIDER` when that is set, and otherwise to
|
|
220
|
+
whichever provider has credentials. Pick a machine default once:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
export EVO_TTS_PROVIDER=openai # what `auto` means here
|
|
224
|
+
export EVO_TTS_VOICE_OPENAI=nova # default voice for that provider only
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Prefer the provider-scoped `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE` over a bare
|
|
228
|
+
`EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because an OpenAI
|
|
229
|
+
voice name is not a Vbee voice code.
|
|
219
230
|
|
|
220
231
|
Playback uses whichever of `ffplay`, `mpv`, `cvlc`, `afplay`, or `paplay`/`aplay` is on PATH, and
|
|
221
232
|
falls back to PowerShell's `MediaPlayer` on Windows. Without any of them the audio is still written
|
|
@@ -186,8 +186,19 @@ evo cred add vbee.token --from-stdin # JWT from the same app page
|
|
|
186
186
|
evo cred add openai_api_key --from-stdin
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
`VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
|
|
190
|
-
|
|
189
|
+
`VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
|
|
190
|
+
|
|
191
|
+
`--provider auto` (the default) resolves to `EVO_TTS_PROVIDER` when that is set, and otherwise to
|
|
192
|
+
whichever provider has credentials. Pick a machine default once:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
export EVO_TTS_PROVIDER=openai # what `auto` means here
|
|
196
|
+
export EVO_TTS_VOICE_OPENAI=nova # default voice for that provider only
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Prefer the provider-scoped `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE` over a bare
|
|
200
|
+
`EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because an OpenAI
|
|
201
|
+
voice name is not a Vbee voice code.
|
|
191
202
|
|
|
192
203
|
Playback uses whichever of `ffplay`, `mpv`, `cvlc`, `afplay`, or `paplay`/`aplay` is on PATH, and
|
|
193
204
|
falls back to PowerShell's `MediaPlayer` on Windows. Without any of them the audio is still written
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.14.0
|
|
@@ -38,7 +38,7 @@ def claude_has_server(name):
|
|
|
38
38
|
return result.returncode == 0
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def add_to_claude(name, spec, scope):
|
|
41
|
+
def add_to_claude(name, spec, scope, force=False):
|
|
42
42
|
step("Adding to Claude Code")
|
|
43
43
|
if not shutil.which("claude"):
|
|
44
44
|
warning("`claude` CLI not found on PATH; skipping Claude Code")
|
|
@@ -46,16 +46,23 @@ def add_to_claude(name, spec, scope):
|
|
|
46
46
|
return False
|
|
47
47
|
|
|
48
48
|
if claude_has_server(name):
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if not force:
|
|
50
|
+
info(f"MCP server [accent]{name}[/accent] already in Claude Code; skipping")
|
|
51
|
+
return True
|
|
52
|
+
info(f"Replacing existing [accent]{name}[/accent] in Claude Code")
|
|
53
|
+
run_command(["claude", "mcp", "remove", "--scope", scope, name], check=False)
|
|
51
54
|
|
|
52
55
|
cmd = ["claude", "mcp", "add", "--scope", scope]
|
|
53
56
|
if is_remote(spec):
|
|
54
57
|
cmd += ["--transport", spec["transport"], name, spec["url"]]
|
|
55
58
|
else:
|
|
59
|
+
# `claude mcp add` declares -e as variadic (`-e <env...>`), so any flag placed
|
|
60
|
+
# before the positional name swallows it as another KEY=value and the command
|
|
61
|
+
# dies with "Invalid environment variable format: <name>". Name first.
|
|
62
|
+
cmd += [name]
|
|
56
63
|
for key, value in (spec.get("env") or {}).items():
|
|
57
64
|
cmd += ["-e", f"{key}={value}"]
|
|
58
|
-
cmd += [
|
|
65
|
+
cmd += ["--", *spec["command"]]
|
|
59
66
|
if spec.get("headers"):
|
|
60
67
|
for key, value in spec["headers"].items():
|
|
61
68
|
cmd += ["--header", f"{key}: {value}"]
|
|
@@ -70,20 +77,24 @@ def add_to_claude(name, spec, scope):
|
|
|
70
77
|
return True
|
|
71
78
|
|
|
72
79
|
|
|
73
|
-
def add_to_opencode(name, spec, project):
|
|
80
|
+
def add_to_opencode(name, spec, project, force=False):
|
|
74
81
|
step("Adding to OpenCode")
|
|
75
82
|
server_config = to_opencode_config(spec)
|
|
76
83
|
|
|
77
84
|
config_path = get_global_config_path()
|
|
78
85
|
existing = load_jsonc(config_path) if config_path.exists() else {}
|
|
79
86
|
mcp = existing.setdefault("mcp", {})
|
|
80
|
-
if name in mcp:
|
|
87
|
+
if name in mcp and not force:
|
|
81
88
|
info(f"MCP server [accent]{name}[/accent] already in OpenCode global; skipping")
|
|
89
|
+
elif name in mcp and mcp[name] == server_config:
|
|
90
|
+
info(f"MCP server [accent]{name}[/accent] already matches in OpenCode global; unchanged")
|
|
82
91
|
else:
|
|
92
|
+
replacing = name in mcp
|
|
83
93
|
mcp[name] = server_config
|
|
84
94
|
header = "// OpenCode global configuration generated by evo"
|
|
85
95
|
save_jsonc(config_path, existing, header=header)
|
|
86
|
-
|
|
96
|
+
verb = "Replaced" if replacing else "Added"
|
|
97
|
+
success(f"{verb} [accent]{name}[/accent] in OpenCode global config")
|
|
87
98
|
info(f"Config: [accent]{config_path}[/accent]")
|
|
88
99
|
|
|
89
100
|
if project:
|
|
@@ -91,7 +102,7 @@ def add_to_opencode(name, spec, project):
|
|
|
91
102
|
proj_path = target / "opencode.json"
|
|
92
103
|
proj_existing = load_jsonc(proj_path) if proj_path.exists() else {}
|
|
93
104
|
proj_mcp = proj_existing.setdefault("mcp", {})
|
|
94
|
-
if name in proj_mcp:
|
|
105
|
+
if name in proj_mcp and not force:
|
|
95
106
|
info(f"MCP server [accent]{name}[/accent] already in project config; skipping")
|
|
96
107
|
else:
|
|
97
108
|
proj_mcp[name] = server_config
|
|
@@ -179,12 +190,17 @@ def list_servers():
|
|
|
179
190
|
)
|
|
180
191
|
@click.option("--claude-only", is_flag=True, help="Only configure Claude Code.")
|
|
181
192
|
@click.option("--opencode-only", is_flag=True, help="Only configure OpenCode.")
|
|
193
|
+
@click.option(
|
|
194
|
+
"--force",
|
|
195
|
+
is_flag=True,
|
|
196
|
+
help="Replace an existing entry instead of leaving it alone (use when --env or the command changed).",
|
|
197
|
+
)
|
|
182
198
|
@click.option(
|
|
183
199
|
"--project",
|
|
184
200
|
type=click.Path(file_okay=False, dir_okay=True),
|
|
185
201
|
help="Also write into a project-level opencode.json at this directory.",
|
|
186
202
|
)
|
|
187
|
-
def add_server(name, url, command, env, transport, scope, claude_only, opencode_only, project):
|
|
203
|
+
def add_server(name, url, command, env, transport, scope, claude_only, opencode_only, force, project):
|
|
188
204
|
"""Add an MCP server to Claude Code and OpenCode.
|
|
189
205
|
|
|
190
206
|
NAME is a key from the library (`evo mcp list`), or any label when paired
|
|
@@ -218,9 +234,9 @@ def add_server(name, url, command, env, transport, scope, claude_only, opencode_
|
|
|
218
234
|
|
|
219
235
|
did_claude = did_opencode = False
|
|
220
236
|
if not opencode_only:
|
|
221
|
-
did_claude = add_to_claude(name, spec, scope)
|
|
237
|
+
did_claude = add_to_claude(name, spec, scope, force=force)
|
|
222
238
|
if not claude_only:
|
|
223
|
-
did_opencode = add_to_opencode(name, spec, project)
|
|
239
|
+
did_opencode = add_to_opencode(name, spec, project, force=force)
|
|
224
240
|
|
|
225
241
|
console.print()
|
|
226
242
|
targets = []
|
|
@@ -42,14 +42,19 @@ def is_windows():
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def get_global_config_dir():
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return home / ".config" / "opencode"
|
|
45
|
+
# OpenCode reads ~/.config/opencode on every platform, Windows included - it does
|
|
46
|
+
# not follow the %APPDATA% convention. Writing to AppData produced a file that
|
|
47
|
+
# `opencode mcp list` never saw, so every server evo "added" was silently inert.
|
|
48
|
+
return Path.home() / ".config" / "opencode"
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def get_global_config_path():
|
|
52
|
-
|
|
52
|
+
directory = get_global_config_dir()
|
|
53
|
+
for name in ("opencode.jsonc", "opencode.json"):
|
|
54
|
+
candidate = directory / name
|
|
55
|
+
if candidate.exists():
|
|
56
|
+
return candidate
|
|
57
|
+
return directory / "opencode.jsonc"
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
def ensure_node_installed():
|
|
@@ -168,7 +168,7 @@ def speak(
|
|
|
168
168
|
if not quiet and not to_stdout:
|
|
169
169
|
info(
|
|
170
170
|
f"Provider [accent]{resolved}[/accent], voice "
|
|
171
|
-
f"[accent]{voice or core.
|
|
171
|
+
f"[accent]{voice or core.default_voice_for(resolved)}[/accent], "
|
|
172
172
|
f"{len(body.strip())} characters (limit {chunks}/request)"
|
|
173
173
|
)
|
|
174
174
|
|
|
@@ -3,6 +3,7 @@ from evo_cli.tts.core import (
|
|
|
3
3
|
PROVIDERS,
|
|
4
4
|
chunk_limit,
|
|
5
5
|
default_voice,
|
|
6
|
+
default_voice_for,
|
|
6
7
|
list_voices,
|
|
7
8
|
resolve_provider,
|
|
8
9
|
supported_formats,
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"TtsError",
|
|
19
20
|
"chunk_limit",
|
|
20
21
|
"default_voice",
|
|
22
|
+
"default_voice_for",
|
|
21
23
|
"list_voices",
|
|
22
24
|
"play",
|
|
23
25
|
"player_hint",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
2
3
|
|
|
3
4
|
from evo_cli.tts import openai as openai_tts
|
|
@@ -15,6 +16,11 @@ def resolve_provider(provider):
|
|
|
15
16
|
if provider in PROVIDERS:
|
|
16
17
|
return provider
|
|
17
18
|
if provider in (None, "", "auto"):
|
|
19
|
+
preferred = (os.environ.get("EVO_TTS_PROVIDER") or "").strip().lower()
|
|
20
|
+
if preferred in PROVIDERS:
|
|
21
|
+
return preferred
|
|
22
|
+
if preferred and preferred != "auto":
|
|
23
|
+
raise TtsError(f"EVO_TTS_PROVIDER is set to '{preferred}', which is not one of: {', '.join(PROVIDERS)}")
|
|
18
24
|
if has_vbee_credentials():
|
|
19
25
|
return "vbee"
|
|
20
26
|
if has_openai_credentials():
|
|
@@ -27,6 +33,14 @@ def resolve_provider(provider):
|
|
|
27
33
|
raise TtsError(f"unknown provider '{provider}' (expected one of: {', '.join(PROVIDERS)}, auto)")
|
|
28
34
|
|
|
29
35
|
|
|
36
|
+
def default_voice_for(provider):
|
|
37
|
+
# Provider-scoped first, so a machine configured for both does not hand a Vbee
|
|
38
|
+
# voice code to OpenAI the moment EVO_TTS_PROVIDER flips.
|
|
39
|
+
scoped = (os.environ.get(f"EVO_TTS_VOICE_{provider.upper()}") or "").strip()
|
|
40
|
+
shared = (os.environ.get("EVO_TTS_VOICE") or "").strip()
|
|
41
|
+
return scoped or shared or default_voice(provider)
|
|
42
|
+
|
|
43
|
+
|
|
30
44
|
def default_voice(provider):
|
|
31
45
|
return vbee.DEFAULT_VOICE if provider == "vbee" else openai_tts.DEFAULT_VOICE
|
|
32
46
|
|
|
@@ -81,6 +95,7 @@ def synthesize(
|
|
|
81
95
|
on_poll=None,
|
|
82
96
|
):
|
|
83
97
|
provider = resolve_provider(provider)
|
|
98
|
+
voice = voice or default_voice_for(provider)
|
|
84
99
|
if mode not in MODES:
|
|
85
100
|
raise TtsError(f"unknown mode '{mode}' (expected one of: {', '.join(MODES)})")
|
|
86
101
|
text = (text or "").strip()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Evolution CLI - a developer toolbox for setting up dev machines
|
|
5
5
|
Author: maycuatroi
|
|
6
6
|
Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
|
|
@@ -214,8 +214,19 @@ evo cred add vbee.token --from-stdin # JWT from the same app page
|
|
|
214
214
|
evo cred add openai_api_key --from-stdin
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
`VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
|
|
218
|
-
|
|
217
|
+
`VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
|
|
218
|
+
|
|
219
|
+
`--provider auto` (the default) resolves to `EVO_TTS_PROVIDER` when that is set, and otherwise to
|
|
220
|
+
whichever provider has credentials. Pick a machine default once:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
export EVO_TTS_PROVIDER=openai # what `auto` means here
|
|
224
|
+
export EVO_TTS_VOICE_OPENAI=nova # default voice for that provider only
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Prefer the provider-scoped `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE` over a bare
|
|
228
|
+
`EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because an OpenAI
|
|
229
|
+
voice name is not a Vbee voice code.
|
|
219
230
|
|
|
220
231
|
Playback uses whichever of `ffplay`, `mpv`, `cvlc`, `afplay`, or `paplay`/`aplay` is on PATH, and
|
|
221
232
|
falls back to PowerShell's `MediaPlayer` on Windows. Without any of them the audio is still written
|
|
@@ -2,7 +2,7 @@ import pytest
|
|
|
2
2
|
from click.testing import CliRunner
|
|
3
3
|
|
|
4
4
|
from evo_cli.cli import cli
|
|
5
|
-
from evo_cli.commands.mcp import resolve_spec, to_opencode_config
|
|
5
|
+
from evo_cli.commands.mcp import add_to_claude, resolve_spec, to_opencode_config
|
|
6
6
|
from evo_cli.commands.opencode import load_jsonc
|
|
7
7
|
|
|
8
8
|
|
|
@@ -128,3 +128,47 @@ def test_add_rejects_both_only_flags(runner, tmp_path, monkeypatch):
|
|
|
128
128
|
)
|
|
129
129
|
assert result.exit_code == 0
|
|
130
130
|
assert "mutually exclusive" in result.output
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_claude_add_puts_the_name_before_the_variadic_env_flag(monkeypatch):
|
|
134
|
+
captured = []
|
|
135
|
+
monkeypatch.setattr("evo_cli.commands.mcp.shutil.which", lambda name: f"/usr/bin/{name}")
|
|
136
|
+
monkeypatch.setattr("evo_cli.commands.mcp.claude_has_server", lambda name: False)
|
|
137
|
+
monkeypatch.setattr("evo_cli.commands.mcp.run_command", lambda cmd, **kw: captured.append(cmd))
|
|
138
|
+
|
|
139
|
+
spec = {"transport": "stdio", "command": ["uv", "run", "server.py"], "env": {"A": "1", "B": "2"}}
|
|
140
|
+
add_to_claude("evo-tts", spec, "user")
|
|
141
|
+
|
|
142
|
+
cmd = captured[0]
|
|
143
|
+
# `claude mcp add` declares -e as variadic, so a name after it is swallowed as
|
|
144
|
+
# another KEY=value and the real CLI errors out.
|
|
145
|
+
assert cmd.index("evo-tts") < cmd.index("-e")
|
|
146
|
+
assert cmd[cmd.index("--") + 1 :] == ["uv", "run", "server.py"]
|
|
147
|
+
assert "A=1" in cmd and "B=2" in cmd
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_opencode_add_leaves_an_existing_entry_alone_without_force(runner, tmp_path, monkeypatch):
|
|
151
|
+
global_path = tmp_path / "global.jsonc"
|
|
152
|
+
monkeypatch.setattr("evo_cli.commands.mcp.get_global_config_path", lambda: global_path)
|
|
153
|
+
|
|
154
|
+
runner.invoke(cli, ["mcp", "add", "srv", "--opencode-only", "--command", "old cmd"])
|
|
155
|
+
result = runner.invoke(cli, ["mcp", "add", "srv", "--opencode-only", "--command", "new cmd"])
|
|
156
|
+
|
|
157
|
+
assert result.exit_code == 0
|
|
158
|
+
assert load_jsonc(global_path)["mcp"]["srv"]["command"] == ["old", "cmd"]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_opencode_add_force_replaces_the_entry(runner, tmp_path, monkeypatch):
|
|
162
|
+
global_path = tmp_path / "global.jsonc"
|
|
163
|
+
monkeypatch.setattr("evo_cli.commands.mcp.get_global_config_path", lambda: global_path)
|
|
164
|
+
|
|
165
|
+
runner.invoke(cli, ["mcp", "add", "srv", "--opencode-only", "--command", "old cmd"])
|
|
166
|
+
result = runner.invoke(
|
|
167
|
+
cli,
|
|
168
|
+
["mcp", "add", "srv", "--opencode-only", "--force", "--env", "K=v", "--command", "new cmd"],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
assert result.exit_code == 0
|
|
172
|
+
entry = load_jsonc(global_path)["mcp"]["srv"]
|
|
173
|
+
assert entry["command"] == ["new", "cmd"]
|
|
174
|
+
assert entry["environment"] == {"K": "v"}
|
|
@@ -5,6 +5,7 @@ import pytest
|
|
|
5
5
|
from click.testing import CliRunner
|
|
6
6
|
|
|
7
7
|
from evo_cli.cli import cli
|
|
8
|
+
from evo_cli.commands import opencode
|
|
8
9
|
from evo_cli.commands.opencode import (
|
|
9
10
|
DEFAULT_MCP_SERVERS,
|
|
10
11
|
EXA_ENV_VAR,
|
|
@@ -209,3 +210,34 @@ def test_setup_opencode_global_only(runner, tmp_path, monkeypatch):
|
|
|
209
210
|
result = runner.invoke(cli, ["setup", "opencode", "--global-only", "--project", str(project)])
|
|
210
211
|
assert result.exit_code == 0
|
|
211
212
|
assert not (project / "opencode.json").exists()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_global_config_dir_is_xdg_style_on_every_platform(monkeypatch, tmp_path):
|
|
216
|
+
monkeypatch.setattr(opencode.Path, "home", classmethod(lambda cls: tmp_path))
|
|
217
|
+
for system in ("Windows", "Linux", "Darwin"):
|
|
218
|
+
monkeypatch.setattr(opencode.platform, "system", lambda system=system: system)
|
|
219
|
+
assert opencode.get_global_config_dir() == tmp_path / ".config" / "opencode"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_global_config_path_reuses_an_existing_json_file(monkeypatch, tmp_path):
|
|
223
|
+
monkeypatch.setattr(opencode.Path, "home", classmethod(lambda cls: tmp_path))
|
|
224
|
+
directory = tmp_path / ".config" / "opencode"
|
|
225
|
+
directory.mkdir(parents=True)
|
|
226
|
+
existing = directory / "opencode.json"
|
|
227
|
+
existing.write_text("{}", encoding="utf-8")
|
|
228
|
+
assert opencode.get_global_config_path() == existing
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_global_config_path_prefers_jsonc_when_both_exist(monkeypatch, tmp_path):
|
|
232
|
+
monkeypatch.setattr(opencode.Path, "home", classmethod(lambda cls: tmp_path))
|
|
233
|
+
directory = tmp_path / ".config" / "opencode"
|
|
234
|
+
directory.mkdir(parents=True)
|
|
235
|
+
(directory / "opencode.json").write_text("{}", encoding="utf-8")
|
|
236
|
+
(directory / "opencode.jsonc").write_text("{}", encoding="utf-8")
|
|
237
|
+
assert opencode.get_global_config_path() == directory / "opencode.jsonc"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_global_config_path_defaults_to_jsonc_when_nothing_exists(monkeypatch, tmp_path):
|
|
241
|
+
monkeypatch.setattr(opencode.Path, "home", classmethod(lambda cls: tmp_path))
|
|
242
|
+
expected = tmp_path / ".config" / "opencode" / "opencode.jsonc"
|
|
243
|
+
assert opencode.get_global_config_path() == expected
|
|
@@ -9,6 +9,24 @@ from evo_cli.tts import core, vbee
|
|
|
9
9
|
from evo_cli.tts.chunking import join_audio, split_text
|
|
10
10
|
from evo_cli.tts.errors import TtsError
|
|
11
11
|
|
|
12
|
+
EVO_TTS_ENV_VARS = (
|
|
13
|
+
"EVO_TTS_PROVIDER",
|
|
14
|
+
"EVO_TTS_VOICE",
|
|
15
|
+
"EVO_TTS_VOICE_OPENAI",
|
|
16
|
+
"EVO_TTS_VOICE_VBEE",
|
|
17
|
+
"EVO_TTS_SPEED",
|
|
18
|
+
"EVO_TTS_DIR",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(autouse=True)
|
|
23
|
+
def clean_tts_env(monkeypatch):
|
|
24
|
+
# These are set for real on machines that picked a default provider, and they
|
|
25
|
+
# would otherwise decide the outcome of the auto-detection tests. CI has none of
|
|
26
|
+
# them set, so without this the suite passes there and fails on a real desktop.
|
|
27
|
+
for name in EVO_TTS_ENV_VARS:
|
|
28
|
+
monkeypatch.delenv(name, raising=False)
|
|
29
|
+
|
|
12
30
|
|
|
13
31
|
@pytest.fixture
|
|
14
32
|
def runner():
|
|
@@ -258,3 +276,54 @@ def test_batch_exits_nonzero_when_an_item_fails(runner, monkeypatch, tmp_path):
|
|
|
258
276
|
result = runner.invoke(cli, ["tts", "batch", "-t", "xin chào", "-o", str(tmp_path / "out")])
|
|
259
277
|
assert result.exit_code == 1
|
|
260
278
|
assert "nope" in result.output
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_resolve_provider_auto_follows_the_env_override(monkeypatch):
|
|
282
|
+
monkeypatch.setenv("EVO_TTS_PROVIDER", "openai")
|
|
283
|
+
monkeypatch.setattr(core, "has_vbee_credentials", lambda: True)
|
|
284
|
+
assert core.resolve_provider("auto") == "openai"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_env_override_does_not_beat_an_explicit_provider(monkeypatch):
|
|
288
|
+
monkeypatch.setenv("EVO_TTS_PROVIDER", "openai")
|
|
289
|
+
assert core.resolve_provider("vbee") == "vbee"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_resolve_provider_rejects_a_bogus_env_override(monkeypatch):
|
|
293
|
+
monkeypatch.setenv("EVO_TTS_PROVIDER", "elevenlabs")
|
|
294
|
+
with pytest.raises(TtsError, match="EVO_TTS_PROVIDER"):
|
|
295
|
+
core.resolve_provider("auto")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_default_voice_for_falls_back_to_the_provider_default(monkeypatch):
|
|
299
|
+
monkeypatch.delenv("EVO_TTS_VOICE", raising=False)
|
|
300
|
+
monkeypatch.delenv("EVO_TTS_VOICE_OPENAI", raising=False)
|
|
301
|
+
assert core.default_voice_for("openai") == core.openai_tts.DEFAULT_VOICE
|
|
302
|
+
assert core.default_voice_for("vbee") == vbee.DEFAULT_VOICE
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_scoped_voice_env_wins_over_the_shared_one(monkeypatch):
|
|
306
|
+
monkeypatch.setenv("EVO_TTS_VOICE", "shared")
|
|
307
|
+
monkeypatch.setenv("EVO_TTS_VOICE_OPENAI", "nova")
|
|
308
|
+
assert core.default_voice_for("openai") == "nova"
|
|
309
|
+
assert core.default_voice_for("vbee") == "shared"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_scoped_voice_does_not_leak_across_providers(monkeypatch):
|
|
313
|
+
monkeypatch.delenv("EVO_TTS_VOICE", raising=False)
|
|
314
|
+
monkeypatch.setenv("EVO_TTS_VOICE_OPENAI", "nova")
|
|
315
|
+
assert core.default_voice_for("vbee") == vbee.DEFAULT_VOICE
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_synthesize_applies_the_env_voice(monkeypatch):
|
|
319
|
+
monkeypatch.delenv("EVO_TTS_VOICE", raising=False)
|
|
320
|
+
monkeypatch.setenv("EVO_TTS_VOICE_OPENAI", "nova")
|
|
321
|
+
seen = {}
|
|
322
|
+
|
|
323
|
+
def fake_synthesize(text, **kwargs):
|
|
324
|
+
seen["voice"] = kwargs["voice"]
|
|
325
|
+
return b"x"
|
|
326
|
+
|
|
327
|
+
monkeypatch.setattr(core.openai_tts, "synthesize", fake_synthesize)
|
|
328
|
+
core.synthesize("xin chào", provider="openai")
|
|
329
|
+
assert seen["voice"] == "nova"
|
evo_cli-0.13.0/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.13.0
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|