evo-cli 0.10.1__tar.gz → 0.10.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.
Files changed (45) hide show
  1. {evo_cli-0.10.1 → evo_cli-0.10.3}/PKG-INFO +1 -1
  2. evo_cli-0.10.3/evo_cli/VERSION +1 -0
  3. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/opencode.py +78 -8
  4. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/console.py +16 -1
  5. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/mcp_registry.py +5 -5
  6. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli.egg-info/PKG-INFO +1 -1
  7. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_opencode.py +26 -3
  8. evo_cli-0.10.1/evo_cli/VERSION +0 -1
  9. {evo_cli-0.10.1 → evo_cli-0.10.3}/Containerfile +0 -0
  10. {evo_cli-0.10.1 → evo_cli-0.10.3}/HISTORY.md +0 -0
  11. {evo_cli-0.10.1 → evo_cli-0.10.3}/LICENSE +0 -0
  12. {evo_cli-0.10.1 → evo_cli-0.10.3}/MANIFEST.in +0 -0
  13. {evo_cli-0.10.1 → evo_cli-0.10.3}/README.md +0 -0
  14. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/__init__.py +0 -0
  15. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/__main__.py +0 -0
  16. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/base.py +0 -0
  17. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/cli.py +0 -0
  18. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/__init__.py +0 -0
  19. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/cloudflare.py +0 -0
  20. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/fix_claude.py +0 -0
  21. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/gdrive.py +0 -0
  22. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/localproxy.py +0 -0
  23. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/mcp.py +0 -0
  24. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/miniconda.py +0 -0
  25. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/netcheck.py +0 -0
  26. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/plantuml.py +0 -0
  27. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/site2s.py +0 -0
  28. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/ssh.py +0 -0
  29. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/sysmon.py +0 -0
  30. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli/commands/wifi.py +0 -0
  31. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli.egg-info/SOURCES.txt +0 -0
  32. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli.egg-info/dependency_links.txt +0 -0
  33. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli.egg-info/entry_points.txt +0 -0
  34. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli.egg-info/requires.txt +0 -0
  35. {evo_cli-0.10.1 → evo_cli-0.10.3}/evo_cli.egg-info/top_level.txt +0 -0
  36. {evo_cli-0.10.1 → evo_cli-0.10.3}/pyproject.toml +0 -0
  37. {evo_cli-0.10.1 → evo_cli-0.10.3}/setup.cfg +0 -0
  38. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/__init__.py +0 -0
  39. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_cli.py +0 -0
  40. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_console.py +0 -0
  41. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_fix_claude.py +0 -0
  42. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_mcp.py +0 -0
  43. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_plantuml.py +0 -0
  44. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_sysmon.py +0 -0
  45. {evo_cli-0.10.1 → evo_cli-0.10.3}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.10.1
3
+ Version: 0.10.3
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
@@ -0,0 +1 @@
1
+ 0.10.3
@@ -1,6 +1,7 @@
1
1
  """Install and configure OpenCode with common MCP servers."""
2
2
 
3
3
  import json
4
+ import os
4
5
  import platform
5
6
  import shutil
6
7
  import subprocess
@@ -22,7 +23,13 @@ EPILOG = Text.from_markup(
22
23
 
23
24
  # Servers bootstrapped by `evo setup opencode`, pulled from the shared library
24
25
  # (evo_cli/mcp_registry.py). Add more servers anytime with `evo mcp add <name>`.
25
- DEFAULT_MCP_SERVERS = opencode_servers("google-search", "playwright")
26
+ # Web search is handled by OpenCode's native Exa-backed `websearch` tool (enabled
27
+ # via OPENCODE_ENABLE_EXA, no API key), not a scraping MCP server.
28
+ DEFAULT_MCP_SERVERS = opencode_servers("playwright")
29
+
30
+ # OpenCode's built-in `websearch` tool only activates for the OpenCode provider or
31
+ # when this env var is truthy, so custom providers need it set explicitly.
32
+ EXA_ENV_VAR = "OPENCODE_ENABLE_EXA"
26
33
 
27
34
 
28
35
  def _local_mcp_commands():
@@ -52,7 +59,14 @@ def ensure_node_installed():
52
59
  npx_cmd = shutil.which("npx")
53
60
 
54
61
  if node_cmd and npm_cmd and npx_cmd:
55
- result = subprocess.run(resolve_executable(["node", "--version"]), capture_output=True, text=True, check=True)
62
+ result = subprocess.run(
63
+ resolve_executable(["node", "--version"]),
64
+ capture_output=True,
65
+ text=True,
66
+ encoding="utf-8",
67
+ errors="replace",
68
+ check=True,
69
+ )
56
70
  info(f"Node.js found: {result.stdout.strip()}")
57
71
  return node_cmd, npm_cmd, npx_cmd
58
72
 
@@ -153,7 +167,12 @@ def opencode_version():
153
167
  return None
154
168
  try:
155
169
  result = subprocess.run(
156
- resolve_executable(["opencode", "--version"]), capture_output=True, text=True, timeout=30
170
+ resolve_executable(["opencode", "--version"]),
171
+ capture_output=True,
172
+ text=True,
173
+ encoding="utf-8",
174
+ errors="replace",
175
+ timeout=30,
157
176
  )
158
177
  return result.stdout.strip() or "unknown"
159
178
  except Exception:
@@ -299,6 +318,8 @@ def verify_mcp_servers():
299
318
  input=init_message,
300
319
  capture_output=True,
301
320
  text=True,
321
+ encoding="utf-8",
322
+ errors="replace",
302
323
  timeout=30,
303
324
  )
304
325
  if result.returncode == 0 and '"jsonrpc":"2.0"' in result.stdout:
@@ -309,6 +330,51 @@ def verify_mcp_servers():
309
330
  warning(f"Could not verify MCP server [accent]{name}[/accent]: {exc}")
310
331
 
311
332
 
333
+ def get_shell_rc_path():
334
+ """Return the shell rc file to wire env vars into, based on the active shell."""
335
+ shell = os.environ.get("SHELL", "")
336
+ home = Path.home()
337
+ if shell.endswith("zsh"):
338
+ return home / ".zshrc"
339
+ if shell.endswith("bash"):
340
+ # macOS login shells read .bash_profile; Linux reads .bashrc.
341
+ return home / (".bash_profile" if platform.system() == "Darwin" else ".bashrc")
342
+ if shell.endswith("fish"):
343
+ return home / ".config" / "fish" / "config.fish"
344
+ return home / ".profile"
345
+
346
+
347
+ def enable_exa_websearch():
348
+ """Enable OpenCode's built-in Exa web search via the OPENCODE_ENABLE_EXA env var.
349
+
350
+ OpenCode ships a native ``websearch`` tool backed by Exa's hosted MCP service
351
+ (no API key needed), but it is only active when using the OpenCode provider or
352
+ when ``OPENCODE_ENABLE_EXA`` is truthy. Custom providers therefore need the env
353
+ var, so wire it into the shell rc idempotently.
354
+ """
355
+ step("Enabling Exa web search")
356
+ if is_windows():
357
+ info(f"Set the {EXA_ENV_VAR}=1 environment variable to enable web search.")
358
+ info("PowerShell: [accent]setx OPENCODE_ENABLE_EXA 1[/accent]")
359
+ return
360
+
361
+ rc_path = get_shell_rc_path()
362
+ if rc_path.exists() and EXA_ENV_VAR in rc_path.read_text(encoding="utf-8"):
363
+ info(f"{EXA_ENV_VAR} already set in [accent]{rc_path}[/accent]; skipping")
364
+ success("Exa web search already enabled")
365
+ return
366
+
367
+ rc_path.parent.mkdir(parents=True, exist_ok=True)
368
+ block = (
369
+ "\n# opencode web search (Exa native, no API key) - added by evo setup opencode\n"
370
+ f"export {EXA_ENV_VAR}=1\n"
371
+ )
372
+ with rc_path.open("a", encoding="utf-8") as fh:
373
+ fh.write(block)
374
+ success(f"Enabled Exa web search in [accent]{rc_path}[/accent]")
375
+ info(f"Run [accent]source {rc_path}[/accent] or open a new terminal to apply.")
376
+
377
+
312
378
  def run_setup_opencode(global_only, project, skip_install=False):
313
379
  step("evo setup opencode")
314
380
 
@@ -320,6 +386,7 @@ def run_setup_opencode(global_only, project, skip_install=False):
320
386
 
321
387
  if not skip_install:
322
388
  install_opencode(npm_cmd)
389
+ enable_exa_websearch()
323
390
  install_mcp_servers()
324
391
  install_playwright_browsers()
325
392
 
@@ -343,8 +410,9 @@ def run_setup_opencode(global_only, project, skip_install=False):
343
410
  )
344
411
  console.print(
345
412
  Panel(
346
- f"OpenCode configured with google-search and playwright MCP servers.\n\n{paths_text}\n\n"
347
- f"{opencode_note} Restart OpenCode to load the new MCP tools.",
413
+ "OpenCode web search enabled via Exa (native, no API key); "
414
+ f"playwright MCP server configured for browser automation.\n\n{paths_text}\n\n"
415
+ f"{opencode_note} Open a new terminal (or `source` your shell rc), then restart OpenCode.",
348
416
  title="setup opencode complete",
349
417
  border_style="success",
350
418
  expand=False,
@@ -373,9 +441,11 @@ def setup_opencode(global_only, skip_install, project):
373
441
 
374
442
  This command bootstraps a fresh machine with the same OpenCode + MCP setup
375
443
  used across devices. It installs the OpenCode CLI (via npm, unless
376
- --skip-install) and the google-search and playwright MCP servers, downloads
377
- the Playwright Chromium browser, and writes both global
378
- (~/.config/opencode/opencode.jsonc) and project-level (opencode.json) configs.
444
+ --skip-install), enables OpenCode's native Exa-backed web search (via the
445
+ OPENCODE_ENABLE_EXA env var, no API key required), installs the playwright MCP
446
+ server for browser automation, downloads the Playwright Chromium browser, and
447
+ writes both global (~/.config/opencode/opencode.jsonc) and project-level
448
+ (opencode.json) configs.
379
449
 
380
450
  No credentials are bundled; only public MCP server references are added.
381
451
  """
@@ -1,5 +1,6 @@
1
1
  import shutil
2
2
  import subprocess
3
+ import sys
3
4
  import urllib.request
4
5
 
5
6
  from rich.console import Console
@@ -13,6 +14,14 @@ from rich.progress import (
13
14
  )
14
15
  from rich.theme import Theme
15
16
 
17
+ for _stream in (sys.stdout, sys.stderr):
18
+ _reconfigure = getattr(_stream, "reconfigure", None)
19
+ if _reconfigure is not None:
20
+ try:
21
+ _reconfigure(encoding="utf-8", errors="replace")
22
+ except (ValueError, OSError):
23
+ pass
24
+
16
25
  EVO_THEME = Theme(
17
26
  {
18
27
  "info": "cyan",
@@ -67,7 +76,13 @@ def run_command(cmd, capture=False, check=True, input_text=None, status=None, ti
67
76
  console.print(f"[cmd]$ {' '.join(cmd)}[/cmd]")
68
77
  exec_cmd = resolve_executable(cmd)
69
78
 
70
- run_kwargs = {"text": True, "input": input_text, "timeout": timeout}
79
+ run_kwargs = {
80
+ "text": True,
81
+ "encoding": "utf-8",
82
+ "errors": "replace",
83
+ "input": input_text,
84
+ "timeout": timeout,
85
+ }
71
86
  # subprocess.run rejects passing both `input` and `stdin`. Only attach an
72
87
  # explicit stdin (e.g. DEVNULL) when we are not feeding input, so callers can
73
88
  # detach a child from the terminal and avoid it blocking on an inherited TTY.
@@ -22,12 +22,12 @@ MCP_REGISTRY = {
22
22
  "homepage": "https://github.com/microsoft/playwright-mcp",
23
23
  "description": "Playwright (Microsoft) - drive a real browser: navigate, click, snapshot.",
24
24
  },
25
- "google-search": {
26
- "transport": "stdio",
27
- "command": ["npx", "-y", "@mcp-server/google-search-mcp@latest"],
25
+ "exa": {
26
+ "transport": "http",
27
+ "url": "https://mcp.exa.ai/mcp",
28
28
  "category": "search",
29
- "homepage": "https://www.npmjs.com/package/@mcp-server/google-search-mcp",
30
- "description": "Google Search - run web searches and fetch result pages.",
29
+ "homepage": "https://exa.ai/docs/reference/exa-mcp",
30
+ "description": "Exa - AI web search (hosted MCP, free tier, no API key).",
31
31
  },
32
32
  "sequential-thinking": {
33
33
  "transport": "stdio",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.10.1
3
+ Version: 0.10.3
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
@@ -7,8 +7,10 @@ from click.testing import CliRunner
7
7
  from evo_cli.cli import cli
8
8
  from evo_cli.commands.opencode import (
9
9
  DEFAULT_MCP_SERVERS,
10
+ EXA_ENV_VAR,
10
11
  configure_opencode_global,
11
12
  configure_opencode_project,
13
+ enable_exa_websearch,
12
14
  install_mcp_servers,
13
15
  install_opencode,
14
16
  load_jsonc,
@@ -55,7 +57,6 @@ def test_merge_mcp_config_preserves_existing():
55
57
  existing = {"mcp": {"playwright": {"enabled": False}}}
56
58
  merged = merge_mcp_config(existing, DEFAULT_MCP_SERVERS)
57
59
  assert merged["mcp"]["playwright"]["enabled"] is False
58
- assert merged["mcp"]["google-search"]["enabled"] is True
59
60
 
60
61
 
61
62
  def test_configure_opencode_global(tmp_path, monkeypatch):
@@ -66,14 +67,33 @@ def test_configure_opencode_global(tmp_path, monkeypatch):
66
67
  path = configure_opencode_global()
67
68
  data = load_jsonc(path)
68
69
  assert "mcp" in data
69
- assert set(data["mcp"].keys()) == {"google-search", "playwright"}
70
+ assert set(data["mcp"].keys()) == {"playwright"}
70
71
 
71
72
 
72
73
  def test_configure_opencode_project(tmp_path):
73
74
  path = configure_opencode_project(tmp_path)
74
75
  data = load_jsonc(path)
75
76
  assert "mcp" in data
76
- assert set(data["mcp"].keys()) == {"google-search", "playwright"}
77
+ assert set(data["mcp"].keys()) == {"playwright"}
78
+
79
+
80
+ def test_enable_exa_websearch_writes_export(tmp_path, monkeypatch):
81
+ rc = tmp_path / ".zshrc"
82
+ monkeypatch.setattr("evo_cli.commands.opencode.is_windows", lambda: False)
83
+ monkeypatch.setattr("evo_cli.commands.opencode.get_shell_rc_path", lambda: rc)
84
+
85
+ enable_exa_websearch()
86
+ assert f"export {EXA_ENV_VAR}=1" in rc.read_text()
87
+
88
+
89
+ def test_enable_exa_websearch_is_idempotent(tmp_path, monkeypatch):
90
+ rc = tmp_path / ".zshrc"
91
+ monkeypatch.setattr("evo_cli.commands.opencode.is_windows", lambda: False)
92
+ monkeypatch.setattr("evo_cli.commands.opencode.get_shell_rc_path", lambda: rc)
93
+
94
+ enable_exa_websearch()
95
+ enable_exa_websearch()
96
+ assert rc.read_text().count(f"export {EXA_ENV_VAR}=1") == 1
77
97
 
78
98
 
79
99
  def test_install_mcp_servers_detaches_stdin_and_sets_timeout(monkeypatch):
@@ -141,6 +161,7 @@ def test_setup_opencode_skip_install(runner, tmp_path, monkeypatch):
141
161
  monkeypatch.setattr("evo_cli.commands.opencode.install_mcp_servers", lambda: None)
142
162
  monkeypatch.setattr("evo_cli.commands.opencode.install_playwright_browsers", lambda: None)
143
163
  monkeypatch.setattr("evo_cli.commands.opencode.verify_mcp_servers", lambda: None)
164
+ monkeypatch.setattr("evo_cli.commands.opencode.enable_exa_websearch", lambda: None)
144
165
 
145
166
  result = runner.invoke(cli, ["setup", "opencode", "--global-only", "--skip-install"])
146
167
  assert result.exit_code == 0
@@ -159,6 +180,7 @@ def test_setup_opencode_writes_global_and_project(runner, tmp_path, monkeypatch)
159
180
  monkeypatch.setattr("evo_cli.commands.opencode.install_mcp_servers", lambda: None)
160
181
  monkeypatch.setattr("evo_cli.commands.opencode.install_playwright_browsers", lambda: None)
161
182
  monkeypatch.setattr("evo_cli.commands.opencode.verify_mcp_servers", lambda: None)
183
+ monkeypatch.setattr("evo_cli.commands.opencode.enable_exa_websearch", lambda: None)
162
184
 
163
185
  result = runner.invoke(cli, ["setup", "opencode", "--project", str(tmp_path)])
164
186
  assert result.exit_code == 0
@@ -180,6 +202,7 @@ def test_setup_opencode_global_only(runner, tmp_path, monkeypatch):
180
202
  monkeypatch.setattr("evo_cli.commands.opencode.install_mcp_servers", lambda: None)
181
203
  monkeypatch.setattr("evo_cli.commands.opencode.install_playwright_browsers", lambda: None)
182
204
  monkeypatch.setattr("evo_cli.commands.opencode.verify_mcp_servers", lambda: None)
205
+ monkeypatch.setattr("evo_cli.commands.opencode.enable_exa_websearch", lambda: None)
183
206
 
184
207
  project = tmp_path / "project"
185
208
  project.mkdir()
@@ -1 +0,0 @@
1
- 0.10.1
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