evo-cli 0.15.2__tar.gz → 0.15.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.
Files changed (109) hide show
  1. {evo_cli-0.15.2 → evo_cli-0.15.4}/PKG-INFO +1 -1
  2. evo_cli-0.15.4/evo_cli/VERSION +1 -0
  3. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/cli.py +2 -0
  4. evo_cli-0.15.4/evo_cli/commands/agent_toy.py +336 -0
  5. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/gdrive.py +2 -2
  6. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/registry.py +6 -3
  7. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli.egg-info/PKG-INFO +1 -1
  8. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli.egg-info/SOURCES.txt +2 -0
  9. evo_cli-0.15.4/tests/test_agent_toy.py +164 -0
  10. evo_cli-0.15.2/evo_cli/VERSION +0 -1
  11. {evo_cli-0.15.2 → evo_cli-0.15.4}/Containerfile +0 -0
  12. {evo_cli-0.15.2 → evo_cli-0.15.4}/HISTORY.md +0 -0
  13. {evo_cli-0.15.2 → evo_cli-0.15.4}/LICENSE +0 -0
  14. {evo_cli-0.15.2 → evo_cli-0.15.4}/MANIFEST.in +0 -0
  15. {evo_cli-0.15.2 → evo_cli-0.15.4}/README.md +0 -0
  16. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/__init__.py +0 -0
  17. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/__main__.py +0 -0
  18. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/base.py +0 -0
  19. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/__init__.py +0 -0
  20. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/claude_code.py +0 -0
  21. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/cloudflare.py +0 -0
  22. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/cred.py +0 -0
  23. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/download.py +0 -0
  24. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/fix_claude.py +0 -0
  25. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/gh.py +0 -0
  26. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/__init__.py +0 -0
  27. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_dag.py +0 -0
  28. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_git.py +0 -0
  29. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_model.py +0 -0
  30. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_mutate.py +0 -0
  31. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_paths.py +0 -0
  32. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_render.py +0 -0
  33. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/_server.py +0 -0
  34. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/check.py +0 -0
  35. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/edit.py +0 -0
  36. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/pull.py +0 -0
  37. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/serve.py +0 -0
  38. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/show.py +0 -0
  39. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/views.py +0 -0
  40. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/index-Bo-8pFen.js +0 -0
  41. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/index-ByHqvOcG.css +0 -0
  42. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  43. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  44. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  45. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  46. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  47. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  48. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  49. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  50. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  51. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  52. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  53. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  54. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/harness/web/index.html +0 -0
  55. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/hwid.py +0 -0
  56. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/hwid_reset.py +0 -0
  57. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/localproxy.py +0 -0
  58. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/mcp.py +0 -0
  59. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/miniconda.py +0 -0
  60. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/netcheck.py +0 -0
  61. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/opencode.py +0 -0
  62. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/plantuml.py +0 -0
  63. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/site2s.py +0 -0
  64. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/ssh.py +0 -0
  65. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/sysmon.py +0 -0
  66. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/tts.py +0 -0
  67. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/update.py +0 -0
  68. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/commands/wifi.py +0 -0
  69. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/console.py +0 -0
  70. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/__init__.py +0 -0
  71. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/doctor.py +0 -0
  72. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/google_oauth.py +0 -0
  73. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/migrate.py +0 -0
  74. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/oauth_flow.py +0 -0
  75. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/store.py +0 -0
  76. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/credentials/sync.py +0 -0
  77. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/mcp_registry.py +0 -0
  78. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/__init__.py +0 -0
  79. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/chunking.py +0 -0
  80. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/core.py +0 -0
  81. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/creds.py +0 -0
  82. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/errors.py +0 -0
  83. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/http.py +0 -0
  84. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/openai.py +0 -0
  85. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/player.py +0 -0
  86. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli/tts/vbee.py +0 -0
  87. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli.egg-info/dependency_links.txt +0 -0
  88. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli.egg-info/entry_points.txt +0 -0
  89. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli.egg-info/requires.txt +0 -0
  90. {evo_cli-0.15.2 → evo_cli-0.15.4}/evo_cli.egg-info/top_level.txt +0 -0
  91. {evo_cli-0.15.2 → evo_cli-0.15.4}/pyproject.toml +0 -0
  92. {evo_cli-0.15.2 → evo_cli-0.15.4}/setup.cfg +0 -0
  93. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/__init__.py +0 -0
  94. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_claude_code.py +0 -0
  95. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_cli.py +0 -0
  96. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_console.py +0 -0
  97. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_cred.py +0 -0
  98. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_download.py +0 -0
  99. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_fix_claude.py +0 -0
  100. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_gh.py +0 -0
  101. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_harness.py +0 -0
  102. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_harness_dag.py +0 -0
  103. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_mcp.py +0 -0
  104. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_opencode.py +0 -0
  105. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_plantuml.py +0 -0
  106. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_sysmon.py +0 -0
  107. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_tts.py +0 -0
  108. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_update.py +0 -0
  109. {evo_cli-0.15.2 → evo_cli-0.15.4}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.15.2
3
+ Version: 0.15.4
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.15.4
@@ -1,6 +1,7 @@
1
1
  import rich_click as click
2
2
 
3
3
  from evo_cli import __version__
4
+ from evo_cli.commands.agent_toy import agent_toy
4
5
  from evo_cli.commands.claude_code import setup_claude
5
6
  from evo_cli.commands.cloudflare import cfssh
6
7
  from evo_cli.commands.cred import cred_group
@@ -51,6 +52,7 @@ def cli():
51
52
  """
52
53
 
53
54
 
55
+ setup_group.add_command(agent_toy)
54
56
  setup_group.add_command(setup_claude)
55
57
  setup_group.add_command(setup_gh)
56
58
  setup_group.add_command(setup_opencode)
@@ -0,0 +1,336 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import rich_click as click
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+ from rich.text import Text
10
+
11
+ from evo_cli.commands.claude_code import ensure_claude_on_path, is_windows
12
+ from evo_cli.console import CommandError, console, error, info, run_command, step, success, warning
13
+
14
+ EPILOG = Text.from_markup(
15
+ "[bold]Examples[/bold]\n\n"
16
+ " [cyan]evo setup agent-toy[/cyan] install every toy\n"
17
+ " [cyan]evo setup agent-toy --list[/cyan] show the catalogue\n"
18
+ " [cyan]evo setup agent-toy plannotator[/cyan] install a single toy\n"
19
+ " [cyan]evo setup agent-toy context-mode agent-skills[/cyan]\n"
20
+ " [cyan]evo setup agent-toy plannotator --minimal[/cyan] binary only, no agent wiring\n"
21
+ " [cyan]evo setup agent-toy --scope project[/cyan] install into the current project"
22
+ )
23
+
24
+ PLANNOTATOR_INSTALL_SH = "https://plannotator.ai/install.sh"
25
+ PLANNOTATOR_INSTALL_PS1 = "https://plannotator.ai/install.ps1"
26
+ PLANNOTATOR_MARKETPLACE = "backnotprop/plannotator"
27
+
28
+ CONTEXT_MODE_MARKETPLACE = "mksglu/context-mode"
29
+
30
+ SKILLFISH_PACKAGE = "skillfish@latest"
31
+ AGENT_SKILLS_REPO = "maycuatroi1/agent-skills"
32
+
33
+ ENTRY_RE = re.compile(r"^[^\w]*([\w][\w.@/-]*)\s*$")
34
+
35
+ INSTALLED = "installed"
36
+ PARTIAL = "partial"
37
+ FAILED = "failed"
38
+
39
+ RESULT_STYLES = {
40
+ INSTALLED: "[success]installed[/success]",
41
+ PARTIAL: "[warning]partial[/warning]",
42
+ FAILED: "[error]failed[/error]",
43
+ }
44
+
45
+
46
+ def parse_entries(output):
47
+ names = []
48
+ for line in (output or "").splitlines():
49
+ if ":" in line:
50
+ continue
51
+ match = ENTRY_RE.match(line)
52
+ if match:
53
+ names.append(match.group(1))
54
+ return names
55
+
56
+
57
+ def claude_entries(*args):
58
+ result = run_command(["claude", "plugin", *args], capture=True, check=False)
59
+ return parse_entries(getattr(result, "stdout", ""))
60
+
61
+
62
+ def ensure_marketplace(name, source, scope):
63
+ if name in claude_entries("marketplace", "list"):
64
+ info(f"Marketplace [accent]{name}[/accent] already configured; refreshing it")
65
+ run_command(
66
+ ["claude", "plugin", "marketplace", "update", name],
67
+ status=f"Updating marketplace {name}",
68
+ timeout=300,
69
+ check=False,
70
+ )
71
+ return True
72
+
73
+ try:
74
+ run_command(
75
+ ["claude", "plugin", "marketplace", "add", source, "--scope", scope],
76
+ status=f"Adding marketplace {source}",
77
+ timeout=300,
78
+ )
79
+ except CommandError as exc:
80
+ error(f"Could not add marketplace {source}: {exc}")
81
+ return False
82
+ return True
83
+
84
+
85
+ def ensure_plugin(plugin, marketplace, scope):
86
+ target = f"{plugin}@{marketplace}"
87
+
88
+ if target in claude_entries("list"):
89
+ info(f"Plugin [accent]{target}[/accent] already installed; updating it")
90
+ try:
91
+ run_command(
92
+ ["claude", "plugin", "update", target, "--scope", scope],
93
+ status=f"Updating {target}",
94
+ timeout=600,
95
+ )
96
+ except CommandError as exc:
97
+ warning(f"Could not update {target}: {exc}")
98
+ return True
99
+
100
+ try:
101
+ run_command(
102
+ ["claude", "plugin", "install", target, "--scope", scope],
103
+ status=f"Installing {target}",
104
+ timeout=600,
105
+ )
106
+ except CommandError as exc:
107
+ error(f"Could not install {target}: {exc}")
108
+ return False
109
+
110
+ success(f"Plugin [accent]{target}[/accent] installed")
111
+ return True
112
+
113
+
114
+ def plannotator_install_command(minimal):
115
+ if is_windows():
116
+ if not shutil.which("powershell"):
117
+ return None
118
+ script = f"irm {PLANNOTATOR_INSTALL_PS1} | iex"
119
+ if minimal:
120
+ script = f"$env:PLANNOTATOR_MINIMAL=1; {script}"
121
+ return ["powershell", "-NoProfile", "-Command", script]
122
+
123
+ if not (shutil.which("curl") and shutil.which("bash")):
124
+ return None
125
+ script = f"curl -fsSL {PLANNOTATOR_INSTALL_SH} | bash"
126
+ if minimal:
127
+ script += " -s -- --minimal"
128
+ return ["bash", "-c", script]
129
+
130
+
131
+ def plannotator_binary():
132
+ found = shutil.which("plannotator")
133
+ if found:
134
+ return found
135
+
136
+ candidates = []
137
+ if is_windows():
138
+ local_app = os.environ.get("LOCALAPPDATA")
139
+ if local_app:
140
+ candidates.append(Path(local_app) / "plannotator" / "plannotator.exe")
141
+ candidates.append(Path.home() / ".local" / "bin" / "plannotator.exe")
142
+ else:
143
+ candidates.append(Path.home() / ".local" / "bin" / "plannotator")
144
+
145
+ for candidate in candidates:
146
+ if candidate.exists():
147
+ return str(candidate)
148
+ return None
149
+
150
+
151
+ def run_plannotator_installer(minimal, attempts=2):
152
+ command = plannotator_install_command(minimal)
153
+ if command is None:
154
+ error("No usable installer here: PowerShell (Windows) or curl + bash (POSIX) is required.")
155
+ return False
156
+
157
+ for attempt in range(1, attempts + 1):
158
+ try:
159
+ run_command(command, status="Running the plannotator installer", timeout=900)
160
+ return True
161
+ except CommandError as exc:
162
+ if attempt < attempts:
163
+ warning(f"The plannotator installer failed ({exc}); retrying")
164
+ else:
165
+ error(f"The plannotator installer failed: {exc}")
166
+ return False
167
+
168
+
169
+ def install_plannotator(options):
170
+ plugin_ok = True
171
+ if options["minimal"]:
172
+ info("Minimal install: no plugin, skills, hooks or slash commands")
173
+ elif not options["claude"]:
174
+ warning("Claude Code is not on PATH; skipping its plugin. Run `evo setup claude` first.")
175
+ plugin_ok = False
176
+ else:
177
+ plugin_ok = ensure_marketplace("plannotator", PLANNOTATOR_MARKETPLACE, options["scope"]) and ensure_plugin(
178
+ "plannotator", "plannotator", options["scope"]
179
+ )
180
+
181
+ installed = run_plannotator_installer(options["minimal"])
182
+ if not installed:
183
+ binary = plannotator_binary()
184
+ if not binary:
185
+ return FAILED
186
+ warning(f"The installer errored but the binary is in place ({binary})")
187
+ warning("Skills and hooks for the other agents may be missing; re-run to finish them.")
188
+
189
+ return INSTALLED if (plugin_ok and installed) else PARTIAL
190
+
191
+
192
+ def install_context_mode(options):
193
+ if not options["claude"]:
194
+ error("Claude Code is not on PATH; run `evo setup claude` first.")
195
+ return FAILED
196
+
197
+ if not ensure_marketplace("context-mode", CONTEXT_MODE_MARKETPLACE, options["scope"]):
198
+ return FAILED
199
+ return INSTALLED if ensure_plugin("context-mode", "context-mode", options["scope"]) else FAILED
200
+
201
+
202
+ def install_agent_skills(options):
203
+ if not shutil.which("npx"):
204
+ error("npx was not found; install Node.js 18+ and retry.")
205
+ return FAILED
206
+
207
+ command = [
208
+ "npx",
209
+ "-y",
210
+ SKILLFISH_PACKAGE,
211
+ "add",
212
+ AGENT_SKILLS_REPO,
213
+ "--all",
214
+ "--yes",
215
+ "--force",
216
+ "--project" if options["scope"] == "project" else "--global",
217
+ ]
218
+ try:
219
+ run_command(command, status="Installing agent-skills via skillfish", timeout=900)
220
+ except CommandError as exc:
221
+ error(f"skillfish failed: {exc}")
222
+ return FAILED
223
+
224
+ success("agent-skills installed")
225
+ return INSTALLED
226
+
227
+
228
+ TOYS = {
229
+ "plannotator": {
230
+ "summary": "Browser review surface for plans, diffs and HTML your agent produces",
231
+ "url": "https://github.com/backnotprop/plannotator",
232
+ "install": install_plannotator,
233
+ },
234
+ "context-mode": {
235
+ "summary": "Sandboxed command execution and an indexed knowledge base that keep raw output out of context",
236
+ "url": "https://github.com/mksglu/context-mode",
237
+ "install": install_context_mode,
238
+ },
239
+ "agent-skills": {
240
+ "summary": "The personal skill library (credentials, slides, gitnexus, life CLI, TTS, ...)",
241
+ "url": "https://github.com/maycuatroi1/agent-skills",
242
+ "install": install_agent_skills,
243
+ },
244
+ }
245
+
246
+
247
+ def print_catalogue():
248
+ table = Table(box=None, pad_edge=False, show_edge=False)
249
+ table.add_column("Toy", style="accent", no_wrap=True)
250
+ table.add_column("What it does")
251
+ table.add_column("Source", style="dim")
252
+ for name, toy in TOYS.items():
253
+ table.add_row(name, toy["summary"], toy["url"])
254
+ console.print()
255
+ console.print(table)
256
+ console.print()
257
+ console.print("Install everything with [accent]evo setup agent-toy[/accent], or name the ones you want.")
258
+
259
+
260
+ def print_summary(results, scope):
261
+ table = Table(box=None, pad_edge=False, show_edge=False)
262
+ table.add_column("Toy", style="accent", no_wrap=True)
263
+ table.add_column("Result", no_wrap=True)
264
+ for name, status in results:
265
+ table.add_row(name, RESULT_STYLES[status])
266
+
267
+ console.print()
268
+ console.print(table)
269
+
270
+ usable = [name for name, status in results if status != FAILED]
271
+ lines = []
272
+ if "plannotator" in usable:
273
+ lines.append("Plannotator opens plans in your browser as soon as your agent proposes one.")
274
+ if "context-mode" in usable:
275
+ lines.append("Run [accent]ctx doctor[/accent] inside Claude Code to verify the hooks.")
276
+ if "agent-skills" in usable:
277
+ target = "./.claude/skills" if scope == "project" else "~/.claude/skills"
278
+ lines.append(f"Skills landed in [accent]{target}[/accent].")
279
+ if lines:
280
+ lines.append("")
281
+ lines.append("Restart Claude Code so the new plugins, hooks and skills load.")
282
+
283
+ complete = all(status == INSTALLED for _, status in results)
284
+ console.print(
285
+ Panel(
286
+ Text.from_markup("\n".join(lines) if lines else "Nothing was installed."),
287
+ title="setup agent-toy complete" if complete else "setup agent-toy incomplete",
288
+ border_style="success" if complete else "warning",
289
+ expand=False,
290
+ )
291
+ )
292
+
293
+
294
+ @click.command("agent-toy", epilog=EPILOG)
295
+ @click.argument("names", nargs=-1, metavar="[TOY]...")
296
+ @click.option("--list", "list_only", is_flag=True, help="Show the catalogue and exit.")
297
+ @click.option(
298
+ "--scope",
299
+ type=click.Choice(["user", "project", "local"]),
300
+ default="user",
301
+ show_default=True,
302
+ help="Where to install: `user` is machine-wide, `project` scopes to the current repo.",
303
+ )
304
+ @click.option("--minimal", is_flag=True, help="plannotator: install only the binary, no hooks or skills.")
305
+ def agent_toy(names, list_only, scope, minimal):
306
+ """Install the agent toolbox: plannotator, context-mode and agent-skills.
307
+
308
+ Each toy is pulled at its latest version and installed idempotently: a
309
+ marketplace that is already configured gets refreshed, a plugin that is
310
+ already installed gets updated, and skills are overwritten in place. Name
311
+ one or more toys to install a subset, or pass no argument to install all of
312
+ them. Run with --list to see what is on offer.
313
+
314
+ plannotator needs PowerShell (Windows) or curl + bash (POSIX); context-mode
315
+ and the plannotator plugin need the Claude Code CLI (`evo setup claude`);
316
+ agent-skills needs Node.js 18+ for npx.
317
+ """
318
+ if list_only:
319
+ print_catalogue()
320
+ return
321
+
322
+ unknown = [name for name in names if name not in TOYS]
323
+ if unknown:
324
+ error(f"Unknown toy(s): {', '.join(unknown)}. Run `evo setup agent-toy --list` to see the catalogue.")
325
+ return
326
+
327
+ selected = list(names) or list(TOYS)
328
+ options = {"scope": scope, "minimal": minimal, "claude": bool(ensure_claude_on_path())}
329
+
330
+ results = []
331
+ for name in selected:
332
+ step(f"evo setup agent-toy: {name}")
333
+ info(TOYS[name]["url"])
334
+ results.append((name, TOYS[name]["install"](options)))
335
+
336
+ print_summary(results, scope)
@@ -466,8 +466,8 @@ def doc_read(target, out_dir, no_images, raw, via_docs_api):
466
466
  """Read a Google Doc and write **doc.md** + downloaded **images/**.
467
467
 
468
468
  `TARGET` is either a Google Docs URL (`https://docs.google.com/document/d/<id>/...`)
469
- or a raw document ID. Auth uses the Drive OAuth token saved in
470
- `~/.omelet.json` under `rclone.token`, refreshing it automatically when expired.
469
+ or a raw document ID. Auth uses the Drive OAuth token saved in
470
+ `~/.omelet.json` under `google_drive.token`, refreshing it automatically when expired.
471
471
 
472
472
  Default path: Drive API export to `text/markdown`, then any inline image URLs
473
473
  in the markdown are downloaded into `images/` and their references rewritten
@@ -167,9 +167,12 @@ SPECS = [
167
167
  "type": "oauth_token",
168
168
  "lifetime": "access ~1h, refresh until revoked",
169
169
  "description": (
170
- "rclone Google Drive remote: OAuth token + root folder. Being replaced by google_drive; "
171
- "still active because red-life/packages/core/src/teaching/drive.ts reads it. Do not mark "
172
- "deprecated until that consumer migrates - compile would drop the key and break it."
170
+ "rclone Google Drive remote: OAuth token + root folder. Being replaced by google_drive. "
171
+ "The in-cluster consumer (red-life/packages/core/src/teaching/drive.ts) now reads "
172
+ "google_drive; rclone stays active because two sibling repos outside the harness still "
173
+ "read and refresh it: evo-lms-harness/cli/modules/gdrive.py and "
174
+ "fu-teching/tools/schedule/drive.py. Do not mark deprecated until both migrate - compile "
175
+ "would drop the key and break them."
173
176
  ),
174
177
  "rotate": "evo cred refresh --service rclone (or rclone config reconnect)",
175
178
  "oauth": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.15.2
3
+ Version: 0.15.4
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
@@ -18,6 +18,7 @@ evo_cli.egg-info/entry_points.txt
18
18
  evo_cli.egg-info/requires.txt
19
19
  evo_cli.egg-info/top_level.txt
20
20
  evo_cli/commands/__init__.py
21
+ evo_cli/commands/agent_toy.py
21
22
  evo_cli/commands/claude_code.py
22
23
  evo_cli/commands/cloudflare.py
23
24
  evo_cli/commands/cred.py
@@ -86,6 +87,7 @@ evo_cli/tts/openai.py
86
87
  evo_cli/tts/player.py
87
88
  evo_cli/tts/vbee.py
88
89
  tests/__init__.py
90
+ tests/test_agent_toy.py
89
91
  tests/test_claude_code.py
90
92
  tests/test_cli.py
91
93
  tests/test_console.py
@@ -0,0 +1,164 @@
1
+ import pytest
2
+ from click.testing import CliRunner
3
+
4
+ from evo_cli.cli import cli
5
+ from evo_cli.commands import agent_toy as toy
6
+ from evo_cli.commands.agent_toy import (
7
+ FAILED,
8
+ INSTALLED,
9
+ PARTIAL,
10
+ TOYS,
11
+ install_agent_skills,
12
+ install_context_mode,
13
+ install_plannotator,
14
+ parse_entries,
15
+ plannotator_install_command,
16
+ )
17
+
18
+ PLUGIN_LIST = """Installed plugins:
19
+
20
+ ❯ context-mode@context-mode
21
+ Version: 1.0.169
22
+ Scope: user
23
+ Status: ✔ enabled
24
+
25
+ ❯ warp@claude-code-warp
26
+ Version: 2.2.0
27
+ Scope: user
28
+ Status: ✔ enabled
29
+ """
30
+
31
+ MARKETPLACE_LIST = """Configured marketplaces:
32
+
33
+ ❯ context-mode
34
+ Source: Git (https://github.com/mksglu/context-mode.git)
35
+
36
+ ❯ plannotator
37
+ Source: GitHub (backnotprop/plannotator)
38
+ """
39
+
40
+
41
+ @pytest.fixture
42
+ def runner():
43
+ return CliRunner()
44
+
45
+
46
+ @pytest.fixture
47
+ def options():
48
+ return {"scope": "user", "minimal": False, "claude": True}
49
+
50
+
51
+ def test_agent_toy_is_registered():
52
+ assert "agent-toy" in cli.commands["setup"].commands
53
+
54
+
55
+ def test_agent_toy_help_runs(runner):
56
+ result = runner.invoke(cli, ["setup", "agent-toy", "--help"])
57
+ assert result.exit_code == 0
58
+ assert "plannotator" in result.output
59
+
60
+
61
+ def test_list_shows_every_toy(runner):
62
+ result = runner.invoke(cli, ["setup", "agent-toy", "--list"])
63
+ assert result.exit_code == 0
64
+ for name in TOYS:
65
+ assert name in result.output
66
+
67
+
68
+ def test_unknown_toy_is_rejected(runner, monkeypatch):
69
+ monkeypatch.setattr(toy, "ensure_claude_on_path", lambda: "/usr/bin/claude")
70
+ result = runner.invoke(cli, ["setup", "agent-toy", "nope"])
71
+ assert "Unknown toy" in result.output
72
+
73
+
74
+ def test_parse_entries_reads_plugin_names():
75
+ assert parse_entries(PLUGIN_LIST) == ["context-mode@context-mode", "warp@claude-code-warp"]
76
+
77
+
78
+ def test_parse_entries_reads_marketplace_names():
79
+ assert parse_entries(MARKETPLACE_LIST) == ["context-mode", "plannotator"]
80
+
81
+
82
+ def test_plannotator_install_command_posix(monkeypatch):
83
+ monkeypatch.setattr(toy, "is_windows", lambda: False)
84
+ monkeypatch.setattr(toy.shutil, "which", lambda name: f"/usr/bin/{name}")
85
+ assert plannotator_install_command(False) == [
86
+ "bash",
87
+ "-c",
88
+ "curl -fsSL https://plannotator.ai/install.sh | bash",
89
+ ]
90
+ assert plannotator_install_command(True)[-1].endswith("| bash -s -- --minimal")
91
+
92
+
93
+ def test_plannotator_install_command_windows(monkeypatch):
94
+ monkeypatch.setattr(toy, "is_windows", lambda: True)
95
+ monkeypatch.setattr(toy.shutil, "which", lambda name: f"C:\\{name}.exe")
96
+ command = plannotator_install_command(True)
97
+ assert command[0] == "powershell"
98
+ assert "PLANNOTATOR_MINIMAL=1" in command[-1]
99
+ assert "install.ps1" in command[-1]
100
+
101
+
102
+ def test_plannotator_install_command_without_a_shell(monkeypatch):
103
+ monkeypatch.setattr(toy, "is_windows", lambda: False)
104
+ monkeypatch.setattr(toy.shutil, "which", lambda name: None)
105
+ assert plannotator_install_command(False) is None
106
+
107
+
108
+ def test_plannotator_is_partial_when_the_installer_fails(monkeypatch, options):
109
+ monkeypatch.setattr(toy, "ensure_marketplace", lambda *args: True)
110
+ monkeypatch.setattr(toy, "ensure_plugin", lambda *args: True)
111
+ monkeypatch.setattr(toy, "run_plannotator_installer", lambda minimal: False)
112
+ monkeypatch.setattr(toy, "plannotator_binary", lambda: "/usr/local/bin/plannotator")
113
+ assert install_plannotator(options) == PARTIAL
114
+
115
+
116
+ def test_plannotator_fails_when_no_binary_landed(monkeypatch, options):
117
+ monkeypatch.setattr(toy, "ensure_marketplace", lambda *args: True)
118
+ monkeypatch.setattr(toy, "ensure_plugin", lambda *args: True)
119
+ monkeypatch.setattr(toy, "run_plannotator_installer", lambda minimal: False)
120
+ monkeypatch.setattr(toy, "plannotator_binary", lambda: None)
121
+ assert install_plannotator(options) == FAILED
122
+
123
+
124
+ def test_plannotator_minimal_skips_the_plugin(monkeypatch, options):
125
+ calls = []
126
+ monkeypatch.setattr(toy, "ensure_marketplace", lambda *args: calls.append(args) or True)
127
+ monkeypatch.setattr(toy, "run_plannotator_installer", lambda minimal: True)
128
+ options["minimal"] = True
129
+ assert install_plannotator(options) == INSTALLED
130
+ assert calls == []
131
+
132
+
133
+ def test_context_mode_needs_claude(options):
134
+ options["claude"] = False
135
+ assert install_context_mode(options) == FAILED
136
+
137
+
138
+ def test_context_mode_installs_the_plugin(monkeypatch, options):
139
+ monkeypatch.setattr(toy, "ensure_marketplace", lambda *args: True)
140
+ monkeypatch.setattr(toy, "ensure_plugin", lambda *args: True)
141
+ assert install_context_mode(options) == INSTALLED
142
+
143
+
144
+ def test_agent_skills_installs_globally(monkeypatch, options):
145
+ captured = {}
146
+ monkeypatch.setattr(toy.shutil, "which", lambda name: f"/usr/bin/{name}")
147
+ monkeypatch.setattr(toy, "run_command", lambda command, **kwargs: captured.setdefault("command", command))
148
+ assert install_agent_skills(options) == INSTALLED
149
+ assert captured["command"][-1] == "--global"
150
+ assert "maycuatroi1/agent-skills" in captured["command"]
151
+
152
+
153
+ def test_agent_skills_scopes_to_the_project(monkeypatch, options):
154
+ captured = {}
155
+ monkeypatch.setattr(toy.shutil, "which", lambda name: f"/usr/bin/{name}")
156
+ monkeypatch.setattr(toy, "run_command", lambda command, **kwargs: captured.setdefault("command", command))
157
+ options["scope"] = "project"
158
+ install_agent_skills(options)
159
+ assert captured["command"][-1] == "--project"
160
+
161
+
162
+ def test_agent_skills_needs_npx(monkeypatch, options):
163
+ monkeypatch.setattr(toy.shutil, "which", lambda name: None)
164
+ assert install_agent_skills(options) == FAILED
@@ -1 +0,0 @@
1
- 0.15.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
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